Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
syjer committed Apr 3, 2021
1 parent 6cd399d commit bb1474f
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 96 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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/ .

Expand Down
10 changes: 9 additions & 1 deletion src/main/java/ch/digitalfondue/vatchecker/BaseFault.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ abstract class BaseFault<T extends Enum<T>> {
protected BaseFault(String faultCode, String fault, Function<String, T> 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() {
Expand All @@ -41,4 +41,12 @@ public String getFaultCode() {
public T getFaultType() {
return faultType;
}

private static <T extends Enum<T>> T tryParse(String fault, Function<String, T> converter, T defaultValue) {
try {
return converter.apply(fault);
} catch (IllegalArgumentException | NullPointerException e) {
return defaultValue;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ public static class Fault extends BaseFault<FaultType> {
}
}


public enum FaultType {
INVALID_INPUT,
NO_INFORMATION,
Expand Down
63 changes: 27 additions & 36 deletions src/main/java/ch/digitalfondue/vatchecker/EUTinChecker.java
Original file line number Diff line number Diff line change
Expand Up @@ -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/ .
*
* <p>
* The main entry points are {@link #doCheck(String, String)} and if more customization is needed {@link #doCheck(String, String, BiFunction)}.
*/
public class EUTinChecker {
Expand All @@ -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 = "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
Expand All @@ -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);
}
Expand All @@ -74,7 +78,7 @@ public EUTinChecker(BiFunction<String, String, InputStream> 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) {
Expand All @@ -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) {
Expand All @@ -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<String, String, InputStream> documentFetcher) {
Objects.requireNonNull(countryCode, "countryCode cannot be null");
Objects.requireNonNull(tinNumber, "tinNumber cannot be null");
try {
HashMap<String, String> 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<String, String> 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
}
}

Expand Down
58 changes: 25 additions & 33 deletions src/main/java/ch/digitalfondue/vatchecker/EUVatChecker.java
Original file line number Diff line number Diff line change
Expand Up @@ -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/ .
*
* <p>
* The main entry points are {@link #doCheck(String, String)} and if more customization is needed {@link #doCheck(String, String, BiFunction)}.
*/
public class EUVatChecker {
Expand All @@ -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<String, String, InputStream> documentFetcher;

Expand All @@ -61,7 +63,7 @@ public EUVatChecker(BiFunction<String, String, InputStream> 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) {
Expand All @@ -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 = "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
"<soapenv:Header/>" +
Expand Down Expand Up @@ -114,29 +119,16 @@ public static EUVatCheckResponse doCheck(String countryCode, String vatNumber) {
public static EUVatCheckResponse doCheck(String countryCode, String vatNumber, BiFunction<String, String, InputStream> documentFetcher) {
Objects.requireNonNull(countryCode, "countryCode cannot be null");
Objects.requireNonNull(vatNumber, "vatNumber cannot be null");
try {
HashMap<String, String> 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<String, String> 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
}
}
}
72 changes: 50 additions & 22 deletions src/main/java/ch/digitalfondue/vatchecker/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand All @@ -128,7 +127,6 @@ static String fromDocument(Document doc) {
}
}


static InputStream doCall(String endpointUrl, String document) {
try {
URL url = new URL(endpointUrl);
Expand All @@ -146,23 +144,53 @@ static InputStream doCall(String endpointUrl, String document) {
}
}

static String prepareTemplate(Document document, Map<String, String> 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<String, String> data) {
Document doc = copyDocument(document);
for (Map.Entry<String, String> 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 extends Enum<T>> T tryParse(String fault, Function<String, T> 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<String> extracted;

ExtractionResult(Node validNode, Node faultNode, List<String> extracted) {
this.validNode = validNode;
this.faultNode = faultNode;
this.extracted = extracted;
}
}

static ExtractionResult doCallAndExtract(Document document,
Map<String, String> params,
String endpointUrl,
BiFunction<String, String, InputStream> 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<String> 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);
}
}
}

0 comments on commit bb1474f

Please sign in to comment.