/* * Copyright (c) 2012 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: * HUMBOLDT EU Integrated Project #030962 * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.hale.ui.service.entity.internal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; 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.atomic.AtomicBoolean; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.MessageBox; import org.eclipse.ui.PlatformUI; import com.google.common.base.Objects; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Collections2; import com.google.common.collect.HashMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.SetMultimap; import eu.esdihumboldt.hale.common.align.model.Alignment; 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.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.Type; 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.DefaultType; import eu.esdihumboldt.hale.common.align.model.impl.PropertyEntityDefinition; import eu.esdihumboldt.hale.common.align.model.impl.TypeEntityDefinition; import eu.esdihumboldt.hale.common.instance.model.Filter; import eu.esdihumboldt.hale.common.schema.SchemaSpaceID; import eu.esdihumboldt.hale.common.schema.model.ChildDefinition; import eu.esdihumboldt.hale.common.schema.model.TypeDefinition; import eu.esdihumboldt.hale.ui.service.align.AlignmentService; import eu.esdihumboldt.hale.ui.service.align.AlignmentServiceListener; import eu.esdihumboldt.hale.ui.service.entity.EntityDefinitionService; import eu.esdihumboldt.hale.ui.service.project.ProjectService; import eu.esdihumboldt.hale.ui.service.project.ProjectServiceAdapter; /** * Manages instance contexts and the corresponding entity definitions. * * @author Simon Templer * @since 2.5 */ public class EntityDefinitionServiceImpl extends AbstractEntityDefinitionService { /** * Stores named instance contexts. The key is the corresponding entity * definition w/ the default context (in the last path element). XXX use * entity definitions as values instead? XXX This storage is based on the * assumption that a named context cannot be combined with any other * context. */ private final SetMultimap<EntityDefinition, Integer> namedContexts = HashMultimap.create(); /** * Stores index contexts. The key is the corresponding entity definition w/ * the default context (in the last path element). XXX use entity * definitions as values instead? XXX This storage is based on the * assumption that an index context cannot be combined with any other * context. */ private final SetMultimap<EntityDefinition, Integer> indexContexts = HashMultimap.create(); /** * Stores condition contexts. The key is the corresponding entity definition * w/ the default context (in the last path element). XXX use entity * definitions as values instead? XXX This storage is based on the * assumption that a condition context cannot be combined with any other * context. */ private final SetMultimap<EntityDefinition, Condition> conditionContexts = HashMultimap .create(); /** * Create the entity definition service * * @param alignmentService the alignment service * @param projectService the project service */ public EntityDefinitionServiceImpl(final AlignmentService alignmentService, ProjectService projectService) { super(); alignmentService.addListener(new AlignmentServiceListener() { @Override public void cellsReplaced(Map<? extends Cell, ? extends Cell> cells) { addMissingContexts(cells.values()); // XXX do anything about replaced cells? } @Override public void cellsAdded(Iterable<Cell> cells) { addMissingContexts(cells); } @Override public void cellsRemoved(Iterable<Cell> cells) { // XXX do anything? } @Override public void alignmentCleared() { // XXX remove all created contexts? } @Override public void cellsPropertyChanged(Iterable<Cell> cells, String propertyName) { // currently no cell property that affects entity definition // contexts } @Override public void customFunctionsChanged() { // custom functions don't affect entity definitions } @Override public void alignmentChanged() { // XXX clear first? addMissingContexts(alignmentService.getAlignment().getCells()); } }); // in case alignment was loaded before service was created -> add // missing contexts now addMissingContexts(alignmentService.getAlignment().getCells()); projectService.addListener(new ProjectServiceAdapter() { @Override public void onClean() { // remove all context definitions clean(); } }); // TODO remove contexts when schema doesn't contain corresponding // definition?! } /** * Remove all defined contexts */ protected void clean() { // XXX nested synchronized? synchronized (namedContexts) { namedContexts.clear(); } synchronized (indexContexts) { indexContexts.clear(); } synchronized (conditionContexts) { conditionContexts.clear(); } } /** * @see EntityDefinitionService#addNamedContext(EntityDefinition) */ @Override public EntityDefinition addNamedContext(EntityDefinition sibling) { List<ChildContext> path = sibling.getPropertyPath(); if (sibling.getSchemaSpace() == SchemaSpaceID.SOURCE || path.isEmpty()) { // not supported for source entities // and not for type entity definitions // XXX throw exception instead? return null; } // XXX any checks? see InstanceContextTester EntityDefinition def = AlignmentUtil.getDefaultEntity(sibling); Integer newName; synchronized (namedContexts) { // get registered context names Collection<Integer> names = namedContexts.get(def); if (names == null || names.isEmpty()) { newName = Integer.valueOf(0); } else { // get the maximum value available as a name SortedSet<Integer> sortedNames = new TreeSet<Integer>(names); int max = sortedNames.last(); // and use its value increased by one newName = Integer.valueOf(max + 1); } namedContexts.put(def, newName); } List<ChildContext> newPath = new ArrayList<ChildContext>(path); ChildDefinition<?> lastChild = newPath.get(newPath.size() - 1).getChild(); newPath.remove(path.size() - 1); // new named context, w/o index or condition context newPath.add(new ChildContext(newName, null, null, lastChild)); EntityDefinition result = createEntity(def.getType(), newPath, sibling.getSchemaSpace(), sibling.getFilter()); notifyContextAdded(result); return result; } /** * @see EntityDefinitionService#addIndexContext(EntityDefinition, Integer) */ @Override public EntityDefinition addIndexContext(EntityDefinition sibling, Integer index) { List<ChildContext> path = sibling.getPropertyPath(); if (sibling.getSchemaSpace() == SchemaSpaceID.TARGET || path.isEmpty()) { // not supported for target entities // and not for type entity definitions // XXX throw exception instead? return null; } // XXX any checks? see InstanceContextTester EntityDefinition def = AlignmentUtil.getDefaultEntity(sibling); boolean doAdd = true; synchronized (indexContexts) { // get registered context indexes Set<Integer> existingIndexes = indexContexts.get(def); if (index == null) { // determine index automatically if (existingIndexes == null || existingIndexes.isEmpty()) { index = Integer.valueOf(0); } else { // get the sorted existing indexes SortedSet<Integer> sortedIndexes = new TreeSet<Integer>(existingIndexes); // find the smallest value not present int expected = 0; Iterator<Integer> it = sortedIndexes.iterator(); while (index == null && it.hasNext()) { int existingIndex = it.next(); if (existingIndex != expected) { index = expected; } expected++; } if (index == null) { index = expected; } } } else if (existingIndexes.contains(index)) { // this index context is not new, but already there doAdd = false; } if (doAdd) { indexContexts.put(def, index); } } List<ChildContext> newPath = new ArrayList<ChildContext>(path); ChildDefinition<?> lastChild = newPath.get(newPath.size() - 1).getChild(); newPath.remove(path.size() - 1); // new index context, w/o name or condition context newPath.add(new ChildContext(null, index, null, lastChild)); EntityDefinition result = createEntity(def.getType(), newPath, sibling.getSchemaSpace(), sibling.getFilter()); if (doAdd) { notifyContextAdded(result); } return result; } /** * @see EntityDefinitionService#addConditionContext(EntityDefinition, * Filter) */ @Override public EntityDefinition addConditionContext(EntityDefinition sibling, Filter filter) { if (filter == null) { throw new NullPointerException("Filter must not be null"); } List<ChildContext> path = sibling.getPropertyPath(); if (sibling.getSchemaSpace() == SchemaSpaceID.TARGET && path.isEmpty()) { // not supported for target type entities // XXX throw exception instead? return null; } Condition condition = new Condition(filter); EntityDefinition def = AlignmentUtil.getDefaultEntity(sibling); boolean doAdd = true; synchronized (conditionContexts) { // get registered context indexes Set<Condition> existingConditions = conditionContexts.get(def); if (existingConditions.contains(condition)) { // this condition context is not new, but already there doAdd = false; } if (doAdd) { conditionContexts.put(def, condition); } } EntityDefinition result = createWithCondition(sibling, condition); if (doAdd) { notifyContextAdded(result); } return result; } /** * Creates a new entity definition. * * @param sibling the entity definition to use as base * @param condition the new condition, not <code>null</code> * @return a new entity definition */ private EntityDefinition createWithCondition(EntityDefinition sibling, Condition condition) { EntityDefinition result; List<ChildContext> path = sibling.getPropertyPath(); if (path.isEmpty()) { // create type entity definition with filter result = createEntity(sibling.getType(), path, sibling.getSchemaSpace(), condition.getFilter()); } else { List<ChildContext> newPath = new ArrayList<ChildContext>(path); ChildContext last = newPath.remove(path.size() - 1); // new condition context, w/o name or index context newPath.add(new ChildContext(null, null, condition, last.getChild())); result = createEntity(sibling.getType(), newPath, sibling.getSchemaSpace(), sibling.getFilter()); } return result; } /** * @see eu.esdihumboldt.hale.ui.service.entity.EntityDefinitionService#editConditionContext(eu.esdihumboldt.hale.common.align.model.EntityDefinition, * eu.esdihumboldt.hale.common.instance.model.Filter) */ @Override public EntityDefinition editConditionContext(final EntityDefinition sibling, Filter filter) { List<ChildContext> path = sibling.getPropertyPath(); if (sibling.getSchemaSpace() == SchemaSpaceID.TARGET && path.isEmpty()) { // not supported for target type entities // XXX throw exception instead? return null; } // Check whether there actually is a change. If not, we are done. Condition oldCondition = AlignmentUtil.getContextCondition(sibling); if (Objects.equal(filter, oldCondition == null ? null : oldCondition.getFilter())) return sibling; // Create the new entity. Do not add context yet, since the user could // still abort the process (see below). EntityDefinition newDef = AlignmentUtil.getDefaultEntity(sibling); if (filter != null) newDef = createWithCondition(sibling, new Condition(filter)); AlignmentService as = PlatformUI.getWorkbench().getService(AlignmentService.class); Alignment alignment = as.getAlignment(); // Collect cells to replace. // All cells of the EntityDefinition's type can be affected. Collection<? extends Cell> potentiallyAffected = alignment.getCells(sibling.getType(), sibling.getSchemaSpace()); Predicate<Cell> associatedCellPredicate = new Predicate<Cell>() { @Override public boolean apply(Cell input) { return input != null && AlignmentUtil.associatedWith(sibling, input, false, true); } }; Collection<? extends Cell> affected = new HashSet<Cell>( Collections2.filter(potentiallyAffected, associatedCellPredicate)); // Check whether base alignment cells are affected. boolean baseCellsAffected = false; Predicate<Cell> baseCellPredicate = new Predicate<Cell>() { @Override public boolean apply(Cell input) { return input != null && input.isBaseCell(); } }; if (Iterables.find(affected, baseCellPredicate, null) != null) { // Check whether the user wants to continue. final Display display = PlatformUI.getWorkbench().getDisplay(); final AtomicBoolean abort = new AtomicBoolean(); display.syncExec(new Runnable() { @Override public void run() { MessageBox mb = new MessageBox(display.getActiveShell(), SWT.YES | SWT.NO | SWT.ICON_QUESTION); mb.setMessage( "Some base alignment cells reference the entity definition you wish to change.\n" + "The change will only affect cells which aren't from any base alignment.\n\n" + "Do you still wish to continue?"); mb.setText("Continue?"); abort.set(mb.open() != SWT.YES); } }); if (abort.get()) return null; // Filter base alignment cells out. baseCellsAffected = true; affected = Collections2.filter(affected, Predicates.not(baseCellPredicate)); } // No more obstacles. Finish! // Add condition context if necessary if (filter != null) addConditionContext(sibling, filter); // Replace affected (filtered) cells. Map<Cell, MutableCell> replaceMap = new HashMap<Cell, MutableCell>(); for (Cell cell : affected) { DefaultCell newCell = new DefaultCell(cell); if (newDef.getSchemaSpace() == SchemaSpaceID.SOURCE) newCell.setSource(replace(newCell.getSource(), sibling, newDef)); else newCell.setTarget(replace(newCell.getTarget(), sibling, newDef)); replaceMap.put(cell, newCell); } as.replaceCells(replaceMap); // Remove old condition context, if it was neither the default context, // nor do any base alignment cells still use it. if (oldCondition != null && !baseCellsAffected) removeContext(sibling); return newDef; } /** * Creates a new ListMultimap with all occurrences of originalDef replaced * by newDef. newDef must be a sibling of originalDef. * * @param entities the original list * @param originalDef the entity definition to be replaced * @param newDef the entity definition to use * @return a new list */ private ListMultimap<String, ? extends Entity> replace( ListMultimap<String, ? extends Entity> entities, EntityDefinition originalDef, EntityDefinition newDef) { ListMultimap<String, Entity> newList = ArrayListMultimap.create(); for (Entry<String, ? extends Entity> entry : entities.entries()) { EntityDefinition entryDef = entry.getValue().getDefinition(); Entity newEntry; if (AlignmentUtil.isParent(originalDef, entryDef)) { if (entry.getValue() instanceof Type) { // entry is a Type, so the changed Definition must be a // Type, too. newEntry = new DefaultType((TypeEntityDefinition) newDef); } else if (entry.getValue() instanceof Property) { // entry is a Property, check changed Definition. if (originalDef.getPropertyPath().isEmpty()) { // Type changed. newEntry = new DefaultProperty(new PropertyEntityDefinition( newDef.getType(), entryDef.getPropertyPath(), entryDef.getSchemaSpace(), newDef.getFilter())); } else { // Some element of the property path changed. List<ChildContext> newPath = new ArrayList<ChildContext>( entryDef.getPropertyPath()); int lastIndexOfChangedDef = newDef.getPropertyPath().size() - 1; newPath.set(lastIndexOfChangedDef, newDef.getPropertyPath().get(lastIndexOfChangedDef)); newEntry = new DefaultProperty( new PropertyEntityDefinition(entryDef.getType(), newPath, entryDef.getSchemaSpace(), entryDef.getFilter())); } } else { throw new IllegalStateException("Entity is neither a Type nor a Property."); } } else { newEntry = entry.getValue(); } newList.put(entry.getKey(), newEntry); } return newList; } /** * @see EntityDefinitionService#getTypeEntities(TypeDefinition, * SchemaSpaceID) */ @Override public Collection<? extends TypeEntityDefinition> getTypeEntities(TypeDefinition type, SchemaSpaceID schemaSpace) { TypeEntityDefinition ted = new TypeEntityDefinition(type, schemaSpace, null); Set<Condition> conditions; synchronized (conditionContexts) { conditions = conditionContexts.get(ted); } List<TypeEntityDefinition> result = new ArrayList<TypeEntityDefinition>(); // add default type entity result.add(ted); // type entity definitions with filters for (Condition condition : conditions) { result.add(new TypeEntityDefinition(type, schemaSpace, condition.getFilter())); } return result; } /** * Add missing contexts for the given cells * * @param cells the cells */ protected void addMissingContexts(Iterable<? extends Cell> cells) { for (Cell cell : cells) { Collection<EntityDefinition> addedContexts = new ArrayList<EntityDefinition>(); synchronized (namedContexts) { synchronized (indexContexts) { synchronized (conditionContexts) { if (cell.getSource() != null) { addedContexts .addAll(addMissingEntityContexts(cell.getSource().values())); } addedContexts.addAll(addMissingEntityContexts(cell.getTarget().values())); } } } if (!addedContexts.isEmpty()) { notifyContextsAdded(addedContexts); } } } /** * Add missing contexts for the given entities * * @param entities the entities * @return all entity definitions for which new contexts have been added */ private Collection<EntityDefinition> addMissingEntityContexts( Iterable<? extends Entity> entities) { Collection<EntityDefinition> addedContexts = new ArrayList<EntityDefinition>(); for (Entity entity : entities) { EntityDefinition entityDef = entity.getDefinition(); addContexts(entityDef, addedContexts); } return addedContexts; } @Override public void addContexts(EntityDefinition entityDef) { Collection<EntityDefinition> addedContexts = new ArrayList<EntityDefinition>(); synchronized (namedContexts) { synchronized (indexContexts) { synchronized (conditionContexts) { addContexts(entityDef, addedContexts); } } } if (!addedContexts.isEmpty()) { notifyContextsAdded(addedContexts); } } /** * Add the missing contexts for the given entity definition. * * @param entityDef the entity definition to add contexts for * @param addedContexts a collection where newly created contexts must be * added */ private void addContexts(EntityDefinition entityDef, Collection<EntityDefinition> addedContexts) { // collect the entity definition and all of its parents LinkedList<EntityDefinition> hierarchy = new LinkedList<EntityDefinition>(); EntityDefinition parent = entityDef; while (parent != null) { hierarchy.addFirst(parent); parent = getParent(parent); } // check if the entity definitions are known starting with the // topmost parent for (EntityDefinition candidate : hierarchy) { Integer contextName = AlignmentUtil.getContextName(candidate); Integer contextIndex = AlignmentUtil.getContextIndex(candidate); Condition contextCondition = AlignmentUtil.getContextCondition(candidate); if (contextName != null || contextIndex != null || contextCondition != null) { if (contextName != null && contextIndex == null && contextCondition == null) { // add named context boolean added = namedContexts.put(AlignmentUtil.getDefaultEntity(candidate), contextName); if (added) { addedContexts.add(candidate); } } else if (contextIndex != null && contextName == null && contextCondition == null) { // add index context boolean added = indexContexts.put(AlignmentUtil.getDefaultEntity(candidate), contextIndex); if (added) { addedContexts.add(candidate); } } else if (contextCondition != null && contextName == null && contextIndex == null) { // add condition context boolean added = conditionContexts.put(AlignmentUtil.getDefaultEntity(candidate), contextCondition); if (added) { addedContexts.add(candidate); } } else { throw new IllegalArgumentException("Illegal combination of instance contexts"); } } } } /** * @see EntityDefinitionService#removeContext(EntityDefinition) */ @Override public void removeContext(EntityDefinition entity) { EntityDefinition def = AlignmentUtil.getDefaultEntity(entity); // XXX any checks? Alignment must still be valid! see also // InstanceContextTester List<ChildContext> path = entity.getPropertyPath(); if (path.isEmpty()) { // type entity definition Filter filter = entity.getFilter(); if (filter != null) { synchronized (conditionContexts) { conditionContexts.remove(def, new Condition(filter)); } // XXX what about the children of this context? } notifyContextRemoved(entity); return; } boolean removed = false; ChildContext lastContext = path.get(path.size() - 1); if (lastContext.getContextName() != null) { synchronized (namedContexts) { namedContexts.remove(def, lastContext.getContextName()); } removed = true; } if (lastContext.getIndex() != null) { synchronized (indexContexts) { indexContexts.remove(def, lastContext.getIndex()); } removed = true; } if (lastContext.getCondition() != null) { synchronized (conditionContexts) { conditionContexts.remove(def, lastContext.getCondition()); } removed = true; } if (removed) { notifyContextRemoved(entity); } } /** * @see EntityDefinitionService#getChildren(EntityDefinition) */ @Override public Collection<? extends EntityDefinition> getChildren(EntityDefinition entity) { List<ChildContext> path = entity.getPropertyPath(); Collection<? extends ChildDefinition<?>> children; if (path == null || path.isEmpty()) { // entity is a type, children are the type children children = entity.getType().getChildren(); } else { // get parent context ChildContext parentContext = path.get(path.size() - 1); if (parentContext.getChild().asGroup() != null) { children = parentContext.getChild().asGroup().getDeclaredChildren(); } else if (parentContext.getChild().asProperty() != null) { children = parentContext.getChild().asProperty().getPropertyType().getChildren(); } else { throw new IllegalStateException("Illegal child definition type encountered"); } } if (children == null || children.isEmpty()) { return Collections.emptyList(); } Collection<EntityDefinition> result = new ArrayList<EntityDefinition>(children.size()); for (ChildDefinition<?> child : children) { // add default child entity definition to result ChildContext context = new ChildContext(child); EntityDefinition defaultEntity = createEntity(entity.getType(), createPath(entity.getPropertyPath(), context), entity.getSchemaSpace(), entity.getFilter()); result.add(defaultEntity); // look up additional instance contexts and add them synchronized (namedContexts) { for (Integer contextName : namedContexts.get(defaultEntity)) { ChildContext namedContext = new ChildContext(contextName, null, null, child); EntityDefinition namedChild = createEntity(entity.getType(), createPath(entity.getPropertyPath(), namedContext), entity.getSchemaSpace(), entity.getFilter()); result.add(namedChild); } } synchronized (indexContexts) { for (Integer index : indexContexts.get(defaultEntity)) { ChildContext indexContext = new ChildContext(null, index, null, child); EntityDefinition indexChild = createEntity(entity.getType(), createPath(entity.getPropertyPath(), indexContext), entity.getSchemaSpace(), entity.getFilter()); result.add(indexChild); } } synchronized (conditionContexts) { for (Condition condition : conditionContexts.get(defaultEntity)) { ChildContext conditionContext = new ChildContext(null, null, condition, child); EntityDefinition conditionChild = createEntity(entity.getType(), createPath(entity.getPropertyPath(), conditionContext), entity.getSchemaSpace(), entity.getFilter()); result.add(conditionChild); } } } return result; } /** * Create a property path * * @param parentPath the parent path * @param context the child context * @return the property path including the child context */ private static List<ChildContext> createPath(List<ChildContext> parentPath, ChildContext context) { if (parentPath == null || parentPath.isEmpty()) { return Collections.singletonList(context); } else { List<ChildContext> result = new ArrayList<ChildContext>(parentPath); result.add(context); return result; } } }