blob: b0a1b250eeede02308d0c760ec278a5bd32a69eb [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2016 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 org.eclipse.dltk.internal.core;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.PlatformObject;
import org.eclipse.dltk.core.DLTKCore;
import org.eclipse.dltk.core.IField;
import org.eclipse.dltk.core.IModelElement;
import org.eclipse.dltk.core.IModelElementMemento;
import org.eclipse.dltk.core.IModelElementVisitor;
import org.eclipse.dltk.core.IModelElementVisitorExtension;
import org.eclipse.dltk.core.IModelStatus;
import org.eclipse.dltk.core.IModelStatusConstants;
import org.eclipse.dltk.core.IOpenable;
import org.eclipse.dltk.core.IParent;
import org.eclipse.dltk.core.IScriptModel;
import org.eclipse.dltk.core.IScriptProject;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.core.ISourceRange;
import org.eclipse.dltk.core.ISourceReference;
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.core.ScriptModelUtil;
import org.eclipse.dltk.core.WorkingCopyOwner;
import org.eclipse.dltk.internal.core.util.MementoTokenizer;
import org.eclipse.dltk.internal.core.util.Util;
import org.eclipse.dltk.utils.CorePrinter;
/**
* Root of model element handle hierarchy.
*
* @see IModelElement
*/
public abstract class ModelElement extends PlatformObject implements
IModelElement, IModelElementMemento {
public static final char JEM_ESCAPE = '\\';
public static final char JEM_SCRIPTPROJECT = '=';
public static final char JEM_PROJECTFRAGMENT = '/';
public static final char JEM_SCRIPTFOLDER = '<';
public static final char JEM_FIELD = '^';
public static final char JEM_METHOD = '~';
public static final char JEM_SOURCEMODULE = '{';
public static final char JEM_TYPE = '[';
public static final char JEM_IMPORTDECLARATION = '&';
public static final char JEM_COUNT = '!';
public static final char JEM_LOCALVARIABLE = '@';
public static final char JEM_TYPE_PARAMETER = ']';
public static final char JEM_PACKAGEDECLARATION = '%';
/**
* This Item is for direct user element handle. Resolving of elements with
* such delimiter requires building of the model.
*/
public static final char JEM_USER_ELEMENT = '}';
public static final String JEM_USER_ELEMENT_ENDING = "=/<^~{[&!@]%}";
// Used to replace path / or \\ symbols in external package names and
// archives.
public static final char JEM_SKIP_DELIMETER = '>';
/**
* This element's parent, or <code>null</code> if this element does not have
* a parent.
*/
protected final ModelElement parent;
protected static final ModelElement[] NO_ELEMENTS = new ModelElement[0];
protected static final Object NO_INFO = new Object();
/**
* Constructs a handle for a model element with the given parent element.
*
* @param parent
* The parent of model element
*
* @exception IllegalArgumentException
* if the type is not one of the valid model element type
* constants
*
*/
protected ModelElement(ModelElement parent) throws IllegalArgumentException {
this.parent = parent;
}
/**
* @see IModelElement
*/
@Override
public boolean exists() {
try {
getElementInfo();
return true;
} catch (ModelException e) {
// element doesn't exist: return false
if (DLTKCore.DEBUG) {
e.printStackTrace();
}
}
return false;
}
/**
* @see IModelElement
*/
@Override
public IModelElement getAncestor(int ancestorType) {
IModelElement element = this;
while (element != null) {
if (element.getElementType() == ancestorType)
return element;
element = element.getParent();
}
return null;
}
@Override
@SuppressWarnings("unchecked")
public <E extends IModelElement> E getAncestor(Class<E> elementClass) {
IModelElement element = this;
do {
if (elementClass.isInstance(element)) {
return (E) element;
}
element = element.getParent();
} while (element != null);
return null;
}
/**
* @see IOpenable
*/
public void close() throws ModelException {
ModelManager.getModelManager().removeInfoAndChildren(this);
}
/**
* This element is being closed. Do any necessary cleanup.
*/
protected abstract void closing(Object info) throws ModelException;
/**
* Returns the info for this handle. If this element is not already open, it
* and all of its parents are opened. Does not return null. NOTE: BinaryType
* infos are NOT rooted under ModelElementInfo.
*
* @exception ModelException
* if the element is not present or not accessible
*/
public Object getElementInfo() throws ModelException {
return getElementInfo(null);
}
/**
* Returns the info for this handle. If this element is not already open, it
* and all of its parents are opened. Does not return null. NOTE: BinaryType
* infos are NOT rooted under ModelElementInfo.
*
* @exception ModelException
* if the element is not present or not accessible
*/
public Object getElementInfo(IProgressMonitor monitor)
throws ModelException {
ModelManager manager = ModelManager.getModelManager();
Object info = manager.getInfo(this);
if (info != null)
return info;
return openWhenClosed(createElementInfo(), monitor);
}
/*
* Opens an <code>Openable</code> that is known to be closed (no check for
* <code>isOpen()</code>). Returns the created element info.
*/
protected Object openWhenClosed(Object info, IProgressMonitor monitor)
throws ModelException {
ModelManager manager = ModelManager.getModelManager();
boolean hadTemporaryCache = manager.hasTemporaryCache();
try {
HashMap newElements = manager.getTemporaryCache();
generateInfos(info, newElements, monitor);
if (info == null) {
info = newElements.get(this);
}
if (info == null) { // a source ref element could not be opened
// close the buffer that was opened for the openable parent
// close only the openable's buffer (see
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=62854)
Openable openable = (Openable) getOpenable();
if (newElements.containsKey(openable)) {
openable.closeBuffer();
}
throw newNotPresentException();
}
if (!hadTemporaryCache) {
manager.putInfos(this, newElements);
}
} finally {
if (!hadTemporaryCache) {
manager.resetTemporaryCache();
}
}
return info;
}
/*
* @see IModelElement
*/
@Override
public IOpenable getOpenable() {
return this.getOpenableParent();
}
/**
* Return the first instance of IOpenable in the parent hierarchy of this
* element.
*
* <p>
* Subclasses that are not IOpenable's must override this method.
*/
public IOpenable getOpenableParent() {
return (IOpenable) this.parent;
}
/**
* @see IModelElement
*/
@Override
public IModelElement getParent() {
return this.parent;
}
/*
* Returns a new element info for this element.
*/
protected abstract Object createElementInfo();
/**
* Generates the element infos for this element, its ancestors (if they are
* not opened) and its children (if it is an Openable). Puts the newly
* created element info in the given map.
*/
protected abstract void generateInfos(Object info, HashMap newElements,
IProgressMonitor pm) throws ModelException;
/**
* @see IAdaptable
*/
@Override
public String getElementName() {
return ""; //$NON-NLS-1$
}
/**
* Creates and returns a new not present exception for this element.
*/
public ModelException newNotPresentException() {
return new ModelException(new ModelStatus(
IModelStatusConstants.ELEMENT_DOES_NOT_EXIST, this));
}
/**
* Returns true if this handle represents the same model element as the
* given handle. By default, two handles represent the same element if they
* are identical or if they represent the same type of element, have equal
* names, parents, and occurrence counts.
*
* <p>
* If a subclass has other requirements for equality, this method must be
* overridden.
*
* @see Object#equals
*/
@Override
public boolean equals(Object o) {
if (this == o)
return true;
// model parent is null
if (this.parent == null) {
return super.equals(o);
}
if (o == null) {
return false;
}
// assume instanceof check is done in subclass
final ModelElement other = (ModelElement) o;
return getElementName().equals(other.getElementName())
&& this.parent.equals(other.parent);
}
/**
* Returns the hash code for this model element. By default, the hash code
* for an element is a combination of its name and parent's hash code.
* Elements with other requirements must override this method.
*/
@Override
public int hashCode() {
if (this.parent == null)
return super.hashCode();
return Util.combineHashCodes(getElementName().hashCode(), this.parent
.hashCode());
}
/**
* Returns true if this element is an ancestor of the given element,
* otherwise false.
*/
public boolean isAncestorOf(IModelElement e) {
IModelElement parentElement = e.getParent();
while (parentElement != null && !parentElement.equals(this)) {
parentElement = parentElement.getParent();
}
return parentElement != null;
}
/**
* @see IModelElement
*/
@Override
public boolean isReadOnly() {
return false;
}
/**
* Returns a collection of (immediate) children of this node of the
* specified type.
*
* @param type
* - one of the EM_* constants defined by ModelElement
*/
protected List<IModelElement> getChildrenOfType(int type)
throws ModelException {
return getChildrenOfType(type, null);
}
protected List<IModelElement> getChildrenOfType(int type,
IProgressMonitor monitor) throws ModelException {
IModelElement[] children = getChildren(monitor);
int size = children.length;
List<IModelElement> list = new ArrayList<IModelElement>(size);
for (int i = 0; i < size; ++i) {
IModelElement elt = children[i];
if (elt.getElementType() == type) {
list.add(elt);
}
}
return list;
}
/**
* @see IParent
*/
public IModelElement[] getChildren() throws ModelException {
return getChildren(null);
}
public IModelElement[] getChildren(IProgressMonitor monitor)
throws ModelException {
Object elementInfo = getElementInfo(monitor);
if (elementInfo instanceof ModelElementInfo) {
return ((ModelElementInfo) elementInfo).getChildren();
} else {
return NO_ELEMENTS;
}
}
/**
* @see IModelElement
*/
@Override
public IScriptModel getModel() {
IModelElement current = this;
do {
if (current instanceof IScriptModel)
return (IScriptModel) current;
} while ((current = current.getParent()) != null);
return null;
}
/**
* Creates and returns a new model exception for this element with the given
* status.
*/
public ModelException newModelException(IStatus status) {
if (status instanceof IModelStatus)
return new ModelException((IModelStatus) status);
else
return new ModelException(new ModelStatus(status.getSeverity(),
status.getCode(), status.getMessage()));
}
/**
* @see IModelElement
*/
@Override
public IScriptProject getScriptProject() {
IModelElement current = this;
do {
if (current instanceof IScriptProject)
return (IScriptProject) current;
} while ((current = current.getParent()) != null);
return null;
}
public boolean hasChildren() throws ModelException {
// if I am not open, return true to avoid opening (case of a project, a
// source module or a binary file).
Object elementInfo = ModelManager.getModelManager().getInfo(this);
if (elementInfo instanceof ModelElementInfo) {
return ((ModelElementInfo) elementInfo).getChildren().length > 0;
} else {
return true;
}
}
/**
* Debugging purposes
*/
protected String tabString(int tab) {
StringBuffer buffer = new StringBuffer();
for (int i = tab; i > 0; i--)
buffer.append(" "); //$NON-NLS-1$
return buffer.toString();
}
/**
* Debugging purposes
*/
public String toDebugString() {
StringBuffer buffer = new StringBuffer();
this.toStringInfo(0, buffer, NO_INFO, true/* show resolved info */);
return buffer.toString();
}
/**
* Debugging purposes
*/
@Override
public String toString() {
StringBuffer buffer = new StringBuffer();
toString(0, buffer);
return buffer.toString();
}
/**
* Debugging purposes
*/
protected void toString(int tab, StringBuffer buffer) {
Object info = this.toStringInfo(tab, buffer);
if (tab == 0) {
this.toStringAncestors(buffer);
}
this.toStringChildren(tab, buffer, info);
}
/**
* Debugging purposes
*/
public String toStringWithAncestors() {
return toStringWithAncestors(true/* show resolved info */);
}
/**
* Debugging purposes
*/
public String toStringWithAncestors(boolean showResolvedInfo) {
StringBuffer buffer = new StringBuffer();
this.toStringInfo(0, buffer, NO_INFO, showResolvedInfo);
this.toStringAncestors(buffer);
return buffer.toString();
}
/**
* Debugging purposes
*/
protected void toStringAncestors(StringBuffer buffer) {
ModelElement parentElement = (ModelElement) this.getParent();
if (parentElement != null && parentElement.getParent() != null) {
buffer.append(" [in "); //$NON-NLS-1$
parentElement.toStringInfo(0, buffer, NO_INFO, false); // don't show
// resolved
// info
parentElement.toStringAncestors(buffer);
buffer.append("]"); //$NON-NLS-1$
}
}
/**
* Debugging purposes
*/
protected void toStringChildren(int tab, StringBuffer buffer, Object info) {
if (info == null || !(info instanceof ModelElementInfo))
return;
IModelElement[] children = ((ModelElementInfo) info).getChildren();
for (int i = 0; i < children.length; i++) {
buffer.append("\n"); //$NON-NLS-1$
((ModelElement) children[i]).toString(tab + 1, buffer);
}
}
/**
* Debugging purposes
*/
public Object toStringInfo(int tab, StringBuffer buffer) {
Object info = ModelManager.getModelManager().peekAtInfo(this);
this.toStringInfo(tab, buffer, info, true/* show resolved info */);
return info;
}
/**
* Debugging purposes
*
* @param showResolvedInfo
* TODO
*/
protected void toStringInfo(int tab, StringBuffer buffer, Object info,
boolean showResolvedInfo) {
buffer.append(this.tabString(tab));
toStringName(buffer);
if (info == null) {
buffer.append(" (not open)"); //$NON-NLS-1$
}
}
/**
* Debugging purposes
*/
protected void toStringName(StringBuffer buffer) {
buffer.append(getElementName());
}
/**
* Returns the element that is located at the given source position in this
* element. This is a helper method for
* <code>ISourceModule#getElementAt</code>, and only works on compilation
* units and types. The position given is known to be within this element's
* source range already, and if no finer grained element is found at the
* position, this element is returned.
*/
protected IModelElement getSourceElementAt(int position)
throws ModelException {
IModelElement res = getSourceElementAtTop(position);
if (res != this)
return res;
if (this instanceof ISourceReference) {
IModelElement[] children = getChildren();
for (int i = children.length - 1; i >= 0; i--) {
IModelElement aChild = children[i];
if (aChild instanceof SourceRefElement) {
SourceRefElement child = (SourceRefElement) children[i];
if (child instanceof IParent) {
res = child.getSourceElementAt(position);
if (res != child)
return res;
}
}
}
} else {
// should not happen
Assert.isTrue(false);
}
return this;
}
/**
* Returns the element that is located at the given source position in this
* element. This is a helper method for
* <code>ISourceModule#getElementAt</code>, and only works on compilation
* units and types. The position given is known to be within this element's
* source range already, and if no finer grained element is found at the
* position, this element is returned.
*/
protected IModelElement getSourceElementAtTop(int position)
throws ModelException {
if (this instanceof ISourceReference) {
IModelElement[] children = getChildren();
for (int i = children.length - 1; i >= 0; i--) {
IModelElement aChild = children[i];
if (aChild instanceof SourceRefElement) {
SourceRefElement child = (SourceRefElement) children[i];
ISourceRange range = child.getSourceRange();
int start = range.getOffset();
int end = start + range.getLength();
if (start <= position && position <= end) {
if (child instanceof IField) {
// check muti-declaration case (see
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=39943
// )
int declarationStart = start;
SourceRefElement candidate = null;
do {
// check name range
range = ((IField) child).getNameRange();
if (position <= range.getOffset()
+ range.getLength()) {
candidate = child;
} else {
return candidate == null ? child
.getSourceElementAt(position)
: candidate
.getSourceElementAt(position);
}
child = --i >= 0 ? (SourceRefElement) children[i]
: null;
} while (child instanceof IField
&& child.getSourceRange().getOffset() == declarationStart);
// position in field's type: use first field
return candidate.getSourceElementAt(position);
} else if (child instanceof IParent) {
return child.getSourceElementAt(position);
} else {
return child;
}
}
}
}
} else {
// should not happen
Assert.isTrue(false);
}
return this;
}
/**
* Returns element type and name. called from
* {@link #printNode(CorePrinter)} only.
*
* @return
*/
protected String describeElement() {
return ScriptModelUtil.describeElementType(getElementType()) + ':'
+ getElementName();
}
/**
* Only for testing. Used to print this node with all sub childs.
*
* @param output
*/
public void printNode(CorePrinter output) {
output.formatPrint(describeElement());
output.indent();
try {
IModelElement modelElements[] = this.getChildren();
for (int i = 0; i < modelElements.length; ++i) {
IModelElement element = modelElements[i];
if (element instanceof ModelElement) {
((ModelElement) element).printNode(output);
} else {
output.print("Unknown element:" + element); //$NON-NLS-1$
}
}
} catch (ModelException ex) {
output.formatPrint(ex.getLocalizedMessage());
}
output.dedent();
}
@Override
public IModelElement getPrimaryElement() {
return getPrimaryElement(true);
}
/*
* Returns the primary element. If checkOwner, and the cu owner is primary,
* return this element.
*/
public IModelElement getPrimaryElement(boolean checkOwner) {
return this;
}
@Override
public IModelElement getHandleFromMemento(MementoTokenizer memento,
WorkingCopyOwner owner) {
if (!memento.hasMoreTokens())
return this;
String token = memento.nextToken();
return getHandleFromMemento(token, memento, owner);
}
@Override
public abstract IModelElement getHandleFromMemento(String token,
MementoTokenizer memento, WorkingCopyOwner owner);
@Override
public String getHandleIdentifier() {
return getHandleMemento();
}
public String getHandleMemento() {
StringBuffer buff = new StringBuffer();
getHandleMemento(buff);
return buff.toString();
}
public void getHandleMemento(StringBuffer buff) {
((ModelElement) getParent()).getHandleMemento(buff);
buff.append(getHandleMementoDelimiter());
escapeMementoName(buff, getElementName());
}
protected abstract char getHandleMementoDelimiter();
protected void escapeMementoName(StringBuffer buffer, String mementoName) {
for (int i = 0, length = mementoName.length(); i < length; i++) {
char character = mementoName.charAt(i);
switch (character) {
case JEM_ESCAPE:
case JEM_COUNT:
case JEM_SCRIPTPROJECT:
case JEM_PROJECTFRAGMENT:
case JEM_SCRIPTFOLDER:
case JEM_FIELD:
case JEM_METHOD:
case JEM_SOURCEMODULE:
case JEM_TYPE:
case JEM_IMPORTDECLARATION:
case JEM_LOCALVARIABLE:
case JEM_TYPE_PARAMETER:
case JEM_USER_ELEMENT:
buffer.append(JEM_ESCAPE);
}
buffer.append(character);
}
}
public ISourceModule getSourceModule() {
return null;
}
@Override
public void accept(IModelElementVisitor visitor) throws ModelException {
if (visitor.visit(this)) {
IModelElement[] elements = getChildren();
for (int i = 0; i < elements.length; ++i) {
elements[i].accept(visitor);
}
if (visitor instanceof IModelElementVisitorExtension) {
((IModelElementVisitorExtension) visitor).endVisit(this);
}
}
}
}