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