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