blob: 45acb14cc22d4e2294770882c53fec887882e91c [file] [log] [blame]
/*
* 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.enhancer.web.topic.resource;
import static javax.ws.rs.core.MediaType.TEXT_HTML;
import java.util.List;
import javax.servlet.ServletContext;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.OPTIONS;
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.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.UriInfo;
import org.apache.clerezza.commons.rdf.ImmutableGraph;
import org.apache.clerezza.commons.rdf.IRI;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.apache.stanbol.commons.web.viewable.Viewable;
import org.apache.stanbol.commons.web.base.resource.BaseStanbolResource;
import org.apache.stanbol.enhancer.servicesapi.EnhancementEngine;
import org.apache.stanbol.enhancer.servicesapi.rdf.OntologicalClasses;
import org.apache.stanbol.enhancer.servicesapi.rdf.Properties;
import org.apache.stanbol.enhancer.topic.api.ClassifierException;
import org.apache.stanbol.enhancer.topic.api.TopicClassifier;
import org.apache.stanbol.enhancer.topic.api.training.TrainingSet;
import org.apache.stanbol.enhancer.topic.api.training.TrainingSetException;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
/**
* RESTful interface for classification models: register concept hierarchies,
* introspect model state and trigger training if a training set is provided.
*
*/
@Component
@Service(Object.class)
@Property(name="javax.ws.rs", boolValue=true)
@Path("/topic/model")
public final class TopicModelResource extends BaseStanbolResource {
private BundleContext bundleContext;
@Activate
protected void activate(ComponentContext context) {
bundleContext = context.getBundleContext();
}
@Path("{classifier}")
public ClassifierResource getClassifier(@PathParam(value = "classifier") String classifierName,
@Context UriInfo uriInfo) throws InvalidSyntaxException {
this.uriInfo = uriInfo;
ServiceReference[] references = bundleContext.getServiceReferences(TopicClassifier.class.getName(),
String.format("(%s=%s)", EnhancementEngine.PROPERTY_NAME, classifierName));
if (references == null || references.length == 0) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
return new ClassifierResource((TopicClassifier) bundleContext.getService(references[0]));
}
public class ClassifierResource extends ResultData {
final TopicClassifier classifier;
public ClassifierResource(TopicClassifier classifier) {
this.classifier = classifier;
}
public TopicClassifier getClassifier() {
return classifier;
}
@GET
@Produces(TEXT_HTML)
public Response get(@Context HttpHeaders headers) {
ResponseBuilder rb = Response.ok(new Viewable("index", this));
rb.header(HttpHeaders.CONTENT_TYPE, TEXT_HTML + "; charset=utf-8");
return rb.build();
}
// TODO: make it possible to fetch concept descriptions (with broader and narrower links) using the GET
// verb
@POST
@Path("concept")
@Consumes(MediaType.WILDCARD)
public Response addConcept(@QueryParam(value = "id") String concept,
@QueryParam(value = "primary_topic") String primaryTopicUri,
@QueryParam(value = "broader") List<String> broaderConcepts,
@Context HttpHeaders headers) throws ClassifierException {
classifier.addConcept(concept, primaryTopicUri, broaderConcepts);
ResponseBuilder rb = Response.ok();
return rb.build();
}
@DELETE
@Path("concept")
@Consumes(MediaType.WILDCARD)
public Response remoteConcept(@QueryParam(value = "id") String concept, @Context HttpHeaders headers) throws ClassifierException {
if (concept != null && !concept.isEmpty()) {
classifier.removeConcept(concept);
} else {
classifier.removeAllConcepts();
}
// TODO: count the number of deleted entries and return is a text entity
ResponseBuilder rb = Response.ok();
return rb.build();
}
@OPTIONS
@Path("performance")
public Response handleCorsPreflightOnPerformance(@Context HttpHeaders headers) {
ResponseBuilder res = Response.ok();
return res.build();
}
// TODO: make it possible to fetch performance reports and evaluation running state using the GET verb
@POST
@Path("performance")
@Consumes(MediaType.WILDCARD)
public Response updatePerformance(@QueryParam(value = "incremental") Boolean incremental,
@Context HttpHeaders headers) throws TrainingSetException,
ClassifierException {
if (incremental == null) {
incremental = Boolean.TRUE;
}
int updated = classifier.updatePerformanceEstimates(incremental);
ResponseBuilder rb = Response.ok(String.format(
"Successfully updated the performance estimates of %d concept(s).\n", updated));
return rb.build();
}
// TODO: make it possible to fetch training set statistics and training state using the GET verb
@POST
@Path("trainer")
@Consumes(MediaType.WILDCARD)
public Response updateModel(@QueryParam(value = "incremental") Boolean incremental,
@Context HttpHeaders headers) throws TrainingSetException,
ClassifierException {
if (incremental == null) {
incremental = Boolean.TRUE;
}
int updated = classifier.updateModel(incremental);
ResponseBuilder rb = Response.ok(String.format(
"Successfully updated the statistical model(s) of %d concept(s).\n", updated));
return rb.build();
}
// TODO: make it possible browse the training set content on the GET verb using a subresource
@POST
@Path("trainingset")
@Consumes(MediaType.TEXT_PLAIN)
public Response registerExample(@QueryParam(value = "example_id") String exampleId,
@QueryParam(value = "concept") List<String> concepts,
String textContent,
@Context HttpHeaders headers) throws TrainingSetException,
ClassifierException {
ResponseBuilder rb;
if (!classifier.isUpdatable()) {
rb = Response.status(Response.Status.BAD_REQUEST).entity(
String.format("Classifier %s is not updateble.\n", classifier.getName()));
} else {
TrainingSet trainingSet = classifier.getTrainingSet();
exampleId = trainingSet.registerExample(exampleId, textContent, concepts);
// TODO: make example GETable resources and return a 201 to it instead of a simple message.
rb = Response.ok(String.format(
"Successfully added or updated example '%s' in training set '%s'.\n", exampleId,
trainingSet.getName()));
}
return rb.build();
}
// TODO make the following a DELETE method on the example sub-resources them-selves once we have a GET for
// them
@DELETE
@Path("trainingset")
@Consumes(MediaType.WILDCARD)
public Response removeExample(@QueryParam(value = "example_id") List<String> exampleIds,
@Context HttpHeaders headers) throws TrainingSetException,
ClassifierException {
ResponseBuilder rb;
if (!classifier.isUpdatable()) {
rb = Response.status(Response.Status.BAD_REQUEST).entity(
String.format("Classifier %s is not updateble.\n", classifier.getName()));
} else {
TrainingSet trainingSet = classifier.getTrainingSet();
if (exampleIds != null && !exampleIds.isEmpty()) {
for (String exampleId : exampleIds) {
trainingSet.registerExample(exampleId, null, null);
}
} else {
// implement a way to cleanup a complete training set? or is it too dangerous and we should
// return an error instead?
}
rb = Response.ok(String.format("Successfully deleted examples in training set '%s'.\n",
trainingSet.getName()));
}
return rb.build();
}
/**
* Simple RDF / SKOS importer that loads the complete model in memory
* for easy parsing and then does graph introspection to find the
* concepts to load into the model.
*
* If a scalable implementation is required, one should probably use a
* transient triple store and pass it the raw RDF stream instead of
* using the naive GraphReader JAX-RS provider.
*/
@POST
@Consumes(MediaType.WILDCARD)
public Response importConceptsFromRDF(@QueryParam(value = "concept_class") String conceptClassUri,
@QueryParam(value = "broader_property") String broaderPropertyUri,
ImmutableGraph graph,
@Context HttpHeaders headers) throws ClassifierException {
IRI conceptClass = OntologicalClasses.SKOS_CONCEPT;
IRI broaderProperty = Properties.SKOS_BROADER;
if (conceptClassUri != null && !conceptClassUri.isEmpty()) {
conceptClass = new IRI(conceptClassUri);
}
if (broaderPropertyUri != null && !broaderPropertyUri.isEmpty()) {
broaderProperty = new IRI(broaderPropertyUri);
}
int imported = classifier.importConceptsFromGraph(graph, conceptClass, broaderProperty);
ResponseBuilder rb;
if (imported == 0) {
rb = Response.status(Response.Status.BAD_REQUEST).entity(
String.format("Could not find any instances of '%s' in payload.\n",
conceptClass.getUnicodeString()));
} else {
rb = Response.ok(String.format("Imported %d instance of '%s'.\n", imported,
conceptClass.getUnicodeString()));
}
return rb.build();
}
}
}