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.CmsFile; 031import org.opencms.file.CmsObject; 032import org.opencms.file.CmsResource; 033import org.opencms.i18n.CmsLocaleManager; 034import org.opencms.main.CmsIllegalArgumentException; 035import org.opencms.main.CmsRuntimeException; 036import org.opencms.main.OpenCms; 037import org.opencms.xml.types.CmsXmlCategoryValue; 038import org.opencms.xml.types.I_CmsXmlContentValue; 039import org.opencms.xml.types.I_CmsXmlSchemaType; 040 041import java.io.ByteArrayOutputStream; 042import java.io.OutputStream; 043import java.util.ArrayList; 044import java.util.Collections; 045import java.util.Comparator; 046import java.util.HashMap; 047import java.util.HashSet; 048import java.util.Iterator; 049import java.util.List; 050import java.util.Locale; 051import java.util.Map; 052import java.util.Set; 053 054import org.dom4j.Attribute; 055import org.dom4j.Document; 056import org.dom4j.Element; 057import org.xml.sax.EntityResolver; 058 059/** 060 * Provides basic XML document handling functions useful when dealing 061 * with XML documents that are stored in the OpenCms VFS.<p> 062 * 063 * @since 6.0.0 064 */ 065public abstract class A_CmsXmlDocument implements I_CmsXmlDocument { 066 067 /** The content conversion to use for this XML document. */ 068 protected String m_conversion; 069 070 /** The document object of the document. */ 071 protected Document m_document; 072 073 /** Maps element names to available locales. */ 074 protected Map<String, Set<Locale>> m_elementLocales; 075 076 /** Maps locales to available element names. */ 077 protected Map<Locale, Set<String>> m_elementNames; 078 079 /** The encoding to use for this XML document. */ 080 protected String m_encoding; 081 082 /** The file that contains the document data (note: is not set when creating an empty or document based document). */ 083 protected CmsFile m_file; 084 085 /** Set of locales contained in this document. */ 086 protected Set<Locale> m_locales; 087 088 /** Reference for named elements in the document. */ 089 private Map<String, I_CmsXmlContentValue> m_bookmarks; 090 091 /** 092 * Default constructor for a XML document 093 * that initializes some internal values.<p> 094 */ 095 protected A_CmsXmlDocument() { 096 097 m_bookmarks = new HashMap<String, I_CmsXmlContentValue>(); 098 m_locales = new HashSet<Locale>(); 099 } 100 101 /** 102 * Creates the bookmark name for a localized element to be used in the bookmark lookup table.<p> 103 * 104 * @param name the element name 105 * @param locale the element locale 106 * @return the bookmark name for a localized element 107 */ 108 protected static final String getBookmarkName(String name, Locale locale) { 109 110 StringBuffer result = new StringBuffer(64); 111 result.append('/'); 112 result.append(locale.toString()); 113 result.append('/'); 114 result.append(name); 115 return result.toString(); 116 } 117 118 /** 119 * @see org.opencms.xml.I_CmsXmlDocument#copyLocale(java.util.List, java.util.Locale) 120 */ 121 public void copyLocale(List<Locale> possibleSources, Locale destination) throws CmsXmlException { 122 123 if (hasLocale(destination)) { 124 throw new CmsXmlException(Messages.get().container(Messages.ERR_LOCALE_ALREADY_EXISTS_1, destination)); 125 } 126 Iterator<Locale> i = possibleSources.iterator(); 127 Locale source = null; 128 while (i.hasNext() && (source == null)) { 129 // check all locales and try to find the first match 130 Locale candidate = i.next(); 131 if (hasLocale(candidate)) { 132 // locale has been found 133 source = candidate; 134 } 135 } 136 if (source != null) { 137 // found a locale, copy this to the destination 138 copyLocale(source, destination); 139 } else { 140 // no matching locale has been found 141 throw new CmsXmlException( 142 Messages.get().container( 143 Messages.ERR_LOCALE_NOT_AVAILABLE_1, 144 CmsLocaleManager.getLocaleNames(possibleSources))); 145 } 146 } 147 148 /** 149 * @see org.opencms.xml.I_CmsXmlDocument#copyLocale(java.util.Locale, java.util.Locale) 150 */ 151 public void copyLocale(Locale source, Locale destination) throws CmsXmlException { 152 153 if (!hasLocale(source)) { 154 throw new CmsXmlException(Messages.get().container(Messages.ERR_LOCALE_NOT_AVAILABLE_1, source)); 155 } 156 if (hasLocale(destination)) { 157 throw new CmsXmlException(Messages.get().container(Messages.ERR_LOCALE_ALREADY_EXISTS_1, destination)); 158 } 159 160 Element sourceElement = null; 161 Element rootNode = m_document.getRootElement(); 162 Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(rootNode); 163 String localeStr = source.toString(); 164 while (i.hasNext()) { 165 Element element = i.next(); 166 String language = element.attributeValue(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE, null); 167 if ((language != null) && (localeStr.equals(language))) { 168 // detach node with the locale 169 sourceElement = element.createCopy(); 170 // there can be only one node for the locale 171 break; 172 } 173 } 174 175 if (sourceElement == null) { 176 // should not happen since this was checked already, just to make sure... 177 throw new CmsXmlException(Messages.get().container(Messages.ERR_LOCALE_NOT_AVAILABLE_1, source)); 178 } 179 180 // switch locale value in attribute of copied node 181 sourceElement.addAttribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE, destination.toString()); 182 // attach the copied node to the root node 183 rootNode.add(sourceElement); 184 185 // re-initialize the document bookmarks 186 initDocument(m_document, m_encoding, getContentDefinition()); 187 } 188 189 /** 190 * Corrects the structure of this XML document.<p> 191 * 192 * @param cms the current OpenCms user context 193 * 194 * @return the file that contains the corrected XML structure 195 * 196 * @throws CmsXmlException if something goes wrong 197 */ 198 public CmsFile correctXmlStructure(CmsObject cms) throws CmsXmlException { 199 200 // apply XSD schema translation 201 Attribute schema = m_document.getRootElement().attribute( 202 I_CmsXmlSchemaType.XSI_NAMESPACE_ATTRIBUTE_NO_SCHEMA_LOCATION); 203 if (schema != null) { 204 String schemaLocation = schema.getValue(); 205 String translatedSchema = OpenCms.getResourceManager().getXsdTranslator().translateResource(schemaLocation); 206 if (!schemaLocation.equals(translatedSchema)) { 207 schema.setValue(translatedSchema); 208 } 209 } 210 updateLocaleNodeSorting(); 211 212 // iterate over all locales 213 Iterator<Locale> i = m_locales.iterator(); 214 while (i.hasNext()) { 215 Locale locale = i.next(); 216 List<String> names = getNames(locale); 217 List<I_CmsXmlContentValue> validValues = new ArrayList<I_CmsXmlContentValue>(); 218 219 // iterate over all nodes per language 220 Iterator<String> j = names.iterator(); 221 while (j.hasNext()) { 222 223 // this step is required for values that need a processing of their content 224 // an example for this is the HTML value that does link replacement 225 String name = j.next(); 226 I_CmsXmlContentValue value = getValue(name, locale); 227 if (value.isSimpleType()) { 228 String content = value.getStringValue(cms); 229 value.setStringValue(cms, content); 230 } 231 232 // save valid elements for later check 233 validValues.add(value); 234 } 235 236 if (isAutoCorrectionEnabled()) { 237 // full correction of XML 238 if (validValues.size() < 1) { 239 // no valid element was in the content 240 if (hasLocale(locale)) { 241 // remove the old locale entirely, as there was no valid element 242 removeLocale(locale); 243 } 244 // add a new default locale, this will also generate the default XML as required 245 addLocale(cms, locale); 246 } else { 247 // there is at least one valid element in the content 248 249 List<Element> roots = new ArrayList<Element>(); 250 List<CmsXmlContentDefinition> rootCds = new ArrayList<CmsXmlContentDefinition>(); 251 List<Element> validElements = new ArrayList<Element>(); 252 253 // gather all XML content definitions and their parent nodes 254 Iterator<I_CmsXmlContentValue> it = validValues.iterator(); 255 while (it.hasNext()) { 256 // collect all root elements, also for the nested content definitions 257 I_CmsXmlContentValue value = it.next(); 258 Element element = value.getElement(); 259 validElements.add(element); 260 if (element.supportsParent()) { 261 // get the parent XML node 262 Element root = element.getParent(); 263 if ((root != null) && !roots.contains(root)) { 264 // this is a parent node we do not have already in our storage 265 CmsXmlContentDefinition rcd = value.getContentDefinition(); 266 if (rcd != null) { 267 // this value has a valid XML content definition 268 roots.add(root); 269 rootCds.add(rcd); 270 } else { 271 // no valid content definition for the XML value 272 throw new CmsXmlException( 273 Messages.get().container( 274 Messages.ERR_CORRECT_NO_CONTENT_DEF_3, 275 value.getName(), 276 value.getTypeName(), 277 value.getPath())); 278 } 279 } 280 } 281 } 282 283 for (int le = 0; le < roots.size(); le++) { 284 // iterate all XML content root nodes and correct each XML subtree 285 286 Element root = roots.get(le); 287 CmsXmlContentDefinition cd = rootCds.get(le); 288 289 // step 1: first sort the nodes according to the schema, this takes care of re-ordered elements 290 List<List<Element>> nodeLists = new ArrayList<List<Element>>(); 291 boolean isMultipleChoice = cd.getSequenceType() == CmsXmlContentDefinition.SequenceType.MULTIPLE_CHOICE; 292 293 // if it's a multiple choice element, the child elements must not be sorted into their types, 294 // but must keep their original order 295 if (isMultipleChoice) { 296 List<Element> nodeList = new ArrayList<Element>(); 297 List<Element> elements = CmsXmlGenericWrapper.elements(root); 298 Set<String> typeNames = cd.getSchemaTypes(); 299 for (Element element : elements) { 300 // check if the node type is still in the definition 301 if (typeNames.contains(element.getName())) { 302 nodeList.add(element); 303 } 304 } 305 checkMaxOccurs(nodeList, cd.getChoiceMaxOccurs(), cd.getTypeName()); 306 nodeLists.add(nodeList); 307 } 308 // if it's a sequence, the children are sorted according to the sequence type definition 309 else { 310 for (I_CmsXmlSchemaType type : cd.getTypeSequence()) { 311 List<Element> elements = CmsXmlGenericWrapper.elements(root, type.getName()); 312 checkMaxOccurs(elements, type.getMaxOccurs(), type.getTypeName()); 313 nodeLists.add(elements); 314 } 315 } 316 317 // step 2: clear the list of nodes (this will remove all invalid nodes) 318 List<Element> nodeList = CmsXmlGenericWrapper.elements(root); 319 nodeList.clear(); 320 Iterator<List<Element>> in = nodeLists.iterator(); 321 while (in.hasNext()) { 322 // now add all valid nodes in the right order 323 List<Element> elements = in.next(); 324 nodeList.addAll(elements); 325 } 326 327 // step 3: now append the missing elements according to the XML content definition 328 cd.addDefaultXml(cms, this, root, locale); 329 } 330 } 331 } 332 initDocument(); 333 } 334 335 // write the modified XML back to the VFS file 336 if (m_file != null) { 337 // make sure the file object is available 338 m_file.setContents(marshal()); 339 } 340 return m_file; 341 } 342 343 /** 344 * @see org.opencms.xml.I_CmsXmlDocument#getBestMatchingLocale(java.util.Locale) 345 */ 346 public Locale getBestMatchingLocale(Locale locale) { 347 348 // the requested locale is the match we want to find most 349 if (hasLocale(locale)) { 350 // check if the requested locale is directly available 351 return locale; 352 } 353 if (locale.getVariant().length() > 0) { 354 // locale has a variant like "en_EN_whatever", try only with language and country 355 Locale check = new Locale(locale.getLanguage(), locale.getCountry(), ""); 356 if (hasLocale(check)) { 357 return check; 358 } 359 } 360 if (locale.getCountry().length() > 0) { 361 // locale has a country like "en_EN", try only with language 362 Locale check = new Locale(locale.getLanguage(), "", ""); 363 if (hasLocale(check)) { 364 return check; 365 } 366 } 367 return null; 368 } 369 370 /** 371 * @see org.opencms.xml.I_CmsXmlDocument#getConversion() 372 */ 373 public String getConversion() { 374 375 return m_conversion; 376 } 377 378 /** 379 * @see org.opencms.xml.I_CmsXmlDocument#getEncoding() 380 */ 381 public String getEncoding() { 382 383 return m_encoding; 384 } 385 386 /** 387 * @see org.opencms.xml.I_CmsXmlDocument#getFile() 388 */ 389 public CmsFile getFile() { 390 391 return m_file; 392 } 393 394 /** 395 * @see org.opencms.xml.I_CmsXmlDocument#getIndexCount(java.lang.String, java.util.Locale) 396 */ 397 public int getIndexCount(String path, Locale locale) { 398 399 List<I_CmsXmlContentValue> elements = getValues(path, locale); 400 if (elements == null) { 401 return 0; 402 } else { 403 return elements.size(); 404 } 405 } 406 407 /** 408 * @see org.opencms.xml.I_CmsXmlDocument#getLocales() 409 */ 410 public List<Locale> getLocales() { 411 412 return new ArrayList<Locale>(m_locales); 413 } 414 415 /** 416 * Returns a List of all locales that have the named element set in this document.<p> 417 * 418 * If no locale for the given element name is available, an empty list is returned.<p> 419 * 420 * @param path the element to look up the locale List for 421 * @return a List of all Locales that have the named element set in this document 422 */ 423 public List<Locale> getLocales(String path) { 424 425 Set<Locale> locales = m_elementLocales.get(CmsXmlUtils.createXpath(path, 1)); 426 if (locales != null) { 427 return new ArrayList<Locale>(locales); 428 } 429 return Collections.emptyList(); 430 } 431 432 /** 433 * @see org.opencms.xml.I_CmsXmlDocument#getNames(java.util.Locale) 434 */ 435 public List<String> getNames(Locale locale) { 436 437 Set<String> names = m_elementNames.get(locale); 438 if (names != null) { 439 return new ArrayList<String>(names); 440 } 441 return Collections.emptyList(); 442 } 443 444 /** 445 * @see org.opencms.xml.I_CmsXmlDocument#getStringValue(org.opencms.file.CmsObject, java.lang.String, java.util.Locale) 446 */ 447 public String getStringValue(CmsObject cms, String path, Locale locale) { 448 449 I_CmsXmlContentValue value = getValueInternal(CmsXmlUtils.createXpath(path, 1), locale); 450 if (value != null) { 451 return value.getStringValue(cms); 452 } 453 return null; 454 } 455 456 /** 457 * @see org.opencms.xml.I_CmsXmlDocument#getStringValue(CmsObject, java.lang.String, Locale, int) 458 */ 459 public String getStringValue(CmsObject cms, String path, Locale locale, int index) { 460 461 // directly calling getValueInternal() is more efficient then calling getStringValue(CmsObject, String, Locale) 462 // since the most costs are generated in resolving the xpath name 463 I_CmsXmlContentValue value = getValueInternal(CmsXmlUtils.createXpath(path, index + 1), locale); 464 if (value != null) { 465 return value.getStringValue(cms); 466 } 467 return null; 468 } 469 470 /** 471 * @see org.opencms.xml.I_CmsXmlDocument#getSubValues(java.lang.String, java.util.Locale) 472 */ 473 public List<I_CmsXmlContentValue> getSubValues(String path, Locale locale) { 474 475 List<I_CmsXmlContentValue> result = new ArrayList<I_CmsXmlContentValue>(); 476 String bookmark = getBookmarkName(CmsXmlUtils.createXpath(path, 1), locale); 477 I_CmsXmlContentValue value = getBookmark(bookmark); 478 if ((value != null) && !value.isSimpleType()) { 479 // calculate level of current bookmark 480 int depth = CmsResource.getPathLevel(bookmark) + 1; 481 Iterator<String> i = getBookmarks().iterator(); 482 while (i.hasNext()) { 483 String bm = i.next(); 484 if (bm.startsWith(bookmark) && (CmsResource.getPathLevel(bm) == depth)) { 485 // add only values directly below the value 486 result.add(getBookmark(bm)); 487 } 488 } 489 } 490 return result; 491 } 492 493 /** 494 * @see org.opencms.xml.I_CmsXmlDocument#getValue(java.lang.String, java.util.Locale) 495 */ 496 public I_CmsXmlContentValue getValue(String path, Locale locale) { 497 498 return getValueInternal(CmsXmlUtils.createXpath(path, 1), locale); 499 } 500 501 /** 502 * @see org.opencms.xml.I_CmsXmlDocument#getValue(java.lang.String, java.util.Locale, int) 503 */ 504 public I_CmsXmlContentValue getValue(String path, Locale locale, int index) { 505 506 return getValueInternal(CmsXmlUtils.createXpath(path, index + 1), locale); 507 } 508 509 /** 510 * @see org.opencms.xml.I_CmsXmlDocument#getValues(java.util.Locale) 511 */ 512 public List<I_CmsXmlContentValue> getValues(Locale locale) { 513 514 List<I_CmsXmlContentValue> result = new ArrayList<I_CmsXmlContentValue>(); 515 516 // bookmarks are stored with the locale as first prefix 517 String prefix = '/' + locale.toString() + '/'; 518 519 // it's better for performance to iterate through the list of bookmarks directly 520 Iterator<Map.Entry<String, I_CmsXmlContentValue>> i = m_bookmarks.entrySet().iterator(); 521 while (i.hasNext()) { 522 Map.Entry<String, I_CmsXmlContentValue> entry = i.next(); 523 if (entry.getKey().startsWith(prefix)) { 524 result.add(entry.getValue()); 525 } 526 } 527 528 // sort the result 529 Collections.sort(result); 530 531 return result; 532 } 533 534 /** 535 * @see org.opencms.xml.I_CmsXmlDocument#getValues(java.lang.String, java.util.Locale) 536 */ 537 public List<I_CmsXmlContentValue> getValues(String path, Locale locale) { 538 539 List<I_CmsXmlContentValue> result = new ArrayList<I_CmsXmlContentValue>(); 540 String bookmark = getBookmarkName(CmsXmlUtils.createXpath(CmsXmlUtils.removeXpathIndex(path), 1), locale); 541 I_CmsXmlContentValue value = getBookmark(bookmark); 542 if (value != null) { 543 if (value.getContentDefinition().getChoiceMaxOccurs() > 1) { 544 // selected value belongs to a xsd:choice 545 String parent = CmsXmlUtils.removeLastXpathElement(bookmark); 546 int depth = CmsResource.getPathLevel(bookmark); 547 Iterator<String> i = getBookmarks().iterator(); 548 while (i.hasNext()) { 549 String bm = i.next(); 550 if (bm.startsWith(parent) && (CmsResource.getPathLevel(bm) == depth)) { 551 result.add(getBookmark(bm)); 552 } 553 } 554 } else { 555 // selected value belongs to a xsd:sequence 556 int index = 1; 557 String bm = CmsXmlUtils.removeXpathIndex(bookmark); 558 while (value != null) { 559 result.add(value); 560 index++; 561 String subpath = CmsXmlUtils.createXpathElement(bm, index); 562 value = getBookmark(subpath); 563 } 564 } 565 } 566 return result; 567 } 568 569 /** 570 * @see org.opencms.xml.I_CmsXmlDocument#hasLocale(java.util.Locale) 571 */ 572 public boolean hasLocale(Locale locale) { 573 574 if (locale == null) { 575 throw new CmsIllegalArgumentException(Messages.get().container(Messages.ERR_NULL_LOCALE_0)); 576 } 577 578 return m_locales.contains(locale); 579 } 580 581 /** 582 * @see org.opencms.xml.I_CmsXmlDocument#hasValue(java.lang.String, java.util.Locale) 583 */ 584 public boolean hasValue(String path, Locale locale) { 585 586 return null != getBookmark(CmsXmlUtils.createXpath(path, 1), locale); 587 } 588 589 /** 590 * @see org.opencms.xml.I_CmsXmlDocument#hasValue(java.lang.String, java.util.Locale, int) 591 */ 592 public boolean hasValue(String path, Locale locale, int index) { 593 594 return null != getBookmark(CmsXmlUtils.createXpath(path, index + 1), locale); 595 } 596 597 /** 598 * @see org.opencms.xml.I_CmsXmlDocument#initDocument() 599 */ 600 public void initDocument() { 601 602 initDocument(m_document, m_encoding, getContentDefinition()); 603 } 604 605 /** 606 * @see org.opencms.xml.I_CmsXmlDocument#isEnabled(java.lang.String, java.util.Locale) 607 */ 608 public boolean isEnabled(String path, Locale locale) { 609 610 return hasValue(path, locale); 611 } 612 613 /** 614 * @see org.opencms.xml.I_CmsXmlDocument#isEnabled(java.lang.String, java.util.Locale, int) 615 */ 616 public boolean isEnabled(String path, Locale locale, int index) { 617 618 return hasValue(path, locale, index); 619 } 620 621 /** 622 * Marshals (writes) the content of the current XML document 623 * into a byte array using the selected encoding.<p> 624 * 625 * @return the content of the current XML document written into a byte array 626 * @throws CmsXmlException if something goes wrong 627 */ 628 public byte[] marshal() throws CmsXmlException { 629 630 return ((ByteArrayOutputStream)marshal(new ByteArrayOutputStream(), m_encoding)).toByteArray(); 631 } 632 633 /** 634 * @see org.opencms.xml.I_CmsXmlDocument#moveLocale(java.util.Locale, java.util.Locale) 635 */ 636 public void moveLocale(Locale source, Locale destination) throws CmsXmlException { 637 638 copyLocale(source, destination); 639 removeLocale(source); 640 } 641 642 /** 643 * @see org.opencms.xml.I_CmsXmlDocument#removeLocale(java.util.Locale) 644 */ 645 public void removeLocale(Locale locale) throws CmsXmlException { 646 647 if (!hasLocale(locale)) { 648 throw new CmsXmlException(Messages.get().container(Messages.ERR_LOCALE_NOT_AVAILABLE_1, locale)); 649 } 650 651 Element rootNode = m_document.getRootElement(); 652 Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(rootNode); 653 String localeStr = locale.toString(); 654 while (i.hasNext()) { 655 Element element = i.next(); 656 String language = element.attributeValue(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE, null); 657 if ((language != null) && (localeStr.equals(language))) { 658 // detach node with the locale 659 element.detach(); 660 // there can be only one node for the locale 661 break; 662 } 663 } 664 665 // re-initialize the document bookmarks 666 initDocument(m_document, m_encoding, getContentDefinition()); 667 } 668 669 /** 670 * Sets the content conversion mode for this document.<p> 671 * 672 * @param conversion the conversion mode to set for this document 673 */ 674 public void setConversion(String conversion) { 675 676 m_conversion = conversion; 677 } 678 679 /** 680 * @see java.lang.Object#toString() 681 */ 682 @Override 683 public String toString() { 684 685 try { 686 return CmsXmlUtils.marshal(m_document, m_encoding); 687 } catch (CmsXmlException e) { 688 throw new CmsRuntimeException(Messages.get().container(Messages.ERR_WRITE_XML_DOC_TO_STRING_0), e); 689 } 690 } 691 692 /** 693 * Validates the XML structure of the document with the DTD or XML schema used by the document.<p> 694 * 695 * This is required in case someone modifies the XML structure of a 696 * document using the "edit control code" option.<p> 697 * 698 * @param resolver the XML entity resolver to use 699 * @throws CmsXmlException if the validation fails 700 */ 701 public void validateXmlStructure(EntityResolver resolver) throws CmsXmlException { 702 703 if (m_file != null) { 704 // file is set, use bytes from file directly 705 CmsXmlUtils.validateXmlStructure(m_file.getContents(), resolver); 706 } else { 707 // use XML document - note that this will be copied in a byte[] array first 708 CmsXmlUtils.validateXmlStructure(m_document, m_encoding, resolver); 709 } 710 } 711 712 /** 713 * Adds a bookmark for the given value.<p> 714 * 715 * @param path the lookup path to use for the bookmark 716 * @param locale the locale to use for the bookmark 717 * @param enabled if true, the value is enabled, if false it is disabled 718 * @param value the value to bookmark 719 */ 720 protected void addBookmark(String path, Locale locale, boolean enabled, I_CmsXmlContentValue value) { 721 722 // add the locale (since the locales are a set adding them more then once does not matter) 723 addLocale(locale); 724 725 // add a bookmark to the provided value 726 m_bookmarks.put(getBookmarkName(path, locale), value); 727 728 Set<Locale> sl; 729 // update mapping of element name to locale 730 if (enabled) { 731 // only include enabled elements 732 sl = m_elementLocales.get(path); 733 if (sl != null) { 734 sl.add(locale); 735 } else { 736 Set<Locale> set = new HashSet<Locale>(); 737 set.add(locale); 738 m_elementLocales.put(path, set); 739 } 740 } 741 // update mapping of locales to element names 742 Set<String> sn = m_elementNames.get(locale); 743 if (sn == null) { 744 sn = new HashSet<String>(); 745 m_elementNames.put(locale, sn); 746 } 747 sn.add(path); 748 } 749 750 /** 751 * Adds a locale to the set of locales of the XML document.<p> 752 * 753 * @param locale the locale to add 754 */ 755 protected void addLocale(Locale locale) { 756 757 // add the locale to all locales in this dcoument 758 m_locales.add(locale); 759 } 760 761 /** 762 * Clears the XML document bookmarks.<p> 763 */ 764 protected void clearBookmarks() { 765 766 m_bookmarks.clear(); 767 } 768 769 /** 770 * Creates a partial deep element copy according to the set of element paths.<p> 771 * Only elements contained in that set will be copied. 772 * 773 * @param element the element to copy 774 * @param copyElements the set of paths for elements to copy 775 * 776 * @return a partial deep copy of <code>element</code> 777 */ 778 protected Element createDeepElementCopy(Element element, Set<String> copyElements) { 779 780 return createDeepElementCopyInternal(null, null, element, copyElements); 781 } 782 783 /** 784 * Returns the bookmarked value for the given bookmark, 785 * which must be a valid bookmark name. 786 * 787 * Use {@link #getBookmarks()} to get the list of all valid bookmark names.<p> 788 * 789 * @param bookmark the bookmark name to look up 790 * @return the bookmarked value for the given bookmark 791 */ 792 protected I_CmsXmlContentValue getBookmark(String bookmark) { 793 794 return m_bookmarks.get(bookmark); 795 } 796 797 /** 798 * Returns the bookmarked value for the given name.<p> 799 * 800 * @param path the lookup path to use for the bookmark 801 * @param locale the locale to get the bookmark for 802 * @return the bookmarked value 803 */ 804 protected I_CmsXmlContentValue getBookmark(String path, Locale locale) { 805 806 return m_bookmarks.get(getBookmarkName(path, locale)); 807 } 808 809 /** 810 * Returns the names of all bookmarked elements.<p> 811 * 812 * @return the names of all bookmarked elements 813 */ 814 protected Set<String> getBookmarks() { 815 816 return m_bookmarks.keySet(); 817 } 818 819 /** 820 * Internal method to look up a value, requires that the name already has been 821 * "normalized" for the bookmark lookup. 822 * 823 * This is required to find names like "title/subtitle" which are stored 824 * internally as "title[0]/subtitle[0]" in the bookmarks. 825 * 826 * @param path the path to look up 827 * @param locale the locale to look up 828 * 829 * @return the value found in the bookmarks 830 */ 831 protected I_CmsXmlContentValue getValueInternal(String path, Locale locale) { 832 833 return getBookmark(path, locale); 834 } 835 836 /** 837 * Initializes an XML document based on the provided document, encoding and content definition.<p> 838 * 839 * @param document the base XML document to use for initializing 840 * @param encoding the encoding to use when marshalling the document later 841 * @param contentDefinition the content definition to use 842 */ 843 protected abstract void initDocument(Document document, String encoding, CmsXmlContentDefinition contentDefinition); 844 845 /** 846 * Returns <code>true</code> if the auto correction feature is enabled for saving this XML content.<p> 847 * 848 * @return <code>true</code> if the auto correction feature is enabled for saving this XML content 849 */ 850 protected boolean isAutoCorrectionEnabled() { 851 852 // by default, this method always returns false 853 return false; 854 } 855 856 /** 857 * Marshals (writes) the content of the current XML document 858 * into an output stream.<p> 859 * 860 * @param out the output stream to write to 861 * @param encoding the encoding to use 862 * @return the output stream with the XML content 863 * @throws CmsXmlException if something goes wrong 864 */ 865 protected OutputStream marshal(OutputStream out, String encoding) throws CmsXmlException { 866 867 return CmsXmlUtils.marshal(m_document, out, encoding); 868 } 869 870 /** 871 * Removes the bookmark for an element with the given name and locale.<p> 872 * 873 * @param path the lookup path to use for the bookmark 874 * @param locale the locale of the element 875 * @return the element removed from the bookmarks or null 876 */ 877 protected I_CmsXmlContentValue removeBookmark(String path, Locale locale) { 878 879 // remove mapping of element name to locale 880 Set<Locale> sl; 881 sl = m_elementLocales.get(path); 882 if (sl != null) { 883 sl.remove(locale); 884 } 885 // remove mapping of locale to element name 886 Set<String> sn = m_elementNames.get(locale); 887 if (sn != null) { 888 sn.remove(path); 889 } 890 // remove the bookmark and return the removed element 891 return m_bookmarks.remove(getBookmarkName(path, locale)); 892 } 893 894 /** 895 * Updates the order of the locale nodes if required.<p> 896 */ 897 protected void updateLocaleNodeSorting() { 898 899 // check if the locale nodes require sorting 900 List<Locale> locales = new ArrayList<Locale>(m_locales); 901 Collections.sort(locales, new Comparator<Locale>() { 902 903 public int compare(Locale o1, Locale o2) { 904 905 return o1.toString().compareTo(o2.toString()); 906 } 907 }); 908 List<Element> localeNodes = new ArrayList<Element>(m_document.getRootElement().elements()); 909 boolean sortRequired = false; 910 if (localeNodes.size() != locales.size()) { 911 sortRequired = true; 912 } else { 913 int i = 0; 914 for (Element el : localeNodes) { 915 if (!locales.get(i).toString().equals(el.attributeValue("language"))) { 916 sortRequired = true; 917 break; 918 } 919 i++; 920 } 921 } 922 923 if (sortRequired) { 924 // do the actual node sorting, by removing the nodes first 925 for (Element el : localeNodes) { 926 m_document.getRootElement().remove(el); 927 } 928 929 Collections.sort(localeNodes, new Comparator<Object>() { 930 931 public int compare(Object o1, Object o2) { 932 933 String locale1 = ((Element)o1).attributeValue("language"); 934 String locale2 = ((Element)o2).attributeValue("language"); 935 return locale1.compareTo(locale2); 936 } 937 }); 938 // re-adding the nodes in alphabetical order 939 for (Element el : localeNodes) { 940 m_document.getRootElement().add(el); 941 } 942 } 943 } 944 945 /** 946 * Removes all nodes that exceed newly defined maxOccurs rules from the list of elements.<p> 947 * 948 * @param elements the list of elements to check 949 * @param maxOccurs maximum number of elements allowed 950 * @param typeName name of the element type 951 */ 952 private void checkMaxOccurs(List<Element> elements, int maxOccurs, String typeName) { 953 954 if (elements.size() > maxOccurs) { 955 if (typeName.equals(CmsXmlCategoryValue.TYPE_NAME)) { 956 if (maxOccurs == 1) { 957 Element category = elements.get(0); 958 List<Element> categories = new ArrayList<Element>(); 959 for (Element value : elements) { 960 Iterator<Element> itLink = value.elementIterator(); 961 while (itLink.hasNext()) { 962 Element link = itLink.next(); 963 categories.add((Element)link.clone()); 964 } 965 } 966 category.clearContent(); 967 for (Element value : categories) { 968 category.add(value); 969 } 970 } 971 } 972 973 // too many nodes of this type appear according to the current schema definition 974 for (int lo = (elements.size() - 1); lo >= maxOccurs; lo--) { 975 elements.remove(lo); 976 } 977 } 978 } 979 980 /** 981 * Creates a partial deep element copy according to the set of element paths.<p> 982 * Only elements contained in that set will be copied. 983 * 984 * @param parentPath the path of the parent element or <code>null</code>, initially 985 * @param parent the parent element 986 * @param element the element to copy 987 * @param copyElements the set of paths for elements to copy 988 * 989 * @return a partial deep copy of <code>element</code> 990 */ 991 private Element createDeepElementCopyInternal( 992 String parentPath, 993 Element parent, 994 Element element, 995 Set<String> copyElements) { 996 997 String elName = element.getName(); 998 if (parentPath != null) { 999 Element first = element.getParent().element(elName); 1000 int elIndex = (element.getParent().indexOf(element) - first.getParent().indexOf(first)) + 1; 1001 elName = parentPath + (parentPath.length() > 0 ? "/" : "") + elName.concat("[" + elIndex + "]"); 1002 } 1003 1004 if ((parentPath == null) || copyElements.contains(elName)) { 1005 // this is a content element we want to copy 1006 Element copy = element.createCopy(); 1007 // copy.detach(); 1008 if (parentPath != null) { 1009 parent.add(copy); 1010 } 1011 1012 // check if we need to copy subelements, too 1013 boolean copyNested = (parentPath == null); 1014 for (Iterator<String> i = copyElements.iterator(); !copyNested && i.hasNext();) { 1015 String path = i.next(); 1016 copyNested = !elName.equals(path) && path.startsWith(elName); 1017 } 1018 1019 if (copyNested) { 1020 copy.clearContent(); 1021 for (Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(element); i.hasNext();) { 1022 Element el = i.next(); 1023 createDeepElementCopyInternal((parentPath == null) ? "" : elName, copy, el, copyElements); 1024 } 1025 } 1026 1027 return copy; 1028 } else { 1029 return null; 1030 } 1031 } 1032}