/******************************************************************************* * 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); } }