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