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.ade.configuration.formatters;
029
030import org.opencms.ade.configuration.CmsConfigurationReader;
031import org.opencms.ade.configuration.CmsPropertyConfig;
032import org.opencms.configuration.CmsConfigurationException;
033import org.opencms.file.CmsObject;
034import org.opencms.file.CmsResource;
035import org.opencms.file.types.CmsResourceTypeFunctionConfig;
036import org.opencms.file.types.I_CmsResourceType;
037import org.opencms.i18n.CmsLocaleManager;
038import org.opencms.jsp.util.CmsFunctionRenderer;
039import org.opencms.jsp.util.CmsMacroFormatterResolver;
040import org.opencms.main.CmsException;
041import org.opencms.main.CmsLog;
042import org.opencms.main.OpenCms;
043import org.opencms.relations.CmsLink;
044import org.opencms.util.CmsStringUtil;
045import org.opencms.util.CmsUUID;
046import org.opencms.xml.containerpage.CmsFlexFormatterBean;
047import org.opencms.xml.containerpage.CmsFormatterBean;
048import org.opencms.xml.containerpage.CmsFunctionFormatterBean;
049import org.opencms.xml.containerpage.CmsMacroFormatterBean;
050import org.opencms.xml.containerpage.CmsMetaMapping;
051import org.opencms.xml.containerpage.I_CmsFormatterBean;
052import org.opencms.xml.content.CmsXmlContent;
053import org.opencms.xml.content.CmsXmlContentProperty;
054import org.opencms.xml.content.CmsXmlContentRootLocation;
055import org.opencms.xml.content.I_CmsXmlContentLocation;
056import org.opencms.xml.content.I_CmsXmlContentValueLocation;
057import org.opencms.xml.types.CmsXmlVfsFileValue;
058import org.opencms.xml.types.I_CmsXmlContentValue;
059
060import java.util.ArrayList;
061import java.util.Collections;
062import java.util.HashMap;
063import java.util.HashSet;
064import java.util.LinkedHashMap;
065import java.util.List;
066import java.util.Locale;
067import java.util.Map;
068import java.util.Set;
069
070import org.apache.commons.logging.Log;
071
072import com.google.common.collect.ArrayListMultimap;
073import com.google.common.collect.Lists;
074
075/**
076 * Parses formatter beans from formatter configuration XML contents.<p>
077 */
078public class CmsFormatterBeanParser {
079
080    /**
081     * Exception for the errors in the configuration file not covered by other exception types.<p>
082     */
083    public static class ParseException extends Exception {
084
085        /** Serial version id. */
086        private static final long serialVersionUID = 1L;
087
088        /**
089         * Creates a new exception.<p>
090         *
091         * @param message the error message
092         */
093        public ParseException(String message) {
094
095            super(message);
096        }
097
098        /**
099         * Creates a new exception.<p>
100         *
101         * @param message the error message
102         * @param cause the cause
103         */
104        public ParseException(String message, Throwable cause) {
105
106            super(message, cause);
107        }
108    }
109
110    /** Content value node name. */
111    public static final String N_ALLOWS_SETTINGS_IN_EDITOR = "AllowsSettingsInEditor";
112
113    /** Content value node name. */
114    public static final String N_ATTRIBUTE = "Attribute";
115
116    /** Content value node name. */
117    public static final String N_AUTO_ENABLED = "AutoEnabled";
118
119    /** Content value node name. */
120    public static final String N_CHOICE_NEW_LINK = "ChoiceNewLink";
121
122    /** Content value node name. */
123    public static final String N_CONTAINER_TYPE = "ContainerType";
124
125    /** Content value node name. */
126    public static final String N_CSS_INLINE = "CssInline";
127
128    /** Content value node name. */
129    public static final String N_CSS_LINK = "CssLink";
130
131    /** Content value node name. */
132    public static final String N_DEFAULT = "Default";
133
134    /** Content value node name. */
135    public static final String N_DEFAULT_CONTENT = "DefaultContent";
136
137    /** Content value node name. */
138    public static final String N_DESCRIPTION = "Description";
139
140    /** Content value node name. */
141    public static final String N_DETAIL = "Detail";
142
143    /** Content value node name. */
144    public static final String N_DISPLAY = "Display";
145
146    /** Content value node name. */
147    public static final String N_ELEMENT = "Element";
148
149    /** Node name. */
150    public static final String N_FORMATTER = "Formatter";
151
152    /** Node name. */
153    public static final String N_FORMATTERS = "Formatters";
154
155    /** Content value node name. */
156    public static final String N_HEAD_INCLUDE_CSS = "HeadIncludeCss";
157
158    /** Content value node name. */
159    public static final String N_HEAD_INCLUDE_JS = "HeadIncludeJs";
160
161    /** Content value node name. */
162    public static final String N_INCLUDE_SETTINGS = "IncludeSettings";
163
164    /** Content value node name. */
165    public static final String N_JAVASCRIPT_INLINE = "JavascriptInline";
166
167    /** Content value node name. */
168    public static final String N_JAVASCRIPT_LINK = "JavascriptLink";
169
170    /** Content value node name. */
171    public static final String N_JSP = "Jsp";
172
173    /** Content value node name. */
174    public static final String N_KEY = "Key";
175
176    /** Node name. */
177    public static final String N_MACRO = "Macro";
178
179    /** Node name. */
180    public static final String N_MACRO_NAME = "MacroName";
181
182    /** Content value node name. */
183    public static final String N_MATCH = "Match";
184
185    /** Content value node name. */
186    public static final String N_MAX_WIDTH = "MaxWidth";
187
188    /** Content value node name. */
189    public static final String N_META_MAPPING = "MetaMapping";
190
191    /** Content value node name. */
192    public static final String N_NESTED_FORMATTER_SETTINGS = "NestedFormatterSettings";
193
194    /** Content value node name. */
195    public static final String N_NICE_NAME = "NiceName";
196
197    /** Content value node name. */
198    public static final String N_ORDER = "Order";
199
200    /** Content value node name. */
201    public static final String N_PARAMETER = "Parameter";
202
203    /** Content value node name. */
204    public static final String N_PLACEHOLDER_MACRO = "PlaceholderMacro";
205
206    /** Node name. */
207    public static final String N_PLACEHOLDER_STRING_TEMPLATE = "PlaceholderStringTemplate";
208
209    /** Content value node name. */
210    public static final String N_PREVIEW = "Preview";
211
212    /** Content value node name. */
213    public static final String N_RANK = "Rank";
214
215    /** Content value node name. */
216    public static final String N_SEARCH_CONTENT = "SearchContent";
217
218    /** Content value node name. */
219    public static final String N_SETTING = "Setting";
220
221    /** Content value node name. */
222    public static final String N_STRICT_CONTAINERS = "StrictContainers";
223
224    /** Node name. */
225    public static final String N_STRING_TEMPLATE = "StringTemplate";
226
227    /** Content value node name. */
228    public static final String N_TYPE = "Type";
229
230    /** Content value node name. */
231    public static final String N_TYPES = "Types";
232
233    /** Node name for the 'use meta mappings for normal elements' check box. */
234    public static final String N_USE_META_MAPPINGS_FOR_NORMAL_ELEMENTS = "AlwaysApplyMetaMappings";
235
236    /** Content value node name. */
237    public static final String N_VALUE = "Value";
238
239    /** Content value node name. */
240    public static final String N_WIDTH = "Width";
241
242    /** The key for the setting display type. */
243    public static final String SETTING_DISPLAY_TYPE = "displayType";
244
245    /** The logger instance for this class. */
246    private static final Log LOG = CmsLog.getLog(CmsFormatterBeanParser.class);
247
248    /** Parsed field. */
249    int m_width;
250
251    /** Additional setting configurations for includes. */
252    private Map<CmsUUID, List<CmsXmlContentProperty>> m_additionalSettingConfigs = new HashMap<>();
253
254    /** Parsed field. */
255    private boolean m_autoEnabled;
256
257    /** The CMS object used for parsing. */
258    private CmsObject m_cms;
259
260    /** Parsed field. */
261    private Set<String> m_containerTypes;
262
263    /** Parsed field. */
264    private List<String> m_cssPaths = new ArrayList<String>();
265
266    /** Parsed field. */
267    private boolean m_extractContent;
268
269    /** Parsed field. */
270    private CmsResource m_formatterResource;
271
272    /** Parsed field. */
273    private StringBuffer m_inlineCss = new StringBuffer();
274
275    /** Parsed field. */
276    private StringBuffer m_inlineJs = new StringBuffer();
277
278    /** Parsed field. */
279    private List<String> m_jsPaths = new ArrayList<String>();
280
281    /** Parsed field. */
282    private int m_maxWidth;
283
284    /** Parsed field. */
285    private String m_niceName;
286
287    /** Parsed field. */
288    private boolean m_preview;
289
290    /** Parsed field. */
291    private int m_rank;
292
293    /** Parsed field. */
294    private Set<String> m_resourceType;
295
296    /** Setting configurations read from content. **/
297    private List<CmsXmlContentProperty> m_settingList = new ArrayList<>();
298
299    /** Settings merged with included settings. */
300    private Map<String, CmsXmlContentProperty> m_settings = new HashMap<>();
301
302    /**
303     * Creates a new parser instance.<p>
304     *
305     * A  new parser instance should be created for every formatter configuration you want to parse.<p>
306     *
307     * @param cms the CMS context to use for parsing
308     * @param settingConfigs the additional setting configurations used for includes
309     */
310    public CmsFormatterBeanParser(CmsObject cms, Map<CmsUUID, List<CmsXmlContentProperty>> settingConfigs) {
311
312        m_cms = cms;
313        m_additionalSettingConfigs = settingConfigs;
314    }
315
316    /**
317     * Creates an xpath from the given components.<p>
318     *
319     * @param components the xpath componentns
320     *
321     * @return the composed xpath
322     */
323    public static String path(String... components) {
324
325        return CmsStringUtil.joinPaths(components);
326    }
327
328    /**
329     * Reads the formatter bean from the given XML content.<p>
330     *
331     * @param content the formatter configuration XML content
332     * @param location a string indicating the location of the configuration
333     * @param id the id to use as the formatter id
334     *
335     * @return the parsed formatter bean
336     *
337     * @throws ParseException if parsing goes wrong
338     * @throws CmsException if something else goes wrong
339     */
340    public I_CmsFormatterBean parse(CmsXmlContent content, String location, String id)
341    throws CmsException, ParseException {
342
343        String path = content.getFile().getRootPath();
344        I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(content.getFile());
345        boolean isMacroFromatter = CmsFormatterConfigurationCache.TYPE_MACRO_FORMATTER.equals(type.getTypeName());
346        boolean isFlexFormatter = CmsFormatterConfigurationCache.TYPE_FLEX_FORMATTER.equals(type.getTypeName());
347        boolean isFunction = OpenCms.getResourceManager().matchResourceType(
348            CmsResourceTypeFunctionConfig.TYPE_NAME,
349            content.getFile().getTypeId());
350
351        Locale en = Locale.ENGLISH;
352        I_CmsXmlContentValue niceName = content.getValue(N_NICE_NAME, en);
353        m_niceName = niceName != null ? niceName.getStringValue(m_cms) : null;
354        CmsXmlContentRootLocation root = new CmsXmlContentRootLocation(content, en);
355        I_CmsXmlContentValueLocation rankLoc = root.getSubValue(N_RANK);
356        if (rankLoc != null) {
357            String rankStr = rankLoc.getValue().getStringValue(m_cms);
358            if (rankStr != null) {
359                rankStr = rankStr.trim();
360            }
361            int rank;
362            try {
363                rank = Integer.parseInt(rankStr);
364            } catch (NumberFormatException e) {
365                rank = CmsFormatterBean.DEFAULT_CONFIGURATION_RANK;
366                LOG.debug("Error parsing formatter rank.", e);
367            }
368            m_rank = rank;
369        }
370
371        m_resourceType = getStringSet(root, N_TYPE);
372        parseSettings(root);
373        List<I_CmsXmlContentValue> settingIncludes = content.getValues(N_INCLUDE_SETTINGS, en);
374        settingIncludes = Lists.reverse(settingIncludes); // make defaults from earlier include files 'win' when merging them into a map
375        Map<String, CmsXmlContentProperty> includesByIncludeName = new HashMap<>();
376        for (I_CmsXmlContentValue settingInclude : settingIncludes) {
377            try {
378                CmsXmlVfsFileValue includeFileVal = (CmsXmlVfsFileValue)settingInclude;
379                CmsUUID includeSettingsId = includeFileVal.getLink(m_cms).getStructureId();
380                List<CmsXmlContentProperty> includedSettings = m_additionalSettingConfigs.get(includeSettingsId);
381                if (includedSettings == null) {
382                    continue;
383                }
384                for (CmsXmlContentProperty prop : includedSettings) {
385                    String includeName = prop.getIncludeName(prop.getName());
386                    if (includeName != null) {
387                        CmsXmlContentProperty existingProp = includesByIncludeName.get(includeName);
388                        if (existingProp != null) {
389                            LOG.warn("Conflict with included setting configuration: " + path);
390                        }
391                        includesByIncludeName.put(includeName, prop);
392                    }
393                }
394            } catch (Exception e) {
395                LOG.error(e.getLocalizedMessage(), e);
396            }
397        }
398
399        Map<String, CmsXmlContentProperty> mergedSettings = new LinkedHashMap<>();
400
401        for (CmsXmlContentProperty setting : m_settingList) {
402            String includeName = setting.getIncludeName(setting.getName());
403            if (includeName == null) {
404                LOG.warn("Neither name nor include name given in setting definition in " + path);
405                continue;
406            }
407            CmsXmlContentProperty defaultSetting = includesByIncludeName.get(includeName);
408            CmsXmlContentProperty mergedSetting;
409            if (defaultSetting != null) {
410                mergedSetting = setting.mergeDefaults(defaultSetting);
411            } else {
412                mergedSetting = setting;
413            }
414            if (mergedSetting.getName() == null) {
415                LOG.warn("Invalid setting without name in " + path);
416                continue;
417            }
418            mergedSettings.put(mergedSetting.getName(), mergedSetting);
419        }
420        m_settings = mergedSettings;
421
422        String isDetailStr = getString(root, N_DETAIL, "false");
423        boolean isDetail = Boolean.parseBoolean(isDetailStr);
424
425        String displayType = getString(root, N_DISPLAY, null);
426        if (CmsStringUtil.isEmptyOrWhitespaceOnly(displayType) || "false".equals(displayType)) {
427            displayType = null;
428        } else if (!m_settings.containsKey(SETTING_DISPLAY_TYPE)) {
429            m_settings.put(
430                SETTING_DISPLAY_TYPE,
431                new CmsXmlContentProperty(
432                    SETTING_DISPLAY_TYPE,
433                    "string",
434                    "hidden",
435                    null,
436                    null,
437                    null,
438                    displayType,
439                    null,
440                    null,
441                    null,
442                    null));
443        }
444
445        String isAllowSettingsStr = getString(root, N_ALLOWS_SETTINGS_IN_EDITOR, "false");
446        boolean isAllowSettings = Boolean.parseBoolean(isAllowSettingsStr);
447
448        String isStrictContainersStr = getString(root, N_STRICT_CONTAINERS, "false");
449        boolean isStrictContainers = Boolean.parseBoolean(isStrictContainersStr);
450
451        String description = getString(root, N_DESCRIPTION, null);
452
453        String autoEnabled = getString(root, N_AUTO_ENABLED, "false");
454        m_autoEnabled = Boolean.parseBoolean(autoEnabled);
455
456        String nestedFormatterSettings = getString(root, N_NESTED_FORMATTER_SETTINGS, "false");
457        boolean nestedFormatters = Boolean.parseBoolean(nestedFormatterSettings);
458
459        String useMetaMappinsForNormalElementsStr = getString(root, N_USE_META_MAPPINGS_FOR_NORMAL_ELEMENTS, "false");
460        boolean useMetaMappingsForNormalElements = Boolean.parseBoolean(useMetaMappinsForNormalElementsStr);
461
462        // Functions which just have been created don't have any matching rules, but should fit anywhere
463        boolean strictMode = !isFunction;
464        parseMatch(root, strictMode);
465
466        List<CmsMetaMapping> mappings = parseMetaMappings(root);
467        Map<String, String> attributes = parseAttributes(root);
468
469        I_CmsFormatterBean formatterBean;
470        if (isMacroFromatter || isFlexFormatter) {
471            // setting macro formatter defaults
472            m_formatterResource = content.getFile();
473            m_preview = false;
474            m_extractContent = true;
475            CmsResource defContentRes = null;
476            I_CmsXmlContentValueLocation defContentLoc = root.getSubValue(N_DEFAULT_CONTENT);
477            if (defContentLoc != null) {
478                CmsXmlVfsFileValue defContentValue = (CmsXmlVfsFileValue)(defContentLoc.getValue());
479                CmsLink defContentLink = defContentValue.getLink(m_cms);
480                if (defContentLink != null) {
481                    CmsUUID defContentID = defContentLink.getStructureId();
482                    defContentRes = m_cms.readResource(defContentID);
483                }
484            }
485            if (isMacroFromatter) {
486                String macroInput = getString(root, N_MACRO, "");
487                String placeholderMacroInput = getString(root, N_PLACEHOLDER_MACRO, "");
488                Map<String, CmsUUID> referencedFormatters = readReferencedFormatters(content);
489                formatterBean = new CmsMacroFormatterBean(
490                    m_containerTypes,
491                    m_formatterResource.getRootPath(),
492                    m_formatterResource.getStructureId(),
493                    m_width,
494                    m_maxWidth,
495                    m_extractContent,
496                    location,
497                    m_niceName,
498                    description,
499                    m_resourceType,
500                    m_rank,
501                    id,
502                    defContentRes != null ? defContentRes.getRootPath() : null,
503                    defContentRes != null ? defContentRes.getStructureId() : null,
504                    m_settings,
505                    m_autoEnabled,
506                    isDetail,
507                    displayType,
508                    isAllowSettings,
509                    macroInput,
510                    placeholderMacroInput,
511                    referencedFormatters,
512                    m_cms.getRequestContext().getCurrentProject().isOnlineProject(),
513                    mappings,
514                    useMetaMappingsForNormalElements);
515            } else {
516                String stringTemplate = getString(root, N_STRING_TEMPLATE, "");
517                String placeholder = getString(root, N_PLACEHOLDER_STRING_TEMPLATE, "");
518                formatterBean = new CmsFlexFormatterBean(
519                    m_containerTypes,
520                    m_formatterResource.getRootPath(),
521                    m_formatterResource.getStructureId(),
522                    m_width,
523                    m_maxWidth,
524                    m_extractContent,
525                    location,
526                    m_niceName,
527                    description,
528                    m_resourceType,
529                    m_rank,
530                    id,
531                    defContentRes != null ? defContentRes.getRootPath() : null,
532                    defContentRes != null ? defContentRes.getStructureId() : null,
533                    m_settings,
534                    m_autoEnabled,
535                    isDetail,
536                    displayType,
537                    isAllowSettings,
538                    stringTemplate,
539                    placeholder,
540                    mappings,
541                    useMetaMappingsForNormalElements);
542            }
543        } else {
544            I_CmsXmlContentValueLocation jspLoc = root.getSubValue(N_JSP);
545            CmsXmlVfsFileValue jspValue = (CmsXmlVfsFileValue)(jspLoc.getValue());
546            CmsLink link = jspValue.getLink(m_cms);
547
548            CmsUUID jspID = null;
549            if (link == null) {
550                if (isFunction) {
551                    CmsResource defaultFormatter = CmsFunctionRenderer.getDefaultFunctionJsp(m_cms);
552                    jspID = defaultFormatter.getStructureId();
553                } else {
554                    // JSP link is not set (for example because the formatter configuration has just been created)
555                    LOG.info("JSP link is null in formatter configuration: " + content.getFile().getRootPath());
556                    return null;
557                }
558            } else {
559                jspID = link.getStructureId();
560            }
561
562            if (jspID == null) {
563                throw new CmsConfigurationException(
564                    org.opencms.main.Messages.get().container(
565                        org.opencms.main.Messages.ERR_READ_FORMATTER_CONFIG_4,
566                        new Object[] {
567                            link != null ? link.getUri() : " ??? ",
568                            m_niceName,
569                            location,
570                            "" + m_resourceType}));
571            }
572
573            CmsResource formatterRes = m_cms.readResource(jspID);
574            m_formatterResource = formatterRes;
575            String previewStr = getString(root, N_PREVIEW, "false");
576            m_preview = Boolean.parseBoolean(previewStr);
577
578            String searchableStr = getString(root, N_SEARCH_CONTENT, "true");
579            m_extractContent = Boolean.parseBoolean(searchableStr);
580            parseHeadIncludes(root);
581            if (isFunction) {
582                CmsResource functionFormatter = m_cms.readResource(CmsResourceTypeFunctionConfig.FORMATTER_PATH);
583                Map<String, String[]> rparams = parseParams(root);
584                formatterBean = new CmsFunctionFormatterBean(
585                    m_containerTypes,
586                    m_formatterResource.getRootPath(),
587                    m_formatterResource.getStructureId(),
588                    functionFormatter.getStructureId(),
589                    m_width,
590                    m_maxWidth,
591                    location,
592                    m_cssPaths,
593                    m_inlineCss.toString(),
594                    m_jsPaths,
595                    m_inlineJs.toString(),
596                    m_niceName,
597                    description,
598                    id,
599                    m_settings,
600                    isAllowSettings,
601                    isStrictContainers,
602                    rparams);
603            } else {
604                formatterBean = new CmsFormatterBean(
605                    m_containerTypes,
606                    m_formatterResource.getRootPath(),
607                    m_formatterResource.getStructureId(),
608                    m_width,
609                    m_maxWidth,
610                    m_preview,
611                    m_extractContent,
612                    location,
613                    m_cssPaths,
614                    m_inlineCss.toString(),
615                    m_jsPaths,
616                    m_inlineJs.toString(),
617                    m_niceName,
618                    description,
619                    m_resourceType,
620                    m_rank,
621                    id,
622                    m_settings,
623                    true,
624                    m_autoEnabled,
625                    isDetail,
626                    displayType,
627                    isAllowSettings,
628                    isStrictContainers,
629                    nestedFormatters,
630                    mappings,
631                    attributes,
632                    useMetaMappingsForNormalElements);
633            }
634        }
635
636        return formatterBean;
637    }
638
639    /**
640     * Gets an XML string value.<p>
641     *
642     * @param val the location of the parent value
643     * @param path the path of the sub-value
644     * @param defaultValue the default value to use if no value was found
645     *
646     * @return the found value
647     */
648    private String getString(I_CmsXmlContentLocation val, String path, String defaultValue) {
649
650        if ((val != null)) {
651            I_CmsXmlContentValueLocation subVal = val.getSubValue(path);
652            if ((subVal != null) && (subVal.getValue() != null)) {
653                return subVal.getValue().getStringValue(m_cms);
654            }
655        }
656        return defaultValue;
657    }
658
659    /**
660     * Returns a set of string values.<p>
661     *
662     * @param val the location of the parent value
663     * @param path the path of the sub-values
664     *
665     * @return a set of string values
666     */
667    private Set<String> getStringSet(I_CmsXmlContentLocation val, String path) {
668
669        Set<String> valueSet = new HashSet<String>();
670        if ((val != null)) {
671            List<I_CmsXmlContentValueLocation> singleValueLocs = val.getSubValues(path);
672            for (I_CmsXmlContentValueLocation singleValueLoc : singleValueLocs) {
673                String value = singleValueLoc.getValue().getStringValue(m_cms).trim();
674                valueSet.add(value);
675            }
676        }
677        return valueSet;
678    }
679
680    /**
681     * Parses formatter attributes.
682     *
683     * @param formatterLoc the node location
684     * @return the map of formatter attributes (unmodifiable)
685     */
686    private Map<String, String> parseAttributes(I_CmsXmlContentLocation formatterLoc) {
687
688        Map<String, String> result = new LinkedHashMap<>();
689        for (I_CmsXmlContentValueLocation mappingLoc : formatterLoc.getSubValues(N_ATTRIBUTE)) {
690            String key = CmsConfigurationReader.getString(m_cms, mappingLoc.getSubValue(N_KEY));
691            String value = CmsConfigurationReader.getString(m_cms, mappingLoc.getSubValue(N_VALUE));
692            result.put(key, value);
693        }
694        return Collections.unmodifiableMap(result);
695    }
696
697    /**
698     * Parses the head includes.<p>
699     *
700     * @param formatterLoc the parent value location
701     */
702    private void parseHeadIncludes(I_CmsXmlContentLocation formatterLoc) {
703
704        I_CmsXmlContentValueLocation headIncludeCss = formatterLoc.getSubValue(N_HEAD_INCLUDE_CSS);
705        if (headIncludeCss != null) {
706            for (I_CmsXmlContentValueLocation inlineCssLoc : headIncludeCss.getSubValues(N_CSS_INLINE)) {
707                String inlineCss = inlineCssLoc.getValue().getStringValue(m_cms);
708                m_inlineCss.append(inlineCss);
709            }
710
711            for (I_CmsXmlContentValueLocation cssLinkLoc : headIncludeCss.getSubValues(N_CSS_LINK)) {
712                CmsXmlVfsFileValue fileValue = (CmsXmlVfsFileValue)cssLinkLoc.getValue();
713                CmsLink link = fileValue.getLink(m_cms);
714                if (link != null) {
715                    String cssPath = link.getTarget();
716                    m_cssPaths.add(cssPath);
717                }
718            }
719        }
720        I_CmsXmlContentValueLocation headIncludeJs = formatterLoc.getSubValue(N_HEAD_INCLUDE_JS);
721        if (headIncludeJs != null) {
722            for (I_CmsXmlContentValueLocation inlineJsLoc : headIncludeJs.getSubValues(N_JAVASCRIPT_INLINE)) {
723                String inlineJs = inlineJsLoc.getValue().getStringValue(m_cms);
724                m_inlineJs.append(inlineJs);
725            }
726            for (I_CmsXmlContentValueLocation jsLinkLoc : headIncludeJs.getSubValues(N_JAVASCRIPT_LINK)) {
727                CmsXmlVfsFileValue fileValue = (CmsXmlVfsFileValue)jsLinkLoc.getValue();
728                CmsLink link = fileValue.getLink(m_cms);
729                if (link != null) {
730                    String jsPath = link.getTarget();
731                    m_jsPaths.add(jsPath);
732                }
733            }
734        }
735    }
736
737    /**
738     * Parses the matching criteria (container types or widths) for the formatter.<p>
739     *
740     * @param linkFormatterLoc the formatter value location
741     * @param strict if we should throw an error for incomplete match
742     *
743     * @throws ParseException if parsing goes wrong
744     */
745    private void parseMatch(I_CmsXmlContentLocation linkFormatterLoc, boolean strict) throws ParseException {
746
747        Set<String> containerTypes = new HashSet<String>();
748        I_CmsXmlContentValueLocation typesLoc = linkFormatterLoc.getSubValue(path(N_MATCH, N_TYPES));
749        I_CmsXmlContentValueLocation widthLoc = linkFormatterLoc.getSubValue(path(N_MATCH, N_WIDTH));
750        if (typesLoc != null) {
751            List<I_CmsXmlContentValueLocation> singleTypeLocs = typesLoc.getSubValues(N_CONTAINER_TYPE);
752            for (I_CmsXmlContentValueLocation singleTypeLoc : singleTypeLocs) {
753                String containerType = singleTypeLoc.getValue().getStringValue(m_cms).trim();
754                containerTypes.add(containerType);
755            }
756            m_containerTypes = containerTypes;
757        } else if (widthLoc != null) {
758            String widthStr = getString(widthLoc, N_WIDTH, null);
759            String maxWidthStr = getString(widthLoc, N_MAX_WIDTH, null);
760            try {
761                m_width = Integer.parseInt(widthStr);
762            } catch (Exception e) {
763                throw new ParseException("Invalid container width: [" + widthStr + "]", e);
764            }
765            try {
766                m_maxWidth = Integer.parseInt(maxWidthStr);
767            } catch (Exception e) {
768                m_maxWidth = Integer.MAX_VALUE;
769                LOG.debug(maxWidthStr, e);
770            }
771        } else {
772            if (strict) {
773                throw new ParseException("Neither container types nor container widths defined!");
774            } else {
775                m_width = -1;
776                m_maxWidth = Integer.MAX_VALUE;
777            }
778        }
779    }
780
781    /**
782     * Parses the mappings.<p>
783     *
784     * @param formatterLoc the formatter value location
785     *
786     * @return the mappings
787     */
788    private List<CmsMetaMapping> parseMetaMappings(I_CmsXmlContentLocation formatterLoc) {
789
790        List<CmsMetaMapping> mappings = new ArrayList<CmsMetaMapping>();
791        for (I_CmsXmlContentValueLocation mappingLoc : formatterLoc.getSubValues(N_META_MAPPING)) {
792            String key = CmsConfigurationReader.getString(m_cms, mappingLoc.getSubValue(N_KEY));
793            String element = CmsConfigurationReader.getString(m_cms, mappingLoc.getSubValue(N_ELEMENT));
794            String defaultValue = CmsConfigurationReader.getString(m_cms, mappingLoc.getSubValue(N_DEFAULT));
795            String orderStr = CmsConfigurationReader.getString(m_cms, mappingLoc.getSubValue(N_ORDER));
796            int order = 1000;
797            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(orderStr)) {
798                try {
799                    order = Integer.parseInt(orderStr);
800                } catch (NumberFormatException e) {
801                    // nothing to do
802                }
803            }
804            CmsMetaMapping mapping = new CmsMetaMapping(key, element, order, defaultValue);
805            mappings.add(mapping);
806        }
807        return mappings;
808    }
809
810    /**
811     * Parse parameters and put them in a map.<p>
812     *
813     * @param root the location from which to start parsing
814     *
815     * @return the parameter map
816     */
817    private Map<String, String[]> parseParams(I_CmsXmlContentLocation root) {
818
819        // first use multimap for convenience, to group values for the same key,
820        // and then convert to result format
821        ArrayListMultimap<String, String> mmap = ArrayListMultimap.create();
822        for (I_CmsXmlContentLocation location : root.getSubValues(N_PARAMETER)) {
823            String key = location.getSubValue(N_KEY).getValue().getStringValue(m_cms);
824            String value = location.getSubValue(N_VALUE).getValue().getStringValue(m_cms);
825            mmap.put(key, value);
826        }
827        Map<String, String[]> result = new HashMap<>();
828        String[] emptyArray = new String[] {}; // need this for toArray
829        for (String key : mmap.keySet()) {
830            List<String> values = mmap.get(key);
831            String[] valuesArray = values.toArray(emptyArray);
832            result.put(key, valuesArray);
833        }
834        return result;
835
836    }
837
838    /**
839     * Parses the settings.<p>
840     *
841     * @param formatterLoc the formatter value location
842     */
843    private void parseSettings(I_CmsXmlContentLocation formatterLoc) {
844
845        for (I_CmsXmlContentValueLocation settingLoc : formatterLoc.getSubValues(N_SETTING)) {
846            CmsPropertyConfig propConfig = CmsConfigurationReader.parseProperty(m_cms, settingLoc);
847            CmsXmlContentProperty property = propConfig.getPropertyData();
848            m_settingList.add(property);
849        }
850    }
851
852    /**
853     * Reads the referenced formatters.<p>
854     *
855     * @param xmlContent the XML content
856     *
857     * @return the referenced formatters
858     */
859    private Map<String, CmsUUID> readReferencedFormatters(CmsXmlContent xmlContent) {
860
861        Map<String, CmsUUID> result = new LinkedHashMap<String, CmsUUID>();
862        List<I_CmsXmlContentValue> formatters = xmlContent.getValues(
863            CmsMacroFormatterResolver.N_FORMATTERS,
864            CmsLocaleManager.MASTER_LOCALE);
865        for (I_CmsXmlContentValue formatterValue : formatters) {
866            CmsXmlVfsFileValue file = (CmsXmlVfsFileValue)xmlContent.getValue(
867                formatterValue.getPath() + "/" + CmsMacroFormatterResolver.N_FORMATTER,
868                CmsLocaleManager.MASTER_LOCALE);
869            CmsUUID formatterId = file.getLink(m_cms).getStructureId();
870            String macroName = xmlContent.getStringValue(
871                m_cms,
872                formatterValue.getPath() + "/" + CmsMacroFormatterResolver.N_MACRO_NAME,
873                CmsLocaleManager.MASTER_LOCALE);
874            result.put(macroName, formatterId);
875        }
876        return result;
877    }
878
879}