org.opencms.xml.content.CmsDefaultXmlContentHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.opencms.xml.content.CmsDefaultXmlContentHandler.java

Source

/*
 * This library is part of OpenCms -
 * the Open Source Content Management System
 *
 * Copyright (c) Alkacon Software GmbH (http://www.alkacon.com)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * For further information about Alkacon Software GmbH, please see the
 * company website: http://www.alkacon.com
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.opencms.xml.content;

import org.opencms.configuration.CmsConfigurationManager;
import org.opencms.db.log.CmsLogEntry;
import org.opencms.file.CmsDataAccessException;
import org.opencms.file.CmsFile;
import org.opencms.file.CmsObject;
import org.opencms.file.CmsProperty;
import org.opencms.file.CmsPropertyDefinition;
import org.opencms.file.CmsResource;
import org.opencms.file.CmsResourceFilter;
import org.opencms.file.CmsVfsResourceNotFoundException;
import org.opencms.i18n.CmsEncoder;
import org.opencms.i18n.CmsListResourceBundle;
import org.opencms.i18n.CmsLocaleManager;
import org.opencms.i18n.CmsMessages;
import org.opencms.i18n.CmsMultiMessages;
import org.opencms.i18n.CmsResourceBundleLoader;
import org.opencms.loader.I_CmsFileNameGenerator;
import org.opencms.lock.CmsLock;
import org.opencms.main.CmsException;
import org.opencms.main.CmsLog;
import org.opencms.main.CmsRuntimeException;
import org.opencms.main.OpenCms;
import org.opencms.relations.CmsCategory;
import org.opencms.relations.CmsCategoryService;
import org.opencms.relations.CmsLink;
import org.opencms.relations.CmsRelationType;
import org.opencms.security.CmsAccessControlEntry;
import org.opencms.security.CmsPrincipal;
import org.opencms.security.I_CmsPrincipal;
import org.opencms.site.CmsSite;
import org.opencms.util.CmsFileUtil;
import org.opencms.util.CmsHtmlConverter;
import org.opencms.util.CmsMacroResolver;
import org.opencms.util.CmsStringUtil;
import org.opencms.widgets.CmsCategoryWidget;
import org.opencms.widgets.CmsDisplayWidget;
import org.opencms.widgets.I_CmsWidget;
import org.opencms.workplace.CmsWorkplace;
import org.opencms.workplace.editors.CmsXmlContentWidgetVisitor;
import org.opencms.xml.CmsXmlContentDefinition;
import org.opencms.xml.CmsXmlEntityResolver;
import org.opencms.xml.CmsXmlException;
import org.opencms.xml.CmsXmlGenericWrapper;
import org.opencms.xml.CmsXmlUtils;
import org.opencms.xml.containerpage.CmsFormatterBean;
import org.opencms.xml.containerpage.CmsFormatterConfiguration;
import org.opencms.xml.types.CmsXmlNestedContentDefinition;
import org.opencms.xml.types.CmsXmlVarLinkValue;
import org.opencms.xml.types.CmsXmlVfsFileValue;
import org.opencms.xml.types.I_CmsXmlContentValue;
import org.opencms.xml.types.I_CmsXmlSchemaType;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;

import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;

/**
 * Default implementation for the XML content handler, will be used by all XML contents that do not
 * provide their own handler.<p>
 * 
 * @since 6.0.0 
 */
public class CmsDefaultXmlContentHandler implements I_CmsXmlContentHandler {

    /** Constant for the "appinfo" element name itself. */
    public static final String APPINFO_APPINFO = "appinfo";

    /** Constant for the "collapse" appinfo attribute name. */
    public static final String APPINFO_ATTR_COLLAPSE = "collapse";

    /** Constant for the "configuration" appinfo attribute name. */
    public static final String APPINFO_ATTR_CONFIGURATION = "configuration";

    /** Constant for the "default" appinfo attribute name. */
    public static final String APPINFO_ATTR_DEFAULT = "default";

    /** Constant for the "description" appinfo attribute name. */
    public static final String APPINFO_ATTR_DESCRIPTION = "description";

    /** Constant for the "element" appinfo attribute name. */
    public static final String APPINFO_ATTR_ELEMENT = "element";

    /** Constant for the "error" appinfo attribute name. */
    public static final String APPINFO_ATTR_ERROR = "error";

    /** Constant for the "invalidate" appinfo attribute name. */
    public static final String APPINFO_ATTR_INVALIDATE = "invalidate";

    /** Constant for the "key" appinfo attribute name. */
    public static final String APPINFO_ATTR_KEY = "key";

    /** Constant for the "locale" appinfo attribute name. */
    public static final String APPINFO_ATTR_LOCALE = "locale";

    /** Constant for the "mapto" appinfo attribute name. */
    public static final String APPINFO_ATTR_MAPTO = "mapto";

    /** Constant for the "maxwidth" appinfo attribute name. */
    public static final String APPINFO_ATTR_MAXWIDTH = "maxwidth";

    /** Constant for the "message" appinfo attribute name. */
    public static final String APPINFO_ATTR_MESSAGE = "message";

    /** Constant for the "minwidth" appinfo attribute name. */
    public static final String APPINFO_ATTR_MINWIDTH = "minwidth";

    /** Constant for the "name" appinfo attribute name. */
    public static final String APPINFO_ATTR_NAME = "name";

    /** Constant for the "nice-name" appinfo attribute name. */
    public static final String APPINFO_ATTR_NICE_NAME = "nice-name";

    /** Constant for the "preview" appinfo attribute name. */
    public static final String APPINFO_ATTR_PREVIEW = "preview";

    /** Constant for the "regex" appinfo attribute name. */
    public static final String APPINFO_ATTR_REGEX = "regex";

    /** Constant for the "rule-regex" appinfo attribute name. */
    public static final String APPINFO_ATTR_RULE_REGEX = "rule-regex";

    /** Constant for the "rule-type" appinfo attribute name. */
    public static final String APPINFO_ATTR_RULE_TYPE = "rule-type";

    /** Constant for the "searchcontent" appinfo attribute name. */
    public static final String APPINFO_ATTR_SEARCHCONTENT = "searchcontent";

    /** Constant for the "select-inherit" appinfo attribute name. */
    public static final String APPINFO_ATTR_SELECT_INHERIT = "select-inherit";

    /** Constant for the "type" appinfo attribute name. */
    public static final String APPINFO_ATTR_TYPE = "type";

    /** Constant for the "node" appinfo attribute value. */
    public static final String APPINFO_ATTR_TYPE_NODE = "node";

    /** Constant for the "parent" appinfo attribute value. */
    public static final String APPINFO_ATTR_TYPE_PARENT = "parent";

    /** Constant for the "warning" appinfo attribute value. */
    public static final String APPINFO_ATTR_TYPE_WARNING = "warning";

    /** Constant for the "uri" appinfo attribute name. */
    public static final String APPINFO_ATTR_URI = "uri";

    /** Constant for the "useall" appinfo attribute name. */
    public static final String APPINFO_ATTR_USEALL = "useall";

    /** Constant for the "value" appinfo attribute name. */
    public static final String APPINFO_ATTR_VALUE = "value";

    /** Constant for the "widget" appinfo attribute name. */
    public static final String APPINFO_ATTR_WIDGET = "widget";

    /** Constant for the "widget-config" appinfo attribute name. */
    public static final String APPINFO_ATTR_WIDGET_CONFIG = "widget-config";

    /** Constant for formatter include resource type 'CSS'. */
    public static final String APPINFO_ATTRIBUTE_TYPE_CSS = "css";

    /** Constant for formatter include resource type 'JAVASCRIPT'. */
    public static final String APPINFO_ATTRIBUTE_TYPE_JAVASCRIPT = "javascript";

    /** Constant for the "bundle" appinfo element name. */
    public static final String APPINFO_BUNDLE = "bundle";

    /** Constant for the "default" appinfo element name. */
    public static final String APPINFO_DEFAULT = "default";

    /** Constant for the "defaults" appinfo element name. */
    public static final String APPINFO_DEFAULTS = "defaults";

    /** Constant for the "formatter" appinfo element name. */
    public static final String APPINFO_FORMATTER = "formatter";

    /** Constant for the "formatters" appinfo element name. */
    public static final String APPINFO_FORMATTERS = "formatters";

    /** Constant for the "headinclude" appinfo element name. */
    public static final String APPINFO_HEAD_INCLUDE = "headinclude";

    /** Constant for the "headincludes" appinfo element name. */
    public static final String APPINFO_HEAD_INCLUDES = "headincludes";

    /** Constant for the "layout" appinfo element name. */
    public static final String APPINFO_LAYOUT = "layout";

    /** Constant for the "layouts" appinfo element name. */
    public static final String APPINFO_LAYOUTS = "layouts";

    /** Constant for the "mapping" appinfo element name. */
    public static final String APPINFO_MAPPING = "mapping";

    /** Constant for the "mappings" appinfo element name. */
    public static final String APPINFO_MAPPINGS = "mappings";

    /** Constant for the "modelfolder" appinfo element name. */
    public static final String APPINFO_MODELFOLDER = "modelfolder";

    /** Constant for the "preview" appinfo element name. */
    public static final String APPINFO_PREVIEW = "preview";

    /** Constant for the "propertybundle" appinfo element name. */
    public static final String APPINFO_PROPERTYBUNDLE = "propertybundle";

    /** Constant for the "relation" appinfo element name. */
    public static final String APPINFO_RELATION = "relation";

    /** Constant for the "relations" appinfo element name. */
    public static final String APPINFO_RELATIONS = "relations";

    /** Constant for the "resource" appinfo element name. */
    public static final String APPINFO_RESOURCE = "resource";

    /** Constant for the "resourcebundle" appinfo element name. */
    public static final String APPINFO_RESOURCEBUNDLE = "resourcebundle";

    /** Constant for the "resourcebundles" appinfo element name. */
    public static final String APPINFO_RESOURCEBUNDLES = "resourcebundles";

    /** Constant for the "rule" appinfo element name. */
    public static final String APPINFO_RULE = "rule";

    /** The file where the default appinfo schema is located. */
    public static final String APPINFO_SCHEMA_FILE = "org/opencms/xml/content/DefaultAppinfo.xsd";

    /** The file where the default appinfo schema types are located. */
    public static final String APPINFO_SCHEMA_FILE_TYPES = "org/opencms/xml/content/DefaultAppinfoTypes.xsd";

    /** The XML system id for the default appinfo schema types. */
    public static final String APPINFO_SCHEMA_SYSTEM_ID = CmsConfigurationManager.DEFAULT_DTD_PREFIX
            + APPINFO_SCHEMA_FILE;

    /** The XML system id for the default appinfo schema types. */
    public static final String APPINFO_SCHEMA_TYPES_SYSTEM_ID = CmsConfigurationManager.DEFAULT_DTD_PREFIX
            + APPINFO_SCHEMA_FILE_TYPES;

    /** Constant for the "searchsetting" appinfo element name. */
    public static final String APPINFO_SEARCHSETTING = "searchsetting";

    /** Constant for the "searchsettings" appinfo element name. */
    public static final String APPINFO_SEARCHSETTINGS = "searchsettings";

    /** Constant for the "setting" appinfo element name. */
    public static final String APPINFO_SETTING = "setting";

    /** Constant for the "settings" appinfo element name. */
    public static final String APPINFO_SETTINGS = "settings";

    /** Constant for the "tab" appinfo element name. */
    public static final String APPINFO_TAB = "tab";

    /** Constant for the "tabs" appinfo element name. */
    public static final String APPINFO_TABS = "tabs";

    /** Constant for the "validationrule" appinfo element name. */
    public static final String APPINFO_VALIDATIONRULE = "validationrule";

    /** Constant for the "validationrules" appinfo element name. */
    public static final String APPINFO_VALIDATIONRULES = "validationrules";

    /** Constant for the "xmlbundle" appinfo element name. */
    public static final String APPINFO_XMLBUNDLE = "xmlbundle";

    /** Constant for head include type attribute: CSS. */
    public static final String ATTRIBUTE_INCLUDE_TYPE_CSS = "css";

    /** Constant for head include type attribute: java-script. */
    public static final String ATTRIBUTE_INCLUDE_TYPE_JAVASCRIPT = "javascript";

    /** Macro for resolving the preview URI. */
    public static final String MACRO_PREVIEW_TEMPFILE = "previewtempfile";

    /** Default message for validation errors. */
    protected static final String MESSAGE_VALIDATION_DEFAULT_ERROR = "${validation.path}: " + "${key."
            + Messages.GUI_EDITOR_XMLCONTENT_VALIDATION_ERROR_2 + "|${validation.value}|[${validation.regex}]}";

    /** Default message for validation warnings. */
    protected static final String MESSAGE_VALIDATION_DEFAULT_WARNING = "${validation.path}: " + "${key."
            + Messages.GUI_EDITOR_XMLCONTENT_VALIDATION_WARNING_2 + "|${validation.value}|[${validation.regex}]}";

    /** The attribute name for the "prefer folder" option for properties. */
    private static final String APPINFO_ATTR_PREFERFOLDER = "PreferFolder";

    /** The log object for this class. */
    private static final Log LOG = CmsLog.getLog(CmsDefaultXmlContentHandler.class);

    /** The principal list separator. */
    private static final String PRINCIPAL_LIST_SEPARATOR = ",";

    /** The configuration values for the element widgets (as defined in the annotations). */
    protected Map<String, String> m_configurationValues;

    /** The CSS resources to include into the html-page head. */
    protected Set<String> m_cssHeadIncludes;

    /** The default values for the elements (as defined in the annotations). */
    protected Map<String, String> m_defaultValues;

    /** The element mappings (as defined in the annotations). */
    protected Map<String, List<String>> m_elementMappings;

    /** The widgets used for the elements (as defined in the annotations). */
    protected Map<String, I_CmsWidget> m_elementWidgets;

    /** The formatter configuration. */
    protected CmsFormatterConfiguration m_formatterConfiguration;

    /** The list of formatters from the XSD. */
    protected List<CmsFormatterBean> m_formatters;

    /** The java-script resources to include into the html-page head. */
    protected Set<String> m_jsHeadIncludes;

    /** The resource bundle name to be used for localization of this content handler. */
    protected List<String> m_messageBundleNames;

    /** The folder containing the model file(s) for the content. */
    protected String m_modelFolder;

    /** The preview location (as defined in the annotations). */
    protected String m_previewLocation;

    /** The relation check rules. */
    protected Map<String, Boolean> m_relationChecks;

    /** The relation check rules. */
    protected Map<String, CmsRelationType> m_relations;

    /** The search settings. */
    protected Map<String, Boolean> m_searchSettings;

    /** The configured settings for the formatters (as defined in the annotations). */
    protected Map<String, CmsXmlContentProperty> m_settings;

    /** The configured tabs. */
    protected List<CmsXmlContentTab> m_tabs;

    /** The list of mappings to the "Title" property. */
    protected List<String> m_titleMappings;

    /** The messages for the error validation rules. */
    protected Map<String, String> m_validationErrorMessages;

    /** The validation rules that cause an error (as defined in the annotations). */
    protected Map<String, String> m_validationErrorRules;

    /** The messages for the warning validation rules. */
    protected Map<String, String> m_validationWarningMessages;

    /** The validation rules that cause a warning (as defined in the annotations). */
    protected Map<String, String> m_validationWarningRules;

    /**
     * Creates a new instance of the default XML content handler.<p>  
     */
    public CmsDefaultXmlContentHandler() {

        init();
    }

    /**
     * Static initializer for caching the default appinfo validation schema.<p>
     */
    static {

        // the schema definition is located in 2 separates file for easier editing
        // 2 files are required in case an extended schema want to use the default definitions,
        // but with an extended "appinfo" node 
        byte[] appinfoSchemaTypes;
        try {
            // first read the default types
            appinfoSchemaTypes = CmsFileUtil.readFile(APPINFO_SCHEMA_FILE_TYPES);
        } catch (Exception e) {
            throw new CmsRuntimeException(Messages.get().container(
                    org.opencms.xml.types.Messages.ERR_XMLCONTENT_LOAD_SCHEMA_1, APPINFO_SCHEMA_FILE_TYPES), e);
        }
        CmsXmlEntityResolver.cacheSystemId(APPINFO_SCHEMA_TYPES_SYSTEM_ID, appinfoSchemaTypes);
        byte[] appinfoSchema;
        try {
            // now read the default base schema
            appinfoSchema = CmsFileUtil.readFile(APPINFO_SCHEMA_FILE);
        } catch (Exception e) {
            throw new CmsRuntimeException(Messages.get().container(
                    org.opencms.xml.types.Messages.ERR_XMLCONTENT_LOAD_SCHEMA_1, APPINFO_SCHEMA_FILE), e);
        }
        CmsXmlEntityResolver.cacheSystemId(APPINFO_SCHEMA_SYSTEM_ID, appinfoSchema);
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getConfiguration(org.opencms.xml.types.I_CmsXmlSchemaType)
     */
    public String getConfiguration(I_CmsXmlSchemaType type) {

        String elementName = type.getName();
        return m_configurationValues.get(elementName);

    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getCSSHeadIncludes()
     */
    public Set<String> getCSSHeadIncludes() {

        return Collections.unmodifiableSet(m_cssHeadIncludes);
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getCSSHeadIncludes(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
     */
    @SuppressWarnings("unused")
    public Set<String> getCSSHeadIncludes(CmsObject cms, CmsResource resource) throws CmsException {

        return getCSSHeadIncludes();
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getDefault(org.opencms.file.CmsObject, I_CmsXmlContentValue, java.util.Locale)
     */
    public String getDefault(CmsObject cms, I_CmsXmlContentValue value, Locale locale) {

        String defaultValue;
        if (value.getElement() == null) {
            // use the "getDefault" method of the given value, will use value from standard XML schema
            defaultValue = value.getDefault(locale);
        } else {
            String xpath = value.getPath();
            // look up the default from the configured mappings
            defaultValue = m_defaultValues.get(xpath);
            if (defaultValue == null) {
                // no value found, try default xpath
                xpath = CmsXmlUtils.removeXpath(xpath);
                xpath = CmsXmlUtils.createXpath(xpath, 1);
                // look up the default value again with default index of 1 in all path elements
                defaultValue = m_defaultValues.get(xpath);
            }
        }
        if (defaultValue != null) {
            CmsObject newCms = cms;
            try {
                // switch the current URI to the XML document resource so that properties can be read
                CmsResource file = value.getDocument().getFile();
                CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(file.getRootPath());
                if (site != null) {
                    newCms = OpenCms.initCmsObject(cms);
                    newCms.getRequestContext().setSiteRoot(site.getSiteRoot());
                    newCms.getRequestContext().setUri(newCms.getSitePath(file));
                }
            } catch (Exception e) {
                // on any error just use the default input OpenCms context
            }
            // return the default value with processed macros
            CmsMacroResolver resolver = CmsMacroResolver.newInstance().setCmsObject(newCms)
                    .setMessages(getMessages(locale));
            return resolver.resolveMacros(defaultValue);
        }
        // no default value is available
        return null;
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getFormatterConfiguration(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
     */
    public CmsFormatterConfiguration getFormatterConfiguration(CmsObject cms, CmsResource resource) {

        if (m_formatterConfiguration == null) {
            m_formatterConfiguration = CmsFormatterConfiguration.create(cms, m_formatters);
        }
        return m_formatterConfiguration;
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getJSHeadIncludes()
     */
    public Set<String> getJSHeadIncludes() {

        return Collections.<String>unmodifiableSet(m_jsHeadIncludes);
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getJSHeadIncludes(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
     */
    @SuppressWarnings("unused")
    public Set<String> getJSHeadIncludes(CmsObject cms, CmsResource resource) throws CmsException {

        return getJSHeadIncludes();
    }

    /**
     * Returns the all mappings defined for the given element xpath.<p>
     * 
     * @since 7.0.2
     * 
     * @param elementName the element xpath to look up the mapping for
     * 
     * @return the mapping defined for the given element xpath
     */
    public List<String> getMappings(String elementName) {

        return m_elementMappings.get(elementName);
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getMessages(java.util.Locale)
     */
    public CmsMessages getMessages(Locale locale) {

        CmsMessages result = null;
        if ((m_messageBundleNames != null) && !m_messageBundleNames.isEmpty()) {
            // a message bundle was initialized
            if (m_messageBundleNames.size() == 1) {
                // single message bundle
                result = new CmsMessages(m_messageBundleNames.get(0), locale);
            } else {
                // multiple message bundle
                CmsMultiMessages multiMessages = new CmsMultiMessages(locale);
                for (String messageBundleName : m_messageBundleNames) {
                    multiMessages.addMessages(new CmsMessages(messageBundleName, locale));
                }
                result = multiMessages;
            }
        }
        return result;
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getModelFolder()
     */
    public String getModelFolder() {

        return m_modelFolder;
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getPreview(org.opencms.file.CmsObject, org.opencms.xml.content.CmsXmlContent, java.lang.String)
     */
    public String getPreview(CmsObject cms, CmsXmlContent content, String resourcename) {

        CmsMacroResolver resolver = CmsMacroResolver.newInstance().setCmsObject(cms);
        resolver.addMacro(MACRO_PREVIEW_TEMPFILE, resourcename);

        return resolver.resolveMacros(m_previewLocation);
    }

    /**
     * @see I_CmsXmlContentHandler#getRelationType(I_CmsXmlContentValue)
     */
    @Deprecated
    public CmsRelationType getRelationType(I_CmsXmlContentValue value) {

        if (value == null) {
            return CmsRelationType.XML_WEAK;
        }
        return getRelationType(value.getPath());
    }

    /**
     * @see I_CmsXmlContentHandler#getRelationType(String)
     */
    public CmsRelationType getRelationType(String xpath) {

        if (xpath == null) {
            return CmsRelationType.XML_WEAK;
        }
        CmsRelationType relationType = null;
        // look up the default from the configured mappings
        relationType = m_relations.get(xpath);
        if (relationType == null) {
            // no value found, try default xpath
            String path = CmsXmlUtils.removeXpathIndex(xpath);
            // look up the default value again without indexes
            relationType = m_relations.get(path);
        }
        if (relationType == null) {
            // no value found, try the last simple type path
            String path = CmsXmlUtils.getLastXpathElement(xpath);
            // look up the default value again for the last simple type
            relationType = m_relations.get(path);
        }
        if (relationType == null) {
            return CmsRelationType.XML_WEAK;
        }
        return relationType;
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getSettings(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
     */
    public Map<String, CmsXmlContentProperty> getSettings(CmsObject cms, CmsResource resource) {

        return Collections.unmodifiableMap(m_settings);
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getTabs()
     */
    public List<CmsXmlContentTab> getTabs() {

        return Collections.unmodifiableList(m_tabs);
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getTitleMapping(org.opencms.file.CmsObject, org.opencms.xml.content.CmsXmlContent, java.util.Locale)
     */
    public String getTitleMapping(CmsObject cms, CmsXmlContent document, Locale locale) {

        String result = null;
        if (m_titleMappings.size() > 0) {
            // a title mapping is available
            String xpath = m_titleMappings.get(0);
            // currently just use the first mapping found, unsure if multiple "Title" mappings would make sense anyway
            result = document.getStringValue(cms, xpath, locale);
        }
        return result;
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#getWidget(org.opencms.xml.types.I_CmsXmlContentValue)
     */
    public I_CmsWidget getWidget(I_CmsXmlContentValue value) {

        // try the specific widget settings first
        I_CmsWidget result = m_elementWidgets.get(value.getName());
        if (result == null) {
            // use default widget mappings
            result = OpenCms.getXmlContentTypeManager().getWidgetDefault(value.getTypeName());
        } else {
            result = result.newInstance();
        }
        // set the configuration value for this widget
        String configuration = getConfiguration(value);
        if (configuration == null) {
            // no individual configuration defined, try to get global default configuration
            configuration = OpenCms.getXmlContentTypeManager().getWidgetDefaultConfiguration(result);
        }
        result.setConfiguration(configuration);

        return result;
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#initialize(org.dom4j.Element, org.opencms.xml.CmsXmlContentDefinition)
     */
    public synchronized void initialize(Element appInfoElement, CmsXmlContentDefinition contentDefinition)
            throws CmsXmlException {

        if (appInfoElement != null) {
            // validate the appinfo element XML content with the default appinfo handler schema
            validateAppinfoElement(appInfoElement);

            // re-initialize the local variables
            init();

            Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(appInfoElement);
            while (i.hasNext()) {
                // iterate all elements in the appinfo node
                Element element = i.next();
                String nodeName = element.getName();
                if (nodeName.equals(APPINFO_MAPPINGS)) {
                    initMappings(element, contentDefinition);
                } else if (nodeName.equals(APPINFO_LAYOUTS)) {
                    initLayouts(element, contentDefinition);
                } else if (nodeName.equals(APPINFO_VALIDATIONRULES)) {
                    initValidationRules(element, contentDefinition);
                } else if (nodeName.equals(APPINFO_RELATIONS)) {
                    initRelations(element, contentDefinition);
                } else if (nodeName.equals(APPINFO_DEFAULTS)) {
                    initDefaultValues(element, contentDefinition);
                } else if (nodeName.equals(APPINFO_MODELFOLDER)) {
                    initModelFolder(element, contentDefinition);
                } else if (nodeName.equals(APPINFO_PREVIEW)) {
                    initPreview(element, contentDefinition);
                } else if (nodeName.equals(APPINFO_RESOURCEBUNDLE)) {
                    initResourceBundle(element, contentDefinition, true);
                } else if (nodeName.equals(APPINFO_RESOURCEBUNDLES)) {
                    initResourceBundle(element, contentDefinition, false);
                } else if (nodeName.equals(APPINFO_SEARCHSETTINGS)) {
                    initSearchSettings(element, contentDefinition);
                } else if (nodeName.equals(APPINFO_TABS)) {
                    initTabs(element, contentDefinition);
                } else if (nodeName.equals(APPINFO_FORMATTERS)) {
                    initFormatters(element, contentDefinition);
                } else if (nodeName.equals(APPINFO_HEAD_INCLUDES)) {
                    initHeadIncludes(element, contentDefinition);
                } else if (nodeName.equals(APPINFO_SETTINGS)) {
                    initSettings(element, contentDefinition);
                }
            }
        }

        // at the end, add default check rules for optional file references
        addDefaultCheckRules(contentDefinition, null, null);
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#invalidateBrokenLinks(CmsObject, CmsXmlContent)
     */
    public void invalidateBrokenLinks(CmsObject cms, CmsXmlContent document) {

        if ((cms == null)
                || (cms.getRequestContext().getRequestTime() == CmsResource.DATE_RELEASED_EXPIRED_IGNORE)) {
            // do not check if the request comes the editor
            return;
        }
        boolean needReinitialization = false;
        // iterate the locales
        Iterator<Locale> itLocales = document.getLocales().iterator();
        while (itLocales.hasNext()) {
            Locale locale = itLocales.next();
            List<String> removedNodes = new ArrayList<String>();
            // iterate the values
            Iterator<I_CmsXmlContentValue> itValues = document.getValues(locale).iterator();
            while (itValues.hasNext()) {
                I_CmsXmlContentValue value = itValues.next();
                String path = value.getPath();
                // check if this value has already been deleted by parent rules 
                boolean alreadyRemoved = false;
                Iterator<String> itRemNodes = removedNodes.iterator();
                while (itRemNodes.hasNext()) {
                    String remNode = itRemNodes.next();
                    if (path.startsWith(remNode)) {
                        alreadyRemoved = true;
                        break;
                    }
                }
                // only continue if not already removed and if a rule match
                if (alreadyRemoved || ((m_relationChecks.get(path) == null)
                        && (m_relationChecks.get(CmsXmlUtils.removeXpath(path)) == null))) {
                    continue;
                }

                // check rule matched
                if (LOG.isDebugEnabled()) {
                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_XMLCONTENT_CHECK_RULE_MATCH_1, path));
                }
                if (validateLink(cms, value, null)) {
                    // invalid link
                    if (LOG.isDebugEnabled()) {
                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_XMLCONTENT_CHECK_WARNING_2, path,
                                value.getStringValue(cms)));
                    }
                    // find the node to remove
                    String parentPath = path;
                    while (isInvalidateParent(parentPath)) {
                        // check parent
                        parentPath = CmsXmlUtils.removeLastXpathElement(parentPath);
                        // log info
                        if (LOG.isDebugEnabled()) {
                            LOG.debug(Messages.get().getBundle().key(Messages.LOG_XMLCONTENT_CHECK_PARENT_2, path,
                                    parentPath));
                        }
                    }
                    value = document.getValue(parentPath, locale);
                    // detach the value node from the XML document
                    value.getElement().detach();
                    // mark node as deleted
                    removedNodes.add(parentPath);
                }
            }
            if (!removedNodes.isEmpty()) {
                needReinitialization = true;
            }
        }
        if (needReinitialization) {
            // re-initialize the XML content 
            document.initDocument();
        }
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#isSearchable(org.opencms.xml.types.I_CmsXmlContentValue)
     */
    public boolean isSearchable(I_CmsXmlContentValue value) {

        // check for name configured in the annotations
        Boolean anno = m_searchSettings.get(value.getName());
        // if no annotation has been found, use default for value
        return (anno == null) ? value.isSearchable() : anno.booleanValue();
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#prepareForUse(org.opencms.file.CmsObject, org.opencms.xml.content.CmsXmlContent)
     */
    public CmsXmlContent prepareForUse(CmsObject cms, CmsXmlContent content) {

        // NOOP, just return the unmodified content
        return content;
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#prepareForWrite(org.opencms.file.CmsObject, org.opencms.xml.content.CmsXmlContent, org.opencms.file.CmsFile)
     */
    public CmsFile prepareForWrite(CmsObject cms, CmsXmlContent content, CmsFile file) throws CmsException {

        if (!content.isAutoCorrectionEnabled()) {
            // check if the XML should be corrected automatically (if not already set)
            Object attribute = cms.getRequestContext().getAttribute(CmsXmlContent.AUTO_CORRECTION_ATTRIBUTE);
            // set the auto correction mode as required
            boolean autoCorrectionEnabled = (attribute != null) && ((Boolean) attribute).booleanValue();
            content.setAutoCorrectionEnabled(autoCorrectionEnabled);
        }
        // validate the XML structure before writing the file if required                 
        if (!content.isAutoCorrectionEnabled()) {
            // an exception will be thrown if the structure is invalid
            content.validateXmlStructure(new CmsXmlEntityResolver(cms));
        }
        // read the content-conversion property
        String contentConversion = CmsHtmlConverter.getConversionSettings(cms, file);
        if (CmsStringUtil.isEmptyOrWhitespaceOnly(contentConversion)) {
            // enable pretty printing and XHTML conversion of XML content html fields by default
            contentConversion = CmsHtmlConverter.PARAM_XHTML;
        }
        content.setConversion(contentConversion);
        // correct the HTML structure
        file = content.correctXmlStructure(cms);
        content.setFile(file);
        // resolve the file mappings
        content.resolveMappings(cms);
        // ensure all property or permission mappings of deleted optional values are removed
        removeEmptyMappings(cms, content);
        // write categories (if there is a category widget present)
        file = writeCategories(cms, file, content);
        // return the result
        return file;
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#resolveMapping(org.opencms.file.CmsObject, org.opencms.xml.content.CmsXmlContent, org.opencms.xml.types.I_CmsXmlContentValue)
     */
    public void resolveMapping(CmsObject cms, CmsXmlContent content, I_CmsXmlContentValue value)
            throws CmsException {

        if (!value.isSimpleType()) {
            // no mappings for a nested schema are possible
            // note that the sub-elements of the nested schema ARE mapped by the node visitor,
            // it's just the nested schema value itself that does not support mapping
            return;
        }

        // get the original VFS file from the content
        CmsFile file = content.getFile();
        if (file == null) {
            throw new CmsXmlException(Messages.get().container(Messages.ERR_XMLCONTENT_RESOLVE_FILE_NOT_FOUND_0));
        }

        // get the mappings for the element name        
        List<String> mappings = getMappings(value.getPath());
        if (mappings == null) {
            // nothing to do if we have no mappings at all
            return;
        }
        // create OpenCms user context initialized with "/" as site root to read all siblings
        CmsObject rootCms = OpenCms.initCmsObject(cms);
        Object logEntry = cms.getRequestContext().getAttribute(CmsLogEntry.ATTR_LOG_ENTRY);
        if (logEntry != null) {
            rootCms.getRequestContext().setAttribute(CmsLogEntry.ATTR_LOG_ENTRY, logEntry);
        }
        rootCms.getRequestContext().setSiteRoot("/");
        // read all siblings of the file
        List<CmsResource> siblings = rootCms.readSiblings(content.getFile().getRootPath(),
                CmsResourceFilter.IGNORE_EXPIRATION);

        Set<CmsResource> urlNameMappingResources = new HashSet<CmsResource>();
        boolean mapToUrlName = false;
        urlNameMappingResources.add(content.getFile());
        // since 7.0.2 multiple mappings are possible
        for (String mapping : mappings) {

            // for multiple language mappings, we need to ensure 
            // a) all siblings are handled
            // b) only the "right" locale is mapped to a sibling
            if (CmsStringUtil.isNotEmpty(mapping)) {
                for (int i = (siblings.size() - 1); i >= 0; i--) {
                    // get filename
                    String filename = (siblings.get(i)).getRootPath();
                    Locale locale = OpenCms.getLocaleManager().getDefaultLocale(rootCms, filename);
                    if (mapping.startsWith(MAPTO_URLNAME)) {
                        // should be written regardless of whether there is a sibling with the correct locale 
                        mapToUrlName = true;
                    }
                    if (!locale.equals(value.getLocale())) {
                        // only map property if the locale fits
                        continue;
                    }

                    // make sure the file is locked
                    CmsLock lock = rootCms.getLock(filename);
                    if (lock.isUnlocked()) {
                        rootCms.lockResource(filename);
                    } else if (!lock.isDirectlyOwnedInProjectBy(rootCms)) {
                        rootCms.changeLock(filename);
                    }

                    // get the string value of the current node
                    String stringValue = value.getStringValue(rootCms);
                    if (mapping.startsWith(MAPTO_PERMISSION) && (value.getIndex() == 0)) {

                        // map value to a permission
                        // example of a mapping: mapto="permission:GROUP:+r+v|GROUP.ALL_OTHERS:|GROUP.Projectmanagers:+r+v+w+c"

                        // get permission(s) to set
                        String permissionMappings = mapping.substring(MAPTO_PERMISSION.length());
                        String mainMapping = permissionMappings;
                        Map<String, String> permissionsToSet = new HashMap<String, String>();

                        // separate permission to set for element value from other permissions to set
                        int sepIndex = permissionMappings.indexOf('|');
                        if (sepIndex != -1) {
                            mainMapping = permissionMappings.substring(0, sepIndex);
                            permissionMappings = permissionMappings.substring(sepIndex + 1);
                            permissionsToSet = CmsStringUtil.splitAsMap(permissionMappings, "|", ":");
                        }

                        // determine principal type and permission string to set
                        String principalType = I_CmsPrincipal.PRINCIPAL_GROUP;
                        String permissionString = mainMapping;
                        sepIndex = mainMapping.indexOf(':');
                        if (sepIndex != -1) {
                            principalType = mainMapping.substring(0, sepIndex);
                            permissionString = mainMapping.substring(sepIndex + 1);
                        }
                        if (permissionString.toLowerCase().indexOf('o') == -1) {
                            permissionString += "+o";
                        }

                        // remove all existing permissions from the file
                        List<CmsAccessControlEntry> aces = rootCms.getAccessControlEntries(filename, false);
                        for (Iterator<CmsAccessControlEntry> j = aces.iterator(); j.hasNext();) {
                            CmsAccessControlEntry ace = j.next();
                            if (ace.getPrincipal().equals(CmsAccessControlEntry.PRINCIPAL_ALL_OTHERS_ID)) {
                                // remove the entry "All others", which has to be treated in a special way
                                rootCms.rmacc(filename, CmsAccessControlEntry.PRINCIPAL_ALL_OTHERS_NAME,
                                        CmsAccessControlEntry.PRINCIPAL_ALL_OTHERS_ID.toString());
                            } else {
                                // this is a group or user principal
                                I_CmsPrincipal principal = CmsPrincipal.readPrincipal(rootCms, ace.getPrincipal());
                                if (principal.isGroup()) {
                                    rootCms.rmacc(filename, I_CmsPrincipal.PRINCIPAL_GROUP, principal.getName());
                                } else if (principal.isUser()) {
                                    rootCms.rmacc(filename, I_CmsPrincipal.PRINCIPAL_USER, principal.getName());
                                }
                            }
                        }

                        // set additional permissions that are defined in mapping
                        for (Iterator<Map.Entry<String, String>> j = permissionsToSet.entrySet().iterator(); j
                                .hasNext();) {
                            Map.Entry<String, String> entry = j.next();
                            sepIndex = entry.getKey().indexOf('.');
                            if (sepIndex != -1) {
                                String type = entry.getKey().substring(0, sepIndex);
                                String name = entry.getKey().substring(sepIndex + 1);
                                String permissions = entry.getValue();
                                if (permissions.toLowerCase().indexOf('o') == -1) {
                                    permissions += "+o";
                                }
                                try {
                                    rootCms.chacc(filename, type, name, permissions);
                                } catch (CmsException e) {
                                    // setting permission did not work
                                    LOG.error(e);
                                }
                            }
                        }

                        // set permission(s) using the element value(s)
                        // the set with all selected principals
                        TreeSet<String> allPrincipals = new TreeSet<String>();
                        String path = CmsXmlUtils.removeXpathIndex(value.getPath());
                        List<I_CmsXmlContentValue> values = content.getValues(path, locale);
                        Iterator<I_CmsXmlContentValue> j = values.iterator();
                        while (j.hasNext()) {
                            I_CmsXmlContentValue val = j.next();
                            String principalName = val.getStringValue(rootCms);
                            // the prinicipal name can be a principal list
                            List<String> principalNames = CmsStringUtil.splitAsList(principalName,
                                    PRINCIPAL_LIST_SEPARATOR);
                            // iterate over the principals
                            Iterator<String> iterPrincipals = principalNames.iterator();
                            while (iterPrincipals.hasNext()) {
                                // get the next principal
                                String principal = iterPrincipals.next();
                                allPrincipals.add(principal);
                            }
                        }
                        // iterate over the set with all principals and set the permissions
                        Iterator<String> iterAllPricinipals = allPrincipals.iterator();
                        while (iterAllPricinipals.hasNext()) {
                            // get the next principal
                            String principal = iterAllPricinipals.next();
                            rootCms.chacc(filename, principalType, principal, permissionString);
                        }
                        // special case: permissions are written only to one sibling, end loop
                        i = 0;
                    } else if (mapping.startsWith(MAPTO_PROPERTY_LIST) && (value.getIndex() == 0)) {

                        boolean mapToShared;
                        int prefixLength;
                        // check which mapping is used (shared or individual)
                        if (mapping.startsWith(MAPTO_PROPERTY_LIST_SHARED)) {
                            mapToShared = true;
                            prefixLength = MAPTO_PROPERTY_LIST_SHARED.length();
                        } else if (mapping.startsWith(MAPTO_PROPERTY_LIST_INDIVIDUAL)) {
                            mapToShared = false;
                            prefixLength = MAPTO_PROPERTY_LIST_INDIVIDUAL.length();
                        } else {
                            mapToShared = false;
                            prefixLength = MAPTO_PROPERTY_LIST.length();
                        }

                        // this is a property list mapping
                        String property = mapping.substring(prefixLength);

                        String path = CmsXmlUtils.removeXpathIndex(value.getPath());
                        List<I_CmsXmlContentValue> values = content.getValues(path, locale);
                        Iterator<I_CmsXmlContentValue> j = values.iterator();
                        StringBuffer result = new StringBuffer(values.size() * 64);
                        while (j.hasNext()) {
                            I_CmsXmlContentValue val = j.next();
                            result.append(val.getStringValue(rootCms));
                            if (j.hasNext()) {
                                result.append(CmsProperty.VALUE_LIST_DELIMITER);
                            }
                        }

                        CmsProperty p;
                        if (mapToShared) {
                            // map to shared value
                            p = new CmsProperty(property, null, result.toString());
                        } else {
                            // map to individual value
                            p = new CmsProperty(property, result.toString(), null);
                        }
                        // write the created list string value in the selected property
                        rootCms.writePropertyObject(filename, p);
                        if (mapToShared) {
                            // special case: shared mappings must be written only to one sibling, end loop
                            i = 0;
                        }

                    } else if (mapping.startsWith(MAPTO_PROPERTY)) {

                        boolean mapToShared;
                        int prefixLength;
                        // check which mapping is used (shared or individual)                        
                        if (mapping.startsWith(MAPTO_PROPERTY_SHARED)) {
                            mapToShared = true;
                            prefixLength = MAPTO_PROPERTY_SHARED.length();
                        } else if (mapping.startsWith(MAPTO_PROPERTY_INDIVIDUAL)) {
                            mapToShared = false;
                            prefixLength = MAPTO_PROPERTY_INDIVIDUAL.length();
                        } else {
                            mapToShared = false;
                            prefixLength = MAPTO_PROPERTY.length();
                        }

                        // this is a property mapping
                        String property = mapping.substring(prefixLength);

                        CmsProperty p;
                        if (mapToShared) {
                            // map to shared value
                            p = new CmsProperty(property, null, stringValue);
                        } else {
                            // map to individual value
                            p = new CmsProperty(property, stringValue, null);
                        }
                        // just store the string value in the selected property
                        rootCms.writePropertyObject(filename, p);
                        if (mapToShared) {
                            // special case: shared mappings must be written only to one sibling, end loop
                            i = 0;
                        }
                    } else if (mapping.startsWith(MAPTO_URLNAME)) {
                        // we write the actual mappings later 
                        urlNameMappingResources.add(siblings.get(i));
                    } else if (mapping.startsWith(MAPTO_ATTRIBUTE)) {

                        // this is an attribute mapping                        
                        String attribute = mapping.substring(MAPTO_ATTRIBUTE.length());
                        switch (ATTRIBUTES.indexOf(attribute)) {
                        case 0: // date released
                            long date = 0;
                            try {
                                date = Long.valueOf(stringValue).longValue();
                            } catch (NumberFormatException e) {
                                // ignore, value can be a macro
                            }
                            if (date == 0) {
                                date = CmsResource.DATE_RELEASED_DEFAULT;
                            }
                            // set the sibling release date
                            rootCms.setDateReleased(filename, date, false);
                            // set current file release date
                            if (filename.equals(rootCms.getSitePath(file))) {
                                file.setDateReleased(date);
                            }
                            break;
                        case 1: // date expired
                            date = 0;
                            try {
                                date = Long.valueOf(stringValue).longValue();
                            } catch (NumberFormatException e) {
                                // ignore, value can be a macro
                            }
                            if (date == 0) {
                                date = CmsResource.DATE_EXPIRED_DEFAULT;
                            }
                            // set the sibling expired date
                            rootCms.setDateExpired(filename, date, false);
                            // set current file expired date
                            if (filename.equals(rootCms.getSitePath(file))) {
                                file.setDateExpired(date);
                            }
                            break;
                        default:
                            // ignore invalid / other mappings                                
                        }
                    }
                }
            }
        }
        if (mapToUrlName) {
            // now actually write the URL name mappings 
            for (CmsResource resourceForUrlNameMapping : urlNameMappingResources) {
                if (!CmsResource.isTemporaryFileName(resourceForUrlNameMapping.getRootPath())) {
                    I_CmsFileNameGenerator nameGen = OpenCms.getResourceManager().getNameGenerator();
                    Iterator<String> nameSeq = nameGen.getUrlNameSequence(value.getStringValue(cms));
                    cms.writeUrlNameMapping(nameSeq, resourceForUrlNameMapping.getStructureId(),
                            value.getLocale().toString());
                }
            }
        }

        // make sure the original is locked
        CmsLock lock = rootCms.getLock(file);
        if (lock.isUnlocked()) {
            rootCms.lockResource(file.getRootPath());
        } else if (!lock.isExclusiveOwnedBy(rootCms.getRequestContext().getCurrentUser())) {
            rootCms.changeLock(file.getRootPath());
        }
    }

    /**
     * @see org.opencms.xml.content.I_CmsXmlContentHandler#resolveValidation(org.opencms.file.CmsObject, org.opencms.xml.types.I_CmsXmlContentValue, org.opencms.xml.content.CmsXmlContentErrorHandler)
     */
    public CmsXmlContentErrorHandler resolveValidation(CmsObject cms, I_CmsXmlContentValue value,
            CmsXmlContentErrorHandler errorHandler) {

        if (errorHandler == null) {
            // init a new error handler if required
            errorHandler = new CmsXmlContentErrorHandler();
        }

        if (!value.isSimpleType()) {
            // no validation for a nested schema is possible
            // note that the sub-elements of the nested schema ARE validated by the node visitor,
            // it's just the nested schema value itself that does not support validation
            return errorHandler;
        }

        // validate the error rules
        errorHandler = validateValue(cms, value, errorHandler, m_validationErrorRules, false);
        // validate the warning rules
        errorHandler = validateValue(cms, value, errorHandler, m_validationWarningRules, true);
        // validate categories
        errorHandler = validateCategories(cms, value, errorHandler);
        // return the result
        return errorHandler;
    }

    /**
     * Adds a check rule for a specified element.<p> 
     * 
     * @param contentDefinition the XML content definition this XML content handler belongs to
     * @param elementName the element name to add the rule to 
     * @param invalidate <code>false</code>, to disable link check /
     *                   <code>true</code> or <code>node</code>, to invalidate just the single node if the link is broken /
     *                   <code>parent</code>, if this rule will invalidate the whole parent node in nested content
     * @param type the relation type
     * 
     * @throws CmsXmlException in case an unknown element name is used
     */
    protected void addCheckRule(CmsXmlContentDefinition contentDefinition, String elementName, String invalidate,
            String type) throws CmsXmlException {

        I_CmsXmlSchemaType schemaType = contentDefinition.getSchemaType(elementName);
        if (schemaType == null) {
            // no element with the given name
            throw new CmsXmlException(
                    Messages.get().container(Messages.ERR_XMLCONTENT_CHECK_INVALID_ELEM_1, elementName));
        }
        if (!CmsXmlVfsFileValue.TYPE_NAME.equals(schemaType.getTypeName())
                && !CmsXmlVarLinkValue.TYPE_NAME.equals(schemaType.getTypeName())) {
            // element is not a OpenCmsVfsFile
            throw new CmsXmlException(
                    Messages.get().container(Messages.ERR_XMLCONTENT_CHECK_INVALID_TYPE_1, elementName));
        }

        // cache the check rule data
        Boolean invalidateParent = null;
        if ((invalidate == null) || invalidate.equalsIgnoreCase(Boolean.TRUE.toString())
                || invalidate.equalsIgnoreCase(APPINFO_ATTR_TYPE_NODE)) {
            invalidateParent = Boolean.FALSE;
        } else if (invalidate.equalsIgnoreCase(APPINFO_ATTR_TYPE_PARENT)) {
            invalidateParent = Boolean.TRUE;
        }
        if (invalidateParent != null) {
            m_relationChecks.put(elementName, invalidateParent);
        }
        CmsRelationType relationType = (type == null ? CmsRelationType.XML_WEAK : CmsRelationType.valueOfXml(type));
        m_relations.put(elementName, relationType);

        if (invalidateParent != null) {
            // check the whole xpath hierarchy
            String path = elementName;
            while (CmsStringUtil.isNotEmptyOrWhitespaceOnly(path)) {
                if (!isInvalidateParent(path)) {
                    // if invalidate type = node, then the node needs to be optional
                    if (contentDefinition.getSchemaType(path).getMinOccurs() > 0) {
                        // element is not optional
                        throw new CmsXmlException(
                                Messages.get().container(Messages.ERR_XMLCONTENT_CHECK_NOT_OPTIONAL_1, path));
                    }
                    // no need to further check
                    break;
                } else if (!CmsXmlUtils.isDeepXpath(path)) {
                    // if invalidate type = parent, then the node needs to be nested
                    // document root can not be invalidated
                    throw new CmsXmlException(
                            Messages.get().container(Messages.ERR_XMLCONTENT_CHECK_NOT_EMPTY_DOC_0));
                }
                path = CmsXmlUtils.removeLastXpathElement(path);
            }
        }
    }

    /**
     * Adds a configuration value for an element widget.<p>
     * 
     * @param contentDefinition the XML content definition this XML content handler belongs to
     * @param elementName the element name to map
     * @param configurationValue the configuration value to use
     * 
     * @throws CmsXmlException in case an unknown element name is used
     */
    protected void addConfiguration(CmsXmlContentDefinition contentDefinition, String elementName,
            String configurationValue) throws CmsXmlException {

        if (contentDefinition.getSchemaType(elementName) == null) {
            throw new CmsXmlException(
                    Messages.get().container(Messages.ERR_XMLCONTENT_CONFIG_ELEM_UNKNOWN_1, elementName));
        }

        m_configurationValues.put(elementName, configurationValue);
    }

    /**
     * Adds a default value for an element.<p>
     * 
     * @param contentDefinition the XML content definition this XML content handler belongs to
     * @param elementName the element name to map
     * @param defaultValue the default value to use
     * 
     * @throws CmsXmlException in case an unknown element name is used
     */
    protected void addDefault(CmsXmlContentDefinition contentDefinition, String elementName, String defaultValue)
            throws CmsXmlException {

        if (contentDefinition.getSchemaType(elementName) == null) {
            throw new CmsXmlException(org.opencms.xml.types.Messages.get()
                    .container(Messages.ERR_XMLCONTENT_INVALID_ELEM_DEFAULT_1, elementName));
        }
        // store mappings as xpath to allow better control about what is mapped
        String xpath = CmsXmlUtils.createXpath(elementName, 1);
        m_defaultValues.put(xpath, defaultValue);
    }

    /**
     * Adds all needed default check rules recursively for the given schema type.<p> 
     * 
     * @param rootContentDefinition the root content definition
     * @param schemaType the schema type to check
     * @param elementPath the current element path
     * 
     * @throws CmsXmlException if something goes wrong
     */
    protected void addDefaultCheckRules(CmsXmlContentDefinition rootContentDefinition,
            I_CmsXmlSchemaType schemaType, String elementPath) throws CmsXmlException {

        if ((schemaType != null) && schemaType.isSimpleType()) {
            if ((schemaType.getMinOccurs() == 0)
                    && (CmsXmlVfsFileValue.TYPE_NAME.equals(schemaType.getTypeName())
                            || CmsXmlVarLinkValue.TYPE_NAME.equals(schemaType.getTypeName()))
                    && !m_relationChecks.containsKey(elementPath) && !m_relations.containsKey(elementPath)) {
                // add default check rule for the element
                addCheckRule(rootContentDefinition, elementPath, null, null);
            }
        } else {
            // recursion required
            CmsXmlContentDefinition nestedContentDefinition = rootContentDefinition;
            if (schemaType != null) {
                CmsXmlNestedContentDefinition nestedDefinition = (CmsXmlNestedContentDefinition) schemaType;
                nestedContentDefinition = nestedDefinition.getNestedContentDefinition();
            }
            Iterator<String> itElems = nestedContentDefinition.getSchemaTypes().iterator();
            while (itElems.hasNext()) {
                String element = itElems.next();
                String path = (schemaType != null) ? CmsXmlUtils.concatXpath(elementPath, element) : element;
                I_CmsXmlSchemaType nestedSchema = nestedContentDefinition.getSchemaType(element);
                if ((schemaType == null) || !nestedSchema.equals(schemaType)) {
                    addDefaultCheckRules(rootContentDefinition, nestedSchema, path);
                }
            }
        }
    }

    /**
     * Adds an element mapping.<p>
     * 
     * @param contentDefinition the XML content definition this XML content handler belongs to
     * @param elementName the element name to map
     * @param mapping the mapping to use
     * 
     * @throws CmsXmlException in case an unknown element name is used
     */
    protected void addMapping(CmsXmlContentDefinition contentDefinition, String elementName, String mapping)
            throws CmsXmlException {

        if (contentDefinition.getSchemaType(elementName) == null) {
            throw new CmsXmlException(
                    Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ELEM_MAPPING_1, elementName));
        }

        // store mappings as xpath to allow better control about what is mapped
        String xpath = CmsXmlUtils.createXpath(elementName, 1);
        // since 7.0.2 multiple mappings are possible, so the mappings are stored in an array
        List<String> values = m_elementMappings.get(xpath);
        if (values == null) {
            // there should not really be THAT much multiple mappings per value...
            values = new ArrayList<String>(4);
            m_elementMappings.put(xpath, values);
        }
        values.add(mapping);
        if (mapping.startsWith(MAPTO_PROPERTY) && mapping.endsWith(":" + CmsPropertyDefinition.PROPERTY_TITLE)) {
            // this is a title mapping
            m_titleMappings.add(xpath);
        }
    }

    /**
     * Adds a search setting for an element.<p>
     * 
     * @param contentDefinition the XML content definition this XML content handler belongs to
     * @param elementName the element name to map
     * @param value the search setting value to store
     * 
     * @throws CmsXmlException in case an unknown element name is used
     */
    protected void addSearchSetting(CmsXmlContentDefinition contentDefinition, String elementName, Boolean value)
            throws CmsXmlException {

        if (contentDefinition.getSchemaType(elementName) == null) {
            throw new CmsXmlException(org.opencms.xml.types.Messages.get()
                    .container(Messages.ERR_XMLCONTENT_INVALID_ELEM_SEARCHSETTINGS_1, elementName));
        }
        // store the search exclusion as defined
        m_searchSettings.put(elementName, value);
    }

    /**
     * Adds a validation rule for a specified element.<p> 
     * 
     * @param contentDefinition the XML content definition this XML content handler belongs to
     * @param elementName the element name to add the rule to 
     * @param regex the validation rule regular expression
     * @param message the message in case validation fails (may be null)
     * @param isWarning if true, this rule is used for warnings, otherwise it's an error
     * 
     * @throws CmsXmlException in case an unknown element name is used
     */
    protected void addValidationRule(CmsXmlContentDefinition contentDefinition, String elementName, String regex,
            String message, boolean isWarning) throws CmsXmlException {

        if (contentDefinition.getSchemaType(elementName) == null) {
            throw new CmsXmlException(
                    Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ELEM_VALIDATION_1, elementName));
        }

        if (isWarning) {
            m_validationWarningRules.put(elementName, regex);
            if (message != null) {
                m_validationWarningMessages.put(elementName, message);
            }
        } else {
            m_validationErrorRules.put(elementName, regex);
            if (message != null) {
                m_validationErrorMessages.put(elementName, message);
            }
        }
    }

    /**
     * Adds a GUI widget for a specified element.<p> 
     * 
     * @param contentDefinition the XML content definition this XML content handler belongs to
     * @param elementName the element name to map
     * @param widgetClassOrAlias the widget to use as GUI for the element (registered alias or class name)
     * 
     * @throws CmsXmlException in case an unknown element name is used
     */
    protected void addWidget(CmsXmlContentDefinition contentDefinition, String elementName,
            String widgetClassOrAlias) throws CmsXmlException {

        if (contentDefinition.getSchemaType(elementName) == null) {
            throw new CmsXmlException(
                    Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ELEM_LAYOUTWIDGET_1, elementName));
        }

        // get the base widget from the XML content type manager
        I_CmsWidget widget = OpenCms.getXmlContentTypeManager().getWidget(widgetClassOrAlias);

        if (widget == null) {
            // no registered widget class found
            if (CmsStringUtil.isValidJavaClassName(widgetClassOrAlias)) {
                // java class name given, try to create new instance of the class and cast to widget
                try {
                    Class<?> specialWidgetClass = Class.forName(widgetClassOrAlias);
                    widget = (I_CmsWidget) specialWidgetClass.newInstance();
                } catch (Exception e) {
                    throw new CmsXmlException(
                            Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_CUSTOM_CLASS_3,
                                    widgetClassOrAlias, elementName, contentDefinition.getSchemaLocation()),
                            e);
                }
            }
            if (widget == null) {
                // no valid widget found
                throw new CmsXmlException(Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_WIDGET_3,
                        widgetClassOrAlias, elementName, contentDefinition.getSchemaLocation()));
            }
        }
        m_elementWidgets.put(elementName, widget);
    }

    /**
     * Returns the default locale in the content of the given resource.<p>
     * 
     * @param cms the cms context
     * @param resource the resource path to get the default locale for
     * 
     * @return the default locale of the resource
     */
    protected Locale getLocaleForResource(CmsObject cms, String resource) {

        Locale locale = OpenCms.getLocaleManager().getDefaultLocale(cms, resource);
        if (locale == null) {
            List<Locale> locales = OpenCms.getLocaleManager().getAvailableLocales();
            if (locales.size() > 0) {
                locale = locales.get(0);
            } else {
                locale = Locale.ENGLISH;
            }
        }
        return locale;
    }

    /**
     * Returns the category reference path for the given value.<p>
     * 
     * @param cms the cms context
     * @param value the xml content value
     * 
     * @return the category reference path for the given value
     */
    protected String getReferencePath(CmsObject cms, I_CmsXmlContentValue value) {

        // get the original file instead of the temp file
        CmsFile file = value.getDocument().getFile();
        String resourceName = cms.getSitePath(file);
        if (CmsWorkplace.isTemporaryFile(file)) {
            StringBuffer result = new StringBuffer(resourceName.length() + 2);
            result.append(CmsResource.getFolderPath(resourceName));
            result.append(CmsResource.getName(resourceName).substring(1));
            resourceName = result.toString();
        }
        try {
            List<CmsResource> listsib = cms.readSiblings(resourceName, CmsResourceFilter.ALL);
            for (int i = 0; i < listsib.size(); i++) {
                CmsResource resource = listsib.get(i);
                // get the default locale of the resource and set the categories
                Locale locale = getLocaleForResource(cms, cms.getSitePath(resource));
                if (value.getLocale().equals(locale)) {
                    return cms.getSitePath(resource);
                }
            }
        } catch (CmsVfsResourceNotFoundException e) {
            // may hapen if editing a new resource
            if (LOG.isDebugEnabled()) {
                LOG.debug(e.getLocalizedMessage(), e);
            }
        } catch (CmsException e) {
            if (LOG.isErrorEnabled()) {
                LOG.error(e.getLocalizedMessage(), e);
            }
        }
        // if the locale can not be found, just take the current file
        return cms.getSitePath(file);
    }

    /**
     * Returns the validation message to be displayed if a certain rule was violated.<p> 
     * 
     * @param cms the current users OpenCms context
     * @param value the value to validate
     * @param regex the rule that was violated
     * @param valueStr the string value of the given value
     * @param matchResult if false, the rule was negated
     * @param isWarning if true, this validation indicate a warning, otherwise an error
     * 
     * @return the validation message to be displayed 
     */
    protected String getValidationMessage(CmsObject cms, I_CmsXmlContentValue value, String regex, String valueStr,
            boolean matchResult, boolean isWarning) {

        String message = null;
        if (isWarning) {
            message = m_validationWarningMessages.get(value.getName());
        } else {
            message = m_validationErrorMessages.get(value.getName());
        }

        if (message == null) {
            if (isWarning) {
                message = MESSAGE_VALIDATION_DEFAULT_WARNING;
            } else {
                message = MESSAGE_VALIDATION_DEFAULT_ERROR;
            }
        }

        // create additional macro values
        Map<String, String> additionalValues = new HashMap<String, String>();
        additionalValues.put(CmsMacroResolver.KEY_VALIDATION_VALUE, valueStr);
        additionalValues.put(CmsMacroResolver.KEY_VALIDATION_REGEX, ((!matchResult) ? "!" : "") + regex);
        additionalValues.put(CmsMacroResolver.KEY_VALIDATION_PATH, value.getPath());

        CmsMacroResolver resolver = CmsMacroResolver.newInstance().setCmsObject(cms)
                .setMessages(getMessages(cms.getRequestContext().getLocale()))
                .setAdditionalMacros(additionalValues);

        return resolver.resolveMacros(message);
    }

    /**
     * Called when this content handler is initialized.<p> 
     */
    protected void init() {

        m_elementMappings = new HashMap<String, List<String>>();
        m_elementWidgets = new HashMap<String, I_CmsWidget>();
        m_validationErrorRules = new HashMap<String, String>();
        m_validationErrorMessages = new HashMap<String, String>();
        m_validationWarningRules = new HashMap<String, String>();
        m_validationWarningMessages = new HashMap<String, String>();
        m_defaultValues = new HashMap<String, String>();
        m_configurationValues = new HashMap<String, String>();
        m_searchSettings = new HashMap<String, Boolean>();
        m_relations = new HashMap<String, CmsRelationType>();
        m_relationChecks = new HashMap<String, Boolean>();
        m_previewLocation = null;
        m_modelFolder = null;
        m_tabs = new ArrayList<CmsXmlContentTab>();
        m_cssHeadIncludes = new LinkedHashSet<String>();
        m_jsHeadIncludes = new LinkedHashSet<String>();
        m_settings = new LinkedHashMap<String, CmsXmlContentProperty>();
        m_titleMappings = new ArrayList<String>(2);
        m_formatters = new ArrayList<CmsFormatterBean>();
    }

    /**
     * Initializes the default values for this content handler.<p>
     * 
     * Using the default values from the appinfo node, it's possible to have more 
     * sophisticated logic for generating the defaults then just using the XML schema "default"
     * attribute.<p> 
     * 
     * @param root the "defaults" element from the appinfo node of the XML content definition
     * @param contentDefinition the content definition the default values belong to
     * @throws CmsXmlException if something goes wrong
     */
    protected void initDefaultValues(Element root, CmsXmlContentDefinition contentDefinition)
            throws CmsXmlException {

        Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(root, APPINFO_DEFAULT);
        while (i.hasNext()) {
            // iterate all "default" elements in the "defaults" node
            Element element = i.next();
            String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
            String defaultValue = element.attributeValue(APPINFO_ATTR_VALUE);
            if ((elementName != null) && (defaultValue != null)) {
                // add a default value mapping for the element
                addDefault(contentDefinition, elementName, defaultValue);
            }
        }
    }

    /**
     * Initializes the formatters for this content handler.<p>
     * 
     * @param root the "formatters" element from the appinfo node of the XML content definition
     * @param contentDefinition the content definition the formatters belong to
     */
    protected void initFormatters(Element root, CmsXmlContentDefinition contentDefinition) {

        // reading the include resources common for all formatters 
        Iterator<Element> itFormatter = CmsXmlGenericWrapper.elementIterator(root, APPINFO_FORMATTER);
        while (itFormatter.hasNext()) {
            // iterate all "formatter" elements in the "formatters" node
            Element element = itFormatter.next();
            String type = element.attributeValue(APPINFO_ATTR_TYPE);
            if (CmsStringUtil.isEmptyOrWhitespaceOnly(type)) {
                // if not set use "*" as default for type
                type = CmsFormatterBean.WILDCARD_TYPE;
            }
            String jspRootPath = element.attributeValue(APPINFO_ATTR_URI);
            String minWidthStr = element.attributeValue(APPINFO_ATTR_MINWIDTH);
            String maxWidthStr = element.attributeValue(APPINFO_ATTR_MAXWIDTH);
            String preview = element.attributeValue(APPINFO_ATTR_PREVIEW);
            String searchContent = element.attributeValue(APPINFO_ATTR_SEARCHCONTENT);
            m_formatters.add(new CmsFormatterBean(type, jspRootPath, minWidthStr, maxWidthStr, preview,
                    searchContent, contentDefinition.getSchemaLocation()));
        }
    }

    /**
     * Initializes the head includes for this content handler.<p>
     * 
     * @param root the "headincludes" element from the appinfo node of the XML content definition
     * @param contentDefinition the content definition the head-includes belong to
     */
    protected void initHeadIncludes(Element root, CmsXmlContentDefinition contentDefinition) {

        Iterator<Element> itInclude = CmsXmlGenericWrapper.elementIterator(root, APPINFO_HEAD_INCLUDE);
        while (itInclude.hasNext()) {
            Element element = itInclude.next();
            String type = element.attributeValue(APPINFO_ATTR_TYPE);
            String uri = element.attributeValue(APPINFO_ATTR_URI);
            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(uri)) {
                if (ATTRIBUTE_INCLUDE_TYPE_CSS.equals(type)) {
                    m_cssHeadIncludes.add(uri);
                } else if (ATTRIBUTE_INCLUDE_TYPE_JAVASCRIPT.equals(type)) {
                    m_jsHeadIncludes.add(uri);
                }
            }
        }
    }

    /**
    * Initializes the layout for this content handler.<p>
    * 
    * Unless otherwise instructed, the editor uses one specific GUI widget for each 
    * XML value schema type. For example, for a {@link org.opencms.xml.types.CmsXmlStringValue} 
    * the default widget is the {@link org.opencms.widgets.CmsInputWidget}.
    * However, certain values can also use more then one widget, for example you may 
    * also use a {@link org.opencms.widgets.CmsCheckboxWidget} for a String value,
    * and as a result the Strings possible values would be eithe <code>"false"</code> or <code>"true"</code>,
    * but nevertheless be a String.<p>
    *
    * The widget to use can further be controlled using the <code>widget</code> attribute.
    * You can specify either a valid widget alias such as <code>StringWidget</code>, 
    * or the name of a Java class that implements <code>{@link I_CmsWidget}</code>.<p>
    * 
    * Configuration options to the widget can be passed using the <code>configuration</code>
    * attribute. You can specify any String as configuration. This String is then passed
    * to the widget during initialization. It's up to the individual widget implementation 
    * to interpret this configuration String.<p>
    * 
    * @param root the "layouts" element from the appinfo node of the XML content definition
    * @param contentDefinition the content definition the layout belongs to
    * 
    * @throws CmsXmlException if something goes wrong
    */
    protected void initLayouts(Element root, CmsXmlContentDefinition contentDefinition) throws CmsXmlException {

        Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(root, APPINFO_LAYOUT);
        while (i.hasNext()) {
            // iterate all "layout" elements in the "layouts" node
            Element element = i.next();
            String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
            String widgetClassOrAlias = element.attributeValue(APPINFO_ATTR_WIDGET);
            String configuration = element.attributeValue(APPINFO_ATTR_CONFIGURATION);
            if ((elementName != null) && (widgetClassOrAlias != null)) {
                // add a widget mapping for the element
                addWidget(contentDefinition, elementName, widgetClassOrAlias);
                if (configuration != null) {
                    addConfiguration(contentDefinition, elementName, configuration);
                }
            }
        }
    }

    /**
     * Initializes the element mappings for this content handler.<p>
     * 
     * Element mappings allow storing values from the XML content in other locations.
     * For example, if you have an element called "Title", it's likely a good idea to 
     * store the value of this element also in the "Title" property of a XML content resource.<p>
     * 
     * @param root the "mappings" element from the appinfo node of the XML content definition
     * @param contentDefinition the content definition the mappings belong to
     * @throws CmsXmlException if something goes wrong
     */
    protected void initMappings(Element root, CmsXmlContentDefinition contentDefinition) throws CmsXmlException {

        Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(root, APPINFO_MAPPING);
        while (i.hasNext()) {
            // iterate all "mapping" elements in the "mappings" node
            Element element = i.next();
            // this is a mapping node
            String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
            String maptoName = element.attributeValue(APPINFO_ATTR_MAPTO);
            if ((elementName != null) && (maptoName != null)) {
                // add the element mapping 
                addMapping(contentDefinition, elementName, maptoName);
            }
        }
    }

    /**
     * Initializes the folder containing the model file(s) for this content handler.<p>
     * 
     * @param root the "modelfolder" element from the appinfo node of the XML content definition
     * @param contentDefinition the content definition the model folder belongs to
     * @throws CmsXmlException if something goes wrong
     */
    protected void initModelFolder(Element root, CmsXmlContentDefinition contentDefinition) throws CmsXmlException {

        String master = root.attributeValue(APPINFO_ATTR_URI);
        if (master == null) {
            throw new CmsXmlException(Messages.get().container(Messages.ERR_XMLCONTENT_MISSING_MODELFOLDER_URI_2,
                    root.getName(), contentDefinition.getSchemaLocation()));
        }
        m_modelFolder = master;
    }

    /**
     * Initializes the preview location for this content handler.<p>
     * 
     * @param root the "preview" element from the appinfo node of the XML content definition
     * @param contentDefinition the content definition the validation rules belong to
     * @throws CmsXmlException if something goes wrong
     */
    protected void initPreview(Element root, CmsXmlContentDefinition contentDefinition) throws CmsXmlException {

        String preview = root.attributeValue(APPINFO_ATTR_URI);
        if (preview == null) {
            throw new CmsXmlException(Messages.get().container(Messages.ERR_XMLCONTENT_MISSING_PREVIEW_URI_2,
                    root.getName(), contentDefinition.getSchemaLocation()));
        }
        m_previewLocation = preview;
    }

    /**
     * Initializes the relation configuration for this content handler.<p>
     * 
     * OpenCms performs link checks for all OPTIONAL links defined in XML content values of type 
     * OpenCmsVfsFile. However, for most projects in the real world a more fine-grained control 
     * over the link check process is required. For these cases, individual relation behavior can 
     * be defined for the appinfo node.<p>
     * 
     * Additional here can be defined an optional type for the relations, for instance.<p>
     * 
     * @param root the "relations" element from the appinfo node of the XML content definition
     * @param contentDefinition the content definition the check rules belong to
     * 
     * @throws CmsXmlException if something goes wrong
     */
    protected void initRelations(Element root, CmsXmlContentDefinition contentDefinition) throws CmsXmlException {

        Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(root, APPINFO_RELATION);
        while (i.hasNext()) {
            // iterate all "checkrule" elements in the "checkrule" node
            Element element = i.next();
            String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
            String invalidate = element.attributeValue(APPINFO_ATTR_INVALIDATE);
            if (invalidate != null) {
                invalidate = invalidate.toUpperCase();
            }
            String type = element.attributeValue(APPINFO_ATTR_TYPE);
            if (type != null) {
                type = type.toLowerCase();
            }
            if (elementName != null) {
                // add a check rule for the element
                addCheckRule(contentDefinition, elementName, invalidate, type);
            }
        }
    }

    /**
     * Initializes the resource bundle to use for localized messages in this content handler.<p>
     * 
     * @param root the "resourcebundle" element from the appinfo node of the XML content definition
     * @param contentDefinition the content definition the validation rules belong to
     * @param single if <code>true</code> we process the classic sinle line entry, otherwise it's the multiple line setting
     * 
     * @throws CmsXmlException if something goes wrong
     */
    protected void initResourceBundle(Element root, CmsXmlContentDefinition contentDefinition, boolean single)
            throws CmsXmlException {

        if (m_messageBundleNames == null) {
            // it's uncommon to have more then one bundle so just initialize an array length of 2
            m_messageBundleNames = new ArrayList<String>(2);
        }

        if (single) {
            // single "resourcebundle" node

            String messageBundleName = root.attributeValue(APPINFO_ATTR_NAME);
            if (messageBundleName == null) {
                throw new CmsXmlException(
                        Messages.get().container(Messages.ERR_XMLCONTENT_MISSING_RESOURCE_BUNDLE_NAME_2,
                                root.getName(), contentDefinition.getSchemaLocation()));
            }
            if (!m_messageBundleNames.contains(messageBundleName)) {
                // avoid duplicates
                m_messageBundleNames.add(messageBundleName);
            }
            // clear the cached resource bundles for this bundle
            CmsResourceBundleLoader.flushBundleCache(messageBundleName);

        } else {
            // multiple "resourcebundles" node

            // get an iterator for all "propertybundle" subnodes
            Iterator<Element> propertybundles = CmsXmlGenericWrapper.elementIterator(root, APPINFO_PROPERTYBUNDLE);
            while (propertybundles.hasNext()) {
                // iterate all "propertybundle" elements in the "resourcebundle" node
                Element propBundle = propertybundles.next();
                String propertyBundleName = propBundle.attributeValue(APPINFO_ATTR_NAME);
                if (!m_messageBundleNames.contains(propertyBundleName)) {
                    // avoid duplicates
                    m_messageBundleNames.add(propertyBundleName);
                }
                // clear the cached resource bundles for this bundle
                CmsResourceBundleLoader.flushBundleCache(propertyBundleName);
            }

            // get an iterator for all "xmlbundle" subnodes
            Iterator<Element> xmlbundles = CmsXmlGenericWrapper.elementIterator(root, APPINFO_XMLBUNDLE);
            while (xmlbundles.hasNext()) {
                Element xmlbundle = xmlbundles.next();
                String xmlBundleName = xmlbundle.attributeValue(APPINFO_ATTR_NAME);
                // cache the bundle from the XML
                if (!m_messageBundleNames.contains(xmlBundleName)) {
                    // avoid duplicates
                    m_messageBundleNames.add(xmlBundleName);
                }
                // clear the cached resource bundles for this bundle
                CmsResourceBundleLoader.flushBundleCache(xmlBundleName);
                Iterator<Element> bundles = CmsXmlGenericWrapper.elementIterator(xmlbundle, APPINFO_BUNDLE);
                while (bundles.hasNext()) {
                    // iterate all "bundle" elements in the "xmlbundle" node
                    Element bundle = bundles.next();
                    String localeStr = bundle.attributeValue(APPINFO_ATTR_LOCALE);
                    Locale locale;
                    if (CmsStringUtil.isEmptyOrWhitespaceOnly(localeStr)) {
                        // no locale set, so use no locale
                        locale = null;
                    } else {
                        // use provided locale
                        locale = CmsLocaleManager.getLocale(localeStr);
                    }
                    if (CmsLocaleManager.getDefaultLocale().equals(locale)) {
                        // in case the default locale is given, we store this as root
                        locale = null;
                    }

                    CmsListResourceBundle xmlBundle = null;

                    Iterator<Element> resources = CmsXmlGenericWrapper.elementIterator(bundle, APPINFO_RESOURCE);
                    while (resources.hasNext()) {
                        // now collect all resource bundle keys
                        Element resource = resources.next();
                        String key = resource.attributeValue(APPINFO_ATTR_KEY);
                        String value = resource.attributeValue(APPINFO_ATTR_VALUE);
                        if (CmsStringUtil.isEmptyOrWhitespaceOnly(value)) {
                            // read from inside XML tag if value attribute is not set
                            value = resource.getTextTrim();
                        }
                        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(key)
                                && CmsStringUtil.isNotEmptyOrWhitespaceOnly(value)) {
                            if (xmlBundle == null) {
                                // use lazy initilaizing of the bundle
                                xmlBundle = new CmsListResourceBundle();
                            }
                            xmlBundle.addMessage(key.trim(), value.trim());
                        }
                    }
                    if (xmlBundle != null) {
                        CmsResourceBundleLoader.addBundleToCache(xmlBundleName, locale, xmlBundle);
                    }
                }
            }
        }
    }

    /**
     * Initializes the search exclusions values for this content handler.<p>
     * 
     * For the full text search, the value of all elements in one locale of the XML content are combined
     * to one big text, which is referred to as the "content" in the context of the full text search.
     * With this option, it is possible to hide certain elements from this "content" that does not make sense 
     * to include in the full text search.<p>   
     * 
     * @param root the "searchsettings" element from the appinfo node of the XML content definition
     * @param contentDefinition the content definition the default values belong to
     * 
     * @throws CmsXmlException if something goes wrong
     */
    protected void initSearchSettings(Element root, CmsXmlContentDefinition contentDefinition)
            throws CmsXmlException {

        Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(root, APPINFO_SEARCHSETTING);
        while (i.hasNext()) {
            // iterate all "searchsetting" elements in the "searchsettings" node
            Element element = i.next();
            String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
            String searchContent = element.attributeValue(APPINFO_ATTR_SEARCHCONTENT);
            boolean include = CmsStringUtil.isEmpty(searchContent) || Boolean.valueOf(searchContent).booleanValue();
            if (elementName != null) {
                // add search exclusion for the element
                // this may also be "false" in case a default of "true" is to be overwritten
                addSearchSetting(contentDefinition, elementName, Boolean.valueOf(include));
            }
        }
    }

    /**
     * Initializes the element settings for this content handler.<p>
     * 
     * @param root the "settings" element from the appinfo node of the XML content definition
     * @param contentDefinition the content definition the element settings belong to
     */
    protected void initSettings(Element root, CmsXmlContentDefinition contentDefinition) {

        Iterator<Element> itProperties = CmsXmlGenericWrapper.elementIterator(root, APPINFO_SETTING);
        while (itProperties.hasNext()) {
            Element element = itProperties.next();
            CmsXmlContentProperty setting = new CmsXmlContentProperty(element.attributeValue(APPINFO_ATTR_NAME),
                    element.attributeValue(APPINFO_ATTR_TYPE), element.attributeValue(APPINFO_ATTR_WIDGET),
                    element.attributeValue(APPINFO_ATTR_WIDGET_CONFIG),
                    element.attributeValue(APPINFO_ATTR_RULE_REGEX), element.attributeValue(APPINFO_ATTR_RULE_TYPE),
                    element.attributeValue(APPINFO_ATTR_DEFAULT), element.attributeValue(APPINFO_ATTR_NICE_NAME),
                    element.attributeValue(APPINFO_ATTR_DESCRIPTION), element.attributeValue(APPINFO_ATTR_ERROR),
                    element.attributeValue(APPINFO_ATTR_PREFERFOLDER));
            String name = setting.getName();
            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(name)) {
                m_settings.put(name, setting);
            }
        }
    }

    /**
     * Initializes the tabs for this content handler.<p>
     * 
     * @param root the "tabs" element from the appinfo node of the XML content definition
     * @param contentDefinition the content definition the tabs belong to
     */
    protected void initTabs(Element root, CmsXmlContentDefinition contentDefinition) {

        if (Boolean.valueOf(root.attributeValue(APPINFO_ATTR_USEALL, CmsStringUtil.FALSE)).booleanValue()) {
            // all first level elements should be treated as tabs
            Iterator<I_CmsXmlSchemaType> i = contentDefinition.getTypeSequence().iterator();
            while (i.hasNext()) {
                // get the type
                I_CmsXmlSchemaType type = i.next();
                m_tabs.add(new CmsXmlContentTab(type.getName()));
            }
        } else {
            // manual definition of tabs
            Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(root, APPINFO_TAB);
            while (i.hasNext()) {
                // iterate all "tab" elements in the "tabs" node
                Element element = i.next();
                // this is a tab node
                String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
                String collapseValue = element.attributeValue(APPINFO_ATTR_COLLAPSE, CmsStringUtil.TRUE);
                String tabName = element.attributeValue(APPINFO_ATTR_NAME, elementName);
                if (elementName != null) {
                    // add the element tab 
                    m_tabs.add(new CmsXmlContentTab(elementName, Boolean.valueOf(collapseValue).booleanValue(),
                            tabName));
                }
            }
            // check if first element has been defined as tab
            I_CmsXmlSchemaType type = contentDefinition.getTypeSequence().get(0);
            CmsXmlContentTab tab = new CmsXmlContentTab(type.getName());
            if (!m_tabs.contains(tab)) {
                m_tabs.add(0, tab);
            }
        }
    }

    /**
     * Initializes the validation rules this content handler.<p>
     * 
     * OpenCms always performs XML schema validation for all XML contents. However,
     * for most projects in the real world a more fine-grained control over the validation process is
     * required. For these cases, individual validation rules can be defined for the appinfo node.<p>
     * 
     * @param root the "validationrules" element from the appinfo node of the XML content definition
     * @param contentDefinition the content definition the validation rules belong to
     * 
     * @throws CmsXmlException if something goes wrong
     */
    protected void initValidationRules(Element root, CmsXmlContentDefinition contentDefinition)
            throws CmsXmlException {

        List<Element> elements = new ArrayList<Element>(CmsXmlGenericWrapper.elements(root, APPINFO_RULE));
        elements.addAll(CmsXmlGenericWrapper.elements(root, APPINFO_VALIDATIONRULE));
        Iterator<Element> i = elements.iterator();
        while (i.hasNext()) {
            // iterate all "rule" or "validationrule" elements in the "validationrules" node
            Element element = i.next();
            String elementName = element.attributeValue(APPINFO_ATTR_ELEMENT);
            String regex = element.attributeValue(APPINFO_ATTR_REGEX);
            String type = element.attributeValue(APPINFO_ATTR_TYPE);
            if (type != null) {
                type = type.toLowerCase();
            }
            String message = element.attributeValue(APPINFO_ATTR_MESSAGE);
            if ((elementName != null) && (regex != null)) {
                // add a validation rule for the element
                addValidationRule(contentDefinition, elementName, regex, message,
                        APPINFO_ATTR_TYPE_WARNING.equals(type));
            }
        }
    }

    /**
     * Returns the is-invalidate-parent flag for the given xpath.<p>
     * 
     * @param xpath the path to get the check rule for
     * 
     * @return the configured is-invalidate-parent flag for the given xpath
     */
    protected boolean isInvalidateParent(String xpath) {

        if (!CmsXmlUtils.isDeepXpath(xpath)) {
            return false;
        }
        Boolean isInvalidateParent = null;
        // look up the default from the configured mappings
        isInvalidateParent = m_relationChecks.get(xpath);
        if (isInvalidateParent == null) {
            // no value found, try default xpath
            String path = CmsXmlUtils.removeXpath(xpath);
            // look up the default value again without indexes
            isInvalidateParent = m_relationChecks.get(path);
        }
        if (isInvalidateParent == null) {
            return false;
        }
        return isInvalidateParent.booleanValue();
    }

    /**
     * Returns the localized resource string for a given message key according to the configured resource bundle
     * of this content handler.<p>
     * 
     * If the key was not found in the configured bundle, or no bundle is configured for this 
     * content handler, the return value is
     * <code>"??? " + keyName + " ???"</code>.<p>
     * 
     * @param keyName the key for the desired string 
     * @param locale the locale to get the key from
     * 
     * @return the resource string for the given key 
     * 
     * @see CmsMessages#formatUnknownKey(String)
     * @see CmsMessages#isUnknownKey(String)
     */
    protected String key(String keyName, Locale locale) {

        CmsMessages messages = getMessages(locale);
        if (messages != null) {
            return messages.key(keyName);
        }
        return CmsMessages.formatUnknownKey(keyName);
    }

    /**
     * Removes property values on resources for non-existing, optional elements.<p>
     * 
     * @param cms the current users OpenCms context
     * @param content the XML content to remove the property values for
     * 
     * @throws CmsException in case of read/write errors accessing the OpenCms VFS
     */
    protected void removeEmptyMappings(CmsObject cms, CmsXmlContent content) throws CmsException {

        List<CmsResource> siblings = null;
        CmsObject rootCms = null;

        Iterator<Map.Entry<String, List<String>>> allMappings = m_elementMappings.entrySet().iterator();
        while (allMappings.hasNext()) {
            Map.Entry<String, List<String>> e = allMappings.next();
            String path = e.getKey();
            List<String> mappings = e.getValue();
            if (mappings == null) {
                // nothing to do if we have no mappings at all
                continue;
            }
            if ((siblings == null) || (rootCms == null)) {
                // create OpenCms user context initialized with "/" as site root to read all siblings
                rootCms = OpenCms.initCmsObject(cms);
                rootCms.getRequestContext().setSiteRoot("/");
                siblings = rootCms.readSiblings(content.getFile().getRootPath(),
                        CmsResourceFilter.IGNORE_EXPIRATION);
            }
            for (int v = mappings.size() - 1; v >= 0; v--) {
                String mapping = mappings.get(v);
                if (mapping.startsWith(MAPTO_PROPERTY_LIST) || mapping.startsWith(MAPTO_PROPERTY)) {

                    for (int i = 0; i < siblings.size(); i++) {

                        // get siblings filename and locale
                        String filename = siblings.get(i).getRootPath();
                        Locale locale = OpenCms.getLocaleManager().getDefaultLocale(rootCms, filename);

                        if (!content.hasLocale(locale)) {
                            // only remove property if the locale fits
                            continue;
                        }
                        if (content.hasValue(path, locale)) {
                            // value is available, property must be kept
                            continue;
                        }

                        String property;
                        if (mapping.startsWith(MAPTO_PROPERTY_LIST)) {
                            // this is a property list mapping
                            property = mapping.substring(MAPTO_PROPERTY_LIST.length());
                        } else {
                            // this is a property mapping
                            property = mapping.substring(MAPTO_PROPERTY.length());
                        }
                        // delete the property value for the not existing node
                        rootCms.writePropertyObject(filename,
                                new CmsProperty(property, CmsProperty.DELETE_VALUE, null));
                    }
                } else if (mapping.startsWith(MAPTO_PERMISSION)) {
                    for (int i = 0; i < siblings.size(); i++) {

                        // get siblings filename and locale
                        String filename = siblings.get(i).getRootPath();
                        Locale locale = OpenCms.getLocaleManager().getDefaultLocale(rootCms, filename);

                        if (!content.hasLocale(locale)) {
                            // only remove property if the locale fits
                            continue;
                        }
                        if (content.hasValue(path, locale)) {
                            // value is available, property must be kept
                            continue;
                        }
                        // remove all existing permissions from the file
                        List<CmsAccessControlEntry> aces = rootCms.getAccessControlEntries(filename, false);
                        for (Iterator<CmsAccessControlEntry> j = aces.iterator(); j.hasNext();) {
                            CmsAccessControlEntry ace = j.next();
                            if (ace.getPrincipal().equals(CmsAccessControlEntry.PRINCIPAL_ALL_OTHERS_ID)) {
                                // remove the entry "All others", which has to be treated in a special way
                                rootCms.rmacc(filename, CmsAccessControlEntry.PRINCIPAL_ALL_OTHERS_NAME,
                                        CmsAccessControlEntry.PRINCIPAL_ALL_OTHERS_ID.toString());
                            } else {
                                // this is a group or user principal
                                I_CmsPrincipal principal = CmsPrincipal.readPrincipal(rootCms, ace.getPrincipal());
                                if (principal.isGroup()) {
                                    rootCms.rmacc(filename, I_CmsPrincipal.PRINCIPAL_GROUP, principal.getName());
                                } else if (principal.isUser()) {
                                    rootCms.rmacc(filename, I_CmsPrincipal.PRINCIPAL_USER, principal.getName());
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Validates if the given <code>appinfo</code> element node from the XML content definition schema
     * is valid according the the capabilities of this content handler.<p> 
     * 
     * @param appinfoElement the <code>appinfo</code> element node to validate
     *  
     * @throws CmsXmlException in case the element validation fails
     */
    protected void validateAppinfoElement(Element appinfoElement) throws CmsXmlException {

        // create a document to validate
        Document doc = DocumentHelper.createDocument();
        Element root = doc.addElement(APPINFO_APPINFO);
        // attach the default appinfo schema
        root.add(I_CmsXmlSchemaType.XSI_NAMESPACE);
        root.addAttribute(I_CmsXmlSchemaType.XSI_NAMESPACE_ATTRIBUTE_NO_SCHEMA_LOCATION, APPINFO_SCHEMA_SYSTEM_ID);
        // append the content from the appinfo node in the content definition 
        root.appendContent(appinfoElement);
        // now validate the document with the default appinfo schema
        CmsXmlUtils.validateXmlStructure(doc, CmsEncoder.ENCODING_UTF_8, new CmsXmlEntityResolver(null));
    }

    /**
     * The errorHandler parameter is optional, if <code>null</code> is given a new error handler 
     * instance must be created.<p>
     * 
     * @param cms the current OpenCms user context
     * @param value the value to resolve the validation rules for
     * @param errorHandler (optional) an error handler instance that contains previous error or warnings
     * 
     * @return an error handler that contains all errors and warnings currently found
     */
    protected CmsXmlContentErrorHandler validateCategories(CmsObject cms, I_CmsXmlContentValue value,
            CmsXmlContentErrorHandler errorHandler) {

        if (!value.isSimpleType()) {
            // do not validate complex types
            return errorHandler;
        }
        I_CmsWidget widget = null;
        try {
            widget = value.getContentDefinition().getContentHandler().getWidget(value);
        } catch (CmsXmlException e) {
            if (LOG.isErrorEnabled()) {
                LOG.error(e.getLocalizedMessage(), e);
            }
        }
        if (!(widget instanceof CmsCategoryWidget)) {
            // do not validate widget that are not category widgets
            return errorHandler;
        }
        String stringValue = value.getStringValue(cms);
        try {
            String catPath = CmsCategoryService.getInstance().getCategory(cms, stringValue).getPath();
            String refPath = getReferencePath(cms, value);
            CmsCategoryService.getInstance().readCategory(cms, catPath, refPath);
            if (((CmsCategoryWidget) widget).isOnlyLeafs()) {
                if (!CmsCategoryService.getInstance().readCategories(cms, catPath, false, refPath).isEmpty()) {
                    errorHandler.addError(value, Messages.get().getBundle(value.getLocale())
                            .key(Messages.GUI_CATEGORY_CHECK_NOLEAF_ERROR_0));
                }
            }
        } catch (CmsDataAccessException e) {
            // expected error in case of empty/invalid value
            // see CmsCategory#getCategoryPath(String, String)
            if (LOG.isDebugEnabled()) {
                LOG.debug(e.getLocalizedMessage(), e);
            }
            errorHandler.addError(value,
                    Messages.get().getBundle(value.getLocale()).key(Messages.GUI_CATEGORY_CHECK_EMPTY_ERROR_0));
        } catch (CmsException e) {
            // unexpected error
            if (LOG.isErrorEnabled()) {
                LOG.error(e.getLocalizedMessage(), e);
            }
            errorHandler.addError(value, e.getLocalizedMessage());
        }
        return errorHandler;
    }

    /**
     * Validates the given rules against the given value.<p> 
     * 
     * @param cms the current users OpenCms context
     * @param value the value to validate
     * @param errorHandler the error handler to use in case errors or warnings are detected
     * 
     * @return if a broken link has been found
     */
    protected boolean validateLink(CmsObject cms, I_CmsXmlContentValue value,
            CmsXmlContentErrorHandler errorHandler) {

        // if there is a value of type file reference
        if ((value == null) || (!(value instanceof CmsXmlVfsFileValue) && !(value instanceof CmsXmlVarLinkValue))) {
            return false;
        }
        // if the value has a link (this will automatically fix, for instance, the path of moved resources)
        CmsLink link = null;
        if (value instanceof CmsXmlVfsFileValue) {
            link = ((CmsXmlVfsFileValue) value).getLink(cms);
        } else if (value instanceof CmsXmlVarLinkValue) {
            link = ((CmsXmlVarLinkValue) value).getLink(cms);
        }
        if ((link == null) || !link.isInternal()) {
            return false;
        }
        try {
            String sitePath = cms.getRequestContext().removeSiteRoot(link.getTarget());
            // validate the link for error
            CmsResource res = null;
            CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(link.getTarget());
            // the link target may be a root path for a resource in another site 
            if (site != null) {
                CmsObject rootCms = OpenCms.initCmsObject(cms);
                rootCms.getRequestContext().setSiteRoot("");
                res = rootCms.readResource(link.getTarget(), CmsResourceFilter.IGNORE_EXPIRATION);
            } else {
                res = cms.readResource(sitePath, CmsResourceFilter.IGNORE_EXPIRATION);
            }
            // check the time range 
            if (res != null) {
                long time = System.currentTimeMillis();
                if (!res.isReleased(time)) {
                    if (errorHandler != null) {
                        // generate warning message
                        errorHandler.addWarning(value, Messages.get().getBundle(value.getLocale())
                                .key(Messages.GUI_XMLCONTENT_CHECK_WARNING_NOT_RELEASED_0));
                    }
                    return true;
                } else if (res.isExpired(time)) {
                    if (errorHandler != null) {
                        // generate warning message
                        errorHandler.addWarning(value, Messages.get().getBundle(value.getLocale())
                                .key(Messages.GUI_XMLCONTENT_CHECK_WARNING_EXPIRED_0));
                    }
                    return true;
                }
            }
        } catch (CmsException e) {
            if (errorHandler != null) {
                // generate error message
                errorHandler.addError(value,
                        Messages.get().getBundle(value.getLocale()).key(Messages.GUI_XMLCONTENT_CHECK_ERROR_0));
            }
            return true;
        }
        return false;
    }

    /**
     * Validates the given rules against the given value.<p> 
     * 
     * @param cms the current users OpenCms context
     * @param value the value to validate
     * @param errorHandler the error handler to use in case errors or warnings are detected
     * @param rules the rules to validate the value against
     * @param isWarning if true, this validation should be stored as a warning, otherwise as an error
     * 
     * @return the updated error handler
     */
    protected CmsXmlContentErrorHandler validateValue(CmsObject cms, I_CmsXmlContentValue value,
            CmsXmlContentErrorHandler errorHandler, Map<String, String> rules, boolean isWarning) {

        if (validateLink(cms, value, errorHandler)) {
            return errorHandler;
        }
        try {
            if (value.getContentDefinition().getContentHandler().getWidget(value) instanceof CmsDisplayWidget) {
                // display widgets should not be validated
                return errorHandler;
            }
        } catch (CmsXmlException e) {
            errorHandler.addError(value, e.getMessage());
            return errorHandler;
        }

        String valueStr;
        try {
            valueStr = value.getStringValue(cms);
        } catch (Exception e) {
            // if the value can not be accessed it's useless to continue
            errorHandler.addError(value, e.getMessage());
            return errorHandler;
        }

        String regex = rules.get(value.getName());
        if (regex == null) {
            // no customized rule, check default XML schema validation rules
            return validateValue(cms, value, valueStr, errorHandler, isWarning);
        }

        boolean matchResult = true;
        if (regex.charAt(0) == '!') {
            // negate the pattern
            matchResult = false;
            regex = regex.substring(1);
        }

        String matchValue = valueStr;
        if (matchValue == null) {
            // set match value to empty String to avoid exceptions in pattern matcher
            matchValue = "";
        }

        // use the custom validation pattern
        if (matchResult != Pattern.matches(regex, matchValue)) {
            // generate the message
            String message = getValidationMessage(cms, value, regex, valueStr, matchResult, isWarning);
            if (isWarning) {
                errorHandler.addWarning(value, message);
            } else {
                errorHandler.addError(value, message);
                // if an error was found, the default XML schema validation is not applied
                return errorHandler;
            }
        }

        // no error found, check default XML schema validation rules
        return validateValue(cms, value, valueStr, errorHandler, isWarning);
    }

    /**
     * Checks the default XML schema validation rules.<p>
     * 
     * These rules should only be tested if this is not a test for warnings.<p>
     * 
     * @param cms the current users OpenCms context
     * @param value the value to validate
     * @param valueStr the string value of the given value
     * @param errorHandler the error handler to use in case errors or warnings are detected
     * @param isWarning if true, this validation should be stored as a warning, otherwise as an error
     * 
     * @return the updated error handler
     */
    protected CmsXmlContentErrorHandler validateValue(CmsObject cms, I_CmsXmlContentValue value, String valueStr,
            CmsXmlContentErrorHandler errorHandler, boolean isWarning) {

        if (isWarning) {
            // default schema validation only applies to errors
            return errorHandler;
        }

        if (!value.validateValue(valueStr)) {
            // value is not valid, add an error to the handler
            String message = getValidationMessage(cms, value, value.getTypeName(), valueStr, true, false);
            errorHandler.addError(value, message);
        }

        return errorHandler;
    }

    /**
     * Writes the categories if a category widget is present.<p>
     * 
     * @param cms the cms context
     * @param file the file
     * @param content the xml content to set the categories for
     * 
     * @return the perhaps modified file
     * 
     * @throws CmsException if something goes wrong
     */
    protected CmsFile writeCategories(CmsObject cms, CmsFile file, CmsXmlContent content) throws CmsException {

        if (CmsWorkplace.isTemporaryFile(file)) {
            // ignore temporary file if the original file exists (not the case for direct edit: "new")
            if (CmsResource.isTemporaryFileName(file.getRootPath())) {
                String originalFileName = CmsResource.getFolderPath(file.getRootPath())
                        + CmsResource.getName(file.getRootPath()).substring(CmsResource.TEMP_FILE_PREFIX.length());
                if (cms.existsResource(cms.getRequestContext().removeSiteRoot(originalFileName))) {
                    // original file exists, ignore it
                    return file;
                }
            } else {
                // file name does not start with temporary prefix, ignore the file
                return file;
            }
        }
        // check the presence of a category widget
        boolean hasCategoryWidget = false;
        Iterator<I_CmsWidget> it = m_elementWidgets.values().iterator();
        while (it.hasNext()) {
            Object widget = it.next();
            if (widget instanceof CmsCategoryWidget) {
                hasCategoryWidget = true;
                break;
            }
        }
        if (!hasCategoryWidget) {
            // nothing to do if no category widget is present
            return file;
        }
        boolean modified = false;
        // clone the cms object, and use the root site
        CmsObject tmpCms = OpenCms.initCmsObject(cms);
        tmpCms.getRequestContext().setSiteRoot("");
        // read all siblings
        try {
            List<CmsResource> listsib = tmpCms.readSiblings(file.getRootPath(), CmsResourceFilter.ALL);
            for (int i = 0; i < listsib.size(); i++) {
                CmsResource resource = listsib.get(i);
                // get the default locale of the sibling
                Locale locale = getLocaleForResource(tmpCms, resource.getRootPath());
                // remove all previously set categories
                CmsCategoryService.getInstance().clearCategoriesForResource(tmpCms, resource.getRootPath());
                // iterate over all values checking for the category widget
                CmsXmlContentWidgetVisitor widgetCollector = new CmsXmlContentWidgetVisitor(locale);
                content.visitAllValuesWith(widgetCollector);
                Iterator<Map.Entry<String, I_CmsXmlContentValue>> itWidgets = widgetCollector.getValues().entrySet()
                        .iterator();
                while (itWidgets.hasNext()) {
                    Map.Entry<String, I_CmsXmlContentValue> entry = itWidgets.next();
                    String xpath = entry.getKey();
                    I_CmsWidget widget = widgetCollector.getWidgets().get(xpath);
                    if (!(widget instanceof CmsCategoryWidget)) {
                        // ignore other values than categories
                        continue;
                    }
                    I_CmsXmlContentValue value = entry.getValue();
                    String catRootPath = value.getStringValue(tmpCms);
                    if (CmsStringUtil.isEmptyOrWhitespaceOnly(catRootPath)) {
                        // skip empty values
                        continue;
                    }
                    try {
                        // add the file to the selected category
                        CmsCategory cat = CmsCategoryService.getInstance().getCategory(tmpCms, catRootPath);
                        CmsCategoryService.getInstance().addResourceToCategory(tmpCms, resource.getRootPath(),
                                cat.getPath());
                    } catch (CmsVfsResourceNotFoundException e) {
                        // invalid category
                        try {
                            // try to remove invalid value
                            content.removeValue(value.getName(), value.getLocale(), value.getIndex());
                            modified = true;
                        } catch (CmsRuntimeException ex) {
                            // in case minoccurs prevents removing the invalid value
                            if (LOG.isDebugEnabled()) {
                                LOG.debug(ex.getLocalizedMessage(), ex);
                            }
                        }
                    }
                }
            }
        } catch (CmsException ex) {
            if (LOG.isErrorEnabled()) {
                LOG.error(ex.getLocalizedMessage(), ex);
            }
        }
        if (modified) {
            // when an invalid category has been removed
            file = content.correctXmlStructure(cms);
            content.setFile(file);
        }
        return file;
    }

}