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