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.containerpage; 029 030import org.opencms.ade.configuration.CmsADEConfigData; 031import org.opencms.ade.configuration.CmsFormatterUtils; 032import org.opencms.ade.containerpage.CmsContainerpageService; 033import org.opencms.ade.containerpage.CmsModelGroupHelper; 034import org.opencms.ade.containerpage.CmsSettingTranslator; 035import org.opencms.ade.containerpage.shared.CmsContainerElement; 036import org.opencms.ade.containerpage.shared.CmsFormatterConfig; 037import org.opencms.file.CmsFile; 038import org.opencms.file.CmsObject; 039import org.opencms.file.CmsResource; 040import org.opencms.file.CmsResourceFilter; 041import org.opencms.i18n.CmsEncoder; 042import org.opencms.i18n.CmsLocaleManager; 043import org.opencms.main.CmsException; 044import org.opencms.main.CmsLog; 045import org.opencms.main.OpenCms; 046import org.opencms.relations.CmsLink; 047import org.opencms.relations.CmsRelationType; 048import org.opencms.util.CmsMacroResolver; 049import org.opencms.util.CmsUUID; 050import org.opencms.xml.CmsXmlContentDefinition; 051import org.opencms.xml.CmsXmlException; 052import org.opencms.xml.CmsXmlGenericWrapper; 053import org.opencms.xml.CmsXmlUtils; 054import org.opencms.xml.content.CmsXmlContent; 055import org.opencms.xml.content.CmsXmlContentMacroVisitor; 056import org.opencms.xml.content.CmsXmlContentProperty; 057import org.opencms.xml.content.CmsXmlContentPropertyHelper; 058import org.opencms.xml.page.CmsXmlPage; 059import org.opencms.xml.types.CmsXmlNestedContentDefinition; 060import org.opencms.xml.types.CmsXmlVfsFileValue; 061import org.opencms.xml.types.I_CmsXmlContentValue; 062import org.opencms.xml.types.I_CmsXmlSchemaType; 063 064import java.util.ArrayList; 065import java.util.Arrays; 066import java.util.Collections; 067import java.util.HashMap; 068import java.util.HashSet; 069import java.util.Iterator; 070import java.util.LinkedHashMap; 071import java.util.List; 072import java.util.Locale; 073import java.util.Map; 074import java.util.Set; 075import java.util.function.Function; 076 077import org.apache.commons.logging.Log; 078 079import org.dom4j.Document; 080import org.dom4j.Element; 081import org.xml.sax.EntityResolver; 082 083import com.google.common.collect.ArrayListMultimap; 084import com.google.common.collect.ComparisonChain; 085import com.google.common.collect.Multimap; 086import com.google.common.collect.Ordering; 087 088/** 089 * Implementation of a object used to access and manage the xml data of a container page.<p> 090 * 091 * In addition to the XML content interface. It also provides access to more comfortable beans. 092 * 093 * @since 7.5.2 094 * 095 * @see #getContainerPage(CmsObject) 096 */ 097public class CmsXmlContainerPage extends CmsXmlContent { 098 099 /** XML node name constants. */ 100 public enum XmlNode { 101 102 /** Container attribute node name. */ 103 Attribute, 104 /** Main node name. */ 105 Containers, 106 /** The create new element node name. */ 107 CreateNew, 108 109 /** Element instance id node name. */ 110 ElementInstanceId, 111 /** Container elements node name. */ 112 Elements, 113 /** Element formatter node name. */ 114 Formatter, 115 116 /** Formatter key node name.*/ 117 FormatterKey, 118 /** The is root container node name. */ 119 IsRootContainer, 120 /** Container attribute key node name. */ 121 Key, 122 /** Container name node name. */ 123 Name, 124 /** Parent element instance id node name. */ 125 ParentInstanceId, 126 /** Container type node name. */ 127 Type, 128 /** Element URI node name. */ 129 Uri, 130 /** Container attribute value node name. */ 131 Value; 132 } 133 134 /** Name for old internal setting names that are not used with the SYSTEM:: prefix in code. */ 135 public static final Set<String> LEGACY_SYSTEM_SETTING_NAMES = Collections.unmodifiableSet( 136 new HashSet<>( 137 Arrays.asList( 138 CmsContainerElement.USE_AS_COPY_MODEL, 139 CmsContainerElement.MODEL_GROUP_ID, 140 CmsContainerElement.MODEL_GROUP_STATE, 141 CmsContainerElement.USE_AS_COPY_MODEL, 142 CmsContainerpageService.SOURCE_CONTAINERPAGE_ID_SETTING))); 143 144 /** Prefix for system element settings. */ 145 public static final String SYSTEM_SETTING_PREFIX = "SYSTEM::"; 146 147 /** The log object for this class. */ 148 private static final Log LOG = CmsLog.getLog(CmsXmlContainerPage.class); 149 150 /** The container page objects. */ 151 private Map<Locale, CmsContainerPageBean> m_cntPages; 152 153 /** 154 * Hides the public constructor.<p> 155 */ 156 protected CmsXmlContainerPage() { 157 158 // noop 159 } 160 161 /** 162 * Creates a new container page based on the provided XML document.<p> 163 * 164 * The given encoding is used when marshalling the XML again later.<p> 165 * 166 * @param cms the cms context, if <code>null</code> no link validation is performed 167 * @param document the document to create the container page from 168 * @param encoding the encoding of the container page 169 * @param resolver the XML entity resolver to use 170 */ 171 protected CmsXmlContainerPage(CmsObject cms, Document document, String encoding, EntityResolver resolver) { 172 173 // must set document first to be able to get the content definition 174 m_document = document; 175 // for the next line to work the document must already be available 176 m_contentDefinition = getContentDefinition(resolver); 177 // initialize the XML content structure 178 initDocument(cms, m_document, encoding, m_contentDefinition); 179 } 180 181 /** 182 * Create a new container page based on the given default content, 183 * that will have all language nodes of the default content and ensures the presence of the given locale.<p> 184 * 185 * The given encoding is used when marshalling the XML again later.<p> 186 * 187 * @param cms the current users OpenCms content 188 * @param locale the locale to generate the default content for 189 * @param modelUri the absolute path to the container page file acting as model 190 * 191 * @throws CmsException in case the model file is not found or not valid 192 */ 193 protected CmsXmlContainerPage(CmsObject cms, Locale locale, String modelUri) 194 throws CmsException { 195 196 // init model from given modelUri 197 CmsFile modelFile = cms.readFile(modelUri, CmsResourceFilter.ONLY_VISIBLE_NO_DELETED); 198 CmsXmlContainerPage model = CmsXmlContainerPageFactory.unmarshal(cms, modelFile); 199 200 // initialize macro resolver to use on model file values 201 CmsMacroResolver macroResolver = CmsMacroResolver.newInstance().setCmsObject(cms); 202 203 // content definition must be set here since it's used during document creation 204 m_contentDefinition = model.getContentDefinition(); 205 // get the document from the default content 206 Document document = (Document)model.m_document.clone(); 207 // initialize the XML content structure 208 initDocument(cms, document, model.getEncoding(), m_contentDefinition); 209 // resolve eventual macros in the nodes 210 visitAllValuesWith(new CmsXmlContentMacroVisitor(cms, macroResolver)); 211 if (!hasLocale(locale)) { 212 // required locale not present, add it 213 try { 214 addLocale(cms, locale); 215 } catch (CmsXmlException e) { 216 // this can not happen since the locale does not exist 217 LOG.error(e.getMessage(), e); 218 } 219 } 220 } 221 222 /** 223 * Create a new container page based on the given content definition, 224 * that will have one language node for the given locale all initialized with default values.<p> 225 * 226 * The given encoding is used when marshalling the XML again later.<p> 227 * 228 * @param cms the current users OpenCms content 229 * @param locale the locale to generate the default content for 230 * @param encoding the encoding to use when marshalling the container page later 231 * @param contentDefinition the content definition to create the content for 232 */ 233 protected CmsXmlContainerPage( 234 CmsObject cms, 235 Locale locale, 236 String encoding, 237 CmsXmlContentDefinition contentDefinition) { 238 239 // content definition must be set here since it's used during document creation 240 m_contentDefinition = contentDefinition; 241 // create the XML document according to the content definition 242 Document document = m_contentDefinition.createDocument(cms, this, CmsLocaleManager.MASTER_LOCALE); 243 // initialize the XML content structure 244 initDocument(cms, document, encoding, m_contentDefinition); 245 } 246 247 /** 248 * Saves a container page bean to the in-memory XML structure and returns the changed content.<p> 249 * 250 * @param cms the current CMS context 251 * @param cntPage the container page bean 252 * @return the new content for the container page 253 * @throws CmsException if something goes wrong 254 */ 255 public byte[] createContainerPageXml(CmsObject cms, CmsContainerPageBean cntPage) throws CmsException { 256 257 // make sure all links are validated 258 writeContainerPage(cms, cntPage); 259 checkLinkConcistency(cms); 260 return marshal(); 261 262 } 263 264 /** 265 * Gets the container page content as a bean.<p> 266 * 267 * @param cms the current CMS context 268 * @return the bean containing the container page data 269 */ 270 public CmsContainerPageBean getContainerPage(CmsObject cms) { 271 272 Locale masterLocale = CmsLocaleManager.MASTER_LOCALE; 273 Locale localeToLoad = null; 274 // always use master locale if possible, otherwise use the first locale. 275 // this is important for 'legacy' container pages which were created before container pages became locale independent 276 if (m_cntPages.containsKey(masterLocale)) { 277 localeToLoad = masterLocale; 278 } else if (!m_cntPages.isEmpty()) { 279 localeToLoad = m_cntPages.keySet().iterator().next(); 280 } 281 if (localeToLoad == null) { 282 return null; 283 } else { 284 CmsContainerPageBean result = m_cntPages.get(localeToLoad); 285 return result; 286 } 287 } 288 289 /** 290 * Calls initDocument, but with a different CmsObject 291 * 292 * @param cms the CmsObject to use 293 */ 294 public void initDocument(CmsObject cms) { 295 296 initDocument(cms, m_document, m_encoding, getContentDefinition()); 297 } 298 299 /** 300 * @see org.opencms.xml.content.CmsXmlContent#isAutoCorrectionEnabled() 301 */ 302 @Override 303 public boolean isAutoCorrectionEnabled() { 304 305 return true; 306 } 307 308 /** 309 * Saves given container page in the current locale, and not only in memory but also to VFS.<p> 310 * 311 * @param cms the current cms context 312 * @param cntPage the container page to save 313 * 314 * @throws CmsException if something goes wrong 315 */ 316 public void save(CmsObject cms, CmsContainerPageBean cntPage) throws CmsException { 317 318 save(cms, cntPage, false); 319 } 320 321 /** 322 * Saves given container page in the current locale, and not only in memory but also to VFS.<p> 323 * 324 * @param cms the current cms context 325 * @param cntPage the container page to save 326 * @param ifChangedOnly <code>true</code> to only write the file if the content has changed 327 * 328 * @throws CmsException if something goes wrong 329 */ 330 public void save(CmsObject cms, CmsContainerPageBean cntPage, boolean ifChangedOnly) throws CmsException { 331 332 CmsFile file = getFile(); 333 byte[] data = createContainerPageXml(cms, cntPage); 334 if (ifChangedOnly && Arrays.equals(file.getContents(), data)) { 335 return; 336 } 337 // lock the file 338 cms.lockResourceTemporary(file); 339 file.setContents(data); 340 cms.writeFile(file); 341 } 342 343 /** 344 * Saves a container page in in-memory XML structure.<p> 345 * 346 * @param cms the current CMS context 347 * @param cntPage the container page bean to save 348 * 349 * @throws CmsException if something goes wrong 350 */ 351 public void writeContainerPage(CmsObject cms, CmsContainerPageBean cntPage) throws CmsException { 352 353 // keep unused containers 354 CmsContainerPageBean savePage = cleanupContainersContainers(cms, cntPage); 355 savePage = removeEmptyContainers(cntPage); 356 // Replace existing locales with master locale 357 for (Locale locale : getLocales()) { 358 removeLocale(locale); 359 } 360 Locale masterLocale = CmsLocaleManager.MASTER_LOCALE; 361 addLocale(cms, masterLocale); 362 363 // add the nodes to the raw XML structure 364 Element parent = getLocaleNode(masterLocale); 365 saveContainerPage(cms, parent, savePage); 366 initDocument(m_document, m_encoding, m_contentDefinition); 367 } 368 369 /** 370 * Checks the link consistency for a given locale and reinitializes the document afterwards.<p> 371 * 372 * @param cms the cms context 373 */ 374 protected void checkLinkConcistency(CmsObject cms) { 375 376 Locale masterLocale = CmsLocaleManager.MASTER_LOCALE; 377 378 for (I_CmsXmlContentValue contentValue : getValues(masterLocale)) { 379 if (contentValue instanceof CmsXmlVfsFileValue) { 380 CmsLink link = ((CmsXmlVfsFileValue)contentValue).getLink(cms); 381 link.checkConsistency(cms); 382 } 383 } 384 initDocument(); 385 } 386 387 /** 388 * Removes all empty containers and merges the containers of the current document that are not used in the given container page with it.<p> 389 * 390 * @param cms the current CMS context 391 * @param cntPage the container page to merge 392 * 393 * @return a new container page with the additional unused containers 394 */ 395 protected CmsContainerPageBean cleanupContainersContainers(CmsObject cms, CmsContainerPageBean cntPage) { 396 397 CmsADEConfigData config = OpenCms.getADEManager().lookupConfiguration(cms, getFile().getRootPath()); 398 // get the used containers first 399 Map<String, CmsContainerBean> currentContainers = cntPage.getContainers(); 400 List<CmsContainerBean> containers = new ArrayList<CmsContainerBean>(); 401 for (String cntName : cntPage.getNames()) { 402 CmsContainerBean container = currentContainers.get(cntName); 403 if (!container.getElements().isEmpty()) { 404 containers.add(container); 405 } 406 } 407 408 // now get the unused containers 409 CmsContainerPageBean currentContainerPage = getContainerPage(cms); 410 if (currentContainerPage != null) { 411 for (String cntName : currentContainerPage.getNames()) { 412 if (!currentContainers.containsKey(cntName)) { 413 CmsContainerBean container = currentContainerPage.getContainers().get(cntName); 414 if (!container.getElements().isEmpty()) { 415 containers.add(container); 416 } 417 } 418 } 419 } 420 421 // check if any nested containers have lost their parent element 422 423 // first collect all present elements 424 Map<String, CmsContainerElementBean> pageElements = new HashMap<String, CmsContainerElementBean>(); 425 Map<String, String> parentContainers = new HashMap<String, String>(); 426 for (CmsContainerBean container : containers) { 427 for (CmsContainerElementBean element : container.getElements()) { 428 try { 429 element.initResource(cms); 430 431 if (!CmsModelGroupHelper.isModelGroupResource(element.getResource())) { 432 pageElements.put(element.getInstanceId(), element); 433 parentContainers.put(element.getInstanceId(), container.getName()); 434 } 435 } catch (CmsException e) { 436 LOG.warn(e.getLocalizedMessage(), e); 437 } 438 } 439 } 440 Iterator<CmsContainerBean> cntIt = containers.iterator(); 441 while (cntIt.hasNext()) { 442 CmsContainerBean container = cntIt.next(); 443 // check all unused nested containers if their parent element is still part of the page 444 if (!currentContainers.containsKey(container.getName()) 445 && (container.isNestedContainer() && !container.isRootContainer())) { 446 boolean remove = !pageElements.containsKey(container.getParentInstanceId()) 447 || container.getElements().isEmpty(); 448 if (!remove) { 449 // check if the parent element formatter is set to strictly render all nested containers 450 CmsContainerElementBean element = pageElements.get(container.getParentInstanceId()); 451 String settingsKey = CmsFormatterConfig.getSettingsKeyForContainer( 452 parentContainers.get(element.getInstanceId())); 453 String formatterId = element.getIndividualSettings().get(settingsKey); 454 I_CmsFormatterBean bean = config.findFormatter(formatterId); 455 if (bean != null) { 456 remove = (bean instanceof CmsFormatterBean) && ((CmsFormatterBean)bean).isStrictContainers(); 457 } 458 } 459 if (remove) { 460 // remove the sub elements from the page list 461 for (CmsContainerElementBean element : container.getElements()) { 462 pageElements.remove(element.getInstanceId()); 463 } 464 // remove the container 465 cntIt.remove(); 466 } 467 } 468 } 469 470 return new CmsContainerPageBean(containers); 471 } 472 473 /** 474 * Fills a {@link CmsXmlVfsFileValue} with the resource identified by the given id.<p> 475 * 476 * @param cms the current CMS context 477 * @param element the XML element to fill 478 * @param resourceId the ID identifying the resource to use 479 * 480 * @return the resource 481 * 482 * @throws CmsException if the resource can not be read 483 */ 484 protected CmsResource fillResource(CmsObject cms, Element element, CmsUUID resourceId) throws CmsException { 485 486 String xpath = element.getPath(); 487 int pos = xpath.lastIndexOf("/" + XmlNode.Containers.name() + "/"); 488 if (pos > 0) { 489 xpath = xpath.substring(pos + 1); 490 } 491 CmsRelationType type = getHandler().getRelationType(xpath); 492 CmsResource res = cms.readResource(resourceId, CmsResourceFilter.IGNORE_EXPIRATION); 493 CmsXmlVfsFileValue.fillEntry(element, res.getStructureId(), res.getRootPath(), type); 494 return res; 495 } 496 497 /** 498 * @see org.opencms.xml.content.CmsXmlContent#initDocument(org.opencms.file.CmsObject, org.dom4j.Document, java.lang.String, org.opencms.xml.CmsXmlContentDefinition) 499 */ 500 @Override 501 protected void initDocument(CmsObject cms, Document document, String encoding, CmsXmlContentDefinition definition) { 502 503 m_document = document; 504 m_contentDefinition = definition; 505 m_encoding = CmsEncoder.lookupEncoding(encoding, encoding); 506 m_elementLocales = new HashMap<String, Set<Locale>>(); 507 m_elementNames = new HashMap<Locale, Set<String>>(); 508 m_locales = new HashSet<Locale>(); 509 m_cntPages = new LinkedHashMap<Locale, CmsContainerPageBean>(); 510 clearBookmarks(); 511 CmsADEConfigData config = null; 512 CmsSettingTranslator settingTranslator = null; 513 if ((getFile() != null) && (cms != null)) { 514 config = OpenCms.getADEManager().lookupConfiguration(cms, getFile().getRootPath()); 515 settingTranslator = new CmsSettingTranslator(config); 516 } 517 518 // initialize the bookmarks 519 for (Iterator<Element> itCntPages = CmsXmlGenericWrapper.elementIterator( 520 m_document.getRootElement()); itCntPages.hasNext();) { 521 Element cntPage = itCntPages.next(); 522 523 try { 524 Locale locale = CmsLocaleManager.getLocale( 525 cntPage.attribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE).getValue()); 526 527 addLocale(locale); 528 529 List<CmsContainerBean> containers = new ArrayList<CmsContainerBean>(); 530 for (Iterator<Element> itCnts = CmsXmlGenericWrapper.elementIterator( 531 cntPage, 532 XmlNode.Containers.name()); itCnts.hasNext();) { 533 Element container = itCnts.next(); 534 535 // container itself 536 int cntIndex = CmsXmlUtils.getXpathIndexInt(container.getUniquePath(cntPage)); 537 String cntPath = CmsXmlUtils.createXpathElement(container.getName(), cntIndex); 538 I_CmsXmlSchemaType cntSchemaType = definition.getSchemaType(container.getName()); 539 I_CmsXmlContentValue cntValue = cntSchemaType.createValue(this, container, locale); 540 addBookmark(cntPath, locale, true, cntValue); 541 CmsXmlContentDefinition cntDef = ((CmsXmlNestedContentDefinition)cntSchemaType).getNestedContentDefinition(); 542 543 // name 544 Element name = container.element(XmlNode.Name.name()); 545 String containerName = name.getText(); 546 addBookmarkForElement(name, locale, container, cntPath, cntDef); 547 548 // type 549 Element type = container.element(XmlNode.Type.name()); 550 addBookmarkForElement(type, locale, container, cntPath, cntDef); 551 552 // parent instance id 553 Element parentInstance = container.element(XmlNode.ParentInstanceId.name()); 554 if (parentInstance != null) { 555 addBookmarkForElement(parentInstance, locale, container, cntPath, cntDef); 556 } 557 558 Element isRootContainer = container.element(XmlNode.IsRootContainer.name()); 559 if (isRootContainer != null) { 560 addBookmarkForElement(isRootContainer, locale, container, cntPath, cntDef); 561 } 562 563 List<CmsContainerElementBean> elements = new ArrayList<CmsContainerElementBean>(); 564 // Elements 565 for (Iterator<Element> itElems = CmsXmlGenericWrapper.elementIterator( 566 container, 567 XmlNode.Elements.name()); itElems.hasNext();) { 568 Element element = itElems.next(); 569 570 // element itself 571 int elemIndex = CmsXmlUtils.getXpathIndexInt(element.getUniquePath(container)); 572 String elemPath = CmsXmlUtils.concatXpath( 573 cntPath, 574 CmsXmlUtils.createXpathElement(element.getName(), elemIndex)); 575 I_CmsXmlSchemaType elemSchemaType = cntDef.getSchemaType(element.getName()); 576 I_CmsXmlContentValue elemValue = elemSchemaType.createValue(this, element, locale); 577 addBookmark(elemPath, locale, true, elemValue); 578 CmsXmlContentDefinition elemDef = ((CmsXmlNestedContentDefinition)elemSchemaType).getNestedContentDefinition(); 579 580 Element instanceIdElem = element.element(XmlNode.ElementInstanceId.name()); 581 String elementInstanceId = null; 582 if (instanceIdElem != null) { 583 elementInstanceId = instanceIdElem.getTextTrim(); 584 } 585 586 Element formatterKeyElem = element.element(XmlNode.FormatterKey.name()); 587 String formatterKey = null; 588 if (formatterKeyElem != null) { 589 formatterKey = formatterKeyElem.getTextTrim(); 590 } 591 592 // uri 593 Element uri = element.element(XmlNode.Uri.name()); 594 CmsUUID elementId = null; 595 if (uri != null) { 596 addBookmarkForElement(uri, locale, element, elemPath, elemDef); 597 Element uriLink = uri.element(CmsXmlPage.NODE_LINK); 598 if (uriLink == null) { 599 // this can happen when adding the elements node to the xml content 600 // it is not dangerous since the link has to be set before saving 601 } else { 602 CmsLink link = new CmsLink(uriLink); 603 if (cms != null) { 604 link.checkConsistency(cms); 605 } 606 elementId = link.getStructureId(); 607 } 608 } 609 // uri may be null for dynamic functions, try find the element id from the settings later 610 611 Element createNewElement = element.element(XmlNode.CreateNew.name()); 612 boolean createNew = (createNewElement != null) 613 && Boolean.parseBoolean(createNewElement.getStringValue()); 614 615 // formatter 616 Element formatter = element.element(XmlNode.Formatter.name()); 617 CmsUUID formatterId = null; 618 if (formatter != null) { 619 addBookmarkForElement(formatter, locale, element, elemPath, elemDef); 620 Element formatterLink = formatter.element(CmsXmlPage.NODE_LINK); 621 622 if (formatterLink == null) { 623 // this can happen when adding the elements node to the xml content 624 // it is not dangerous since the link has to be set before saving 625 } else { 626 CmsLink link = new CmsLink(formatterLink); 627 if (cms != null) { 628 link.checkConsistency(cms); 629 } 630 formatterId = link.getStructureId(); 631 } 632 } 633 634 // the properties 635 Map<String, String> propertiesMap = CmsXmlContentPropertyHelper.readProperties( 636 this, 637 locale, 638 element, 639 elemPath, 640 elemDef); 641 propertiesMap = translateMapKeys(propertiesMap, this::translateSettingNameForLoad); 642 if ((config != null) && (getFile() != null)) { 643 propertiesMap = fixNestedFormatterSettings(cms, config, propertiesMap); 644 } 645 if (formatterKey != null) { 646 propertiesMap.put(CmsFormatterConfig.FORMATTER_SETTINGS_KEY + containerName, formatterKey); 647 } 648 649 I_CmsFormatterBean dynamicFormatter = null; 650 if (config != null) { 651 // make sure alias keys are replaced with main keys in the settings 652 String key1 = CmsFormatterConfig.FORMATTER_SETTINGS_KEY + containerName; 653 String key2 = CmsFormatterConfig.FORMATTER_SETTINGS_KEY; 654 for (String key : new String[] {key1, key2}) { 655 String value = propertiesMap.get(key); 656 if (value != null) { 657 I_CmsFormatterBean temp = config.findFormatter(value); 658 if (temp != null) { 659 dynamicFormatter = temp; 660 propertiesMap.put(key, dynamicFormatter.getKeyOrId()); 661 break; 662 } 663 } 664 } 665 } 666 if ((config != null) && (dynamicFormatter != null) && (settingTranslator != null)) { 667 propertiesMap = settingTranslator.translateSettings(dynamicFormatter, propertiesMap); 668 } 669 670 if (elementInstanceId != null) { 671 propertiesMap.put(CmsContainerElement.ELEMENT_INSTANCE_ID, elementInstanceId); 672 } 673 674 CmsUUID pageId; 675 if (getFile() != null) { 676 pageId = getFile().getStructureId(); 677 } else { 678 pageId = CmsUUID.getNullUUID(); 679 } 680 propertiesMap.put(CmsContainerElement.SETTING_PAGE_ID, "" + pageId); 681 682 boolean createNewFromSetting = Boolean.parseBoolean( 683 propertiesMap.remove(CmsContainerElement.SETTING_CREATE_NEW)); 684 createNew |= createNewFromSetting; 685 686 if (config != null) { 687 // in the new container page format, new dynamic functions are not stored with their URIs in the page 688 String key = CmsFormatterUtils.getFormatterKey(containerName, propertiesMap); 689 I_CmsFormatterBean maybeFunction = config.findFormatter(key); 690 if (maybeFunction instanceof CmsFunctionFormatterBean) { 691 elementId = new CmsUUID(maybeFunction.getId()); 692 } 693 } 694 695 if (elementId != null) { 696 elements.add(new CmsContainerElementBean(elementId, formatterId, propertiesMap, createNew)); 697 } 698 } 699 CmsContainerBean newContainerBean = new CmsContainerBean( 700 name.getText(), 701 type.getText(), 702 parentInstance != null ? parentInstance.getText() : null, 703 (isRootContainer != null) && Boolean.valueOf(isRootContainer.getText()).booleanValue(), 704 elements); 705 containers.add(newContainerBean); 706 } 707 708 m_cntPages.put(locale, new CmsContainerPageBean(containers)); 709 } catch (NullPointerException e) { 710 LOG.error( 711 org.opencms.xml.content.Messages.get().getBundle().key( 712 org.opencms.xml.content.Messages.LOG_XMLCONTENT_INIT_BOOKMARKS_0), 713 e); 714 } 715 } 716 717 if (cms != null) { 718 // this will remove all invalid links 719 getHandler().invalidateBrokenLinks(cms, this); 720 } 721 } 722 723 /** 724 * @see org.opencms.xml.A_CmsXmlDocument#initDocument(org.dom4j.Document, java.lang.String, org.opencms.xml.CmsXmlContentDefinition) 725 */ 726 @Override 727 protected void initDocument(Document document, String encoding, CmsXmlContentDefinition definition) { 728 729 initDocument(null, document, encoding, definition); 730 } 731 732 /** 733 * Removes all empty containers to clean up container page XML.<p> 734 * 735 * @param cntPage the container page bean 736 * 737 * @return the newly generated result 738 */ 739 protected CmsContainerPageBean removeEmptyContainers(CmsContainerPageBean cntPage) { 740 741 List<CmsContainerBean> containers = new ArrayList<CmsContainerBean>(); 742 for (CmsContainerBean container : cntPage.getContainers().values()) { 743 if (container.getElements().size() > 0) { 744 containers.add(container); 745 } 746 } 747 return new CmsContainerPageBean(containers); 748 } 749 750 /** 751 * Adds the given container page to the given element.<p> 752 * 753 * @param cms the current CMS object 754 * @param parent the element to add it 755 * @param cntPage the container page to add 756 * 757 * @throws CmsException if something goes wrong 758 */ 759 protected void saveContainerPage(CmsObject cms, Element parent, CmsContainerPageBean cntPage) throws CmsException { 760 761 parent.clearContent(); 762 763 CmsADEConfigData adeConfig = OpenCms.getADEManager().lookupConfiguration(cms, getFile().getRootPath()); 764 if (adeConfig.isUseFormatterKeys()) { 765 saveContainerPageV2(cms, parent, cntPage, adeConfig); 766 } else { 767 saveContainerPageV1(cms, parent, cntPage, adeConfig); 768 } 769 } 770 771 /** 772 * @see org.opencms.xml.content.CmsXmlContent#setFile(org.opencms.file.CmsFile) 773 */ 774 @Override 775 protected void setFile(CmsFile file) { 776 777 // just for visibility from the factory 778 super.setFile(file); 779 } 780 781 /** 782 * Replaces formatter id prefixes for nested settings with corresponding formatter keys, if possible.<p> 783 * 784 * Also handles replacement of alias keys with main keys in nested settings. 785 * 786 * @param cms the CMS Context 787 * @param config the sitemap configuration 788 *@param propertiesMap the map of setting s 789 * @return the modified settings 790 */ 791 private Map<String, String> fixNestedFormatterSettings( 792 CmsObject cms, 793 CmsADEConfigData config, 794 Map<String, String> propertiesMap) { 795 796 Map<String, String> result = new HashMap<>(); 797 for (Map.Entry<String, String> entry : propertiesMap.entrySet()) { 798 String key = entry.getKey(); 799 800 // replace structure ids, fallback keys or alias keys with the main key if possible 801 802 int underscorePos = key.indexOf("_"); 803 if (underscorePos >= 0) { 804 String prefix = key.substring(0, underscorePos); 805 I_CmsFormatterBean formatter = config.findFormatter(prefix, /* noWarn = */true); 806 if (formatter != null) { 807 key = formatter.getKeyOrId() + key.substring(underscorePos); 808 } 809 } 810 811 result.put(key, entry.getValue()); 812 } 813 return result; 814 } 815 816 /** 817 * Do some processing for the element settings before saving them. 818 * 819 * @param config the ADE configuration 820 * @param settings the element settings 821 * @return the modified element settings 822 */ 823 private Map<String, String> processSettingsForSaveV1(CmsADEConfigData config, Map<String, String> settings) { 824 825 Map<String, String> result = new LinkedHashMap<>(); 826 for (Map.Entry<String, String> entry : settings.entrySet()) { 827 String key = entry.getKey(); 828 String value = entry.getValue(); 829 if (key.startsWith(CmsFormatterConfig.FORMATTER_SETTINGS_KEY)) { 830 if (!CmsUUID.isValidUUID(value)) { 831 I_CmsFormatterBean dynamicFmt = config.findFormatter(value); 832 if ((dynamicFmt != null) && (dynamicFmt.getId() != null)) { 833 value = dynamicFmt.getId(); 834 } 835 } 836 } else { 837 // nested formatters 838 int underscorePos = key.indexOf("_"); 839 if (underscorePos != -1) { 840 String partBeforeUnderscore = key.substring(0, underscorePos); 841 String partAfterUnderscore = key.substring(underscorePos + 1); 842 I_CmsFormatterBean dynamicFmt = config.findFormatter(partBeforeUnderscore); 843 if ((dynamicFmt != null) && dynamicFmt.getSettings(config).containsKey(partAfterUnderscore)) { 844 String id = dynamicFmt.getId(); 845 if (id != null) { 846 key = id + "_" + partAfterUnderscore; 847 } 848 } 849 } 850 } 851 result.put(key, value); 852 } 853 result.remove(CmsContainerElement.SETTING_PAGE_ID); 854 return result; 855 } 856 857 /** 858 * Do some processing for the element settings before saving them. 859 * 860 * @param config the ADE configuration 861 * @param settings the element settings 862 * @return the modified element settings 863 */ 864 private Map<String, String> processSettingsForSaveV2(CmsADEConfigData config, Map<String, String> settings) { 865 866 Map<String, String> result = new LinkedHashMap<>(); 867 868 for (Map.Entry<String, String> entry : settings.entrySet()) { 869 String key = entry.getKey(); 870 String value = entry.getValue(); 871 if (key.startsWith(CmsFormatterConfig.FORMATTER_SETTINGS_KEY)) { 872 if (CmsUUID.isValidUUID(value)) { 873 I_CmsFormatterBean dynamicFmt = config.findFormatter(value); 874 if ((dynamicFmt != null) && (dynamicFmt.getKey() != null)) { 875 value = dynamicFmt.getKey(); 876 } 877 } 878 } 879 result.put(key, value); 880 } 881 result.remove(CmsContainerElement.SETTING_PAGE_ID); 882 result = sortSettingsForSave(translateMapKeys(result, this::translateSettingNameForSave)); 883 return result; 884 } 885 886 /** 887 * Adds the given container page to the given element.<p> 888 * 889 * @param cms the current CMS object 890 * @param parent the element to add it 891 * @param cntPage the container page to add 892 * @param adeConfig the current sitemap configuration 893 * 894 * @throws CmsException if something goes wrong 895 */ 896 private void saveContainerPageV1( 897 CmsObject cms, 898 Element parent, 899 CmsContainerPageBean cntPage, 900 CmsADEConfigData adeConfig) 901 throws CmsException { 902 903 // save containers in a defined order 904 List<String> containerNames = new ArrayList<String>(cntPage.getNames()); 905 Collections.sort(containerNames); 906 907 for (String containerName : containerNames) { 908 CmsContainerBean container = cntPage.getContainers().get(containerName); 909 910 // the container 911 Element cntElement = parent.addElement(XmlNode.Containers.name()); 912 cntElement.addElement(XmlNode.Name.name()).addCDATA(container.getName()); 913 cntElement.addElement(XmlNode.Type.name()).addCDATA(container.getType()); 914 if (container.isNestedContainer()) { 915 cntElement.addElement(XmlNode.ParentInstanceId.name()).addCDATA(container.getParentInstanceId()); 916 } 917 if (container.isRootContainer()) { 918 cntElement.addElement(XmlNode.IsRootContainer.name()).addText(Boolean.TRUE.toString()); 919 } 920 921 // the elements 922 for (CmsContainerElementBean element : container.getElements()) { 923 Element elemElement = cntElement.addElement(XmlNode.Elements.name()); 924 925 // the element 926 Element uriElem = elemElement.addElement(XmlNode.Uri.name()); 927 CmsResource uriRes = fillResource(cms, uriElem, element.getId()); 928 if (element.getFormatterId() != null) { 929 Element formatterElem = elemElement.addElement(XmlNode.Formatter.name()); 930 fillResource(cms, formatterElem, element.getFormatterId()); 931 } 932 if (element.isCreateNew()) { 933 Element createNewElem = elemElement.addElement(XmlNode.CreateNew.name()); 934 createNewElem.addText(Boolean.TRUE.toString()); 935 } 936 // the properties 937 Map<String, String> properties = element.getIndividualSettings(); 938 Map<String, String> processedSettings = processSettingsForSaveV1(adeConfig, properties); 939 Map<String, CmsXmlContentProperty> propertiesConf = OpenCms.getADEManager().getElementSettings( 940 cms, 941 uriRes); 942 943 CmsXmlContentPropertyHelper.saveProperties(cms, elemElement, processedSettings, propertiesConf, true); 944 } 945 } 946 } 947 948 /** 949 * Adds the given container page to the given element.<p> 950 * 951 * @param cms the current CMS object 952 * @param parent the element to add it 953 * @param cntPage the container page to add 954 * @param adeConfig the current sitemap configuration 955 * 956 * @throws CmsException if something goes wrong 957 */ 958 private void saveContainerPageV2( 959 CmsObject cms, 960 Element parent, 961 CmsContainerPageBean cntPage, 962 CmsADEConfigData adeConfig) 963 throws CmsException { 964 965 // save containers in a defined order 966 List<String> containerNames = sortContainerNames(cntPage); 967 968 for (String containerName : containerNames) { 969 CmsContainerBean container = cntPage.getContainers().get(containerName); 970 971 // the container 972 Element cntElement = parent.addElement(XmlNode.Containers.name()); 973 cntElement.addElement(XmlNode.Name.name()).addCDATA(container.getName()); 974 cntElement.addElement(XmlNode.Type.name()).addCDATA(container.getType()); 975 if (container.isNestedContainer()) { 976 cntElement.addElement(XmlNode.ParentInstanceId.name()).addCDATA(container.getParentInstanceId()); 977 } 978 if (container.isRootContainer()) { 979 cntElement.addElement(XmlNode.IsRootContainer.name()).addText(Boolean.TRUE.toString()); 980 } 981 982 // the elements 983 for (CmsContainerElementBean element : container.getElements()) { 984 Element elemElement = cntElement.addElement(XmlNode.Elements.name()); 985 986 Map<String, String> properties = new HashMap<>(element.getIndividualSettings()); 987 988 String instanceId = properties.remove(CmsContainerElement.ELEMENT_INSTANCE_ID); 989 if (instanceId != null) { 990 Element instanceIdElem = elemElement.addElement(XmlNode.ElementInstanceId.name()); 991 instanceIdElem.addText(instanceId); 992 } 993 994 String formatterKey = CmsFormatterUtils.removeFormatterKey(containerName, properties); 995 I_CmsFormatterBean formatter = null; 996 if (formatterKey != null) { 997 Element formatterKeyElem = elemElement.addElement(XmlNode.FormatterKey.name()); 998 999 formatter = adeConfig.findFormatter(formatterKey); 1000 if ((formatter != null) && (formatter.getKeyOrId() != null)) { 1001 formatterKey = formatter.getKeyOrId(); 1002 } 1003 formatterKeyElem.addText(formatterKey); 1004 } 1005 1006 CmsResource elementRes; 1007 if (!(formatter instanceof CmsFunctionFormatterBean)) { 1008 // the element 1009 Element uriElem = elemElement.addElement(XmlNode.Uri.name()); 1010 elementRes = fillResource(cms, uriElem, element.getId()); 1011 if ((element.getFormatterId() != null) && (formatterKey == null)) { 1012 Element formatterElem = elemElement.addElement(XmlNode.Formatter.name()); 1013 fillResource(cms, formatterElem, element.getFormatterId()); 1014 } 1015 } else { 1016 elementRes = cms.readResource(element.getId(), CmsResourceFilter.IGNORE_EXPIRATION); 1017 } 1018 if (element.isCreateNew()) { 1019 properties.put(CmsContainerElement.SETTING_CREATE_NEW, "true"); 1020 } 1021 // the properties 1022 1023 Map<String, String> processedSettings = processSettingsForSaveV2(adeConfig, properties); 1024 Map<String, CmsXmlContentProperty> propertiesConf = OpenCms.getADEManager().getElementSettings( 1025 cms, 1026 elementRes); 1027 CmsXmlContentPropertyHelper.saveProperties(cms, elemElement, processedSettings, propertiesConf, false); 1028 } 1029 } 1030 } 1031 1032 /** 1033 * Computes a container sort ordering for saving the containers of a container page bean.<p> 1034 * 1035 * @param page the container page bean 1036 * @return the sorted list of container names 1037 */ 1038 private List<String> sortContainerNames(CmsContainerPageBean page) { 1039 1040 Multimap<String, CmsContainerBean> containersByParentId = ArrayListMultimap.create(); 1041 Map<String, CmsContainerElementBean> elementsById = new HashMap<>(); 1042 List<CmsContainerBean> rootContainers = new ArrayList<>(); 1043 1044 // make table of container elements by instance id 1045 1046 for (CmsContainerBean container : page.getContainers().values()) { 1047 for (CmsContainerElementBean element : container.getElements()) { 1048 if (element.getInstanceId() != null) { 1049 elementsById.put(element.getInstanceId(), element); 1050 } 1051 } 1052 } 1053 1054 // make table of containers by their parent instance id 1055 1056 for (CmsContainerBean container : page.getContainers().values()) { 1057 String parentInstanceId = container.getParentInstanceId(); 1058 if (parentInstanceId != null) { 1059 containersByParentId.put(parentInstanceId, container); 1060 } 1061 if ((parentInstanceId == null) || !elementsById.containsKey(parentInstanceId)) { 1062 rootContainers.add(container); 1063 } 1064 } 1065 1066 // Visit all containers via depth-first traversal, using the previously constructed tables and a stack. 1067 // Record their names in the order they were encountered. 1068 // For children of the same container, they are ordered by name. 1069 1070 rootContainers.sort((a, b) -> b.getName().compareTo(a.getName())); // we put them on a stack, so the last element should be the smallest one 1071 ArrayList<CmsContainerBean> stack = new ArrayList<>(); 1072 stack.addAll(rootContainers); 1073 Map<String, Integer> order = new HashMap<>(); 1074 int counter = 0; 1075 while (stack.size() > 0) { 1076 CmsContainerBean container = stack.remove(stack.size() - 1); 1077 1078 // avoid already visited containers, in case there are cycles (possible in principle, if you change the container page manually) 1079 if (order.containsKey(container.getName())) { 1080 continue; 1081 } 1082 order.put(container.getName(), Integer.valueOf(counter)); 1083 counter += 1; 1084 1085 for (CmsContainerElementBean element : container.getElements()) { 1086 String instanceId = element.getInstanceId(); 1087 if (instanceId != null) { 1088 List<CmsContainerBean> childContainers = new ArrayList<>(containersByParentId.get(instanceId)); 1089 childContainers.sort((a, b) -> b.getName().compareTo(a.getName())); 1090 stack.addAll(childContainers); 1091 } 1092 } 1093 } 1094 List<String> result = new ArrayList<>(page.getContainers().keySet()); 1095 1096 result.sort( 1097 ( 1098 a, 1099 b) -> ComparisonChain.start().compare( 1100 order.get(a), 1101 order.get(b), 1102 Ordering.natural().nullsLast()).compare(a, b).result()); 1103 return result; 1104 } 1105 1106 /** 1107 * Sort element settings such that system settings come first and normal element settings after that, with each group alphabetically sorted. 1108 * 1109 * @param settings the map of settings 1110 * @return the sorted settings map 1111 */ 1112 private LinkedHashMap<String, String> sortSettingsForSave(Map<String, String> settings) { 1113 1114 LinkedHashMap<String, String> result = new LinkedHashMap<>(); 1115 List<String> keys = new ArrayList<>(settings.keySet()); 1116 keys.sort( 1117 ( 1118 a, 1119 b) -> ComparisonChain.start().compareTrueFirst( 1120 a.startsWith(SYSTEM_SETTING_PREFIX), 1121 b.startsWith(SYSTEM_SETTING_PREFIX)).compare(a, b).result()); 1122 for (String key : keys) { 1123 result.put(key, settings.get(key)); 1124 } 1125 return result; 1126 } 1127 1128 /** 1129 * Converts a string map to a new map by applying a translation function to the map keys. 1130 * 1131 * @param settings the original map 1132 * @param translation the translation function 1133 * @return the new map with the translated keys 1134 */ 1135 private Map<String, String> translateMapKeys(Map<String, String> settings, Function<String, String> translation) { 1136 1137 LinkedHashMap<String, String> result = new LinkedHashMap<>(); 1138 settings.entrySet().forEach(e -> result.put(translation.apply(e.getKey()), e.getValue())); 1139 return result; 1140 1141 } 1142 1143 /** 1144 * Translates new SYSTEM:: prefixed names for legacy system element settings to their non-prefixed form. 1145 * 1146 * @param name the setting name 1147 * @return the translated setting name 1148 */ 1149 private String translateSettingNameForLoad(String name) { 1150 1151 if (name.startsWith(SYSTEM_SETTING_PREFIX)) { 1152 String remainder = name.substring(SYSTEM_SETTING_PREFIX.length()); 1153 if (LEGACY_SYSTEM_SETTING_NAMES.contains(remainder)) { 1154 return remainder; 1155 } 1156 } 1157 return name; 1158 } 1159 1160 /** 1161 * Translates legacy non-prefixed system settings to the form prefixed with SYSTEM:: . 1162 * 1163 * @param name a setting name 1164 * @return the translated setting name 1165 */ 1166 private String translateSettingNameForSave(String name) { 1167 1168 if (LEGACY_SYSTEM_SETTING_NAMES.contains(name)) { 1169 return SYSTEM_SETTING_PREFIX + name; 1170 } 1171 return name; 1172 } 1173 1174}