gdv.xport.util.SatzRegistry Maven / Gradle / Ivy
Show all versions of gdv-xport-lib Show documentation
/*
* Copyright (c) 2021 by Oli B.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express orimplied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* (c)reated 13.01.2021 by Oli B. ([email protected])
*/
package gdv.xport.util;
import gdv.xport.Datenpaket;
import gdv.xport.config.Config;
import gdv.xport.io.PushbackLineNumberReader;
import gdv.xport.satz.Datensatz;
import gdv.xport.satz.Nachsatz;
import gdv.xport.satz.Satz;
import gdv.xport.satz.Vorsatz;
import gdv.xport.satz.xml.SatzXml;
import gdv.xport.satz.xml.XmlService;
import org.apache.commons.lang3.Range;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.validation.ValidationException;
import javax.xml.stream.XMLStreamException;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Diese Klasse wurde von SatzFactory kopiert und so angepasst, dass auch
* verschiedene Instanzen fuer die jeweilige XML-Beschreibung der GDV-
* Datensaetze erzeugt werden koennen. Dies ist mit der SatzFactory-Klasse
* nicht moeglich, da sie ein rein statische Klasse ist.
*
* Da die Klasse auch zum Registrieren eigener Satzarten gedacht ist, wurde
* sie in SatzRegistry umbenannt. Funktional entspricht sie aber der
* {@link SatzFactory}-Klasse (s.o.).
*
*
* @author oliver ([email protected])
* @since 5.0
*/
public class SatzRegistry implements VersionHandler {
private static final Logger LOG = LogManager.getLogger(SatzRegistry.class);
/** Default-Validator, der nur Satzart 800 - 899 akzeptiert. */
public static final Validator VALIDATOR = new Validator();
/** Dieser Validator akzeptiert alle Satzarten zwischen 0 und 9999. */
public static final Validator NO_VALIDATOR = new Validator(Range.between(0, 9999));
private static final Map INSTANCES = new HashMap<>();
private static final Map, Satz> SATZTYP_VERSIONEN = new HashMap<>();
private final Map registeredSaetze = new ConcurrentHashMap<>();
private final XmlService xmlService;
private SatzRegistry(XmlService xmlService) {
this.xmlService = xmlService;
}
/**
* Hierueber kann man sich die Default-Factory mit der aktuell gueltigen
* XML-Beschreibung der GDV-Datensaetze holen.
*
* @return Factory auf Basis von VUVM2018.xml
*/
public static SatzRegistry getInstance() {
return getInstance(Config.getInstance());
}
/**
* Hierueber kann man sich die Default-Factory anhand der gewuenschten
* Konfiguration die XML-Beschreibung der GDV-Datensaetze holen.
*
* @param cfg gewuenschte Konfiguration
* @return Factory auf Basis der uebergebenen Config
*/
public static SatzRegistry getInstance(final Config cfg) {
SatzRegistry factory = INSTANCES.get(cfg);
try {
if (factory == null) {
factory = new SatzRegistry(XmlService.getInstance(cfg));
INSTANCES.put(cfg, factory);
LOG.info("{} wurde angelegt.", factory);
}
return factory;
} catch (XMLStreamException | IOException ex) {
throw new IllegalArgumentException("invalid config: " + cfg, ex);
}
}
/**
* Hierueber kann man sich die Default-Factory mit der gewuenschten
* XML-Beschreibung der GDV-Datensaetze holen. Es wird dabei fuer die
* gleiche Resource auch die gleiche Instanz zurueckgegeben.
*
* @param resource z.B. "VUVM2015.xml"
* @return Factory auf Basis der uebergebenen Resource
*/
public static SatzRegistry getInstance(final String resource) {
return getInstance(Config.EMPTY.withProperty("gdv.XML-Resource", resource));
}
/**
* Liefert den Datensatz mit der gewuenschten Version. Dazu werden die
* Instanzen mit den verschiedenen XML-Beschreibungen durchsucht. Wird
* keine gefunden, wird der Satz der aktuellen Instanz zurueckgegeben.
*
* @param satzTyp SatzTyp
* @param version gewuenschte Version
* @return Satz mit der gewuenschten Version
* @since 5.2
*/
public static Satz getSatz(SatzTyp satzTyp, String version) {
Map.Entry satzTypVersion = new AbstractMap.SimpleEntry<>(satzTyp, version);
Satz satz = SATZTYP_VERSIONEN.get(satzTypVersion);
if (satz == null) {
satz = getSatz(satzTypVersion);
SATZTYP_VERSIONEN.put(satzTypVersion, satz);
}
try {
return (Satz) satz.clone();
} catch (CloneNotSupportedException ex) {
LOG.warn("Clone von {} hat nicht geklappt:", satz, ex);
return getSatz(satzTypVersion);
}
}
private static Satz getSatz(Map.Entry satzTypVersion) {
createInstances();
SatzTyp satzTyp = satzTypVersion.getKey();
Satz satz = getInstance().getSatz(satzTyp);
String version = satzTypVersion.getValue();
float satzVersion = asFloat(satz.getVersion());
float requiredVersion = asFloat(version);
for (SatzRegistry registry : INSTANCES.values()) {
try {
Satz ds = registry.getSatz(satzTyp);
if (version.equals(ds.getVersion())) {
return ds;
} else if ((asFloat(ds.getVersion()) < satzVersion) && (asFloat(ds.getVersion()) > requiredVersion)) {
satz = ds;
satzVersion = asFloat(ds.getVersion());
}
} catch (NotRegisteredException e) {
// satzTyp ist in dieser Version nicht registriert, gehe zur nächsten Version
LOG.debug("Satzart {} in {} nicht registriert, suche weiter", satzTyp, registry);
LOG.trace("Details:", e);
}
}
if (LOG.isDebugEnabled()) {
LOG.debug("Exakte Version {} fuer {} wurde nicht gefunden - verwende {} (Version {}).", version,
satzTyp, satz.toShortString(), satz.getVersion());
}
return satz;
}
private static float asFloat(String version) {
try {
return Float.parseFloat(version);
} catch (NumberFormatException ex) {
LOG.info("Kann aus '{}' keine Version ermitteln ({}).", version, ex);
LOG.debug("Details:", ex);
return 0.0F;
}
}
private static void createInstances() {
getInstance(Config.VUVM2009);
getInstance(Config.VUVM2013);
getInstance(Config.VUVM2015);
getInstance(Config.VUVM2018);
}
/**
* Mit dieser Klasse konnen die Registrierungen wieder komplett
* rueckgaengig gemacht werden. Diese Methode wurde vor allem zur
* Unterstuetzung der Unit-Tests eingefuehrt, wird aber seit der
* Umstellung von {@link SatzFactory} auf {@link SatzRegistry}
* nicht mehr benoetigt.
*
* TODO: wird mit v7 entfernt
*
*
* @deprecated wird nicht mehr benoetigt
*/
@Deprecated
public void reset() {
registeredSaetze.clear();
LOG.debug("{} wurde zurueckgesetzt.", this);
}
/**
* Mit dieser Methode koennen eigene Klassen fuer (z.B. noch nicht
* unterstuetzte Datensaetze) registriert werden. Die Kasse muss
* einen Default-Konstruktor bereitstellen. Ansonsten wird hier eine
* {@link IllegalArgumentException} geworfen.
*
* @param clazz the clazz
* @param satzart the satzart
*/
public void register(final Class extends Satz> clazz, final int satzart) {
register(clazz, satzart, VALIDATOR);
}
/**
* Mit dieser Methode kann ein Validator mit uebergeben werden. So kann
* z.B. {@link SatzRegistry#NO_VALIDATOR} mit uebergeben werden, um die
* Default-Validierung abzuschalten. Als Default werden nur Datensaetze
* mit der Satzart 800 - 899 zugelassen.
*
* @param clazz the clazz
* @param satzart the satzart
* @param validator fuer die Validierung der Satzart
* @see #register(Satz, SatzTyp)
*/
public void register(final Class extends Satz> clazz, final int satzart, Validator validator) {
try {
Constructor extends Satz> ctor = clazz.getConstructor();
LOG.debug("Default constructor {} found.", ctor);
} catch (NoSuchMethodException ex) {
throw new IllegalArgumentException("no default constructor found in " + clazz, ex);
}
SatzTyp satzTyp = SatzTyp.of(satzart);
register(newInstance(satzTyp, clazz), satzTyp, validator);
}
/**
* Mit dieser Methode kann ein beliebiger Satz registriert werden. Sie
* loest die alter registerEnum-Methode ab, in der ein Satz mithilfe einer
* Enum-Beschreibung registriert werden konnte.
*
* Mit der verbesserten Unterstuetzung der GDV-XML-Beschreibung in v5.0
* kann jetzt auch diese XML-Beschreibung fuer die Registrierung eigener
* Datensaetze verwendet und hierueber registriert werden. So kann z.B.
* mit
*
*
* SatzRegistry.getDefault().register(SatzXml.of("Satz0221.051.xml"), SatzTyp.of("0221.051"));
*
*
* eine eigene Beschreibung fuer Satzart 0221.051 registriert werden.
*
*
* @param satz Satz-Vorlage (z.B. SatzXml.of("Satz0221.051.xml"))
* @param satzNr Satzart (z.B. SatzTyp.of("0221.051"))
*/
public void register(final Satz satz, final SatzTyp satzNr) {
register(satz, satzNr, VALIDATOR);
}
/**
* Mit dieser register-Methode kann ein eigener Validator mit uebergeben
* werden. Dies ist hilfreich, wenn man den vom GDV vorgegebenen Bereich
* fuer eigene Datensaetze (800-899) verlassen will. Dann kann man z.B.
* den NO_VALIDATOR uebergeben, der alle Satzarten zwischen 0 und 9999
* akzeptiert.
*
* @param satz Satz-Vorlage (z.B. SatzXml.of("Satz0221.051.xml"))
* @param satzNr Satzart (z.B. SatzTyp.of("0221.051"))
* @param validator Validator fuer den SatzTyp
*/
public void register(final Satz satz, final SatzTyp satzNr, Validator validator) {
validator.validate(satzNr);
registeredSaetze.put(satzNr, satz);
}
/**
* Hiermit kann man eine Registrierung rueckgaengig machen (was z.B. fuer's
* Testen hilfreich sein kann). Diese unregister-Methode ersetzt ab 4.2 die
* anderen unregister-Methoden.
*
* @param typ SatzTyp bzw. Satzart
*/
public void unregister(SatzTyp typ) {
registeredSaetze.remove(typ);
SATZTYP_VERSIONEN.clear();
}
/**
* Mit dieser Methode koennen eigene Klassen fuer (z.B. noch nicht
* unterstuetzte Datensaetze) registriert werden.
*
* @param clazz the clazz
* @param satzNr the satz nr
*/
public void register(final Class extends Datensatz> clazz, final SatzTyp satzNr) {
registeredSaetze.put(satzNr, generateDatensatz(satzNr, clazz)); }
/**
* Liefert einen (normalerweise) einen {@link Datensatz} zurueck.
* Ausser bei Satzart 0001 und 9999 (Vorsatz und Nachsatz), da dies
* nur normale Saetze sind.
*
* @param satztyp der Satztyp
* @return angeforderter Satz
*/
public Satz getSatz(final SatzTyp satztyp) {
Satz satz = registeredSaetze.get(satztyp);
if (satz == null) {
return getSatzFromXmlService(satztyp);
}
try {
return (Satz) satz.clone();
} catch (CloneNotSupportedException ex) {
throw new IllegalArgumentException(satztyp + " laesst sich nicht clonen", ex);
}
}
private static Satz newInstance(SatzTyp satztyp, Class extends Satz> clazz) {
try {
Satz satz = clazz.getDeclaredConstructor().newInstance();
if (satz.getSatzart() != satztyp.getSatzart()) {
Constructor extends Satz> ctor = clazz.getConstructor(int.class);
satz = ctor.newInstance(satztyp.getSatzart());
}
return satz;
} catch (Exception e) {
LOG.info("default constructor does not work (" + e + "), trying another ctor...");
Constructor extends Satz> ctor = null;
try {
ctor = clazz.getConstructor(int.class);
return ctor.newInstance(satztyp.getSatzart());
} catch (InvocationTargetException ite) {
throw new ShitHappenedException(ite.getTargetException() + " in " + ctor, ite);
} catch (NoSuchMethodException nsme) {
throw new UnsupportedOperationException("registered " + clazz + " has not the required ctor", nsme);
} catch (InstantiationException ie) {
throw new ShitHappenedException("registered " + clazz + " can't be instantiated", ie);
} catch (IllegalAccessException iae) {
throw new IllegalStateException("registered " + clazz + " can't be accessed", iae);
}
}
}
private Satz getSatzFromXmlService(SatzTyp satztyp) {
SatzXml satz = xmlService.getSatzart(satztyp);
satz.init(satztyp);
return satz;
}
/**
* Versucht anhand des uebergebenen Strings herauszufinden, um was fuer eine
* Satzart es sich handelt und liefert dann einen entsprechenden (gefuellten)
* Satz zurueck.
*
* Im ersten Schritt wird versucht, einen moeglichst passenden SatzTyp zu ermitteln. Zu diesem
* SatzTyp wird dann versucht, eine registrierte Satzart zu finden. Wenn das fehlschlaegt, wird im
* 2. Schritt als Ersatz eine Satzart generiert aus "satzart" und "sparte".
*
*
* ACHTUNG: Um den ganz korrekten Satzaufbau zu liefern, muesste dazu die Version der Satzatz
* bekannt sein! Diese Info steht immer im Vorsatz des zugehörigen Datenpaketes. Lt. Auskunft vom
* GDV werden z.T. noch Saetze aus Release 01.11.2009 verarbeitet. Da hier aber die aktuellste
* Version verwendet wird, kann der zurueckgegebene Satz mehr Felder enthalten, als die
* tatsaechliche Version. Diese Unschaerfe wird hier in Kauf genommen, da i.d.R. immer nur Felder
* hinzugefuegt werden. Dies muss beim Zugriff ueber die Feld-Nr. beachtet werden.
*
*
* @param content the content
* @return einen gefuellten Satz
*/
public Satz getSatz(final String content) {
/*
* @Oli: diese Variante sollte deutlich besser passen als die bisherige. Jedoch habe ich nicht
* bis zum Exzess getestet. Bitte pruef das doch noch mal.
*/
SatzTyp satzTyp;
try {
satzTyp = errateSatzTyp(content);
} catch (IOException ioe) {
throw new IllegalArgumentException("can't recognize SatzTyp " + content, ioe);
}
Satz satz;
try {
satz = getSatz(satzTyp);
} catch (NotRegisteredException e) {
LOG.debug("Kann Satz '{}' nicht bestimmen und verwende Fallback:", satzTyp, e);
satz = generateDatensatz(satzTyp);
}
try {
satz.importFrom(content);
return satz;
} catch (IOException ioe) {
throw new IllegalArgumentException("can't parse " + content, ioe);
}
}
private SatzTyp errateSatzTyp(final String content) throws IOException {
int satzart = Integer.parseInt(content.substring(0, 4));
if (satzart == 1 || satzart == 9999) {
return SatzTyp.of(satzart);
}
// Da das im Wesentlichen der Methode "Datenpaket.importDatensatz(...)" entspricht...
try (PushbackLineNumberReader reader = new PushbackLineNumberReader(new StringReader(content))) {
Satz satz = Datenpaket.importSatz(reader);
return satz.getSatzTyp();
}
}
/**
* Liefert den passenden Vorsatz.
*
* @return Vorsatz
* @since 5.0
*/
public Vorsatz getVorsatz() {
return new Vorsatz(this);
}
/**
* Liefert den passenden Nachsatz.
*
* @return Nachsatz
* @since 5.0
*/
public Nachsatz getNachsatz() {
return new Nachsatz(this);
}
/**
* Liefert den gewuenschten Datensatz. Mit der uebergebenen Satznummer wird
* der Datensatz spezifizert, der folgendes enthaelt:
*
* - Satzart (z.B. 210)
* - Sparte (z.B. 70 fuer Rechtsschutz)
* - Wagnisart (z.B. 1 fuer Kapitallebensversicherung)
* - Teildatensatz-Nummer (6 = Bezugsrechte, 7 = Auszahlungen, 8 =
* zukünftige Summenänderungen, 9 = Wertungssummen)
*
*
* Falls der gewuenschte Datensatz nicht registriert ist, wird der Datensatz
* anhand der von {@link XmlService} bestimmt.
*
*
* Im Gegensatz zu {@link #getSatz(SatzTyp)} wird hier auf jeden Fall
* ein Datensatz zurueckgeliefert, auch wenn der SatzTyp weder registriert
* noch ueber den {@link XmlService} verfuegbar ist. Dies ist vor allem
* fuer den Import relevant, damit er nicht bei unbekannten Datensaetzen
* abbricht.
*
*
* @param satzNr z.B. SatzTyp.of("0210.070.1.6")
* @return den passenden Datensatz
* @deprecated wurde durch {@link #getSatz(SatzTyp)} und {@link #generateDatensatz(SatzTyp)}
* abgeloest
*/
@Deprecated
public Datensatz getDatensatz(final SatzTyp satzNr) {
Satz satz = registeredSaetze.get(satzNr);
if (satz instanceof Datensatz) {
try {
return (Datensatz) satz.clone();
} catch (CloneNotSupportedException ex) {
throw new IllegalArgumentException(satzNr + " laesst sich nicht clonen", ex);
}
}
return generateDatensatz(satzNr);
}
private Datensatz generateDatensatz(SatzTyp satzNr, Class extends Datensatz> clazz) {
try {
Constructor extends Datensatz> ctor = clazz.getConstructor(int.class, int.class);
return ctor.newInstance(satzNr.getSatzart(), satzNr.getSparte());
} catch (NoSuchMethodException exWithTwoParams) {
LOG.info("constructor " + clazz + "(int, int) not found (" + exWithTwoParams + ")");
return getDatensatz(satzNr.getSparte(), clazz);
} catch (InstantiationException exWithTwoParams) {
LOG.info(clazz + "(int, int) can't be instantiated (" + exWithTwoParams + ")");
return getDatensatz(satzNr.getSparte(), clazz);
} catch (IllegalAccessException exWithTwoParams) {
LOG.info(clazz + "(int, int) can't be accessed (" + exWithTwoParams + ")");
return getDatensatz(satzNr.getSparte(), clazz);
} catch (InvocationTargetException exWithTwoParams) {
LOG.info("error in calling " + clazz + "(int, int): " + exWithTwoParams);
return getDatensatz(satzNr.getSparte(), clazz);
}
}
private static Datensatz getDatensatz(final int sparte, final Class extends Datensatz> clazz) {
try {
Constructor extends Datensatz> ctor = clazz.getConstructor(int.class);
return ctor.newInstance(sparte);
} catch (NoSuchMethodException nsme) {
LOG.info(clazz + " found but no " + clazz.getSimpleName() + "(" + sparte + ") constructor (" + nsme + ")");
return getDatensatz(clazz);
} catch (Exception exWithOneParam) {
LOG.warn("constructor problem with " + clazz, exWithOneParam);
return getDatensatz(clazz);
}
}
private static Datensatz getDatensatz(final Class extends Datensatz> clazz) {
try {
return clazz.getDeclaredConstructor().newInstance();
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException("can't instantiate " + clazz, e);
}
}
private Datensatz generateDatensatz(final SatzTyp satzNr) {
try {
Datensatz fallback = (Datensatz) getSatz(satzNr);
if (satzNr.hasSparte()) {
fallback.setSparte(satzNr.getSparte());
}
return fallback;
} catch (NotRegisteredException re) {
LOG.info("Datensatz fuer Satzart {} wird kreiert.", satzNr);
LOG.debug("Details:", re);
Datensatz satz = new Datensatz(SatzTyp.of(satzNr.getSatzart(), satzNr.getSparte()));
satz.addFiller();
return satz;
}
}
/**
* Liefert ein Datenpaket mit allen unterstuetzten Satzarten.
*
* Satzarten, die mit {@link #register(Class, int)} registriert wurden,
* werden nicht aufgefuehrt!
*
* Grund: Ein Objekt vom Typ <code>Satz</code> kann
* nicht auf <code>Datensatz</code> gecastet werden.
*
*
* @return Datenpaket mit allen unterstuetzten Satzarten
*/
public Datenpaket getAllSupportedSaetze() {
Map supportedSaetze = new HashMap<>(xmlService.getSatzarten());
for (Map.Entry entry : registeredSaetze.entrySet()) {
Satz value = entry.getValue();
if (value instanceof Datensatz) {
supportedSaetze.put(entry.getKey(), value);
}
}
return createDatenpaket(supportedSaetze);
}
/**
* Liefert ein Datenpaket mit den angegebenen Satzarten.
*
* @param typen gewuenschte Satzarten
* @return Datenpaket mit gewuenschten Satzarten
* @since 5.2
*/
public Datenpaket getSupportedSaetzeWith(SatzTyp ... typen) {
Map supportedSaetze = new HashMap<>();
for (SatzTyp t : typen) {
supportedSaetze.put(t, xmlService.getSatzart(t));
}
return createDatenpaket(supportedSaetze);
}
private Datenpaket createDatenpaket(Map supportedSaetze) {
supportedSaetze.remove(Vorsatz.SATZART);
supportedSaetze.put(Vorsatz.SATZART, new Vorsatz(this));
supportedSaetze.remove(Nachsatz.SATZART);
supportedSaetze.put(Nachsatz.SATZART, new Nachsatz(this));
return Datenpaket.of(supportedSaetze.values(), xmlService.getConfig());
}
/**
* Liefert das Release der jeweiligen XML-Beschreibung, aus der die GDV-Datensaetze erzeugt
* wurden.
*
* @return das Release der erzeugten XmlSaetze
*/
public String getGdvRelease() {
return xmlService.getGdvRelease();
}
@Override
public String toString() {
return this.getClass().getSimpleName() + " mit " + this.xmlService;
}
/**
* Liefert die Version der angefragten Satzart
*
* @param satzTyp Satzart
* @return z.B. "2.4"
*/
@Override
public String getVersionOf(SatzTyp satzTyp) {
return getSatz(satzTyp).getSatzversion().getInhalt();
}
static class Validator {
private final Range allowed;
public Validator() {
this(Range.between(800, 899));
}
public Validator(Range allowed) {
this.allowed = allowed;
}
public SatzTyp validate(SatzTyp x) {
if (allowed.contains(x.getSatzart())) {
return x;
} else {
throw new ValidationException(x.getSatzart() + " liegt ausserhalb von " + allowed);
}
}
}
}