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, 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.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 javax.servlet.ServletRequest; 076 077import org.apache.commons.logging.Log; 078 079import org.dom4j.Element; 080 081import com.google.common.base.Objects; 082import com.google.common.base.Supplier; 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.getType(), 735 resolver.resolveMacros(property.getWidget()), 736 resolver.resolveMacros(property.getWidgetConfiguration()), 737 property.getRuleRegex(), 738 property.getRuleType(), 739 property.getDefault(), 740 resolver.resolveMacros(property.getNiceName()), 741 resolver.resolveMacros(property.getDescription()), 742 resolver.resolveMacros(property.getError()), 743 property.isPreferFolder() ? "true" : "false"); 744 result.m_visibility = property.m_visibility; 745 return result; 746 } 747 748 /** 749 * Saves the given properties to the given xml element.<p> 750 * 751 * @param cms the current CMS context 752 * @param parentElement the parent xml element 753 * @param properties the properties to save, if there is a list of resources, every entry can be a site path or a UUID 754 * @param propertiesConf the configuration of the properties 755 * @param sort if true, properties will be sorted by map keys via string co 756 */ 757 public static void saveProperties( 758 CmsObject cms, 759 Element parentElement, 760 Map<String, String> properties, 761 Map<String, CmsXmlContentProperty> propertiesConf, 762 boolean sort) { 763 764 // remove old entries 765 for (Object propElement : parentElement.elements(CmsXmlContentProperty.XmlNode.Properties.name())) { 766 parentElement.remove((Element)propElement); 767 } 768 769 // use a sorted map to force a defined order 770 Map<String, String> props; 771 if (sort) { 772 props = new TreeMap<String, String>(properties); 773 } else { 774 props = properties; 775 } 776 777 // create new entries 778 for (Map.Entry<String, String> property : props.entrySet()) { 779 String propName = property.getKey(); 780 String propValue = property.getValue(); 781 if ((propValue == null) || (propValue.length() == 0)) { 782 continue; 783 } 784 // only if the property is configured in the schema we will save it 785 Element propElement = parentElement.addElement(CmsXmlContentProperty.XmlNode.Properties.name()); 786 787 // the property name 788 propElement.addElement(CmsXmlContentProperty.XmlNode.Name.name()).addCDATA(propName); 789 Element valueElement = propElement.addElement(CmsXmlContentProperty.XmlNode.Value.name()); 790 boolean isVfs = false; 791 CmsXmlContentProperty propDef = propertiesConf.get(propName); 792 if (propDef != null) { 793 isVfs = CmsXmlContentProperty.PropType.isVfsList(propDef.getType()); 794 } 795 if (!isVfs) { 796 // string value 797 valueElement.addElement(CmsXmlContentProperty.XmlNode.String.name()).addCDATA(propValue); 798 } else { 799 addFileListPropertyValue(cms, valueElement, propValue); 800 } 801 } 802 } 803 804 /** 805 * Adds the XML for a property value of a property of type 'vfslist' to the DOM.<p> 806 * 807 * @param cms the current CMS context 808 * @param valueElement the element to which the vfslist property value should be added 809 * @param propValue the property value which should be saved 810 */ 811 protected static void addFileListPropertyValue(CmsObject cms, Element valueElement, String propValue) { 812 813 // resource list value 814 Element filelistElem = valueElement.addElement(CmsXmlContentProperty.XmlNode.FileList.name()); 815 for (String strId : CmsStringUtil.splitAsList(propValue, CmsXmlContentProperty.PROP_SEPARATOR)) { 816 try { 817 Element fileValueElem = filelistElem.addElement(CmsXmlContentProperty.XmlNode.Uri.name()); 818 CmsVfsFileValueBean fileValue = getFileValueForIdOrUri(cms, strId); 819 // HACK: here we assume weak relations, but it would be more robust to check it, with smth like: 820 // type = xmlContent.getContentDefinition().getContentHandler().getRelationType(fileValueElem.getPath()); 821 CmsRelationType type = CmsRelationType.XML_WEAK; 822 CmsXmlVfsFileValue.fillEntry(fileValueElem, fileValue.getId(), fileValue.getPath(), type); 823 } catch (CmsException e) { 824 // should never happen 825 LOG.error(e.getLocalizedMessage(), e); 826 } 827 } 828 } 829 830 /** 831 * Converts a string containing zero or more structure ids into a string containing the corresponding VFS paths.<p> 832 * 833 * @param cms the CmsObject to use for the VFS operations 834 * @param value a string representation of a list of ids 835 * 836 * @return a string representation of a list of paths 837 */ 838 protected static String convertIdsToPaths(CmsObject cms, String value) { 839 840 if (value == null) { 841 return null; 842 } 843 // represent vfslists as lists of path in JSON 844 List<String> ids = CmsStringUtil.splitAsList(value, CmsXmlContentProperty.PROP_SEPARATOR); 845 List<String> paths = new ArrayList<String>(); 846 for (String id : ids) { 847 try { 848 String path = getUriForId(cms, new CmsUUID(id)); 849 paths.add(path); 850 } catch (Exception e) { 851 // should never happen 852 LOG.error(e.getLocalizedMessage(), e); 853 continue; 854 } 855 } 856 return CmsStringUtil.listAsString(paths, CmsXmlContentProperty.PROP_SEPARATOR); 857 } 858 859 /** 860 * Converts a string containing zero or more VFS paths into a string containing the corresponding structure ids.<p> 861 * 862 * @param cms the CmsObject to use for the VFS operations 863 * @param value a string representation of a list of paths 864 * 865 * @return a string representation of a list of ids 866 */ 867 protected static String convertPathsToIds(CmsObject cms, String value) { 868 869 if (value == null) { 870 return null; 871 } 872 // represent vfslists as lists of path in JSON 873 List<String> paths = CmsStringUtil.splitAsList(value, CmsXmlContentProperty.PROP_SEPARATOR); 874 List<String> ids = new ArrayList<String>(); 875 for (String path : paths) { 876 try { 877 CmsUUID id = getIdForUri(cms, path); 878 ids.add(id.toString()); 879 } catch (CmsException e) { 880 // should never happen 881 LOG.error(e.getLocalizedMessage(), e); 882 continue; 883 } 884 } 885 return CmsStringUtil.listAsString(ids, CmsXmlContentProperty.PROP_SEPARATOR); 886 } 887 888 /** 889 * Helper method for converting a map of properties from client format to server format or vice versa.<p> 890 * 891 * @param cms the CmsObject to use for VFS operations 892 * @param props the map of properties 893 * @param propConfig the property configuration 894 * @param toClient if true, convert from server to client, else from client to server 895 * 896 * @return the converted property map 897 */ 898 protected static Map<String, String> convertProperties( 899 CmsObject cms, 900 Map<String, String> props, 901 Map<String, CmsXmlContentProperty> propConfig, 902 boolean toClient) { 903 904 Map<String, String> result = new HashMap<String, String>(); 905 for (Map.Entry<String, String> entry : props.entrySet()) { 906 String propName = entry.getKey(); 907 String propValue = entry.getValue(); 908 String type = "string"; 909 CmsXmlContentProperty configEntry = getPropertyConfig(propConfig, propName); 910 if (configEntry != null) { 911 type = configEntry.getType(); 912 } 913 String newValue = convertStringPropertyValue(cms, propValue, type, toClient); 914 result.put(propName, newValue); 915 } 916 return result; 917 } 918 919 /** 920 * Converts a property value given as a string between server format and client format.<p> 921 * 922 * @param cms the current CMS context 923 * @param propValue the property value to convert 924 * @param type the type of the property 925 * @param toClient if true, convert to client format, else convert to server format 926 * 927 * @return the converted property value 928 */ 929 protected static String convertStringPropertyValue(CmsObject cms, String propValue, String type, boolean toClient) { 930 931 if (propValue == null) { 932 return null; 933 } 934 if (toClient) { 935 return CmsXmlContentPropertyHelper.getPropValuePaths(cms, type, propValue); 936 } else { 937 return CmsXmlContentPropertyHelper.getPropValueIds(cms, type, propValue); 938 } 939 } 940 941 /** 942 * Given a string which might be a id or a (sitemap or VFS) URI, this method will return 943 * a bean containing the right (sitemap or vfs) root path and (sitemap entry or structure) id.<p> 944 * 945 * @param cms the current CMS context 946 * @param idOrUri a string containing an id or an URI 947 * 948 * @return a bean containing a root path and an id 949 * 950 * @throws CmsException if something goes wrong 951 */ 952 protected static CmsVfsFileValueBean getFileValueForIdOrUri(CmsObject cms, String idOrUri) throws CmsException { 953 954 CmsVfsFileValueBean result; 955 if (CmsUUID.isValidUUID(idOrUri)) { 956 CmsUUID id = new CmsUUID(idOrUri); 957 String uri = getUriForId(cms, id); 958 result = new CmsVfsFileValueBean(cms.getRequestContext().addSiteRoot(uri), id); 959 } else { 960 String uri = idOrUri; 961 CmsUUID id = getIdForUri(cms, idOrUri); 962 result = new CmsVfsFileValueBean(cms.getRequestContext().addSiteRoot(uri), id); 963 } 964 return result; 965 966 } 967 968 /** 969 * Helper method for accessing the property configuration for a single property.<p> 970 * 971 * This method uses the base name of the property to access the property configuration, 972 * i.e. if propName starts with a '#', the part after the '#' will be used as the key for 973 * the property configuration.<p> 974 * 975 * @param propertyConfig the property configuration map 976 * @param propName the name of a property 977 * @return the property configuration for the given property name 978 */ 979 protected static CmsXmlContentProperty getPropertyConfig( 980 Map<String, CmsXmlContentProperty> propertyConfig, 981 String propName) { 982 983 return propertyConfig.get(propName); 984 } 985 986}