001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.xml.xml2json;
029
030import org.opencms.util.CmsStringUtil;
031import org.opencms.xml.CmsXmlContentDefinition;
032import org.opencms.xml.CmsXmlContentDefinition.SequenceType;
033import org.opencms.xml.CmsXmlUtils;
034import org.opencms.xml.content.CmsXmlContent;
035import org.opencms.xml.types.CmsXmlNestedContentDefinition;
036import org.opencms.xml.types.I_CmsXmlContentValue;
037import org.opencms.xml.types.I_CmsXmlSchemaType;
038
039import java.util.ArrayList;
040import java.util.Arrays;
041import java.util.Collections;
042import java.util.HashMap;
043import java.util.IdentityHashMap;
044import java.util.List;
045import java.util.Locale;
046import java.util.Map;
047import java.util.function.Consumer;
048
049import org.dom4j.Element;
050
051/**
052 * Tree representation of CmsXmlContent which is suitable for XML-to-JSON transformations.
053 */
054public class CmsXmlContentTree {
055
056    /**
057     * Field of a complex type.
058     *
059     * <p>
060     * This represents one or more elements with the same field name in a sequence in an XML content.
061     */
062    public class Field {
063
064        /** The field definition. */
065        I_CmsXmlSchemaType m_fieldDef;
066
067        /** The nodes for the individual field elements. */
068        List<Node> m_nodes;
069
070        /** The nested content definition (may be null). */
071        private CmsXmlContentDefinition m_nestedDef;
072
073        /** The parent node of the field. */
074        protected Node m_parentNode;
075
076        /**
077        * Create a new instance.
078        *
079        * @param fieldDef the field definition
080        * @param nodes the nodes for the individual field elements
081        */
082        public Field(I_CmsXmlSchemaType fieldDef, List<Node> nodes) {
083
084            m_fieldDef = fieldDef;
085            if (fieldDef instanceof CmsXmlNestedContentDefinition) {
086                m_nestedDef = ((CmsXmlNestedContentDefinition)fieldDef).getNestedContentDefinition();
087            }
088            m_nodes = nodes;
089        }
090
091        /**
092         * Gets the field definition.
093         *
094         * @return the field definition
095         */
096        public I_CmsXmlSchemaType getFieldDefinition() {
097
098            return m_fieldDef;
099        }
100
101        /**
102         * Gets the field name.
103         *
104         * @return the field name
105         */
106        public String getName() {
107
108            return m_fieldDef.getName();
109        }
110
111        /**
112         * Gets the node if there is exactly one in the node list, otherwise throws an error.
113         *
114         * @return the node
115         */
116        public Node getNode() {
117
118            if (m_nodes.size() != 1) {
119                throw new IllegalStateException(
120                    "Can't call getNode for field with a number of nodes different from 1.");
121            }
122            return m_nodes.get(0);
123        }
124
125        /**
126         * Gets the nodes for the individual elements with that field name.
127         *
128         * @return the sub-nodes for this field
129         */
130        public List<Node> getNodes() {
131
132            return m_nodes;
133        }
134
135        /**
136         * Gets the parent node.
137         *
138         * @return the parent node
139         */
140        public Node getParentNode() {
141
142            return m_parentNode;
143        }
144
145        /**
146         * Returns true if the field refers to a choice element with maxOccurs greater than 1.
147         *
148         * @return true if this is a multichoice attribute
149         */
150        public boolean isMultiChoice() {
151
152            return (m_nestedDef != null) && (m_nestedDef.getChoiceMaxOccurs() > 1);
153
154        }
155
156        /**
157         * Returns true if this is a multivalue field.
158         *
159         * @return true if this is a multivalue field
160         */
161        public boolean isMultivalue() {
162
163            return m_fieldDef.getMaxOccurs() > 1;
164        }
165
166        /**
167         * Returns true if this is an optional field.
168         *
169         * @return true if this is an optional field
170         */
171        public boolean isOptional() {
172
173            return m_fieldDef.getMinOccurs() == 0;
174        }
175
176        /**
177         * Sets the parent node of the field.
178         *
179         * @param parentNode the parent node
180         */
181        public void setParentNode(Node parentNode) {
182
183            m_parentNode = parentNode;
184        }
185
186        /**
187         * @see java.lang.Object#toString()
188         */
189        @Override
190        public String toString() {
191
192            String result = "FL " + m_fieldDef.getName() + ":\n";
193            for (Node child : m_nodes) {
194                String entry = CmsStringUtil.indentLines(child.toString(), 4) + "\n";
195                result += entry;
196
197            }
198            return result;
199        }
200    }
201
202    /**
203     * Represents a sequence in the XML content.
204     */
205    public class Node {
206
207        /** The content definition. */
208        private CmsXmlContentDefinition m_contentDefinition;
209
210        /** The underlying element. */
211        private Element m_elem;
212
213        /** The list of sequence fields. */
214        private List<Field> m_fields;
215
216        /** The node type. */
217        private NodeType m_type;
218
219        /** The content value. */
220        private I_CmsXmlContentValue m_value;
221
222        /**
223         * Creates a new instance.
224         * @param type the node type
225         * @param value the content value
226         * @param contentDef the content definition
227         * @param elem the underlying XML element
228         * @param fields the fields
229         */
230        public Node(
231            NodeType type,
232            I_CmsXmlContentValue value,
233            CmsXmlContentDefinition contentDef,
234            Element elem,
235            List<Field> fields) {
236
237            m_type = type;
238            m_contentDefinition = contentDef;
239            m_elem = elem;
240            m_fields = fields;
241            m_value = value;
242            if (m_fields != null) {
243                for (Field field : m_fields) {
244                    field.setParentNode(this);
245                }
246            } else {
247                m_fields = Collections.emptyList();
248            }
249        }
250
251        /**
252         * Gets the content definition.
253         *
254         * @return the contnt definition
255         */
256        public CmsXmlContentDefinition getContentDefinition() {
257
258            return m_contentDefinition;
259        }
260
261        /**
262         * Gets the DOM element for the node.
263         *
264         * @return the DOM element
265         */
266        public Element getElement() {
267
268            return m_elem;
269        }
270
271        /**
272         * Gets the fields for the sequence.
273         *
274         * @return the list of fields
275         */
276        public List<Field> getFields() {
277
278            return m_fields;
279        }
280
281        /**
282         * Gets the path of the node.
283         *
284         * @return the path of the node
285         */
286        @SuppressWarnings("synthetic-access")
287        public String getPath() {
288
289            return getValuePath(m_elem);
290        }
291
292        /**
293         * Gets the node type.
294         *
295         * @return the node type
296         */
297        public NodeType getType() {
298
299            return m_type;
300        }
301
302        /**
303         * Gets the content value (null for root node).
304         *
305         * @return the content value
306         */
307        public I_CmsXmlContentValue getValue() {
308
309            return m_value;
310        }
311
312        /**
313         * @see java.lang.Object#toString()
314         */
315        @Override
316        public String toString() {
317
318            StringBuilder buffer = new StringBuilder();
319            buffer.append("ND:\n");
320            for (Field field : m_fields) {
321                buffer.append(CmsStringUtil.indentLines(field.toString(), 4));
322            }
323            return buffer.toString();
324
325        }
326
327    }
328
329    /**
330     * Enum representing the type of the tree node.
331     */
332    public enum NodeType {
333        /** Complex value: choice. */
334        choice,
335        /** Complex value: sequence. */
336        sequence,
337
338        /** Simple value. */
339        simple;
340    }
341
342    /** Map from values to nodes. */
343    private IdentityHashMap<I_CmsXmlContentValue, Node> m_valueToNodeCache = new IdentityHashMap<>();
344
345    /** The content. */
346    private CmsXmlContent m_content;
347
348    /** The locale for which the tree should be generated. */
349    private Locale m_locale;
350
351    /** The root node. */
352    private Node m_root;
353
354    /**
355     * Creates a new instance and initializes the full tree for the given locale.
356     *
357     * @param content the content from which the tree should be generated
358     * @param locale the locale for which the tree should be generated
359     */
360    public CmsXmlContentTree(CmsXmlContent content, Locale locale) {
361
362        m_content = content;
363        m_locale = locale;
364        m_root = createNode(content.getLocaleNode(locale), content.getContentDefinition());
365        visitNodes(m_root, node -> {
366            if (node.getValue() != null) {
367                m_valueToNodeCache.put(node.getValue(), node);
368            }
369        });
370    }
371
372    /**
373     * Visits all Node instances that are descendants of a given node (including that node itself).
374     *
375     * @param node the root node
376     * @param handler the handler to be invoked for all descendant nodes
377     */
378    public static void visitNodes(Node node, Consumer<Node> handler) {
379
380        handler.accept(node);
381        for (Field field : node.getFields()) {
382            for (Node child : field.getNodes()) {
383                visitNodes(child, handler);
384            }
385        }
386    }
387
388    /**
389     * Creates a node for the given content definition and DOM element.
390     *
391     * @param elem the XML DOM element
392     * @param contentDef the content definition (null for non-nested values)
393     *
394     * @return the created node
395     */
396    public Node createNode(Element elem, CmsXmlContentDefinition contentDef) {
397
398        String path = getValuePath(elem);
399        I_CmsXmlContentValue value = path.isEmpty() ? null : m_content.getValue(path, m_locale);
400        if (contentDef == null) {
401            Node node = new Node(NodeType.simple, value, null, elem, null);
402            return node;
403        }
404
405        SequenceType seqType = contentDef.getSequenceType();
406        if ((seqType == SequenceType.MULTIPLE_CHOICE) || (seqType == SequenceType.SINGLE_CHOICE)) {
407            List<I_CmsXmlSchemaType> fieldDefinitions = contentDef.getTypeSequence();
408            Map<String, I_CmsXmlSchemaType> defMap = new HashMap<>();
409            for (I_CmsXmlSchemaType fieldDef : fieldDefinitions) {
410                String fieldName = fieldDef.getName();
411                defMap.put(fieldName, fieldDef);
412            }
413            List<Field> choiceFields = new ArrayList<>();
414            for (Element child : elem.elements()) {
415                I_CmsXmlSchemaType childFieldDef = defMap.get(child.getName());
416                CmsXmlContentDefinition nestedDef = null;
417                if (childFieldDef instanceof CmsXmlNestedContentDefinition) {
418                    CmsXmlNestedContentDefinition nestedDefType = (CmsXmlNestedContentDefinition)childFieldDef;
419                    nestedDef = nestedDefType.getNestedContentDefinition();
420                }
421                Node childNode = createNode(child, nestedDef);
422                Field choiceField = new Field(childFieldDef, Arrays.asList(childNode));
423                choiceFields.add(choiceField);
424            }
425            Node node = new Node(NodeType.choice, value, contentDef, elem, choiceFields);
426            return node;
427        }
428
429        if (seqType == SequenceType.SEQUENCE) {
430            List<I_CmsXmlSchemaType> fieldDefinitions = contentDef.getTypeSequence();
431            List<Field> fields = new ArrayList<>();
432            for (I_CmsXmlSchemaType fieldDef : fieldDefinitions) {
433                CmsXmlContentDefinition nestedDef = null;
434                if (fieldDef instanceof CmsXmlNestedContentDefinition) {
435                    CmsXmlNestedContentDefinition nestedDefType = (CmsXmlNestedContentDefinition)fieldDef;
436                    nestedDef = nestedDefType.getNestedContentDefinition();
437                }
438                String fieldName = fieldDef.getName();
439                String fieldPath = CmsXmlUtils.concatXpath(path, fieldName);
440                List<I_CmsXmlContentValue> fieldValues = m_content.getValues(fieldPath, m_locale);
441                List<Node> fieldChildren = new ArrayList<>();
442                for (int i = 0; i < fieldValues.size(); i++) {
443                    Element subElement = fieldValues.get(i).getElement();
444                    Node fieldChild = createNode(subElement, nestedDef);
445                    fieldChildren.add(fieldChild);
446                }
447                Field field = new Field(fieldDef, fieldChildren);
448                fields.add(field);
449            }
450            Node seqNode = new Node(NodeType.sequence, value, contentDef, elem, fields);
451            return seqNode;
452        }
453        throw new IllegalStateException(
454            "Invalid content definition type encounterered while processing " + m_content.getFile().getRootPath());
455    }
456
457    /**
458     * Gets the node corresponding to the given value.
459     *
460     * @param value a content value
461     * @return the node for the value, or null if no node is found
462     */
463    public Node getNodeForValue(I_CmsXmlContentValue value) {
464
465        return m_valueToNodeCache.get(value);
466    }
467
468    /**
469     * Returns the root node.
470     *
471     * @return the root node
472     */
473    public Node getRoot() {
474
475        return m_root;
476    }
477
478    /**
479     * @see java.lang.Object#toString()
480     */
481    @Override
482    public String toString() {
483
484        return m_content.getFile().getRootPath() + ":\n" + CmsStringUtil.indentLines(m_root.toString(), 4);
485    }
486
487    /**
488     * Gets a path for an element which can be fed to CmsXmlContent.getValue().
489     *
490     * @param element the element
491     * @return the path
492     */
493    private String getValuePath(Element element) {
494
495        String fullPath = element.getUniquePath();
496        String prefix = m_content.getLocaleNode(m_locale).getUniquePath();
497        String result = fullPath.substring(prefix.length());
498        if (result.startsWith("/")) {
499            result = result.substring(1);
500        }
501        return result;
502    }
503
504}