Last active
December 7, 2017 20:00
-
-
Save tmclnk/777fdb161c6772d60766b6e490040210 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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