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 GmbH & Co. KG, 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;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsResource;
032import org.opencms.file.CmsResourceFilter;
033import org.opencms.file.types.CmsResourceTypeXmlContent;
034import org.opencms.file.types.I_CmsResourceType;
035import org.opencms.main.CmsException;
036import org.opencms.main.CmsLog;
037import org.opencms.main.OpenCms;
038import org.opencms.relations.CmsRelation;
039import org.opencms.relations.CmsRelationFilter;
040import org.opencms.relations.CmsRelationType;
041import org.opencms.util.CmsStringUtil;
042import org.opencms.xml.content.CmsDefaultXmlContentHandler;
043import org.opencms.xml.content.CmsXmlContent;
044import org.opencms.xml.content.CmsXmlContentFactory;
045import org.opencms.xml.content.I_CmsXmlContentHandler;
046import org.opencms.xml.types.CmsXmlDynamicCategoryValue;
047import org.opencms.xml.types.CmsXmlLocaleValue;
048import org.opencms.xml.types.CmsXmlNestedContentDefinition;
049import org.opencms.xml.types.CmsXmlStringValue;
050import org.opencms.xml.types.I_CmsXmlContentValue;
051import org.opencms.xml.types.I_CmsXmlSchemaType;
052
053import java.io.IOException;
054import java.util.ArrayList;
055import java.util.Arrays;
056import java.util.Collections;
057import java.util.HashMap;
058import java.util.HashSet;
059import java.util.Iterator;
060import java.util.List;
061import java.util.Locale;
062import java.util.Map;
063import java.util.Set;
064import java.util.concurrent.ConcurrentHashMap;
065import java.util.function.BiConsumer;
066import java.util.stream.Collectors;
067
068import org.apache.commons.logging.Log;
069
070import org.dom4j.Attribute;
071import org.dom4j.Document;
072import org.dom4j.DocumentHelper;
073import org.dom4j.Element;
074import org.dom4j.Namespace;
075import org.dom4j.QName;
076import org.xml.sax.EntityResolver;
077import org.xml.sax.InputSource;
078import org.xml.sax.SAXException;
079
080/**
081 * Describes the structure definition of an XML content object.<p>
082 *
083 * @since 6.0.0
084 */
085public class CmsXmlContentDefinition implements Cloneable {
086
087    /**
088     * Enumeration of possible sequence types in a content definition.
089     */
090    public enum SequenceType {
091        /** A <code>xsd:choice</code> where the choice elements can appear more than once in a mix. */
092        MULTIPLE_CHOICE,
093        /** A simple <code>xsd:sequence</code>. */
094        SEQUENCE,
095        /** A <code>xsd:choice</code> where only one choice element can be selected. */
096        SINGLE_CHOICE
097    }
098
099    /** Constant for the XML schema attribute "mapto". */
100    public static final String XSD_ATTRIBUTE_DEFAULT = "default";
101
102    /** Constant for the XML schema attribute "elementFormDefault". */
103    public static final String XSD_ATTRIBUTE_ELEMENT_FORM_DEFAULT = "elementFormDefault";
104
105    /** Constant for the XML schema attribute "maxOccurs". */
106    public static final String XSD_ATTRIBUTE_MAX_OCCURS = "maxOccurs";
107
108    /** Constant for the XML schema attribute "minOccurs". */
109    public static final String XSD_ATTRIBUTE_MIN_OCCURS = "minOccurs";
110
111    /** Constant for the XML schema attribute "name". */
112    public static final String XSD_ATTRIBUTE_NAME = "name";
113
114    /** Constant for the XML schema attribute "schemaLocation". */
115    public static final String XSD_ATTRIBUTE_SCHEMA_LOCATION = "schemaLocation";
116
117    /** Constant for the XML schema attribute "type". */
118    public static final String XSD_ATTRIBUTE_TYPE = "type";
119
120    /** Constant for the XML schema attribute "use". */
121    public static final String XSD_ATTRIBUTE_USE = "use";
122
123    /** Constant for the XML schema attribute value "language". */
124    public static final String XSD_ATTRIBUTE_VALUE_LANGUAGE = "language";
125
126    /** Constant for the XML schema attribute value "1". */
127    public static final String XSD_ATTRIBUTE_VALUE_ONE = "1";
128
129    /** Constant for the XML schema attribute value "optional". */
130    public static final String XSD_ATTRIBUTE_VALUE_OPTIONAL = "optional";
131
132    /** Constant for the XML schema attribute value "qualified". */
133    public static final String XSD_ATTRIBUTE_VALUE_QUALIFIED = "qualified";
134
135    /** Constant for the XML schema attribute value "required". */
136    public static final String XSD_ATTRIBUTE_VALUE_REQUIRED = "required";
137
138    /** Constant for the XML schema attribute value "unbounded". */
139    public static final String XSD_ATTRIBUTE_VALUE_UNBOUNDED = "unbounded";
140
141    /** Constant for the XML schema attribute value "0". */
142    public static final String XSD_ATTRIBUTE_VALUE_ZERO = "0";
143
144    /** The opencms default type definition include. */
145    public static final String XSD_INCLUDE_OPENCMS = CmsXmlEntityResolver.OPENCMS_SCHEME + "opencms-xmlcontent.xsd";
146
147    /** The schema definition namespace. */
148    public static final Namespace XSD_NAMESPACE = Namespace.get("xsd", "http://www.w3.org/2001/XMLSchema");
149
150    /** Constant for the "annotation" node in the XML schema namespace. */
151    public static final QName XSD_NODE_ANNOTATION = QName.get("annotation", XSD_NAMESPACE);
152
153    /** Constant for the "appinfo" node in the XML schema namespace. */
154    public static final QName XSD_NODE_APPINFO = QName.get("appinfo", XSD_NAMESPACE);
155
156    /** Constant for the "attribute" node in the XML schema namespace. */
157    public static final QName XSD_NODE_ATTRIBUTE = QName.get("attribute", XSD_NAMESPACE);
158
159    /** Constant for the "choice" node in the XML schema namespace. */
160    public static final QName XSD_NODE_CHOICE = QName.get("choice", XSD_NAMESPACE);
161
162    /** Constant for the "complexType" node in the XML schema namespace. */
163    public static final QName XSD_NODE_COMPLEXTYPE = QName.get("complexType", XSD_NAMESPACE);
164
165    /** Constant for the "element" node in the XML schema namespace. */
166    public static final QName XSD_NODE_ELEMENT = QName.get("element", XSD_NAMESPACE);
167
168    /** Constant for the "include" node in the XML schema namespace. */
169    public static final QName XSD_NODE_INCLUDE = QName.get("include", XSD_NAMESPACE);
170
171    /** Constant for the "schema" node in the XML schema namespace. */
172    public static final QName XSD_NODE_SCHEMA = QName.get("schema", XSD_NAMESPACE);
173
174    /** Constant for the "sequence" node in the XML schema namespace. */
175    public static final QName XSD_NODE_SEQUENCE = QName.get("sequence", XSD_NAMESPACE);
176
177    /** The log object for this class. */
178    private static final Log LOG = CmsLog.getLog(CmsXmlContentDefinition.class);
179
180    /** Null schema type value, required for map lookups. */
181    private static final I_CmsXmlSchemaType NULL_SCHEMA_TYPE = new CmsXmlStringValue("NULL", "0", "0");
182
183    /** Max occurs value for xsd:choice definitions. */
184    private int m_choiceMaxOccurs;
185
186    /** The XML content handler. */
187    private I_CmsXmlContentHandler m_contentHandler;
188
189    /** The Map of configured types indexed by the element xpath. */
190    private Map<String, I_CmsXmlSchemaType> m_elementTypes;
191
192    /** The set of included additional XML content definitions. */
193    private Set<CmsXmlContentDefinition> m_includes;
194
195    /** The inner element name of the content definition (type sequence). */
196    private String m_innerName;
197
198    /** The outer element name of the content definition (language sequence). */
199    private String m_outerName;
200
201    /** The XML document from which the schema was unmarshalled. */
202    private Document m_schemaDocument;
203
204    /** The location from which the XML schema was read (XML system id). */
205    private String m_schemaLocation;
206
207    /** Indicates the sequence type of this content definition. */
208    private SequenceType m_sequenceType;
209
210    /** The main type name of this XML content definition. */
211    private String m_typeName;
212
213    /** The Map of configured types. */
214    private Map<String, I_CmsXmlSchemaType> m_types;
215
216    /** The type sequence. */
217    private List<I_CmsXmlSchemaType> m_typeSequence;
218
219    /**
220     * Creates a new XML content definition.<p>
221     *
222     * @param innerName the inner element name to use for the content definiton
223     * @param schemaLocation the location from which the XML schema was read (system id)
224     */
225    public CmsXmlContentDefinition(String innerName, String schemaLocation) {
226
227        this(innerName + "s", innerName, schemaLocation);
228    }
229
230    /**
231     * Creates a new XML content definition.<p>
232     *
233     * @param outerName the outer element name to use for the content definition
234     * @param innerName the inner element name to use for the content definition
235     * @param schemaLocation the location from which the XML schema was read (system id)
236     */
237    public CmsXmlContentDefinition(String outerName, String innerName, String schemaLocation) {
238
239        m_outerName = outerName;
240        m_innerName = innerName;
241        setInnerName(innerName);
242        m_typeSequence = new ArrayList<I_CmsXmlSchemaType>();
243        m_types = new HashMap<String, I_CmsXmlSchemaType>();
244        m_includes = new HashSet<CmsXmlContentDefinition>();
245        m_schemaLocation = schemaLocation;
246        m_contentHandler = new CmsDefaultXmlContentHandler();
247        m_sequenceType = SequenceType.SEQUENCE;
248        m_elementTypes = new ConcurrentHashMap<String, I_CmsXmlSchemaType>();
249    }
250
251    /**
252     * Required empty constructor for clone operation.<p>
253     */
254    protected CmsXmlContentDefinition() {
255
256        // noop, required for clone operation
257    }
258
259    /**
260     * Factory method that returns the XML content definition instance for a given resource.<p>
261     *
262     * @param cms the cms-object
263     * @param resource the resource
264     *
265     * @return the XML content definition
266     *
267     * @throws CmsException if something goes wrong
268     */
269    public static CmsXmlContentDefinition getContentDefinitionForResource(CmsObject cms, CmsResource resource)
270    throws CmsException {
271
272        CmsXmlContentDefinition contentDef = null;
273        I_CmsResourceType resType = OpenCms.getResourceManager().getResourceType(resource.getTypeId());
274        String schema = resType.getConfiguration().get(CmsResourceTypeXmlContent.CONFIGURATION_SCHEMA);
275        if (schema != null) {
276            try {
277                // this wont in most cases read the file content because of caching
278                contentDef = unmarshal(cms, schema);
279            } catch (CmsException e) {
280                // this should never happen, unless the configured schema is different than the schema in the XML
281                if (!LOG.isDebugEnabled()) {
282                    LOG.warn(e.getLocalizedMessage(), e);
283                }
284                LOG.debug(e.getLocalizedMessage(), e);
285            }
286        }
287        if (contentDef == null) {
288            // could still be empty since it is not mandatory to configure the resource type in the XML configuration
289            // try through the XSD relation
290            List<CmsRelation> relations = cms.getRelationsForResource(
291                resource,
292                CmsRelationFilter.TARGETS.filterType(CmsRelationType.XSD));
293            if ((relations != null) && !relations.isEmpty()) {
294                CmsXmlEntityResolver entityResolver = new CmsXmlEntityResolver(cms);
295                String xsd = cms.getSitePath(relations.get(0).getTarget(cms, CmsResourceFilter.ALL));
296                contentDef = entityResolver.getCachedContentDefinition(xsd);
297            }
298        }
299        if (contentDef == null) {
300            // could still be empty if the XML content has been saved with an OpenCms before 8.0.0
301            // so, to unmarshal is the only possibility left
302            CmsXmlContent content = CmsXmlContentFactory.unmarshal(cms, cms.readFile(resource));
303            contentDef = content.getContentDefinition();
304        }
305
306        return contentDef;
307    }
308
309    /**
310     * Reads the content definition which is configured for a resource type.<p>
311     *
312     * @param cms the current CMS context
313     * @param typeName the type name
314     *
315     * @return the content definition
316     *
317     * @throws CmsException if something goes wrong
318     */
319    public static CmsXmlContentDefinition getContentDefinitionForType(CmsObject cms, String typeName)
320    throws CmsException {
321
322        I_CmsResourceType resType = OpenCms.getResourceManager().getResourceType(typeName);
323        String schema = resType.getConfiguration().get(CmsResourceTypeXmlContent.CONFIGURATION_SCHEMA);
324        CmsXmlContentDefinition contentDef = null;
325        if (schema == null) {
326            return null;
327        }
328        contentDef = unmarshal(cms, schema);
329        return contentDef;
330    }
331
332    /**
333     * Returns a content handler instance for the given resource.<p>
334     *
335     * @param cms the cms-object
336     * @param resource the resource
337     *
338     * @return the content handler
339     *
340     * @throws CmsException if something goes wrong
341     */
342    public static I_CmsXmlContentHandler getContentHandlerForResource(CmsObject cms, CmsResource resource)
343    throws CmsException {
344
345        return getContentDefinitionForResource(cms, resource).getContentHandler();
346    }
347
348    /**
349     * Factory method to unmarshal (read) a XML content definition instance from a byte array
350     * that contains XML data.<p>
351     *
352     * @param xmlData the XML data in a byte array
353     * @param schemaLocation the location from which the XML schema was read (system id)
354     * @param resolver the XML entity resolver to use
355     *
356     * @return a XML content definition instance unmarshalled from the byte array
357     *
358     * @throws CmsXmlException if something goes wrong
359     */
360    public static CmsXmlContentDefinition unmarshal(byte[] xmlData, String schemaLocation, EntityResolver resolver)
361    throws CmsXmlException {
362
363        schemaLocation = translateSchema(schemaLocation);
364        CmsXmlContentDefinition result = getCachedContentDefinition(schemaLocation, resolver);
365        if (result == null) {
366            // content definition was not found in the cache, unmarshal the XML document
367            result = unmarshalInternal(CmsXmlUtils.unmarshalHelper(xmlData, resolver), schemaLocation, resolver);
368        }
369        return result;
370    }
371
372    /**
373     * Factory method to unmarshal (read) a XML content definition instance from the OpenCms VFS resource name.<p>
374     *
375     * @param cms the current users CmsObject
376     * @param resourcename the resource name to unmarshal the XML content definition from
377     *
378     * @return a XML content definition instance unmarshalled from the VFS resource
379     *
380     * @throws CmsXmlException if something goes wrong
381     */
382    public static CmsXmlContentDefinition unmarshal(CmsObject cms, String resourcename) throws CmsXmlException {
383
384        CmsXmlEntityResolver resolver = new CmsXmlEntityResolver(cms);
385        String schemaLocation = CmsXmlEntityResolver.OPENCMS_SCHEME.concat(resourcename.substring(1));
386        schemaLocation = translateSchema(schemaLocation);
387        CmsXmlContentDefinition result = getCachedContentDefinition(schemaLocation, resolver);
388        if (result == null) {
389            // content definition was not found in the cache, unmarshal the XML document
390            InputSource source = null;
391            try {
392                source = resolver.resolveEntity(null, schemaLocation);
393                result = unmarshalInternal(CmsXmlUtils.unmarshalHelper(source, resolver), schemaLocation, resolver);
394            } catch (IOException e) {
395                throw new CmsXmlException(
396                    Messages.get().container(
397                        Messages.ERR_UNMARSHALLING_XML_SCHEMA_NOT_FOUND_2,
398                        resourcename,
399                        schemaLocation));
400            }
401        }
402        return result;
403    }
404
405    /**
406     * Factory method to unmarshal (read) a XML content definition instance from a XML document.<p>
407     *
408     * This method does additional validation to ensure the document has the required
409     * XML structure for a OpenCms content definition schema.<p>
410     *
411     * @param document the XML document to generate a XML content definition from
412     * @param schemaLocation the location from which the XML schema was read (system id)
413     *
414     * @return a XML content definition instance unmarshalled from the XML document
415     *
416     * @throws CmsXmlException if something goes wrong
417     */
418    public static CmsXmlContentDefinition unmarshal(Document document, String schemaLocation) throws CmsXmlException {
419
420        schemaLocation = translateSchema(schemaLocation);
421        EntityResolver resolver = document.getEntityResolver();
422        CmsXmlContentDefinition result = getCachedContentDefinition(schemaLocation, resolver);
423        if (result == null) {
424            // content definition was not found in the cache, unmarshal the XML document
425            result = unmarshalInternal(document, schemaLocation, resolver);
426        }
427        return result;
428    }
429
430    /**
431     * Factory method to unmarshal (read) a XML content definition instance from a XML InputSource.<p>
432     *
433     * @param source the XML InputSource to use
434     * @param schemaLocation the location from which the XML schema was read (system id)
435     * @param resolver the XML entity resolver to use
436     *
437     * @return a XML content definition instance unmarshalled from the InputSource
438     *
439     * @throws CmsXmlException if something goes wrong
440     */
441    public static CmsXmlContentDefinition unmarshal(InputSource source, String schemaLocation, EntityResolver resolver)
442    throws CmsXmlException {
443
444        schemaLocation = translateSchema(schemaLocation);
445        CmsXmlContentDefinition result = getCachedContentDefinition(schemaLocation, resolver);
446        if (result == null) {
447            // content definition was not found in the cache, unmarshal the XML document
448            if (null == source) {
449                throw new CmsXmlException(
450                    Messages.get().container(
451                        Messages.ERR_UNMARSHALLING_XML_DOC_1,
452                        String.format("schemaLocation: '%s'. source: null!", schemaLocation)));
453            }
454            Document doc = CmsXmlUtils.unmarshalHelper(source, resolver);
455            result = unmarshalInternal(doc, schemaLocation, resolver);
456        }
457        return result;
458    }
459
460    /**
461     * Factory method to unmarshal (read) a XML content definition instance from a given XML schema location.<p>
462     *
463     * The XML content definition data to unmarshal will be read from the provided schema location using
464     * an XML InputSource.<p>
465     *
466     * @param schemaLocation the location from which to read the XML schema (system id)
467     * @param resolver the XML entity resolver to use
468     *
469     * @return a XML content definition instance unmarshalled from the InputSource
470     *
471     * @throws CmsXmlException if something goes wrong
472     * @throws SAXException if the XML schema location could not be converted to an XML InputSource
473     * @throws IOException if the XML schema location could not be converted to an XML InputSource
474     */
475    public static CmsXmlContentDefinition unmarshal(String schemaLocation, EntityResolver resolver)
476    throws CmsXmlException, SAXException, IOException {
477
478        schemaLocation = translateSchema(schemaLocation);
479        CmsXmlContentDefinition result = getCachedContentDefinition(schemaLocation, resolver);
480        if (result == null) {
481            // content definition was not found in the cache, unmarshal the XML document
482            InputSource source = resolver.resolveEntity(null, schemaLocation);
483            result = unmarshalInternal(CmsXmlUtils.unmarshalHelper(source, resolver), schemaLocation, resolver);
484        }
485        return result;
486    }
487
488    /**
489     * Factory method to unmarshal (read) a XML content definition instance from a String
490     * that contains XML data.<p>
491     *
492     * @param xmlData the XML data in a String
493     * @param schemaLocation the location from which the XML schema was read (system id)
494     * @param resolver the XML entity resolver to use
495     *
496     * @return a XML content definition instance unmarshalled from the byte array
497     *
498     * @throws CmsXmlException if something goes wrong
499     */
500    public static CmsXmlContentDefinition unmarshal(String xmlData, String schemaLocation, EntityResolver resolver)
501    throws CmsXmlException {
502
503        schemaLocation = translateSchema(schemaLocation);
504        CmsXmlContentDefinition result = getCachedContentDefinition(schemaLocation, resolver);
505        if (result == null) {
506            // content definition was not found in the cache, unmarshal the XML document
507            try {
508                Document doc = CmsXmlUtils.unmarshalHelper(xmlData, resolver);
509                result = unmarshalInternal(doc, schemaLocation, resolver);
510            } catch (CmsXmlException e) {
511                throw new CmsXmlException(
512                    Messages.get().container(
513                        Messages.ERR_UNMARSHALLING_XML_DOC_1,
514                        String.format("schemaLocation: '%s'. xml: '%s'", schemaLocation, xmlData)),
515                    e);
516            }
517        }
518        return result;
519    }
520
521    /**
522     * Creates the name of the type attribute from the given content name.<p>
523     *
524     * @param name the name to use
525     *
526     * @return the name of the type attribute
527     */
528    protected static String createTypeName(String name) {
529
530        StringBuffer result = new StringBuffer(32);
531        result.append("OpenCms");
532        result.append(name.substring(0, 1).toUpperCase());
533        if (name.length() > 1) {
534            result.append(name.substring(1));
535        }
536        return result.toString();
537    }
538
539    /**
540     * Validates if a given attribute exists at the given element with an (optional) specified value.<p>
541     *
542     * If the required value is not <code>null</code>, the attribute must have exactly this
543     * value set.<p>
544     *
545     * If no value is required, some simple validation is performed on the attribute value,
546     * like a check that the value does not have leading or trailing white spaces.<p>
547     *
548     * @param element the element to validate
549     * @param attributeName the attribute to check for
550     * @param requiredValue the required value of the attribute, or <code>null</code> if any value is allowed
551     *
552     * @return the value of the attribute
553     *
554     * @throws CmsXmlException if the element does not have the required attribute set, or if the validation fails
555     */
556    protected static String validateAttribute(Element element, String attributeName, String requiredValue)
557    throws CmsXmlException {
558
559        Attribute attribute = element.attribute(attributeName);
560        if (attribute == null) {
561            throw new CmsXmlException(
562                Messages.get().container(Messages.ERR_EL_MISSING_ATTRIBUTE_2, element.getUniquePath(), attributeName));
563        }
564        String value = attribute.getValue();
565
566        if (requiredValue == null) {
567            if (CmsStringUtil.isEmptyOrWhitespaceOnly(value) || !value.equals(value.trim())) {
568                throw new CmsXmlException(
569                    Messages.get().container(
570                        Messages.ERR_EL_BAD_ATTRIBUTE_WS_3,
571                        element.getUniquePath(),
572                        attributeName,
573                        value));
574            }
575        } else {
576            if (!requiredValue.equals(value)) {
577                throw new CmsXmlException(
578                    Messages.get().container(
579                        Messages.ERR_EL_BAD_ATTRIBUTE_VALUE_4,
580                        new Object[] {element.getUniquePath(), attributeName, requiredValue, value}));
581            }
582        }
583        return value;
584    }
585
586    /**
587     * Validates if a given element has exactly the required attributes set.<p>
588     *
589     * @param element the element to validate
590     * @param requiredAttributes the list of required attributes
591     * @param optionalAttributes the list of optional attributes
592     *
593     * @throws CmsXmlException if the validation fails
594     */
595    protected static void validateAttributesExists(
596        Element element,
597        String[] requiredAttributes,
598        String[] optionalAttributes)
599    throws CmsXmlException {
600
601        if (element.attributeCount() < requiredAttributes.length) {
602            throw new CmsXmlException(
603                Messages.get().container(
604                    Messages.ERR_EL_ATTRIBUTE_TOOFEW_3,
605                    element.getUniquePath(),
606                    Integer.valueOf(requiredAttributes.length),
607                    Integer.valueOf(element.attributeCount())));
608        }
609
610        if (element.attributeCount() > (requiredAttributes.length + optionalAttributes.length)) {
611            throw new CmsXmlException(
612                Messages.get().container(
613                    Messages.ERR_EL_ATTRIBUTE_TOOMANY_3,
614                    element.getUniquePath(),
615                    Integer.valueOf(requiredAttributes.length + optionalAttributes.length),
616                    Integer.valueOf(element.attributeCount())));
617        }
618
619        for (int i = 0; i < requiredAttributes.length; i++) {
620            String attributeName = requiredAttributes[i];
621            if (element.attribute(attributeName) == null) {
622                throw new CmsXmlException(
623                    Messages.get().container(
624                        Messages.ERR_EL_MISSING_ATTRIBUTE_2,
625                        element.getUniquePath(),
626                        attributeName));
627            }
628        }
629
630        List<String> rA = Arrays.asList(requiredAttributes);
631        List<String> oA = Arrays.asList(optionalAttributes);
632
633        for (int i = 0; i < element.attributes().size(); i++) {
634            String attributeName = element.attribute(i).getName();
635            if (!rA.contains(attributeName) && !oA.contains(attributeName)) {
636                throw new CmsXmlException(
637                    Messages.get().container(
638                        Messages.ERR_EL_INVALID_ATTRIBUTE_2,
639                        element.getUniquePath(),
640                        attributeName));
641            }
642        }
643    }
644
645    /**
646     * Validates the given element as a complex type sequence.<p>
647     *
648     * @param element the element to validate
649     * @param includes the XML schema includes
650     *
651     * @return a data structure containing the validated complex type sequence data
652     *
653     * @throws CmsXmlException if the validation fails
654     */
655    protected static CmsXmlComplexTypeSequence validateComplexTypeSequence(
656        Element element,
657        Set<CmsXmlContentDefinition> includes)
658    throws CmsXmlException {
659
660        validateAttributesExists(element, new String[] {XSD_ATTRIBUTE_NAME}, new String[0]);
661
662        String name = validateAttribute(element, XSD_ATTRIBUTE_NAME, null);
663
664        // now check the type definition list
665        List<Element> mainElements = CmsXmlGenericWrapper.elements(element);
666        List<Element> attributes = mainElements.stream().filter(
667            elem -> XSD_NODE_ATTRIBUTE.equals(elem.getQName())).collect(Collectors.toList());
668
669        boolean hasLanguageAttribute = false;
670
671        // two elements in the master list: the second must be the "language" attribute definition
672
673        Element languageAttribute = attributes.stream().filter(
674            elem -> elem.attribute(XSD_ATTRIBUTE_NAME).getValue().equals(
675                XSD_ATTRIBUTE_VALUE_LANGUAGE)).findFirst().orElse(null);
676        if (languageAttribute != null) {
677
678            validateAttribute(languageAttribute, XSD_ATTRIBUTE_TYPE, CmsXmlLocaleValue.TYPE_NAME);
679            try {
680                validateAttribute(languageAttribute, XSD_ATTRIBUTE_USE, XSD_ATTRIBUTE_VALUE_REQUIRED);
681            } catch (CmsXmlException e) {
682                validateAttribute(languageAttribute, XSD_ATTRIBUTE_USE, XSD_ATTRIBUTE_VALUE_OPTIONAL);
683            }
684            // no error: then the language attribute is valid
685            hasLanguageAttribute = true;
686        }
687
688        // the type of the sequence
689        SequenceType sequenceType;
690        int choiceMaxOccurs = 0;
691
692        // check the main element type sequence
693        Element typeSequenceElement = mainElements.get(0);
694        if (!XSD_NODE_SEQUENCE.equals(typeSequenceElement.getQName())) {
695            if (!XSD_NODE_CHOICE.equals(typeSequenceElement.getQName())) {
696                throw new CmsXmlException(
697                    Messages.get().container(
698                        Messages.ERR_CD_ELEMENT_NAME_4,
699                        new Object[] {
700                            typeSequenceElement.getUniquePath(),
701                            XSD_NODE_SEQUENCE.getQualifiedName(),
702                            XSD_NODE_CHOICE.getQualifiedName(),
703                            typeSequenceElement.getQName().getQualifiedName()}));
704            } else {
705                // this is a xsd:choice, check if this is single or multiple choice
706                String minOccursStr = typeSequenceElement.attributeValue(XSD_ATTRIBUTE_MIN_OCCURS);
707                int minOccurs = 1;
708                if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(minOccursStr)) {
709                    try {
710                        minOccurs = Integer.parseInt(minOccursStr.trim());
711                    } catch (NumberFormatException e) {
712                        throw new CmsXmlException(
713                            Messages.get().container(
714                                Messages.ERR_EL_BAD_ATTRIBUTE_3,
715                                element.getUniquePath(),
716                                XSD_ATTRIBUTE_MIN_OCCURS,
717                                minOccursStr == null ? "1" : minOccursStr));
718                    }
719                }
720                String maxOccursStr = typeSequenceElement.attributeValue(XSD_ATTRIBUTE_MAX_OCCURS);
721                choiceMaxOccurs = 1;
722                if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(maxOccursStr)) {
723                    if (CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_UNBOUNDED.equals(maxOccursStr.trim())) {
724                        choiceMaxOccurs = Integer.MAX_VALUE;
725                    } else {
726                        try {
727                            choiceMaxOccurs = Integer.parseInt(maxOccursStr.trim());
728                        } catch (NumberFormatException e) {
729                            throw new CmsXmlException(
730                                Messages.get().container(
731                                    Messages.ERR_EL_BAD_ATTRIBUTE_3,
732                                    element.getUniquePath(),
733                                    XSD_ATTRIBUTE_MAX_OCCURS,
734                                    maxOccursStr));
735                        }
736                    }
737                }
738                if ((minOccurs == 0) && (choiceMaxOccurs == 1)) {
739                    // minOccurs 0 and maxOccurs 1, this is a single choice sequence
740                    sequenceType = SequenceType.SINGLE_CHOICE;
741                } else {
742                    // this is a multiple choice sequence
743                    if (minOccurs > choiceMaxOccurs) {
744                        throw new CmsXmlException(
745                            Messages.get().container(
746                                Messages.ERR_EL_BAD_ATTRIBUTE_3,
747                                element.getUniquePath(),
748                                XSD_ATTRIBUTE_MIN_OCCURS,
749                                minOccursStr == null ? "1" : minOccursStr));
750                    }
751                    sequenceType = SequenceType.MULTIPLE_CHOICE;
752                }
753            }
754        } else {
755            // this is a simple sequence
756            sequenceType = SequenceType.SEQUENCE;
757        }
758
759        // check the type definition sequence
760        List<Element> typeSequenceElements = CmsXmlGenericWrapper.elements(typeSequenceElement);
761        if (typeSequenceElements.size() < 1) {
762            throw new CmsXmlException(
763                Messages.get().container(
764                    Messages.ERR_TS_SUBELEMENT_TOOFEW_3,
765                    typeSequenceElement.getUniquePath(),
766                    Integer.valueOf(1),
767                    Integer.valueOf(typeSequenceElements.size())));
768        }
769
770        // now add all type definitions from the schema
771        List<I_CmsXmlSchemaType> sequence = new ArrayList<I_CmsXmlSchemaType>();
772
773        if (hasLanguageAttribute) {
774            // only generate types for sequence node with language attribute
775
776            CmsXmlContentTypeManager typeManager = OpenCms.getXmlContentTypeManager();
777            Iterator<Element> i = typeSequenceElements.iterator();
778            while (i.hasNext()) {
779                Element typeElement = i.next();
780                if (sequenceType != SequenceType.SEQUENCE) {
781                    // in case of xsd:choice, need to make sure "minOccurs" for all type elements is 0
782                    String minOccursStr = typeElement.attributeValue(XSD_ATTRIBUTE_MIN_OCCURS);
783                    int minOccurs = 1;
784                    if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(minOccursStr)) {
785                        try {
786                            minOccurs = Integer.parseInt(minOccursStr.trim());
787                        } catch (NumberFormatException e) {
788                            // ignore
789                        }
790                    }
791                    // minOccurs must be "0"
792                    if (minOccurs != 0) {
793                        throw new CmsXmlException(
794                            Messages.get().container(
795                                Messages.ERR_EL_BAD_ATTRIBUTE_3,
796                                typeElement.getUniquePath(),
797                                XSD_ATTRIBUTE_MIN_OCCURS,
798                                minOccursStr == null ? "1" : minOccursStr));
799                    }
800                }
801                // create the type with the type manager
802                I_CmsXmlSchemaType type = typeManager.getContentType(typeElement, includes);
803
804                if (type.getTypeName().equals(CmsXmlDynamicCategoryValue.TYPE_NAME)
805                    && ((type.getMaxOccurs() > 1) || (type.getMinOccurs() > 1))) {
806                    throw new CmsXmlException(
807                        Messages.get().container(
808                            Messages.ERR_EL_OF_TYPE_MUST_OCCUR_AT_MOST_ONCE_2,
809                            typeElement.getUniquePath(),
810                            type.getTypeName()));
811                }
812
813                if (sequenceType == SequenceType.MULTIPLE_CHOICE) {
814                    // if this is a multiple choice sequence,
815                    // all elements must have "minOccurs" 0 or 1 and "maxOccurs" of 1
816                    if ((type.getMinOccurs() < 0) || (type.getMinOccurs() > 1) || (type.getMaxOccurs() != 1)) {
817                        throw new CmsXmlException(
818                            Messages.get().container(
819                                Messages.ERR_EL_BAD_ATTRIBUTE_3,
820                                typeElement.getUniquePath(),
821                                XSD_ATTRIBUTE_MAX_OCCURS,
822                                typeElement.attributeValue(XSD_ATTRIBUTE_MAX_OCCURS)));
823                    }
824                }
825                sequence.add(type);
826            }
827        } else {
828            // generate a nested content definition for the main type sequence
829
830            Element e = typeSequenceElements.get(0);
831            String typeName = validateAttribute(e, XSD_ATTRIBUTE_NAME, null);
832            String minOccurs = validateAttribute(e, XSD_ATTRIBUTE_MIN_OCCURS, XSD_ATTRIBUTE_VALUE_ZERO);
833            String maxOccurs = validateAttribute(e, XSD_ATTRIBUTE_MAX_OCCURS, XSD_ATTRIBUTE_VALUE_UNBOUNDED);
834            validateAttribute(e, XSD_ATTRIBUTE_TYPE, createTypeName(typeName));
835
836            CmsXmlNestedContentDefinition cd = new CmsXmlNestedContentDefinition(null, typeName, minOccurs, maxOccurs);
837            sequence.add(cd);
838        }
839
840        // return a data structure with the collected values
841        return new CmsXmlComplexTypeSequence(name, sequence, hasLanguageAttribute, sequenceType, choiceMaxOccurs);
842    }
843
844    /**
845     * Looks up the given XML content definition system id in the internal content definition cache.<p>
846     *
847     * @param schemaLocation the system id of the XML content definition to look up
848     * @param resolver the XML entity resolver to use (contains the cache)
849     *
850     * @return the XML content definition found, or null if no definition is cached for the given system id
851     */
852    private static CmsXmlContentDefinition getCachedContentDefinition(String schemaLocation, EntityResolver resolver) {
853
854        if (resolver instanceof CmsXmlEntityResolver) {
855            // check for a cached version of this content definition
856            CmsXmlEntityResolver cmsResolver = (CmsXmlEntityResolver)resolver;
857            return cmsResolver.getCachedContentDefinition(schemaLocation);
858        }
859        return null;
860    }
861
862    /**
863     * Translates the XSD schema location.<p>
864     *
865     * @param schemaLocation the location to translate
866     *
867     * @return the translated schema location
868     */
869    private static String translateSchema(String schemaLocation) {
870
871        if (OpenCms.getRepositoryManager() != null) {
872            return OpenCms.getResourceManager().getXsdTranslator().translateResource(schemaLocation);
873        }
874        return schemaLocation;
875    }
876
877    /**
878     * Internal method to unmarshal (read) a XML content definition instance from a XML document.<p>
879     *
880     * It is assumed that the XML content definition cache has already been tested and the document
881     * has not been found in the cache. After the XML content definition has been successfully created,
882     * it is placed in the cache.<p>
883     *
884     * @param document the XML document to generate a XML content definition from
885     * @param schemaLocation the location from which the XML schema was read (system id)
886     * @param resolver the XML entity resolver used by the given XML document
887     *
888     * @return a XML content definition instance unmarshalled from the XML document
889     *
890     * @throws CmsXmlException if something goes wrong
891     */
892    private static CmsXmlContentDefinition unmarshalInternal(
893        Document document,
894        String schemaLocation,
895        EntityResolver resolver)
896    throws CmsXmlException {
897
898        // analyze the document and generate the XML content type definition
899        Element root = document.getRootElement();
900        if (!XSD_NODE_SCHEMA.equals(root.getQName())) {
901            // schema node is required
902            throw new CmsXmlException(Messages.get().container(Messages.ERR_CD_NO_SCHEMA_NODE_0));
903        }
904
905        List<Element> includes = CmsXmlGenericWrapper.elements(root, XSD_NODE_INCLUDE);
906        if (includes.size() < 1) {
907            // one include is required
908            throw new CmsXmlException(Messages.get().container(Messages.ERR_CD_ONE_INCLUDE_REQUIRED_0));
909        }
910
911        Element include = includes.get(0);
912        String target = validateAttribute(include, XSD_ATTRIBUTE_SCHEMA_LOCATION, null);
913        if (!XSD_INCLUDE_OPENCMS.equals(target)) {
914            // the first include must point to the default OpenCms standard schema include
915            throw new CmsXmlException(
916                Messages.get().container(Messages.ERR_CD_FIRST_INCLUDE_2, XSD_INCLUDE_OPENCMS, target));
917        }
918
919        boolean recursive = false;
920        Set<CmsXmlContentDefinition> nestedDefinitions = new HashSet<CmsXmlContentDefinition>();
921        if (includes.size() > 1) {
922            // resolve additional, nested include calls
923            for (int i = 1; i < includes.size(); i++) {
924
925                Element inc = includes.get(i);
926                String schemaLoc = validateAttribute(inc, XSD_ATTRIBUTE_SCHEMA_LOCATION, null);
927                if (!(schemaLoc.equals(schemaLocation))) {
928                    InputSource source = null;
929                    try {
930                        source = resolver.resolveEntity(null, schemaLoc);
931                    } catch (Exception e) {
932                        throw new CmsXmlException(
933                            Messages.get().container(
934                                Messages.ERR_CD_BAD_INCLUDE_3,
935                                schemaLoc,
936                                schemaLocation,
937                                document.asXML()),
938                            e);
939                    }
940                    // Couldn't resolve the entity?
941                    if (null == source) {
942                        throw new CmsXmlException(
943                            Messages.get().container(
944                                Messages.ERR_CD_BAD_INCLUDE_3,
945                                schemaLoc,
946                                schemaLocation,
947                                document.asXML()));
948                    }
949                    CmsXmlContentDefinition xmlContentDefinition = unmarshal(source, schemaLoc, resolver);
950                    nestedDefinitions.add(xmlContentDefinition);
951                } else {
952                    // recursion
953                    recursive = true;
954                }
955            }
956        }
957
958        List<Element> elements = CmsXmlGenericWrapper.elements(root, XSD_NODE_ELEMENT);
959        if (elements.size() != 1) {
960            // only one root element is allowed
961            throw new CmsXmlException(
962                Messages.get().container(
963                    Messages.ERR_CD_ROOT_ELEMENT_COUNT_1,
964                    XSD_INCLUDE_OPENCMS,
965                    Integer.valueOf(elements.size())));
966        }
967
968        // collect the data from the root element node
969        Element main = elements.get(0);
970        String name = validateAttribute(main, XSD_ATTRIBUTE_NAME, null);
971
972        // now process the complex types
973        List<Element> complexTypes = CmsXmlGenericWrapper.elements(root, XSD_NODE_COMPLEXTYPE);
974        if (complexTypes.size() != 2) {
975            // exactly two complex types are required
976            throw new CmsXmlException(
977                Messages.get().container(Messages.ERR_CD_COMPLEX_TYPE_COUNT_1, Integer.valueOf(complexTypes.size())));
978        }
979
980        // get the outer element sequence, this must be the first element
981        CmsXmlComplexTypeSequence outerSequence = validateComplexTypeSequence(complexTypes.get(0), nestedDefinitions);
982        CmsXmlNestedContentDefinition outer = (CmsXmlNestedContentDefinition)outerSequence.getSequence().get(0);
983
984        // make sure the inner and outer element names are as required
985        String outerTypeName = createTypeName(name);
986        String innerTypeName = createTypeName(outer.getName());
987        validateAttribute(complexTypes.get(0), XSD_ATTRIBUTE_NAME, outerTypeName);
988        validateAttribute(complexTypes.get(1), XSD_ATTRIBUTE_NAME, innerTypeName);
989        validateAttribute(main, XSD_ATTRIBUTE_TYPE, outerTypeName);
990
991        // generate the result XML content definition
992        CmsXmlContentDefinition result = new CmsXmlContentDefinition(name, null, schemaLocation);
993
994        // set the nested definitions
995        result.m_includes = nestedDefinitions;
996        // set the schema document
997        result.m_schemaDocument = document;
998
999        // the inner name is the element name set in the outer sequence
1000        result.setInnerName(outer.getName());
1001        if (recursive) {
1002            nestedDefinitions.add(result);
1003        }
1004
1005        // get the inner element sequence, this must be the second element
1006        CmsXmlComplexTypeSequence innerSequence = validateComplexTypeSequence(complexTypes.get(1), nestedDefinitions);
1007
1008        // add the types from the main sequence node
1009        Iterator<I_CmsXmlSchemaType> it = innerSequence.getSequence().iterator();
1010        while (it.hasNext()) {
1011            result.addType(it.next());
1012        }
1013
1014        // store if this content definition contains a xsd:choice sequence
1015        result.m_sequenceType = innerSequence.getSequenceType();
1016        result.m_choiceMaxOccurs = innerSequence.getChoiceMaxOccurs();
1017
1018        // resolve the XML content handler information
1019        List<Element> annotations = CmsXmlGenericWrapper.elements(root, XSD_NODE_ANNOTATION);
1020        I_CmsXmlContentHandler contentHandler = null;
1021        Element appInfoElement = null;
1022
1023        if (annotations.size() > 0) {
1024            List<Element> appinfos = CmsXmlGenericWrapper.elements(annotations.get(0), XSD_NODE_APPINFO);
1025
1026            if (appinfos.size() > 0) {
1027                // the first appinfo node contains the specific XML content data
1028                appInfoElement = appinfos.get(0);
1029
1030                // check for a special content handler in the appinfo node
1031                Element handlerElement = appInfoElement.element("handler");
1032                if (handlerElement != null) {
1033                    String className = handlerElement.attributeValue("class");
1034                    if (className != null) {
1035                        contentHandler = OpenCms.getXmlContentTypeManager().getFreshContentHandler(className);
1036                    }
1037                }
1038            }
1039        }
1040
1041        if (contentHandler == null) {
1042            // if no content handler is defined, the default handler is used
1043            contentHandler = OpenCms.getXmlContentTypeManager().getFreshContentHandler(
1044                CmsDefaultXmlContentHandler.class.getName());
1045        }
1046
1047        // analyze the app info node with the selected XML content handler
1048        contentHandler.initialize(appInfoElement, result);
1049        result.m_contentHandler = contentHandler;
1050
1051        result.freeze();
1052
1053        if (resolver instanceof CmsXmlEntityResolver) {
1054            // put the generated content definition in the cache
1055            ((CmsXmlEntityResolver)resolver).cacheContentDefinition(schemaLocation, result);
1056        }
1057
1058        return result;
1059    }
1060
1061    /**
1062     * Adds the missing default XML according to this content definition to the given document element.<p>
1063     *
1064     * In case the root element already contains sub nodes, only missing sub nodes are added.<p>
1065     *
1066     * @param cms the current users OpenCms context
1067     * @param document the document where the XML is added in (required for default XML generation)
1068     * @param root the root node to add the missing XML for
1069     * @param locale the locale to add the XML for
1070     *
1071     * @return the given root element with the missing content added
1072     */
1073    public Element addDefaultXml(CmsObject cms, I_CmsXmlDocument document, Element root, Locale locale) {
1074
1075        Iterator<I_CmsXmlSchemaType> i = m_typeSequence.iterator();
1076        int currentPos = 0;
1077        List<Element> allElements = CmsXmlGenericWrapper.elements(root);
1078
1079        while (i.hasNext()) {
1080            I_CmsXmlSchemaType type = i.next();
1081
1082            // check how many elements of this type already exist in the XML
1083            String elementName = type.getName();
1084            List<Element> elements = CmsXmlGenericWrapper.elements(root, elementName);
1085
1086            currentPos += elements.size();
1087            for (int j = elements.size(); j < type.getMinOccurs(); j++) {
1088                // append the missing elements
1089                Element typeElement = type.generateXml(cms, document, root, locale);
1090                // need to check for default value again because the of appinfo "mappings" node
1091                I_CmsXmlContentValue value = type.createValue(document, typeElement, locale);
1092                String defaultValue = document.getHandler().getDefault(cms, value, locale);
1093                if (defaultValue != null) {
1094                    // only if there is a default value available use it to overwrite the initial default
1095                    value.setStringValue(cms, defaultValue);
1096                }
1097
1098                // re-sort elements as they have been appended to the end of the XML root, not at the correct position
1099                typeElement.detach();
1100                allElements.add(currentPos, typeElement);
1101                currentPos++;
1102            }
1103        }
1104
1105        return root;
1106    }
1107
1108    /**
1109     * Adds a nested (included) XML content definition.<p>
1110     *
1111     * @param nestedSchema the nested (included) XML content definition to add
1112     */
1113    public void addInclude(CmsXmlContentDefinition nestedSchema) {
1114
1115        m_includes.add(nestedSchema);
1116    }
1117
1118    /**
1119     * Adds the given content type.<p>
1120     *
1121     * @param type the content type to add
1122     *
1123     * @throws CmsXmlException in case an unregistered type is added
1124     */
1125    public void addType(I_CmsXmlSchemaType type) throws CmsXmlException {
1126
1127        // check if the type to add actually exists in the type manager
1128        CmsXmlContentTypeManager typeManager = OpenCms.getXmlContentTypeManager();
1129        if (type.isSimpleType() && (typeManager.getContentType(type.getTypeName()) == null)) {
1130            throw new CmsXmlException(Messages.get().container(Messages.ERR_UNREGISTERED_TYPE_1, type.getTypeName()));
1131        }
1132
1133        // add the type to the internal type sequence and lookup table
1134        m_typeSequence.add(type);
1135        m_types.put(type.getName(), type);
1136
1137        // store reference to the content definition in the type
1138        type.setContentDefinition(this);
1139    }
1140
1141    /**
1142     * Creates a clone of this XML content definition.<p>
1143     *
1144     * @return a clone of this XML content definition
1145     */
1146    @Override
1147    public Object clone() {
1148
1149        CmsXmlContentDefinition result = new CmsXmlContentDefinition();
1150        result.m_innerName = m_innerName;
1151        result.m_schemaLocation = m_schemaLocation;
1152        result.m_typeSequence = m_typeSequence;
1153        result.m_types = m_types;
1154        result.m_contentHandler = m_contentHandler;
1155        result.m_typeName = m_typeName;
1156        result.m_includes = m_includes;
1157        result.m_sequenceType = m_sequenceType;
1158        result.m_choiceMaxOccurs = m_choiceMaxOccurs;
1159        result.m_elementTypes = m_elementTypes;
1160        return result;
1161    }
1162
1163    /**
1164     * Generates the default XML content for this content definition, and append it to the given root element.<p>
1165     *
1166     * Please note: The default values for the annotations are read from the content definition of the given
1167     * document. For a nested content definitions, this means that all defaults are set in the annotations of the
1168     * "outer" or "main" content definition.<p>
1169     *
1170     * @param cms the current users OpenCms context
1171     * @param document the OpenCms XML document the XML is created for
1172     * @param root the node of the document where to append the generated XML to
1173     * @param locale the locale to create the default element in the document with
1174     *
1175     * @return the default XML content for this content definition, and append it to the given root element
1176     */
1177    public Element createDefaultXml(CmsObject cms, I_CmsXmlDocument document, Element root, Locale locale) {
1178
1179        Iterator<I_CmsXmlSchemaType> i = m_typeSequence.iterator();
1180        while (i.hasNext()) {
1181            I_CmsXmlSchemaType type = i.next();
1182            for (int j = 0; j < type.getMinOccurs(); j++) {
1183                Element typeElement = type.generateXml(cms, document, root, locale);
1184                // need to check for default value again because of the appinfo "mappings" node
1185                I_CmsXmlContentValue value = type.createValue(document, typeElement, locale);
1186                String defaultValue = document.getHandler().getDefault(cms, value, locale);
1187                if (defaultValue != null) {
1188                    // only if there is a default value available use it to overwrite the initial default
1189                    value.setStringValue(cms, defaultValue);
1190                }
1191            }
1192        }
1193
1194        return root;
1195    }
1196
1197    /**
1198     * Generates a valid XML document according to the XML schema of this content definition.<p>
1199     *
1200     * @param cms the current users OpenCms context
1201     * @param document the OpenCms XML document the XML is created for
1202     * @param locale the locale to create the default element in the document with
1203     *
1204     * @return a valid XML document according to the XML schema of this content definition
1205     */
1206    public Document createDocument(CmsObject cms, I_CmsXmlDocument document, Locale locale) {
1207
1208        Document doc = DocumentHelper.createDocument();
1209
1210        Element root = doc.addElement(getOuterName());
1211
1212        root.add(I_CmsXmlSchemaType.XSI_NAMESPACE);
1213        root.addAttribute(I_CmsXmlSchemaType.XSI_NAMESPACE_ATTRIBUTE_NO_SCHEMA_LOCATION, getSchemaLocation());
1214        int version = getVersion();
1215        if (version != 0) {
1216            root.addAttribute(CmsXmlContent.A_VERSION, "" + version);
1217        }
1218        createLocale(cms, document, root, locale);
1219        return doc;
1220    }
1221
1222    /**
1223     * Generates a valid locale (language) element for the XML schema of this content definition.<p>
1224     *
1225     * @param cms the current users OpenCms context
1226     * @param document the OpenCms XML document the XML is created for
1227     * @param root the root node of the document where to append the locale to
1228     * @param locale the locale to create the default element in the document with
1229     *
1230     * @return a valid XML element for the locale according to the XML schema of this content definition
1231     */
1232    public Element createLocale(CmsObject cms, I_CmsXmlDocument document, Element root, Locale locale) {
1233
1234        // add an element with a "locale" attribute to the given root node
1235        Element element = root.addElement(getInnerName());
1236        element.addAttribute(XSD_ATTRIBUTE_VALUE_LANGUAGE, locale.toString());
1237
1238        // now generate the default XML for the element
1239        return createDefaultXml(cms, document, element, locale);
1240    }
1241
1242    /**
1243     * @see java.lang.Object#equals(java.lang.Object)
1244     */
1245    @Override
1246    public boolean equals(Object obj) {
1247
1248        if (obj == this) {
1249            return true;
1250        }
1251        if (!(obj instanceof CmsXmlContentDefinition)) {
1252            return false;
1253        }
1254        CmsXmlContentDefinition other = (CmsXmlContentDefinition)obj;
1255        if (!getInnerName().equals(other.getInnerName())) {
1256            return false;
1257        }
1258        if (!getOuterName().equals(other.getOuterName())) {
1259            return false;
1260        }
1261        return m_typeSequence.equals(other.m_typeSequence);
1262    }
1263
1264    /**
1265     * Iterates over all schema types along a given xpath, starting from a root content definition.<p>
1266     *
1267     * @param path the path
1268     * @param consumer a handler that consumes both the schema type and the remaining suffix of the path, relative to the schema type
1269     *
1270     * @return true if for all path components a schema type could be found
1271     */
1272    public boolean findSchemaTypesForPath(String path, BiConsumer<I_CmsXmlSchemaType, String> consumer) {
1273
1274        path = CmsXmlUtils.removeAllXpathIndices(path);
1275        List<String> pathComponents = CmsXmlUtils.splitXpath(path);
1276        List<I_CmsXmlSchemaType> result = new ArrayList<>();
1277        CmsXmlContentDefinition currentContentDef = this;
1278        for (int i = 0; i < pathComponents.size(); i++) {
1279            String pathComponent = pathComponents.get(i);
1280
1281            if (currentContentDef == null) {
1282                return false;
1283            }
1284            I_CmsXmlSchemaType schemaType = currentContentDef.getSchemaType(pathComponent);
1285            if (schemaType == null) {
1286                return false;
1287            } else {
1288                String remainingPath = CmsStringUtil.listAsString(
1289                    pathComponents.subList(i + 1, pathComponents.size()),
1290                    "/");
1291                consumer.accept(schemaType, remainingPath);
1292                result.add(schemaType);
1293                if (schemaType instanceof CmsXmlNestedContentDefinition) {
1294                    currentContentDef = ((CmsXmlNestedContentDefinition)schemaType).getNestedContentDefinition();
1295                } else {
1296                    currentContentDef = null; //
1297                }
1298            }
1299        }
1300        return true;
1301    }
1302
1303    /**
1304     * Freezes this content definition, making all internal data structures
1305     * unmodifiable.<p>
1306     *
1307     * This is required to prevent modification of a cached content definition.<p>
1308     */
1309    public void freeze() {
1310
1311        m_types = Collections.unmodifiableMap(m_types);
1312        m_typeSequence = Collections.unmodifiableList(m_typeSequence);
1313    }
1314
1315    /**
1316     * Returns the maxOccurs value for the choice in case this is a <code>xsd:choice</code> content definition.<p>
1317     *
1318     * This content definition is a <code>xsd:choice</code> sequence if the returned value is larger then 0.<p>
1319     *
1320     * @return the maxOccurs value for the choice in case this is a <code>xsd:choice</code> content definition
1321     */
1322    public int getChoiceMaxOccurs() {
1323
1324        return m_choiceMaxOccurs;
1325    }
1326
1327    /**
1328     * Returns the selected XML content handler for this XML content definition.<p>
1329     *
1330     * If no specific XML content handler was provided in the "appinfo" node of the
1331     * XML schema, the default XML content handler <code>{@link CmsDefaultXmlContentHandler}</code> is used.<p>
1332     *
1333     * @return the contentHandler
1334     */
1335    public I_CmsXmlContentHandler getContentHandler() {
1336
1337        return m_contentHandler;
1338    }
1339
1340    /**
1341     * Returns the set of nested (included) XML content definitions.<p>
1342     *
1343     * @return the set of nested (included) XML content definitions
1344     */
1345    public Set<CmsXmlContentDefinition> getIncludes() {
1346
1347        return m_includes;
1348    }
1349
1350    /**
1351     * Returns the inner element name of this content definition.<p>
1352     *
1353     * @return the inner element name of this content definition
1354     */
1355    public String getInnerName() {
1356
1357        return m_innerName;
1358    }
1359
1360    /**
1361     * Returns the outer element name of this content definition.<p>
1362     *
1363     * @return the outer element name of this content definition
1364     */
1365    public String getOuterName() {
1366
1367        return m_outerName;
1368    }
1369
1370    /**
1371     * Generates an XML schema for the content definition.<p>
1372     *
1373     * @return the generated XML schema
1374     */
1375    public Document getSchema() {
1376
1377        Document result;
1378
1379        if (m_schemaDocument == null) {
1380            result = DocumentHelper.createDocument();
1381            Element root = result.addElement(XSD_NODE_SCHEMA);
1382            root.addAttribute(XSD_ATTRIBUTE_ELEMENT_FORM_DEFAULT, XSD_ATTRIBUTE_VALUE_QUALIFIED);
1383
1384            Element include = root.addElement(XSD_NODE_INCLUDE);
1385            include.addAttribute(XSD_ATTRIBUTE_SCHEMA_LOCATION, XSD_INCLUDE_OPENCMS);
1386
1387            if (m_includes.size() > 0) {
1388                Iterator<CmsXmlContentDefinition> i = m_includes.iterator();
1389                while (i.hasNext()) {
1390                    CmsXmlContentDefinition definition = i.next();
1391                    root.addElement(XSD_NODE_INCLUDE).addAttribute(
1392                        XSD_ATTRIBUTE_SCHEMA_LOCATION,
1393                        definition.m_schemaLocation);
1394                }
1395            }
1396
1397            String outerTypeName = createTypeName(getOuterName());
1398            String innerTypeName = createTypeName(getInnerName());
1399
1400            Element content = root.addElement(XSD_NODE_ELEMENT);
1401            content.addAttribute(XSD_ATTRIBUTE_NAME, getOuterName());
1402            content.addAttribute(XSD_ATTRIBUTE_TYPE, outerTypeName);
1403
1404            Element list = root.addElement(XSD_NODE_COMPLEXTYPE);
1405            list.addAttribute(XSD_ATTRIBUTE_NAME, outerTypeName);
1406
1407            Element listSequence = list.addElement(XSD_NODE_SEQUENCE);
1408            Element listElement = listSequence.addElement(XSD_NODE_ELEMENT);
1409            listElement.addAttribute(XSD_ATTRIBUTE_NAME, getInnerName());
1410            listElement.addAttribute(XSD_ATTRIBUTE_TYPE, innerTypeName);
1411            listElement.addAttribute(XSD_ATTRIBUTE_MIN_OCCURS, XSD_ATTRIBUTE_VALUE_ZERO);
1412            listElement.addAttribute(XSD_ATTRIBUTE_MAX_OCCURS, XSD_ATTRIBUTE_VALUE_UNBOUNDED);
1413
1414            Element main = root.addElement(XSD_NODE_COMPLEXTYPE);
1415            main.addAttribute(XSD_ATTRIBUTE_NAME, innerTypeName);
1416
1417            Element mainSequence;
1418            if (m_sequenceType == SequenceType.SEQUENCE) {
1419                mainSequence = main.addElement(XSD_NODE_SEQUENCE);
1420            } else {
1421                mainSequence = main.addElement(XSD_NODE_CHOICE);
1422                if (getChoiceMaxOccurs() > 1) {
1423                    mainSequence.addAttribute(XSD_ATTRIBUTE_MAX_OCCURS, String.valueOf(getChoiceMaxOccurs()));
1424                } else {
1425                    mainSequence.addAttribute(XSD_ATTRIBUTE_MIN_OCCURS, XSD_ATTRIBUTE_VALUE_ZERO);
1426                    mainSequence.addAttribute(XSD_ATTRIBUTE_MAX_OCCURS, XSD_ATTRIBUTE_VALUE_ONE);
1427                }
1428            }
1429
1430            Iterator<I_CmsXmlSchemaType> i = m_typeSequence.iterator();
1431            while (i.hasNext()) {
1432                I_CmsXmlSchemaType schemaType = i.next();
1433                schemaType.appendXmlSchema(mainSequence);
1434            }
1435
1436            Element language = main.addElement(XSD_NODE_ATTRIBUTE);
1437            language.addAttribute(XSD_ATTRIBUTE_NAME, XSD_ATTRIBUTE_VALUE_LANGUAGE);
1438            language.addAttribute(XSD_ATTRIBUTE_TYPE, CmsXmlLocaleValue.TYPE_NAME);
1439            language.addAttribute(XSD_ATTRIBUTE_USE, XSD_ATTRIBUTE_VALUE_OPTIONAL);
1440        } else {
1441            result = (Document)m_schemaDocument.clone();
1442        }
1443        return result;
1444    }
1445
1446    /**
1447     * Returns the location from which the XML schema was read (XML system id).<p>
1448     *
1449     * @return the location from which the XML schema was read (XML system id)
1450     */
1451    public String getSchemaLocation() {
1452
1453        return m_schemaLocation;
1454    }
1455
1456    /**
1457     * Returns the schema type for the given element name, or <code>null</code> if no
1458     * node is defined with this name.<p>
1459     *
1460     * @param elementPath the element xpath to look up the type for
1461     * @return the type for the given element name, or <code>null</code> if no
1462     *      node is defined with this name
1463     */
1464    public I_CmsXmlSchemaType getSchemaType(String elementPath) {
1465
1466        String path = CmsXmlUtils.removeXpath(elementPath);
1467        I_CmsXmlSchemaType result = m_elementTypes.get(path);
1468        if (result == null) {
1469            result = getSchemaTypeRecusive(path);
1470            if (result != null) {
1471                m_elementTypes.put(path, result);
1472            } else {
1473                m_elementTypes.put(path, NULL_SCHEMA_TYPE);
1474            }
1475        } else if (result == NULL_SCHEMA_TYPE) {
1476            result = null;
1477        }
1478        return result;
1479    }
1480
1481    /**
1482     * Returns the internal set of schema type names.<p>
1483     *
1484     * @return the internal set of schema type names
1485     */
1486    public Set<String> getSchemaTypes() {
1487
1488        return m_types.keySet();
1489    }
1490
1491    /**
1492     * Returns the sequence type of this content definition.<p>
1493     *
1494     * @return the sequence type of this content definition
1495     */
1496    public SequenceType getSequenceType() {
1497
1498        return m_sequenceType;
1499    }
1500
1501    /**
1502     * Returns the main type name of this XML content definition.<p>
1503     *
1504     * @return the main type name of this XML content definition
1505     */
1506    public String getTypeName() {
1507
1508        return m_typeName;
1509    }
1510
1511    /**
1512     * Returns the type sequence, contains instances of {@link I_CmsXmlSchemaType}.<p>
1513     *
1514     * @return the type sequence, contains instances of {@link I_CmsXmlSchemaType}
1515     */
1516    public List<I_CmsXmlSchemaType> getTypeSequence() {
1517
1518        return m_typeSequence;
1519    }
1520
1521    /**
1522     * Gets the version.
1523     *
1524     * @return the version number
1525     */
1526    public int getVersion() {
1527
1528        return CmsXmlUtils.getSchemaVersion(m_schemaDocument);
1529    }
1530
1531    /**
1532     * @see java.lang.Object#hashCode()
1533     */
1534    @Override
1535    public int hashCode() {
1536
1537        return getInnerName().hashCode();
1538    }
1539
1540    /**
1541     * @see java.lang.Object#toString()
1542     */
1543    public String toString() {
1544
1545        return CmsXmlContentDefinition.class.getSimpleName() + " " + m_schemaLocation;
1546    }
1547
1548    /**
1549     * Sets the inner element name to use for the content definition.<p>
1550     *
1551     * @param innerName the inner element name to set
1552     */
1553    protected void setInnerName(String innerName) {
1554
1555        m_innerName = innerName;
1556        if (m_innerName != null) {
1557            m_typeName = createTypeName(innerName);
1558        }
1559    }
1560
1561    /**
1562     * Sets the outer element name to use for the content definition.<p>
1563     *
1564     * @param outerName the outer element name to set
1565     */
1566    protected void setOuterName(String outerName) {
1567
1568        m_outerName = outerName;
1569    }
1570
1571    /**
1572     * Calculates the schema type for the given element name by recursing into the schema structure.<p>
1573     *
1574     * @param elementPath the element xpath to look up the type for
1575     * @return the type for the given element name, or <code>null</code> if no
1576     *      node is defined with this name
1577     */
1578    private I_CmsXmlSchemaType getSchemaTypeRecusive(String elementPath) {
1579
1580        String path = CmsXmlUtils.getFirstXpathElement(elementPath);
1581
1582        I_CmsXmlSchemaType type = m_types.get(path);
1583        if (type == null) {
1584            // no node with the given path defined in schema
1585            return null;
1586        }
1587
1588        // check if recursion is required to get value from a nested schema
1589        if (type.isSimpleType() || !CmsXmlUtils.isDeepXpath(elementPath)) {
1590            // no recursion required
1591            return type;
1592        }
1593
1594        // recursion required since the path is an xpath and the type must be a nested content definition
1595        CmsXmlNestedContentDefinition nestedDefinition = (CmsXmlNestedContentDefinition)type;
1596        path = CmsXmlUtils.removeFirstXpathElement(elementPath);
1597        return nestedDefinition.getNestedContentDefinition().getSchemaType(path);
1598    }
1599
1600}