/*
* Copyright (c) 2006, 2010 Borland Software Corporation 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:
* Michael Golubev (Borland) - initial API and implementation
*/
package org.eclipse.gmf.internal.common.reconcile;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature.Setting;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.util.FeatureMapUtil;
import org.eclipse.gmf.internal.common.Activator;
public class Reconciler {
private final ReconcilerConfig myConfig;
// Maps EObject referenced elsewhere to a setting representing the use
private final Map<EObject, List<Setting>> myCrossRefsToFix;
private final Map<EObject, EObject> myMatches;
private boolean myIsMatching;
private final boolean traceMatches;
private final boolean traceDecision;
private final boolean traceFeatureInDecision;
private final boolean traceCrossRefUpdate;
public Reconciler(ReconcilerConfig config){
myConfig = config;
myCrossRefsToFix = new LinkedHashMap<EObject, List<Setting>>();
myMatches = new LinkedHashMap<EObject, EObject>();
final String recon = "/reconciler/", op1 = "traceMatches", op2 = "traceDecision", op3 = "/features", op4 = "traceCrossRefUpdate";
traceMatches = Boolean.parseBoolean(Platform.getDebugOption(Activator.getID() + recon + op1));
traceDecision = Boolean.parseBoolean(Platform.getDebugOption(Activator.getID() + recon + op2));
traceFeatureInDecision = Boolean.parseBoolean(Platform.getDebugOption(Activator.getID() + recon + op2 + op3));
traceCrossRefUpdate = Boolean.parseBoolean(Platform.getDebugOption(Activator.getID() + recon + op4));
}
protected void handleNotMatchedCurrent(EObject current){
Cleaner cleaner = myConfig.getCleaner(current.eClass());
cleaner.clear(current);
}
protected EObject handleNotMatchedOld(EObject currentParent, EObject notMatchedOld) {
Copier copier = myConfig.getCopier(notMatchedOld.eClass());
return copier.copyToCurrent(currentParent, notMatchedOld, this);
}
public void reconcileResource(Resource current, Resource old){
reconcileContents(null, current.getContents(), old.getContents());
updateCrossReferences();
}
public void reconcileTree(EObject currentRoot, EObject oldRoot){
internalReconcileTree(currentRoot, oldRoot);
updateCrossReferences();
}
protected void reconcileVertex(EObject current, EObject old){
assert current.eClass().equals(old.eClass());
registerMatch(current, old);
for (Decision decision : myConfig.getDecisions(current.eClass())){
decision.apply(current, old);
if (traceDecision) {
trace(traceFeatureInDecision ? "[decision] %s (%s)" : "[decision] %s", decision.getClass().getName(), decision.getFeature().getName());
}
}
}
protected void internalReconcileTree(EObject currentRoot, EObject oldRoot){
reconcileVertex(currentRoot, oldRoot);
reconcileContents(currentRoot, currentRoot.eContents(), oldRoot.eContents());
}
protected void registerMatch(EObject current, EObject old) {
myMatches.put(old, current);
if (traceMatches) {
trace("[matched]%s -> %s", old.eClass().getName(), current.eClass().getName());
}
}
protected void updateCrossReferences() {
for (Map.Entry<EObject, List<Setting>> e : myCrossRefsToFix.entrySet()) {
final EObject oldReferenceTarget = e.getKey();
if (myMatches.containsKey(oldReferenceTarget)) {
EObject copied = myMatches.get(oldReferenceTarget);
if (traceCrossRefUpdate) {
trace("[crossRefUpd] matched %s -> %s", oldReferenceTarget, copied);
}
for (Setting s : e.getValue()) {
if (myMatches.containsKey(s.getEObject())) {
EObject newOwner = myMatches.get(s.getEObject());
if (traceCrossRefUpdate) {
trace("[crossRefUpd] updating '%s' value of %s", s.getEStructuralFeature().getName(), newOwner);
}
if (s.getEStructuralFeature().isMany() || FeatureMapUtil.isMany(s.getEObject(), s.getEStructuralFeature())) {
@SuppressWarnings("unchecked")
List<EObject> values = (List<EObject>) newOwner.eGet(s.getEStructuralFeature());
assert !values.contains(copied); // sanity, wonder if that may happen, ever
if (values.contains(oldReferenceTarget)) {
// replace old value, keep position
values.set(values.indexOf(oldReferenceTarget), copied);
} else {
values.add(copied);
}
} else {
newOwner.eSet(s.getEStructuralFeature(), copied);
}
} else {
if (traceCrossRefUpdate) {
trace("[crossRefUpd] no matching owner for %s (old owner: %s)", s.getEStructuralFeature().getName(), s.getEObject());
}
}
}
} else {
if (traceCrossRefUpdate) {
trace("[crossRefUpd] no match for old %s", oldReferenceTarget);
}
}
}
// TODO Auto-generated method stub
}
/* package-local */void registerCrossReferencesToUpdate(Map<EObject, Collection<Setting>> crossReferences) {
for (Map.Entry<EObject, Collection<Setting>> e : crossReferences.entrySet()) {
List<Setting> entries = myCrossRefsToFix.get(e.getKey());
if (entries == null) {
entries = new LinkedList<Setting>();
myCrossRefsToFix.put(e.getKey(), entries);
}
for (Setting s : e.getValue()) {
if (s.getEStructuralFeature().isChangeable()) {
entries.add(s);
}
}
if (entries.isEmpty()) {
myCrossRefsToFix.remove(e.getKey()); // none changeable, no reason to keep empty list
}
}
}
private void reconcileContents(EObject currentParent, Collection<EObject> allCurrents, Collection<EObject> allOlds) {
if (allCurrents.isEmpty() && allOlds.isEmpty()) {
return;
}
final List<Pair> storage = new LinkedList<Pair>();
match(allCurrents, allOlds, storage);
for (Pair next : storage) {
EObject nextCurrent = next.current;
EObject nextOld = next.old;
assert (nextCurrent != null || nextOld != null);
if (nextCurrent == null) {
if (currentParent != null) { // never copy top-level resource contents
nextCurrent = handleNotMatchedOld(currentParent, nextOld);
}
}
if (nextCurrent != null && nextOld != null) {
internalReconcileTree(nextCurrent, nextOld);
} else if (nextOld == null) {
handleNotMatchedCurrent(nextCurrent);
}
}
}
private void match(Collection<EObject> currents, Collection<EObject> olds, Collection<Pair> output) {
assert !myIsMatching;
final Collection<EObject> myOlds;
final Collection<EObject> myCurrents;
try {
myIsMatching = true;
myOlds = new LinkedHashSet<EObject>(olds);
myCurrents = new LinkedList<EObject>(currents);
for (Iterator<EObject> currentContents = myCurrents.iterator(); !myOlds.isEmpty() && currentContents.hasNext();) {
EObject nextCurrent = currentContents.next();
EObject matchedOld = removeMatched(nextCurrent, myOlds);
output.add(new Pair(nextCurrent, matchedOld));
currentContents.remove();
}
for (Iterator<EObject> notMatchedOlds = myOlds.iterator(); notMatchedOlds.hasNext();) {
output.add(new Pair(null, notMatchedOlds.next()));
}
} finally {
myIsMatching = false;
}
}
private EObject removeMatched(EObject current, Collection<EObject> allOld) {
EClass eClass = current.eClass();
Matcher matcher = myConfig.getMatcher(eClass);
EObject result = null;
if (matcher != Matcher.FALSE) {
for (Iterator<EObject> all = allOld.iterator(); all.hasNext();) {
EObject next = all.next();
if (eClass.equals(next.eClass()) && matcher.match(current, next)) {
result = next;
all.remove();
break;
}
}
}
return result;
}
private static void trace(String format, Object... args) {
Activator.log(new Status(IStatus.INFO, Activator.getID(), String.format(format, args)));
}
private static class Pair {
public final EObject current;
public final EObject old;
public Pair(EObject cur, EObject old) {
this.current = cur;
this.old = old;
}
}
}