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