blob: 7924e8ca05cc0cbe9842e2ce895438d51e5da6f9 [file] [log] [blame]
/*******************************************************************************
* 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);
}
}