/** * Copyright (c) Codice Foundation * * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. * **/ package org.codice.ddf.spatial.ogc.wfs.catalog.endpoint; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import javax.xml.namespace.QName; import org.apache.cxf.common.util.StringUtils; import org.apache.ws.commons.schema.XmlSchema; import org.apache.ws.commons.schema.XmlSchemaCollection; import org.apache.ws.commons.schema.XmlSchemaComplexContent; import org.apache.ws.commons.schema.XmlSchemaComplexContentExtension; import org.apache.ws.commons.schema.XmlSchemaComplexType; import org.apache.ws.commons.schema.XmlSchemaElement; import org.apache.ws.commons.schema.XmlSchemaForm; import org.apache.ws.commons.schema.XmlSchemaImport; import org.apache.ws.commons.schema.XmlSchemaSequence; import org.apache.ws.commons.schema.XmlSchemaSequenceMember; import org.apache.ws.commons.schema.XmlSchemaSerializer; import org.apache.ws.commons.schema.constants.Constants; import org.apache.ws.commons.schema.utils.NamespaceMap; import org.codice.ddf.spatial.ogc.wfs.catalog.common.AttributeDescriptorComparator; import org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsConstants; import org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsQnameBuilder; import org.codice.ddf.spatial.ogc.wfs.catalog.endpoint.utils.ServicePropertiesMap; import org.codice.ddf.spatial.ogc.wfs.v1_0_0.catalog.common.Wfs10Constants; import org.osgi.framework.BundleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ddf.catalog.CatalogFramework; import ddf.catalog.data.AttributeDescriptor; import ddf.catalog.data.AttributeType.AttributeFormat; import ddf.catalog.data.ContentType; import ddf.catalog.data.Metacard; import ddf.catalog.data.MetacardType; import ddf.catalog.data.impl.BasicTypes; import ddf.catalog.operation.SourceInfoResponse; import ddf.catalog.operation.impl.SourceInfoRequestEnterprise; import ddf.catalog.source.SourceDescriptor; import ddf.catalog.source.SourceUnavailableException; /** * Translates MetacardTypes registered in the OSGi Service Registry into an XmlSchema that adheres * to the WFS v1.0.0 specification. These schemas are used in response to a "DescribeFeatureType" * request. */ public class FeatureTypeSchemaCache { // Protected for unit test protected static final QName GML_GEO_PROPERTY_TYPE = new QName(Wfs10Constants.GML_NAMESPACE, "GeometryPropertyType"); private static final Logger LOGGER = LoggerFactory.getLogger(FeatureTypeSchemaCache.class); private static final QName GML_FEATURE = new QName(Wfs10Constants.GML_NAMESPACE, "_Feature"); private static final QName ABSTRACT_FEATURE_TYPE = new QName(Wfs10Constants.GML_NAMESPACE, "AbstractFeatureType"); private Map<MetacardType, Map<String, Object>> services; private CatalogFramework framework; private Map<QName, XmlSchema> contentTypeSchemas = new ConcurrentHashMap<QName, XmlSchema>(); public FeatureTypeSchemaCache(BundleContext context, ServicePropertiesMap services, CatalogFramework ddf) { this.services = services; this.framework = ddf; updateAvailableTypesFromFramework(); } /** * Retrieves a schema based on its {@link QName}. * * @param qname * - the {@link QName} of the type. * @return - the schema that describes the type. */ public XmlSchema getSchemaByQname(final QName qname) { updateAvailableTypesFromFramework(); if (null == qname) { return null; } // If no namespace is provided check against the "localPart" of the // QName if (StringUtils.isEmpty(qname.getNamespaceURI())) { QName foundQname = getQnamefromLocalPart(qname.getLocalPart()); if (null != foundQname) { return contentTypeSchemas.get(foundQname); } } return contentTypeSchemas.get(qname); } /** * Returns a list of {@link QName}s of all the cached types. * * @return - the {@link Set} of {@link QName}s of all the cached types. */ public Set<QName> getFeatureTypeQnames() { updateAvailableTypesFromFramework(); return contentTypeSchemas.keySet(); } public QName getQnamefromLocalPart(String localPart) { QName foundQname = null; for (QName key : contentTypeSchemas.keySet()) { if (key.getLocalPart().equals(localPart)) { if (null == foundQname) { foundQname = key; } else { // If we found this type more than once we don't know // which to return, so // return null return null; } } } return foundQname; } // Called to update the cache of schemas whenever the service registry // changes. private Map<QName, XmlSchema> updateSchemas() { Map<QName, XmlSchema> schemas = new HashMap<QName, XmlSchema>(); List<String> metacardTypes = new ArrayList<String>(); for (Entry<MetacardType, Map<String, Object>> entry : services.entrySet()) { // Get the Content Types this MetacardType supports Object propertyValue = entry.getValue().get(Metacard.CONTENT_TYPE); if (propertyValue instanceof String[]) { MetacardType metacardType = entry.getKey(); final String metacardTypeName = metacardType.getName(); List<String> contentTypes = Arrays.asList((String[]) propertyValue); LOGGER.debug("The CONTENT_TYPE property was set to: {}", Arrays.toString(contentTypes.toArray())); for (String contentTypeName : contentTypes) { // Build the QName to uniquely identify this content type QName qname = WfsQnameBuilder.buildQName(metacardTypeName, contentTypeName); if (null == contentTypeSchemas.get(qname)) { // Add the schema to the map. schemas.put(qname, buildSchemaFromMetacardType(metacardType, qname)); } metacardTypes.add(qname.getPrefix()); } } } // Remove the schemas associated to metacards that no longer exist removeInvalidMetacardTypes(metacardTypes); return schemas; } // Helper method to see if a schema is already registered with this name // TODO - What about duplicates??? private void createSchemaByLocalName(final String typeName) { boolean found = false; for (QName qname : contentTypeSchemas.keySet()) { if (qname.getLocalPart().equals(typeName)) { found = true; } } // If we did not find the schema then we need to create one based on // BASIC_METACARD and add it to the map if (!found) { LOGGER.debug("Creating a new schema from BASIC_METACARD for: {}", typeName); QName newQname = WfsQnameBuilder .buildQName(BasicTypes.BASIC_METACARD.getName(), typeName); XmlSchema schema = buildSchemaFromMetacardType(BasicTypes.BASIC_METACARD, newQname); contentTypeSchemas.put(newQname, schema); } } private void removeInvalidMetacardTypes(List<String> validNames) { Set<QName> qnames = contentTypeSchemas.keySet(); Set<QName> missingTypes = new HashSet<QName>(); for (QName qname : qnames) { if (!qname.getPrefix().startsWith(BasicTypes.BASIC_METACARD.getName())) { if (!validNames.contains(qname.getPrefix())) { missingTypes.add(qname); } } } if (!missingTypes.isEmpty()) { LOGGER.debug("Removing the following from the schema cache: {}", Arrays.toString(missingTypes.toArray())); qnames.removeAll(missingTypes); } } private XmlSchema buildSchemaFromMetacardType(MetacardType metacardType, QName qname) { final String typeName = qname.getLocalPart(); XmlSchema schema = new XmlSchema(qname.getNamespaceURI(), new XmlSchemaCollection()); schema.setElementFormDefault(XmlSchemaForm.QUALIFIED); // Add the GML Namespace NamespaceMap nsMap = new NamespaceMap(); nsMap.add("", XmlSchemaSerializer.XSD_NAMESPACE); nsMap.add(WfsConstants.GML_PREFIX, Wfs10Constants.GML_NAMESPACE); nsMap.add(qname.getPrefix(), qname.getNamespaceURI()); schema.setNamespaceContext(nsMap); // Add the import GML statement so the schema will validate XmlSchemaImport gmlSchemaImport = new XmlSchemaImport(schema); gmlSchemaImport.setNamespace(Wfs10Constants.GML_NAMESPACE); gmlSchemaImport.setSchemaLocation(Wfs10Constants.GML_SCHEMA_LOCATION); schema.getExternals().add(gmlSchemaImport); // Add the metacardType name as the root Element name XmlSchemaElement rootElement = new XmlSchemaElement(schema, true); rootElement.setName(typeName); XmlSchemaSequence rootSequence = new XmlSchemaSequence(); // Add the translated attributes // Set<AttributeDescriptor> attributeDescriptors = new // HashSet<AttributeDescriptor>(); // attributeDescriptors.addAll(metacardType.getAttributeDescriptors()); rootSequence.getItems() .addAll(translateAttributeDescriptors(metacardType.getAttributeDescriptors(), schema)); XmlSchemaComplexContentExtension contentExtension = new XmlSchemaComplexContentExtension(); contentExtension.setBaseTypeName(ABSTRACT_FEATURE_TYPE); contentExtension.setParticle(rootSequence); XmlSchemaComplexType rootComplexType = new XmlSchemaComplexType(schema, true); rootComplexType.setName(typeName + "Type"); XmlSchemaComplexContent complexContent = new XmlSchemaComplexContent(); complexContent.setContent(contentExtension); rootComplexType.setContentModel(complexContent); rootElement.setSchemaTypeName(rootComplexType.getQName()); rootElement.setSubstitutionGroup(GML_FEATURE); // Add the Element to the Schema schema.getElements().put(rootElement.getQName(), rootElement); return schema; } private List<XmlSchemaSequenceMember> translateAttributeDescriptors( Set<AttributeDescriptor> attributeDescriptors, XmlSchema parent) { // Sort the AttributeDescriptors SortedSet<AttributeDescriptor> sortedADs = new TreeSet<AttributeDescriptor>( new AttributeDescriptorComparator()); sortedADs.addAll(attributeDescriptors); List<XmlSchemaSequenceMember> members = new ArrayList<XmlSchemaSequenceMember>(); for (AttributeDescriptor descriptor : sortedADs) { if (null != descriptor && descriptor.isStored()) { QName xsdType = getXsdTypeFromAttributeFormat( descriptor.getType().getAttributeFormat()); if (null != xsdType) { XmlSchemaElement simpleElement = new XmlSchemaElement(parent, false); simpleElement.setName(descriptor.getName()); simpleElement.setSchemaTypeName(xsdType); simpleElement.setMinOccurs(0); if (descriptor.isMultiValued()) { // Unbounded simpleElement.setMaxOccurs(Long.MAX_VALUE); } if (!Metacard.ID.equalsIgnoreCase(descriptor.getName())) { simpleElement.setMinOccurs(0); } members.add(simpleElement); } } } return members; } private QName getXsdTypeFromAttributeFormat(AttributeFormat format) { switch (format) { case STRING: return Constants.XSD_STRING; case XML: return Constants.XSD_ANYTYPE; case BOOLEAN: return Constants.XSD_BOOLEAN; case DATE: return Constants.XSD_DATETIME; case SHORT: return Constants.XSD_SHORT; case INTEGER: return Constants.XSD_INTEGER; case LONG: return Constants.XSD_LONG; case FLOAT: return Constants.XSD_FLOAT; case DOUBLE: return Constants.XSD_DOUBLE; case GEOMETRY: return GML_GEO_PROPERTY_TYPE; case BINARY: return Constants.XSD_BASE64; case OBJECT: default: // No translation can be made. return null; } } private void updateAvailableTypesFromFramework() { List<String> contentTypes = new ArrayList<String>(); SourceInfoResponse response = null; try { // Get info on all the configured Sources response = framework.getSourceInfo(new SourceInfoRequestEnterprise(true)); } catch (SourceUnavailableException e) { // Ignore this. We should never get this exception since we are // doing an enterprise // request. LOGGER.error("Found Source unavailable: {}", e); } if (response != null) { Set<SourceDescriptor> sourceDescriptors = response.getSourceInfo(); // Get the Content Types for each Source. for (SourceDescriptor sourceDescriptor : sourceDescriptors) { Set<ContentType> types = sourceDescriptor.getContentTypes(); for (ContentType contentType : types) { contentTypes.add(contentType.getName()); } } } // First check if any of the serviceRefernces have been updated. contentTypeSchemas.putAll(updateSchemas()); if (contentTypes != null) { for (String name : contentTypes) { // This will determine if the schema exists or create it if it // doesn't. createSchemaByLocalName(name); } } // Remove the content types that are no longer available Set<QName> missingTypes = new HashSet<QName>(); for (QName qname : contentTypeSchemas.keySet()) { if (!contentTypes.contains(qname.getLocalPart())) { missingTypes.add(qname); } } if (!missingTypes.isEmpty()) { LOGGER.debug("Removing the following from the schema cache: {}", Arrays.toString(missingTypes.toArray())); contentTypeSchemas.keySet().removeAll(missingTypes); } } }