/* This program 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.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package org.opentripplanner.api.ws;
import java.io.InputStream;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
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 javax.ws.rs.core.Response.Status;
import javax.xml.bind.annotation.XmlRootElement;
import org.codehaus.jettison.json.JSONException;
import org.opentripplanner.api.model.RouterInfo;
import org.opentripplanner.api.model.RouterList;
import org.opentripplanner.api.ws.impl.StoredHullService;
import org.opentripplanner.api.ws.services.HullService;
import org.opentripplanner.common.geometry.GraphUtils;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graph.Graph.LoadLevel;
import org.opentripplanner.routing.impl.GraphServiceImpl;
import org.opentripplanner.routing.services.GraphService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.annotation.Secured;
import com.sun.jersey.api.NotFoundException;
import com.sun.jersey.api.Responses;
import com.sun.jersey.api.core.InjectParam;
import com.sun.jersey.api.spring.Autowire;
import com.vividsolutions.jts.geom.Geometry;
/**
* This REST API endpoint allows remotely loading, reloading, and evicting graphs on a running server.
*
* A GraphService maintains a mapping between routerIds and specific Graph objects.
* The HTTP verbs are used as follows to manipulate that mapping:
*
* GET - see the registered routerIds and Graphs, verify whether a particular routerId is registered
* PUT - create or replace a mapping from a routerId to a Graph loaded from the server filesystem
* POST - create or replace a mapping from a routerId to a serialized Graph sent in the request
* DELETE - de-register a routerId, releasing the reference to the associated graph
*
* The HTTP request URLs are of the form /ws/routers/{routerId}, where the routerId is optional.
* If a routerId is supplied in the URL, the verb will act upon the mapping for that specific
* routerId. If no routerId is given, the verb will act upon all routerIds currently registered.
*
* For example:
*
* GET http://localhost/opentripplanner-api-webapp/ws/routers
* will retrieve a list of all registered routerId -> Graph mappings and their geographic bounds.
*
* GET http://localhost/opentripplanner-api-webapp/ws/routers/london
* will return status code 200 and a brief description of the 'london' graph including geographic
* bounds, or 404 if the 'london' routerId is not registered.
*
* PUT http://localhost/opentripplanner-api-webapp/ws/routers
* will reload the graphs for all currently registered routerIds from disk.
*
* PUT http://localhost/opentripplanner-api-webapp/ws/routers/paris
* will load a Graph from a sub-directory called 'paris' and associate it with the routerId 'paris'.
*
* DELETE http://localhost/opentripplanner-api-webapp/ws/routers/paris
* will release the Paris Graph and de-register the 'paris' routerId.
*
* DELETE http://localhost/opentripplanner-api-webapp/ws/routers
* will de-register all currently registered routerIds.
*
* The GET methods are not secured, but all other methods are secured under ROLE_ROUTERS.
* See documentation for individual methods for additional parameters.
*/
@Path("/routers")
@XmlRootElement
@Autowire
public class Routers {
private static final Logger LOG = LoggerFactory.getLogger(Routers.class);
@InjectParam GraphService graphService;
/**
* Returns a list of routers and their bounds.
* @return a representation of the graphs and their geographic bounds, in JSON or XML depending
* on the Accept header in the HTTP request.
*/
@GET
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
public RouterList getRouterIds()
throws JSONException {
RouterList routerList = new RouterList();
for (String id : graphService.getRouterIds()) {
routerList.routerInfo.add(getRouterInfo(id));
}
return routerList;
}
/**
* Returns the bounds for a specific routerId, or verifies whether it is registered.
* @returns status code 200 if the routerId is registered, otherwise a 404.
*/
@GET @Path("{routerId}") @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
public RouterInfo getGraphId(@PathParam("routerId") String routerId) {
// factor out build one entry
RouterInfo routerInfo = getRouterInfo(routerId);
if (routerInfo == null)
throw new WebApplicationException(Responses.notFound()
.entity("Graph id '" + routerId + "' not registered.").type("text/plain")
.build());
return routerInfo;
}
private RouterInfo getRouterInfo(String routerId) {
Graph graph = graphService.getGraph(routerId);
if (graph == null)
return null;
RouterInfo routerInfo = new RouterInfo();
routerInfo.routerId = routerId;
HullService service = graph.getService(HullService.class);
if (service == null) {
Geometry hull = GraphUtils.makeConvexHull(graph);
service = new StoredHullService(hull);
graph.putService(HullService.class, service);
}
routerInfo.polygon = service.getHull();
return routerInfo;
}
/**
* Reload the graphs for all registered routerIds from disk.
*/
@Secured({ "ROLE_ROUTERS" })
@PUT @Produces({ MediaType.APPLICATION_JSON })
public Response reloadGraphs(@QueryParam("path") String path,
@QueryParam("preEvict") @DefaultValue("true") boolean preEvict) {
graphService.reloadGraphs(preEvict);
return Response.status(Status.OK).build();
}
/**
* Load the graph for the specified routerId from disk.
* @param preEvict before reloading each graph, evict the existing graph. This will prevent
* memory usage from increasing during the reload, but routing will be unavailable on this
* routerId for the duration of the operation.
* @param upload read the graph from the PUT data stream instead of from disk.
*/
@Secured({ "ROLE_ROUTERS" })
@PUT @Path("{routerId}") @Produces({ MediaType.TEXT_PLAIN })
public Response putGraphId(
@PathParam("routerId") String routerId,
@QueryParam("preEvict") @DefaultValue("true") boolean preEvict) {
if (preEvict) {
LOG.debug("pre-evicting graph");
graphService.evictGraph(routerId);
}
LOG.debug("attempting to load graph from server's local filsystem.");
boolean success = graphService.registerGraph(routerId, preEvict);
if (success)
return Response.status(201).entity("graph registered.").build();
else
return Response.status(404).entity("graph not found or other error.").build();
}
/**
* Deserialize a graph sent with the HTTP request as POST data, associating it with the given
* routerId.
*/
@Secured({ "ROLE_ROUTERS" })
@POST @Path("{routerId}") @Produces({ MediaType.TEXT_PLAIN })
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
public Response postGraphOverWire (
@PathParam("routerId") String routerId,
@QueryParam("preEvict") @DefaultValue("true") boolean preEvict,
@QueryParam("loadLevel") @DefaultValue("FULL") LoadLevel level,
InputStream is) {
if (preEvict) {
LOG.debug("pre-evicting graph");
graphService.evictGraph(routerId);
}
LOG.debug("deserializing graph from POST data stream...");
Graph graph;
try {
graph = Graph.load(is, level);
graphService.registerGraph(routerId, graph);
return Response.status(Status.CREATED).entity(graph.toString()).build();
} catch (Exception e) {
return Response.status(Status.BAD_REQUEST).entity(e.toString()).build();
}
}
/** De-register all registered routerIds, evicting them from memory. */
@Secured({ "ROLE_ROUTERS" })
@DELETE @Produces({ MediaType.TEXT_PLAIN })
public Response deleteAll() {
int nEvicted = graphService.evictAll();
String message = String.format("%d graphs evicted.", nEvicted);
return Response.status(200).entity(message).build();
}
/**
* De-register a specific routerId, evicting the associated graph from memory.
* @return status code 200 if the routerId was de-registered,
* 404 if the routerId was not registered.
*/
@Secured({ "ROLE_ROUTERS" })
@DELETE @Path("{routerId}") @Produces({ MediaType.TEXT_PLAIN })
public Response deleteGraphId(@PathParam("routerId") String routerId) {
boolean existed = graphService.evictGraph(routerId);
if (existed)
return Response.status(200).entity("graph evicted.").build();
else
return Response.status(404).entity("graph did not exist.").build();
}
}