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