/******************************************************************************* * Copyright (c) 2014 Fraunhofer IWU and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Fraunhofer IWU - initial API and implementation *******************************************************************************/ package net.enilink.komma.edit.refactor; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.Map; import java.util.Queue; import java.util.Set; import net.enilink.komma.common.command.AbstractCommand; import net.enilink.komma.common.command.CommandResult; import net.enilink.komma.core.IQuery; import net.enilink.komma.core.IReference; import net.enilink.komma.core.IStatement; import net.enilink.komma.core.ITransaction; import net.enilink.komma.core.KommaException; import net.enilink.komma.core.Statement; import net.enilink.komma.core.URI; import net.enilink.komma.edit.KommaEditPlugin; import net.enilink.komma.edit.domain.AdapterFactoryEditingDomain; import net.enilink.komma.edit.domain.IEditingDomain; import net.enilink.komma.edit.refactor.Change.StatementChange; import net.enilink.komma.edit.refactor.Change.StatementChange.Type; import net.enilink.komma.em.concepts.IResource; import net.enilink.komma.em.util.ISparqlConstants; import net.enilink.komma.model.IModel; import net.enilink.komma.model.IModelAware; import net.enilink.komma.model.IModelSet; import net.enilink.komma.model.IObject; import net.enilink.vocab.owl.OWL; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; public class RefactoringProcessor { protected IEditingDomain domain; public RefactoringProcessor(IEditingDomain domain) { this.domain = domain; } /** * Create a collection of changes resulting in the move (and possibly * renaming) of the given elements into the given target model. * * @param elements * The elements to be moved. * @param targetModel * The target model. * @param keepNamespace * Flag indicating if the elements should keep their original * namespace or be renamed to the target model's namespace. * @return The changes (statements to be added or removed) to source and * target models. */ public Collection<Change> createMoveChanges(Collection<?> elements, IModel targetModel, boolean keepNamespace) { Map<IModel, Collection<IStatement>> addMap = new LinkedHashMap<IModel, Collection<IStatement>>(); Map<IModel, Collection<IStatement>> removeMap = new LinkedHashMap<IModel, Collection<IStatement>>(); Map<IReference, IReference> renameMap = new LinkedHashMap<IReference, IReference>(); Set<IModel> models = new LinkedHashSet<IModel>(); Collection<IObject> objects = new LinkedHashSet<IObject>(); for (Object wrappedObject : elements) { Object object = AdapterFactoryEditingDomain.unwrap(wrappedObject); if (object instanceof IObject) { models.add(((IModelAware) object).getModel()); objects.add((IObject) object); // walks object contents transitively by using a queue // does not need any inferencer Queue<IResource> queue = new LinkedList<IResource>( ((IResource) object).getContents()); while (!queue.isEmpty()) { IResource contentObject = queue.remove(); objects.add((IObject) contentObject); queue.addAll(contentObject.getContents()); } } } if (!keepNamespace) { for (IObject object : objects) { renameMap.put( object, targetModel.getURI().appendFragment( object.getURI().fragment())); } } // determine changes for the move operation itself Set<IReference> necessaryImports = new LinkedHashSet<IReference>(); for (IModel model : models) { if (domain.isReadOnly(model)) { continue; } // for this source model, get all statements belonging to selected // objects and mark them as moved to the target model for (IObject object : objects) { // determine CBD with object as root node // see http://answers.semanticweb.com/questions/26220 IQuery<?> subjectQuery = model.getManager().createQuery( ISparqlConstants.PREFIX // + "CONSTRUCT { " // + " ?s ?p ?o" // + "} WHERE { " // + " GRAPH ?g { " // + " { " // + " bind(?object as ?s) " // + " } union { " // + " ?object (!<a:b>|!<b:a>)+ ?s . " // + " FILTER (isBlank(?s)) " // + " } " // + " ?s ?p ?o ." // + " } " // + "}", false); // no inference subjectQuery.setParameter("g", model); // restrict to own graph subjectQuery.setParameter("object", object); Collection<IStatement> statements = subjectQuery.evaluate( IStatement.class).toList(); // the query above results in a graph that contains the CBD, but // may also contain extra statements (see the linked discussion) // filter these out for now // FIXME: use the two-query solution with some transient store statements = filterCBD(statements, object); // mark statements as removed from source model addToMap(removeMap, model, statements); // mark statements (possibly changed) as added to target model addToMap(addMap, targetModel, applyRenames(statements, renameMap)); } // determine if target model imports all source model imports necessaryImports.addAll(model.getImports()); necessaryImports.remove(targetModel); necessaryImports.removeAll(targetModel.getImports()); } // add the additional import statements to the target model Collection<IStatement> importStatements = new LinkedHashSet<IStatement>(); for (IReference uri : necessaryImports) { importStatements.add(new Statement(targetModel.getURI(), OWL.PROPERTY_IMPORTS, uri)); } addToMap(addMap, targetModel, importStatements); Set<IModel> affectedModels = new LinkedHashSet<IModel>(); affectedModels.addAll(addMap.keySet()); affectedModels.addAll(removeMap.keySet()); // change all statements involving renamed references in affected models for (IReference ref : renameMap.keySet()) { for (IModel model : affectedModels) { if (domain.isReadOnly(model)) { continue; } IQuery<?> objectQuery = model.getManager().createQuery( ISparqlConstants.PREFIX // + "CONSTRUCT { " // + " ?s ?p ?ref . " // + " ?s ?ref ?o . " // + " ?ref ?p ?o . " // + "} WHERE {" // + " GRAPH ?g { " // + " { " // + " ?s ?p ?ref . " // + " } UNION { " // + " ?s ?ref ?o . " // + " } UNION { " // + " ?ref ?p ?o . " // + " } " // + " } " // + "}", false); objectQuery.setParameter("g", model); // restrict to own graph objectQuery.setParameter("ref", ref); Collection<IStatement> objectRefStatements = objectQuery .evaluate(IStatement.class).toList(); // ignore those already marked as removed for (Iterator<IStatement> it = objectRefStatements.iterator(); it .hasNext();) { IStatement st = it.next(); if (removeMap.get(model) != null && removeMap.get(model).contains(st)) { it.remove(); } } // remove the statements referring to the old reference addToMap(removeMap, model, objectRefStatements); // add the statements referring to the new reference addToMap(addMap, model, applyRenames(objectRefStatements, renameMap)); } } // create the changes Collection<Change> result = new ArrayList<Change>(); for (IModel model : affectedModels) { Collection<StatementChange> changes = new LinkedHashSet<StatementChange>(); if (addMap.get(model) != null) { for (IStatement st : addMap.get(model)) { changes.add(new StatementChange(st, Type.ADD)); } } if (removeMap.get(model) != null) { for (IStatement st : removeMap.get(model)) { changes.add(new StatementChange(st, Type.REMOVE)); } } if (!changes.isEmpty()) { result.add(new Change(model, changes)); } } return result; } /** * Create a collection of changes resulting in the renaming of the given * elements using the given target URIs. * * @param elements * A map of elements and their new URIs. * @return The changes (statements to be added or removed) to source and * target models. */ public Collection<Change> createRenameChanges(Map<?, IReference> elements) { Map<IModel, Collection<IStatement>> addMap = new LinkedHashMap<IModel, Collection<IStatement>>(); Map<IModel, Collection<IStatement>> removeMap = new LinkedHashMap<IModel, Collection<IStatement>>(); Set<IModel> models = new LinkedHashSet<IModel>(); Map<IObject, IReference> renameMap = new LinkedHashMap<IObject, IReference>(); for (Map.Entry<?, IReference> entry : elements.entrySet()) { Object wrappedObject = entry.getKey(); Object object = AdapterFactoryEditingDomain.unwrap(wrappedObject); if (object instanceof IObject) { models.add(((IModelAware) object).getModel()); renameMap.put((IObject) object, entry.getValue()); } } // FIXME: get the proper list of models that are affected by this change // it can not be generally assumed to just be the selection's model(s) Set<IModel> affectedModels = new LinkedHashSet<IModel>(); for (IModel model : models) { affectedModels.add(model); for (URI importedModelURI : model.getImportsClosure()) { affectedModels.add(model.getModelSet().getModel( importedModelURI, false)); } } // change all statements involving renamed references in affected models for (IReference ref : renameMap.keySet()) { for (IModel model : affectedModels) { if (domain.isReadOnly(model)) { continue; } IQuery<?> objectQuery = model.getManager().createQuery( ISparqlConstants.PREFIX // + "CONSTRUCT { " // + " ?s ?p ?ref . " // + " ?s ?ref ?o . " // + " ?ref ?p ?o . " // + "} WHERE {" // + " GRAPH ?g { " // + " { " // + " ?s ?p ?ref . " // + " } UNION { " // + " ?s ?ref ?o . " // + " } UNION { " // + " ?ref ?p ?o . " // + " } " // + " } " // + "}", false); objectQuery.setParameter("g", model); // restrict to own graph objectQuery.setParameter("ref", ref); Collection<IStatement> objectRefStatements = objectQuery .evaluate(IStatement.class).toList(); // remove the statements referring to the old reference addToMap(removeMap, model, objectRefStatements); // add the statements referring to the new reference addToMap(addMap, model, applyRenames(objectRefStatements, renameMap)); } } // create the changes Collection<Change> result = new ArrayList<Change>(); for (IModel model : affectedModels) { Collection<StatementChange> changes = new LinkedHashSet<StatementChange>(); if (addMap.get(model) != null) { for (IStatement st : addMap.get(model)) { changes.add(new StatementChange(st, Type.ADD)); } } if (removeMap.get(model) != null) { for (IStatement st : removeMap.get(model)) { changes.add(new StatementChange(st, Type.REMOVE)); } } if (!changes.isEmpty()) { result.add(new Change(model, changes)); } } return result; } // FIXME: use a proper transient store/manager to do this private Collection<IStatement> filterCBD(Collection<IStatement> statements, IReference root) { Set<IReference> objects = new LinkedHashSet<IReference>(); objects.add(root); for (IStatement st : statements) { if (st.getObject() instanceof IReference) { objects.add((IReference) st.getObject()); } } for (Iterator<IStatement> it = statements.iterator(); it.hasNext();) { IStatement st = it.next(); if (!objects.contains(st.getSubject())) { it.remove(); } } return statements; } private boolean addToMap(Map<IModel, Collection<IStatement>> map, IModel model, Collection<IStatement> statements) { if (statements == null || statements.isEmpty()) { return false; } Collection<IStatement> mappedStatements = map.get(model); if (mappedStatements == null) { mappedStatements = new LinkedHashSet<IStatement>(); map.put(model, mappedStatements); } return mappedStatements.addAll(statements); } private Collection<IStatement> applyRenames( Collection<IStatement> statements, Map<? extends IReference, IReference> renameMap) { if (statements == null || statements.isEmpty() // || renameMap == null || renameMap.isEmpty()) { return statements; } Collection<IStatement> result = new LinkedHashSet<IStatement>(); for (IStatement statement : statements) { IReference subject = statement.getSubject(); IReference predicate = statement.getPredicate(); Object object = statement.getObject(); if (renameMap.containsKey(subject)) { subject = renameMap.get(subject); } if (renameMap.containsKey(predicate)) { predicate = renameMap.get(predicate); } if (renameMap.containsKey(object)) { object = renameMap.get(object); } result.add(new Statement(subject, predicate, object, statement .getContext())); } return result; } /** * Command for applying the given changes. Implements undo and redo itself, * using the added/removed statements or their reversal. * <p> * Returns all references that appear as subjects of statements as its * result. */ private class ApplyChangesCommand extends AbstractCommand implements AbstractCommand.INoChangeRecording { protected Collection<Change> changes; public ApplyChangesCommand(Collection<Change> changes) { this.changes = changes; } @Override protected boolean prepare() { return true; } @Override public Collection<?> getAffectedResources(Object type) { if (IModel.class.equals(type)) { Collection<IModel> models = new LinkedHashSet<IModel>(); for (Change change : changes) { models.add(change.getModel()); } return models; } if (IModelSet.class.equals(type)) { Collection<IModelSet> modelSets = new LinkedHashSet<IModelSet>(); for (Change change : changes) { IModel model = change.getModel(); if (!modelSets.contains(model.getModelSet())) { modelSets.add(model.getModelSet()); } } return modelSets; } return super.getAffectedResources(type); } @Override protected CommandResult doExecuteWithResult( IProgressMonitor progressMonitor, IAdaptable info) throws ExecutionException { return internalApplyChanges(progressMonitor, info, false); } @Override protected CommandResult doRedoWithResult( IProgressMonitor progressMonitor, IAdaptable info) throws ExecutionException { // redo is identical to execute return internalApplyChanges(progressMonitor, info, false); } @Override protected CommandResult doUndoWithResult( IProgressMonitor progressMonitor, IAdaptable info) throws ExecutionException { // undo reverts the changes return internalApplyChanges(progressMonitor, info, true); } protected CommandResult internalApplyChanges( IProgressMonitor progressMonitor, IAdaptable info, boolean revert) throws ExecutionException { Collection<IModelSet> modelSets = new LinkedHashSet<IModelSet>(); Collection<ITransaction> transactions = new ArrayList<ITransaction>(); for (Change change : changes) { IModel model = change.getModel(); if (!modelSets.contains(model.getModelSet())) { model.getModelSet().getUnitOfWork().begin(); modelSets.add(model.getModelSet()); transactions.add(model.getManager().getTransaction()); } } Collection<ITransaction> startedTransactions = new ArrayList<ITransaction>(); for (ITransaction t : transactions) { if (!t.isActive()) { t.begin(); startedTransactions.add(t); } } try { Collection<IReference> affectedObjects = new LinkedHashSet<IReference>(); for (Change change : changes) { Collection<IStatement> remove = new LinkedHashSet<IStatement>(); Collection<IStatement> add = new LinkedHashSet<IStatement>(); for (StatementChange statementChange : change .getStatementChanges()) { affectedObjects.add(statementChange.getStatement() .getSubject()); if (statementChange.getType() == Type.REMOVE) { remove.add(statementChange.getStatement()); } if (statementChange.getType() == Type.ADD) { add.add(statementChange.getStatement()); } } // apply the changes as recorded for execute and redo // revert the recorded changes for undo change.getModel().getManager() .remove(revert ? add : remove); change.getModel().getManager() .add(revert ? remove : add, true); } for (ITransaction t : startedTransactions) { t.commit(); } return CommandResult.newOKCommandResult(affectedObjects); } catch (KommaException ke) { for (ITransaction t : startedTransactions) { if (t.isActive()) { t.rollback(); } } return CommandResult.newErrorCommandResult(ke); } finally { for (IModelSet modelSet : modelSets) { modelSet.getUnitOfWork().end(); } } } }; /** * Apply the given collection of changes to the respective models. * * @param changes * The changes generated earlier. * @return Flag indicating result status. */ public IStatus applyChanges(Collection<Change> changes, IProgressMonitor progressMonitor, IAdaptable info) { try { return domain.getCommandStack().execute( new ApplyChangesCommand(changes), progressMonitor, info); } catch (Exception e) { return new Status(IStatus.ERROR, KommaEditPlugin.PLUGIN_ID, e.getMessage(), e); } } }