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