diff --git a/README.md b/README.md index 14897f6..2c9a188 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ -# vatchecker: a basic java library for fetching VAT information from the VIES webservice and TIN webservice +# vatchecker: a java library for fetching information from the VIES and TIN webservice [![Maven Central](https://img.shields.io/maven-central/v/ch.digitalfondue.vatchecker/vatchecker.svg)](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22vatchecker%22) [![Build Status](https://img.shields.io/github/workflow/status/digitalfondue/vatchecker/Java%20CI%20with%20Maven)](https://github.com/digitalfondue/vatchecker/actions?query=workflow%3A%22Java+CI+with+Maven%22) - -A small java client for calling +A small java client with 0 dependencies for calling: - the European "VAT Information Exchange System" (VIES) webservice for validating the VAT numbers. See http://ec.europa.eu/taxation_customs/vies/ . - the European "TIN" webservice. See https://ec.europa.eu/taxation_customs/tin/ . diff --git a/src/main/java/ch/digitalfondue/vatchecker/BaseFault.java b/src/main/java/ch/digitalfondue/vatchecker/BaseFault.java index 5963f62..b794e4f 100644 --- a/src/main/java/ch/digitalfondue/vatchecker/BaseFault.java +++ b/src/main/java/ch/digitalfondue/vatchecker/BaseFault.java @@ -27,7 +27,7 @@ abstract class BaseFault> { protected BaseFault(String faultCode, String fault, Function converter, T defaultValue) { this.faultCode = faultCode; this.fault = fault; - this.faultType = Utils.tryParse(fault, converter, defaultValue); + this.faultType = tryParse(fault, converter, defaultValue); } public String getFault() { @@ -41,4 +41,12 @@ public String getFaultCode() { public T getFaultType() { return faultType; } + + private static > T tryParse(String fault, Function converter, T defaultValue) { + try { + return converter.apply(fault); + } catch (IllegalArgumentException | NullPointerException e) { + return defaultValue; + } + } } diff --git a/src/main/java/ch/digitalfondue/vatchecker/EUTinCheckResponse.java b/src/main/java/ch/digitalfondue/vatchecker/EUTinCheckResponse.java index cb3c261..8f66ee8 100644 --- a/src/main/java/ch/digitalfondue/vatchecker/EUTinCheckResponse.java +++ b/src/main/java/ch/digitalfondue/vatchecker/EUTinCheckResponse.java @@ -53,7 +53,6 @@ public static class Fault extends BaseFault { } } - public enum FaultType { INVALID_INPUT, NO_INFORMATION, diff --git a/src/main/java/ch/digitalfondue/vatchecker/EUTinChecker.java b/src/main/java/ch/digitalfondue/vatchecker/EUTinChecker.java index a8924ca..459440d 100644 --- a/src/main/java/ch/digitalfondue/vatchecker/EUTinChecker.java +++ b/src/main/java/ch/digitalfondue/vatchecker/EUTinChecker.java @@ -17,18 +17,21 @@ package ch.digitalfondue.vatchecker; import org.w3c.dom.Document; -import org.w3c.dom.Node; -import javax.xml.xpath.*; -import java.io.*; -import java.nio.charset.StandardCharsets; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import java.io.InputStream; +import java.io.StringReader; import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.function.BiFunction; /** * A small utility for calling the TIN webservice. See https://ec.europa.eu/taxation_customs/tin/ . - * + *

* The main entry points are {@link #doCheck(String, String)} and if more customization is needed {@link #doCheck(String, String, BiFunction)}. */ public class EUTinChecker { @@ -38,8 +41,7 @@ public class EUTinChecker { private static final Document BASE_DOCUMENT_TEMPLATE; private static final XPathExpression VALID_ELEMENT_MATCHER; - private static final XPathExpression VALID_STRUCTURE_MATCHER; - private static final XPathExpression VALID_SYNTAX_MATCHER; + private static final XPathExpression[] VALID_EXTRACTORS; static { String soapCallTemplate = "" + @@ -55,8 +57,10 @@ public class EUTinChecker { XPath xPath = XPathFactory.newInstance().newXPath(); try { VALID_ELEMENT_MATCHER = xPath.compile("//*[local-name()='checkTinResponse']"); - VALID_STRUCTURE_MATCHER = xPath.compile("//*[local-name()='checkTinResponse']/*[local-name()='validStructure']"); - VALID_SYNTAX_MATCHER = xPath.compile("//*[local-name()='checkTinResponse']/*[local-name()='validSyntax']"); + VALID_EXTRACTORS = new XPathExpression[]{ + xPath.compile("//*[local-name()='checkTinResponse']/*[local-name()='validSyntax']"), + xPath.compile("//*[local-name()='checkTinResponse']/*[local-name()='validStructure']") + }; } catch (XPathExpressionException e) { throw new IllegalStateException(e); } @@ -74,7 +78,7 @@ public EUTinChecker(BiFunction documentFetcher) { * See {@link #doCheck(String, String)}. * * @param countryCode 2 character ISO country code. Note: Greece is EL, not GR. - * @param tinNr TIN number + * @param tinNr TIN number * @return the response, see {@link EUTinCheckResponse} */ public EUTinCheckResponse check(String countryCode, String tinNr) { @@ -85,7 +89,7 @@ public EUTinCheckResponse check(String countryCode, String tinNr) { * Do a call to the EU tin checker web service. * * @param countryCode 2 character ISO country code. Note: Greece is EL, not GR. - * @param tinNr the tin number to check + * @param tinNr the tin number to check * @return the response, see {@link EUTinCheckResponse} */ public static EUTinCheckResponse doCheck(String countryCode, String tinNr) { @@ -96,37 +100,24 @@ public static EUTinCheckResponse doCheck(String countryCode, String tinNr) { * See {@link #doCheck(String, String)}. This method accept a documentFetcher if you need to customize the * http client. * - * @param countryCode 2 character ISO country code. Note: Greece is EL, not GR. - * @param tinNumber TIN number + * @param countryCode 2 character ISO country code. Note: Greece is EL, not GR. + * @param tinNumber TIN number * @param documentFetcher the function that, given the url of the web service and the body to post, return the resulting body as InputStream * @return the response, see {@link EUTinCheckResponse} */ public static EUTinCheckResponse doCheck(String countryCode, String tinNumber, BiFunction documentFetcher) { Objects.requireNonNull(countryCode, "countryCode cannot be null"); Objects.requireNonNull(tinNumber, "tinNumber cannot be null"); - try { - HashMap params = new HashMap<>(); - params.put("countryCode", countryCode); - params.put("tinNumber", tinNumber); - String body = Utils.prepareTemplate(BASE_DOCUMENT_TEMPLATE, params); - try (InputStream is = documentFetcher.apply(ENDPOINT, body); Reader isr = new InputStreamReader(is, StandardCharsets.UTF_8)) { - Document result = Utils.toDocument(isr); - Node validNode = (Node) VALID_ELEMENT_MATCHER.evaluate(result, XPathConstants.NODE); - Node faultNode = (Node) Utils.SOAP_FAULT_MATCHER.evaluate(result, XPathConstants.NODE); - if (validNode != null) { - Node validStructure = (Node) VALID_STRUCTURE_MATCHER.evaluate(result, XPathConstants.NODE); - Node validSyntax = (Node) VALID_SYNTAX_MATCHER.evaluate(result, XPathConstants.NODE); - return new EUTinCheckResponse("true".equals(Utils.textNode(validSyntax)), "true".equals(Utils.textNode(validStructure)), false, null); - } else if (faultNode != null) { - Node faultCode = (Node) Utils.SOAP_FAULT_CODE_MATCHER.evaluate(result, XPathConstants.NODE); - Node faultString = (Node) Utils.SOAP_FAULT_STRING_MATCHER.evaluate(result, XPathConstants.NODE); - return new EUTinCheckResponse(false, false, true, new EUTinCheckResponse.Fault(Utils.textNode(faultCode), Utils.textNode(faultString))); - } else { - return new EUTinCheckResponse(false, false, true, null); // should not enter here in theory - } - } - } catch (IOException | XPathExpressionException e) { - throw new IllegalStateException(e); + Map params = new HashMap<>(); + params.put("countryCode", countryCode); + params.put("tinNumber", tinNumber); + Utils.ExtractionResult res = Utils.doCallAndExtract(BASE_DOCUMENT_TEMPLATE, params, ENDPOINT, documentFetcher, VALID_ELEMENT_MATCHER, VALID_EXTRACTORS); + if (res.validNode != null) { + return new EUTinCheckResponse("true".equals(res.extracted.get(0)), "true".equals(res.extracted.get(1)), false, null); + } else if (res.faultNode != null) { + return new EUTinCheckResponse(false, false, true, new EUTinCheckResponse.Fault(res.extracted.get(0), res.extracted.get(1))); + } else { + return new EUTinCheckResponse(false, false, true, null); // should not enter here in theory } } diff --git a/src/main/java/ch/digitalfondue/vatchecker/EUVatChecker.java b/src/main/java/ch/digitalfondue/vatchecker/EUVatChecker.java index f854d60..8080867 100644 --- a/src/main/java/ch/digitalfondue/vatchecker/EUVatChecker.java +++ b/src/main/java/ch/digitalfondue/vatchecker/EUVatChecker.java @@ -17,18 +17,21 @@ package ch.digitalfondue.vatchecker; import org.w3c.dom.Document; -import org.w3c.dom.Node; -import javax.xml.xpath.*; -import java.io.*; -import java.nio.charset.StandardCharsets; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import java.io.InputStream; +import java.io.StringReader; import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.function.BiFunction; /** * A small utility for calling the VIES webservice. See https://ec.europa.eu/taxation_customs/vies/ . - * + *

* The main entry points are {@link #doCheck(String, String)} and if more customization is needed {@link #doCheck(String, String, BiFunction)}. */ public class EUVatChecker { @@ -37,8 +40,7 @@ public class EUVatChecker { private static final String ENDPOINT = "https://ec.europa.eu/taxation_customs/vies/services/checkVatService"; private static final XPathExpression VALID_ELEMENT_MATCHER; - private static final XPathExpression NAME_ELEMENT_MATCHER; - private static final XPathExpression ADDRESS_ELEMENT_MATCHER; + private static final XPathExpression[] VALID_EXTRACTORS; private final BiFunction documentFetcher; @@ -61,7 +63,7 @@ public EUVatChecker(BiFunction documentFetcher) { * See {@link #doCheck(String, String)}. * * @param countryCode 2 character ISO country code. Note: Greece is EL, not GR. See http://ec.europa.eu/taxation_customs/vies/faq.html#item_11 - * @param vatNr vat number + * @param vatNr vat number * @return the response, see {@link EUVatCheckResponse} */ public EUVatCheckResponse check(String countryCode, String vatNr) { @@ -72,8 +74,11 @@ public EUVatCheckResponse check(String countryCode, String vatNr) { XPath xPath = XPathFactory.newInstance().newXPath(); try { VALID_ELEMENT_MATCHER = xPath.compile("//*[local-name()='checkVatResponse']/*[local-name()='valid']"); - NAME_ELEMENT_MATCHER = xPath.compile("//*[local-name()='checkVatResponse']/*[local-name()='name']"); - ADDRESS_ELEMENT_MATCHER = xPath.compile("//*[local-name()='checkVatResponse']/*[local-name()='address']"); + VALID_EXTRACTORS = new XPathExpression[]{ + VALID_ELEMENT_MATCHER, + xPath.compile("//*[local-name()='checkVatResponse']/*[local-name()='name']"), + xPath.compile("//*[local-name()='checkVatResponse']/*[local-name()='address']") + }; String soapCallTemplate = "" + "" + @@ -114,29 +119,16 @@ public static EUVatCheckResponse doCheck(String countryCode, String vatNumber) { public static EUVatCheckResponse doCheck(String countryCode, String vatNumber, BiFunction documentFetcher) { Objects.requireNonNull(countryCode, "countryCode cannot be null"); Objects.requireNonNull(vatNumber, "vatNumber cannot be null"); - try { - HashMap params = new HashMap<>(); - params.put("countryCode", countryCode); - params.put("vatNumber", vatNumber); - String body = Utils.prepareTemplate(BASE_DOCUMENT_TEMPLATE, params); - try (InputStream is = documentFetcher.apply(ENDPOINT, body); Reader isr = new InputStreamReader(is, StandardCharsets.UTF_8)) { - Document result = Utils.toDocument(isr); - Node validNode = (Node) VALID_ELEMENT_MATCHER.evaluate(result, XPathConstants.NODE); - Node faultNode = (Node) Utils.SOAP_FAULT_MATCHER.evaluate(result, XPathConstants.NODE); - if (validNode != null) { - Node nameNode = (Node) NAME_ELEMENT_MATCHER.evaluate(result, XPathConstants.NODE); - Node addressNode = (Node) ADDRESS_ELEMENT_MATCHER.evaluate(result, XPathConstants.NODE); - return new EUVatCheckResponse("true".equals(Utils.textNode(validNode)), Utils.textNode(nameNode), Utils.textNode(addressNode), false, null); - } else if (faultNode != null) { - Node faultCode = (Node) Utils.SOAP_FAULT_CODE_MATCHER.evaluate(result, XPathConstants.NODE); - Node faultString = (Node) Utils.SOAP_FAULT_STRING_MATCHER.evaluate(result, XPathConstants.NODE); - return new EUVatCheckResponse(false, null, null, true, new EUVatCheckResponse.Fault(Utils.textNode(faultCode), Utils.textNode(faultString))); - } else { - return new EUVatCheckResponse(false, null, null, true, null); // should not enter here in theory - } - } - } catch (IOException | XPathExpressionException e) { - throw new IllegalStateException(e); + Map params = new HashMap<>(); + params.put("countryCode", countryCode); + params.put("vatNumber", vatNumber); + Utils.ExtractionResult res = Utils.doCallAndExtract(BASE_DOCUMENT_TEMPLATE, params, ENDPOINT, documentFetcher, VALID_ELEMENT_MATCHER, VALID_EXTRACTORS); + if (res.validNode != null) { + return new EUVatCheckResponse("true".equals(res.extracted.get(0)), res.extracted.get(1), res.extracted.get(2), false, null); + } else if (res.faultNode != null) { + return new EUVatCheckResponse(false, null, null, true, new EUVatCheckResponse.Fault(res.extracted.get(0), res.extracted.get(1))); + } else { + return new EUVatCheckResponse(false, null, null, true, null); // should not enter here in theory } } } diff --git a/src/main/java/ch/digitalfondue/vatchecker/Utils.java b/src/main/java/ch/digitalfondue/vatchecker/Utils.java index 958f6dc..3a9f4a4 100644 --- a/src/main/java/ch/digitalfondue/vatchecker/Utils.java +++ b/src/main/java/ch/digitalfondue/vatchecker/Utils.java @@ -32,22 +32,21 @@ import javax.xml.transform.dom.DOMResult; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathExpression; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; +import javax.xml.xpath.*; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import java.util.Map; -import java.util.function.Function; +import java.util.function.BiFunction; class Utils { - static final XPathExpression SOAP_FAULT_MATCHER; - static final XPathExpression SOAP_FAULT_CODE_MATCHER; - static final XPathExpression SOAP_FAULT_STRING_MATCHER; + private static final XPathExpression SOAP_FAULT_MATCHER; + private static final XPathExpression SOAP_FAULT_CODE_MATCHER; + private static final XPathExpression SOAP_FAULT_STRING_MATCHER; static { XPath xPath = XPathFactory.newInstance().newXPath(); @@ -60,7 +59,7 @@ class Utils { } } - static Document copyDocument(Document document) { + private static Document copyDocument(Document document) { try { Transformer tx = getTransformer(); DOMSource source = new DOMSource(document); @@ -115,7 +114,7 @@ private static void setFeature(DocumentBuilderFactory dbFactory, String feature, } } - static String fromDocument(Document doc) { + private static String fromDocument(Document doc) { try { DOMSource domSource = new DOMSource(doc); Transformer transformer = getTransformer(); @@ -128,7 +127,6 @@ static String fromDocument(Document doc) { } } - static InputStream doCall(String endpointUrl, String document) { try { URL url = new URL(endpointUrl); @@ -146,23 +144,53 @@ static InputStream doCall(String endpointUrl, String document) { } } - static String prepareTemplate(Document document, Map data) { - Document doc = Utils.copyDocument(document); - data.forEach((tagName, value) -> { - doc.getElementsByTagName(tagName).item(0).setTextContent(value); - }); - return Utils.fromDocument(doc); + private static String prepareTemplate(Document document, Map data) { + Document doc = copyDocument(document); + for (Map.Entry kv : data.entrySet()) { + doc.getElementsByTagName(kv.getKey()).item(0).setTextContent(kv.getValue()); + } + return fromDocument(doc); } static final String textNode(Node node) { return node != null ? node.getTextContent() : null; } - static > T tryParse(String fault, Function converter, T defaultValue) { - try { - return converter.apply(fault); - } catch (IllegalArgumentException | NullPointerException e) { - return defaultValue; + static class ExtractionResult { + final Node validNode; + final Node faultNode; + final List extracted; + + ExtractionResult(Node validNode, Node faultNode, List extracted) { + this.validNode = validNode; + this.faultNode = faultNode; + this.extracted = extracted; + } + } + + static ExtractionResult doCallAndExtract(Document document, + Map params, + String endpointUrl, + BiFunction documentFetcher, + XPathExpression validElementMatcher, + XPathExpression[] validElementExtractors) { + String body = Utils.prepareTemplate(document, params); + try (InputStream is = documentFetcher.apply(endpointUrl, body); Reader isr = new InputStreamReader(is, StandardCharsets.UTF_8)) { + Document result = Utils.toDocument(isr); + Node validNode = (Node) validElementMatcher.evaluate(result, XPathConstants.NODE); + Node faultNode = (Node) SOAP_FAULT_MATCHER.evaluate(result, XPathConstants.NODE); + List extracted = new ArrayList<>(validElementExtractors.length); + if (validNode != null) { + for (XPathExpression exp : validElementExtractors) { + extracted.add(textNode((Node) exp.evaluate(result, XPathConstants.NODE))); + } + } else if (faultNode != null) { + extracted.add(textNode((Node) Utils.SOAP_FAULT_CODE_MATCHER.evaluate(result, XPathConstants.NODE))); + extracted.add(textNode((Node) Utils.SOAP_FAULT_STRING_MATCHER.evaluate(result, XPathConstants.NODE))); + } + return new ExtractionResult(validNode, faultNode, extracted); + } catch (IOException | XPathExpressionException e) { + throw new IllegalStateException(e); } } }