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