/* * Copyright (C) 2005-2012 BetaCONCEPT Limited * * This file is part of Astroboa. * * Astroboa is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Astroboa is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Astroboa. If not, see <http://www.gnu.org/licenses/>. */ package org.betaconceptframework.astroboa.resourceapi.resource; import java.io.IOException; import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HttpMethod; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.Response.Status; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.time.DurationFormatUtils; import org.betaconceptframework.astroboa.api.model.BinaryChannel.ContentDispositionType; import org.betaconceptframework.astroboa.api.model.BinaryProperty; import org.betaconceptframework.astroboa.api.model.CalendarProperty; import org.betaconceptframework.astroboa.api.model.CmsProperty; import org.betaconceptframework.astroboa.api.model.ContentObject; import org.betaconceptframework.astroboa.api.model.SimpleCmsProperty; import org.betaconceptframework.astroboa.api.model.ValueType; import org.betaconceptframework.astroboa.api.model.io.FetchLevel; import org.betaconceptframework.astroboa.api.model.io.ImportConfiguration; import org.betaconceptframework.astroboa.api.model.io.ImportConfiguration.PersistMode; import org.betaconceptframework.astroboa.api.model.io.ResourceRepresentationType; import org.betaconceptframework.astroboa.api.model.query.CacheRegion; import org.betaconceptframework.astroboa.api.model.query.CmsOutcome; import org.betaconceptframework.astroboa.api.model.query.Order; import org.betaconceptframework.astroboa.api.model.query.criteria.ContentObjectCriteria; import org.betaconceptframework.astroboa.api.security.exception.CmsUnauthorizedAccessException; import org.betaconceptframework.astroboa.client.AstroboaClient; import org.betaconceptframework.astroboa.commons.excelbuilder.WorkbookBuilder; import org.betaconceptframework.astroboa.model.factory.CmsCriteriaFactory; import org.betaconceptframework.astroboa.model.factory.CriterionFactory; import org.betaconceptframework.astroboa.resourceapi.utility.ContentApiUtils; import org.betaconceptframework.astroboa.resourceapi.utility.IndexExtractor; import org.betaconceptframework.astroboa.util.CmsConstants; import org.betaconceptframework.astroboa.util.DateUtils; import org.betaconceptframework.astroboa.util.PropertyExtractor; import org.jboss.resteasy.plugins.providers.multipart.InputPart; import org.jboss.resteasy.plugins.providers.multipart.MultipartRelatedInput; import org.jboss.resteasy.util.GenericType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Gregory Chomatas (gchomatas@betaconcept.com) * @author Savvas Triantafyllou (striantafyllou@betaconcept.com) * */ public class ContentObjectResource extends AstroboaResource{ private final Logger logger = LoggerFactory.getLogger(getClass()); public ContentObjectResource(AstroboaClient astroboaClient) { super(astroboaClient); } // The methods which produce JSON or XML allow "callback" as one extra query parameter // in order to support XML with Padding or JSON with Padding (JSONP) and overcome the SPO restriction of browsers // This means that if a "callback" query parameter is provided then the XML or JSON result will be wrapped inside a "callback" script // API URLs for single content object resource @GET @Produces("*/*") @Path("/{contentObjectIdOrName: " + CmsConstants.UUID_OR_SYSTEM_NAME_REG_EXP_FOR_RESTEASY + "}") public Response getContentObjectByIdOrNameAnyResponse( @PathParam("contentObjectIdOrName") String contentObjectIdOrName, @QueryParam("projectionPaths") String commaDelimitedProjectionPaths, @QueryParam("output") String output, @QueryParam("callback") String callback, @QueryParam("prettyPrint") String prettyPrint){ boolean prettyPrintEnabled = ContentApiUtils.isPrettyPrintEnabled(prettyPrint); /*if (output == null) { return getContentObjectByIdOrName(contentObjectIdOrName, Output.XML, callback,prettyPrintEnabled); }*/ Output outputEnum = ContentApiUtils.getOutputType(output, Output.XML); return getContentObjectByIdOrName(contentObjectIdOrName, commaDelimitedProjectionPaths, outputEnum, callback,prettyPrintEnabled); } @GET @Produces({MediaType.APPLICATION_JSON}) @Path("/{contentObjectIdOrName: " + CmsConstants.UUID_OR_SYSTEM_NAME_REG_EXP_FOR_RESTEASY + "}") public Response getContentObjectByIdAsJson( @PathParam("contentObjectIdOrName") String contentObjectIdOrName, @QueryParam("projectionPaths") String commaDelimitedProjectionPaths, @QueryParam("output") String output, @QueryParam("callback") String callback, @QueryParam("prettyPrint") String prettyPrint){ // URL-based negotiation overrides any Accept header sent by the client //i.e. if the url specifies the desired response type in the "output" parameter this method // will return the media type specified in "output" request parameter. Output outputEnum = Output.JSON; if (StringUtils.isNotBlank(output)) { outputEnum = Output.valueOf(output.toUpperCase()); } boolean prettyPrintEnabled = ContentApiUtils.isPrettyPrintEnabled(prettyPrint); return getContentObjectByIdOrName(contentObjectIdOrName, commaDelimitedProjectionPaths, outputEnum, callback, prettyPrintEnabled); } @GET @Produces(MediaType.APPLICATION_XML) @Path("/{contentObjectIdOrName: " + CmsConstants.UUID_OR_SYSTEM_NAME_REG_EXP_FOR_RESTEASY + "}") public Response getContentObjectByIdAsXml( @PathParam("contentObjectIdOrName") String contentObjectIdOrName, @QueryParam("projectionPaths") String commaDelimitedProjectionPaths, @QueryParam("output") String output, @QueryParam("callback") String callback, @QueryParam("prettyPrint") String prettyPrint){ // URL-based negotiation overrides any Accept header sent by the client //i.e. if the url specifies the desired response type in the "output" parameter this method // will return the media type specified in "output" request parameter. Output outputEnum = Output.XML; if (StringUtils.isNotBlank(output)) { outputEnum = Output.valueOf(output.toUpperCase()); } boolean prettyPrintEnabled = ContentApiUtils.isPrettyPrintEnabled(prettyPrint); return getContentObjectByIdOrName(contentObjectIdOrName, commaDelimitedProjectionPaths, outputEnum, callback,prettyPrintEnabled); } // API URLs for content object properties //The propertyPath at the end of the URL is the full path to a property value. // Property Path segments are separated by dots // For multiple value properties the identifier of the property or the value if it is a binary channel between brackets indicates which one of the values to return. @Path("/{contentObjectIdOrName: " + CmsConstants.UUID_OR_SYSTEM_NAME_REG_EXP_FOR_RESTEASY + "}" + "/{propertyPath: " + CmsConstants.PROPERTY_PATH_WITH_ID_REG_EXP_FOR_RESTEASY + "}") public AstroboaResource getContentObjectPropertyUsingIdentifierInsteadOfIndex( @PathParam("contentObjectIdOrName") String contentObjectIdOrName, @PathParam("propertyPath") String propertyPath) { try { ContentObject contentObject = retrieveContentObjectByIdOrSystemName(contentObjectIdOrName, FetchLevel.ENTITY, null); if (contentObject == null) { logger.warn("The provided content object id / system name {} does not correspond to a content object or you do not have permission to access the requested object", contentObjectIdOrName); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } // We allow to put a mime type suffix (i.e. .jpg, .png, .doc) at the end of the property path // This is required when accessing binary properties and the programs which consume the // URL binary outcome do not read the mime type and filename from the header but rather depend on the mime type suffix at // the end of the URL in order to determine how to treat the binary content. // Additionally we may utilize this suffix in latter version of the API to support the delivery of different representations // of the property contents. // So we need to check if the property path contains a mime type suffix and remove it // This may cause problems if a requested property itself is named under the name of a mime type suffix. // To resolve this potential problem it is required to always put a mime type suffix at the end of URLs that read property values // if the requested property is named under the name of a mime type suffix // (i.e. .../objects/{contentObjectId}/myImageWithMultipleFormats.jpg.jpg this will result in removing the last "jpg" suffix but keep the previous one which corresponds to a // property named "jpg") if (propertyPath != null && !propertyPath.endsWith("]")){ String candidateMimeTypeSuffix = StringUtils.substringAfterLast(propertyPath, "."); if (ContentApiUtils.isKnownMimeTypeSuffix(candidateMimeTypeSuffix)) { propertyPath = StringUtils.substringBeforeLast(propertyPath, "."); } } //Extract property along with the value identifier or the value index PropertyExtractor propertyExtractor = null; //Load Property according to property path CmsProperty property = null; try{ propertyExtractor = new PropertyExtractor(contentObject, propertyPath); property = propertyExtractor.getProperty(); } catch (Exception e){ logger.warn("Could not load provided property using path '"+ propertyPath+"' from contentObject "+contentObjectIdOrName, e); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } if (property == null) { logger.warn("The provided property '{}' for content object with id or system name '{}' does not exist",propertyPath, contentObjectIdOrName); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } switch (property.getValueType()) { case Complex: logger.warn("The provided property '{}' for content object with id or system name '{}' is complex. Currently only simple type property values or binary channel content can be returned through this API call",propertyPath, contentObjectIdOrName); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); case Binary: if (propertyExtractor.getIdentifierOfTheValueOfTheProperty() == null){ return new BinaryChannelResource(astroboaClient, contentObject, (BinaryProperty) property, propertyExtractor.getIndexOfTheValueOfTheProperty()); } else{ return new BinaryChannelResource(astroboaClient, contentObject, (BinaryProperty) property, propertyExtractor.getIdentifierOfTheValueOfTheProperty()); } case ContentType: logger.error("Astroboa returned value type '"+ValueType.ContentType+"' for property '{}' for content object with id or system name '{}'. This should never happen", propertyPath, contentObjectIdOrName); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); default: if (propertyExtractor.getIdentifierOfTheValueOfTheProperty() != null){ logger.warn("The provided property '{}' for content object with id or system name '{}' is a simple non-binary property but user has provided an identifier instead of an index.",propertyPath, contentObjectIdOrName); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } return new SimplePropertyResource(astroboaClient, contentObject, (SimpleCmsProperty) property, propertyExtractor.getIndexOfTheValueOfTheProperty()); } } catch (WebApplicationException e) { throw e; } catch (Exception e) { logger.error("A problem occured while retrieving property: '" + propertyPath + "' for content object with id or system name: " + contentObjectIdOrName, e); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } } // API URLs for content object properties //The propertyPath at the end of the URL is the full path to a property value. // Property Path segments are separated by dots // For multiple value properties an index between brackets indicates which one of the values to return. // For example departments.department[0] or departments.department[0].jobPositions.jobPosition[0] // The regular expression for the path is ^[A-Za-z0-9_\\-]+(\\[[0-9]+\\])?(\\.[A-Za-z0-9_\\-]+(\\[[0-9]+\\])?)*$ @Path("/{contentObjectIdOrName: " + CmsConstants.UUID_OR_SYSTEM_NAME_REG_EXP_FOR_RESTEASY + "}" + "/{propertyPath: " + CmsConstants.PROPERTY_PATH_REG_EXP_FOR_RESTEASY + "}") public AstroboaResource getContentObjectProperty( @PathParam("contentObjectIdOrName") String contentObjectIdOrName, @PathParam("propertyPath") String propertyPath) { try { ContentObject contentObject = retrieveContentObjectByIdOrSystemName(contentObjectIdOrName, FetchLevel.ENTITY, null); if (contentObject == null) { logger.warn("The provided content object id / system name {} does not correspond to a content object or you do not have permission to access the requested object", contentObjectIdOrName); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } // We allow to put a mime type suffix (i.e. .jpg, .png, .doc) at the end of the property path // This is required when accessing binary properties and the programs which consume the // URL binary outcome do not read the mime type and filename from the header but rather depend on the mime type suffix at // the end of the URL in order to determine how to treat the binary content. // Additionally we may utilize this suffix in latter version of the API to support the delivery of different representations // of the property contents. // So we need to check if the property path contains a mime type suffix and remove it // This may cause problems if a requested property itself is named under the name of a mime type suffix. // To resolve this potential problem it is required to always put a mime type suffix at the end of URLs that read property values // if the requested property is named under the name of a mime type suffix // (i.e. .../objects/{contentObjectId}/myImageWithMultipleFormats.jpg.jpg this will result in removing the last "jpg" suffix but keep the previous one which corresponds to a // property named "jpg") if (propertyPath != null && !propertyPath.endsWith("]")){ String candidateMimeTypeSuffix = StringUtils.substringAfterLast(propertyPath, "."); if (ContentApiUtils.isKnownMimeTypeSuffix(candidateMimeTypeSuffix)) { propertyPath = StringUtils.substringBeforeLast(propertyPath, "."); } } //Check if a value index exists and extract it IndexExtractor indexExtractor = new IndexExtractor(propertyPath); String propertyPathWithoutIndex = indexExtractor.getPropertyPathWithoutIndex(); int valueIndex = indexExtractor.getIndex(); //Load Property according to property path CmsProperty property = null; try{ property = contentObject.getCmsProperty(propertyPathWithoutIndex); } catch(Exception e){ logger.warn("Could not load provided property using path '"+ propertyPathWithoutIndex+"' from contentObject "+contentObjectIdOrName, e); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } if (property == null) { logger.warn("The provided property '{}' for content object with id or system name '{}' does not exist",propertyPathWithoutIndex, contentObjectIdOrName); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } switch (property.getValueType()) { case Complex: logger.warn("The provided property '{}' for content object with id or system name '{}' is complex. Currently only simple type property values or binary channel content can be returned through this API call",propertyPathWithoutIndex, contentObjectIdOrName); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); case Binary: return new BinaryChannelResource(astroboaClient, contentObject, (BinaryProperty) property, valueIndex); case ContentType: logger.error("Astroboa returned value type '"+ValueType.ContentType+"' for property '{}' for content object with id or system name '{}'. This should never happen", propertyPathWithoutIndex, contentObjectIdOrName); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); default: return new SimplePropertyResource(astroboaClient, contentObject, (SimpleCmsProperty) property, valueIndex); } } catch (WebApplicationException e) { throw e; } catch (Exception e) { logger.error("A problem occured while retrieving property: '" + propertyPath + "' for content object with id or system name: " + contentObjectIdOrName, e); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } } // API URLs for content object resource collections @GET @Produces(MediaType.APPLICATION_XML) public Response getContentObjectsAsXML( @QueryParam("cmsQuery") String cmsQuery, @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit, @QueryParam("projectionPaths") String commaDelimitedProjectionPaths, @QueryParam("orderBy") String orderBy, @QueryParam("output") String output, @QueryParam("template") String templateIdOrSystemName, @QueryParam("callback") String callback, @QueryParam("prettyPrint") String prettyPrint){ // URL-based negotiation overrides any Accept header sent by the client //i.e. if the url specifies the desired response type in the "output" parameter this method // will return the media type specified in "output" request parameter. Output outputEnum = Output.XML; if (StringUtils.isNotBlank(output)) { outputEnum = Output.valueOf(output.toUpperCase()); } return retrieveContentObjects( cmsQuery, offset, limit, commaDelimitedProjectionPaths, orderBy, outputEnum, templateIdOrSystemName, callback, prettyPrint); } @GET @Produces(MediaType.APPLICATION_JSON) public Response getContentObjectsAsJson( @QueryParam("cmsQuery") String cmsQuery, @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit, @QueryParam("projectionPaths") String commaDelimitedProjectionPaths, @QueryParam("orderBy") String orderBy, @QueryParam("output") String output, @QueryParam("template") String templateIdOrSystemName, @QueryParam("callback") String callback, @QueryParam("prettyPrint") String prettyPrint){ // URL-based negotiation overrides any Accept header sent by the client //i.e. if the url specifies the desired response type in the "output" parameter this method // will return the media type specified in "output" request parameter. Output outputEnum = Output.JSON; if (StringUtils.isNotBlank(output)) { outputEnum = Output.valueOf(output.toUpperCase()); } return retrieveContentObjects( cmsQuery, offset, limit, commaDelimitedProjectionPaths, orderBy, outputEnum, templateIdOrSystemName, callback, prettyPrint); } @GET @Produces("application/vnd.ms-excel") public Response getContentObjectsInXls( @QueryParam("cmsQuery") String cmsQuery, @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit, @QueryParam("projectionPaths") String commaDelimitedProjectionPaths, @QueryParam("orderBy") String orderBy, @QueryParam("output") String output, @QueryParam("template") String templateIdOrSystemName, @QueryParam("callback") String callback, @QueryParam("prettyPrint") String prettyPrint){ // URL-based negotiation overrides any Accept header sent by the client //i.e. if the url specifies the desired response type in the "output" parameter this method // will return the media type specified in "output" request parameter. Output outputEnum = Output.XLS; if (StringUtils.isNotBlank(output)) { outputEnum = Output.valueOf(output.toUpperCase()); } return retrieveContentObjects( cmsQuery, offset, limit, commaDelimitedProjectionPaths, orderBy, outputEnum, templateIdOrSystemName, callback, prettyPrint); } /* Returning html or pdf is based on facelets and seam which have been currently * removed due to problems with seam resource servlet when multiple seam wars are deployed @GET @Produces({MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML_XML}) public Response getContentObjectsAsXHTML( @QueryParam("cmsQuery") String cmsQuery, @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit, @QueryParam("projectionPaths") String commaDelimitedProjectionPaths, @QueryParam("orderBy") String orderBy, @QueryParam("output") String output, @QueryParam("template") String templateIdOrSystemName, @QueryParam("callback") String callback){ // URL-based negotiation overrides any Accept header sent by the client //i.e. if the url specifies the desired response type in the "output" parameter this method // will return the media type specified in "output" request parameter. Output outputEnum = Output.XHTML; if (StringUtils.isNotBlank(output)) { outputEnum = Output.valueOf(output.toUpperCase()); } return retrieveContentObjects( cmsQuery, offset, limit, commaDelimitedProjectionPaths, orderBy, outputEnum, templateIdOrSystemName, callback); } @GET @Produces({"application/pdf"}) public Response getContentObjectsAsPDF( @QueryParam("cmsQuery") String cmsQuery, @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit, @QueryParam("projectionPaths") String commaDelimitedProjectionPaths, @QueryParam("orderBy") String orderBy, @QueryParam("output") String output, @QueryParam("template") String templateIdOrSystemName, @QueryParam("callback") String callback){ // URL-based negotiation overrides any Accept header sent by the client //i.e. if the url specifies the desired response type in the "output" parameter this method // will return the media type specified in "output" request parameter. Output outputEnum = Output.PDF; if (StringUtils.isNotBlank(output)) { outputEnum = Output.valueOf(output.toUpperCase()); } return retrieveContentObjects( cmsQuery, offset, limit, commaDelimitedProjectionPaths, orderBy, outputEnum, templateIdOrSystemName, callback); } */ @GET @Produces("*/*") public Response getContentObjects( @QueryParam("cmsQuery") String cmsQuery, @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit, @QueryParam("projectionPaths") String commaDelimitedProjectionPaths, @QueryParam("orderBy") String orderBy, @QueryParam("output") String output, @QueryParam("template") String templateIdOrSystemName, @QueryParam("callback") String callback, @QueryParam("prettyPrint") String prettyPrint){ /*if (output == null) { return retrieveContentObjects(cmsQuery, offset, limit, commaDelimitedProjectionPaths, orderBy, Output.XML, templateIdOrSystemName, callback); }*/ Output outputEnum = ContentApiUtils.getOutputType(output, Output.XML); return retrieveContentObjects( cmsQuery, offset, limit, commaDelimitedProjectionPaths, orderBy, outputEnum, templateIdOrSystemName, callback, prettyPrint); } private Response getContentObjectByIdOrName(String contentObjectIdOrSystemName, String commaDelimitedProjectionPaths, Output output, String callback, boolean prettyPrint){ if (StringUtils.isBlank(contentObjectIdOrSystemName)){ logger.warn("No contentObjectId provided"); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } Date lastModified = null; String contentObjectXmlorJson = retrieveContentObjectXMLorJSONByIdOrSystemName(contentObjectIdOrSystemName, commaDelimitedProjectionPaths, output, lastModified, prettyPrint); if (StringUtils.isBlank(contentObjectXmlorJson)) { throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } try { StringBuilder resourceRepresentation = new StringBuilder(); if (StringUtils.isBlank(callback)) { resourceRepresentation.append(contentObjectXmlorJson); } else { switch (output) { case XML: { ContentApiUtils.generateXMLP(resourceRepresentation, contentObjectXmlorJson, callback); break; } case JSON: ContentApiUtils.generateJSONP(resourceRepresentation, contentObjectXmlorJson, callback); break; } } return ContentApiUtils.createResponse(resourceRepresentation, output, callback, lastModified); } catch(Exception e) { logger.error("ContentObejct id/name " + contentObjectIdOrSystemName,e); throw new WebApplicationException(HttpURLConnection.HTTP_BAD_REQUEST); } } private Response retrieveContentObjects( String cmsQuery, Integer offset,Integer limit, String commaDelimitedProjectionPaths, String orderBy, Output output, String templateIdOrSystemName, String callback, String prettyPrint) { if (output == null) { output = Output.XML; } boolean prettyPrintEnabled = ContentApiUtils.isPrettyPrintEnabled(prettyPrint); try { //Build ContentObject criteria ContentObjectCriteria contentObjectCriteria = buildCriteria(cmsQuery, offset, limit, commaDelimitedProjectionPaths, orderBy, prettyPrintEnabled); String queryResult = null; StringBuilder resourceRepresentation = new StringBuilder(); switch (output) { case XML:{ queryResult = astroboaClient.getContentService().searchContentObjects(contentObjectCriteria, ResourceRepresentationType.XML); if (StringUtils.isBlank(callback)) { resourceRepresentation.append(queryResult); } else { ContentApiUtils.generateXMLP(resourceRepresentation, queryResult, callback); } break; } case JSON: queryResult = astroboaClient.getContentService().searchContentObjects(contentObjectCriteria, ResourceRepresentationType.JSON); if (StringUtils.isBlank(callback)) { resourceRepresentation.append(queryResult); } else { ContentApiUtils.generateJSONP(resourceRepresentation, queryResult, callback); } break; case XLS : CmsOutcome<ContentObject> outcome = astroboaClient.getContentService().searchContentObjects(contentObjectCriteria, ResourceRepresentationType.CONTENT_OBJECT_LIST); WorkbookBuilder workbookBuilder = new WorkbookBuilder(astroboaClient.getDefinitionService(), "en"); int rowIndex = 2; for (ContentObject object : outcome.getResults()) { workbookBuilder.addContentObjectToWorkbook(object); ++rowIndex; //Limit to the first 5000 content objects if (rowIndex > 5000){ break; } } String filename = createFilename(workbookBuilder); return ContentApiUtils.createResponseForExcelWorkbook( workbookBuilder.getWorkbook(), ContentDispositionType.ATTACHMENT, filename + ".xls", null); /* This functionality is temporarily removed until the resolution of seam resource servlet problems * when multiple wars are deployed case XHTML: { List<ContentObject> contentObjects = searchContentObjects(contentObjectCriteria); Contexts.getEventContext().set("contentObjects", contentObjects); Contexts.getEventContext().set("repositoryId", AstroboaClientContextHolder.getActiveRepositoryId()); Contexts.getEventContext().set("templateObjectIdOrSystemName", templateIdOrSystemName); Contexts.getEventContext().set("templateProperty", "xhtml"); Renderer renderer = Renderer.instance(); String xhtmlOutput = renderer.render("/dynamicPage.xhtml"); if (StringUtils.isNotBlank(xhtmlOutput)) { resourceRepresentation.append(xhtmlOutput); } break; } case PDF: { List<ContentObject> contentObjects = searchContentObjects(contentObjectCriteria); Contexts.getEventContext().set("contentObjects", contentObjects); Contexts.getEventContext().set("repositoryId", AstroboaClientContextHolder.getActiveRepositoryId()); Contexts.getEventContext().set("templateObjectIdOrSystemName", templateIdOrSystemName); Contexts.getEventContext().set("templateProperty", "pdf"); byte[] pdfBytes = createPDF(""); return ContentApiUtils.createBinaryResponse( pdfBytes, "application/pdf", ContentDispositionType.ATTACHMENT, contentObjectCriteria.getXPathQuery() + ".pdf", null); } */ } return ContentApiUtils.createResponse(resourceRepresentation, output, callback, null); } catch(Exception e){ return ContentApiUtils.createResponseForException(Response.Status.BAD_REQUEST, e, true, "Cms Query "+cmsQuery); } } private ContentObjectCriteria buildCriteria(String cmsQuery, Integer offset, Integer limit, String commaDelimitedProjectionPaths, String orderBy, boolean prettyPrint) { //Build ContentObject criteria ContentObjectCriteria contentObjectCriteria = CmsCriteriaFactory.newContentObjectCriteria(); if (offset == null || offset < 0){ contentObjectCriteria.setOffset(0); } else{ contentObjectCriteria.setOffset(offset); } if (limit == null || limit < 0){ contentObjectCriteria.setLimit(50); } else{ contentObjectCriteria.setLimit(limit); } if (StringUtils.isNotBlank(commaDelimitedProjectionPaths)){ contentObjectCriteria.getPropertyPathsWhichWillBePreLoaded().addAll(Arrays.asList(commaDelimitedProjectionPaths.split(CmsConstants.COMMA))); } else{ contentObjectCriteria.getRenderProperties().renderAllContentObjectProperties(true); } contentObjectCriteria.getRenderProperties().prettyPrint(prettyPrint); //Parse query if (StringUtils.isNotBlank(cmsQuery)) { CriterionFactory.parse(cmsQuery, contentObjectCriteria); } else{ logger.warn("No query parameter was found. All content objects will be returned according to limit {} and offset {}", contentObjectCriteria.getLimit(), contentObjectCriteria.getOffset()); } //Parse order by //Order by value expects to follow pattern // property.path asc,property.path2 desc,property.path3 if (StringUtils.isNotBlank(orderBy)) { String[] propertyPathsWithOrder = StringUtils.split(orderBy, ","); if (!ArrayUtils.isEmpty(propertyPathsWithOrder)) { for (String propertyWithOrder : propertyPathsWithOrder) { String[] propertyItems = StringUtils.split(propertyWithOrder, " "); if (! ArrayUtils.isEmpty(propertyItems) && propertyItems.length == 2) { String property = propertyItems[0]; String order = propertyItems[1]; //Check to see if order is valid if (StringUtils.isNotBlank(property)) { if (StringUtils.equals("desc", order)) { contentObjectCriteria.addOrderProperty(property, Order.descending); } else { //Any other value (even invalid) set default order which is ascending contentObjectCriteria.addOrderProperty(property, Order.ascending); } } } } } } return contentObjectCriteria; } private ContentObject retrieveContentObjectByIdOrSystemName(String contentObjectIdOrSystemName, FetchLevel fetchLevel, List<String> propertyPaths) { try { return astroboaClient.getContentService().getContentObject(contentObjectIdOrSystemName, ResourceRepresentationType.CONTENT_OBJECT_INSTANCE, fetchLevel, CacheRegion.NONE, propertyPaths, false); } catch (Exception e) { return null; } } private String retrieveContentObjectXMLorJSONByIdOrSystemName(String contentObjectIdOrSystemName, String commaDelimitedProjectionPaths, Output output, Date lastModified, boolean prettyPrint) { try { ContentObject contentObject = null; String[] propertyPathsArray = null; if (StringUtils.isBlank(commaDelimitedProjectionPaths)) { contentObject = retrieveContentObjectByIdOrSystemName(contentObjectIdOrSystemName, FetchLevel.FULL, null); } else { propertyPathsArray = commaDelimitedProjectionPaths.split(","); List<String> propertyPaths = Arrays.asList(propertyPathsArray); contentObject = retrieveContentObjectByIdOrSystemName(contentObjectIdOrSystemName, FetchLevel.ENTITY, propertyPaths); } if (contentObject == null) { return null; } lastModified = ((CalendarProperty)contentObject.getCmsProperty("profile.modified")).getSimpleTypeValueAsDate(); //Default output is XML if (output == null) { return contentObject.xml(prettyPrint); } switch (output) { case XML: return contentObject.xml(prettyPrint, false, propertyPathsArray); case JSON: return contentObject.json(prettyPrint, false, propertyPathsArray); default: return contentObject.xml(prettyPrint, false, propertyPathsArray); } } catch (Exception e) { return null; } } /* This functionality is temporarily removed until the resolution of seam resource servlet problems * when multiple wars are deployed private byte[] createPDF(String path) { // String DATA_STORE = // "org.jboss.seam.document.documentStore.dataStore"; //EmptyFacesContext emptyFacesContext = new EmptyFacesContext(); byte[] bytes = null; try { Renderer render = Renderer.instance(); render.render("/dynamicPage.xhtml"); DocumentStore doc = DocumentStore.instance(); if (doc != null) { DocumentData data = doc.getDocumentData("1"); ByteArrayDocumentData byteData = null; if (data instanceof ByteArrayDocumentData) { byteData = (ByteArrayDocumentData) data; } else { throw new IllegalArgumentException("Couldnt get the bytes from the pdf document, unkown class " + data.getClass().getName()); } bytes = byteData.getData(); } } catch (Exception ex) { logger.error("Error when trying to get the content of the pdf in bytes with the message #0", ex.getMessage()); ex.printStackTrace(); } finally { //emptyFacesContext.restore(); } return bytes; } */ private List<ContentObject> searchContentObjects(ContentObjectCriteria contentObjectCriteria) { CmsOutcome<ContentObject> cmsOutcome = astroboaClient.getContentService().searchContentObjects(contentObjectCriteria, ResourceRepresentationType.CONTENT_OBJECT_LIST); if (cmsOutcome.getResults() != null){ return cmsOutcome.getResults(); } return new ArrayList<ContentObject>(); } @POST @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response postContentObject(String requestContent) { long start = System.currentTimeMillis(); Response response = saveContentObjectString(requestContent, HttpMethod.POST, true, true); logger.debug(" POST ContentObject in {}", DurationFormatUtils.formatDurationHMS(System.currentTimeMillis() - start)); return response; } @PUT @Path("/{contentObjectIdOrName: " + CmsConstants.UUID_OR_SYSTEM_NAME_REG_EXP_FOR_RESTEASY + "}") @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Response putContentObjectByIdOrName( @PathParam("contentObjectIdOrName") String contentObjectIdOrName, @QueryParam("updateLastModificationTime") String updateLastModificationTime, String requestContent){ if (StringUtils.isBlank(contentObjectIdOrName)){ logger.warn("Use HTTP PUT to save object {} but no id or system name was provided ", requestContent); throw new WebApplicationException(HttpURLConnection.HTTP_BAD_REQUEST); } long start = System.currentTimeMillis(); Response response = saveContentObjectByIdOrName(contentObjectIdOrName, requestContent, HttpMethod.PUT, ContentApiUtils.shouldUpdateLastModificationTime(updateLastModificationTime)); logger.debug(" PUT ContentObject {} in {}", contentObjectIdOrName, DurationFormatUtils.formatDurationHMS(System.currentTimeMillis() - start)); return response; } @DELETE @Path("/{contentObjectIdOrName: " + CmsConstants.UUID_OR_SYSTEM_NAME_REG_EXP_FOR_RESTEASY + "}") public Response deleteContentObjectByIdOrName( @PathParam("contentObjectIdOrName") String contentObjectIdOrName){ if (StringUtils.isBlank(contentObjectIdOrName)){ logger.warn("No id or system name was provided. Delete request cannot proceed"); throw new WebApplicationException(HttpURLConnection.HTTP_BAD_REQUEST); } try{ boolean objectDeleted = astroboaClient.getContentService().deleteContentObject(contentObjectIdOrName); return ContentApiUtils.createResponseForHTTPDelete(objectDeleted,contentObjectIdOrName); } catch(CmsUnauthorizedAccessException e){ throw new WebApplicationException(HttpURLConnection.HTTP_UNAUTHORIZED); } catch(Exception e){ logger.error("",e); throw new WebApplicationException(HttpURLConnection.HTTP_BAD_REQUEST); } } @POST @Consumes("multipart/related") public Response postObjectMultipartRelated(MultipartRelatedInput multipartRelatedInput){ return saveContentFromMultipartRelatedRequest(null, multipartRelatedInput, HttpMethod.POST, true); } @PUT @Consumes("multipart/related") @Path("/{contentObjectIdOrName: " + CmsConstants.UUID_OR_SYSTEM_NAME_REG_EXP_FOR_RESTEASY + "}") public Response putObjectMultipartRelated( @PathParam("contentObjectIdOrName") String contentObjectIdOrName, @QueryParam("updateLastModificationTime") String updateLastModificationTime, MultipartRelatedInput multipartRelatedInput){ return saveContentFromMultipartRelatedRequest(contentObjectIdOrName, multipartRelatedInput, HttpMethod.PUT, ContentApiUtils.shouldUpdateLastModificationTime(updateLastModificationTime)); } private Response saveContentFromMultipartRelatedRequest(String contentObjectIdOrName, MultipartRelatedInput multipartRelatedInput, String httpMethod, boolean updateLastModificationTime) { try { //Obtain the part which contains the object's JSON/XML InputPart partWhichContainsObjectSource = getMessagePartWithObjectSource(multipartRelatedInput); //Check that mime type of the content is a valid one ResourceRepresentationType resourceRepresentationType = checkMediaTypeIsValid(partWhichContainsObjectSource); ContentObject contentObjectToBeSaved = retrieveObjectSourceFromMessageAndImportWithoutSave(multipartRelatedInput, partWhichContainsObjectSource); boolean entityIsNew = objectIsNew(contentObjectIdOrName, httpMethod, contentObjectToBeSaved); //Save content object try{ contentObjectToBeSaved = astroboaClient.getContentService().save(contentObjectToBeSaved, false, updateLastModificationTime, null); return ContentApiUtils.createSuccessfulResponseForPUTOrPOST(contentObjectToBeSaved, httpMethod, resourceRepresentationType, entityIsNew); } catch(CmsUnauthorizedAccessException e){ throw new WebApplicationException(HttpURLConnection.HTTP_UNAUTHORIZED); } catch(Exception e){ logger.error("",e); throw new WebApplicationException(e, HttpURLConnection.HTTP_INTERNAL_ERROR); } } catch (WebApplicationException e) { throw e; } catch (Exception e) { throw new WebApplicationException(ContentApiUtils.createResponseForException(Status.INTERNAL_SERVER_ERROR, e, true, "A problem occured while saving form data for object with id or system name: " + contentObjectIdOrName)); } } private ContentObject retrieveObjectSourceFromMessageAndImportWithoutSave( MultipartRelatedInput multipartRelatedInput, InputPart partWhichContainsObjectSource) throws IOException { Map<String, byte[]> binaryContentMap= new HashMap<String, byte[]>(); //Iterate through the input parts to collect binary data for (Entry<String, InputPart> inputPartEntry : multipartRelatedInput.getRelatedMap().entrySet()){ String partId = inputPartEntry.getKey(); if (StringUtils.equals(partId, multipartRelatedInput.getStart())){ //Do not process root part continue; } InputPart inputPart = inputPartEntry.getValue(); boolean base64Encoded = partIsBase64Encoded(inputPart.getHeaders()); byte[] binaryContent = inputPart.getBody(new GenericType<byte[]>() {}); if (base64Encoded){ binaryContent = Base64.decodeBase64(binaryContent); } binaryContentMap.put(partId, binaryContent); } //Import content but do not save it ImportConfiguration importConfiguration = ImportConfiguration.object() .persist(PersistMode.DO_NOT_PERSIST) .addBinaryContent(binaryContentMap) .build(); ContentObject contentObjectToBeSaved = astroboaClient.getImportService().importContentObject(partWhichContainsObjectSource.getBodyAsString(), importConfiguration); if (logger.isDebugEnabled()){ logger.debug("XML output of imported content object \n{}", contentObjectToBeSaved.xml(true)); } return contentObjectToBeSaved; } private boolean partIsBase64Encoded(MultivaluedMap<String, String> headers) { if (headers!=null && headers.isEmpty()){ for (Entry<String, List<String>> headerEntry: headers.entrySet()){ if (StringUtils.equalsIgnoreCase(headerEntry.getKey(), "Content-Transfer-Encoding")){ if (headerEntry.getValue() != null && headerEntry.getValue().contains("base64")){ return true ; } } } } return false; } private ResourceRepresentationType checkMediaTypeIsValid(InputPart partWhichContainsObjectSource) { MediaType mediaType = partWhichContainsObjectSource.getMediaType(); if (mediaType == null ){ throw new WebApplicationException(ContentApiUtils.createResponseForException(Status.BAD_REQUEST, null, false, "No Content-Type Header found for object content")); } if (mediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE)){ return ResourceRepresentationType.JSON; } else if (mediaType.isCompatible(MediaType.APPLICATION_XML_TYPE)){ return ResourceRepresentationType.XML; } else if (mediaType.isCompatible(MediaType.TEXT_PLAIN_TYPE)){ return null; } else{ throw new WebApplicationException(ContentApiUtils.createResponseForException(Status.BAD_REQUEST, null, false, "Invalid Content-Type Header "+mediaType.toString()+" found for object content")); } } //According to the RestEASY API, method getRootPart() //returns the root part of the message. //If a start parameter was set in the message header //the part with that id is returned. //If no start parameter was set the first part is returned. private InputPart getMessagePartWithObjectSource( MultipartRelatedInput multipartRelatedInput) { InputPart partWhichContainsObjectSource = multipartRelatedInput.getRootPart(); if (partWhichContainsObjectSource == null){ if (StringUtils.isBlank(multipartRelatedInput.getStart())){ throw new WebApplicationException(ContentApiUtils.createResponseForException(Status.BAD_REQUEST, null, false, "'start' parameter in Content-Type header is not provided. There is no way ot determine which part of the message contains the object's content")); } else{ throw new WebApplicationException(ContentApiUtils.createResponseForException(Status.BAD_REQUEST, null, false, "Could not locate message part with id "+ multipartRelatedInput.getStart() +". Object's content is not provided")); } } return partWhichContainsObjectSource; } private Response saveContentObjectByIdOrName( @PathParam("contentObjectIdOrName") String contentObjectIdOrName, String requestContent, String httpMethod, boolean updateLastModificationTime){ //Import from xml or json. ContentObject will not be saved ImportConfiguration importConfiguration = ImportConfiguration.object() .persist(PersistMode.DO_NOT_PERSIST) .updateLastModificationTime(updateLastModificationTime) .build(); ContentObject contentObjectToBeSaved = astroboaClient.getImportService().importContentObject(requestContent, importConfiguration); if (logger.isDebugEnabled()){ logger.debug("XML output of imported content object \n{}", contentObjectToBeSaved.xml(true)); } boolean entityIsNew = objectIsNew(contentObjectIdOrName, httpMethod, contentObjectToBeSaved); //Save content object return saveContentObject(contentObjectToBeSaved, httpMethod, requestContent, entityIsNew, updateLastModificationTime); } private boolean objectIsNew(String contentObjectIdOrName, String httpMethod, ContentObject contentObjectToBeSaved) { if (contentObjectIdOrName == null){ return true; } ContentObject existingObject = astroboaClient.getContentService().getContentObject(contentObjectIdOrName, ResourceRepresentationType.CONTENT_OBJECT_INSTANCE, FetchLevel.ENTITY, CacheRegion.NONE, null, false); boolean entityIsNew = existingObject == null; if (CmsConstants.UUIDPattern.matcher(contentObjectIdOrName).matches()){ //Save content object by Id if (contentObjectToBeSaved.getId() == null){ contentObjectToBeSaved.setId(contentObjectIdOrName); } else{ //Payload contains id. Check if they are the same if (! StringUtils.equals(contentObjectIdOrName, contentObjectToBeSaved.getId())){ logger.warn("Try to "+httpMethod + " content object with ID "+contentObjectIdOrName + " but payload contains id "+ contentObjectToBeSaved.getId()); throw new WebApplicationException(HttpURLConnection.HTTP_BAD_REQUEST); } } } else{ //Save content object by SystemName //Check that payload contains id if (contentObjectToBeSaved.getId() == null){ if (existingObject != null){ //A content object with system name 'contentObjectIdOrName' exists, but in payload no id was provided //Set this id to ContentObject representing the payload contentObjectToBeSaved.setId(existingObject.getId()); } } else{ //Payload contains an id. if (existingObject != null){ //if this is not the same with the id returned from repository raise an exception if (!StringUtils.equals(existingObject.getId(), contentObjectToBeSaved.getId())){ logger.warn("Try to "+httpMethod + " content object with system name "+contentObjectIdOrName + " which corresponds to an existed content object in repository with id " + existingObject.getId()+" but payload contains a different id "+ contentObjectToBeSaved.getId()); throw new WebApplicationException(HttpURLConnection.HTTP_BAD_REQUEST); } } } } return entityIsNew; } private Response saveContentObjectString(String contentSource, String httpMethod, boolean entityIsNew, boolean updateLastModificationTime) { try{ //Must determine whether a single or a collection of objects is saved if (contentSource == null){ throw new WebApplicationException(HttpURLConnection.HTTP_BAD_REQUEST); } if (contentSource.contains(CmsConstants.RESOURCE_RESPONSE_PREFIXED_NAME) || contentSource.contains(CmsConstants.RESOURCE_COLLECTION)){ List<ContentObject> contentObjects = astroboaClient.getContentService().saveContentObjectResourceCollection(contentSource, false, updateLastModificationTime, null); //TODO : Improve response details. ResponseBuilder responseBuilder = Response.status(Status.OK); responseBuilder.header("Content-Disposition", "inline"); responseBuilder.type(MediaType.TEXT_PLAIN + "; charset=utf-8"); return responseBuilder.build(); } else{ ContentObject contentObject = astroboaClient.getContentService().save(contentSource, false, updateLastModificationTime, null); return ContentApiUtils.createResponseForPutOrPostOfACmsEntity(contentObject,httpMethod, contentSource, entityIsNew); } } catch(CmsUnauthorizedAccessException e){ throw new WebApplicationException(HttpURLConnection.HTTP_UNAUTHORIZED); } catch(Exception e){ logger.error("",e); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } } private Response saveContentObject(ContentObject contentObject, String httpMethod, String requestContent, boolean entityIsNew, boolean updateLastModificationTime) { try{ contentObject = astroboaClient.getContentService().save(contentObject, false, updateLastModificationTime, null); return ContentApiUtils.createResponseForPutOrPostOfACmsEntity(contentObject,httpMethod, requestContent, entityIsNew); } catch(CmsUnauthorizedAccessException e){ throw new WebApplicationException(HttpURLConnection.HTTP_UNAUTHORIZED); } catch(Exception e){ logger.error("",e); throw new WebApplicationException(e, HttpURLConnection.HTTP_INTERNAL_ERROR); } } private String createFilename(WorkbookBuilder workbookBuilder) { String filename = workbookBuilder.getWorkbookName(); if (filename.length()>50){ filename = filename.substring(0, 49); } filename = filename + "-"+DateUtils.format(Calendar.getInstance(), "ddMMyyyyHHmm"); return filename; } }