/*
* 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.metadata.ws.rs;
// java se dependencies
import org.apache.sis.xml.Namespaces;
import org.constellation.ServiceDef;
import org.constellation.ServiceDef.Specification;
import org.constellation.jaxb.CstlXMLSerializer;
import org.constellation.metadata.CSWworker;
import org.constellation.metadata.configuration.CSWConfigurer;
import org.constellation.metadata.utils.CSWUtils;
import org.constellation.metadata.utils.SerializerResponse;
import org.constellation.ws.CstlServiceException;
import org.constellation.ws.MimeType;
import org.constellation.ws.ServiceConfigurer;
import org.constellation.ws.UnauthorizedException;
import org.constellation.ws.WebServiceUtilities;
import org.constellation.ws.Worker;
import org.constellation.ws.rs.OGCWebService;
import org.geotoolkit.csw.xml.CSWResponse;
import org.geotoolkit.csw.xml.CswXmlFactory;
import org.geotoolkit.csw.xml.DescribeRecord;
import org.geotoolkit.csw.xml.DistributedSearch;
import org.geotoolkit.csw.xml.ElementSetName;
import org.geotoolkit.csw.xml.ElementSetType;
import org.geotoolkit.csw.xml.GetCapabilities;
import org.geotoolkit.csw.xml.GetDomain;
import org.geotoolkit.csw.xml.GetRecordById;
import org.geotoolkit.csw.xml.GetRecordsRequest;
import org.geotoolkit.csw.xml.Harvest;
import org.geotoolkit.csw.xml.Query;
import org.geotoolkit.csw.xml.QueryConstraint;
import org.geotoolkit.csw.xml.ResultType;
import org.geotoolkit.csw.xml.Transaction;
import org.geotoolkit.ebrim.xml.EBRIMMarshallerPool;
import org.geotoolkit.ogc.xml.v110.FilterType;
import org.geotoolkit.ogc.xml.v110.SortByType;
import org.geotoolkit.ogc.xml.v110.SortOrderType;
import org.geotoolkit.ogc.xml.v110.SortPropertyType;
import org.geotoolkit.ows.xml.AcceptFormats;
import org.geotoolkit.ows.xml.AcceptVersions;
import org.geotoolkit.ows.xml.RequestBase;
import org.geotoolkit.ows.xml.Sections;
import org.geotoolkit.ows.xml.v100.ExceptionReport;
import org.geotoolkit.ows.xml.v100.SectionsType;
import javax.inject.Singleton;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.Duration;
import javax.xml.namespace.QName;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import static org.constellation.api.QueryConstants.ACCEPT_FORMATS_PARAMETER;
import static org.constellation.api.QueryConstants.ACCEPT_VERSIONS_PARAMETER;
import static org.constellation.api.QueryConstants.SECTIONS_PARAMETER;
import static org.constellation.api.QueryConstants.SERVICE_PARAMETER;
import static org.constellation.api.QueryConstants.UPDATESEQUENCE_PARAMETER;
import static org.constellation.api.QueryConstants.VERSION_PARAMETER;
import static org.constellation.metadata.CSWConstants.MALFORMED;
import static org.constellation.metadata.CSWConstants.NAMESPACE;
import static org.constellation.metadata.CSWConstants.NOT_EXIST;
import static org.geotoolkit.ows.xml.OWSExceptionCode.INVALID_PARAMETER_VALUE;
import static org.geotoolkit.ows.xml.OWSExceptionCode.OPERATION_NOT_SUPPORTED;
// Geotoolkit dependencies
/**
* RestFul CSW service.
*
* @author Guilhem Legal
* @author Benjamin Garcia (Geomatys)
*
* @version 0.9
*/
@Path("csw/{serviceId}")
@Singleton
public class CSWService extends OGCWebService<CSWworker> {
/**
* Build a new Restful CSW service.
*/
public CSWService() {
super(Specification.CSW);
setXMLContext(EBRIMMarshallerPool.getInstance());
LOGGER.log(Level.INFO, "CSW REST service running ({0} instances)", getWorkerMapSize());
}
/**
* {@inheritDoc}
*/
@Override
protected Class getWorkerClass() {
return CSWworker.class;
}
/**
* {@inheritDoc}
*/
@Override
protected Class<? extends ServiceConfigurer> getConfigurerClass() {
return CSWConfigurer.class;
}
/**
* This method has to be overridden by child classes.
*
* @return
*/
protected CstlXMLSerializer getXMLSerializer() {
return null;
}
/**
* {@inheritDoc}
*/
@Override
protected Response treatIncomingRequest(final Object objectRequest, final CSWworker worker) {
ServiceDef serviceDef = null;
try {
// if the request is not an xml request we fill the request parameter.
final RequestBase request;
if (objectRequest == null) {
request = adaptQuery(getParameter("REQUEST", true), worker);
} else if (objectRequest instanceof RequestBase) {
request = (RequestBase) objectRequest;
} else {
throw new CstlServiceException("The operation " + objectRequest.getClass().getName() + " is not supported by the service",
INVALID_PARAMETER_VALUE, "request");
}
serviceDef = worker.getVersionFromNumber(request.getVersion());
if (request instanceof GetCapabilities) {
final GetCapabilities gc = (GetCapabilities) request;
final String outputFormat = MimeType.APPLICATION_XML; // TODO
return Response.ok(worker.getCapabilities(gc), outputFormat).build();
}
if (request instanceof GetRecordsRequest) {
final GetRecordsRequest gr = (GetRecordsRequest)request;
final String outputFormat = CSWUtils.getOutputFormat(gr);
// we pass the serializer to the messageBodyWriter
final SerializerResponse response = new SerializerResponse((CSWResponse) worker.getRecords(gr), getXMLSerializer());
return Response.ok(response, outputFormat).build();
}
if (request instanceof GetRecordById) {
final GetRecordById grbi = (GetRecordById)request;
final String outputFormat = CSWUtils.getOutputFormat(grbi);
// we pass the serializer to the messageBodyWriter
final SerializerResponse response = new SerializerResponse((CSWResponse) worker.getRecordById(grbi), getXMLSerializer());
return Response.ok(response, outputFormat).build();
}
if (request instanceof DescribeRecord) {
final DescribeRecord dr = (DescribeRecord)request;
final String outputFormat = CSWUtils.getOutputFormat(dr);
return Response.ok(worker.describeRecord(dr), outputFormat).build();
}
if (request instanceof GetDomain) {
final GetDomain gd = (GetDomain)request;
final String outputFormat = CSWUtils.getOutputFormat(gd);
return Response.ok(worker.getDomain(gd), outputFormat).build();
}
if (request instanceof Transaction) {
final Transaction tr = (Transaction)request;
final String outputFormat = CSWUtils.getOutputFormat(tr);
return Response.ok(worker.transaction(tr), outputFormat).build();
}
if (request instanceof Harvest) {
final Harvest hv = (Harvest)request;
final String outputFormat = CSWUtils.getOutputFormat(hv);
return Response.ok(worker.harvest(hv), outputFormat).build();
}
throw new CstlServiceException("The operation " + request.getClass().getName() + " is not supported by the service",
INVALID_PARAMETER_VALUE, "request");
} catch (CstlServiceException ex) {
return processExceptionResponse(ex, serviceDef, worker);
}
}
/**
* {@inheritDoc}
*/
@Override
protected Response processExceptionResponse(final CstlServiceException ex, ServiceDef serviceDef, final Worker w) {
// asking for authentication
if (ex instanceof UnauthorizedException) {
return Response.status(Status.UNAUTHORIZED).header("WWW-Authenticate", " Basic").build();
}
logException(ex);
if (serviceDef == null) {
serviceDef = w.getBestVersion(null);
}
final String version = serviceDef.exceptionVersion.toString();
final String code = getOWSExceptionCodeRepresentation(ex.getExceptionCode());
final ExceptionReport report = new ExceptionReport(ex.getMessage(), code, ex.getLocator(), version);
return Response.ok(report, MimeType.TEXT_XML).build();
}
/**
* Build request object from KVP parameters.
*
* @param request
* @return
* @throws CstlServiceException
*/
private RequestBase adaptQuery(final String request, final Worker w) throws CstlServiceException {
if ("GetCapabilities".equalsIgnoreCase(request)) {
return createNewGetCapabilitiesRequest(w);
} else if ("GetRecords".equalsIgnoreCase(request)) {
return createNewGetRecordsRequest();
} else if ("GetRecordById".equalsIgnoreCase(request)) {
return createNewGetRecordByIdRequest();
} else if ("DescribeRecord".equalsIgnoreCase(request)) {
return createNewDescribeRecordRequest();
} else if ("GetDomain".equalsIgnoreCase(request)) {
return createNewGetDomainRequest();
} else if ("Transaction".equalsIgnoreCase(request)) {
throw new CstlServiceException("The Operation transaction is not available in KVP", OPERATION_NOT_SUPPORTED, "transaction");
} else if ("Harvest".equalsIgnoreCase(request)) {
return createNewHarvestRequest();
}
throw new CstlServiceException("The operation " + request + " is not supported by the service",
INVALID_PARAMETER_VALUE, "request");
}
/**
* Build a new GetCapabilities request object with the url parameters
*/
private GetCapabilities createNewGetCapabilitiesRequest(final Worker w) throws CstlServiceException {
final String service = getParameter(SERVICE_PARAMETER, true);
String acceptVersion = getParameter(ACCEPT_VERSIONS_PARAMETER, false);
final AcceptVersions versions;
final String version;
if (acceptVersion != null) {
if (acceptVersion.indexOf(',') != -1) {
acceptVersion = acceptVersion.substring(0, acceptVersion.indexOf(','));
}
version = acceptVersion;
w.checkVersionSupported(version, true);
versions = CswXmlFactory.buildAcceptVersion(version, Arrays.asList(acceptVersion));
} else {
version = "2.0.2";
versions = CswXmlFactory.buildAcceptVersion(version, Arrays.asList("2.0.2"));
}
final AcceptFormats formats = CswXmlFactory.buildAcceptFormat(version, Arrays.asList(getParameter(ACCEPT_FORMATS_PARAMETER, false)));
final String updateSequence = getParameter(UPDATESEQUENCE_PARAMETER, false);
//We transform the String of sections in a list.
//In the same time we verify that the requested sections are valid.
final String section = getParameter(SECTIONS_PARAMETER, false);
List<String> requestedSections = new ArrayList<>();
if (section != null && !section.equalsIgnoreCase("All")) {
final StringTokenizer tokens = new StringTokenizer(section, ",;");
while (tokens.hasMoreTokens()) {
final String token = tokens.nextToken().trim();
if (SectionsType.getExistingSections().contains(token)){
requestedSections.add(token);
} else {
throw new CstlServiceException("The section " + token + NOT_EXIST,
INVALID_PARAMETER_VALUE, "Sections");
}
}
} else {
//if there is no requested Sections we add all the sections
requestedSections = SectionsType.getExistingSections();
}
final Sections sections = CswXmlFactory.buildSections(version, requestedSections);
return CswXmlFactory.createGetCapabilities(version, versions, sections, formats, updateSequence, service);
}
/**
* Build a new GetRecords request object with the url parameters
*/
private GetRecordsRequest createNewGetRecordsRequest() throws CstlServiceException {
final String version = getParameter(VERSION_PARAMETER, true);
final String service = getParameter(SERVICE_PARAMETER, true);
//we get the value of result type, if not set we put default value "HITS"
final String resultTypeName = getParameter("RESULTTYPE", false);
ResultType resultType = ResultType.HITS;
if (resultTypeName != null) {
try {
resultType = ResultType.fromValue(resultTypeName);
} catch (IllegalArgumentException e){
throw new CstlServiceException("The resultType " + resultTypeName + NOT_EXIST,
INVALID_PARAMETER_VALUE, "ResultType");
}
}
final String requestID = getParameter("REQUESTID", false);
String outputFormat = getParameter("OUTPUTFORMAT", false);
if (outputFormat == null) {
outputFormat = MimeType.APPLICATION_XML;
}
String outputSchema = getParameter("OUTPUTSCHEMA", false);
if (outputSchema == null) {
outputSchema = Namespaces.CSW;
}
//we get the value of start position, if not set we put default value "1"
final String startPos = getParameter("STARTPOSITION", false);
Integer startPosition = Integer.valueOf("1");
if (startPos != null) {
try {
startPosition = Integer.valueOf(startPos);
} catch (NumberFormatException e){
throw new CstlServiceException("The positif integer " + startPos + MALFORMED,
INVALID_PARAMETER_VALUE, "startPosition");
}
}
//we get the value of max record, if not set we put default value "10"
final String maxRec = getParameter("MAXRECORDS", false);
Integer maxRecords= Integer.valueOf("10");
if (maxRec != null) {
try {
maxRecords = Integer.valueOf(maxRec);
} catch (NumberFormatException e){
throw new CstlServiceException("The positif integer " + maxRec + MALFORMED,
INVALID_PARAMETER_VALUE, "maxRecords");
}
}
/*
* here we build the "Query" object
*/
// we get the namespaces.
final String namespace = getParameter("NAMESPACE", false);
final Map<String, String> namespaces = WebServiceUtilities.extractNamespace(namespace);
//if there is not namespace specified, using the default namespace
if (namespaces.isEmpty()) {
namespaces.put("csw", Namespaces.CSW);
namespaces.put("gmd", Namespaces.GMD);
}
final String names = getParameter("TYPENAMES", true);
final List<QName> typeNames = new ArrayList<>();
StringTokenizer tokens = new StringTokenizer(names, ",;");
while (tokens.hasMoreTokens()) {
final String token = tokens.nextToken().trim();
if (token.indexOf(':') != -1) {
final String prefix = token.substring(0, token.indexOf(':'));
final String localPart = token.substring(token.indexOf(':') + 1);
typeNames.add(new QName(namespaces.get(prefix), localPart, prefix));
} else {
throw new CstlServiceException("The QName " + token + MALFORMED,
INVALID_PARAMETER_VALUE, NAMESPACE);
}
}
final String eSetName = getParameter("ELEMENTSETNAME", false);
ElementSetType elementSet = ElementSetType.SUMMARY;
if (eSetName != null) {
try {
elementSet = ElementSetType.fromValue(eSetName);
} catch (IllegalArgumentException e){
throw new CstlServiceException("The ElementSet Name " + eSetName + NOT_EXIST,
INVALID_PARAMETER_VALUE, "ElementSetName");
}
}
final ElementSetName setName = CswXmlFactory.createElementSetName(version, elementSet);
//we get the list of sort by object
final String sort = getParameter("SORTBY", false);
final List<SortPropertyType> sorts = new ArrayList<>();
SortByType sortBy = null;
if (sort != null) {
tokens = new StringTokenizer(sort, ",;");
while (tokens.hasMoreTokens()) {
final String token = tokens.nextToken().trim();
if (token.indexOf(':') != -1) {
final String propName = token.substring(0, token.indexOf(':'));
final String order = token.substring(token.indexOf(':') + 1);
SortOrderType orderType;
try {
orderType = SortOrderType.fromValue(order);
} catch (IllegalArgumentException e){
throw new CstlServiceException("The SortOrder Name " + order + NOT_EXIST,
INVALID_PARAMETER_VALUE, "SortBy");
}
sorts.add(new SortPropertyType(propName, orderType));
} else {
throw new CstlServiceException("The expression " + token + MALFORMED,
INVALID_PARAMETER_VALUE, "SortBy");
}
}
sortBy = new SortByType(sorts);
}
/*
* here we build the constraint object
*/
final String constLanguage = getParameter("CONSTRAINTLANGUAGE", false);
QueryConstraint constraint = null;
if (constLanguage != null) {
final String languageVersion = getParameter("CONSTRAINT_LANGUAGE_VERSION", true);
if (constLanguage.equalsIgnoreCase("CQL_TEXT")) {
String constraintObject = getParameter("CONSTRAINT", false);
if (constraintObject == null) {
constraintObject = "AnyText LIKE '%%'";
}
constraint = CswXmlFactory.createQueryConstraint(version, constraintObject, languageVersion);
} else if (constLanguage.equalsIgnoreCase("FILTER")) {
final Object constraintObject = getComplexParameter("CONSTRAINT", false);
if (constraintObject == null) {
// do nothing
} else if (constraintObject instanceof FilterType){
constraint = CswXmlFactory.createQueryConstraint(version, (FilterType)constraintObject, languageVersion);
} else {
throw new CstlServiceException("The filter type is not supported:" + constraintObject.getClass().getName(),
INVALID_PARAMETER_VALUE, "Constraint");
}
} else {
throw new CstlServiceException("The constraint language " + constLanguage + " is not supported",
INVALID_PARAMETER_VALUE, "ConstraintLanguage");
}
}
final Query query = CswXmlFactory.createQuery(version, typeNames, setName, sortBy, constraint);
/*
* here we build a optionnal ditributed search object
*/
final String distrib = getParameter("DISTRIBUTEDSEARCH", false);
DistributedSearch distribSearch = null;
if (distrib != null && distrib.equalsIgnoreCase("true")) {
final String count = getParameter("HOPCOUNT", false);
Integer hopCount = 2;
if (count != null) {
try {
hopCount = Integer.parseInt(count);
} catch (NumberFormatException e){
throw new CstlServiceException("The positif integer " + count + MALFORMED,
INVALID_PARAMETER_VALUE, "HopCount");
}
}
distribSearch = CswXmlFactory.createDistributedSearch(version, hopCount);
}
// TODO not implemented yet
// String handler = getParameter("RESPONSEHANDLER", false);
return CswXmlFactory.createGetRecord(version, service, resultType, requestID, outputFormat, outputSchema, startPosition, maxRecords, query, distribSearch);
}
/**
* Build a new GetRecordById request object with the url parameters
*/
private GetRecordById createNewGetRecordByIdRequest() throws CstlServiceException {
final String version = getParameter(VERSION_PARAMETER, true);
final String service = getParameter(SERVICE_PARAMETER, true);
String eSetName = getParameter("ELEMENTSETNAME", false);
ElementSetType elementSet = ElementSetType.SUMMARY;
if (eSetName != null) {
try {
eSetName = eSetName.toLowerCase();
elementSet = ElementSetType.fromValue(eSetName);
} catch (IllegalArgumentException e){
throw new CstlServiceException("The ElementSet Name " + eSetName + NOT_EXIST,
INVALID_PARAMETER_VALUE, "ElementSetName");
}
}
final ElementSetName setName = CswXmlFactory.createElementSetName(version, elementSet);
String outputFormat = getParameter("OUTPUTFORMAT", false);
if (outputFormat == null) {
outputFormat = MimeType.APPLICATION_XML;
}
String outputSchema = getParameter("OUTPUTSCHEMA", false);
if (outputSchema == null) {
outputSchema = Namespaces.CSW;
}
final String ids = getParameter("ID", true);
final List<String> id = new ArrayList<>();
final StringTokenizer tokens = new StringTokenizer(ids, ",;");
while (tokens.hasMoreTokens()) {
final String token = tokens.nextToken().trim();
id.add(token);
}
return CswXmlFactory.createGetRecordById(version, service, setName, outputFormat, outputSchema, id);
}
/**
* Build a new DescribeRecord request object with the url parameters
*/
private DescribeRecord createNewDescribeRecordRequest() throws CstlServiceException {
final String version = getParameter(VERSION_PARAMETER, true);
final String service = getParameter(SERVICE_PARAMETER, true);
String outputFormat = getParameter("OUTPUTFORMAT", false);
if (outputFormat == null) {
outputFormat = MimeType.APPLICATION_XML;
}
String schemaLanguage = getParameter("SCHEMALANGUAGE", false);
if (schemaLanguage == null) {
schemaLanguage = "XMLSCHEMA";
}
// we get the namespaces.
final String namespace = getParameter("NAMESPACE", false);
final Map<String, String> namespaces = WebServiceUtilities.extractNamespace(namespace);
//if there is not namespace specified, using the default namespace
// TODO add gmd...
if (namespaces.isEmpty()) {
namespaces.put("csw", Namespaces.CSW);
namespaces.put("gmd", Namespaces.GMD);
}
final List<QName> typeNames = new ArrayList<>();
final String names = getParameter("TYPENAME", false);
if (names != null) {
final StringTokenizer tokens = new StringTokenizer(names, ",;");
while (tokens.hasMoreTokens()) {
final String token = tokens.nextToken().trim();
if (token.indexOf(':') != -1) {
final String prefix = token.substring(0, token.indexOf(':'));
final String localPart = token.substring(token.indexOf(':') + 1);
typeNames.add(new QName(namespaces.get(prefix), localPart));
} else {
throw new CstlServiceException("The QName " + token + MALFORMED,
INVALID_PARAMETER_VALUE, NAMESPACE);
}
}
}
return CswXmlFactory.createDescribeRecord(version, service, typeNames, outputFormat, schemaLanguage);
}
/**
* Build a new GetDomain request object with the url parameters
*/
private GetDomain createNewGetDomainRequest() throws CstlServiceException {
final String version = getParameter(VERSION_PARAMETER, true);
final String service = getParameter(SERVICE_PARAMETER, true);
//not supported by the ISO profile
final String parameterName = getParameter("PARAMETERNAME", false);
final String propertyName = getParameter("PROPERTYNAME", false);
if (propertyName != null && parameterName != null) {
throw new CstlServiceException("One of propertyName or parameterName must be null",
INVALID_PARAMETER_VALUE, "parameterName");
}
return CswXmlFactory.createGetDomain(version, service, propertyName, parameterName);
}
/**
* Build a new GetDomain request object with the url parameters
*/
private Harvest createNewHarvestRequest() throws CstlServiceException {
final String version = getParameter(VERSION_PARAMETER, true);
final String service = getParameter(SERVICE_PARAMETER, true);
final String source = getParameter("SOURCE", true);
final String resourceType = getParameter("RESOURCETYPE", true);
String resourceFormat = getParameter("RESOURCEFORMAT", false);
if (resourceFormat == null) {
resourceFormat = MimeType.APPLICATION_XML;
}
final String handler = getParameter("RESPONSEHANDLER", false);
final String interval = getParameter("HARVESTINTERVAL", false);
Duration harvestInterval = null;
if (interval != null) {
try {
final DatatypeFactory factory = DatatypeFactory.newInstance();
harvestInterval = factory.newDuration(interval) ;
} catch (DatatypeConfigurationException ex) {
throw new CstlServiceException("The Duration " + interval + MALFORMED,
INVALID_PARAMETER_VALUE, "HarvestInsterval");
}
}
return CswXmlFactory.createHarvest(version, service, source, resourceType, resourceFormat, handler, harvestInterval);
}
}