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:
2025-10-24 16:53:24 -07:00
parent 49cbbf691d
commit 2269b610bc

View 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 );
}
}
}
}