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