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