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, 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.content; 029 030import org.opencms.ade.configuration.CmsADEConfigData; 031import org.opencms.ade.containerpage.shared.CmsFormatterConfig; 032import org.opencms.file.CmsObject; 033import org.opencms.file.CmsProperty; 034import org.opencms.file.CmsResource; 035import org.opencms.file.types.CmsResourceTypeXmlContent; 036import org.opencms.gwt.shared.CmsGwtConstants; 037import org.opencms.i18n.CmsMultiMessages; 038import org.opencms.json.JSONException; 039import org.opencms.json.JSONObject; 040import org.opencms.main.CmsException; 041import org.opencms.main.CmsLog; 042import org.opencms.main.OpenCms; 043import org.opencms.relations.CmsLink; 044import org.opencms.relations.CmsRelationType; 045import org.opencms.search.galleries.CmsGalleryNameMacroResolver; 046import org.opencms.util.CmsMacroResolver; 047import org.opencms.util.CmsStringUtil; 048import org.opencms.util.CmsUUID; 049import org.opencms.util.I_CmsMacroResolver; 050import org.opencms.xml.CmsXmlContentDefinition; 051import org.opencms.xml.CmsXmlGenericWrapper; 052import org.opencms.xml.CmsXmlUtils; 053import org.opencms.xml.containerpage.I_CmsFormatterBean; 054import org.opencms.xml.content.CmsXmlContentProperty.PropType; 055import org.opencms.xml.page.CmsXmlPage; 056import org.opencms.xml.types.CmsXmlNestedContentDefinition; 057import org.opencms.xml.types.CmsXmlVfsFileValue; 058import org.opencms.xml.types.I_CmsXmlContentValue; 059import org.opencms.xml.types.I_CmsXmlSchemaType; 060 061import java.util.ArrayList; 062import java.util.Collections; 063import java.util.HashMap; 064import java.util.HashSet; 065import java.util.Iterator; 066import java.util.LinkedHashMap; 067import java.util.List; 068import java.util.Locale; 069import java.util.Map; 070import java.util.Map.Entry; 071import java.util.Set; 072import java.util.TreeMap; 073import java.util.function.Function; 074 075import org.apache.commons.logging.Log; 076 077import org.dom4j.Element; 078 079import com.google.common.base.Objects; 080import com.google.common.base.Supplier; 081 082import jakarta.servlet.ServletRequest; 083 084/** 085 * Provides common methods on XML property configuration.<p> 086 * 087 * @since 8.0.0 088 */ 089public final class CmsXmlContentPropertyHelper implements Cloneable { 090 091 /** Element Property json property constants. */ 092 public enum JsonProperty { 093 094 /** Property's default value. */ 095 defaultValue, 096 /** Property's description. */ 097 description, 098 /** Property's error message. */ 099 error, 100 /** Property's nice name. */ 101 niceName, 102 /** Property's validation regular expression. */ 103 ruleRegex, 104 /** Property's validation rule type. */ 105 ruleType, 106 /** Property's type. */ 107 type, 108 /** Property's value. */ 109 value, 110 /** Property's widget. */ 111 widget, 112 /** Property's widget configuration. */ 113 widgetConf; 114 } 115 116 /** The prefix for macros used to acess properties of the current container page. */ 117 public static final String PAGE_PROPERTY_PREFIX = "page-property:"; 118 119 /** If a property has this value, the page-property macro for this property will expand to the empty string instead. */ 120 protected static final Object PROPERTY_EMPTY_MARKER = "-"; 121 122 /** Widget configuration key-value separator constant. */ 123 private static final String CONF_KEYVALUE_SEPARATOR = ":"; 124 125 /** Widget configuration parameter separator constant. */ 126 private static final String CONF_PARAM_SEPARATOR = "\\|"; 127 128 /** The log object for this class. */ 129 private static final Log LOG = CmsLog.getLog(CmsXmlContentPropertyHelper.class); 130 131 /** 132 * Hidden constructor.<p> 133 */ 134 private CmsXmlContentPropertyHelper() { 135 136 // prevent instantiation 137 } 138 139 /** 140 * Converts a map of properties from server format to client format.<p> 141 * 142 * @param cms the CmsObject to use for VFS operations 143 * @param props the map of properties 144 * @param propConfig the property configuration 145 * 146 * @return the converted property map 147 */ 148 public static Map<String, String> convertPropertiesToClientFormat( 149 CmsObject cms, 150 Map<String, String> props, 151 Map<String, CmsXmlContentProperty> propConfig) { 152 153 return convertProperties(cms, props, propConfig, true); 154 } 155 156 /** 157 * Converts a map of properties from client format to server format.<p> 158 * 159 * @param cms the CmsObject to use for VFS operations 160 * @param props the map of properties 161 * @param propConfig the property configuration 162 * 163 * @return the converted property map 164 */ 165 public static Map<String, String> convertPropertiesToServerFormat( 166 CmsObject cms, 167 Map<String, String> props, 168 Map<String, CmsXmlContentProperty> propConfig) { 169 170 return convertProperties(cms, props, propConfig, false); 171 } 172 173 /** 174 * Creates a deep copy of a property configuration map.<p> 175 * 176 * @param propConfig the property configuration which should be copied 177 * 178 * @return a copy of the property configuration 179 */ 180 public static Map<String, CmsXmlContentProperty> copyPropertyConfiguration( 181 Map<String, CmsXmlContentProperty> propConfig) { 182 183 Map<String, CmsXmlContentProperty> result = new LinkedHashMap<String, CmsXmlContentProperty>(); 184 for (Map.Entry<String, CmsXmlContentProperty> entry : propConfig.entrySet()) { 185 String key = entry.getKey(); 186 CmsXmlContentProperty propDef = entry.getValue(); 187 result.put(key, propDef.copy()); 188 } 189 return result; 190 } 191 192 /** 193 * Looks up an URI in the sitemap and returns either a sitemap entry id (if the URI is a sitemap URI) 194 * or the structure id of a resource (if the URI is a VFS path).<p> 195 * 196 * @param cms the current CMS context 197 * @param uri the URI to look up 198 * @return a sitemap entry id or a structure id 199 * 200 * @throws CmsException if something goes wrong 201 */ 202 public static CmsUUID getIdForUri(CmsObject cms, String uri) throws CmsException { 203 204 return cms.readResource(uri).getStructureId(); 205 } 206 207 /** 208 * Creates and configures a new macro resolver for resolving macros which occur in property definitions.<p> 209 * 210 * @param cms the CMS context 211 * @param contentHandler the content handler which contains the message bundle that should be available in the macro resolver 212 * @param content the XML content object 213 * @param stringtemplateSource provides stringtemplate templates for use in %(stringtemplate:...) macros 214 * @param containerPage the current container page 215 * 216 * @return a new macro resolver 217 */ 218 public static CmsMacroResolver getMacroResolverForProperties( 219 final CmsObject cms, 220 final I_CmsXmlContentHandler contentHandler, 221 final CmsXmlContent content, 222 final Function<String, String> stringtemplateSource, 223 final CmsResource containerPage) { 224 225 Locale locale = OpenCms.getLocaleManager().getBestAvailableLocaleForXmlContent(cms, content.getFile(), content); 226 final CmsGalleryNameMacroResolver resolver = new CmsGalleryNameMacroResolver(cms, content, locale) { 227 228 @SuppressWarnings("synthetic-access") 229 @Override 230 public String getMacroValue(String macro) { 231 232 if (macro.startsWith(PAGE_PROPERTY_PREFIX)) { 233 String remainder = macro.substring(PAGE_PROPERTY_PREFIX.length()); 234 int secondColonPos = remainder.indexOf(":"); 235 String defaultValue = ""; 236 String propName = null; 237 if (secondColonPos >= 0) { 238 propName = remainder.substring(0, secondColonPos); 239 defaultValue = remainder.substring(secondColonPos + 1); 240 } else { 241 propName = remainder; 242 } 243 if (containerPage != null) { 244 try { 245 CmsProperty prop = cms.readPropertyObject(containerPage, propName, true); 246 String propValue = prop.getValue(); 247 if ((propValue == null) || PROPERTY_EMPTY_MARKER.equals(propValue)) { 248 propValue = defaultValue; 249 } 250 return propValue; 251 } catch (CmsException e) { 252 LOG.error(e.getLocalizedMessage(), e); 253 return defaultValue; 254 } 255 } 256 257 } 258 return super.getMacroValue(macro); 259 } 260 261 }; 262 263 resolver.setStringTemplateSource(stringtemplateSource); 264 Locale wpLocale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms); 265 CmsMultiMessages messages = new CmsMultiMessages(wpLocale); 266 messages.addMessages(OpenCms.getWorkplaceManager().getMessages(wpLocale)); 267 messages.addMessages(content.getContentDefinition().getContentHandler().getMessages(wpLocale)); 268 resolver.setCmsObject(cms); 269 resolver.setKeepEmptyMacros(true); 270 resolver.setMessages(messages); 271 return resolver; 272 } 273 274 /** 275 * Returns the property information for the given resource (type) AND the current user.<p> 276 * 277 * @param cms the current CMS context 278 * @param page the current container page 279 * @param resource the resource 280 * 281 * @return the property information 282 * 283 * @throws CmsException if something goes wrong 284 */ 285 public static Map<String, CmsXmlContentProperty> getPropertyInfo( 286 CmsObject cms, 287 CmsResource page, 288 CmsResource resource) 289 throws CmsException { 290 291 if (CmsResourceTypeXmlContent.isXmlContent(resource)) { 292 I_CmsXmlContentHandler contentHandler = CmsXmlContentDefinition.getContentHandlerForResource(cms, resource); 293 Map<String, CmsXmlContentProperty> propertiesConf = contentHandler.getSettings(cms, resource); 294 CmsXmlContent content = CmsXmlContentFactory.unmarshal(cms, cms.readFile(resource)); 295 CmsMacroResolver resolver = getMacroResolverForProperties(cms, contentHandler, content, null, page); 296 return resolveMacrosInProperties(propertiesConf, resolver); 297 } 298 return Collections.<String, CmsXmlContentProperty> emptyMap(); 299 } 300 301 /** 302 * Returns a converted property value depending on the given type.<p> 303 * 304 * If the type is {@link CmsXmlContentProperty.PropType#vfslist}, the value is parsed as a 305 * list of paths and converted to a list of IDs.<p> 306 * 307 * @param cms the current CMS context 308 * @param type the property type 309 * @param value the raw property value 310 * 311 * @return a converted property value depending on the given type 312 */ 313 public static String getPropValueIds(CmsObject cms, String type, String value) { 314 315 if (PropType.isVfsList(type)) { 316 return convertPathsToIds(cms, value); 317 } 318 return value; 319 } 320 321 /** 322 * Returns a converted property value depending on the given type.<p> 323 * 324 * If the type is {@link CmsXmlContentProperty.PropType#vfslist}, the value is parsed as a 325 * list of IDs and converted to a list of paths.<p> 326 * 327 * @param cms the current CMS context 328 * @param type the property type 329 * @param value the raw property value 330 * 331 * @return a converted property value depending on the given type 332 */ 333 public static String getPropValuePaths(CmsObject cms, String type, String value) { 334 335 if (PropType.isVfsList(type)) { 336 return convertIdsToPaths(cms, value); 337 } 338 return value; 339 } 340 341 /** 342 * Returns a sitemap or VFS path given a sitemap entry id or structure id.<p> 343 * 344 * This method first tries to read a sitemap entry with the given id. If this succeeds, 345 * the sitemap entry's sitemap path will be returned. If it fails, the method interprets 346 * the id as a structure id and tries to read the corresponding resource, and then returns 347 * its VFS path.<p> 348 * 349 * @param cms the CMS context 350 * @param id a sitemap entry id or structure id 351 * 352 * @return a sitemap or VFS uri 353 * 354 * @throws CmsException if something goes wrong 355 */ 356 public static String getUriForId(CmsObject cms, CmsUUID id) throws CmsException { 357 358 CmsResource res = cms.readResource(id); 359 return cms.getSitePath(res); 360 } 361 362 /** 363 * Returns the widget configuration string parsed into a JSONObject.<p> 364 * 365 * The configuration string should be a map of key value pairs separated by ':' and '|': KEY_1:VALUE_1|KEY_2:VALUE_2 ... 366 * 367 * @param widgetConfiguration the configuration to parse 368 * 369 * @return the configuration JSON 370 */ 371 public static JSONObject getWidgetConfigurationAsJSON(String widgetConfiguration) { 372 373 JSONObject result = new JSONObject(); 374 if (CmsStringUtil.isEmptyOrWhitespaceOnly(widgetConfiguration)) { 375 return result; 376 } 377 Map<String, String> confEntries = CmsStringUtil.splitAsMap( 378 widgetConfiguration, 379 CONF_PARAM_SEPARATOR, 380 CONF_KEYVALUE_SEPARATOR); 381 for (Map.Entry<String, String> entry : confEntries.entrySet()) { 382 try { 383 result.put(entry.getKey(), entry.getValue()); 384 } catch (JSONException e) { 385 // should never happen 386 LOG.error( 387 Messages.get().container(Messages.ERR_XMLCONTENT_UNKNOWN_ELEM_PATH_SCHEMA_1, widgetConfiguration), 388 e); 389 } 390 } 391 return result; 392 } 393 394 /** 395 * Extends the given properties with the default values 396 * from the resource's property configuration.<p> 397 * 398 * @param cms the current CMS context 399 * @param config the current sitemap configuration 400 * @param resource the resource to get the property configuration from 401 * @param properties the properties to extend 402 * @param locale the content locale 403 * @param request the current request, if available 404 * 405 * @return a merged map of properties 406 */ 407 public static Map<String, String> mergeDefaults( 408 CmsObject cms, 409 CmsADEConfigData config, 410 CmsResource resource, 411 Map<String, String> properties, 412 Locale locale, 413 ServletRequest request) { 414 415 Map<String, CmsXmlContentProperty> propertyConfig = null; 416 if (CmsResourceTypeXmlContent.isXmlContent(resource)) { 417 I_CmsFormatterBean formatter = null; 418 // check formatter configuration setting 419 for (Entry<String, String> property : properties.entrySet()) { 420 if (property.getKey().startsWith(CmsFormatterConfig.FORMATTER_SETTINGS_KEY)) { 421 I_CmsFormatterBean dynamicFmt = config.findFormatter(property.getValue()); 422 if (dynamicFmt != null) { 423 formatter = dynamicFmt; 424 break; 425 } 426 } 427 428 } 429 430 try { 431 432 if (formatter != null) { 433 propertyConfig = OpenCms.getADEManager().getFormatterSettings( 434 cms, 435 config, 436 formatter, 437 resource, 438 locale, 439 request); 440 } else { 441 // fall back to schema configuration 442 propertyConfig = CmsXmlContentDefinition.getContentHandlerForResource(cms, resource).getSettings( 443 cms, 444 resource); 445 } 446 } catch (CmsException e) { 447 // should never happen 448 LOG.error(e.getLocalizedMessage(), e); 449 } 450 } 451 return mergeDefaults(cms, propertyConfig, properties); 452 } 453 454 /** 455 * Extends the given properties with the default values 456 * from property configuration.<p> 457 * 458 * @param cms the current CMS context 459 * @param propertyConfig the property configuration 460 * @param properties the properties to extend 461 * 462 * @return a merged map of properties 463 */ 464 public static Map<String, String> mergeDefaults( 465 CmsObject cms, 466 Map<String, CmsXmlContentProperty> propertyConfig, 467 Map<String, String> properties) { 468 469 Set<String> hidden = new HashSet<>(); 470 Map<String, String> result = new HashMap<String, String>(); 471 if (propertyConfig != null) { 472 for (Map.Entry<String, CmsXmlContentProperty> entry : propertyConfig.entrySet()) { 473 CmsXmlContentProperty prop = entry.getValue(); 474 String value = getPropValueIds(cms, prop.getType(), prop.getDefault()); 475 if (CmsGwtConstants.HIDDEN_SETTINGS_WIDGET_NAME.equals(prop.getWidget())) { 476 hidden.add(entry.getKey()); 477 } 478 if (value != null) { 479 result.put(entry.getKey(), value); 480 } 481 } 482 } 483 properties.forEach((key, value) -> { 484 if (!hidden.contains(key)) { 485 result.put(key, value); 486 } else { 487 // 'hidden' widget, but we still got a setting value. The setting value is probably left over 488 // from a previous formatter - ignore it. This can happen with list display formatters, because 489 // they are selected in the list configuration content, and editing the content itself doesn't affect 490 // the settings of the corresponding container page element(s). 491 if (!Objects.equal(value, result.get(key))) { 492 LOG.info( 493 "Discarding setting value because configured widget is 'hidden': key = " 494 + key 495 + ", value = " 496 + value 497 + ", original value = " 498 + result.get(key)); 499 } 500 } 501 }); 502 503 return result; 504 } 505 506 /** 507 * Reads property nodes from the given location.<p> 508 * 509 * @param cms the current cms context 510 * @param baseLocation the base location 511 * 512 * @return the properties 513 */ 514 public static Map<String, String> readProperties(CmsObject cms, I_CmsXmlContentLocation baseLocation) { 515 516 Map<String, String> result = new HashMap<String, String>(); 517 String elementName = CmsXmlContentProperty.XmlNode.Properties.name(); 518 String nameElementName = CmsXmlContentProperty.XmlNode.Name.name(); 519 List<I_CmsXmlContentValueLocation> propertyLocations = baseLocation.getSubValues(elementName); 520 for (I_CmsXmlContentValueLocation propertyLocation : propertyLocations) { 521 I_CmsXmlContentValueLocation nameLocation = propertyLocation.getSubValue(nameElementName); 522 String name = nameLocation.asString(cms).trim(); 523 String value = null; 524 I_CmsXmlContentValueLocation valueLocation = propertyLocation.getSubValue( 525 CmsXmlContentProperty.XmlNode.Value.name()); 526 I_CmsXmlContentValueLocation stringLocation = valueLocation.getSubValue( 527 CmsXmlContentProperty.XmlNode.String.name()); 528 I_CmsXmlContentValueLocation fileListLocation = valueLocation.getSubValue( 529 CmsXmlContentProperty.XmlNode.FileList.name()); 530 if (stringLocation != null) { 531 value = stringLocation.asString(cms).trim(); 532 } else if (fileListLocation != null) { 533 List<CmsUUID> idList = new ArrayList<CmsUUID>(); 534 List<I_CmsXmlContentValueLocation> fileLocations = fileListLocation.getSubValues( 535 CmsXmlContentProperty.XmlNode.Uri.name()); 536 for (I_CmsXmlContentValueLocation fileLocation : fileLocations) { 537 CmsUUID structureId = fileLocation.asId(cms); 538 idList.add(structureId); 539 } 540 value = CmsStringUtil.listAsString(idList, CmsXmlContentProperty.PROP_SEPARATOR); 541 } 542 if (value != null) { 543 result.put(name, value); 544 } 545 } 546 return result; 547 } 548 549 /** 550 * Reads the properties from property-enabled xml content values.<p> 551 * 552 * @param xmlContent the xml content 553 * @param locale the current locale 554 * @param element the xml element 555 * @param elemPath the xpath 556 * @param elemDef the element definition 557 * 558 * @return the read property map 559 * 560 * @see org.opencms.xml.containerpage.CmsXmlContainerPage.XmlNode#Elements 561 */ 562 public static Map<String, String> readProperties( 563 CmsXmlContent xmlContent, 564 Locale locale, 565 Element element, 566 String elemPath, 567 CmsXmlContentDefinition elemDef) { 568 569 Map<String, String> propertiesMap = new HashMap<String, String>(); 570 // Properties 571 for (Iterator<Element> itProps = CmsXmlGenericWrapper.elementIterator( 572 element, 573 CmsXmlContentProperty.XmlNode.Properties.name()); itProps.hasNext();) { 574 Element property = itProps.next(); 575 576 // property itself 577 int propIndex = CmsXmlUtils.getXpathIndexInt(property.getUniquePath(element)); 578 String propPath = CmsXmlUtils.concatXpath( 579 elemPath, 580 CmsXmlUtils.createXpathElement(property.getName(), propIndex)); 581 I_CmsXmlSchemaType propSchemaType = elemDef.getSchemaType(property.getName()); 582 I_CmsXmlContentValue propValue = propSchemaType.createValue(xmlContent, property, locale); 583 xmlContent.addBookmarkForValue(propValue, propPath, locale, true); 584 CmsXmlContentDefinition propDef = ((CmsXmlNestedContentDefinition)propSchemaType).getNestedContentDefinition(); 585 586 // name 587 Element propName = property.element(CmsXmlContentProperty.XmlNode.Name.name()); 588 xmlContent.addBookmarkForElement(propName, locale, property, propPath, propDef); 589 590 // choice value 591 Element value = property.element(CmsXmlContentProperty.XmlNode.Value.name()); 592 if (value == null) { 593 // this can happen when adding the elements node to the xml content 594 continue; 595 } 596 int valueIndex = CmsXmlUtils.getXpathIndexInt(value.getUniquePath(property)); 597 String valuePath = CmsXmlUtils.concatXpath( 598 propPath, 599 CmsXmlUtils.createXpathElement(value.getName(), valueIndex)); 600 I_CmsXmlSchemaType valueSchemaType = propDef.getSchemaType(value.getName()); 601 I_CmsXmlContentValue valueValue = valueSchemaType.createValue(xmlContent, value, locale); 602 xmlContent.addBookmarkForValue(valueValue, valuePath, locale, true); 603 CmsXmlContentDefinition valueDef = ((CmsXmlNestedContentDefinition)valueSchemaType).getNestedContentDefinition(); 604 605 String val = null; 606 Element string = value.element(CmsXmlContentProperty.XmlNode.String.name()); 607 if (string != null) { 608 // string value 609 xmlContent.addBookmarkForElement(string, locale, value, valuePath, valueDef); 610 val = string.getTextTrim(); 611 } else { 612 // file list value 613 Element valueFileList = value.element(CmsXmlContentProperty.XmlNode.FileList.name()); 614 if (valueFileList == null) { 615 // this can happen when adding the elements node to the xml content 616 continue; 617 } 618 int valueFileListIndex = CmsXmlUtils.getXpathIndexInt(valueFileList.getUniquePath(value)); 619 String valueFileListPath = CmsXmlUtils.concatXpath( 620 valuePath, 621 CmsXmlUtils.createXpathElement(valueFileList.getName(), valueFileListIndex)); 622 I_CmsXmlSchemaType valueFileListSchemaType = valueDef.getSchemaType(valueFileList.getName()); 623 I_CmsXmlContentValue valueFileListValue = valueFileListSchemaType.createValue( 624 xmlContent, 625 valueFileList, 626 locale); 627 xmlContent.addBookmarkForValue(valueFileListValue, valueFileListPath, locale, true); 628 CmsXmlContentDefinition valueFileListDef = ((CmsXmlNestedContentDefinition)valueFileListSchemaType).getNestedContentDefinition(); 629 630 List<CmsUUID> idList = new ArrayList<CmsUUID>(); 631 // files 632 for (Iterator<Element> itFiles = CmsXmlGenericWrapper.elementIterator( 633 valueFileList, 634 CmsXmlContentProperty.XmlNode.Uri.name()); itFiles.hasNext();) { 635 636 Element valueUri = itFiles.next(); 637 xmlContent.addBookmarkForElement( 638 valueUri, 639 locale, 640 valueFileList, 641 valueFileListPath, 642 valueFileListDef); 643 Element valueUriLink = valueUri.element(CmsXmlPage.NODE_LINK); 644 CmsUUID fileId = null; 645 if (valueUriLink == null) { 646 // this can happen when adding the elements node to the xml content 647 // it is not dangerous since the link has to be set before saving 648 } else { 649 fileId = new CmsLink(valueUriLink).getStructureId(); 650 idList.add(fileId); 651 } 652 } 653 // comma separated list of UUIDs 654 val = CmsStringUtil.listAsString(idList, CmsXmlContentProperty.PROP_SEPARATOR); 655 } 656 657 propertiesMap.put(propName.getTextTrim(), val); 658 } 659 return propertiesMap; 660 } 661 662 /** 663 * Resolves macros in the given property information for the given resource (type) AND the current user.<p> 664 * 665 * @param cms the current CMS context 666 * @param page the current container page 667 * @param resource the resource 668 * @param contentGetter loads the actual content 669 * @param stringtemplateSource provider for stringtemplate templates 670 * @param propertiesConf the property information 671 * 672 * @return the property information 673 * 674 * @throws CmsException if something goes wrong 675 */ 676 public static Map<String, CmsXmlContentProperty> resolveMacrosForPropertyInfo( 677 CmsObject cms, 678 CmsResource page, 679 CmsResource resource, 680 Supplier<CmsXmlContent> contentGetter, 681 Function<String, String> stringtemplateSource, 682 Map<String, CmsXmlContentProperty> propertiesConf) 683 throws CmsException { 684 685 if (CmsResourceTypeXmlContent.isXmlContent(resource)) { 686 I_CmsXmlContentHandler contentHandler = CmsXmlContentDefinition.getContentHandlerForResource(cms, resource); 687 CmsMacroResolver resolver = getMacroResolverForProperties( 688 cms, 689 contentHandler, 690 contentGetter.get(), 691 stringtemplateSource, 692 page); 693 return resolveMacrosInProperties(propertiesConf, resolver); 694 } 695 return propertiesConf; 696 } 697 698 /** 699 * Resolves macros in all properties in a map.<p> 700 * 701 * @param properties the map of properties in which macros should be resolved 702 * @param resolver the macro resolver to use 703 * 704 * @return a new map of properties with resolved macros 705 */ 706 public static Map<String, CmsXmlContentProperty> resolveMacrosInProperties( 707 Map<String, CmsXmlContentProperty> properties, 708 I_CmsMacroResolver resolver) { 709 710 Map<String, CmsXmlContentProperty> result = new LinkedHashMap<String, CmsXmlContentProperty>(); 711 for (Map.Entry<String, CmsXmlContentProperty> entry : properties.entrySet()) { 712 String key = entry.getKey(); 713 CmsXmlContentProperty prop = entry.getValue(); 714 result.put(key, resolveMacrosInProperty(prop, resolver)); 715 } 716 return result; 717 } 718 719 /** 720 * Resolves the macros in a single property.<p> 721 * 722 * @param property the property in which macros should be resolved 723 * @param resolver the macro resolver to use 724 * 725 * @return a new property with resolved macros 726 */ 727 public static CmsXmlContentProperty resolveMacrosInProperty( 728 CmsXmlContentProperty property, 729 I_CmsMacroResolver resolver) { 730 731 String propName = property.getName(); 732 CmsXmlContentProperty result = new CmsXmlContentProperty( 733 propName, 734 property.getAliasName(), 735 property.getType(), 736 property.getConfiguredVisibility(), 737 resolver.resolveMacros(property.getWidget()), 738 resolver.resolveMacros(property.getWidgetConfiguration()), 739 property.getRuleRegex(), 740 property.getRuleType(), 741 property.getDefault(), 742 resolver.resolveMacros(property.getPlaceholder()), 743 resolver.resolveMacros(property.getNiceName()), 744 resolver.resolveMacros(property.getDescription()), 745 resolver.resolveMacros(property.getError()), 746 property.isPreferFolder() ? "true" : "false", 747 property.getTranslationStr()); 748 return result; 749 } 750 751 /** 752 * Saves the given properties to the given xml element.<p> 753 * 754 * @param cms the current CMS context 755 * @param parentElement the parent xml element 756 * @param properties the properties to save, if there is a list of resources, every entry can be a site path or a UUID 757 * @param propertiesConf the configuration of the properties 758 * @param sort if true, properties will be sorted by map keys via string co 759 */ 760 public static void saveProperties( 761 CmsObject cms, 762 Element parentElement, 763 Map<String, String> properties, 764 Map<String, CmsXmlContentProperty> propertiesConf, 765 boolean sort) { 766 767 // remove old entries 768 for (Object propElement : parentElement.elements(CmsXmlContentProperty.XmlNode.Properties.name())) { 769 parentElement.remove((Element)propElement); 770 } 771 772 // use a sorted map to force a defined order 773 Map<String, String> props; 774 if (sort) { 775 props = new TreeMap<String, String>(properties); 776 } else { 777 props = properties; 778 } 779 780 // create new entries 781 for (Map.Entry<String, String> property : props.entrySet()) { 782 String propName = property.getKey(); 783 String propValue = property.getValue(); 784 if ((propValue == null) || (propValue.length() == 0)) { 785 continue; 786 } 787 // only if the property is configured in the schema we will save it 788 Element propElement = parentElement.addElement(CmsXmlContentProperty.XmlNode.Properties.name()); 789 790 // the property name 791 propElement.addElement(CmsXmlContentProperty.XmlNode.Name.name()).addCDATA(propName); 792 Element valueElement = propElement.addElement(CmsXmlContentProperty.XmlNode.Value.name()); 793 boolean isVfs = false; 794 CmsXmlContentProperty propDef = propertiesConf.get(propName); 795 if (propDef != null) { 796 isVfs = CmsXmlContentProperty.PropType.isVfsList(propDef.getType()); 797 } 798 if (!isVfs) { 799 // string value 800 valueElement.addElement(CmsXmlContentProperty.XmlNode.String.name()).addCDATA(propValue); 801 } else { 802 addFileListPropertyValue(cms, valueElement, propValue); 803 } 804 } 805 } 806 807 /** 808 * Adds the XML for a property value of a property of type 'vfslist' to the DOM.<p> 809 * 810 * @param cms the current CMS context 811 * @param valueElement the element to which the vfslist property value should be added 812 * @param propValue the property value which should be saved 813 */ 814 protected static void addFileListPropertyValue(CmsObject cms, Element valueElement, String propValue) { 815 816 // resource list value 817 Element filelistElem = valueElement.addElement(CmsXmlContentProperty.XmlNode.FileList.name()); 818 for (String strId : CmsStringUtil.splitAsList(propValue, CmsXmlContentProperty.PROP_SEPARATOR)) { 819 try { 820 Element fileValueElem = filelistElem.addElement(CmsXmlContentProperty.XmlNode.Uri.name()); 821 CmsVfsFileValueBean fileValue = getFileValueForIdOrUri(cms, strId); 822 // HACK: here we assume weak relations, but it would be more robust to check it, with smth like: 823 // type = xmlContent.getContentDefinition().getContentHandler().getRelationType(fileValueElem.getPath()); 824 CmsRelationType type = CmsRelationType.XML_WEAK; 825 CmsXmlVfsFileValue.fillEntry(fileValueElem, fileValue.getId(), fileValue.getPath(), type); 826 } catch (CmsException e) { 827 // should never happen 828 LOG.error(e.getLocalizedMessage(), e); 829 } 830 } 831 } 832 833 /** 834 * Converts a string containing zero or more structure ids into a string containing the corresponding VFS paths.<p> 835 * 836 * @param cms the CmsObject to use for the VFS operations 837 * @param value a string representation of a list of ids 838 * 839 * @return a string representation of a list of paths 840 */ 841 protected static String convertIdsToPaths(CmsObject cms, String value) { 842 843 if (value == null) { 844 return null; 845 } 846 // represent vfslists as lists of path in JSON 847 List<String> ids = CmsStringUtil.splitAsList(value, CmsXmlContentProperty.PROP_SEPARATOR); 848 List<String> paths = new ArrayList<String>(); 849 for (String id : ids) { 850 try { 851 String path = getUriForId(cms, new CmsUUID(id)); 852 paths.add(path); 853 } catch (Exception e) { 854 // should never happen 855 LOG.error(e.getLocalizedMessage(), e); 856 continue; 857 } 858 } 859 return CmsStringUtil.listAsString(paths, CmsXmlContentProperty.PROP_SEPARATOR); 860 } 861 862 /** 863 * Converts a string containing zero or more VFS paths into a string containing the corresponding structure ids.<p> 864 * 865 * @param cms the CmsObject to use for the VFS operations 866 * @param value a string representation of a list of paths 867 * 868 * @return a string representation of a list of ids 869 */ 870 protected static String convertPathsToIds(CmsObject cms, String value) { 871 872 if (value == null) { 873 return null; 874 } 875 // represent vfslists as lists of path in JSON 876 List<String> paths = CmsStringUtil.splitAsList(value, CmsXmlContentProperty.PROP_SEPARATOR); 877 List<String> ids = new ArrayList<String>(); 878 for (String path : paths) { 879 try { 880 CmsUUID id = getIdForUri(cms, path); 881 ids.add(id.toString()); 882 } catch (CmsException e) { 883 // should never happen 884 LOG.error(e.getLocalizedMessage(), e); 885 continue; 886 } 887 } 888 return CmsStringUtil.listAsString(ids, CmsXmlContentProperty.PROP_SEPARATOR); 889 } 890 891 /** 892 * Helper method for converting a map of properties from client format to server format or vice versa.<p> 893 * 894 * @param cms the CmsObject to use for VFS operations 895 * @param props the map of properties 896 * @param propConfig the property configuration 897 * @param toClient if true, convert from server to client, else from client to server 898 * 899 * @return the converted property map 900 */ 901 protected static Map<String, String> convertProperties( 902 CmsObject cms, 903 Map<String, String> props, 904 Map<String, CmsXmlContentProperty> propConfig, 905 boolean toClient) { 906 907 Map<String, String> result = new HashMap<String, String>(); 908 for (Map.Entry<String, String> entry : props.entrySet()) { 909 String propName = entry.getKey(); 910 String propValue = entry.getValue(); 911 String type = "string"; 912 CmsXmlContentProperty configEntry = getPropertyConfig(propConfig, propName); 913 if (configEntry != null) { 914 type = configEntry.getType(); 915 } 916 String newValue = convertStringPropertyValue(cms, propValue, type, toClient); 917 result.put(propName, newValue); 918 } 919 return result; 920 } 921 922 /** 923 * Converts a property value given as a string between server format and client format.<p> 924 * 925 * @param cms the current CMS context 926 * @param propValue the property value to convert 927 * @param type the type of the property 928 * @param toClient if true, convert to client format, else convert to server format 929 * 930 * @return the converted property value 931 */ 932 protected static String convertStringPropertyValue(CmsObject cms, String propValue, String type, boolean toClient) { 933 934 if (propValue == null) { 935 return null; 936 } 937 if (toClient) { 938 return CmsXmlContentPropertyHelper.getPropValuePaths(cms, type, propValue); 939 } else { 940 return CmsXmlContentPropertyHelper.getPropValueIds(cms, type, propValue); 941 } 942 } 943 944 /** 945 * Given a string which might be a id or a (sitemap or VFS) URI, this method will return 946 * a bean containing the right (sitemap or vfs) root path and (sitemap entry or structure) id.<p> 947 * 948 * @param cms the current CMS context 949 * @param idOrUri a string containing an id or an URI 950 * 951 * @return a bean containing a root path and an id 952 * 953 * @throws CmsException if something goes wrong 954 */ 955 protected static CmsVfsFileValueBean getFileValueForIdOrUri(CmsObject cms, String idOrUri) throws CmsException { 956 957 CmsVfsFileValueBean result; 958 if (CmsUUID.isValidUUID(idOrUri)) { 959 CmsUUID id = new CmsUUID(idOrUri); 960 String uri = getUriForId(cms, id); 961 result = new CmsVfsFileValueBean(cms.getRequestContext().addSiteRoot(uri), id); 962 } else { 963 String uri = idOrUri; 964 CmsUUID id = getIdForUri(cms, idOrUri); 965 result = new CmsVfsFileValueBean(cms.getRequestContext().addSiteRoot(uri), id); 966 } 967 return result; 968 969 } 970 971 /** 972 * Helper method for accessing the property configuration for a single property.<p> 973 * 974 * This method uses the base name of the property to access the property configuration, 975 * i.e. if propName starts with a '#', the part after the '#' will be used as the key for 976 * the property configuration.<p> 977 * 978 * @param propertyConfig the property configuration map 979 * @param propName the name of a property 980 * @return the property configuration for the given property name 981 */ 982 protected static CmsXmlContentProperty getPropertyConfig( 983 Map<String, CmsXmlContentProperty> propertyConfig, 984 String propName) { 985 986 return propertyConfig.get(propName); 987 } 988 989}