| /******************************************************************************* |
| * Copyright (c) 2004, 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 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| |
| package org.eclipse.ui.internal.intro.impl.model; |
| |
| import java.util.Iterator; |
| import java.util.Vector; |
| |
| import org.eclipse.core.runtime.IConfigurationElement; |
| import org.eclipse.help.UAContentFilter; |
| import org.eclipse.help.internal.UAElementFactory; |
| import org.eclipse.ui.internal.intro.impl.model.loader.ExtensionPointManager; |
| import org.eclipse.ui.internal.intro.impl.util.IntroEvaluationContext; |
| import org.eclipse.ui.internal.intro.impl.util.Log; |
| import org.eclipse.ui.internal.intro.impl.util.StringUtil; |
| import org.osgi.framework.Bundle; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| |
| /** |
| * An intro config component that is a container, ie: it can have children. |
| */ |
| public abstract class AbstractIntroContainer extends AbstractBaseIntroElement { |
| |
| protected static final String ATT_BG_IMAGE = "bgImage"; //$NON-NLS-1$ |
| // vector is lazily created when children are loaded in a call to |
| // loadChildren(). |
| protected Vector<AbstractIntroElement> children; |
| protected boolean loaded = false; |
| protected boolean resolved = false; |
| protected Element element; |
| |
| // store the base since it will needed later to resolve children. |
| protected String base; |
| |
| /** |
| * @param element |
| */ |
| AbstractIntroContainer(IConfigurationElement element) { |
| super(element); |
| } |
| |
| /** |
| * @param element |
| */ |
| AbstractIntroContainer(Element element, Bundle bundle) { |
| super(element, bundle); |
| this.element = element; |
| } |
| |
| /** |
| * @param element |
| */ |
| AbstractIntroContainer(Element element, Bundle bundle, String base) { |
| super(element, bundle); |
| this.element = element; |
| this.base = base; |
| } |
| |
| |
| /** |
| * Get the children of this container. Loading children and resolving |
| * includes and extension is delayed until this method call. |
| * |
| * @return Returns all the children of this container. |
| */ |
| public AbstractIntroElement[] getChildren() { |
| if (!loaded) |
| loadChildren(); |
| |
| if (!loaded) |
| // if loaded still is false, something went wrong. This could happen |
| // when loading content from another external content files. |
| return new AbstractIntroElement[0]; |
| |
| if (!resolved) |
| resolveChildren(); |
| |
| Vector filtered = filterChildren(children); |
| |
| AbstractIntroElement[] childrenElements = (AbstractIntroElement[]) convertToModelArray( |
| filtered, AbstractIntroElement.ELEMENT); |
| return childrenElements; |
| } |
| |
| /** |
| * Returns all the children of this container that are of the specified |
| * type(s). <br> |
| * An example of an element mask is as follows: |
| * <p> |
| * <code> |
| * int elementMask = IntroElement.IMAGE | IntroElement.DEFAULT_LINK; |
| * int elementMask = IntroElement.ABSTRACT_CONTAINER; |
| * </code> |
| * The return type is determined depending on the mask. If the mask is a |
| * predefined constant in the IntroElement, and it does not correspond to an |
| * abstract model class, then the object returned can be safely cast to an |
| * array of the corresponding model class. For exmaple, the following code |
| * gets all groups in the given page, in the same order they appear in the |
| * plugin.xml markup: |
| * <p> |
| * <code> |
| * Introgroup[] groups = (IntroGroup[])page.getChildrenOfType(IntroElement.GROUP); |
| * </code> |
| * |
| * However, if the element mask is not homogenous (for example: LINKS | |
| * GROUP) then the returned array must be cast to an array of |
| * IntroElements.For exmaple, the following code gets all images and links |
| * in the given page, in the same order they appear in the plugin.xml |
| * markup: |
| * <p> |
| * <code> |
| * int elementMask = IntroElement.IMAGE | IntroElement.DEFAULT_LINK; |
| * IntroElement[] imagesAndLinks = |
| * (IntroElement[])page.getChildrenOfType(elementMask); |
| * </code> |
| * |
| * @return An array of elements of the right type. If the container has no |
| * children, or no children of the specified types, returns an empty |
| * array. |
| */ |
| public Object[] getChildrenOfType(int elementMask) { |
| |
| AbstractIntroElement[] childrenElements = getChildren(); |
| // if we have no children, we still need to return an empty array of |
| // the correct type. |
| Vector<AbstractIntroElement> typedChildren = new Vector<>(); |
| for (int i = 0; i < childrenElements.length; i++) { |
| AbstractIntroElement element = childrenElements[i]; |
| if (element.isOfType(elementMask)) |
| typedChildren.addElement(element); |
| } |
| return convertToModelArray(typedChildren, elementMask); |
| } |
| |
| /** |
| * Utility method to convert all the content of a vector of |
| * AbstractIntroElements into an array of IntroElements cast to the correct |
| * class type. It is assumed that all elements in this vector are |
| * IntroElement instances. If elementMask is a predefined model type (ie: |
| * homogenous), then return array of corresponding type. Else, returns an |
| * array of IntroElements. |
| * |
| * @param vector |
| */ |
| private Object[] convertToModelArray(Vector vector, int elementMask) { |
| int size = vector.size(); |
| Object[] src = null; |
| switch (elementMask) { |
| // homogenous vector. |
| case AbstractIntroElement.GROUP: |
| src = new IntroGroup[size]; |
| break; |
| case AbstractIntroElement.LINK: |
| src = new IntroLink[size]; |
| break; |
| case AbstractIntroElement.TEXT: |
| src = new IntroText[size]; |
| break; |
| case AbstractIntroElement.IMAGE: |
| src = new IntroImage[size]; |
| break; |
| case AbstractIntroElement.HR: |
| src = new IntroSeparator[size]; |
| break; |
| case AbstractIntroElement.HTML: |
| src = new IntroHTML[size]; |
| break; |
| case AbstractIntroElement.INCLUDE: |
| src = new IntroInclude[size]; |
| break; |
| case AbstractIntroElement.PAGE: |
| src = new IntroPage[size]; |
| break; |
| case AbstractIntroElement.ABSTRACT_PAGE: |
| src = new AbstractIntroPage[size]; |
| break; |
| case AbstractIntroElement.ABSTRACT_CONTAINER: |
| src = new AbstractIntroContainer[size]; |
| break; |
| case AbstractIntroElement.HEAD: |
| src = new IntroHead[size]; |
| break; |
| case AbstractIntroElement.PAGE_TITLE: |
| src = new IntroPageTitle[size]; |
| break; |
| case AbstractIntroElement.ANCHOR: |
| src = new IntroAnchor[size]; |
| break; |
| case AbstractIntroElement.CONTENT_PROVIDER: |
| src = new IntroContentProvider[size]; |
| break; |
| |
| default: |
| // now handle left over abstract types. Vector is not homogenous. |
| src = new AbstractIntroElement[size]; |
| break; |
| } |
| |
| vector.copyInto(src); |
| return src; |
| |
| } |
| |
| /** |
| * Load all the children of this container. A container can have other |
| * containers, links, htmls, text, image, include. Load them in the order |
| * they appear in the xml content file. |
| */ |
| protected void loadChildren() { |
| // init the children vector. old children are disposed automatically. |
| children = new Vector<>(); |
| |
| |
| NodeList nodeList = element.getChildNodes(); |
| Vector<Node> vector = new Vector<>(); |
| for (int i = 0; i < nodeList.getLength(); i++) { |
| Node node = nodeList.item(i); |
| if (node.getNodeType() == Node.ELEMENT_NODE) |
| vector.add(node); |
| } |
| Element[] filteredElements = new Element[vector.size()]; |
| vector.copyInto(filteredElements); |
| // add the elements at the end children's vector. |
| insertElementsBefore(filteredElements, getBundle(), base, children |
| .size(), null); |
| loaded = true; |
| // we cannot free DOM model element because a page's children may be |
| // nulled when reflowing a content provider. |
| } |
| |
| /** |
| * Adds the given elements as children of this container, before the |
| * specified index. |
| * |
| * @param childElements |
| */ |
| protected void insertElementsBefore(Element[] childElements, Bundle bundle, |
| String base, int index, String mixinStyle) { |
| for (int i = 0; i < childElements.length; i++) { |
| Element childElement = childElements[i]; |
| AbstractIntroElement child = getModelChild(childElement, bundle, |
| base); |
| if (child != null) { |
| child.setParent(this); |
| child.setMixinStyle(mixinStyle); |
| children.add(index, child); |
| // index is only incremented if we actually added a child. |
| index++; |
| } |
| } |
| } |
| |
| /** |
| * Adds the given elements as children of this container, before the |
| * specified element. The element must be a direct child of this container. |
| * |
| * @param childElements |
| */ |
| protected void insertElementsBefore(Element[] childElements, Bundle bundle, |
| String base, AbstractIntroElement child, String mixinStyle) { |
| int childLocation = children.indexOf(child); |
| if (childLocation == -1) |
| // bad reference child. |
| return; |
| insertElementsBefore(childElements, bundle, base, childLocation, mixinStyle); |
| } |
| |
| |
| |
| /** |
| * Adds a child to this container, depending on its type. Subclasses may |
| * override if there is a child specific to the subclass. |
| * |
| * @param childElements |
| */ |
| protected AbstractIntroElement getModelChild(Element childElement, |
| Bundle bundle, String base) { |
| |
| AbstractIntroElement child = null; |
| if (childElement.getNodeName().equalsIgnoreCase(IntroGroup.TAG_GROUP)) |
| child = new IntroGroup(childElement, bundle, base); |
| else if (childElement.getNodeName() |
| .equalsIgnoreCase(IntroLink.TAG_LINK)) |
| child = new IntroLink(childElement, bundle, base); |
| else if (childElement.getNodeName() |
| .equalsIgnoreCase(IntroText.TAG_TEXT)) |
| child = new IntroText(childElement, bundle); |
| else if (childElement.getNodeName().equalsIgnoreCase( |
| IntroImage.TAG_IMAGE)) |
| child = new IntroImage(childElement, bundle, base); |
| else if (childElement.getNodeName().equalsIgnoreCase( |
| IntroSeparator.TAG_HR)) |
| child = new IntroSeparator(childElement, bundle, base); |
| else if (childElement.getNodeName() |
| .equalsIgnoreCase(IntroHTML.TAG_HTML)) |
| child = new IntroHTML(childElement, bundle, base); |
| else if (childElement.getNodeName().equalsIgnoreCase( |
| IntroInclude.TAG_INCLUDE)) |
| child = new IntroInclude(childElement, bundle); |
| else if (childElement.getNodeName().equalsIgnoreCase( |
| IntroAnchor.TAG_ANCHOR)) |
| child = new IntroAnchor(childElement, bundle); |
| else if (childElement.getNodeName().equalsIgnoreCase( |
| IntroContentProvider.TAG_CONTENT_PROVIDER)) |
| child = new IntroContentProvider(childElement, bundle); |
| return child; |
| } |
| |
| |
| /** |
| * Resolve each include in this container's children. Includes are lazily |
| * resolved on a per container basis, when the container is resolved. |
| */ |
| protected void resolveChildren() { |
| AbstractIntroElement[] array = (AbstractIntroElement[])children.toArray(new AbstractIntroElement[children.size()]); |
| for (int i=0;i<array.length;++i) { |
| AbstractIntroElement child = array[i]; |
| if (UAContentFilter.isFiltered(UAElementFactory.newElement(child.getElement()), IntroEvaluationContext.getContext())) { |
| children.remove(child); |
| } |
| else if (child.getType() == AbstractIntroElement.INCLUDE) { |
| resolveInclude((IntroInclude) child); |
| } |
| } |
| resolved = true; |
| } |
| |
| /** |
| * Resolves an include. Gets the intro element pointed to by the include, |
| * and adds it as a child of this current container. If target is not a |
| * group, or any element that can be included in a group, ignore this |
| * include. |
| * |
| * @param include |
| */ |
| private void resolveInclude(IntroInclude include) { |
| AbstractIntroElement target = findIncludeTarget(include); |
| if (target == null) |
| // target could not be found. |
| return; |
| if (target.isOfType(AbstractIntroElement.GROUP |
| | AbstractIntroElement.ABSTRACT_TEXT |
| | AbstractIntroElement.IMAGE | AbstractIntroElement.TEXT |
| | AbstractIntroElement.PAGE_TITLE)) |
| // be picky about model elements to include. Can not use |
| // BASE_ELEMENT model class because pages can not be included. |
| insertTarget(include, target); |
| } |
| |
| /** |
| * Filters the appropriate elements from the given Vector, according to the current |
| * environment. For example, if one of the elements has a tag to filter for os=linux and |
| * the os is win32, the element will not be returned in the resulting Vector. |
| * |
| * @param unfiltered the unfiltered elements |
| * @return a new Vector with elements filtered |
| */ |
| private <T> Vector<T> filterChildren(Vector<T> unfiltered) { |
| Vector<T> filtered = new Vector<>(); |
| Iterator<T> iter = unfiltered.iterator(); |
| while (iter.hasNext()) { |
| T element = iter.next(); |
| if (!UAContentFilter.isFiltered(element, IntroEvaluationContext.getContext())) { |
| filtered.add(element); |
| } |
| } |
| return filtered; |
| } |
| |
| /** |
| * Find the target element pointed to by the path in the include. It is |
| * assumed that configId always points to an external config, and not the |
| * same config of the inlcude. |
| * |
| * @param include |
| * @param path |
| * @return |
| */ |
| private AbstractIntroElement findIncludeTarget(IntroInclude include) { |
| String path = include.getPath(); |
| IntroModelRoot targetModelRoot = (IntroModelRoot) getParentPage() |
| .getParent(); |
| String targetConfigID = include.getConfigId(); |
| if (targetConfigID != null) |
| targetModelRoot = ExtensionPointManager.getInst().getModel( |
| targetConfigID); |
| if (targetModelRoot == null) |
| // if the target config was not found, skip this include. |
| return null; |
| AbstractIntroElement target = findTarget(targetModelRoot, path); |
| return target; |
| } |
| |
| /** |
| * Finds the child element that corresponds to the given path in the passed |
| * model.<br> |
| * ps: This method could be a static method, but left as instance for model |
| * enhancements. |
| * |
| * @param model |
| * @param path |
| * @return |
| */ |
| public AbstractIntroElement findTarget(AbstractIntroContainer container, |
| String path) { |
| // extract path segments. Get first segment to start search. |
| String[] pathSegments = StringUtil.split(path, "/"); //$NON-NLS-1$ |
| if (container == null) |
| return null; |
| |
| AbstractIntroElement target = container.findChild(pathSegments[0]); |
| if (target == null) |
| // there is no direct child with the specified first path segment. |
| return null; |
| |
| // found parent segment. now find each child segment. |
| for (int i = 1; i < pathSegments.length; i++) { |
| if (!(target instanceof AbstractIntroContainer)) { |
| // parent is not a container, so no point going on. |
| return null; |
| } |
| String pathSegment = pathSegments[i]; |
| target = ((AbstractIntroContainer) target).findChild(pathSegment); |
| if (target == null) |
| // tried to find next segment and failed. |
| return null; |
| } |
| return target; |
| } |
| |
| public AbstractIntroElement findTarget(AbstractIntroContainer container, |
| String path, String extensionId) { |
| // resolve path segments if they are incomplete. |
| if (path.indexOf("@")!= -1) { //$NON-NLS-1$ |
| // new in 3.2: dynamic resolution of incomplete target paths |
| IntroModelRoot root = getModelRoot(); |
| if (root!=null) { |
| path = root.resolvePath(extensionId, path); |
| if (path==null) |
| return null; |
| } |
| |
| } |
| return this.findTarget(container, path); |
| } |
| |
| |
| public AbstractIntroElement findTarget(String path) { |
| return findTarget(this, path); |
| } |
| |
| |
| |
| /* |
| * searches direct children for the first child with the given id. The type |
| * of the child can be any model element that has an id. ie: |
| * AbstractIntroIdElement |
| * |
| * @see org.eclipse.ui.internal.intro.impl.model.IntroElement#getType() |
| */ |
| public AbstractIntroElement findChild(String elementId) { |
| return findChild(elementId, ID_ELEMENT); |
| } |
| |
| /* |
| * searches direct children for the first child with the given id. The type |
| * of the child must be of the passed model types mask. This method handles |
| * the 3.0 style model for content. Pages enhance this behavior with DOM |
| * apis. |
| * |
| * @see org.eclipse.ui.internal.intro.impl.model.IntroElement#getType() |
| */ |
| public AbstractIntroElement findChild(String elementId, int elementMask) { |
| if (!loaded) |
| loadChildren(); |
| |
| for (int i = 0; i < children.size(); i++) { |
| AbstractIntroElement aChild = (AbstractIntroElement) children |
| .elementAt(i); |
| if (!aChild.isOfType(ID_ELEMENT)) |
| // includes and heads do not have ids, and so can not be |
| // referenced directly. This means that they can not be |
| // targets for other includes. Skip, just in case someone |
| // adds an id to it! Also, this applies to all elements in |
| // the model that do not have ids. |
| continue; |
| AbstractIntroIdElement child = (AbstractIntroIdElement) aChild; |
| if (child.getId() != null && child.getId().equals(elementId) |
| && child.isOfType(elementMask)) |
| return child; |
| } |
| // no child with given id and type found. |
| return null; |
| } |
| |
| |
| |
| private void insertTarget(IntroInclude include, AbstractIntroElement target) { |
| int includeLocation = children.indexOf(include); |
| if (includeLocation == -1) |
| // should never be here. |
| return; |
| children.remove(includeLocation); |
| // handle merging target styles first, before changing target parent to |
| // enable inheritance of styles. |
| handleIncludeStyleInheritence(include, target); |
| // now clone the target node because original model should be kept |
| // intact. |
| AbstractIntroElement clonedTarget = null; |
| try { |
| clonedTarget = (AbstractIntroElement) target.clone(); |
| } catch (CloneNotSupportedException ex) { |
| // should never be here. |
| Log.error("Failed to clone Intro model node.", ex); //$NON-NLS-1$ |
| return; |
| } |
| // set parent of cloned target to be this container. |
| clonedTarget.setParent(this); |
| children.insertElementAt(clonedTarget, includeLocation); |
| } |
| |
| /** |
| * Updates the inherited styles based on the merge-style attribute. If we |
| * are including a shared group, or if we are including an element from the |
| * same page, do nothing. For inherited alt-styles, we have to cache the pd |
| * from which we inherited the styles to be able to access resources in that |
| * plugin. Also note that when including a container, it must be resolved |
| * otherwise reparenting will cause includes in this target container to |
| * fail. |
| * |
| * @param include |
| * @param target |
| */ |
| private void handleIncludeStyleInheritence(IntroInclude include, |
| AbstractIntroElement target) { |
| |
| if (include.getMergeStyle() == false) |
| // target styles are not needed. nothing to do. |
| return; |
| |
| if (target.getParent().getType() == AbstractIntroElement.MODEL_ROOT |
| || target.getParentPage().equals(include.getParentPage())) |
| // If we are including from this same page ie: target is in the |
| // same page, OR if we are including a shared group, defined |
| // under a config, do not include styles. |
| return; |
| |
| // Update the parent page styles. skip style if it is null. Note, |
| // include both the target page styles and inherited styles. The full |
| // page styles need to be include. |
| String style = target.getParentPage().getStyle(); |
| if (style != null) |
| getParentPage().addStyle(style); |
| |
| // for alt-style cache bundle for loading resources. |
| style = target.getParentPage().getAltStyle(); |
| if (style != null) { |
| Bundle bundle = target.getBundle(); |
| getParentPage().addAltStyle(style, bundle); |
| } |
| |
| // now add inherited styles. Race condition could happen here if Page A |
| // is including from Page B which is in turn including from Page A. |
| getParentPage().addStyles(target.getParentPage().getStyles()); |
| getParentPage().addAltStyles(target.getParentPage().getAltStyles()); |
| |
| } |
| |
| /** |
| * Creates a clone of the given target node. A clone is create by simply |
| * recreating that protion of the model. |
| * |
| * Note: looked into the clonable interface in Java, but it was not used |
| * because it makes modifications/additions to the model harder to maintain. |
| * Will revisit later. |
| * |
| * @param targer |
| * @return |
| */ |
| protected AbstractIntroElement cloneTarget(AbstractIntroElement target) { |
| return null; |
| } |
| |
| @Override |
| public int getType() { |
| return AbstractIntroElement.ABSTRACT_CONTAINER; |
| } |
| |
| /** |
| * Deep copy since class has mutable objects. Leave DOM element as a shallow |
| * reference copy since DOM is immutable. |
| */ |
| @Override |
| public Object clone() throws CloneNotSupportedException { |
| AbstractIntroContainer clone = (AbstractIntroContainer) super.clone(); |
| clone.children = new Vector<>(); |
| if (children != null) { |
| for (int i = 0; i < children.size(); i++) { |
| AbstractIntroElement cloneChild = (AbstractIntroElement) ((AbstractIntroElement) children |
| .elementAt(i)).clone(); |
| cloneChild.setParent(clone); |
| clone.children.add(i, cloneChild); |
| } |
| } |
| return clone; |
| } |
| |
| /** |
| * Returns the element. |
| * |
| * @return |
| */ |
| @Override |
| public Element getElement() { |
| return this.element; |
| } |
| |
| public String getBase() { |
| return base; |
| } |
| |
| |
| /* |
| * Clears this container. This means emptying the children, and resetting |
| * flags. |
| */ |
| public void clearChildren() { |
| this.children.clear(); |
| } |
| |
| |
| /** |
| * Adds a model element as a child. Caller is responsible for inserting |
| * model elements that rea valid as children. |
| * |
| * @param child |
| */ |
| public void addChild(AbstractIntroElement child) { |
| children.add(child); |
| } |
| |
| public void removeChild(AbstractIntroElement child) { |
| children.remove(child); |
| } |
| |
| public String getBackgroundImage() { |
| return getAttribute(element, ATT_BG_IMAGE); |
| } |
| |
| |
| } |