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