Classes utilitaire qui fournit des méthodes pour extraire des données à partir d'objets XML Document et créer des représentations textuelles de ces objets.
This commit is contained in:
457
src/main/java/common/XmlUtils.java
Normal file
457
src/main/java/common/XmlUtils.java
Normal file
@@ -0,0 +1,457 @@
|
||||
package common;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.text.SimpleDateFormat;
|
||||
import org.w3c.dom.*;
|
||||
|
||||
import javax.xml.namespace.NamespaceContext;
|
||||
import javax.xml.transform.OutputKeys;
|
||||
import javax.xml.transform.Transformer;
|
||||
import javax.xml.transform.TransformerException;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import javax.xml.xpath.XPath;
|
||||
import javax.xml.xpath.XPathConstants;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Utility methods for XML parsing and manipulation.
|
||||
*/
|
||||
public class XmlUtils
|
||||
{
|
||||
public static final String ADOBE_ADEPT_NS = "http://ns.adobe.com/adept";
|
||||
// static because the methods in this class are all static
|
||||
private static final Logger LOGGER = Logger.getLogger( XmlUtils.class.getName() );
|
||||
|
||||
public static class AdeptNamespaceContext implements NamespaceContext
|
||||
{
|
||||
private final Document adeptDoc;
|
||||
|
||||
public AdeptNamespaceContext( Document doc )
|
||||
{
|
||||
adeptDoc = doc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNamespaceURI( String s )
|
||||
{
|
||||
return ADOBE_ADEPT_NS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrefix( String s )
|
||||
{
|
||||
return adeptDoc.lookupPrefix( s );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> getPrefixes( String s )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts text content of a given XML element in an XMLDocument by element name using XPath.
|
||||
*
|
||||
* @param doc The XML document.
|
||||
* @param xPathExpression The XPath expression to the element.
|
||||
* @param required If true, throws common.GourouException if element not found.
|
||||
*
|
||||
* @return An empty string if the XPath expressions is not found and is required.
|
||||
* For TEXT, CDATA and COMMENT nodes, returns the text content of the element; for
|
||||
* ATTR nodes, returns the attribute value, and for all others an empty string.
|
||||
*
|
||||
* @throws GourouException if the XPath expression is invalid or element not found (if required).
|
||||
*/
|
||||
public static String extractTextElem( Document doc, String xPathExpression, boolean required )
|
||||
{
|
||||
try
|
||||
{
|
||||
XPath xpath = XPathFactory.newInstance().newXPath();
|
||||
xpath.setNamespaceContext( new AdeptNamespaceContext( doc ) );
|
||||
Node node = (Node) xpath.evaluate( xPathExpression, doc, XPathConstants.NODE );
|
||||
if (node != null && node.getFirstChild() != null)
|
||||
{
|
||||
String value = node.getFirstChild().getNodeValue();
|
||||
if (null == value)
|
||||
return "";
|
||||
return value;
|
||||
}
|
||||
if (required)
|
||||
{
|
||||
throw new GourouException( "Mandatory XML element not found for XPath: " + xPathExpression );
|
||||
}
|
||||
return "";
|
||||
}
|
||||
catch (XPathExpressionException e)
|
||||
{
|
||||
LOGGER.log( Level.SEVERE, "XPath expression error: " + xPathExpression, e );
|
||||
throw new GourouException( "Invalid XPath expression: " + xPathExpression, e );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts text content of an XML element using XPath (default required to true).
|
||||
*
|
||||
* @param doc The XML document.
|
||||
* @param xPathExpression The XPath expression to the element.
|
||||
* @return The text content of the element.
|
||||
* @throws GourouException if the XPath expression is invalid or element not found.
|
||||
*/
|
||||
public static String extractTextElem( Document doc, String xPathExpression )
|
||||
{
|
||||
return extractTextElem( doc, xPathExpression, true );
|
||||
}
|
||||
|
||||
public static Element getChildElement( Element parentElement, String tagName )
|
||||
{
|
||||
NodeList els = parentElement.getElementsByTagName( tagName );
|
||||
if (0 <els.getLength())
|
||||
return (Element) els.item( 0 );
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text content from a specific child element within a parent Element
|
||||
*
|
||||
* @param parentElement The XML document element to search
|
||||
* @param tagName The name of the tag sought. If there are multiple elements with the same
|
||||
* name the text contents of the first child will be returned
|
||||
* @param throwOnNull if true, a Gourou exception will be thrown if no matching child can be found
|
||||
* @return The text content of the first matching child, or the empty string if not found
|
||||
* @throws GourouException if throwOnNull is true and the child node cannot be found
|
||||
*/
|
||||
public static String getChildElementText( Element parentElement, String tagName, boolean throwOnNull )
|
||||
throws GourouException
|
||||
{
|
||||
String localName = tagName.contains( ":" ) ? tagName.split( ":" )[1] : tagName;
|
||||
NodeList nodes = parentElement.getElementsByTagNameNS( ADOBE_ADEPT_NS, localName );
|
||||
if (nodes.getLength() > 0 && nodes.item( 0 ).getFirstChild() != null)
|
||||
{
|
||||
// if the node has multiple child text elements, this will collapse them.
|
||||
return nodes.item( 0 ).getTextContent().trim();
|
||||
}
|
||||
else if (throwOnNull)
|
||||
{
|
||||
throw new GourouException(
|
||||
"Missing XML element: " + tagName + " within " + parentElement.getNodeName() );
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extracts an attribute value from an XML element using XPath.
|
||||
*
|
||||
* @param doc The XML document.
|
||||
* @param xPathExpression The XPath expression to the element.
|
||||
* @param attributeName The name of the attribute.
|
||||
* @param mandatory If true, throws common.GourouException if element/attribute not found.
|
||||
* @return The attribute value, or empty string if not found and not mandatory.
|
||||
* @throws GourouException if the XPath expression is invalid or element/attribute not found (if mandatory).
|
||||
*/
|
||||
public static String extractTextAttribute( Element doc, String xPathExpression, String attributeName, boolean mandatory )
|
||||
throws GourouException
|
||||
{
|
||||
try
|
||||
{
|
||||
XPath xpath = XPathFactory.newInstance().newXPath();
|
||||
Node node = (Node) xpath.evaluate( xPathExpression, doc, XPathConstants.NODE );
|
||||
if (node != null && node.getNodeType() == Node.ELEMENT_NODE)
|
||||
{
|
||||
Element element = (Element) node;
|
||||
if (element.hasAttribute( attributeName ))
|
||||
{
|
||||
return element.getAttribute( attributeName );
|
||||
}
|
||||
}
|
||||
if (mandatory)
|
||||
{
|
||||
throw new GourouException(
|
||||
"Mandatory XML attribute '" + attributeName + "' not found for XPath: " + xPathExpression );
|
||||
}
|
||||
return "";
|
||||
}
|
||||
catch (XPathExpressionException e)
|
||||
{
|
||||
LOGGER.log( Level.SEVERE, "XPath expression error: " + xPathExpression, e );
|
||||
throw new GourouException( "Invalid XPath expression: " + xPathExpression, e );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a text element to a parent XML node.
|
||||
*
|
||||
* @param parent The parent XML element.
|
||||
* @param tagName The tag name of the new element.
|
||||
* @param textContent The text content for the new element.
|
||||
*
|
||||
* @return the Element created (in case it needs attributes)
|
||||
*/
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public static Element appendTextElem( Element parent, String tagName, String textContent )
|
||||
{
|
||||
Document doc = parent.getOwnerDocument();
|
||||
// Use createElementNS to ensure correct namespace for adept: tags
|
||||
Element newElement = doc.createElementNS( ADOBE_ADEPT_NS, tagName );
|
||||
newElement.appendChild( doc.createTextNode( textContent ) );
|
||||
parent.appendChild( newElement );
|
||||
return newElement;
|
||||
}
|
||||
|
||||
public static String collapseLines( String xmlStr )
|
||||
{
|
||||
String oldText;
|
||||
// fix windows CRLF fuck-ups
|
||||
Pattern p = Pattern.compile( "\r\n", Pattern.MULTILINE );
|
||||
do
|
||||
{
|
||||
oldText = xmlStr;
|
||||
Matcher m = p.matcher( xmlStr );
|
||||
xmlStr = m.replaceAll( "\n" );
|
||||
}
|
||||
while (!xmlStr.equals( oldText ));
|
||||
|
||||
// A new line character followed by another line that is exclusively whitespace...
|
||||
p = Pattern.compile( "\n[\\s]*$", Pattern.MULTILINE );
|
||||
do
|
||||
{
|
||||
oldText = xmlStr;
|
||||
Matcher m = p.matcher( xmlStr );
|
||||
xmlStr = m.replaceAll( "" );
|
||||
}
|
||||
while (!xmlStr.equals( oldText ));
|
||||
return xmlStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an XML Document to a formatted String.
|
||||
*
|
||||
* @param doc The XML Document to convert.
|
||||
* @param omitXmlDeclaration a flag to supress the xml declaration at the beginning of the document
|
||||
* @return A string representation of the XML.
|
||||
* @throws GourouException if there's an error during transformation.
|
||||
*/
|
||||
public static String docToString( Document doc, boolean omitXmlDeclaration )
|
||||
{
|
||||
System.setProperty("line.separator", "\n");
|
||||
try
|
||||
{
|
||||
TransformerFactory transformerFactory = TransformerFactory.newInstance();
|
||||
Transformer transformer = transformerFactory.newTransformer();
|
||||
transformer.setOutputProperty( OutputKeys.INDENT, "yes" );
|
||||
transformer.setOutputProperty( "{http://xml.apache.org/xslt}indent-amount", "2" );
|
||||
transformer.setOutputProperty( OutputKeys.ENCODING, "UTF-8" ); // Ensure UTF-8 output
|
||||
if (omitXmlDeclaration)
|
||||
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
|
||||
StringWriter writer = new StringWriter();
|
||||
transformer.transform( new DOMSource( doc ), new StreamResult( writer ) );
|
||||
String xmlStr = writer.toString();
|
||||
// Java's XML Transformer has a tendency to create unnecessary blonk lines; use a regex function to remove them.
|
||||
return collapseLines( xmlStr );
|
||||
}
|
||||
catch (TransformerException e)
|
||||
{
|
||||
LOGGER.log( Level.SEVERE, "Error converting XML document to string", e );
|
||||
throw new GourouException( "Error generating XML string", e );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single node based on an XPath expression.
|
||||
*
|
||||
* @param doc The XML document.
|
||||
* @param xPathExpression The XPath expression.
|
||||
* @param mandatory If true, throws common.GourouException if node not found.
|
||||
* @return The found Node, or null if not found and not mandatory.
|
||||
*/
|
||||
public static Node getNode( Document doc, String xPathExpression, boolean mandatory )
|
||||
{
|
||||
try
|
||||
{
|
||||
XPath xpath = XPathFactory.newInstance().newXPath();
|
||||
xpath.setNamespaceContext( new AdeptNamespaceContext( doc ) );
|
||||
Node node = (Node) xpath.evaluate( xPathExpression, doc, XPathConstants.NODE );
|
||||
if (node == null && mandatory)
|
||||
{
|
||||
throw new GourouException( "Mandatory XML node not found for XPath: " + xPathExpression );
|
||||
}
|
||||
return node;
|
||||
}
|
||||
catch (XPathExpressionException e)
|
||||
{
|
||||
LOGGER.log( Level.SEVERE, "XPath expression error: " + xPathExpression, e );
|
||||
throw new GourouException( "Invalid XPath expression: " + xPathExpression, e );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an `adept:nonce` and `adept:expiration` element to an XML node.
|
||||
*
|
||||
* @param root The XML element to add nonce to.
|
||||
*/
|
||||
public static void addNonce( Element root )
|
||||
{
|
||||
long timeSecs = System.currentTimeMillis() / 1000; // current time in seconds
|
||||
long timeUsec = (System.currentTimeMillis() % 1000) * 1000; // microsecond portion only of current time
|
||||
|
||||
// The constants 0x6f046000 and 0x388a are specific to the C++ implementation.
|
||||
// Their exact meaning and generation is unknown; use as is.
|
||||
int[] nonce32 = {0x6f046000, 0x388a}; // Use long for unsigned 32-bit values
|
||||
|
||||
long bigtime = timeSecs * 1000; // this now becomes the current time in milliseconds.
|
||||
// #ifdef STATIC_NONCE - skip for now, use dynamic time
|
||||
// bigtime = 0xAA001122BBCCAAL;
|
||||
|
||||
// timeUsec is the microsecond portion of current time; timeUsec / 1000 converts it back to milliseconds.
|
||||
nonce32[0] += (int) ((bigtime & 0xFFFFFFFFL) + (timeUsec / 1000L)); // milliseconds
|
||||
nonce32[1] += (int) ((bigtime >>> 32) & 0xFFFFFFFFL); // seconds
|
||||
|
||||
// Convert long[] (two 32-bit values) to byte array (8 bytes total), and append 4 null bytes
|
||||
// Create a ByteBuffer from the nonce32 byte array
|
||||
ByteBuffer buffer = ByteBuffer.allocate( 12 );
|
||||
|
||||
// I suspect little-endian because Adobe favors Windows
|
||||
buffer.order( ByteOrder.LITTLE_ENDIAN ).putInt( nonce32[0] ).putInt( nonce32[1] );
|
||||
byte[] bytes = buffer.array();
|
||||
String b64 = Base64.getEncoder().encodeToString( bytes );
|
||||
XmlUtils.appendTextElem( root, "adept:nonce", b64 );
|
||||
|
||||
long expirationTimeMillis = System.currentTimeMillis() + 30 * 60 * 1000; // Cur time + 10 minutes
|
||||
Date expirationDate = new Date( expirationTimeMillis );
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss'Z'" );
|
||||
sdf.setTimeZone( TimeZone.getTimeZone( "UTC" ) ); // gmtime equivalent
|
||||
String expiration = sdf.format( expirationDate );
|
||||
|
||||
XmlUtils.appendTextElem( root, "adept:expiration", expiration );
|
||||
}
|
||||
|
||||
// ASN tags for XML hashing
|
||||
private static final byte ASN_NONE = 0x00;
|
||||
private static final byte ASN_NS_TAG = 0x01;
|
||||
private static final byte ASN_CHILD = 0x02;
|
||||
private static final byte ASN_END_TAG = 0x03;
|
||||
private static final byte ASN_TEXT = 0x04;
|
||||
private static final byte ASN_ATTRIBUTE = 0x05;
|
||||
|
||||
private static void pushString( String string, ByteArrayOutputStream bytes )
|
||||
{
|
||||
byte[] stringBytes = string.getBytes( StandardCharsets.UTF_8 );
|
||||
short length = (short) stringBytes.length;
|
||||
|
||||
// Convert short length to a byte array in Adobe byte order (little-endian)
|
||||
byte[] nlength = new byte[2];
|
||||
nlength[0] = (byte) ((length >> 8) & 0xFF);
|
||||
nlength[1] = (byte) (length & 0xFF);
|
||||
|
||||
bytes.writeBytes( nlength );
|
||||
bytes.writeBytes( stringBytes );
|
||||
String info = String.format( "%02x %02x %s", nlength[0], nlength[1], string );
|
||||
LOGGER.log( Level.FINEST, info );
|
||||
}
|
||||
|
||||
public static void xml2ASN( Node node, Map<String, String> nsDict, ByteArrayOutputStream bytes )
|
||||
{
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE)
|
||||
{
|
||||
Element element = (Element) node;
|
||||
String nodeNameWithoutPrefix = element.getTagName(); // This includes prefix if present
|
||||
String nodePrefix = null;
|
||||
int colonIndex = nodeNameWithoutPrefix.indexOf( ':' );
|
||||
if (colonIndex != -1)
|
||||
{
|
||||
nodePrefix = nodeNameWithoutPrefix.substring( 0, colonIndex );
|
||||
nodeNameWithoutPrefix = nodeNameWithoutPrefix.substring( colonIndex + 1 );
|
||||
}
|
||||
|
||||
// Look for "xmlns[:]" attribute
|
||||
String nsUri = element.getNamespaceURI();
|
||||
if (null != nsUri && !nsUri.isEmpty())
|
||||
{
|
||||
String nsPrefix;
|
||||
if (null != nodePrefix && !nodePrefix.isEmpty())
|
||||
{
|
||||
nsPrefix = nodePrefix;
|
||||
}
|
||||
else
|
||||
{
|
||||
nsPrefix = "GENERICNS"; // Default for xmlns="..."
|
||||
}
|
||||
nsDict.put( nsPrefix, nsUri );
|
||||
}
|
||||
|
||||
if (nodePrefix != null && nsDict.containsKey( nodePrefix ))
|
||||
{
|
||||
bytes.write( ASN_NS_TAG );
|
||||
pushString( nsDict.get( nodePrefix ), bytes );
|
||||
}
|
||||
else if (nsDict.containsKey( "GENERICNS" ))
|
||||
{
|
||||
// Only if the element itself is in the default namespace (no prefix)
|
||||
if ( nodePrefix == null && element.getNamespaceURI() != null
|
||||
&& element.getNamespaceURI().equals( nsDict.get( "GENERICNS" )))
|
||||
{
|
||||
bytes.write( ASN_NS_TAG );
|
||||
pushString( nsDict.get( "GENERICNS" ), bytes );
|
||||
}
|
||||
}
|
||||
|
||||
pushString( nodeNameWithoutPrefix, bytes );
|
||||
|
||||
NamedNodeMap attributes = element.getAttributes();
|
||||
List<String> attributeNames = new ArrayList<>();
|
||||
for (int i = 0; i < attributes.getLength(); i++)
|
||||
{
|
||||
Node attr = attributes.item( i );
|
||||
if (!attr.getNodeName().startsWith( "xmlns" ))
|
||||
{ // Exclude xmlns attributes themselves
|
||||
attributeNames.add( attr.getNodeName() );
|
||||
}
|
||||
}
|
||||
Collections.sort( attributeNames ); // Attributes must be handled in alphabetical order
|
||||
|
||||
for (String attrName : attributeNames)
|
||||
{
|
||||
String attrValue = element.getAttribute( attrName );
|
||||
bytes.write( ASN_ATTRIBUTE );
|
||||
pushString( "", bytes );
|
||||
pushString( attrName, bytes );
|
||||
pushString( attrValue, bytes );
|
||||
}
|
||||
bytes.write( ASN_CHILD );
|
||||
|
||||
NodeList children = element.getChildNodes();
|
||||
for (int i = 0; i < children.getLength(); i++)
|
||||
{
|
||||
Node child = children.item( i );
|
||||
xml2ASN( child, nsDict, bytes ); // Recursive call
|
||||
}
|
||||
bytes.write( ASN_END_TAG );
|
||||
}
|
||||
else if ( node.getNodeType() == Node.TEXT_NODE
|
||||
|| node.getNodeType() == Node.CDATA_SECTION_NODE)
|
||||
{
|
||||
if (null == node.getNodeValue())
|
||||
return;
|
||||
String trimmed = node.getNodeValue().trim();
|
||||
if (!trimmed.isEmpty())
|
||||
{
|
||||
bytes.write( ASN_TEXT );
|
||||
pushString( trimmed, bytes );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user