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