/*
* 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.reasoners.web.resources;
import static javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED;
import static javax.ws.rs.core.MediaType.MULTIPART_FORM_DATA;
import static javax.ws.rs.core.MediaType.TEXT_HTML;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
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.Context;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.UriInfo;
import org.apache.clerezza.jaxrs.utils.form.FormFile;
import org.apache.clerezza.jaxrs.utils.form.MultiPartBody;
import org.apache.clerezza.rdf.core.access.TcManager;
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;
import org.apache.stanbol.commons.jobs.api.JobManager;
import org.apache.stanbol.commons.web.viewable.Viewable;
import org.apache.stanbol.commons.web.base.format.KRFormat;
import org.apache.stanbol.commons.web.base.resource.BaseStanbolResource;
import org.apache.stanbol.ontologymanager.servicesapi.scope.ScopeManager;
import org.apache.stanbol.ontologymanager.servicesapi.session.SessionManager;
import org.apache.stanbol.reasoners.jena.JenaReasoningService;
import org.apache.stanbol.reasoners.owlapi.OWLApiReasoningService;
import org.apache.stanbol.reasoners.servicesapi.ReasoningService;
import org.apache.stanbol.reasoners.servicesapi.ReasoningServiceInputManager;
import org.apache.stanbol.reasoners.servicesapi.ReasoningServicesManager;
import org.apache.stanbol.reasoners.servicesapi.UnboundReasoningServiceException;
import org.apache.stanbol.reasoners.servicesapi.annotations.Documentation;
import org.apache.stanbol.reasoners.web.input.impl.SimpleInputManager;
import org.apache.stanbol.reasoners.web.input.provider.impl.ByteArrayInputProvider;
import org.apache.stanbol.reasoners.web.input.provider.impl.OntologyManagerInputProvider;
import org.apache.stanbol.reasoners.web.input.provider.impl.RecipeInputProvider;
import org.apache.stanbol.reasoners.web.input.provider.impl.UrlInputProvider;
import org.apache.stanbol.reasoners.web.utils.ReasoningServiceExecutor;
import org.apache.stanbol.reasoners.web.utils.ReasoningServiceResult;
import org.apache.stanbol.reasoners.web.utils.ResponseTaskBuilder;
import org.apache.stanbol.rules.base.api.RuleAdapterManager;
import org.apache.stanbol.rules.base.api.RuleStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Endpoint for reasoning services. Services can be invoked using the service name and task in the request
* path. The related active SCR service is selected, then the concrete execution is delegated to a {@see
* ReasoningServiceExecutor}.
*
* Two different kind of implementation of {@see ReasoningService} are supported: {@see JenaReasoningService}
* and {@see OWLApiReasonngService}.
*
* This class includes methods to prepare the input and dispatch the output (back to the client in the
* requested format or saved in the triple store).
*
* Support for long term operations is provided by adding /job to the request URI.
*
*/
@Component
@Service(Object.class)
@Property(name="javax.ws.rs", boolValue=true)
@Path("/reasoners/{service}/{task: [^/]+}{job: (/job)?}")
public class ReasoningServiceTaskResource extends BaseStanbolResource {
private Logger log = LoggerFactory.getLogger(getClass());
// private ServletContext context;
private ReasoningService<?,?,?> service;
private Map<String,List<String>> parameters;
private FormFile file = null;
@Reference
protected TcManager tcManager;
@Reference
protected ScopeManager onm;
@Reference
protected SessionManager sessionManager;
@Reference
protected RuleStore ruleStore;
@Reference
protected RuleAdapterManager adapterManager;
private boolean job = false;
private String jobLocation = "";
@Reference
protected ReasoningServicesManager reasoningServicesManager;
@Reference
protected JobManager jobManager;
@Context
private UriInfo uriInfo;
@Context
private Form form = null;
@Context
private HttpHeaders headers;
private String taskID;
/**
* Constructor
*
*/
public ReasoningServiceTaskResource() {
super();
}
public void prepare(String serviceID, String taskIDstr, String jobFlag) {
this.taskID = taskIDstr;
log.debug("Called service {} to perform task {}", serviceID, taskID);
// Parameters for customized reasoning services
this.parameters = prepareParameters();
// Check if method is allowed
// FIXME Supported methods are only GET and POST, but also PUT comes here, why?
// String[] supported = {"GET", "POST"};
// if (!Arrays.asList(supported).contains(this.httpContext.getRequest().getMethod())) {
// throw new WebApplicationException(405);
// }
// Retrieve the service
try {
service = getService(serviceID);
} catch (UnboundReasoningServiceException e) {
log.error("Service not found: {}", serviceID);
throw new WebApplicationException(e, Response.Status.NOT_FOUND);
}
log.debug("Service retrieved");
// Check if the task is allowed
if (this.service.supportsTask(taskID) || taskID.equals(ReasoningServiceExecutor.TASK_CHECK)) {
// Ok
} else {
log.error("Unsupported task (not found): {}", taskID);
throw new WebApplicationException(new Exception("Unsupported task (not found): " + taskID),
Response.Status.NOT_FOUND);
}
// Check for the job parameter
if (!jobFlag.equals("")) {
log.debug("Job param is {}", job);
if (jobFlag.equals("/job")) {
log.debug("Ask for background job");
this.job = true;
} else {
log.error("Malformed request");
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
}
// Now we check if the service implementation is supported
if (getCurrentService() instanceof JenaReasoningService) {} else if (getCurrentService() instanceof OWLApiReasoningService) {} else {
log.error("This implementation of ReasoningService is not supported: {}", getCurrentService()
.getClass());
throw new WebApplicationException(new Exception(
"This implementation of ReasoningService is not supported: "
+ getCurrentService().getClass()), Response.Status.INTERNAL_SERVER_ERROR);
}
log.debug("Implementation is supported");
}
/**
*
* @return
*/
private Map<String,List<String>> prepareParameters() {
Map<String,List<String>> parameters = new HashMap<String,List<String>>();
log.debug("Preparing parameters...");
// Parameters for a GET request and POST
Map<?,?> queryParameters = uriInfo.getQueryParameters();
log.debug("... {} query parameters found", queryParameters.size());
for (Entry<?,?> e : queryParameters.entrySet()) {
String k = (String) e.getKey();
log.debug(" param: {} ", k);
List<String> v = new ArrayList<String>();
/*
* XXX
* It looks like that param values here are not urldecoded
* This is odd because the not on the method says exactly the opposite.
* @see getQueryParameters()
*/
for(String s : (List<String>) e.getValue()){
try {
s = URLDecoder.decode(s, "UTF-8");
} catch (UnsupportedEncodingException e1) {
e1.printStackTrace();
}
log.debug(" value {}", v);
v.add(s);
}
parameters.put(k, v);
}
// Parameters for a POST request with content-type
// application/x-www-form-urlencoded
if(form!=null){
MultivaluedMap<String,String> formParameters = form.asMap();
log.debug("... {} form urlencoded parameters found", formParameters.size());
for (Entry<String,List<String>> e : formParameters.entrySet()) {
parameters.put(e.getKey(), e.getValue());
}
}
log.debug("Parameters prepared");
return parameters;
}
/**
* This is an alias of the get method.
*
* @param url
* @param targetGraphID
* @return
*/
@POST
@Consumes({APPLICATION_FORM_URLENCODED})
@Produces({TEXT_HTML, "text/plain", KRFormat.RDF_XML, KRFormat.TURTLE, "text/turtle", "text/n3"})
public Response post(@PathParam(value = "service") String serviceID,
@PathParam(value = "task") String taskID,
@PathParam(value = "job") String jobFlg) {
prepare(serviceID, taskID, jobFlg);
return processRequest();
}
private Response processRequest() {
if (job) {
log.trace("Processing in background");
return processBackgroundRequest();
} else {
log.trace("Processing in foreground");
return processRealTimeRequest();
}
}
/**
* Get the inferences from input URL. If url param is null, get the HTML description of this service/task
*
* @param url
* @return
*/
@GET
@Produces({TEXT_HTML, "text/plain", KRFormat.RDF_XML, KRFormat.TURTLE, "text/turtle", "text/n3"})
public Response get(@QueryParam("target") String targetGraphID,
@PathParam(value = "service") String serviceID,
@PathParam(value = "task") String taskID,
@PathParam(value = "job") String jobFlg) {
log.debug("Called GET serviceID {} taskID {}",serviceID, taskID);
prepare(serviceID, taskID, jobFlg);
log.debug("Called GET with parameters: {} ", parameters.keySet()
.toArray(new String[parameters.keySet().size()]));
return processRequest();
}
/**
* Process a background request. This service use the Stanbol Commons Jobs API to start a background job.
* Returns 201 on success, with HTTP header Location pointing to the Job resource.
*
* @return
*/
private Response processBackgroundRequest() {
// If parameters is empty it's a bad request...
if (this.parameters.isEmpty()) {
log.error("Cannot start job without input parameters... sending BAD REQUEST");
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
String target = getTarget();
// Setup the input
ReasoningServiceInputManager imngr = prepareInput();
// The service executor
ReasoningServiceExecutor executor = new ReasoningServiceExecutor(tcManager, imngr,
getCurrentService(), getCurrentTask(), target, parameters);
String jid = getJobManager().execute(executor);
URI location = URI.create(getPublicBaseUri() + "jobs/" + jid);
this.jobLocation = location.toString();
/**
* If everything went well, we return 201 Created We include the header Location: with the Job URL
*/
Viewable view = new Viewable("created", this);
return Response.created(location).entity(view).build();
}
/**
* Process a real-time operation. Returns 200 when the process is ready, 500 if some error occurs
*
* @return
*/
private Response processRealTimeRequest() {
// If all parameters are missing we produce the service/task welcome
// page
if (this.parameters.isEmpty() && file == null) {
log.debug("no parameters no input file, show default index page");
// return Response.ok(new Viewable("index", this)).build();
ResponseBuilder rb = Response.ok(new Viewable("index", this));
rb.header(HttpHeaders.CONTENT_TYPE, TEXT_HTML + "; charset=utf-8");
return rb.build();
}
try {
String target = getTarget();
// Setup the input
ReasoningServiceInputManager imngr = prepareInput();
// The service executor
ReasoningServiceExecutor executor = new ReasoningServiceExecutor(tcManager, imngr,
getCurrentService(), getCurrentTask(), target, parameters);
ReasoningServiceResult<?> result = executor.call();
return new ResponseTaskBuilder(new ReasoningTaskResult(uriInfo, headers)).build(result);
} catch (Exception e) {
if (e instanceof RuntimeException) {
throw (RuntimeException)e;
}
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
}
}
/**
* Get the target graph, or null if no target graph have been given
*
* @return
*/
private String getTarget() {
String target = null;
if (parameters.get("target") != null) {
if (!parameters.get("target").isEmpty()) {
target = parameters.get("target").iterator().next();
if (target.equals("")) {
// Parameter exists with empty string value
log.error("Parameter 'target' must have a value!");
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
} else {
// Parameter exists with empty value
log.error("Parameter 'target' must have a value!");
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
}
return target;
}
/**
* To catch additional parameters in case of a POST with content-type multipart/form-data, we need to
* access the {@link FormDataMultiPart} representation of the input.
*
* @param data
* @return
*/
@POST
@Consumes({MULTIPART_FORM_DATA})
@Produces({TEXT_HTML, "text/plain", KRFormat.RDF_XML, KRFormat.TURTLE, "text/turtle", "text/n3"})
public Response post(MultiPartBody data,
@PathParam(value = "service") String serviceID,
@PathParam(value = "task") String taskID,
@PathParam(value = "job") String jobFlg) {
prepare(serviceID, taskID, jobFlg);
log.debug("Called POST post(MultiPartBody data, ...)");
// In this case we setup the parameter from a multipart request
if(data.getFormFileParameterValues("file").length > 0){
file = data.getFormFileParameterValues("file")[0];
}else{
log.debug("No files in multipart body");
}
for(String p : data.getTextParameterNames()){
this.parameters.put(p, Arrays.asList(data.getTextParameterValues(p)));
}
log.debug(" parameters: {}", parameters);
log.debug(" file: {}", file);
// // Then add the file
// if (file != null) {
// List<String> values = new ArrayList<String>();
// try {
// if (file.canRead() && file.exists()) {
// values.add(file.toURI().toURL().toString());
// } else {
// log.error("Bad request");
// log.error(" file is: {}", file);
// throw new WebApplicationException(Response.Status.BAD_REQUEST);
// }
// } catch (MalformedURLException e) {
// // This should never happen
// throw new WebApplicationException();
// }
// this.parameters.put("file", values);
// }
return processRequest();
}
/**
* Binds the request parameters to a list of {@see ReasoningServiceInputProvider}s, and fed a {@see
* SimpleInputManager}. TODO In the future we may want to decouple this process from this
* resource/submodule.
*
* @return
*/
private ReasoningServiceInputManager prepareInput() {
ReasoningServiceInputManager inmgr = new SimpleInputManager();
String scope = null;
String session = null;
if(file != null){
inmgr.addInputProvider(new ByteArrayInputProvider(file.getContent()));
}
for (Entry<String,List<String>> entry : this.parameters.entrySet()) {
if (entry.getKey().equals("url")) {
if (!entry.getValue().isEmpty()) {
// We keep only the first value
// XXX (make sense support multiple values?)
inmgr.addInputProvider(new UrlInputProvider(entry.getValue().iterator().next()));
// We remove it form the additional parameter list
this.parameters.remove("url");
} else {
// Parameter exists with no value
log.error("Parameter 'url' must have a value!");
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
}
// else if (entry.getKey().equals("file")) {
// if (!entry.getValue().isEmpty()) {
// // We keep only the first value
// // FIXME We create the file once again...
// String fv = entry.getValue().iterator().next();
// log.debug("File value is: {}", fv);
// inmgr.addInputProvider(new FileInputProvider(new File(URI.create(fv))));
// // We remove it form the additional parameter list
// this.parameters.remove("url");
// } else {
// // Parameter exists with no value
// log.error("Parameter 'url' must have a value!");
// throw new WebApplicationException(Response.Status.BAD_REQUEST);
// }
// }
else if (entry.getKey().equals("scope")) {
if (!entry.getValue().isEmpty()) {
scope = entry.getValue().iterator().next();
} else {
// Parameter exists with no value
log.error("Parameter 'scope' must have a value!");
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
} else if (entry.getKey().equals("session")) {
if (!entry.getValue().isEmpty()) {
session = entry.getValue().iterator().next();
} else {
// Parameter exists with no value
log.error("Parameter 'session' must have a value!");
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
} else if (entry.getKey().equals("recipe")) {
if (!entry.getValue().isEmpty()) {
inmgr.addInputProvider(new RecipeInputProvider(ruleStore, adapterManager, entry
.getValue().iterator().next()));
// We remove it form the additional parameter list
this.parameters.remove("url");
} else {
// Parameter exists with no value
log.error("Parameter 'recipe' must have a value!");
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
}
}
if (scope != null) {
inmgr.addInputProvider(new OntologyManagerInputProvider(onm, sessionManager, scope, session));
this.parameters.remove("scope");
this.parameters.remove("session");
}
return inmgr;
}
/**
* The actual path, to be used in the template.
*
* @return
*/
public String getCurrentPath() {
return uriInfo.getPath().replaceAll("[\\/]*$", "");
}
/**
* The selected service
*
* @return
*/
public ReasoningService<?,?,?> getCurrentService() {
return this.service;
}
/**
* The selected task
*
* @return
*/
public String getCurrentTask() {
return this.taskID;
}
/**
* If this resource created a job, this field contains the location to be rendered in the viewable.
*
* @return
*/
public String getJobLocation() {
return this.jobLocation;
}
/**
* The list of supported tasks. We include CHECK, which is managed directly by the endpoint.
*/
public List<String> getSupportedTasks() {
List<String> supported = new ArrayList<String>();
supported.add(ReasoningServiceExecutor.TASK_CHECK);
supported.addAll(getCurrentService().getSupportedTasks());
return supported;
}
/**
* To retrieve the service using the service manager
*
* @param servicePath
* @return
* @throws UnboundReasoningServiceException
*/
private ReasoningService<?,?,?> getService(String servicePath) throws UnboundReasoningServiceException {
return getServicesManager().get(servicePath);
}
/**
* Get the service manager from the context
*
* @return
*/
private ReasoningServicesManager getServicesManager() {
log.debug("(getServicesManager()) ");
return reasoningServicesManager;
}
/**
* Gets the list of active services
*
* @return
*/
public Set<ReasoningService<?,?,?>> getActiveServices() {
log.debug("(getActiveServices()) There are {} reasoning services", getServicesManager().size());
return getServicesManager().asUnmodifiableSet();
}
/**
* Gets the job manager
*
* @return
*/
private JobManager getJobManager() {
log.debug("(getJobManager()) ");
return jobManager;
}
public Map<String,String> getServiceDescription() {
return getServiceDescription(service);
}
public Map<String,String> getServiceDescription(ReasoningService<?,?,?> service) {
Class<?> serviceC = service.getClass();
String name;
try {
name = serviceC.getAnnotation(Documentation.class).name();
} catch (NullPointerException e) {
log.warn("The service {} is not documented: missing name", serviceC);
name = "";
}
String description;
try {
description = serviceC.getAnnotation(Documentation.class).description();
} catch (NullPointerException e) {
log.warn("The service {} is not documented: missing description", serviceC);
description = "";
}
// String file = serviceC.getAnnotation(Documentation.class).file();
Map<String,String> serviceProperties = new HashMap<String,String>();
serviceProperties.put("name", name);
serviceProperties.put("description", description);
// serviceProperties.put("file", file);
serviceProperties.put("path", service.getPath());
return serviceProperties;
}
public class ReasoningTaskResult extends ResultData implements ReasoningResult {
private Object result;
private UriInfo uriInfo;
private HttpHeaders headers;
public ReasoningTaskResult(UriInfo uriInfo, HttpHeaders headers) {
this.headers = headers;
this.uriInfo = uriInfo;
}
public void setResult(Object result){
this.result = result;
}
public Object getResult() {
return this.result;
}
public HttpHeaders getHeaders() {
return headers;
}
public UriInfo getUriInfo(){
return uriInfo;
}
}
}