com.helger.xml.serialize.write.AbstractXMLSerializer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ph-xml Show documentation
Show all versions of ph-xml Show documentation
Java 1.8+ Library with XML handling routines
/*
* Copyright (C) 2014-2024 Philip Helger (www.helger.com)
* philip[at]helger[dot]com
*
* 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 com.helger.xml.serialize.write;
import java.io.OutputStream;
import java.io.Writer;
import java.util.Map;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.WillNotClose;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.OverrideOnDemand;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.CommonsHashMap;
import com.helger.commons.collection.impl.CommonsLinkedHashMap;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.collection.impl.ICommonsMap;
import com.helger.commons.collection.impl.ICommonsOrderedMap;
import com.helger.commons.io.stream.NonBlockingBufferedWriter;
import com.helger.commons.io.stream.StreamHelper;
import com.helger.commons.string.StringHelper;
import com.helger.commons.string.ToStringGenerator;
import com.helger.xml.XMLHelper;
import com.helger.xml.namespace.IIterableNamespaceContext;
/**
* Abstract XML serializer implementation that works with IMicroNode and
* org.w3c.dom.Node objects.
*
* @author Philip Helger
* @param
* The DOM node type to use
*/
public abstract class AbstractXMLSerializer
{
/**
* The prefix to be used for created namespace prefixes :) (e.g. for "ns0" or
* "ns1")
*/
public static final String DEFAULT_NAMESPACE_PREFIX_PREFIX = "ns";
private static final Logger LOGGER = LoggerFactory.getLogger (AbstractXMLSerializer.class);
/**
* Contains the XML namespace definitions for a single element.
*
* @author Philip Helger
*/
protected static final class NamespaceLevel
{
@SuppressWarnings ("hiding")
private static final Logger LOGGER = LoggerFactory.getLogger (NamespaceLevel.class);
private String m_sDefaultNamespaceURI;
private ICommonsMap m_aURL2PrefixMap;
/**
* Ctor
*/
public NamespaceLevel ()
{}
/**
* Get the URL matching a given namespace prefix in this level.
*
* @param sPrefix
* The prefix to be searched. If it is null
the default
* namespace URL is returned.
* @return null
if the namespace mapping is not used or the URL
* otherwise.
*/
@Nullable
public String getNamespaceURIOfPrefix (@Nullable final String sPrefix)
{
if (StringHelper.hasNoText (sPrefix))
return m_sDefaultNamespaceURI;
if (m_aURL2PrefixMap != null)
for (final Map.Entry aEntry : m_aURL2PrefixMap.entrySet ())
if (aEntry.getValue ().equals (sPrefix))
return aEntry.getKey ();
return null;
}
public void addPrefixNamespaceMapping (@Nullable final String sPrefix, @Nonnull final String sNamespaceURI)
{
if (LOGGER.isTraceEnabled ())
LOGGER.trace ("Adding namespace mapping " + sPrefix + ":" + sNamespaceURI);
// namespace prefix uniqueness check
final String sExistingNamespaceURI = getNamespaceURIOfPrefix (sPrefix);
if (sExistingNamespaceURI != null && !sExistingNamespaceURI.equals (sNamespaceURI))
LOGGER.warn ("Overwriting namespace prefix '" +
sPrefix +
"' to use URL '" +
sNamespaceURI +
"' instead of '" +
sExistingNamespaceURI +
"'");
if (StringHelper.hasNoText (sPrefix))
{
if (m_sDefaultNamespaceURI != null)
LOGGER.warn ("Overwriting default namespace '" +
m_sDefaultNamespaceURI +
"' with namespace '" +
sNamespaceURI +
"'");
m_sDefaultNamespaceURI = sNamespaceURI;
}
else
{
if (m_aURL2PrefixMap == null)
m_aURL2PrefixMap = new CommonsHashMap <> ();
m_aURL2PrefixMap.put (sNamespaceURI, sPrefix);
}
}
@Nullable
public String getDefaultNamespaceURI ()
{
return m_sDefaultNamespaceURI;
}
@Nullable
public String getPrefixOfNamespaceURI (@Nonnull final String sNamespaceURI)
{
// is it the default?
if (sNamespaceURI.equals (m_sDefaultNamespaceURI))
return null;
// Check in the map
return m_aURL2PrefixMap == null ? null : m_aURL2PrefixMap.get (sNamespaceURI);
}
@Nonnegative
public int getNamespaceCount ()
{
return (m_sDefaultNamespaceURI == null ? 0 : 1) + (m_aURL2PrefixMap == null ? 0 : m_aURL2PrefixMap.size ());
}
public boolean hasAnyNamespace ()
{
return m_sDefaultNamespaceURI != null || (m_aURL2PrefixMap != null && !m_aURL2PrefixMap.isEmpty ());
}
@Override
public String toString ()
{
return new ToStringGenerator (this).append ("defaultNSURI", m_sDefaultNamespaceURI)
.append ("url2prefix", m_aURL2PrefixMap)
.getToString ();
}
}
/**
* Contains the hierarchy of XML namespaces within a document structure.
* Important: null namespace URIs are different from empty namespace URIs!
*
* @author Philip Helger
*/
protected static final class NamespaceStack
{
private final ICommonsList m_aStack = new CommonsArrayList <> ();
private final NamespaceContext m_aNamespaceCtx;
public NamespaceStack (@Nonnull final NamespaceContext aNamespaceCtx)
{
m_aNamespaceCtx = aNamespaceCtx;
}
/**
* Start a new namespace level.
*/
public void push ()
{
final NamespaceLevel aNSL = new NamespaceLevel ();
// add at front
m_aStack.add (0, aNSL);
}
/**
* Add a new prefix-namespace URI mapping at the current stack level
*
* @param sPrefix
* Prefix to use. May be null
.
* @param sNamespaceURI
* Namespace URI to use. May neither be null
nor empty.
*/
public void addNamespaceMapping (@Nullable final String sPrefix, @Nonnull @Nonempty final String sNamespaceURI)
{
// Add the namespace to the current level
m_aStack.getFirstOrNull ().addPrefixNamespaceMapping (sPrefix, sNamespaceURI);
}
/**
* End the current namespace level.
*/
public void pop ()
{
// remove at front
m_aStack.removeFirstOrNull ();
}
@Nonnegative
public int size ()
{
return m_aStack.size ();
}
/**
* @return The namespace URI that is currently active in the stack. May be
* null
for no specific namespace.
*/
@Nullable
private String _getDefaultNamespaceURI ()
{
// iterate from front to end
for (final NamespaceLevel aNSLevel : m_aStack)
{
final String sDefaultNamespaceURI = aNSLevel.getDefaultNamespaceURI ();
if (StringHelper.hasText (sDefaultNamespaceURI))
return sDefaultNamespaceURI;
}
// no default namespace
return null;
}
/**
* Resolve the given namespace URI to a prefix using the known namespaces of
* this stack.
*
* @param sNamespaceURI
* The namespace URI to resolve. May not be null
. Pass
* in an empty string for an empty namespace URI!
* @return null
if no namespace prefix is required.
*/
@Nullable
private String _getUsedPrefixOfNamespace (@Nonnull final String sNamespaceURI)
{
ValueEnforcer.notNull (sNamespaceURI, "NamespaceURI");
// find existing prefix (iterate current to root)
for (final NamespaceLevel aNSLevel : m_aStack)
{
final String sPrefix = aNSLevel.getPrefixOfNamespaceURI (sNamespaceURI);
if (sPrefix != null)
return sPrefix;
}
// no matching prefix found
return null;
}
private boolean _containsNoNamespace ()
{
return m_aStack.containsNone (NamespaceLevel::hasAnyNamespace);
}
/**
* Check if the whole prefix is used somewhere in the stack.
*
* @param sPrefix
* The prefix to be checked
* @return true
if somewhere in the stack, the specified prefix
* is already used
*/
private boolean _containsNoPrefix (@Nonnull final String sPrefix)
{
// find existing prefix (iterate current to root)
return m_aStack.containsNone (x -> x.getNamespaceURIOfPrefix (sPrefix) != null);
}
/**
* Check if the passed namespace URI is mapped in the namespace context.
*
* @param sNamespaceURI
* The namespace URI to check. May not be null
.
* @return null
if no namespace context mapping is present
*/
@Nullable
private String _getMappedPrefix (@Nonnull final String sNamespaceURI)
{
ValueEnforcer.notNull (sNamespaceURI, "NamespaceURI");
// If a mapping is defined, it always takes precedence over the default
// namespace
if (m_aNamespaceCtx != null)
{
// Is a mapping defined?
final String sPrefix = m_aNamespaceCtx.getPrefix (sNamespaceURI);
if (sPrefix != null)
return sPrefix;
}
return null;
}
/**
* Create a new unique namespace prefix.
*
* @return null
or empty if the default namespace is available,
* the prefix otherwise.
*/
@Nullable
private String _createUniquePrefix ()
{
// Is the default namespace available?
if (_containsNoNamespace ())
{
// Use the default namespace
return null;
}
// find a unique prefix
int nCount = 0;
do
{
final String sNSPrefix = DEFAULT_NAMESPACE_PREFIX_PREFIX + nCount;
if (_containsNoPrefix (sNSPrefix))
return sNSPrefix;
++nCount;
} while (true);
}
@Nullable
public String getElementNamespacePrefixToUse (@Nonnull final String sNamespaceURI,
final boolean bIsRootElement,
@Nonnull final Map aAttrMap)
{
final String sDefaultNamespaceURI = StringHelper.getNotNull (_getDefaultNamespaceURI ());
if (sNamespaceURI.equals (sDefaultNamespaceURI))
{
// It's the default namespace
return null;
}
// Check if an existing prefix is in use
String sNSPrefix = _getUsedPrefixOfNamespace (sNamespaceURI);
// Do we need to create a prefix?
if (sNSPrefix == null && (!bIsRootElement || sNamespaceURI.length () > 0))
{
// Ensure to use the correct prefix (check if defined in namespace
// context)
sNSPrefix = _getMappedPrefix (sNamespaceURI);
// Do not create a prefix for the root element
if (sNSPrefix == null && !bIsRootElement)
sNSPrefix = _createUniquePrefix ();
// Add and remember the attribute
aAttrMap.put (XMLHelper.getXMLNSAttrQName (sNSPrefix), sNamespaceURI);
addNamespaceMapping (sNSPrefix, sNamespaceURI);
}
return sNSPrefix;
}
@Nullable
public String getAttributeNamespacePrefixToUse (@Nonnull final String sNamespaceURI,
@Nonnull final String sName,
@Nonnull final String sValue,
@Nonnull final Map aAttrMap)
{
final String sDefaultNamespaceURI = StringHelper.getNotNull (_getDefaultNamespaceURI ());
if (sNamespaceURI.equals (sDefaultNamespaceURI))
{
// It's the default namespace
return null;
}
String sNSPrefix = _getUsedPrefixOfNamespace (sNamespaceURI);
// Do we need to create a prefix?
if (sNSPrefix == null)
{
if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals (sNamespaceURI))
{
// It is an "xmlns:xyz" attribute
sNSPrefix = sName;
// Don't emit "xmlns:xmlns"
if (!XMLConstants.XMLNS_ATTRIBUTE.equals (sName))
{
// Add and remember the attribute
aAttrMap.put (XMLHelper.getXMLNSAttrQName (sNSPrefix), sValue);
}
addNamespaceMapping (sNSPrefix, sValue);
}
else
{
// Ensure to use the correct prefix (namespace context)
sNSPrefix = _getMappedPrefix (sNamespaceURI);
// Do not create a prefix for the root element
if (sNSPrefix == null)
{
if (sNamespaceURI.length () == 0)
{
// Don't create a namespace mapping for attributes without a
// namespace URI
return null;
}
sNSPrefix = _createUniquePrefix ();
}
// Don't emit "xmlns:xml"
if (!XMLConstants.XML_NS_PREFIX.equals (sNSPrefix))
{
// Add and remember the attribute
aAttrMap.put (XMLHelper.getXMLNSAttrQName (sNSPrefix), sNamespaceURI);
}
addNamespaceMapping (sNSPrefix, sNamespaceURI);
}
}
return sNSPrefix;
}
}
/**
* The serialization settings
*/
protected final IXMLWriterSettings m_aSettings;
/**
* helper variable: current indentation.
*/
protected final StringBuilder m_aIndent = new StringBuilder (32);
/**
* Current stack of namespaces.
*/
protected final NamespaceStack m_aNSStack;
private final ICommonsOrderedMap m_aRootNSMap = new CommonsLinkedHashMap <> ();
protected AbstractXMLSerializer (@Nonnull final IXMLWriterSettings aSettings)
{
m_aSettings = ValueEnforcer.notNull (aSettings, "Settings");
final NamespaceContext aNC = aSettings.getNamespaceContext ();
m_aNSStack = new NamespaceStack (aNC);
if (aSettings.isPutNamespaceContextPrefixesInRoot ())
{
if (aNC instanceof IIterableNamespaceContext)
{
// Put all on top-level
for (final Map.Entry aEntry : ((IIterableNamespaceContext) aNC).getPrefixToNamespaceURIMap ()
.entrySet ())
{
final String sNSPrefix = aEntry.getKey ();
final String sNamespaceURI = aEntry.getValue ();
m_aRootNSMap.put (sNSPrefix, sNamespaceURI);
}
}
else
LOGGER.error ("XMLWriter settings has 'putNamespaceContextPrefixesInRoot' set to 'true', but the 'NamespaceContext' instance does not implement the 'IIterableNamespaceContext' interface. This functionality therefore does not work.");
}
}
@Nonnull
public final IXMLWriterSettings getSettings ()
{
return m_aSettings;
}
/**
* This method handles the case, if all namespace context entries should be
* emitted on the root element.
*
* @param aAttrMap
* the attribute map to be filled. May not be null
.
*/
protected final void handlePutNamespaceContextPrefixInRoot (@Nonnull final Map aAttrMap)
{
if (m_aSettings.isEmitNamespaces () &&
m_aNSStack.size () == 1 &&
m_aSettings.isPutNamespaceContextPrefixesInRoot ())
{
// The only place where the namespace context prefixes are added to the
// root element
for (final Map.Entry aEntry : m_aRootNSMap.entrySet ())
{
aAttrMap.put (XMLHelper.getXMLNSAttrQName (aEntry.getKey ()), aEntry.getValue ());
m_aNSStack.addNamespaceMapping (aEntry.getKey (), aEntry.getValue ());
}
}
}
protected abstract void emitNode (@Nonnull final XMLEmitter aXMLWriter,
@Nullable final NODETYPE aParentNode,
@Nullable final NODETYPE aPrevSibling,
@Nonnull final NODETYPE aNode,
@Nullable final NODETYPE aNextSibling);
@Nonnull
@OverrideOnDemand
protected XMLEmitter createXMLEmitter (@Nonnull @WillNotClose final Writer aWriter,
@Nonnull final IXMLWriterSettings aSettings)
{
return new XMLEmitter (aWriter, aSettings);
}
public final void write (@Nonnull final NODETYPE aNode, @Nonnull final XMLEmitter aXMLEmitter)
{
// No parent node
// No previous and no next sibling
emitNode (aXMLEmitter, null, null, aNode, null);
}
/**
* Write the specified node to the specified {@link OutputStream}.
*
* @param aNode
* The node to write. May not be null
.
* @param aOS
* The stream to serialize onto. May not be null
.
*/
public final void write (@Nonnull final NODETYPE aNode, @Nonnull @WillNotClose final OutputStream aOS)
{
ValueEnforcer.notNull (aNode, "Node");
ValueEnforcer.notNull (aOS, "OutputStream");
// Create a writer for the passed output stream
final NonBlockingBufferedWriter aWriter = new NonBlockingBufferedWriter (StreamHelper.createWriter (aOS,
m_aSettings.getCharset ()));
// Inside the other write method, the writer must be flushed!
write (aNode, aWriter);
// Do not close the writer!
}
/**
* Write the specified node to the specified {@link Writer}.
*
* @param aNode
* The node to write. May not be null
.
* @param aWriter
* The writer to serialize onto. May not be null
.
*/
public final void write (@Nonnull final NODETYPE aNode, @Nonnull @WillNotClose final Writer aWriter)
{
final XMLEmitter aXMLWriter = createXMLEmitter (aWriter, m_aSettings);
// No parent node
// No previous and no next sibling
emitNode (aXMLWriter, null, null, aNode, null);
// Flush is important for Writer!
StreamHelper.flush (aWriter);
}
@Override
public String toString ()
{
return new ToStringGenerator (this).append ("Settings", m_aSettings)
.append ("Indent", m_aIndent.toString ())
.append ("NamespaceStack", m_aNSStack)
.getToString ();
}
}