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