/* * 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.soap; // J2SE dependencies import org.apache.sis.util.logging.Logging; import org.apache.sis.xml.MarshallerPool; import org.constellation.ServiceDef.Specification; import org.constellation.admin.SpringHelper; import org.constellation.business.IServiceBusiness; import org.constellation.ws.CstlServiceException; import org.constellation.ws.WSEngine; import org.constellation.ws.WebServiceUtilities; import org.constellation.ws.Worker; import org.constellation.xml.PrefixMappingInvocationHandler; import org.opengis.util.CodeList; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.xml.sax.SAXParseException; import javax.annotation.PreDestroy; import javax.annotation.Resource; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.soap.MessageFactory; import javax.xml.soap.SOAPConstants; import javax.xml.soap.SOAPException; import javax.xml.soap.SOAPMessage; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.transform.dom.DOMSource; import javax.xml.validation.Schema; import javax.xml.ws.Provider; import javax.xml.ws.WebServiceContext; import javax.xml.ws.handler.MessageContext; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import static org.geotoolkit.ows.xml.OWSExceptionCode.INVALID_REQUEST; import static org.geotoolkit.ows.xml.OWSExceptionCode.NO_APPLICABLE_CODE; import static org.geotoolkit.ows.xml.OWSExceptionCode.OPERATION_NOT_SUPPORTED; // Constellation dependencies // Geotoolkit dependencies // GeoAPI dependencies /** * Abstract parent SOAP facade for all OGC web services in Constellation. * <p> * This class * </p> * <p> * The Open Geospatial Consortium (OGC) has defined a number of web services for * geospatial data such as: * <ul> * <li><b>CSW</b> -- Catalog Service for the Web</li> * <li><b>WCS</b> -- Web Coverage Service</li> * <li><b>SOS</b> -- Sensor Observation Service</li> * </ul> * Many of these Web Services have been defined to work with SOAP based HTTP * message exchange; this class provides base functionality for those services. * </p> * * @version $Id$ * * @author Guilhem Legal (Geomatys) * @since 0.7 */ public abstract class OGCWebService<W extends Worker> implements Provider<SOAPMessage> {//Source> { /** * use for debugging purpose */ protected static final Logger LOGGER = Logging.getLogger("org.constellation.ws.soap"); private final Specification specification; protected static final QName SENDER_CODE = new QName("http://www.w3.org/2003/05/soap-envelope", "Sender"); protected static final QName RECEIVER_CODE = new QName("http://www.w3.org/2003/05/soap-envelope", "Receiver"); /** * A pool of JAXB unmarshaller used to create Java objects from XML files. */ private MarshallerPool marshallerPool; @Resource private volatile WebServiceContext context; @Inject private IServiceBusiness serviceBusiness; /** * Initialize the basic attributes of a web serviceType. * */ public OGCWebService(final Specification spec) { LOGGER.log(Level.INFO, "Starting the SOAP {0} service facade.\n", spec.name()); this.specification = spec; SpringHelper.injectDependencies(this); WSEngine.registerService(specification.name(), "SOAP", getWorkerClass(), getConfigurerClass()); /* * build the map of Workers, by scanning the sub-directories of its * service directory. */ if (!WSEngine.isSetService(specification.name())) { buildWorkerMap(); } else { LOGGER.log(Level.INFO, "Workers already set for {0}", specification.name()); } } /** * Initialize the JAXB context. */ protected synchronized void setXMLContext(final MarshallerPool pool) { LOGGER.finer("SETTING XML CONTEXT: marshaller Pool version"); marshallerPool = pool; } protected synchronized MarshallerPool getMarshallerPool() { return marshallerPool; } /** * Scan the configuration directory to instantiate Web service workers. */ private void buildWorkerMap() { final Map<String, Worker> workersMap = new HashMap<>(); for (String serviceID : serviceBusiness.getServiceIdentifiers(specification.name().toLowerCase())) { final W newWorker = createWorker(serviceID); workersMap.put(serviceID, newWorker); } WSEngine.setServiceInstances(specification.name(), workersMap); } /** * Build a new instance of Web service worker with the specified configuration directory * * @param instanceDirectory The configuration directory of the instance. * @return */ protected abstract W createWorker(final String identifier); /** * @return the worker binding class of the current service. */ protected abstract Class getWorkerClass(); /** * @return the {@link org.constellation.configuration.ServiceConfigurer} class implementation. */ protected abstract Class getConfigurerClass(); /** * extract the service URL (before serviceName/serviceID?) * @return */ protected String getServiceURL() { final HttpServletRequest request = (HttpServletRequest) context.getMessageContext().get(MessageContext.SERVLET_REQUEST); String url = ""; if (request != null) { url = request.getRequestURL().toString(); url = url.substring(0, url.lastIndexOf('/')); url = url.substring(0, url.lastIndexOf('/') + 1); } else { LOGGER.warning("uable to find the service URL"); } return url; } /** * Extract the instance ID from the URL. * * @return */ private String extractWorkerID() { final String pathInfo = (String) context.getMessageContext().get(MessageContext.PATH_INFO); final HttpServletRequest request = (HttpServletRequest) context.getMessageContext().get(MessageContext.SERVLET_REQUEST); if (request != null) { final String url = request.getRequestURL().toString(); return url.substring(url.lastIndexOf('/') + 1); } else if (pathInfo != null) { return pathInfo.substring(pathInfo.lastIndexOf('/') + 1); } else { LOGGER.severe("Unable to extract the servletRequest"); return null; } } /** * This method is used in adition to a * @SchemaValidation(handler = ValidationHandler.class) annotations on a JAX-WS service class * * @throws CstlServiceException */ protected void verifyValidation() throws CstlServiceException { final SAXParseException e = (SAXParseException) context.getMessageContext().get(ValidationHandler.ERROR); if (e != null) { String errorMsg = e.getMessage(); if (errorMsg == null) { if (e.getCause() != null && e.getCause().getMessage() != null) { errorMsg = e.getCause().getMessage(); } } final CodeList codeName; if (errorMsg != null && errorMsg.startsWith("unexpected element")) { codeName = OPERATION_NOT_SUPPORTED; } else { codeName = INVALID_REQUEST; } final String locator = WebServiceUtilities.getValidationLocator(errorMsg, WebServiceUtilities.DUMMY_MAPPING); throw new CstlServiceException("The XML request is not valid.\nCause:" + errorMsg, codeName, locator); } } /** * Return the current worker specified by the URL. * * @return * @throws CstlServiceException */ protected W getCurrentWorker() throws CstlServiceException { final String serviceID = extractWorkerID(); if (serviceID == null || !WSEngine.serviceInstanceExist(specification.name(), serviceID)) { LOGGER.log(Level.WARNING, "Received request on undefined instance identifier:{0}", serviceID); final Set<String> instanceNames = WSEngine.getInstanceNames(specification.name()); final String msg; if (serviceID == null) { msg = "You must specify an instance id.\n available instance:" + instanceNames; } else { msg = "Undefined instance id.\n available instance:" + instanceNames; } throw new CstlServiceException(msg); // TODO return Response.status(Response.Status.NOT_FOUND).build(); } else { return (W) WSEngine.getInstance(specification.name(), serviceID); } } /** * Return the number of instance if the web-service */ protected int getWorkerMapSize() { return WSEngine.getInstanceSize(specification.name()); } @PreDestroy public void destroy() { LOGGER.log(Level.INFO, "Shutting down the SOAP {0} service facade.", specification.name()); WSEngine.destroyInstances(specification.name()); } @Override public SOAPMessage invoke(final SOAPMessage requestMsg) { final Map<String, String> prefixMapping = new LinkedHashMap<>(); try { final W worker = getCurrentWorker(); worker.setServiceUrl(getServiceURL()); /* * Unmarshal Request */ final Unmarshaller ummarshaller = marshallerPool.acquireUnmarshaller(); final Node requestNode = requestMsg.getSOAPBody().extractContentAsDocument(); Object request; if (worker.isRequestValidationActivated()) { final List<Schema> schemas = worker.getRequestValidationSchema(); for (Schema schema : schemas) { ummarshaller.setSchema(schema); } request = unmarshallRequestWithMapping(ummarshaller, requestNode, prefixMapping); } else { request = unmarshallRequest(ummarshaller, requestNode); } if (request instanceof JAXBElement) { request = ((JAXBElement) request).getValue(); } marshallerPool.recycle(ummarshaller); final Object result = treatIncomingRequest(request, worker); final Marshaller m = marshallerPool.acquireMarshaller(); final DocumentBuilderFactory dfactory = DocumentBuilderFactory.newInstance(); final Document resultNode = dfactory.newDocumentBuilder().newDocument(); m.marshal(result, resultNode); marshallerPool.recycle(m); final MessageFactory factory = MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL); final SOAPMessage response = factory.createMessage(); response.getSOAPBody().addDocument(resultNode); return response; } catch (CstlServiceException e) { return processExceptionResponse(e.getMessage(), e.getExceptionCode().name(), e.getLocator()); } 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 processExceptionResponse("The XML request is not valid.\nCause:" + errorMsg, codeName, locator); } catch (SOAPException e) { return processExceptionResponse(e.getMessage(), NO_APPLICABLE_CODE.name(), null); } catch (ParserConfigurationException e) { return processExceptionResponse(e.getMessage(), NO_APPLICABLE_CODE.name(), null); } } /** * 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 Node is) throws JAXBException, CstlServiceException { return unmarshaller.unmarshal(is); } protected Object unmarshallRequestWithMapping(final Unmarshaller unmarshaller, final Node is, final Map<String, String> prefixMapping) throws JAXBException { try { final DOMSource source = new DOMSource(is); final XMLEventReader rootEventReader = XMLInputFactory.newInstance().createXMLEventReader(source); 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); } } protected abstract SOAPMessage processExceptionResponse(final String message, final String code, final String locator); /** * Treat the incoming request and call the right function. * * @param objectRequest if the server receive a POST request in XML, * this object contain the request. Else for a GET or a POST kvp * request this parameter is {@code null} * * @param worker the selected worker on which apply the request. * * @return an xml response. */ protected abstract Object treatIncomingRequest(final Object objectRequest,final W worker) throws CstlServiceException; }