/* * 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, 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.db.CmsUserSettings; import org.opencms.file.CmsObject; import org.opencms.file.CmsResource; import org.opencms.file.types.CmsResourceTypeXmlContent; import org.opencms.i18n.CmsMessages; import org.opencms.json.JSONException; import org.opencms.json.JSONObject; import org.opencms.main.CmsException; import org.opencms.main.CmsLog; import org.opencms.relations.CmsLink; import org.opencms.relations.CmsRelationType; import org.opencms.util.CmsMacroResolver; import org.opencms.util.CmsStringUtil; import org.opencms.util.CmsUUID; import org.opencms.xml.CmsXmlContentDefinition; import org.opencms.xml.CmsXmlGenericWrapper; import org.opencms.xml.CmsXmlUtils; import org.opencms.xml.content.CmsXmlContentProperty.PropType; import org.opencms.xml.page.CmsXmlPage; import org.opencms.xml.types.CmsXmlNestedContentDefinition; 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.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import org.apache.commons.logging.Log; import org.dom4j.Element; /** * Provides common methods on XML property configuration.<p> * * @since 8.0.0 */ public final class CmsXmlContentPropertyHelper implements Cloneable { /** Element Property json property constants. */ public enum JsonProperty { /** Property's default value. */ defaultValue, /** Property's description. */ description, /** Property's error message. */ error, /** Property's nice name. */ niceName, /** Property's validation regular expression. */ ruleRegex, /** Property's validation rule type. */ ruleType, /** Property's type. */ type, /** Property's value. */ value, /** Property's widget. */ widget, /** Property's widget configuration. */ widgetConf; } /** Widget configuration key-value separator constant. */ private static final String CONF_KEYVALUE_SEPARATOR = ":"; /** Widget configuration parameter separator constant. */ private static final String CONF_PARAM_SEPARATOR = "\\|"; /** The log object for this class. */ private static final Log LOG = CmsLog.getLog(CmsXmlContentPropertyHelper.class); /** * Hidden constructor.<p> */ private CmsXmlContentPropertyHelper() { // prevent instantiation } /** * Converts a map of properties from server format to client format.<p> * * @param cms the CmsObject to use for VFS operations * @param props the map of properties * @param propConfig the property configuration * * @return the converted property map */ public static Map<String, String> convertPropertiesToClientFormat( CmsObject cms, Map<String, String> props, Map<String, CmsXmlContentProperty> propConfig) { return convertProperties(cms, props, propConfig, true); } /** * Converts a map of properties from client format to server format.<p> * * @param cms the CmsObject to use for VFS operations * @param props the map of properties * @param propConfig the property configuration * * @return the converted property map */ public static Map<String, String> convertPropertiesToServerFormat( CmsObject cms, Map<String, String> props, Map<String, CmsXmlContentProperty> propConfig) { return convertProperties(cms, props, propConfig, false); } /** * Creates a deep copy of a property configuration map.<p> * * @param propConfig the property configuration which should be copied * * @return a copy of the property configuration */ public static Map<String, CmsXmlContentProperty> copyPropertyConfiguration( Map<String, CmsXmlContentProperty> propConfig) { Map<String, CmsXmlContentProperty> result = new LinkedHashMap<String, CmsXmlContentProperty>(); for (Map.Entry<String, CmsXmlContentProperty> entry : propConfig.entrySet()) { String key = entry.getKey(); CmsXmlContentProperty propDef = entry.getValue(); result.put(key, propDef.copy()); } return result; } /** * Looks up an URI in the sitemap and returns either a sitemap entry id (if the URI is a sitemap URI) * or the structure id of a resource (if the URI is a VFS path).<p> * * @param cms the current CMS context * @param uri the URI to look up * @return a sitemap entry id or a structure id * * @throws CmsException if something goes wrong */ public static CmsUUID getIdForUri(CmsObject cms, String uri) throws CmsException { return cms.readResource(uri).getStructureId(); } /** * Creates and configures a new macro resolver for resolving macros which occur in property definitions.<p> * * @param cms the CMS context * @param contentHandler the content handler which contains the message bundle that should be available in the macro resolver * * @return a new macro resolver */ public static CmsMacroResolver getMacroResolverForProperties(CmsObject cms, I_CmsXmlContentHandler contentHandler) { CmsMacroResolver resolver = new CmsMacroResolver(); resolver.setCmsObject(cms); CmsUserSettings settings = new CmsUserSettings(cms.getRequestContext().getCurrentUser()); CmsMessages messages = contentHandler.getMessages(settings.getLocale()); resolver.setMessages(messages); resolver.setKeepEmptyMacros(true); return resolver; } /** * Returns the property information for the given resource (type) AND the current user.<p> * * @param cms the current CMS context * @param resource the resource * * @return the property information * * @throws CmsException if something goes wrong */ public static Map<String, CmsXmlContentProperty> getPropertyInfo(CmsObject cms, CmsResource resource) throws CmsException { if (CmsResourceTypeXmlContent.isXmlContent(resource)) { I_CmsXmlContentHandler contentHandler = CmsXmlContentDefinition.getContentHandlerForResource(cms, resource); Map<String, CmsXmlContentProperty> propertiesConf = contentHandler.getSettings(cms, resource); CmsMacroResolver resolver = getMacroResolverForProperties(cms, contentHandler); return resolveMacrosInProperties(propertiesConf, resolver); } return Collections.<String, CmsXmlContentProperty> emptyMap(); } /** * Returns a converted property value depending on the given type.<p> * * If the type is {@link CmsXmlContentProperty.PropType#vfslist}, the value is parsed as a * list of paths and converted to a list of IDs.<p> * * @param cms the current CMS context * @param type the property type * @param value the raw property value * * @return a converted property value depending on the given type */ public static String getPropValueIds(CmsObject cms, String type, String value) { if (PropType.isVfsList(type)) { return convertPathsToIds(cms, value); } return value; } /** * Returns a converted property value depending on the given type.<p> * * If the type is {@link CmsXmlContentProperty.PropType#vfslist}, the value is parsed as a * list of IDs and converted to a list of paths.<p> * * @param cms the current CMS context * @param type the property type * @param value the raw property value * * @return a converted property value depending on the given type */ public static String getPropValuePaths(CmsObject cms, String type, String value) { if (PropType.isVfsList(type)) { return convertIdsToPaths(cms, value); } return value; } /** * Returns a sitemap or VFS path given a sitemap entry id or structure id.<p> * * This method first tries to read a sitemap entry with the given id. If this succeeds, * the sitemap entry's sitemap path will be returned. If it fails, the method interprets * the id as a structure id and tries to read the corresponding resource, and then returns * its VFS path.<p> * * @param cms the CMS context * @param id a sitemap entry id or structure id * * @return a sitemap or VFS uri * * @throws CmsException if something goes wrong */ public static String getUriForId(CmsObject cms, CmsUUID id) throws CmsException { CmsResource res = cms.readResource(id); return cms.getSitePath(res); } /** * Returns the widget configuration string parsed into a JSONObject.<p> * * The configuration string should be a map of key value pairs separated by ':' and '|': KEY_1:VALUE_1|KEY_2:VALUE_2 ... * * @param widgetConfiguration the configuration to parse * * @return the configuration JSON */ public static JSONObject getWidgetConfigurationAsJSON(String widgetConfiguration) { JSONObject result = new JSONObject(); if (CmsStringUtil.isEmptyOrWhitespaceOnly(widgetConfiguration)) { return result; } Map<String, String> confEntries = CmsStringUtil.splitAsMap( widgetConfiguration, CONF_PARAM_SEPARATOR, CONF_KEYVALUE_SEPARATOR); for (Map.Entry<String, String> entry : confEntries.entrySet()) { try { result.put(entry.getKey(), entry.getValue()); } catch (JSONException e) { // should never happen LOG.error( Messages.get().container(Messages.ERR_XMLCONTENT_UNKNOWN_ELEM_PATH_SCHEMA_1, widgetConfiguration), e); } } return result; } /** * Returns true if the property name passed as a parameter is the name of a special system property.<p> * * @param name the property name * * @return true if the property name is the name of a special property */ public static boolean isSpecialProperty(String name) { return name.startsWith("#") || name.startsWith("*"); } /** * Extends the given properties with the default values * from the resource's property configuration.<p> * * @param cms the current CMS context * @param resource the resource to get the property configuration from * @param properties the properties to extend * * @return a merged map of properties */ public static Map<String, String> mergeDefaults(CmsObject cms, CmsResource resource, Map<String, String> properties) { Map<String, String> result = new HashMap<String, String>(); if (CmsResourceTypeXmlContent.isXmlContent(resource)) { try { Map<String, CmsXmlContentProperty> propertyConfig = CmsXmlContentDefinition.getContentHandlerForResource( cms, resource).getSettings(cms, resource); for (Map.Entry<String, CmsXmlContentProperty> entry : propertyConfig.entrySet()) { CmsXmlContentProperty prop = entry.getValue(); result.put(entry.getKey(), getPropValueIds(cms, prop.getType(), prop.getDefault())); } } catch (CmsException e) { // should never happen LOG.error(e.getLocalizedMessage(), e); } } result.putAll(properties); return result; } /** * Reads the properties from property-enabled xml content values.<p> * * @param xmlContent the xml content * @param locale the current locale * @param element the xml element * @param elemPath the xpath * @param elemDef the element definition * * @return the read property map * * @see org.opencms.xml.containerpage.CmsXmlContainerPage.XmlNode#Elements */ public static Map<String, String> readProperties( CmsXmlContent xmlContent, Locale locale, Element element, String elemPath, CmsXmlContentDefinition elemDef) { Map<String, String> propertiesMap = new HashMap<String, String>(); // Properties for (Iterator<Element> itProps = CmsXmlGenericWrapper.elementIterator( element, CmsXmlContentProperty.XmlNode.Properties.name()); itProps.hasNext();) { Element property = itProps.next(); // property itself int propIndex = CmsXmlUtils.getXpathIndexInt(property.getUniquePath(element)); String propPath = CmsXmlUtils.concatXpath( elemPath, CmsXmlUtils.createXpathElement(property.getName(), propIndex)); I_CmsXmlSchemaType propSchemaType = elemDef.getSchemaType(property.getName()); I_CmsXmlContentValue propValue = propSchemaType.createValue(xmlContent, property, locale); xmlContent.addBookmarkForValue(propValue, propPath, locale, true); CmsXmlContentDefinition propDef = ((CmsXmlNestedContentDefinition)propSchemaType).getNestedContentDefinition(); // name Element propName = property.element(CmsXmlContentProperty.XmlNode.Name.name()); xmlContent.addBookmarkForElement(propName, locale, property, propPath, propDef); // choice value Element value = property.element(CmsXmlContentProperty.XmlNode.Value.name()); if (value == null) { // this can happen when adding the elements node to the xml content continue; } int valueIndex = CmsXmlUtils.getXpathIndexInt(value.getUniquePath(property)); String valuePath = CmsXmlUtils.concatXpath( propPath, CmsXmlUtils.createXpathElement(value.getName(), valueIndex)); I_CmsXmlSchemaType valueSchemaType = propDef.getSchemaType(value.getName()); I_CmsXmlContentValue valueValue = valueSchemaType.createValue(xmlContent, value, locale); xmlContent.addBookmarkForValue(valueValue, valuePath, locale, true); CmsXmlContentDefinition valueDef = ((CmsXmlNestedContentDefinition)valueSchemaType).getNestedContentDefinition(); String val = null; Element string = value.element(CmsXmlContentProperty.XmlNode.String.name()); if (string != null) { // string value xmlContent.addBookmarkForElement(string, locale, value, valuePath, valueDef); val = string.getTextTrim(); } else { // file list value Element valueFileList = value.element(CmsXmlContentProperty.XmlNode.FileList.name()); if (valueFileList == null) { // this can happen when adding the elements node to the xml content continue; } int valueFileListIndex = CmsXmlUtils.getXpathIndexInt(valueFileList.getUniquePath(value)); String valueFileListPath = CmsXmlUtils.concatXpath( valuePath, CmsXmlUtils.createXpathElement(valueFileList.getName(), valueFileListIndex)); I_CmsXmlSchemaType valueFileListSchemaType = valueDef.getSchemaType(valueFileList.getName()); I_CmsXmlContentValue valueFileListValue = valueFileListSchemaType.createValue( xmlContent, valueFileList, locale); xmlContent.addBookmarkForValue(valueFileListValue, valueFileListPath, locale, true); CmsXmlContentDefinition valueFileListDef = ((CmsXmlNestedContentDefinition)valueFileListSchemaType).getNestedContentDefinition(); List<CmsUUID> idList = new ArrayList<CmsUUID>(); // files for (Iterator<Element> itFiles = CmsXmlGenericWrapper.elementIterator( valueFileList, CmsXmlContentProperty.XmlNode.Uri.name()); itFiles.hasNext();) { Element valueUri = itFiles.next(); xmlContent.addBookmarkForElement( valueUri, locale, valueFileList, valueFileListPath, valueFileListDef); Element valueUriLink = valueUri.element(CmsXmlPage.NODE_LINK); CmsUUID fileId = null; if (valueUriLink == null) { // this can happen when adding the elements node to the xml content // it is not dangerous since the link has to be set before saving } else { fileId = new CmsLink(valueUriLink).getStructureId(); idList.add(fileId); } } // comma separated list of UUIDs val = CmsStringUtil.listAsString(idList, CmsXmlContentProperty.PROP_SEPARATOR); } propertiesMap.put(propName.getTextTrim(), val); } return propertiesMap; } /** * Resolves macros in all properties in a map.<p> * * @param properties the map of properties in which macros should be resolved * @param resolver the macro resolver to use * * @return a new map of properties with resolved macros */ public static Map<String, CmsXmlContentProperty> resolveMacrosInProperties( Map<String, CmsXmlContentProperty> properties, CmsMacroResolver resolver) { Map<String, CmsXmlContentProperty> result = new LinkedHashMap<String, CmsXmlContentProperty>(); for (Map.Entry<String, CmsXmlContentProperty> entry : properties.entrySet()) { String key = entry.getKey(); CmsXmlContentProperty prop = entry.getValue(); result.put(key, resolveMacrosInProperty(prop, resolver)); } return result; } /** * Resolves the macros in a single property.<p> * * @param property the property in which macros should be resolved * @param resolver the macro resolver to use * * @return a new property with resolved macros */ public static CmsXmlContentProperty resolveMacrosInProperty( CmsXmlContentProperty property, CmsMacroResolver resolver) { String propName = property.getName(); CmsXmlContentProperty result = new CmsXmlContentProperty( propName, property.getType(), property.getWidget(), resolver.resolveMacros(property.getWidgetConfiguration()), property.getRuleRegex(), property.getRuleType(), property.getDefault(), resolver.resolveMacros(property.getNiceName()), resolver.resolveMacros(property.getDescription()), resolver.resolveMacros(property.getError()), property.isPreferFolder() ? "true" : "false"); return result; } /** * Saves the given properties to the given xml element.<p> * * @param cms the current CMS context * @param parentElement the parent xml element * @param properties the properties to save, if there is a list of resources, every entry can be a site path or a UUID * @param resource the resource to get the property configuration from * @param propertiesConf the configuration of the properties */ public static void saveProperties( CmsObject cms, Element parentElement, Map<String, String> properties, CmsResource resource, Map<String, CmsXmlContentProperty> propertiesConf) { // remove old entries for (Object propElement : parentElement.elements(CmsXmlContentProperty.XmlNode.Properties.name())) { parentElement.remove((Element)propElement); } // create new entries for (Map.Entry<String, String> property : properties.entrySet()) { String propName = property.getKey(); String propValue = property.getValue(); boolean isSpecial = isSpecialProperty(propName); if (!isSpecial && (!propertiesConf.containsKey(propName) || (propValue == null))) { continue; } // only if the property is configured in the schema we will save it Element propElement = parentElement.addElement(CmsXmlContentProperty.XmlNode.Properties.name()); // the property name propElement.addElement(CmsXmlContentProperty.XmlNode.Name.name()).addCDATA(propName); Element valueElement = propElement.addElement(CmsXmlContentProperty.XmlNode.Value.name()); String baseName = isSpecial ? propName.substring(1) : propName; boolean isVfs = false; CmsXmlContentProperty propDef = propertiesConf.get(baseName); if (propDef != null) { isVfs = CmsXmlContentProperty.PropType.isVfsList(propDef.getType()); } if (!isVfs) { // string value valueElement.addElement(CmsXmlContentProperty.XmlNode.String.name()).addCDATA(propValue); } else { addFileListPropertyValue(cms, valueElement, propValue); } } } /** * Adds the XML for a property value of a property of type 'vfslist' to the DOM.<p> * * @param cms the current CMS context * @param valueElement the element to which the vfslist property value should be added * @param propValue the property value which should be saved */ protected static void addFileListPropertyValue(CmsObject cms, Element valueElement, String propValue) { // resource list value Element filelistElem = valueElement.addElement(CmsXmlContentProperty.XmlNode.FileList.name()); for (String strId : CmsStringUtil.splitAsList(propValue, CmsXmlContentProperty.PROP_SEPARATOR)) { try { Element fileValueElem = filelistElem.addElement(CmsXmlContentProperty.XmlNode.Uri.name()); CmsVfsFileValueBean fileValue = getFileValueForIdOrUri(cms, strId); // HACK: here we assume weak relations, but it would be more robust to check it, with smth like: // type = xmlContent.getContentDefinition().getContentHandler().getRelationType(fileValueElem.getPath()); CmsRelationType type = CmsRelationType.XML_WEAK; CmsXmlVfsFileValue.fillEntry(fileValueElem, fileValue.getId(), fileValue.getPath(), type); } catch (CmsException e) { // should never happen LOG.error(e.getLocalizedMessage(), e); } } } /** * Converts a string containing zero or more structure ids into a string containing the corresponding VFS paths.<p> * * @param cms the CmsObject to use for the VFS operations * @param value a string representation of a list of ids * * @return a string representation of a list of paths */ protected static String convertIdsToPaths(CmsObject cms, String value) { if (value == null) { return null; } // represent vfslists as lists of path in JSON List<String> ids = CmsStringUtil.splitAsList(value, CmsXmlContentProperty.PROP_SEPARATOR); List<String> paths = new ArrayList<String>(); for (String id : ids) { try { String path = getUriForId(cms, new CmsUUID(id)); paths.add(path); } catch (Exception e) { // should never happen LOG.error(e.getLocalizedMessage(), e); continue; } } return CmsStringUtil.listAsString(paths, CmsXmlContentProperty.PROP_SEPARATOR); } /** * Converts a string containing zero or more VFS paths into a string containing the corresponding structure ids.<p> * * @param cms the CmsObject to use for the VFS operations * @param value a string representation of a list of paths * * @return a string representation of a list of ids */ protected static String convertPathsToIds(CmsObject cms, String value) { if (value == null) { return null; } // represent vfslists as lists of path in JSON List<String> paths = CmsStringUtil.splitAsList(value, CmsXmlContentProperty.PROP_SEPARATOR); List<String> ids = new ArrayList<String>(); for (String path : paths) { try { CmsUUID id = getIdForUri(cms, path); ids.add(id.toString()); } catch (CmsException e) { // should never happen LOG.error(e.getLocalizedMessage(), e); continue; } } return CmsStringUtil.listAsString(ids, CmsXmlContentProperty.PROP_SEPARATOR); } /** * Helper method for converting a map of properties from client format to server format or vice versa.<p> * * @param cms the CmsObject to use for VFS operations * @param props the map of properties * @param propConfig the property configuration * @param toClient if true, convert from server to client, else from client to server * * @return the converted property map */ protected static Map<String, String> convertProperties( CmsObject cms, Map<String, String> props, Map<String, CmsXmlContentProperty> propConfig, boolean toClient) { Map<String, String> result = new HashMap<String, String>(); for (Map.Entry<String, String> entry : props.entrySet()) { String propName = entry.getKey(); String propValue = entry.getValue(); String type; CmsXmlContentProperty configEntry = getPropertyConfig(propConfig, propName); if (configEntry == null) { continue; // ignore properties which are not configured anymore } type = configEntry.getType(); String newValue = convertStringPropertyValue(cms, propValue, type, toClient); result.put(propName, newValue); } return result; } /** * Converts a property value given as a string between server format and client format.<p> * * @param cms the current CMS context * @param propValue the property value to convert * @param type the type of the property * @param toClient if true, convert to client format, else convert to server format * * @return the converted property value */ protected static String convertStringPropertyValue(CmsObject cms, String propValue, String type, boolean toClient) { if (propValue == null) { return null; } if (toClient) { return CmsXmlContentPropertyHelper.getPropValuePaths(cms, type, propValue); } else { return CmsXmlContentPropertyHelper.getPropValueIds(cms, type, propValue); } } /** * Given a string which might be a id or a (sitemap or VFS) URI, this method will return * a bean containing the right (sitemap or vfs) root path and (sitemap entry or structure) id.<p> * * @param cms the current CMS context * @param idOrUri a string containing an id or an URI * * @return a bean containing a root path and an id * * @throws CmsException if something goes wrong */ protected static CmsVfsFileValueBean getFileValueForIdOrUri(CmsObject cms, String idOrUri) throws CmsException { CmsVfsFileValueBean result; if (CmsUUID.isValidUUID(idOrUri)) { CmsUUID id = new CmsUUID(idOrUri); String uri = getUriForId(cms, id); result = new CmsVfsFileValueBean(cms.getRequestContext().addSiteRoot(uri), id); } else { String uri = idOrUri; CmsUUID id = getIdForUri(cms, idOrUri); result = new CmsVfsFileValueBean(cms.getRequestContext().addSiteRoot(uri), id); } return result; } /** * Returns the base name of a given property name.<p> * * If propName starts with a '#' character, the base name equals the part * after the '#', otherwise the base name is identical to propName.<p> * * @param propName a property name * @return the base name of the property name */ protected static String getPropertyBaseName(String propName) { if (isSpecialProperty(propName)) { return propName.substring(1); } return propName; } /** * Helper method for accessing the property configuration for a single property.<p> * * This method uses the base name of the property to access the property configuration, * i.e. if propName starts with a '#', the part after the '#' will be used as the key for * the property configuration.<p> * * @param propertyConfig the property configuration map * @param propName the name of a property * @return the property configuration for the given property name */ protected static CmsXmlContentProperty getPropertyConfig( Map<String, CmsXmlContentProperty> propertyConfig, String propName) { return propertyConfig.get(getPropertyBaseName(propName)); } }