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
10 * Oliver Kopp - initial API and implementation
11 *******************************************************************************/
12 package org.eclipse.winery.repository.resources;
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;
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;
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;
69 import com.sun.jersey.api.view.Viewable;
72 * Resource for a component (
74 * <li>ServiceTemplates,</li>
75 * <li>EntityTypes,</li>
76 * <li>EntityTypeImplementations,</li>
77 * <li>EntityTemplates</li>
79 * ). A component is directly nested in a TDefinitions element. See also
80 * {@link org.eclipse.winery.common.ids.definitions.TOSCAComponentId}
82 * Bundles all operations required for all components. e.g., namespace+XMLid,
83 * object comparison, import, export, tags
85 * Uses a TDefinitions document as storage.
87 * Additional setters and getters are added if it comes to Winery's extensions
88 * such as the color of a relationship type
90 public abstract class AbstractComponentInstanceResource implements Comparable<AbstractComponentInstanceResource>, IPersistable {
92 private static final Logger logger = LoggerFactory.getLogger(AbstractComponentInstanceResource.class);
94 protected final TOSCAComponentId id;
96 private final RepositoryFileReference ref;
98 // the object representing the data of this resource
99 private Definitions definitions = null;
101 // shortcut for this.definitions.getServiceTemplateOrNodeTypeOrNodeTypeImplementation().get(0);
102 protected TExtensibleElements element = null;
106 * Instantiates the resource. Assumes that the resource should exist
107 * (assured by the caller)
109 * The caller should <em>not</em> create the resource by other ways. E.g.,
110 * by instantiating this resource and then adding data.
112 public AbstractComponentInstanceResource(TOSCAComponentId id) {
115 // the resource itself exists
116 assert (Repository.INSTANCE.exists(id));
118 // the data file might not exist
119 this.ref = BackendUtils.getRefOfDefinitions(id);
120 if (Repository.INSTANCE.exists(this.ref)) {
128 * Convenience method for getId().getNamespace()
130 public final Namespace getNamespace() {
131 return this.id.getNamespace();
135 * Convenience method for getId().getXmlId()
137 public final XMLId getXmlId() {
138 return this.id.getXmlId();
142 * Convenience method for getId().getQName();
144 * @return the QName associated with this resource
146 public final QName getQName() {
147 return this.getId().getQName();
151 * Returns the id associated with this resource
153 public final TOSCAComponentId getId() {
158 * called from AbstractComponentResource
161 public final Response onDelete() {
162 Response res = BackendUtils.delete(this.id);
167 public final int compareTo(AbstractComponentInstanceResource o) {
168 return this.id.compareTo(o.id);
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());
180 throw new IllegalStateException();
183 throw new IllegalStateException();
188 public final int hashCode() {
189 return this.getId().hashCode();
194 public String getTOSCAId() {
195 return this.id.getXmlId().getDecoded();
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();
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})
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
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();
223 if (definitions != null) {
224 return Utils.getDefinitionsOfSelectedResource(this, uriInfo.getBaseUri());
225 } else if (csar != null) {
226 return this.getCSAR();
228 String type = Utils.getTypeForInstance(this.getClass());
229 String viewableName = "/jsp/" + Utils.getIntermediateLocationStringForType(type, "/") + "/" + type.toLowerCase() + ".jsp";
230 Viewable viewable = new Viewable(viewableName, this);
232 return Response.ok().entity(viewable).build();
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
238 // return Response.ok().header(HttpHeaders.VARY,
239 // HttpHeaders.ACCEPT).entity(viewable).build();
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();
249 return Utils.getCSARofSelectedResource(this);
253 * Returns the definitions of this resource. Includes required imports of
256 * @param csar used because plan generator's GET request lands here
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();
266 return Utils.getCSARofSelectedResource(this);
269 StreamingOutput so = new StreamingOutput() {
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<>();
277 exporter.exportTOSCA(AbstractComponentInstanceResource.this.id, output, conf);
278 } catch (JAXBException e) {
279 throw new WebApplicationException(e);
283 return Response.ok().type(MediaType.TEXT_XML).entity(so).build();
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
292 private void load() {
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);
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
311 throw new IllegalStateException("Wrong storage format: No ServiceTemplateOrNodeTypeOrNodeTypeImplementation found.");
317 public void persist() throws IOException {
318 BackendUtils.persist(this.definitions, this.ref, MediaTypes.MEDIATYPE_TOSCA_DEFINITIONS);
322 * Creates a new instance of the object represented by this resource
324 private void createNew() {
325 this.definitions = BackendUtils.createWrapperDefinitions(this.getId());
327 // create empty element
328 this.element = this.createNewElement();
330 // add the element to the definitions
331 this.definitions.getServiceTemplateOrNodeTypeOrNodeTypeImplementation().add(this.element);
334 this.copyIdToFields();
336 // ensure that the definitions is persisted. Ensures that export works.
337 BackendUtils.persist(this);
341 * Creates an empty instance of an Element.
343 * The implementors do <em>not</em>have to copy the ns and the id to the
344 * appropriate fields.
346 * we have two implementation possibilities:
348 * <li>a) each subclass implements this method and returns the appropriate
350 * <li>b) we use java reflection to invoke the right constructor as done in
353 * We opted for a) to increase readability of the code
355 protected abstract TExtensibleElements createNewElement();
358 * Copies the current id of the resource to the appropriate fields in the
361 * For instance, the id is put in the "name" field for EntityTypes
363 * We opted for a separate method from createNewElement to enable renaming
366 protected abstract void copyIdToFields();
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
374 * getDefinitions().getServiceTemplateOrNodeTypeOrNodeTypeImplementation
377 * @return TCapabilityType|...
379 public TExtensibleElements getElement() {
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
389 protected List<TImport> getImport() {
390 if (this.definitions == null) {
391 throw new IllegalStateException("Trying to access uninitalized definitions object");
393 return this.definitions.getImport();
397 * Returns an XML representation of the definitions
399 * We return the complete definitions to allow the user changes to it, such
400 * as adding imports, etc.
402 public String getDefinitionsAsXMLString() {
403 StringWriter w = new StringWriter();
404 Marshaller m = JAXBSupport.createMarshaller(true);
406 m.marshal(this.definitions, w);
407 } catch (JAXBException e) {
408 AbstractComponentInstanceResource.logger.error("Could not marshal definitions", e);
409 throw new IllegalStateException(e);
411 String res = w.toString();
416 * @return the reference to the internal Definitions object
418 public Definitions getDefinitions() {
419 return this.definitions;
423 @Consumes({MimeTypes.MIMETYPE_TOSCA_DEFINITIONS, MediaType.APPLICATION_XML, MediaType.TEXT_XML})
424 public Response updateDefinitions(InputStream requestBodyStream) {
428 final StringBuilder sb = new StringBuilder();
430 DocumentBuilder db = TOSCADocumentBuilderFactory.INSTANCE.getTOSCADocumentBuilder();
431 db.setErrorHandler(new ErrorHandler() {
434 public void warning(SAXParseException exception) throws SAXException {
439 public void fatalError(SAXParseException exception) throws SAXException {
440 sb.append("Fatal Error: ");
441 sb.append(exception.getMessage());
446 public void error(SAXParseException exception) throws SAXException {
447 sb.append("Fatal Error: ");
448 sb.append(exception.getMessage());
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();
458 } catch (SAXException | IOException e) {
459 AbstractComponentInstanceResource.logger.debug("Could not parse XML", e);
460 return Utils.getResponseForException(e);
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);
470 // initial validity check
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();
477 // this.definitions.setTargetNamespace(this.id.getNamespace().getDecoded());
479 // TODO: check the provided definitions for validity
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();
486 this.definitions = defs;
488 // replace existing element by retrieved data
489 this.element = this.definitions.getServiceTemplateOrNodeTypeOrNodeTypeImplementation().get(0);
491 // ensure that ids did not change
492 // TODO: future work: raise error if user changed id or namespace
493 this.copyIdToFields();
495 return BackendUtils.persist(this);
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();
506 @Path("documentation/")
507 public DocumentationsResource getDocumentationsResource() {
508 return new DocumentationsResource(this, this.getElement().getDocumentation());
512 public final TagsResource getTags() {
513 return new TagsResource(this.id);