Java tutorial
/* * 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.jena.fuseki.servlets; import static java.lang.String.format; import static org.apache.commons.lang3.StringUtils.isEmpty; import static org.apache.jena.fuseki.server.Operation.*; import static org.apache.jena.fuseki.servlets.ActionExecLib.allocHttpAction; import java.util.Collection; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.jena.atlas.lib.InternalErrorException; import org.apache.jena.fuseki.Fuseki; import org.apache.jena.fuseki.auth.Auth; import org.apache.jena.fuseki.server.*; import org.apache.jena.riot.web.HttpNames; import org.apache.jena.web.HttpSC; import org.slf4j.Logger; /** * Dispatch on registered datasets. This is the entry point into Fuseki for dataset * operations. * * Administration operations, and directly registered servlets and static content are * called through the usual web server process. * * HTTP Request URLs, after servlet context removed, take the form {@code /dataset} or {@code /dataset/service}. * The most general URL is {@code /context/dataset/service}. * The {@link DataAccessPointRegistry} maps {@code /dataset} to a {@link DataAccessPoint}. */ public class Dispatcher { // Development debugging only. Excessive for normal operation. private static final boolean LogDispatch = false; private static Logger LOG = Fuseki.serverLog; /** * Handle an HTTP request if it is sent to a registered dataset. * * Fuseki uses dynamic dispatch, the set of registered datasets can change while * the server is running, so dispatch is driven off Fuseki system registries. * * If the request URL matches a registered dataset, process the request, and send * the response. * * This function is called by {@link FusekiFilter#doFilter}. * * @param request * HttpServletRequest * @param response * HttpServletResponse * @return Returns {@code true} if the request has been handled, else false (no * response sent). */ public static boolean dispatch(HttpServletRequest request, HttpServletResponse response) { // Path component of the URI, without context path String uri = ActionLib.actionURI(request); String datasetUri = ActionLib.mapActionRequestToDataset(uri); if (LogDispatch) { LOG.info("Filter: Request URI = " + request.getRequestURI()); LOG.info("Filter: Action URI = " + uri); LOG.info("Filter: Dataset URI = " + datasetUri); } if (datasetUri == null) return false; DataAccessPointRegistry registry = DataAccessPointRegistry.get(request.getServletContext()); if (!registry.isRegistered(datasetUri)) { if (LogDispatch) LOG.debug("No dispatch for '" + datasetUri + "'"); return false; } DataAccessPoint dap = registry.get(datasetUri); process(dap, request, response); return true; } /** Set up and handle a HTTP request for a dataset. */ private static void process(DataAccessPoint dap, HttpServletRequest request, HttpServletResponse response) { HttpAction action = allocHttpAction(dap, Fuseki.actionLog, request, response); dispatchAction(action); } /** * Determine and call the {@link ActionProcessor} to handle this * {@link HttpAction}, including access control at the dataset and service levels. */ public static void dispatchAction(HttpAction action) { ActionExecLib.execAction(action, () -> chooseProcessor(action)); } /** * Find the ActionProcessor or return null if there can't determine one. This * function does NOT return null; it throws ActionErrorException after sending an * HTTP error response. * * Returning null indicates an error, and the HTTP response * has been done. */ private static ActionProcessor chooseProcessor(HttpAction action) { // "return null" indicates that processing failed to find a ActionProcessor DataAccessPoint dataAccessPoint = action.getDataAccessPoint(); DataService dataService = action.getDataService(); if (!dataService.isAcceptingRequests()) { ServletOps.error(HttpSC.SERVICE_UNAVAILABLE_503, "Dataset not currently active"); return null; } // ---- Determine Endpoint. String endpointName = mapRequestToOperation(action, dataAccessPoint); Endpoint ep = chooseEndpoint(action, dataService, endpointName); if (ep == null) { if (isEmpty(endpointName)) ServletOps.errorBadRequest("No operation for request: " + action.getActionURI()); else ServletOps.errorNotFound("No endpoint: " + action.getActionURI()); return null; } Operation operation = ep.getOperation(); if (operation == null) { ServletOps.errorNotFound("No operation: " + action.getActionURI()); return null; } action.setEndpoint(ep); // ---- Authorization // -- Server-level authorization. // Checking was carried out by servlet filter AuthFilter. // Need to check Data service and endpoint authorization policies. String user = action.getUser(); // -- Data service level authorization if (dataService.authPolicy() != null) { if (!dataService.authPolicy().isAllowed(user)) ServletOps.errorForbidden(); } // -- Endpoint level authorization // Make sure all contribute authentication. Auth.allow(user, action.getEndpoint().getAuthPolicy(), ServletOps::errorForbidden); if (isEmpty(endpointName) && !ep.isUnnamed()) { // [DISPATCH LEGACY] // If choice was by looking in all named endpoints for a unnamed endpoint // request, ensure all choices allow access. // There may be several endpoints for the operation. // Authorization is the AND of all endpoints. Collection<Endpoint> x = getEndpoints(dataService, operation); if (x.isEmpty()) throw new InternalErrorException("Inconsistent: no endpoints for " + operation); x.forEach(ept -> Auth.allow(user, ept.getAuthPolicy(), ServletOps::errorForbidden)); } // ---- Authorization checking. // ---- Handler. // Decide the code to execute the request. // ActionProcessor handler = target(action, operation); ActionProcessor processor = target(action, operation); if (processor == null) ServletOps.errorBadRequest(format("dataset=%s: op=%s", dataAccessPoint.getName(), operation.getName())); return processor; } // operation to code for operation. private static ActionProcessor target(HttpAction action, Operation operation) { return action.getOperationRegistry().findHandler(operation); } /** * Map request to operation name. * Returns the service name (the part after the "/" of the dataset part) or "". */ protected static String mapRequestToOperation(HttpAction action, DataAccessPoint dataAccessPoint) { return ActionLib.mapRequestToOperation(action, dataAccessPoint); } // Find the endpoints for an operation. // This is GSP_R/GSP_RW and Quads_R/Quads_RW aware. // If asked for GSP_R and there are no endpoints for GSP_R, try GSP_RW. // Ditto Quads_R -> Quads_RW. private static Collection<Endpoint> getEndpoints(DataService dataService, Operation operation) { Collection<Endpoint> x = dataService.getEndpoints(operation); if (x == null || x.isEmpty()) { if (operation == GSP_R) x = dataService.getEndpoints(GSP_RW); } return x; } /** * Choose an endpoint. This can be with or without endpointName. * If there is no endpoint and the action is on the data service itself (unnamed endpoint) * look for a named endpoint that supplies the operation. */ private static Endpoint chooseEndpoint(HttpAction action, DataService dataService, String endpointName) { Endpoint ep = chooseEndpointNoLegacy(action, dataService, endpointName); if (ep != null) return ep; if (!isEmpty(endpointName)) return ep; // [DISPATCH LEGACY] Operation operation = chooseOperation(action); // Search for an endpoint that provides the operation. // No guarantee it has the access controls for the operation // but in this case, access control will validate against all possible endpoints. ep = findEndpointForOperation(action, dataService, operation, true); return ep; } /** Choose an endpoint. */ private static Endpoint chooseEndpointNoLegacy(HttpAction action, DataService dataService, String endpointName) { EndpointSet epSet = isEmpty(endpointName) ? dataService.getEndpointSet() : dataService.getEndpointSet(endpointName); if (epSet == null || epSet.isEmpty()) return null; // If there is one endpoint, dispatch there directly. Endpoint ep = epSet.getOnly(); if (ep != null) return ep; // if ( ep != null ) { // if ( ! isGSP(ep.getOperation()) ) // return ep; // // GSP but if not valid, let it upgrade to quads. // if ( hasGSPParams(action) ) // return ep; // ep = null; // } // No single direct dispatch. // Work out which operation we are looking for. Operation operation = chooseOperation(action); ep = epSet.get(operation); if (ep != null) return ep; return null; } private static boolean isGSP(Operation operation) { return operation.equals(GSP_R) || operation.equals(GSP_RW); } private static boolean hasGSPParams(HttpAction action) { boolean hasParamGraphDefault = action.request.getParameter(HttpNames.paramGraphDefault) != null; if (hasParamGraphDefault) return true; boolean hasParamGraph = action.request.getParameter(HttpNames.paramGraph) != null; if (hasParamGraph) return true; return false; } /** Find an endpoint for an operation. * This returns all endpoints of a {@link DataService} that provide the {@link Operation}. * This understands that Quads_RW can service Quads_R and GSP_RW can service GSP_R. */ private static Endpoint findEndpointForOperation(HttpAction action, DataService dataService, Operation operation, boolean preferUnnamed) { Endpoint ep = findEndpointForOperationExact(dataService, operation, preferUnnamed); if (ep != null) return ep; // [DISPATCH LEGACY] // Try to find "R" functionality from an RW. if (GSP_R.equals(operation)) return findEndpointForOperationExact(dataService, GSP_RW, preferUnnamed); // Instead of 404, return 405 if asked for RW but only R available. if (GSP_RW.equals(operation) && dataService.hasOperation(GSP_R)) ServletOps.errorMethodNotAllowed(action.getMethod()); return null; } /** Find a matching endpoint for exactly this operation. */ private static Endpoint findEndpointForOperationExact(DataService dataService, Operation operation, boolean preferUnnamed) { List<Endpoint> eps = dataService.getEndpoints(operation); if (eps == null || eps.isEmpty()) return null; // ==== Legacy compatibility. // Find a named service if an unnamed one is not available. Endpoint epAlt = null; for (Endpoint ep : eps) { if (operation.equals(ep.getOperation())) { if (ep.isUnnamed() && preferUnnamed) return ep; if (!ep.isUnnamed() && !preferUnnamed) return ep; epAlt = ep; } } // Did not find a preferred one. return epAlt; } /** * Identify the operation being requested. * It is analysing the HTTP request using global configuration. * The decision is is based on * <ul> * <li>Query parameters (URL query string or HTML form)</li> * <li>Content-Type header</li> * <li>Otherwise it is a plain REST (quads) operation.chooseOperation</li> * </ul> * The HTTP Method is not considered. * <p> * The operation is not guaranteed to be supported on every {@link DataService} * nor that access control will allow it to be performed. */ public static Operation chooseOperation(HttpAction action) { HttpServletRequest request = action.getRequest(); // ---- Dispatch based on HttpParams : Query, Update, GSP. // -- Query boolean isQuery = request.getParameter(HttpNames.paramQuery) != null; if (isQuery) return Query; // -- Update // Standards name "update", non-standard name "request" (old use by Fuseki) boolean isUpdate = request.getParameter(HttpNames.paramUpdate) != null || request.getParameter(HttpNames.paramRequest) != null; if (isUpdate) // The SPARQL_Update servlet will deal with using GET. return Update; // -- SPARQL Graph Store Protocol boolean hasParamGraph = request.getParameter(HttpNames.paramGraph) != null; boolean hasParamGraphDefault = request.getParameter(HttpNames.paramGraphDefault) != null; if (hasParamGraph || hasParamGraphDefault) return gspOperation(action, request); // -- Any other queryString // Place for an extension point. boolean hasParams = request.getParameterMap().size() > 0; if (hasParams) { // Unrecognized ?key=value ServletOps.errorBadRequest( "Malformed request: unrecognized query string parameters: " + request.getQueryString()); } // ---- Content-type // We don't wire in all the RDF syntaxes. // Instead, "Quads" drops through to the default operation. // This does not have the ";charset=" String ct = request.getContentType(); if (ct != null) { Operation operation = action.getOperationRegistry().findByContentType(ct); if (operation != null) return operation; } // ---- No registered content type, no query parameters. // Plain HTTP operation on the dataset handled as quads or rejected. return quadsOperation(action, request); } /** * Determine the {@link Operation} for a SPARQL Graph Store Protocol (GSP) action. * <p> * Assumes, and does not check, that the action is a GSP action. * * @throws ActionErrorException * (which causes a servlet 4xx response) if the operaton is not permitted. */ private static Operation gspOperation(HttpAction action, HttpServletRequest request) throws ActionErrorException { // Check enabled. if (isReadMethod(request)) return GSP_R; else return GSP_RW; } /** * Determine the {@link Operation} for a Quads operation. (GSP, except on the * whole dataset). * <p> * Assumes, and does not check, that the action is a Quads action. * * @throws ActionErrorException * (which causes a servlet 405 response) if the operaton is not permitted. */ private static Operation quadsOperation(HttpAction action, HttpServletRequest request) throws ActionErrorException { // Check enabled. Extends GSP. if (isReadMethod(request)) return GSP_R; else return GSP_RW; } private static boolean isReadMethod(HttpServletRequest request) { String method = request.getMethod(); // REST dataset. boolean isGET = method.equals(HttpNames.METHOD_GET); boolean isHEAD = method.equals(HttpNames.METHOD_HEAD); return isGET || isHEAD; } }