001: /*******************************************************************************
002: * Copyright (c) 2004, 2007 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.ui.internal.intro.impl.model;
011:
012: import java.util.Iterator;
013: import java.util.Vector;
014:
015: import org.eclipse.core.runtime.IConfigurationElement;
016: import org.eclipse.help.UAContentFilter;
017: import org.eclipse.help.internal.UAElementFactory;
018: import org.eclipse.ui.internal.intro.impl.model.loader.ExtensionPointManager;
019: import org.eclipse.ui.internal.intro.impl.util.IntroEvaluationContext;
020: import org.eclipse.ui.internal.intro.impl.util.Log;
021: import org.eclipse.ui.internal.intro.impl.util.StringUtil;
022: import org.osgi.framework.Bundle;
023: import org.w3c.dom.Element;
024: import org.w3c.dom.Node;
025: import org.w3c.dom.NodeList;
026:
027: /**
028: * An intro config component that is a container, ie: it can have children.
029: */
030: public abstract class AbstractIntroContainer extends
031: AbstractBaseIntroElement {
032:
033: protected static final String ATT_BG_IMAGE = "bgImage"; //$NON-NLS-1$
034: // vector is lazily created when children are loaded in a call to
035: // loadChildren().
036: protected Vector children;
037: protected boolean loaded = false;
038: protected boolean resolved = false;
039: protected Element element;
040:
041: // store the base since it will needed later to resolve children.
042: protected String base;
043:
044: /**
045: * @param element
046: */
047: AbstractIntroContainer(IConfigurationElement element) {
048: super (element);
049: }
050:
051: /**
052: * @param element
053: */
054: AbstractIntroContainer(Element element, Bundle bundle) {
055: super (element, bundle);
056: this .element = element;
057: }
058:
059: /**
060: * @param element
061: */
062: AbstractIntroContainer(Element element, Bundle bundle, String base) {
063: super (element, bundle);
064: this .element = element;
065: this .base = base;
066: }
067:
068: /**
069: * Get the children of this container. Loading children and resolving
070: * includes and extension is delayed until this method call.
071: *
072: * @return Returns all the children of this container.
073: */
074: public AbstractIntroElement[] getChildren() {
075: if (!loaded)
076: loadChildren();
077:
078: if (!loaded)
079: // if loaded still is false, something went wrong. This could happen
080: // when loading content from another external content files.
081: return new AbstractIntroElement[0];
082:
083: if (!resolved)
084: resolveChildren();
085:
086: Vector filtered = filterChildren(children);
087:
088: AbstractIntroElement[] childrenElements = (AbstractIntroElement[]) convertToModelArray(
089: filtered, AbstractIntroElement.ELEMENT);
090: return childrenElements;
091: }
092:
093: /**
094: * Returns all the children of this container that are of the specified
095: * type(s). <br>
096: * An example of an element mask is as follows:
097: * <p>
098: * <code>
099: * int elementMask = IntroElement.IMAGE | IntroElement.DEFAULT_LINK;
100: * int elementMask = IntroElement.ABSTRACT_CONTAINER;
101: * </code>
102: * The return type is determined depending on the mask. If the mask is a
103: * predefined constant in the IntroElement, and it does not correspond to an
104: * abstract model class, then the object returned can be safely cast to an
105: * array of the corresponding model class. For exmaple, the following code
106: * gets all groups in the given page, in the same order they appear in the
107: * plugin.xml markup:
108: * <p>
109: * <code>
110: * Introgroup[] groups = (IntroGroup[])page.getChildrenOfType(IntroElement.GROUP);
111: * </code>
112: *
113: * However, if the element mask is not homogenous (for example: LINKS |
114: * GROUP) then the returned array must be cast to an array of
115: * IntroElements.For exmaple, the following code gets all images and links
116: * in the given page, in the same order they appear in the plugin.xml
117: * markup:
118: * <p>
119: * <code>
120: * int elementMask = IntroElement.IMAGE | IntroElement.DEFAULT_LINK;
121: * IntroElement[] imagesAndLinks =
122: * (IntroElement[])page.getChildrenOfType(elementMask);
123: * </code>
124: *
125: * @return An array of elements of the right type. If the container has no
126: * children, or no children of the specified types, returns an empty
127: * array.
128: */
129: public Object[] getChildrenOfType(int elementMask) {
130:
131: AbstractIntroElement[] childrenElements = getChildren();
132: // if we have no children, we still need to return an empty array of
133: // the correct type.
134: Vector typedChildren = new Vector();
135: for (int i = 0; i < childrenElements.length; i++) {
136: AbstractIntroElement element = childrenElements[i];
137: if (element.isOfType(elementMask))
138: typedChildren.addElement(element);
139: }
140: return convertToModelArray(typedChildren, elementMask);
141: }
142:
143: /**
144: * Utility method to convert all the content of a vector of
145: * AbstractIntroElements into an array of IntroElements cast to the correct
146: * class type. It is assumed that all elements in this vector are
147: * IntroElement instances. If elementMask is a predefined model type (ie:
148: * homogenous), then return array of corresponding type. Else, returns an
149: * array of IntroElements.
150: *
151: * @param vector
152: */
153: private Object[] convertToModelArray(Vector vector, int elementMask) {
154: int size = vector.size();
155: Object[] src = null;
156: switch (elementMask) {
157: // homogenous vector.
158: case AbstractIntroElement.GROUP:
159: src = new IntroGroup[size];
160: break;
161: case AbstractIntroElement.LINK:
162: src = new IntroLink[size];
163: break;
164: case AbstractIntroElement.TEXT:
165: src = new IntroText[size];
166: break;
167: case AbstractIntroElement.IMAGE:
168: src = new IntroImage[size];
169: break;
170: case AbstractIntroElement.HR:
171: src = new IntroSeparator[size];
172: break;
173: case AbstractIntroElement.HTML:
174: src = new IntroHTML[size];
175: break;
176: case AbstractIntroElement.INCLUDE:
177: src = new IntroInclude[size];
178: break;
179: case AbstractIntroElement.PAGE:
180: src = new IntroPage[size];
181: break;
182: case AbstractIntroElement.ABSTRACT_PAGE:
183: src = new AbstractIntroPage[size];
184: break;
185: case AbstractIntroElement.ABSTRACT_CONTAINER:
186: src = new AbstractIntroContainer[size];
187: break;
188: case AbstractIntroElement.HEAD:
189: src = new IntroHead[size];
190: break;
191: case AbstractIntroElement.PAGE_TITLE:
192: src = new IntroPageTitle[size];
193: break;
194: case AbstractIntroElement.ANCHOR:
195: src = new IntroAnchor[size];
196: break;
197: case AbstractIntroElement.CONTENT_PROVIDER:
198: src = new IntroContentProvider[size];
199: break;
200:
201: default:
202: // now handle left over abstract types. Vector is not homogenous.
203: src = new AbstractIntroElement[size];
204: break;
205: }
206: if (src == null)
207: return new Object[0];
208:
209: vector.copyInto(src);
210: return src;
211:
212: }
213:
214: /**
215: * Load all the children of this container. A container can have other
216: * containers, links, htmls, text, image, include. Load them in the order
217: * they appear in the xml content file.
218: */
219: protected void loadChildren() {
220: // init the children vector. old children are disposed automatically.
221: children = new Vector();
222:
223: NodeList nodeList = element.getChildNodes();
224: Vector vector = new Vector();
225: for (int i = 0; i < nodeList.getLength(); i++) {
226: Node node = nodeList.item(i);
227: if (node.getNodeType() == Node.ELEMENT_NODE)
228: vector.add(node);
229: }
230: Element[] filteredElements = new Element[vector.size()];
231: vector.copyInto(filteredElements);
232: // add the elements at the end children's vector.
233: insertElementsBefore(filteredElements, getBundle(), base,
234: children.size(), null);
235: loaded = true;
236: // we cannot free DOM model element because a page's children may be
237: // nulled when reflowing a content provider.
238: }
239:
240: /**
241: * Adds the given elements as children of this container, before the
242: * specified index.
243: *
244: * @param childElements
245: */
246: protected void insertElementsBefore(Element[] childElements,
247: Bundle bundle, String base, int index, String mixinStyle) {
248: for (int i = 0; i < childElements.length; i++) {
249: Element childElement = childElements[i];
250: AbstractIntroElement child = getModelChild(childElement,
251: bundle, base);
252: if (child != null) {
253: child.setParent(this );
254: child.setMixinStyle(mixinStyle);
255: children.add(index, child);
256: // index is only incremented if we actually added a child.
257: index++;
258: }
259: }
260: }
261:
262: /**
263: * Adds the given elements as children of this container, before the
264: * specified element. The element must be a direct child of this container.
265: *
266: * @param childElements
267: */
268: protected void insertElementsBefore(Element[] childElements,
269: Bundle bundle, String base, AbstractIntroElement child,
270: String mixinStyle) {
271: int childLocation = children.indexOf(child);
272: if (childLocation == -1)
273: // bad reference child.
274: return;
275: insertElementsBefore(childElements, bundle, base,
276: childLocation, mixinStyle);
277: }
278:
279: /**
280: * Adds a child to this container, depending on its type. Subclasses may
281: * override if there is a child specific to the subclass.
282: *
283: * @param childElements
284: */
285: protected AbstractIntroElement getModelChild(Element childElement,
286: Bundle bundle, String base) {
287:
288: AbstractIntroElement child = null;
289: if (childElement.getNodeName().equalsIgnoreCase(
290: IntroGroup.TAG_GROUP))
291: child = new IntroGroup(childElement, bundle, base);
292: else if (childElement.getNodeName().equalsIgnoreCase(
293: IntroLink.TAG_LINK))
294: child = new IntroLink(childElement, bundle, base);
295: else if (childElement.getNodeName().equalsIgnoreCase(
296: IntroText.TAG_TEXT))
297: child = new IntroText(childElement, bundle);
298: else if (childElement.getNodeName().equalsIgnoreCase(
299: IntroImage.TAG_IMAGE))
300: child = new IntroImage(childElement, bundle, base);
301: else if (childElement.getNodeName().equalsIgnoreCase(
302: IntroSeparator.TAG_HR))
303: child = new IntroSeparator(childElement, bundle, base);
304: else if (childElement.getNodeName().equalsIgnoreCase(
305: IntroHTML.TAG_HTML))
306: child = new IntroHTML(childElement, bundle, base);
307: else if (childElement.getNodeName().equalsIgnoreCase(
308: IntroInclude.TAG_INCLUDE))
309: child = new IntroInclude(childElement, bundle);
310: else if (childElement.getNodeName().equalsIgnoreCase(
311: IntroAnchor.TAG_ANCHOR))
312: child = new IntroAnchor(childElement, bundle);
313: else if (childElement.getNodeName().equalsIgnoreCase(
314: IntroContentProvider.TAG_CONTENT_PROVIDER))
315: child = new IntroContentProvider(childElement, bundle);
316: return child;
317: }
318:
319: /**
320: * Resolve each include in this container's children. Includes are lazily
321: * resolved on a per container basis, when the container is resolved.
322: */
323: protected void resolveChildren() {
324: AbstractIntroElement[] array = (AbstractIntroElement[]) children
325: .toArray(new AbstractIntroElement[children.size()]);
326: for (int i = 0; i < array.length; ++i) {
327: AbstractIntroElement child = array[i];
328: if (UAContentFilter.isFiltered(UAElementFactory
329: .newElement(child.getElement()),
330: IntroEvaluationContext.getContext())) {
331: children.remove(child);
332: } else if (child.getType() == AbstractIntroElement.INCLUDE) {
333: resolveInclude((IntroInclude) child);
334: }
335: }
336: resolved = true;
337: }
338:
339: /**
340: * Resolves an include. Gets the intro element pointed to by the include,
341: * and adds it as a child of this current container. If target is not a
342: * group, or any element that can be included in a group, ignore this
343: * include.
344: *
345: * @param include
346: */
347: private void resolveInclude(IntroInclude include) {
348: AbstractIntroElement target = findIncludeTarget(include);
349: if (target == null)
350: // target could not be found.
351: return;
352: if (target.isOfType(AbstractIntroElement.GROUP
353: | AbstractIntroElement.ABSTRACT_TEXT
354: | AbstractIntroElement.IMAGE
355: | AbstractIntroElement.TEXT
356: | AbstractIntroElement.PAGE_TITLE))
357: // be picky about model elements to include. Can not use
358: // BASE_ELEMENT model class because pages can not be included.
359: insertTarget(include, target);
360: }
361:
362: /**
363: * Filters the appropriate elements from the given Vector, according to the current
364: * environment. For example, if one of the elements has a tag to filter for os=linux and
365: * the os is win32, the element will not be returned in the resulting Vector.
366: *
367: * @param unfiltered the unfiltered elements
368: * @return a new Vector with elements filtered
369: */
370: private Vector filterChildren(Vector unfiltered) {
371: Vector filtered = new Vector();
372: Iterator iter = unfiltered.iterator();
373: while (iter.hasNext()) {
374: Object element = iter.next();
375: if (!UAContentFilter.isFiltered(element,
376: IntroEvaluationContext.getContext())) {
377: filtered.add(element);
378: }
379: }
380: return filtered;
381: }
382:
383: /**
384: * Find the target element pointed to by the path in the include. It is
385: * assumed that configId always points to an external config, and not the
386: * same config of the inlcude.
387: *
388: * @param include
389: * @param path
390: * @return
391: */
392: private AbstractIntroElement findIncludeTarget(IntroInclude include) {
393: String path = include.getPath();
394: IntroModelRoot targetModelRoot = (IntroModelRoot) getParentPage()
395: .getParent();
396: String targetConfigID = include.getConfigId();
397: if (targetConfigID != null)
398: targetModelRoot = ExtensionPointManager.getInst().getModel(
399: targetConfigID);
400: if (targetModelRoot == null)
401: // if the target config was not found, skip this include.
402: return null;
403: AbstractIntroElement target = findTarget(targetModelRoot, path);
404: return target;
405: }
406:
407: /**
408: * Finds the child element that corresponds to the given path in the passed
409: * model.<br>
410: * ps: This method could be a static method, but left as instance for model
411: * enhancements.
412: *
413: * @param model
414: * @param path
415: * @return
416: */
417: public AbstractIntroElement findTarget(
418: AbstractIntroContainer container, String path) {
419: // extract path segments. Get first segment to start search.
420: String[] pathSegments = StringUtil.split(path, "/"); //$NON-NLS-1$
421: if (container == null)
422: return null;
423:
424: AbstractIntroElement target = container
425: .findChild(pathSegments[0]);
426: if (target == null)
427: // there is no direct child with the specified first path segment.
428: return null;
429:
430: // found parent segment. now find each child segment.
431: for (int i = 1; i < pathSegments.length; i++) {
432: if (!(target instanceof AbstractIntroContainer)) {
433: // parent is not a container, so no point going on.
434: return null;
435: }
436: String pathSegment = pathSegments[i];
437: target = ((AbstractIntroContainer) target)
438: .findChild(pathSegment);
439: if (target == null)
440: // tried to find next segment and failed.
441: return null;
442: }
443: return target;
444: }
445:
446: public AbstractIntroElement findTarget(
447: AbstractIntroContainer container, String path,
448: String extensionId) {
449: // resolve path segments if they are incomplete.
450: if (path.indexOf("@") != -1) { //$NON-NLS-1$
451: // new in 3.2: dynamic resolution of incomplete target paths
452: IntroModelRoot root = getModelRoot();
453: if (root != null) {
454: path = root.resolvePath(extensionId, path);
455: if (path == null)
456: return null;
457: }
458:
459: }
460: return this .findTarget(container, path);
461: }
462:
463: public AbstractIntroElement findTarget(String path) {
464: return findTarget(this , path);
465: }
466:
467: /*
468: * searches direct children for the first child with the given id. The type
469: * of the child can be any model element that has an id. ie:
470: * AbstractIntroIdElement
471: *
472: * @see org.eclipse.ui.internal.intro.impl.model.IntroElement#getType()
473: */
474: public AbstractIntroElement findChild(String elementId) {
475: return findChild(elementId, ID_ELEMENT);
476: }
477:
478: /*
479: * searches direct children for the first child with the given id. The type
480: * of the child must be of the passed model types mask. This method handles
481: * the 3.0 style model for content. Pages enhance this behavior with DOM
482: * apis.
483: *
484: * @see org.eclipse.ui.internal.intro.impl.model.IntroElement#getType()
485: */
486: public AbstractIntroElement findChild(String elementId,
487: int elementMask) {
488: if (!loaded)
489: loadChildren();
490:
491: for (int i = 0; i < children.size(); i++) {
492: AbstractIntroElement aChild = (AbstractIntroElement) children
493: .elementAt(i);
494: if (!aChild.isOfType(ID_ELEMENT))
495: // includes and heads do not have ids, and so can not be
496: // referenced directly. This means that they can not be
497: // targets for other includes. Skip, just in case someone
498: // adds an id to it! Also, this applies to all elements in
499: // the model that do not have ids.
500: continue;
501: AbstractIntroIdElement child = (AbstractIntroIdElement) aChild;
502: if (child.getId() != null
503: && child.getId().equals(elementId)
504: && child.isOfType(elementMask))
505: return child;
506: }
507: // no child with given id and type found.
508: return null;
509: }
510:
511: private void insertTarget(IntroInclude include,
512: AbstractIntroElement target) {
513: int includeLocation = children.indexOf(include);
514: if (includeLocation == -1)
515: // should never be here.
516: return;
517: children.remove(includeLocation);
518: // handle merging target styles first, before changing target parent to
519: // enable inheritance of styles.
520: handleIncludeStyleInheritence(include, target);
521: // now clone the target node because original model should be kept
522: // intact.
523: AbstractIntroElement clonedTarget = null;
524: try {
525: clonedTarget = (AbstractIntroElement) target.clone();
526: } catch (CloneNotSupportedException ex) {
527: // should never be here.
528: Log.error("Failed to clone Intro model node.", ex); //$NON-NLS-1$
529: return;
530: }
531: // set parent of cloned target to be this container.
532: clonedTarget.setParent(this );
533: children.insertElementAt(clonedTarget, includeLocation);
534: }
535:
536: /**
537: * Updates the inherited styles based on the merge-style attribute. If we
538: * are including a shared group, or if we are including an element from the
539: * same page, do nothing. For inherited alt-styles, we have to cache the pd
540: * from which we inherited the styles to be able to access resources in that
541: * plugin. Also note that when including a container, it must be resolved
542: * otherwise reparenting will cause includes in this target container to
543: * fail.
544: *
545: * @param include
546: * @param target
547: */
548: private void handleIncludeStyleInheritence(IntroInclude include,
549: AbstractIntroElement target) {
550:
551: if (include.getMergeStyle() == false)
552: // target styles are not needed. nothing to do.
553: return;
554:
555: if (target.getParent().getType() == AbstractIntroElement.MODEL_ROOT
556: || target.getParentPage().equals(
557: include.getParentPage()))
558: // If we are including from this same page ie: target is in the
559: // same page, OR if we are including a shared group, defined
560: // under a config, do not include styles.
561: return;
562:
563: // Update the parent page styles. skip style if it is null. Note,
564: // include both the target page styles and inherited styles. The full
565: // page styles need to be include.
566: String style = target.getParentPage().getStyle();
567: if (style != null)
568: getParentPage().addStyle(style);
569:
570: // for alt-style cache bundle for loading resources.
571: style = target.getParentPage().getAltStyle();
572: if (style != null) {
573: Bundle bundle = target.getBundle();
574: getParentPage().addAltStyle(style, bundle);
575: }
576:
577: // now add inherited styles. Race condition could happen here if Page A
578: // is including from Page B which is in turn including from Page A.
579: getParentPage().addStyles(target.getParentPage().getStyles());
580: getParentPage().addAltStyles(
581: target.getParentPage().getAltStyles());
582:
583: }
584:
585: /**
586: * Creates a clone of the given target node. A clone is create by simply
587: * recreating that protion of the model.
588: *
589: * Note: looked into the clonable interface in Java, but it was not used
590: * because it makes modifications/additions to the model harder to maintain.
591: * Will revisit later.
592: *
593: * @param targer
594: * @return
595: */
596: protected AbstractIntroElement cloneTarget(
597: AbstractIntroElement target) {
598: return null;
599: }
600:
601: /*
602: * (non-Javadoc)
603: *
604: * @see org.eclipse.ui.internal.intro.impl.model.IntroElement#getType()
605: */
606: public int getType() {
607: return AbstractIntroElement.ABSTRACT_CONTAINER;
608: }
609:
610: /**
611: * Deep copy since class has mutable objects. Leave DOM element as a shallow
612: * reference copy since DOM is immutable.
613: */
614: public Object clone() throws CloneNotSupportedException {
615: AbstractIntroContainer clone = (AbstractIntroContainer) super
616: .clone();
617: clone.children = new Vector();
618: if (children != null) {
619: for (int i = 0; i < children.size(); i++) {
620: AbstractIntroElement cloneChild = (AbstractIntroElement) ((AbstractIntroElement) children
621: .elementAt(i)).clone();
622: cloneChild.setParent(clone);
623: clone.children.add(i, cloneChild);
624: }
625: }
626: return clone;
627: }
628:
629: /**
630: * Returns the element.
631: *
632: * @return
633: */
634: public Element getElement() {
635: return this .element;
636: }
637:
638: public String getBase() {
639: return base;
640: }
641:
642: /*
643: * Clears this container. This means emptying the children, and resetting
644: * flags.
645: */
646: public void clearChildren() {
647: this .children.clear();
648: }
649:
650: /**
651: * Adds a model element as a child. Caller is responsible for inserting
652: * model elements that rea valid as children.
653: *
654: * @param child
655: */
656: public void addChild(AbstractIntroElement child) {
657: children.add(child);
658: }
659:
660: public void removeChild(AbstractIntroElement child) {
661: children.remove(child);
662: }
663:
664: public String getBackgroundImage() {
665: return getAttribute(element, ATT_BG_IMAGE);
666: }
667:
668: }
|