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