diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ca77abb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build + run: ./gradlew build + + - name: Upload test reports + if: failure() + uses: actions/upload-artifact@v4 + with: + name: test-reports + path: build/reports/tests/ diff --git a/.gitignore b/.gitignore index d671e00..d6421f6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # Package Files # *.jar +!gradle/wrapper/gradle-wrapper.jar *.war *.ear diff --git a/README.md b/README.md index 598b516..8dc7e78 100644 --- a/README.md +++ b/README.md @@ -79,11 +79,11 @@ For large files, use callbacks to process records without storing them all in me ```java SieDocumentReader reader = new SieDocumentReader(); -reader.streamValues = true; -reader.callbacks.setVER(voucher -> { +reader.setStreamValues(true); +reader.getCallbacks().setVER(voucher -> { // Process each voucher as it is parsed }); -reader.callbacks.setIB(periodValue -> { +reader.getCallbacks().setIB(periodValue -> { // Process each opening balance entry }); reader.readDocument("large-file.SE"); diff --git a/docs/DEVELOPER.md b/docs/DEVELOPER.md index 5eb32be..cfe0377 100644 --- a/docs/DEVELOPER.md +++ b/docs/DEVELOPER.md @@ -168,7 +168,7 @@ RuntimeException ├── SieDateException ├── SieMissingMandatoryDateException ├── SieMissingObjectException - ├── SieVoucherMissmatchException + ├── SieVoucherMismatchException └── MissingFieldException ``` diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f8e1ee3 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/src/main/java/alipsa/sieparser/Encoding.java b/src/main/java/alipsa/sieparser/Encoding.java index c964a96..9639477 100644 --- a/src/main/java/alipsa/sieparser/Encoding.java +++ b/src/main/java/alipsa/sieparser/Encoding.java @@ -25,13 +25,7 @@ of this software and associated documentation files (the "Software"), to deal package alipsa.sieparser; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; /** * Handles character encoding for SIE files. @@ -41,9 +35,7 @@ public class Encoding { private Encoding() {} - Logger logger = LoggerFactory.getLogger(Encoding.class); - - private static Charset defaultCharset=Charset.forName("IBM437"); + private static final Charset defaultCharset = Charset.forName("IBM437"); /** * Returns the charset used for SIE file encoding (IBM437). @@ -55,23 +47,12 @@ public static Charset getCharset() { } /** - * Converts a string to a collection of bytes using the SIE charset. + * Converts a string to a byte array using the SIE charset. * * @param value the string to convert - * @return a collection of bytes representing the string in IBM437 encoding + * @return a byte array representing the string in IBM437 encoding */ - public static Collection getBytes(String value) { - byte[] byteArray = value.getBytes(getCharset()); - List byteList = new ArrayList<>(); - for (byte b : byteArray) { - byteList.add(Byte.valueOf(b)); - } - return byteList; - } - - /* - public byte[] getByteArray(String value) { + public static byte[] getBytes(String value) { return value.getBytes(getCharset()); } - */ } diff --git a/src/main/java/alipsa/sieparser/IoUtil.java b/src/main/java/alipsa/sieparser/IoUtil.java index 6e2a5cb..b3c2c36 100644 --- a/src/main/java/alipsa/sieparser/IoUtil.java +++ b/src/main/java/alipsa/sieparser/IoUtil.java @@ -24,11 +24,7 @@ of this software and associated documentation files (the "Software"), to deal package alipsa.sieparser; import java.io.*; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CharsetEncoder; -import java.nio.charset.CodingErrorAction; import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; @@ -47,14 +43,7 @@ private IoUtil() {} * @throws IOException if the file cannot be opened */ public static BufferedReader getReader(String fileName) throws IOException { - /* - CharsetDecoder decoder = Encoding.getCharset().newDecoder(); - decoder.onMalformedInput(CodingErrorAction.REPORT); - InputStreamReader isReader = new InputStreamReader(new FileInputStream(fileName), decoder); - return new BufferedReader( isReader ); - */ return Files.newBufferedReader(Paths.get(fileName), Encoding.getCharset()); - } /** @@ -66,19 +55,7 @@ public static BufferedReader getReader(String fileName) throws IOException { * @throws IOException if the file cannot be opened or created */ public static BufferedWriter getWriter(String fileName) throws IOException { - /* - CharsetEncoder encoder = Encoding.getCharset().newEncoder(); - encoder.onUnmappableCharacter(CodingErrorAction.REPORT); - OutputStreamWriter osWriter = new OutputStreamWriter(new FileOutputStream(fileName), encoder); - return new BufferedWriter( osWriter ); - */ - Path filePath = Paths.get(fileName); - StandardOpenOption option; - if (Files.exists(filePath)) { - option = StandardOpenOption.WRITE; - } else { - option = StandardOpenOption.CREATE_NEW; - } - return Files.newBufferedWriter(Paths.get(fileName),Encoding.getCharset(), option); + return Files.newBufferedWriter(Paths.get(fileName), Encoding.getCharset(), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } } diff --git a/src/main/java/alipsa/sieparser/MissingFieldException.java b/src/main/java/alipsa/sieparser/MissingFieldException.java index 235b126..ec20c0b 100644 --- a/src/main/java/alipsa/sieparser/MissingFieldException.java +++ b/src/main/java/alipsa/sieparser/MissingFieldException.java @@ -38,4 +38,14 @@ public class MissingFieldException extends SieException { public MissingFieldException(String s) { super(s); } + + /** + * Creates a new MissingFieldException with the given message and cause. + * + * @param s the detail message describing which field is missing + * @param cause the underlying cause + */ + public MissingFieldException(String s, Throwable cause) { + super(s, cause); + } } diff --git a/src/main/java/alipsa/sieparser/SieCRC32.java b/src/main/java/alipsa/sieparser/SieCRC32.java index 225924c..502266a 100644 --- a/src/main/java/alipsa/sieparser/SieCRC32.java +++ b/src/main/java/alipsa/sieparser/SieCRC32.java @@ -25,9 +25,6 @@ of this software and associated documentation files (the "Software"), to deal package alipsa.sieparser; -import java.util.ArrayList; -import java.util.List; - /** * Implementation of the CRC32 checksum algorithm as specified in the SIE file format (#KSUMMA). * Used to verify the integrity of SIE file data. @@ -79,13 +76,11 @@ public void start() { * @param item the data item to include in the checksum */ public void addData(SieDataItem item) { - List buffer = new ArrayList<>(); - buffer.addAll(Encoding.getBytes(item.getItemType())); + crcAccumulate(Encoding.getBytes(item.getItemType())); for (String d : item.getData()) { String foo = d.replace("{", "").replace("}", ""); - buffer.addAll(Encoding.getBytes(foo)); + crcAccumulate(Encoding.getBytes(foo)); } - cRC_accumulate(buffer); } /** @@ -96,12 +91,12 @@ public long checksum() { return (~crc); } - private void cRC_accumulate(List buffer) { + private void crcAccumulate(byte[] buffer) { long temp1; long temp2; - for (Byte p : buffer) { + for (byte p : buffer) { temp1 = (crc >> 8) & 0x00FFFFFF; - temp2 = CRCTable[((int) crc ^ p.byteValue()) & 0xff]; + temp2 = CRCTable[((int) crc ^ p) & 0xff]; crc = temp1 ^ temp2; } } diff --git a/src/main/java/alipsa/sieparser/SieDataItem.java b/src/main/java/alipsa/sieparser/SieDataItem.java index 82aa090..49f83e9 100644 --- a/src/main/java/alipsa/sieparser/SieDataItem.java +++ b/src/main/java/alipsa/sieparser/SieDataItem.java @@ -265,13 +265,18 @@ public LocalDate getDate(int field) { if (foo.isEmpty()) return null; if (foo.length() != 8) { - getDocumentReader().callbacks.callbackException(new SieDateException(foo + " is not a valid date")); + getDocumentReader().getCallbacks().callbackException(new SieDateException(foo + " is not a valid date")); + return null; + } + try { + int y = Integer.parseInt(foo.substring(0, 4)); + int m = Integer.parseInt(foo.substring(4, 6)); + int d = Integer.parseInt(foo.substring(6, 8)); + return LocalDate.of(y, m, d); + } catch (NumberFormatException | java.time.DateTimeException e) { + getDocumentReader().getCallbacks().callbackException(new SieDateException(foo + " is not a valid date")); return null; } - int y = Integer.parseInt(foo.substring(0, 4)); - int m = Integer.parseInt(foo.substring(4, 6)); - int d = Integer.parseInt(foo.substring(6, 8)); - return LocalDate.of(y, m, d); } /** @@ -292,7 +297,7 @@ public List getObjects() { } } if (data == null) { - getDocumentReader().callbacks.callbackException(new SieMissingObjectException(getRawData())); + getDocumentReader().getCallbacks().callbackException(new SieMissingObjectException(getRawData())); return null; } diff --git a/src/main/java/alipsa/sieparser/SieDateException.java b/src/main/java/alipsa/sieparser/SieDateException.java index 3b6ecb3..0926b0e 100644 --- a/src/main/java/alipsa/sieparser/SieDateException.java +++ b/src/main/java/alipsa/sieparser/SieDateException.java @@ -40,4 +40,13 @@ public SieDateException(String description) { super(description); } + /** + * Creates a new SieDateException with the given description and cause. + * + * @param description a message describing the invalid date + * @param cause the underlying cause + */ + public SieDateException(String description, Throwable cause) { + super(description, cause); + } } diff --git a/src/main/java/alipsa/sieparser/SieDocument.java b/src/main/java/alipsa/sieparser/SieDocument.java index d7c29b9..1d973c0 100644 --- a/src/main/java/alipsa/sieparser/SieDocument.java +++ b/src/main/java/alipsa/sieparser/SieDocument.java @@ -471,13 +471,11 @@ public void setUB(List value) { } /** - * Returns the currency code (#VALUTA), or an empty string if not set. - * @return the currency code + * Returns the currency code (#VALUTA), or {@code null} if not set. + * Per the SIE specification, if this post is absent the reader should assume SEK. + * @return the currency code, or {@code null} if not set */ public String getVALUTA() { - if (valuta == null) { - return ""; - } return valuta; } diff --git a/src/main/java/alipsa/sieparser/SieDocumentComparer.java b/src/main/java/alipsa/sieparser/SieDocumentComparer.java index a411513..6f63b5d 100644 --- a/src/main/java/alipsa/sieparser/SieDocumentComparer.java +++ b/src/main/java/alipsa/sieparser/SieDocumentComparer.java @@ -120,7 +120,7 @@ private void compareNonListItems() { if (docA.getTAXAR() != docB.getTAXAR()) errors.add("TAXAR differs First, Second: '" + docA.getTAXAR() + "' , '" + docB.getTAXAR() + "'"); - if (!docA.getVALUTA().equals(docB.getVALUTA())) + if (!StringUtil.equals(docA.getVALUTA(), docB.getVALUTA())) errors.add("VALUTA differs First, Second: '" + docA.getVALUTA() + "' , '" + docB.getVALUTA() + "'"); } diff --git a/src/main/java/alipsa/sieparser/SieDocumentReader.java b/src/main/java/alipsa/sieparser/SieDocumentReader.java index 01ab924..36617f1 100644 --- a/src/main/java/alipsa/sieparser/SieDocumentReader.java +++ b/src/main/java/alipsa/sieparser/SieDocumentReader.java @@ -31,7 +31,9 @@ of this software and associated documentation files (the "Software"), to deal import java.time.LocalDate; import java.util.ArrayList; import java.util.EnumSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.function.Consumer; /** @@ -41,33 +43,151 @@ of this software and associated documentation files (the "Software"), to deal */ public class SieDocumentReader { - /** Callbacks invoked during document reading for streaming processing. */ - public SieCallbacks callbacks = new SieCallbacks(); - /** When {@code true}, #BTRANS rows are ignored during parsing. */ - public boolean ignoreBTRANS = false; - /** When {@code true}, a missing #OMFATTN is not treated as a validation error. */ - public boolean ignoreMissingOMFATTNING = false; - /** When {@code true}, #RTRANS rows are ignored during parsing. */ - public boolean ignoreRTRANS = false; - /** When {@code true}, vouchers whose rows do not sum to zero are accepted. */ - public boolean allowUnbalancedVoucher = false; - /** When {@code true}, the #KSUMMA checksum is not verified. */ - public boolean ignoreKSUMMA = false; - /** When {@code true}, #UNDERDIM definitions are allowed. */ - public boolean allowUnderDimensions = false; - /** When {@code true}, references to undefined dimensions are accepted. */ - public boolean ignoreMissingDIM = false; + private SieCallbacks callbacks = new SieCallbacks(); + private boolean ignoreBTRANS = false; + private boolean ignoreMissingOMFATTNING = false; + private boolean ignoreRTRANS = false; + private boolean allowUnbalancedVoucher = false; + private boolean ignoreKSUMMA = false; + private boolean allowUnderDimensions = false; + private boolean ignoreMissingDIM = false; private EnumSet acceptSIETypes = null; private SieDocument sieDocument; private List validationExceptions; - /** When {@code true}, period values are only streamed via callbacks and not stored in the document. */ - public boolean streamValues = false; - /** The CRC32 calculator used for checksum verification. */ - public SieCRC32 CRC = new SieCRC32(); - /** When {@code true}, parsing errors are thrown as exceptions instead of being silently collected. */ - public boolean throwErrors = true; + private boolean streamValues = false; + private SieCRC32 CRC = new SieCRC32(); + private boolean throwErrors = true; private String fileName; private int parsingLineNumber = 0; + private SieVoucher curVoucher; + private boolean abortParsing; + private final Map> handlers = new LinkedHashMap<>(); + + /** + * Returns the callbacks invoked during document reading. + * @return the callbacks + */ + public SieCallbacks getCallbacks() { return callbacks; } + + /** + * Sets the callbacks invoked during document reading. + * @param callbacks the callbacks to use + */ + public void setCallbacks(SieCallbacks callbacks) { this.callbacks = callbacks; } + + /** + * Returns whether #BTRANS rows are ignored during parsing. + * @return true if #BTRANS rows are ignored + */ + public boolean isIgnoreBTRANS() { return ignoreBTRANS; } + + /** + * Sets whether #BTRANS rows are ignored during parsing. + * @param ignoreBTRANS true to ignore #BTRANS rows + */ + public void setIgnoreBTRANS(boolean ignoreBTRANS) { this.ignoreBTRANS = ignoreBTRANS; } + + /** + * Returns whether a missing #OMFATTN is accepted. + * @return true if a missing #OMFATTN is accepted + */ + public boolean isIgnoreMissingOMFATTNING() { return ignoreMissingOMFATTNING; } + + /** + * Sets whether a missing #OMFATTN is accepted. + * @param ignoreMissingOMFATTNING true to accept a missing #OMFATTN + */ + public void setIgnoreMissingOMFATTNING(boolean ignoreMissingOMFATTNING) { this.ignoreMissingOMFATTNING = ignoreMissingOMFATTNING; } + + /** + * Returns whether #RTRANS rows are ignored during parsing. + * @return true if #RTRANS rows are ignored + */ + public boolean isIgnoreRTRANS() { return ignoreRTRANS; } + + /** + * Sets whether #RTRANS rows are ignored during parsing. + * @param ignoreRTRANS true to ignore #RTRANS rows + */ + public void setIgnoreRTRANS(boolean ignoreRTRANS) { this.ignoreRTRANS = ignoreRTRANS; } + + /** + * Returns whether unbalanced vouchers are accepted. + * @return true if unbalanced vouchers are accepted + */ + public boolean isAllowUnbalancedVoucher() { return allowUnbalancedVoucher; } + + /** + * Sets whether unbalanced vouchers are accepted. + * @param allowUnbalancedVoucher true to accept unbalanced vouchers + */ + public void setAllowUnbalancedVoucher(boolean allowUnbalancedVoucher) { this.allowUnbalancedVoucher = allowUnbalancedVoucher; } + + /** + * Returns whether the #KSUMMA checksum is ignored. + * @return true if the checksum is ignored + */ + public boolean isIgnoreKSUMMA() { return ignoreKSUMMA; } + + /** + * Sets whether the #KSUMMA checksum is ignored. + * @param ignoreKSUMMA true to ignore the checksum + */ + public void setIgnoreKSUMMA(boolean ignoreKSUMMA) { this.ignoreKSUMMA = ignoreKSUMMA; } + + /** + * Returns whether #UNDERDIM definitions are allowed. + * @return true if #UNDERDIM is allowed + */ + public boolean isAllowUnderDimensions() { return allowUnderDimensions; } + + /** + * Sets whether #UNDERDIM definitions are allowed. + * @param allowUnderDimensions true to allow #UNDERDIM + */ + public void setAllowUnderDimensions(boolean allowUnderDimensions) { this.allowUnderDimensions = allowUnderDimensions; } + + /** + * Returns whether references to undefined dimensions are accepted. + * @return true if undefined dimension references are accepted + */ + public boolean isIgnoreMissingDIM() { return ignoreMissingDIM; } + + /** + * Sets whether references to undefined dimensions are accepted. + * @param ignoreMissingDIM true to accept undefined dimension references + */ + public void setIgnoreMissingDIM(boolean ignoreMissingDIM) { this.ignoreMissingDIM = ignoreMissingDIM; } + + /** + * Returns whether period values are only streamed via callbacks and not stored in the document. + * @return true if values are streamed only + */ + public boolean isStreamValues() { return streamValues; } + + /** + * Sets whether period values are only streamed via callbacks and not stored in the document. + * @param streamValues true to stream values only + */ + public void setStreamValues(boolean streamValues) { this.streamValues = streamValues; } + + /** + * Returns the CRC32 calculator used for checksum verification. + * @return the CRC32 calculator + */ + public SieCRC32 getCRC() { return CRC; } + + /** + * Returns whether parsing errors are thrown as exceptions instead of being silently collected. + * @return true if errors are thrown + */ + public boolean isThrowErrors() { return throwErrors; } + + /** + * Sets whether parsing errors are thrown as exceptions instead of being silently collected. + * @param throwErrors true to throw errors as exceptions + */ + public void setThrowErrors(boolean throwErrors) { this.throwErrors = throwErrors; } /** * Returns the SIE type version from a file without fully parsing it. @@ -93,6 +213,7 @@ public static int getSieVersion(String fileName) throws IOException { public SieDocumentReader() { sieDocument = new SieDocument(); setValidationExceptions(new ArrayList<>()); + initHandlers(); } /** @@ -142,6 +263,9 @@ public void setAcceptSIETypes(EnumSet acceptSIETypes) { */ public SieDocument readDocument(String fileName) throws IOException { this.fileName = fileName; + curVoucher = null; + abortParsing = false; + if (throwErrors) { Consumer existing = callbacks.getSieException(); callbacks.setSieException(ex -> { @@ -151,7 +275,6 @@ public SieDocument readDocument(String fileName) throws IOException { }); } - SieVoucher curVoucher = null; boolean firstLine = true; parsingLineNumber = 0; @@ -171,141 +294,20 @@ public SieDocument readDocument(String fileName) throws IOException { if (!ignoreKSUMMA && CRC.isStarted() && !SIE.KSUMMA.equals(di.getItemType())) CRC.addData(di); - SiePeriodValue pv; String itemType = di.getItemType(); - if (SIE.ADRESS.equals(itemType)) { - sieDocument.getFNAMN().setContact(di.getString(0)); - sieDocument.getFNAMN().setStreet(di.getString(1)); - sieDocument.getFNAMN().setZipCity(di.getString(2)); - sieDocument.getFNAMN().setPhone(di.getString(3)); - } else if (SIE.BKOD.equals(itemType)) { - sieDocument.getFNAMN().setSni(di.getInt(0)); - } else if (SIE.BTRANS.equals(itemType)) { - if (!ignoreBTRANS) { - if (curVoucher == null) { - callbacks.callbackException(new SieParseException( - "#BTRANS outside #VER block at line " + parsingLineNumber)); - } else { - parseTRANS(di, curVoucher); - } - } - } else if (SIE.DIM.equals(itemType)) { - parseDimension(di); - } else if (SIE.ENHET.equals(itemType)) { - parseENHET(di); - } else if (SIE.FLAGGA.equals(itemType)) { - sieDocument.setFLAGGA(di.getInt(0)); - } else if (SIE.FNAMN.equals(itemType)) { - sieDocument.getFNAMN().setName(di.getString(0)); - } else if (SIE.FNR.equals(itemType)) { - sieDocument.getFNAMN().setCode(di.getString(0)); - } else if (SIE.FORMAT.equals(itemType)) { - sieDocument.setFORMAT(di.getString(0)); - } else if (SIE.FTYP.equals(itemType)) { - sieDocument.getFNAMN().setOrgType(di.getString(0)); - } else if (SIE.GEN.equals(itemType)) { - sieDocument.setGEN_DATE(di.getDate(0)); - sieDocument.setGEN_NAMN(di.getString(1)); - } else if (SIE.IB.equals(itemType)) { - parseIB(di); - } else if (SIE.KONTO.equals(itemType)) { - parseKONTO(di); - } else if (SIE.KSUMMA.equals(itemType)) { - if (!ignoreKSUMMA) { - if (CRC.isStarted()) { - parseKSUMMA(di); - } else { - CRC.start(); - } - } - } else if (SIE.KTYP.equals(itemType)) { - parseKTYP(di); - } else if (SIE.KPTYP.equals(itemType)) { - sieDocument.setKPTYP(di.getString(0)); - } else if (SIE.OBJEKT.equals(itemType)) { - parseOBJEKT(di); - } else if (SIE.OIB.equals(itemType)) { - pv = parseOIB_OUB(di); - callbacks.callbackOIB(pv); - if (!streamValues) sieDocument.getOIB().add(pv); - } else if (SIE.OUB.equals(itemType)) { - pv = parseOIB_OUB(di); - callbacks.callbackOUB(pv); - if (!streamValues) sieDocument.getOUB().add(pv); - } else if (SIE.ORGNR.equals(itemType)) { - sieDocument.getFNAMN().setOrgIdentifier(di.getString(0)); - } else if (SIE.OMFATTN.equals(itemType)) { - sieDocument.setOMFATTN(di.getDate(0)); - } else if (SIE.PBUDGET.equals(itemType)) { - pv = parsePBUDGET_PSALDO(di); - if (pv != null) { - callbacks.callbackPBUDGET(pv); - if (!streamValues) sieDocument.getPBUDGET().add(pv); - } - } else if (SIE.PROGRAM.equals(itemType)) { - sieDocument.setPROGRAM(di.getData()); - } else if (SIE.PROSA.equals(itemType)) { - sieDocument.setPROSA(di.getString(0)); - } else if (SIE.PSALDO.equals(itemType)) { - pv = parsePBUDGET_PSALDO(di); - if (pv != null) { - callbacks.callbackPSALDO(pv); - if (!streamValues) sieDocument.getPSALDO().add(pv); - } - } else if (SIE.RAR.equals(itemType)) { - parseRAR(di); - } else if (SIE.RTRANS.equals(itemType)) { - if (!ignoreRTRANS) { - if (curVoucher == null) { - callbacks.callbackException(new SieParseException( - "#RTRANS outside #VER block at line " + parsingLineNumber)); - } else { - parseTRANS(di, curVoucher); - } - } - } else if (SIE.SIETYP.equals(itemType)) { - sieDocument.setSIETYP(di.getInt(0)); - if (acceptSIETypes != null) { - try { - SieType parsed = SieType.fromValue(di.getInt(0)); - if (!acceptSIETypes.contains(parsed)) { - callbacks.callbackException(new SieInvalidFeatureException( - "SIE type " + di.getInt(0) + " is not accepted")); - return null; - } - } catch (IllegalArgumentException e) { - callbacks.callbackException(new SieInvalidFeatureException( - "Unknown SIE type: " + di.getInt(0))); - } - } - } else if (SIE.SRU.equals(itemType)) { - parseSRU(di); - } else if (SIE.TAXAR.equals(itemType)) { - sieDocument.setTAXAR(di.getInt(0)); - } else if (SIE.UB.equals(itemType)) { - parseUB(di); - } else if (SIE.TRANS.equals(itemType)) { - if (curVoucher == null) { - callbacks.callbackException(new SieParseException( - "#TRANS outside #VER block at line " + parsingLineNumber)); - } else { - parseTRANS(di, curVoucher); - } - } else if (SIE.RES.equals(itemType)) { - parseRES(di); - } else if (SIE.UNDERDIM.equals(itemType)) { - parseUnderDimension(di); - } else if (SIE.VALUTA.equals(itemType)) { - sieDocument.setVALUTA(di.getString(0)); - } else if (SIE.VER.equals(itemType)) { - curVoucher = parseVER(di); - } else if ("".equals(itemType)) { - } else if ("{".equals(itemType)) { + if ("".equals(itemType) || "{".equals(itemType)) { + // blank lines and opening braces are ignored } else if ("}".equals(itemType)) { if (curVoucher != null) closeVoucher(curVoucher); curVoucher = null; } else { - callbacks.callbackException(new UnsupportedOperationException(di.getItemType())); + Consumer handler = handlers.get(itemType); + if (handler != null) { + handler.accept(di); + if (abortParsing) return null; + } else { + callbacks.callbackException(new UnsupportedOperationException(di.getItemType())); + } } } } @@ -319,6 +321,143 @@ public SieDocument readDocument(String fileName) throws IOException { return sieDocument; } + private void initHandlers() { + handlers.put(SIE.ADRESS, this::handleADRESS); + handlers.put(SIE.BKOD, di -> sieDocument.getFNAMN().setSni(di.getInt(0))); + handlers.put(SIE.BTRANS, this::handleBTRANS); + handlers.put(SIE.DIM, this::parseDimension); + handlers.put(SIE.ENHET, this::parseENHET); + handlers.put(SIE.FLAGGA, di -> sieDocument.setFLAGGA(di.getInt(0))); + handlers.put(SIE.FNAMN, di -> sieDocument.getFNAMN().setName(di.getString(0))); + handlers.put(SIE.FNR, di -> sieDocument.getFNAMN().setCode(di.getString(0))); + handlers.put(SIE.FORMAT, di -> sieDocument.setFORMAT(di.getString(0))); + handlers.put(SIE.FTYP, di -> sieDocument.getFNAMN().setOrgType(di.getString(0))); + handlers.put(SIE.GEN, this::handleGEN); + handlers.put(SIE.IB, this::parseIB); + handlers.put(SIE.KONTO, this::parseKONTO); + handlers.put(SIE.KSUMMA, this::handleKSUMMA); + handlers.put(SIE.KTYP, this::parseKTYP); + handlers.put(SIE.KPTYP, di -> sieDocument.setKPTYP(di.getString(0))); + handlers.put(SIE.OBJEKT, this::parseOBJEKT); + handlers.put(SIE.OIB, this::handleOIB); + handlers.put(SIE.OUB, this::handleOUB); + handlers.put(SIE.ORGNR, di -> sieDocument.getFNAMN().setOrgIdentifier(di.getString(0))); + handlers.put(SIE.OMFATTN, di -> sieDocument.setOMFATTN(di.getDate(0))); + handlers.put(SIE.PBUDGET, this::handlePBUDGET); + handlers.put(SIE.PROGRAM, di -> sieDocument.setPROGRAM(di.getData())); + handlers.put(SIE.PROSA, di -> sieDocument.setPROSA(di.getString(0))); + handlers.put(SIE.PSALDO, this::handlePSALDO); + handlers.put(SIE.RAR, this::parseRAR); + handlers.put(SIE.RTRANS, this::handleRTRANS); + handlers.put(SIE.SIETYP, this::handleSIETYP); + handlers.put(SIE.SRU, this::parseSRU); + handlers.put(SIE.TAXAR, di -> sieDocument.setTAXAR(di.getInt(0))); + handlers.put(SIE.UB, this::parseUB); + handlers.put(SIE.TRANS, this::handleTRANS); + handlers.put(SIE.RES, this::parseRES); + handlers.put(SIE.UNDERDIM, this::parseUnderDimension); + handlers.put(SIE.VALUTA, di -> sieDocument.setVALUTA(di.getString(0))); + handlers.put(SIE.VER, di -> curVoucher = parseVER(di)); + } + + private void handleADRESS(SieDataItem di) { + sieDocument.getFNAMN().setContact(di.getString(0)); + sieDocument.getFNAMN().setStreet(di.getString(1)); + sieDocument.getFNAMN().setZipCity(di.getString(2)); + sieDocument.getFNAMN().setPhone(di.getString(3)); + } + + private void handleGEN(SieDataItem di) { + sieDocument.setGEN_DATE(di.getDate(0)); + sieDocument.setGEN_NAMN(di.getString(1)); + } + + private void handleBTRANS(SieDataItem di) { + if (!ignoreBTRANS) { + if (curVoucher == null) { + callbacks.callbackException(new SieParseException( + "#BTRANS outside #VER block at line " + parsingLineNumber)); + } else { + parseTRANS(di, curVoucher); + } + } + } + + private void handleRTRANS(SieDataItem di) { + if (!ignoreRTRANS) { + if (curVoucher == null) { + callbacks.callbackException(new SieParseException( + "#RTRANS outside #VER block at line " + parsingLineNumber)); + } else { + parseTRANS(di, curVoucher); + } + } + } + + private void handleTRANS(SieDataItem di) { + if (curVoucher == null) { + callbacks.callbackException(new SieParseException( + "#TRANS outside #VER block at line " + parsingLineNumber)); + } else { + parseTRANS(di, curVoucher); + } + } + + private void handleKSUMMA(SieDataItem di) { + if (!ignoreKSUMMA) { + if (CRC.isStarted()) { + parseKSUMMA(di); + } else { + CRC.start(); + } + } + } + + private void handleSIETYP(SieDataItem di) { + sieDocument.setSIETYP(di.getInt(0)); + if (acceptSIETypes != null) { + try { + SieType parsed = SieType.fromValue(di.getInt(0)); + if (!acceptSIETypes.contains(parsed)) { + callbacks.callbackException(new SieInvalidFeatureException( + "SIE type " + di.getInt(0) + " is not accepted")); + abortParsing = true; + } + } catch (IllegalArgumentException e) { + callbacks.callbackException(new SieInvalidFeatureException( + "Unknown SIE type: " + di.getInt(0))); + } + } + } + + private void handleOIB(SieDataItem di) { + SiePeriodValue pv = parseOIB_OUB(di); + callbacks.callbackOIB(pv); + if (!streamValues) sieDocument.getOIB().add(pv); + } + + private void handleOUB(SieDataItem di) { + SiePeriodValue pv = parseOIB_OUB(di); + callbacks.callbackOUB(pv); + if (!streamValues) sieDocument.getOUB().add(pv); + } + + private void handlePBUDGET(SieDataItem di) { + SiePeriodValue pv = parsePBUDGET_PSALDO(di); + if (pv != null) { + callbacks.callbackPBUDGET(pv); + if (!streamValues) sieDocument.getPBUDGET().add(pv); + } + } + + private void handlePSALDO(SieDataItem di) { + SiePeriodValue pv = parsePBUDGET_PSALDO(di); + if (pv != null) { + callbacks.callbackPSALDO(pv); + if (!streamValues) sieDocument.getPSALDO().add(pv); + } + } + private void parseRAR(SieDataItem di) { SieBookingYear rar = new SieBookingYear(); rar.setId(di.getInt(0)); @@ -602,7 +741,7 @@ private void closeVoucher(SieVoucher v) { check = check.add(r.getAmount()); } if (check.compareTo(BigDecimal.ZERO) != 0) - callbacks.callbackException(new SieVoucherMissmatchException( + callbacks.callbackException(new SieVoucherMismatchException( v.getSeries() + "." + v.getNumber() + " Sum is not zero.")); } diff --git a/src/main/java/alipsa/sieparser/SieDocumentWriter.java b/src/main/java/alipsa/sieparser/SieDocumentWriter.java index 8693ae5..e39da94 100644 --- a/src/main/java/alipsa/sieparser/SieDocumentWriter.java +++ b/src/main/java/alipsa/sieparser/SieDocumentWriter.java @@ -68,9 +68,6 @@ public SieDocumentWriter(SieDocument sie, WriteOptions options) { * @throws IOException if an I/O error occurs */ public void write(String fileName) throws IOException { - File file = new File(fileName); - if (file.exists()) file.delete(); - try (BufferedWriter bw = IoUtil.getWriter(fileName)) { writer = bw; writeContent(); @@ -329,12 +326,17 @@ private void writeKPTYP() throws IOException { } private void writeADRESS() throws IOException { - if (!(sieDoc.getFNAMN().getContact() == null && sieDoc.getFNAMN().getStreet() == null && sieDoc.getFNAMN().getZipCity() == null && sieDoc.getFNAMN().getPhone() == null)) { - writeLine(SIE.ADRESS + " \"" + sieDoc.getFNAMN().getContact() + "\" \"" + sieDoc.getFNAMN().getStreet() + - "\" \"" + sieDoc.getFNAMN().getZipCity() + "\" \"" + sieDoc.getFNAMN().getPhone() + "\""); + SieCompany fnamn = sieDoc.getFNAMN(); + if (!(fnamn.getContact() == null && fnamn.getStreet() == null && fnamn.getZipCity() == null && fnamn.getPhone() == null)) { + writeLine(SIE.ADRESS + " \"" + nullToEmpty(fnamn.getContact()) + "\" \"" + nullToEmpty(fnamn.getStreet()) + + "\" \"" + nullToEmpty(fnamn.getZipCity()) + "\" \"" + nullToEmpty(fnamn.getPhone()) + "\""); } } + private static String nullToEmpty(String s) { + return s == null ? "" : s; + } + private String getFNAMN() { return SIE.FNAMN + " \"" + sieDoc.getFNAMN().getName() + "\""; } diff --git a/src/main/java/alipsa/sieparser/SieInvalidChecksumException.java b/src/main/java/alipsa/sieparser/SieInvalidChecksumException.java index 419e98e..59ab719 100644 --- a/src/main/java/alipsa/sieparser/SieInvalidChecksumException.java +++ b/src/main/java/alipsa/sieparser/SieInvalidChecksumException.java @@ -39,4 +39,13 @@ public SieInvalidChecksumException(String description) { super(description); } + /** + * Creates a new SieInvalidChecksumException with the given description and cause. + * + * @param description a message describing the checksum mismatch + * @param cause the underlying cause + */ + public SieInvalidChecksumException(String description, Throwable cause) { + super(description, cause); + } } diff --git a/src/main/java/alipsa/sieparser/SieInvalidFeatureException.java b/src/main/java/alipsa/sieparser/SieInvalidFeatureException.java index c744f98..0010e5a 100644 --- a/src/main/java/alipsa/sieparser/SieInvalidFeatureException.java +++ b/src/main/java/alipsa/sieparser/SieInvalidFeatureException.java @@ -40,4 +40,13 @@ public SieInvalidFeatureException(String description) { super(description); } + /** + * Creates a new SieInvalidFeatureException with the given description and cause. + * + * @param description a message describing the invalid feature usage + * @param cause the underlying cause + */ + public SieInvalidFeatureException(String description, Throwable cause) { + super(description, cause); + } } diff --git a/src/main/java/alipsa/sieparser/SieInvalidFileException.java b/src/main/java/alipsa/sieparser/SieInvalidFileException.java index 1af5766..8f07d4b 100644 --- a/src/main/java/alipsa/sieparser/SieInvalidFileException.java +++ b/src/main/java/alipsa/sieparser/SieInvalidFileException.java @@ -40,4 +40,13 @@ public SieInvalidFileException(String description) { super(description); } + /** + * Creates a new SieInvalidFileException with the given description and cause. + * + * @param description a message identifying the invalid file + * @param cause the underlying cause + */ + public SieInvalidFileException(String description, Throwable cause) { + super(description, cause); + } } diff --git a/src/main/java/alipsa/sieparser/SieMissingMandatoryDateException.java b/src/main/java/alipsa/sieparser/SieMissingMandatoryDateException.java index 23537f1..48d6a2b 100644 --- a/src/main/java/alipsa/sieparser/SieMissingMandatoryDateException.java +++ b/src/main/java/alipsa/sieparser/SieMissingMandatoryDateException.java @@ -39,4 +39,13 @@ public SieMissingMandatoryDateException(String description) { super(description); } + /** + * Creates a new SieMissingMandatoryDateException with the given description and cause. + * + * @param description a message describing which date is missing + * @param cause the underlying cause + */ + public SieMissingMandatoryDateException(String description, Throwable cause) { + super(description, cause); + } } diff --git a/src/main/java/alipsa/sieparser/SieMissingObjectException.java b/src/main/java/alipsa/sieparser/SieMissingObjectException.java index 9e77172..23e7c30 100644 --- a/src/main/java/alipsa/sieparser/SieMissingObjectException.java +++ b/src/main/java/alipsa/sieparser/SieMissingObjectException.java @@ -39,4 +39,13 @@ public SieMissingObjectException(String description) { super(description); } + /** + * Creates a new SieMissingObjectException with the given description and cause. + * + * @param description a message describing the missing object + * @param cause the underlying cause + */ + public SieMissingObjectException(String description, Throwable cause) { + super(description, cause); + } } diff --git a/src/main/java/alipsa/sieparser/SiePeriodValue.java b/src/main/java/alipsa/sieparser/SiePeriodValue.java index 80d105b..6f45ad5 100644 --- a/src/main/java/alipsa/sieparser/SiePeriodValue.java +++ b/src/main/java/alipsa/sieparser/SiePeriodValue.java @@ -170,17 +170,4 @@ public String getToken() { public void setToken(String token) { this.token = token; } - - /* - public SieVoucherRow toVoucherRow() throws Exception { - SieVoucherRow vr = new SieVoucherRow(); - return vr; - } - - public SieVoucherRow toInvertedVoucherRow() throws Exception { - SieVoucherRow vr = toVoucherRow(); - vr.setAmount(vr.getAmount() * -1); - return vr; - } - */ } diff --git a/src/main/java/alipsa/sieparser/SieVoucherMissmatchException.java b/src/main/java/alipsa/sieparser/SieVoucherMismatchException.java similarity index 71% rename from src/main/java/alipsa/sieparser/SieVoucherMissmatchException.java rename to src/main/java/alipsa/sieparser/SieVoucherMismatchException.java index a15bb46..454c068 100644 --- a/src/main/java/alipsa/sieparser/SieVoucherMissmatchException.java +++ b/src/main/java/alipsa/sieparser/SieVoucherMismatchException.java @@ -29,14 +29,23 @@ of this software and associated documentation files (the "Software"), to deal /** * Thrown when the transaction rows of a voucher do not sum to zero. */ -public class SieVoucherMissmatchException extends SieException { +public class SieVoucherMismatchException extends SieException { /** - * Creates a new SieVoucherMissmatchException with the given description. + * Creates a new SieVoucherMismatchException with the given description. * * @param description a message identifying the mismatched voucher */ - public SieVoucherMissmatchException(String description) { + public SieVoucherMismatchException(String description) { super(description); } + /** + * Creates a new SieVoucherMismatchException with the given description and cause. + * + * @param description a message identifying the mismatched voucher + * @param cause the underlying cause + */ + public SieVoucherMismatchException(String description, Throwable cause) { + super(description, cause); + } } diff --git a/src/main/java/alipsa/sieparser/sie5/Sie5DocumentReader.java b/src/main/java/alipsa/sieparser/sie5/Sie5DocumentReader.java index 05d484a..31c5d87 100644 --- a/src/main/java/alipsa/sieparser/sie5/Sie5DocumentReader.java +++ b/src/main/java/alipsa/sieparser/sie5/Sie5DocumentReader.java @@ -1,5 +1,6 @@ package alipsa.sieparser.sie5; +import alipsa.sieparser.SieException; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Unmarshaller; @@ -40,11 +41,15 @@ public Sie5DocumentReader() {} * * @param fileName the path to the SIE 5 XML file * @return the parsed {@link Sie5Document} - * @throws JAXBException if the XML cannot be parsed or does not conform to the expected structure + * @throws SieException if the XML cannot be parsed or does not conform to the expected structure */ - public Sie5Document readDocument(String fileName) throws JAXBException { - Unmarshaller unmarshaller = CONTEXT.createUnmarshaller(); - return (Sie5Document) unmarshaller.unmarshal(new File(fileName)); + public Sie5Document readDocument(String fileName) { + try { + Unmarshaller unmarshaller = CONTEXT.createUnmarshaller(); + return (Sie5Document) unmarshaller.unmarshal(new File(fileName)); + } catch (JAXBException e) { + throw new SieException("Failed to read SIE 5 document: " + fileName, e); + } } /** @@ -52,11 +57,15 @@ public Sie5Document readDocument(String fileName) throws JAXBException { * * @param stream the input stream containing SIE 5 XML data * @return the parsed {@link Sie5Document} - * @throws JAXBException if the XML cannot be parsed or does not conform to the expected structure + * @throws SieException if the XML cannot be parsed or does not conform to the expected structure */ - public Sie5Document readDocument(InputStream stream) throws JAXBException { - Unmarshaller unmarshaller = CONTEXT.createUnmarshaller(); - return (Sie5Document) unmarshaller.unmarshal(stream); + public Sie5Document readDocument(InputStream stream) { + try { + Unmarshaller unmarshaller = CONTEXT.createUnmarshaller(); + return (Sie5Document) unmarshaller.unmarshal(stream); + } catch (JAXBException e) { + throw new SieException("Failed to read SIE 5 document from stream", e); + } } /** @@ -64,11 +73,15 @@ public Sie5Document readDocument(InputStream stream) throws JAXBException { * * @param fileName the path to the SIE 5 entry XML file * @return the parsed {@link Sie5Entry} - * @throws JAXBException if the XML cannot be parsed or does not conform to the expected structure + * @throws SieException if the XML cannot be parsed or does not conform to the expected structure */ - public Sie5Entry readEntry(String fileName) throws JAXBException { - Unmarshaller unmarshaller = CONTEXT.createUnmarshaller(); - return (Sie5Entry) unmarshaller.unmarshal(new File(fileName)); + public Sie5Entry readEntry(String fileName) { + try { + Unmarshaller unmarshaller = CONTEXT.createUnmarshaller(); + return (Sie5Entry) unmarshaller.unmarshal(new File(fileName)); + } catch (JAXBException e) { + throw new SieException("Failed to read SIE 5 entry: " + fileName, e); + } } /** @@ -79,10 +92,14 @@ public Sie5Entry readEntry(String fileName) throws JAXBException { * * @param stream the input stream containing SIE 5 entry XML data * @return the parsed {@link Sie5Entry} - * @throws JAXBException if the XML cannot be parsed or does not conform to the expected structure + * @throws SieException if the XML cannot be parsed or does not conform to the expected structure */ - public Sie5Entry readEntry(InputStream stream) throws JAXBException { - Unmarshaller unmarshaller = CONTEXT.createUnmarshaller(); - return unmarshaller.unmarshal(new StreamSource(stream), Sie5Entry.class).getValue(); + public Sie5Entry readEntry(InputStream stream) { + try { + Unmarshaller unmarshaller = CONTEXT.createUnmarshaller(); + return unmarshaller.unmarshal(new StreamSource(stream), Sie5Entry.class).getValue(); + } catch (JAXBException e) { + throw new SieException("Failed to read SIE 5 entry from stream", e); + } } } diff --git a/src/main/java/alipsa/sieparser/sie5/Sie5DocumentWriter.java b/src/main/java/alipsa/sieparser/sie5/Sie5DocumentWriter.java index d68fb43..95f3879 100644 --- a/src/main/java/alipsa/sieparser/sie5/Sie5DocumentWriter.java +++ b/src/main/java/alipsa/sieparser/sie5/Sie5DocumentWriter.java @@ -1,5 +1,6 @@ package alipsa.sieparser.sie5; +import alipsa.sieparser.SieException; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Marshaller; @@ -38,11 +39,15 @@ public Sie5DocumentWriter() {} * * @param doc the document to write * @param fileName the path of the output file - * @throws JAXBException if the document cannot be marshalled + * @throws SieException if the document cannot be marshalled */ - public void write(Sie5Document doc, String fileName) throws JAXBException { - Marshaller marshaller = createMarshaller(); - marshaller.marshal(doc, new File(fileName)); + public void write(Sie5Document doc, String fileName) { + try { + Marshaller marshaller = createMarshaller(); + marshaller.marshal(doc, new File(fileName)); + } catch (JAXBException e) { + throw new SieException("Failed to write SIE 5 document: " + fileName, e); + } } /** @@ -50,11 +55,15 @@ public void write(Sie5Document doc, String fileName) throws JAXBException { * * @param doc the document to write * @param stream the output stream to write to - * @throws JAXBException if the document cannot be marshalled + * @throws SieException if the document cannot be marshalled */ - public void write(Sie5Document doc, OutputStream stream) throws JAXBException { - Marshaller marshaller = createMarshaller(); - marshaller.marshal(doc, stream); + public void write(Sie5Document doc, OutputStream stream) { + try { + Marshaller marshaller = createMarshaller(); + marshaller.marshal(doc, stream); + } catch (JAXBException e) { + throw new SieException("Failed to write SIE 5 document to stream", e); + } } /** @@ -62,11 +71,15 @@ public void write(Sie5Document doc, OutputStream stream) throws JAXBException { * * @param entry the entry document to write * @param fileName the path of the output file - * @throws JAXBException if the document cannot be marshalled + * @throws SieException if the document cannot be marshalled */ - public void writeEntry(Sie5Entry entry, String fileName) throws JAXBException { - Marshaller marshaller = createMarshaller(); - marshaller.marshal(entry, new File(fileName)); + public void writeEntry(Sie5Entry entry, String fileName) { + try { + Marshaller marshaller = createMarshaller(); + marshaller.marshal(entry, new File(fileName)); + } catch (JAXBException e) { + throw new SieException("Failed to write SIE 5 entry: " + fileName, e); + } } /** @@ -74,11 +87,15 @@ public void writeEntry(Sie5Entry entry, String fileName) throws JAXBException { * * @param entry the entry document to write * @param stream the output stream to write to - * @throws JAXBException if the document cannot be marshalled + * @throws SieException if the document cannot be marshalled */ - public void writeEntry(Sie5Entry entry, OutputStream stream) throws JAXBException { - Marshaller marshaller = createMarshaller(); - marshaller.marshal(entry, stream); + public void writeEntry(Sie5Entry entry, OutputStream stream) { + try { + Marshaller marshaller = createMarshaller(); + marshaller.marshal(entry, stream); + } catch (JAXBException e) { + throw new SieException("Failed to write SIE 5 entry to stream", e); + } } private Marshaller createMarshaller() throws JAXBException { diff --git a/src/test/java/alipsa/sieparser/SieDocumentReaderTest.java b/src/test/java/alipsa/sieparser/SieDocumentReaderTest.java index 144918b..516e0ca 100644 --- a/src/test/java/alipsa/sieparser/SieDocumentReaderTest.java +++ b/src/test/java/alipsa/sieparser/SieDocumentReaderTest.java @@ -1,11 +1,17 @@ package alipsa.sieparser; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.io.File; import java.io.IOException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; import java.util.EnumSet; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.*; @@ -27,7 +33,7 @@ public void readType2() throws IOException { URL url = Thread.currentThread().getContextClassLoader().getResource("samples/1_BL0001_typ2.SE"); assertNotNull(url); SieDocumentReader reader = new SieDocumentReader(); - reader.ignoreMissingOMFATTNING = true; + reader.setIgnoreMissingOMFATTNING(true); SieDocument doc = reader.readDocument(new File(url.getFile()).getAbsolutePath()); assertEquals(2, doc.getSIETYP()); } @@ -45,7 +51,7 @@ public void readType4() throws IOException { @Test public void allowUnbalancedVoucher() throws IOException { SieDocumentReader reader = new SieDocumentReader(); - reader.allowUnbalancedVoucher = true; + reader.setAllowUnbalancedVoucher(true); assertDoesNotThrow(() -> { // Should not throw even if vouchers are unbalanced // (the flag prevents the balance check) @@ -60,7 +66,7 @@ public void acceptSieTypesFilter() throws IOException { // Only accept type 2 - should reject type 4 file SieDocumentReader reader = new SieDocumentReader(); reader.setAcceptSIETypes(EnumSet.of(SieType.TYPE_2)); - reader.throwErrors = true; + reader.setThrowErrors(true); assertThrows(RuntimeException.class, () -> { reader.readDocument(new File(url.getFile()).getAbsolutePath()); @@ -85,7 +91,7 @@ public void ignoreKSUMMA() throws IOException { assertNotNull(url); SieDocumentReader reader = new SieDocumentReader(); - reader.ignoreKSUMMA = true; + reader.setIgnoreKSUMMA(true); SieDocument doc = reader.readDocument(new File(url.getFile()).getAbsolutePath()); assertNotNull(doc); } @@ -106,7 +112,7 @@ public void ignoreBTRANS() throws IOException { assertNotNull(url); SieDocumentReader reader = new SieDocumentReader(); - reader.ignoreBTRANS = true; + reader.setIgnoreBTRANS(true); SieDocument doc = reader.readDocument(new File(url.getFile()).getAbsolutePath()); assertNotNull(doc); @@ -148,4 +154,105 @@ public void rarsParsed() throws IOException { SieDocument doc = reader.readDocument(new File(url.getFile()).getAbsolutePath()); assertFalse(doc.getRars().isEmpty(), "Booking years should be parsed"); } + + @Test + public void ignoreRTRANS() throws IOException { + URL url = Thread.currentThread().getContextClassLoader().getResource("samples/3_BL0001_typ4.SE"); + assertNotNull(url); + + SieDocumentReader reader = new SieDocumentReader(); + reader.setIgnoreRTRANS(true); + SieDocument doc = reader.readDocument(new File(url.getFile()).getAbsolutePath()); + assertNotNull(doc); + + for (SieVoucher v : doc.getVER()) { + for (SieVoucherRow r : v.getRows()) { + assertNotEquals("#RTRANS", r.getToken()); + } + } + } + + @Test + public void streamValuesDoesNotStoreInDocument() throws IOException { + URL url = Thread.currentThread().getContextClassLoader().getResource("samples/1_BL0001_typ2.SE"); + assertNotNull(url); + + AtomicInteger ibCount = new AtomicInteger(0); + AtomicInteger ubCount = new AtomicInteger(0); + + SieDocumentReader reader = new SieDocumentReader(); + reader.setStreamValues(true); + reader.setIgnoreMissingOMFATTNING(true); + reader.getCallbacks().setIB(pv -> ibCount.incrementAndGet()); + reader.getCallbacks().setUB(pv -> ubCount.incrementAndGet()); + SieDocument doc = reader.readDocument(new File(url.getFile()).getAbsolutePath()); + + assertTrue(doc.getIB().isEmpty(), "IB should not be stored when streaming"); + assertTrue(doc.getUB().isEmpty(), "UB should not be stored when streaming"); + assertTrue(ibCount.get() > 0, "IB callback should have been called"); + assertTrue(ubCount.get() > 0, "UB callback should have been called"); + } + + @Test + public void constructorWithParameters() throws IOException { + URL url = Thread.currentThread().getContextClassLoader().getResource("samples/1_BL0001_typ2.SE"); + assertNotNull(url); + + SieDocumentReader reader = new SieDocumentReader(false, true, false, true); + assertTrue(reader.isIgnoreMissingOMFATTNING()); + assertTrue(reader.isThrowErrors()); + assertFalse(reader.isIgnoreBTRANS()); + assertFalse(reader.isStreamValues()); + + SieDocument doc = reader.readDocument(new File(url.getFile()).getAbsolutePath()); + assertNotNull(doc); + } + + @Test + public void invalidFileThrowsException(@TempDir Path tempDir) throws IOException { + Path badFile = tempDir.resolve("bad.SE"); + Files.writeString(badFile, "This is not a SIE file\n"); + + SieDocumentReader reader = new SieDocumentReader(); + reader.setThrowErrors(true); + assertThrows(SieInvalidFileException.class, () -> { + reader.readDocument(badFile.toString()); + }); + } + + @Test + public void throwErrorsFalseCollectsExceptions(@TempDir Path tempDir) throws IOException { + Path badFile = tempDir.resolve("bad.SE"); + Files.writeString(badFile, "This is not a SIE file\n"); + + SieDocumentReader reader = new SieDocumentReader(); + reader.setThrowErrors(false); + List collected = new ArrayList<>(); + reader.getCallbacks().setSieException(collected::add); + SieDocument doc = reader.readDocument(badFile.toString()); + + assertNull(doc, "Should return null for invalid file"); + assertFalse(collected.isEmpty(), "Exceptions should be collected"); + assertInstanceOf(SieInvalidFileException.class, collected.get(0)); + } + + @Test + public void invalidDateHandledGracefully(@TempDir Path tempDir) throws IOException { + // Create a minimal SIE file with an invalid date (Feb 30) + String content = "#FLAGGA 0\n#SIETYP 4\n#PROGRAM \"Test\" 1.0\n#FORMAT PC8\n" + + "#GEN 20220230\n#FNAMN \"Test\"\n#KONTO 1000 \"Kassa\"\n#RAR 0 20220101 20221231\n"; + Path sieFile = tempDir.resolve("invalid_date.SE"); + Files.writeString(sieFile, content, Encoding.getCharset()); + + List collected = new ArrayList<>(); + SieDocumentReader reader = new SieDocumentReader(); + reader.setThrowErrors(false); + reader.getCallbacks().setSieException(collected::add); + SieDocument doc = reader.readDocument(sieFile.toString()); + + // GEN date should be null due to invalid date + assertNull(doc.getGEN_DATE(), "Invalid date should result in null"); + assertTrue(collected.stream().anyMatch(e -> e instanceof SieDateException), + "Should report a SieDateException for invalid date"); + } } diff --git a/src/test/java/alipsa/sieparser/TestSieDocument.java b/src/test/java/alipsa/sieparser/TestSieDocument.java index 02ef44a..73cd881 100644 --- a/src/test/java/alipsa/sieparser/TestSieDocument.java +++ b/src/test/java/alipsa/sieparser/TestSieDocument.java @@ -162,7 +162,7 @@ private SieDocument readDocument(String fileName, boolean ignoreOMFATTN) { assertTrue(file.exists(), "SIE file not found at " + file.getAbsolutePath()); SieDocumentReader reader = new SieDocumentReader(); if (ignoreOMFATTN) { - reader.ignoreMissingOMFATTNING = true; + reader.setIgnoreMissingOMFATTNING(true); } doc = reader.readDocument(file.getAbsolutePath()); if (reader.getValidationExceptions().size() > 0) {