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