All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opencms.xml.CmsXmlContentDefinition Maven / Gradle / Ivy

Go to download

OpenCms is an enterprise-ready, easy to use website content management system based on Java and XML technology. Offering a complete set of features, OpenCms helps content managers worldwide to create and maintain beautiful websites fast and efficiently.

There is a newer version: 18.0
Show newest version
/*
 * This library is part of OpenCms -
 * the Open Source Content Management System
 *
 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * For further information about Alkacon Software GmbH & Co. KG, please see the
 * company website: http://www.alkacon.com
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.opencms.xml;

import org.opencms.file.CmsObject;
import org.opencms.file.CmsResource;
import org.opencms.file.CmsResourceFilter;
import org.opencms.file.types.CmsResourceTypeXmlContent;
import org.opencms.file.types.I_CmsResourceType;
import org.opencms.main.CmsException;
import org.opencms.main.CmsLog;
import org.opencms.main.OpenCms;
import org.opencms.relations.CmsRelation;
import org.opencms.relations.CmsRelationFilter;
import org.opencms.relations.CmsRelationType;
import org.opencms.util.CmsStringUtil;
import org.opencms.xml.content.CmsDefaultXmlContentHandler;
import org.opencms.xml.content.CmsXmlContent;
import org.opencms.xml.content.CmsXmlContentFactory;
import org.opencms.xml.content.I_CmsXmlContentHandler;
import org.opencms.xml.types.CmsXmlDynamicCategoryValue;
import org.opencms.xml.types.CmsXmlLocaleValue;
import org.opencms.xml.types.CmsXmlNestedContentDefinition;
import org.opencms.xml.types.CmsXmlStringValue;
import org.opencms.xml.types.I_CmsXmlContentValue;
import org.opencms.xml.types.I_CmsXmlSchemaType;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.logging.Log;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Namespace;
import org.dom4j.QName;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Describes the structure definition of an XML content object.

* * @since 6.0.0 */ public class CmsXmlContentDefinition implements Cloneable { /** * Enumeration of possible sequence types in a content definition. */ public enum SequenceType { /** A xsd:choice where the choice elements can appear more than once in a mix. */ MULTIPLE_CHOICE, /** A simple xsd:sequence. */ SEQUENCE, /** A xsd:choice where only one choice element can be selected. */ SINGLE_CHOICE } /** Constant for the XML schema attribute "mapto". */ public static final String XSD_ATTRIBUTE_DEFAULT = "default"; /** Constant for the XML schema attribute "elementFormDefault". */ public static final String XSD_ATTRIBUTE_ELEMENT_FORM_DEFAULT = "elementFormDefault"; /** Constant for the XML schema attribute "maxOccurs". */ public static final String XSD_ATTRIBUTE_MAX_OCCURS = "maxOccurs"; /** Constant for the XML schema attribute "minOccurs". */ public static final String XSD_ATTRIBUTE_MIN_OCCURS = "minOccurs"; /** Constant for the XML schema attribute "name". */ public static final String XSD_ATTRIBUTE_NAME = "name"; /** Constant for the XML schema attribute "schemaLocation". */ public static final String XSD_ATTRIBUTE_SCHEMA_LOCATION = "schemaLocation"; /** Constant for the XML schema attribute "type". */ public static final String XSD_ATTRIBUTE_TYPE = "type"; /** Constant for the XML schema attribute "use". */ public static final String XSD_ATTRIBUTE_USE = "use"; /** Constant for the XML schema attribute value "language". */ public static final String XSD_ATTRIBUTE_VALUE_LANGUAGE = "language"; /** Constant for the XML schema attribute value "1". */ public static final String XSD_ATTRIBUTE_VALUE_ONE = "1"; /** Constant for the XML schema attribute value "optional". */ public static final String XSD_ATTRIBUTE_VALUE_OPTIONAL = "optional"; /** Constant for the XML schema attribute value "qualified". */ public static final String XSD_ATTRIBUTE_VALUE_QUALIFIED = "qualified"; /** Constant for the XML schema attribute value "required". */ public static final String XSD_ATTRIBUTE_VALUE_REQUIRED = "required"; /** Constant for the XML schema attribute value "unbounded". */ public static final String XSD_ATTRIBUTE_VALUE_UNBOUNDED = "unbounded"; /** Constant for the XML schema attribute value "0". */ public static final String XSD_ATTRIBUTE_VALUE_ZERO = "0"; /** The opencms default type definition include. */ public static final String XSD_INCLUDE_OPENCMS = CmsXmlEntityResolver.OPENCMS_SCHEME + "opencms-xmlcontent.xsd"; /** The schema definition namespace. */ public static final Namespace XSD_NAMESPACE = Namespace.get("xsd", "http://www.w3.org/2001/XMLSchema"); /** Constant for the "annotation" node in the XML schema namespace. */ public static final QName XSD_NODE_ANNOTATION = QName.get("annotation", XSD_NAMESPACE); /** Constant for the "appinfo" node in the XML schema namespace. */ public static final QName XSD_NODE_APPINFO = QName.get("appinfo", XSD_NAMESPACE); /** Constant for the "attribute" node in the XML schema namespace. */ public static final QName XSD_NODE_ATTRIBUTE = QName.get("attribute", XSD_NAMESPACE); /** Constant for the "choice" node in the XML schema namespace. */ public static final QName XSD_NODE_CHOICE = QName.get("choice", XSD_NAMESPACE); /** Constant for the "complexType" node in the XML schema namespace. */ public static final QName XSD_NODE_COMPLEXTYPE = QName.get("complexType", XSD_NAMESPACE); /** Constant for the "element" node in the XML schema namespace. */ public static final QName XSD_NODE_ELEMENT = QName.get("element", XSD_NAMESPACE); /** Constant for the "include" node in the XML schema namespace. */ public static final QName XSD_NODE_INCLUDE = QName.get("include", XSD_NAMESPACE); /** Constant for the "schema" node in the XML schema namespace. */ public static final QName XSD_NODE_SCHEMA = QName.get("schema", XSD_NAMESPACE); /** Constant for the "sequence" node in the XML schema namespace. */ public static final QName XSD_NODE_SEQUENCE = QName.get("sequence", XSD_NAMESPACE); /** The log object for this class. */ private static final Log LOG = CmsLog.getLog(CmsXmlContentDefinition.class); /** Null schema type value, required for map lookups. */ private static final I_CmsXmlSchemaType NULL_SCHEMA_TYPE = new CmsXmlStringValue("NULL", "0", "0"); /** Max occurs value for xsd:choice definitions. */ private int m_choiceMaxOccurs; /** The XML content handler. */ private I_CmsXmlContentHandler m_contentHandler; /** The Map of configured types indexed by the element xpath. */ private Map m_elementTypes; /** The set of included additional XML content definitions. */ private Set m_includes; /** The inner element name of the content definition (type sequence). */ private String m_innerName; /** The outer element name of the content definition (language sequence). */ private String m_outerName; /** The XML document from which the schema was unmarshalled. */ private Document m_schemaDocument; /** The location from which the XML schema was read (XML system id). */ private String m_schemaLocation; /** Indicates the sequence type of this content definition. */ private SequenceType m_sequenceType; /** The main type name of this XML content definition. */ private String m_typeName; /** The Map of configured types. */ private Map m_types; /** The type sequence. */ private List m_typeSequence; /** * Creates a new XML content definition.

* * @param innerName the inner element name to use for the content definiton * @param schemaLocation the location from which the XML schema was read (system id) */ public CmsXmlContentDefinition(String innerName, String schemaLocation) { this(innerName + "s", innerName, schemaLocation); } /** * Creates a new XML content definition.

* * @param outerName the outer element name to use for the content definition * @param innerName the inner element name to use for the content definition * @param schemaLocation the location from which the XML schema was read (system id) */ public CmsXmlContentDefinition(String outerName, String innerName, String schemaLocation) { m_outerName = outerName; m_innerName = innerName; setInnerName(innerName); m_typeSequence = new ArrayList(); m_types = new HashMap(); m_includes = new HashSet(); m_schemaLocation = schemaLocation; m_contentHandler = new CmsDefaultXmlContentHandler(); m_sequenceType = SequenceType.SEQUENCE; m_elementTypes = new ConcurrentHashMap(); } /** * Required empty constructor for clone operation.

*/ protected CmsXmlContentDefinition() { // noop, required for clone operation } /** * Factory method that returns the XML content definition instance for a given resource.

* * @param cms the cms-object * @param resource the resource * * @return the XML content definition * * @throws CmsException if something goes wrong */ public static CmsXmlContentDefinition getContentDefinitionForResource(CmsObject cms, CmsResource resource) throws CmsException { CmsXmlContentDefinition contentDef = null; I_CmsResourceType resType = OpenCms.getResourceManager().getResourceType(resource.getTypeId()); String schema = resType.getConfiguration().get(CmsResourceTypeXmlContent.CONFIGURATION_SCHEMA); if (schema != null) { try { // this wont in most cases read the file content because of caching contentDef = unmarshal(cms, schema); } catch (CmsException e) { // this should never happen, unless the configured schema is different than the schema in the XML if (!LOG.isDebugEnabled()) { LOG.warn(e); } LOG.debug(e.getLocalizedMessage(), e); } } if (contentDef == null) { // could still be empty since it is not mandatory to configure the resource type in the XML configuration // try through the XSD relation List relations = cms.getRelationsForResource( resource, CmsRelationFilter.TARGETS.filterType(CmsRelationType.XSD)); if ((relations != null) && !relations.isEmpty()) { CmsXmlEntityResolver entityResolver = new CmsXmlEntityResolver(cms); String xsd = cms.getSitePath(relations.get(0).getTarget(cms, CmsResourceFilter.ALL)); contentDef = entityResolver.getCachedContentDefinition(xsd); } } if (contentDef == null) { // could still be empty if the XML content has been saved with an OpenCms before 8.0.0 // so, to unmarshal is the only possibility left CmsXmlContent content = CmsXmlContentFactory.unmarshal(cms, cms.readFile(resource)); contentDef = content.getContentDefinition(); } return contentDef; } /** * Reads the content definition which is configured for a resource type.

* * @param cms the current CMS context * @param typeName the type name * * @return the content definition * * @throws CmsException if something goes wrong */ public static CmsXmlContentDefinition getContentDefinitionForType(CmsObject cms, String typeName) throws CmsException { I_CmsResourceType resType = OpenCms.getResourceManager().getResourceType(typeName); String schema = resType.getConfiguration().get(CmsResourceTypeXmlContent.CONFIGURATION_SCHEMA); CmsXmlContentDefinition contentDef = null; if (schema == null) { return null; } contentDef = unmarshal(cms, schema); return contentDef; } /** * Returns a content handler instance for the given resource.

* * @param cms the cms-object * @param resource the resource * * @return the content handler * * @throws CmsException if something goes wrong */ public static I_CmsXmlContentHandler getContentHandlerForResource(CmsObject cms, CmsResource resource) throws CmsException { return getContentDefinitionForResource(cms, resource).getContentHandler(); } /** * Factory method to unmarshal (read) a XML content definition instance from a byte array * that contains XML data.

* * @param xmlData the XML data in a byte array * @param schemaLocation the location from which the XML schema was read (system id) * @param resolver the XML entity resolver to use * * @return a XML content definition instance unmarshalled from the byte array * * @throws CmsXmlException if something goes wrong */ public static CmsXmlContentDefinition unmarshal(byte[] xmlData, String schemaLocation, EntityResolver resolver) throws CmsXmlException { schemaLocation = translateSchema(schemaLocation); CmsXmlContentDefinition result = getCachedContentDefinition(schemaLocation, resolver); if (result == null) { // content definition was not found in the cache, unmarshal the XML document result = unmarshalInternal(CmsXmlUtils.unmarshalHelper(xmlData, resolver), schemaLocation, resolver); } return result; } /** * Factory method to unmarshal (read) a XML content definition instance from the OpenCms VFS resource name.

* * @param cms the current users CmsObject * @param resourcename the resource name to unmarshal the XML content definition from * * @return a XML content definition instance unmarshalled from the VFS resource * * @throws CmsXmlException if something goes wrong */ public static CmsXmlContentDefinition unmarshal(CmsObject cms, String resourcename) throws CmsXmlException { CmsXmlEntityResolver resolver = new CmsXmlEntityResolver(cms); String schemaLocation = CmsXmlEntityResolver.OPENCMS_SCHEME.concat(resourcename.substring(1)); schemaLocation = translateSchema(schemaLocation); CmsXmlContentDefinition result = getCachedContentDefinition(schemaLocation, resolver); if (result == null) { // content definition was not found in the cache, unmarshal the XML document InputSource source = resolver.resolveEntity(null, schemaLocation); result = unmarshalInternal(CmsXmlUtils.unmarshalHelper(source, resolver), schemaLocation, resolver); } return result; } /** * Factory method to unmarshal (read) a XML content definition instance from a XML document.

* * This method does additional validation to ensure the document has the required * XML structure for a OpenCms content definition schema.

* * @param document the XML document to generate a XML content definition from * @param schemaLocation the location from which the XML schema was read (system id) * * @return a XML content definition instance unmarshalled from the XML document * * @throws CmsXmlException if something goes wrong */ public static CmsXmlContentDefinition unmarshal(Document document, String schemaLocation) throws CmsXmlException { schemaLocation = translateSchema(schemaLocation); EntityResolver resolver = document.getEntityResolver(); CmsXmlContentDefinition result = getCachedContentDefinition(schemaLocation, resolver); if (result == null) { // content definition was not found in the cache, unmarshal the XML document result = unmarshalInternal(document, schemaLocation, resolver); } return result; } /** * Factory method to unmarshal (read) a XML content definition instance from a XML InputSource.

* * @param source the XML InputSource to use * @param schemaLocation the location from which the XML schema was read (system id) * @param resolver the XML entity resolver to use * * @return a XML content definition instance unmarshalled from the InputSource * * @throws CmsXmlException if something goes wrong */ public static CmsXmlContentDefinition unmarshal(InputSource source, String schemaLocation, EntityResolver resolver) throws CmsXmlException { schemaLocation = translateSchema(schemaLocation); CmsXmlContentDefinition result = getCachedContentDefinition(schemaLocation, resolver); if (result == null) { // content definition was not found in the cache, unmarshal the XML document result = unmarshalInternal(CmsXmlUtils.unmarshalHelper(source, resolver), schemaLocation, resolver); } return result; } /** * Factory method to unmarshal (read) a XML content definition instance from a given XML schema location.

* * The XML content definition data to unmarshal will be read from the provided schema location using * an XML InputSource.

* * @param schemaLocation the location from which to read the XML schema (system id) * @param resolver the XML entity resolver to use * * @return a XML content definition instance unmarshalled from the InputSource * * @throws CmsXmlException if something goes wrong * @throws SAXException if the XML schema location could not be converted to an XML InputSource * @throws IOException if the XML schema location could not be converted to an XML InputSource */ public static CmsXmlContentDefinition unmarshal(String schemaLocation, EntityResolver resolver) throws CmsXmlException, SAXException, IOException { schemaLocation = translateSchema(schemaLocation); CmsXmlContentDefinition result = getCachedContentDefinition(schemaLocation, resolver); if (result == null) { // content definition was not found in the cache, unmarshal the XML document InputSource source = resolver.resolveEntity(null, schemaLocation); result = unmarshalInternal(CmsXmlUtils.unmarshalHelper(source, resolver), schemaLocation, resolver); } return result; } /** * Factory method to unmarshal (read) a XML content definition instance from a String * that contains XML data.

* * @param xmlData the XML data in a String * @param schemaLocation the location from which the XML schema was read (system id) * @param resolver the XML entity resolver to use * * @return a XML content definition instance unmarshalled from the byte array * * @throws CmsXmlException if something goes wrong */ public static CmsXmlContentDefinition unmarshal(String xmlData, String schemaLocation, EntityResolver resolver) throws CmsXmlException { schemaLocation = translateSchema(schemaLocation); CmsXmlContentDefinition result = getCachedContentDefinition(schemaLocation, resolver); if (result == null) { // content definition was not found in the cache, unmarshal the XML document result = unmarshalInternal(CmsXmlUtils.unmarshalHelper(xmlData, resolver), schemaLocation, resolver); } return result; } /** * Creates the name of the type attribute from the given content name.

* * @param name the name to use * * @return the name of the type attribute */ protected static String createTypeName(String name) { StringBuffer result = new StringBuffer(32); result.append("OpenCms"); result.append(name.substring(0, 1).toUpperCase()); if (name.length() > 1) { result.append(name.substring(1)); } return result.toString(); } /** * Validates if a given attribute exists at the given element with an (optional) specified value.

* * If the required value is not null, the attribute must have exactly this * value set.

* * If no value is required, some simple validation is performed on the attribute value, * like a check that the value does not have leading or trailing white spaces.

* * @param element the element to validate * @param attributeName the attribute to check for * @param requiredValue the required value of the attribute, or null if any value is allowed * * @return the value of the attribute * * @throws CmsXmlException if the element does not have the required attribute set, or if the validation fails */ protected static String validateAttribute(Element element, String attributeName, String requiredValue) throws CmsXmlException { Attribute attribute = element.attribute(attributeName); if (attribute == null) { throw new CmsXmlException( Messages.get().container(Messages.ERR_EL_MISSING_ATTRIBUTE_2, element.getUniquePath(), attributeName)); } String value = attribute.getValue(); if (requiredValue == null) { if (CmsStringUtil.isEmptyOrWhitespaceOnly(value) || !value.equals(value.trim())) { throw new CmsXmlException( Messages.get().container( Messages.ERR_EL_BAD_ATTRIBUTE_WS_3, element.getUniquePath(), attributeName, value)); } } else { if (!requiredValue.equals(value)) { throw new CmsXmlException( Messages.get().container( Messages.ERR_EL_BAD_ATTRIBUTE_VALUE_4, new Object[] {element.getUniquePath(), attributeName, requiredValue, value})); } } return value; } /** * Validates if a given element has exactly the required attributes set.

* * @param element the element to validate * @param requiredAttributes the list of required attributes * @param optionalAttributes the list of optional attributes * * @throws CmsXmlException if the validation fails */ protected static void validateAttributesExists( Element element, String[] requiredAttributes, String[] optionalAttributes) throws CmsXmlException { if (element.attributeCount() < requiredAttributes.length) { throw new CmsXmlException( Messages.get().container( Messages.ERR_EL_ATTRIBUTE_TOOFEW_3, element.getUniquePath(), new Integer(requiredAttributes.length), new Integer(element.attributeCount()))); } if (element.attributeCount() > (requiredAttributes.length + optionalAttributes.length)) { throw new CmsXmlException( Messages.get().container( Messages.ERR_EL_ATTRIBUTE_TOOMANY_3, element.getUniquePath(), new Integer(requiredAttributes.length + optionalAttributes.length), new Integer(element.attributeCount()))); } for (int i = 0; i < requiredAttributes.length; i++) { String attributeName = requiredAttributes[i]; if (element.attribute(attributeName) == null) { throw new CmsXmlException( Messages.get().container( Messages.ERR_EL_MISSING_ATTRIBUTE_2, element.getUniquePath(), attributeName)); } } List rA = Arrays.asList(requiredAttributes); List oA = Arrays.asList(optionalAttributes); for (int i = 0; i < element.attributes().size(); i++) { String attributeName = element.attribute(i).getName(); if (!rA.contains(attributeName) && !oA.contains(attributeName)) { throw new CmsXmlException( Messages.get().container( Messages.ERR_EL_INVALID_ATTRIBUTE_2, element.getUniquePath(), attributeName)); } } } /** * Validates the given element as a complex type sequence.

* * @param element the element to validate * @param includes the XML schema includes * * @return a data structure containing the validated complex type sequence data * * @throws CmsXmlException if the validation fails */ protected static CmsXmlComplexTypeSequence validateComplexTypeSequence( Element element, Set includes) throws CmsXmlException { validateAttributesExists(element, new String[] {XSD_ATTRIBUTE_NAME}, new String[0]); String name = validateAttribute(element, XSD_ATTRIBUTE_NAME, null); // now check the type definition list List mainElements = CmsXmlGenericWrapper.elements(element); if ((mainElements.size() != 1) && (mainElements.size() != 2)) { throw new CmsXmlException( Messages.get().container( Messages.ERR_TS_SUBELEMENT_COUNT_2, element.getUniquePath(), new Integer(mainElements.size()))); } boolean hasLanguageAttribute = false; if (mainElements.size() == 2) { // two elements in the master list: the second must be the "language" attribute definition Element typeAttribute = mainElements.get(1); if (!XSD_NODE_ATTRIBUTE.equals(typeAttribute.getQName())) { throw new CmsXmlException( Messages.get().container( Messages.ERR_CD_ELEMENT_NAME_3, typeAttribute.getUniquePath(), XSD_NODE_ATTRIBUTE.getQualifiedName(), typeAttribute.getQName().getQualifiedName())); } validateAttribute(typeAttribute, XSD_ATTRIBUTE_NAME, XSD_ATTRIBUTE_VALUE_LANGUAGE); validateAttribute(typeAttribute, XSD_ATTRIBUTE_TYPE, CmsXmlLocaleValue.TYPE_NAME); try { validateAttribute(typeAttribute, XSD_ATTRIBUTE_USE, XSD_ATTRIBUTE_VALUE_REQUIRED); } catch (CmsXmlException e) { validateAttribute(typeAttribute, XSD_ATTRIBUTE_USE, XSD_ATTRIBUTE_VALUE_OPTIONAL); } // no error: then the language attribute is valid hasLanguageAttribute = true; } // the type of the sequence SequenceType sequenceType; int choiceMaxOccurs = 0; // check the main element type sequence Element typeSequenceElement = mainElements.get(0); if (!XSD_NODE_SEQUENCE.equals(typeSequenceElement.getQName())) { if (!XSD_NODE_CHOICE.equals(typeSequenceElement.getQName())) { throw new CmsXmlException( Messages.get().container( Messages.ERR_CD_ELEMENT_NAME_4, new Object[] { typeSequenceElement.getUniquePath(), XSD_NODE_SEQUENCE.getQualifiedName(), XSD_NODE_CHOICE.getQualifiedName(), typeSequenceElement.getQName().getQualifiedName()})); } else { // this is a xsd:choice, check if this is single or multiple choice String minOccursStr = typeSequenceElement.attributeValue(XSD_ATTRIBUTE_MIN_OCCURS); int minOccurs = 1; if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(minOccursStr)) { try { minOccurs = Integer.parseInt(minOccursStr.trim()); } catch (NumberFormatException e) { throw new CmsXmlException( Messages.get().container( Messages.ERR_EL_BAD_ATTRIBUTE_3, element.getUniquePath(), XSD_ATTRIBUTE_MIN_OCCURS, minOccursStr == null ? "1" : minOccursStr)); } } String maxOccursStr = typeSequenceElement.attributeValue(XSD_ATTRIBUTE_MAX_OCCURS); choiceMaxOccurs = 1; if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(maxOccursStr)) { if (CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_UNBOUNDED.equals(maxOccursStr.trim())) { choiceMaxOccurs = Integer.MAX_VALUE; } else { try { choiceMaxOccurs = Integer.parseInt(maxOccursStr.trim()); } catch (NumberFormatException e) { throw new CmsXmlException( Messages.get().container( Messages.ERR_EL_BAD_ATTRIBUTE_3, element.getUniquePath(), XSD_ATTRIBUTE_MAX_OCCURS, maxOccursStr)); } } } if ((minOccurs == 0) && (choiceMaxOccurs == 1)) { // minOccurs 0 and maxOccurs 1, this is a single choice sequence sequenceType = SequenceType.SINGLE_CHOICE; } else { // this is a multiple choice sequence if (minOccurs > choiceMaxOccurs) { throw new CmsXmlException( Messages.get().container( Messages.ERR_EL_BAD_ATTRIBUTE_3, element.getUniquePath(), XSD_ATTRIBUTE_MIN_OCCURS, minOccursStr == null ? "1" : minOccursStr)); } sequenceType = SequenceType.MULTIPLE_CHOICE; } } } else { // this is a simple sequence sequenceType = SequenceType.SEQUENCE; } // check the type definition sequence List typeSequenceElements = CmsXmlGenericWrapper.elements(typeSequenceElement); if (typeSequenceElements.size() < 1) { throw new CmsXmlException( Messages.get().container( Messages.ERR_TS_SUBELEMENT_TOOFEW_3, typeSequenceElement.getUniquePath(), new Integer(1), new Integer(typeSequenceElements.size()))); } // now add all type definitions from the schema List sequence = new ArrayList(); if (hasLanguageAttribute) { // only generate types for sequence node with language attribute CmsXmlContentTypeManager typeManager = OpenCms.getXmlContentTypeManager(); Iterator i = typeSequenceElements.iterator(); while (i.hasNext()) { Element typeElement = i.next(); if (sequenceType != SequenceType.SEQUENCE) { // in case of xsd:choice, need to make sure "minOccurs" for all type elements is 0 String minOccursStr = typeElement.attributeValue(XSD_ATTRIBUTE_MIN_OCCURS); int minOccurs = 1; if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(minOccursStr)) { try { minOccurs = Integer.parseInt(minOccursStr.trim()); } catch (NumberFormatException e) { // ignore } } // minOccurs must be "0" if (minOccurs != 0) { throw new CmsXmlException( Messages.get().container( Messages.ERR_EL_BAD_ATTRIBUTE_3, typeElement.getUniquePath(), XSD_ATTRIBUTE_MIN_OCCURS, minOccursStr == null ? "1" : minOccursStr)); } } // create the type with the type manager I_CmsXmlSchemaType type = typeManager.getContentType(typeElement, includes); if (type.getTypeName().equals(CmsXmlDynamicCategoryValue.TYPE_NAME) && ((type.getMaxOccurs() != 1) || (type.getMinOccurs() != 1))) { throw new CmsXmlException( Messages.get().container( Messages.ERR_EL_OF_TYPE_MUST_OCCUR_EXACTLY_ONCE_2, typeElement.getUniquePath(), type.getTypeName())); } if (sequenceType == SequenceType.MULTIPLE_CHOICE) { // if this is a multiple choice sequence, // all elements must have "minOccurs" 0 or 1 and "maxOccurs" of 1 if ((type.getMinOccurs() < 0) || (type.getMinOccurs() > 1) || (type.getMaxOccurs() != 1)) { throw new CmsXmlException( Messages.get().container( Messages.ERR_EL_BAD_ATTRIBUTE_3, typeElement.getUniquePath(), XSD_ATTRIBUTE_MAX_OCCURS, typeElement.attributeValue(XSD_ATTRIBUTE_MAX_OCCURS))); } } sequence.add(type); } } else { // generate a nested content definition for the main type sequence Element e = typeSequenceElements.get(0); String typeName = validateAttribute(e, XSD_ATTRIBUTE_NAME, null); String minOccurs = validateAttribute(e, XSD_ATTRIBUTE_MIN_OCCURS, XSD_ATTRIBUTE_VALUE_ZERO); String maxOccurs = validateAttribute(e, XSD_ATTRIBUTE_MAX_OCCURS, XSD_ATTRIBUTE_VALUE_UNBOUNDED); validateAttribute(e, XSD_ATTRIBUTE_TYPE, createTypeName(typeName)); CmsXmlNestedContentDefinition cd = new CmsXmlNestedContentDefinition(null, typeName, minOccurs, maxOccurs); sequence.add(cd); } // return a data structure with the collected values return new CmsXmlComplexTypeSequence(name, sequence, hasLanguageAttribute, sequenceType, choiceMaxOccurs); } /** * Looks up the given XML content definition system id in the internal content definition cache.

* * @param schemaLocation the system id of the XML content definition to look up * @param resolver the XML entity resolver to use (contains the cache) * * @return the XML content definition found, or null if no definition is cached for the given system id */ private static CmsXmlContentDefinition getCachedContentDefinition(String schemaLocation, EntityResolver resolver) { if (resolver instanceof CmsXmlEntityResolver) { // check for a cached version of this content definition CmsXmlEntityResolver cmsResolver = (CmsXmlEntityResolver)resolver; return cmsResolver.getCachedContentDefinition(schemaLocation); } return null; } /** * Translates the XSD schema location.

* * @param schemaLocation the location to translate * * @return the translated schema location */ private static String translateSchema(String schemaLocation) { if (OpenCms.getRepositoryManager() != null) { return OpenCms.getResourceManager().getXsdTranslator().translateResource(schemaLocation); } return schemaLocation; } /** * Internal method to unmarshal (read) a XML content definition instance from a XML document.

* * It is assumed that the XML content definition cache has already been tested and the document * has not been found in the cache. After the XML content definition has been successfully created, * it is placed in the cache.

* * @param document the XML document to generate a XML content definition from * @param schemaLocation the location from which the XML schema was read (system id) * @param resolver the XML entity resolver used by the given XML document * * @return a XML content definition instance unmarshalled from the XML document * * @throws CmsXmlException if something goes wrong */ private static CmsXmlContentDefinition unmarshalInternal( Document document, String schemaLocation, EntityResolver resolver) throws CmsXmlException { // analyze the document and generate the XML content type definition Element root = document.getRootElement(); if (!XSD_NODE_SCHEMA.equals(root.getQName())) { // schema node is required throw new CmsXmlException(Messages.get().container(Messages.ERR_CD_NO_SCHEMA_NODE_0)); } List includes = CmsXmlGenericWrapper.elements(root, XSD_NODE_INCLUDE); if (includes.size() < 1) { // one include is required throw new CmsXmlException(Messages.get().container(Messages.ERR_CD_ONE_INCLUDE_REQUIRED_0)); } Element include = includes.get(0); String target = validateAttribute(include, XSD_ATTRIBUTE_SCHEMA_LOCATION, null); if (!XSD_INCLUDE_OPENCMS.equals(target)) { // the first include must point to the default OpenCms standard schema include throw new CmsXmlException( Messages.get().container(Messages.ERR_CD_FIRST_INCLUDE_2, XSD_INCLUDE_OPENCMS, target)); } boolean recursive = false; Set nestedDefinitions = new HashSet(); if (includes.size() > 1) { // resolve additional, nested include calls for (int i = 1; i < includes.size(); i++) { Element inc = includes.get(i); String schemaLoc = validateAttribute(inc, XSD_ATTRIBUTE_SCHEMA_LOCATION, null); if (!(schemaLoc.equals(schemaLocation))) { InputSource source = null; try { source = resolver.resolveEntity(null, schemaLoc); } catch (Exception e) { throw new CmsXmlException(Messages.get().container(Messages.ERR_CD_BAD_INCLUDE_1, schemaLoc)); } CmsXmlContentDefinition xmlContentDefinition = unmarshal(source, schemaLoc, resolver); nestedDefinitions.add(xmlContentDefinition); } else { // recursion recursive = true; } } } List elements = CmsXmlGenericWrapper.elements(root, XSD_NODE_ELEMENT); if (elements.size() != 1) { // only one root element is allowed throw new CmsXmlException( Messages.get().container( Messages.ERR_CD_ROOT_ELEMENT_COUNT_1, XSD_INCLUDE_OPENCMS, new Integer(elements.size()))); } // collect the data from the root element node Element main = elements.get(0); String name = validateAttribute(main, XSD_ATTRIBUTE_NAME, null); // now process the complex types List complexTypes = CmsXmlGenericWrapper.elements(root, XSD_NODE_COMPLEXTYPE); if (complexTypes.size() != 2) { // exactly two complex types are required throw new CmsXmlException( Messages.get().container(Messages.ERR_CD_COMPLEX_TYPE_COUNT_1, new Integer(complexTypes.size()))); } // get the outer element sequence, this must be the first element CmsXmlComplexTypeSequence outerSequence = validateComplexTypeSequence(complexTypes.get(0), nestedDefinitions); CmsXmlNestedContentDefinition outer = (CmsXmlNestedContentDefinition)outerSequence.getSequence().get(0); // make sure the inner and outer element names are as required String outerTypeName = createTypeName(name); String innerTypeName = createTypeName(outer.getName()); validateAttribute(complexTypes.get(0), XSD_ATTRIBUTE_NAME, outerTypeName); validateAttribute(complexTypes.get(1), XSD_ATTRIBUTE_NAME, innerTypeName); validateAttribute(main, XSD_ATTRIBUTE_TYPE, outerTypeName); // generate the result XML content definition CmsXmlContentDefinition result = new CmsXmlContentDefinition(name, null, schemaLocation); // set the nested definitions result.m_includes = nestedDefinitions; // set the schema document result.m_schemaDocument = document; // the inner name is the element name set in the outer sequence result.setInnerName(outer.getName()); if (recursive) { nestedDefinitions.add(result); } // get the inner element sequence, this must be the second element CmsXmlComplexTypeSequence innerSequence = validateComplexTypeSequence(complexTypes.get(1), nestedDefinitions); // add the types from the main sequence node Iterator it = innerSequence.getSequence().iterator(); while (it.hasNext()) { result.addType(it.next()); } // store if this content definition contains a xsd:choice sequence result.m_sequenceType = innerSequence.getSequenceType(); result.m_choiceMaxOccurs = innerSequence.getChoiceMaxOccurs(); // resolve the XML content handler information List annotations = CmsXmlGenericWrapper.elements(root, XSD_NODE_ANNOTATION); I_CmsXmlContentHandler contentHandler = null; Element appInfoElement = null; if (annotations.size() > 0) { List appinfos = CmsXmlGenericWrapper.elements(annotations.get(0), XSD_NODE_APPINFO); if (appinfos.size() > 0) { // the first appinfo node contains the specific XML content data appInfoElement = appinfos.get(0); // check for a special content handler in the appinfo node Element handlerElement = appInfoElement.element("handler"); if (handlerElement != null) { String className = handlerElement.attributeValue("class"); if (className != null) { contentHandler = OpenCms.getXmlContentTypeManager().getFreshContentHandler(className); } } } } if (contentHandler == null) { // if no content handler is defined, the default handler is used contentHandler = OpenCms.getXmlContentTypeManager().getFreshContentHandler( CmsDefaultXmlContentHandler.class.getName()); } // analyze the app info node with the selected XML content handler contentHandler.initialize(appInfoElement, result); result.m_contentHandler = contentHandler; result.freeze(); if (resolver instanceof CmsXmlEntityResolver) { // put the generated content definition in the cache ((CmsXmlEntityResolver)resolver).cacheContentDefinition(schemaLocation, result); } return result; } /** * Adds the missing default XML according to this content definition to the given document element.

* * In case the root element already contains sub nodes, only missing sub nodes are added.

* * @param cms the current users OpenCms context * @param document the document where the XML is added in (required for default XML generation) * @param root the root node to add the missing XML for * @param locale the locale to add the XML for * * @return the given root element with the missing content added */ public Element addDefaultXml(CmsObject cms, I_CmsXmlDocument document, Element root, Locale locale) { Iterator i = m_typeSequence.iterator(); int currentPos = 0; List allElements = CmsXmlGenericWrapper.elements(root); while (i.hasNext()) { I_CmsXmlSchemaType type = i.next(); // check how many elements of this type already exist in the XML String elementName = type.getName(); List elements = CmsXmlGenericWrapper.elements(root, elementName); currentPos += elements.size(); for (int j = elements.size(); j < type.getMinOccurs(); j++) { // append the missing elements Element typeElement = type.generateXml(cms, document, root, locale); // need to check for default value again because the of appinfo "mappings" node I_CmsXmlContentValue value = type.createValue(document, typeElement, locale); String defaultValue = document.getHandler().getDefault(cms, value, locale); if (defaultValue != null) { // only if there is a default value available use it to overwrite the initial default value.setStringValue(cms, defaultValue); } // re-sort elements as they have been appended to the end of the XML root, not at the correct position typeElement.detach(); allElements.add(currentPos, typeElement); currentPos++; } } return root; } /** * Adds a nested (included) XML content definition.

* * @param nestedSchema the nested (included) XML content definition to add */ public void addInclude(CmsXmlContentDefinition nestedSchema) { m_includes.add(nestedSchema); } /** * Adds the given content type.

* * @param type the content type to add * * @throws CmsXmlException in case an unregistered type is added */ public void addType(I_CmsXmlSchemaType type) throws CmsXmlException { // check if the type to add actually exists in the type manager CmsXmlContentTypeManager typeManager = OpenCms.getXmlContentTypeManager(); if (type.isSimpleType() && (typeManager.getContentType(type.getTypeName()) == null)) { throw new CmsXmlException(Messages.get().container(Messages.ERR_UNREGISTERED_TYPE_1, type.getTypeName())); } // add the type to the internal type sequence and lookup table m_typeSequence.add(type); m_types.put(type.getName(), type); // store reference to the content definition in the type type.setContentDefinition(this); } /** * Creates a clone of this XML content definition.

* * @return a clone of this XML content definition */ @Override public Object clone() { CmsXmlContentDefinition result = new CmsXmlContentDefinition(); result.m_innerName = m_innerName; result.m_schemaLocation = m_schemaLocation; result.m_typeSequence = m_typeSequence; result.m_types = m_types; result.m_contentHandler = m_contentHandler; result.m_typeName = m_typeName; result.m_includes = m_includes; result.m_sequenceType = m_sequenceType; result.m_choiceMaxOccurs = m_choiceMaxOccurs; result.m_elementTypes = m_elementTypes; return result; } /** * Generates the default XML content for this content definition, and append it to the given root element.

* * Please note: The default values for the annotations are read from the content definition of the given * document. For a nested content definitions, this means that all defaults are set in the annotations of the * "outer" or "main" content definition.

* * @param cms the current users OpenCms context * @param document the OpenCms XML document the XML is created for * @param root the node of the document where to append the generated XML to * @param locale the locale to create the default element in the document with * * @return the default XML content for this content definition, and append it to the given root element */ public Element createDefaultXml(CmsObject cms, I_CmsXmlDocument document, Element root, Locale locale) { Iterator i = m_typeSequence.iterator(); while (i.hasNext()) { I_CmsXmlSchemaType type = i.next(); for (int j = 0; j < type.getMinOccurs(); j++) { Element typeElement = type.generateXml(cms, document, root, locale); // need to check for default value again because of the appinfo "mappings" node I_CmsXmlContentValue value = type.createValue(document, typeElement, locale); String defaultValue = document.getHandler().getDefault(cms, value, locale); if (defaultValue != null) { // only if there is a default value available use it to overwrite the initial default value.setStringValue(cms, defaultValue); } } } return root; } /** * Generates a valid XML document according to the XML schema of this content definition.

* * @param cms the current users OpenCms context * @param document the OpenCms XML document the XML is created for * @param locale the locale to create the default element in the document with * * @return a valid XML document according to the XML schema of this content definition */ public Document createDocument(CmsObject cms, I_CmsXmlDocument document, Locale locale) { Document doc = DocumentHelper.createDocument(); Element root = doc.addElement(getOuterName()); root.add(I_CmsXmlSchemaType.XSI_NAMESPACE); root.addAttribute(I_CmsXmlSchemaType.XSI_NAMESPACE_ATTRIBUTE_NO_SCHEMA_LOCATION, getSchemaLocation()); createLocale(cms, document, root, locale); return doc; } /** * Generates a valid locale (language) element for the XML schema of this content definition.

* * @param cms the current users OpenCms context * @param document the OpenCms XML document the XML is created for * @param root the root node of the document where to append the locale to * @param locale the locale to create the default element in the document with * * @return a valid XML element for the locale according to the XML schema of this content definition */ public Element createLocale(CmsObject cms, I_CmsXmlDocument document, Element root, Locale locale) { // add an element with a "locale" attribute to the given root node Element element = root.addElement(getInnerName()); element.addAttribute(XSD_ATTRIBUTE_VALUE_LANGUAGE, locale.toString()); // now generate the default XML for the element return createDefaultXml(cms, document, element, locale); } /** * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof CmsXmlContentDefinition)) { return false; } CmsXmlContentDefinition other = (CmsXmlContentDefinition)obj; if (!getInnerName().equals(other.getInnerName())) { return false; } if (!getOuterName().equals(other.getOuterName())) { return false; } return m_typeSequence.equals(other.m_typeSequence); } /** * Freezes this content definition, making all internal data structures * unmodifiable.

* * This is required to prevent modification of a cached content definition.

*/ public void freeze() { m_types = Collections.unmodifiableMap(m_types); m_typeSequence = Collections.unmodifiableList(m_typeSequence); } /** * Returns the maxOccurs value for the choice in case this is a xsd:choice content definition.

* * This content definition is a xsd:choice sequence if the returned value is larger then 0.

* * @return the maxOccurs value for the choice in case this is a xsd:choice content definition */ public int getChoiceMaxOccurs() { return m_choiceMaxOccurs; } /** * Returns the selected XML content handler for this XML content definition.

* * If no specific XML content handler was provided in the "appinfo" node of the * XML schema, the default XML content handler {@link CmsDefaultXmlContentHandler} is used.

* * @return the contentHandler */ public I_CmsXmlContentHandler getContentHandler() { return m_contentHandler; } /** * Returns the set of nested (included) XML content definitions.

* * @return the set of nested (included) XML content definitions */ public Set getIncludes() { return m_includes; } /** * Returns the inner element name of this content definition.

* * @return the inner element name of this content definition */ public String getInnerName() { return m_innerName; } /** * Returns the outer element name of this content definition.

* * @return the outer element name of this content definition */ public String getOuterName() { return m_outerName; } /** * Generates an XML schema for the content definition.

* * @return the generated XML schema */ public Document getSchema() { Document result; if (m_schemaDocument == null) { result = DocumentHelper.createDocument(); Element root = result.addElement(XSD_NODE_SCHEMA); root.addAttribute(XSD_ATTRIBUTE_ELEMENT_FORM_DEFAULT, XSD_ATTRIBUTE_VALUE_QUALIFIED); Element include = root.addElement(XSD_NODE_INCLUDE); include.addAttribute(XSD_ATTRIBUTE_SCHEMA_LOCATION, XSD_INCLUDE_OPENCMS); if (m_includes.size() > 0) { Iterator i = m_includes.iterator(); while (i.hasNext()) { CmsXmlContentDefinition definition = i.next(); root.addElement(XSD_NODE_INCLUDE).addAttribute( XSD_ATTRIBUTE_SCHEMA_LOCATION, definition.m_schemaLocation); } } String outerTypeName = createTypeName(getOuterName()); String innerTypeName = createTypeName(getInnerName()); Element content = root.addElement(XSD_NODE_ELEMENT); content.addAttribute(XSD_ATTRIBUTE_NAME, getOuterName()); content.addAttribute(XSD_ATTRIBUTE_TYPE, outerTypeName); Element list = root.addElement(XSD_NODE_COMPLEXTYPE); list.addAttribute(XSD_ATTRIBUTE_NAME, outerTypeName); Element listSequence = list.addElement(XSD_NODE_SEQUENCE); Element listElement = listSequence.addElement(XSD_NODE_ELEMENT); listElement.addAttribute(XSD_ATTRIBUTE_NAME, getInnerName()); listElement.addAttribute(XSD_ATTRIBUTE_TYPE, innerTypeName); listElement.addAttribute(XSD_ATTRIBUTE_MIN_OCCURS, XSD_ATTRIBUTE_VALUE_ZERO); listElement.addAttribute(XSD_ATTRIBUTE_MAX_OCCURS, XSD_ATTRIBUTE_VALUE_UNBOUNDED); Element main = root.addElement(XSD_NODE_COMPLEXTYPE); main.addAttribute(XSD_ATTRIBUTE_NAME, innerTypeName); Element mainSequence; if (m_sequenceType == SequenceType.SEQUENCE) { mainSequence = main.addElement(XSD_NODE_SEQUENCE); } else { mainSequence = main.addElement(XSD_NODE_CHOICE); if (getChoiceMaxOccurs() > 1) { mainSequence.addAttribute(XSD_ATTRIBUTE_MAX_OCCURS, String.valueOf(getChoiceMaxOccurs())); } else { mainSequence.addAttribute(XSD_ATTRIBUTE_MIN_OCCURS, XSD_ATTRIBUTE_VALUE_ZERO); mainSequence.addAttribute(XSD_ATTRIBUTE_MAX_OCCURS, XSD_ATTRIBUTE_VALUE_ONE); } } Iterator i = m_typeSequence.iterator(); while (i.hasNext()) { I_CmsXmlSchemaType schemaType = i.next(); schemaType.appendXmlSchema(mainSequence); } Element language = main.addElement(XSD_NODE_ATTRIBUTE); language.addAttribute(XSD_ATTRIBUTE_NAME, XSD_ATTRIBUTE_VALUE_LANGUAGE); language.addAttribute(XSD_ATTRIBUTE_TYPE, CmsXmlLocaleValue.TYPE_NAME); language.addAttribute(XSD_ATTRIBUTE_USE, XSD_ATTRIBUTE_VALUE_OPTIONAL); } else { result = (Document)m_schemaDocument.clone(); } return result; } /** * Returns the location from which the XML schema was read (XML system id).

* * @return the location from which the XML schema was read (XML system id) */ public String getSchemaLocation() { return m_schemaLocation; } /** * Returns the schema type for the given element name, or null if no * node is defined with this name.

* * @param elementPath the element xpath to look up the type for * @return the type for the given element name, or null if no * node is defined with this name */ public I_CmsXmlSchemaType getSchemaType(String elementPath) { String path = CmsXmlUtils.removeXpath(elementPath); I_CmsXmlSchemaType result = m_elementTypes.get(path); if (result == null) { result = getSchemaTypeRecusive(path); if (result != null) { m_elementTypes.put(path, result); } else { m_elementTypes.put(path, NULL_SCHEMA_TYPE); } } else if (result == NULL_SCHEMA_TYPE) { result = null; } return result; } /** * Returns the internal set of schema type names.

* * @return the internal set of schema type names */ public Set getSchemaTypes() { return m_types.keySet(); } /** * Returns the sequence type of this content definition.

* * @return the sequence type of this content definition */ public SequenceType getSequenceType() { return m_sequenceType; } /** * Returns the main type name of this XML content definition.

* * @return the main type name of this XML content definition */ public String getTypeName() { return m_typeName; } /** * Returns the type sequence, contains instances of {@link I_CmsXmlSchemaType}.

* * @return the type sequence, contains instances of {@link I_CmsXmlSchemaType} */ public List getTypeSequence() { return m_typeSequence; } /** * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return getInnerName().hashCode(); } /** * Sets the inner element name to use for the content definition.

* * @param innerName the inner element name to set */ protected void setInnerName(String innerName) { m_innerName = innerName; if (m_innerName != null) { m_typeName = createTypeName(innerName); } } /** * Sets the outer element name to use for the content definition.

* * @param outerName the outer element name to set */ protected void setOuterName(String outerName) { m_outerName = outerName; } /** * Calculates the schema type for the given element name by recursing into the schema structure.

* * @param elementPath the element xpath to look up the type for * @return the type for the given element name, or null if no * node is defined with this name */ private I_CmsXmlSchemaType getSchemaTypeRecusive(String elementPath) { String path = CmsXmlUtils.getFirstXpathElement(elementPath); I_CmsXmlSchemaType type = m_types.get(path); if (type == null) { // no node with the given path defined in schema return null; } // check if recursion is required to get value from a nested schema if (type.isSimpleType() || !CmsXmlUtils.isDeepXpath(elementPath)) { // no recursion required return type; } // recursion required since the path is an xpath and the type must be a nested content definition CmsXmlNestedContentDefinition nestedDefinition = (CmsXmlNestedContentDefinition)type; path = CmsXmlUtils.removeFirstXpathElement(elementPath); return nestedDefinition.getNestedContentDefinition().getSchemaType(path); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy