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.BufferedWriter;
025import java.io.IOException;
026import java.io.StringWriter;
027import java.io.Writer;
028import java.util.ArrayList;
029import java.util.Enumeration;
030import java.util.Iterator;
031import java.util.List;
032import java.util.Map;
033import java.util.Properties;
034
035import javax.xml.parsers.DocumentBuilder;
036import javax.xml.parsers.DocumentBuilderFactory;
037import javax.xml.parsers.ParserConfigurationException;
038
039import org.apache.commons.logging.Log;
040import org.apache.commons.logging.LogFactory;
041import org.apache.xml.serialize.OutputFormat;
042import org.apache.xml.serialize.XMLSerializer;
043import org.nuxeo.theme.Manager;
044import org.nuxeo.theme.Utils;
045import org.nuxeo.theme.elements.Element;
046import org.nuxeo.theme.elements.ElementFormatter;
047import org.nuxeo.theme.elements.ThemeElement;
048import org.nuxeo.theme.formats.Format;
049import org.nuxeo.theme.formats.styles.Style;
050import org.nuxeo.theme.fragments.Fragment;
051import org.nuxeo.theme.nodes.Node;
052import org.nuxeo.theme.perspectives.PerspectiveType;
053import org.nuxeo.theme.presets.PresetManager;
054import org.nuxeo.theme.presets.PresetType;
055import org.nuxeo.theme.properties.FieldIO;
056import org.nuxeo.theme.uids.Identifiable;
057import org.w3c.dom.DOMException;
058import org.w3c.dom.Document;
059
060public class ThemeSerializer {
061
062    private static final Log log = LogFactory.getLog(ThemeSerializer.class);
063
064    private static final String DOCROOT_NAME = "theme";
065
066    private Document doc;
067
068    private List<Element> elements;
069
070    public Document serialize(final String src) throws ParserConfigurationException, DOMException, ThemeException,
071            ThemeIOException {
072        final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
073        try {
074            dbf.setFeature("http://xml.org/sax/features/validation", false);
075            dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
076        } catch (ParserConfigurationException e) {
077            log.debug("Could not set DTD non-validation feature");
078        }
079        final DocumentBuilder db = dbf.newDocumentBuilder();
080        doc = db.newDocument();
081        elements = new ArrayList<Element>();
082        final org.w3c.dom.Element root = doc.createElement(DOCROOT_NAME);
083        final ThemeManager themeManager = Manager.getThemeManager();
084        ThemeElement theme = themeManager.getThemeBySrc(src);
085
086        // Theme description and name
087        final String description = theme.getDescription();
088        if (description != null) {
089            root.setAttribute("description", description);
090        }
091
092        final String themeName = theme.getName();
093        root.setAttribute("name", themeName);
094
095        ThemeDescriptor themeDef = ThemeManager.getThemeDescriptor(src);
096        List<String> templateEngines = themeDef.getTemplateEngines();
097        if (templateEngines != null && templateEngines.size() > 0) {
098            final StringBuilder sb = new StringBuilder();
099            final Iterator<String> it = templateEngines.iterator();
100            while (it.hasNext()) {
101                sb.append(it.next());
102                if (it.hasNext()) {
103                    sb.append(",");
104                }
105            }
106            root.setAttribute("template-engines", sb.toString());
107        }
108
109        // Resource bank
110        String resourceBankName = themeDef.getResourceBankName();
111        if (resourceBankName != null) {
112            root.setAttribute("resource-bank", resourceBankName);
113        }
114
115        doc.appendChild(root);
116
117        // layout
118        final org.w3c.dom.Element layoutNode = doc.createElement("layout");
119        root.appendChild(layoutNode);
120
121        for (Node page : theme.getChildren()) {
122            serializeLayout((Element) page, layoutNode);
123        }
124
125        // element properties
126        for (Element element : elements) {
127            serializeProperties(element, root);
128        }
129
130        // presets
131        List<PresetType> customPresets = PresetManager.getCustomPresets(themeName);
132        if (!customPresets.isEmpty()) {
133            final org.w3c.dom.Element presetsNode = doc.createElement("presets");
134            root.appendChild(presetsNode);
135            for (PresetType preset : customPresets) {
136                final org.w3c.dom.Element presetNode = doc.createElement("preset");
137                presetNode.setAttribute("name", preset.getName());
138                presetNode.setAttribute("category", preset.getCategory());
139                presetNode.setAttribute("label", preset.getLabel());
140                presetNode.setAttribute("description", preset.getDescription());
141                presetNode.appendChild(doc.createTextNode(preset.getValue()));
142                presetsNode.appendChild(presetNode);
143            }
144        }
145
146        // formats
147        final org.w3c.dom.Element formatNode = doc.createElement("formats");
148        root.appendChild(formatNode);
149
150        for (String formatTypeName : themeManager.getFormatTypeNames()) {
151            // export named styles
152            for (Identifiable object : themeManager.getNamedObjects(themeName, formatTypeName)) {
153                Format format = (Format) object;
154                // skip unused remote styles
155                if (!format.isCustomized() && ThemeManager.listFormatsDirectlyInheritingFrom(format).isEmpty()) {
156                    continue;
157                }
158                serializeFormat(format, formatNode);
159            }
160            for (Format format : themeManager.getFormatsByTypeName(formatTypeName)) {
161                if (format.isNamed()) {
162                    continue;
163                }
164                // make sure that the format is used by this theme
165                boolean isUsedByThisTheme = false;
166                for (Element element : ElementFormatter.getElementsFor(format)) {
167                    if (element.isChildOf(theme) || element == theme) {
168                        isUsedByThisTheme = true;
169                        break;
170                    }
171                }
172                if (isUsedByThisTheme) {
173                    serializeFormat(format, formatNode);
174                }
175            }
176        }
177        return doc;
178    }
179
180    private void serializeProperties(final Element parent, final org.w3c.dom.Element domParent) throws DOMException,
181            ThemeIOException {
182        final org.w3c.dom.Element domProperties = doc.createElement("properties");
183        domProperties.setAttribute("element", parent.computeXPath());
184        for (Map.Entry<Object, Object> entry : FieldIO.dumpFieldsToProperties(parent).entrySet()) {
185            final org.w3c.dom.Element domProperty = doc.createElement((String) entry.getKey());
186            final String value = (String) entry.getValue();
187            domProperty.appendChild(doc.createTextNode(Utils.cleanUp(value)));
188            domProperties.appendChild(domProperty);
189        }
190        if (domProperties.hasChildNodes()) {
191            domParent.appendChild(domProperties);
192        }
193    }
194
195    private void serializeLayout(final Element parent, final org.w3c.dom.Element domParent) {
196        final String typeName = parent.getElementType().getTypeName();
197        final org.w3c.dom.Element domElement = doc.createElement(typeName);
198
199        elements.add(parent);
200
201        final String elementName = parent.getName();
202        if (elementName != null) {
203            domElement.setAttribute("name", elementName);
204        }
205
206        final String elementClassName = parent.getCssClassName();
207        if (elementClassName != null) {
208            domElement.setAttribute("class", elementClassName);
209        }
210
211        if (parent instanceof Fragment) {
212            domElement.setAttribute("type", ((Fragment) parent).getFragmentType().getTypeName());
213
214            // perspectives
215            final StringBuilder s = new StringBuilder();
216            final Iterator<PerspectiveType> it = ((Fragment) parent).getVisibilityPerspectives().iterator();
217            while (it.hasNext()) {
218                PerspectiveType perspective = it.next();
219                s.append(perspective.getTypeName());
220                if (it.hasNext()) {
221                    s.append(",");
222                }
223            }
224            if (s.length() > 0) {
225                domElement.setAttribute("perspectives", s.toString());
226            }
227        }
228
229        String description = parent.getDescription();
230        if (description != null) {
231            domParent.appendChild(doc.createComment(String.format(" %s ", description)));
232        }
233
234        domParent.appendChild(domElement);
235        for (Node child : parent.getChildren()) {
236            serializeLayout((Element) child, domElement);
237        }
238    }
239
240    private void serializeFormat(final Format format, final org.w3c.dom.Element domParent) {
241        final String typeName = format.getFormatType().getTypeName();
242        final org.w3c.dom.Element domElement = doc.createElement(typeName);
243
244        final String description = format.getDescription();
245        if (description != null) {
246            domParent.appendChild(doc.createComment(String.format(" %s ", description)));
247        }
248
249        StringBuilder s = new StringBuilder();
250        Iterator<Element> iter = ElementFormatter.getElementsFor(format).iterator();
251        boolean hasElement = iter.hasNext();
252        while (iter.hasNext()) {
253            Element element = iter.next();
254            s.append(element.computeXPath());
255            if (iter.hasNext()) {
256                s.append("|");
257            }
258        }
259        if (hasElement) {
260            domElement.setAttribute("element", s.toString());
261        }
262
263        // widgets
264        if ("widget".equals(typeName)) {
265            // view name
266            String viewName = format.getName();
267            org.w3c.dom.Element domView = doc.createElement("view");
268            domView.appendChild(doc.createTextNode(viewName));
269            domElement.appendChild(domView);
270
271            // properties
272            Properties properties = format.getProperties();
273            Enumeration<?> names = properties.propertyNames();
274
275            while (names.hasMoreElements()) {
276                String name = (String) names.nextElement();
277                if ("view".equals(name)) {
278                    continue;
279                }
280                String value = properties.getProperty(name);
281                org.w3c.dom.Element domAttr = doc.createElement(name);
282                domAttr.appendChild(doc.createTextNode(Utils.cleanUp(value)));
283                domElement.appendChild(domAttr);
284            }
285        }
286
287        // layout
288        else if ("layout".equals(typeName)) {
289            Properties properties = format.getProperties();
290            Enumeration<?> names = properties.propertyNames();
291            while (names.hasMoreElements()) {
292                String name = (String) names.nextElement();
293                String value = properties.getProperty(name);
294                org.w3c.dom.Element domView = doc.createElement(name);
295                domView.appendChild(doc.createTextNode(Utils.cleanUp(value)));
296                domElement.appendChild(domView);
297            }
298        }
299
300        // style
301        else if ("style".equals(typeName)) {
302            Style style = (Style) format;
303            if (style.isExternal()) {
304                return;
305            }
306            String styleName = style.getName();
307            Style ancestor = (Style) ThemeManager.getAncestorFormatOf(style);
308            if (styleName != null) {
309                domElement.setAttribute("name", styleName);
310            }
311            if (ancestor != null) {
312                domElement.setAttribute("inherit", ancestor.getName());
313            }
314            if (style.isRemote()) {
315                domElement.setAttribute("remote", "true");
316            }
317            if ((!style.isRemote() || style.isCustomized())) {
318                for (String viewName : style.getSelectorViewNames()) {
319                    for (String path : style.getPathsForView(viewName)) {
320                        Properties styleProperties = style.getPropertiesFor(viewName, path);
321                        if (styleProperties.isEmpty()) {
322                            continue;
323                        }
324                        org.w3c.dom.Element domSelector = doc.createElement("selector");
325                        path = Utils.cleanUp(path);
326                        domSelector.setAttribute("path", path);
327                        if (!"*".equals(viewName)) {
328                            domSelector.setAttribute("view", viewName);
329                        }
330
331                        for (Map.Entry<Object, Object> entry : styleProperties.entrySet()) {
332                            org.w3c.dom.Element domProperty = doc.createElement((String) entry.getKey());
333                            String value = (String) entry.getValue();
334                            String presetName = PresetManager.extractPresetName(null, value);
335                            if (presetName != null) {
336                                domProperty.setAttribute("preset", presetName);
337                            } else {
338                                domProperty.appendChild(doc.createTextNode(Utils.cleanUp(value)));
339                            }
340                            domSelector.appendChild(domProperty);
341                        }
342
343                        // Set selector description
344                        String selectorDescription = style.getSelectorDescription(path, viewName);
345                        if (selectorDescription != null) {
346                            domElement.appendChild(doc.createComment(String.format(" %s ", selectorDescription)));
347                        }
348
349                        domElement.appendChild(domSelector);
350                    }
351                }
352            }
353        }
354        domParent.appendChild(domElement);
355    }
356
357    public String serializeToXml(final String src) throws ThemeIOException {
358        return serializeToXml(src, 0);
359    }
360
361    public String serializeToXml(final String src, final int indent) throws ThemeIOException {
362        // serialize the theme into a document
363        try {
364            serialize(src);
365            // convert the document to XML
366            StringWriter sw = new StringWriter();
367            OutputFormat format = new OutputFormat(doc);
368            format.setIndenting(true);
369            format.setIndent(indent);
370            Writer output = new BufferedWriter(sw);
371            XMLSerializer serializer = new XMLSerializer(output, format);
372            serializer.serialize(doc);
373            return sw.toString();
374        } catch (IOException | DOMException | ParserConfigurationException | ThemeException e) {
375            throw new ThemeIOException(e);
376        }
377
378    }
379
380}