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