001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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 GmbH & Co. KG, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://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.CmsConfigurationReader;
031import org.opencms.ade.contenteditor.CmsAccessRestrictionInfo;
032import org.opencms.ade.contenteditor.CmsWidgetUtil;
033import org.opencms.configuration.CmsConfigurationManager;
034import org.opencms.configuration.CmsParameterConfiguration;
035import org.opencms.db.log.CmsLogEntry;
036import org.opencms.file.CmsDataAccessException;
037import org.opencms.file.CmsFile;
038import org.opencms.file.CmsGroup;
039import org.opencms.file.CmsObject;
040import org.opencms.file.CmsProperty;
041import org.opencms.file.CmsPropertyDefinition;
042import org.opencms.file.CmsResource;
043import org.opencms.file.CmsResourceFilter;
044import org.opencms.file.CmsUser;
045import org.opencms.file.CmsVfsResourceNotFoundException;
046import org.opencms.i18n.CmsEncoder;
047import org.opencms.i18n.CmsListResourceBundle;
048import org.opencms.i18n.CmsLocaleManager;
049import org.opencms.i18n.CmsMessageContainer;
050import org.opencms.i18n.CmsMessages;
051import org.opencms.i18n.CmsMultiMessages;
052import org.opencms.i18n.CmsMultiMessages.I_KeyFallbackHandler;
053import org.opencms.i18n.CmsResourceBundleLoader;
054import org.opencms.jsp.util.CmsKeyDummyMacroResolver;
055import org.opencms.lock.CmsLock;
056import org.opencms.main.CmsException;
057import org.opencms.main.CmsLog;
058import org.opencms.main.CmsRuntimeException;
059import org.opencms.main.CmsStaticResourceHandler;
060import org.opencms.main.OpenCms;
061import org.opencms.relations.CmsCategory;
062import org.opencms.relations.CmsCategoryService;
063import org.opencms.relations.CmsLink;
064import org.opencms.relations.CmsRelationType;
065import org.opencms.search.fields.CmsGeoCoordinateFieldMapping;
066import org.opencms.search.fields.CmsSearchField;
067import org.opencms.search.fields.CmsSearchFieldMapping;
068import org.opencms.search.fields.CmsSearchFieldMappingType;
069import org.opencms.search.fields.I_CmsSearchFieldMapping;
070import org.opencms.search.galleries.CmsGalleryNameMacroResolver;
071import org.opencms.search.solr.CmsSolrField;
072import org.opencms.security.CmsAccessControlEntry;
073import org.opencms.security.CmsPrincipal;
074import org.opencms.security.CmsRole;
075import org.opencms.security.I_CmsPrincipal;
076import org.opencms.site.CmsSite;
077import org.opencms.util.CmsDefaultSet;
078import org.opencms.util.CmsFileUtil;
079import org.opencms.util.CmsHtmlConverter;
080import org.opencms.util.CmsMacroResolver;
081import org.opencms.util.CmsPair;
082import org.opencms.util.CmsStringUtil;
083import org.opencms.util.CmsUUID;
084import org.opencms.util.I_CmsMacroResolver;
085import org.opencms.widgets.CmsCategoryWidget;
086import org.opencms.widgets.CmsDisplayWidget;
087import org.opencms.widgets.I_CmsComplexWidget;
088import org.opencms.widgets.I_CmsWidget;
089import org.opencms.workplace.CmsWorkplace;
090import org.opencms.workplace.editors.CmsXmlContentWidgetVisitor;
091import org.opencms.workplace.editors.directedit.I_CmsEditHandler;
092import org.opencms.xml.CmsXmlContentDefinition;
093import org.opencms.xml.CmsXmlEntityResolver;
094import org.opencms.xml.CmsXmlException;
095import org.opencms.xml.CmsXmlGenericWrapper;
096import org.opencms.xml.CmsXmlUtils;
097import org.opencms.xml.containerpage.CmsFormatterBean;
098import org.opencms.xml.containerpage.CmsFormatterConfiguration;
099import org.opencms.xml.containerpage.CmsSchemaFormatterBeanWrapper;
100import org.opencms.xml.containerpage.I_CmsFormatterBean;
101import org.opencms.xml.content.CmsGeoMappingConfiguration.Entry;
102import org.opencms.xml.content.CmsGeoMappingConfiguration.EntryType;
103import org.opencms.xml.content.CmsMappingResolutionContext.AttributeType;
104import org.opencms.xml.types.CmsXmlAccessRestrictionValue;
105import org.opencms.xml.types.CmsXmlCategoryValue;
106import org.opencms.xml.types.CmsXmlDisplayFormatterValue;
107import org.opencms.xml.types.CmsXmlDynamicCategoryValue;
108import org.opencms.xml.types.CmsXmlNestedContentDefinition;
109import org.opencms.xml.types.CmsXmlStringValue;
110import org.opencms.xml.types.CmsXmlVarLinkValue;
111import org.opencms.xml.types.CmsXmlVfsFileValue;
112import org.opencms.xml.types.I_CmsXmlContentValue;
113import org.opencms.xml.types.I_CmsXmlContentValue.CmsSearchContentConfig;
114import org.opencms.xml.types.I_CmsXmlContentValue.SearchContentType;
115import org.opencms.xml.types.I_CmsXmlSchemaType;
116import org.opencms.xml.types.I_CmsXmlValidateWithMessage;
117
118import java.io.IOException;
119import java.io.InputStream;
120import java.util.ArrayList;
121import java.util.Arrays;
122import java.util.Collection;
123import java.util.Collections;
124import java.util.HashMap;
125import java.util.HashSet;
126import java.util.Iterator;
127import java.util.LinkedHashMap;
128import java.util.LinkedHashSet;
129import java.util.List;
130import java.util.Locale;
131import java.util.Map;
132import java.util.Set;
133import java.util.TreeSet;
134import java.util.regex.Pattern;
135import java.util.regex.PatternSyntaxException;
136import java.util.stream.Collectors;
137
138import javax.servlet.ServletRequest;
139
140import org.apache.commons.logging.Log;
141
142import org.antlr.stringtemplate.StringTemplate;
143import org.antlr.stringtemplate.StringTemplateGroup;
144import org.dom4j.Document;
145import org.dom4j.DocumentException;
146import org.dom4j.DocumentHelper;
147import org.dom4j.Element;
148import org.dom4j.Node;
149
150import com.google.common.base.Optional;
151import com.google.common.collect.ArrayListMultimap;
152import com.google.common.collect.Lists;
153import com.google.common.collect.Maps;
154import com.google.common.collect.Multimap;
155
156/**
157 * Default implementation for the XML content handler, will be used by all XML contents that do not
158 * provide their own handler.<p>
159 *
160 * @since 6.0.0
161 */
162public class CmsDefaultXmlContentHandler implements I_CmsXmlContentHandler, I_CmsXmlContentVisibilityHandler {
163
164    /**
165     * Enum for IfInvalidRelation field setting values.
166     */
167    public enum InvalidRelationAction {
168        /** Remove the field's parent. */
169        removeParent,
170
171        /** Only remove the field itself. */
172        removeSelf
173    }
174
175    /**
176     * Contains the visibility handler configuration for a content field path.<p>
177     */
178    protected static class VisibilityConfiguration {
179
180        /** The handler instance. */
181        private I_CmsXmlContentVisibilityHandler m_handler;
182
183        /** The handler configuration parameters. */
184        private String m_params;
185
186        /**
187         * Constructor.<p>
188         *
189         * @param handler the handler instance
190         * @param params the handler configuration parameteres
191         */
192        protected VisibilityConfiguration(I_CmsXmlContentVisibilityHandler handler, String params) {
193
194            m_handler = handler;
195            m_params = params;
196        }
197
198        /**
199         * Returns the visibility handler instance.<p>
200         *
201         * @return the handler instance
202         */
203        public I_CmsXmlContentVisibilityHandler getHandler() {
204
205            return m_handler;
206        }
207
208        /**
209         * Returns the visibility handler configuration parameters.<p>
210         *
211         * @return the configuration parameters
212         */
213        public String getParams() {
214
215            return m_params;
216        }
217    }
218
219    /** Enum for field setting element names which are not already defined elsewhere. */
220    enum FieldSettingElems {
221
222        /** Element name. */
223        Agents,
224
225        /** Element name. */
226        Class,
227
228        /** Element name. */
229        DefaultResolveMacros,
230
231        /** Element name. */
232        Display,
233
234        /** Element name. */
235        FieldVisibility,
236
237        /** Element name. */
238        IfInvalidRelation,
239
240        /** Element name. */
241        Invalidate,
242
243        /** Element name. */
244        Mapping,
245
246        /** Element name. */
247        MapTo,
248
249        /** Element name. */
250        NestedFormatter,
251
252        /** Element name. */
253        Params,
254
255        /** Element name. */
256        Relation,
257
258        /** Element name. */
259        Search,
260
261        /** Element name. */
262        Synchronization,
263
264        /** Element name. */
265        Type,
266
267        /** Element name. */
268        UseDefault,
269
270        /** Element name. */
271        Visibility;
272    }
273
274    /**
275     * Callback interface for methods that take an XML element and throw CmsXmlException.<p>
276     */
277    interface I_Callback {
278
279        /**
280         * Callback method.<p>
281         *
282         * @param elem the parameter element
283         * @throws CmsXmlException for XML errors
284         */
285        void accept(Element elem) throws CmsXmlException;
286    }
287
288    /**
289     * Bean for holding information about a single mapping.
290     */
291    private class MappingInfo {
292
293        /** The mapping source. */
294        private String m_source;
295
296        /** The mapping target. */
297        private String m_target;
298
299        /**
300         * Creates a new instance.
301         *
302         * @param source the mapping source
303         * @param target the mapping target
304         */
305        public MappingInfo(String source, String target) {
306
307            super();
308            m_source = source;
309            m_target = target;
310        }
311
312        /**
313         * Checks if the mapping can be used for reverse mapping of availability data.
314         *
315         * @return true if the mapping can be used for reverse availability mapping
316         */
317        public boolean canBeUsedForReverseAvailabilityMapping() {
318
319            return exists() && !isMappingUsingDefault(m_source, m_target) && checkIndexesNotSetOrOne(m_source);
320        }
321
322        /**
323         * Checks if the mapping actually exists.
324         *
325         * @return true if the mapping exists
326         */
327        public boolean exists() {
328
329            return (m_source != null) && (m_target != null);
330        }
331
332        /**
333         * Gets the mapping source.
334         *
335         * @return the mapping source
336         */
337        public String getSource() {
338
339            return m_source;
340        }
341
342        /**
343         * Gets the mapping target.
344         *
345         * @return the mapping target
346         */
347        public String getTarget() {
348
349            return m_target;
350        }
351
352        /**
353         * Checks that the components of an xpath have no indexes or index [1].
354         *
355         * @param xpath the xpath to check
356         *
357         * @return true if all indexes are either [1] or not set
358         */
359        private boolean checkIndexesNotSetOrOne(String xpath) {
360
361            return CmsXmlUtils.splitXpath(xpath).stream().allMatch(
362                component -> indexesNotSetOrOne.contains(CmsXmlUtils.getXpathIndex(component)));
363
364        }
365
366    }
367
368    /** Attribute name for configuration string. */
369    public static final String A_CONFIGURATION = "configuration";
370
371    /** Constant for the "appinfo" element name itself. */
372    public static final String APPINFO_APPINFO = "appinfo";
373
374    /** Constant for the "addto" appinfo attribute name. */
375    public static final String APPINFO_ATTR_ADD_TO = "addto";
376
377    /** Constant for the "boost" appinfo attribute name. */
378    public static final String APPINFO_ATTR_BOOST = "boost";
379
380    /** Constant for the "class" appinfo attribute name. */
381    public static final String APPINFO_ATTR_CLASS = "class";
382
383    /** Constant for the "collapse" appinfo attribute name. */
384    public static final String APPINFO_ATTR_COLLAPSE = "collapse";
385
386    /** Constant for the "configuration" appinfo attribute name. */
387    public static final String APPINFO_ATTR_CONFIGURATION = "configuration";
388
389    /** The exclude from index attribute. */
390    public static final String APPINFO_ATTR_CONTAINER_PAGE_ONLY = "containerPageOnly";
391
392    /** Constant for the "copyfields" appinfo attribute name. */
393    public static final String APPINFO_ATTR_COPY_FIELDS = "copyfields";
394
395    /** Constant for the "default" appinfo attribute name. */
396    public static final String APPINFO_ATTR_DEFAULT = "default";
397
398    /** Constant for the "description" appinfo attribute name. */
399    public static final String APPINFO_ATTR_DESCRIPTION = "description";
400
401    /** Constant for the "displaycompact" appinfo attribute name. */
402    public static final String APPINFO_ATTR_DISPLAY = "display";
403
404    /** Constant for the "element" appinfo attribute name. */
405    public static final String APPINFO_ATTR_ELEMENT = "element";
406
407    /** Constant for the "error" appinfo attribute name. */
408    public static final String APPINFO_ATTR_ERROR = "error";
409
410    /** Constant for the "invalidate" appinfo attribute name. */
411    public static final String APPINFO_ATTR_INVALIDATE = "invalidate";
412
413    /** Constant for the "key" appinfo attribute name. */
414    public static final String APPINFO_ATTR_KEY = "key";
415
416    /** Constant for the "locale" appinfo attribute name. */
417    public static final String APPINFO_ATTR_LOCALE = "locale";
418
419    /** Constant for the "mapping" appinfo attribute name. */
420    public static final String APPINFO_ATTR_MAPPING = "mapping";
421
422    /** Constant for the "mapto" appinfo attribute name. */
423    public static final String APPINFO_ATTR_MAPTO = "mapto";
424
425    /** Constant for the "maxwidth" appinfo attribute name. */
426    public static final String APPINFO_ATTR_MAXWIDTH = "maxwidth";
427
428    /** Constant for the "message" appinfo attribute name. */
429    public static final String APPINFO_ATTR_MESSAGE = "message";
430
431    /** Constant for the "minwidth" appinfo attribute name. */
432    public static final String APPINFO_ATTR_MINWIDTH = "minwidth";
433
434    /** Constant for the "name" appinfo attribute name. */
435    public static final String APPINFO_ATTR_NAME = "name";
436
437    /** Constant for the "nice-name" appinfo attribute name. */
438    public static final String APPINFO_ATTR_NICE_NAME = "nice-name";
439
440    /** Constant for the "params" appinfo attribute name. */
441    public static final String APPINFO_ATTR_PARAMS = "params";
442
443    /** Constant for the "preview" appinfo attribute name. */
444    public static final String APPINFO_ATTR_PREVIEW = "preview";
445
446    /** Constant for the "regex" appinfo attribute name. */
447    public static final String APPINFO_ATTR_REGEX = "regex";
448
449    /** Constant for the "resolveMacros" attribute name. */
450    public static final String APPINFO_ATTR_RESOLVE_MACROS = "resolveMacros";
451
452    /** Constant for the "rule-regex" appinfo attribute name. */
453    public static final String APPINFO_ATTR_RULE_REGEX = "rule-regex";
454
455    /** Constant for the "rule-type" appinfo attribute name. */
456    public static final String APPINFO_ATTR_RULE_TYPE = "rule-type";
457
458    /** Constant for the "scope" appinfo attribute name. */
459    public static final String APPINFO_ATTR_SCOPE = "scope";
460
461    /** Constant for the "searchcontent" appinfo attribute name. */
462    public static final String APPINFO_ATTR_SEARCHCONTENT = "searchcontent";
463
464    /** Constant for the "select-inherit" appinfo attribute name. */
465    public static final String APPINFO_ATTR_SELECT_INHERIT = "select-inherit";
466
467    /** Constant for the "sourcefield" appinfo attribute name. */
468    public static final String APPINFO_ATTR_SOURCE_FIELD = "sourcefield";
469
470    /** Constant for the "targetfield" appinfo attribute name. */
471    public static final String APPINFO_ATTR_TARGET_FIELD = "targetfield";
472
473    /** Constant for the "type" appinfo attribute name. */
474    public static final String APPINFO_ATTR_TYPE = "type";
475
476    /** Constant for the "node" appinfo attribute value. */
477    public static final String APPINFO_ATTR_TYPE_NODE = "node";
478
479    /** Constant for the "parent" appinfo attribute value. */
480    public static final String APPINFO_ATTR_TYPE_PARENT = "parent";
481
482    /** Constant for the "warning" appinfo attribute value. */
483    public static final String APPINFO_ATTR_TYPE_WARNING = "warning";
484
485    /** Constant for the "uri" appinfo attribute name. */
486    public static final String APPINFO_ATTR_URI = "uri";
487
488    /** Constant for the "useall" appinfo attribute name. */
489    public static final String APPINFO_ATTR_USEALL = "useall";
490
491    /** Constant for the "value" appinfo attribute name. */
492    public static final String APPINFO_ATTR_VALUE = "value";
493
494    /** Constant for the "widget" appinfo attribute name. */
495    public static final String APPINFO_ATTR_WIDGET = "widget";
496
497    /** Constant for the "widget-config" appinfo attribute name. */
498    public static final String APPINFO_ATTR_WIDGET_CONFIG = "widget-config";
499
500    /** Constant for formatter include resource type 'CSS'. */
501    public static final String APPINFO_ATTRIBUTE_TYPE_CSS = "css";
502
503    /** Constant for formatter include resource type 'JAVASCRIPT'. */
504    public static final String APPINFO_ATTRIBUTE_TYPE_JAVASCRIPT = "javascript";
505
506    /** Constant for the "bundle" appinfo element name. */
507    public static final String APPINFO_BUNDLE = "bundle";
508
509    /** Constant for the "default" appinfo element name. */
510    public static final String APPINFO_DEFAULT = "default";
511
512    /** Constant for the "defaults" appinfo element name. */
513    public static final String APPINFO_DEFAULTS = "defaults";
514
515    /** Constant for the "edithandler" appinfo element name. */
516    public static final String APPINFO_EDIT_HANDLER = "edithandler";
517
518    /** Constant for the "editorchangehandler" appinfo element name. */
519    public static final String APPINFO_EDITOR_CHANGE_HANDLER = "editorchangehandler";
520
521    /** Constant for the "editorchangehandlers" appinfo element name. */
522    public static final String APPINFO_EDITOR_CHANGE_HANDLERS = "editorchangehandlers";
523
524    /** Constant for the "forbidden-contexts" appinfo attribute name. */
525    public static final String APPINFO_FORBIDDEN_CONTEXTS = "forbidden-contexts";
526
527    /** Constant for the "formatter" appinfo element name. */
528    public static final String APPINFO_FORMATTER = "formatter";
529
530    /** Constant for the "formatters" appinfo element name. */
531    public static final String APPINFO_FORMATTERS = "formatters";
532
533    /** Constant for the 'geomapping' node. */
534    public static final String APPINFO_GEOMAPPING = "geomapping";
535
536    /** Constant for the "headinclude" appinfo element name. */
537    public static final String APPINFO_HEAD_INCLUDE = "headinclude";
538
539    /** Constant for the "headincludes" appinfo element name. */
540    public static final String APPINFO_HEAD_INCLUDES = "headincludes";
541
542    /** Constant for the "layout" appinfo element name. */
543    public static final String APPINFO_LAYOUT = "layout";
544
545    /** Constant for the "layouts" appinfo element name. */
546    public static final String APPINFO_LAYOUTS = "layouts";
547
548    /** Constant for the "mapping" appinfo element name. */
549    public static final String APPINFO_MAPPING = "mapping";
550
551    /** Constant for the "mappings" appinfo element name. */
552    public static final String APPINFO_MAPPINGS = "mappings";
553
554    /** Constant for the 'messagekeyhandler' node. */
555    public static final String APPINFO_MESSAGEKEYHANDLER = "messagekeyhandler";
556
557    /** Constant for the "modelfolder" appinfo element name. */
558    public static final String APPINFO_MODELFOLDER = "modelfolder";
559
560    /** Constant for the "nestedformatter" appinfo element name. */
561    public static final String APPINFO_NESTED_FORMATTER = "nestedformatter";
562
563    /** Constant for the "nestedformatters" appinfo element name. */
564    public static final String APPINFO_NESTED_FORMATTERS = "nestedformatters";
565
566    /** Constant for the "param" appinfo attribute name. */
567    public static final String APPINFO_PARAM = "param";
568
569    /** Constant for the "parameters" appinfo element name. */
570    public static final String APPINFO_PARAMETERS = "parameters";
571
572    /** Constant for the "preview" appinfo element name. */
573    public static final String APPINFO_PREVIEW = "preview";
574
575    /** Constant for the "propertybundle" appinfo element name. */
576    public static final String APPINFO_PROPERTYBUNDLE = "propertybundle";
577
578    /** Constant for the "relation" appinfo element name. */
579    public static final String APPINFO_RELATION = "relation";
580
581    /** Constant for the "relations" appinfo element name. */
582    public static final String APPINFO_RELATIONS = "relations";
583
584    /** Constant for the "resource" appinfo element name. */
585    public static final String APPINFO_RESOURCE = "resource";
586
587    /** Constant for the "resourcebundle" appinfo element name. */
588    public static final String APPINFO_RESOURCEBUNDLE = "resourcebundle";
589
590    /** Constant for the "resourcebundles" appinfo element name. */
591    public static final String APPINFO_RESOURCEBUNDLES = "resourcebundles";
592
593    /** Constant for the reverse-mapping-enabled appinfo element name. */
594    public static final String APPINFO_REVERSE_MAPPING_ENABLED = "reverse-mapping-enabled";
595
596    /** Constant for the "rule" appinfo element name. */
597    public static final String APPINFO_RULE = "rule";
598
599    /** The file where the default appinfo schema is located. */
600    public static final String APPINFO_SCHEMA_FILE = "org/opencms/xml/content/DefaultAppinfo.xsd";
601
602    /** The file where the default appinfo schema types are located. */
603    public static final String APPINFO_SCHEMA_FILE_TYPES = "org/opencms/xml/content/DefaultAppinfoTypes.xsd";
604
605    /** The XML system id for the default appinfo schema types. */
606    public static final String APPINFO_SCHEMA_SYSTEM_ID = CmsConfigurationManager.DEFAULT_DTD_PREFIX
607        + APPINFO_SCHEMA_FILE;
608
609    /** The XML system id for the default appinfo schema types. */
610    public static final String APPINFO_SCHEMA_TYPES_SYSTEM_ID = CmsConfigurationManager.DEFAULT_DTD_PREFIX
611        + APPINFO_SCHEMA_FILE_TYPES;
612
613    /** Constant for the "searchsetting" appinfo element name. */
614    public static final String APPINFO_SEARCHSETTING = "searchsetting";
615
616    /** Constant for the "searchsettings" appinfo element name. */
617    public static final String APPINFO_SEARCHSETTINGS = "searchsettings";
618
619    /** Constant for the "setting" appinfo element name. */
620    public static final String APPINFO_SETTING = "setting";
621
622    /** Constant for the "settings" appinfo element name. */
623    public static final String APPINFO_SETTINGS = "settings";
624
625    /** Constant for the "solrfield" appinfo element name. */
626    public static final String APPINFO_SOLR_FIELD = "solrfield";
627
628    /** Constant for the "synchronization" appinfo element name. */
629    public static final String APPINFO_SYNCHRONIZATION = "synchronization";
630
631    /** Constant for the "synchronizations" appinfo element name. */
632    public static final String APPINFO_SYNCHRONIZATIONS = "synchronizations";
633
634    /** Constant for the "tab" appinfo element name. */
635    public static final String APPINFO_TAB = "tab";
636
637    /** Constant for the "tabs" appinfo element name. */
638    public static final String APPINFO_TABS = "tabs";
639
640    /** Node name. */
641    public static final String APPINFO_TEMPLATE = "template";
642
643    /** Node name. */
644    public static final String APPINFO_TEMPLATES = "templates";
645
646    /** Constant for the "validationrule" appinfo element name. */
647    public static final String APPINFO_VALIDATIONRULE = "validationrule";
648
649    /** Constant for the "validationrules" appinfo element name. */
650    public static final String APPINFO_VALIDATIONRULES = "validationrules";
651
652    /** Constant for the "element" value of the appinfo attribute "addto". */
653    public static final String APPINFO_VALUE_ADD_TO_CONTENT = "element";
654
655    /** Constant for the "page" value of the appinfo attribute "addto". */
656    public static final String APPINFO_VALUE_ADD_TO_PAGE = "page";
657
658    /** version-transformation node name. */
659    public static final String APPINFO_VERSION_TRANSFORMATION = "versiontransformation";
660
661    /** Constant for the "visibilities" appinfo element name. */
662    public static final String APPINFO_VISIBILITIES = "visibilities";
663
664    /** Constant for the "visibility" appinfo element name. */
665    public static final String APPINFO_VISIBILITY = "visibility";
666
667    /** Constant for the "xmlbundle" appinfo element name. */
668    public static final String APPINFO_XMLBUNDLE = "xmlbundle";
669
670    /** Attribute name. */
671    public static final String ATTR_ENABLED = "enabled";
672
673    /** Attribute name. */
674    public static final String ATTR_ENABLED_BY_DEFAULT = "enabledByDefault";
675
676    /** Attribute name. */
677    public static final String ATTR_USE_ACACIA = "useAcacia";
678
679    /** Constant for head include type attribute: CSS. */
680    public static final String ATTRIBUTE_INCLUDE_TYPE_CSS = "css";
681
682    /** Constant for head include type attribute: java-script. */
683    public static final String ATTRIBUTE_INCLUDE_TYPE_JAVASCRIPT = "javascript";
684
685    /** Field for mapping geo-coordinates. */
686    public static final String GEOMAPPING_FIELD = "geocoords_loc";
687
688    /** Macro for resolving the preview URI. */
689    public static final String MACRO_PREVIEW_TEMPFILE = "previewtempfile";
690
691    /** Node name for change handler. */
692    public static final String N_CHANGEHANDLER = "ChangeHandler";
693
694    /** Constant for the 'Setting' node name. */
695    public static final String N_SETTING = "Setting";
696
697    /** Default message for validation errors. */
698    protected static final String MESSAGE_VALIDATION_DEFAULT_ERROR = "${validation.path}: "
699        + "${key."
700        + Messages.GUI_EDITOR_XMLCONTENT_VALIDATION_ERROR_2
701        + "|${validation.value}|[${validation.regex}]}";
702
703    /** Default message for validation warnings. */
704    protected static final String MESSAGE_VALIDATION_DEFAULT_WARNING = "${validation.path}: "
705        + "${key."
706        + Messages.GUI_EDITOR_XMLCONTENT_VALIDATION_WARNING_2
707        + "|${validation.value}|[${validation.regex}]}";
708
709    /** Set of xpath indexes that allow reverse availability mappings. */
710    static Set<String> indexesNotSetOrOne = new HashSet<>(Arrays.asList("", "[1]"));
711
712    /** The attribute name for the "prefer folder" option for properties. */
713    private static final String APPINFO_ATTR_PREFERFOLDER = "PreferFolder";
714
715    /** The 'useDefault' attribute name. */
716    private static final String APPINFO_ATTR_USE_DEFAULT = "useDefault";
717
718    /** The node name for the default complex widget configuration. */
719    private static final Object APPINFO_DEFAULTWIDGET = "defaultwidget";
720
721    /** Node name for the list of field declarations. */
722    private static final Object APPINFO_FIELD_SETTINGS = "FieldSettings";
723
724    /** JSON renderer node name. */
725    private static final Object APPINFO_JSON_RENDERER = "jsonrenderer";
726
727    /** Attribute name for the context used for resolving content mappings. */
728    private static final String ATTR_MAPPING_RESOLUTION_CONTEXT = "MAPPING_RESOLUTION_CONTEXT";
729
730    /** The log object for this class. */
731    private static final Log LOG = CmsLog.getLog(CmsDefaultXmlContentHandler.class);
732
733    /** The principal list separator. */
734    private static final String PRINCIPAL_LIST_SEPARATOR = ",";
735
736    /** The title property individual mapping key. */
737    private static final String TITLE_PROPERTY_INDIVIDUAL_MAPPING = MAPTO_PROPERTY_INDIVIDUAL
738        + CmsPropertyDefinition.PROPERTY_TITLE;
739
740    /** The title property mapping key. */
741    private static final String TITLE_PROPERTY_MAPPING = MAPTO_PROPERTY + CmsPropertyDefinition.PROPERTY_TITLE;
742
743    /** The title property shared mapping key. */
744    private static final String TITLE_PROPERTY_SHARED_MAPPING = MAPTO_PROPERTY_SHARED
745        + CmsPropertyDefinition.PROPERTY_TITLE;
746
747    /**
748     * Static initializer for caching the default appinfo validation schema.<p>
749     */
750    static {
751
752        // the schema definition is located in 2 separates file for easier editing
753        // 2 files are required in case an extended schema want to use the default definitions,
754        // but with an extended "appinfo" node
755        byte[] appinfoSchemaTypes;
756        try {
757            // first read the default types
758            appinfoSchemaTypes = CmsFileUtil.readFile(APPINFO_SCHEMA_FILE_TYPES);
759        } catch (Exception e) {
760            throw new CmsRuntimeException(
761                Messages.get().container(
762                    org.opencms.xml.types.Messages.ERR_XMLCONTENT_LOAD_SCHEMA_1,
763                    APPINFO_SCHEMA_FILE_TYPES),
764                e);
765        }
766        CmsXmlEntityResolver.cacheSystemId(APPINFO_SCHEMA_TYPES_SYSTEM_ID, appinfoSchemaTypes);
767        byte[] appinfoSchema;
768        try {
769            // now read the default base schema
770            appinfoSchema = CmsFileUtil.readFile(APPINFO_SCHEMA_FILE);
771        } catch (Exception e) {
772            throw new CmsRuntimeException(
773                Messages.get().container(
774                    org.opencms.xml.types.Messages.ERR_XMLCONTENT_LOAD_SCHEMA_1,
775                    APPINFO_SCHEMA_FILE),
776                e);
777        }
778        CmsXmlEntityResolver.cacheSystemId(APPINFO_SCHEMA_SYSTEM_ID, appinfoSchema);
779    }
780
781    /** The configured agent tags for fields. */
782    private Map<String, Set<String>> m_agentTags = new HashMap<>();
783
784    /** The set of allowed templates. */
785    protected CmsDefaultSet<String> m_allowedTemplates = new CmsDefaultSet<String>();
786
787    /** The cached map of combined synchronization information. */
788    protected LinkedHashMap<String, SynchronizationMode> m_combinedSynchronizations;
789
790    /** The configuration values for the element widgets (as defined in the annotations). */
791    protected Map<String, String> m_configurationValues;
792
793    /** The CSS resources to include into the html-page head. */
794    protected Set<String> m_cssHeadIncludes;
795
796    /** The default values for the elements (as defined in the annotations). */
797    protected Map<String, String> m_defaultValues;
798
799    /** The element mappings (as defined in the annotations). */
800    protected Map<String, List<String>> m_elementMappings;
801
802    /** The formatter configuration. */
803    protected CmsFormatterConfiguration m_formatterConfiguration;
804
805    /** The list of formatters from the XSD. */
806    protected List<CmsFormatterBean> m_formatters;
807
808    /** The configured geo-coordinate mapping configuration entries. */
809    protected List<CmsGeoMappingConfiguration.Entry> m_geomappingEntries = new ArrayList<>();
810
811    /** Relation actions. */
812    protected Map<String, InvalidRelationAction> m_invalidRelationActions = new HashMap<>();
813
814    /** The java-script resources to include into the html-page head. */
815    protected Set<String> m_jsHeadIncludes;
816
817    /** The resource bundle name to be used for localization of this content handler. */
818    protected List<String> m_messageBundleNames;
819
820    /** The folder containing the model file(s) for the content. */
821    protected String m_modelFolder;
822
823    /** The preview location (as defined in the annotations). */
824    protected String m_previewLocation;
825
826    /** Name of the field used for geo-coordinate mapping. */
827    protected String m_primaryGeomappingField;
828
829    /** The relation check rules. */
830    protected Map<String, Boolean> m_relationChecks;
831
832    /** The relation check rules. */
833    protected Map<String, CmsRelationType> m_relations;
834
835    /** The Solr field configurations. */
836    protected Map<String, CmsSearchField> m_searchFields;
837
838    /** The Solr field configurations added to the container pages contents are on. */
839    protected Map<String, CmsSearchField> m_searchFieldsPage;
840
841    /** The search settings. */
842    protected Map<String, CmsSearchContentConfig> m_searchSettings;
843
844    /** String template group for the simple search setting expansions. */
845    protected StringTemplateGroup m_searchTemplateGroup;
846
847    /** The configured settings for the formatters (as defined in the annotations). */
848    protected Map<String, CmsXmlContentProperty> m_settings;
849
850    /** The configured locale synchronization elements. */
851    protected LinkedHashMap<String, SynchronizationMode> m_synchronizations = new LinkedHashMap<>();
852
853    /** The configured tabs. */
854    protected List<CmsXmlContentTab> m_tabs;
855
856    /** The list of mappings to the "Title" property. */
857    protected List<String> m_titleMappings;
858
859    /** Flag which controls whether the Acacia editor should be disabled for this type. */
860    protected boolean m_useAcacia = true;
861
862    /** The messages for the error validation rules. */
863    protected Map<String, String> m_validationErrorMessages;
864
865    /** The validation rules that cause an error (as defined in the annotations). */
866    protected Map<String, String> m_validationErrorRules;
867
868    /** The messages for the warning validation rules. */
869    protected Map<String, String> m_validationWarningMessages;
870
871    /** The validation rules that cause a warning (as defined in the annotations). */
872    protected Map<String, String> m_validationWarningRules;
873
874    /** Path to XSL transform in VFS to use for version transformation. */
875    protected String m_versionTransformation;
876
877    /** Change handler configurations. */
878    private List<CmsChangeHandlerConfig> m_changeHandlerConfigs = new ArrayList<>();
879
880    /** The container page only flag, indicating if this XML content should be indexed on container pages only. */
881    private boolean m_containerPageOnly;
882
883    /** The content definition for which this content handler is configured. */
884    private CmsXmlContentDefinition m_contentDefinition;
885
886    /** The default complex widget class name. */
887    private String m_defaultWidget;
888
889    /** The default complex widget configuration. */
890    private String m_defaultWidgetConfig;
891
892    /** The default complex widget for this type. */
893    private I_CmsComplexWidget m_defaultWidgetInstance;
894
895    /** The elements to display in ncompact view. */
896    private HashMap<String, DisplayType> m_displayTypes;
897
898    /** An optional edit handler. */
899    private I_CmsEditHandler m_editHandler;
900
901    /** The editor change handlers. */
902    private List<I_CmsXmlContentEditorChangeHandler> m_editorChangeHandlers;
903
904    /** The descriptions for the fields. */
905    private Map<String, String> m_fieldDescriptions = new HashMap<>();
906
907    /** The nice names for the fields. */
908    private Map<String, String> m_fieldNiceNames = new HashMap<>();
909
910    /** Cached boolean indicating whether the content has category widgets. */
911    private volatile Boolean m_hasCategoryWidget;
912
913    /** The JSON renderer settings. */
914    private JsonRendererSettings m_jsonRendererSettings;
915
916    /** A set of keys identifying the mappings which should use default values if the corresponding values are not set in the XML content. */
917    private Set<String> m_mappingsUsingDefault = new HashSet<String>();
918
919    /** Message key fallback handler for the editor. */
920    private CmsMultiMessages.I_KeyFallbackHandler m_messageKeyHandler = new CmsMultiMessages.I_KeyFallbackHandler() {
921
922        public Optional<String> getFallbackKey(String key) {
923
924            return Optional.absent();
925        }
926    };
927
928    /** The nested formatter elements. */
929    private Set<String> m_nestedFormatterElements;
930
931    /** The paths of values for which no macros should be resolved when getting the default value. */
932    private Set<String> m_nonMacroResolvableDefaults = new HashSet<String>();
933
934    /** The parameters. */
935    private CmsParameterConfiguration m_parameters = new CmsParameterConfiguration();
936
937    /** Option to disable reverse mapping for this content type. */
938    private boolean m_reverseMappingEnabled = true;
939
940    /** The visibility configurations by element path. */
941    private Map<String, VisibilityConfiguration> m_visibilityConfigurations = new HashMap<String, VisibilityConfiguration>();
942
943    /** The map of widget names by path. */
944    private Map<String, String> m_widgetNames = new HashMap<>();
945
946    /**
947     * Creates a new instance of the default XML content handler.<p>
948     */
949    public CmsDefaultXmlContentHandler() {
950
951        init();
952    }
953
954    /**
955     * Collects change handler confiugrations for all nested contents.
956     *
957     * @param contentDef the content definition
958     * @param parentPath the parent path
959     * @param result the multimap to collect the handler configurations in, with the key being the path of the nested content in whose schema they are configured
960     */
961    private static void collectNestedChangeHandlerConfigs(
962        CmsXmlContentDefinition contentDef,
963        String parentPath,
964        Multimap<String, CmsChangeHandlerConfig> result) {
965
966        I_CmsXmlContentHandler handler = contentDef.getContentHandler();
967        List<CmsChangeHandlerConfig> handlerConfigs = handler.getChangeHandlerConfigs();
968        for (CmsChangeHandlerConfig handlerConfig : handlerConfigs) {
969            result.put(parentPath, handlerConfig);
970        }
971
972        for (I_CmsXmlSchemaType schemaType : contentDef.getTypeSequence()) {
973            String name = schemaType.getName();
974            if (schemaType instanceof CmsXmlNestedContentDefinition) {
975                CmsXmlNestedContentDefinition nested = (CmsXmlNestedContentDefinition)schemaType;
976                collectNestedChangeHandlerConfigs(nested.getNestedContentDefinition(), parentPath + "/" + name, result);
977            }
978        }
979    }
980
981    /**
982     * Gets the invalid relation action for the given value.
983     * @param value the value
984     * @return the invalid relation action
985     */
986    private static InvalidRelationAction getInvalidRelationActionForValue(I_CmsXmlContentValue value) {
987
988        try {
989            String path = value.getPath();
990            String simpleName = CmsXmlUtils.getLastXpathElement(path);
991            return value.getContentDefinition().getContentHandler().getInvalidRelationAction(simpleName);
992        } catch (Exception e) {
993            LOG.error(e.getLocalizedMessage(), e);
994            return null;
995        }
996    }
997
998    /**
999     * Makes a path suitable for use as a change handler scope by appending wildcards to every path segment.
1000     *
1001     * @param path the path to process
1002     * @return the scope for the editor change handler
1003     */
1004    private static String normalizeChangeHandlerScope(String path) {
1005
1006        List<String> normalizedKeyParts = new ArrayList<>();
1007        // Append wildcard to every path component that doesn't end with a wildcard or index
1008        for (String keyPart : path.split("/")) {
1009            String normalizedKeyPart = null;
1010            if (keyPart.endsWith("*") || keyPart.endsWith("]")) {
1011                normalizedKeyPart = keyPart;
1012            } else {
1013                normalizedKeyPart = keyPart + "*";
1014            }
1015            normalizedKeyParts.add(normalizedKeyPart);
1016        }
1017        String normalizedKey = CmsStringUtil.listAsString(normalizedKeyParts, "/");
1018        return normalizedKey;
1019
1020    }
1021
1022    /**
1023     * @see org.opencms.xml.content.I_CmsXmlContentHandler#applyReverseAvailabilityMapping(org.opencms.file.CmsObject, org.opencms.xml.content.CmsXmlContent, org.opencms.xml.content.CmsMappingResolutionContext.AttributeType, java.util.List, long)
1024     */
1025    public boolean applyReverseAvailabilityMapping(
1026        CmsObject cms,
1027        CmsXmlContent content,
1028        AttributeType attr,
1029        List<Locale> resourceLocales,
1030        long valueToSet) {
1031
1032        MappingInfo info = getAttributeMapping(attr);
1033        if (!info.canBeUsedForReverseAvailabilityMapping()) {
1034            return false;
1035        }
1036
1037        long defaultValue = -1;
1038        switch (attr) {
1039            case expiration:
1040                defaultValue = CmsResource.DATE_EXPIRED_DEFAULT;
1041                break;
1042            case release:
1043                defaultValue = CmsResource.DATE_RELEASED_DEFAULT;
1044                break;
1045            default:
1046                return false;
1047        }
1048        String mappedElement = info.getSource();
1049
1050        I_CmsXmlSchemaType type = m_contentDefinition.getSchemaType(mappedElement);
1051        // change value in the first locale from the list of resource locales that we have in the content
1052        List<Locale> localesToProcess = Collections.emptyList();
1053        for (Locale locale : resourceLocales) {
1054            if (content.hasLocale(locale)) {
1055                localesToProcess = Collections.singletonList(locale);
1056                break;
1057            }
1058        }
1059        if (localesToProcess.size() > 0) {
1060            Locale locale = localesToProcess.get(0);
1061            if (content.hasValue(mappedElement, locale)) {
1062                I_CmsXmlContentValue value = content.getValue(mappedElement, locale);
1063                String stringValue = value.getStringValue(cms);
1064                if (stringValue.contains("%")) {
1065                    LOG.debug(
1066                        content.getFile().getRootPath()
1067                            + ": Didn't apply reverse availability mapping because of macro value "
1068                            + stringValue);
1069                    return false;
1070                }
1071                if (valueToSet == defaultValue) {
1072                    if (type.getMinOccurs() == 0) {
1073                        content.removeValue(mappedElement, locale, 0);
1074                    } else if (type instanceof CmsXmlStringValue) {
1075                        content.getValue(mappedElement, locale).setStringValue(cms, "");
1076                    } else {
1077                        LOG.warn(
1078                            content.getFile().getRootPath()
1079                                + ": Could not apply reverse availability mapping because the field "
1080                                + mappedElement
1081                                + " is neither optional nor of type OpenCmsString.");
1082                    }
1083                } else {
1084                    content.getValue(mappedElement, locale).setStringValue(cms, "" + valueToSet);
1085                }
1086            } else if (valueToSet != defaultValue) {
1087                Set<String> parentSet = new HashSet<>();
1088                String currentPath = mappedElement;
1089                while (!parentSet.contains(currentPath)) {
1090                    parentSet.add(currentPath);
1091                    currentPath = CmsXmlUtils.removeLastXpathElement(currentPath);
1092                }
1093                List<String> sortedParents = new ArrayList<>(parentSet);
1094                Collections.sort(sortedParents);
1095                for (String parent : sortedParents) {
1096                    if (!content.hasValue(parent, locale)) {
1097                        content.addValue(cms, parent, locale, 0);
1098                    }
1099                }
1100                content.getValue(mappedElement, locale).setStringValue(cms, "" + valueToSet);
1101            }
1102        }
1103        return true;
1104    }
1105
1106    /**
1107     * @see org.opencms.xml.content.I_CmsXmlContentHandler#canUseReverseAvailabilityMapping(org.opencms.xml.content.CmsMappingResolutionContext.AttributeType)
1108     */
1109    public boolean canUseReverseAvailabilityMapping(AttributeType attr) {
1110
1111        if (!m_reverseMappingEnabled) {
1112            return false;
1113        }
1114
1115        MappingInfo info = getAttributeMapping(attr);
1116        return info.canBeUsedForReverseAvailabilityMapping();
1117
1118    }
1119
1120    /**
1121     * We clear all potential property mappings.
1122     *
1123     * @see org.opencms.xml.content.I_CmsXmlContentHandler#clearMappings(org.opencms.file.CmsObject, org.opencms.xml.content.CmsXmlContent)
1124     */
1125    @Override
1126    public void clearMappings(CmsObject cms, CmsXmlContent content) throws CmsException {
1127
1128        CmsObject rootCms = createRootCms(cms);
1129        Set<String> mappings = new HashSet<>();
1130        getMappings().values().forEach(mps -> mappings.addAll(mps));
1131        CmsFile f = content.getFile();
1132        String filename = f.getRootPath();
1133        Collection<Locale> locales = OpenCms.getLocaleManager().getAvailableLocales();
1134
1135        for (String mapping : mappings) {
1136            CmsPair<String, Boolean> propertyInfo = getMapToProperty(mapping);
1137            if (null != propertyInfo) {
1138                String prop = propertyInfo.getFirst();
1139                Set<String> propNames = new HashSet<>(locales.size());
1140                // We do not delete the property itself - would be cleaner, but may cause backward compatibility issues
1141                //propNames.add(prop);
1142                propNames.addAll(
1143                    locales.stream().map(l -> CmsProperty.getLocaleSpecificPropertyName(prop, l)).collect(
1144                        Collectors.toSet()));
1145                boolean isShared = propertyInfo.getSecond().booleanValue();
1146                for (String propName : propNames) {
1147                    try {
1148                        rootCms.readPropertyDefinition(propName);
1149                        CmsProperty p = rootCms.readPropertyObject(f, propName, false);
1150                        if (!p.isNullProperty()) {
1151                            String v = isShared ? p.getResourceValue() : p.getStructureValue();
1152                            if ((null != v) && !v.isEmpty()) {
1153                                // make sure the file is locked
1154                                CmsLock lock = rootCms.getLock(filename);
1155                                if (lock.isUnlocked()) {
1156                                    rootCms.lockResource(filename);
1157                                } else if (!lock.isDirectlyOwnedInProjectBy(rootCms)) {
1158                                    rootCms.changeLock(filename);
1159                                }
1160                                rootCms.writePropertyObject(
1161                                    filename,
1162                                    createProperty(propName, CmsProperty.DELETE_VALUE, isShared));
1163                            }
1164                        }
1165                    } catch (CmsException e) {
1166                        // property not defined, do nothing.
1167                    }
1168                }
1169            }
1170        }
1171        // make sure the original is locked
1172        CmsLock lock = rootCms.getLock(f);
1173        if (lock.isUnlocked()) {
1174            rootCms.lockResource(f.getRootPath());
1175        } else if (!lock.isExclusiveOwnedBy(rootCms.getRequestContext().getCurrentUser())) {
1176            rootCms.changeLock(f.getRootPath());
1177        }
1178
1179    }
1180
1181    /**
1182     * Copies a given CMS context and set the copy's site root to '/'.<p>
1183     *
1184     * @param cms the CMS context to copy
1185     * @return the copy
1186     *
1187     * @throws CmsException if something goes wrong
1188     */
1189    public CmsObject createRootCms(CmsObject cms) throws CmsException {
1190
1191        CmsObject rootCms = OpenCms.initCmsObject(cms);
1192        Object logEntry = cms.getRequestContext().getAttribute(CmsLogEntry.ATTR_LOG_ENTRY);
1193        if (logEntry != null) {
1194            rootCms.getRequestContext().setAttribute(CmsLogEntry.ATTR_LOG_ENTRY, logEntry);
1195        }
1196        rootCms.getRequestContext().setSiteRoot("/");
1197        return rootCms;
1198    }
1199
1200    /**
1201     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getAgentTags(java.lang.String)
1202     */
1203    public Set<String> getAgentTags(String remainingPath) {
1204
1205        Set<String> result = m_agentTags.get(remainingPath);
1206        if (result == null) {
1207            result = Collections.emptySet();
1208        }
1209        return result;
1210
1211    }
1212
1213    /**
1214     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getAllowedTemplates()
1215     */
1216    public CmsDefaultSet<String> getAllowedTemplates() {
1217
1218        return m_allowedTemplates;
1219
1220    }
1221
1222    /**
1223     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getChangeHandlerConfigs()
1224     */
1225    public List<CmsChangeHandlerConfig> getChangeHandlerConfigs() {
1226
1227        return Collections.unmodifiableList(m_changeHandlerConfigs);
1228    }
1229
1230    /**
1231     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getComplexWidget(org.opencms.file.CmsObject, java.lang.String)
1232     */
1233    public I_CmsComplexWidget getComplexWidget(CmsObject cms, String path) {
1234
1235        String widgetName = m_widgetNames.get(path);
1236        if (widgetName == null) {
1237            return null;
1238        }
1239        if (cms != null) {
1240            CmsMacroResolver resolver = new CmsMacroResolver();
1241            resolver.setCmsObject(cms);
1242            widgetName = resolver.resolveMacros(widgetName);
1243        }
1244        if (CmsStringUtil.isValidJavaClassName(widgetName)) {
1245            try {
1246                Class<?> cls = Class.forName(widgetName, false, getClass().getClassLoader());
1247                if (I_CmsComplexWidget.class.isAssignableFrom(cls)) {
1248                    return (I_CmsComplexWidget)(cls.newInstance());
1249                }
1250            } catch (Exception e) {
1251                LOG.warn(e.getLocalizedMessage(), e);
1252                return null;
1253            }
1254        }
1255        return null;
1256
1257    }
1258
1259    /**
1260     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getConfiguration(org.opencms.xml.types.I_CmsXmlSchemaType)
1261     */
1262    public String getConfiguration(I_CmsXmlSchemaType type) {
1263
1264        String elementName = type.getName();
1265        return m_configurationValues.get(elementName);
1266
1267    }
1268
1269    /**
1270     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getConfiguration(org.opencms.xml.types.I_CmsXmlSchemaType)
1271     */
1272    public String getConfiguration(String path) {
1273
1274        return m_configurationValues.get(path);
1275
1276    }
1277
1278    /**
1279     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getConfiguredDisplayType(java.lang.String, org.opencms.xml.content.I_CmsXmlContentHandler.DisplayType)
1280     */
1281    public DisplayType getConfiguredDisplayType(String path, DisplayType defaultValue) {
1282
1283        DisplayType result = m_displayTypes.get(path);
1284        if (result == null) {
1285            result = defaultValue;
1286        }
1287        return result;
1288
1289    }
1290
1291    /**
1292     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getCSSHeadIncludes()
1293     */
1294    public Set<String> getCSSHeadIncludes() {
1295
1296        return Collections.unmodifiableSet(m_cssHeadIncludes);
1297    }
1298
1299    /***
1300     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getCSSHeadIncludes(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
1301     */
1302    @SuppressWarnings("unused")
1303    public Set<String> getCSSHeadIncludes(CmsObject cms, CmsResource resource) throws CmsException {
1304
1305        return getCSSHeadIncludes();
1306    }
1307
1308    /**
1309     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getDefault(org.opencms.file.CmsObject, org.opencms.file.CmsResource, org.opencms.xml.types.I_CmsXmlSchemaType, java.lang.String, java.util.Locale)
1310     */
1311    public String getDefault(CmsObject cms, CmsResource resource, I_CmsXmlSchemaType type, String path, Locale locale) {
1312
1313        String defaultValue;
1314        if (CmsStringUtil.isEmptyOrWhitespaceOnly(path)) {
1315            // ( path can be empty if this is called from createValue )
1316            // use the "getDefault" method of the given value, will use value from standard XML schema
1317            defaultValue = type.getDefault(locale);
1318        } else {
1319            // look up the default from the configured mappings
1320            defaultValue = m_defaultValues.get(path);
1321            if (defaultValue == null) {
1322                // no value found, try default xpath
1323                path = CmsXmlUtils.removeXpath(path);
1324                path = CmsXmlUtils.createXpath(path, 1);
1325                // look up the default value again with default index of 1 in all path elements
1326                defaultValue = m_defaultValues.get(path);
1327            }
1328        }
1329        if (defaultValue != null) {
1330            CmsObject newCms = cms;
1331            if (resource != null) {
1332                try {
1333                    // switch the current URI to the XML document resource so that properties can be read
1334                    CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(resource.getRootPath());
1335                    if (site != null) {
1336                        newCms = OpenCms.initCmsObject(cms);
1337                        newCms.getRequestContext().setSiteRoot(site.getSiteRoot());
1338                        newCms.getRequestContext().setUri(newCms.getSitePath(resource));
1339                    }
1340                } catch (Exception e) {
1341                    // on any error just use the default input OpenCms context
1342                }
1343            }
1344            // return the default value with processed macros
1345            String result = defaultValue;
1346            if (!m_nonMacroResolvableDefaults.contains(path)) {
1347                CmsMacroResolver resolver = CmsMacroResolver.newInstance().setCmsObject(newCms).setMessages(
1348                    getMessages(locale));
1349                result = resolver.resolveMacros(defaultValue);
1350            }
1351            return result;
1352        } else if (!CmsStringUtil.isEmptyOrWhitespaceOnly(path) && CmsXmlUtils.isDeepXpath(path)) {
1353
1354            // try to delegate to content handler of nested content
1355
1356            String subPath = CmsXmlUtils.removeFirstXpathElement(path);
1357            I_CmsXmlSchemaType nestedType = m_contentDefinition.getSchemaType(
1358                CmsXmlUtils.removeXpath(CmsXmlUtils.getFirstXpathElement(path)));
1359            if (nestedType instanceof CmsXmlNestedContentDefinition) {
1360                CmsXmlContentDefinition nestedDef = ((CmsXmlNestedContentDefinition)nestedType).getNestedContentDefinition();
1361                if (nestedDef != null) {
1362                    I_CmsXmlContentHandler subHandler = nestedDef.getContentHandler();
1363                    if (subHandler != null) {
1364                        return subHandler.getDefault(cms, resource, nestedType, subPath, locale);
1365                    }
1366                }
1367            }
1368        }
1369        // no default value is available
1370        return null;
1371    }
1372
1373    /**
1374     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getDefault(org.opencms.file.CmsObject, I_CmsXmlContentValue, java.util.Locale)
1375     */
1376    public String getDefault(CmsObject cms, I_CmsXmlContentValue value, Locale locale) {
1377
1378        String path = null;
1379        if (value.getElement() != null) {
1380            path = value.getPath();
1381        }
1382
1383        return getDefault(cms, value.getDocument() != null ? value.getDocument().getFile() : null, value, path, locale);
1384    }
1385
1386    /**
1387     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getDefaultComplexWidget()
1388     */
1389    public I_CmsComplexWidget getDefaultComplexWidget() {
1390
1391        return m_defaultWidgetInstance;
1392    }
1393
1394    /**
1395     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getDefaultComplexWidgetClass()
1396     */
1397    public String getDefaultComplexWidgetClass() {
1398
1399        return m_defaultWidget;
1400    }
1401
1402    /**
1403     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getDefaultComplexWidgetConfiguration()
1404     */
1405    public String getDefaultComplexWidgetConfiguration() {
1406
1407        return m_defaultWidgetConfig;
1408    }
1409
1410    /**
1411     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getDisplayType(org.opencms.xml.types.I_CmsXmlSchemaType)
1412     */
1413    public DisplayType getDisplayType(I_CmsXmlSchemaType type) {
1414
1415        if (m_displayTypes.containsKey(type.getName())) {
1416            return m_displayTypes.get(type.getName());
1417        } else {
1418            return DisplayType.none;
1419        }
1420    }
1421
1422    /**
1423     * Returns the edit handler if configured.<p>
1424     *
1425     * @return the edit handler
1426     */
1427    public I_CmsEditHandler getEditHandler() {
1428
1429        return m_editHandler;
1430    }
1431
1432    /**
1433     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getEditorChangeHandlers(boolean)
1434     */
1435    public List<I_CmsXmlContentEditorChangeHandler> getEditorChangeHandlers(boolean selfOnly) {
1436
1437        if (selfOnly) {
1438            return Collections.unmodifiableList(m_editorChangeHandlers);
1439        } else {
1440            List<I_CmsXmlContentEditorChangeHandler> result = new ArrayList<>(m_editorChangeHandlers);
1441            List<I_CmsXmlContentEditorChangeHandler> nestedHandlers = getNestedEditorChangeHandlers();
1442            result.addAll(nestedHandlers);
1443            return result;
1444        }
1445    }
1446
1447    /**
1448     * Gets the help texts for the fields.<p>
1449     *
1450     * @return the help texts for the fields
1451     */
1452    public Map<String, String> getFieldHelp() {
1453
1454        return Collections.unmodifiableMap(m_fieldDescriptions);
1455    }
1456
1457    /**
1458     * Gets the labels for the fields.<p>
1459     *
1460     * @return the labels for the fields
1461     */
1462    public Map<String, String> getFieldLabels() {
1463
1464        return Collections.unmodifiableMap(m_fieldNiceNames);
1465    }
1466
1467    /**
1468     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getFormatterConfiguration(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
1469     */
1470    public CmsFormatterConfiguration getFormatterConfiguration(CmsObject cms, CmsResource resource) {
1471
1472        List<I_CmsFormatterBean> wrappers = Lists.newArrayList();
1473        for (CmsFormatterBean formatter : m_formatters) {
1474            CmsSchemaFormatterBeanWrapper wrapper = new CmsSchemaFormatterBeanWrapper(cms, formatter, this, resource);
1475            wrappers.add(wrapper);
1476        }
1477        return CmsFormatterConfiguration.create(cms, wrappers);
1478    }
1479
1480    /**
1481     * Gets the geo mapping configuration.
1482     *
1483     * @return the geo mapping configuration
1484     */
1485    public CmsGeoMappingConfiguration getGeoMappingConfiguration() {
1486
1487        if ((m_primaryGeomappingField == null) && (m_geomappingEntries.size() == 0)) {
1488            return null;
1489        }
1490        List<CmsGeoMappingConfiguration.Entry> configEntries = new ArrayList<>();
1491        if (m_primaryGeomappingField != null) {
1492            configEntries.add(new CmsGeoMappingConfiguration.Entry(EntryType.field, m_primaryGeomappingField));
1493        }
1494        configEntries.addAll(m_geomappingEntries);
1495        return new CmsGeoMappingConfiguration(configEntries);
1496    }
1497
1498    /**
1499     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getInvalidRelationAction(java.lang.String)
1500     */
1501    public InvalidRelationAction getInvalidRelationAction(String name) {
1502
1503        return m_invalidRelationActions.get(name);
1504    }
1505
1506    /**
1507     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getJSHeadIncludes()
1508     */
1509    public Set<String> getJSHeadIncludes() {
1510
1511        return Collections.<String> unmodifiableSet(m_jsHeadIncludes);
1512    }
1513
1514    /**
1515     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getJSHeadIncludes(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
1516     */
1517    @SuppressWarnings("unused")
1518    public Set<String> getJSHeadIncludes(CmsObject cms, CmsResource resource) throws CmsException {
1519
1520        return getJSHeadIncludes();
1521    }
1522
1523    /**
1524     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getJsonRendererSettings()
1525     */
1526    public JsonRendererSettings getJsonRendererSettings() {
1527
1528        return m_jsonRendererSettings;
1529    }
1530
1531    /**
1532     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getMappings()
1533     */
1534    public Map<String, List<String>> getMappings() {
1535
1536        Map<String, List<String>> result = new HashMap<>();
1537        for (Map.Entry<String, List<String>> entry : m_elementMappings.entrySet()) {
1538            result.put(entry.getKey(), Collections.unmodifiableList(entry.getValue()));
1539        }
1540        return result;
1541    }
1542
1543    /**
1544     * Returns the all mappings defined for the given element xpath.<p>
1545     *
1546     * @since 7.0.2
1547     *
1548     * @param elementName the element xpath to look up the mapping for
1549     *
1550     * @return the mapping defined for the given element xpath
1551     */
1552    public List<String> getMappings(String elementName) {
1553
1554        List<String> result = m_elementMappings.get(elementName);
1555        if (result == null) {
1556            result = Collections.emptyList();
1557        }
1558        return result;
1559    }
1560
1561    /**
1562     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getMessageKeyHandler()
1563     */
1564    public I_KeyFallbackHandler getMessageKeyHandler() {
1565
1566        return m_messageKeyHandler;
1567    }
1568
1569    /**
1570     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getMessages(java.util.Locale)
1571     */
1572    public CmsMessages getMessages(Locale locale) {
1573
1574        CmsMessages result = null;
1575        if ((m_messageBundleNames == null) || m_messageBundleNames.isEmpty()) {
1576            return new CmsMessages(Messages.get().getBundleName(), locale);
1577        } else {
1578            // a message bundle was initialized
1579            CmsMultiMessages multiMessages = new CmsMultiMessages(locale);
1580            for (String messageBundleName : m_messageBundleNames) {
1581                multiMessages.addMessages(new CmsMessages(messageBundleName, locale));
1582            }
1583            if (!m_messageBundleNames.contains(Messages.get().getBundleName())) {
1584                multiMessages.addMessages(new CmsMessages(Messages.get().getBundleName(), locale));
1585            }
1586            result = multiMessages;
1587
1588        }
1589        return result;
1590    }
1591
1592    /**
1593     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getModelFolder()
1594     */
1595    public String getModelFolder() {
1596
1597        return m_modelFolder;
1598    }
1599
1600    /**
1601     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getNestedFormatters(org.opencms.file.CmsObject, org.opencms.file.CmsResource, java.util.Locale, javax.servlet.ServletRequest)
1602     */
1603    public List<String> getNestedFormatters(CmsObject cms, CmsResource res, Locale locale, ServletRequest req) {
1604
1605        List<String> result = new ArrayList<String>();
1606        if (hasNestedFormatters()) {
1607            try {
1608                CmsXmlContent content;
1609                if (req != null) {
1610                    content = CmsXmlContentFactory.unmarshal(cms, res, req);
1611                } else {
1612                    content = CmsXmlContentFactory.unmarshal(cms, cms.readFile(res));
1613                }
1614                Locale matchingLocale = content.getBestMatchingLocale(locale);
1615                if (matchingLocale == null) {
1616                    matchingLocale = content.getLocales().get(0);
1617                }
1618                if (matchingLocale != null) {
1619                    for (String elementPath : m_nestedFormatterElements) {
1620                        List<I_CmsXmlContentValue> values = content.getValues(elementPath, matchingLocale);
1621                        for (I_CmsXmlContentValue value : values) {
1622                            if (value instanceof CmsXmlDisplayFormatterValue) {
1623                                String formatterId = ((CmsXmlDisplayFormatterValue)value).getFormatterId();
1624                                if ((formatterId != null) && !CmsUUID.getNullUUID().toString().equals(formatterId)) {
1625                                    result.add(formatterId);
1626                                }
1627                            } else if (value instanceof CmsXmlVarLinkValue) {
1628                                CmsLink link = ((CmsXmlVarLinkValue)value).getLink(cms);
1629                                CmsUUID formatterId = link.getStructureId();
1630                                if ((formatterId != null) && !formatterId.isNullUUID()) {
1631                                    result.add(formatterId.toString());
1632                                }
1633                            } else if (value instanceof CmsXmlVfsFileValue) {
1634                                CmsLink link = ((CmsXmlVfsFileValue)value).getLink(cms);
1635                                CmsUUID formatterId = link.getStructureId();
1636                                if ((formatterId != null) && !formatterId.isNullUUID()) {
1637                                    result.add(formatterId.toString());
1638                                }
1639                            }
1640                        }
1641                    }
1642                }
1643            } catch (Exception e) {
1644                LOG.error(e.getLocalizedMessage(), e);
1645            }
1646        }
1647        return result;
1648    }
1649
1650    /**
1651     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getParameter(java.lang.String)
1652     */
1653    public String getParameter(String name) {
1654
1655        return m_parameters.get(name);
1656    }
1657
1658    /**
1659     *
1660     * Gets the set of parameters.<p>
1661     *
1662     * @return zhr drz og pstsmrzrtd d
1663     */
1664    public CmsParameterConfiguration getParameters() {
1665
1666        return m_parameters;
1667    }
1668
1669    /**
1670     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getPreview(org.opencms.file.CmsObject, org.opencms.xml.content.CmsXmlContent, java.lang.String)
1671     */
1672    public String getPreview(CmsObject cms, CmsXmlContent content, String resourcename) {
1673
1674        CmsMacroResolver resolver = CmsMacroResolver.newInstance().setCmsObject(cms);
1675        resolver.addMacro(MACRO_PREVIEW_TEMPFILE, resourcename);
1676
1677        return resolver.resolveMacros(m_previewLocation);
1678    }
1679
1680    /**
1681     * @see I_CmsXmlContentHandler#getRelationType(I_CmsXmlContentValue)
1682     */
1683    @Deprecated
1684    public CmsRelationType getRelationType(I_CmsXmlContentValue value) {
1685
1686        if (value == null) {
1687            return CmsRelationType.XML_WEAK;
1688        }
1689        return getRelationType(value.getPath());
1690    }
1691
1692    /**
1693     * @see I_CmsXmlContentHandler#getRelationType(String)
1694     */
1695    public CmsRelationType getRelationType(String xpath) {
1696
1697        return getRelationType(xpath, CmsRelationType.XML_WEAK);
1698    }
1699
1700    /**
1701     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getRelationType(java.lang.String, org.opencms.relations.CmsRelationType)
1702     */
1703    public CmsRelationType getRelationType(String xpath, CmsRelationType defaultType) {
1704
1705        CmsRelationType relationType = null;
1706        if (xpath != null) {
1707
1708            // look up the default from the configured mappings
1709            relationType = m_relations.get(xpath);
1710            if (relationType == null) {
1711                // no value found, try default xpath
1712                String path = CmsXmlUtils.removeAllXpathIndices(xpath);
1713                // look up the default value again without indexes
1714                relationType = m_relations.get(path);
1715            }
1716            if (relationType == null) {
1717                // no value found, try the last simple type path
1718                String path = CmsXmlUtils.getLastXpathElement(xpath);
1719                // look up the default value again for the last simple type
1720                relationType = m_relations.get(path);
1721            }
1722        }
1723        if (relationType == null) {
1724            relationType = defaultType;
1725        }
1726        return relationType;
1727    }
1728
1729    /**
1730     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getSearchContentConfig(org.opencms.xml.types.I_CmsXmlContentValue)
1731     */
1732    @Override
1733    public CmsSearchContentConfig getSearchContentConfig(I_CmsXmlContentValue value) {
1734
1735        String path = CmsXmlUtils.removeXpath(value.getPath());
1736        // check for name configured in the annotations
1737        CmsSearchContentConfig searchSetting = m_searchSettings.get(path);
1738        // if no search setting is found within the root handler, move the path upwards to look for other configurations
1739        if (searchSetting == null) {
1740            String[] pathElements = path.split("/");
1741            I_CmsXmlSchemaType type = value.getDocument().getContentDefinition().getSchemaType(pathElements[0]);
1742            for (int i = 1; i < pathElements.length; i++) {
1743                type = ((CmsXmlNestedContentDefinition)type).getNestedContentDefinition().getSchemaType(
1744                    pathElements[i]);
1745                String subPath = getSubPath(pathElements, i);
1746                searchSetting = type.getContentDefinition().getContentHandler().getSearchSettings().get(subPath);
1747                if (searchSetting != null) {
1748                    break;
1749                }
1750            }
1751        }
1752        // if no annotation has been found, use default for value
1753        return (searchSetting == null) ? value.getSearchContentConfig() : searchSetting;
1754    }
1755
1756    /**
1757     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getSearchFields()
1758     */
1759    public Set<CmsSearchField> getSearchFields() {
1760
1761        return Collections.unmodifiableSet(new HashSet<CmsSearchField>(m_searchFields.values()));
1762    }
1763
1764    /**
1765     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getSearchFieldsForPage()
1766     */
1767    public Set<CmsSearchField> getSearchFieldsForPage() {
1768
1769        return Collections.unmodifiableSet(new HashSet<CmsSearchField>(m_searchFieldsPage.values()));
1770    }
1771
1772    /**
1773     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getSearchSettings()
1774     */
1775    public Map<String, CmsSearchContentConfig> getSearchSettings() {
1776
1777        return m_searchSettings;
1778    }
1779
1780    /**
1781     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getSettings(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
1782     */
1783    public Map<String, CmsXmlContentProperty> getSettings(CmsObject cms, CmsResource resource) {
1784
1785        return Collections.unmodifiableMap(m_settings);
1786    }
1787
1788    /**
1789     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getSynchronizations(boolean)
1790     */
1791    public CmsSynchronizationSpec getSynchronizations(boolean recursive) {
1792
1793        if (!recursive) {
1794            return new CmsSynchronizationSpec(m_synchronizations);
1795        } else {
1796            if (m_combinedSynchronizations == null) {
1797                LinkedHashMap<String, SynchronizationMode> combinedSynchronizations = new LinkedHashMap<>();
1798                combineSynchronizations(m_contentDefinition, "", combinedSynchronizations);
1799                m_combinedSynchronizations = combinedSynchronizations;
1800            }
1801            return new CmsSynchronizationSpec(m_combinedSynchronizations);
1802        }
1803    }
1804
1805    /**
1806     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getTabs()
1807     */
1808    public List<CmsXmlContentTab> getTabs() {
1809
1810        return Collections.unmodifiableList(m_tabs);
1811    }
1812
1813    /**
1814     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getTitleMapping(org.opencms.file.CmsObject, org.opencms.xml.content.CmsXmlContent, java.util.Locale)
1815     */
1816    public String getTitleMapping(CmsObject cms, CmsXmlContent document, Locale locale) {
1817
1818        String result = null;
1819        if (m_titleMappings.size() > 0) {
1820            // a title mapping is available
1821            String xpath = m_titleMappings.get(0);
1822            // currently just use the first mapping found, unsure if multiple "Title" mappings would make sense anyway
1823            result = document.getStringValue(cms, xpath, locale);
1824            if ((result == null)
1825                && (isMappingUsingDefault(xpath, TITLE_PROPERTY_MAPPING)
1826                    || isMappingUsingDefault(xpath, TITLE_PROPERTY_SHARED_MAPPING)
1827                    || isMappingUsingDefault(xpath, TITLE_PROPERTY_INDIVIDUAL_MAPPING))) {
1828                result = getDefault(cms, document.getFile(), null, xpath, locale);
1829            }
1830            if (result != null) {
1831                try {
1832                    CmsGalleryNameMacroResolver resolver = new CmsGalleryNameMacroResolver(
1833                        createRootCms(cms),
1834                        document,
1835                        locale);
1836                    resolver.setKeepEmptyMacros(true);
1837                    result = resolver.resolveMacros(result);
1838                } catch (Exception e) {
1839                    LOG.error(e.getMessage(), e);
1840                }
1841            }
1842        }
1843        return result;
1844    }
1845
1846    /**
1847     * Gets the validation error message configured in the schema for the element.
1848     *
1849     * @param elementName the name of the element
1850     * @return the validation message
1851     */
1852    public String getValidationError(String elementName) {
1853
1854        return m_validationErrorMessages.get(elementName);
1855    }
1856
1857    /**
1858     * Gets the validation warning message configured in the schema for the element.
1859     *
1860     * @param elementName the name of the element
1861     * @return the validation message
1862     */
1863    public String getValidationWarning(String elementName) {
1864
1865        return m_validationWarningMessages.get(elementName);
1866    }
1867
1868    /**
1869     * Helper method for reading a validation message or the corresponding message key.
1870     *
1871     * @param cms the current CMS context
1872     * @param locale the locale
1873     * @param elementName the element name
1874     * @param isWarning true if we want the warning message, false for the error message
1875     * @param keyOnly true if we want the key rather than the message
1876     *
1877     * @return the message or message key
1878     */
1879    public String getValidationWarningOrErrorMessage(
1880        CmsObject cms,
1881        Locale locale,
1882        String elementName,
1883        boolean isWarning,
1884        boolean keyOnly) {
1885
1886        String rawValue = (isWarning ? m_validationWarningMessages : m_validationErrorMessages).get(elementName);
1887        if (rawValue == null) {
1888            return null;
1889        }
1890        CmsMacroResolver resolver = CmsMacroResolver.newInstance().setCmsObject(cms).setMessages(getMessages(locale));
1891        if (keyOnly) {
1892            resolver = new CmsKeyDummyMacroResolver(resolver);
1893        }
1894        String resolved = resolver.resolveMacros(rawValue);
1895        if (keyOnly) {
1896            return CmsKeyDummyMacroResolver.getKey(resolved);
1897        } else {
1898            return resolved;
1899        }
1900    }
1901
1902    /**
1903     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getVersionTransformation()
1904     */
1905    public String getVersionTransformation() {
1906
1907        return m_versionTransformation;
1908    }
1909
1910    /**
1911     * Returns the configured visibility parameter string for the given field if the content handler itself is the
1912     * visibility handler, and null otherwise.
1913     *
1914     * @param field a field name
1915     * @return the visibility parameter
1916     */
1917    public String getVisibilityConfigString(String field) {
1918
1919        VisibilityConfiguration visConfig = m_visibilityConfigurations.get(field);
1920        if (visConfig == null) {
1921            return null;
1922        }
1923        if (visConfig.getHandler() == this) {
1924            return visConfig.getParams();
1925        }
1926        return null;
1927    }
1928
1929    /**
1930     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getWidget(org.opencms.file.CmsObject, java.lang.String)
1931     */
1932    public I_CmsWidget getWidget(CmsObject cms, String path) {
1933
1934        String widgetName = m_widgetNames.get(path);
1935        if (widgetName == null) {
1936            return null;
1937        }
1938
1939        // First resolve macros, then try resulting string as widget alias, finally try interpreting it as a class name
1940        if (cms != null) {
1941            CmsMacroResolver resolver = new CmsMacroResolver();
1942            resolver.setCmsObject(cms);
1943            widgetName = resolver.resolveMacros(widgetName);
1944        }
1945        I_CmsWidget result = null;
1946        result = OpenCms.getXmlContentTypeManager().getWidget(widgetName);
1947        if (result != null) {
1948            return result.newInstance();
1949        }
1950        if (CmsStringUtil.isValidJavaClassName(widgetName)) {
1951            try {
1952                Class<?> cls = Class.forName(widgetName, false, getClass().getClassLoader());
1953                if (I_CmsWidget.class.isAssignableFrom(cls)) {
1954                    return (I_CmsWidget)(cls.newInstance());
1955                }
1956            } catch (Exception e) {
1957                LOG.warn(e.getLocalizedMessage(), e);
1958                return null;
1959            }
1960        }
1961        return null;
1962
1963    }
1964
1965    /**
1966     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getWidget(org.opencms.xml.types.I_CmsXmlSchemaType)
1967     */
1968    @Deprecated
1969    public I_CmsWidget getWidget(I_CmsXmlSchemaType value) {
1970
1971        // try the specific widget settings first
1972        I_CmsWidget result = getWidget(null, value.getName());
1973        if (result == null) {
1974            // use default widget mappings
1975            result = OpenCms.getXmlContentTypeManager().getWidgetDefault(value.getTypeName());
1976        } else {
1977            result = result.newInstance();
1978        }
1979        if (result != null) {
1980            // set the configuration value for this widget
1981            String configuration = getConfiguration(value);
1982            if (configuration == null) {
1983                // no individual configuration defined, try to get global default configuration
1984                configuration = OpenCms.getXmlContentTypeManager().getWidgetDefaultConfiguration(result);
1985            }
1986            result.setConfiguration(configuration);
1987        }
1988        return result;
1989    }
1990
1991    /**
1992     * @see org.opencms.xml.content.I_CmsXmlContentHandler#hasModifiableFormatters()
1993     */
1994    public boolean hasModifiableFormatters() {
1995
1996        return (m_formatters != null) && (m_formatters.size() > 0);
1997    }
1998
1999    /**
2000     * @see org.opencms.xml.content.I_CmsXmlContentHandler#hasNestedFormatters()
2001     */
2002    public boolean hasNestedFormatters() {
2003
2004        return !m_nestedFormatterElements.isEmpty();
2005    }
2006
2007    /**
2008     * @see org.opencms.xml.content.I_CmsXmlContentHandler#hasSynchronizedElements()
2009     */
2010    public boolean hasSynchronizedElements() {
2011
2012        return !m_synchronizations.isEmpty();
2013    }
2014
2015    /**
2016     * @see org.opencms.xml.content.I_CmsXmlContentHandler#hasVisibilityHandlers()
2017     */
2018    public boolean hasVisibilityHandlers() {
2019
2020        return (m_visibilityConfigurations != null) && !m_visibilityConfigurations.isEmpty();
2021    }
2022
2023    /**
2024     * @see org.opencms.xml.content.I_CmsXmlContentHandler#initialize(org.dom4j.Element, org.opencms.xml.CmsXmlContentDefinition)
2025     */
2026    public synchronized void initialize(Element appInfoElement, CmsXmlContentDefinition contentDefinition)
2027    throws CmsXmlException {
2028
2029        if (appInfoElement != null) {
2030            // validate the appinfo element XML content with the default appinfo handler schema
2031            validateAppinfoElement(appInfoElement);
2032
2033            // re-initialize the local variables
2034            init();
2035
2036            Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(appInfoElement);
2037            while (i.hasNext()) {
2038                // iterate all elements in the appinfo node
2039                Element element = i.next();
2040                String nodeName = element.getName();
2041                if (nodeName.equals(APPINFO_MAPPINGS)) {
2042                    initMappings(element, contentDefinition);
2043                } else if (nodeName.equals(APPINFO_LAYOUTS)) {
2044                    initLayouts(element, contentDefinition);
2045                } else if (nodeName.equals(APPINFO_VALIDATIONRULES)) {
2046                    initValidationRules(element, contentDefinition);
2047                } else if (nodeName.equals(APPINFO_RELATIONS)) {
2048                    initRelations(element, contentDefinition);
2049                } else if (nodeName.equals(APPINFO_DEFAULTS)) {
2050                    initDefaultValues(element, contentDefinition);
2051                } else if (nodeName.equals(APPINFO_MODELFOLDER)) {
2052                    initModelFolder(element, contentDefinition);
2053                } else if (nodeName.equals(APPINFO_PREVIEW)) {
2054                    initPreview(element, contentDefinition);
2055                } else if (nodeName.equals(APPINFO_RESOURCEBUNDLE)) {
2056                    initResourceBundle(element, contentDefinition, true);
2057                } else if (nodeName.equals(APPINFO_RESOURCEBUNDLES)) {
2058                    initResourceBundle(element, contentDefinition, false);
2059                } else if (nodeName.equals(APPINFO_SEARCHSETTINGS)) {
2060                    initSearchSettings(element, contentDefinition);
2061                } else if (nodeName.equals(APPINFO_TABS)) {
2062                    initTabs(element, contentDefinition);
2063                } else if (nodeName.equals(APPINFO_FORMATTERS)) {
2064                    initFormatters(element, contentDefinition);
2065                } else if (nodeName.equals(APPINFO_HEAD_INCLUDES)) {
2066                    initHeadIncludes(element, contentDefinition);
2067                } else if (nodeName.equals(APPINFO_SETTINGS)) {
2068                    initSettings(element, contentDefinition);
2069                } else if (nodeName.equals(APPINFO_EDIT_HANDLER)) {
2070                    initEditHandler(element);
2071                } else if (nodeName.equals(APPINFO_NESTED_FORMATTERS)) {
2072                    initNestedFormatters(element, contentDefinition);
2073                } else if (nodeName.equals(APPINFO_TEMPLATES)) {
2074                    initTemplates(element, contentDefinition);
2075                } else if (nodeName.equals(APPINFO_DEFAULTWIDGET)) {
2076                    initDefaultWidget(element);
2077                } else if (nodeName.equals(APPINFO_VISIBILITIES)) {
2078                    initVisibilities(element, contentDefinition);
2079                } else if (nodeName.equals(APPINFO_SYNCHRONIZATIONS)) {
2080                    initSynchronizations(element, contentDefinition);
2081                } else if (nodeName.equals(APPINFO_EDITOR_CHANGE_HANDLERS)) {
2082                    initEditorChangeHandlers(element);
2083                } else if (nodeName.equals(APPINFO_MESSAGEKEYHANDLER)) {
2084                    initMessageKeyHandler(element);
2085                } else if (nodeName.equals(APPINFO_PARAMETERS)) {
2086                    initParameters(element);
2087                } else if (nodeName.equals(APPINFO_FIELD_SETTINGS)) {
2088                    initFields(element, contentDefinition);
2089                } else if (nodeName.equals(APPINFO_JSON_RENDERER)) {
2090                    initJsonRenderer(element);
2091                } else if (nodeName.equals(APPINFO_REVERSE_MAPPING_ENABLED)) {
2092                    m_reverseMappingEnabled = Boolean.parseBoolean(element.getTextTrim());
2093                } else if (nodeName.equals(APPINFO_GEOMAPPING)) {
2094                    initGeoMappingEntries(element);
2095                } else if (nodeName.equals(APPINFO_VERSION_TRANSFORMATION)) {
2096                    m_versionTransformation = element.getTextTrim();
2097                }
2098            }
2099        }
2100        m_contentDefinition = contentDefinition;
2101        addGeoMappingField();
2102
2103        // at the end, add default check rules for optional file references
2104        addDefaultCheckRules(contentDefinition, null, null);
2105    }
2106
2107    /**
2108     * @see org.opencms.xml.content.I_CmsXmlContentHandler#invalidateBrokenLinks(CmsObject, CmsXmlContent)
2109     */
2110    public void invalidateBrokenLinks(CmsObject cms, CmsXmlContent document) {
2111
2112        if ((cms == null) || (cms.getRequestContext().getRequestTime() == CmsResource.DATE_RELEASED_EXPIRED_IGNORE)) {
2113            // do not check if the request comes the editor
2114            return;
2115        }
2116        boolean needReinitialization = false;
2117        // iterate the locales
2118        Iterator<Locale> itLocales = document.getLocales().iterator();
2119        while (itLocales.hasNext()) {
2120            Locale locale = itLocales.next();
2121            List<String> removedNodes = new ArrayList<String>();
2122            Map<String, I_CmsXmlContentValue> valuesToRemove = Maps.newHashMap();
2123            // iterate the values
2124            Iterator<I_CmsXmlContentValue> itValues = document.getValues(locale).iterator();
2125            while (itValues.hasNext()) {
2126                I_CmsXmlContentValue value = itValues.next();
2127                InvalidRelationAction invalidRelationAction = getInvalidRelationActionForValue(value);
2128                String path = value.getPath();
2129                // check if this value has already been deleted by parent rules
2130                boolean alreadyRemoved = false;
2131                Iterator<String> itRemNodes = removedNodes.iterator();
2132                while (itRemNodes.hasNext()) {
2133                    String remNode = itRemNodes.next();
2134                    if (path.startsWith(remNode)) {
2135                        alreadyRemoved = true;
2136                        break;
2137                    }
2138                }
2139                // only continue if not already removed and if a rule match
2140                if (alreadyRemoved
2141                    || ((m_relationChecks.get(path) == null)
2142                        && (invalidRelationAction == null)
2143                        && (m_relationChecks.get(CmsXmlUtils.removeXpath(path)) == null))) {
2144                    continue;
2145                }
2146
2147                // check rule matched
2148                if (LOG.isDebugEnabled()) {
2149                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_XMLCONTENT_CHECK_RULE_MATCH_1, path));
2150                }
2151                if (validateLink(cms, value, null)) {
2152                    // invalid link
2153                    if (LOG.isDebugEnabled()) {
2154                        LOG.debug(
2155                            Messages.get().getBundle().key(
2156                                Messages.LOG_XMLCONTENT_CHECK_WARNING_2,
2157                                path,
2158                                value.getStringValue(cms)));
2159                    }
2160                    // find the node to remove
2161                    String parentPath = path;
2162                    boolean firstIteration = true;
2163                    while (isInvalidateParent(parentPath)
2164                        || (firstIteration && (invalidRelationAction == InvalidRelationAction.removeParent))) {
2165                        firstIteration = false;
2166                        // check parent
2167                        parentPath = CmsXmlUtils.removeLastXpathElement(parentPath);
2168                        // log info
2169                        if (LOG.isDebugEnabled()) {
2170                            LOG.debug(
2171                                Messages.get().getBundle().key(
2172                                    Messages.LOG_XMLCONTENT_CHECK_PARENT_2,
2173                                    path,
2174                                    parentPath));
2175                        }
2176                    }
2177                    value = document.getValue(parentPath, locale);
2178                    // Doing the actual DOM modifications here would make the bookmarks for this locale invalid,
2179                    // so we delay it until later because we need the bookmarks for document.getValue() in the next loop iterations
2180                    valuesToRemove.put(parentPath, value);
2181                    // mark node as deleted
2182                    removedNodes.add(parentPath);
2183                }
2184            }
2185            for (I_CmsXmlContentValue valueToRemove : valuesToRemove.values()) {
2186                // detach the value node from the XML document
2187                valueToRemove.getElement().detach();
2188                needReinitialization = true;
2189            }
2190        }
2191        if (needReinitialization) {
2192            document.m_hasInvalidatedBrokenLinks = true;
2193            // re-initialize the XML content
2194            document.initDocument();
2195        }
2196    }
2197
2198    /**
2199     * Returns true if the Acacia editor is disabled for this type.<p>
2200     *
2201     * @return true if the acacia editor is disabled
2202     */
2203    public boolean isAcaciaEditorDisabled() {
2204
2205        return !m_useAcacia;
2206    }
2207
2208    /**
2209     * @see org.opencms.xml.content.I_CmsXmlContentHandler#isContainerPageOnly()
2210     */
2211    public boolean isContainerPageOnly() {
2212
2213        return m_containerPageOnly;
2214    }
2215
2216    /**
2217     * Returns the content field visibilty.<p>
2218     *
2219     * This implementation will be used as default if no other <link>org.opencms.xml.content.I_CmsXmlContentVisibilityHandler</link> is configured.<p>
2220     *
2221     * Only users that are member in one of the specified groups will be allowed to view and edit the given content field.<p>
2222     * The parameter should contain a '|' separated list of group names.<p>
2223     *
2224     * @see org.opencms.xml.content.I_CmsXmlContentVisibilityHandler#isValueVisible(org.opencms.file.CmsObject, org.opencms.xml.types.I_CmsXmlSchemaType, java.lang.String, java.lang.String, org.opencms.file.CmsResource, java.util.Locale)
2225     */
2226    public boolean isValueVisible(
2227        CmsObject cms,
2228        I_CmsXmlSchemaType value,
2229        String elementName,
2230        String params,
2231        CmsResource resource,
2232        Locale contentLocale) {
2233
2234        CmsUser user = cms.getRequestContext().getCurrentUser();
2235        boolean result = false;
2236
2237        try {
2238            List<CmsRole> roles = OpenCms.getRoleManager().getRolesOfUser(cms, user.getName(), "", true, false, true);
2239            List<CmsGroup> groups = cms.getGroupsOfUser(user.getName(), false);
2240            CmsMacroResolver resolver = new CmsMacroResolver();
2241            resolver.setCmsObject(cms);
2242            Locale wpLocale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms);
2243            resolver.setMessages(OpenCms.getWorkplaceManager().getMessages(wpLocale));
2244            params = resolver.resolveMacros(params);
2245
2246            if ("visible".equals(params.trim())) {
2247                return true;
2248            }
2249            String[] allowedPrincipals = params.split("\\|");
2250            List<String> groupNames = new ArrayList<String>();
2251            List<String> roleNames = new ArrayList<String>();
2252
2253            for (CmsGroup group : groups) {
2254                groupNames.add(group.getName());
2255            }
2256            for (CmsRole role : roles) {
2257                roleNames.add(role.getRoleName());
2258            }
2259            for (String principal : allowedPrincipals) {
2260                if (CmsRole.hasPrefix(principal)) {
2261                    // prefixed as a role
2262                    principal = CmsRole.removePrefix(principal);
2263                    if (roleNames.contains(principal)) {
2264                        result = true;
2265                        break;
2266                    }
2267                } else {
2268                    // otherwise we always assume this is a group, will work if prefixed or not
2269                    principal = CmsGroup.removePrefix(principal);
2270                    if (groupNames.contains(principal)) {
2271                        result = true;
2272                        break;
2273                    }
2274                }
2275            }
2276        } catch (CmsException e) {
2277            LOG.error(e.getLocalizedMessage(), e);
2278        }
2279
2280        return result;
2281    }
2282
2283    /**
2284     * @see org.opencms.xml.content.I_CmsXmlContentHandler#isVisible(org.opencms.file.CmsObject, org.opencms.xml.types.I_CmsXmlSchemaType, java.lang.String, org.opencms.file.CmsResource, java.util.Locale)
2285     */
2286    public boolean isVisible(
2287        CmsObject cms,
2288        I_CmsXmlSchemaType contentValue,
2289        String valuePath,
2290        CmsResource resource,
2291        Locale contentLocale) {
2292
2293        if (contentValue instanceof CmsXmlAccessRestrictionValue) {
2294            CmsAccessRestrictionInfo restrictionInfo = CmsAccessRestrictionInfo.getRestrictionInfo(
2295                cms,
2296                m_contentDefinition);
2297            if (restrictionInfo == null) {
2298                return false;
2299            }
2300        }
2301
2302        if (hasVisibilityHandlers() && m_visibilityConfigurations.containsKey(valuePath)) {
2303            VisibilityConfiguration config = m_visibilityConfigurations.get(valuePath);
2304            return config.getHandler().isValueVisible(
2305                cms,
2306                contentValue,
2307                valuePath,
2308                config.getParams(),
2309                resource,
2310                contentLocale);
2311        }
2312        return true;
2313
2314    }
2315
2316    /**
2317     * @see org.opencms.xml.content.I_CmsXmlContentHandler#prepareForUse(org.opencms.file.CmsObject, org.opencms.xml.content.CmsXmlContent)
2318     */
2319    public CmsXmlContent prepareForUse(CmsObject cms, CmsXmlContent content) {
2320
2321        // NOOP, just return the unmodified content
2322        return content;
2323    }
2324
2325    /**
2326     * @see org.opencms.xml.content.I_CmsXmlContentHandler#prepareForWrite(org.opencms.file.CmsObject, org.opencms.xml.content.CmsXmlContent, org.opencms.file.CmsFile)
2327     */
2328    public CmsFile prepareForWrite(CmsObject cms, CmsXmlContent content, CmsFile file) throws CmsException {
2329
2330        if (!content.isAutoCorrectionEnabled()) {
2331            // check if the XML should be corrected automatically (if not already set)
2332            Object attribute = cms.getRequestContext().getAttribute(CmsXmlContent.AUTO_CORRECTION_ATTRIBUTE);
2333            // set the auto correction mode as required
2334            boolean autoCorrectionEnabled = (attribute != null) && ((Boolean)attribute).booleanValue();
2335            content.setAutoCorrectionEnabled(autoCorrectionEnabled);
2336        }
2337        // validate the XML structure before writing the file if required
2338        if (!content.isAutoCorrectionEnabled()) {
2339            // an exception will be thrown if the structure is invalid
2340            content.validateXmlStructure(new CmsXmlEntityResolver(cms));
2341        }
2342        // read the content-conversion property
2343        String contentConversion = CmsHtmlConverter.getConversionSettings(cms, file);
2344        if (CmsStringUtil.isEmptyOrWhitespaceOnly(contentConversion)) {
2345            // enable pretty printing and XHTML conversion of XML content html fields by default
2346            contentConversion = CmsHtmlConverter.PARAM_XHTML;
2347        }
2348        content.setConversion(contentConversion);
2349        // correct the HTML structure
2350        file = content.correctXmlStructure(cms);
2351        content.setFile(file);
2352
2353        // check if any field has a configured attribute mapping
2354        boolean hasAttributeMappings = m_elementMappings.values().stream().flatMap(List::stream).filter(
2355            mapping -> mapping.startsWith(MAPTO_ATTRIBUTE)).findAny().isPresent();
2356
2357        // resolve the file mappings
2358        CmsMappingResolutionContext mappingContext = new CmsMappingResolutionContext(content, hasAttributeMappings);
2359        mappingContext.setCmsObject(cms);
2360        // pass the mapping context as a request context attribute to preserve interface compatibility
2361        cms.getRequestContext().setAttribute(ATTR_MAPPING_RESOLUTION_CONTEXT, mappingContext);
2362        content.resolveMappings(cms);
2363        // ensure all property or permission mappings of deleted optional values are removed
2364        removeEmptyMappings(cms, file, content);
2365        resolveDefaultMappings(cms, file, content);
2366        cms.getRequestContext().removeAttribute(ATTR_MAPPING_RESOLUTION_CONTEXT);
2367        mappingContext.finalizeMappings();
2368        // write categories (if there is a category widget present)
2369        file = writeCategories(cms, file, content);
2370        // return the result
2371        return file;
2372    }
2373
2374    /**
2375     * @see org.opencms.xml.content.I_CmsXmlContentHandler#resolveMapping(org.opencms.file.CmsObject, org.opencms.xml.content.CmsXmlContent, org.opencms.xml.types.I_CmsXmlContentValue)
2376     */
2377    public void resolveMapping(CmsObject cms, CmsXmlContent content, I_CmsXmlContentValue value) throws CmsException {
2378
2379        if (content.getFile() == null) {
2380            throw new CmsXmlException(Messages.get().container(Messages.ERR_XMLCONTENT_RESOLVE_FILE_NOT_FOUND_0));
2381        }
2382
2383        // get the mappings for the element name
2384        boolean valueIsSimple = value.isSimpleType();
2385        String valuePath = value.getPath();
2386        int valueIndex = value.getIndex();
2387        Locale valueLocale = value.getLocale();
2388        CmsObject rootCms1 = createRootCms(cms);
2389        String originalStringValue = null;
2390        if (valueIsSimple) {
2391            originalStringValue = value.getStringValue(rootCms1);
2392        }
2393        resolveMapping(cms, content, valuePath, valueIsSimple, valueIndex, valueLocale, originalStringValue);
2394    }
2395
2396    /**
2397     * @see org.opencms.xml.content.I_CmsXmlContentHandler#resolveValidation(org.opencms.file.CmsObject, org.opencms.xml.types.I_CmsXmlContentValue, org.opencms.xml.content.CmsXmlContentErrorHandler)
2398     */
2399    public CmsXmlContentErrorHandler resolveValidation(
2400        CmsObject cms,
2401        I_CmsXmlContentValue value,
2402        CmsXmlContentErrorHandler errorHandler) {
2403
2404        if (errorHandler == null) {
2405            // init a new error handler if required
2406            errorHandler = new CmsXmlContentErrorHandler();
2407        }
2408
2409        if (!value.isSimpleType()) {
2410            // no validation for a nested schema is possible
2411            // note that the sub-elements of the nested schema ARE validated by the node visitor,
2412            // it's just the nested schema value itself that does not support validation
2413            return errorHandler;
2414        }
2415
2416        // validate the error rules
2417        errorHandler = validateValue(cms, value, errorHandler, m_validationErrorRules, false);
2418        // validate the warning rules
2419        errorHandler = validateValue(cms, value, errorHandler, m_validationWarningRules, true);
2420        // validate categories
2421        errorHandler = validateCategories(cms, value, errorHandler);
2422        // return the result
2423        return errorHandler;
2424    }
2425
2426    /**
2427     * Adds a check rule for a specified element.<p>
2428     *
2429     * @param contentDefinition the XML content definition this XML content handler belongs to
2430     * @param elementName the element name to add the rule to
2431     * @param invalidate <code>false</code>, to disable link check /
2432     *                   <code>true</code> or <code>node</code>, to invalidate just the single node if the link is broken /
2433     *                   <code>parent</code>, if this rule will invalidate the whole parent node in nested content
2434     * @param type the relation type
2435     *
2436     * @throws CmsXmlException in case an unknown element name is used
2437     */
2438    protected void addCheckRule(
2439        CmsXmlContentDefinition contentDefinition,
2440        String elementName,
2441        String invalidate,
2442        String type)
2443    throws CmsXmlException {
2444
2445        I_CmsXmlSchemaType schemaType = contentDefinition.getSchemaType(elementName);
2446        if (schemaType == null) {
2447            // no element with the given name
2448            throw new CmsXmlException(
2449                Messages.get().container(Messages.ERR_XMLCONTENT_CHECK_INVALID_ELEM_1, elementName));
2450        }
2451        if (!CmsXmlVfsFileValue.TYPE_NAME.equals(schemaType.getTypeName())
2452            && !CmsXmlVarLinkValue.TYPE_NAME.equals(schemaType.getTypeName())) {
2453            // element is not a OpenCmsVfsFile
2454            throw new CmsXmlException(
2455                Messages.get().container(Messages.ERR_XMLCONTENT_CHECK_INVALID_TYPE_1, elementName));
2456        }
2457
2458        // cache the check rule data
2459        Boolean invalidateParent = null;
2460        if ((invalidate == null)
2461            || invalidate.equalsIgnoreCase(Boolean.TRUE.toString())
2462            || invalidate.equalsIgnoreCase(APPINFO_ATTR_TYPE_NODE)) {
2463            invalidateParent = Boolean.FALSE;
2464        } else if (invalidate.equalsIgnoreCase(APPINFO_ATTR_TYPE_PARENT)) {
2465            invalidateParent = Boolean.TRUE;
2466        }
2467        if (invalidateParent != null) {
2468            m_relationChecks.put(elementName, invalidateParent);
2469        }
2470        CmsRelationType relationType = (type == null ? CmsRelationType.XML_WEAK : CmsRelationType.valueOfXml(type));
2471        m_relations.put(elementName, relationType);
2472
2473        if (invalidateParent != null) {
2474            // check the whole xpath hierarchy
2475            String path = elementName;
2476            while (CmsStringUtil.isNotEmptyOrWhitespaceOnly(path)) {
2477                if (!isInvalidateParent(path)) {
2478                    // if invalidate type = node, then the node needs to be optional
2479                    if (contentDefinition.getSchemaType(path).getMinOccurs() > 0) {
2480                        // element is not optional
2481                        throw new CmsXmlException(
2482                            Messages.get().container(Messages.ERR_XMLCONTENT_CHECK_NOT_OPTIONAL_1, path));
2483                    }
2484                    // no need to further check
2485                    break;
2486                } else if (!CmsXmlUtils.isDeepXpath(path)) {
2487                    // if invalidate type = parent, then the node needs to be nested
2488                    // document root can not be invalidated
2489                    throw new CmsXmlException(Messages.get().container(Messages.ERR_XMLCONTENT_CHECK_NOT_EMPTY_DOC_0));
2490                }
2491                path = CmsXmlUtils.removeLastXpathElement(path);
2492            }
2493        }
2494    }
2495
2496    /**
2497     * Adds a configuration value for an element widget.<p>
2498     *
2499     * @param contentDefinition the XML content definition this XML content handler belongs to
2500     * @param elementName the element name
2501     * @param configurationValue the configuration value to use
2502     *
2503     * @throws CmsXmlException in case an unknown element name is used
2504     */
2505    protected void addConfiguration(
2506        CmsXmlContentDefinition contentDefinition,
2507        String elementName,
2508        String configurationValue)
2509    throws CmsXmlException {
2510
2511        if (!elementName.contains("/") && (contentDefinition.getSchemaType(elementName) == null)) {
2512            throw new CmsXmlException(
2513                Messages.get().container(Messages.ERR_XMLCONTENT_CONFIG_ELEM_UNKNOWN_1, elementName));
2514        }
2515
2516        m_configurationValues.put(elementName, configurationValue);
2517    }
2518
2519    /**
2520     * Adds a default value for an element.<p>
2521     *
2522     * @param contentDefinition the XML content definition this XML content handler belongs to
2523     * @param elementName the element name to map
2524     * @param defaultValue the default value to use
2525     * @param resolveMacrosValue the value of the 'resolveMacros' attribute
2526     *
2527     * @throws CmsXmlException in case an unknown element name is used
2528     */
2529    protected void addDefault(
2530        CmsXmlContentDefinition contentDefinition,
2531        String elementName,
2532        String defaultValue,
2533        String resolveMacrosValue)
2534    throws CmsXmlException {
2535
2536        if (contentDefinition.getSchemaType(elementName) == null) {
2537            throw new CmsXmlException(
2538                org.opencms.xml.types.Messages.get().container(
2539                    Messages.ERR_XMLCONTENT_INVALID_ELEM_DEFAULT_1,
2540                    elementName));
2541        }
2542        // store mappings as xpath to allow better control about what is mapped
2543        String xpath = CmsXmlUtils.createXpath(elementName, 1);
2544        m_defaultValues.put(xpath, defaultValue);
2545
2546        // macros are resolved by default
2547        if ((resolveMacrosValue != null) && !Boolean.parseBoolean(resolveMacrosValue)) {
2548            m_nonMacroResolvableDefaults.add(xpath);
2549        }
2550    }
2551
2552    /**
2553     * Adds all needed default check rules recursively for the given schema type.<p>
2554     *
2555     * @param rootContentDefinition the root content definition
2556     * @param schemaType the schema type to check
2557     * @param elementPath the current element path
2558     *
2559     * @throws CmsXmlException if something goes wrong
2560     */
2561    protected void addDefaultCheckRules(
2562        CmsXmlContentDefinition rootContentDefinition,
2563        I_CmsXmlSchemaType schemaType,
2564        String elementPath)
2565    throws CmsXmlException {
2566
2567        if ((schemaType != null) && schemaType.isSimpleType()) {
2568            if ((schemaType.getMinOccurs() == 0)
2569                && (CmsXmlVfsFileValue.TYPE_NAME.equals(schemaType.getTypeName())
2570                    || CmsXmlVarLinkValue.TYPE_NAME.equals(schemaType.getTypeName()))
2571                && !m_relationChecks.containsKey(elementPath)
2572                && !m_relations.containsKey(elementPath)) {
2573                // add default check rule for the element
2574                addCheckRule(rootContentDefinition, elementPath, null, null);
2575            }
2576        } else {
2577            // recursion required
2578            CmsXmlContentDefinition nestedContentDefinition = rootContentDefinition;
2579            if (schemaType != null) {
2580                CmsXmlNestedContentDefinition nestedDefinition = (CmsXmlNestedContentDefinition)schemaType;
2581                nestedContentDefinition = nestedDefinition.getNestedContentDefinition();
2582            }
2583            Iterator<String> itElems = nestedContentDefinition.getSchemaTypes().iterator();
2584            while (itElems.hasNext()) {
2585                String element = itElems.next();
2586                String path = (schemaType != null) ? CmsXmlUtils.concatXpath(elementPath, element) : element;
2587                I_CmsXmlSchemaType nestedSchema = nestedContentDefinition.getSchemaType(element);
2588                if ((schemaType == null) || !nestedSchema.equals(schemaType)) {
2589                    addDefaultCheckRules(rootContentDefinition, nestedSchema, path);
2590                }
2591            }
2592        }
2593    }
2594
2595    /**
2596     * Adds the given element to the compact view set.<p>
2597     *
2598     * @param contentDefinition the XML content definition this XML content handler belongs to
2599     * @param elementName the element name
2600     * @param displayType the display type to use for the element widget
2601     *
2602     * @throws CmsXmlException in case an unknown element name is used
2603     */
2604    protected void addDisplayType(
2605        CmsXmlContentDefinition contentDefinition,
2606        String elementName,
2607        DisplayType displayType)
2608    throws CmsXmlException {
2609
2610        if (contentDefinition.getSchemaType(elementName) == null) {
2611            throw new CmsXmlException(
2612                Messages.get().container(Messages.ERR_XMLCONTENT_CONFIG_ELEM_UNKNOWN_1, elementName));
2613        }
2614        m_displayTypes.put(elementName, displayType);
2615    }
2616
2617    /**
2618     * Finally adds the field used for geo-coordinate mapping by combining the configuration
2619     * from the geomapping section and the field settings.
2620     */
2621    protected void addGeoMappingField() {
2622
2623        CmsGeoMappingConfiguration mappingConfig = getGeoMappingConfiguration();
2624        if (mappingConfig != null) {
2625            CmsSolrField field = new CmsSolrField(
2626                GEOMAPPING_FIELD,
2627                Collections.emptyList(),
2628                CmsLocaleManager.getDefaultLocale(),
2629                "0.000000,0.000000");
2630            I_CmsSearchFieldMapping mapping = new CmsGeoCoordinateFieldMapping(getGeoMappingConfiguration());
2631            field.addMapping(mapping);
2632            m_searchFields.put("__geocoord__", field);
2633        }
2634    }
2635
2636    /**
2637     * Adds an element mapping.<p>
2638     *
2639     * @param contentDefinition the XML content definition this XML content handler belongs to
2640     * @param elementName the element name to map
2641     * @param mapping the mapping to use
2642     * @param useDefault the 'useDefault' attribute
2643     *
2644     * @throws CmsXmlException in case an unknown element name is used
2645     */
2646    protected void addMapping(
2647        CmsXmlContentDefinition contentDefinition,
2648        String elementName,
2649        String mapping,
2650        String useDefault)
2651    throws CmsXmlException {
2652
2653        if (contentDefinition.getSchemaType(elementName) == null) {
2654            throw new CmsXmlException(
2655                Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ELEM_MAPPING_1, elementName));
2656        }
2657
2658        // store mappings as xpath to allow better control about what is mapped
2659        String xpath = CmsXmlUtils.createXpath(elementName, 1);
2660        // since 7.0.2 multiple mappings are possible, so the mappings are stored in an array
2661        List<String> values = m_elementMappings.get(xpath);
2662        if (values == null) {
2663            // there should not really be THAT much multiple mappings per value...
2664            values = new ArrayList<String>(4);
2665            m_elementMappings.put(xpath, values);
2666        }
2667        if (Boolean.parseBoolean(useDefault)) {
2668            m_mappingsUsingDefault.add(xpath + ":" + mapping);
2669        }
2670        values.add(mapping);
2671        if (mapping.startsWith(MAPTO_PROPERTY) && mapping.endsWith(":" + CmsPropertyDefinition.PROPERTY_TITLE)) {
2672            // this is a title mapping
2673            m_titleMappings.add(xpath);
2674        }
2675    }
2676
2677    /**
2678     * Adds a nested formatter element.<p>
2679     *
2680     * @param elementName the element name
2681     * @param contentDefinition the content definition
2682     *
2683     * @throws CmsXmlException in case something goes wrong
2684     */
2685    protected void addNestedFormatter(String elementName, CmsXmlContentDefinition contentDefinition)
2686    throws CmsXmlException {
2687
2688        if (contentDefinition.getSchemaType(elementName) == null) {
2689            throw new CmsXmlException(
2690                Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ELEM_MAPPING_1, elementName));
2691        }
2692        m_nestedFormatterElements.add(elementName);
2693    }
2694
2695    /**
2696     * Adds a Solr field for an element.<p>
2697     *
2698     * @param contentDefinition the XML content definition this XML content handler belongs to
2699     * @param field the Solr field
2700     */
2701    @Deprecated
2702    protected void addSearchField(CmsXmlContentDefinition contentDefinition, CmsSearchField field) {
2703
2704        addSearchField(contentDefinition, field, I_CmsXmlContentHandler.MappingType.ELEMENT);
2705    }
2706
2707    /**
2708     * Adds a Solr field for an element.<p>
2709     *
2710     * @param contentDefinition the XML content definition this XML content handler belongs to
2711     * @param field the Solr field
2712     * @param type the type, specifying if the field should be attached to the document of the XML content or to all container pages the content is placed on
2713     */
2714    protected void addSearchField(
2715        CmsXmlContentDefinition contentDefinition,
2716        CmsSearchField field,
2717        I_CmsXmlContentHandler.MappingType type) {
2718
2719        Locale locale = null;
2720        if (field instanceof CmsSolrField) {
2721            locale = ((CmsSolrField)field).getLocale();
2722        }
2723        String key = CmsXmlUtils.concatXpath(locale != null ? locale.toString() : null, field.getName());
2724        switch (type) {
2725            case PAGE:
2726                m_searchFieldsPage.put(key, field);
2727                break;
2728            case ELEMENT:
2729            default:
2730                m_searchFields.put(key, field);
2731                break;
2732        }
2733    }
2734
2735    /**
2736     * Adds a search setting for an element.<p>
2737     *
2738     * @param contentDefinition the XML content definition this XML content handler belongs to
2739     * @param elementName the element name to map
2740     * @param value the search setting value to store
2741     *
2742     * @throws CmsXmlException in case an unknown element name is used
2743     */
2744    protected void addSearchSetting(
2745        CmsXmlContentDefinition contentDefinition,
2746        String elementName,
2747        CmsSearchContentConfig value)
2748    throws CmsXmlException {
2749
2750        if (contentDefinition.getSchemaType(elementName) == null) {
2751            throw new CmsXmlException(
2752                Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ELEM_SEARCHSETTINGS_1, elementName));
2753        }
2754        // store the search exclusion as defined
2755        m_searchSettings.put(elementName, value);
2756    }
2757
2758    /**
2759     * Adds search settings as defined by 'simple' syntax in fields.<p>
2760     *
2761     * @param contentDef the content definition
2762     * @param name the element name
2763     * @param value the search setting value
2764     * @throws CmsXmlException if something goes wrong
2765     */
2766    protected void addSimpleSearchSetting(CmsXmlContentDefinition contentDef, String name, String value)
2767    throws CmsXmlException {
2768
2769        SearchContentType searchContentType = SearchContentType.fromString(value);
2770        if (null != searchContentType) {
2771            addSearchSetting(contentDef, name, CmsSearchContentConfig.get(searchContentType));
2772        } else {
2773            if ("geocoords".equals(value) || "listgeocoords".equals(value)) {
2774                m_primaryGeomappingField = name;
2775                m_searchSettings.put(CmsXmlUtils.removeXpath(name), I_CmsXmlContentValue.CmsSearchContentConfig.FALSE);
2776            } else {
2777                StringTemplate template = m_searchTemplateGroup.getInstanceOf(value);
2778                if ((template != null) && (template.getFormalArgument("name") != null)) {
2779                    template.setAttribute("name", CmsEncoder.escapeXml(name));
2780                    String xml = template.toString();
2781                    try {
2782                        Document doc = DocumentHelper.parseText(xml);
2783                        initSearchSettings(doc.getRootElement(), contentDef);
2784                    } catch (DocumentException e) {
2785                        LOG.error(e.getLocalizedMessage(), e);
2786                    }
2787                }
2788            }
2789        }
2790    }
2791
2792    /**
2793     * Adds a validation rule for a specified element.<p>
2794     *
2795     * @param contentDefinition the XML content definition this XML content handler belongs to
2796     * @param elementName the element name to add the rule to
2797     * @param regex the validation rule regular expression
2798     * @param message the message in case validation fails (may be null)
2799     * @param isWarning if true, this rule is used for warnings, otherwise it's an error
2800     *
2801     * @throws CmsXmlException in case an unknown element name is used
2802     */
2803    protected void addValidationRule(
2804        CmsXmlContentDefinition contentDefinition,
2805        String elementName,
2806        String regex,
2807        String message,
2808        boolean isWarning)
2809    throws CmsXmlException {
2810
2811        if (contentDefinition.getSchemaType(elementName) == null) {
2812            throw new CmsXmlException(
2813                Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ELEM_VALIDATION_1, elementName));
2814        }
2815
2816        if (isWarning) {
2817            m_validationWarningRules.put(elementName, regex);
2818            if (message != null) {
2819                m_validationWarningMessages.put(elementName, message);
2820            }
2821        } else {
2822            m_validationErrorRules.put(elementName, regex);
2823            if (message != null) {
2824                m_validationErrorMessages.put(elementName, message);
2825            }
2826        }
2827    }
2828
2829    /**
2830     * Adds a GUI widget for a specified element.<p>
2831     *
2832     * @param contentDefinition the XML content definition this XML content handler belongs to
2833     * @param elementName the element name to map
2834     * @param name the widget to use as GUI for the element (registered alias or class name)
2835     *
2836     * @throws CmsXmlException in case an unknown element name is used
2837     */
2838    protected void addWidget(CmsXmlContentDefinition contentDefinition, String elementName, String name)
2839    throws CmsXmlException {
2840
2841        if (!elementName.contains("/") && (contentDefinition.getSchemaType(elementName) == null)) {
2842            throw new CmsXmlException(
2843                Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ELEM_LAYOUTWIDGET_1, elementName));
2844        }
2845
2846        if (name.indexOf(I_CmsMacroResolver.MACRO_DELIMITER) == -1) {
2847            // we can only validate this if we don't have macros
2848            if (OpenCms.getXmlContentTypeManager().getWidget(name) == null) {
2849                if (CmsStringUtil.isValidJavaClassName(name)) {
2850                    try {
2851                        Class<?> cls = Class.forName(name, false, getClass().getClassLoader());
2852                        if (!I_CmsWidget.class.isAssignableFrom(cls)
2853                            && !I_CmsComplexWidget.class.isAssignableFrom(cls)) {
2854                            throw new CmsXmlException(
2855                                Messages.get().container(
2856                                    Messages.ERR_XMLCONTENT_INVALID_CUSTOM_CLASS_3,
2857                                    name,
2858                                    elementName,
2859                                    contentDefinition.getSchemaLocation()));
2860
2861                        }
2862                    } catch (Exception e) {
2863                        throw new CmsXmlException(
2864                            Messages.get().container(
2865                                Messages.ERR_XMLCONTENT_INVALID_CUSTOM_CLASS_3,
2866                                name,
2867                                elementName,
2868                                contentDefinition.getSchemaLocation()),
2869                            e);
2870                    }
2871                }
2872            }
2873
2874        }
2875        m_widgetNames.put(elementName, name);
2876    }
2877
2878    /**
2879     * Helper method to create a visibility configuration.<p>
2880     *
2881     * @param className the visibility handler class name
2882     * @param params the parameters for the visibility
2883     *
2884     * @return the visibility configuration
2885     */
2886    protected VisibilityConfiguration createVisibilityConfiguration(String className, String params) {
2887
2888        I_CmsXmlContentVisibilityHandler handler = this;
2889        if (className != null) {
2890            try {
2891                handler = (I_CmsXmlContentVisibilityHandler)(Class.forName(className).newInstance());
2892            } catch (Exception e) {
2893                LOG.error(e.getLocalizedMessage(), e);
2894            }
2895        }
2896        VisibilityConfiguration result = new VisibilityConfiguration(handler, params);
2897        return result;
2898    }
2899
2900    /**
2901     * Returns information about the availability mapping for the given availability attribute.
2902     *
2903     * @param attr the availability attribute
2904     * @return the information about the mapping
2905     */
2906    protected MappingInfo getAttributeMapping(AttributeType attr) {
2907
2908        String target = null;
2909        String source = null;
2910        switch (attr) {
2911            case expiration:
2912                target = MAPTO_ATTRIBUTE + ATTRIBUTE_DATEEXPIRED;
2913                break;
2914
2915            case release:
2916                target = MAPTO_ATTRIBUTE + ATTRIBUTE_DATERELEASED;
2917                break;
2918
2919            default:
2920                break;
2921        }
2922        if (target != null) {
2923            source = getMappingSource(target);
2924        }
2925
2926        return new MappingInfo(source, target);
2927    }
2928
2929    /**
2930     * Returns the configured default locales for the content of the given resource.<p>
2931     *
2932     * @param cms the cms context
2933     * @param resource the resource path to get the default locales for
2934     *
2935     * @return the default locales of the resource
2936     */
2937    protected List<Locale> getLocalesForResource(CmsObject cms, String resource) {
2938
2939        List<Locale> locales = OpenCms.getLocaleManager().getDefaultLocales(cms, resource);
2940        if ((locales == null) || locales.isEmpty()) {
2941            locales = OpenCms.getLocaleManager().getAvailableLocales();
2942        }
2943        return locales;
2944    }
2945
2946    /**
2947     * Creates editor change handler instances for all nested fields that have configured them in their field settings
2948     *
2949     * @return editor change handlers for all nested fields for which they are configured
2950     */
2951    protected List<I_CmsXmlContentEditorChangeHandler> getNestedEditorChangeHandlers() {
2952
2953        Multimap<String, CmsChangeHandlerConfig> configMap = ArrayListMultimap.create();
2954        collectNestedChangeHandlerConfigs(m_contentDefinition, "", configMap);
2955        List<I_CmsXmlContentEditorChangeHandler> result = new ArrayList<>();
2956        for (String key : configMap.keySet()) {
2957            for (CmsChangeHandlerConfig handlerConfig : configMap.get(key)) {
2958                String path = CmsStringUtil.joinPaths(key, handlerConfig.getField());
2959                path = CmsFileUtil.removeLeadingSeparator(path);
2960                String scope = normalizeChangeHandlerScope(path);
2961                java.util.Optional<I_CmsXmlContentEditorChangeHandler> optHandler = handlerConfig.newHandler(scope);
2962                if (optHandler.isPresent()) {
2963                    result.add(optHandler.get());
2964                }
2965            }
2966        }
2967        List<I_CmsXmlContentEditorChangeHandler> nestedHandlers = result;
2968        return nestedHandlers;
2969    }
2970
2971    /**
2972     * Returns the category reference path for the given value.<p>
2973     *
2974     * @param cms the cms context
2975     * @param value the xml content value
2976     *
2977     * @return the category reference path for the given value
2978     */
2979    protected String getReferencePath(CmsObject cms, I_CmsXmlContentValue value) {
2980
2981        // get the original file instead of the temp file
2982        CmsFile file = value.getDocument().getFile();
2983        String resourceName = cms.getSitePath(file);
2984        if (CmsWorkplace.isTemporaryFile(file)) {
2985            StringBuffer result = new StringBuffer(resourceName.length() + 2);
2986            result.append(CmsResource.getFolderPath(resourceName));
2987            result.append(CmsResource.getName(resourceName).substring(1));
2988            resourceName = result.toString();
2989        }
2990        try {
2991            List<CmsResource> listsib = cms.readSiblings(resourceName, CmsResourceFilter.ALL);
2992            for (int i = 0; i < listsib.size(); i++) {
2993                CmsResource resource = listsib.get(i);
2994                // get the default locale of the resource and set the categories
2995                List<Locale> locales = getLocalesForResource(cms, cms.getSitePath(resource));
2996                for (Locale l : locales) {
2997                    if (value.getLocale().equals(l)) {
2998                        return cms.getSitePath(resource);
2999                    }
3000                }
3001            }
3002        } catch (CmsVfsResourceNotFoundException e) {
3003            // may hapen if editing a new resource
3004            if (LOG.isDebugEnabled()) {
3005                LOG.debug(e.getLocalizedMessage(), e);
3006            }
3007        } catch (CmsException e) {
3008            if (LOG.isErrorEnabled()) {
3009                LOG.error(e.getLocalizedMessage(), e);
3010            }
3011        }
3012        // if the locale can not be found, just take the current file
3013        return cms.getSitePath(file);
3014    }
3015
3016    /**
3017     * Returns the validation message to be displayed if a certain rule was violated.<p>
3018     *
3019     * @param cms the current users OpenCms context
3020     * @param value the value to validate
3021     * @param regex the rule that was violated
3022     * @param valueStr the string value of the given value
3023     * @param matchResult if false, the rule was negated
3024     * @param isWarning if true, this validation indicate a warning, otherwise an error
3025     *
3026     * @return the validation message to be displayed
3027     */
3028    protected String getValidationMessage(
3029        CmsObject cms,
3030        I_CmsXmlContentValue value,
3031        String regex,
3032        String valueStr,
3033        boolean matchResult,
3034        boolean isWarning) {
3035
3036        String message = null;
3037        if (isWarning) {
3038            message = m_validationWarningMessages.get(value.getName());
3039        } else {
3040            message = m_validationErrorMessages.get(value.getName());
3041        }
3042
3043        if (message == null) {
3044            if (isWarning) {
3045                message = MESSAGE_VALIDATION_DEFAULT_WARNING;
3046            } else {
3047                message = MESSAGE_VALIDATION_DEFAULT_ERROR;
3048            }
3049        }
3050
3051        // create additional macro values
3052        Map<String, String> additionalValues = new HashMap<String, String>();
3053        additionalValues.put(CmsMacroResolver.KEY_VALIDATION_VALUE, valueStr);
3054        additionalValues.put(CmsMacroResolver.KEY_VALIDATION_REGEX, ((!matchResult) ? "!" : "") + regex);
3055        additionalValues.put(CmsMacroResolver.KEY_VALIDATION_PATH, value.getPath());
3056
3057        CmsMacroResolver resolver = CmsMacroResolver.newInstance().setCmsObject(cms).setMessages(
3058            getMessages(OpenCms.getWorkplaceManager().getWorkplaceLocale(cms))).setAdditionalMacros(additionalValues);
3059
3060        return resolver.resolveMacros(message);
3061    }
3062
3063    /**
3064     * Called when this content handler is initialized.<p>
3065     */
3066    protected void init() {
3067
3068        m_elementMappings = new HashMap<String, List<String>>();
3069        m_validationErrorRules = new HashMap<String, String>();
3070        m_validationErrorMessages = new HashMap<String, String>();
3071        m_validationWarningRules = new HashMap<String, String>();
3072        m_validationWarningMessages = new HashMap<String, String>();
3073        m_defaultValues = new HashMap<String, String>();
3074        m_configurationValues = new HashMap<String, String>();
3075        m_searchSettings = new HashMap<String, CmsSearchContentConfig>();
3076        m_relations = new HashMap<String, CmsRelationType>();
3077        m_relationChecks = new HashMap<String, Boolean>();
3078        m_previewLocation = null;
3079        m_modelFolder = null;
3080        m_tabs = new ArrayList<CmsXmlContentTab>();
3081        m_cssHeadIncludes = new LinkedHashSet<String>();
3082        m_jsHeadIncludes = new LinkedHashSet<String>();
3083        m_settings = new LinkedHashMap<String, CmsXmlContentProperty>();
3084        m_titleMappings = new ArrayList<String>(2);
3085        m_formatters = new ArrayList<CmsFormatterBean>();
3086        m_searchFields = new HashMap<String, CmsSearchField>();
3087        m_searchFieldsPage = new HashMap<String, CmsSearchField>();
3088        m_allowedTemplates = new CmsDefaultSet<String>();
3089        m_allowedTemplates.setDefaultMembership(true);
3090        m_displayTypes = new HashMap<String, DisplayType>();
3091        m_editorChangeHandlers = new ArrayList<I_CmsXmlContentEditorChangeHandler>();
3092        m_nestedFormatterElements = new HashSet<String>();
3093        try (
3094        InputStream stream = CmsDefaultXmlContentHandler.class.getResourceAsStream("simple-searchsetting-configs.st")) {
3095            m_searchTemplateGroup = CmsStringUtil.readStringTemplateGroup(stream);
3096        } catch (IOException e) {
3097            LOG.error(e.getLocalizedMessage(), e);
3098        }
3099    }
3100
3101    /**
3102    * Initializes the default values for this content handler.<p>
3103    *
3104    * Using the default values from the appinfo node, it's possible to have more
3105    * sophisticated logic for generating the defaults then just using the XML schema "default"
3106    * attribute.<p>
3107    *
3108    * @param root the "defaults" element from the appinfo node of the XML content definition
3109    * @param contentDefinition the content definition the default values belong to
3110    * @throws CmsXmlException if something goes wrong
3111    */
3112    protected void initDefaultValues(Element root, CmsXmlContentDefinition contentDefinition) throws CmsXmlException {
3113
3114        Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(root, APPINFO_DEFAULT);
3115        while (i.hasNext()) {
3116            // iterate all "default" elements in the "defaults" node
3117            Element element = i.next();
3118            String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
3119            String defaultValue = element.attributeValue(APPINFO_ATTR_VALUE);
3120            String resolveMacrosValue = element.attributeValue(APPINFO_ATTR_RESOLVE_MACROS);
3121            if ((elementName != null) && (defaultValue != null)) {
3122                // add a default value mapping for the element
3123                addDefault(contentDefinition, elementName, defaultValue, resolveMacrosValue);
3124            }
3125        }
3126    }
3127
3128    /**
3129     * Initializes the default complex widget.<p>
3130     *
3131     * @param element the element in which the default complex widget is configured
3132     */
3133    protected void initDefaultWidget(Element element) {
3134
3135        m_defaultWidget = element.attributeValue(APPINFO_ATTR_WIDGET);
3136        m_defaultWidgetConfig = element.attributeValue(APPINFO_ATTR_CONFIGURATION);
3137        try {
3138            m_defaultWidgetInstance = (I_CmsComplexWidget)(Class.forName(m_defaultWidget).newInstance());
3139        } catch (Exception e) {
3140            LOG.error(e.getLocalizedMessage(), e);
3141        }
3142    }
3143
3144    /**
3145     * Initializes the edit handler.<p>
3146     *
3147     * @param handlerElement the edit handler element
3148     */
3149    protected void initEditHandler(Element handlerElement) {
3150
3151        String editHandlerClass = handlerElement.attributeValue(APPINFO_ATTR_CLASS);
3152        Map<String, String> params = Maps.newHashMap();
3153        Element paramsElement = handlerElement.element(APPINFO_PARAMETERS);
3154        if (paramsElement != null) {
3155            for (Element paramElement : paramsElement.elements(APPINFO_PARAM)) {
3156                String name = paramElement.attributeValue(APPINFO_ATTR_NAME);
3157                String value = paramElement.getText();
3158                params.put(name, value);
3159            }
3160        }
3161        try {
3162            m_editHandler = (I_CmsEditHandler)Class.forName(editHandlerClass).newInstance();
3163            m_editHandler.setParameters(params);
3164        } catch (Exception e) {
3165            LOG.error(e.getMessage(), e);
3166        }
3167    }
3168
3169    /**
3170     * Initializes the editor change handlers.<p>
3171     *
3172     * @param element the editorchangehandlers node of the app info
3173     */
3174    protected void initEditorChangeHandlers(Element element) {
3175
3176        Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(element, APPINFO_EDITOR_CHANGE_HANDLER);
3177        while (i.hasNext()) {
3178            // iterate all "default" elements in the "defaults" node
3179            Element handlerElement = i.next();
3180            String handlerClass = handlerElement.attributeValue(APPINFO_ATTR_CLASS);
3181            String configuration = handlerElement.attributeValue(APPINFO_ATTR_CONFIGURATION);
3182            String scope = handlerElement.attributeValue(APPINFO_ATTR_SCOPE);
3183            try {
3184                I_CmsXmlContentEditorChangeHandler handler = (I_CmsXmlContentEditorChangeHandler)Class.forName(
3185                    handlerClass).newInstance();
3186                handler.setConfiguration(configuration);
3187                handler.setScope(scope);
3188                m_editorChangeHandlers.add(handler);
3189            } catch (Exception e) {
3190                LOG.error(e.getLocalizedMessage(), e);
3191            }
3192        }
3193    }
3194
3195    /**
3196     * Processes a single field definition.<p>
3197     *
3198     * @param elem the parent element
3199     * @param contentDef the content definition
3200     *
3201     * @throws CmsXmlException if something goes wrong
3202     */
3203    protected void initField(Element elem, CmsXmlContentDefinition contentDef) throws CmsXmlException {
3204
3205        String nameVal = elem.elementText(CmsConfigurationReader.N_PROPERTY_NAME);
3206        if (nameVal == null) {
3207            throw new CmsXmlException(Messages.get().container(Messages.ERR_XMLCONTENT_BAD_FIELD_NAME_1, nameVal));
3208        }
3209        final String name = nameVal.trim();
3210
3211        String ruleRegex = elem.elementText(CmsConfigurationReader.N_RULE_REGEX);
3212        String ruleType = elem.elementText(CmsConfigurationReader.N_RULE_TYPE);
3213        String error = elem.elementText(CmsConfigurationReader.N_ERROR);
3214        if (error == null) {
3215            error = "";
3216        }
3217        if (!CmsStringUtil.isEmptyOrWhitespaceOnly(ruleRegex)) {
3218            addValidationRule(contentDef, name, ruleRegex, error, "warning".equalsIgnoreCase(ruleType));
3219        } else if (!CmsStringUtil.isEmptyOrWhitespaceOnly(error)) {
3220            if ("warning".equalsIgnoreCase(ruleType)) {
3221                m_validationWarningMessages.put(name, error);
3222            } else {
3223                m_validationErrorMessages.put(name, error);
3224            }
3225        }
3226
3227        String defaultValue = elem.elementText(CmsConfigurationReader.N_DEFAULT);
3228        String defaultResolveMacros = elem.elementTextTrim(FieldSettingElems.DefaultResolveMacros.name());
3229        if (!CmsStringUtil.isEmptyOrWhitespaceOnly(defaultValue)) {
3230            addDefault(contentDef, name, defaultValue, defaultResolveMacros);
3231        }
3232
3233        String widget = elem.elementText(CmsConfigurationReader.N_WIDGET);
3234        String widgetConfig = elem.elementText(CmsConfigurationReader.N_WIDGET_CONFIG);
3235        if (widget != null) {
3236            addWidget(contentDef, name, widget);
3237        }
3238        if (widgetConfig != null) {
3239            widgetConfig = widgetConfig.trim();
3240            addConfiguration(contentDef, name, widgetConfig);
3241        }
3242
3243        String niceName = elem.elementText(CmsConfigurationReader.N_DISPLAY_NAME);
3244        if (niceName != null) {
3245            m_fieldNiceNames.put(name, niceName);
3246        }
3247        String description = elem.elementText(CmsConfigurationReader.N_DESCRIPTION);
3248        if (description != null) {
3249            m_fieldDescriptions.put(name, description);
3250        }
3251        for (Element mappingElem : elem.elements(FieldSettingElems.Mapping.name())) {
3252            String mapTo = mappingElem.elementText(FieldSettingElems.MapTo.name());
3253            String useDefault = mappingElem.elementText(FieldSettingElems.UseDefault.name());
3254            if (mapTo != null) {
3255                addMapping(contentDef, name, mapTo, useDefault);
3256            }
3257        }
3258        String display = elem.elementTextTrim(FieldSettingElems.Display.name());
3259        if (!CmsStringUtil.isEmptyOrWhitespaceOnly(display)) {
3260            try {
3261                addDisplayType(contentDef, name, DisplayType.valueOf(display));
3262            } catch (Exception e) {
3263                LOG.error(e.getLocalizedMessage(), e);
3264            }
3265        }
3266        String synchronization = elem.elementTextTrim(FieldSettingElems.Synchronization.name());
3267        if (synchronization != null) {
3268            if ("strong".equals(synchronization)) {
3269                m_synchronizations.put(name, SynchronizationMode.strong);
3270            } else if (Boolean.parseBoolean(synchronization)) {
3271                m_synchronizations.put(name, SynchronizationMode.standard);
3272            } else {
3273                // we use a distinct value rather than just leaving it empty because we want to be able to override the synchronization
3274                // definition in a nested schema with the one in the top-level schema
3275                m_synchronizations.put(name, SynchronizationMode.none);
3276            }
3277        }
3278
3279        for (Element relElem : elem.elements(FieldSettingElems.Relation.name())) {
3280            String type = relElem.elementTextTrim(FieldSettingElems.Type.name());
3281            String invalidate = relElem.elementTextTrim(FieldSettingElems.Invalidate.name());
3282            if (type != null) {
3283                type = type.toLowerCase();
3284            }
3285            if (invalidate != null) {
3286                invalidate = invalidate.toLowerCase();
3287            }
3288            addCheckRule(contentDef, name, invalidate, type);
3289        }
3290
3291        for (Element visElem : elem.elements(FieldSettingElems.Visibility.name())) {
3292            String params = visElem.getText();
3293            VisibilityConfiguration visConfig = createVisibilityConfiguration(null, params);
3294            m_visibilityConfigurations.put(name, visConfig);
3295        }
3296
3297        for (Element visElem : elem.elements(FieldSettingElems.FieldVisibility.name())) {
3298            String className = visElem.elementTextTrim(FieldSettingElems.Class.name());
3299            String params = visElem.elementTextTrim(FieldSettingElems.Params.name());
3300            VisibilityConfiguration visConfig = createVisibilityConfiguration(className, params);
3301            m_visibilityConfigurations.put(name, visConfig);
3302        }
3303
3304        String nestedFormatter = elem.elementTextTrim(FieldSettingElems.NestedFormatter.name());
3305        if (Boolean.parseBoolean(nestedFormatter)) {
3306            m_nestedFormatterElements.add(name);
3307        }
3308
3309        String search = elem.elementTextTrim(FieldSettingElems.Search.name());
3310        if (search != null) {
3311            addSimpleSearchSetting(contentDef, name, search);
3312        }
3313
3314        String ifInvalidRelationStr = elem.elementTextTrim(FieldSettingElems.IfInvalidRelation.name());
3315        if (CmsStringUtil.isEmptyOrWhitespaceOnly(ifInvalidRelationStr)) {
3316            ifInvalidRelationStr = null;
3317        }
3318        if (ifInvalidRelationStr != null) {
3319            if (name.contains("[") || name.contains("/")) {
3320                LOG.error("Only simple field names allowed for the IfInvalidRelation field setting.");
3321            } else {
3322                try {
3323                    InvalidRelationAction ifInvalidRelation = InvalidRelationAction.valueOf(ifInvalidRelationStr);
3324                    m_invalidRelationActions.put(name, ifInvalidRelation);
3325                } catch (Exception e) {
3326                    LOG.error(e.getLocalizedMessage(), e);
3327                }
3328            }
3329
3330        }
3331
3332        for (Element changeHandlerElem : elem.elements(N_CHANGEHANDLER)) {
3333            String config = changeHandlerElem.attributeValue(A_CONFIGURATION);
3334            String className = changeHandlerElem.getText().trim();
3335            CmsChangeHandlerConfig entry = new CmsChangeHandlerConfig(name, className, config);
3336            m_changeHandlerConfigs.add(entry);
3337        }
3338
3339        String agentsStr = elem.elementTextTrim(FieldSettingElems.Agents.name());
3340        if (!CmsStringUtil.isEmptyOrWhitespaceOnly(agentsStr)) {
3341            Set<String> agentTags = new HashSet<>();
3342            for (String token : CmsStringUtil.splitAsList(agentsStr, ",")) {
3343                token = token.trim();
3344                if (!CmsStringUtil.isEmptyOrWhitespaceOnly(token)) {
3345                    agentTags.add(token);
3346                }
3347            }
3348            m_agentTags.put(name, Collections.unmodifiableSet(agentTags));
3349        }
3350
3351    }
3352
3353    /**
3354     * Processes all field declarations in the schema.<p>
3355     *
3356     * @param parent the parent element
3357     * @param contentDef the content definition
3358     *
3359     * @throws CmsXmlException if something goes wrong
3360     */
3361    protected void initFields(Element parent, CmsXmlContentDefinition contentDef) throws CmsXmlException {
3362
3363        for (Element fieldElem : parent.elements(N_SETTING)) {
3364            initField(fieldElem, contentDef);
3365        }
3366    }
3367
3368    /**
3369     * Initializes the formatters for this content handler.<p>
3370     *
3371     * @param root the "formatters" element from the appinfo node of the XML content definition
3372     * @param contentDefinition the content definition the formatters belong to
3373     */
3374    protected void initFormatters(Element root, CmsXmlContentDefinition contentDefinition) {
3375
3376        // reading the include resources common for all formatters
3377        Iterator<Element> itFormatter = CmsXmlGenericWrapper.elementIterator(root, APPINFO_FORMATTER);
3378        while (itFormatter.hasNext()) {
3379            // iterate all "formatter" elements in the "formatters" node
3380            Element element = itFormatter.next();
3381            String type = element.attributeValue(APPINFO_ATTR_TYPE);
3382            if (CmsStringUtil.isEmptyOrWhitespaceOnly(type)) {
3383                // if not set use "*" as default for type
3384                type = CmsFormatterBean.WILDCARD_TYPE;
3385            }
3386            String jspRootPath = element.attributeValue(APPINFO_ATTR_URI);
3387            String minWidthStr = element.attributeValue(APPINFO_ATTR_MINWIDTH);
3388            String maxWidthStr = element.attributeValue(APPINFO_ATTR_MAXWIDTH);
3389            String preview = element.attributeValue(APPINFO_ATTR_PREVIEW);
3390            String searchContent = element.attributeValue(APPINFO_ATTR_SEARCHCONTENT);
3391            m_formatters.add(
3392                new CmsFormatterBean(
3393                    type,
3394                    jspRootPath,
3395                    minWidthStr,
3396                    maxWidthStr,
3397                    preview,
3398                    searchContent,
3399                    contentDefinition.getSchemaLocation()));
3400        }
3401    }
3402
3403    /**
3404     * Initializes the head includes for this content handler.<p>
3405     *
3406     * @param root the "headincludes" element from the appinfo node of the XML content definition
3407     * @param contentDefinition the content definition the head-includes belong to
3408     */
3409    protected void initHeadIncludes(Element root, CmsXmlContentDefinition contentDefinition) {
3410
3411        Iterator<Element> itInclude = CmsXmlGenericWrapper.elementIterator(root, APPINFO_HEAD_INCLUDE);
3412        while (itInclude.hasNext()) {
3413            Element element = itInclude.next();
3414            String type = element.attributeValue(APPINFO_ATTR_TYPE);
3415            String uri = element.attributeValue(APPINFO_ATTR_URI);
3416            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(uri)) {
3417                if (ATTRIBUTE_INCLUDE_TYPE_CSS.equals(type)) {
3418                    m_cssHeadIncludes.add(uri);
3419                } else if (ATTRIBUTE_INCLUDE_TYPE_JAVASCRIPT.equals(type)) {
3420                    m_jsHeadIncludes.add(uri);
3421                }
3422            }
3423        }
3424    }
3425
3426    /**
3427     * Reads the JSON renderer settings.
3428     *
3429     * @param element the configuration XML element
3430     */
3431    protected void initJsonRenderer(Element element) {
3432
3433        String cls = element.attributeValue(APPINFO_ATTR_CLASS);
3434        Map<String, String> params = new HashMap<>();
3435        for (Element paramElement : element.elements(APPINFO_PARAM)) {
3436            String name = paramElement.attributeValue(APPINFO_ATTR_NAME);
3437            String value = paramElement.getText();
3438            params.put(name, value);
3439        }
3440        m_jsonRendererSettings = new JsonRendererSettings(cls, params);
3441
3442    }
3443
3444    /**
3445    * Initializes the layout for this content handler.<p>
3446    *
3447    * Unless otherwise instructed, the editor uses one specific GUI widget for each
3448    * XML value schema type. For example, for a {@link org.opencms.xml.types.CmsXmlStringValue}
3449    * the default widget is the {@link org.opencms.widgets.CmsInputWidget}.
3450    * However, certain values can also use more then one widget, for example you may
3451    * also use a {@link org.opencms.widgets.CmsCheckboxWidget} for a String value,
3452    * and as a result the Strings possible values would be either <code>"false"</code> or <code>"true"</code>,
3453    * but nevertheless be a String.<p>
3454    *
3455    * The widget to use can further be controlled using the <code>widget</code> attribute.
3456    * You can specify either a valid widget alias such as <code>StringWidget</code>,
3457    * or the name of a Java class that implements <code>{@link I_CmsWidget}</code>.<p>
3458    *
3459    * Configuration options to the widget can be passed using the <code>configuration</code>
3460    * attribute. You can specify any String as configuration. This String is then passed
3461    * to the widget during initialization. It's up to the individual widget implementation
3462    * to interpret this configuration String.<p>
3463    *
3464    * @param root the "layouts" element from the appinfo node of the XML content definition
3465    * @param contentDefinition the content definition the layout belongs to
3466    *
3467    * @throws CmsXmlException if something goes wrong
3468    */
3469    protected void initLayouts(Element root, CmsXmlContentDefinition contentDefinition) throws CmsXmlException {
3470
3471        m_useAcacia = safeParseBoolean(root.attributeValue(ATTR_USE_ACACIA), true);
3472        Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(root, APPINFO_LAYOUT);
3473        while (i.hasNext()) {
3474            // iterate all "layout" elements in the "layouts" node
3475            Element element = i.next();
3476            String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
3477            String widgetClassOrAlias = element.attributeValue(APPINFO_ATTR_WIDGET);
3478            String configuration = element.attributeValue(APPINFO_ATTR_CONFIGURATION);
3479            String displayStr = element.attributeValue(APPINFO_ATTR_DISPLAY);
3480            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(displayStr) && (elementName != null)) {
3481                addDisplayType(contentDefinition, elementName, DisplayType.valueOf(displayStr));
3482            }
3483            if ((elementName != null) && CmsStringUtil.isNotEmptyOrWhitespaceOnly(widgetClassOrAlias)) {
3484                // add a widget mapping for the element
3485                addWidget(contentDefinition, elementName, widgetClassOrAlias);
3486                if (configuration != null) {
3487                    addConfiguration(contentDefinition, elementName, configuration);
3488                }
3489            }
3490        }
3491    }
3492
3493    /**
3494    * Initializes the element mappings for this content handler.<p>
3495    *
3496    * Element mappings allow storing values from the XML content in other locations.
3497    * For example, if you have an element called "Title", it's likely a good idea to
3498    * store the value of this element also in the "Title" property of a XML content resource.<p>
3499    *
3500    * @param root the "mappings" element from the appinfo node of the XML content definition
3501    * @param contentDefinition the content definition the mappings belong to
3502    * @throws CmsXmlException if something goes wrong
3503    */
3504    protected void initMappings(Element root, CmsXmlContentDefinition contentDefinition) throws CmsXmlException {
3505
3506        Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(root, APPINFO_MAPPING);
3507        while (i.hasNext()) {
3508            // iterate all "mapping" elements in the "mappings" node
3509            Element element = i.next();
3510            // this is a mapping node
3511            String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
3512            String maptoName = element.attributeValue(APPINFO_ATTR_MAPTO);
3513            String useDefault = element.attributeValue(APPINFO_ATTR_USE_DEFAULT);
3514            if ((elementName != null) && (maptoName != null)) {
3515                // add the element mapping
3516                addMapping(contentDefinition, elementName, maptoName, useDefault);
3517            }
3518        }
3519    }
3520
3521    /**
3522     * Initializes the folder containing the model file(s) for this content handler.<p>
3523     *
3524     * @param root the "modelfolder" element from the appinfo node of the XML content definition
3525     * @param contentDefinition the content definition the model folder belongs to
3526     * @throws CmsXmlException if something goes wrong
3527     */
3528    protected void initModelFolder(Element root, CmsXmlContentDefinition contentDefinition) throws CmsXmlException {
3529
3530        String master = root.attributeValue(APPINFO_ATTR_URI);
3531        if (master == null) {
3532            throw new CmsXmlException(
3533                Messages.get().container(
3534                    Messages.ERR_XMLCONTENT_MISSING_MODELFOLDER_URI_2,
3535                    root.getName(),
3536                    contentDefinition.getSchemaLocation()));
3537        }
3538        m_modelFolder = master;
3539    }
3540
3541    /**
3542     * Initializes the nested formatter fields.<p>
3543     *
3544     * @param element the formatters element
3545     * @param contentDefinition the content definition
3546     *
3547     * @throws CmsXmlException in case something goes wron
3548     */
3549    protected void initNestedFormatters(Element element, CmsXmlContentDefinition contentDefinition)
3550    throws CmsXmlException {
3551
3552        Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(element, APPINFO_NESTED_FORMATTER);
3553        while (i.hasNext()) {
3554            // iterate all "default" elements in the "defaults" node
3555            Element handlerElement = i.next();
3556            String formatterElement = handlerElement.attributeValue(APPINFO_ATTR_ELEMENT);
3557            addNestedFormatter(formatterElement, contentDefinition);
3558        }
3559    }
3560
3561    /**
3562     * Initializes the parameters from the schema.<p>
3563     *
3564     * @param root the parameter root element
3565     */
3566    protected void initParameters(Element root) {
3567
3568        m_parameters.clear();
3569        for (Element paramElement : root.elements(APPINFO_PARAM)) {
3570            String name = paramElement.attributeValue(APPINFO_ATTR_NAME);
3571            String value = paramElement.getText();
3572            m_parameters.put(name, value);
3573        }
3574
3575    }
3576
3577    /**
3578     * Initializes the preview location for this content handler.<p>
3579     *
3580     * @param root the "preview" element from the appinfo node of the XML content definition
3581     * @param contentDefinition the content definition the validation rules belong to
3582     * @throws CmsXmlException if something goes wrong
3583     */
3584    protected void initPreview(Element root, CmsXmlContentDefinition contentDefinition) throws CmsXmlException {
3585
3586        String preview = root.attributeValue(APPINFO_ATTR_URI);
3587        if (preview == null) {
3588            throw new CmsXmlException(
3589                Messages.get().container(
3590                    Messages.ERR_XMLCONTENT_MISSING_PREVIEW_URI_2,
3591                    root.getName(),
3592                    contentDefinition.getSchemaLocation()));
3593        }
3594        m_previewLocation = preview;
3595    }
3596
3597    /**
3598     * Initializes the relation configuration for this content handler.<p>
3599     *
3600     * OpenCms performs link checks for all OPTIONAL links defined in XML content values of type
3601     * OpenCmsVfsFile. However, for most projects in the real world a more fine-grained control
3602     * over the link check process is required. For these cases, individual relation behavior can
3603     * be defined for the appinfo node.<p>
3604     *
3605     * Additional here can be defined an optional type for the relations, for instance.<p>
3606     *
3607     * @param root the "relations" element from the appinfo node of the XML content definition
3608     * @param contentDefinition the content definition the check rules belong to
3609     *
3610     * @throws CmsXmlException if something goes wrong
3611     */
3612    protected void initRelations(Element root, CmsXmlContentDefinition contentDefinition) throws CmsXmlException {
3613
3614        Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(root, APPINFO_RELATION);
3615        while (i.hasNext()) {
3616            // iterate all "checkrule" elements in the "checkrule" node
3617            Element element = i.next();
3618            String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
3619            String invalidate = element.attributeValue(APPINFO_ATTR_INVALIDATE);
3620            if (invalidate != null) {
3621                invalidate = invalidate.toUpperCase();
3622            }
3623            String type = element.attributeValue(APPINFO_ATTR_TYPE);
3624            if (type != null) {
3625                type = type.toLowerCase();
3626            }
3627            if (elementName != null) {
3628                // add a check rule for the element
3629                addCheckRule(contentDefinition, elementName, invalidate, type);
3630            }
3631        }
3632    }
3633
3634    /**
3635     * Initializes the resource bundle to use for localized messages in this content handler.<p>
3636     *
3637     * @param root the "resourcebundle" element from the appinfo node of the XML content definition
3638     * @param contentDefinition the content definition the validation rules belong to
3639     * @param single if <code>true</code> we process the classic sinle line entry, otherwise it's the multiple line setting
3640     *
3641     * @throws CmsXmlException if something goes wrong
3642     */
3643    protected void initResourceBundle(Element root, CmsXmlContentDefinition contentDefinition, boolean single)
3644    throws CmsXmlException {
3645
3646        if (m_messageBundleNames == null) {
3647            // it's uncommon to have more then one bundle so just initialize an array length of 2
3648            m_messageBundleNames = new ArrayList<String>(2);
3649        }
3650
3651        if (single) {
3652            // single "resourcebundle" node
3653
3654            String messageBundleName = root.attributeValue(APPINFO_ATTR_NAME);
3655            if (messageBundleName == null) {
3656                throw new CmsXmlException(
3657                    Messages.get().container(
3658                        Messages.ERR_XMLCONTENT_MISSING_RESOURCE_BUNDLE_NAME_2,
3659                        root.getName(),
3660                        contentDefinition.getSchemaLocation()));
3661            }
3662            if (!m_messageBundleNames.contains(messageBundleName)) {
3663                // avoid duplicates
3664                m_messageBundleNames.add(messageBundleName);
3665            }
3666            // clear the cached resource bundles for this bundle
3667            CmsResourceBundleLoader.flushBundleCache(messageBundleName, false);
3668
3669        } else {
3670            // multiple "resourcebundles" node
3671
3672            // get an iterator for all "propertybundle" subnodes
3673            Iterator<Element> propertybundles = CmsXmlGenericWrapper.elementIterator(root, APPINFO_PROPERTYBUNDLE);
3674            while (propertybundles.hasNext()) {
3675                // iterate all "propertybundle" elements in the "resourcebundle" node
3676                Element propBundle = propertybundles.next();
3677                String propertyBundleName = propBundle.attributeValue(APPINFO_ATTR_NAME);
3678                if (!m_messageBundleNames.contains(propertyBundleName)) {
3679                    // avoid duplicates
3680                    m_messageBundleNames.add(propertyBundleName);
3681                }
3682                // clear the cached resource bundles for this bundle
3683                CmsResourceBundleLoader.flushBundleCache(propertyBundleName, false);
3684            }
3685
3686            // get an iterator for all "xmlbundle" subnodes
3687            Iterator<Element> xmlbundles = CmsXmlGenericWrapper.elementIterator(root, APPINFO_XMLBUNDLE);
3688            while (xmlbundles.hasNext()) {
3689                Element xmlbundle = xmlbundles.next();
3690                String xmlBundleName = xmlbundle.attributeValue(APPINFO_ATTR_NAME);
3691                // cache the bundle from the XML
3692                if (!m_messageBundleNames.contains(xmlBundleName)) {
3693                    // avoid duplicates
3694                    m_messageBundleNames.add(xmlBundleName);
3695                }
3696                // clear the cached resource bundles for this bundle
3697                CmsResourceBundleLoader.flushBundleCache(xmlBundleName, true);
3698                Iterator<Element> bundles = CmsXmlGenericWrapper.elementIterator(xmlbundle, APPINFO_BUNDLE);
3699                while (bundles.hasNext()) {
3700                    // iterate all "bundle" elements in the "xmlbundle" node
3701                    Element bundle = bundles.next();
3702                    String localeStr = bundle.attributeValue(APPINFO_ATTR_LOCALE);
3703                    Locale locale;
3704                    if (CmsStringUtil.isEmptyOrWhitespaceOnly(localeStr)) {
3705                        // no locale set, so use no locale
3706                        locale = null;
3707                    } else {
3708                        // use provided locale
3709                        locale = CmsLocaleManager.getLocale(localeStr);
3710                    }
3711                    boolean isDefaultLocaleAndNotNull = (locale != null)
3712                        && locale.equals(CmsLocaleManager.getDefaultLocale());
3713
3714                    CmsListResourceBundle xmlBundle = null;
3715
3716                    Iterator<Element> resources = CmsXmlGenericWrapper.elementIterator(bundle, APPINFO_RESOURCE);
3717                    while (resources.hasNext()) {
3718                        // now collect all resource bundle keys
3719                        Element resource = resources.next();
3720                        String key = resource.attributeValue(APPINFO_ATTR_KEY);
3721                        String value = resource.attributeValue(APPINFO_ATTR_VALUE);
3722                        if (CmsStringUtil.isEmptyOrWhitespaceOnly(value)) {
3723                            // read from inside XML tag if value attribute is not set
3724                            value = resource.getTextTrim();
3725                        }
3726                        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(key)
3727                            && CmsStringUtil.isNotEmptyOrWhitespaceOnly(value)) {
3728                            if (xmlBundle == null) {
3729                                // use lazy initilaizing of the bundle
3730                                xmlBundle = new CmsListResourceBundle();
3731                            }
3732                            xmlBundle.addMessage(key.trim(), value.trim());
3733                        }
3734                    }
3735                    if (xmlBundle != null) {
3736                        CmsResourceBundleLoader.addBundleToCache(xmlBundleName, locale, xmlBundle);
3737                        if (isDefaultLocaleAndNotNull) {
3738                            CmsResourceBundleLoader.addBundleToCache(xmlBundleName, null, xmlBundle);
3739                        }
3740                    }
3741                }
3742            }
3743        }
3744    }
3745
3746    /**
3747     * Initializes the search exclusions values for this content handler.<p>
3748     *
3749     * For the full text search, the value of all elements in one locale of the XML content are combined
3750     * to one big text, which is referred to as the "content" in the context of the full text search.
3751     * With this option, it is possible to hide certain elements from this "content" that does not make sense
3752     * to include in the full text search.<p>
3753     *
3754     * @param root the "searchsettings" element from the appinfo node of the XML content definition
3755     * @param contentDefinition the content definition the default values belong to
3756     *
3757     * @throws CmsXmlException if something goes wrong
3758     */
3759    protected void initSearchSettings(Element root, CmsXmlContentDefinition contentDefinition) throws CmsXmlException {
3760
3761        String containerPageOnly = root.attributeValue(APPINFO_ATTR_CONTAINER_PAGE_ONLY);
3762        if (!CmsStringUtil.isEmpty(containerPageOnly)) {
3763            m_containerPageOnly = Boolean.valueOf(containerPageOnly).booleanValue();
3764        }
3765        Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(root, APPINFO_SEARCHSETTING);
3766        while (i.hasNext()) {
3767            Element element = i.next();
3768            String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
3769            String searchContent = element.attributeValue(APPINFO_ATTR_SEARCHCONTENT);
3770            SearchContentType searchContentType = SearchContentType.fromString(searchContent);
3771            String adjustmentClass = element.attributeValue(APPINFO_ATTR_CLASS);
3772            if (elementName != null) {
3773                CmsSearchContentConfig config = CmsSearchContentConfig.get(searchContentType, adjustmentClass);
3774                addSearchSetting(contentDefinition, elementName, config);
3775            }
3776            Iterator<Element> it = CmsXmlGenericWrapper.elementIterator(element, APPINFO_SOLR_FIELD);
3777            Element solrElement;
3778            while (it.hasNext()) {
3779                solrElement = it.next();
3780
3781                String localeNames = solrElement.attributeValue(APPINFO_ATTR_LOCALE);
3782                boolean localized = true;
3783                if ((localeNames != null)
3784                    && (localeNames.equals("none") || localeNames.equals("null") || localeNames.trim().equals(""))) {
3785                    localized = false;
3786                }
3787                List<Locale> locales = null;
3788                if (localized) {
3789                    locales = OpenCms.getLocaleManager().getAvailableLocales(localeNames);
3790                    if (localized && ((locales == null) || locales.isEmpty())) {
3791                        locales = OpenCms.getLocaleManager().getAvailableLocales();
3792                    } else if (locales.isEmpty()) {
3793                        locales.add(CmsLocaleManager.getDefaultLocale());
3794                    }
3795                } else {
3796                    locales = Collections.singletonList(null);
3797                }
3798                for (Locale locale : locales) {
3799                    String targetField = solrElement.attributeValue(APPINFO_ATTR_TARGET_FIELD);
3800                    if (localized) {
3801                        targetField = targetField + "_" + locale.toString();
3802                    }
3803                    String sourceField = solrElement.attributeValue(APPINFO_ATTR_SOURCE_FIELD);
3804                    if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(sourceField)) {
3805                        int lastUnderScore = sourceField.lastIndexOf("_");
3806                        if (lastUnderScore > 0) {
3807                            sourceField = sourceField.substring(lastUnderScore);
3808                        }
3809                        targetField += sourceField;
3810                    }
3811
3812                    String copyFieldNames = solrElement.attributeValue(APPINFO_ATTR_COPY_FIELDS, "");
3813                    List<String> copyFields = CmsStringUtil.splitAsList(copyFieldNames, ',');
3814                    String defaultValue = solrElement.attributeValue(APPINFO_ATTR_DEFAULT);
3815                    CmsSolrField field = new CmsSolrField(targetField, copyFields, locale, defaultValue);
3816
3817                    // create the field mappings for this element
3818                    Iterator<Element> ite = CmsXmlGenericWrapper.elementIterator(solrElement, APPINFO_ATTR_MAPPING);
3819                    while (ite.hasNext()) {
3820                        Element mappingElement = ite.next();
3821                        field.addMapping(
3822                            createSearchFieldMapping(contentDefinition, mappingElement, locale, elementName));
3823                    }
3824
3825                    // if no mapping was defined yet, create a mapping for the element itself
3826                    if ((field.getMappings() == null) || field.getMappings().isEmpty()) {
3827                        CmsSearchFieldMapping map = new CmsSearchFieldMapping(
3828                            CmsSearchFieldMappingType.ITEM,
3829                            elementName);
3830                        if (localized) {
3831                            map.setLocale(locale);
3832                        }
3833                        field.addMapping(map);
3834                    }
3835                    Set<I_CmsXmlContentHandler.MappingType> mappingTypes = parseSearchMappingTypes(solrElement);
3836                    for (I_CmsXmlContentHandler.MappingType type : mappingTypes) {
3837                        addSearchField(contentDefinition, field, type);
3838                    }
3839                }
3840            }
3841        }
3842    }
3843
3844    /**
3845     * Initializes the element settings for this content handler.<p>
3846     *
3847     * @param root the "settings" element from the appinfo node of the XML content definition
3848     * @param contentDefinition the content definition the element settings belong to
3849     */
3850    protected void initSettings(Element root, CmsXmlContentDefinition contentDefinition) {
3851
3852        Iterator<Element> itProperties = CmsXmlGenericWrapper.elementIterator(root, APPINFO_SETTING);
3853        while (itProperties.hasNext()) {
3854            Element element = itProperties.next();
3855            CmsXmlContentProperty setting = new CmsXmlContentProperty(
3856                element.attributeValue(APPINFO_ATTR_NAME),
3857                element.attributeValue(APPINFO_ATTR_TYPE),
3858                element.attributeValue(APPINFO_ATTR_WIDGET),
3859                element.attributeValue(APPINFO_ATTR_WIDGET_CONFIG),
3860                element.attributeValue(APPINFO_ATTR_RULE_REGEX),
3861                element.attributeValue(APPINFO_ATTR_RULE_TYPE),
3862                element.attributeValue(APPINFO_ATTR_DEFAULT),
3863                element.attributeValue(APPINFO_ATTR_NICE_NAME),
3864                element.attributeValue(APPINFO_ATTR_DESCRIPTION),
3865                element.attributeValue(APPINFO_ATTR_ERROR),
3866                element.attributeValue(APPINFO_ATTR_PREFERFOLDER));
3867            String name = setting.getName();
3868            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(name)) {
3869                m_settings.put(name, setting);
3870            }
3871        }
3872    }
3873
3874    /**
3875     * Initializes the locale synchronizations elements.<p>
3876     *
3877     * @param root the synchronizations element of the content schema appinfo.
3878     * @param contentDefinition the content definition
3879     */
3880    protected void initSynchronizations(Element root, CmsXmlContentDefinition contentDefinition) {
3881
3882        List<Element> elements = new ArrayList<Element>(CmsXmlGenericWrapper.elements(root, APPINFO_SYNCHRONIZATION));
3883        for (Element element : elements) {
3884            String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
3885            // 'strong' not supported in the old notation
3886            m_synchronizations.put(elementName, SynchronizationMode.standard);
3887        }
3888    }
3889
3890    /**
3891     * Initializes the tabs for this content handler.<p>
3892     *
3893     * @param root the "tabs" element from the appinfo node of the XML content definition
3894     * @param contentDefinition the content definition the tabs belong to
3895     */
3896    protected void initTabs(Element root, CmsXmlContentDefinition contentDefinition) {
3897
3898        if (Boolean.valueOf(root.attributeValue(APPINFO_ATTR_USEALL, CmsStringUtil.FALSE)).booleanValue()) {
3899            // all first level elements should be treated as tabs
3900            Iterator<I_CmsXmlSchemaType> i = contentDefinition.getTypeSequence().iterator();
3901            while (i.hasNext()) {
3902                // get the type
3903                I_CmsXmlSchemaType type = i.next();
3904                m_tabs.add(new CmsXmlContentTab(type.getName()));
3905            }
3906        } else {
3907            // manual definition of tabs
3908            Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(root, APPINFO_TAB);
3909            while (i.hasNext()) {
3910                // iterate all "tab" elements in the "tabs" node
3911                Element element = i.next();
3912                // this is a tab node
3913                String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
3914                String collapseValue = element.attributeValue(APPINFO_ATTR_COLLAPSE, CmsStringUtil.TRUE);
3915                Node descriptionNode = element.selectSingleNode(APPINFO_ATTR_DESCRIPTION + "/text()");
3916                String description = null;
3917                if (descriptionNode != null) {
3918                    description = descriptionNode.getText();
3919                } else {
3920                    description = element.attributeValue(APPINFO_ATTR_DESCRIPTION);
3921                }
3922
3923                String tabName = element.attributeValue(APPINFO_ATTR_NAME, elementName);
3924                if (elementName != null) {
3925                    // add the element tab
3926                    m_tabs.add(
3927                        new CmsXmlContentTab(
3928                            elementName,
3929                            Boolean.valueOf(collapseValue).booleanValue(),
3930                            tabName,
3931                            description));
3932                }
3933            }
3934            // check if first element has been defined as tab
3935            I_CmsXmlSchemaType type = contentDefinition.getTypeSequence().get(0);
3936            CmsXmlContentTab tab = new CmsXmlContentTab(type.getName());
3937            if (!m_tabs.contains(tab)) {
3938                m_tabs.add(0, tab);
3939            }
3940        }
3941    }
3942
3943    /**
3944     * Initializes the forbidden template contexts.<p>
3945     *
3946     * @param root the root XML element
3947     * @param contentDefinition the content definition
3948     */
3949    protected void initTemplates(Element root, CmsXmlContentDefinition contentDefinition) {
3950
3951        String strEnabledByDefault = root.attributeValue(ATTR_ENABLED_BY_DEFAULT);
3952        m_allowedTemplates.setDefaultMembership(safeParseBoolean(strEnabledByDefault, true));
3953        List<Node> elements = root.selectNodes(APPINFO_TEMPLATE);
3954        for (Node elem : elements) {
3955            boolean enabled = safeParseBoolean(((Element)elem).attributeValue(ATTR_ENABLED), true);
3956            String templateName = elem.getText().trim();
3957            m_allowedTemplates.setContains(templateName, enabled);
3958        }
3959        m_allowedTemplates.freeze();
3960    }
3961
3962    /**
3963     * Initializes the validation rules this content handler.<p>
3964     *
3965     * OpenCms always performs XML schema validation for all XML contents. However,
3966     * for most projects in the real world a more fine-grained control over the validation process is
3967     * required. For these cases, individual validation rules can be defined for the appinfo node.<p>
3968     *
3969     * @param root the "validationrules" element from the appinfo node of the XML content definition
3970     * @param contentDefinition the content definition the validation rules belong to
3971     *
3972     * @throws CmsXmlException if something goes wrong
3973     */
3974    protected void initValidationRules(Element root, CmsXmlContentDefinition contentDefinition) throws CmsXmlException {
3975
3976        List<Element> elements = new ArrayList<Element>(CmsXmlGenericWrapper.elements(root, APPINFO_RULE));
3977        elements.addAll(CmsXmlGenericWrapper.elements(root, APPINFO_VALIDATIONRULE));
3978        Iterator<Element> i = elements.iterator();
3979        while (i.hasNext()) {
3980            // iterate all "rule" or "validationrule" elements in the "validationrules" node
3981            Element element = i.next();
3982            String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
3983            String regex = element.attributeValue(APPINFO_ATTR_REGEX);
3984            String type = element.attributeValue(APPINFO_ATTR_TYPE);
3985            if (type != null) {
3986                type = type.toLowerCase();
3987            }
3988            String message = element.attributeValue(APPINFO_ATTR_MESSAGE);
3989            if ((elementName != null) && (regex != null)) {
3990                // add a validation rule for the element
3991                addValidationRule(
3992                    contentDefinition,
3993                    elementName,
3994                    regex,
3995                    message,
3996                    APPINFO_ATTR_TYPE_WARNING.equals(type));
3997            }
3998        }
3999    }
4000
4001    /**
4002     * Initializes the content visibility settings.<p>
4003     *
4004     * @param root the visibilities appinfo element
4005     * @param contentDefinition the content definition
4006     */
4007    protected void initVisibilities(Element root, CmsXmlContentDefinition contentDefinition) {
4008
4009        m_visibilityConfigurations = new HashMap<String, VisibilityConfiguration>();
4010        String mainHandlerClassName = root.attributeValue(APPINFO_ATTR_CLASS);
4011        // using self as the default visibility handler implementation
4012        I_CmsXmlContentVisibilityHandler mainHandler = this;
4013        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(mainHandlerClassName)) {
4014            try {
4015                // in case there is a main handler configured, try to instanciate it
4016                Class<?> handlerClass = Class.forName(mainHandlerClassName);
4017                mainHandler = (I_CmsXmlContentVisibilityHandler)handlerClass.newInstance();
4018            } catch (Exception e) {
4019                LOG.error(e.getLocalizedMessage(), e);
4020            }
4021        }
4022        List<Element> elements = new ArrayList<Element>(CmsXmlGenericWrapper.elements(root, APPINFO_VISIBILITY));
4023        for (Element element : elements) {
4024            try {
4025                String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
4026                String handlerClassName = element.attributeValue(APPINFO_ATTR_CLASS);
4027                String params = element.attributeValue(APPINFO_ATTR_PARAMS);
4028                I_CmsXmlContentVisibilityHandler handler = null;
4029                if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(handlerClassName)) {
4030
4031                    Class<?> handlerClass = Class.forName(handlerClassName);
4032                    handler = (I_CmsXmlContentVisibilityHandler)handlerClass.newInstance();
4033                } else {
4034                    handler = mainHandler;
4035                }
4036                m_visibilityConfigurations.put(elementName, new VisibilityConfiguration(handler, params));
4037
4038            } catch (Exception e) {
4039                LOG.error(e.getLocalizedMessage(), e);
4040            }
4041        }
4042    }
4043
4044    /**
4045     * Returns the is-invalidate-parent flag for the given xpath.<p>
4046     *
4047     * @param xpath the path to get the check rule for
4048     *
4049     * @return the configured is-invalidate-parent flag for the given xpath
4050     */
4051    protected boolean isInvalidateParent(String xpath) {
4052
4053        if (!CmsXmlUtils.isDeepXpath(xpath)) {
4054            return false;
4055        }
4056        Boolean isInvalidateParent = null;
4057        // look up the default from the configured mappings
4058        isInvalidateParent = m_relationChecks.get(xpath);
4059        if (isInvalidateParent == null) {
4060            // no value found, try default xpath
4061            String path = CmsXmlUtils.removeXpath(xpath);
4062            // look up the default value again without indexes
4063            isInvalidateParent = m_relationChecks.get(path);
4064        }
4065        if (isInvalidateParent == null) {
4066            return false;
4067        }
4068        return isInvalidateParent.booleanValue();
4069    }
4070
4071    /**
4072     * Returns the localized resource string for a given message key according to the configured resource bundle
4073     * of this content handler.<p>
4074     *
4075     * If the key was not found in the configured bundle, or no bundle is configured for this
4076     * content handler, the return value is
4077     * <code>"??? " + keyName + " ???"</code>.<p>
4078     *
4079     * @param keyName the key for the desired string
4080     * @param locale the locale to get the key from
4081     *
4082     * @return the resource string for the given key
4083     *
4084     * @see CmsMessages#formatUnknownKey(String)
4085     * @see CmsMessages#isUnknownKey(String)
4086     */
4087    protected String key(String keyName, Locale locale) {
4088
4089        CmsMessages messages = getMessages(locale);
4090        if (messages != null) {
4091            return messages.key(keyName);
4092        }
4093        return CmsMessages.formatUnknownKey(keyName);
4094    }
4095
4096    /**
4097     * @param solrElement the XML node of the &lt;solrfield&gt; node
4098     * @return parsed values of the attribute "addto"
4099     */
4100    protected Set<MappingType> parseSearchMappingTypes(Element solrElement) {
4101
4102        Set<MappingType> result = new HashSet<MappingType>();
4103        String mappingTypes = solrElement.attributeValue(APPINFO_ATTR_ADD_TO);
4104        if (mappingTypes != null) {
4105            String[] types = mappingTypes.split(",");
4106            for (int i = 0; i < types.length; i++) {
4107                String type = types[i].trim();
4108                if (APPINFO_VALUE_ADD_TO_PAGE.equals(type)) {
4109                    result.add(MappingType.PAGE);
4110                } else if (APPINFO_VALUE_ADD_TO_CONTENT.equals(type)) {
4111                    result.add(MappingType.ELEMENT);
4112                }
4113            }
4114        } else {
4115            // for backwards compatibility
4116            result.add(MappingType.ELEMENT);
4117        }
4118
4119        return result;
4120    }
4121
4122    /**
4123     * Removes property values on resources for non-existing, optional elements.<p>
4124     *
4125     * @param cms the current users OpenCms context
4126     * @param file the file which is currently being prepared for writing
4127     * @param content the XML content to remove the property values for
4128     * @throws CmsException in case of read/write errors accessing the OpenCms VFS
4129     */
4130    protected void removeEmptyMappings(CmsObject cms, CmsFile file, CmsXmlContent content) throws CmsException {
4131
4132        List<CmsResource> siblings = null;
4133        CmsObject rootCms = null;
4134
4135        Iterator<Map.Entry<String, List<String>>> allMappings = m_elementMappings.entrySet().iterator();
4136        while (allMappings.hasNext()) {
4137            Map.Entry<String, List<String>> e = allMappings.next();
4138            String path = e.getKey();
4139            List<String> mappings = e.getValue();
4140            if (mappings == null) {
4141                // nothing to do if we have no mappings at all
4142                continue;
4143            }
4144            if ((siblings == null) || (rootCms == null)) {
4145                // create OpenCms user context initialized with "/" as site root to read all siblings
4146                rootCms = OpenCms.initCmsObject(cms);
4147                rootCms.getRequestContext().setSiteRoot("/");
4148                siblings = rootCms.readSiblings(content.getFile().getRootPath(), CmsResourceFilter.IGNORE_EXPIRATION);
4149            }
4150            for (int v = mappings.size() - 1; v >= 0; v--) {
4151                String mapping = mappings.get(v);
4152
4153                if (mapping.startsWith(MAPTO_PROPERTY_LIST) || mapping.startsWith(MAPTO_PROPERTY)) {
4154                    for (int i = 0; i < siblings.size(); i++) {
4155
4156                        // get siblings filename and locale
4157                        String filename = siblings.get(i).getRootPath();
4158                        Locale locale = OpenCms.getLocaleManager().getDefaultLocale(rootCms, filename);
4159
4160                        if (!content.hasLocale(locale)) {
4161                            // only remove property if the locale fits
4162                            continue;
4163                        }
4164                        if (content.hasValue(path, locale)) {
4165                            // value is available, property must be kept
4166                            continue;
4167                        }
4168
4169                        if (mapping.startsWith(MAPTO_PROPERTY_LIST) || mapping.startsWith(MAPTO_PROPERTY)) {
4170
4171                            String property;
4172                            boolean shared = false;
4173                            if (mapping.startsWith(MAPTO_PROPERTY_LIST_INDIVIDUAL)) {
4174                                property = mapping.substring(MAPTO_PROPERTY_LIST_INDIVIDUAL.length());
4175                            } else if (mapping.startsWith(MAPTO_PROPERTY_LIST_SHARED)) {
4176                                property = mapping.substring(MAPTO_PROPERTY_LIST_SHARED.length());
4177                                shared = true;
4178                            } else if (mapping.startsWith(MAPTO_PROPERTY_LIST)) {
4179                                property = mapping.substring(MAPTO_PROPERTY_LIST.length());
4180                            } else if (mapping.startsWith(MAPTO_PROPERTY_SHARED)) {
4181                                property = mapping.substring(MAPTO_PROPERTY_SHARED.length());
4182                                shared = true;
4183                            } else if (mapping.startsWith(MAPTO_PROPERTY_INDIVIDUAL)) {
4184                                property = mapping.substring(MAPTO_PROPERTY_INDIVIDUAL.length());
4185                            } else {
4186                                property = mapping.substring(MAPTO_PROPERTY.length());
4187                            }
4188                            rootCms.writePropertyObject(
4189                                filename,
4190                                new CmsProperty(
4191                                    property,
4192                                    CmsProperty.DELETE_VALUE,
4193                                    shared ? CmsProperty.DELETE_VALUE : null));
4194                        }
4195                    }
4196                } else if (mapping.startsWith(MAPTO_PERMISSION)) {
4197                    for (int i = 0; i < siblings.size(); i++) {
4198
4199                        // get siblings filename and locale
4200                        String filename = siblings.get(i).getRootPath();
4201                        Locale locale = OpenCms.getLocaleManager().getDefaultLocale(rootCms, filename);
4202
4203                        if (!content.hasLocale(locale)) {
4204                            // only remove property if the locale fits
4205                            continue;
4206                        }
4207                        if (content.hasValue(path, locale)) {
4208                            // value is available, property must be kept
4209                            continue;
4210                        }
4211                        // remove all existing permissions from the file
4212                        List<CmsAccessControlEntry> aces = rootCms.getAccessControlEntries(filename, false);
4213                        for (Iterator<CmsAccessControlEntry> j = aces.iterator(); j.hasNext();) {
4214                            CmsAccessControlEntry ace = j.next();
4215                            if (ace.getPrincipal().equals(CmsAccessControlEntry.PRINCIPAL_ALL_OTHERS_ID)) {
4216                                // remove the entry "All others", which has to be treated in a special way
4217                                rootCms.rmacc(
4218                                    filename,
4219                                    CmsAccessControlEntry.PRINCIPAL_ALL_OTHERS_NAME,
4220                                    CmsAccessControlEntry.PRINCIPAL_ALL_OTHERS_ID.toString());
4221                            } else {
4222                                // this is a group or user principal
4223                                I_CmsPrincipal principal = CmsPrincipal.readPrincipal(rootCms, ace.getPrincipal());
4224                                if (principal.isGroup()) {
4225                                    rootCms.rmacc(filename, I_CmsPrincipal.PRINCIPAL_GROUP, principal.getName());
4226                                } else if (principal.isUser()) {
4227                                    rootCms.rmacc(filename, I_CmsPrincipal.PRINCIPAL_USER, principal.getName());
4228                                }
4229                            }
4230                        }
4231                    }
4232                }
4233            }
4234        }
4235    }
4236
4237    /**
4238     * Resolves those mappings for which no content value exists and useDefault is set to true.<p>
4239     *
4240     * @param cms the CMS context to use
4241     * @param file the content file
4242     * @param content the content object
4243     *
4244     * @throws CmsException if something goes wrong
4245     */
4246    protected void resolveDefaultMappings(CmsObject cms, CmsFile file, CmsXmlContent content) throws CmsException {
4247
4248        for (Map.Entry<String, List<String>> e : m_elementMappings.entrySet()) {
4249            String path = e.getKey();
4250            List<String> mappings = e.getValue();
4251            if (mappings == null) {
4252                // nothing to do if we have no mappings at all
4253                continue;
4254            }
4255            for (int v = mappings.size() - 1; v >= 0; v--) {
4256                String mapping = mappings.get(v);
4257                if (!isMappingUsingDefault(path, mapping)) {
4258                    continue;
4259                }
4260                for (Locale locale : content.getLocales()) {
4261                    if (content.hasValue(path, locale)) {
4262                        continue;
4263                    } else {
4264                        String defaultValue = getDefault(cms, file, null, path, locale);
4265                        if (defaultValue != null) {
4266                            resolveMapping(cms, content, path, true, 0, locale, defaultValue);
4267                        }
4268                    }
4269                }
4270
4271            }
4272        }
4273    }
4274
4275    /**
4276     * Validates if the given <code>appinfo</code> element node from the XML content definition schema
4277     * is valid according the the capabilities of this content handler.<p>
4278     *
4279     * @param appinfoElement the <code>appinfo</code> element node to validate
4280     *
4281     * @throws CmsXmlException in case the element validation fails
4282     */
4283    protected void validateAppinfoElement(Element appinfoElement) throws CmsXmlException {
4284
4285        // create a document to validate
4286        Document doc = DocumentHelper.createDocument();
4287        Element root = doc.addElement(APPINFO_APPINFO);
4288        // attach the default appinfo schema
4289        root.add(I_CmsXmlSchemaType.XSI_NAMESPACE);
4290        root.addAttribute(I_CmsXmlSchemaType.XSI_NAMESPACE_ATTRIBUTE_NO_SCHEMA_LOCATION, APPINFO_SCHEMA_SYSTEM_ID);
4291        // append the content from the appinfo node in the content definition
4292        root.appendContent(appinfoElement);
4293        // now validate the document with the default appinfo schema
4294        CmsXmlUtils.validateXmlStructure(doc, CmsEncoder.ENCODING_UTF_8, new CmsXmlEntityResolver(null));
4295    }
4296
4297    /**
4298     * The errorHandler parameter is optional, if <code>null</code> is given a new error handler
4299     * instance must be created.<p>
4300     *
4301     * @param cms the current OpenCms user context
4302     * @param value the value to resolve the validation rules for
4303     * @param errorHandler (optional) an error handler instance that contains previous error or warnings
4304     *
4305     * @return an error handler that contains all errors and warnings currently found
4306     */
4307    protected CmsXmlContentErrorHandler validateCategories(
4308        CmsObject cms,
4309        I_CmsXmlContentValue value,
4310        CmsXmlContentErrorHandler errorHandler) {
4311
4312        if (!value.isSimpleType()) {
4313            // do not validate complex types
4314            return errorHandler;
4315        }
4316        I_CmsWidget widget = null;
4317
4318        widget = CmsWidgetUtil.collectWidgetInfo(cms, value).getWidget();
4319        if (!(widget instanceof CmsCategoryWidget)) {
4320            // do not validate widget that are not category widgets
4321            return errorHandler;
4322        }
4323        String stringValue = value.getStringValue(cms);
4324        if (stringValue.isEmpty()) {
4325            return errorHandler;
4326        }
4327        try {
4328            String[] values = stringValue.split(",");
4329            for (int i = 0; i < values.length; i++) {
4330                String val = values[i];
4331                String catPath = CmsCategoryService.getInstance().getCategory(cms, val).getPath();
4332                String refPath = getReferencePath(cms, value);
4333                CmsCategoryService.getInstance().readCategory(cms, catPath, refPath);
4334                if (((CmsCategoryWidget)widget).isOnlyLeafs()) {
4335                    if (!CmsCategoryService.getInstance().readCategories(cms, catPath, false, refPath).isEmpty()) {
4336                        errorHandler.addError(
4337                            value,
4338                            Messages.get().getBundle(value.getLocale()).key(
4339                                Messages.GUI_CATEGORY_CHECK_NOLEAF_ERROR_0));
4340                    }
4341                }
4342            }
4343        } catch (CmsDataAccessException e) {
4344            // expected error in case of empty/invalid value
4345            // see CmsCategory#getCategoryPath(String, String)
4346            if (LOG.isDebugEnabled()) {
4347                LOG.debug(e.getLocalizedMessage(), e);
4348            }
4349            errorHandler.addError(
4350                value,
4351                Messages.get().getBundle(value.getLocale()).key(Messages.GUI_CATEGORY_CHECK_EMPTY_ERROR_0));
4352        } catch (CmsException e) {
4353            // unexpected error
4354            if (LOG.isErrorEnabled()) {
4355                LOG.error(e.getLocalizedMessage(), e);
4356            }
4357            errorHandler.addError(value, e.getLocalizedMessage());
4358        }
4359        return errorHandler;
4360    }
4361
4362    /**
4363     * Validates the given rules against the given value.<p>
4364     *
4365     * @param cms the current users OpenCms context
4366     * @param value the value to validate
4367     * @param errorHandler the error handler to use in case errors or warnings are detected
4368     *
4369     * @return if a broken link has been found
4370     */
4371    protected boolean validateLink(CmsObject cms, I_CmsXmlContentValue value, CmsXmlContentErrorHandler errorHandler) {
4372
4373        // if there is a value of type file reference
4374        if ((value == null) || (!(value instanceof CmsXmlVfsFileValue) && !(value instanceof CmsXmlVarLinkValue))) {
4375            return false;
4376        }
4377        // if the value has a link (this will automatically fix, for instance, the path of moved resources)
4378        CmsLink link = null;
4379        if (value instanceof CmsXmlVfsFileValue) {
4380            link = ((CmsXmlVfsFileValue)value).getLink(cms);
4381        } else if (value instanceof CmsXmlVarLinkValue) {
4382            link = ((CmsXmlVarLinkValue)value).getLink(cms);
4383        }
4384        if ((link == null) || !link.isInternal()) {
4385            return false;
4386        }
4387        try {
4388            String sitePath = cms.getRequestContext().removeSiteRoot(link.getTarget());
4389
4390            // check for links to static resources
4391            if (CmsStaticResourceHandler.isStaticResourceUri(sitePath)) {
4392                return false;
4393            }
4394            // validate the link for error
4395            CmsResource res = null;
4396            CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(link.getTarget());
4397            // the link target may be a root path for a resource in another site
4398            if (site != null) {
4399                CmsObject rootCms = OpenCms.initCmsObject(cms);
4400                rootCms.getRequestContext().setSiteRoot("");
4401                res = rootCms.readResource(link.getTarget(), CmsResourceFilter.IGNORE_EXPIRATION);
4402            } else {
4403                res = cms.readResource(sitePath, CmsResourceFilter.IGNORE_EXPIRATION);
4404            }
4405            // check the time range
4406            if (res != null) {
4407                long time = System.currentTimeMillis();
4408                if (!res.isReleased(time)) {
4409                    if (errorHandler != null) {
4410                        // generate warning message
4411                        errorHandler.addWarning(
4412                            value,
4413                            Messages.get().getBundle(OpenCms.getWorkplaceManager().getWorkplaceLocale(cms)).key(
4414                                Messages.GUI_XMLCONTENT_CHECK_WARNING_NOT_RELEASED_0));
4415                    }
4416                    return true;
4417                } else if (res.isExpired(time)) {
4418                    if (errorHandler != null) {
4419                        // generate warning message
4420                        errorHandler.addWarning(
4421                            value,
4422                            Messages.get().getBundle(value.getLocale()).key(
4423                                Messages.GUI_XMLCONTENT_CHECK_WARNING_EXPIRED_0));
4424                    }
4425                    return true;
4426                }
4427            }
4428        } catch (CmsException e) {
4429            if (errorHandler != null) {
4430                String message = getErrorMessage(cms, value.getName());
4431                if (message == null) {
4432                    message = Messages.get().getBundle(OpenCms.getWorkplaceManager().getWorkplaceLocale(cms)).key(
4433                        Messages.GUI_XMLCONTENT_CHECK_ERROR_0);
4434                }
4435                // generate error message
4436                errorHandler.addError(value, message);
4437            }
4438            return true;
4439        }
4440        return false;
4441    }
4442
4443    /**
4444     * Validates the given rules against the given value.<p>
4445     *
4446     * @param cms the current users OpenCms context
4447     * @param value the value to validate
4448     * @param errorHandler the error handler to use in case errors or warnings are detected
4449     * @param rules the rules to validate the value against
4450     * @param isWarning if true, this validation should be stored as a warning, otherwise as an error
4451     *
4452     * @return the updated error handler
4453     */
4454    protected CmsXmlContentErrorHandler validateValue(
4455        CmsObject cms,
4456        I_CmsXmlContentValue value,
4457        CmsXmlContentErrorHandler errorHandler,
4458        Map<String, String> rules,
4459        boolean isWarning) {
4460
4461        if (validateLink(cms, value, errorHandler)) {
4462            return errorHandler;
4463        }
4464
4465        if (CmsWidgetUtil.collectWidgetInfo(cms, value).getWidget() instanceof CmsDisplayWidget) {
4466            // display widgets should not be validated
4467            return errorHandler;
4468        }
4469
4470        String valueStr;
4471        try {
4472            valueStr = value.getStringValue(cms);
4473        } catch (Exception e) {
4474            // if the value can not be accessed it's useless to continue
4475            errorHandler.addError(value, e.getMessage());
4476            return errorHandler;
4477        }
4478
4479        String regex = rules.get(value.getName());
4480        if (regex == null) {
4481            // no customized rule, check default XML schema validation rules
4482            return validateValue(cms, value, valueStr, errorHandler, isWarning);
4483        }
4484
4485        boolean matchSign = true;
4486        if (regex.charAt(0) == '!') {
4487            // negate the pattern
4488            matchSign = false;
4489            regex = regex.substring(1);
4490        }
4491
4492        String stringToBeMatched = valueStr;
4493        if (stringToBeMatched == null) {
4494            // set match value to empty String to avoid exceptions in pattern matcher
4495            stringToBeMatched = "";
4496        }
4497
4498        // use the custom validation pattern
4499        final boolean matches;
4500        try {
4501            matches = Pattern.matches(regex, stringToBeMatched);
4502        } catch (PatternSyntaxException | StackOverflowError e) {
4503            final String localizedMessage = (e.getLocalizedMessage() != null ? e.getLocalizedMessage() : "");
4504            final String ticket = String.valueOf(System.currentTimeMillis());
4505
4506            Throwable trace = e;
4507            if (e instanceof StackOverflowError) {
4508                final String stackOverflowInfoMessage = "StackOverflowError thrown on pattern matching during xml"
4509                    + " content validation. (Cause will be also logged in DEBUG level.)\n"
4510                    + "Note 1.- Possible cause: The Java regex engine uses recursive method calls to implement"
4511                    + " backtracking. When a repetition inside a regular expression contains multiple paths"
4512                    + " (i.e. the body of the repetition contains an alternation (|), an optional element or another"
4513                    + " repetition), trying to match the regular expression can cause a stack overflow on large inputs."
4514                    + " This does not happen when using a possessive quantifier (such as *+ instead of *) or when using"
4515                    + " a character class inside a repetition (e.g. [ab]* instead of (a|b)*).\n"
4516                    + "Note 2.- On StackOverflowError, the size of the stacktraces could be limited by the JVM "
4517                    + " and we could be missing information to identify the origin of the problem. To help in this"
4518                    + " case, we create a new exception close to this origin. Alternatively, you can increase"
4519                    + " the depth of the stack trace (for instance, '-XX:MaxJavaStackTraceDepth=1000000') to"
4520                    + " identify it";
4521                trace = LOG.isDebugEnabled()
4522                ? new Exception(stackOverflowInfoMessage, e)
4523                : new Exception(stackOverflowInfoMessage);
4524                errorHandler.addError(
4525                    value,
4526                    Messages.get().getBundle(OpenCms.getWorkplaceManager().getWorkplaceLocale(cms)).key(
4527                        Messages.GUI_EDITOR_XMLCONTENT_CANNOT_VALIDATE_ERROR_3,
4528                        ticket,
4529                        regex,
4530                        stringToBeMatched));
4531            } else {
4532                errorHandler.addError(
4533                    value,
4534                    Messages.get().getBundle(OpenCms.getWorkplaceManager().getWorkplaceLocale(cms)).key(
4535                        Messages.GUI_EDITOR_XMLCONTENT_INVALID_RULE_3,
4536                        ticket,
4537                        regex,
4538                        localizedMessage));
4539            }
4540
4541            LOG.warn(
4542                "Ticket "
4543                    + ticket
4544                    + " - "
4545                    + localizedMessage
4546                    + "\n"
4547                    + " Regex='"
4548                    + (matchSign ? "" : "!")
4549                    + regex
4550                    + "'\n"
4551                    + " Path='"
4552                    + value.getPath()
4553                    + "'\n"
4554                    + " Input='"
4555                    + stringToBeMatched
4556                    + "'",
4557                trace);
4558
4559            return errorHandler;
4560        }
4561        if (matchSign != matches) {
4562            // generate the message
4563            String message = getValidationMessage(cms, value, regex, valueStr, matchSign, isWarning);
4564            if (isWarning) {
4565                errorHandler.addWarning(value, message);
4566            } else {
4567                errorHandler.addError(value, message);
4568                // if an error was found, the default XML schema validation is not applied
4569                return errorHandler;
4570            }
4571        }
4572
4573        // no error found, check default XML schema validation rules
4574        return validateValue(cms, value, valueStr, errorHandler, isWarning);
4575    }
4576
4577    /**
4578     * Checks the default XML schema validation rules.<p>
4579     *
4580     * These rules should only be tested if this is not a test for warnings.<p>
4581     *
4582     * @param cms the current users OpenCms context
4583     * @param value the value to validate
4584     * @param valueStr the string value of the given value
4585     * @param errorHandler the error handler to use in case errors or warnings are detected
4586     * @param isWarning if true, this validation should be stored as a warning, otherwise as an error
4587     *
4588     * @return the updated error handler
4589     */
4590    protected CmsXmlContentErrorHandler validateValue(
4591        CmsObject cms,
4592        I_CmsXmlContentValue value,
4593        String valueStr,
4594        CmsXmlContentErrorHandler errorHandler,
4595        boolean isWarning) {
4596
4597        if (isWarning) {
4598            // default schema validation only applies to errors
4599            return errorHandler;
4600        }
4601
4602        String message = null;
4603        if (value instanceof I_CmsXmlValidateWithMessage) {
4604            CmsMessageContainer messageContainer = ((I_CmsXmlValidateWithMessage)value).validateWithMessage(valueStr);
4605            if (null != messageContainer) {
4606                message = messageContainer.key(OpenCms.getWorkplaceManager().getWorkplaceLocale(cms));
4607            }
4608        } else {
4609            if (!value.validateValue(valueStr)) {
4610                // value is not valid, add an error to the handler
4611                message = getValidationMessage(cms, value, value.getTypeName(), valueStr, true, false);
4612            }
4613        }
4614        if (null != message) {
4615            errorHandler.addError(value, message);
4616        }
4617
4618        return errorHandler;
4619    }
4620
4621    /**
4622     * Writes the categories if a category widget is present.<p>
4623     *
4624     * @param cms the cms context
4625     * @param file the file
4626     * @param content the xml content to set the categories for
4627     *
4628     * @return the perhaps modified file
4629     *
4630     * @throws CmsException if something goes wrong
4631     */
4632    protected CmsFile writeCategories(CmsObject cms, CmsFile file, CmsXmlContent content) throws CmsException {
4633
4634        if (CmsWorkplace.isTemporaryFile(file)) {
4635            // ignore temporary file if the original file exists (not the case for direct edit: "new")
4636            if (CmsResource.isTemporaryFileName(file.getRootPath())) {
4637                String originalFileName = CmsResource.getFolderPath(file.getRootPath())
4638                    + CmsResource.getName(file.getRootPath()).substring(CmsResource.TEMP_FILE_PREFIX.length());
4639                if (cms.existsResource(cms.getRequestContext().removeSiteRoot(originalFileName))) {
4640                    // original file exists, ignore it
4641                    return file;
4642                }
4643            } else {
4644                // file name does not start with temporary prefix, ignore the file
4645                return file;
4646            }
4647        }
4648        // check the presence of a category widget
4649        boolean hasCategoryWidget = hasCategoryWidget();
4650        if (!hasCategoryWidget) {
4651            // nothing to do if no category widget is present
4652            return file;
4653        }
4654        boolean modified = false;
4655        // clone the cms object, and use the root site
4656        CmsObject tmpCms = OpenCms.initCmsObject(cms);
4657        tmpCms.getRequestContext().setSiteRoot("");
4658        // read all siblings
4659        try {
4660            List<CmsResource> listsib = tmpCms.readSiblings(file.getRootPath(), CmsResourceFilter.ALL);
4661            for (int i = 0; i < listsib.size(); i++) {
4662                CmsResource resource = listsib.get(i);
4663                // get the default locale of the sibling
4664                List<Locale> locales = getLocalesForResource(tmpCms, resource.getRootPath());
4665                Locale locale = locales.get(0);
4666                for (Locale l : locales) {
4667                    if (content.hasLocale(l)) {
4668                        locale = l;
4669                        break;
4670                    }
4671                }
4672                // remove all previously set categories
4673                boolean clearedCategories = false;
4674                // iterate over all values checking for the category widget
4675                CmsXmlContentWidgetVisitor widgetCollector = new CmsXmlContentWidgetVisitor(cms, locale);
4676                content.visitAllValuesWith(widgetCollector);
4677                Iterator<Map.Entry<String, I_CmsXmlContentValue>> itWidgets = widgetCollector.getValues().entrySet().iterator();
4678                while (itWidgets.hasNext()) {
4679                    Map.Entry<String, I_CmsXmlContentValue> entry = itWidgets.next();
4680                    String xpath = entry.getKey();
4681                    I_CmsWidget widget = widgetCollector.getWidgets().get(xpath);
4682                    I_CmsXmlContentValue value = entry.getValue();
4683                    if (!(widget instanceof CmsCategoryWidget)
4684                        || value.getTypeName().equals(CmsXmlDynamicCategoryValue.TYPE_NAME)) {
4685                        // ignore other values than categories
4686                        continue;
4687                    }
4688                    if (!clearedCategories) {
4689                        CmsCategoryService.getInstance().clearCategoriesForResource(tmpCms, resource.getRootPath());
4690                        clearedCategories = true;
4691                    }
4692                    String stringValue = value.getStringValue(tmpCms);
4693                    if (CmsStringUtil.isEmptyOrWhitespaceOnly(stringValue)) {
4694                        // skip empty values
4695                        continue;
4696                    }
4697                    try {
4698                        // add the file to the selected category
4699                        String[] catRootPathes = stringValue.split(",");
4700                        for (String catRootPath : catRootPathes) {
4701                            CmsCategory cat = CmsCategoryService.getInstance().getCategory(tmpCms, catRootPath);
4702                            CmsCategoryService.getInstance().addResourceToCategory(
4703                                tmpCms,
4704                                resource.getRootPath(),
4705                                cat.getPath());
4706                        }
4707                    } catch (CmsVfsResourceNotFoundException e) {
4708                        // invalid category
4709                        try {
4710                            // try to remove invalid value
4711                            content.removeValue(value.getName(), value.getLocale(), value.getIndex());
4712                            modified = true;
4713                        } catch (CmsRuntimeException ex) {
4714                            // in case minoccurs prevents removing the invalid value
4715                            if (LOG.isDebugEnabled()) {
4716                                LOG.debug(ex.getLocalizedMessage(), ex);
4717                            }
4718                        }
4719                    }
4720                }
4721            }
4722        } catch (CmsException ex) {
4723            if (LOG.isErrorEnabled()) {
4724                LOG.error(ex.getLocalizedMessage(), ex);
4725            }
4726        }
4727        if (modified) {
4728            // when an invalid category has been removed
4729            file = content.correctXmlStructure(cms);
4730            content.setFile(file);
4731        }
4732        return file;
4733    }
4734
4735    /**
4736     * Helper method to combine synchronizations from a content definition and its nested content definitions.
4737     *
4738     * @param contentDefinition the content definition to start with
4739     * @param path the the path to this content definition
4740     * @param combinedSynchronizations the map in which the combined synchronizations should be stored
4741     */
4742    private void combineSynchronizations(
4743        CmsXmlContentDefinition contentDefinition,
4744        String path,
4745        LinkedHashMap<String, SynchronizationMode> combinedSynchronizations) {
4746
4747        // put the synchronization definitions from nested contents in the map before the definitions from the current content definition,
4748        // so the latter can override the former
4749
4750        for (String name : contentDefinition.getSchemaTypes()) {
4751            I_CmsXmlSchemaType type = contentDefinition.getSchemaType(name);
4752            if (type instanceof CmsXmlNestedContentDefinition) {
4753                CmsXmlContentDefinition nestedDef = ((CmsXmlNestedContentDefinition)type).getNestedContentDefinition();
4754                String subPath = "".equals(path) ? name : path + "/" + name;
4755                combineSynchronizations(nestedDef, subPath, combinedSynchronizations);
4756            }
4757        }
4758        CmsSynchronizationSpec synchs = contentDefinition.getContentHandler().getSynchronizations(false);
4759        for (Map.Entry<String, SynchronizationMode> entry : synchs.asMap().entrySet()) {
4760            String subPath = "".equals(path) ? entry.getKey() : path + "/" + entry.getKey();
4761            combinedSynchronizations.put(subPath, entry.getValue());
4762        }
4763    }
4764
4765    /**
4766     * Creates a property object.
4767     *
4768     * @param propertyName name of the property
4769     * @param value value to set for the property
4770     * @param mapToShared flag, indicating if the value should be set as shared property (in contrast to an individual property)
4771     *
4772     * @return the created property object.
4773     */
4774    private CmsProperty createProperty(String propertyName, String value, boolean mapToShared) {
4775
4776        if (mapToShared) {
4777            // map to shared value
4778            return new CmsProperty(propertyName, null, value);
4779        }
4780        // map to individual value
4781        return new CmsProperty(propertyName, value, null);
4782    }
4783
4784    /**
4785     * Creates a search field mapping for the given mapping element and the locale.<p>
4786     *
4787     * @param contentDefinition the content definition
4788     * @param element the mapping element configured in the schema
4789     * @param locale the locale
4790     *
4791     * @return the created search field mapping
4792     *
4793     * @throws CmsXmlException if the dynamic field class could not be found
4794     */
4795    private I_CmsSearchFieldMapping createSearchFieldMapping(
4796        CmsXmlContentDefinition contentDefinition,
4797        Element element,
4798        Locale locale,
4799        String defaultParamValue)
4800    throws CmsXmlException {
4801
4802        I_CmsSearchFieldMapping fieldMapping = null;
4803        String typeAsString = element.attributeValue(APPINFO_ATTR_TYPE);
4804        CmsSearchFieldMappingType type = CmsSearchFieldMappingType.valueOf(typeAsString);
4805        if (type == null) {
4806            throw new CmsXmlException(
4807                Messages.get().container(
4808                    Messages.ERR_XML_SCHEMA_MAPPING_TYPE_NOT_EXIST_3,
4809                    typeAsString,
4810                    contentDefinition.getTypeName(),
4811                    contentDefinition.getSchemaLocation()));
4812        }
4813        String mappingClass = element.attributeValue(APPINFO_ATTR_CLASS);
4814        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(mappingClass)) {
4815            try {
4816                fieldMapping = (I_CmsSearchFieldMapping)Class.forName(mappingClass).newInstance();
4817            } catch (Exception e) {
4818                throw new CmsXmlException(
4819                    Messages.get().container(
4820                        Messages.ERR_XML_SCHEMA_MAPPING_CLASS_NOT_EXIST_3,
4821                        mappingClass,
4822                        contentDefinition.getTypeName(),
4823                        contentDefinition.getSchemaLocation()));
4824            }
4825        } else {
4826            fieldMapping = new CmsSearchFieldMapping();
4827        }
4828        fieldMapping.setType(type);
4829        String paramValue = element.getStringValue();
4830        if ((paramValue == null) || paramValue.isEmpty()) {
4831            paramValue = defaultParamValue;
4832        }
4833        fieldMapping.setParam(paramValue);
4834        fieldMapping.setLocale(locale);
4835        fieldMapping.setDefaultValue(element.attributeValue(APPINFO_ATTR_DEFAULT));
4836        return fieldMapping;
4837    }
4838
4839    /**
4840     * Gets the localized error message for a specific field.
4841     * @param cms the CMS context
4842     * @param element the field name
4843     */
4844    private String getErrorMessage(CmsObject cms, String element) {
4845
4846        String configuredMessage = m_validationErrorMessages.get(element);
4847        CmsMacroResolver resolver = CmsMacroResolver.newInstance().setCmsObject(cms).setMessages(
4848            getMessages(OpenCms.getWorkplaceManager().getWorkplaceLocale(cms)));
4849        return resolver.resolveMacros(configuredMessage);
4850    }
4851
4852    /**
4853     * Gets the xpath mapped to a given target, if the mapping exists, and null otherwise.
4854     *
4855     * @param target the mapping target
4856     * @return the xpath mapped to the target
4857     */
4858    private String getMappingSource(String target) {
4859
4860        for (Map.Entry<String, List<String>> entry : m_elementMappings.entrySet()) {
4861            if (entry.getValue().contains(target)) {
4862                return entry.getKey();
4863            }
4864        }
4865        return null;
4866    }
4867
4868    /**
4869     * Returns the name of the property to map to in case of a property or property list mapping,
4870     * combined with the information if the mapping is to the shared property.
4871     * Otherwise null is returned.
4872     *
4873     * @param mapping the mapping to get the property for.
4874     *
4875     * @return the property to map to, combined with the information if the mapping is for the shared property version,
4876     *  or null if the provided mapping was not for properties.
4877     */
4878    private CmsPair<String, Boolean> getMapToProperty(String mapping) {
4879
4880        if (mapping.startsWith(MAPTO_PROPERTY)) {
4881            if (mapping.startsWith(MAPTO_PROPERTY_INDIVIDUAL)) {
4882                return new CmsPair<>(mapping.substring(MAPTO_PROPERTY_INDIVIDUAL.length()), Boolean.FALSE);
4883            } else if (mapping.startsWith(MAPTO_PROPERTY_SHARED)) {
4884                return new CmsPair<>(mapping.substring(MAPTO_PROPERTY_SHARED.length()), Boolean.TRUE);
4885            } else {
4886                return new CmsPair<>(mapping.substring(MAPTO_PROPERTY.length()), Boolean.FALSE);
4887            }
4888        } else if (mapping.startsWith(MAPTO_PROPERTY_LIST)) {
4889            if (mapping.startsWith(MAPTO_PROPERTY_LIST_INDIVIDUAL)) {
4890                return new CmsPair<>(mapping.substring(MAPTO_PROPERTY_LIST_INDIVIDUAL.length()), Boolean.FALSE);
4891            } else if (mapping.startsWith(MAPTO_PROPERTY_LIST_SHARED)) {
4892                return new CmsPair<>(mapping.substring(MAPTO_PROPERTY_LIST_SHARED.length()), Boolean.TRUE);
4893            } else {
4894                return new CmsPair<>(mapping.substring(MAPTO_PROPERTY_LIST.length()), Boolean.FALSE);
4895            }
4896        }
4897        return null;
4898    }
4899
4900    /**
4901     * Returns the value to map for property list mappings.
4902     * @param rootCms the context
4903     * @param content the content
4904     * @param valuePath the value path (to the value list) to map
4905     * @param locale the locale to map
4906     * @return the value to map for property list mappings.
4907     */
4908    private String getPropertyListMappingValue(
4909        CmsObject rootCms,
4910        CmsXmlContent content,
4911        String valuePath,
4912        Locale locale) {
4913
4914        String path = CmsXmlUtils.removeXpathIndex(valuePath);
4915        List<I_CmsXmlContentValue> values = content.getValues(path, locale);
4916        Iterator<I_CmsXmlContentValue> j = values.iterator();
4917        StringBuffer result = new StringBuffer(values.size() * 64);
4918        while (j.hasNext()) {
4919            I_CmsXmlContentValue val = j.next();
4920            result.append(val.getStringValue(rootCms));
4921            if (j.hasNext()) {
4922                result.append(CmsProperty.VALUE_LIST_DELIMITER);
4923            }
4924        }
4925        return result.toString();
4926    }
4927
4928    /**
4929     * Utility method to return a path fragment.<p>
4930     *
4931     * @param pathElements the path elements
4932     * @param begin the begin index
4933     *
4934     * @return the path
4935     */
4936    private String getSubPath(String[] pathElements, int begin) {
4937
4938        String result = "";
4939        for (int i = begin; i < pathElements.length; i++) {
4940            result += pathElements[i] + "/";
4941        }
4942        if (result.length() > 0) {
4943            result = result.substring(0, result.length() - 1);
4944        }
4945        return result;
4946    }
4947
4948    /**
4949     * Checks if any configured value type is an OpenCmsCategory.
4950     *
4951     * @return true if any configured value type is an OpenCmsCategory
4952     */
4953    private boolean hasCategoryType() {
4954
4955        try {
4956            for (I_CmsXmlSchemaType typeEntry : m_contentDefinition.getTypeSequence()) {
4957                String typeName = typeEntry.getTypeName();
4958                I_CmsXmlSchemaType type = OpenCms.getXmlContentTypeManager().getContentType(typeName);
4959                if (type instanceof CmsXmlCategoryValue) {
4960                    return true;
4961                }
4962            }
4963        } catch (Exception e) {
4964            LOG.debug(e.getLocalizedMessage(), e);
4965        }
4966        return false;
4967
4968    }
4969
4970    /**
4971     * Checks whether a category widget is configured.
4972     *
4973     * @return true if a category widget is configured
4974     */
4975    private boolean hasCategoryWidget() {
4976
4977        if (m_hasCategoryWidget == null) {
4978            boolean result = false;
4979            for (Map.Entry<String, String> widgetEntry : m_widgetNames.entrySet()) {
4980                String widgetName = widgetEntry.getValue();
4981                I_CmsWidget widget = OpenCms.getXmlContentTypeManager().getWidget(widgetName);
4982                if ((widget != null) && (widget instanceof CmsCategoryWidget)) {
4983                    result = true;
4984                    break;
4985                }
4986            }
4987            result = result || hasCategoryType();
4988            m_hasCategoryWidget = Boolean.valueOf(result);
4989            return result;
4990        }
4991        return m_hasCategoryWidget.booleanValue();
4992
4993    }
4994
4995    /**
4996     * Initializes the geo-mapping configuration.
4997     *
4998     * @param element the configuration node
4999     */
5000    private void initGeoMappingEntries(Element element) {
5001
5002        try {
5003            for (Element child : element.elements()) {
5004                EntryType type = EntryType.valueOf(child.getName());
5005                String value = child.getText();
5006                Entry entry = new Entry(type, value.trim());
5007                m_geomappingEntries.add(entry);
5008            }
5009        } catch (Exception e) {
5010            LOG.error(e.getLocalizedMessage(), e);
5011        }
5012    }
5013
5014    /**
5015     * Initializes the message key fall back handler.<p>
5016     *
5017     * @param element the XML element node
5018     */
5019    private void initMessageKeyHandler(Element element) {
5020
5021        String className = element.attributeValue(APPINFO_ATTR_CLASS);
5022        String configuration = element.attributeValue(APPINFO_ATTR_CONFIGURATION);
5023        try {
5024            Object messageKeyHandler = Class.forName(className).getConstructor(String.class).newInstance(configuration);
5025            m_messageKeyHandler = (CmsMultiMessages.I_KeyFallbackHandler)messageKeyHandler;
5026        } catch (Exception e) {
5027            LOG.error(e.getLocalizedMessage(), e);
5028        }
5029    }
5030
5031    /**
5032     * Checks if the locale specific value would be the same as the fallback
5033     * @param rootCms the context
5034     * @param content the content
5035     * @param mapping the mapping to resolve
5036     * @param valuePath the path to the value (sequence) to map
5037     * @param valueIndex the index of the value
5038     * @param valueLocale the locale to map the value
5039     * @param defaultLocale the default locale
5040     * @param isSequence flag, indicating if a sequence should be mapped
5041     * @param stringValueToMap the value that would be mapped to the locale specific property
5042     * @return true iff the value would be the same as its fallback.
5043     */
5044    private boolean isLocalePropertyValueEqualToFallback(
5045        CmsObject rootCms,
5046        CmsXmlContent content,
5047        String mapping,
5048        String valuePath,
5049        int valueIndex,
5050        Locale valueLocale,
5051        Locale defaultLocale,
5052        boolean isSequence,
5053        String stringValueToMap) {
5054
5055        String valueLocaleString = valueLocale.toString();
5056        Locale fallbackLocale = null;
5057        String fallbackValue = null;
5058        List<String> localeStrings = new ArrayList<>(4);
5059        while (valueLocaleString.contains("_")) {
5060            valueLocaleString = valueLocaleString.substring(0, valueLocaleString.lastIndexOf('_'));
5061            localeStrings.add(valueLocaleString);
5062        }
5063        localeStrings.add(defaultLocale.toString());
5064        Iterator<String> localeIterator = localeStrings.iterator();
5065        while ((fallbackLocale == null) && localeIterator.hasNext()) {
5066            valueLocaleString = localeIterator.next();
5067            Locale l = CmsLocaleManager.getLocale(valueLocaleString);
5068            if (content.hasLocale(l)
5069                && (content.hasValue(valuePath, l)
5070                    || (isSequence && content.hasValue(CmsXmlUtils.removeXpathIndex(valuePath), l)))) {
5071                fallbackLocale = l;
5072            } else if (isMappingUsingDefault(valuePath, mapping)) {
5073                String potentialFallbackValue = getDefault(rootCms, content.getFile(), null, valuePath, l);
5074                if ((potentialFallbackValue != null) && !potentialFallbackValue.isEmpty()) {
5075                    if (isSequence) {
5076                        fallbackLocale = l;
5077                        fallbackValue = potentialFallbackValue;
5078                    } else {
5079                        CmsGalleryNameMacroResolver resolver = new CmsGalleryNameMacroResolver(rootCms, content, l);
5080                        resolver.setKeepEmptyMacros(true);
5081                        potentialFallbackValue = resolver.resolveMacros(potentialFallbackValue);
5082                        if ((null != potentialFallbackValue) && !potentialFallbackValue.isEmpty()) {
5083                            fallbackLocale = l;
5084                            fallbackValue = potentialFallbackValue;
5085                        }
5086                    }
5087                }
5088            }
5089        }
5090        if (null != fallbackLocale) {
5091            String fallbackStringValue = null;
5092            if (isSequence) {
5093                fallbackStringValue = fallbackValue != null
5094                ? fallbackValue
5095                : getPropertyListMappingValue(rootCms, content, valuePath, fallbackLocale);
5096            } else {
5097                if (null != fallbackValue) {
5098                    // This is already resolved.
5099                    fallbackStringValue = fallbackValue;
5100                } else {
5101                    String originalFallbackStringValue = content.getValue(valuePath, fallbackLocale).getStringValue(
5102                        rootCms);
5103                    CmsGalleryNameMacroResolver resolver = new CmsGalleryNameMacroResolver(
5104                        rootCms,
5105                        content,
5106                        fallbackLocale);
5107                    resolver.setKeepEmptyMacros(true);
5108                    fallbackStringValue = resolver.resolveMacros(originalFallbackStringValue);
5109                }
5110            }
5111            if (null != fallbackStringValue) {
5112                fallbackStringValue = fallbackStringValue.trim();
5113            }
5114            return stringValueToMap.equals(fallbackStringValue);
5115        }
5116
5117        return false;
5118    }
5119
5120    /**
5121     * Checks if the given mapping has the 'useDefault' flag set to true.<p>
5122     *
5123     * @param path the mapping path
5124     * @param mapping the mapping type
5125     *
5126     * @return true if 'useDefault' is enabled for this mapping
5127     */
5128    private boolean isMappingUsingDefault(String path, String mapping) {
5129
5130        String key = path + ":" + mapping;
5131        return m_mappingsUsingDefault.contains(key);
5132    }
5133
5134    /**
5135     * Helper method which does most of the mapping resolution work.<p>
5136     *
5137     * @param cms the CMS context to use
5138     * @param content the content object
5139     * @param valuePath the xpath of the value
5140     * @param valueIsSimple true if this is a simple value
5141     * @param valueIndex the index of the value
5142     * @param valueLocale the locale of the value
5143     * @param originalStringValue the value as a string
5144     *
5145     * @throws CmsException if something goes wrong
5146     */
5147    private void resolveMapping(
5148        CmsObject cms,
5149        CmsXmlContent content,
5150        String valuePath,
5151        boolean valueIsSimple,
5152        int valueIndex,
5153        Locale valueLocale,
5154        String originalStringValue)
5155    throws CmsException {
5156
5157        CmsObject rootCms = createRootCms(cms);
5158        // get the original VFS file from the content
5159        CmsFile file = content.getFile();
5160        if (!valueIsSimple) {
5161            // no mappings for a nested schema are possible
5162            // note that the sub-elements of the nested schema ARE mapped by the node visitor,
5163            // it's just the nested schema value itself that does not support mapping
5164            return;
5165        }
5166
5167        List<String> mappings = getMappings(valuePath);
5168        if (mappings.size() == 0) {
5169            // nothing to do if we have no mappings at all
5170            return;
5171        }
5172        // create OpenCms user context initialized with "/" as site root to read all siblings
5173        // read all siblings of the file
5174        List<CmsResource> siblings = rootCms.readSiblings(
5175            content.getFile().getRootPath(),
5176            CmsResourceFilter.IGNORE_EXPIRATION);
5177
5178        Set<CmsResource> urlNameMappingResources = new HashSet<CmsResource>();
5179        boolean mapToUrlName = false;
5180        urlNameMappingResources.add(content.getFile());
5181        // since 7.0.2 multiple mappings are possible
5182
5183        // get the string value of the current node
5184
5185        CmsGalleryNameMacroResolver resolver = new CmsGalleryNameMacroResolver(rootCms, content, valueLocale);
5186        resolver.setKeepEmptyMacros(true);
5187        String stringValue = resolver.resolveMacros(originalStringValue);
5188        CmsMappingResolutionContext mappingContext = (CmsMappingResolutionContext)(cms.getRequestContext().getAttribute(
5189            ATTR_MAPPING_RESOLUTION_CONTEXT));
5190
5191        for (String mapping : mappings) {
5192
5193            if (CmsStringUtil.isNotEmpty(mapping)) {
5194
5195                // attribute mapping now does its own handling of siblings/locales in CmsMappingResolutionContext,
5196                // so we just save the mapped release/expiration dates for later, and we do this before the sibling/locale handling
5197                // logic in this method.
5198                if (mapping.startsWith(MAPTO_ATTRIBUTE)) {
5199
5200                    // this is an attribute mapping
5201                    String attribute = mapping.substring(MAPTO_ATTRIBUTE.length());
5202                    switch (ATTRIBUTES.indexOf(attribute)) {
5203                        case 0: // date released
5204                            long date = 0;
5205                            try {
5206                                date = Long.valueOf(stringValue).longValue();
5207                            } catch (NumberFormatException e) {
5208                                // ignore, value can be a macro
5209                            }
5210                            if (date == 0) {
5211                                date = CmsResource.DATE_RELEASED_DEFAULT;
5212                            }
5213                            mappingContext.putReleaseDate(valueLocale, date);
5214                            break;
5215                        case 1: // date expired
5216                            date = 0;
5217                            try {
5218                                date = Long.valueOf(stringValue).longValue();
5219                            } catch (NumberFormatException e) {
5220                                // ignore, value can be a macro
5221                            }
5222                            if (date == 0) {
5223                                date = CmsResource.DATE_EXPIRED_DEFAULT;
5224                            }
5225                            mappingContext.putExpirationDate(valueLocale, date);
5226                            break;
5227                        default:
5228                            // ignore invalid / other mappings
5229                    }
5230                    continue; // skip to next mapping
5231                }
5232
5233                // Get the property to map to in case we have a property (list) mapping
5234                CmsPair<String, Boolean> mapToPropertyInfo = getMapToProperty(mapping);
5235                String mapToProperty = mapToPropertyInfo == null ? null : mapToPropertyInfo.getFirst();
5236                // We need to determine if locale specific property mappings are necessary
5237                // If not, we can skip much code
5238                boolean needsLocaleSpecificMapping = false;
5239                if ((null != mapToProperty) && !mapToProperty.isBlank()) {
5240                    // We check if the locale specific property is defined.
5241                    // Only for defined properties we do mappings.
5242                    String localeSpecificProperty = CmsProperty.getLocaleSpecificPropertyName(
5243                        mapToProperty,
5244                        valueLocale);
5245                    try {
5246                        needsLocaleSpecificMapping = null != rootCms.readPropertyDefinition(localeSpecificProperty);
5247                    } catch (CmsException e) {
5248                        // Do nothing, this is thrown if the property definition does not exist and in this case we
5249                        // do not want to perform the mapping.
5250                    }
5251                }
5252
5253                // for multiple language mappings, we need to ensure
5254                // a) all siblings are handled
5255                // b) only the "right" locale is mapped to a sibling
5256                for (int i = (siblings.size() - 1); i >= 0; i--) {
5257                    // get filename
5258                    String filename = (siblings.get(i)).getRootPath();
5259                    if (mapping.startsWith(MAPTO_URLNAME)) {
5260                        // should be written regardless of whether there is a sibling with the correct locale
5261                        mapToUrlName = true;
5262                    }
5263
5264                    Locale locale = OpenCms.getLocaleManager().getDefaultLocale(rootCms, filename);
5265                    boolean localeIsValueLocale = locale.equals(valueLocale);
5266
5267                    if (!(localeIsValueLocale || needsLocaleSpecificMapping)) {
5268                        // only map if the locale fits, but for properties map to locale-specific properties, even if it does not fit.
5269                        continue;
5270                    }
5271
5272                    // make sure the file is locked
5273                    CmsLock lock = rootCms.getLock(filename);
5274                    if (lock.isUnlocked()) {
5275                        rootCms.lockResource(filename);
5276                    } else if (!lock.isDirectlyOwnedInProjectBy(rootCms)) {
5277                        rootCms.changeLock(filename);
5278                    }
5279
5280                    if (localeIsValueLocale && mapping.startsWith(MAPTO_PERMISSION) && (valueIndex == 0)) {
5281
5282                        // map value to a permission
5283                        // example of a mapping: mapto="permission:GROUP:+r+v|GROUP.ALL_OTHERS:|GROUP.Projectmanagers:+r+v+w+c"
5284
5285                        // get permission(s) to set
5286                        String permissionMappings = mapping.substring(MAPTO_PERMISSION.length());
5287                        String mainMapping = permissionMappings;
5288                        Map<String, String> permissionsToSet = new HashMap<String, String>();
5289
5290                        // separate permission to set for element value from other permissions to set
5291                        int sepIndex = permissionMappings.indexOf('|');
5292                        if (sepIndex != -1) {
5293                            mainMapping = permissionMappings.substring(0, sepIndex);
5294                            permissionMappings = permissionMappings.substring(sepIndex + 1);
5295                            permissionsToSet = CmsStringUtil.splitAsMap(permissionMappings, "|", ":");
5296                        }
5297
5298                        // determine principal type and permission string to set
5299                        String principalType = I_CmsPrincipal.PRINCIPAL_GROUP;
5300                        String permissionString = mainMapping;
5301                        sepIndex = mainMapping.indexOf(':');
5302                        if (sepIndex != -1) {
5303                            principalType = mainMapping.substring(0, sepIndex);
5304                            permissionString = mainMapping.substring(sepIndex + 1);
5305                        }
5306                        if (permissionString.toLowerCase().indexOf('o') == -1) {
5307                            permissionString += "+o";
5308                        }
5309
5310                        // remove all existing permissions from the file
5311                        List<CmsAccessControlEntry> aces = rootCms.getAccessControlEntries(filename, false);
5312                        for (Iterator<CmsAccessControlEntry> j = aces.iterator(); j.hasNext();) {
5313                            CmsAccessControlEntry ace = j.next();
5314                            if (ace.getPrincipal().equals(CmsAccessControlEntry.PRINCIPAL_ALL_OTHERS_ID)) {
5315                                // remove the entry "All others", which has to be treated in a special way
5316                                rootCms.rmacc(
5317                                    filename,
5318                                    CmsAccessControlEntry.PRINCIPAL_ALL_OTHERS_NAME,
5319                                    CmsAccessControlEntry.PRINCIPAL_ALL_OTHERS_ID.toString());
5320                            } else {
5321                                // this is a group or user principal
5322                                I_CmsPrincipal principal = CmsPrincipal.readPrincipal(rootCms, ace.getPrincipal());
5323                                if (principal.isGroup()) {
5324                                    rootCms.rmacc(filename, I_CmsPrincipal.PRINCIPAL_GROUP, principal.getName());
5325                                } else if (principal.isUser()) {
5326                                    rootCms.rmacc(filename, I_CmsPrincipal.PRINCIPAL_USER, principal.getName());
5327                                }
5328                            }
5329                        }
5330
5331                        // set additional permissions that are defined in mapping
5332                        for (Iterator<Map.Entry<String, String>> j = permissionsToSet.entrySet().iterator(); j.hasNext();) {
5333                            Map.Entry<String, String> entry = j.next();
5334                            sepIndex = entry.getKey().indexOf('.');
5335                            if (sepIndex != -1) {
5336                                String type = entry.getKey().substring(0, sepIndex);
5337                                String name = entry.getKey().substring(sepIndex + 1);
5338                                String permissions = entry.getValue();
5339                                if (permissions.toLowerCase().indexOf('o') == -1) {
5340                                    permissions += "+o";
5341                                }
5342                                try {
5343                                    rootCms.chacc(filename, type, name, permissions);
5344                                } catch (CmsException e) {
5345                                    // setting permission did not work
5346                                    LOG.error(e.getLocalizedMessage(), e);
5347                                }
5348                            }
5349                        }
5350
5351                        // set permission(s) using the element value(s)
5352                        // the set with all selected principals
5353                        TreeSet<String> allPrincipals = new TreeSet<String>();
5354                        String path = CmsXmlUtils.removeXpathIndex(valuePath);
5355                        List<I_CmsXmlContentValue> values = content.getValues(path, valueLocale);
5356                        Iterator<I_CmsXmlContentValue> j = values.iterator();
5357                        while (j.hasNext()) {
5358                            I_CmsXmlContentValue val = j.next();
5359                            String principalName = val.getStringValue(rootCms);
5360                            // the prinicipal name can be a principal list
5361                            List<String> principalNames = CmsStringUtil.splitAsList(
5362                                principalName,
5363                                PRINCIPAL_LIST_SEPARATOR);
5364                            // iterate over the principals
5365                            Iterator<String> iterPrincipals = principalNames.iterator();
5366                            while (iterPrincipals.hasNext()) {
5367                                // get the next principal
5368                                String principal = iterPrincipals.next();
5369                                allPrincipals.add(principal);
5370                            }
5371                        }
5372                        // iterate over the set with all principals and set the permissions
5373                        Iterator<String> iterAllPricinipals = allPrincipals.iterator();
5374                        while (iterAllPricinipals.hasNext()) {
5375                            // get the next principal
5376                            String principal = iterAllPricinipals.next();
5377                            rootCms.chacc(filename, principalType, principal, permissionString);
5378                        }
5379                        // special case: permissions are written only to one sibling, end loop
5380                        i = 0;
5381                    } else if (mapping.startsWith(MAPTO_PROPERTY_LIST) && (valueIndex == 0)) {
5382
5383                        // check which mapping is used (shared or individual)
5384                        boolean mapToShared = mapping.startsWith(MAPTO_PROPERTY_LIST_SHARED);
5385
5386                        String result = getPropertyListMappingValue(rootCms, content, valuePath, valueLocale).trim();
5387                        if (localeIsValueLocale) {
5388                            rootCms.writePropertyObject(filename, createProperty(mapToProperty, result, mapToShared));
5389                        }
5390                        if (needsLocaleSpecificMapping) {
5391                            boolean removePropertyValue = localeIsValueLocale
5392                                || isLocalePropertyValueEqualToFallback(
5393                                    rootCms,
5394                                    content,
5395                                    mapping,
5396                                    valuePath,
5397                                    valueIndex,
5398                                    valueLocale,
5399                                    locale,
5400                                    true,
5401                                    result);
5402                            rootCms.writePropertyObject(
5403                                filename,
5404                                createProperty(
5405                                    CmsProperty.getLocaleSpecificPropertyName(mapToProperty, valueLocale),
5406                                    removePropertyValue ? CmsProperty.DELETE_VALUE : result,
5407                                    mapToShared));
5408                        }
5409                        if (mapToShared) {
5410                            // special case: shared mappings must be written only to one sibling, end loop
5411                            i = 0;
5412                        }
5413
5414                    } else if (mapping.startsWith(MAPTO_PROPERTY)) {
5415
5416                        // check which mapping is used (shared or individual)
5417                        boolean mapToShared = mapping.startsWith(MAPTO_PROPERTY_SHARED);
5418
5419                        if (null != stringValue) {
5420                            stringValue = stringValue.trim();
5421                        }
5422
5423                        if (localeIsValueLocale) {
5424                            rootCms.writePropertyObject(
5425                                filename,
5426                                createProperty(mapToProperty, stringValue, mapToShared));
5427                        }
5428                        if (needsLocaleSpecificMapping) {
5429                            boolean removePropertyValue = localeIsValueLocale
5430                                || isLocalePropertyValueEqualToFallback(
5431                                    rootCms,
5432                                    content,
5433                                    mapping,
5434                                    valuePath,
5435                                    valueIndex,
5436                                    valueLocale,
5437                                    locale,
5438                                    false,
5439                                    stringValue);
5440                            rootCms.writePropertyObject(
5441                                filename,
5442                                createProperty(
5443                                    CmsProperty.getLocaleSpecificPropertyName(mapToProperty, valueLocale),
5444                                    removePropertyValue ? CmsProperty.DELETE_VALUE : stringValue,
5445                                    mapToShared));
5446                        }
5447                        if (mapToShared) {
5448                            // special case: shared mappings must be written only to one sibling, end loop
5449                            i = 0;
5450                        }
5451                    } else if (localeIsValueLocale && mapping.startsWith(MAPTO_URLNAME)) {
5452                        // we write the actual mappings later
5453                        urlNameMappingResources.add(siblings.get(i));
5454                    }
5455                }
5456            }
5457        }
5458        if (mapToUrlName) {
5459            for (CmsResource resourceForUrlNameMapping : urlNameMappingResources) {
5460                if (!CmsResource.isTemporaryFileName(resourceForUrlNameMapping.getRootPath())) {
5461                    String mappedName = stringValue;
5462                    if (!CmsStringUtil.isEmptyOrWhitespaceOnly(mappedName)) {
5463                        mappedName = mappedName.trim();
5464                        mappingContext.addUrlNameMapping(
5465                            mappedName,
5466                            valueLocale,
5467                            resourceForUrlNameMapping.getStructureId());
5468                    }
5469                }
5470            }
5471        }
5472
5473        // make sure the original is locked
5474        CmsLock lock = rootCms.getLock(file);
5475        if (lock.isUnlocked()) {
5476            rootCms.lockResource(file.getRootPath());
5477        } else if (!lock.isExclusiveOwnedBy(rootCms.getRequestContext().getCurrentUser())) {
5478            rootCms.changeLock(file.getRootPath());
5479        }
5480    }
5481
5482    /**
5483     * Parses a boolean from a string and returns a default value if the string couldn't be parsed.<p>
5484     *
5485     * @param text the text from which to get the boolean value
5486     * @param defaultValue the value to return if parsing fails
5487     *
5488     * @return the parsed boolean
5489     */
5490    private boolean safeParseBoolean(String text, boolean defaultValue) {
5491
5492        if (text == null) {
5493            return defaultValue;
5494        }
5495        try {
5496            return Boolean.parseBoolean(text);
5497        } catch (Throwable t) {
5498            return defaultValue;
5499        }
5500    }
5501
5502}