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