package org.kapott.hbci.GV; import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Enumeration; import java.util.List; import java.util.Properties; import org.kapott.hbci.GV.generators.ISEPAGenerator; import org.kapott.hbci.GV.generators.SEPAGeneratorFactory; import org.kapott.hbci.GV_Result.HBCIJobResultImpl; import org.kapott.hbci.comm.Comm; import org.kapott.hbci.exceptions.HBCI_Exception; import org.kapott.hbci.manager.HBCIHandler; import org.kapott.hbci.manager.HBCIUtils; import org.kapott.hbci.manager.HBCIUtilsInternal; import org.kapott.hbci.passport.HBCIPassport; import org.kapott.hbci.passport.HBCIPassportInternal; import org.kapott.hbci.sepa.PainVersion; import org.kapott.hbci.sepa.PainVersion.Type; /** * Abstrakte Basis-Klasse fuer JAXB-basierte SEPA-Jobs. */ public abstract class AbstractSEPAGV extends HBCIJobImpl { /** * Token, der als End-to-End ID Platzhalter verwendet wird, wenn keine angegeben wurde. * In pain.001.001.02 wurde dieser Token noch explizit erwaehnt. Inzwischen nicht mehr. * Nach Ruecksprache mit Holger vom onlinebanking-forum.de weiss ich aber, dass VRNetworld * den auch verwendet und er von Banken als solcher erkannt wird. */ public final static String ENDTOEND_ID_NOTPROVIDED = "NOTPROVIDED"; protected final Properties sepaParams = new Properties(); private PainVersion pain = null; private ISEPAGenerator generator = null; /** * Liefert die Default-PAIN-Version, das verwendet werden soll, * wenn von der Bank keine geliefert wurden. * @return Default-Pain-Version. */ protected abstract PainVersion getDefaultPainVersion(); /** * Liefert den PAIN-Type. * @return der PAIN-Type. */ protected abstract Type getPainType(); /** * ct. * @param handler * @param name */ public AbstractSEPAGV(HBCIHandler handler, String name) { super(handler, name, new HBCIJobResultImpl()); this.pain = this.determinePainVersion(handler,name); } /** * ct. * @param handler * @param name * @param jobResult */ public AbstractSEPAGV(HBCIHandler handler, String name, HBCIJobResultImpl jobResult) { super(handler, name, jobResult); this.pain = this.determinePainVersion(handler,name); } /** * Durchsucht das BPD-Segment "HISPAS" nach dem Property "cannationalacc" * um herauszufinden, ob beim Versand eines SEPA-Auftrages die nationale Bankverbindung * angegeben sein darf. * * Siehe FinTS_3.0_Messages_Geschaeftsvorfaelle_2013-05-28_final_version.pdf - Kapitel B.3.2 * * @param handler * @return true, wenn der BPD-Parameter von der Bank mit "J" befuellt ist und die * nationale Bankverbindung angegeben sein darf. */ protected boolean canNationalAcc(HBCIHandler handler) { // Checken, ob das Flag im Passport durch die Anwendung hart codiert ist. // Dort kann die Entscheidung ueberschrieben werden, ob die nationale Kontoverbindung // mitgeschickt wird oder nicht. // Das wird voraussichtlich u.a. fuer die Postbank benoetigt, weil die in HISPAS // zwar mitteilt, dass die nationale Kontoverbindung NICHT angegeben werden soll. // Beim anschliessenden Einreichen einer SEPA-Ueberweisung beschwert sie sich aber, // wenn man sie nicht mitgesendet hat. Die verbieten also erst das Senden der // nationalen Kontoverbindung, verlangen sie anschliessend aber. Ein Fehler der // Bank. Siehe http://www.onlinebanking-forum.de/forum/topic.php?p=86444#real86444 HBCIPassport passport = handler.getPassport(); if (passport instanceof HBCIPassportInternal) { HBCIPassportInternal pi = (HBCIPassportInternal) passport; Object o = pi.getPersistentData("cannationalacc"); if (o != null) { String s = o.toString(); HBCIUtils.log("value of \"cannationalacc\" overwritten in passport, value: " + s,HBCIUtils.LOG_INFO); return s.equalsIgnoreCase("J"); } } HBCIUtils.log("searching for value of \"cannationalacc\" in HISPAS",HBCIUtils.LOG_INFO); // Ansonsten suchen wir in HISPAS - aber nur, wenn wir die Daten schon haben if (handler.getSupportedLowlevelJobs().getProperty("SEPAInfo") == null) { HBCIUtils.log("no HISPAS data found",HBCIUtils.LOG_INFO); return false; // Ne, noch nicht. Dann lassen wir das erstmal weg } // SEPAInfo laden und dar�ber iterieren Properties props = handler.getLowlevelJobRestrictions("SEPAInfo"); String value = props.getProperty("cannationalacc"); HBCIUtils.log("cannationalacc=" + value,HBCIUtils.LOG_INFO); return value != null && value.equalsIgnoreCase("J"); } /** * Diese Methode schaut in den BPD nach den unterst�tzen pain Versionen * (bei LastSEPA pain.008.xxx.xx) und vergleicht diese mit den von HBCI4Java * unterst�tzen pain Versionen. Der gr��te gemeinsamme Nenner wird * zurueckgeliefert. * @param handler * @param gvName der Geschaeftsvorfall fuer den in den BPD nach dem PAIN-Versionen * gesucht werden soll. * @return die ermittelte PAIN-Version. */ private PainVersion determinePainVersion(HBCIHandler handler, String gvName) { // Schritt 1: Wir holen uns die globale maximale PAIN-Version PainVersion globalVersion = this.determinePainVersionInternal(handler,GVSEPAInfo.getLowlevelName()); // Schritt 2: Die des Geschaeftsvorfalls - fuer den Fall, dass die Bank // dort weitere Einschraenkungen hinterlegt hat PainVersion jobVersion = this.determinePainVersionInternal(handler,gvName); // Wir haben gar keine PAIN-Version gefunden if (globalVersion == null && jobVersion == null) { PainVersion def = this.getDefaultPainVersion(); HBCIUtils.log("unable to determine matching pain version, using default: " + def,HBCIUtils.LOG_WARN); return def; } // Wenn wir keine GV-spezifische haben, dann nehmen wir die globale if (jobVersion == null) { HBCIUtils.log("have no job-specific pain version, using global pain version: " + globalVersion,HBCIUtils.LOG_DEBUG); return globalVersion; } // Ansonsten hat die vom Job Vorrang: HBCIUtils.log("using job-specific pain version: " + jobVersion,HBCIUtils.LOG_DEBUG); return jobVersion; } /** * Diese Methode schaut in den BPD nach den unterst�tzen pain Versionen * (bei LastSEPA pain.008.xxx.xx) und vergleicht diese mit den von HBCI4Java * unterst�tzen pain Versionen. Der gr��te gemeinsamme Nenner wird * zurueckgeliefert. * @param handler * @param gvName der Geschaeftsvorfall fuer den in den BPD nach dem PAIN-Versionen * gesucht werden soll. * @return die ermittelte PAIN-Version oder NULL wenn keine ermittelt werden konnte. */ private PainVersion determinePainVersionInternal(HBCIHandler handler, final String gvName) { HBCIUtils.log("searching for supported pain versions for GV " + gvName,HBCIUtils.LOG_DEBUG); if (handler.getSupportedLowlevelJobs().getProperty(gvName) == null) { HBCIUtils.log("don't have any BPD for GV " + gvName,HBCIUtils.LOG_DEBUG); return null; } List<PainVersion> found = new ArrayList<PainVersion>(); // GV-Restrictions laden und dar�ber iterieren Properties props = handler.getLowlevelJobRestrictions(gvName); Enumeration e = props.propertyNames(); while (e.hasMoreElements()) { String key = (String) e.nextElement(); // Die Keys, welche die Schema-Versionen enthalten, heissen alle "suppformats*" if (!key.startsWith("suppformats")) continue; String urn = props.getProperty(key); try { PainVersion version = PainVersion.byURN(urn); if (version.getType() == this.getPainType()) { if (!version.isSupported(this.getPainJobName())) { HBCIUtils.log(" unsupported " + version,HBCIUtils.LOG_DEBUG); continue; } // Frueher wurde hier noch geschaut, ob die PAIN-Version per // PainVersion.getKnownVersions bekannt ist. In dem Fall wurde // stattdessen unsere verwendet, damit beim Senden des Auftrages // der korrekte URN verwendet wird. Das ist inzwischen nicht mehr // noetig, da das "PainVersion.byURN" (siehe oben) ohnehin bereits // macht - wenn wir die PAIN-Version kennen, nehmen wir gleich die // eigene Instanz. Siehe auch // TestPainVersion#test011 bzw. http://www.onlinebanking-forum.de/phpBB2/viewtopic.php?p=95160#95160 HBCIUtils.log(" found " + version,HBCIUtils.LOG_DEBUG); found.add(version); } } catch (Exception ex) { HBCIUtils.log("ignoring invalid pain version " + urn,HBCIUtils.LOG_WARN); HBCIUtils.log(ex,HBCIUtils.LOG_DEBUG); } } return PainVersion.findGreatest(found); } /** * @see org.kapott.hbci.GV.HBCIJobImpl#setLowlevelParam(java.lang.String, java.lang.String) * This is needed to "redirect" the sepa values. They dont have to stored * directly in the message, but have to go into the SEPA document which will * by created later (in verifyConstraints()) */ protected void setLowlevelParam(String key, String value) { String intern = getName() + ".sepa."; if (key.startsWith(intern)) { String realKey = key.substring(intern.length()); this.sepaParams.setProperty(realKey, value); HBCIUtils.log("setting SEPA param " + realKey + " = " + value, HBCIUtils.LOG_DEBUG); } else { super.setLowlevelParam(key, value); } } /** * This is needed for verifyConstraints(). Because verifyConstraints() tries * to read the lowlevel-values for each constraint, the lowlevel-values for * sepa.xxx would always be empty (because they do not exist in hbci * messages). So we read the sepa lowlevel-values from the special sepa * structure instead from the lowlevel params for the message * @param key * @return the lowlevel param. */ public String getLowlevelParam(String key) { String result; String intern = getName() + ".sepa."; if (key.startsWith(intern)) { String realKey = key.substring(intern.length()); result = getSEPAParam(realKey); } else { result = super.getLowlevelParam(key); } return result; } /** * Gibt die SEPA Message ID als String zur�ck. Existiert noch keine wird sie * aus Datum und User ID erstellt. * * @return SEPA Message ID */ public String getSEPAMessageId() { String result = getSEPAParam("messageId"); if (result == null) { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss:SSSS"); result = format.format(new Date()); result = result.substring(0, Math.min(result.length(), 35)); setSEPAParam("messageId", result); } return result; } /** * Liefert den passenden SEPA-Generator. * @return der SEPA-Generator. */ protected final ISEPAGenerator getSEPAGenerator() { if (this.generator == null) { try { this.generator = SEPAGeneratorFactory.get(this, this.getPainVersion()); } catch (Exception e) { String msg=HBCIUtilsInternal.getLocMsg("EXCMSG_JOB_CREATE_ERR",this.getPainJobName()); if (!HBCIUtilsInternal.ignoreError(null,"client.errors.ignoreCreateJobErrors",msg)) throw new HBCI_Exception(msg,e); } } return this.generator; } /** * Liefert den zu verwendenden PAIN-Version fuer die HBCI-Nachricht. * @return der zu verwendende PAIN-Version fuer die HBCI-Nachricht. */ protected PainVersion getPainVersion() { return this.pain; } /** * Erstellt die XML f�r diesen Job und schreibt diese in den _sepapain * Parameter des Jobs */ protected void createSEPAFromParams() { // Hier wird die XML rein geschrieben ByteArrayOutputStream o = new ByteArrayOutputStream(); // Passenden SEPA Generator zur verwendeten pain Version laden ISEPAGenerator gen = this.getSEPAGenerator(); // Die XML in den baos schreiben, ggf fehler behandeln try { boolean validate = HBCIUtils.getParam("sepa.schema.validation","0").equals("1"); HBCIUtils.log("schema validation enabled: " + validate,HBCIUtils.LOG_DEBUG); gen.generate(this.sepaParams, o, validate); } catch (HBCI_Exception he) { throw he; } catch (Exception e) { throw new HBCI_Exception("*** the _sepapain segment for this job can not be created",e); } // Pr�fen ob die XML erfolgreich generiert wurde if (o.size() == 0) throw new HBCI_Exception("*** the _sepapain segment for this job can not be created"); try { String xml = o.toString(Comm.ENCODING); HBCIUtils.log("generated XML:\n" + xml,HBCIUtils.LOG_DEBUG); setParam("_sepapain", "B" + xml); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } /** * @see org.kapott.hbci.GV.HBCIJobImpl#addConstraint(java.lang.String, java.lang.String, java.lang.String, int, boolean) * Ueberschrieben, um die Default-Werte der SEPA-Parameter vorher rauszufischen und in "this.sepaParams" zu * speichern. Die brauchen wir "createSEPAFromParams" beim Erstellen des XML - sie wuerden dort sonst aber * fehlen, weil Default-Werte eigentlich erst in "verifyConstraints" uebernommen werden. */ @Override protected void addConstraint(String frontendName, String destinationName, String defValue, int logFilterLevel, boolean indexed) { super.addConstraint(frontendName, destinationName, defValue, logFilterLevel, indexed); if (destinationName.startsWith("sepa.") && defValue != null) { this.sepaParams.put(frontendName,defValue); } } /** * Bei SEPA Gesch�ftsvorf�llen m�ssen wir verifyConstraints �berschreiben um * die SEPA XML zu generieren */ public void verifyConstraints() { // creating SEPA document and storing it in _sepapain if(this.acceptsParam("_sepapain")) { createSEPAFromParams(); } super.verifyConstraints(); // TODO: checkIBANCRC } protected void setSEPAParam(String name, String value) { this.sepaParams.setProperty(name, value); } /** * Liest den Parameter zu einem gegeben Key aus dem speziellen SEPA * Parametern aus * * @param name * @return Value */ public String getSEPAParam(String name) { return this.sepaParams.getProperty(name); } /** * Referenzierter pain-Jobname. Bei vielen Gesch�ftsvorf�llen * (z.B. Dauerauftr�gen) wird die pain der Einzeltransaktion verwendet. * @return Value */ public String getPainJobName() { return this.getJobName(); } }