d4cc2013c9d6dc939895350e34894fb0c7b6f075
[vfc/nfvo/wfengine.git] /
1 /*******************************************************************************
2  * Copyright (c) 2012-2013,2015 University of Stuttgart.
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Eclipse Public License v1.0
5  * and the Apache License 2.0 which both accompany this distribution,
6  * and are available at http://www.eclipse.org/legal/epl-v10.html
7  * and http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Contributors:
10  *     Oliver Kopp - initial API and implementation
11  *******************************************************************************/
12 package org.eclipse.winery.repository.resources;
13
14 import java.io.IOException;
15 import java.io.InputStream;
16 import java.io.OutputStream;
17 import java.io.StringWriter;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21
22 import javax.ws.rs.Consumes;
23 import javax.ws.rs.DELETE;
24 import javax.ws.rs.FormParam;
25 import javax.ws.rs.GET;
26 import javax.ws.rs.PUT;
27 import javax.ws.rs.Path;
28 import javax.ws.rs.Produces;
29 import javax.ws.rs.QueryParam;
30 import javax.ws.rs.WebApplicationException;
31 import javax.ws.rs.core.Context;
32 import javax.ws.rs.core.MediaType;
33 import javax.ws.rs.core.Response;
34 import javax.ws.rs.core.Response.Status;
35 import javax.ws.rs.core.StreamingOutput;
36 import javax.ws.rs.core.UriInfo;
37 import javax.xml.bind.JAXBException;
38 import javax.xml.bind.Marshaller;
39 import javax.xml.bind.Unmarshaller;
40 import javax.xml.namespace.QName;
41 import javax.xml.parsers.DocumentBuilder;
42
43 import org.eclipse.winery.common.RepositoryFileReference;
44 import org.eclipse.winery.common.TOSCADocumentBuilderFactory;
45 import org.eclipse.winery.common.constants.MimeTypes;
46 import org.eclipse.winery.common.ids.Namespace;
47 import org.eclipse.winery.common.ids.XMLId;
48 import org.eclipse.winery.common.ids.definitions.TOSCAComponentId;
49 import org.eclipse.winery.model.tosca.Definitions;
50 import org.eclipse.winery.model.tosca.TExtensibleElements;
51 import org.eclipse.winery.model.tosca.TImport;
52 import org.eclipse.winery.repository.JAXBSupport;
53 import org.eclipse.winery.repository.Utils;
54 import org.eclipse.winery.repository.backend.BackendUtils;
55 import org.eclipse.winery.repository.backend.Repository;
56 import org.eclipse.winery.repository.backend.constants.MediaTypes;
57 import org.eclipse.winery.repository.export.TOSCAExportUtil;
58 import org.eclipse.winery.repository.resources._support.IPersistable;
59 import org.eclipse.winery.repository.resources.documentation.DocumentationsResource;
60 import org.eclipse.winery.repository.resources.imports.genericimports.GenericImportResource;
61 import org.eclipse.winery.repository.resources.tags.TagsResource;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64 import org.w3c.dom.Document;
65 import org.xml.sax.ErrorHandler;
66 import org.xml.sax.SAXException;
67 import org.xml.sax.SAXParseException;
68
69 import com.sun.jersey.api.view.Viewable;
70
71 /**
72  * Resource for a component (
73  * <ul>
74  * <li>ServiceTemplates,</li>
75  * <li>EntityTypes,</li>
76  * <li>EntityTypeImplementations,</li>
77  * <li>EntityTemplates</li>
78  * </ul>
79  * ). A component is directly nested in a TDefinitions element. See also
80  * {@link org.eclipse.winery.common.ids.definitions.TOSCAComponentId}
81  * 
82  * Bundles all operations required for all components. e.g., namespace+XMLid,
83  * object comparison, import, export, tags
84  * 
85  * Uses a TDefinitions document as storage.
86  * 
87  * Additional setters and getters are added if it comes to Winery's extensions
88  * such as the color of a relationship type
89  */
90 public abstract class AbstractComponentInstanceResource implements Comparable<AbstractComponentInstanceResource>, IPersistable {
91         
92         private static final Logger logger = LoggerFactory.getLogger(AbstractComponentInstanceResource.class);
93         
94         protected final TOSCAComponentId id;
95         
96         private final RepositoryFileReference ref;
97         
98         // the object representing the data of this resource
99         private Definitions definitions = null;
100         
101         // shortcut for this.definitions.getServiceTemplateOrNodeTypeOrNodeTypeImplementation().get(0);
102         protected TExtensibleElements element = null;
103         
104         
105         /**
106          * Instantiates the resource. Assumes that the resource should exist
107          * (assured by the caller)
108          * 
109          * The caller should <em>not</em> create the resource by other ways. E.g.,
110          * by instantiating this resource and then adding data.
111          */
112         public AbstractComponentInstanceResource(TOSCAComponentId id) {
113                 this.id = id;
114                 
115                 // the resource itself exists
116                 assert (Repository.INSTANCE.exists(id));
117                 
118                 // the data file might not exist
119                 this.ref = BackendUtils.getRefOfDefinitions(id);
120                 if (Repository.INSTANCE.exists(this.ref)) {
121                         this.load();
122                 } else {
123                         this.createNew();
124                 }
125         }
126         
127         /**
128          * Convenience method for getId().getNamespace()
129          */
130         public final Namespace getNamespace() {
131                 return this.id.getNamespace();
132         }
133         
134         /**
135          * Convenience method for getId().getXmlId()
136          */
137         public final XMLId getXmlId() {
138                 return this.id.getXmlId();
139         }
140         
141         /**
142          * Convenience method for getId().getQName();
143          * 
144          * @return the QName associated with this resource
145          */
146         public final QName getQName() {
147                 return this.getId().getQName();
148         }
149         
150         /**
151          * Returns the id associated with this resource
152          */
153         public final TOSCAComponentId getId() {
154                 return this.id;
155         }
156         
157         /**
158          * called from AbstractComponentResource
159          */
160         @DELETE
161         public final Response onDelete() {
162                 Response res = BackendUtils.delete(this.id);
163                 return res;
164         }
165         
166         @Override
167         public final int compareTo(AbstractComponentInstanceResource o) {
168                 return this.id.compareTo(o.id);
169         }
170         
171         @Override
172         public final boolean equals(Object o) {
173                 if (o instanceof String) {
174                         throw new IllegalStateException();
175                 } else if (o instanceof AbstractComponentInstanceResource) {
176                         if (o.getClass().equals(this.getClass())) {
177                                 // only compare if the two objects are from the same class
178                                 return ((AbstractComponentInstanceResource) o).getId().equals(this.getId());
179                         } else {
180                                 throw new IllegalStateException();
181                         }
182                 } else {
183                         throw new IllegalStateException();
184                 }
185         }
186         
187         @Override
188         public final int hashCode() {
189                 return this.getId().hashCode();
190         }
191         
192         @GET
193         @Path("id")
194         public String getTOSCAId() {
195                 return this.id.getXmlId().getDecoded();
196         }
197         
198         @PUT
199         @Path("id")
200         public Response putId(@FormParam("id") String id) {
201                 // this renames the entity type resource
202                 // TODO: implement rename functionality
203                 return Response.serverError().entity("not yet implemented").build();
204         }
205         
206         /**
207          * Main page
208          */
209         // @Produces(MediaType.TEXT_HTML) // not true because of ?csar leads to send
210         // a csar. We nevertheless have to annotate that to be able to get a JSON
211         // representation required for the file upload (in {@link
212         // ArtifactTemplateResource})
213         //
214         // we cannot issue a request expecting content-type application/zip as it is
215         // not possible to offer the result in a "save-as"-dialog:
216         // http://stackoverflow.com/questions/7464665/ajax-response-content-disposition-attachment
217         @GET
218         @Produces(MediaType.TEXT_HTML)
219         public final Response getHTML(@QueryParam(value = "definitions") String definitions, @QueryParam(value = "csar") String csar, @Context UriInfo uriInfo) {
220                 if (!Repository.INSTANCE.exists(this.id)) {
221                         return Response.status(Status.NOT_FOUND).build();
222                 }
223                 if (definitions != null) {
224                         return Utils.getDefinitionsOfSelectedResource(this, uriInfo.getBaseUri());
225                 } else if (csar != null) {
226                         return this.getCSAR();
227                 } else {
228                         String type = Utils.getTypeForInstance(this.getClass());
229                         String viewableName = "/jsp/" + Utils.getIntermediateLocationStringForType(type, "/") + "/" + type.toLowerCase() + ".jsp";
230                         Viewable viewable = new Viewable(viewableName, this);
231                         
232                         return Response.ok().entity(viewable).build();
233                         
234                         // we can't do the following as the GET request from the browser
235                         // cannot set the accept header properly
236                         // "vary: accept" header has to be set as we may also return a THOR
237                         // on the same URL
238                         // return Response.ok().header(HttpHeaders.VARY,
239                         // HttpHeaders.ACCEPT).entity(viewable).build();
240                 }
241         }
242         
243         @GET
244         @Produces(MimeTypes.MIMETYPE_ZIP)
245         public final Response getCSAR() {
246                 if (!Repository.INSTANCE.exists(this.id)) {
247                         return Response.status(Status.NOT_FOUND).build();
248                 }
249                 return Utils.getCSARofSelectedResource(this);
250         }
251         
252         /**
253          * Returns the definitions of this resource. Includes required imports of
254          * other definitions
255          * 
256          * @param csar used because plan generator's GET request lands here
257          */
258         @GET
259         @Produces({MimeTypes.MIMETYPE_TOSCA_DEFINITIONS, MediaType.APPLICATION_XML, MediaType.TEXT_XML})
260         public Response getDefinitionsAsResponse(@QueryParam(value = "csar") String csar) {
261                 if (!Repository.INSTANCE.exists(this.id)) {
262                         return Response.status(Status.NOT_FOUND).build();
263                 }
264                 
265                 if (csar != null) {
266                         return Utils.getCSARofSelectedResource(this);
267                 }
268                 
269                 StreamingOutput so = new StreamingOutput() {
270                         
271                         @Override
272                         public void write(OutputStream output) throws IOException, WebApplicationException {
273                                 TOSCAExportUtil exporter = new TOSCAExportUtil();
274                                 // we include everything related
275                                 Map<String, Object> conf = new HashMap<>();
276                                 try {
277                                         exporter.exportTOSCA(AbstractComponentInstanceResource.this.id, output, conf);
278                                 } catch (JAXBException e) {
279                                         throw new WebApplicationException(e);
280                                 }
281                         }
282                 };
283                 return Response.ok().type(MediaType.TEXT_XML).entity(so).build();
284         }
285         
286         /**
287          * @throws IllegalStateException if an IOException occurred. We opted not to
288          *             propagate the IOException directly as this exception occurs
289          *             seldom and is a not an exception to be treated by all callers
290          *             in the prototype.
291          */
292         private void load() {
293                 try {
294                         InputStream is = Repository.INSTANCE.newInputStream(this.ref);
295                         Unmarshaller u = JAXBSupport.createUnmarshaller();
296                         this.definitions = (Definitions) u.unmarshal(is);
297                 } catch (Exception e) {
298                         AbstractComponentInstanceResource.logger.error("Could not read content from file " + this.ref, e);
299                         throw new IllegalStateException(e);
300                 }
301                 try {
302                         this.element = this.definitions.getServiceTemplateOrNodeTypeOrNodeTypeImplementation().get(0);
303                 } catch (IndexOutOfBoundsException e) {
304                         if (this instanceof GenericImportResource) {
305                                 // everything allright:
306                                 // ImportResource is a quick hack using 99% of the functionality offered here
307                                 // As only 1% has to be "quick hacked", we do that instead of a clean design
308                                 // Clean design: Introduce a class between this and AbstractComponentInstanceResource, where this class and ImportResource inhertis from
309                                 // A clean design introducing a super class AbstractDefinitionsBackedResource does not work, as we currently also support PropertiesBackedResources and such a super class would required multi-inheritance
310                         } else {
311                                 throw new IllegalStateException("Wrong storage format: No ServiceTemplateOrNodeTypeOrNodeTypeImplementation found.");
312                         }
313                 }
314         }
315         
316         @Override
317         public void persist() throws IOException {
318                 BackendUtils.persist(this.definitions, this.ref, MediaTypes.MEDIATYPE_TOSCA_DEFINITIONS);
319         }
320         
321         /**
322          * Creates a new instance of the object represented by this resource
323          */
324         private void createNew() {
325                 this.definitions = BackendUtils.createWrapperDefinitions(this.getId());
326                 
327                 // create empty element
328                 this.element = this.createNewElement();
329                 
330                 // add the element to the definitions
331                 this.definitions.getServiceTemplateOrNodeTypeOrNodeTypeImplementation().add(this.element);
332                 
333                 // copy ns + id
334                 this.copyIdToFields();
335                 
336                 // ensure that the definitions is persisted. Ensures that export works.
337                 BackendUtils.persist(this);
338         }
339         
340         /**
341          * Creates an empty instance of an Element.
342          * 
343          * The implementors do <em>not</em>have to copy the ns and the id to the
344          * appropriate fields.
345          * 
346          * we have two implementation possibilities:
347          * <ul>
348          * <li>a) each subclass implements this method and returns the appropriate
349          * object</li>
350          * <li>b) we use java reflection to invoke the right constructor as done in
351          * the resources</li>
352          * </ul>
353          * We opted for a) to increase readability of the code
354          */
355         protected abstract TExtensibleElements createNewElement();
356         
357         /**
358          * Copies the current id of the resource to the appropriate fields in the
359          * element.
360          * 
361          * For instance, the id is put in the "name" field for EntityTypes
362          * 
363          * We opted for a separate method from createNewElement to enable renaming
364          * of the object
365          */
366         protected abstract void copyIdToFields();
367         
368         /**
369          * Returns the Element belonging to this resource. As Java does not allow
370          * overriding returned classes, we expect the caller to either cast right or
371          * to use "getXY" defined by each subclass, where XY is the concrete type
372          * 
373          * Shortcut for
374          * getDefinitions().getServiceTemplateOrNodeTypeOrNodeTypeImplementation
375          * ().get(0);
376          * 
377          * @return TCapabilityType|...
378          */
379         public TExtensibleElements getElement() {
380                 return this.element;
381         }
382         
383         /**
384          * @return the reference to the internal list of imports. Can be changed if
385          *         some imports are required or should be removed
386          * @throws IllegalStateException if definitions was not loaded or not
387          *             initialized
388          */
389         protected List<TImport> getImport() {
390                 if (this.definitions == null) {
391                         throw new IllegalStateException("Trying to access uninitalized definitions object");
392                 }
393                 return this.definitions.getImport();
394         }
395         
396         /**
397          * Returns an XML representation of the definitions
398          * 
399          * We return the complete definitions to allow the user changes to it, such
400          * as adding imports, etc.
401          */
402         public String getDefinitionsAsXMLString() {
403                 StringWriter w = new StringWriter();
404                 Marshaller m = JAXBSupport.createMarshaller(true);
405                 try {
406                         m.marshal(this.definitions, w);
407                 } catch (JAXBException e) {
408                         AbstractComponentInstanceResource.logger.error("Could not marshal definitions", e);
409                         throw new IllegalStateException(e);
410                 }
411                 String res = w.toString();
412                 return res;
413         }
414         
415         /**
416          * @return the reference to the internal Definitions object
417          */
418         public Definitions getDefinitions() {
419                 return this.definitions;
420         }
421         
422         @PUT
423         @Consumes({MimeTypes.MIMETYPE_TOSCA_DEFINITIONS, MediaType.APPLICATION_XML, MediaType.TEXT_XML})
424         public Response updateDefinitions(InputStream requestBodyStream) {
425                 Unmarshaller u;
426                 Definitions defs;
427                 Document doc;
428                 final StringBuilder sb = new StringBuilder();
429                 try {
430                         DocumentBuilder db = TOSCADocumentBuilderFactory.INSTANCE.getTOSCADocumentBuilder();
431                         db.setErrorHandler(new ErrorHandler() {
432                                 
433                                 @Override
434                                 public void warning(SAXParseException exception) throws SAXException {
435                                         // we don't care
436                                 }
437                                 
438                                 @Override
439                                 public void fatalError(SAXParseException exception) throws SAXException {
440                                         sb.append("Fatal Error: ");
441                                         sb.append(exception.getMessage());
442                                         sb.append("\n");
443                                 }
444                                 
445                                 @Override
446                                 public void error(SAXParseException exception) throws SAXException {
447                                         sb.append("Fatal Error: ");
448                                         sb.append(exception.getMessage());
449                                         sb.append("\n");
450                                 }
451                         });
452                         doc = db.parse(requestBodyStream);
453                         if (sb.length() > 0) {
454                                 // some error happened
455                                 // doc is not null, because the parser parses even if it is not XSD conforming
456                                 return Response.status(Status.BAD_REQUEST).entity(sb.toString()).build();
457                         }
458                 } catch (SAXException | IOException e) {
459                         AbstractComponentInstanceResource.logger.debug("Could not parse XML", e);
460                         return Utils.getResponseForException(e);
461                 }
462                 try {
463                         u = JAXBSupport.createUnmarshaller();
464                         defs = (Definitions) u.unmarshal(doc);
465                 } catch (JAXBException e) {
466                         AbstractComponentInstanceResource.logger.debug("Could not unmarshal from request body stream", e);
467                         return Utils.getResponseForException(e);
468                 }
469                 
470                 // initial validity check
471                 
472                 // we allow changing the target namespace and the id
473                 // This allows for inserting arbitrary definitions XML
474                 //              if (!this.definitions.getTargetNamespace().equals(this.id.getNamespace().getDecoded())) {
475                 //                      return Response.status(Status.BAD_REQUEST).entity("Changing of the namespace is not supported").build();
476                 //              }
477                 //              this.definitions.setTargetNamespace(this.id.getNamespace().getDecoded());
478                 
479                 // TODO: check the provided definitions for validity
480                 
481                 TExtensibleElements tExtensibleElements = defs.getServiceTemplateOrNodeTypeOrNodeTypeImplementation().get(0);
482                 if (!tExtensibleElements.getClass().equals(this.createNewElement().getClass())) {
483                         return Response.status(Status.BAD_REQUEST).entity("First type in Definitions is not matching the type modeled by this resource").build();
484                 }
485                 
486                 this.definitions = defs;
487                 
488                 // replace existing element by retrieved data
489                 this.element = this.definitions.getServiceTemplateOrNodeTypeOrNodeTypeImplementation().get(0);
490                 
491                 // ensure that ids did not change
492                 // TODO: future work: raise error if user changed id or namespace
493                 this.copyIdToFields();
494                 
495                 return BackendUtils.persist(this);
496         }
497         
498         @GET
499         @Path("xml/")
500         @Produces(MediaType.TEXT_HTML)
501         public Response getXML() {
502                 Viewable viewable = new Viewable("/jsp/xmlSource.jsp", this);
503                 return Response.ok().entity(viewable).build();
504         }
505         
506         @Path("documentation/")
507         public DocumentationsResource getDocumentationsResource() {
508                 return new DocumentationsResource(this, this.getElement().getDocumentation());
509         }
510         
511         @Path("tags/")
512         public final TagsResource getTags() {
513                 return new TagsResource(this.id);
514         }
515         
516 }