/* * 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.file.CmsFile; import org.opencms.file.CmsObject; import org.opencms.file.CmsResource; import org.opencms.file.CmsResourceFilter; import org.opencms.i18n.CmsEncoder; import org.opencms.i18n.CmsLocaleManager; import org.opencms.main.CmsException; import org.opencms.main.CmsIllegalArgumentException; import org.opencms.main.CmsLog; import org.opencms.main.CmsRuntimeException; import org.opencms.staticexport.CmsLinkProcessor; import org.opencms.staticexport.CmsLinkTable; import org.opencms.util.CmsMacroResolver; import org.opencms.xml.A_CmsXmlDocument; 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.types.CmsXmlNestedContentDefinition; import org.opencms.xml.types.I_CmsXmlContentValue; import org.opencms.xml.types.I_CmsXmlSchemaType; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Set; import org.apache.commons.logging.Log; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.Node; import org.xml.sax.EntityResolver; import org.xml.sax.SAXException; /** * Implementation of a XML content object, * used to access and manage structured content.<p> * * Use the {@link org.opencms.xml.content.CmsXmlContentFactory} to generate an * instance of this class.<p> * * @since 6.0.0 */ public class CmsXmlContent extends A_CmsXmlDocument { /** The name of the XML content auto correction runtime attribute, this must always be a Boolean. */ public static final String AUTO_CORRECTION_ATTRIBUTE = CmsXmlContent.class.getName() + ".autoCorrectionEnabled"; /** The property to set to enable xerces schema validation. */ public static final String XERCES_SCHEMA_PROPERTY = "http://apache.org/xml/properties/schema/external-noNamespaceSchemaLocation"; /** * Comparator to sort values according to the XML element position.<p> */ private static final Comparator<I_CmsXmlContentValue> COMPARE_INDEX = new Comparator<I_CmsXmlContentValue>() { public int compare(I_CmsXmlContentValue v1, I_CmsXmlContentValue v2) { return v1.getIndex() - v2.getIndex(); } }; /** The log object for this class. */ private static final Log LOG = CmsLog.getLog(CmsXmlContent.class); /** Flag to control if auto correction is enabled when saving this XML content. */ protected boolean m_autoCorrectionEnabled; /** The XML content definition object (i.e. XML schema) used by this content. */ protected CmsXmlContentDefinition m_contentDefinition; /** * Hides the public constructor.<p> */ protected CmsXmlContent() { // noop } /** * Creates a new XML content based on the provided XML document.<p> * * The given encoding is used when marshalling the XML again later.<p> * * @param cms the cms context, if <code>null</code> no link validation is performed * @param document the document to create the xml content from * @param encoding the encoding of the xml content * @param resolver the XML entitiy resolver to use */ protected CmsXmlContent(CmsObject cms, Document document, String encoding, EntityResolver resolver) { // must set document first to be able to get the content definition m_document = document; // for the next line to work the document must already be available m_contentDefinition = getContentDefinition(resolver); // initialize the XML content structure initDocument(cms, m_document, encoding, m_contentDefinition); } /** * Create a new XML content based on the given default content, * that will have all language nodes of the default content and ensures the presence of the given locale.<p> * * The given encoding is used when marshalling the XML again later.<p> * * @param cms the current users OpenCms content * @param locale the locale to generate the default content for * @param modelUri the absolute path to the XML content file acting as model * * @throws CmsException in case the model file is not found or not valid */ protected CmsXmlContent(CmsObject cms, Locale locale, String modelUri) throws CmsException { // init model from given modelUri CmsFile modelFile = cms.readFile(modelUri, CmsResourceFilter.ONLY_VISIBLE_NO_DELETED); CmsXmlContent model = CmsXmlContentFactory.unmarshal(cms, modelFile); // initialize macro resolver to use on model file values CmsMacroResolver macroResolver = CmsMacroResolver.newInstance().setCmsObject(cms); // content defition must be set here since it's used during document creation m_contentDefinition = model.getContentDefinition(); // get the document from the default content Document document = (Document)model.m_document.clone(); // initialize the XML content structure initDocument(cms, document, model.getEncoding(), m_contentDefinition); // resolve eventual macros in the nodes visitAllValuesWith(new CmsXmlContentMacroVisitor(cms, macroResolver)); if (!hasLocale(locale)) { // required locale not present, add it try { addLocale(cms, locale); } catch (CmsXmlException e) { // this can not happen since the locale does not exist } } } /** * Create a new XML content based on the given content definiton, * that will have one language node for the given locale all initialized with default values.<p> * * The given encoding is used when marshalling the XML again later.<p> * * @param cms the current users OpenCms content * @param locale the locale to generate the default content for * @param encoding the encoding to use when marshalling the XML content later * @param contentDefinition the content definiton to create the content for */ protected CmsXmlContent(CmsObject cms, Locale locale, String encoding, CmsXmlContentDefinition contentDefinition) { // content defition must be set here since it's used during document creation m_contentDefinition = contentDefinition; // create the XML document according to the content definition Document document = m_contentDefinition.createDocument(cms, this, locale); // initialize the XML content structure initDocument(cms, document, encoding, m_contentDefinition); } /** * @see org.opencms.xml.I_CmsXmlDocument#addLocale(org.opencms.file.CmsObject, java.util.Locale) */ public void addLocale(CmsObject cms, Locale locale) throws CmsXmlException { if (hasLocale(locale)) { throw new CmsXmlException(org.opencms.xml.page.Messages.get().container( org.opencms.xml.page.Messages.ERR_XML_PAGE_LOCALE_EXISTS_1, locale)); } // add element node for Locale m_contentDefinition.createLocale(cms, this, m_document.getRootElement(), locale); // re-initialize the bookmarks initDocument(cms, m_document, m_encoding, m_contentDefinition); } /** * Adds a new XML content value for the given element name and locale at the given index position * to this XML content document.<p> * * @param cms the current users OpenCms context * @param path the path to the XML content value element * @param locale the locale where to add the new value * @param index the index where to add the value (relative to all other values of this type) * * @return the created XML content value * * @throws CmsIllegalArgumentException if the given path is invalid * @throws CmsRuntimeException if the element identified by the path already occurred {@link I_CmsXmlSchemaType#getMaxOccurs()} * or the given <code>index</code> is invalid (too high). */ public I_CmsXmlContentValue addValue(CmsObject cms, String path, Locale locale, int index) throws CmsIllegalArgumentException, CmsRuntimeException { // get the schema type of the requested path I_CmsXmlSchemaType type = m_contentDefinition.getSchemaType(path); if (type == null) { throw new CmsIllegalArgumentException(Messages.get().container( Messages.ERR_XMLCONTENT_UNKNOWN_ELEM_PATH_SCHEMA_1, path)); } Element parentElement; String elementName; CmsXmlContentDefinition contentDefinition; if (CmsXmlUtils.isDeepXpath(path)) { // this is a nested content definition, so the parent element must be in the bookmarks String parentPath = CmsXmlUtils.createXpath(CmsXmlUtils.removeLastXpathElement(path), 1); Object o = getBookmark(parentPath, locale); if (o == null) { throw new CmsIllegalArgumentException(Messages.get().container( Messages.ERR_XMLCONTENT_UNKNOWN_ELEM_PATH_1, path)); } CmsXmlNestedContentDefinition parentValue = (CmsXmlNestedContentDefinition)o; parentElement = parentValue.getElement(); elementName = CmsXmlUtils.getLastXpathElement(path); contentDefinition = parentValue.getNestedContentDefinition(); } else { // the parent element is the locale element parentElement = getLocaleNode(locale); elementName = CmsXmlUtils.removeXpathIndex(path); contentDefinition = m_contentDefinition; } // read the XML siblings from the parent node List<Element> siblings = CmsXmlGenericWrapper.elements(parentElement, elementName); int insertIndex; if (contentDefinition.getChoiceMaxOccurs() > 0) { // for a choice sequence we do not check the index position, we rather do a full XML validation afterwards insertIndex = index; } else if (siblings.size() > 0) { // we want to add an element to a sequence, and there are elements already of the same type if (siblings.size() >= type.getMaxOccurs()) { // must not allow adding an element if max occurs would be violated throw new CmsRuntimeException(Messages.get().container( Messages.ERR_XMLCONTENT_ELEM_MAXOCCURS_2, elementName, new Integer(type.getMaxOccurs()))); } if (index > siblings.size()) { // index position behind last element of the list throw new CmsRuntimeException(Messages.get().container( Messages.ERR_XMLCONTENT_ADD_ELEM_INVALID_IDX_3, new Integer(index), new Integer(siblings.size()))); } // check for offset required to append beyond last position int offset = (index == siblings.size()) ? 1 : 0; // get the element from the parent at the selected position Element sibling = siblings.get(index - offset); // check position of the node in the parent node content insertIndex = sibling.getParent().content().indexOf(sibling) + offset; } else { // we want to add an element to a sequence, but there are no elements of the same type yet if (index > 0) { // since the element does not occur, index must be 0 throw new CmsRuntimeException(Messages.get().container( Messages.ERR_XMLCONTENT_ADD_ELEM_INVALID_IDX_2, new Integer(index), elementName)); } // check where in the type sequence the type should appear int typeIndex = contentDefinition.getTypeSequence().indexOf(type); if (typeIndex == 0) { // this is the first type, so we just add at the very first position insertIndex = 0; } else { // create a list of all element names that should occur before the selected type List<String> previousTypeNames = new ArrayList<String>(); for (int i = 0; i < typeIndex; i++) { I_CmsXmlSchemaType t = contentDefinition.getTypeSequence().get(i); previousTypeNames.add(t.getName()); } // iterate all elements of the parent node Iterator<Node> i = CmsXmlGenericWrapper.content(parentElement).iterator(); int pos = 0; while (i.hasNext()) { Node node = i.next(); if (node instanceof Element) { if (!previousTypeNames.contains(node.getName())) { // the element name is NOT in the list of names that occurs before the selected type, // so it must be an element that occurs AFTER the type break; } } pos++; } insertIndex = pos; } } I_CmsXmlContentValue newValue; if (contentDefinition.getChoiceMaxOccurs() > 0) { // for a choice we do a full XML validation try { // append the new element at the calculated position newValue = addValue(cms, parentElement, type, locale, insertIndex); // validate the XML structure to see if the index position was valid CmsXmlUtils.validateXmlStructure(m_document, m_encoding, new CmsXmlEntityResolver(cms)); } catch (Exception e) { throw new CmsRuntimeException(Messages.get().container( Messages.ERR_XMLCONTENT_ADD_ELEM_INVALID_IDX_CHOICE_3, new Integer(insertIndex), elementName, parentElement.getUniquePath())); } } else { // just append the new element at the calculated position newValue = addValue(cms, parentElement, type, locale, insertIndex); } // re-initialize this XML content initDocument(m_document, m_encoding, m_contentDefinition); // return the value instance that was stored in the bookmarks // just returning "newValue" isn't enough since this instance is NOT stored in the bookmarks return getBookmark(getBookmarkName(newValue.getPath(), locale)); } /** * Copies the content of the given source locale to the given destination locale in this XML document.<p> * * @param source the source locale * @param destination the destination loacle * @param elements the set of elements to copy * @throws CmsXmlException if something goes wrong */ public void copyLocale(Locale source, Locale destination, Set<String> elements) throws CmsXmlException { if (!hasLocale(source)) { throw new CmsXmlException(Messages.get().container( org.opencms.xml.Messages.ERR_LOCALE_NOT_AVAILABLE_1, source)); } if (hasLocale(destination)) { throw new CmsXmlException(Messages.get().container( org.opencms.xml.Messages.ERR_LOCALE_ALREADY_EXISTS_1, destination)); } Element sourceElement = null; Element rootNode = m_document.getRootElement(); Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(rootNode); String localeStr = source.toString(); while (i.hasNext()) { Element element = i.next(); String language = element.attributeValue(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE, null); if ((language != null) && (localeStr.equals(language))) { // detach node with the locale sourceElement = createDeepElementCopy(element, elements); // there can be only one node for the locale break; } } if (sourceElement == null) { // should not happen since this was checked already, just to make sure... throw new CmsXmlException(Messages.get().container( org.opencms.xml.Messages.ERR_LOCALE_NOT_AVAILABLE_1, source)); } // switch locale value in attribute of copied node sourceElement.addAttribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE, destination.toString()); // attach the copied node to the root node rootNode.add(sourceElement); // re-initialize the document bookmarks initDocument(m_document, m_encoding, getContentDefinition()); } /** * Returns the list of choice options for the given xpath in the selected locale.<p> * * In case the xpath does not select a nested choice content definition, * or in case the xpath does not exist at all, <code>null</code> is returned.<p> * * @param xpath the xpath to check the choice options for * @param locale the locale to check * * @return the list of choice options for the given xpath */ public List<I_CmsXmlSchemaType> getChoiceOptions(String xpath, Locale locale) { I_CmsXmlSchemaType type = m_contentDefinition.getSchemaType(xpath); if (type == null) { // the xpath is not valid in the document return null; } if (!type.isChoiceType() && !type.isChoiceOption()) { // type is neither defining a choice nor part of a choice return null; } if (type.isChoiceType()) { // the type defines a choice sequence CmsXmlContentDefinition cd = ((CmsXmlNestedContentDefinition)type).getNestedContentDefinition(); return cd.getTypeSequence(); } // type must be a choice option I_CmsXmlContentValue value = getValue(xpath, locale); if ((value == null) || (value.getContentDefinition().getChoiceMaxOccurs() > 1)) { // value does not exist in the document or is a multiple choice value return type.getContentDefinition().getTypeSequence(); } // value must be a single choice that already exists in the document, so we must return null return null; } /** * @see org.opencms.xml.I_CmsXmlDocument#getContentDefinition() */ public CmsXmlContentDefinition getContentDefinition() { return m_contentDefinition; } /** * @see org.opencms.xml.I_CmsXmlDocument#getHandler() */ public I_CmsXmlContentHandler getHandler() { return getContentDefinition().getContentHandler(); } /** * @see org.opencms.xml.A_CmsXmlDocument#getLinkProcessor(org.opencms.file.CmsObject, org.opencms.staticexport.CmsLinkTable) */ public CmsLinkProcessor getLinkProcessor(CmsObject cms, CmsLinkTable linkTable) { // initialize link processor String relativeRoot = null; if (m_file != null) { relativeRoot = CmsResource.getParentFolder(cms.getSitePath(m_file)); } return new CmsLinkProcessor(cms, linkTable, getEncoding(), relativeRoot); } /** * Returns the list of sub-value for the given xpath in the selected locale.<p> * * @param path the xpath to look up the sub-value for * @param locale the locale to use * * @return the list of sub-value for the given xpath in the selected locale */ @Override public List<I_CmsXmlContentValue> getSubValues(String path, Locale locale) { List<I_CmsXmlContentValue> result = new ArrayList<I_CmsXmlContentValue>(); String bookmark = getBookmarkName(CmsXmlUtils.createXpath(path, 1), locale); int depth = CmsResource.getPathLevel(bookmark) + 1; Iterator<String> i = getBookmarks().iterator(); while (i.hasNext()) { String bm = i.next(); if (bm.startsWith(bookmark) && (CmsResource.getPathLevel(bm) == depth)) { result.add(getBookmark(bm)); } } if (result.size() > 0) { Collections.sort(result, COMPARE_INDEX); } return result; } /** * @see org.opencms.xml.I_CmsXmlDocument#getValue(java.lang.String, java.util.Locale, int) */ @Override public I_CmsXmlContentValue getValue(String path, Locale locale, int index) { I_CmsXmlContentValue result = super.getValue(CmsXmlUtils.removeXpathIndex(path), locale, index); // check if the last node is a choice node if (((result == null) || result.isChoiceOption()) && CmsXmlUtils.isDeepXpath(path)) { String parentPath = CmsXmlUtils.removeLastXpathElement(path); I_CmsXmlContentValue v2 = super.getValue(parentPath, locale); if ((v2 != null) && v2.isChoiceType()) { // the parent is a choice node - check the sub-nodes using to the absolute index List<I_CmsXmlContentValue> values = getSubValues(parentPath, locale); if (index < values.size()) { I_CmsXmlContentValue v3 = values.get(index); if (v3.getName().equals(CmsXmlUtils.getLastXpathElement(path))) { // name of value at the position must match the provided name result = v3; } } } } return result; } /** * Returns the value sequence for the selected element xpath in this XML content.<p> * * If the given element xpath is not valid according to the schema of this XML content, * <code>null</code> is returned.<p> * * @param xpath the element xpath to get the value sequence for * @param locale the locale to get the value sequence for * * @return the value sequence for the selected element name in this XML content */ public CmsXmlContentValueSequence getValueSequence(String xpath, Locale locale) { I_CmsXmlSchemaType type = m_contentDefinition.getSchemaType(xpath); if (type == null) { return null; } return new CmsXmlContentValueSequence(xpath, locale, this); } /** * Returns <code>true</code> if choice options exist for the given xpath in the selected locale.<p> * * In case the xpath does not select a nested choice content definition, * or in case the xpath does not exist at all, <code>false</code> is returned.<p> * * @param xpath the xpath to check the choice options for * @param locale the locale to check * * @return <code>true</code> if choice options exist for the given xpath in the selected locale */ public boolean hasChoiceOptions(String xpath, Locale locale) { List<I_CmsXmlSchemaType> options = getChoiceOptions(xpath, locale); if ((options == null) || (options.size() <= 1)) { return false; } return true; } /** * @see org.opencms.xml.A_CmsXmlDocument#isAutoCorrectionEnabled() */ @Override public boolean isAutoCorrectionEnabled() { return m_autoCorrectionEnabled; } /** * Removes an existing XML content value of the given element name and locale at the given index position * from this XML content document.<p> * * @param name the name of the XML content value element * @param locale the locale where to remove the value * @param index the index where to remove the value (relative to all other values of this type) */ public void removeValue(String name, Locale locale, int index) { // first get the value from the selected locale and index I_CmsXmlContentValue value = getValue(name, locale, index); if (!value.isChoiceOption()) { // check for the min / max occurs constrains List<I_CmsXmlContentValue> values = getValues(name, locale); if (values.size() <= value.getMinOccurs()) { // must not allow removing an element if min occurs would be violated throw new CmsRuntimeException(Messages.get().container( Messages.ERR_XMLCONTENT_ELEM_MINOCCURS_2, name, new Integer(value.getMinOccurs()))); } } // detach the value node from the XML document value.getElement().detach(); // re-initialize this XML content initDocument(m_document, m_encoding, m_contentDefinition); } /** * Resolves the mappings for all values of this XML content.<p> * * @param cms the current users OpenCms context */ public void resolveMappings(CmsObject cms) { // iterate through all initialized value nodes in this XML content CmsXmlContentMappingVisitor visitor = new CmsXmlContentMappingVisitor(cms, this); visitAllValuesWith(visitor); } /** * Sets the flag to control if auto correction is enabled when saving this XML content.<p> * * @param value the flag to control if auto correction is enabled when saving this XML content */ public void setAutoCorrectionEnabled(boolean value) { m_autoCorrectionEnabled = value; } /** * @see org.opencms.xml.I_CmsXmlDocument#validate(org.opencms.file.CmsObject) */ public CmsXmlContentErrorHandler validate(CmsObject cms) { // iterate through all initialized value nodes in this XML content CmsXmlContentValidationVisitor visitor = new CmsXmlContentValidationVisitor(cms); visitAllValuesWith(visitor); return visitor.getErrorHandler(); } /** * Visits all values of this XML content with the given value visitor.<p> * * Please note that the order in which the values are visited may NOT be the * order they appear in the XML document. It is ensured that the the parent * of a nested value is visited before the element it contains.<p> * * @param visitor the value visitor implementation to visit the values with */ public void visitAllValuesWith(I_CmsXmlContentValueVisitor visitor) { List<String> bookmarks = new ArrayList<String>(getBookmarks()); Collections.sort(bookmarks); for (int i = 0; i < bookmarks.size(); i++) { String key = bookmarks.get(i); I_CmsXmlContentValue value = getBookmark(key); visitor.visit(value); } } /** * Creates a new bookmark for the given element.<p> * * @param element the element to create the bookmark for * @param locale the locale * @param parent the parent node of the element * @param parentPath the parent's path * @param parentDef the parent's content definition */ protected void addBookmarkForElement( Element element, Locale locale, Element parent, String parentPath, CmsXmlContentDefinition parentDef) { int elemIndex = CmsXmlUtils.getXpathIndexInt(element.getUniquePath(parent)); String elemPath = CmsXmlUtils.concatXpath(parentPath, CmsXmlUtils.createXpathElement( element.getName(), elemIndex)); I_CmsXmlSchemaType elemSchemaType = parentDef.getSchemaType(element.getName()); I_CmsXmlContentValue elemValue = elemSchemaType.createValue(this, element, locale); addBookmark(elemPath, locale, true, elemValue); } /** * Adds a bookmark for the given value.<p> * * @param value the value to bookmark * @param path the lookup path to use for the bookmark * @param locale the locale to use for the bookmark * @param enabled if true, the value is enabled, if false it is disabled */ protected void addBookmarkForValue(I_CmsXmlContentValue value, String path, Locale locale, boolean enabled) { addBookmark(path, locale, enabled, value); } /** * Adds a new XML schema type with the default value to the given parent node.<p> * * @param cms the cms context * @param parent the XML parent element to add the new value to * @param type the type of the value to add * @param locale the locale to add the new value for * @param insertIndex the index in the XML document where to add the XML node * * @return the created XML content value */ protected I_CmsXmlContentValue addValue( CmsObject cms, Element parent, I_CmsXmlSchemaType type, Locale locale, int insertIndex) { // first generate the XML element for the new value Element element = type.generateXml(cms, this, parent, locale); // detach the XML element from the appended position in order to insert it at the required position element.detach(); // add the XML element at the required position in the parent XML node CmsXmlGenericWrapper.content(parent).add(insertIndex, element); // create the type and return it I_CmsXmlContentValue value = type.createValue(this, element, locale); // generate the default value again - required for nested mappings because only now the full path is available String defaultValue = m_contentDefinition.getContentHandler().getDefault(cms, value, locale); if (defaultValue != null) { // only if there is a default value available use it to overwrite the initial default value.setStringValue(cms, defaultValue); } // finally return the value return value; } /** * @see org.opencms.xml.A_CmsXmlDocument#getBookmark(java.lang.String) */ @Override protected I_CmsXmlContentValue getBookmark(String bookmark) { // allows package classes to directly access the bookmark information of the XML content return super.getBookmark(bookmark); } /** * @see org.opencms.xml.A_CmsXmlDocument#getBookmarks() */ @Override protected Set<String> getBookmarks() { // allows package classes to directly access the bookmark information of the XML content return super.getBookmarks(); } /** * Returns the content definition object for this xml content object.<p> * * @param resolver the XML entity resolver to use, required for VFS access * * @return the content definition object for this xml content object * * @throws CmsRuntimeException if the schema location attribute (<code>systemId</code>)cannot be found, * parsing of the schema fails, an underlying IOException occurs or unmarshalling fails * */ protected CmsXmlContentDefinition getContentDefinition(EntityResolver resolver) throws CmsRuntimeException { String schemaLocation = m_document.getRootElement().attributeValue( I_CmsXmlSchemaType.XSI_NAMESPACE_ATTRIBUTE_NO_SCHEMA_LOCATION); // Note regarding exception handling: // Since this object already is a valid XML content object, // it must have a valid schema, otherwise it would not exist. // Therefore the exceptions should never be really thrown. if (schemaLocation == null) { throw new CmsRuntimeException(Messages.get().container(Messages.ERR_XMLCONTENT_MISSING_SCHEMA_0)); } try { return CmsXmlContentDefinition.unmarshal(schemaLocation, resolver); } catch (SAXException e) { throw new CmsRuntimeException(Messages.get().container(Messages.ERR_XML_SCHEMA_PARSE_1, schemaLocation), e); } catch (IOException e) { throw new CmsRuntimeException(Messages.get().container(Messages.ERR_XML_SCHEMA_IO_1, schemaLocation), e); } catch (CmsXmlException e) { throw new CmsRuntimeException( Messages.get().container(Messages.ERR_XMLCONTENT_UNMARSHAL_1, schemaLocation), e); } } /** * Returns the XML root element node for the given locale.<p> * * @param locale the locale to get the root element for * * @return the XML root element node for the given locale * * @throws CmsRuntimeException if no language element is found in the document */ protected Element getLocaleNode(Locale locale) throws CmsRuntimeException { String localeStr = locale.toString(); Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(m_document.getRootElement()); while (i.hasNext()) { Element element = i.next(); if (localeStr.equals(element.attributeValue(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE))) { // language element found, return it return element; } } // language element was not found throw new CmsRuntimeException(Messages.get().container(Messages.ERR_XMLCONTENT_MISSING_LOCALE_1, locale)); } /** * Initializes an XML document based on the provided document, encoding and content definition.<p> * * Checks the links and removes invalid ones in the initialized document.<p> * * @param cms the current users OpenCms content * @param document the base XML document to use for initializing * @param encoding the encoding to use when marshalling the document later * @param definition the content definition to use */ protected void initDocument(CmsObject cms, Document document, String encoding, CmsXmlContentDefinition definition) { initDocument(document, encoding, definition); // check invalid links if (cms != null) { // this will remove all invalid links getHandler().invalidateBrokenLinks(cms, this); } } /** * @see org.opencms.xml.A_CmsXmlDocument#initDocument(org.dom4j.Document, java.lang.String, org.opencms.xml.CmsXmlContentDefinition) */ @Override protected void initDocument(Document document, String encoding, CmsXmlContentDefinition definition) { m_document = document; m_contentDefinition = definition; m_encoding = CmsEncoder.lookupEncoding(encoding, encoding); m_elementLocales = new HashMap<String, Set<Locale>>(); m_elementNames = new HashMap<Locale, Set<String>>(); m_locales = new HashSet<Locale>(); clearBookmarks(); // initialize the bookmarks for (Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(m_document.getRootElement()); i.hasNext();) { Element node = i.next(); try { Locale locale = CmsLocaleManager.getLocale(node.attribute( CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE).getValue()); addLocale(locale); processSchemaNode(node, null, locale, definition); } catch (NullPointerException e) { LOG.error(Messages.get().getBundle().key(Messages.LOG_XMLCONTENT_INIT_BOOKMARKS_0), e); } } } /** * Processes a document node and extracts the values of the node according to the provided XML * content definition.<p> * * @param root the root node element to process * @param rootPath the Xpath of the root node in the document * @param locale the locale * @param definition the XML content definition to use for processing the values */ protected void processSchemaNode(Element root, String rootPath, Locale locale, CmsXmlContentDefinition definition) { // iterate all XML nodes List<Node> content = CmsXmlGenericWrapper.content(root); for (int i = content.size() - 1; i >= 0; i--) { Node node = content.get(i); if (!(node instanceof Element)) { // this node is not an element, so it must be a white space text node, remove it node.detach(); } else { // node must be an element Element element = (Element)node; String name = element.getName(); int xpathIndex = CmsXmlUtils.getXpathIndexInt(element.getUniquePath(root)); // build the Xpath expression for the current node String path; if (rootPath != null) { StringBuffer b = new StringBuffer(rootPath.length() + name.length() + 6); b.append(rootPath); b.append('/'); b.append(CmsXmlUtils.createXpathElement(name, xpathIndex)); path = b.toString(); } else { path = CmsXmlUtils.createXpathElement(name, xpathIndex); } // create a XML content value element I_CmsXmlSchemaType schemaType = definition.getSchemaType(name); if (schemaType != null) { // directly add simple type to schema I_CmsXmlContentValue value = schemaType.createValue(this, element, locale); addBookmark(path, locale, true, value); if (!schemaType.isSimpleType()) { // recurse for nested schema CmsXmlNestedContentDefinition nestedSchema = (CmsXmlNestedContentDefinition)schemaType; processSchemaNode(element, path, locale, nestedSchema.getNestedContentDefinition()); } } else { // unknown XML node name according to schema if (LOG.isWarnEnabled()) { LOG.warn(Messages.get().getBundle().key( Messages.LOG_XMLCONTENT_INVALID_ELEM_2, name, definition.getSchemaLocation())); } } } } } /** * Sets the file this XML content is written to.<p> * * @param file the file this XML content content is written to */ protected void setFile(CmsFile file) { m_file = file; } }