/*
* 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;
// J2SE dependencies
import net.iharder.Base64;
import org.apache.sis.util.iso.Types;
import org.apache.sis.xml.MarshallerPool;
import org.constellation.ServiceDef;
import org.constellation.ServiceDef.Specification;
import org.constellation.admin.SpringHelper;
import org.constellation.business.IServiceBusiness;
import org.constellation.configuration.ConfigurationException;
import org.constellation.generic.database.GenericDatabaseMarshallerPool;
import org.constellation.security.IncorrectCredentialsException;
import org.constellation.security.SecurityManagerHolder;
import org.constellation.security.UnknownAccountException;
import org.constellation.ws.CstlServiceException;
import org.constellation.ws.ServiceConfigurer;
import org.constellation.ws.WSEngine;
import org.constellation.ws.Worker;
import org.geotoolkit.ows.xml.OWSExceptionCode;
import org.geotoolkit.util.StringUtilities;
import org.opengis.util.CodeList;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.ws.rs.core.Response;
import javax.xml.bind.JAXBElement;
import javax.xml.validation.Schema;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import static org.geotoolkit.ows.xml.OWSExceptionCode.INVALID_CRS;
import static org.geotoolkit.ows.xml.OWSExceptionCode.INVALID_FORMAT;
import static org.geotoolkit.ows.xml.OWSExceptionCode.INVALID_PARAMETER_VALUE;
import static org.geotoolkit.ows.xml.OWSExceptionCode.INVALID_POINT;
import static org.geotoolkit.ows.xml.OWSExceptionCode.INVALID_REQUEST;
import static org.geotoolkit.ows.xml.OWSExceptionCode.INVALID_UPDATE_SEQUENCE;
import static org.geotoolkit.ows.xml.OWSExceptionCode.INVALID_VALUE;
import static org.geotoolkit.ows.xml.OWSExceptionCode.LAYER_NOT_DEFINED;
import static org.geotoolkit.ows.xml.OWSExceptionCode.MISSING_PARAMETER_VALUE;
import static org.geotoolkit.ows.xml.OWSExceptionCode.OPERATION_NOT_SUPPORTED;
import static org.geotoolkit.ows.xml.OWSExceptionCode.STYLE_NOT_DEFINED;
import static org.geotoolkit.ows.xml.OWSExceptionCode.VERSION_NEGOTIATION_FAILED;
// Apache SIS dependencies
/**
* Abstract parent REST 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>WMS</b> -- Web Map Service</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 REST based HTTP
* message exchange; this class provides base functionality for those services.
* </p>
*
* @version $Id$
*
* @author Guilhem Legal (Geomatys)
* @author Cédric Briançon (Geomatys)
* @author Garcia Benjamin (Geomatys)
*
* @version 0.9
* @since 0.3
*/
public abstract class OGCWebService<W extends Worker> extends AbstractWebService {
private final String serviceName;
@Inject
private IServiceBusiness serviceBusiness;
/**
* Initialize the basic attributes of a web serviceType.
*
* @param specification The OGC specification.
*/
public OGCWebService(final Specification specification) {
super();
if (specification == null){
throw new IllegalArgumentException("It is compulsory for a web service to have a specification.");
}
SpringHelper.injectDependencies(this);
this.serviceName = specification.name();
LOGGER.log(Level.INFO, "Starting the REST {0} service facade.", serviceName);
WSEngine.registerService(serviceName, "REST", getWorkerClass(), getConfigurerClass());
/*
* build the map of Workers, by scanning the sub-directories of its
* service directory.
*/
if (!WSEngine.isSetService(serviceName)) {
try {
serviceBusiness.start(serviceName.toLowerCase());
} catch (ConfigurationException ex) {
LOGGER.log(Level.WARNING, "Error while starting services for :" + serviceName, ex);
}
} else {
LOGGER.log(Level.INFO, "Workers already set for {0}", serviceName);
}
}
/**
* @return the worker class of the service.
*/
protected abstract Class getWorkerClass();
/**
* Returns the {@link ServiceConfigurer} class implementation.
*
* @return the implementation {@link Class}
*/
protected abstract Class<? extends ServiceConfigurer> getConfigurerClass();
/**
* {@inheritDoc}
*/
@Override
protected boolean isRequestValidationActivated(final String serviceID) {
if (serviceID != null && WSEngine.serviceInstanceExist(serviceName, serviceID)) {
final W worker = (W) WSEngine.getInstance(serviceName, serviceID);
return worker.isRequestValidationActivated();
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
protected List<Schema> getRequestValidationSchema(final String serviceID) {
if (serviceID != null && WSEngine.serviceInstanceExist(serviceName, serviceID)) {
final W worker = (W) WSEngine.getInstance(serviceName, serviceID);
return worker.getRequestValidationSchema();
}
return new ArrayList<>();
}
/**
* {@inheritDoc}
*/
@Override
public Response treatIncomingRequest(final Object request) {
try {
processAuthentication();
} catch (UnknownAccountException ex) {
LOGGER.log(Level.FINER, "Unknow acount", ex);
SecurityManagerHolder.getInstance().logout();
return Response.status(Response.Status.UNAUTHORIZED).build();
} catch (IncorrectCredentialsException ex) {
LOGGER.log(Level.FINER, "incorrect password", ex);
SecurityManagerHolder.getInstance().logout();
return Response.status(Response.Status.UNAUTHORIZED).build();
}
final Object objectRequest;
if (request instanceof JAXBElement) {
objectRequest = ((JAXBElement) request).getValue();
LOGGER.log(Level.FINER, "request type:{0}", request.getClass().getName());
} else {
objectRequest = request;
}
final String serviceID = getSafeParameter("serviceId");
// request is send to the specified worker
if (serviceID != null && WSEngine.serviceInstanceExist(serviceName, serviceID)) {
final W worker = (W) WSEngine.getInstance(serviceName, serviceID);
if (worker.isSecured()) {
final String ip = getHttpServletRequest().getRemoteAddr();
final String referer = httpHeaders.getHeaderString("referer");
if (!worker.isAuthorized(ip, referer)) {
LOGGER.log(Level.INFO, "Received a request from unauthorized ip:{0} or referer:{1}",
new String[]{ip, referer});
return Response.status(Response.Status.UNAUTHORIZED).build();
}
}
if (worker.isPostRequestLog()) {
logPostParameters(request);
}
if (worker.isPrintRequestParameter()) {
logParameters();
}
worker.setServiceUrl(getServiceURL());
return treatIncomingRequest(objectRequest, worker);
// unbounded URL
} else {
LOGGER.log(Level.WARNING, "Received request on undefined instance identifier:{0}", serviceID);
return Response.status(Response.Status.NOT_FOUND).build();
}
}
private void processAuthentication() throws UnknownAccountException, IncorrectCredentialsException{
if (httpHeaders != null) {
final String authorization = httpHeaders.getHeaderString("authorization");
if (authorization != null) {
if (authorization.startsWith("Basic ")) {
final String toDecode = authorization.substring(6);
try {
final String logPass = new String(Base64.decode(toDecode));
final int separatorIndex = logPass.indexOf(":");
if (separatorIndex != -1) {
final String login = logPass.substring(0, separatorIndex);
final String passw = logPass.substring(separatorIndex + 1);
SecurityManagerHolder.getInstance().login(login, passw);
} else {
LOGGER.warning("separator missing in authorization header");
}
} catch (IOException ex) {
LOGGER.log(Level.WARNING, "IO exception while cdecoding basic authentication", ex);
}
} else {
LOGGER.info("only basic authorization are handled for now");
}
}
}
}
/**
* Service specific task which has to be executed when a restart is asked.
*
* @param identifier the instance identifier or {@code null} for all the instance.
*/
protected void specificRestart(final String identifier) {
// do nothing in this implementation
}
/**
* 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 Response treatIncomingRequest(final Object objectRequest,final W worker);
/**
* Handle all exceptions returned by a web service operation in two ways:
* <ul>
* <li>if the exception code indicates a mistake done by the user, just display a single
* line message in logs.</li>
* <li>otherwise logs the full stack trace in logs, because it is something interesting for
* a developer</li>
* </ul>
* In both ways, the exception is then marshalled and returned to the client.
*
* @param ex The exception that has been generated during the web-service operation requested.
* @param serviceDef The service definition, from which the version number of exception report will
* be extracted.
* @return An XML representing the exception.
*/
protected abstract Response processExceptionResponse(final CstlServiceException ex, final ServiceDef serviceDef, final Worker w);
/**
* The shared method to build a service ExceptionReport.
*
* @param message
* @param codeName
* @return
*/
@Override
protected Response launchException(final String message, String codeName, final String locator) {
final String serviceID = getSafeParameter("serviceId");
final W worker = (W) WSEngine.getInstance(serviceName, serviceID);
ServiceDef mainVersion = null;
if (worker != null) {
mainVersion = worker.getBestVersion(null);
if (mainVersion.owsCompliant) {
codeName = StringUtilities.transformCodeName(codeName);
}
}
final OWSExceptionCode code = Types.forCodeName(OWSExceptionCode.class, codeName, true);
final CstlServiceException ex = new CstlServiceException(message, code, locator);
return processExceptionResponse(ex, mainVersion, worker);
}
/**
* Return the correct representation of an OWS exceptionCode
*
* @param exceptionCode
* @return
*/
protected String getOWSExceptionCodeRepresentation(final CodeList exceptionCode) {
final String codeRepresentation;
if (exceptionCode instanceof org.constellation.ws.ExceptionCode) {
codeRepresentation = StringUtilities.transformCodeName(exceptionCode.name());
} else {
codeRepresentation = exceptionCode.name();
}
return codeRepresentation;
}
/**
* We don't print the stack trace:
* - if the user have forget a mandatory parameter.
* - if the version number is wrong.
* - if the user have send a wrong request parameter
*/
protected void logException(final CstlServiceException ex) {
if (!ex.getExceptionCode().equals(MISSING_PARAMETER_VALUE) && !ex.getExceptionCode().equals(org.constellation.ws.ExceptionCode.MISSING_PARAMETER_VALUE) &&
!ex.getExceptionCode().equals(VERSION_NEGOTIATION_FAILED) && !ex.getExceptionCode().equals(org.constellation.ws.ExceptionCode.VERSION_NEGOTIATION_FAILED) &&
!ex.getExceptionCode().equals(INVALID_PARAMETER_VALUE) && !ex.getExceptionCode().equals(org.constellation.ws.ExceptionCode.INVALID_PARAMETER_VALUE) &&
!ex.getExceptionCode().equals(OPERATION_NOT_SUPPORTED) && !ex.getExceptionCode().equals(org.constellation.ws.ExceptionCode.OPERATION_NOT_SUPPORTED) &&
!ex.getExceptionCode().equals(STYLE_NOT_DEFINED) && !ex.getExceptionCode().equals(org.constellation.ws.ExceptionCode.STYLE_NOT_DEFINED) &&
!ex.getExceptionCode().equals(INVALID_POINT) && !ex.getExceptionCode().equals(org.constellation.ws.ExceptionCode.INVALID_POINT) &&
!ex.getExceptionCode().equals(INVALID_FORMAT) && !ex.getExceptionCode().equals(org.constellation.ws.ExceptionCode.INVALID_FORMAT) &&
!ex.getExceptionCode().equals(INVALID_CRS) && !ex.getExceptionCode().equals(org.constellation.ws.ExceptionCode.INVALID_CRS) &&
!ex.getExceptionCode().equals(LAYER_NOT_DEFINED) && !ex.getExceptionCode().equals(org.constellation.ws.ExceptionCode.LAYER_NOT_DEFINED) &&
!ex.getExceptionCode().equals(INVALID_REQUEST) && !ex.getExceptionCode().equals(org.constellation.ws.ExceptionCode.INVALID_REQUEST) &&
!ex.getExceptionCode().equals(INVALID_UPDATE_SEQUENCE) && !ex.getExceptionCode().equals(org.constellation.ws.ExceptionCode.INVALID_UPDATE_SEQUENCE) &&
!ex.getExceptionCode().equals(INVALID_VALUE) &&
!ex.getExceptionCode().equals(org.constellation.ws.ExceptionCode.INVALID_SRS)) {
LOGGER.log(Level.WARNING, ex.getMessage(), ex);
} else {
LOGGER.info("SENDING EXCEPTION: " + ex.getExceptionCode().name() + " " + ex.getMessage() + '\n');
}
}
/**
* Return the number of instance if the web-service
*/
protected int getWorkerMapSize() {
return WSEngine.getInstanceSize(serviceName);
}
/**
* {@inheritDoc}
*/
@PreDestroy
@Override
public void destroy() {
super.destroy();
LOGGER.log(Level.INFO, "Shutting down the REST {0} service facade.", serviceName);
WSEngine.destroyInstances(serviceName);
}
/**
* {@inheritDoc}
*/
@Override
protected MarshallerPool getConfigurationPool() {
return GenericDatabaseMarshallerPool.getInstance();
}
}