001    /**
002     * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
014    
015    package com.liferay.portlet.dynamicdatamapping.util;
016    
017    import com.liferay.portal.kernel.exception.PortalException;
018    import com.liferay.portal.kernel.exception.SystemException;
019    import com.liferay.portal.kernel.log.Log;
020    import com.liferay.portal.kernel.log.LogFactoryUtil;
021    import com.liferay.portal.kernel.security.pacl.DoPrivileged;
022    import com.liferay.portal.kernel.util.HtmlUtil;
023    import com.liferay.portal.kernel.util.LocaleUtil;
024    import com.liferay.portal.kernel.util.StringBundler;
025    import com.liferay.portal.kernel.util.StringPool;
026    import com.liferay.portal.kernel.util.StringUtil;
027    import com.liferay.portal.kernel.util.Validator;
028    import com.liferay.portal.kernel.xml.Attribute;
029    import com.liferay.portal.kernel.xml.Document;
030    import com.liferay.portal.kernel.xml.DocumentException;
031    import com.liferay.portal.kernel.xml.Element;
032    import com.liferay.portal.kernel.xml.Node;
033    import com.liferay.portal.kernel.xml.SAXReaderUtil;
034    import com.liferay.portal.kernel.xml.XMLSchema;
035    import com.liferay.portal.kernel.xml.XPath;
036    import com.liferay.portlet.dynamicdatamapping.StructureDefinitionException;
037    import com.liferay.portlet.dynamicdatamapping.StructureDuplicateElementException;
038    import com.liferay.portlet.dynamicdatamapping.model.DDMStructure;
039    import com.liferay.portlet.dynamicdatamapping.model.DDMStructureConstants;
040    import com.liferay.portlet.dynamicdatamapping.storage.Field;
041    import com.liferay.portlet.dynamicdatamapping.storage.FieldConstants;
042    import com.liferay.portlet.dynamicdatamapping.storage.Fields;
043    import com.liferay.util.xml.XMLFormatter;
044    
045    import java.io.IOException;
046    import java.io.Serializable;
047    
048    import java.util.Date;
049    import java.util.HashSet;
050    import java.util.Iterator;
051    import java.util.List;
052    import java.util.Locale;
053    import java.util.Set;
054    
055    /**
056     * @author Bruno Basto
057     * @author Brian Wing Shun Chan
058     */
059    @DoPrivileged
060    public class DDMXMLImpl implements DDMXML {
061    
062            @Override
063            public String formatXML(Document document) {
064                    try {
065                            return document.formattedString(_XML_INDENT);
066                    }
067                    catch (IOException ioe) {
068                            throw new SystemException(ioe);
069                    }
070            }
071    
072            @Override
073            public String formatXML(String xml) {
074    
075                    // This is only supposed to format your xml, however, it will also
076                    // unwantingly change © and other characters like it into their
077                    // respective readable versions
078    
079                    try {
080                            xml = StringUtil.replace(xml, "&#", "[$SPECIAL_CHARACTER$]");
081                            xml = XMLFormatter.toString(xml, _XML_INDENT);
082                            xml = StringUtil.replace(xml, "[$SPECIAL_CHARACTER$]", "&#");
083    
084                            return xml;
085                    }
086                    catch (IOException ioe) {
087                            throw new SystemException(ioe);
088                    }
089                    catch (org.dom4j.DocumentException de) {
090                            throw new SystemException(de);
091                    }
092            }
093    
094            @Override
095            public Fields getFields(DDMStructure structure, String xml)
096                    throws PortalException {
097    
098                    return getFields(structure, null, xml, null);
099            }
100    
101            @Override
102            public Fields getFields(
103                            DDMStructure structure, XPath xPath, String xml,
104                            List<String> fieldNames)
105                    throws PortalException {
106    
107                    Document document = null;
108    
109                    try {
110                            document = SAXReaderUtil.read(xml);
111                    }
112                    catch (DocumentException de) {
113                            if (_log.isDebugEnabled()) {
114                                    _log.debug(de.getMessage(), de);
115                            }
116    
117                            return null;
118                    }
119    
120                    if ((xPath != null) && !xPath.booleanValueOf(document)) {
121                            return null;
122                    }
123    
124                    Fields fields = new Fields();
125    
126                    Element rootElement = document.getRootElement();
127    
128                    List<Element> dynamicElementElements = rootElement.elements(
129                            "dynamic-element");
130    
131                    for (Element dynamicElementElement : dynamicElementElements) {
132                            String fieldName = dynamicElementElement.attributeValue("name");
133    
134                            if (!structure.hasField(fieldName) ||
135                                    ((fieldNames != null) && !fieldNames.contains(fieldName))) {
136    
137                                    continue;
138                            }
139    
140                            String fieldDataType = structure.getFieldDataType(fieldName);
141    
142                            List<Element> dynamicContentElements =
143                                    dynamicElementElement.elements("dynamic-content");
144    
145                            for (Element dynamicContentElement : dynamicContentElements) {
146                                    String fieldValue = dynamicContentElement.getText();
147    
148                                    String languageId = dynamicContentElement.attributeValue(
149                                            "language-id");
150    
151                                    Locale locale = LocaleUtil.fromLanguageId(languageId);
152    
153                                    Serializable fieldValueSerializable =
154                                            FieldConstants.getSerializable(fieldDataType, fieldValue);
155    
156                                    Field field = fields.get(fieldName);
157    
158                                    if (field == null) {
159                                            field = new Field();
160    
161                                            String defaultLanguageId =
162                                                    dynamicElementElement.attributeValue(
163                                                            "default-language-id");
164    
165                                            Locale defaultLocale = LocaleUtil.fromLanguageId(
166                                                    defaultLanguageId);
167    
168                                            field.setDefaultLocale(defaultLocale);
169    
170                                            field.setDDMStructureId(structure.getStructureId());
171                                            field.setName(fieldName);
172                                            field.setValue(locale, fieldValueSerializable);
173    
174                                            fields.put(field);
175                                    }
176                                    else {
177                                            field.addValue(locale, fieldValueSerializable);
178                                    }
179                            }
180                    }
181    
182                    return fields;
183            }
184    
185            @Override
186            public String getXML(Document document, Fields fields) {
187                    Element rootElement = null;
188    
189                    try {
190                            if (document != null) {
191                                    rootElement = document.getRootElement();
192                            }
193                            else {
194                                    document = SAXReaderUtil.createDocument();
195    
196                                    rootElement = document.addElement("root");
197                            }
198    
199                            Iterator<Field> itr = fields.iterator(true);
200    
201                            while (itr.hasNext()) {
202                                    Field field = itr.next();
203    
204                                    List<Node> nodes = getElementsByName(document, field.getName());
205    
206                                    for (Node node : nodes) {
207                                            document.remove(node);
208                                    }
209    
210                                    appendField(rootElement, field);
211                            }
212    
213                            return document.formattedString();
214                    }
215                    catch (IOException ioe) {
216                            throw new SystemException(ioe);
217                    }
218            }
219    
220            @Override
221            public String getXML(Fields fields) {
222                    return getXML(null, fields);
223            }
224    
225            public void setXMLSchema(XMLSchema xmlSchema) {
226                    _xmlSchema = xmlSchema;
227            }
228    
229            @Override
230            public String updateXMLDefaultLocale(
231                    String xml, Locale contentDefaultLocale,
232                    Locale contentNewDefaultLocale) {
233    
234                    try {
235                            if (LocaleUtil.equals(
236                                            contentDefaultLocale, contentNewDefaultLocale)) {
237    
238                                    return xml;
239                            }
240    
241                            Document document = SAXReaderUtil.read(xml);
242    
243                            Element rootElement = document.getRootElement();
244    
245                            Attribute availableLocalesAttribute = rootElement.attribute(
246                                    _AVAILABLE_LOCALES);
247    
248                            String contentNewDefaultLanguageId = LocaleUtil.toLanguageId(
249                                    contentNewDefaultLocale);
250    
251                            String availableLocalesAttributeValue =
252                                    availableLocalesAttribute.getValue();
253    
254                            if (!availableLocalesAttributeValue.contains(
255                                            contentNewDefaultLanguageId)) {
256    
257                                    StringBundler sb = new StringBundler(3);
258    
259                                    sb.append(availableLocalesAttribute.getValue());
260                                    sb.append(StringPool.COMMA);
261                                    sb.append(contentNewDefaultLanguageId);
262    
263                                    availableLocalesAttribute.setValue(sb.toString());
264                            }
265    
266                            Attribute defaultLocaleAttribute = rootElement.attribute(
267                                    _DEFAULT_LOCALE);
268    
269                            defaultLocaleAttribute.setValue(contentNewDefaultLanguageId);
270    
271                            fixElementsDefaultLocale(
272                                    rootElement, contentDefaultLocale, contentNewDefaultLocale);
273    
274                            return document.formattedString();
275                    }
276                    catch (DocumentException de) {
277                            throw new SystemException(de);
278                    }
279                    catch (IOException ioe) {
280                            throw new SystemException(ioe);
281                    }
282            }
283    
284            @Override
285            public String validateXML(String xml) throws PortalException {
286                    try {
287                            Document document = SAXReaderUtil.read(xml, _xmlSchema);
288    
289                            validate(document);
290    
291                            return document.asXML();
292                    }
293                    catch (StructureDefinitionException sde) {
294                            throw sde;
295                    }
296                    catch (StructureDuplicateElementException sdee) {
297                            throw sdee;
298                    }
299                    catch (Exception e) {
300                            if (_log.isDebugEnabled()) {
301                                    _log.debug("Invalid XML content " + e.getMessage(), e);
302                            }
303    
304                            throw new StructureDefinitionException(e);
305                    }
306            }
307    
308            protected void appendField(Element element, Field field) {
309                    Element dynamicElementElement = element.addElement("dynamic-element");
310    
311                    dynamicElementElement.addAttribute(
312                            "default-language-id",
313                            LocaleUtil.toLanguageId(field.getDefaultLocale()));
314                    dynamicElementElement.addAttribute("name", field.getName());
315    
316                    for (Locale locale : field.getAvailableLocales()) {
317                            List<Serializable> values = field.getValues(locale);
318    
319                            for (Serializable value : values) {
320                                    Element dynamicContentElement =
321                                            dynamicElementElement.addElement("dynamic-content");
322    
323                                    dynamicContentElement.addAttribute(
324                                            "language-id", LocaleUtil.toLanguageId(locale));
325    
326                                    updateField(dynamicContentElement, value);
327                            }
328                    }
329            }
330    
331            protected void fixElementsDefaultLocale(
332                    Element element, Locale contentDefaultLocale,
333                    Locale contentNewDefaultLocale) {
334    
335                    for (Element dynamicElementElement :
336                                    element.elements(_DYNAMIC_ELEMENT)) {
337    
338                            Element importMetaDataElement =
339                                    (Element)dynamicElementElement.selectSingleNode(
340                                            "meta-data[@locale='" + contentNewDefaultLocale.toString() +
341                                                    "']");
342    
343                            if (importMetaDataElement == null) {
344                                    Element metaDataElement =
345                                            (Element)dynamicElementElement.selectSingleNode(
346                                                    "meta-data[@locale='" +
347                                                            contentDefaultLocale.toString() + "']");
348    
349                                    Element copiedMetadataElement = metaDataElement.createCopy();
350    
351                                    Attribute localeAttribute = copiedMetadataElement.attribute(
352                                            _LOCALE);
353    
354                                    String contentNewDefaultLanguageId = LocaleUtil.toLanguageId(
355                                            contentNewDefaultLocale);
356    
357                                    localeAttribute.setValue(contentNewDefaultLanguageId);
358    
359                                    dynamicElementElement.add(copiedMetadataElement);
360                            }
361    
362                            fixElementsDefaultLocale(
363                                    dynamicElementElement, contentDefaultLocale,
364                                    contentNewDefaultLocale);
365                    }
366            }
367    
368            protected List<Node> getElementsByName(Document document, String name) {
369                    name = HtmlUtil.escapeXPathAttribute(name);
370    
371                    XPath xPathSelector = SAXReaderUtil.createXPath(
372                            "//dynamic-element[@name=".concat(name).concat("]"));
373    
374                    return xPathSelector.selectNodes(document);
375            }
376    
377            protected void updateField(
378                    Element dynamicContentElement, Serializable fieldValue) {
379    
380                    dynamicContentElement.clearContent();
381    
382                    if (fieldValue instanceof Date) {
383                            Date valueDate = (Date)fieldValue;
384    
385                            fieldValue = valueDate.getTime();
386                    }
387    
388                    String valueString = String.valueOf(fieldValue);
389    
390                    dynamicContentElement.addCDATA(valueString.trim());
391            }
392    
393            protected void validate(Document document) throws Exception {
394                    XPath xPathSelector = SAXReaderUtil.createXPath("//dynamic-element");
395    
396                    List<Node> nodes = xPathSelector.selectNodes(document);
397    
398                    Set<String> elementNames = new HashSet<>();
399    
400                    for (Node node : nodes) {
401                            Element element = (Element)node;
402    
403                            String name = StringUtil.toLowerCase(
404                                    element.attributeValue("name"));
405    
406                            if (Validator.isNull(name)) {
407                                    throw new StructureDefinitionException(
408                                            "Element must have a name attribute " +
409                                                    element.formattedString());
410                            }
411    
412                            if (name.startsWith(DDMStructureConstants.XSD_NAME_RESERVED)) {
413                                    throw new StructureDefinitionException(
414                                            "Element name " + name + " is reserved");
415                            }
416    
417                            if (elementNames.contains(name)) {
418                                    throw new StructureDuplicateElementException(
419                                            "Element with name " + name + " already exists");
420                            }
421    
422                            elementNames.add(name);
423                    }
424            }
425    
426            private static final String _AVAILABLE_LOCALES = "available-locales";
427    
428            private static final String _DEFAULT_LOCALE = "default-locale";
429    
430            private static final String _DYNAMIC_ELEMENT = "dynamic-element";
431    
432            private static final String _LOCALE = "locale";
433    
434            private static final String _XML_INDENT = "  ";
435    
436            private static final Log _log = LogFactoryUtil.getLog(DDMXMLImpl.class);
437    
438            private XMLSchema _xmlSchema;
439    
440    }