/* * 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.internal; import static eu.esdihumboldt.hale.io.appschema.writer.AppSchemaMappingUtils.QNAME_XSI_NIL; import static eu.esdihumboldt.hale.io.appschema.writer.AppSchemaMappingUtils.XSI_PREFIX; import static eu.esdihumboldt.hale.io.appschema.writer.AppSchemaMappingUtils.XSI_URI; import static eu.esdihumboldt.hale.io.appschema.writer.AppSchemaMappingUtils.findOwningType; import static eu.esdihumboldt.hale.io.appschema.writer.AppSchemaMappingUtils.getGeometryPropertyEntity; import static eu.esdihumboldt.hale.io.appschema.writer.AppSchemaMappingUtils.getTargetProperty; import static eu.esdihumboldt.hale.io.appschema.writer.AppSchemaMappingUtils.getTargetType; import static eu.esdihumboldt.hale.io.appschema.writer.AppSchemaMappingUtils.isGeometryType; import static eu.esdihumboldt.hale.io.appschema.writer.AppSchemaMappingUtils.isGmlId; import static eu.esdihumboldt.hale.io.appschema.writer.AppSchemaMappingUtils.isNested; import static eu.esdihumboldt.hale.io.appschema.writer.AppSchemaMappingUtils.isNilReason; import static eu.esdihumboldt.hale.io.appschema.writer.AppSchemaMappingUtils.isNillable; import static eu.esdihumboldt.hale.io.appschema.writer.AppSchemaMappingUtils.isXmlAttribute; import java.util.List; import javax.xml.namespace.QName; import org.geotools.filter.text.cql2.CQL; import org.geotools.filter.text.cql2.CQLException; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.Literal; import com.google.common.base.Strings; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.common.align.model.AlignmentUtil; import eu.esdihumboldt.hale.common.align.model.Cell; import eu.esdihumboldt.hale.common.align.model.ChildContext; import eu.esdihumboldt.hale.common.align.model.Condition; import eu.esdihumboldt.hale.common.align.model.EntityDefinition; import eu.esdihumboldt.hale.common.align.model.Property; import eu.esdihumboldt.hale.common.align.model.impl.PropertyEntityDefinition; import eu.esdihumboldt.hale.common.schema.model.Definition; import eu.esdihumboldt.hale.common.schema.model.PropertyDefinition; import eu.esdihumboldt.hale.common.schema.model.TypeDefinition; import eu.esdihumboldt.hale.io.appschema.impl.internal.generated.app_schema.AttributeExpressionMappingType; import eu.esdihumboldt.hale.io.appschema.impl.internal.generated.app_schema.AttributeMappingType; import eu.esdihumboldt.hale.io.appschema.impl.internal.generated.app_schema.AttributeMappingType.ClientProperty; import eu.esdihumboldt.hale.io.appschema.impl.internal.generated.app_schema.NamespacesPropertyType.Namespace; import eu.esdihumboldt.hale.io.appschema.impl.internal.generated.app_schema.TypeMappingsPropertyType.FeatureTypeMapping; import eu.esdihumboldt.hale.io.appschema.model.ChainConfiguration; import eu.esdihumboldt.hale.io.appschema.model.FeatureChaining; import eu.esdihumboldt.hale.io.appschema.writer.AppSchemaMappingUtils; import eu.esdihumboldt.hale.io.xsd.constraint.XmlAttributeFlag; /** * Base class for property transformation handlers. * * @author Stefano Costa, GeoSolutions */ public abstract class AbstractPropertyTransformationHandler implements PropertyTransformationHandler { private static final ALogger log = ALoggerFactory .getLogger(AbstractPropertyTransformationHandler.class); /** * The app-schema mapping configuration under construction. */ protected AppSchemaMappingWrapper mapping; /** * The type cell owning the property cell to handle. */ protected Cell typeCell; /** * The property cell to handle. */ protected Cell propertyCell; /** * The target property. */ protected Property targetProperty; /** * The feature type mapping which is the parent of the attribute mapping * generated by this handler. */ protected FeatureTypeMapping featureTypeMapping; /** * The attribute mapping generated by this handler. */ protected AttributeMappingType attributeMapping; /** * @see eu.esdihumboldt.hale.io.appschema.writer.internal.PropertyTransformationHandler#handlePropertyTransformation(eu.esdihumboldt.hale.common.align.model.Cell, * eu.esdihumboldt.hale.common.align.model.Cell, * eu.esdihumboldt.hale.io.appschema.writer.internal.AppSchemaMappingContext) */ @Override public AttributeMappingType handlePropertyTransformation(Cell typeCell, Cell propertyCell, AppSchemaMappingContext context) { this.mapping = context.getMappingWrapper(); this.typeCell = typeCell; this.propertyCell = propertyCell; // TODO: does this hold for any transformation function? this.targetProperty = getTargetProperty(propertyCell); PropertyEntityDefinition targetPropertyEntityDef = targetProperty.getDefinition(); PropertyDefinition targetPropertyDef = targetPropertyEntityDef.getDefinition(); TypeDefinition featureType = null; String mappingName = null; if (AppSchemaMappingUtils.isJoin(typeCell)) { if (context.getFeatureChaining() != null) { ChainConfiguration chainConf = findChainConfiguration(context); if (chainConf != null) { featureType = chainConf.getNestedTypeTargetType(); mappingName = chainConf.getMappingName(); } } else { // this is just a best effort attempt to determine the target // feature type, may result in incorrect mappings featureType = findOwningType(targetPropertyEntityDef, context.getRelevantTargetTypes()); } } if (featureType == null) { featureType = getTargetType(typeCell).getDefinition().getType(); } // double check: don't map properties that belong to a feature // chaining configuration other than the current one if (context.getFeatureChaining() != null) { for (String joinId : context.getFeatureChaining().getJoins().keySet()) { List<ChainConfiguration> chains = context.getFeatureChaining().getChains(joinId); ChainConfiguration chainConf = findLongestNestedPath( targetPropertyEntityDef.getPropertyPath(), chains); if (chainConf != null && !chainConf.getNestedTypeTargetType().equals(featureType)) { // don't translate mapping, will do it (or have done it) // elsewhere! featureType = null; break; } } } if (featureType != null) { // fetch FeatureTypeMapping from mapping configuration this.featureTypeMapping = mapping.getOrCreateFeatureTypeMapping(featureType, mappingName); // TODO: verify source property (if any) belongs to mapped source // type // fetch AttributeMappingType from mapping if (isXmlAttribute(targetPropertyDef)) { // gml:id attribute requires special handling, i.e. an // <idExpression> tag must be added to the attribute mapping for // target feature types and geometry types TypeDefinition parentType = targetPropertyDef.getParentType(); if (isGmlId(targetPropertyDef)) { // TODO: handle gml:id for geometry types if (featureType.equals(parentType)) { handleAsFeatureGmlId(featureType, mappingName); } else if (isGeometryType(parentType)) { handleAsGeometryGmlId(featureType, mappingName); } else { handleAsXmlAttribute(featureType, mappingName); } } else { handleAsXmlAttribute(featureType, mappingName); } } else { handleAsXmlElement(featureType, mappingName); } } return attributeMapping; } /** * @param context the mapping context * @return the chain configuration that applies to the current property * mapping */ private ChainConfiguration findChainConfiguration(AppSchemaMappingContext context) { ChainConfiguration chainConf = null; PropertyEntityDefinition targetPropertyEntityDef = targetProperty.getDefinition(); FeatureChaining featureChaining = context.getFeatureChaining(); if (featureChaining != null) { List<ChildContext> targetPropertyPath = targetPropertyEntityDef.getPropertyPath(); List<ChainConfiguration> chains = featureChaining.getChains(typeCell.getId()); chainConf = findLongestNestedPath(targetPropertyPath, chains); } return chainConf; } private ChainConfiguration findLongestNestedPath(List<ChildContext> targetPropertyPath, List<ChainConfiguration> chains) { ChainConfiguration chainConf = null; if (chains != null && chains.size() > 0 && targetPropertyPath != null && targetPropertyPath.size() > 0) { int maxPathLength = 0; for (ChainConfiguration chain : chains) { List<ChildContext> nestedTargetPath = chain.getNestedTypeTarget().getPropertyPath(); boolean isNested = isNested(nestedTargetPath, targetPropertyPath); if (isNested && maxPathLength < nestedTargetPath.size()) { maxPathLength = nestedTargetPath.size(); chainConf = chain; } } } return chainConf; } /** * This method is invoked when the target type is the feature type owning * this attribute mapping, and the target property is <code>gml:id</code>, * which needs special handling. * * <p> * In practice, this means that <code><idExpression></code> is used in * place of: * * <pre> * <ClientProperty> * <name>...</name> * <value>...</value> * </ClientProperty> * </pre> * * and that the target attribute is set to the mapped feature type name. * * </p> * * @param featureType the target feature type * @param mappingName the target feature type's mapping name (may be * <code>null</code>) */ protected void handleAsFeatureGmlId(TypeDefinition featureType, String mappingName) { PropertyEntityDefinition targetPropertyEntityDef = targetProperty.getDefinition(); List<ChildContext> gmlIdPath = targetPropertyEntityDef.getPropertyPath(); attributeMapping = mapping.getOrCreateAttributeMapping(featureType, mappingName, gmlIdPath); // set targetAttribute to feature type qualified name attributeMapping.setTargetAttribute(featureTypeMapping.getTargetElement()); // set id expression AttributeExpressionMappingType idExpression = new AttributeExpressionMappingType(); idExpression.setOCQL(getSourceExpressionAsCQL()); // TODO: not sure whether any CQL expression can be used here attributeMapping.setIdExpression(idExpression); } /** * This method is invoked when the target property's parent is a geometry * and the target property is <code>gml:id</code> (which needs special * handling). * * <p> * In practice, this means that <code><idExpression></code> is used in * place of: * * <pre> * <ClientProperty> * <name>...</name> * <value>...</value> * </ClientProperty> * </pre> * * @param featureType the target feature type * @param mappingName the target feature type's mapping name (may be * <code>null</code>) */ protected void handleAsGeometryGmlId(TypeDefinition featureType, String mappingName) { PropertyEntityDefinition targetPropertyEntityDef = targetProperty.getDefinition(); PropertyEntityDefinition geometry = (PropertyEntityDefinition) AlignmentUtil .getParent(targetPropertyEntityDef); createGeometryAttributeMapping(featureType, mappingName, geometry); // set id expression AttributeExpressionMappingType idExpression = new AttributeExpressionMappingType(); idExpression.setOCQL(getSourceExpressionAsCQL()); // TODO: not sure whether any CQL expression can be used here attributeMapping.setIdExpression(idExpression); } /** * This method is invoked when the target property is an XML attribute ( * {@link XmlAttributeFlag} constraint is set). * * <p> * The property transformation is translated to: * * <pre> * <code><ClientProperty> * <name>[target property name]</name> * <value>[CQL expression]</value> * </ClientProperty></code> * </pre> * * and added to the attribute mapping generated for the XML element owning * the attribute. * </p> * * @param featureType the target feature type * @param mappingName the target feature type's mapping name (may be * <code>null</code>) */ protected void handleAsXmlAttribute(TypeDefinition featureType, String mappingName) { PropertyEntityDefinition targetPropertyEntityDef = targetProperty.getDefinition(); PropertyDefinition targetPropertyDef = targetPropertyEntityDef.getDefinition(); // fetch attribute mapping for parent property EntityDefinition parentDef = AlignmentUtil.getParent(targetPropertyEntityDef); if (parentDef != null) { List<ChildContext> parentPropertyPath = parentDef.getPropertyPath(); PropertyDefinition parentPropertyDef = parentPropertyPath .get(parentPropertyPath.size() - 1).getChild().asProperty(); if (parentPropertyDef != null) { attributeMapping = mapping.getOrCreateAttributeMapping(featureType, mappingName, parentPropertyPath); // set targetAttribute if empty if (attributeMapping.getTargetAttribute() == null || attributeMapping.getTargetAttribute().isEmpty()) { attributeMapping.setTargetAttribute(mapping.buildAttributeXPath(featureType, parentPropertyPath)); } Namespace targetPropNS = mapping.getOrCreateNamespace(targetPropertyDef.getName() .getNamespaceURI(), targetPropertyDef.getName().getPrefix()); String unqualifiedName = targetPropertyDef.getName().getLocalPart(); boolean isQualified = targetPropNS != null && !Strings.isNullOrEmpty(targetPropNS.getPrefix()); // encode attribute as <ClientProperty> ClientProperty clientProperty = new ClientProperty(); @SuppressWarnings("null") String clientPropName = (isQualified) ? targetPropNS.getPrefix() + ":" + unqualifiedName : unqualifiedName; clientProperty.setName(clientPropName); clientProperty.setValue(getSourceExpressionAsCQL()); setEncodeIfEmpty(clientProperty); // don't add client property if it already exists if (!hasClientProperty(clientProperty.getName())) { attributeMapping.getClientProperty().add(clientProperty); // if mapping nilReason, parent property is nillable and no // sourceExpression has been set yet, add xsi:nil attribute // following the same encoding logic of nilReason (i.e. null // when nilReason is null and viceversa) if (isNilReason(targetPropertyDef) && isNillable(parentPropertyDef) && attributeMapping.getSourceExpression() == null) { addOrReplaceXsiNilAttribute(clientProperty.getValue(), true); } } } } } /** * This method is invoked when the target property is a regular XML element. * * @param featureType the target feature type * @param mappingName the target feature type's mapping name (may be * <code>null</code>) */ protected void handleAsXmlElement(TypeDefinition featureType, String mappingName) { PropertyEntityDefinition targetPropertyEntityDef = targetProperty.getDefinition(); PropertyDefinition targetPropertyDef = targetPropertyEntityDef.getDefinition(); TypeDefinition targetPropertyType = targetPropertyDef.getPropertyType(); if (isGeometryType(targetPropertyType)) { handleXmlElementAsGeometryType(featureType, mappingName); } else { attributeMapping = mapping.getOrCreateAttributeMapping(featureType, mappingName, targetPropertyEntityDef.getPropertyPath()); List<ChildContext> targetPropertyPath = targetPropertyEntityDef.getPropertyPath(); // set target attribute attributeMapping.setTargetAttribute(mapping.buildAttributeXPath(featureType, targetPropertyPath)); } // set source expression AttributeExpressionMappingType sourceExpression = new AttributeExpressionMappingType(); // TODO: is this general enough? sourceExpression.setOCQL(getSourceExpressionAsCQL()); attributeMapping.setSourceExpression(sourceExpression); if (AppSchemaMappingUtils.isMultiple(targetPropertyDef)) { attributeMapping.setIsMultiple(true); } // if element is nillable, add xsi:nil attribute with inverted logic // (i.e. null if source expression is NOT null, and viceversa) if (isNillable(targetPropertyDef)) { addOrReplaceXsiNilAttribute(attributeMapping.getSourceExpression().getOCQL(), false); } // TODO: isList? // TODO: targetAttributeNode? // TODO: encodeIfEmpty? } /** * This method is invoked when the target property is a GML geometry type. * * <p> * The target attribute is set to <code>gml:AbstractGeometry</code> and the * concrete geometry type is specified in a * <code><targetAttributeNode></code> tag. * </p> * * @param featureType the target feature type * @param mappingName the target feature type's mapping name (may be * <code>null</code>) */ protected void handleXmlElementAsGeometryType(TypeDefinition featureType, String mappingName) { PropertyEntityDefinition geometry = targetProperty.getDefinition(); createGeometryAttributeMapping(featureType, mappingName, geometry); // GeometryTypes require special handling TypeDefinition geometryType = geometry.getDefinition().getPropertyType(); QName geomTypeName = geometryType.getName(); Namespace geomNS = mapping.getOrCreateNamespace(geomTypeName.getNamespaceURI(), geomTypeName.getPrefix()); attributeMapping.setTargetAttributeNode(geomNS.getPrefix() + ":" + geomTypeName.getLocalPart()); // set target attribute to parent (should be gml:AbstractGeometry) // TODO: this is really ugly, but I don't see a better way to do it // since HALE renames // {http://www.opengis.net/gml/3.2}AbstractGeometry element // to // {http://www.opengis.net/gml/3.2/AbstractGeometry}choice EntityDefinition parentEntityDef = AlignmentUtil.getParent(geometry); Definition<?> parentDef = parentEntityDef.getDefinition(); String parentQName = geomNS.getPrefix() + ":" + parentDef.getDisplayName(); List<ChildContext> targetPropertyPath = parentEntityDef.getPropertyPath(); attributeMapping.setTargetAttribute(mapping.buildAttributeXPath(featureType, targetPropertyPath) + "/" + parentQName); } /** * Wraps the provided CQL expression in a conditional expression, based on * the filter defined on the property. * * <p> * TODO: current implementation is broken, don't use it (first argument of * if_then_else must be an expression, cannot be a filter (i.e. cannot * contain '=' sign))! * </p> * * @param propertyEntityDef the property definition defining the condition * @param cql the CQL expression to wrap * @return a conditional expression wrapping the provided CQL expression */ protected static String getConditionalExpression(PropertyEntityDefinition propertyEntityDef, String cql) { if (propertyEntityDef != null) { String propertyName = propertyEntityDef.getDefinition().getName().getLocalPart(); List<ChildContext> propertyPath = propertyEntityDef.getPropertyPath(); // TODO: conditions are supported only on simple (not nested) // properties if (propertyPath.size() == 1) { Condition condition = propertyPath.get(0).getCondition(); if (condition != null) { String fitlerText = AlignmentUtil.getFilterText(condition.getFilter()); // remove "parent" references fitlerText = fitlerText.replace("parent.", ""); // replace "value" references with the local name of the // property itself fitlerText = fitlerText.replace("value", propertyName); return String.format("if_then_else(%s, %s, Expression.NIL)", fitlerText, cql); } } } return cql; } /** * Template method to be implemented by subclasses. * * <p> * This is where the translation logic should go. Basically, the propety * transformation must be converted to a CQL expression producing the same * result. * </p> * * @return a CQL expression producing the same result as the HALE * transformation */ protected abstract String getSourceExpressionAsCQL(); private void createGeometryAttributeMapping(TypeDefinition featureType, String mappingName, PropertyEntityDefinition geometry) { EntityDefinition geometryProperty = getGeometryPropertyEntity(geometry); // use geometry property path to create / retrieve attribute mapping attributeMapping = mapping.getOrCreateAttributeMapping(featureType, mappingName, geometryProperty.getPropertyPath()); } /** * If client property is set to a constant expression, add * <encodeIfEmpty>true</encodeIfEmpty> to the attribute mapping * to make sure the element is encoded also if it has no value. * * @param clientProperty the client property to test */ private void setEncodeIfEmpty(ClientProperty clientProperty) { try { Expression expr = CQL.toExpression(getSourceExpressionAsCQL()); if (expr != null && expr instanceof Literal) { attributeMapping.setEncodeIfEmpty(true); } } catch (CQLException e) { log.warn("Cannot set encodeIfEmpty value. Reason: " + e.getMessage()); } } /** * Adds an {@code xsi:nil} client property to the attribute mapping, or * updates the existing one. * * <p> * The CQL expression for the property's value is derived from the provided * source expression in this way: * * <ul> * <li>{@code sameLogic=true} --> * {@code if_then_else(isNull([sourceExpression]), Expression.NIL, 'true')}</li> * <li>{@code sameLogic=false} --> * {@code if_then_else(isNull([sourceExpression]), 'true', Expression.NIL)}</li> * </ul> * </p> * * @param sourceExpression the source expression * @param sameLogic if {@code true}, {@code xsi:nil} will be {@code null} * when {@code sourceExpression} is and {@code 'true'} when it * isn't, if {@code false} the opposite applies */ private void addOrReplaceXsiNilAttribute(String sourceExpression, boolean sameLogic) { final String sameLogicPattern = "if_then_else(isNull(%s), Expression.NIL, 'true')"; final String invertedLogicPattern = "if_then_else(isNull(%s), 'true', Expression.NIL)"; final String pattern = sameLogic ? sameLogicPattern : invertedLogicPattern; // make sure xsi namespace is included in the mapping mapping.getOrCreateNamespace(XSI_URI, XSI_PREFIX); String xsiNilQName = QNAME_XSI_NIL.getPrefix() + ":" + QNAME_XSI_NIL.getLocalPart(); ClientProperty xsiNil = getClientProperty(xsiNilQName); if (xsiNil == null) { xsiNil = new ClientProperty(); // add xsi:nil attribute attributeMapping.getClientProperty().add(xsiNil); } xsiNil.setName(xsiNilQName); xsiNil.setValue(String.format(pattern, sourceExpression)); } private boolean hasClientProperty(String name) { return getClientProperty(name) != null; } private ClientProperty getClientProperty(String name) { for (ClientProperty existentProperty : attributeMapping.getClientProperty()) { if (name.equals(existentProperty.getName())) { return existentProperty; } } return null; } }