/******************************************************************************* * Copyright (c) 2005, 2017 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<>(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); } } } }