/*
* Constellation - An open source and standard compliant SDI
* http://www.constellation-sdi.org
*
* Copyright 2014 Geomatys.
*
* Licensed 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.constellation.ws.rs;
import static org.constellation.ws.ExceptionCode.INVALID_PARAMETER_VALUE;
import static org.constellation.ws.ExceptionCode.INVALID_REQUEST;
import static org.constellation.ws.ExceptionCode.MISSING_PARAMETER_VALUE;
import static org.constellation.ws.ExceptionCode.OPERATION_NOT_SUPPORTED;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringReader;
import java.lang.reflect.Proxy;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.validation.Schema;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.xml.MarshallerPool;
import org.constellation.configuration.AppProperty;
import org.constellation.configuration.Application;
import org.constellation.configuration.ConfigDirectory;
import org.constellation.ws.CstlServiceException;
import org.constellation.ws.MimeType;
import org.constellation.ws.WebServiceUtilities;
import org.constellation.xml.PrefixMappingInvocationHandler;
import org.geotoolkit.util.StringUtilities;
import org.glassfish.jersey.internal.util.collection.StringKeyIgnoreCaseMultivaluedMap;
/**
* Abstract parent of all REST facade classes for Constellation web services.
* <p>
* This class begins the handling of all REST message exchange processing. In
* the REST style of web service, message parameters either are passed directly
* as arguments to the query, e.g.<br>
* {@code protocol://some.url/service?param=value¶m2=othervalue }<br>
* or are passed as raw messages in the body of an HTTP POST message, for
* example as Key-Value Pairs (KVP) or as XML documents.
* </p>
* <p>
* <i>Note:</i> This use of the term REST does not imply the services are
* RESTful; we use the term to distinguish these classes from the other facade
* classes in Constellation which use SOAP to exchange messages in HTTP POST
* exchanges and JAXB to automatically unmarshall those messages into Java
* objects.
* </p>
* <p>
* All incoming requests are handled by one of the {@code doGET} or
* {@code doPOST*} methods. These methods handle the incoming requests by
* ensuring all KVP parameters are in the {@code uriContext} object and all
* other information is in a serializable object of the right kind. The methods
* then call the abstract {@code treatIncomingRequest(Object)} passing any
* serializable object as the method parameter. Sub-classes then handle the
* request calling the {@code uriContext} object or using the method parameter
* as needed.
* </p>
* <p>
* Two other abstract methods need to be implemented by extending classes. The
* method {@code destroy()} will be called prior to the container shutting down
* the service providing an opportunity to log that event. The method
* {@code launchException(..)} forms part of the Constellation exception
* handling design.
* </p>
* <p>
* TODO: explain the design for exception handling.
* </p>
* <p>
* Concrete extensions of this class should, in their constructor, call one of
* the {@code setXMLContext(..)} methods to initialize the JAXB context and
* populate the {@code marshaller} and {@code unmarshaller} fields.
* </p>
* <p>
* Classes extending this one provide the REST facade to Constellation. Most of
* the concrete extensions of this class in Constellation itself implement the
* logic of {@code treatIncomingRequest(Object)} by calling a appropriate
* method in a {@code Worker} object. Those same methods in the {@code Worker}
* object are also called by the classes implementing the SOAP facade, enabling
* the re-use of the logic.
* </p>
*
* @author Guilhem Legal (Geomatys)
* @author Cédric Briançon (Geomatys)
* @author Adrian Custer (Geomatys)
* @since 0.1
*/
public abstract class AbstractWebService implements WebService{
/**
* The default debugging logger for all web services.
*/
protected static final Logger LOGGER = Logging.getLogger("org.constellation.ws.rs");
private static final String PROPERTIES_URL = ConfigDirectory.getServiceURL();
/**
* Automatically set by Jersey.
*
* Provides access to the URI used in the method call, for instance, to
* obtain the Key-Value Pairs in the request. The field is injected, thanks
* to the annotation, when a request arrives.
*/
@Context
private volatile UriInfo uriContext;
/**
* Automatically set by Jersey.
*
* Used to communicate with the Servlet container, for example, to obtain
* the MIME type of a file, to dispatch requests or to write to a log file.
* The field is injected, thanks to the annotation, when a request arrives.
*/
@Context
private volatile ServletContext servletContext;
/**
* Automatically set by Jersey.
*
* The HTTP context used to get information about the client which sent the
* request. The field is injected, thanks to the annotation, when a request
* arrives.
*/
@Context
protected volatile HttpHeaders httpHeaders;
/**
* Automatically set by Jersey.
*
* The HTTP Servlet request used to get information about the client which
* sent the request. The field is injected, thanks to the annotation, when a
* request arrives.
*/
@Context
private volatile HttpServletRequest httpServletRequest;
/**
* If this flag is set the method logParameters() will write the entire request in the logs
* instead of the parameters map.
*
* @deprecated move to worker
*/
@Deprecated
private boolean fullRequestLog = false;
/**
* The POST kvp request parameters (one by thread)
*/
private final ThreadLocal<MultivaluedMap<String, String>> postKvpParameters = new ThreadLocal<MultivaluedMap<String, String>>(){
@Override
protected MultivaluedMap initialValue() {
return new StringKeyIgnoreCaseMultivaluedMap();
}
};
/**
* A pool of JAXB unmarshaller used to create Java objects from XML files.
*/
private MarshallerPool marshallerPool;
/**
* Provides access to the URI used in the method call, for instance, to
* obtain the Key-Value Pairs in the request.
*
* @return
*/
protected final UriInfo getUriContext(){
return uriContext;
}
/**
* Used to communicate with the servlet container, for example, to obtain
* the MIME type of a file, to dispatch requests or to write to a log file.
*
* @return
*/
protected final ServletContext getServletContext(){
return servletContext;
}
/**
* The HTTP servlet request used to get information about the client which
* sent the request.
*
* @return
*/
protected final HttpServletRequest getHttpServletRequest(){
return httpServletRequest;
}
/**
* Treat the incoming request and call the right function in the worker.
* <p>
* The parent class will have processed the request sufficiently to ensure
* all the relevant information is either in the {@code uriContext} field or
* in the {@code Object} passed in as a parameter. Here we proceed a step
* further to ensure the request is encapsulated in a Java object which we
* then pass to the worker when calling the appropriate method.
* </p>
*
* @param objectRequest an object encapsulating the request or {@code null}
* if the request parameters are all in the
* {@code uriContext} field.
* @return a Response, either an image or an XML document depending on the
* user's request.
*/
public abstract Response treatIncomingRequest(Object objectRequest);
/**
* {@inheritDoc }
*/
@Override
public void destroy(){
}
/**
* build an service Exception and marshall it into a StringWriter
*
* @param message
* @param codeName
* @param locator
* @return
*/
protected abstract Response launchException(String message, String codeName, String locator);
/**
* Provide the marshaller pool.
* Live it's instantiation to implementations.
*/
protected synchronized MarshallerPool getMarshallerPool() {
return marshallerPool;
}
/**
* Initialize the JAXB context.
*/
protected synchronized void setXMLContext(final MarshallerPool pool) {
LOGGER.finer("SETTING XML CONTEXT: marshaller Pool version");
marshallerPool = pool;
}
/**
* Treat the incoming GET request.
*
* @return an image or xml response.
*/
@GET
public Response doGET() {
return treatIncomingRequest(null);
}
/**
* Treat the incoming POST request encoded in kvp.
* for each parameters in the request it fill the httpContext.
*
* @param request
* @return an image or xml response.
*/
@POST
@Consumes("application/x-www-form-urlencoded")
public Response doPOSTKvp(final String request) {
/**
* decode string that can be encoded to utf8 url. ie : image%2Fpng will
* be image/png
*/
final String params = StringUtilities.decodeUTF8URL(request);
final StringTokenizer tokens = new StringTokenizer(params, "&");
final StringBuilder log = new StringBuilder("request POST kvp: ");
log.append(params).append('\n');
final MultivaluedMap kvpMap = new StringKeyIgnoreCaseMultivaluedMap();
while (tokens.hasMoreTokens()) {
final String token = tokens.nextToken().trim();
final int equalsIndex = token.indexOf('=');
final String paramName = token.substring(0, equalsIndex);
final String paramValue = token.substring(equalsIndex + 1);
// special case for XML request parameter
if ("request".equalsIgnoreCase(paramName) && (paramValue.startsWith("<") || paramValue.startsWith("%3C"))) {
final String xml = StringUtilities.decodeUTF8URL(paramValue);
final InputStream in = new ByteArrayInputStream(xml.getBytes());
return doPOSTXml(in);
}
log.append("put: ").append(paramName).append("=").append(paramValue).append('\n');
kvpMap.add(paramName, paramValue);
}
postKvpParameters.set(kvpMap);
try {
LOGGER.info(log.toString());
return treatIncomingRequest(null);
} finally {
postKvpParameters.set(new StringKeyIgnoreCaseMultivaluedMap());
}
}
/**
* Treat the incoming POST request encoded in xml.
*
* @return an image or xml response.
* @throw JAXBException
*/
@POST
@Consumes("*/xml")
public Response doPOSTXml(final InputStream is) {
if (marshallerPool != null) {
final Object request;
final MarshallerPool pool;
// we look for a configuration query
final boolean requestValidationActivated;
final List<Schema> schemas;
final List<String> serviceId = getParameter("serviceId");
if (serviceId != null && !serviceId.isEmpty()){
requestValidationActivated = isRequestValidationActivated(serviceId.get(0));
schemas = getRequestValidationSchema(serviceId.get(0));
if ("admin".equals(serviceId.get(0))) {
pool = getConfigurationPool();
} else {
pool = getMarshallerPool();
}
} else {
schemas = null;
pool = getMarshallerPool();
requestValidationActivated = false;
}
final Map<String, String> prefixMapping = new LinkedHashMap<>();
try {
final Unmarshaller unmarshaller = pool.acquireUnmarshaller();
if (requestValidationActivated) {
for (Schema schema : schemas) {
unmarshaller.setSchema(schema);
}
request = unmarshallRequestWithMapping(unmarshaller, is, prefixMapping);
} else {
request = unmarshallRequest(unmarshaller, is);
}
pool.recycle(unmarshaller);
} catch (JAXBException e) {
String errorMsg = e.getMessage();
if (errorMsg == null) {
if (e.getCause() != null && e.getCause().getMessage() != null) {
errorMsg = e.getCause().getMessage();
} else if (e.getLinkedException() != null && e.getLinkedException().getMessage() != null) {
errorMsg = e.getLinkedException().getMessage();
}
}
final String codeName;
if (errorMsg != null && errorMsg.startsWith("unexpected element")) {
codeName = OPERATION_NOT_SUPPORTED.name();
} else {
codeName = INVALID_REQUEST.name();
}
final String locator = WebServiceUtilities.getValidationLocator(errorMsg, prefixMapping);
return launchException("The XML request is not valid.\nCause:" + errorMsg, codeName, locator);
} catch (CstlServiceException e) {
return launchException(e.getMessage(), e.getExceptionCode().identifier(), e.getLocator());
}
/* parameters are now immutable
* if (request instanceof Versioned) {
final Versioned ar = (Versioned) request;
if (ar.getVersion() != null) {
getUriContext().getQueryParameters().add("VERSION", ar.getVersion().toString());
}
}*/
return treatIncomingRequest(request);
} else {
return Response.ok("This service is not running", MimeType.TEXT_PLAIN).build();
}
}
protected abstract boolean isRequestValidationActivated(final String workerID);
protected abstract List<Schema> getRequestValidationSchema(final String workerID);
/**
* A method simply unmarshalling the request with the specified unmarshaller from the specified inputStream.
* can be overriden by child class in case of specific extractionfrom the stream.
*
* @param unmarshaller A JAXB Unmarshaller correspounding to the service context.
* @param is The request input stream.
* @return
* @throws JAXBException
*/
protected Object unmarshallRequest(final Unmarshaller unmarshaller, final InputStream is) throws JAXBException, CstlServiceException {
return unmarshaller.unmarshal(is);
}
protected Object unmarshallRequestWithMapping(final Unmarshaller unmarshaller, final InputStream is, final Map<String, String> prefixMapping) throws JAXBException {
try {
final XMLEventReader rootEventReader = XMLInputFactory.newInstance().createXMLEventReader(is);
final XMLEventReader eventReader = (XMLEventReader) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class[]{XMLEventReader.class}, new PrefixMappingInvocationHandler(rootEventReader, prefixMapping));
return unmarshaller.unmarshal(eventReader);
} catch (XMLStreamException ex) {
throw new JAXBException(ex);
}
}
/**
* Treat the incoming POST request encoded in text plain.
*
* @return an xml exception report.
*/
@POST
@Consumes("text/plain")
public Response doPOSTPlain(final InputStream is) {
LOGGER.warning("request POST plain sending Exception");
return launchException("The plain text content type is not allowed. Send " +
"a message body with key=value pairs in the " +
"application/x-www-form-urlencoded MIME type, or " +
"an XML file using an application/xml or text/xml " +
"MIME type.", INVALID_REQUEST.name(), null);
}
/**
* Extracts the value, for a parameter specified, from a query.
*
* @param parameterName The name of the parameter.
*
* @return the parameter, or {@code null} if not specified.
*/
private List<String> getParameter(final String parameterName) {
final MultivaluedMap<String,String> parameters = uriContext.getQueryParameters();
List<String> values = parameters.get(parameterName);
//maybe the parameterName is case sensitive.
if (values == null) {
for(final Entry<String, List<String>> key : parameters.entrySet()){
if(key.getKey().equalsIgnoreCase(parameterName)){
values = key.getValue();
break;
}
}
}
// look in the POST kvp parameter
if (values == null) {
values = postKvpParameters.get().get(parameterName);
}
// look in Path parameters
if (values == null) {
final MultivaluedMap<String,String> pathParameters = uriContext.getPathParameters();
values = pathParameters.get(parameterName);
}
return values;
}
/**
* Extracts the value, for a parameter specified, from a query.
* If it is a mandatory one, and if it is {@code null}, it throws an exception.
* Otherwise returns {@code null} in the case of an optional parameter not found.
* The parameter is then parsed as boolean.
*
* @param parameterName The name of the parameter.
* @param mandatory true if this parameter is mandatory, false if its optional.
*
* @return the parameter, or {@code null} if not specified and not mandatory.
* @throw CstlServiceException
*/
protected boolean getBooleanParameter(final String parameterName, final boolean mandatory) throws CstlServiceException {
return Boolean.parseBoolean(getParameter(parameterName, mandatory));
}
/**
* Extracts the value, for a parameter specified, from a query.
* If it is a mandatory one, and if it is {@code null}, it throws an exception.
* Otherwise returns {@code null} in the case of an optional parameter not found.
*
* @param parameterName The name of the parameter.
* @param mandatory true if this parameter is mandatory, false if its optional.
*
* @return the parameter, or {@code null} if not specified and not mandatory.
* @throw CstlServiceException
*/
protected String getParameter(final String parameterName, final boolean mandatory) throws CstlServiceException {
final List<String> values = getParameter(parameterName);
if (values == null) {
if (mandatory) {
throw new CstlServiceException("The parameter " + parameterName + " must be specified",
MISSING_PARAMETER_VALUE, parameterName.toLowerCase());
}
return null;
} else {
final String value = values.get(0);
if (value == null && mandatory) {
throw new CstlServiceException("The parameter " + parameterName + " should have a value",
MISSING_PARAMETER_VALUE, parameterName.toLowerCase());
} else {
return value;
}
}
}
protected String getSafeParameter(final String parameterName) {
final List<String> values = getParameter(parameterName);
if (values == null) {
return null;
} else {
return values.get(0);
}
}
/**
* Return a map of parameters put in the query.
* @return
*/
public MultivaluedMap<String, String> getParameters() {
final MultivaluedMap<String, String> results = new StringKeyIgnoreCaseMultivaluedMap();
// GET parameters
for (final Entry<String, List<String>> entry : uriContext.getQueryParameters().entrySet()) {
if (entry.getValue() != null && !entry.getValue().isEmpty()) {
results.add(entry.getKey(), entry.getValue().get(0));
}
}
// POST kvp parameters
for (final Entry<String, List<String>> entry : postKvpParameters.get().entrySet()) {
if (entry.getValue() != null && !entry.getValue().isEmpty()) {
results.add(entry.getKey(), entry.getValue().get(0));
}
}
return results;
}
/**
* Extract all The parameters from the query and write it in the console.
* It is a debug method.
*/
protected void logParameters() {
if (!fullRequestLog) {
final MultivaluedMap<String,String> parameters = getUriContext().getQueryParameters();
if (!parameters.isEmpty()) {
// we don't write POST request with VERSION parameters automatically put
if (parameters.size() != 1 || !parameters.containsKey("VERSION")) {
LOGGER.info(parameters.toString());
}
}
} else {
if (getUriContext().getRequestUri() != null) {
LOGGER.info(getUriContext().getRequestUri().toString());
}
}
}
protected void logPostParameters(final Object request) {
if (request != null) {
final MarshallerPool pool = getMarshallerPool();
try {
final Marshaller m = pool.acquireMarshaller();
m.marshal(request, System.out);
pool.recycle(m);
} catch (JAXBException ex) {
LOGGER.log(Level.WARNING, "Error while marshalling the request", ex);
}
}
}
/**
* Extract The complex parameter encoded in XML from the query.
* If the parameter is mandatory and if it is null it throw an exception.
* else it return null.
*
* @param parameterName The name of the parameter.
* @param mandatory true if this parameter is mandatory, false if its optional.
*
* @return the parameter or null if not specified
* @throw CstlServiceException
*/
protected Object getComplexParameter(final String parameterName, final boolean mandatory) throws CstlServiceException {
try {
final String value = getParameter(parameterName, mandatory);
final StringReader sr = new StringReader(value);
final Unmarshaller unmarshaller = marshallerPool.acquireUnmarshaller();
Object result = unmarshaller.unmarshal(sr);
marshallerPool.recycle(unmarshaller);
if (result instanceof JAXBElement) {
result = ((JAXBElement)result).getValue();
}
return result;
} catch (JAXBException ex) {
throw new CstlServiceException("The xml object for parameter " + parameterName + " is not well formed:" + '\n' +
ex, INVALID_PARAMETER_VALUE);
}
}
/**
* Return the service URL obtain by the first request made.
* something like : http://localhost:8080/constellation/WS/
*
* @return the service uURL.
*/
protected String getServiceURL() {
String result = Application.getProperty(AppProperty.CSTL_SERVICE_URL);
if (result == null) {
result = getUriContext().getBaseUri().toString();
}
return result;
}
/**
* @return the fullRequestLog
*/
public boolean isFullRequestLog() {
return fullRequestLog;
}
/**
* @param fullRequestLog the fullRequestLog to set
*/
public void setFullRequestLog(final boolean fullRequestLog) {
this.fullRequestLog = fullRequestLog;
}
/**
* Return the Marshaller pool for configuration request
* @return
*/
protected abstract MarshallerPool getConfigurationPool();
}