/* * 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; } }