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}