/*******************************************************************************
* Copyright (c) 2000, 2007 IBM 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
*
*******************************************************************************/
package com.aptana.editor.php.internal.model.impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.aptana.editor.php.core.model.IModelElement;
import com.aptana.editor.php.core.model.IModelElementDelta;
import com.aptana.editor.php.core.model.IParent;
import com.aptana.editor.php.core.model.env.MemberElementInfo;
import com.aptana.editor.php.core.model.env.ModelElementInfo;
import com.aptana.editor.php.core.model.env.SourceMethodElementInfo;
import com.aptana.editor.php.internal.model.ModelElementDelta;
import com.aptana.editor.php.internal.model.impl.env.SourceTypeElementInfo;
/**
* A script element delta biulder creates a script element delta on a script element between the version of the script
* element at the time the comparator was created and the current version of the script element. It performs this
* operation by locally caching the contents of the script element when it is created. When the method createDeltas() is
* called, it creates a delta over the cached contents and the new contents.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public class ModelElementDeltaBuilder
{
/**
* The model element handle
*/
IModelElement modelElement;
/**
* The maximum depth in the script element children we should look into
*/
int maxDepth = Integer.MAX_VALUE;
/**
* The old handle to info relationships
*/
Map infos;
/**
* The old position info
*/
Map oldPositions;
/**
* The new position info
*/
Map newPositions;
/**
* Change delta
*/
ModelElementDelta delta;
/**
* List of added elements
*/
List added;
/**
* List of removed elements
*/
List removed;
/**
* Doubly linked list item
*/
static class ListItem
{
public IModelElement previous;
public IModelElement next;
protected ListItem(IModelElement previous, IModelElement next)
{
this.previous = previous;
this.next = next;
}
}
/**
* Creates a script element comparator on a script element looking as deep as necessary.
*/
public ModelElementDeltaBuilder(IModelElement modelElement)
{
this.modelElement = modelElement;
this.initialize();
this.recordElementInfo(modelElement, (SourceModel) this.modelElement.getModel(), 0);
}
/**
* Creates a script element comparator on a script element looking only 'maxDepth' levels deep.
*/
public ModelElementDeltaBuilder(IModelElement modelElement, int maxDepth)
{
this.modelElement = modelElement;
this.maxDepth = maxDepth;
this.initialize();
this.recordElementInfo(modelElement, (SourceModel) this.modelElement.getModel(), 0);
}
/**
* Repairs the positioning information after an element has been added
*/
private void added(IModelElement element)
{
this.added.add(element);
ListItem current = this.getNewPosition(element);
ListItem previous = null, next = null;
if (current.previous != null)
previous = this.getNewPosition(current.previous);
if (current.next != null)
next = this.getNewPosition(current.next);
if (previous != null)
previous.next = current.next;
if (next != null)
next.previous = current.previous;
}
/**
* Builds the script element deltas between the old content of the compilation unit and its new content.
*
* @return delta
*/
public ModelElementDelta buildDeltas()
{
this.delta = new ModelElementDelta(modelElement);
// if building a delta on a compilation unit or below,
// it's a fine grained delta
if (modelElement.getElementType() >= IModelElement.MODULE)
{
this.delta.fineGrained();
}
this.recordNewPositions(this.modelElement, 0);
this.findAdditions(this.modelElement, 0);
this.findDeletions();
this.findChangesInPositioning(this.modelElement, 0);
this.trimDelta(this.delta);
if (this.delta.getAffectedChildren().length == 0)
{
// this is a fine grained but not children affected -> mark as
// content changed
this.delta.contentChanged();
}
return this.delta;
}
// private boolean equals(char[][][] first, char[][][] second) {
// if (first == second)
// return true;
// if (first == null || second == null)
// return false;
// if (first.length != second.length)
// return false;
//
// for (int i = first.length; --i >= 0;)
// if (!CharOperation.equals(first[i], second[i]))
// return false;
// return true;
// }
/**
* Finds elements which have been added or changed.
*/
private void findAdditions(IModelElement newElement, int depth)
{
ModelElementInfo oldInfo = this.getElementInfo(newElement);
if (oldInfo == null && depth < this.maxDepth)
{
this.delta.added(newElement);
added(newElement);
}
else
{
this.removeElementInfo(newElement);
}
if (depth >= this.maxDepth)
{
// mark element as changed
this.delta.changed(newElement, IModelElementDelta.F_CONTENT);
return;
}
ModelElementInfo newInfo = null;
newInfo = (ModelElementInfo) ((AbstractModelElement) newElement).getElementInfo();
this.findContentChange(oldInfo, newInfo, newElement);
if (oldInfo != null && newElement instanceof IParent)
{
IModelElement[] children = newInfo.getChildren();
if (children != null)
{
int length = children.length;
for (int i = 0; i < length; i++)
{
this.findAdditions(children[i], depth + 1);
}
}
}
}
/**
* Looks for changed positioning of elements.
*/
private void findChangesInPositioning(IModelElement element, int depth)
{
if (depth >= this.maxDepth || this.added.contains(element) || this.removed.contains(element))
return;
if (!isPositionedCorrectly(element))
{
this.delta.changed(element, IModelElementDelta.F_REORDER);
}
if (element instanceof IParent)
{
ModelElementInfo info = null;
info = (ModelElementInfo) ((AbstractModelElement) element).getElementInfo();
IModelElement[] children = info.getChildren();
if (children != null)
{
int length = children.length;
for (int i = 0; i < length; i++)
{
this.findChangesInPositioning(children[i], depth + 1);
}
}
}
}
/**
* The elements are equivalent, but might have content changes.
*/
private void findContentChange(ModelElementInfo oldInfo, ModelElementInfo newInfo, IModelElement newElement)
{
if (oldInfo instanceof MemberElementInfo && newInfo instanceof MemberElementInfo)
{
if (((MemberElementInfo) oldInfo).getModifiers() != ((MemberElementInfo) newInfo).getModifiers())
{
this.delta.changed(newElement, IModelElementDelta.F_MODIFIERS);
}
else if (oldInfo instanceof SourceMethodElementInfo && newInfo instanceof SourceMethodElementInfo)
{
SourceMethodElementInfo oldSourceMethodInfo = (SourceMethodElementInfo) oldInfo;
SourceMethodElementInfo newSourceMethodInfo = (SourceMethodElementInfo) newInfo;
if (!equals(oldSourceMethodInfo.getArgumentNames(), newSourceMethodInfo.getArgumentNames())
|| !equals(oldSourceMethodInfo.getArgumentInitializers(),
newSourceMethodInfo.getArgumentInitializers()))
{
this.delta.changed(newElement, IModelElementDelta.F_CONTENT);
}
}
}
if (oldInfo instanceof SourceTypeElementInfo && newInfo instanceof SourceTypeElementInfo)
{
SourceTypeElementInfo oldSourceTypeInfo = (SourceTypeElementInfo) oldInfo;
SourceTypeElementInfo newSourceTypeInfo = (SourceTypeElementInfo) newInfo;
if (!equals(oldSourceTypeInfo.getSuperclassNames(), newSourceTypeInfo.getSuperclassNames()))
{
this.delta.changed(newElement, IModelElementDelta.F_SUPER_TYPES);
}
}
}
/**
* Adds removed deltas for any handles left in the table
*/
private void findDeletions()
{
Iterator iter = this.infos.keySet().iterator();
while (iter.hasNext())
{
IModelElement element = (IModelElement) iter.next();
this.delta.removed(element);
this.removed(element);
}
}
private ModelElementInfo getElementInfo(IModelElement element)
{
return (ModelElementInfo) this.infos.get(element);
}
private ListItem getNewPosition(IModelElement element)
{
return (ListItem) this.newPositions.get(element);
}
private ListItem getOldPosition(IModelElement element)
{
return (ListItem) this.oldPositions.get(element);
}
private void initialize()
{
this.infos = new HashMap(20);
this.oldPositions = new HashMap(20);
this.newPositions = new HashMap(20);
this.putOldPosition(this.modelElement, new ListItem(null, null));
this.putNewPosition(this.modelElement, new ListItem(null, null));
this.added = new ArrayList(5);
this.removed = new ArrayList(5);
}
/**
* Inserts position information for the elements into the new or old positions table
*/
private void insertPositions(IModelElement[] elements, boolean isNew)
{
int length = elements.length;
IModelElement previous = null, current = null, next = (length > 0) ? elements[0] : null;
for (int i = 0; i < length; i++)
{
previous = current;
current = next;
next = (i + 1 < length) ? elements[i + 1] : null;
if (isNew)
{
this.putNewPosition(current, new ListItem(previous, next));
}
else
{
this.putOldPosition(current, new ListItem(previous, next));
}
}
}
/**
* Returns whether the elements position has not changed.
*/
private boolean isPositionedCorrectly(IModelElement element)
{
ListItem oldListItem = this.getOldPosition(element);
if (oldListItem == null)
return false;
ListItem newListItem = this.getNewPosition(element);
if (newListItem == null)
return false;
IModelElement oldPrevious = oldListItem.previous;
IModelElement newPrevious = newListItem.previous;
if (oldPrevious == null)
{
return newPrevious == null;
}
else
{
return oldPrevious.equals(newPrevious);
}
}
private void putElementInfo(IModelElement element, ModelElementInfo info)
{
this.infos.put(element, info);
}
private void putNewPosition(IModelElement element, ListItem position)
{
this.newPositions.put(element, position);
}
private void putOldPosition(IModelElement element, ListItem position)
{
this.oldPositions.put(element, position);
}
/**
* Records this elements info, and attempts to record the info for the children.
*/
private void recordElementInfo(IModelElement element, SourceModel model, int depth)
{
if (depth >= this.maxDepth)
{
return;
}
ModelElementInfo info = null;
if (element instanceof AbstractModelElement)
{
info = ((AbstractModelElement) element).getElementInfo();
}
if (info == null) // no longer in the model.
return;
int sizeBeforeInsert = this.infos.size();
this.putElementInfo(element, info);
if (sizeBeforeInsert == this.infos.size())
{
return;
}
if (element instanceof IParent)
{
IModelElement[] children = info.getChildren();
if (children != null)
{
insertPositions(children, false);
for (int i = 0, length = children.length; i < length; i++)
recordElementInfo(children[i], model, depth + 1);
}
}
}
/**
* Fills the newPositions hashtable with the new position information
*/
private void recordNewPositions(IModelElement newElement, int depth)
{
if (depth < this.maxDepth && newElement instanceof IParent)
{
ModelElementInfo info = null;
info = (ModelElementInfo) ((AbstractModelElement) newElement).getElementInfo();
IModelElement[] children = info.getChildren();
if (children != null)
{
int sizeBeforeInsertion = this.newPositions.size();
insertPositions(children, true);
if (sizeBeforeInsertion == this.newPositions.size())
{
return;
}
for (int i = 0, length = children.length; i < length; i++)
{
recordNewPositions(children[i], depth + 1);
}
}
}
}
/**
* Repairs the positioning information after an element has been removed
*/
private void removed(IModelElement element)
{
this.removed.add(element);
ListItem current = this.getOldPosition(element);
ListItem previous = null, next = null;
if (current.previous != null)
previous = this.getOldPosition(current.previous);
if (current.next != null)
next = this.getOldPosition(current.next);
if (previous != null)
previous.next = current.next;
if (next != null)
next.previous = current.previous;
}
private void removeElementInfo(IModelElement element)
{
this.infos.remove(element);
}
public String toString()
{
StringBuffer buffer = new StringBuffer();
buffer.append("Built delta:\n"); //$NON-NLS-1$ // $codepro.audit.disable platformSpecificLineSeparator
buffer.append(this.delta.toString());
return buffer.toString();
}
/**
* Trims deletion deltas to only report the highest level of deletion
*/
private void trimDelta(ModelElementDelta elementDelta)
{
if (elementDelta.getKind() == IModelElementDelta.REMOVED)
{
IModelElementDelta[] children = elementDelta.getAffectedChildren();
for (int i = 0, length = children.length; i < length; i++)
{
elementDelta.removeAffectedChild((ModelElementDelta) children[i]);
}
}
else
{
IModelElementDelta[] children = elementDelta.getAffectedChildren();
for (int i = 0, length = children.length; i < length; i++)
{
trimDelta((ModelElementDelta) children[i]);
}
}
}
private static final boolean equals(String[] first, String[] second)
{
if (first == second)
return true;
if (first == null || second == null)
return false;
if (first.length != second.length)
return false;
for (int i = first.length - 1; i >= 0; i--)
{
if (first[i] == null && second[i] != null)
{
return false;
}
if (first[i] != null && second[i] == null)
{
return false;
}
if (first[i] != null && !first[i].equals(second[i]))
return false;
}
return true;
}
}