/* * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * * Jalview is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * Jalview 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 Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ package jalview.rest; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.lang.reflect.InvocationTargetException; import java.net.BindException; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.server.handler.ContextHandler; import jalview.bin.Console; import jalview.httpserver.AbstractRequestHandler; import jalview.httpserver.HttpServer; /** * A simple handler to process (or delegate) HTTP requests on /jalview/rest */ public class RestHandler extends AbstractRequestHandler { public enum Status { STARTED, IN_PROGRESS, FINISHED, ERROR, NOT_RUN } public interface EndpointI { public String getPath(); public String getName(); public String getParameters(); public String getDescription(); public void processEndpoint(HttpServletRequest request, HttpServletResponse response); } private static final String MY_PATH = "rest"; private static final String MY_NAME = "Rest"; private String missingEndpointMessage = null; private boolean init = false; // map of method names and method handlers private Map endpoints = null; protected Map getEndpoints() { return endpoints; } protected AbstractEndpoint getNewEndpoint(String name) { if (getEndpoints() == null) { return null; } try { return getEndpoints().get(name).getClass() .getDeclaredConstructor(API.class).newInstance(this); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { Console.debug("Could not instantiate new endpoint '" + name + "'", e); } return null; } /** * Singleton instance of this class */ private static RestHandler instance = null; /** * Returns the singleton instance of this class * * @return * @throws BindException */ public static RestHandler getInstance() throws BindException { synchronized (RestHandler.class) { if (instance == null) { instance = new RestHandler(); } } return instance; } /** * Private constructor enforces use of singleton * * @throws BindException */ protected RestHandler() throws BindException { init(); /* * We don't register the handler here - this is done as a special case in * HttpServer initialisation; to do it here would invite an infinite loop of * RestHandler/HttpServer constructor */ } /** * Handle a jalview/rest request * * @throws IOException */ @Override protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws IOException { /* * Currently just echoes the request; add helper classes as required to * process requests */ // This "pointless" call to request.getInputStream() seems to preserve the // InputStream for use later in getRequestBody. request.getInputStream(); String remoteAddr = request.getRemoteAddr(); if (!("127.0.0.1".equals(remoteAddr) || "localhost".equals(remoteAddr))) { returnError(request, response, "Not authorised: " + remoteAddr, HttpServletResponse.SC_UNAUTHORIZED); return; } if (getEndpoints() == null) { final String queryString = request.getQueryString(); final String reply = "REST not yet implemented; received " + request.getMethod() + ": " + request.getRequestURL() + (queryString == null ? "" : "?" + queryString); Console.error(reply); response.setHeader("Cache-Control", "no-cache/no-store"); response.setHeader("Content-type", "text/plain"); final PrintWriter writer = response.getWriter(); writer.write(reply); return; } String endpointName = getRequestedEndpointName(request); Console.debug(endpointName); if (!getEndpoints().containsKey(endpointName) || getEndpoints().get(endpointName) == null) { Console.debug( "RestHandler did not find endpoint '" + endpointName + "'"); response.setHeader("Cache-Control", "no-cache/no-store"); response.setHeader("Content-type", "text/plain"); PrintWriter writer = response.getWriter(); writer.write(missingEndpointMessage == null ? "REST endpoint '" + endpointName + "' not defined" : missingEndpointMessage); writer.write("\n"); writer.write("Available endpoints are:\n"); ContextHandler ch = HttpServer.getInstance().getContextHandler(this); String base = HttpServer.getInstance().getUri().toString(); String contextPath = ch == null ? "" : ch.getContextPath(); for (String key : getEndpoints().keySet()) { writer.write(base + contextPath + "/" + key + "\n"); } response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return; } response.setHeader("Cache-Control", "no-cache/no-store"); response.setHeader("Content-type", "text/plain"); EndpointI ep = getNewEndpoint(endpointName); ep.processEndpoint(request, response); return; } /** * Returns a display name for this service */ @Override public String getName() { return MY_NAME; } /** * Initialise methods * * @throws BindException */ protected void init() throws BindException { init(MY_PATH); } protected void init(String path) throws BindException { setPath(path); // Override this in extended class // e.g. registerHandler and addEndpoints } protected void addEndpoint(AbstractEndpoint ep) { if (getEndpoints() == null) { endpoints = new HashMap<>(); } endpoints.put(ep.getPath(), ep); Console.debug("REST API, added endpoint '" + ep.getPath() + "'"); } protected String getRequestedEndpointName(HttpServletRequest request) { String pathInfo = request.getPathInfo(); int slashpos = pathInfo.indexOf('/', 1); return slashpos > 1 ? pathInfo.substring(1, slashpos) : pathInfo.substring(1); } protected void returnError(HttpServletRequest request, HttpServletResponse response, String message) { returnError(request, response, message, HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } protected void returnError(HttpServletRequest request, HttpServletResponse response, String message, int statusCode) { response.setStatus(statusCode); String endpointName = getRequestedEndpointName(request); Console.error(getName() + " error: endpoint " + endpointName + " failed: '" + message + "'"); try { PrintWriter writer = response.getWriter(); writer.write("Endpoint " + endpointName + ": " + message); } catch (IOException e) { Console.debug("Could not write to REST response for endpoint " + endpointName, e); } } protected void returnStatus(HttpServletResponse response, String id, Status status) { try { PrintWriter writer = response.getWriter(); if (id != null) writer.write("id=" + id + "\n"); if (status != null) writer.write("status=" + status.toString() + "\n"); } catch (IOException e) { Console.debug("Could not write status to REST response for id:" + id, e); } } protected String getRequestBody(HttpServletRequest request, HttpServletResponse response) throws IOException { StringBuilder sb = new StringBuilder(); BufferedReader reader = null; Console.debug("REQUEST=" + request.toString()); Console.debug("REQUEST.Content-Length=" + request.getContentLength()); try { reader = request.getReader(); Console.debug("Using getReader()"); } catch (IllegalStateException e) { ServletInputStream is = request.getInputStream(); reader = new BufferedReader(new InputStreamReader(is)); Console.debug("Using getInputStream()"); } if (reader != null) { try { String line; while ((line = reader.readLine()) != null) { sb.append(line).append('\n'); } } finally { reader.close(); } } else { returnError(request, response, "Error reading body of HTTP request"); } return sb.toString(); } }