/* * 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.wps.utils; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.logging.Logging; import org.apache.sis.xml.MarshallerPool; import org.constellation.ws.CstlServiceException; import org.geotoolkit.feature.type.FeatureType; import org.geotoolkit.feature.xml.jaxb.JAXBFeatureTypeWriter; import org.geotoolkit.ows.xml.v110.CodeType; import org.geotoolkit.ows.xml.v110.DomainMetadataType; import org.geotoolkit.ows.xml.v110.LanguageStringType; import org.geotoolkit.process.ProcessDescriptor; import org.geotoolkit.process.ProcessFinder; import org.geotoolkit.wps.io.WPSIO; import org.geotoolkit.wps.xml.WPSMarshallerPool; import org.geotoolkit.wps.xml.v100.ComplexDataCombinationType; import org.geotoolkit.wps.xml.v100.ComplexDataCombinationsType; import org.geotoolkit.wps.xml.v100.ComplexDataDescriptionType; import org.geotoolkit.wps.xml.v100.DataInputsType; import org.geotoolkit.wps.xml.v100.DocumentOutputDefinitionType; import org.geotoolkit.wps.xml.v100.Execute; import org.geotoolkit.wps.xml.v100.InputType; import org.geotoolkit.wps.xml.v100.ProcessBriefType; import org.geotoolkit.wps.xml.v100.ResponseFormType; import org.geotoolkit.wps.xml.v100.SupportedComplexDataInputType; import org.geotoolkit.wps.xml.v100.SupportedUOMsType; import org.geotoolkit.wps.xml.v100.UOMsType; import org.geotoolkit.xsd.xml.v2001.Schema; import org.geotoolkit.xsd.xml.v2001.XSDMarshallerPool; import org.opengis.parameter.GeneralParameterDescriptor; import org.opengis.parameter.ParameterDescriptor; import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.util.NoSuchIdentifierException; import javax.measure.unit.NonSI; import javax.measure.unit.SI; import javax.measure.unit.Unit; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.XMLGregorianCalendar; import java.io.File; import java.math.BigInteger; import java.util.ArrayList; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; 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.constellation.wps.ws.WPSConstant.IDENTIFER_PARAMETER; import static org.constellation.wps.ws.WPSConstant.MAX_MB_INPUT_COMPLEX; import static org.constellation.wps.ws.WPSConstant.PROCESS_PREFIX; import static org.constellation.wps.ws.WPSConstant.WPS_1_0_0; import static org.geotoolkit.ows.xml.OWSExceptionCode.INVALID_PARAMETER_VALUE; import static org.geotoolkit.ows.xml.OWSExceptionCode.MISSING_PARAMETER_VALUE; import org.opengis.parameter.ParameterNotFoundException; /** * Set of utilities method used by WPS worker. * * @author Quentin Boileau (Geomatys). */ public class WPSUtils { private static final Logger LOGGER = Logging.getLogger("org.constellation.wps.utils"); private WPSUtils() { } /** * Return the process descriptor from a process identifier * * @param identifier like "urn:ogc:cstl:wps:math:add" * @return ProcessDescriptor * @throws CstlServiceException in case of an unknown process identifier. */ public static ProcessDescriptor getProcessDescriptor(final String identifier) throws CstlServiceException { final String processFactory = extractFactoryFromIdentifier(identifier); final String processName = extractProcessNameFromIdentifier(identifier); ProcessDescriptor processDesc = null; try { processDesc = ProcessFinder.getProcessDescriptor(processFactory, processName); } catch (NoSuchIdentifierException ex) { throw new CstlServiceException("The process " + IDENTIFER_PARAMETER.toLowerCase() + " : " + identifier + " does not exist.", INVALID_PARAMETER_VALUE, IDENTIFER_PARAMETER.toLowerCase()); } return processDesc; } /** * Get the type of the input/output parameter of a given process * @param processIdentifier identifier of the desired process * @param inOutIdentifier identifier of the desired input/output * @return the type of the researched input/output * @throws CstlServiceException when the given process identifier can not be found * @throws ParameterNotFoundException when the input/output identifier can not be found */ public static Class getIOClassFromIdentifier(final String processIdentifier, final String inOutIdentifier) throws CstlServiceException, ParameterNotFoundException { ProcessDescriptor processDescriptor = getProcessDescriptor(processIdentifier); GeneralParameterDescriptor gpd; String ioCode = extractProcessIOCode(inOutIdentifier); // We first try to get a descriptor from the InputDescriptor try { gpd = processDescriptor.getInputDescriptor().descriptor(ioCode); } catch (ParameterNotFoundException ex) { // Then if the InputDescriptor returns nothing we try on the OutputDescriptor gpd = processDescriptor.getOutputDescriptor().descriptor(ioCode); } // If the parameter descriptor is a group // We don't go through the group hierarchy if (gpd instanceof ParameterDescriptorGroup) { ParameterDescriptorGroup pdg = ((ParameterDescriptorGroup)gpd); throw new UnsupportedOperationException("Not implemented yet : the desired input/output is a group"); } else { // It should be a parameter descriptor assert gpd instanceof ParameterDescriptor; ParameterDescriptor paramDescriptor = (ParameterDescriptor)gpd; return paramDescriptor.getValueClass(); } } /** * Return a brief description of the process from his process descriptor. * * @param processDesc * @return ProcessBriefType */ public static ProcessBriefType generateProcessBrief(final ProcessDescriptor processDesc) { final ProcessBriefType brief = new ProcessBriefType(); brief.setIdentifier(new CodeType(buildProcessIdentifier(processDesc))); brief.setTitle(buildProcessTitle(processDesc)); brief.setAbstract(capitalizeFirstLetter(processDesc.getProcedureDescription().toString())); brief.setProcessVersion(WPS_1_0_0); brief.setWSDL(null); return brief; } /** * Build OGC URN unique identifier for a process from his process descriptor. * * @param processDesc * @return */ public static LanguageStringType buildProcessTitle(final ProcessDescriptor processDesc) { ArgumentChecks.ensureNonNull("processDesc", processDesc); final String title = capitalizeFirstLetter(processDesc.getIdentifier().getAuthority().getTitle().toString()).getValue() + " : " + capitalizeFirstLetter(processDesc.getIdentifier().getCode()).getValue(); return new LanguageStringType(title); } /** * Build OGC URN unique identifier for a process from his process descriptor. * * @param processDesc * @return */ public static String buildProcessIdentifier(final ProcessDescriptor processDesc) { ArgumentChecks.ensureNonNull("processDesc", processDesc); return PROCESS_PREFIX + processDesc.getIdentifier().getAuthority().getTitle().toString() + ":" + processDesc.getIdentifier().getCode(); } /** * Build layerName * * @param processDesc * @return authority.code */ public static String buildLayerName(final ProcessDescriptor processDesc) { ArgumentChecks.ensureNonNull("processDesc", processDesc); return processDesc.getIdentifier().getAuthority().getTitle().toString() + "." + processDesc.getIdentifier().getCode(); } /** * Extract the factory name from a process identifier. e.g : urn:ogc:geomatys:wps:math:add return math. * * @param identifier * @return factory name. */ public static String extractFactoryFromIdentifier(final String identifier) throws CstlServiceException { ArgumentChecks.ensureNonNull("identifier", identifier); final String factAndProcess = extractFactoryAndProcessNameFromIdentifier(identifier); if (factAndProcess.contains(":")) { return factAndProcess.split(":")[0]; } throw new CstlServiceException("Invalid process identifier. Must be an URN code like " + PROCESS_PREFIX + "factory:process", INVALID_PARAMETER_VALUE, IDENTIFER_PARAMETER.toLowerCase()); } /** * Extract the process name from a process identifier. e.g : urn:ogc:geomatys:wps:math:add return add. * * @param identifier * @return process name. */ public static String extractProcessNameFromIdentifier(final String identifier) throws CstlServiceException { ArgumentChecks.ensureNonNull("identifier", identifier); final String factAndProcess = extractFactoryAndProcessNameFromIdentifier(identifier); if (factAndProcess.contains(":")) { return factAndProcess.split(":")[1]; } throw new CstlServiceException("Invalid process identifier. Must be an URN code like " + PROCESS_PREFIX + "factory:process", INVALID_PARAMETER_VALUE, IDENTIFER_PARAMETER.toLowerCase()); } /** * Extract the factory name and the process name from a process identifier. e.g : urn:ogc:geomatys:wps:math:add * return math:add. * * @param identifier * @return factoryName:processName. */ private static String extractFactoryAndProcessNameFromIdentifier(final String identifier) throws CstlServiceException { if (identifier.contains(PROCESS_PREFIX)) { return identifier.replace(PROCESS_PREFIX, ""); } throw new CstlServiceException("Invalid process identifier. Must be an URN code like " + PROCESS_PREFIX + "factory:process", INVALID_PARAMETER_VALUE, IDENTIFER_PARAMETER.toLowerCase()); } /** * Generate process INPUT/OUPTUT identifiers based on process identifier. e.g : * urn:ogc:cstl:wps:math:add:input:number1, urn:ogc:cstl:wps:math:add:ouput:result * * @param procDesc * @param input * @param ioType * @return processIdentifier:ioType:paramName */ public static String buildProcessIOIdentifiers(final ProcessDescriptor procDesc, final GeneralParameterDescriptor input, final WPSIO.IOType ioType) { if (input != null) { if (ioType.equals(WPSIO.IOType.INPUT)) { return buildProcessIdentifier(procDesc) + ":input:" + input.getName().getCode(); } else { return buildProcessIdentifier(procDesc) + ":output:" + input.getName().getCode(); } } return null; } /** * Extract the process INPUT/OUPTUT code. e.g : urn:ogc:geomatys:wps:math:add:input:number1 will return number1 * * @param identifier Input/Output identifier. * @return string code. */ public static String extractProcessIOCode(final String identifier) { ArgumentChecks.ensureNonNull("identifier", identifier); return identifier.substring(identifier.lastIndexOf(":") + 1, identifier.length()); } /** * Return the given String with the first letter to upper case. * * @param value * @return LanguageStringType */ public static LanguageStringType capitalizeFirstLetter(final String value) { if (value != null && !value.isEmpty()) { final StringBuilder result = new StringBuilder(value); result.replace(0, 1, result.substring(0, 1).toUpperCase()); return new LanguageStringType(result.toString()); } return new LanguageStringType(value); } /** * Generate supported UOM (Units) for a given ParameterDescriptor. If this descriptor have default unit, supported * UOM returned will be all the compatible untis to the default one. * * @param param * @return SupportedUOMsType or null if the parameter does'nt have any default unit. */ static public SupportedUOMsType generateUOMs(final ParameterDescriptor param) { if (param != null && param.getUnit() != null) { final Unit unit = param.getUnit(); final Set<Unit<?>> siUnits = SI.getInstance().getUnits(); final Set<Unit<?>> nonisUnits = NonSI.getInstance().getUnits(); final SupportedUOMsType supportedUOMsType = new SupportedUOMsType(); final SupportedUOMsType.Default defaultUOM = new SupportedUOMsType.Default(); final UOMsType supportedUOM = new UOMsType(); defaultUOM.setUOM(new DomainMetadataType(unit.toString(), null)); for (Unit u : siUnits) { if (unit.isCompatible(u)) { supportedUOM.getUOM().add(new DomainMetadataType(u.toString(), null)); } } for (Unit u : nonisUnits) { if (unit.isCompatible(u)) { supportedUOM.getUOM().add(new DomainMetadataType(u.toString(), null)); } } supportedUOMsType.setDefault(defaultUOM); supportedUOMsType.setSupported(supportedUOM); return supportedUOMsType; } return null; } /** * Test if a process is supported by the WPS. * * @param descriptor * @return true if process is supported, false if is not. */ public static boolean isSupportedProcess(final ProcessDescriptor descriptor) { //Inputs final GeneralParameterDescriptor inputDesc = descriptor.getInputDescriptor(); if(!isSupportedParameter(inputDesc, WPSIO.IOType.INPUT)) { return false; } //Outputs GeneralParameterDescriptor outputDesc = descriptor.getOutputDescriptor(); if(!isSupportedParameter(outputDesc, WPSIO.IOType.OUTPUT)) { return false; } return true; } /** * A function which test if the given parameter can be proceed by the WPS. * @param toTest The descriptor of the parameter to test. * @param type The parameter type (input or output). * @return true if the WPS can work with this parameter, false otherwise. */ public static boolean isSupportedParameter(GeneralParameterDescriptor toTest, WPSIO.IOType type) { boolean isClean = false; if (toTest instanceof ParameterDescriptorGroup) { final List<GeneralParameterDescriptor> descs = ((ParameterDescriptorGroup) toTest).descriptors(); if (descs.isEmpty()) { isClean = true; } else { for (GeneralParameterDescriptor desc : descs) { isClean = isSupportedParameter(desc, type); if (!isClean) { break; } } } } else if (toTest instanceof ParameterDescriptor) { final ParameterDescriptor param = (ParameterDescriptor) toTest; final Class paramClass = param.getValueClass(); isClean = (type.equals(WPSIO.IOType.INPUT)) ? WPSIO.isSupportedInputClass(paramClass) : WPSIO.isSupportedOutputClass(paramClass); } return isClean; } /** * Return the SupportedComplexDataInputType for the given class. * * @param attributeClass The java class to get complex type from. * @param ioType The type of parameter to describe (input or output). * @param type The complex type (complex, reference, etc.). * @return SupportedComplexDataInputType */ public static SupportedComplexDataInputType describeComplex(final Class attributeClass, final WPSIO.IOType ioType, final WPSIO.FormChoice type) { return describeComplex(attributeClass, ioType, type, null); } /** * Return the SupportedComplexDataInputType for the given class. * * @param attributeClass The java class to get complex type from. * @param ioType The type of parameter to describe (input or output). * @param type The complex type (complex, reference, etc.). * @param userData A map containing user's options for type support. * @return SupportedComplexDataInputType */ public static SupportedComplexDataInputType describeComplex(final Class attributeClass, final WPSIO.IOType ioType, final WPSIO.FormChoice type, final Map<String, Object> userData) { final SupportedComplexDataInputType complex = new SupportedComplexDataInputType(); final ComplexDataCombinationsType complexCombs = new ComplexDataCombinationsType(); final ComplexDataCombinationType complexComb = new ComplexDataCombinationType(); List<WPSIO.FormatSupport> infos = null; String schema = null; if (userData != null) { try { infos = (List<WPSIO.FormatSupport>) userData.get(WPSIO.SUPPORTED_FORMATS_KEY); schema = (String) userData.get(WPSIO.SCHEMA_KEY); } catch (Exception e) { LOGGER.log(Level.WARNING, "A parameter type definition can't be read.", e); } } if (infos == null) { infos = WPSIO.getFormats(attributeClass, ioType); } if (infos != null) { for (WPSIO.FormatSupport inputClass : infos) { final ComplexDataDescriptionType complexDesc = new ComplexDataDescriptionType(); complexDesc.setEncoding(inputClass.getEncoding()); //Encoding complexDesc.setMimeType(inputClass.getMimeType()); //Mime complexDesc.setSchema(schema != null ? schema : inputClass.getSchema()); //URL to xsd schema if (inputClass.isDefaultFormat()) { complexComb.setFormat(complexDesc); } complexCombs.getFormat().add(complexDesc); } } complex.setDefault(complexComb); complex.setSupported(complexCombs); //Set MaximumMegabyte only for the complex input descritpion if (ioType == WPSIO.IOType.INPUT) { complex.setMaximumMegabytes(BigInteger.valueOf(MAX_MB_INPUT_COMPLEX)); } return complex; } /** * Check if all requested inputs/outputs are present in the process descriptor. Check also if all mandatory * inputs/outputs are specified. If an non allowed input/output is requested or if a mandatory input/output is * missing, an {@link CstlServiceException CstlServiceException} will be throw. * * @param processDesc * @param request * @throws CstlServiceException if an non allowed input/output is requested or if a mandatory input/output is * missing. */ public static void checkValidInputOuputRequest(final ProcessDescriptor processDesc, final Execute request) throws CstlServiceException { //check inputs final List<String> inputIdentifiers = extractRequestInputIdentifiers(request); final ParameterDescriptorGroup inputDescriptorGroup = processDesc.getInputDescriptor(); final Map<String, Boolean> inputDescMap = descriptorsAsMap(inputDescriptorGroup, processDesc, WPSIO.IOType.INPUT); checkIOIdentifiers(inputDescMap, inputIdentifiers, WPSIO.IOType.INPUT); //check outputs final List<String> outputIdentifiers = extractRequestOutputIdentifiers(request); final ParameterDescriptorGroup outputDescriptorGroup = processDesc.getOutputDescriptor(); final Map<String, Boolean> outputDescMap = descriptorsAsMap(outputDescriptorGroup, processDesc, WPSIO.IOType.OUTPUT); checkIOIdentifiers(outputDescMap, outputIdentifiers, WPSIO.IOType.OUTPUT); } /** * Extract the list of identifiers {@code String} requested in input. * * @param request * @return a list of identifiers */ public static List<String> extractRequestInputIdentifiers(final Execute request) { final List<String> identifiers = new ArrayList<String>(); if (request != null && request.getDataInputs() != null) { final DataInputsType dataInput = request.getDataInputs(); final List<InputType> inputs = dataInput.getInput(); for (final InputType in : inputs) { identifiers.add(in.getIdentifier().getValue()); } } return identifiers; } /** * Extract the list of identifiers {@code String} requested in output. * * @param request * @return a list of identifiers */ public static List<String> extractRequestOutputIdentifiers(final Execute request) { final List<String> identifiers = new ArrayList<String>(); if (request != null && request.getResponseForm() != null) { final ResponseFormType responseForm = request.getResponseForm(); if (responseForm.getRawDataOutput() != null) { identifiers.add(responseForm.getRawDataOutput().getIdentifier().getValue()); } else if (responseForm.getResponseDocument() != null && responseForm.getResponseDocument().getOutput() != null) { final List<DocumentOutputDefinitionType> outputs = responseForm.getResponseDocument().getOutput(); for (final DocumentOutputDefinitionType out : outputs) { identifiers.add(out.getIdentifier().getValue()); } } } return identifiers; } /** * Build a {@code Map} from a {@link ParameterDescriptorGroup ParameterDescriptorGroup}. The map keys are the * parameter identifier as code and the boolean value the mandatory of the parameter. * * @param descGroup * @return all parameters code and there mandatory value as map. */ private static Map<String, Boolean> descriptorsAsMap(final ParameterDescriptorGroup descGroup, final ProcessDescriptor procDesc, final WPSIO.IOType iOType) { final Map<String, Boolean> map = new HashMap<String, Boolean>(); if (descGroup != null && descGroup.descriptors() != null) { final List<GeneralParameterDescriptor> descriptors = descGroup.descriptors(); for (final GeneralParameterDescriptor geneDesc : descriptors) { final String id = buildProcessIOIdentifiers(procDesc, geneDesc, iOType); final boolean required; if (geneDesc instanceof ParameterDescriptor && ((ParameterDescriptor)geneDesc).getDefaultValue() != null) { required = false; } else { required = geneDesc.getMinimumOccurs() > 0; } map.put(id, required); } } return map; } /** * Confronts the process parameters {@code Map} to the list of requested identifiers for an type of IO * (Input,Output). If there is a missing mandatory parameter in the list of requested identifiers, an {@link CstlServiceException CstlServiceException} * will be throw. If an unknown parameter is requested, it also throw an {@link CstlServiceException CstlServiceException} * * @param descMap - {@code Map} contain all parameters with their mandatory attributes for INPUT or OUTPUT. * @param requestIdentifiers - {@code List} of requested identifiers in INPUT or OUTPUT. * @param iotype - {@link WPSIO.IOType type}. * @throws CstlServiceException for missing or unknown parameter. */ private static void checkIOIdentifiers(final Map<String, Boolean> descMap, final List<String> requestIdentifiers, final WPSIO.IOType iotype) throws CstlServiceException { final String type = iotype == WPSIO.IOType.INPUT ? "INPUT" : "OUTPUT"; if (descMap.isEmpty() && !requestIdentifiers.isEmpty()) { throw new CstlServiceException("This process have no inputs.", INVALID_PARAMETER_VALUE, "input"); //process have no input } else { //check for Unknown parameter. for (final String identifier : requestIdentifiers) { if (!descMap.containsKey(identifier)) { throw new CstlServiceException("Unknow " + type + " parameter : " + identifier + ".", INVALID_PARAMETER_VALUE, identifier); } } //check for missing parameters. if (descMap.containsValue(Boolean.TRUE)) { for (Map.Entry<String, Boolean> entry : descMap.entrySet()) { if (entry.getValue() == Boolean.TRUE) { if (!requestIdentifiers.contains(entry.getKey())) { throw new CstlServiceException("Mandatory " + type + " parameter " + entry.getKey() + " is missing.", MISSING_PARAMETER_VALUE, entry.getKey()); } } } } } } /** * Store the given object into a temorary file specified by the given fileName into the temporary folder. The object * to store is marshalled by the {@link WPSMarshallerPool}. If the temporary file already exist he will be * overwrited. * * @param obj object to marshalle and store to a temporary file. * @param fileName temporary file name. * @return */ public static boolean storeResponse(final Object obj, final String folderPath, final String fileName) { ArgumentChecks.ensureNonNull("obj", obj); final MarshallerPool marshallerPool = WPSMarshallerPool.getInstance(); boolean success = false; try { final File outputFile = new File(folderPath, fileName); final Marshaller marshaller = marshallerPool.acquireMarshaller(); marshaller.marshal(obj, outputFile); marshallerPool.recycle(marshaller); success = outputFile.exists(); } catch (JAXBException ex) { LOGGER.log(Level.WARNING, "Error during unmarshalling", ex); } return success; } /** * Return tuple toString mime/encoding/schema. "[mimeType, encoding, schema]". * @param requestedOuptut DocumentOutputDefinitionType * @return tuple string. */ public static String outputDefinitionToString(final DocumentOutputDefinitionType requestedOuptut) { final StringBuilder builder = new StringBuilder(); final String begin = "["; final String end = "]"; final String separator = ", "; builder.append(begin); builder.append("mimeType="); builder.append(requestedOuptut.getMimeType()); builder.append(separator); builder.append("encoding="); builder.append(requestedOuptut.getEncoding()); builder.append(separator); builder.append("schema="); builder.append(requestedOuptut.getSchema()); builder.append(end); return builder.toString(); } /** * A function to retrieve a Feature schema, and store it into the given file * as an xsd. * * @param source The feature to get schema from. * @param destination The file where we want to save our feature schema. * @throws JAXBException If we can't parse / write the schema properly. */ public static void storeFeatureSchema(FeatureType source, File destination) throws JAXBException { JAXBFeatureTypeWriter writer = new JAXBFeatureTypeWriter(); Schema s = writer.getSchemaFromFeatureType(source); MarshallerPool pool = XSDMarshallerPool.getInstance(); Marshaller marsh = pool.acquireMarshaller(); marsh.marshal(s, destination); } /** * @return the current time in an XMLGregorianCalendar. */ public static XMLGregorianCalendar getCurrentXMLGregorianCalendar(){ XMLGregorianCalendar xcal = null; try { final GregorianCalendar c = new GregorianCalendar(); c.setTime(new Date()); xcal = DatatypeFactory.newInstance().newXMLGregorianCalendar(c); } catch (DatatypeConfigurationException ex) { LOGGER.log(Level.INFO, "Can't create the creation time of the status."); } return xcal; } }