/* * Copyright (c) 2013 Fraunhofer IGD * * 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: * Fraunhofer IGD */ package eu.esdihumboldt.hale.app.bgis.ade.propagate; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map.Entry; import java.util.Queue; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.namespace.QName; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.HashMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.SetMultimap; import eu.esdihumboldt.hale.app.bgis.ade.common.BGISAppConstants; import eu.esdihumboldt.hale.app.bgis.ade.common.BGISAppUtil; import eu.esdihumboldt.hale.app.bgis.ade.common.EntityVisitor; import eu.esdihumboldt.hale.app.bgis.ade.propagate.config.FeatureMap; import eu.esdihumboldt.hale.app.bgis.ade.propagate.internal.TypeEntityIndex; 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.align.model.EntityDefinition; import eu.esdihumboldt.hale.common.align.model.MutableCell; import eu.esdihumboldt.hale.common.align.model.Property; import eu.esdihumboldt.hale.common.align.model.impl.DefaultCell; import eu.esdihumboldt.hale.common.align.model.impl.DefaultProperty; import eu.esdihumboldt.hale.common.align.model.impl.PropertyEntityDefinition; import eu.esdihumboldt.hale.common.schema.SchemaSpaceID; import eu.esdihumboldt.hale.common.schema.model.ChildDefinition; import eu.esdihumboldt.hale.common.schema.model.DefinitionGroup; import eu.esdihumboldt.hale.common.schema.model.DefinitionUtil; import eu.esdihumboldt.hale.common.schema.model.Schema; import eu.esdihumboldt.hale.common.schema.model.TypeDefinition; import eu.esdihumboldt.hale.io.gml.CityGMLConstants; /** * Entity visitor that creates cells from example cells. * * @author Simon Templer */ public class CityGMLPropagateVisitor extends EntityVisitor implements BGISAppConstants, CityGMLConstants { /** * The created cells. */ private final List<MutableCell> cells = new ArrayList<MutableCell>(); private final Multimap<String, Cell> bgisExamples; private final Multimap<QName, Cell> cityGMLExamples; private final Schema cityGMLSource; private final FeatureMap featureMap; private final SetMultimap<Cell, TypeDefinition> handledTargets = HashMultimap.create(); private final String cellNote; /** * Create an example cell visitor creating derived cells. * * @param cityGMLSource the CityGML source schema to use for the created * mapping cells * @param bgisExamples example cells, with the target ADE property name as * key * @param cityGMLExamples example cells, with the target CityGML property * name as key * @param featureMap the feature map * @param cellNote note to append to generated cell's notes */ public CityGMLPropagateVisitor(Schema cityGMLSource, Multimap<String, Cell> bgisExamples, Multimap<QName, Cell> cityGMLExamples, FeatureMap featureMap, String cellNote) { this.bgisExamples = bgisExamples; this.cityGMLExamples = cityGMLExamples; this.cityGMLSource = cityGMLSource; this.featureMap = featureMap; this.cellNote = cellNote; } @Override protected boolean visit(PropertyEntityDefinition ped) { if (ADE_NS.equals(ped.getDefinition().getName().getNamespaceURI())) { // property is from ADE for (Cell exampleCell : bgisExamples.get(ped.getDefinition().getName().getLocalPart())) { // handle each example cell propagateCell(exampleCell, ped); } return true; } else if (ped.getDefinition().getName().getNamespaceURI().startsWith(CITYGML_NAMESPACE_CORE)) { // is a CityGML property /* * FIXME do only for certain types, namely those the target property * is defined in, to prevent duplicated cells. But those will not be * supplied! XXX think about it */ Pattern nsPattern = Pattern.compile("^" + Pattern.quote(CITYGML_NAMESPACE_CORE) + "(/[^/]+)?/([^/]+)$"); Matcher matcher = nsPattern.matcher(ped.getDefinition().getName().getNamespaceURI()); if (matcher.find()) { // name of the CityGML module expected // String module = matcher.group(1); for (Entry<QName, Cell> example : cityGMLExamples.entries()) { // check each example cell if (example.getKey().getLocalPart() .equals(ped.getDefinition().getName().getLocalPart())) { // local name matches Matcher exMatcher = nsPattern.matcher(example.getKey().getNamespaceURI()); if (exMatcher.find()) { /* * The module is not compared after all, as they may * be different and still propagation is desired. * This is the case for instance for * lod1MultiSurface, which may occur with building, * vegetation and other module namespaces. * * FIXME check module name for class/function/usage? */ // String exampleModule = exMatcher.group(1); // if (Objects.equals(module, exampleModule)) { // CityGML module matches propagateCell(example.getValue(), ped); // } } } } } else { System.err.println("ERROR: Failure analysing CityGML namespace"); } // XXX only level one CityGML target properties supported! return false; } return false; } /** * Propagate a given cell to the given target property and possible source * types. * * @param exampleCell the example cell * @param ped the target property */ private void propagateCell(Cell exampleCell, PropertyEntityDefinition ped) { /* * Find the type where the property actually is defined, as if possible * a super type mapping should be used. */ TypeDefinition targetType = findTypeDefining(ped); if (!targetType.equals(ped.getType())) { ped = new PropertyEntityDefinition(targetType, ped.getPropertyPath(), ped.getSchemaSpace(), ped.getFilter()); } // check if the cell was already handled for the type if (handledTargets.get(exampleCell).contains(targetType)) { // don't produce any duplicates return; } handledTargets.put(exampleCell, targetType); TypeEntityIndex<List<ChildContext>> index = new TypeEntityIndex<List<ChildContext>>(); Collection<TypeDefinition> sourceTypes = findSourceTypes(exampleCell, targetType, index); if (sourceTypes != null) { for (TypeDefinition sourceType : sourceTypes) { // copy cell DefaultCell cell = new DefaultCell(exampleCell); // reset ID cell.setId(null); // assign new target ListMultimap<String, Entity> target = ArrayListMultimap.create(); target.put(cell.getTarget().keys().iterator().next(), new DefaultProperty(ped)); cell.setTarget(target); // assign new source(s) ListMultimap<String, Entity> source = ArrayListMultimap.create(); for (Entry<String, ? extends Entity> entry : cell.getSource().entries()) { // create new source entity List<ChildContext> path = index.get(sourceType, entry.getValue()); if (path == null) { throw new IllegalStateException("No replacement property path computed"); } Property newSource = new DefaultProperty(new PropertyEntityDefinition( sourceType, path, SchemaSpaceID.SOURCE, null)); source.put(entry.getKey(), newSource); } cell.setSource(source); BGISAppUtil.appendNote(cell, cellNote); cells.add(cell); } } } /** * Find the type that actually defines the property referenced in the given * property entity definition. * * @param ped the property entity definition * @return the type defining the referenced property */ private TypeDefinition findTypeDefining(PropertyEntityDefinition ped) { /* * The type we look for is either the one given in the entity * definition, or a super type. */ TypeDefinition parent = ped.getType(); TypeDefinition superType = parent.getSuperType(); while (superType != null) { if (!hasProperty(superType, ped.getPropertyPath())) { return parent; } parent = superType; superType = parent.getSuperType(); } return parent; } /** * Tests the given type if it has the properties defined in the given * property path. * * @param type the type definition or definition group * @param propertyPath the property path to test * @return if the property path is valid for the given type */ private boolean hasProperty(DefinitionGroup type, List<ChildContext> propertyPath) { if (propertyPath == null || propertyPath.isEmpty()) { return true; } else { ChildDefinition<?> child = type.getChild(propertyPath.get(0).getChild().getName()); if (child != null) { if (propertyPath.size() == 1) { return true; } else { return hasProperty(DefinitionUtil.getDefinitionGroup(child), propertyPath.subList(1, propertyPath.size())); } } else { return false; } } } /** * Find source types to use to propagate the given example cell. If * possible, common super types will be returned. * * @param exampleCell the example cell * @param targetType the target type * @param index the index to store the replacement property paths in * @return the source types to propagate the cell to */ private Collection<TypeDefinition> findSourceTypes(Cell exampleCell, TypeDefinition targetType, TypeEntityIndex<List<ChildContext>> index) { Set<TypeDefinition> possibleSources = findAllPossibleSources(targetType); /* * Add all super types, because if possible, we want to do the mapping * on super types. */ Set<TypeDefinition> superTypes = new HashSet<TypeDefinition>(); for (TypeDefinition type : possibleSources) { TypeDefinition superType = type.getSuperType(); while (superType != null) { if (superTypes.add(superType) || !possibleSources.contains(superType)) { superType = superType.getSuperType(); } else { superType = null; } } } possibleSources.addAll(superTypes); /* * Check source entities and filter all source types that don't match * the entity. */ TypeDefinition originalSource = null; for (Entity source : exampleCell.getSource().values()) { EntityDefinition ed = source.getDefinition(); // check source type if (originalSource == null) { originalSource = ed.getType(); } else { if (!originalSource.equals(ed.getType())) { System.err.println("WARNING: ignoring cell with sources in different types"); return null; } } if (ed.getPropertyPath().isEmpty()) { // don't handle type cells return null; } // remove all types w/o compatible property Iterator<TypeDefinition> it = possibleSources.iterator(); while (it.hasNext()) { TypeDefinition type = it.next(); List<ChildContext> newPath = hasCompatibleProperty(type, ed.getPropertyPath()); if (newPath == null) { it.remove(); } else { // remember child path per root type and entity index.put(type, source, newPath); } } } /* * Remove all types that have super types contained in the set. */ Set<TypeDefinition> toTest = new HashSet<TypeDefinition>(possibleSources); for (TypeDefinition type : toTest) { TypeDefinition superType = type.getSuperType(); while (superType != null) { if (possibleSources.contains(superType)) { possibleSources.remove(type); // other super types are tested on their own break; } superType = superType.getSuperType(); } } return possibleSources; } /** * Find all possible CityGML source types for the given target type based on * the feature map configuration. Also takes into account the possible * sources for sub-types of the given target type. * * @param targetType the target type definition * @return the set of possible source type definitions */ private Set<TypeDefinition> findAllPossibleSources(TypeDefinition targetType) { // find all possible source types, taking into account also sub-types Queue<TypeDefinition> toTest = new LinkedList<TypeDefinition>(); Set<String> sourceTypeNames = new HashSet<String>(); toTest.add(targetType); while (!toTest.isEmpty()) { TypeDefinition type = toTest.poll(); sourceTypeNames.addAll(featureMap.getPossibleSourceTypes(type.getDisplayName())); toTest.addAll(type.getSubTypes()); } Set<TypeDefinition> types = new HashSet<TypeDefinition>(); for (TypeDefinition type : cityGMLSource.getTypes()) { if (type.getName().getNamespaceURI().startsWith(CITYGML_NAMESPACE_CORE) && sourceTypeNames.contains(type.getDisplayName()) && BGISAppUtil.isFeatureType(type)) { /* * Type is a feature type from CityGML and is one of the * possible source types */ types.add(type); } } return types; } private List<ChildContext> hasCompatibleProperty(DefinitionGroup type, List<ChildContext> propertyPath) { int propIndex = -1; // find index of first property (ignoring groups) for (int i = 0; i < propertyPath.size() && propIndex < 0; i++) { if (propertyPath.get(i).getChild().asProperty() != null) { propIndex = i; } } if (propIndex < 0) { // now there something is not right return null; } QName name = propertyPath.get(propIndex).getChild().getName(); if (!name.getNamespaceURI().startsWith(CITYGML_NAMESPACE_CORE)) { System.err.println("ERROR: only cells on CityGML source properties will be propagated"); return null; } // look for a potential match Collection<? extends ChildDefinition<?>> children = DefinitionUtil.getAllChildren(type); for (ChildDefinition<?> candidate : children) { if (candidate.asProperty() != null) { if (candidate.getName().getNamespaceURI().startsWith(CITYGML_NAMESPACE_CORE) && candidate.getName().getLocalPart().equals(name.getLocalPart()) && candidate .asProperty() .getPropertyType() .getName() .getLocalPart() .equals(propertyPath.get(propIndex).getChild().asProperty() .getPropertyType().getName().getLocalPart())) { /* * Property has CityGML namespace, matching local name and * matching property type local name. */ List<ChildContext> newPath = new ArrayList<ChildContext>(); ChildContext org = propertyPath.get(propIndex); ChildContext rep = new ChildContext(org.getContextName(), org.getIndex(), org.getCondition(), candidate); newPath.add(rep); if (propIndex + 1 >= propertyPath.size()) { // last property, return return newPath; } else { // check path further List<ChildContext> childPath = hasCompatibleProperty(candidate.asProperty() .getPropertyType(), propertyPath.subList(propIndex + 1, propertyPath.size())); if (childPath != null) { newPath.addAll(childPath); return newPath; } } } } else if (candidate.asGroup() != null) { // check path further for the same property List<ChildContext> childPath = hasCompatibleProperty(candidate.asGroup(), propertyPath.subList(propIndex, propertyPath.size())); if (childPath != null) { // prepend group to path childPath.add(0, new ChildContext(candidate)); return childPath; } } } return null; } /** * Get the created cells. * * @return the cells assigning default values */ public List<MutableCell> getCells() { return cells; } }