/* * ModeShape (http://www.modeshape.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.modeshape.jcr; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.jcr.ItemVisitor; import javax.jcr.NamespaceRegistry; import javax.jcr.Node; import javax.jcr.RepositoryException; import org.modeshape.common.annotation.NotThreadSafe; import org.modeshape.common.text.TextEncoder; import org.modeshape.common.text.XmlNameEncoder; import org.modeshape.common.xml.StreamingContentHandler; import org.modeshape.jcr.cache.NodeKey; import org.modeshape.jcr.value.Name; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; /** * Superclass of ModeShape JCR exporters, provides basic support for traversing through the nodes recursively (if needed), * exception wrapping (since {@link ItemVisitor} does not allow checked exceptions to be thrown from its visit* methods, and the * ability to wrap an {@link OutputStream} with a {@link ContentHandler}. * <p /> * Each exporter is only intended to be used once (by calling <code>exportView</code>) and discarded. This class is <b>NOT</b> * thread-safe. * * @see JcrSystemViewExporter * @see JcrDocumentViewExporter */ @NotThreadSafe abstract class AbstractJcrExporter { /** * Encoder to properly escape XML names. * * @see XmlNameEncoder */ private static final TextEncoder NAME_ENCODER = new XmlNameEncoder(); /** * The list of XML namespaces that are predefined and should not be exported by the content handler. */ private static final List<String> UNEXPORTABLE_NAMESPACES = Arrays.asList("", "xml", "xmlns"); /** * The session in which this exporter was created. */ protected final JcrSession session; /** * The list of XML namespace prefixes that should never be exported. */ private final Collection<String> restrictedPrefixes; /** * Cache from {@link Name}s to their rewritten version based on session uri mappings. */ private final Map<Name, String> prefixedNames; /** * The set of node keys for the shareable nodes that were already exported. */ private final Set<NodeKey> shareableNodes = new HashSet<NodeKey>(); /** * Creates the exporter * * @param session the session in which the exporter is created * @param restrictedPrefixes the list of XML namespace prefixes that should not be exported */ AbstractJcrExporter( JcrSession session, Collection<String> restrictedPrefixes ) { this.session = session; this.restrictedPrefixes = restrictedPrefixes; this.prefixedNames = new HashMap<Name, String>(); } /** * Returns the "prefixed" or rewritten version of <code>baseName</code> based on the URI mappings in the current * session. For example:</p> If the namespace "http://www.example.com/JCR/example/1.0" is mapped to the prefix * "foo" in the current session (or as a persistent mapping that has not been re-mapped in the current session), * this method will return the string "foo:bar" when passed a {@link Name} with uri * "http://www.example.com/JCR/example/1.0" and local name "bar".</p> This method does manage and utilize * a {@link Name} to {@link String} cache at the instance scope. * * @param baseName the name to be re-mapped into its prefixed version * @return the prefixed version of <code>baseName</code> based on the current session URI mappings (which include all * persistent URI mappings by default). * @see #prefixedNames * @see javax.jcr.Session#setNamespacePrefix(String, String) * @see javax.jcr.Session#getNamespacePrefix(String) */ protected String getPrefixedName( Name baseName ) { String prefixedName = prefixedNames.get(baseName); if (prefixedName == null) { prefixedName = baseName.getString(session.namespaces()); prefixedNames.put(baseName, prefixedName); } return prefixedName; } protected void startDocument( ContentHandler handler ) throws SAXException { shareableNodes.clear(); handler.startDocument(); } protected void endDocument( ContentHandler handler ) throws SAXException { handler.endDocument(); } /** * Exports <code>node</code> (or the subtree rooted at <code>node</code>) into an XML document by invoking SAX events on * <code>contentHandler</code>. * * @param exportRootNode the node which should be exported. If <code>noRecursion</code> was set to <code>false</code> in the * constructor, the entire subtree rooted at <code>node</code> will be exported. * @param contentHandler the SAX content handler for which SAX events will be invoked as the XML document is created. * @param skipBinary if <code>true</code>, indicates that binary properties should not be exported * @param noRecurse if<code>true</code>, indicates that only the given node should be exported, otherwise a recursice export * and not any of its child nodes. * @throws SAXException if an exception occurs during generation of the XML document * @throws RepositoryException if an exception occurs accessing the content repository */ public void exportView( Node exportRootNode, ContentHandler contentHandler, boolean skipBinary, boolean noRecurse ) throws RepositoryException, SAXException { assert exportRootNode != null; assert contentHandler != null; session.checkLive(); // Export the namespace mappings used in this session NamespaceRegistry registry = session.getWorkspace().getNamespaceRegistry(); startDocument(contentHandler); String[] namespacePrefixes = registry.getPrefixes(); for (int i = 0; i < namespacePrefixes.length; i++) { String prefix = namespacePrefixes[i]; if (!restrictedPrefixes.contains(prefix)) { contentHandler.startPrefixMapping(prefix, registry.getURI(prefix)); } } exportNode(exportRootNode, contentHandler, skipBinary, noRecurse); for (int i = 0; i < namespacePrefixes.length; i++) { if (!restrictedPrefixes.contains(namespacePrefixes[i])) { contentHandler.endPrefixMapping(namespacePrefixes[i]); } } endDocument(contentHandler); } /** * Exports <code>node</code> (or the subtree rooted at <code>node</code>) into an XML document that is written to * <code>os</code>. * * @param node the node which should be exported. If <code>noRecursion</code> was set to <code>false</code> in the * constructor, the entire subtree rooted at <code>node</code> will be exported. * @param os the {@link OutputStream} to which the XML document will be written * @param skipBinary if <code>true</code>, indicates that binary properties should not be exported * @param noRecurse if<code>true</code>, indicates that only the given node should be exported, otherwise a recursive export * and not any of its child nodes. * @throws RepositoryException if an exception occurs accessing the content repository, generating the XML document, or * writing it to the output stream <code>os</code>. * @throws IOException if there is a problem writing to the supplied stream */ public void exportView( Node node, OutputStream os, boolean skipBinary, boolean noRecurse ) throws IOException, RepositoryException { try { exportView(node, new StreamingContentHandler(os, UNEXPORTABLE_NAMESPACES), skipBinary, noRecurse); os.flush(); } catch (SAXException se) { throw new RepositoryException(se); } } /** * Exports <code>node</code> (or the subtree rooted at <code>node</code>) into an XML document by invoking SAX events on * <code>contentHandler</code>. * * @param node the node which should be exported. If <code>noRecursion</code> was set to <code>false</code> in the * constructor, the entire subtree rooted at <code>node</code> will be exported. * @param contentHandler the SAX content handler for which SAX events will be invoked as the XML document is created. * @param skipBinary if <code>true</code>, indicates that binary properties should not be exported * @param noRecurse if<code>true</code>, indicates that only the given node should be exported, otherwise a recursive export * and not any of its child nodes. * @throws SAXException if an exception occurs during generation of the XML document * @throws RepositoryException if an exception occurs accessing the content repository */ public abstract void exportNode( Node node, ContentHandler contentHandler, boolean skipBinary, boolean noRecurse ) throws RepositoryException, SAXException; /** * Convenience method to invoke the {@link ContentHandler#startElement(String, String, String, Attributes)} method on the * given content handler. The name will be encoded to properly escape invalid XML characters. * * @param contentHandler the content handler on which the <code>startElement</code> method should be invoked. * @param name the un-encoded, un-prefixed name of the element to start * @param atts the attributes that should be created for the given element * @throws SAXException if there is an error starting the element */ protected void startElement( ContentHandler contentHandler, Name name, Attributes atts ) throws SAXException { contentHandler.startElement(name.getNamespaceUri(), NAME_ENCODER.encode(name.getLocalName()), NAME_ENCODER.encode(getPrefixedName(name)), atts); } /** * Convenience method to invoke the {@link ContentHandler#endElement(String, String, String)} method on the given content * handler. The name will be encoded to properly escape invalid XML characters. * * @param contentHandler the content handler on which the <code>endElement</code> method should be invoked. * @param name the un-encoded, un-prefixed name of the element to end * @throws SAXException if there is an error ending the element */ protected void endElement( ContentHandler contentHandler, Name name ) throws SAXException { contentHandler.endElement(name.getNamespaceUri(), NAME_ENCODER.encode(name.getLocalName()), NAME_ENCODER.encode(getPrefixedName(name))); } protected void exporting( Node node ) throws RepositoryException { if (node instanceof AbstractJcrNode) { AbstractJcrNode jcrNode = (AbstractJcrNode)node; if (jcrNode.isShareable()) { shareableNodes.add(jcrNode.key()); } } } protected JcrSharedNode asSharedNode( Node node ) { if (node instanceof JcrSharedNode) { JcrSharedNode sharedNode = (JcrSharedNode)node; NodeKey shareableKey = sharedNode.key(); // See if the node is already exported ... if (!shareableNodes.add(shareableKey)) { // Already saw it, so return the shared node ... return sharedNode; } } // Otherwise, it's not shareable or it is but hasn't yet been seen, so return null so that it's treated like a regular // node return null; } }