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