/* * 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.net.HttpURLConnection; 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.Response; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.betaconceptframework.astroboa.api.model.Topic; 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.Order; import org.betaconceptframework.astroboa.api.model.query.criteria.TopicCriteria; import org.betaconceptframework.astroboa.api.security.exception.CmsUnauthorizedAccessException; import org.betaconceptframework.astroboa.client.AstroboaClient; 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.util.CmsConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Gregory Chomatas (gchomatas@betaconcept.com) * @author Savvas Triantafyllou (striantafyllou@betaconcept.com) * */ /* @Name("org.betaconceptframework.astroboa.resourceapi.resource.TopicResource") */ public class TopicResource extends AstroboaResource{ private final Logger logger = LoggerFactory.getLogger(getClass()); public TopicResource(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 // **** THE FOLLOWING THREE METHODS SUPPORT IMEDIATE RETRIEVAL OF TAXONOMY_INSTANCE TOPICS // WITHOUT THE NEED TO PROVIDE THE TAXONOMY_INSTANCE. THIS IS POSSIBLE SINCE TOPIC_INSTANCE IDs OR // SYSTEM NAMES ARE UNIQUE ACROSS TAXONOMIES // // **** TOPICS CAN BE ALSO RETRIEVED THROUGH THE TaxonomyResource **** // **** READ THE RELEVANT COMMENTS IN TaxonomyResource CLASS FOR A MORE DETAILED EXPLANATION **** // // In short use the following rule to understand the difference of topic retrieval through the // TaxonomyResource or the TopicResource: // When you use the TopicResource you should imagine that you go inside a bucket that keeps // all topics flat i.e. without any hierarchy. If you know the topic or system name of the topic, the // bucket will give you back the topic. // When you use the TaxonomyResource think that you are in browsing mode. The TaxonomyResource in not a bucket but // a tree. You traverse the tree of taxonomies to find the required taxonomy and then find the root topics inside the taxonomy // and then find the child topics inside each root topic and so on. @GET @Produces("*/*") @Path("/{topicIdOrName: " + CmsConstants.UUID_OR_SYSTEM_NAME_REG_EXP_FOR_RESTEASY + "}") public Response getTopic( @PathParam("topicIdOrName") String topicIdOrName, @QueryParam("output") String output, @QueryParam("callback") String callback, @QueryParam("prettyPrint") String prettyPrint, @QueryParam("depth") String depth){ /*if (output == null) { return getTopicInternal(topicIdOrName, Output.XML, callback); }*/ Output outputEnum = ContentApiUtils.getOutputType(output, Output.XML); FetchLevel fetchLevel = ContentApiUtils.getFetchLevel(depth, FetchLevel.ENTITY_AND_CHILDREN); return getTopicInternal(topicIdOrName, outputEnum, callback, prettyPrint,fetchLevel); } @GET @Produces({MediaType.APPLICATION_JSON}) @Path("/{topicIdOrName: " + CmsConstants.UUID_OR_SYSTEM_NAME_REG_EXP_FOR_RESTEASY + "}") public Response getRootTopicsInTaxonomyAsJson( @PathParam("topicIdOrName") String topicIdOrName, @QueryParam("output") String output, @QueryParam("callback") String callback, @QueryParam("prettyPrint") String prettyPrint, @QueryParam("depth") String depth) { // 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()); }*/ Output outputEnum = ContentApiUtils.getOutputType(output, Output.JSON); FetchLevel fetchLevel = ContentApiUtils.getFetchLevel(depth, FetchLevel.ENTITY_AND_CHILDREN); return getTopicInternal(topicIdOrName, outputEnum, callback, prettyPrint,fetchLevel); } @GET @Produces(MediaType.APPLICATION_XML) @Path("/{topicIdOrName: " + CmsConstants.UUID_OR_SYSTEM_NAME_REG_EXP_FOR_RESTEASY + "}") public Response getRootTopicsInTaxonomyAsXml( @PathParam("topicIdOrName") String topicIdOrName, @QueryParam("output") String output, @QueryParam("callback") String callback, @QueryParam("prettyPrint") String prettyPrint, @QueryParam("depth") String depth) { // 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()); }*/ Output outputEnum = ContentApiUtils.getOutputType(output, Output.XML); FetchLevel fetchLevel = ContentApiUtils.getFetchLevel(depth, FetchLevel.ENTITY_AND_CHILDREN); return getTopicInternal(topicIdOrName, outputEnum, callback, prettyPrint,fetchLevel); } @PUT @Path("/{topicIdOrName: " + CmsConstants.UUID_OR_SYSTEM_NAME_REG_EXP_FOR_RESTEASY + "}") public Response putTopicByIdOrName( @PathParam("topicIdOrName") String topicIdOrName, String requestContent){ if (StringUtils.isBlank(topicIdOrName)){ logger.warn("Use HTTP PUT to save topic {} but no id or name was provided ", requestContent); throw new WebApplicationException(HttpURLConnection.HTTP_BAD_REQUEST); } return saveTopicByIdOrName(topicIdOrName, requestContent, HttpMethod.PUT); } @POST public Response postTopicByIdOrName(String requestContent){ return saveTopicSource(requestContent, HttpMethod.POST, true); } private Response saveTopicByIdOrName(String topicNameOrId, String requestContent, String httpMethod){ //Import from xml or json. Topic will not be saved ImportConfiguration configuration = ImportConfiguration.topic() .persist(PersistMode.DO_NOT_PERSIST) .build(); Topic topicToBeSaved = astroboaClient.getImportService().importTopic(requestContent, configuration); Topic existingTopic = astroboaClient.getTopicService().getTopic(topicNameOrId, ResourceRepresentationType.TOPIC_INSTANCE, FetchLevel.ENTITY, false); boolean entityIsNew = existingTopic == null; if (CmsConstants.UUIDPattern.matcher(topicNameOrId).matches()){ //Save topic by Id if (topicToBeSaved.getId() == null){ topicToBeSaved.setId(topicNameOrId); } else{ //Payload contains id. Check if they are the same if (! StringUtils.equals(topicNameOrId, topicToBeSaved.getId())){ logger.warn("Try to "+httpMethod + " topic with ID "+topicNameOrId + " but payload contains id "+ topicToBeSaved.getId()); throw new WebApplicationException(HttpURLConnection.HTTP_BAD_REQUEST); } } } else{ //Save content object by SystemName //Check that payload contains id if (topicToBeSaved.getId() == null){ if (existingTopic != null){ //A topic with name 'topicIdOrName' exists, but in payload no id was provided //Set this id to Topic representing the payload topicToBeSaved.setId(existingTopic.getId()); } } else{ //Payload contains an id. if (existingTopic != null){ //if this is not the same with the id returned from repository raise an exception if (!StringUtils.equals(existingTopic.getId(), topicToBeSaved.getId())){ logger.warn("Try to "+httpMethod + " topic with name "+topicNameOrId + " which corresponds to an existed topic in repository with id " + existingTopic.getId()+" but payload contains a different id "+ topicToBeSaved.getId()); throw new WebApplicationException(HttpURLConnection.HTTP_BAD_REQUEST); } } } } //Save content object return saveTopic(topicToBeSaved, httpMethod, requestContent, entityIsNew); } @DELETE @Path("/{topicIdOrName: " + CmsConstants.UUID_OR_SYSTEM_NAME_REG_EXP_FOR_RESTEASY +"}") public Response deleteTopicResource(@PathParam("topicIdOrName") String topicIdOrName) { if (StringUtils.isEmpty(topicIdOrName)){ throw new WebApplicationException(Response.Status.BAD_REQUEST); } try { boolean topicDeleted = astroboaClient.getTopicService().deleteTopicTree(topicIdOrName); return ContentApiUtils.createResponseForHTTPDelete(topicDeleted, topicIdOrName); }catch(CmsUnauthorizedAccessException e){ throw new WebApplicationException(HttpURLConnection.HTTP_UNAUTHORIZED); } catch(Exception e){ logger.error("",e); throw new WebApplicationException(HttpURLConnection.HTTP_BAD_REQUEST); } } private Response saveTopicSource(String topicSource, String httpMethod, boolean entityIsNew) { logger.debug("Want to save a new topic {}",topicSource); try{ ImportConfiguration configuration = ImportConfiguration.topic() .persist(PersistMode.PERSIST_ENTITY_TREE) .build(); Topic topic = astroboaClient.getImportService().importTopic(topicSource, configuration); return ContentApiUtils.createResponseForPutOrPostOfACmsEntity(topic,httpMethod, topicSource, 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 saveTopic(Topic topic, String httpMethod, String requestContent, boolean entityIsNew) { try{ topic = astroboaClient.getTopicService().save(topic); return ContentApiUtils.createResponseForPutOrPostOfACmsEntity(topic,httpMethod, requestContent, entityIsNew); } catch(CmsUnauthorizedAccessException e){ throw new WebApplicationException(HttpURLConnection.HTTP_UNAUTHORIZED); } catch(Exception e){ logger.error("",e); throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } } @GET @Produces(MediaType.APPLICATION_XML) public Response getTopicsAsXML( @QueryParam("cmsQuery") String cmsQuery, @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit, @QueryParam("orderBy") String orderBy, @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()); } return retrieveTopics( cmsQuery, offset, limit, orderBy, outputEnum, callback, prettyPrint); } @GET @Produces(MediaType.APPLICATION_JSON) public Response getTopicsAsJson( @QueryParam("cmsQuery") String cmsQuery, @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit, @QueryParam("orderBy") String orderBy, @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()); } return retrieveTopics( cmsQuery, offset, limit, orderBy, outputEnum, callback, prettyPrint); } @GET @Produces("*/*") public Response getTopics( @QueryParam("cmsQuery") String cmsQuery, @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit, @QueryParam("orderBy") String orderBy, @QueryParam("output") String output, @QueryParam("callback") String callback, @QueryParam("prettyPrint") String prettyPrint){ if (output == null) { return retrieveTopics(cmsQuery, offset, limit, orderBy, Output.XML, callback, prettyPrint); } Output outputEnum = Output.valueOf(output.toUpperCase()); return retrieveTopics( cmsQuery, offset, limit, orderBy, outputEnum, callback, prettyPrint); } private Response retrieveTopics( String cmsQuery, Integer offset,Integer limit, String orderBy, Output output, String callback, String prettyPrint) { if (output == null) { output = Output.XML; } boolean prettyPrintEnabled = ContentApiUtils.isPrettyPrintEnabled(prettyPrint); try { //Build Topic criteria TopicCriteria topicCriteria = buildCriteria(cmsQuery, offset, limit, orderBy, prettyPrintEnabled); String queryResult = null; StringBuilder resourceRepresentation = new StringBuilder(); switch (output) { case XML: { queryResult = astroboaClient.getTopicService().searchTopics(topicCriteria, ResourceRepresentationType.XML); if (StringUtils.isBlank(callback)) { resourceRepresentation.append(queryResult); } else { ContentApiUtils.generateXMLP(resourceRepresentation, queryResult, callback); } break; } case JSON: queryResult = astroboaClient.getTopicService().searchTopics(topicCriteria, ResourceRepresentationType.JSON); if (StringUtils.isBlank(callback)) { resourceRepresentation.append(queryResult); } else { ContentApiUtils.generateJSONP(resourceRepresentation, queryResult, callback); } break; /* 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){ logger.error("Cms query "+cmsQuery,e); throw new WebApplicationException(HttpURLConnection.HTTP_BAD_REQUEST); } } private TopicCriteria buildCriteria(String cmsQuery, Integer offset, Integer limit, String orderBy, boolean prettyPrint) { //Build Topic criteria TopicCriteria topicCriteria = CmsCriteriaFactory.newTopicCriteria(); if (offset == null || offset < 0){ topicCriteria.setOffset(0); } else{ topicCriteria.setOffset(offset); } if (limit == null || limit < 0){ topicCriteria.setLimit(50); } else{ topicCriteria.setLimit(limit); } topicCriteria.getRenderProperties().renderAllContentObjectProperties(true); topicCriteria.getRenderProperties().prettyPrint(prettyPrint); //Parse query if (StringUtils.isNotBlank(cmsQuery)) { CriterionFactory.parse(cmsQuery, topicCriteria); } else{ logger.warn("No query parameter was found. All topics will be returned according to limit and offset"); } //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)) { topicCriteria.addOrderProperty(property, Order.descending); } else { //Any other value (even invalid) set default order which is ascending topicCriteria.addOrderProperty(property, Order.ascending); } } } } } } return topicCriteria; } private Response getTopicInternal(String topicIdOrName, Output output, String callback, String prettyPrint, FetchLevel fetchLevel){ //OLD Method //return generateTopicResponseUsingTopicInstance(topicIdOrName, output, callback); boolean prettyPrintEnabled = ContentApiUtils.isPrettyPrintEnabled(prettyPrint); try { StringBuilder topicAsXMLOrJSONBuilder = new StringBuilder(); switch (output) { case XML:{ String topicXML = astroboaClient.getTopicService().getTopic(topicIdOrName, ResourceRepresentationType.XML, fetchLevel, prettyPrintEnabled); if (StringUtils.isBlank(topicXML)){ throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } if (StringUtils.isBlank(callback)) { topicAsXMLOrJSONBuilder.append(topicXML); } else { ContentApiUtils.generateXMLP(topicAsXMLOrJSONBuilder, topicXML, callback); } break; } case JSON:{ String topicJSON = astroboaClient.getTopicService().getTopic(topicIdOrName, ResourceRepresentationType.JSON, fetchLevel, prettyPrintEnabled); if (StringUtils.isBlank(topicJSON)){ throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } if (StringUtils.isBlank(callback)) { topicAsXMLOrJSONBuilder.append(topicJSON); } else { ContentApiUtils.generateJSONP(topicAsXMLOrJSONBuilder, topicJSON, callback); } break; } } return ContentApiUtils.createResponse(topicAsXMLOrJSONBuilder, output, callback, null); } catch(WebApplicationException wae){ throw wae; } catch(Exception e){ logger.error("Topic IdOrName: " + topicIdOrName, e); throw new WebApplicationException(HttpURLConnection.HTTP_BAD_REQUEST); } } /* * This is the old version of the code (2.1.2.GA) * It should be deleted once new code is stable */ private Response generateTopicResponseUsingTopicInstance( String topicIdOrName, Output output, String callback, boolean prettyPrint) { try { Topic topic = findTopicByTopicIdOrName(topicIdOrName); if (topic == null) { throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND); } topic.getChildren(); StringBuilder topicAsXMLOrJSONBuilder = new StringBuilder(); switch (output) { case XML: { if (StringUtils.isBlank(callback)) { topicAsXMLOrJSONBuilder.append(topic.xml(prettyPrint)); } else { ContentApiUtils.generateXMLP(topicAsXMLOrJSONBuilder, topic.xml(prettyPrint), callback); } break; } case JSON: if (StringUtils.isBlank(callback)) { topicAsXMLOrJSONBuilder.append(topic.json(prettyPrint)); } else { ContentApiUtils.generateJSONP(topicAsXMLOrJSONBuilder, topic.json(prettyPrint), callback); } break; } return ContentApiUtils.createResponse(topicAsXMLOrJSONBuilder, output, callback, null); } catch(Exception e){ logger.error("Topic IdOrName: " + topicIdOrName, e); throw new WebApplicationException(HttpURLConnection.HTTP_BAD_REQUEST); } } private Topic findTopicByTopicIdOrName( String topicIdOrName){ if (StringUtils.isBlank(topicIdOrName)) { logger.warn("The provided topic name is blank. NULL will be returned"); return null; } Topic topic = astroboaClient.getTopicService().getTopic(topicIdOrName, ResourceRepresentationType.TOPIC_INSTANCE, FetchLevel.ENTITY_AND_CHILDREN, false); if (topic == null) { logger.info("The provided topicIdOrName: " + topicIdOrName + " does not exist."); return null; } return topic; /*TopicCriteria topicCriteria = CmsCriteriaFactory.newTopicCriteria(); if (CmsConstants.UUIDPattern.matcher(topicIdOrName).matches()) { topicCriteria.addIdEqualsCriterion(topicIdOrName); } else { topicCriteria.addNameEqualsCriterion(topicIdOrName); } CmsOutcome<Topic> topicsFound = astroboaClient.getTopicService().searchTopics(topicCriteria); if (CollectionUtils.isNotEmpty(topicsFound.getResults())) { Topic firstTopic = topicsFound.getResults().get(0); // if more than one topics correspond to the same name then we choose the first one but we generate a warning if (topicsFound.getResults().size() > 1) logger.warn("More than one topics correspond to topicIdOrName: " + topicIdOrName + " The first from list will be returned. Topic id and names should be unique across all taxonomies. Please inform your support team about this bug !!"); return firstTopic; } else { logger.info("The provided topicIdOrName: " + topicIdOrName + " does not exist."); return null; }*/ } }