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