/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community 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://opensource.org/licenses/ecl2.txt
*
* 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.opencastproject.search.endpoint;
import org.opencastproject.job.api.JaxbJob;
import org.opencastproject.job.api.Job;
import org.opencastproject.job.api.JobProducer;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageImpl;
import org.opencastproject.rest.AbstractJobProducerEndpoint;
import org.opencastproject.search.api.SearchException;
import org.opencastproject.search.api.SearchQuery;
import org.opencastproject.search.impl.SearchServiceImpl;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.serviceregistry.api.ServiceRegistry;
import org.opencastproject.util.doc.rest.RestParameter;
import org.opencastproject.util.doc.rest.RestQuery;
import org.opencastproject.util.doc.rest.RestResponse;
import org.opencastproject.util.doc.rest.RestService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
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.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
/**
* The REST endpoint
*/
@Path("/")
@RestService(name = "search", title = "Search Service", abstractText = "This service indexes and queries available (distributed) episodes.", notes = {
"All paths above are relative to the REST endpoint base (something like http://your.server/files)",
"If the service is down or not working it will return a status 503, this means the the underlying service is "
+ "not working and is either restarting or has failed",
"A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In "
+ "other words, there is a bug! You should file an error report with your server logs from the time when the "
+ "error occurred: <a href=\"https://opencast.jira.com\">Opencast Issue Tracker</a>" })
public class SearchRestService extends AbstractJobProducerEndpoint {
private static final Logger logger = LoggerFactory.getLogger(SearchRestService.class);
/** The constant used to switch the direction of the sorting querystring parameter. */
public static final String DESCENDING_SUFFIX = "_DESC";
/** The search service */
protected SearchServiceImpl searchService;
/** The service registry */
private ServiceRegistry serviceRegistry;
public String getSampleMediaPackage() {
return "<mediapackage xmlns=\"http://mediapackage.opencastproject.org\" start=\"2007-12-05T13:40:00\" duration=\"1004400000\"><title>t1</title>\n"
+ " <metadata>\n"
+ " <catalog id=\"catalog-1\" type=\"dublincore/episode\">\n"
+ " <mimetype>text/xml</mimetype>\n"
+ " <url>https://opencast.jira.com/svn/MH/trunk/modules/matterhorn-kernel/src/test/resources/dublincore.xml</url>\n"
+ " <checksum type=\"md5\">2b8a52878c536e64e20e309b5d7c1070</checksum>\n"
+ " </catalog>\n"
+ " <catalog id=\"catalog-3\" type=\"metadata/mpeg-7\" ref=\"track:track-1\">\n"
+ " <mimetype>text/xml</mimetype>\n"
+ " <url>https://opencast.jira.com/svn/MH/trunk/modules/matterhorn-kernel/src/test/resources/mpeg7.xml</url>\n"
+ " <checksum type=\"md5\">2b8a52878c536e64e20e309b5d7c1070</checksum>\n"
+ " </catalog>\n"
+ " </metadata>\n" + "</mediapackage>";
}
@POST
@Path("add")
@Produces(MediaType.APPLICATION_XML)
@RestQuery(name = "add", description = "Adds a mediapackage to the search index.", restParameters = { @RestParameter(description = "The media package to add to the search index.", isRequired = true, name = "mediapackage", type = RestParameter.Type.TEXT, defaultValue = "${this.sampleMediaPackage}") }, reponses = {
@RestResponse(description = "XML encoded receipt is returned", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "There has been an internal error and the mediapackage could not be added", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "The job receipt")
public Response add(@FormParam("mediapackage") MediaPackageImpl mediaPackage) throws SearchException {
try {
Job job = searchService.add(mediaPackage);
return Response.ok(new JaxbJob(job)).build();
} catch (Exception e) {
logger.info(e.getMessage());
return Response.serverError().build();
}
}
@DELETE
@Path("{id}")
@Produces(MediaType.APPLICATION_XML)
@RestQuery(name = "remove", description = "Removes a mediapackage from the search index.", pathParameters = { @RestParameter(description = "The media package ID to remove from the search index.", isRequired = true, name = "id", type = RestParameter.Type.STRING) }, reponses = {
@RestResponse(description = "The removing job.", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "There has been an internal error and the mediapackage could not be deleted", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "The job receipt")
public Response remove(@PathParam("id") String mediaPackageId) throws SearchException {
try {
Job job = searchService.delete(mediaPackageId);
return Response.ok(new JaxbJob(job)).build();
} catch (Exception e) {
logger.info(e.getMessage());
return Response.serverError().build();
}
}
@GET
@Path("series.{format:xml|json}")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@RestQuery(name = "series", description = "Search for series matching the query parameters.", pathParameters = { @RestParameter(description = "The output format (json or xml) of the response body.", isRequired = true, name = "format", type = RestParameter.Type.STRING) }, restParameters = {
@RestParameter(description = "The series ID. If the additional boolean parameter \"episodes\" is \"true\", "
+ "the result set will include this series episodes.", isRequired = false, name = "id", type = RestParameter.Type.STRING),
@RestParameter(description = "Any series that matches this free-text query. If the additional boolean parameter \"episodes\" is \"true\", "
+ "the result set will include this series episodes.", isRequired = false, name = "q", type = RestParameter.Type.STRING),
@RestParameter(defaultValue = "false", description = "Whether to include this series episodes. This can be used in combination with \"id\" or \"q\".", isRequired = false, name = "episodes", type = RestParameter.Type.STRING),
@RestParameter(name = "sort", isRequired = false, description = "The sort order. May include any "
+ "of the following: DATE_CREATED, DATE_PUBLISHED, TITLE, SERIES_ID, MEDIA_PACKAGE_ID, CREATOR, "
+ "CONTRIBUTOR, LANGUAGE, LICENSE, SUBJECT, DESCRIPTION, PUBLISHER. Add '_DESC' to reverse the sort order (e.g. TITLE_DESC).", type = RestParameter.Type.STRING),
@RestParameter(defaultValue = "20", description = "The maximum number of items to return per page.", isRequired = false, name = "limit", type = RestParameter.Type.STRING),
@RestParameter(defaultValue = "0", description = "The page number.", isRequired = false, name = "offset", type = RestParameter.Type.STRING),
@RestParameter(defaultValue = "false", description = "Whether this is an administrative query", isRequired = false, name = "admin", type = RestParameter.Type.BOOLEAN) }, reponses = { @RestResponse(description = "The request was processed succesfully.", responseCode = HttpServletResponse.SC_OK) }, returnDescription = "The search results, expressed as xml or json.")
public Response getEpisodeAndSeriesById(
@QueryParam("id") String id,
@QueryParam("q") String text,
@QueryParam("episodes") boolean includeEpisodes,
@QueryParam("sort") String sort,
@QueryParam("limit") int limit,
@QueryParam("offset") int offset,
@QueryParam("admin") boolean admin,
@PathParam("format") String format
) throws SearchException, UnauthorizedException {
SearchQuery query = new SearchQuery();
// If id is specified, do a search based on id
if (StringUtils.isNotBlank(id))
query.withId(id);
// Include series data in the results?
query.includeSeries(true);
// Include episodes in the result?
query.includeEpisodes(includeEpisodes);
// Include free-text search?
if (StringUtils.isNotBlank(text))
query.withText(text);
query.withSort(SearchQuery.Sort.DATE_CREATED, false);
if (StringUtils.isNotBlank(sort)) {
// Parse the sort field and direction
SearchQuery.Sort sortField = null;
if (sort.endsWith(DESCENDING_SUFFIX)) {
String enumKey = sort.substring(0, sort.length() - DESCENDING_SUFFIX.length()).toUpperCase();
try {
sortField = SearchQuery.Sort.valueOf(enumKey);
query.withSort(sortField, false);
} catch (IllegalArgumentException e) {
logger.warn("No sort enum matches '{}'", enumKey);
}
} else {
try {
sortField = SearchQuery.Sort.valueOf(sort);
query.withSort(sortField);
} catch (IllegalArgumentException e) {
logger.warn("No sort enum matches '{}'", sort);
}
}
}
query.withLimit(limit);
query.withOffset(offset);
// Build the response
ResponseBuilder rb = Response.ok();
if (admin) {
rb.entity(searchService.getForAdministrativeRead(query));
} else {
rb.entity(searchService.getByQuery(query));
}
if ("json".equals(format)) {
rb.type(MediaType.APPLICATION_JSON);
} else {
rb.type(MediaType.TEXT_XML);
}
return rb.build();
}
// CHECKSTYLE:OFF
@GET
@Path("episode.{format:xml|json}")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@RestQuery(name = "episodes", description = "Search for episodes matching the query parameters.", pathParameters = { @RestParameter(description = "The output format (json or xml) of the response body.", isRequired = true, name = "format", type = RestParameter.Type.STRING) }, restParameters = {
@RestParameter(description = "The ID of the single episode to be returned, if it exists.", isRequired = false, name = "id", type = RestParameter.Type.STRING),
@RestParameter(description = "Any episode that matches this free-text query.", isRequired = false, name = "q", type = RestParameter.Type.STRING),
@RestParameter(description = "Any episode that belongs to specified series id.", isRequired = false, name = "sid", type = RestParameter.Type.STRING),
// @RestParameter(defaultValue = "false", description =
// "Whether to include this series episodes. This can be used in combination with \"id\" or \"q\".",
// isRequired = false, name = "episodes", type = RestParameter.Type.STRING),
@RestParameter(name = "sort", isRequired = false, description = "The sort order. May include any "
+ "of the following: DATE_CREATED, DATE_PUBLISHED, TITLE, SERIES_ID, MEDIA_PACKAGE_ID, CREATOR, "
+ "CONTRIBUTOR, LANGUAGE, LICENSE, SUBJECT, DESCRIPTION, PUBLISHER. Add '_DESC' to reverse the sort order (e.g. TITLE_DESC).", type = RestParameter.Type.STRING),
@RestParameter(defaultValue = "20", description = "The maximum number of items to return per page.", isRequired = false, name = "limit", type = RestParameter.Type.STRING),
@RestParameter(defaultValue = "0", description = "The page number.", isRequired = false, name = "offset", type = RestParameter.Type.STRING),
@RestParameter(defaultValue = "false", description = "Whether this is an administrative query", isRequired = false, name = "admin", type = RestParameter.Type.BOOLEAN) }, reponses = { @RestResponse(description = "The request was processed succesfully.", responseCode = HttpServletResponse.SC_OK) }, returnDescription = "The search results, expressed as xml or json.")
public Response getEpisode(@QueryParam("id") String id, @QueryParam("q") String text,
@QueryParam("sid") String seriesId, @QueryParam("sort") String sort, @QueryParam("tag") String[] tags, @QueryParam("flavor") String[] flavors,
@QueryParam("limit") int limit, @QueryParam("offset") int offset, @QueryParam("admin") boolean admin,
@PathParam("format") String format) throws SearchException, UnauthorizedException {
// CHECKSTYLE:ON
// Prepare the flavors
List<MediaPackageElementFlavor> flavorSet = new ArrayList<MediaPackageElementFlavor>();
if (flavors != null) {
for (String f : flavors) {
try {
flavorSet.add(MediaPackageElementFlavor.parseFlavor(f));
} catch (IllegalArgumentException e) {
logger.debug("invalid flavor '{}' specified in query", f);
}
}
}
SearchQuery search = new SearchQuery();
search.withId(id).withSeriesId(seriesId)
.withElementFlavors(flavorSet.toArray(new MediaPackageElementFlavor[flavorSet.size()]))
.withElementTags(tags).withLimit(limit).withOffset(offset);
if (StringUtils.isNotBlank(text)) {
search.withText(text);
}
search.withSort(SearchQuery.Sort.DATE_CREATED, false);
if (StringUtils.isNotBlank(sort)) {
// Parse the sort field and direction
SearchQuery.Sort sortField = null;
if (sort.endsWith(DESCENDING_SUFFIX)) {
String enumKey = sort.substring(0, sort.length() - DESCENDING_SUFFIX.length()).toUpperCase();
try {
sortField = SearchQuery.Sort.valueOf(enumKey);
search.withSort(sortField, false);
} catch (IllegalArgumentException e) {
logger.warn("No sort enum matches '{}'", enumKey);
}
} else {
try {
sortField = SearchQuery.Sort.valueOf(sort);
search.withSort(sortField);
} catch (IllegalArgumentException e) {
logger.warn("No sort enum matches '{}'", sort);
}
}
}
// Build the response
ResponseBuilder rb = Response.ok();
if (admin) {
rb.entity(searchService.getForAdministrativeRead(search));
} else {
rb.entity(searchService.getByQuery(search));
}
if ("json".equals(format)) {
rb.type(MediaType.APPLICATION_JSON);
} else {
rb.type(MediaType.TEXT_XML);
}
return rb.build();
}
@GET
@Path("lucene.{format:xml|json}")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@RestQuery(name = "lucene", description = "Search a lucene query.", pathParameters = { @RestParameter(description = "The output format (json or xml) of the response body.", isRequired = true, name = "format", type = RestParameter.Type.STRING) }, restParameters = {
@RestParameter(defaultValue = "", description = "The lucene query.", isRequired = false, name = "q", type = RestParameter.Type.STRING),
@RestParameter(name = "sort", isRequired = false, description = "The sort order. May include any "
+ "of the following: DATE_CREATED, DATE_PUBLISHED, TITLE, SERIES_ID, MEDIA_PACKAGE_ID, CREATOR, "
+ "CONTRIBUTOR, LANGUAGE, LICENSE, SUBJECT, DESCRIPTION, PUBLISHER. Add '_DESC' to reverse the sort order (e.g. TITLE_DESC).", type = RestParameter.Type.STRING),
@RestParameter(defaultValue = "20", description = "The maximum number of items to return per page.", isRequired = false, name = "limit", type = RestParameter.Type.STRING),
@RestParameter(defaultValue = "0", description = "The page number.", isRequired = false, name = "offset", type = RestParameter.Type.STRING),
@RestParameter(defaultValue = "false", description = "Whether this is an administrative query", isRequired = false, name = "admin", type = RestParameter.Type.BOOLEAN) }, reponses = { @RestResponse(description = "The request was processed succesfully.", responseCode = HttpServletResponse.SC_OK) }, returnDescription = "The search results, expressed as xml or json")
public Response getByLuceneQuery(@QueryParam("q") String q, @QueryParam("sort") String sort, @QueryParam("limit") int limit,
@QueryParam("offset") int offset, @QueryParam("admin") boolean admin, @PathParam("format") String format)
throws SearchException, UnauthorizedException {
SearchQuery query = new SearchQuery();
if (!StringUtils.isBlank(q))
query.withQuery(q);
query.withSort(SearchQuery.Sort.DATE_CREATED, false);
if (StringUtils.isNotBlank(sort)) {
// Parse the sort field and direction
SearchQuery.Sort sortField = null;
if (sort.endsWith(DESCENDING_SUFFIX)) {
String enumKey = sort.substring(0, sort.length() - DESCENDING_SUFFIX.length()).toUpperCase();
try {
sortField = SearchQuery.Sort.valueOf(enumKey);
query.withSort(sortField, false);
} catch (IllegalArgumentException e) {
logger.warn("No sort enum matches '{}'", enumKey);
}
} else {
try {
sortField = SearchQuery.Sort.valueOf(sort);
query.withSort(sortField);
} catch (IllegalArgumentException e) {
logger.warn("No sort enum matches '{}'", sort);
}
}
}
query.withLimit(limit);
query.withOffset(offset);
// Build the response
ResponseBuilder rb = Response.ok();
if (admin) {
rb.entity(searchService.getForAdministrativeRead(query));
} else {
rb.entity(searchService.getByQuery(query));
}
if ("json".equals(format)) {
rb.type(MediaType.APPLICATION_JSON);
} else {
rb.type(MediaType.TEXT_XML);
}
return rb.build();
}
/**
* @see org.opencastproject.rest.AbstractJobProducerEndpoint#getService()
*/
@Override
public JobProducer getService() {
return searchService;
}
/**
* Callback from OSGi to set the search service implementation.
*
* @param searchService
* the service implementation
*/
public void setSearchService(SearchServiceImpl searchService) {
this.searchService = searchService;
}
/**
* Callback from OSGi to set the service registry implementation.
*
* @param serviceRegistry
* the service registry
*/
public void setServiceRegistry(ServiceRegistry serviceRegistry) {
this.serviceRegistry = serviceRegistry;
}
/**
* @see org.opencastproject.rest.AbstractJobProducerEndpoint#getServiceRegistry()
*/
@Override
public ServiceRegistry getServiceRegistry() {
return serviceRegistry;
}
}