/* * Copyright (c) 2015 Data Harmonisation Panel * * All rights reserved. This program and the accompanying materials are made * available under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.hale.io.appschema.writer; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.namespace.QName; import javax.xml.transform.stream.StreamSource; import com.google.common.collect.ListMultimap; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.common.align.model.Alignment; import eu.esdihumboldt.hale.common.align.model.Cell; import eu.esdihumboldt.hale.common.align.model.ChildContext; import eu.esdihumboldt.hale.common.align.model.Entity; import eu.esdihumboldt.hale.common.core.io.report.IOReporter; import eu.esdihumboldt.hale.common.core.io.report.impl.IOMessageImpl; import eu.esdihumboldt.hale.common.schema.model.PropertyDefinition; import eu.esdihumboldt.hale.common.schema.model.Schema; import eu.esdihumboldt.hale.common.schema.model.SchemaSpace; import eu.esdihumboldt.hale.io.appschema.AppSchemaIO; import eu.esdihumboldt.hale.io.appschema.impl.internal.generated.app_schema.AppSchemaDataAccessType; import eu.esdihumboldt.hale.io.appschema.impl.internal.generated.app_schema.NamespacesPropertyType.Namespace; import eu.esdihumboldt.hale.io.appschema.impl.internal.generated.app_schema.ObjectFactory; import eu.esdihumboldt.hale.io.appschema.impl.internal.generated.app_schema.SourceDataStoresPropertyType.DataStore; import eu.esdihumboldt.hale.io.appschema.impl.internal.generated.app_schema.SourceDataStoresPropertyType.DataStore.Parameters.Parameter; import eu.esdihumboldt.hale.io.appschema.impl.internal.generated.app_schema.TypeMappingsPropertyType.FeatureTypeMapping; import eu.esdihumboldt.hale.io.appschema.model.FeatureChaining; import eu.esdihumboldt.hale.io.appschema.writer.internal.AppSchemaMappingContext; import eu.esdihumboldt.hale.io.appschema.writer.internal.AppSchemaMappingWrapper; import eu.esdihumboldt.hale.io.appschema.writer.internal.PropertyTransformationHandler; import eu.esdihumboldt.hale.io.appschema.writer.internal.PropertyTransformationHandlerFactory; import eu.esdihumboldt.hale.io.appschema.writer.internal.TypeTransformationHandler; import eu.esdihumboldt.hale.io.appschema.writer.internal.TypeTransformationHandlerFactory; import eu.esdihumboldt.hale.io.appschema.writer.internal.UnsupportedTransformationException; import eu.esdihumboldt.hale.io.geoserver.AppSchemaDataStore; import eu.esdihumboldt.hale.io.geoserver.FeatureType; import eu.esdihumboldt.hale.io.geoserver.Layer; import eu.esdihumboldt.hale.io.geoserver.ResourceBuilder; import eu.esdihumboldt.hale.io.geoserver.Workspace; /** * Translates a HALE alignment to an app-schema mapping configuration. * * @author Stefano Costa, GeoSolutions */ public class AppSchemaMappingGenerator { private static final ALogger log = ALoggerFactory.getLogger(AppSchemaMappingGenerator.class); private static final String NET_OPENGIS_OGC_CONTEXT = "eu.esdihumboldt.hale.io.appschema.impl.internal.generated.net_opengis_ogc"; private static final String APP_SCHEMA_CONTEXT = "eu.esdihumboldt.hale.io.appschema.impl.internal.generated.app_schema"; private final Alignment alignment; private final SchemaSpace targetSchemaSpace; private final Schema targetSchema; private final DataStore dataStore; private final FeatureChaining chainingConf; private AppSchemaMappingWrapper mappingWrapper; private AppSchemaDataAccessType mainMapping; private AppSchemaDataAccessType includedTypesMapping; /** * Constructor. * * @param alignment the alignment to translate * @param targetSchemaSpace the target schema space * @param dataStore the DataStore configuration to use * @param chainingConf the feature chaining configuration */ public AppSchemaMappingGenerator(Alignment alignment, SchemaSpace targetSchemaSpace, DataStore dataStore, FeatureChaining chainingConf) { this.alignment = alignment; this.targetSchemaSpace = targetSchemaSpace; // pick the target schemas from which interpolation variables will be // derived this.targetSchema = pickTargetSchema(); this.dataStore = dataStore; this.chainingConf = chainingConf; } /** * Generates the app-schema mapping configuration. * * @param reporter status reporter * @return the generated app-schema mapping configuration * @throws IOException if an error occurs loading the mapping template file */ public AppSchemaMappingWrapper generateMapping(IOReporter reporter) throws IOException { // reset wrapper resetMappingState(); try { AppSchemaDataAccessType mapping = loadMappingTemplate(); mappingWrapper = new AppSchemaMappingWrapper(mapping); // create namespace objects for all target types / properties // TODO: this removes all namespaces that were defined in the // template file, add code to cope with pre-configured namespaces // instead mapping.getNamespaces().getNamespace().clear(); createNamespaces(); // apply datastore configuration, if any // TODO: for now, only a single datastore is supported applyDataStoreConfig(); // populate targetTypes element createTargetTypes(); // populate typeMappings element AppSchemaMappingContext context = new AppSchemaMappingContext(mappingWrapper, alignment, targetSchema.getMappingRelevantTypes(), chainingConf); createTypeMappings(context, reporter); // cache mainMapping and includedTypesMapping for performance mainMapping = mappingWrapper.getMainMapping(); includedTypesMapping = mappingWrapper.getIncludedTypesMapping(); return mappingWrapper; } catch (Exception e) { // making sure state is reset in case an exception is thrown resetMappingState(); throw e; } } private void resetMappingState() { mappingWrapper = null; mainMapping = null; includedTypesMapping = null; } /** * @return the generated mapping configuration */ public AppSchemaMappingWrapper getGeneratedMapping() { checkMappingGenerated(); return mappingWrapper; } /** * Generates the app-schema mapping configuration and writes it to the * provided output stream. * * <p> * If the mapping configuration requires multiple files, only the main * configuration file will be written. * </p> * * @param output the output stream to write to * @param reporter the status reporter * @throws IOException if an I/O error occurs */ public void generateMapping(OutputStream output, IOReporter reporter) throws IOException { generateMapping(reporter); writeMappingConf(output); } /** * Generates the app-schema mapping configuration for the included types * (non-feature types or non-top level feature types) and writes it to the * provided output stream. * * <p> * If the mapping configuration does not require multiple files, an * {@link IllegalStateException} is thrown. * </p> * * @param output the output stream to write to * @param reporter the status reporter * @throws IOException if an I/O error occurs * @throws IllegalStateException if the mapping configuration does not * require multiple files */ public void generateIncludedTypesMapping(OutputStream output, IOReporter reporter) throws IOException { generateMapping(reporter); writeIncludedTypesMappingConf(output); } /** * Updates a schema URI in the generated mapping configuration. * * <p> * It is used mainly by exporters that need to change the target schema * location. * </p> * * @param oldSchemaURI the current schema URI * @param newSchemaURI the updated schema URI */ public void updateSchemaURI(String oldSchemaURI, String newSchemaURI) { checkMappingGenerated(); mappingWrapper.updateSchemaURI(oldSchemaURI, newSchemaURI); // regenerate cached mappings mainMapping = mappingWrapper.getMainMapping(); includedTypesMapping = mappingWrapper.getIncludedTypesMapping(); } /** * Returns the generated app-schema datastore configuration. * * @return the generated datastore configuration * @throws IllegalStateException if no app-schema mapping configuration has * been generated yet or if no target schema is available */ public eu.esdihumboldt.hale.io.geoserver.DataStore getAppSchemaDataStore() { checkMappingGenerated(); checkTargetSchemaAvailable(); eu.esdihumboldt.hale.io.geoserver.Namespace ns = getMainNamespace(); Workspace ws = getMainWorkspace(); String workspaceId = (String) ws.getAttribute(Workspace.ID); String dataStoreName = extractSchemaName(targetSchema.getLocation()); String dataStoreId = dataStoreName + "_datastore"; String mappingFileName = dataStoreName + ".xml"; Map<String, String> connectionParameters = new HashMap<String, String>(); connectionParameters.put("uri", (String) ns.getAttribute(eu.esdihumboldt.hale.io.geoserver.Namespace.URI)); connectionParameters.put("workspaceName", ws.name()); connectionParameters.put("mappingFileName", mappingFileName); return ResourceBuilder .dataStore(dataStoreName, AppSchemaDataStore.class) .setAttribute(eu.esdihumboldt.hale.io.geoserver.DataStore.ID, dataStoreId) .setAttribute(eu.esdihumboldt.hale.io.geoserver.DataStore.WORKSPACE_ID, workspaceId) .setAttribute(eu.esdihumboldt.hale.io.geoserver.DataStore.CONNECTION_PARAMS, connectionParameters).build(); } /** * Returns the generated workspace configuration for the main workspace. * * @return the main workspace configuration * @throws IllegalStateException if the no app-schema mapping configuration * has been generated yet or if no target schema is available */ public Workspace getMainWorkspace() { checkMappingGenerated(); checkTargetSchemaAvailable(); Namespace ns = mappingWrapper.getOrCreateNamespace(targetSchema.getNamespace(), null); return getWorkspace(ns.getPrefix()); } /** * Returns the generated namespace configuration for the main namespace. * * @return the main namespace configuration * @throws IllegalStateException if no app-schema mapping configuration has * been generated yet or if no target schema is available */ public eu.esdihumboldt.hale.io.geoserver.Namespace getMainNamespace() { checkMappingGenerated(); checkTargetSchemaAvailable(); Namespace ns = mappingWrapper.getOrCreateNamespace(targetSchema.getNamespace(), null); return getNamespace(ns); } /** * Returns the generated namespace configuration for secondary namespaces. * * @return the secondary namespaces configuration * @throws IllegalStateException if no app-schema mapping configuration has * been generated yet or if no target schema is available */ public List<eu.esdihumboldt.hale.io.geoserver.Namespace> getSecondaryNamespaces() { checkMappingGenerated(); checkTargetSchemaAvailable(); List<eu.esdihumboldt.hale.io.geoserver.Namespace> secondaryNamespaces = new ArrayList<eu.esdihumboldt.hale.io.geoserver.Namespace>(); // for (Namespace ns : mappingWrapper.getAppSchemaMapping().getNamespaces().getNamespace()) { for (Namespace ns : mainMapping.getNamespaces().getNamespace()) { if (!ns.getUri().equals(targetSchema.getNamespace())) { secondaryNamespaces.add(getNamespace(ns)); } } return secondaryNamespaces; } /** * Returns the configuration of the workspace associated to the provided * namespace. * * @param ns the namespace * @return the configuration of the workspace associated to <code>ns</code> */ public Workspace getWorkspace(eu.esdihumboldt.hale.io.geoserver.Namespace ns) { return getWorkspace(ns.name()); } private eu.esdihumboldt.hale.io.geoserver.Namespace getNamespace(Namespace ns) { String prefix = ns.getPrefix(); String uri = ns.getUri(); String namespaceId = prefix + "_namespace"; return ResourceBuilder.namespace(prefix) .setAttribute(eu.esdihumboldt.hale.io.geoserver.Namespace.ID, namespaceId) .setAttribute(eu.esdihumboldt.hale.io.geoserver.Namespace.URI, uri).build(); } private Workspace getWorkspace(String nsPrefix) { String workspaceId = nsPrefix + "_workspace"; String workspaceName = nsPrefix; return ResourceBuilder.workspace(workspaceName).setAttribute(Workspace.ID, workspaceId) .build(); } /** * Returns the generated feature type configuration for all mapped feature * types. * * @return the generated feature type configuration */ public List<FeatureType> getFeatureTypes() { checkMappingGenerated(); eu.esdihumboldt.hale.io.geoserver.DataStore dataStore = getAppSchemaDataStore(); List<FeatureType> featureTypes = new ArrayList<FeatureType>(); // for (FeatureTypeMapping ftMapping : mappingWrapper.getAppSchemaMapping().getTypeMappings() // .getFeatureTypeMapping()) { for (FeatureTypeMapping ftMapping : mainMapping.getTypeMappings().getFeatureTypeMapping()) { featureTypes.add(getFeatureType(dataStore, ftMapping)); } return featureTypes; } private FeatureType getFeatureType(eu.esdihumboldt.hale.io.geoserver.DataStore dataStore, FeatureTypeMapping ftMapping) { String featureTypeName = stripPrefix(ftMapping.getTargetElement()); String featureTypeId = featureTypeName + "_featureType"; String dataStoreId = (String) dataStore .getAttribute(eu.esdihumboldt.hale.io.geoserver.DataStore.ID); eu.esdihumboldt.hale.io.geoserver.Namespace ns = getMainNamespace(); return ResourceBuilder .featureType(featureTypeName) .setAttribute(FeatureType.ID, featureTypeId) .setAttribute(FeatureType.DATASTORE_ID, dataStoreId) .setAttribute(FeatureType.NAMESPACE_ID, ns.getAttribute(eu.esdihumboldt.hale.io.geoserver.Namespace.ID)).build(); } /** * Returns the layer configuration for the provided feature type. * * @param featureType the feature type * @return the layer configuration */ public Layer getLayer(FeatureType featureType) { String featureTypeName = featureType.name(); String featureTypeId = (String) featureType.getAttribute(FeatureType.ID); String layerName = featureTypeName; String layerId = layerName + "_layer"; return ResourceBuilder.layer(layerName).setAttribute(Layer.ID, layerId) .setAttribute(Layer.FEATURE_TYPE_ID, featureTypeId).build(); } private void checkMappingGenerated() { if (mappingWrapper == null || mainMapping == null || (includedTypesMapping == null && mappingWrapper.requiresMultipleFiles())) { throw new IllegalStateException("No mapping has been generated yet"); } } private void checkTargetSchemaAvailable() { if (targetSchema == null) { throw new IllegalStateException("Target schema not available"); } } private Schema pickTargetSchema() { if (this.targetSchemaSpace == null) { return null; } return this.targetSchemaSpace.getSchemas().iterator().next(); } private String extractSchemaName(URI schemaLocation) { String path = schemaLocation.getPath(); String fragment = schemaLocation.getFragment(); if (fragment != null && !fragment.isEmpty()) { path = path.replace(fragment, ""); } int lastSlashIdx = path.lastIndexOf('/'); int lastDotIdx = path.lastIndexOf('.'); if (lastSlashIdx >= 0) { if (lastDotIdx >= 0) { return path.substring(lastSlashIdx + 1, lastDotIdx); } else { // no dot return path.substring(lastSlashIdx + 1); } } else { // no slash, no dot return path; } } private String stripPrefix(String qualifiedName) { if (qualifiedName == null) { return null; } String[] prefixAndName = qualifiedName.split(":"); if (prefixAndName.length == 2) { return prefixAndName[1]; } else { return null; } } private void applyDataStoreConfig() { if (dataStore != null && dataStore.getParameters() != null) { DataStore targetDS = mappingWrapper.getDefaultDataStore(); List<Parameter> inputParameters = dataStore.getParameters().getParameter(); List<Parameter> targetParameters = targetDS.getParameters().getParameter(); // update destination parameters for (Parameter inputParam : inputParameters) { boolean updated = false; for (Parameter targetParam : targetParameters) { if (inputParam.getName().equals(targetParam.getName())) { targetParam.setValue(inputParam.getValue()); updated = true; break; } } if (!updated) { // parameter was not already present: add it to the list targetParameters.add(inputParam); } } } } private void createNamespaces() { Collection<? extends Cell> typeCells = alignment.getTypeCells(); for (Cell typeCell : typeCells) { ListMultimap<String, ? extends Entity> targetEntities = typeCell.getTarget(); if (targetEntities != null) { for (Entity entity : targetEntities.values()) { createNamespaceForEntity(entity, mappingWrapper); } } Collection<? extends Cell> propertyCells = alignment.getPropertyCells(typeCell); for (Cell propCell : propertyCells) { Collection<? extends Entity> targetProperties = propCell.getTarget().values(); if (targetProperties != null) { for (Entity property : targetProperties) { createNamespaceForEntity(property, mappingWrapper); } } } } } private void createNamespaceForEntity(Entity entity, AppSchemaMappingWrapper wrapper) { QName typeName = entity.getDefinition().getType().getName(); String namespaceURI = typeName.getNamespaceURI(); String prefix = typeName.getPrefix(); wrapper.getOrCreateNamespace(namespaceURI, prefix); List<ChildContext> propertyPath = entity.getDefinition().getPropertyPath(); createNamespacesForPath(propertyPath, wrapper); } private void createNamespacesForPath(List<ChildContext> propertyPath, AppSchemaMappingWrapper wrapper) { if (propertyPath != null) { for (ChildContext childContext : propertyPath) { PropertyDefinition child = childContext.getChild().asProperty(); if (child != null) { String namespaceURI = child.getName().getNamespaceURI(); String prefix = child.getName().getPrefix(); wrapper.getOrCreateNamespace(namespaceURI, prefix); } } } } private void createTargetTypes() { Iterable<? extends Schema> targetSchemas = targetSchemaSpace.getSchemas(); if (targetSchemas != null) { for (Schema targetSchema : targetSchemas) { mappingWrapper.addSchemaURI(targetSchema.getLocation().toString()); } } } private void createTypeMappings(AppSchemaMappingContext context, IOReporter reporter) { Collection<? extends Cell> typeCells = alignment.getTypeCells(); for (Cell typeCell : typeCells) { String typeTransformId = typeCell.getTransformationIdentifier(); TypeTransformationHandler typeTransformHandler = null; try { typeTransformHandler = TypeTransformationHandlerFactory.getInstance() .createTypeTransformationHandler(typeTransformId); FeatureTypeMapping ftMapping = typeTransformHandler.handleTypeTransformation( typeCell, context); if (ftMapping != null) { Collection<? extends Cell> propertyCells = alignment.getPropertyCells(typeCell); for (Cell propertyCell : propertyCells) { String propertyTransformId = propertyCell.getTransformationIdentifier(); PropertyTransformationHandler propertyTransformHandler = null; try { propertyTransformHandler = PropertyTransformationHandlerFactory .getInstance().createPropertyTransformationHandler( propertyTransformId); propertyTransformHandler.handlePropertyTransformation(typeCell, propertyCell, context); } catch (UnsupportedTransformationException e) { String errMsg = MessageFormat.format( "Error processing property cell {0}", propertyCell.getId()); log.warn(errMsg, e); if (reporter != null) { reporter.warn(new IOMessageImpl(errMsg, e)); } } } } } catch (UnsupportedTransformationException e) { String errMsg = MessageFormat.format("Error processing type cell{0}", typeCell.getId()); log.warn(errMsg, e); if (reporter != null) { reporter.warn(new IOMessageImpl(errMsg, e)); } } } } private AppSchemaDataAccessType loadMappingTemplate() throws IOException { InputStream is = getClass().getResourceAsStream(AppSchemaIO.MAPPING_TEMPLATE); JAXBElement<AppSchemaDataAccessType> templateElement = null; try { JAXBContext context = createJaxbContext(); Unmarshaller unmarshaller = context.createUnmarshaller(); templateElement = unmarshaller.unmarshal(new StreamSource(is), AppSchemaDataAccessType.class); } catch (JAXBException e) { throw new IOException(e); } return templateElement.getValue(); } /** * Writes the generated app-schema mapping to the provided output stream. * * <p> * If the mapping configuration requires multiple files, only the main * configuration file will be written. * </p> * * @param out the output stream to write to * @throws IOException if an I/O error occurs */ public void writeMappingConf(OutputStream out) throws IOException { checkMappingGenerated(); try { writeMapping(out, mainMapping); } catch (JAXBException e) { throw new IOException(e); } } /** * Writes the generated app-schema mapping configuration for the included * types (non-feature types or non-top level feature types) to the provided * output stream. * * <p> * If the mapping configuration does not require multiple files, an * {@link IllegalStateException} is thrown. * </p> * * @param out the output stream to write to * @throws IOException if an I/O error occurs * @throws IllegalStateException if the mapping configuration does not * require multiple files */ public void writeIncludedTypesMappingConf(OutputStream out) throws IOException { checkMappingGenerated(); if (!mappingWrapper.requiresMultipleFiles()) { throw new IllegalStateException( "No included types configuration is available for the generated mapping"); } try { writeMapping(out, includedTypesMapping); } catch (JAXBException e) { throw new IOException(e); } } static void writeMapping(OutputStream out, AppSchemaDataAccessType mapping) throws JAXBException { JAXBContext context = createJaxbContext(); Marshaller marshaller = context.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); JAXBElement<AppSchemaDataAccessType> mappingConfElement = new ObjectFactory() .createAppSchemaDataAccess(mapping); marshaller.marshal(mappingConfElement, out); } private static JAXBContext createJaxbContext() throws JAXBException { JAXBContext context = JAXBContext.newInstance(NET_OPENGIS_OGC_CONTEXT + ":" + APP_SCHEMA_CONTEXT); return context; } }