/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.stanbol.entityhub.jersey.resource; import static javax.ws.rs.core.MediaType.TEXT_HTML; import static org.apache.clerezza.rdf.core.serializedform.SupportedFormat.N3; import static org.apache.clerezza.rdf.core.serializedform.SupportedFormat.N_TRIPLE; import static org.apache.clerezza.rdf.core.serializedform.SupportedFormat.RDF_JSON; import static org.apache.clerezza.rdf.core.serializedform.SupportedFormat.RDF_XML; import static org.apache.clerezza.rdf.core.serializedform.SupportedFormat.TURTLE; import static org.apache.clerezza.rdf.core.serializedform.SupportedFormat.X_TURTLE; import static org.apache.stanbol.commons.web.base.utils.MediaTypeUtil.getAcceptableMediaType; import static org.apache.stanbol.entityhub.jersey.utils.LDPathHelper.getLDPathParseExceptionMessage; import static org.apache.stanbol.entityhub.jersey.utils.LDPathHelper.handleLDPathRequest; import static org.apache.stanbol.entityhub.jersey.utils.LDPathHelper.prepareQueryLDPathProgram; import static org.apache.stanbol.entityhub.jersey.utils.LDPathHelper.transformQueryResults; import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.TreeSet; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.OPTIONS; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriInfo; import org.apache.clerezza.rdf.ontologies.RDFS; import org.apache.marmotta.ldpath.exception.LDPathParseException; import org.apache.marmotta.ldpath.model.programs.Program; import org.apache.stanbol.commons.indexedgraph.IndexedGraph; import org.apache.stanbol.commons.namespaceprefix.NamespaceMappingUtils; import org.apache.stanbol.commons.namespaceprefix.NamespacePrefixService; import org.apache.stanbol.commons.web.base.resource.BaseStanbolResource; import org.apache.stanbol.commons.web.viewable.Viewable; import org.apache.stanbol.entityhub.core.query.QueryResultListImpl; import org.apache.stanbol.entityhub.jersey.utils.JerseyUtils; import org.apache.stanbol.entityhub.ldpath.EntityhubLDPath; import org.apache.stanbol.entityhub.ldpath.backend.SiteManagerBackend; import org.apache.stanbol.entityhub.ldpath.query.LDPathSelect; import org.apache.stanbol.entityhub.model.clerezza.RdfValueFactory; import org.apache.stanbol.entityhub.servicesapi.model.Entity; import org.apache.stanbol.entityhub.servicesapi.model.Representation; import org.apache.stanbol.entityhub.servicesapi.model.ValueFactory; import org.apache.stanbol.entityhub.servicesapi.query.FieldQuery; import org.apache.stanbol.entityhub.servicesapi.query.QueryResultList; import org.apache.stanbol.entityhub.servicesapi.site.SiteManager; import org.apache.stanbol.entityhub.servicesapi.util.AdaptingIterator; import org.codehaus.jettison.json.JSONArray; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; /** * RDFTerm to provide a REST API for the {@link SiteManager}. * * TODO: add description */ @Component @Service(Object.class) @Property(name="javax.ws.rs", boolValue=true) @Path("/entityhub/sites") public class SiteManagerRootResource extends BaseStanbolResource { private final Logger log = LoggerFactory.getLogger(getClass()); @Reference private NamespacePrefixService nsPrefixService; @Reference private SiteManager referencedSiteManager; public static final Set<String> RDF_MEDIA_TYPES = new TreeSet<String>(Arrays.asList(N3, N_TRIPLE, RDF_XML, TURTLE, X_TURTLE, RDF_JSON)); /** * The Field used for find requests if not specified TODO: Will be depreciated as soon as EntityQuery is * implemented */ private static final String DEFAULT_FIND_FIELD = RDFS.label.getUnicodeString(); /** * The default number of maximal results of searched sites. */ private static final int DEFAULT_FIND_RESULT_LIMIT = 5; @OPTIONS public Response handleCorsPreflight(@Context HttpHeaders headers){ ResponseBuilder res = Response.ok(); //enableCORS(servletContext, res, headers); return res.build(); } @OPTIONS @Path("/find") public Response handleCorsPreflightFind(@Context HttpHeaders headers){ ResponseBuilder res = Response.ok(); //enableCORS(servletContext, res, headers); return res.build(); } @OPTIONS @Path("/query") public Response handleCorsPreflightQuery(@Context HttpHeaders headers){ ResponseBuilder res = Response.ok(); //enableCORS(servletContext, res, headers); return res.build(); } @GET @Produces(MediaType.TEXT_HTML) public Response getSitesPage(@Context HttpHeaders headers) { ResponseBuilder rb = Response.ok(new Viewable("index", this)); rb.header(HttpHeaders.CONTENT_TYPE, TEXT_HTML+"; charset=utf-8"); //addCORSOrigin(servletContext, rb, headers); return rb.build(); } // removed to allow request with Accept headers other than text/html to return // the JSON array // @GET // @Path("/referenced") // @Produces(MediaType.TEXT_HTML) // public Response getReferencedSitesPage() { // return Response.ok(new Viewable("referenced", this)) // .header(HttpHeaders.CONTENT_TYPE, TEXT_HTML+"; charset=utf-8").build(); // } /** * Getter for the id's of all referenced sites * * @return the id's of all referenced sites. */ @GET @Path(value = "/referenced") @Produces({MediaType.APPLICATION_JSON,MediaType.TEXT_HTML}) public Response getReferencedSites(@Context UriInfo uriInfo, @Context HttpHeaders headers) { MediaType acceptable = getAcceptableMediaType(headers, Arrays.asList(MediaType.APPLICATION_JSON,MediaType.TEXT_HTML) , MediaType.APPLICATION_JSON_TYPE); if(MediaType.TEXT_HTML_TYPE.isCompatible(acceptable)){ ResponseBuilder rb = Response.ok(new Viewable("referenced", this)); rb.header(HttpHeaders.CONTENT_TYPE, TEXT_HTML+"; charset=utf-8"); //addCORSOrigin(servletContext, rb, headers); return rb.build(); } else { JSONArray referencedSites = new JSONArray(); for (String site : referencedSiteManager.getSiteIds()) { referencedSites.put(String.format("%sentityhub/site/%s/", uriInfo.getBaseUri(), site)); } ResponseBuilder rb = Response.ok(referencedSites.toString()); rb.header(HttpHeaders.CONTENT_TYPE, acceptable+"; charset=utf-8"); //addCORSOrigin(servletContext, rb, headers); return rb.build(); } } @OPTIONS @Path("/entity") public Response handleCorsPreflightEntity(@Context HttpHeaders headers){ ResponseBuilder res = Response.ok(); //enableCORS(servletContext, res, headers,OPTIONS,GET); return res.build(); } /** * Cool URI handler for Signs. * * @param id * The id of the entity (required) * @param headers * the request headers used to get the requested {@link MediaType} * @return a redirection to either a browser view, the RDF meta data or the raw binary content */ @GET @Path("/entity") public Response getEntityById(@QueryParam(value = "id") String id, @Context HttpHeaders headers) { log.debug("getSignById() request\n\t> id : {}\n\t> accept : {}\n\t> mediaType: {}", new Object[] {id, headers.getAcceptableMediaTypes(), headers.getMediaType()}); Collection<String> supported = new HashSet<String>(JerseyUtils.ENTITY_SUPPORTED_MEDIA_TYPES); supported.add(TEXT_HTML); final MediaType acceptedMediaType = getAcceptableMediaType( headers, supported, MediaType.APPLICATION_JSON_TYPE); if (id == null || id.isEmpty()) { if(MediaType.TEXT_HTML_TYPE.isCompatible(acceptedMediaType)){ ResponseBuilder rb = Response.ok(new Viewable("entity", this)); rb.header(HttpHeaders.CONTENT_TYPE, TEXT_HTML+"; charset=utf-8"); //addCORSOrigin(servletContext, rb, headers); return rb.build(); } else { return Response.status(Status.BAD_REQUEST) .entity("No or empty ID was parsed. Missing parameter id.\n") .header(HttpHeaders.ACCEPT, acceptedMediaType).build(); } } Entity sign = referencedSiteManager.getEntity(id); if (sign != null) { ResponseBuilder rb = Response.ok(sign); rb.header(HttpHeaders.CONTENT_TYPE, acceptedMediaType+"; charset=utf-8"); //addCORSOrigin(servletContext, rb, headers); return rb.build(); } else { // TODO: How to parse an ErrorMessage? // create an Response with the the Error? log.info("getSignById() entity {} not found on any referenced site"); return Response.status(Status.NOT_FOUND) .entity("Entity with ID '"+id+"' not found an any referenced site\n") .header(HttpHeaders.ACCEPT, acceptedMediaType).build(); } } // @GET // @Path("/find") // @Produces(MediaType.TEXT_HTML) // public Response getFindPage() { // return Response.ok(new Viewable("find", this)) // .header(HttpHeaders.CONTENT_TYPE, TEXT_HTML+"; charset=utf-8").build(); // } @GET @Path("/find") public Response findEntityfromGet(@QueryParam(value = "name") String name, @QueryParam(value = "field") String field, @QueryParam(value = "lang") String language, // @QueryParam(value="select") String select, @QueryParam(value = "limit") @DefaultValue(value = "-1") int limit, @QueryParam(value = "offset") @DefaultValue(value = "0") int offset, @QueryParam(value = "ldpath") String ldpath, @Context HttpHeaders headers) { return findEntity(name, field, language, limit, offset, ldpath, headers); } @POST @Path("/find") public Response findEntity(@FormParam(value = "name") String name, @FormParam(value = "field") String parsedField, @FormParam(value = "lang") String language, // @FormParam(value="select") String select, @FormParam(value = "limit") Integer limit, @FormParam(value = "offset") Integer offset, @FormParam(value = "ldpath") String ldpath, @Context HttpHeaders headers) { log.debug("findEntity() Request"); Collection<String> supported = new HashSet<String>(JerseyUtils.QUERY_RESULT_SUPPORTED_MEDIA_TYPES); supported.add(TEXT_HTML); final MediaType acceptedMediaType = getAcceptableMediaType( headers, supported, MediaType.APPLICATION_JSON_TYPE); if(name == null || name.isEmpty()){ if(MediaType.TEXT_HTML_TYPE.isCompatible(acceptedMediaType)){ ResponseBuilder rb = Response.ok(new Viewable("find", this)); rb.header(HttpHeaders.CONTENT_TYPE, TEXT_HTML+"; charset=utf-8"); //addCORSOrigin(servletContext, rb, headers); return rb.build(); } else { return Response.status(Status.BAD_REQUEST) .entity("The name must not be null nor empty for find requests. Missing parameter name.\n") .header(HttpHeaders.ACCEPT, acceptedMediaType).build(); } } final String property; if (parsedField == null) { property = DEFAULT_FIND_FIELD; } else { parsedField = parsedField.trim(); if (parsedField.isEmpty()) { property = DEFAULT_FIND_FIELD; } else { property = nsPrefixService.getFullName(parsedField); if(property == null){ String messsage = String.format("The prefix '%s' of the parsed field '%' is not " + "mapped to any namespace. Please parse the full URI instead!\n", NamespaceMappingUtils.getPrefix(parsedField),parsedField); return Response.status(Status.BAD_REQUEST) .entity(messsage) .header(HttpHeaders.ACCEPT, acceptedMediaType).build(); } } } FieldQuery query = JerseyUtils.createFieldQueryForFindRequest(name, property, language, limit == null || limit < 1 ? DEFAULT_FIND_RESULT_LIMIT : limit, offset,ldpath); return executeQuery(referencedSiteManager, query, acceptedMediaType, headers); } @GET @Path("/query") public Response getQueryDocumentation(@Context HttpHeaders headers){ ResponseBuilder rb = Response.ok(new Viewable("query", this)); rb.header(HttpHeaders.CONTENT_TYPE, TEXT_HTML+"; charset=utf-8"); //addCORSOrigin(servletContext, rb, headers); return rb.build(); } /** * Allows to parse any kind of {@link FieldQuery} in its JSON Representation. * <p> * TODO: as soon as the entityhub supports multiple query types this need to be refactored. The idea is * that this dynamically detects query types and than redirects them to the referenced site * implementation. * * @param query * The field query in JSON format * @param headers * the header information of the request * @return the results of the query */ @POST @Path("/query") @Consumes( {MediaType.APPLICATION_JSON}) public Response queryEntities(FieldQuery query, @Context HttpHeaders headers) { Collection<String> supported = new HashSet<String>(JerseyUtils.QUERY_RESULT_SUPPORTED_MEDIA_TYPES); supported.add(TEXT_HTML); final MediaType acceptedMediaType = getAcceptableMediaType( headers, supported, MediaType.APPLICATION_JSON_TYPE); if(query == null){ //if query is null nd the mediaType is HTML we need to print the //Documentation of the RESTful API if(MediaType.TEXT_HTML_TYPE.isCompatible(acceptedMediaType)){ return getQueryDocumentation(headers); } else { return Response.status(Status.BAD_REQUEST) .entity("The query must not be null nor empty for query requests. Missing parameter query.\n") .header(HttpHeaders.ACCEPT, acceptedMediaType).build(); } } else { return executeQuery(referencedSiteManager, query, acceptedMediaType, headers); } } /* * LDPath support */ @OPTIONS @Path("/ldpath") public Response handleCorsPreflightLDPath(@Context HttpHeaders headers){ ResponseBuilder res = Response.ok(); //enableCORS(servletContext, res, headers,OPTIONS,GET,POST); return res.build(); } @GET @Path("/ldpath") public Response handleLDPathGet( @QueryParam(value = "context")Set<String> contexts, @QueryParam(value = "ldpath")String ldpath, @Context HttpHeaders headers){ return handleLDPathPost(contexts, ldpath, headers); } @POST @Path("/ldpath") public Response handleLDPathPost( @FormParam(value = "context")Set<String> contexts, @FormParam(value = "ldpath")String ldpath, @Context HttpHeaders headers){ return handleLDPathRequest(this,new SiteManagerBackend(referencedSiteManager), ldpath, contexts, headers); } /** * Executes the query parsed by {@link #queryEntities(String, File, HttpHeaders)} or created based * {@link #findEntity(String, String, String, int, int, HttpHeaders)} * * @param manager The {@link SiteManager} * @param query * The query to execute * @param headers the request headers * @return the response (results of error) */ private Response executeQuery(SiteManager manager, FieldQuery query, MediaType mediaType, HttpHeaders headers) throws WebApplicationException { if(query instanceof LDPathSelect && ((LDPathSelect)query).getLDPathSelect() != null){ //use the LDPath variant to process this query return executeLDPathQuery(manager, query, ((LDPathSelect)query).getLDPathSelect(), mediaType, headers); } else { //use the default query execution QueryResultList<Representation> result = manager.find(query); ResponseBuilder rb = Response.ok(result); rb.header(HttpHeaders.CONTENT_TYPE, mediaType+"; charset=utf-8"); //addCORSOrigin(servletContext, rb, headers); return rb.build(); } } /** * Execute a Query that uses LDPath to process results. * @param query the query * @param mediaType the mediaType for the response * @param headers the http headers of the request * @return the response */ private Response executeLDPathQuery(SiteManager manager,FieldQuery query, String ldpathProgramString, MediaType mediaType, HttpHeaders headers) { QueryResultList<Representation> result; ValueFactory vf = new RdfValueFactory(new IndexedGraph()); SiteManagerBackend backend = new SiteManagerBackend(manager); EntityhubLDPath ldPath = new EntityhubLDPath(backend,vf); //copy the selected fields, because we might need to delete some during //the preparation phase Set<String> selectedFields = new HashSet<String>(query.getSelectedFields()); //first prepare (only execute the query if the parameters are valid) Program<Object> program; try { program = prepareQueryLDPathProgram(ldpathProgramString, selectedFields, backend, ldPath); } catch (LDPathParseException e) { log.warn("Unable to parse LDPath program used as select for a Query to the '/sites' endpoint:"); log.warn("FieldQuery: \n {}",query); log.warn("LDPath: \n {}",((LDPathSelect)query).getLDPathSelect()); log.warn("Exception:",e); return Response.status(Status.BAD_REQUEST) .entity(("Unable to parse LDPath program (Messages: "+ getLDPathParseExceptionMessage(e)+")!\n")) .header(HttpHeaders.ACCEPT, mediaType).build(); } catch (IllegalStateException e) { log.warn("parsed LDPath program is not compatible with the Query " + "parsed to the '/sites' endpoint!",e); return Response.status(Status.BAD_REQUEST) .entity(e.getMessage()) .header(HttpHeaders.ACCEPT, mediaType).build(); } //2. execute the query // we need to adapt from Entity to Representation //TODO: should we add the metadata to the result? Iterator<Representation> resultIt = new AdaptingIterator<Entity,Representation>(manager.findEntities(query).iterator(), new AdaptingIterator.Adapter<Entity,Representation>() { @Override public Representation adapt(Entity value, Class<Representation> type) { return value.getRepresentation(); }},Representation.class); //process the results Collection<Representation> transformedResults = transformQueryResults(resultIt, program, selectedFields, ldPath, backend, vf); result = new QueryResultListImpl<Representation>(query, transformedResults, Representation.class); ResponseBuilder rb = Response.ok(result); rb.header(HttpHeaders.CONTENT_TYPE, mediaType+"; charset=utf-8"); //addCORSOrigin(servletContext, rb, headers); return rb.build(); } }