// $Id $ // (C) cantamen/Dirk Hillbrecht 2013. Released under the LGPL 2.0 or higher. // Version: 2013-10-23-02 // For more information, contact info@cantamen.de or dh@cantamen.de. See http://www.cantamen.de package ENTER-PACKAGE-HERE; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.security.*; import java.security.cert.*; import java.security.cert.Certificate; import javax.net.ssl.*; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.*; /** Connector to IBAN conversion service from Theano GmbH/IBAN-BIC.com. * * This is a Java-6-SE-only class which connects to the IBAN conversion service of Theano GmbH as described on www.iban-bic.com. * It hides all the implementation of the connection behind some nice Java classes which do the dirty work. *
 * Use this by creating one instance of the connector per thread and then calling generateIBAN() or another of the top level public
 * methods. The methods run synchronously and return either one of the defined result object (e.g. IBANReply) or throw an
 * IBANConverterException. Assuming the existance of some "BankAccountData" data type, a typical usage could be like this:
 *
 * 
IBANConverterServiceConnector connector=new IBANConverterServiceConnector("username","secret");
for (BankAccountData accountdata:getSomeBankAccountData(...whatever...)) {
  try {
    IBANConverterServiceConnector.IBANReply ibanreply=connector.generateIBAN(accountdata.getCountryCode(),accountdata.getBankCode(),accountdata.getAccountNumber());
    if (ibanreply.isPassed() && ibanreply.getResultCode()==0)
      accountdata.setIBAN(ibanreply.getIBAN());
    else {
      accountdata.setProbableIBAN(ibanreply.getIBAN());
      accountdata.setIBANResultCode(ibanreply.getResultCode());
    }
  }
  catch (IBANConverterException ice) {
    accountdata.setIBANConversionProblem(ice);
  }
}
 *
 * The class contains some static helper classes for result and exception transport. It is fully self-contained with no other external references but the Java 6 SE
 * runtime environment.
 * 
* Internally, IBANConverterServiceConnector connects to the service via its HTTP-based interface. It passes the information through URL parameters and receives * the reply as XML string in the HTTP query response. That one is converted into a DOM tree from where the reply data is extracted and stored in the reply class. * The class does not use any SOAP or other frameworks above the W3C DOM system. * * @author (C) cantamen/Dirk Hillbrecht 2013. Released under the LGPL 2.0 or higher. For more information, contact info@cantamen.de or dh@cantamen.de. See http://www.cantamen.de * @version $Id $ */ public class IBANConverterServiceConnector { // ************************************************** // *** Public interface and public helper classes *** // ************************************************** /** Global flag to set this into debug mode. * * If set to true, the whole connector does not query the conversion service over the web, but simply returns a precompiled answer. * Set this to true during development and debugging so that you do not call the service everytime you run your tests. In production environments, * this should always be set to false. */ private static final boolean DEBUG=false; /** Instantiate a service connector for the IBAN conversion. * * Converters are single-threaded! It takes the username/password information which are used for all ongoing queries. * * @param usernamex Username to use for the IBAN conversion service * @param passwordx Passwort for authenticating to the IBAN conversion service */ public IBANConverterServiceConnector(String usernamex,String passwordx) { username=usernamex; password=passwordx; /* - This is for password hashing which does not seem to work on Java try { md5 = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("error.nomd5digesterfound",e); } */ dbf=DocumentBuilderFactory.newInstance(); dbf.setValidating(false); dbf.setIgnoringComments(false); dbf.setIgnoringElementContentWhitespace(true); dbf.setNamespaceAware(true); // dbf.setCoalescing(true); // dbf.setExpandEntityReferences(true); } /** Special exception class for signalling problems with the IBAN conversion. * * This exception is thrown if something goes severely wrong with the IBAN conversion. It is only thrown if the service is cannot be connected * or returns syntactically unexpected replies. If the IBAN was incontructible from the given data, no exception is thrown but instead a reply * is returned which describes the problems. *
   * This exceptions is derived from RuntimeException so that you might more or less "ignore" it while coding. You should not do, however, as such
   * behaviour will always bite you and your project when you do not expect it.
   */
  public static class IBANConverterException extends RuntimeException {
    public IBANConverterException(String s) { super(s); }
    public IBANConverterException(String s,Throwable c) { super(s,c); }
  }
  /** Reply class for IBAN query results.
   *
   * This class is returned by generateIBAN() and friends. It contains the - probably - generated IBAN and other values as they are returned by the
   * IBAN conversion web service. An instance of this class is immutable.
   */
  public static class IBANReply {
    private final String iban;
    private final boolean passed;
    private final int resultcode;
    private final int balance;
    public IBANReply(String ibanx,boolean passedx,int resultcodex,int balancex) {
      iban=ibanx;passed=passedx;resultcode=resultcodex;balance=balancex;
    }
    /** Return the IBAN as created by the service. */
    public String getIBAN() { return iban; }
    /** Return whether the IBAN generation passed or failed ("result" value of the IBAN creation service) */
    public boolean isPassed() { return passed; }
    /** Return the result code as generated by the IBAN creation service. */
    public int getResultCode() { return resultcode; }
    /** Returns the account balance information from the IBAN creation service. */
    public int getBalance() { return balance; }
    /** Return a string representation. */
    @Override
    public String toString() { return "{IBANReply, passed: "+passed+", result code: "+resultcode+", IBAN: "+iban+", balance: "+balance+"}"; }
  }
  /** Builder for the IBANReply class. */
  private static class IBANReplyBuilder {
    private String iban=""; public IBANReplyBuilder setIBAN(String ibanx) { iban=ibanx; return this; }
    private boolean passed=false; public IBANReplyBuilder setResult(String resultstring) { passed="passed".equalsIgnoreCase(resultstring); return this; }
    private int resultcode=-1; public IBANReplyBuilder setResultCode(String resultcodestring) { resultcode=Integer.parseInt(resultcodestring); return this; }
    private int balance=-1; public IBANReplyBuilder setBalance(String balancestring) { balance=Integer.parseInt(balancestring); return this; }
    public IBANReply build() { return new IBANReply(iban,passed,resultcode,balance); }
  }
  /** Convert a legacy German bankcode/account pair to an IBAN.
   *
   * This method gets a german bank account at a German bank and returns the IBAN for this account. The result also contains result and error codes of
   * the conversion.
   *
   * @param bankcode Bank code of the account to convert
   * @param account Account number
   * @returns IBANReply instance with the generated IBAN or the appropriate error information if no IBAN was created.
   */
  public IBANReply generateGermanIBAN(String bankcode,String account) throws IBANConverterException {
    return generateIBAN("DE",bankcode,account);
  }
  /** Convert a legacy bankcode/account pair to an IBAN.
   *
   * This method gets a bank account at a bank in the country given by the country code and returns the IBAN for this account.
   * The result also contains result and error codes of the conversion.
   *
   * @param countrycode 2-character ISO country code of the account's and bank's country
   * @param bankcode Bank code of the account to convert
   * @param account Account number
   * @returns IBANReply instance with the generated IBAN or the appropriate error information if no IBAN was created.
   */
  public IBANReply generateIBAN(String countrycode,String bankcode,String account) throws IBANConverterException {
    try {
      Document reply;
      if (DEBUG) {
        System.err.println("IBAN converter in DEBUG MODE!!!");
        reply=dbf.newDocumentBuilder().parse(new ByteArrayInputStream(debugresult.getBytes()));
      }
      else {
        URL url = new URL(generateCalculateIBANURL(countrycode,bankcode,account));
        HttpsURLConnection con = (HttpsURLConnection)url.openConnection();
        try {
          // Here we set the special socket factory for accessing the IBAN service
          con.setSSLSocketFactory(ibanrechnersslcontext.getSocketFactory());
          if (con.getResponseCode()!=HttpURLConnection.HTTP_OK)
            throw new IllegalStateException("error.httpnotok|"+con.getResponseCode()+"|"+con.getResponseMessage());
          // We have to remove any leading empty lines from the body as this confuses the DOM parser. So, eat up characters until "<" appears.
          PushbackInputStream pbis=new PushbackInputStream(con.getInputStream());
          int ch;
          while ((ch=pbis.read())=='\n'){}
          pbis.unread(ch);
          reply=dbf.newDocumentBuilder().parse(pbis);
        }
        finally {
          con.disconnect();
        }
      }
      if (reply==null)
        throw new IllegalStateException("error.noreply");
      Node result=reply.getFirstChild();
      if (result==null)
        throw new IllegalStateException("error.noresultnode");
      if (!"result".equals(result.getNodeName()))
        throw new IllegalStateException("error.resultnodename|"+result.getNodeName());
      IBANReplyBuilder builder=new IBANReplyBuilder();
      NodeList nl=result.getChildNodes();
      for (int i=0;i