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