Skip to content

Instantly share code, notes, and snippets.

@tmclnk
Last active December 7, 2017 20:00
Show Gist options
  • Save tmclnk/777fdb161c6772d60766b6e490040210 to your computer and use it in GitHub Desktop.
Save tmclnk/777fdb161c6772d60766b6e490040210 to your computer and use it in GitHub Desktop.
package be.shouldyou;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.xml.bind.DatatypeConverter;
import javax.xml.namespace.QName;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* A stateful wrapper class around {@link XPath} instances, used to
* perform repeated queries against a single {@link Node}.
* All {@link XPathExpressionException}s are rethrown as unchecked exceptions.
* Type conversions are done with {@link DatatypeConverter}.
* <br/>
* Examples:
* <code>
* <pre>
* XpathConverter xpath = new XpathConverter(payloadXML);
* String submissionId = xpath.asString("IRSDataForStateSubmission/SubmissionId");
* BigInteger submissionId = xpath.asBigInteger("IRSDataForStateSubmission/EID");
* Calendar recvd = xpath.asDateTime("IRSDataForStateSubmission/ReceivedAtIRSTs");
* </pre>
* </code>
*
* Using {@link #with(String)}
* <code>
* <pre>
* Document stateXML = sa.getStateXML(false);
* XpathConverter xpath = new XpathConverter(stateXML);
* xpath.with("ReturnState/ReturnDataState/FormNE1120SN");
* BigInteger neIncTaxWithNonResAmt = xpath.asBigInteger("NeIncTaxWithNonResAmt");
* BigInteger f3800NCreditOrRecaptureAmt = xpath.asBigInteger("F3800NCreditOrRecaptureAmt");
* BigInteger taxDepositedExtOrEstPytAmt = xpath.asBigInteger("TaxDepositedExtOrEstPytAmt");
* </pre>
* </code>
*/
public class XpathWrapper {
@SuppressWarnings("unused")
private static final Logger logger = LoggerFactory.getLogger(XpathWrapper.class);
private final XPath delegate = XPathFactory.newInstance().newXPath();
private final Node node;
private Node withNode;
/**
* @param node {@link Node} to which all later xpath expressions will
* apply. The namespaciness of this node determines the types of
* expressions that should be used.
*/
public XpathWrapper(Node node) {
this.node = node;
this.withNode = node;
}
/**
* A convenience method which will perform all subsequent evaluations
* against the given node.
*
* The "with" idiom is derived from the the language construct
* in other languages.
* This can be reset using {@code with("")} or {@code with(null)}
* @param with the prefix to apply to all subsequent calls; null is ok here
* @return the value of the expression, possibly null
*/
public Node with(String expression) {
if(expression == null || expression.isEmpty()){
withNode = node;
return withNode;
}
try {
withNode = (Node)delegate.evaluate(expression, this.node, XPathConstants.NODE);
return withNode;
} catch (XPathExpressionException e) {
throw new RuntimeException("Failed to evaluate " + expression, e);
}
}
public void reset(){
with(null);
}
/**
* <p>Evaluate an <code>XPath</code> expression in the specified context and return the result as the specified type.</p>
*
* <p>See <a href="#XPath-evaluation">Evaluation of XPath Expressions</a> for context item evaluation,
* variable, function and <code>QName</code> resolution and return type conversion.</p>
*
* <p>If <code>returnType</code> is not one of the types defined in {@link XPathConstants} (
* {@link XPathConstants#NUMBER NUMBER},
* {@link XPathConstants#STRING STRING},
* {@link XPathConstants#BOOLEAN BOOLEAN},
* {@link XPathConstants#NODE NODE} or
* {@link XPathConstants#NODESET NODESET})
* then an <code>IllegalArgumentException</code> is thrown.</p>
*
* <p>If a <code>null</code> value is provided for
* <code>item</code>, an empty document will be used for the
* context.
* If <code>expression</code> or <code>returnType</code> is <code>null</code>, then a
* <code>NullPointerException</code> is thrown.</p>
*
* @param expression The XPath expression.
* @param returnType The desired return type.
*
* @return Result of evaluating an XPath expression as an <code>Object</code> of <code>returnType</code>.
*
* @throws XPathExpressionException If <code>expression</code> cannot be evaluated.
* @throws IllegalArgumentException If <code>returnType</code> is not one of the types defined in {@link XPathConstants}.
* @throws NullPointerException If <code>expression</code> or <code>returnType</code> is <code>null</code>.
*/
public Object evaluate(String expression, QName returnType) {
try {
return delegate.evaluate(expression, withNode, returnType);
} catch (XPathExpressionException e) {
throw new RuntimeException("Failed to evaluate " + expression, e);
}
}
/**
* Evaluates the expression as a Boolean. This can be
* checking for the presence of a node, e.g. "/Path/To/CheckboxInd"
* It may also explicit xpath boolean functions. Note that
* this is *different* from parsing a text value like "true"!
*
* Any
* {@link XPathExpressionException}s thrown will be wrapped
* as unchecked exceptions.
* @param expression xpath expression
* @return {@link Boolean}
*/
public boolean asBoolean(String expression){
try {
return (Boolean) delegate.evaluate(expression, withNode, XPathConstants.BOOLEAN);
} catch (XPathExpressionException e) {
throw new RuntimeException("Failed to evaluate " + expression, e);
}
}
/**
* Parses the {@code xsd:boolean} value from the given expression.
* This is much different than {@link #asBoolean(String)}!
* @param expression
* @return whether or not the expression parsed into true or false,
* based on the definition of {@code xsd:boolean};
*/
public boolean toBoolean(String expression){
try {
return DatatypeConverter.parseBoolean(delegate.evaluate(expression, withNode));
} catch (XPathExpressionException e) {
throw new RuntimeException("Failed to evaluate " + expression, e);
}
}
/**
* Evaluates the given expression to a NodeList. Any
* {@link XPathExpressionException}s thrown will be wrapped
* as unchecked exceptions.
* @param expression
* @return NodeList (possibly empty)
*/
public NodeList asNodeList(String expression){
try {
return (NodeList) delegate.evaluate(expression, withNode, XPathConstants.NODESET);
} catch (XPathExpressionException e) {
throw new RuntimeException("Failed to evaluate " + expression, e);
}
}
public List<String> asList(String expression){
List<String> list = new ArrayList<>();
NodeList nodeList = asNodeList(expression);
for( int i = 0; i < nodeList.getLength(); i++){
list.add(nodeList.item(i).getTextContent());
}
return list;
}
public <T> List<T> asList(String expression, Function<Node, T> callback){
List<T> list = new ArrayList<>();
NodeList nodeList = asNodeList(expression);
for( int i = 0; i < nodeList.getLength(); i++){
Node node = nodeList.item(i);
T val = callback.apply(node);
list.add(val);
}
return list;
}
public Stream<Node> stream(String expression){
NodeList nodes = asNodeList(expression);
Stream.Builder<Node> builder = Stream.builder();
for(int i = 0; i < nodes.getLength(); i++){
Node n = nodes.item(i);
builder.accept(n);
}
return builder.build();
}
public <T> Stream<T> map(String expression, Function<Node, T> callback){
NodeList nodes = asNodeList(expression);
Stream.Builder<T> builder = Stream.builder();
for(int i = 0; i < nodes.getLength(); i++){
Node n = nodes.item(i);
T val = callback.apply(n);
builder.accept(val);
}
return builder.build();
}
public String join(String expression, String delimeter){
StringJoiner j = new StringJoiner(delimeter);
asList(expression).forEach(j::add);
return j.toString();
}
/**
* Evaluates the given expression to a Node. Any
* {@link XPathExpressionException}s thrown will be wrapped
* as unchecked exceptions.
* @param expression
* @return Node or {@code null}
*/
public Node asNode(String expression){
try {
return (Node) delegate.evaluate(expression, withNode, XPathConstants.NODE);
} catch (XPathExpressionException e) {
throw new RuntimeException("Failed to evaluate " + expression, e);
}
}
/**
* Evaluate the {@code expression} as a {@link Calendar}. The result
* should be in the same format as {@code xsd:date}. Any
* {@link XPathExpressionException}s thrown will be wrapped
* as unchecked exceptions.
* @param expression xpath expression which can be evaluated to a String
* @return {@link Calendar}
* @throws IllegalArgumentException if the value isn't an xsd:date
* @see DatatypeConverter#parseDate(String)
*/
public Calendar toDate(String expression){
String s = asString(expression);
return DatatypeConverter.parseDate(s);
}
public String formatDate(String expression, String format){
Calendar cal = toDate(expression);
Date date = cal.getTime();
SimpleDateFormat formatter = new SimpleDateFormat(format);
return formatter.format(date);
}
/**
* Format using {@link DecimalFormat}. Examples of format.
*
* <pre>
* ####.##
* -###.##
* -000.00
* ##%
* </pre>
* @param expression
* @param pattern
* @return
*/
public String formatDecimal(String expression, String pattern){
BigDecimal val = toBigDecimal(expression);
DecimalFormat format = new DecimalFormat(pattern);
return format.format(val.doubleValue());
}
/**
* Evaluate the {@code expression} as a {@link Calendar}. The result
* should be in the same format as {@code xsd:dateTime}. Any
* {@link XPathExpressionException}s thrown will be wrapped
* as unchecked exceptions.
* @param expression xpath expression which can be evaluated to a String
* @return {@link Calendar}
* @throws IllegalArgumentException if the value isn't an xsd:dateTime
* @see DatatypeConverter#parseDate(String)
*/
public Calendar toDateTime(String expression){
String s = asString(expression);
return DatatypeConverter.parseDateTime(s);
}
/**
* Evaluate the {@code expression} as a {@link BigDecimal}. Any
* {@link XPathExpressionException}s thrown will be wrapped
* as unchecked exceptions.
* @param expression xpath expression which can be evaluated to a String
* @return {@link BigDecimal} or {@link BigDecimal#ZERO} if no value
* is returned
* @throws IllegalArgumentException if the expression can't be parsed
* as {@link BigDecimal} or isn't an empty string.
* @see DatatypeConverter#parseDecimal(String)
*/
public BigDecimal toBigDecimal(String expression){
String s = asString(expression);
if(s.isEmpty()){
return BigDecimal.ZERO;
} else {
return DatatypeConverter.parseDecimal(s);
}
}
public <T> T toObject (String expression, Function<Node, T> callback){
Node n = asNode(expression);
return callback.apply(n);
}
/**
* Evaluate the {@code expression} as a {@link BigInteger}. Any
* {@link XPathExpressionException}s thrown will be wrapped
* as unchecked exceptions.
* @param expression xpath expression which can be evaluated to a String
* @return {@link BigInteger} or {@link BigInteger#ZERO} if no value
* is returned
* @throws IllegalArgumentException if the expression can't be parsed
* as {@link BigInteger} or isn't an empty string.
* @see DatatypeConverter#parseInteger(String)
*/
public BigInteger toBigInteger(String expression){
String s = asString(expression);
if(s.isEmpty()){
return BigInteger.ZERO;
} else {
return DatatypeConverter.parseInteger(s);
}
}
/**
* @param expression xpath expression which can be evaluated to a String
* @return {@link Integer} or 0 if no value
* is returned
* @throws IllegalArgumentException if the expression can't be parsed
* as {@link Integer} or isn't an empty string.
* @see DatatypeConverter#parseInteger(String)
*/
public Integer toInteger(String expression){
return toBigInteger(expression).intValue();
}
/**
* Evaluates xpath expression to a String, wrapping any
* {@link XPathExpressionException}s as unchecked exceptions.
* @param expression string-valued xpath expression
* @return the String value of the result, or "" if nothing is found
* @see XPath#evaluate(String, Object)
*/
public String asString(String expression) {
try {
return delegate.evaluate(expression, withNode);
} catch (XPathExpressionException e) {
throw new RuntimeException("Failed to evaluate " + expression, e);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment