/*
* 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;
//J2SE dependencies
import static org.geotoolkit.ows.xml.OWSExceptionCode.INVALID_PARAMETER_VALUE;
import static org.geotoolkit.ows.xml.OWSExceptionCode.VERSION_NEGOTIATION_FAILED;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.apache.sis.internal.util.UnmodifiableArrayList;
import org.apache.sis.util.Version;
import org.apache.sis.util.logging.Logging;
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.dto.Details;
import org.constellation.database.api.jooq.tables.pojos.Service;
import org.constellation.security.SecurityManagerHolder;
import org.constellation.ws.security.SimplePDP;
import org.geotoolkit.ows.xml.AbstractCapabilitiesCore;
import org.geotoolkit.ows.xml.OWSExceptionCode;
import org.geotoolkit.util.StringUtilities;
import org.opengis.util.CodeList;
import org.xml.sax.SAXException;
/**
* Abstract definition of a {@code Web Map Service} worker called by a facade
* to perform the logic for a particular WMS instance.
*
* @version $Id: AbstractWMSWorker.java 1889 2009-10-14 16:05:52Z eclesia $
*
* @author Cédric Briançon (Geomatys)
* @author Johann Sorel (Geomatys)
* @author Guilhem Legal (Geomatys)
*/
public abstract class AbstractWorker implements Worker {
/**
* The default logger.
*/
protected static final Logger LOGGER = Logging.getLogger("org.constellation.ws");
/**
* A flag indicating if the worker is correctly started.
*/
protected boolean isStarted;
/**
* A message keeping the reason of the start error of the service
*/
protected String startError;
/**
* Contains the service url used in capabilities document.
*/
private String serviceUrl = null;
/**
* The log level off al the informations log.
*/
protected Level logLevel = Level.INFO;
/**
* A map containing the Capabilities Object already loaded from file.
*/
private final Map<String, Details> capabilities = Collections.synchronizedMap(new HashMap<String,Details>());
/**
* Output responses of a GetCapabilities request.
*/
private static final Map<String,AbstractCapabilitiesCore> CAPS_RESPONSE = new HashMap<>();
/**
* The identifier of the worker.
*/
private final String id;
/**
* The specification for this worker.
*/
protected final Specification specification;
protected UnmodifiableArrayList<ServiceDef> supportedVersions;
/**
* use this flag to pass the shiro security when using the worker in a non web context.
*/
protected boolean shiroAccessible = true;
/**
* use this flag to pass the shiro security when using the worker in a non web context.
*/
protected boolean cacheCapabilities = true;
/**
* A Policy Decision Point (PDP) if some security constraints have been defined.
*/
protected SimplePDP pdp = null;
private List<Schema> schemas = null;
private long currentUpdateSequence = System.currentTimeMillis();
@Inject
protected IServiceBusiness serviceBusiness;
public AbstractWorker(final String id, final Specification specification) {
this.id = id;
this.specification = specification;
SpringHelper.injectDependencies(this);
}
/**
* this method initialize the supproted version of the worker.
* Must be called after the configuration Object is set (need to call getProperty).
*
* @throws org.constellation.ws.CstlServiceException if a version in the property "supported_versions" is not supported.
*/
protected void applySupportedVersion() throws CstlServiceException {
final Service service = serviceBusiness.getServiceByIdentifierAndType(specification.name(), id);
if (service != null) {
final List<ServiceDef> definitions = new ArrayList<>();
final StringTokenizer tokenizer = new StringTokenizer(service.getVersions(), "µ");
while (tokenizer.hasMoreTokens()) {
final String version = tokenizer.nextToken();
final ServiceDef def = ServiceDef.getServiceDefinition(specification, version);
if (def == null) {
throw new CstlServiceException("Unable to find a service specification for:" + specification.name() + " version:" + version);
} else {
definitions.add(def);
}
}
setSupportedVersion(definitions);
} else {
setSupportedVersion(ServiceDef.getAllSupportedVersionForSpecification(specification));
}
}
protected String getUserLogin() {
final String userLogin;
if (shiroAccessible) {
userLogin = SecurityManagerHolder.getInstance().getCurrentUserLogin();
} else {
userLogin = null;
}
return userLogin;
}
private void setSupportedVersion(final List<ServiceDef> supportedVersions) {
this.supportedVersions = UnmodifiableArrayList.wrap(supportedVersions.toArray(new ServiceDef[supportedVersions.size()]));
}
protected boolean isSupportedVersion(final String version) {
final ServiceDef.Version vv = new ServiceDef.Version(version);
for (ServiceDef sd : supportedVersions) {
if (sd.version.equals(vv)) {
return true;
}
}
return false;
}
/**
* Verify if the version is supported by this serviceType.
* <p>
* If the version is not accepted we send an exception.
* </p>
*/
@Override
public void checkVersionSupported(final String versionNumber, final boolean getCapabilities) throws CstlServiceException {
if (getVersionFromNumber(versionNumber) == null) {
final StringBuilder messageb = new StringBuilder("The parameter ");
for (ServiceDef vers : supportedVersions) {
messageb.append("VERSION=").append(vers.version.toString()).append(" OR ");
}
messageb.delete(messageb.length()-4, messageb.length()-1);
messageb.append(" must be specified");
final CodeList code;
if (getCapabilities) {
code = VERSION_NEGOTIATION_FAILED;
} else {
code = INVALID_PARAMETER_VALUE;
}
throw new CstlServiceException(messageb.toString(), code, "version");
}
}
/**
* Return a Version Object from the version number.
* if the version number is not correct return the default version.
*
* @param number the version number.
* @return
*/
@Override
public ServiceDef getVersionFromNumber(final Version number) {
if (number != null) {
for (ServiceDef v : supportedVersions) {
if (v.version.toString().equals(number.toString())){
return v;
}
}
}
return null;
}
/**
* Return a Version Object from the version number.
* if the version number is not correct return the default version.
*
* @param number the version number.
* @return
*
*/
@Override
public ServiceDef getVersionFromNumber(final String number) {
for (ServiceDef v : supportedVersions) {
if (v.version.toString().equals(number)){
return v;
}
}
return null;
}
/**
* If the requested version number is not available we choose the best version to return.
*
* @param number A version number, which will be compared to the ones specified.
* Can be {@code null}, in this case the best version specified is just returned.
* @return The best version (the highest one) specified for this web service.
*
*/
@Override
public ServiceDef getBestVersion(final String number) {
for (ServiceDef v : supportedVersions) {
if (v.version.toString().equals(number)){
return v;
}
}
final ServiceDef firstSpecifiedVersion = supportedVersions.get(0);
if (number == null || number.isEmpty()) {
return firstSpecifiedVersion;
}
final ServiceDef.Version wrongVersion = new ServiceDef.Version(number);
if (wrongVersion.compareTo(firstSpecifiedVersion.version) > 0) {
return firstSpecifiedVersion;
} else {
if (wrongVersion.compareTo(supportedVersions.get(supportedVersions.size() - 1).version) < 0) {
return supportedVersions.get(supportedVersions.size() - 1);
}
}
return firstSpecifiedVersion;
}
/**
* {@inheritDoc }
*/
@Override
public void setServiceUrl(final String serviceUrl) {
if (serviceUrl.endsWith("/")) {
this.serviceUrl = serviceUrl + specification.toString().toLowerCase() + '/' + id + '?';
} else {
this.serviceUrl = serviceUrl + '/' + specification.toString().toLowerCase() + '/' + id + '?';
}
}
/**
* return the current service URL.
* @return
*/
@Override
public synchronized String getServiceUrl(){
return serviceUrl;
}
@Override
public String getId() {
return id;
}
/**
* {@inheritDoc }
*/
@Override
public void setLogLevel(final Level logLevel) {
this.logLevel = logLevel;
}
@Override
public void setShiroAccessible(final boolean shiroAccessible) {
this.shiroAccessible = shiroAccessible;
}
protected abstract String getProperty(final String propertyName);
/**
* Returns the file where to read the capabilities document for each service.
* If no such file is found, then this method returns {@code null}.
* This method has a cache system, the object will be read from the file system only one time.
*
* @param service The service type identifier. example "WMS"
* @param language The language of the metadata.
*
* @return The capabilities Object, or {@code null} if none.
*
* @throws CstlServiceException if an error occurs during the unmarshall of the document.
*/
protected Details getStaticCapabilitiesObject(final String service, final String language) throws CstlServiceException {
final String key;
if (language == null) {
key = getId() + service;
} else {
key = getId() + service + "-" + language;
}
Details details = capabilities.get(key);
if (details == null) {
try {
details = serviceBusiness.getInstanceDetails(service, getId(), language);
capabilities.put(key, details);
} catch (ConfigurationException ex) {
LOGGER.log(Level.WARNING, "An error occurred when trying to read the service metadata. Returning default capabilities.", ex);
}
}
return details;
}
/**
* Return the marshaller pool used to unmarshaller the capabilities documents of the service.
*
* @return the marshaller pool used to unmarshaller the capabilities documents of the service.
*/
protected abstract MarshallerPool getMarshallerPool();
/**
* Throw and exception if the service is not working
*
* @throws org.constellation.ws.CstlServiceException
*/
protected void isWorking() throws CstlServiceException {
if (!isStarted) {
throw new CstlServiceException("The service is not running!\nCause:" + startError, OWSExceptionCode.NO_APPLICABLE_CODE);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean isStarted() {
return isStarted;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isPostRequestLog() {
final String value = getProperty("postRequestLog");
if (value != null) {
return Boolean.parseBoolean(value);
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isPrintRequestParameter() {
final String value = getProperty("printRequestParameter");
if (value != null) {
return Boolean.parseBoolean(value);
}
return true;
}
/**
* A flag indicating if the transaction methods of the worker are securized.
*/
protected boolean isTransactionSecurized() {
final String value = getProperty("transactionSecurized");
if (value != null) {
return Boolean.parseBoolean(value);
}
return true;
}
@Override
public boolean isRequestValidationActivated() {
final String value = getProperty("requestValidationActivated");
if (value != null) {
return Boolean.parseBoolean(value);
}
return false;
}
@Override
public List<Schema> getRequestValidationSchema() {
if (schemas == null) {
final String value = getProperty("requestValidationSchema");
schemas = new ArrayList<>();
if (value != null) {
final List<String> schemaPaths = StringUtilities.toStringList(value);
LOGGER.info("Reading schemas. This may take some times ...");
final SchemaFactory sf = SchemaFactory.newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI);
for (String schemaPath : schemaPaths) {
LOGGER.log(Level.INFO, "Reading {0}", schemaPath);
try {
schemas.add(sf.newSchema(new URL(schemaPath)));
} catch (SAXException ex) {
LOGGER.warning("SAX exception while adding the Validator to the JAXB unmarshaller");
} catch (MalformedURLException ex) {
LOGGER.warning("MalformedURL exception while adding the Validator to the JAXB unmarshaller");
}
}
}
}
return schemas;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isAuthorized(final String ip, final String referer) {
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isSecured() {
return false;
}
/**
* @return the currentUpdateSequence
*/
protected String getCurrentUpdateSequence() {
return Long.toString(currentUpdateSequence);
}
/**
* Set the current date to the updateSequence parameter
*/
protected void refreshUpdateSequence() {
currentUpdateSequence = System.currentTimeMillis();
}
protected boolean returnUpdateSequenceDocument(final String updateSequence) throws CstlServiceException {
if (updateSequence == null) {
return false;
}
try {
final long sequenceNumber = Long.parseLong(updateSequence);
if (sequenceNumber == currentUpdateSequence) {
return true;
} else if (sequenceNumber > currentUpdateSequence) {
throw new CstlServiceException("The update sequence parameter is invalid (higher value than the current)", OWSExceptionCode.INVALID_UPDATE_SEQUENCE, "updateSequence");
}
return false;
} catch(NumberFormatException ex) {
throw new CstlServiceException("The update sequence must be an integer", ex, OWSExceptionCode.INVALID_PARAMETER_VALUE, "updateSequence");
}
}
/**
* Return a cached capabilities response.
*
* @param version
* @return r
*/
protected AbstractCapabilitiesCore getCapabilitiesFromCache(final String version, final String language) {
final String keyCache = specification.name() + '-' + id + '-' + version + '-' + language;
AbstractCapabilitiesCore cachedCapabilities = CAPS_RESPONSE.get(keyCache);
if (cachedCapabilities != null) {
cachedCapabilities.updateURL(getServiceUrl());
}
return cachedCapabilities;
}
/**
* Add the capabilities object to the cache.
*
* @param version
* @param language
* @param capabilities
*/
protected void putCapabilitiesInCache(final String version, final String language, final AbstractCapabilitiesCore capabilities) {
if (cacheCapabilities) {
final String keyCache = specification.name() + '-' + id + '-' + version + '-' + language;
CAPS_RESPONSE.put(keyCache, capabilities);
}
}
protected void clearCapabilitiesCache() {
final List<String> toClear = new ArrayList<>();
for (String key: CAPS_RESPONSE.keySet()) {
if (key.startsWith(specification.name() + '-')) {
toClear.add(key);
}
}
for (String key : toClear) {
CAPS_RESPONSE.remove(key);
}
}
@Override
public void destroy() {
clearCapabilitiesCache();
}
@Override
public Object getConfiguration() {
return null;
}
}