001/*
002 * (C) Copyright 2006-2007 Nuxeo SAS <http://nuxeo.com> and others
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Jean-Marc Orliaguet, Chalmers
011 *
012 * $Id$
013 */
014
015package org.nuxeo.theme.themes;
016
017import java.io.ByteArrayInputStream;
018import java.io.FileNotFoundException;
019import java.io.IOException;
020import java.io.InputStream;
021import java.net.MalformedURLException;
022import java.net.URL;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Date;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.LinkedHashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Properties;
032import java.util.Set;
033
034import javax.xml.parsers.DocumentBuilder;
035import javax.xml.parsers.DocumentBuilderFactory;
036import javax.xml.parsers.ParserConfigurationException;
037import javax.xml.xpath.XPath;
038import javax.xml.xpath.XPathConstants;
039import javax.xml.xpath.XPathExpressionException;
040import javax.xml.xpath.XPathFactory;
041
042import org.apache.commons.logging.Log;
043import org.apache.commons.logging.LogFactory;
044import org.nuxeo.runtime.api.Framework;
045import org.nuxeo.theme.Manager;
046import org.nuxeo.theme.elements.Element;
047import org.nuxeo.theme.elements.ElementFactory;
048import org.nuxeo.theme.elements.ElementFormatter;
049import org.nuxeo.theme.elements.ThemeElement;
050import org.nuxeo.theme.formats.Format;
051import org.nuxeo.theme.formats.FormatFactory;
052import org.nuxeo.theme.formats.styles.Style;
053import org.nuxeo.theme.fragments.Fragment;
054import org.nuxeo.theme.fragments.FragmentFactory;
055import org.nuxeo.theme.nodes.NodeException;
056import org.nuxeo.theme.perspectives.PerspectiveType;
057import org.nuxeo.theme.presets.CustomPresetType;
058import org.nuxeo.theme.presets.PresetManager;
059import org.nuxeo.theme.presets.PresetType;
060import org.nuxeo.theme.properties.FieldIO;
061import org.nuxeo.theme.resources.ResourceBank;
062import org.nuxeo.theme.types.TypeFamily;
063import org.nuxeo.theme.types.TypeRegistry;
064import org.w3c.dom.Document;
065import org.w3c.dom.NamedNodeMap;
066import org.w3c.dom.Node;
067import org.w3c.dom.NodeList;
068import org.xml.sax.InputSource;
069import org.xml.sax.SAXException;
070
071public class ThemeParser {
072
073    private static final Log log = LogFactory.getLog(ThemeParser.class);
074
075    private static final String DOCROOT_NAME = "theme";
076
077    private static final XPath xpath = XPathFactory.newInstance().newXPath();
078
079    public static void registerTheme(final ThemeDescriptor themeDescriptor, final boolean preload)
080            throws ThemeIOException {
081        registerTheme(themeDescriptor, null, preload);
082    }
083
084    public static void registerTheme(final ThemeDescriptor themeDescriptor, final String xmlSource,
085            final boolean preload) throws ThemeIOException {
086        final String src = themeDescriptor.getSrc();
087        InputStream in = null;
088        try {
089            if (xmlSource == null) {
090                URL url = null;
091                try {
092                    url = new URL(src);
093                } catch (MalformedURLException e) {
094                    if (themeDescriptor.getContext() != null) {
095                        url = themeDescriptor.getContext().getResource(src);
096                    } else {
097                        url = Thread.currentThread().getContextClassLoader().getResource(src);
098                    }
099                }
100                if (url == null) {
101                    throw new ThemeIOException("Incorrect theme URL: " + src);
102                }
103                in = url.openStream();
104            } else {
105                in = new ByteArrayInputStream(xmlSource.getBytes());
106            }
107            registerThemeFromInputStream(themeDescriptor, in, preload);
108        } catch (FileNotFoundException e) {
109            throw new ThemeIOException("File not found: " + src, e);
110        } catch (IOException e) {
111            throw new ThemeIOException("Could not open file: " + src, e);
112        } catch (ThemeException e) {
113            throw new ThemeIOException("Parsing error: " + src, e);
114        } finally {
115            if (in != null) {
116                try {
117                    in.close();
118                } catch (IOException e) {
119                    log.error(e);
120                } finally {
121                    in = null;
122                }
123            }
124        }
125    }
126
127    private static void registerThemeFromInputStream(final ThemeDescriptor themeDescriptor, final InputStream in,
128            boolean preload) throws ThemeIOException, ThemeException {
129        String themeName = null;
130
131        final InputSource is = new InputSource(in);
132        final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
133        try {
134            dbf.setFeature("http://xml.org/sax/features/validation", false);
135            dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
136        } catch (ParserConfigurationException e) {
137            log.debug("Could not set DTD non-validation feature");
138        }
139
140        DocumentBuilder db;
141        try {
142            db = dbf.newDocumentBuilder();
143        } catch (ParserConfigurationException e) {
144            throw new ThemeIOException(e);
145        }
146        Document document;
147        try {
148            document = db.parse(is);
149        } catch (SAXException e) {
150            throw new ThemeIOException(e);
151        } catch (IOException e) {
152            throw new ThemeIOException(e);
153        }
154        final org.w3c.dom.Element docElem = document.getDocumentElement();
155        if (!docElem.getNodeName().equals(DOCROOT_NAME)) {
156            throw new ThemeIOException("No <" + DOCROOT_NAME + "> document tag found in " + in.toString()
157                    + ", ignoring the resource.");
158        }
159
160        themeName = docElem.getAttributes().getNamedItem("name").getNodeValue();
161        if (!ThemeManager.validateThemeName(themeName)) {
162            throw new ThemeIOException(
163                    "Theme names may only contain alpha-numeric characters, underscores and hyphens: " + themeName);
164        }
165        themeDescriptor.setName(themeName);
166
167        loadTheme(themeDescriptor, docElem, preload);
168    }
169
170    private static void loadTheme(ThemeDescriptor themeDescriptor, org.w3c.dom.Element docElem, boolean preload)
171            throws ThemeException, ThemeIOException {
172        final ThemeManager themeManager = Manager.getThemeManager();
173
174        // remove old theme
175        String themeName = themeDescriptor.getName();
176        ThemeElement oldTheme = themeManager.getThemeByName(themeName);
177        if (oldTheme != null) {
178            try {
179                themeManager.destroyElement(oldTheme);
180            } catch (NodeException e) {
181                throw new ThemeIOException("Failed to destroy theme: " + themeName, e);
182            }
183        }
184
185        Node baseNode = getBaseNode(docElem);
186
187        final Map<Integer, String> inheritanceMap = new HashMap<Integer, String>();
188        final Map<Style, Map<String, Properties>> commonStyles = new LinkedHashMap<Style, Map<String, Properties>>();
189
190        // create a new theme
191        ThemeElement theme = (ThemeElement) ElementFactory.create("theme");
192        theme.setName(themeName);
193        Node description = docElem.getAttributes().getNamedItem("description");
194        if (description != null) {
195            theme.setDescription(description.getNodeValue());
196        }
197
198        String resourceBankName = null;
199        Node resourceBankNode = docElem.getAttributes().getNamedItem("resource-bank");
200        if (resourceBankNode != null) {
201            resourceBankName = resourceBankNode.getNodeValue();
202            themeDescriptor.setResourceBankName(resourceBankName);
203        }
204
205        Node templateEngines = docElem.getAttributes().getNamedItem("template-engines");
206        if (templateEngines != null) {
207            themeDescriptor.setTemplateEngines(Arrays.asList(templateEngines.getNodeValue().split(",")));
208        }
209
210        if (preload) {
211            // Only register pages
212            registerThemePages(theme, baseNode);
213
214        } else {
215            // Register resources from remote bank
216            if (resourceBankName != null) {
217                try {
218                    ResourceBank resourceBank = ThemeManager.getResourceBank(resourceBankName);
219                    resourceBank.connect(themeName);
220                } catch (ThemeException e) {
221                    log.warn("Resource bank not found: " + resourceBankName);
222                }
223            }
224
225            // register custom presets
226            for (Node n : getChildElementsByTagName(docElem, "presets")) {
227                parsePresets(theme, n);
228            }
229
230            // register formats
231            for (Node n : getChildElementsByTagName(docElem, "formats")) {
232                parseFormats(theme, docElem, commonStyles, inheritanceMap, n);
233            }
234
235            // setup style inheritance
236            for (Map.Entry<Integer, String> entry : inheritanceMap.entrySet()) {
237                Integer styleUid = entry.getKey();
238                String inheritedStyleName = entry.getValue();
239                Format style = ThemeManager.getFormatById(styleUid);
240                Format inheritedStyle = (Format) themeManager.getNamedObject(themeName, "style", inheritedStyleName);
241                if (inheritedStyle == null) {
242                    log.warn("Cannot make style inherit from unknown style : " + inheritedStyleName);
243                    continue;
244                }
245                themeManager.makeFormatInherit(style, inheritedStyle);
246            }
247
248            // styles created by the parser
249            createCommonStyles(themeName, commonStyles);
250
251            // register element properties
252            for (Node n : getChildElementsByTagName(docElem, "properties")) {
253                parseProperties(docElem, n);
254            }
255
256            parseLayout(theme, baseNode);
257
258            themeManager.removeOrphanedFormats();
259        }
260
261        if (preload) {
262            log.debug("Registered THEME: " + themeName);
263            themeDescriptor.setLastLoaded(null);
264        } else {
265            log.debug("Loaded THEME: " + themeName);
266            themeDescriptor.setLastLoaded(new Date());
267        }
268
269        // Register in the type registry
270        themeManager.registerTheme(theme);
271    }
272
273    public static boolean checkElementName(String name) throws ThemeIOException {
274        return name.matches("[a-z][a-z0-9_\\-\\s]+");
275    }
276
277    public static void registerThemePages(final Element parent, Node node) throws ThemeIOException, ThemeException {
278        for (Node n : getChildElements(node)) {
279            String nodeName = n.getNodeName();
280            NamedNodeMap attributes = n.getAttributes();
281            Element elem;
282            if ("page".equals(nodeName)) {
283                elem = ElementFactory.create(nodeName);
284
285                Node nameAttr = attributes.getNamedItem("name");
286                if (nameAttr != null) {
287                    String elementName = nameAttr.getNodeValue();
288                    if (checkElementName(elementName)) {
289                        elem.setName(elementName);
290                    } else {
291                        throw new ThemeIOException("Page name not allowed: " + elementName);
292                    }
293                }
294
295                try {
296                    parent.addChild(elem);
297                } catch (NodeException e) {
298                    throw new ThemeIOException("Failed to parse layout.", e);
299                }
300            }
301        }
302    }
303
304    public static void parseLayout(final Element parent, Node node) throws ThemeIOException, ThemeException {
305        TypeRegistry typeRegistry = Manager.getTypeRegistry();
306        ThemeManager themeManager = Manager.getThemeManager();
307        for (String formatName : typeRegistry.getTypeNames(TypeFamily.FORMAT)) {
308            Format format = (Format) node.getUserData(formatName);
309            if (format != null) {
310                if (ElementFormatter.getElementsFor(format).isEmpty()) {
311                    ElementFormatter.setFormat(parent, format);
312                } else {
313                    Format duplicatedFormat = themeManager.duplicateFormat(format);
314                    ElementFormatter.setFormat(parent, duplicatedFormat);
315                }
316            }
317        }
318
319        Properties properties = (Properties) node.getUserData("properties");
320        if (properties != null) {
321            FieldIO.updateFieldsFromProperties(parent, properties);
322        }
323
324        for (Node n : getChildElements(node)) {
325            String nodeName = n.getNodeName();
326            NamedNodeMap attributes = n.getAttributes();
327            Element elem;
328
329            if ("fragment".equals(nodeName)) {
330                String fragmentType = attributes.getNamedItem("type").getNodeValue();
331                elem = FragmentFactory.create(fragmentType);
332                if (elem == null) {
333                    log.error("Could not create fragment: " + fragmentType);
334                    continue;
335                }
336                Fragment fragment = (Fragment) elem;
337                Node perspectives = attributes.getNamedItem("perspectives");
338                if (perspectives != null) {
339                    for (String perspectiveName : perspectives.getNodeValue().split(",")) {
340
341                        PerspectiveType perspective = (PerspectiveType) typeRegistry.lookup(TypeFamily.PERSPECTIVE,
342                                perspectiveName);
343
344                        if (perspective == null) {
345                            log.warn("Could not find perspective: " + perspectiveName);
346                        } else {
347                            fragment.setVisibleInPerspective(perspective);
348                        }
349                    }
350                }
351            } else {
352                elem = ElementFactory.create(nodeName);
353            }
354
355            if (elem == null) {
356                throw new ThemeIOException("Could not parse node: " + nodeName);
357            }
358
359            Node nameAttr = attributes.getNamedItem("name");
360            if (nameAttr != null) {
361                String elementName = nameAttr.getNodeValue();
362                if (checkElementName(elementName)) {
363                    elem.setName(elementName);
364                } else {
365                    log.warn("Element names may only contain lower-case alpha-numeric characters, digits, underscores, spaces and dashes: "
366                            + elementName);
367                }
368            }
369
370            Node classAttr = attributes.getNamedItem("class");
371            if (classAttr != null) {
372                elem.setCssClassName(classAttr.getNodeValue());
373            }
374
375            String description = getCommentAssociatedTo(n);
376            if (description != null) {
377                elem.setDescription(description);
378            }
379
380            try {
381                parent.addChild(elem);
382            } catch (NodeException e) {
383                throw new ThemeIOException("Failed to parse layout.", e);
384            }
385            parseLayout(elem, n);
386        }
387    }
388
389    public static void parsePresets(final ThemeElement theme, Node node) {
390        final TypeRegistry typeRegistry = Manager.getTypeRegistry();
391        final String themeName = theme.getName();
392        PresetManager.clearCustomPresets(themeName);
393        for (Node n : getChildElements(node)) {
394            NamedNodeMap attrs = n.getAttributes();
395            final String name = attrs.getNamedItem("name").getNodeValue();
396            final String category = attrs.getNamedItem("category").getNodeValue();
397            final String value = n.getTextContent();
398            final String group = themeName; // use the theme's name as
399            // group name
400
401            final Node labelAttr = attrs.getNamedItem("label");
402            String label = "";
403            if (labelAttr != null) {
404                label = labelAttr.getNodeValue();
405            }
406
407            final Node descriptionAttr = attrs.getNamedItem("description");
408            String description = "";
409            if (descriptionAttr != null) {
410                description = descriptionAttr.getNodeValue();
411            }
412
413            PresetType preset = new CustomPresetType(name, value, group, category, label, description);
414            typeRegistry.register(preset);
415        }
416    }
417
418    public static void parseFormats(final ThemeElement theme, org.w3c.dom.Element doc,
419            Map<Style, Map<String, Properties>> commonStyles, Map<Integer, String> inheritanceMap, Node node)
420            throws ThemeIOException, ThemeException {
421        Node baseNode = getBaseNode(doc);
422        String themeName = theme.getName();
423
424        String resourceBankName = null;
425        ThemeDescriptor themeDescriptor = ThemeManager.getThemeDescriptorByThemeName(themeName);
426        if (themeDescriptor != null) {
427            resourceBankName = themeDescriptor.getResourceBankName();
428        }
429
430        ThemeManager themeManager = Manager.getThemeManager();
431
432        for (Node n : getChildElements(node)) {
433            String nodeName = n.getNodeName();
434            NamedNodeMap attributes = n.getAttributes();
435            Node elementItem = attributes.getNamedItem("element");
436            String elementXPath = null;
437            if (elementItem != null) {
438                elementXPath = elementItem.getNodeValue();
439            }
440
441            Format format;
442            try {
443                format = FormatFactory.create(nodeName);
444            } catch (ThemeException e) {
445                throw new ThemeIOException(e);
446            }
447            format.setProperties(getPropertiesFromNode(n));
448
449            String description = getCommentAssociatedTo(n);
450            if (description != null) {
451                format.setDescription(description);
452            }
453
454            if ("widget".equals(nodeName)) {
455                List<Node> viewNodes = getChildElementsByTagName(n, "view");
456                if (!viewNodes.isEmpty()) {
457                    format.setName(viewNodes.get(0).getTextContent());
458                }
459
460            } else if ("layout".equals(nodeName)) {
461                // TODO: validate layout properties
462
463            } else if ("style".equals(nodeName)) {
464                Node nameAttr = attributes.getNamedItem("name");
465                Style style = (Style) format;
466
467                // register the style name
468                String styleName = null;
469                if (nameAttr != null) {
470                    styleName = nameAttr.getNodeValue();
471                    // the style may have been registered already
472                    Style registeredStyle = (Style) themeManager.getNamedObject(themeName, "style", styleName);
473                    if (registeredStyle == null) {
474                        style.setName(styleName);
475                        themeManager.setNamedObject(themeName, "style", style);
476                    } else {
477                        style = registeredStyle;
478                    }
479                }
480
481                Node inheritedAttr = attributes.getNamedItem("inherit");
482                if (inheritedAttr != null) {
483                    String inheritedName = inheritedAttr.getNodeValue();
484                    if ("".equals(inheritedName)) {
485                        continue;
486                    }
487                    inheritanceMap.put(style.getUid(), inheritedName);
488                }
489
490                Node remoteAttr = attributes.getNamedItem("remote");
491                if (remoteAttr != null) {
492                    Boolean remote = Boolean.valueOf(remoteAttr.getNodeValue());
493                    if (style.isNamed()) {
494                        style.setRemote(remote);
495                    } else {
496                        log.warn("Only named styles can be remote, ignoring remote attribute on" + style.getUid());
497                    }
498                }
499
500                if (styleName != null && elementXPath != null) {
501                    log.warn("Style parser: named style '" + styleName + "' cannot have an 'element' attribute: '"
502                            + elementXPath + "'.");
503                    continue;
504                }
505
506                List<Node> selectorNodes = getChildElementsByTagName(n, "selector");
507
508                if (style.isRemote() && resourceBankName != null) {
509                    if (!selectorNodes.isEmpty()) {
510                        style.setCustomized(true);
511                    }
512                }
513
514                // Use style properties from the theme
515                for (Node selectorNode : selectorNodes) {
516                    NamedNodeMap attrs = selectorNode.getAttributes();
517                    Node pathAttr = attrs.getNamedItem("path");
518                    if (pathAttr == null) {
519                        log.warn(String.format("Style parser: named style '%s' has a selector with no path: ignored",
520                                styleName));
521                        continue;
522                    }
523                    String path = pathAttr.getNodeValue();
524
525                    String viewName = null;
526                    Node viewAttr = attrs.getNamedItem("view");
527                    if (viewAttr != null) {
528                        viewName = viewAttr.getNodeValue();
529                    }
530
531                    String selectorDescription = getCommentAssociatedTo(selectorNode);
532                    if (selectorDescription != null) {
533                        style.setSelectorDescription(path, viewName, selectorDescription);
534                    }
535
536                    // BBB: remove in a later release
537                    if (elementXPath != null && (viewName == null || viewName.equals("*"))) {
538                        log.warn("Style parser: trying to guess the view name for: " + elementXPath);
539                        viewName = guessViewNameFor(doc, elementXPath);
540                        if (viewName == null) {
541                            if (!commonStyles.containsKey(style)) {
542                                commonStyles.put(style, new LinkedHashMap<String, Properties>());
543                            }
544                            commonStyles.get(style).put(path, getPropertiesFromNode(selectorNode));
545                        }
546                    }
547
548                    if (styleName != null) {
549                        if (viewName != null) {
550                            log.info("Style parser: ignoring view name '" + viewName + "' in named style '" + styleName
551                                    + "'.");
552                        }
553                        viewName = "*";
554                    }
555
556                    if (viewName != null) {
557                        style.setPropertiesFor(viewName, path, getPropertiesFromNode(selectorNode));
558                    }
559                }
560            }
561
562            themeManager.registerFormat(format);
563            if (elementXPath != null) {
564                if ("".equals(elementXPath)) {
565                    baseNode.setUserData(nodeName, format, null);
566                } else {
567                    for (Node element : getNodesByXPath(baseNode, elementXPath)) {
568                        element.setUserData(nodeName, format, null);
569                    }
570                }
571            }
572        }
573
574    }
575
576    public static void createCommonStyles(String themeName, Map<Style, Map<String, Properties>> commonStyles)
577            throws ThemeException {
578        ThemeManager themeManager = Manager.getThemeManager();
579        int count = 1;
580        for (Style parent : commonStyles.keySet()) {
581            Style s = (Style) FormatFactory.create("style");
582            String name = "";
583            while (true) {
584                name = String.format("common style %s", count);
585                if (themeManager.getNamedObject(themeName, "style", name) == null) {
586                    break;
587                }
588                count += 1;
589            }
590            s.setName(name);
591            themeManager.registerFormat(s);
592            themeManager.setNamedObject(themeName, "style", s);
593            Map<String, Properties> map = commonStyles.get(parent);
594            for (Map.Entry<String, Properties> entry : map.entrySet()) {
595                s.setPropertiesFor("*", entry.getKey(), entry.getValue());
596            }
597            // if the style already inherits, preserve the inheritance
598            Style ancestor = (Style) ThemeManager.getAncestorFormatOf(parent);
599            if (ancestor != null) {
600                themeManager.makeFormatInherit(s, ancestor);
601            }
602
603            themeManager.makeFormatInherit(parent, s);
604            log.info("Created extra style: " + s.getName());
605        }
606    }
607
608    public static void parseProperties(org.w3c.dom.Element doc, Node node) throws ThemeIOException {
609        NamedNodeMap attributes = node.getAttributes();
610        Node elementAttr = attributes.getNamedItem("element");
611        if (elementAttr == null) {
612            throw new ThemeIOException("<properties> node has no 'element' attribute.");
613        }
614        String elementXPath = elementAttr.getNodeValue();
615
616        Node baseNode = getBaseNode(doc);
617        Node element = null;
618        try {
619            element = (Node) xpath.evaluate(elementXPath, baseNode, XPathConstants.NODE);
620        } catch (XPathExpressionException e) {
621            throw new ThemeIOException(e);
622        }
623        if (element == null) {
624            throw new ThemeIOException("Could not find the element associated to: " + elementXPath);
625        }
626        Properties properties = getPropertiesFromNode(node);
627        if (properties != null) {
628            element.setUserData("properties", properties, null);
629        }
630    }
631
632    private static Properties getPropertiesFromNode(Node node) {
633        Properties properties = new Properties();
634        for (Node n : getChildElements(node)) {
635            String textContent = n.getTextContent();
636            Node presetAttr = n.getAttributes().getNamedItem("preset");
637            if (presetAttr != null) {
638                String presetName = presetAttr.getNodeValue();
639                if (presetName != null) {
640                    textContent = String.format("\"%s\"", presetName);
641                }
642            }
643            properties.setProperty(n.getNodeName(), Framework.expandVars(textContent));
644        }
645        return properties;
646    }
647
648    private static List<Node> getChildElements(Node node) {
649        List<Node> nodes = new ArrayList<Node>();
650        NodeList childNodes = node.getChildNodes();
651        for (int i = 0; i < childNodes.getLength(); i++) {
652            Node n = childNodes.item(i);
653            if (n.getNodeType() == Node.ELEMENT_NODE) {
654                nodes.add(n);
655            }
656        }
657        return nodes;
658    }
659
660    public static List<Node> getChildElementsByTagName(Node node, String tagName) {
661        List<Node> nodes = new ArrayList<Node>();
662        NodeList childNodes = node.getChildNodes();
663        for (int i = 0; i < childNodes.getLength(); i++) {
664            Node n = childNodes.item(i);
665            if (n.getNodeType() == Node.ELEMENT_NODE && tagName.equals(n.getNodeName())) {
666                nodes.add(n);
667            }
668        }
669        return nodes;
670    }
671
672    public static Node getBaseNode(org.w3c.dom.Element doc) throws ThemeIOException {
673        Node baseNode = null;
674        try {
675            baseNode = (Node) xpath.evaluate('/' + DOCROOT_NAME + "/layout", doc, XPathConstants.NODE);
676        } catch (XPathExpressionException e) {
677            throw new ThemeIOException(e);
678        }
679        if (baseNode == null) {
680            throw new ThemeIOException("No <layout> section found.");
681        }
682        return baseNode;
683    }
684
685    private static String getCommentAssociatedTo(Node node) {
686        Node n = node;
687        while (true) {
688            n = n.getPreviousSibling();
689            if (n == null) {
690                break;
691            }
692            if (n.getNodeType() == Node.ELEMENT_NODE) {
693                break;
694            }
695            if (n.getNodeType() == Node.COMMENT_NODE) {
696                return n.getNodeValue().trim();
697            }
698        }
699        return null;
700    }
701
702    // BBB shouldn't have to guess view names
703    private static String guessViewNameFor(org.w3c.dom.Element doc, String elementXPath) {
704        NodeList widgetNodes = doc.getElementsByTagName("widget");
705        Set<String> candidates = new HashSet<String>();
706        String[] elements = elementXPath.split("\\|");
707        for (int i = 0; i < widgetNodes.getLength(); i++) {
708            Node node = widgetNodes.item(i);
709            NamedNodeMap attributes = node.getAttributes();
710            Node elementAttr = attributes.getNamedItem("element");
711            if (elementAttr != null) {
712                String[] widgetElements = elementAttr.getNodeValue().split("\\|");
713                for (String element : elements) {
714                    for (String widgetElement : widgetElements) {
715                        if (element.equals(widgetElement)) {
716                            List<Node> viewNodes = getChildElementsByTagName(node, "view");
717                            if (!viewNodes.isEmpty()) {
718                                candidates.add(viewNodes.get(0).getTextContent());
719                            }
720                        }
721                    }
722                }
723            }
724        }
725        if (candidates.size() == 1) {
726            return candidates.iterator().next();
727        }
728        return null;
729    }
730
731    private static List<Node> getNodesByXPath(Node baseNode, String elementXPath) throws ThemeIOException {
732        final List<Node> nodes = new ArrayList<Node>();
733        if (elementXPath != null) {
734            try {
735                NodeList elementNodes = (NodeList) xpath.evaluate(elementXPath, baseNode, XPathConstants.NODESET);
736                for (int i = 0; i < elementNodes.getLength(); i++) {
737                    nodes.add(elementNodes.item(i));
738                }
739            } catch (XPathExpressionException e) {
740                throw new ThemeIOException(e);
741            }
742        }
743        return nodes;
744    }
745}