From 577992f744233d106ef603fcc316416936d418aa Mon Sep 17 00:00:00 2001 From: fillg1 Date: Wed, 8 Apr 2026 21:47:13 +0200 Subject: [PATCH 01/26] init --- .gitignore | 43 ++++ build.gradle | 20 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 234 ++++++++++++++++++ gradlew.bat | 89 +++++++ settings.gradle | 1 + src/main/java/ch/illgner/OrderCalculator.java | 21 ++ .../java/ch/illgner/OrderCalculatorTest.java | 32 +++ 9 files changed, 446 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/ch/illgner/OrderCalculator.java create mode 100644 src/test/java/ch/illgner/OrderCalculatorTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1fac4d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +.kotlin + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..304e7e2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java' +} + +group = 'ch.illgner' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform('org.junit:junit-bom:6.0.0') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..1a25252 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'Benchmark' \ No newline at end of file diff --git a/src/main/java/ch/illgner/OrderCalculator.java b/src/main/java/ch/illgner/OrderCalculator.java new file mode 100644 index 0000000..68fa8a9 --- /dev/null +++ b/src/main/java/ch/illgner/OrderCalculator.java @@ -0,0 +1,21 @@ +package ch.illgner; + +public class OrderCalculator { + + /** + * Berechnet den Endpreis nach Rabatt und inklusive 19% MwSt. + * @param basePrice Der Nettopreis + * @param discountRate Der Rabatt (z.B. 0.1 für 10%) + * @return Der Endpreis gerundet auf 2 Nachkommastellen + */ + public double calculateFinalPrice(double basePrice, double discountRate) { + // FEHLER 1: Logikfehler - berechnet nur den Rabattwert, nicht den neuen Preis + double discountedPrice = basePrice * discountRate; + + // FEHLER 2: MwSt wird auf den falschen Wert aufgeschlagen + double priceWithTax = discountedPrice * 1.19; + + // FEHLER 3: Keine Rundung auf 2 Nachkommastellen + return priceWithTax; + } +} \ No newline at end of file diff --git a/src/test/java/ch/illgner/OrderCalculatorTest.java b/src/test/java/ch/illgner/OrderCalculatorTest.java new file mode 100644 index 0000000..f62acda --- /dev/null +++ b/src/test/java/ch/illgner/OrderCalculatorTest.java @@ -0,0 +1,32 @@ +package ch.illgner; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class OrderCalculatorTest { + + @Test + public void testCalculateFinalPrice_NormalScenario() { + OrderCalculator calculator = new OrderCalculator(); + + // Szenario: 100€ Basis, 10% Rabatt = 90€. + // 90€ + 19% MwSt (17.10€) = 107.10€ + double result = calculator.calculateFinalPrice(100.0, 0.10); + + assertEquals(107.10, result, "Der Endpreis für 100€ mit 10% Rabatt muss 107.10 sein."); + } + + @Test + public void testCalculateFinalPrice_Rounding() { + OrderCalculator calculator = new OrderCalculator(); + + // Szenario: 10.55€ Basis, 5% Rabatt = 10.0225€ + // 10.0225€ + 19% MwSt = 11.926775€ -> Gerundet: 11.93€ + double result = calculator.calculateFinalPrice(10.55, 0.05); + + assertEquals(11.93, result, "Der Preis muss korrekt auf 2 Stellen gerundet werden."); + } +} \ No newline at end of file From 41e82217ea1db36f97a4660a8844c3ecb1a73ae1 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Wed, 8 Apr 2026 21:47:13 +0200 Subject: [PATCH 02/26] init --- .gitignore | 43 ++++ build.gradle | 20 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 234 ++++++++++++++++++ gradlew.bat | 89 +++++++ settings.gradle | 1 + src/main/java/ch/illgner/OrderCalculator.java | 18 ++ .../java/ch/illgner/OrderCalculatorTest.java | 32 +++ 9 files changed, 443 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/ch/illgner/OrderCalculator.java create mode 100644 src/test/java/ch/illgner/OrderCalculatorTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1fac4d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +.kotlin + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..304e7e2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java' +} + +group = 'ch.illgner' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform('org.junit:junit-bom:6.0.0') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 zcmb5WV{~QRw(p$^Dz@00IL3?^hro$gg*4VI_WAaTyVM5Foj~O|-84 z$;06hMwt*rV;^8iB z1~&0XWpYJmG?Ts^K9PC62H*`G}xom%S%yq|xvG~FIfP=9*f zZoDRJBm*Y0aId=qJ?7dyb)6)JGWGwe)MHeNSzhi)Ko6J<-m@v=a%NsP537lHe0R* z`If4$aaBA#S=w!2z&m>{lpTy^Lm^mg*3?M&7HFv}7K6x*cukLIGX;bQG|QWdn{%_6 zHnwBKr84#B7Z+AnBXa16a?or^R?+>$4`}{*a_>IhbjvyTtWkHw)|ay)ahWUd-qq$~ zMbh6roVsj;_qnC-R{G+Cy6bApVOinSU-;(DxUEl!i2)1EeQ9`hrfqj(nKI7?Z>Xur zoJz-a`PxkYit1HEbv|jy%~DO^13J-ut986EEG=66S}D3!L}Efp;Bez~7tNq{QsUMm zh9~(HYg1pA*=37C0}n4g&bFbQ+?-h-W}onYeE{q;cIy%eZK9wZjSwGvT+&Cgv z?~{9p(;bY_1+k|wkt_|N!@J~aoY@|U_RGoWX<;p{Nu*D*&_phw`8jYkMNpRTWx1H* z>J-Mi_!`M468#5Aix$$u1M@rJEIOc?k^QBc?T(#=n&*5eS#u*Y)?L8Ha$9wRWdH^3D4|Ps)Y?m0q~SiKiSfEkJ!=^`lJ(%W3o|CZ zSrZL-Xxc{OrmsQD&s~zPfNJOpSZUl%V8tdG%ei}lQkM+z@-4etFPR>GOH9+Y_F<3=~SXln9Kb-o~f>2a6Xz@AS3cn^;c_>lUwlK(n>z?A>NbC z`Ud8^aQy>wy=$)w;JZzA)_*Y$Z5hU=KAG&htLw1Uh00yE!|Nu{EZkch zY9O6x7Y??>!7pUNME*d!=R#s)ghr|R#41l!c?~=3CS8&zr6*aA7n9*)*PWBV2w+&I zpW1-9fr3j{VTcls1>ua}F*bbju_Xq%^v;-W~paSqlf zolj*dt`BBjHI)H9{zrkBo=B%>8}4jeBO~kWqO!~Thi!I1H(in=n^fS%nuL=X2+s!p}HfTU#NBGiwEBF^^tKU zbhhv+0dE-sbK$>J#t-J!B$TMgN@Wh5wTtK2BG}4BGfsZOoRUS#G8Cxv|6EI*n&Xxq zt{&OxCC+BNqz$9b0WM7_PyBJEVObHFh%%`~!@MNZlo*oXDCwDcFwT~Rls!aApL<)^ zbBftGKKBRhB!{?fX@l2_y~%ygNFfF(XJzHh#?`WlSL{1lKT*gJM zs>bd^H9NCxqxn(IOky5k-wALFowQr(gw%|`0991u#9jXQh?4l|l>pd6a&rx|v=fPJ z1mutj{YzpJ_gsClbWFk(G}bSlFi-6@mwoQh-XeD*j@~huW4(8ub%^I|azA)h2t#yG z7e_V_<4jlM3D(I+qX}yEtqj)cpzN*oCdYHa!nm%0t^wHm)EmFP*|FMw!tb@&`G-u~ zK)=Sf6z+BiTAI}}i{*_Ac$ffr*Wrv$F7_0gJkjx;@)XjYSh`RjAgrCck`x!zP>Ifu z&%he4P|S)H*(9oB4uvH67^0}I-_ye_!w)u3v2+EY>eD3#8QR24<;7?*hj8k~rS)~7 zSXs5ww)T(0eHSp$hEIBnW|Iun<_i`}VE0Nc$|-R}wlSIs5pV{g_Dar(Zz<4X3`W?K z6&CAIl4U(Qk-tTcK{|zYF6QG5ArrEB!;5s?tW7 zrE3hcFY&k)+)e{+YOJ0X2uDE_hd2{|m_dC}kgEKqiE9Q^A-+>2UonB+L@v3$9?AYw zVQv?X*pK;X4Ovc6Ev5Gbg{{Eu*7{N3#0@9oMI~}KnObQE#Y{&3mM4`w%wN+xrKYgD zB-ay0Q}m{QI;iY`s1Z^NqIkjrTlf`B)B#MajZ#9u41oRBC1oM1vq0i|F59> z#StM@bHt|#`2)cpl_rWB($DNJ3Lap}QM-+A$3pe}NyP(@+i1>o^fe-oxX#Bt`mcQc zb?pD4W%#ep|3%CHAYnr*^M6Czg>~L4?l16H1OozM{P*en298b+`i4$|w$|4AHbzqB zHpYUsHZET$Z0ztC;U+0*+amF!@PI%^oUIZy{`L{%O^i{Xk}X0&nl)n~tVEpcAJSJ} zverw15zP1P-O8h9nd!&hj$zuwjg?DoxYIw{jWM zW5_pj+wFy8Tsa9g<7Qa21WaV&;ejoYflRKcz?#fSH_)@*QVlN2l4(QNk| z4aPnv&mrS&0|6NHq05XQw$J^RR9T{3SOcMKCXIR1iSf+xJ0E_Wv?jEc*I#ZPzyJN2 zUG0UOXHl+PikM*&g$U@g+KbG-RY>uaIl&DEtw_Q=FYq?etc!;hEC_}UX{eyh%dw2V zTTSlap&5>PY{6I#(6`j-9`D&I#|YPP8a;(sOzgeKDWsLa!i-$frD>zr-oid!Hf&yS z!i^cr&7tN}OOGmX2)`8k?Tn!!4=tz~3hCTq_9CdiV!NIblUDxHh(FJ$zs)B2(t5@u z-`^RA1ShrLCkg0)OhfoM;4Z{&oZmAec$qV@ zGQ(7(!CBk<5;Ar%DLJ0p0!ResC#U<+3i<|vib1?{5gCebG7$F7URKZXuX-2WgF>YJ^i zMhHDBsh9PDU8dlZ$yJKtc6JA#y!y$57%sE>4Nt+wF1lfNIWyA`=hF=9Gj%sRwi@vd z%2eVV3y&dvAgyuJ=eNJR+*080dbO_t@BFJO<@&#yqTK&+xc|FRR;p;KVk@J3$S{p` zGaMj6isho#%m)?pOG^G0mzOAw0z?!AEMsv=0T>WWcE>??WS=fII$t$(^PDPMU(P>o z_*0s^W#|x)%tx8jIgZY~A2yG;US0m2ZOQt6yJqW@XNY_>_R7(Nxb8Ged6BdYW6{prd!|zuX$@Q2o6Ona8zzYC1u!+2!Y$Jc9a;wy+pXt}o6~Bu1oF1c zp7Y|SBTNi@=I(K%A60PMjM#sfH$y*c{xUgeSpi#HB`?|`!Tb&-qJ3;vxS!TIzuTZs-&%#bAkAyw9m4PJgvey zM5?up*b}eDEY+#@tKec)-c(#QF0P?MRlD1+7%Yk*jW;)`f;0a-ZJ6CQA?E%>i2Dt7T9?s|9ZF|KP4;CNWvaVKZ+Qeut;Jith_y{v*Ny6Co6!8MZx;Wgo z=qAi%&S;8J{iyD&>3CLCQdTX*$+Rx1AwA*D_J^0>suTgBMBb=*hefV+Ars#mmr+YsI3#!F@Xc1t4F-gB@6aoyT+5O(qMz*zG<9Qq*f0w^V!03rpr*-WLH}; zfM{xSPJeu6D(%8HU%0GEa%waFHE$G?FH^kMS-&I3)ycx|iv{T6Wx}9$$D&6{%1N_8 z_CLw)_9+O4&u94##vI9b-HHm_95m)fa??q07`DniVjAy`t7;)4NpeyAY(aAk(+T_O z1om+b5K2g_B&b2DCTK<>SE$Ode1DopAi)xaJjU>**AJK3hZrnhEQ9E`2=|HHe<^tv z63e(bn#fMWuz>4erc47}!J>U58%<&N<6AOAewyzNTqi7hJc|X{782&cM zHZYclNbBwU6673=!ClmxMfkC$(CykGR@10F!zN1Se83LR&a~$Ht&>~43OX22mt7tcZUpa;9@q}KDX3O&Ugp6< zLZLfIMO5;pTee1vNyVC$FGxzK2f>0Z-6hM82zKg44nWo|n}$Zk6&;5ry3`(JFEX$q zK&KivAe${e^5ZGc3a9hOt|!UOE&OocpVryE$Y4sPcs4rJ>>Kbi2_subQ9($2VN(3o zb~tEzMsHaBmBtaHAyES+d3A(qURgiskSSwUc9CfJ@99&MKp2sooSYZu+-0t0+L*!I zYagjOlPgx|lep9tiU%ts&McF6b0VE57%E0Ho%2oi?=Ks+5%aj#au^OBwNwhec zta6QAeQI^V!dF1C)>RHAmB`HnxyqWx?td@4sd15zPd*Fc9hpDXP23kbBenBxGeD$k z;%0VBQEJ-C)&dTAw_yW@k0u?IUk*NrkJ)(XEeI z9Y>6Vel>#s_v@=@0<{4A{pl=9cQ&Iah0iD0H`q)7NeCIRz8zx;! z^OO;1+IqoQNak&pV`qKW+K0^Hqp!~gSohcyS)?^P`JNZXw@gc6{A3OLZ?@1Uc^I2v z+X!^R*HCm3{7JPq{8*Tn>5;B|X7n4QQ0Bs79uTU%nbqOJh`nX(BVj!#f;#J+WZxx4 z_yM&1Y`2XzhfqkIMO7tB3raJKQS+H5F%o83bM+hxbQ zeeJm=Dvix$2j|b4?mDacb67v-1^lTp${z=jc1=j~QD>7c*@+1?py>%Kj%Ejp7Y-!? z8iYRUlGVrQPandAaxFfks53@2EC#0)%mrnmGRn&>=$H$S8q|kE_iWko4`^vCS2aWg z#!`RHUGyOt*k?bBYu3*j3u0gB#v(3tsije zgIuNNWNtrOkx@Pzs;A9un+2LX!zw+p3_NX^Sh09HZAf>m8l@O*rXy_82aWT$Q>iyy zqO7Of)D=wcSn!0+467&!Hl))eff=$aneB?R!YykdKW@k^_uR!+Q1tR)+IJb`-6=jj zymzA>Sv4>Z&g&WWu#|~GcP7qP&m*w-S$)7Xr;(duqCTe7p8H3k5>Y-n8438+%^9~K z3r^LIT_K{i7DgEJjIocw_6d0!<;wKT`X;&vv+&msmhAAnIe!OTdybPctzcEzBy88_ zWO{6i4YT%e4^WQZB)KHCvA(0tS zHu_Bg+6Ko%a9~$EjRB90`P(2~6uI@SFibxct{H#o&y40MdiXblu@VFXbhz>Nko;7R z70Ntmm-FePqhb%9gL+7U8@(ch|JfH5Fm)5${8|`Lef>LttM_iww6LW2X61ldBmG0z zax3y)njFe>j*T{i0s8D4=L>X^j0)({R5lMGVS#7(2C9@AxL&C-lZQx~czI7Iv+{%1 z2hEG>RzX4S8x3v#9sgGAnPzptM)g&LB}@%E>fy0vGSa(&q0ch|=ncKjNrK z`jA~jObJhrJ^ri|-)J^HUyeZXz~XkBp$VhcTEcTdc#a2EUOGVX?@mYx#Vy*!qO$Jv zQ4rgOJ~M*o-_Wptam=~krnmG*p^j!JAqoQ%+YsDFW7Cc9M%YPiBOrVcD^RY>m9Pd< zu}#9M?K{+;UIO!D9qOpq9yxUquQRmQNMo0pT`@$pVt=rMvyX)ph(-CCJLvUJy71DI zBk7oc7)-%ngdj~s@76Yse3L^gV0 z2==qfp&Q~L(+%RHP0n}+xH#k(hPRx(!AdBM$JCfJ5*C=K3ts>P?@@SZ_+{U2qFZb>4kZ{Go37{# zSQc+-dq*a-Vy4?taS&{Ht|MLRiS)Sn14JOONyXqPNnpq&2y~)6wEG0oNy>qvod$FF z`9o&?&6uZjhZ4_*5qWVrEfu(>_n2Xi2{@Gz9MZ8!YmjYvIMasE9yVQL10NBrTCczq zcTY1q^PF2l!Eraguf{+PtHV3=2A?Cu&NN&a8V(y;q(^_mFc6)%Yfn&X&~Pq zU1?qCj^LF(EQB1F`8NxNjyV%fde}dEa(Hx=r7$~ts2dzDwyi6ByBAIx$NllB4%K=O z$AHz1<2bTUb>(MCVPpK(E9wlLElo(aSd(Os)^Raum`d(g9Vd_+Bf&V;l=@mM=cC>) z)9b0enb)u_7V!!E_bl>u5nf&Rl|2r=2F3rHMdb7y9E}}F82^$Rf+P8%dKnOeKh1vs zhH^P*4Ydr^$)$h@4KVzxrHyy#cKmWEa9P5DJ|- zG;!Qi35Tp7XNj60=$!S6U#!(${6hyh7d4q=pF{`0t|N^|L^d8pD{O9@tF~W;#Je*P z&ah%W!KOIN;SyAEhAeTafJ4uEL`(RtnovM+cb(O#>xQnk?dzAjG^~4$dFn^<@-Na3 z395;wBnS{t*H;Jef2eE!2}u5Ns{AHj>WYZDgQJt8v%x?9{MXqJsGP|l%OiZqQ1aB! z%E=*Ig`(!tHh>}4_z5IMpg{49UvD*Pp9!pxt_gdAW%sIf3k6CTycOT1McPl=_#0?8 zVjz8Hj*Vy9c5-krd-{BQ{6Xy|P$6LJvMuX$* zA+@I_66_ET5l2&gk9n4$1M3LN8(yEViRx&mtd#LD}AqEs?RW=xKC(OCWH;~>(X6h!uDxXIPH06xh z*`F4cVlbDP`A)-fzf>MuScYsmq&1LUMGaQ3bRm6i7OsJ|%uhTDT zlvZA1M}nz*SalJWNT|`dBm1$xlaA>CCiQ zK`xD-RuEn>-`Z?M{1%@wewf#8?F|(@1e0+T4>nmlSRrNK5f)BJ2H*$q(H>zGD0>eL zQ!tl_Wk)k*e6v^m*{~A;@6+JGeWU-q9>?+L_#UNT%G?4&BnOgvm9@o7l?ov~XL+et zbGT)|G7)KAeqb=wHSPk+J1bdg7N3$vp(ekjI1D9V$G5Cj!=R2w=3*4!z*J-r-cyeb zd(i2KmX!|Lhey!snRw z?#$Gu%S^SQEKt&kep)up#j&9}e+3=JJBS(s>MH+|=R(`8xK{mmndWo_r`-w1#SeRD&YtAJ#GiVI*TkQZ}&aq<+bU2+coU3!jCI6E+Ad_xFW*ghnZ$q zAoF*i&3n1j#?B8x;kjSJD${1jdRB;)R*)Ao!9bd|C7{;iqDo|T&>KSh6*hCD!rwv= zyK#F@2+cv3=|S1Kef(E6Niv8kyLVLX&e=U;{0x{$tDfShqkjUME>f8d(5nzSkY6@! z^-0>DM)wa&%m#UF1F?zR`8Y3X#tA!*7Q$P3lZJ%*KNlrk_uaPkxw~ zxZ1qlE;Zo;nb@!SMazSjM>;34ROOoygo%SF);LL>rRonWwR>bmSd1XD^~sGSu$Gg# zFZ`|yKU0%!v07dz^v(tY%;So(e`o{ZYTX`hm;@b0%8|H>VW`*cr8R%3n|ehw2`(9B+V72`>SY}9^8oh$En80mZK9T4abVG*to;E z1_S6bgDOW?!Oy1LwYy=w3q~KKdbNtyH#d24PFjX)KYMY93{3-mPP-H>@M-_>N~DDu zENh~reh?JBAK=TFN-SfDfT^=+{w4ea2KNWXq2Y<;?(gf(FgVp8Zp-oEjKzB%2Iqj;48GmY3h=bcdYJ}~&4tS`Q1sb=^emaW$IC$|R+r-8V- zf0$gGE(CS_n4s>oicVk)MfvVg#I>iDvf~Ov8bk}sSxluG!6#^Z_zhB&U^`eIi1@j( z^CK$z^stBHtaDDHxn+R;3u+>Lil^}fj?7eaGB z&5nl^STqcaBxI@v>%zG|j))G(rVa4aY=B@^2{TFkW~YP!8!9TG#(-nOf^^X-%m9{Z zCC?iC`G-^RcBSCuk=Z`(FaUUe?hf3{0C>>$?Vs z`2Uud9M+T&KB6o4o9kvdi^Q=Bw!asPdxbe#W-Oaa#_NP(qpyF@bVxv5D5))srkU#m zj_KA+#7sqDn*Ipf!F5Byco4HOSd!Ui$l94|IbW%Ny(s1>f4|Mv^#NfB31N~kya9!k zWCGL-$0ZQztBate^fd>R!hXY_N9ZjYp3V~4_V z#eB)Kjr8yW=+oG)BuNdZG?jaZlw+l_ma8aET(s+-x+=F-t#Qoiuu1i`^x8Sj>b^U} zs^z<()YMFP7CmjUC@M=&lA5W7t&cxTlzJAts*%PBDAPuqcV5o7HEnqjif_7xGt)F% zGx2b4w{@!tE)$p=l3&?Bf#`+!-RLOleeRk3 z7#pF|w@6_sBmn1nECqdunmG^}pr5(ZJQVvAt$6p3H(16~;vO>?sTE`Y+mq5YP&PBo zvq!7#W$Gewy`;%6o^!Dtjz~x)T}Bdk*BS#=EY=ODD&B=V6TD2z^hj1m5^d6s)D*wk zu$z~D7QuZ2b?5`p)E8e2_L38v3WE{V`bVk;6fl#o2`) z99JsWhh?$oVRn@$S#)uK&8DL8>An0&S<%V8hnGD7Z^;Y(%6;^9!7kDQ5bjR_V+~wp zfx4m3z6CWmmZ<8gDGUyg3>t8wgJ5NkkiEm^(sedCicP^&3D%}6LtIUq>mXCAt{9eF zNXL$kGcoUTf_Lhm`t;hD-SE)m=iBnxRU(NyL}f6~1uH)`K!hmYZjLI%H}AmEF5RZt z06$wn63GHnApHXZZJ}s^s)j9(BM6e*7IBK6Bq(!)d~zR#rbxK9NVIlgquoMq z=eGZ9NR!SEqP6=9UQg#@!rtbbSBUM#ynF);zKX+|!Zm}*{H z+j=d?aZ2!?@EL7C~%B?6ouCKLnO$uWn;Y6Xz zX8dSwj732u(o*U3F$F=7xwxm>E-B+SVZH;O-4XPuPkLSt_?S0)lb7EEg)Mglk0#eS z9@jl(OnH4juMxY+*r03VDfPx_IM!Lmc(5hOI;`?d37f>jPP$?9jQQIQU@i4vuG6MagEoJrQ=RD7xt@8E;c zeGV*+Pt+t$@pt!|McETOE$9k=_C!70uhwRS9X#b%ZK z%q(TIUXSS^F0`4Cx?Rk07C6wI4!UVPeI~-fxY6`YH$kABdOuiRtl73MqG|~AzZ@iL&^s?24iS;RK_pdlWkhcF z@Wv-Om(Aealfg)D^adlXh9Nvf~Uf@y;g3Y)i(YP zEXDnb1V}1pJT5ZWyw=1i+0fni9yINurD=EqH^ciOwLUGi)C%Da)tyt=zq2P7pV5-G zR7!oq28-Fgn5pW|nlu^b!S1Z#r7!Wtr{5J5PQ>pd+2P7RSD?>(U7-|Y z7ZQ5lhYIl_IF<9?T9^IPK<(Hp;l5bl5tF9>X-zG14_7PfsA>6<$~A338iYRT{a@r_ zuXBaT=`T5x3=s&3=RYx6NgG>No4?5KFBVjE(swfcivcIpPQFx5l+O;fiGsOrl5teR z_Cm+;PW}O0Dwe_(4Z@XZ)O0W-v2X><&L*<~*q3dg;bQW3g7)a#3KiQP>+qj|qo*Hk z?57>f2?f@`=Fj^nkDKeRkN2d$Z@2eNKpHo}ksj-$`QKb6n?*$^*%Fb3_Kbf1(*W9K>{L$mud2WHJ=j0^=g30Xhg8$#g^?36`p1fm;;1@0Lrx+8t`?vN0ZorM zSW?rhjCE8$C|@p^sXdx z|NOHHg+fL;HIlqyLp~SSdIF`TnSHehNCU9t89yr@)FY<~hu+X`tjg(aSVae$wDG*C zq$nY(Y494R)hD!i1|IIyP*&PD_c2FPgeY)&mX1qujB1VHPG9`yFQpLFVQ0>EKS@Bp zAfP5`C(sWGLI?AC{XEjLKR4FVNw(4+9b?kba95ukgR1H?w<8F7)G+6&(zUhIE5Ef% z=fFkL3QKA~M@h{nzjRq!Y_t!%U66#L8!(2-GgFxkD1=JRRqk=n%G(yHKn%^&$dW>; zSjAcjETMz1%205se$iH_)ZCpfg_LwvnsZQAUCS#^FExp8O4CrJb6>JquNV@qPq~3A zZ<6dOU#6|8+fcgiA#~MDmcpIEaUO02L5#T$HV0$EMD94HT_eXLZ2Zi&(! z&5E>%&|FZ`)CN10tM%tLSPD*~r#--K(H-CZqIOb99_;m|D5wdgJ<1iOJz@h2Zkq?} z%8_KXb&hf=2Wza(Wgc;3v3TN*;HTU*q2?#z&tLn_U0Nt!y>Oo>+2T)He6%XuP;fgn z-G!#h$Y2`9>Jtf}hbVrm6D70|ERzLAU>3zoWhJmjWfgM^))T+2u$~5>HF9jQDkrXR z=IzX36)V75PrFjkQ%TO+iqKGCQ-DDXbaE;C#}!-CoWQx&v*vHfyI>$HNRbpvm<`O( zlx9NBWD6_e&J%Ous4yp~s6)Ghni!I6)0W;9(9$y1wWu`$gs<$9Mcf$L*piP zPR0Av*2%ul`W;?-1_-5Zy0~}?`e@Y5A&0H!^ApyVTT}BiOm4GeFo$_oPlDEyeGBbh z1h3q&Dx~GmUS|3@4V36&$2uO8!Yp&^pD7J5&TN{?xphf*-js1fP?B|`>p_K>lh{ij zP(?H%e}AIP?_i^f&Li=FDSQ`2_NWxL+BB=nQr=$ zHojMlXNGauvvwPU>ZLq!`bX-5F4jBJ&So{kE5+ms9UEYD{66!|k~3vsP+mE}x!>%P za98bAU0!h0&ka4EoiDvBM#CP#dRNdXJcb*(%=<(g+M@<)DZ!@v1V>;54En?igcHR2 zhubQMq}VSOK)onqHfczM7YA@s=9*ow;k;8)&?J3@0JiGcP! zP#00KZ1t)GyZeRJ=f0^gc+58lc4Qh*S7RqPIC6GugG1gXe$LIQMRCo8cHf^qXgAa2 z`}t>u2Cq1CbSEpLr~E=c7~=Qkc9-vLE%(v9N*&HF`(d~(0`iukl5aQ9u4rUvc8%m) zr2GwZN4!s;{SB87lJB;veebPmqE}tSpT>+`t?<457Q9iV$th%i__Z1kOMAswFldD6 ztbOvO337S5o#ZZgN2G99_AVqPv!?Gmt3pzgD+Hp3QPQ`9qJ(g=kjvD+fUSS3upJn! zqoG7acIKEFRX~S}3|{EWT$kdz#zrDlJU(rPkxjws_iyLKU8+v|*oS_W*-guAb&Pj1 z35Z`3z<&Jb@2Mwz=KXucNYdY#SNO$tcVFr9KdKm|%^e-TXzs6M`PBper%ajkrIyUe zp$vVxVs9*>Vp4_1NC~Zg)WOCPmOxI1V34QlG4!aSFOH{QqSVq1^1)- z0P!Z?tT&E-ll(pwf0?=F=yOzik=@nh1Clxr9}Vij89z)ePDSCYAqw?lVI?v?+&*zH z)p$CScFI8rrwId~`}9YWPFu0cW1Sf@vRELs&cbntRU6QfPK-SO*mqu|u~}8AJ!Q$z znzu}50O=YbjwKCuSVBs6&CZR#0FTu)3{}qJJYX(>QPr4$RqWiwX3NT~;>cLn*_&1H zaKpIW)JVJ>b{uo2oq>oQt3y=zJjb%fU@wLqM{SyaC6x2snMx-}ivfU<1- znu1Lh;i$3Tf$Kh5Uk))G!D1UhE8pvx&nO~w^fG)BC&L!_hQk%^p`Kp@F{cz>80W&T ziOK=Sq3fdRu*V0=S53rcIfWFazI}Twj63CG(jOB;$*b`*#B9uEnBM`hDk*EwSRdwP8?5T?xGUKs=5N83XsR*)a4|ijz|c{4tIU+4j^A5C<#5 z*$c_d=5ml~%pGxw#?*q9N7aRwPux5EyqHVkdJO=5J>84!X6P>DS8PTTz>7C#FO?k#edkntG+fJk8ZMn?pmJSO@`x-QHq;7^h6GEXLXo1TCNhH z8ZDH{*NLAjo3WM`xeb=X{((uv3H(8&r8fJJg_uSs_%hOH%JDD?hu*2NvWGYD+j)&` zz#_1%O1wF^o5ryt?O0n;`lHbzp0wQ?rcbW(F1+h7_EZZ9{>rePvLAPVZ_R|n@;b$;UchU=0j<6k8G9QuQf@76oiE*4 zXOLQ&n3$NR#p4<5NJMVC*S);5x2)eRbaAM%VxWu9ohlT;pGEk7;002enCbQ>2r-us z3#bpXP9g|mE`65VrN`+3mC)M(eMj~~eOf)do<@l+fMiTR)XO}422*1SL{wyY(%oMpBgJagtiDf zz>O6(m;};>Hi=t8o{DVC@YigqS(Qh+ix3Rwa9aliH}a}IlOCW1@?%h_bRbq-W{KHF z%Vo?-j@{Xi@=~Lz5uZP27==UGE15|g^0gzD|3x)SCEXrx`*MP^FDLl%pOi~~Il;dc z^hrwp9sYeT7iZ)-ajKy@{a`kr0-5*_!XfBpXwEcFGJ;%kV$0Nx;apKrur zJN2J~CAv{Zjj%FolyurtW8RaFmpn&zKJWL>(0;;+q(%(Hx!GMW4AcfP0YJ*Vz!F4g z!ZhMyj$BdXL@MlF%KeInmPCt~9&A!;cRw)W!Hi@0DY(GD_f?jeV{=s=cJ6e}JktJw zQORnxxj3mBxfrH=x{`_^Z1ddDh}L#V7i}$njUFRVwOX?qOTKjfPMBO4y(WiU<)epb zvB9L=%jW#*SL|Nd_G?E*_h1^M-$PG6Pc_&QqF0O-FIOpa4)PAEPsyvB)GKasmBoEt z?_Q2~QCYGH+hW31x-B=@5_AN870vY#KB~3a*&{I=f);3Kv7q4Q7s)0)gVYx2#Iz9g(F2;=+Iy4 z6KI^8GJ6D@%tpS^8boU}zpi=+(5GfIR)35PzrbuXeL1Y1N%JK7PG|^2k3qIqHfX;G zQ}~JZ-UWx|60P5?d1e;AHx!_;#PG%d=^X(AR%i`l0jSpYOpXoKFW~7ip7|xvN;2^? zsYC9fanpO7rO=V7+KXqVc;Q5z%Bj})xHVrgoR04sA2 zl~DAwv=!(()DvH*=lyhIlU^hBkA0$e*7&fJpB0|oB7)rqGK#5##2T`@_I^|O2x4GO z;xh6ROcV<9>?e0)MI(y++$-ksV;G;Xe`lh76T#Htuia+(UrIXrf9?

L(tZ$0BqX1>24?V$S+&kLZ`AodQ4_)P#Q3*4xg8}lMV-FLwC*cN$< zt65Rf%7z41u^i=P*qO8>JqXPrinQFapR7qHAtp~&RZ85$>ob|Js;GS^y;S{XnGiBc zGa4IGvDl?x%gY`vNhv8wgZnP#UYI-w*^4YCZnxkF85@ldepk$&$#3EAhrJY0U)lR{F6sM3SONV^+$;Zx8BD&Eku3K zKNLZyBni3)pGzU0;n(X@1fX8wYGKYMpLmCu{N5-}epPDxClPFK#A@02WM3!myN%bkF z|GJ4GZ}3sL{3{qXemy+#Uk{4>Kf8v11;f8I&c76+B&AQ8udd<8gU7+BeWC`akUU~U zgXoxie>MS@rBoyY8O8Tc&8id!w+_ooxcr!1?#rc$-|SBBtH6S?)1e#P#S?jFZ8u-Bs&k`yLqW|{j+%c#A4AQ>+tj$Y z^CZajspu$F%73E68Lw5q7IVREED9r1Ijsg#@DzH>wKseye>hjsk^{n0g?3+gs@7`i zHx+-!sjLx^fS;fY!ERBU+Q zVJ!e0hJH%P)z!y%1^ZyG0>PN@5W~SV%f>}c?$H8r;Sy-ui>aruVTY=bHe}$e zi&Q4&XK!qT7-XjCrDaufT@>ieQ&4G(SShUob0Q>Gznep9fR783jGuUynAqc6$pYX; z7*O@@JW>O6lKIk0G00xsm|=*UVTQBB`u1f=6wGAj%nHK_;Aqmfa!eAykDmi-@u%6~ z;*c!pS1@V8r@IX9j&rW&d*}wpNs96O2Ute>%yt{yv>k!6zfT6pru{F1M3P z2WN1JDYqoTB#(`kE{H676QOoX`cnqHl1Yaru)>8Ky~VU{)r#{&s86Vz5X)v15ULHA zAZDb{99+s~qI6;-dQ5DBjHJP@GYTwn;Dv&9kE<0R!d z8tf1oq$kO`_sV(NHOSbMwr=To4r^X$`sBW4$gWUov|WY?xccQJN}1DOL|GEaD_!@& z15p?Pj+>7d`@LvNIu9*^hPN)pwcv|akvYYq)ks%`G>!+!pW{-iXPZsRp8 z35LR;DhseQKWYSD`%gO&k$Dj6_6q#vjWA}rZcWtQr=Xn*)kJ9kacA=esi*I<)1>w^ zO_+E>QvjP)qiSZg9M|GNeLtO2D7xT6vsj`88sd!94j^AqxFLi}@w9!Y*?nwWARE0P znuI_7A-saQ+%?MFA$gttMV-NAR^#tjl_e{R$N8t2NbOlX373>e7Ox=l=;y#;M7asp zRCz*CLnrm$esvSb5{T<$6CjY zmZ(i{Rs_<#pWW>(HPaaYj`%YqBra=Ey3R21O7vUbzOkJJO?V`4-D*u4$Me0Bx$K(lYo`JO}gnC zx`V}a7m-hLU9Xvb@K2ymioF)vj12<*^oAqRuG_4u%(ah?+go%$kOpfb`T96P+L$4> zQ#S+sA%VbH&mD1k5Ak7^^dZoC>`1L%i>ZXmooA!%GI)b+$D&ziKrb)a=-ds9xk#~& z7)3iem6I|r5+ZrTRe_W861x8JpD`DDIYZNm{$baw+$)X^Jtjnl0xlBgdnNY}x%5za zkQ8E6T<^$sKBPtL4(1zi_Rd(tVth*3Xs!ulflX+70?gb&jRTnI8l+*Aj9{|d%qLZ+ z>~V9Z;)`8-lds*Zgs~z1?Fg?Po7|FDl(Ce<*c^2=lFQ~ahwh6rqSjtM5+$GT>3WZW zj;u~w9xwAhOc<kF}~`CJ68 z?(S5vNJa;kriPlim33{N5`C{9?NWhzsna_~^|K2k4xz1`xcui*LXL-1#Y}Hi9`Oo!zQ>x-kgAX4LrPz63uZ+?uG*84@PKq-KgQlMNRwz=6Yes) zY}>YN+qP}nwr$(CZQFjUOI=-6J$2^XGvC~EZ+vrqWaOXB$k?%Suf5k=4>AveC1aJ! ziaW4IS%F$_Babi)kA8Y&u4F7E%99OPtm=vzw$$ zEz#9rvn`Iot_z-r3MtV>k)YvErZ<^Oa${`2>MYYODSr6?QZu+be-~MBjwPGdMvGd!b!elsdi4% z`37W*8+OGulab8YM?`KjJ8e+jM(tqLKSS@=jimq3)Ea2EB%88L8CaM+aG7;27b?5` z4zuUWBr)f)k2o&xg{iZ$IQkJ+SK>lpq4GEacu~eOW4yNFLU!Kgc{w4&D$4ecm0f}~ zTTzquRW@`f0}|IILl`!1P+;69g^upiPA6F{)U8)muWHzexRenBU$E^9X-uIY2%&1w z_=#5*(nmxJ9zF%styBwivi)?#KMG96-H@hD-H_&EZiRNsfk7mjBq{L%!E;Sqn!mVX*}kXhwH6eh;b42eD!*~upVG@ z#smUqz$ICm!Y8wY53gJeS|Iuard0=;k5i5Z_hSIs6tr)R4n*r*rE`>38Pw&lkv{_r!jNN=;#?WbMj|l>cU(9trCq; z%nN~r^y7!kH^GPOf3R}?dDhO=v^3BeP5hF|%4GNQYBSwz;x({21i4OQY->1G=KFyu z&6d`f2tT9Yl_Z8YACZaJ#v#-(gcyeqXMhYGXb=t>)M@fFa8tHp2x;ODX=Ap@a5I=U z0G80^$N0G4=U(>W%mrrThl0DjyQ-_I>+1Tdd_AuB3qpYAqY54upwa3}owa|x5iQ^1 zEf|iTZxKNGRpI>34EwkIQ2zHDEZ=(J@lRaOH>F|2Z%V_t56Km$PUYu^xA5#5Uj4I4RGqHD56xT%H{+P8Ag>e_3pN$4m8n>i%OyJFPNWaEnJ4McUZPa1QmOh?t8~n& z&RulPCors8wUaqMHECG=IhB(-tU2XvHP6#NrLVyKG%Ee*mQ5Ps%wW?mcnriTVRc4J`2YVM>$ixSF2Xi+Wn(RUZnV?mJ?GRdw%lhZ+t&3s7g!~g{%m&i<6 z5{ib-<==DYG93I(yhyv4jp*y3#*WNuDUf6`vTM%c&hiayf(%=x@4$kJ!W4MtYcE#1 zHM?3xw63;L%x3drtd?jot!8u3qeqctceX3m;tWetK+>~q7Be$h>n6riK(5@ujLgRS zvOym)k+VAtyV^mF)$29Y`nw&ijdg~jYpkx%*^ z8dz`C*g=I?;clyi5|!27e2AuSa$&%UyR(J3W!A=ZgHF9OuKA34I-1U~pyD!KuRkjA zbkN!?MfQOeN>DUPBxoy5IX}@vw`EEB->q!)8fRl_mqUVuRu|C@KD-;yl=yKc=ZT0% zB$fMwcC|HE*0f8+PVlWHi>M`zfsA(NQFET?LrM^pPcw`cK+Mo0%8*x8@65=CS_^$cG{GZQ#xv($7J z??R$P)nPLodI;P!IC3eEYEHh7TV@opr#*)6A-;EU2XuogHvC;;k1aI8asq7ovoP!* z?x%UoPrZjj<&&aWpsbr>J$Er-7!E(BmOyEv!-mbGQGeJm-U2J>74>o5x`1l;)+P&~ z>}f^=Rx(ZQ2bm+YE0u=ZYrAV@apyt=v1wb?R@`i_g64YyAwcOUl=C!i>=Lzb$`tjv zOO-P#A+)t-JbbotGMT}arNhJmmGl-lyUpMn=2UacVZxmiG!s!6H39@~&uVokS zG=5qWhfW-WOI9g4!R$n7!|ViL!|v3G?GN6HR0Pt_L5*>D#FEj5wM1DScz4Jv@Sxnl zB@MPPmdI{(2D?;*wd>3#tjAirmUnQoZrVv`xM3hARuJksF(Q)wd4P$88fGYOT1p6U z`AHSN!`St}}UMBT9o7i|G`r$ zrB=s$qV3d6$W9@?L!pl0lf%)xs%1ko^=QY$ty-57=55PvP(^6E7cc zGJ*>m2=;fOj?F~yBf@K@9qwX0hA803Xw+b0m}+#a(>RyR8}*Y<4b+kpp|OS+!whP( zH`v{%s>jsQI9rd$*vm)EkwOm#W_-rLTHcZRek)>AtF+~<(did)*oR1|&~1|e36d-d zgtm5cv1O0oqgWC%Et@P4Vhm}Ndl(Y#C^MD03g#PH-TFy+7!Osv1z^UWS9@%JhswEq~6kSr2DITo59+; ze=ZC}i2Q?CJ~Iyu?vn|=9iKV>4j8KbxhE4&!@SQ^dVa-gK@YfS9xT(0kpW*EDjYUkoj! zE49{7H&E}k%5(>sM4uGY)Q*&3>{aitqdNnRJkbOmD5Mp5rv-hxzOn80QsG=HJ_atI-EaP69cacR)Uvh{G5dTpYG7d zbtmRMq@Sexey)||UpnZ?;g_KMZq4IDCy5}@u!5&B^-=6yyY{}e4Hh3ee!ZWtL*s?G zxG(A!<9o!CL+q?u_utltPMk+hn?N2@?}xU0KlYg?Jco{Yf@|mSGC<(Zj^yHCvhmyx z?OxOYoxbptDK()tsJ42VzXdINAMWL$0Gcw?G(g8TMB)Khw_|v9`_ql#pRd2i*?CZl z7k1b!jQB=9-V@h%;Cnl7EKi;Y^&NhU0mWEcj8B|3L30Ku#-9389Q+(Yet0r$F=+3p z6AKOMAIi|OHyzlHZtOm73}|ntKtFaXF2Fy|M!gOh^L4^62kGUoWS1i{9gsds_GWBc zLw|TaLP64z3z9?=R2|T6Xh2W4_F*$cq>MtXMOy&=IPIJ`;!Tw?PqvI2b*U1)25^<2 zU_ZPoxg_V0tngA0J+mm?3;OYw{i2Zb4x}NedZug!>EoN3DC{1i)Z{Z4m*(y{ov2%- zk(w>+scOO}MN!exSc`TN)!B=NUX`zThWO~M*ohqq;J2hx9h9}|s#?@eR!=F{QTrq~ zTcY|>azkCe$|Q0XFUdpFT=lTcyW##i;-e{}ORB4D?t@SfqGo_cS z->?^rh$<&n9DL!CF+h?LMZRi)qju!meugvxX*&jfD!^1XB3?E?HnwHP8$;uX{Rvp# zh|)hM>XDv$ZGg=$1{+_bA~u-vXqlw6NH=nkpyWE0u}LQjF-3NhATL@9rRxMnpO%f7 z)EhZf{PF|mKIMFxnC?*78(}{Y)}iztV12}_OXffJ;ta!fcFIVjdchyHxH=t%ci`Xd zX2AUB?%?poD6Zv*&BA!6c5S#|xn~DK01#XvjT!w!;&`lDXSJT4_j$}!qSPrb37vc{ z9^NfC%QvPu@vlxaZ;mIbn-VHA6miwi8qJ~V;pTZkKqqOii<1Cs}0i?uUIss;hM4dKq^1O35y?Yp=l4i zf{M!@QHH~rJ&X~8uATV><23zZUbs-J^3}$IvV_ANLS08>k`Td7aU_S1sLsfi*C-m1 z-e#S%UGs4E!;CeBT@9}aaI)qR-6NU@kvS#0r`g&UWg?fC7|b^_HyCE!8}nyh^~o@< zpm7PDFs9yxp+byMS(JWm$NeL?DNrMCNE!I^ko-*csB+dsf4GAq{=6sfyf4wb>?v1v zmb`F*bN1KUx-`ra1+TJ37bXNP%`-Fd`vVQFTwWpX@;s(%nDQa#oWhgk#mYlY*!d>( zE&!|ySF!mIyfING+#%RDY3IBH_fW$}6~1%!G`suHub1kP@&DoAd5~7J55;5_noPI6eLf{t;@9Kf<{aO0`1WNKd?<)C-|?C?)3s z>wEq@8=I$Wc~Mt$o;g++5qR+(6wt9GI~pyrDJ%c?gPZe)owvy^J2S=+M^ z&WhIE`g;;J^xQLVeCtf7b%Dg#Z2gq9hp_%g)-%_`y*zb; zn9`f`mUPN-Ts&fFo(aNTsXPA|J!TJ{0hZp0^;MYHLOcD=r_~~^ymS8KLCSeU3;^QzJNqS z5{5rEAv#l(X?bvwxpU;2%pQftF`YFgrD1jt2^~Mt^~G>T*}A$yZc@(k9orlCGv&|1 zWWvVgiJsCAtamuAYT~nzs?TQFt<1LSEx!@e0~@yd6$b5!Zm(FpBl;(Cn>2vF?k zOm#TTjFwd2D-CyA!mqR^?#Uwm{NBemP>(pHmM}9;;8`c&+_o3#E5m)JzfwN?(f-a4 zyd%xZc^oQx3XT?vcCqCX&Qrk~nu;fxs@JUoyVoi5fqpi&bUhQ2y!Ok2pzsFR(M(|U zw3E+kH_zmTRQ9dUMZWRE%Zakiwc+lgv7Z%|YO9YxAy`y28`Aw;WU6HXBgU7fl@dnt z-fFBV)}H-gqP!1;V@Je$WcbYre|dRdp{xt!7sL3Eoa%IA`5CAA%;Wq8PktwPdULo! z8!sB}Qt8#jH9Sh}QiUtEPZ6H0b*7qEKGJ%ITZ|vH)5Q^2m<7o3#Z>AKc%z7_u`rXA zqrCy{-{8;9>dfllLu$^M5L z-hXs))h*qz%~ActwkIA(qOVBZl2v4lwbM>9l70Y`+T*elINFqt#>OaVWoja8RMsep z6Or3f=oBnA3vDbn*+HNZP?8LsH2MY)x%c13@(XfuGR}R?Nu<|07{$+Lc3$Uv^I!MQ z>6qWgd-=aG2Y^24g4{Bw9ueOR)(9h`scImD=86dD+MnSN4$6 z^U*o_mE-6Rk~Dp!ANp#5RE9n*LG(Vg`1)g6!(XtDzsov$Dvz|Gv1WU68J$CkshQhS zCrc|cdkW~UK}5NeaWj^F4MSgFM+@fJd{|LLM)}_O<{rj z+?*Lm?owq?IzC%U%9EBga~h-cJbIu=#C}XuWN>OLrc%M@Gu~kFEYUi4EC6l#PR2JS zQUkGKrrS#6H7}2l0F@S11DP`@pih0WRkRJl#F;u{c&ZC{^$Z+_*lB)r)-bPgRFE;* zl)@hK4`tEP=P=il02x7-C7p%l=B`vkYjw?YhdJU9!P!jcmY$OtC^12w?vy3<<=tlY zUwHJ_0lgWN9vf>1%WACBD{UT)1qHQSE2%z|JHvP{#INr13jM}oYv_5#xsnv9`)UAO zuwgyV4YZ;O)eSc3(mka6=aRohi!HH@I#xq7kng?Acdg7S4vDJb6cI5fw?2z%3yR+| zU5v@Hm}vy;${cBp&@D=HQ9j7NcFaOYL zj-wV=eYF{|XTkFNM2uz&T8uH~;)^Zo!=KP)EVyH6s9l1~4m}N%XzPpduPg|h-&lL` zAXspR0YMOKd2yO)eMFFJ4?sQ&!`dF&!|niH*!^*Ml##o0M(0*uK9&yzekFi$+mP9s z>W9d%Jb)PtVi&-Ha!o~Iyh@KRuKpQ@)I~L*d`{O8!kRObjO7=n+Gp36fe!66neh+7 zW*l^0tTKjLLzr`x4`_8&on?mjW-PzheTNox8Hg7Nt@*SbE-%kP2hWYmHu#Fn@Q^J(SsPUz*|EgOoZ6byg3ew88UGdZ>9B2Tq=jF72ZaR=4u%1A6Vm{O#?@dD!(#tmR;eP(Fu z{$0O%=Vmua7=Gjr8nY%>ul?w=FJ76O2js&17W_iq2*tb!i{pt#`qZB#im9Rl>?t?0c zicIC}et_4d+CpVPx)i4~$u6N-QX3H77ez z?ZdvXifFk|*F8~L(W$OWM~r`pSk5}#F?j_5u$Obu9lDWIknO^AGu+Blk7!9Sb;NjS zncZA?qtASdNtzQ>z7N871IsPAk^CC?iIL}+{K|F@BuG2>qQ;_RUYV#>hHO(HUPpk@ z(bn~4|F_jiZi}Sad;_7`#4}EmD<1EiIxa48QjUuR?rC}^HRocq`OQPM@aHVKP9E#q zy%6bmHygCpIddPjE}q_DPC`VH_2m;Eey&ZH)E6xGeStOK7H)#+9y!%-Hm|QF6w#A( zIC0Yw%9j$s-#odxG~C*^MZ?M<+&WJ+@?B_QPUyTg9DJGtQN#NIC&-XddRsf3n^AL6 zT@P|H;PvN;ZpL0iv$bRb7|J{0o!Hq+S>_NrH4@coZtBJu#g8#CbR7|#?6uxi8d+$g z87apN>EciJZ`%Zv2**_uiET9Vk{pny&My;+WfGDw4EVL#B!Wiw&M|A8f1A@ z(yFQS6jfbH{b8Z-S7D2?Ixl`j0{+ZnpT=;KzVMLW{B$`N?Gw^Fl0H6lT61%T2AU**!sX0u?|I(yoy&Xveg7XBL&+>n6jd1##6d>TxE*Vj=8lWiG$4=u{1UbAa5QD>5_ z;Te^42v7K6Mmu4IWT6Rnm>oxrl~b<~^e3vbj-GCdHLIB_>59}Ya+~OF68NiH=?}2o zP(X7EN=quQn&)fK>M&kqF|<_*H`}c zk=+x)GU>{Af#vx&s?`UKUsz})g^Pc&?Ka@t5$n$bqf6{r1>#mWx6Ep>9|A}VmWRnowVo`OyCr^fHsf# zQjQ3Ttp7y#iQY8l`zEUW)(@gGQdt(~rkxlkefskT(t%@i8=|p1Y9Dc5bc+z#n$s13 zGJk|V0+&Ekh(F};PJzQKKo+FG@KV8a<$gmNSD;7rd_nRdc%?9)p!|B-@P~kxQG}~B zi|{0}@}zKC(rlFUYp*dO1RuvPC^DQOkX4<+EwvBAC{IZQdYxoq1Za!MW7%p7gGr=j zzWnAq%)^O2$eItftC#TTSArUyL$U54-O7e|)4_7%Q^2tZ^0-d&3J1}qCzR4dWX!)4 zzIEKjgnYgMus^>6uw4Jm8ga6>GBtMjpNRJ6CP~W=37~||gMo_p@GA@#-3)+cVYnU> zE5=Y4kzl+EbEh%dhQokB{gqNDqx%5*qBusWV%!iprn$S!;oN_6E3?0+umADVs4ako z?P+t?m?};gev9JXQ#Q&KBpzkHPde_CGu-y z<{}RRAx=xlv#mVi+Ibrgx~ujW$h{?zPfhz)Kp7kmYS&_|97b&H&1;J-mzrBWAvY} zh8-I8hl_RK2+nnf&}!W0P+>5?#?7>npshe<1~&l_xqKd0_>dl_^RMRq@-Myz&|TKZBj1=Q()) zF{dBjv5)h=&Z)Aevx}+i|7=R9rG^Di!sa)sZCl&ctX4&LScQ-kMncgO(9o6W6)yd< z@Rk!vkja*X_N3H=BavGoR0@u0<}m-7|2v!0+2h~S2Q&a=lTH91OJsvms2MT~ zY=c@LO5i`mLpBd(vh|)I&^A3TQLtr>w=zoyzTd=^f@TPu&+*2MtqE$Avf>l>}V|3-8Fp2hzo3y<)hr_|NO(&oSD z!vEjTWBxbKTiShVl-U{n*B3#)3a8$`{~Pk}J@elZ=>Pqp|MQ}jrGv7KrNcjW%TN_< zZz8kG{#}XoeWf7qY?D)L)8?Q-b@Na&>i=)(@uNo zr;cH98T3$Iau8Hn*@vXi{A@YehxDE2zX~o+RY`)6-X{8~hMpc#C`|8y> zU8Mnv5A0dNCf{Ims*|l-^ z(MRp{qoGohB34|ggDI*p!Aw|MFyJ|v+<+E3brfrI)|+l3W~CQLPbnF@G0)P~Ly!1TJLp}xh8uW`Q+RB-v`MRYZ9Gam3cM%{ zb4Cb*f)0deR~wtNb*8w-LlIF>kc7DAv>T0D(a3@l`k4TFnrO+g9XH7;nYOHxjc4lq zMmaW6qpgAgy)MckYMhl?>sq;-1E)-1llUneeA!ya9KM$)DaNGu57Z5aE>=VST$#vb zFo=uRHr$0M{-ha>h(D_boS4zId;3B|Tpqo|?B?Z@I?G(?&Iei+-{9L_A9=h=Qfn-U z1wIUnQe9!z%_j$F_{rf&`ZFSott09gY~qrf@g3O=Y>vzAnXCyL!@(BqWa)Zqt!#_k zfZHuwS52|&&)aK;CHq9V-t9qt0au{$#6c*R#e5n3rje0hic7c7m{kW$p(_`wB=Gw7 z4k`1Hi;Mc@yA7dp@r~?@rfw)TkjAW++|pkfOG}0N|2guek}j8Zen(!+@7?qt_7ndX zB=BG6WJ31#F3#Vk3=aQr8T)3`{=p9nBHlKzE0I@v`{vJ}h8pd6vby&VgFhzH|q;=aonunAXL6G2y(X^CtAhWr*jI zGjpY@raZDQkg*aMq}Ni6cRF z{oWv}5`nhSAv>usX}m^GHt`f(t8@zHc?K|y5Zi=4G*UG1Sza{$Dpj%X8 zzEXaKT5N6F5j4J|w#qlZP!zS7BT)9b+!ZSJdToqJts1c!)fwih4d31vfb{}W)EgcA zH2pZ^8_k$9+WD2n`6q5XbOy8>3pcYH9 z07eUB+p}YD@AH!}p!iKv><2QF-Y^&xx^PAc1F13A{nUeCDg&{hnix#FiO!fe(^&%Qcux!h znu*S!s$&nnkeotYsDthh1dq(iQrE|#f_=xVgfiiL&-5eAcC-> z5L0l|DVEM$#ulf{bj+Y~7iD)j<~O8CYM8GW)dQGq)!mck)FqoL^X zwNdZb3->hFrbHFm?hLvut-*uK?zXn3q1z|UX{RZ;-WiLoOjnle!xs+W0-8D)kjU#R z+S|A^HkRg$Ij%N4v~k`jyHffKaC~=wg=9)V5h=|kLQ@;^W!o2^K+xG&2n`XCd>OY5Ydi= zgHH=lgy++erK8&+YeTl7VNyVm9-GfONlSlVb3)V9NW5tT!cJ8d7X)!b-$fb!s76{t z@d=Vg-5K_sqHA@Zx-L_}wVnc@L@GL9_K~Zl(h5@AR#FAiKad8~KeWCo@mgXIQ#~u{ zgYFwNz}2b6Vu@CP0XoqJ+dm8px(5W5-Jpis97F`+KM)TuP*X8H@zwiVKDKGVp59pI zifNHZr|B+PG|7|Y<*tqap0CvG7tbR1R>jn70t1X`XJixiMVcHf%Ez*=xm1(CrTSDt z0cle!+{8*Ja&EOZ4@$qhBuKQ$U95Q%rc7tg$VRhk?3=pE&n+T3upZg^ZJc9~c2es% zh7>+|mrmA-p&v}|OtxqmHIBgUxL~^0+cpfkSK2mhh+4b=^F1Xgd2)}U*Yp+H?ls#z zrLxWg_hm}AfK2XYWr!rzW4g;+^^&bW%LmbtRai9f3PjU${r@n`JThy-cphbcwn)rq9{A$Ht`lmYKxOacy z6v2R(?gHhD5@&kB-Eg?4!hAoD7~(h>(R!s1c1Hx#s9vGPePUR|of32bS`J5U5w{F) z>0<^ktO2UHg<0{oxkdOQ;}coZDQph8p6ruj*_?uqURCMTac;>T#v+l1Tc~%^k-Vd@ zkc5y35jVNc49vZpZx;gG$h{%yslDI%Lqga1&&;mN{Ush1c7p>7e-(zp}6E7f-XmJb4nhk zb8zS+{IVbL$QVF8pf8}~kQ|dHJAEATmmnrb_wLG}-yHe>W|A&Y|;muy-d^t^<&)g5SJfaTH@P1%euONny=mxo+C z4N&w#biWY41r8k~468tvuYVh&XN&d#%QtIf9;iVXfWY)#j=l`&B~lqDT@28+Y!0E+MkfC}}H*#(WKKdJJq=O$vNYCb(ZG@p{fJgu;h z21oHQ(14?LeT>n5)s;uD@5&ohU!@wX8w*lB6i@GEH0pM>YTG+RAIWZD;4#F1&F%Jp zXZUml2sH0!lYJT?&sA!qwez6cXzJEd(1ZC~kT5kZSp7(@=H2$Azb_*W&6aA|9iwCL zdX7Q=42;@dspHDwYE?miGX#L^3xD&%BI&fN9^;`v4OjQXPBaBmOF1;#C)8XA(WFlH zycro;DS2?(G&6wkr6rqC>rqDv3nfGw3hmN_9Al>TgvmGsL8_hXx09};l9Ow@)F5@y z#VH5WigLDwZE4nh^7&@g{1FV^UZ%_LJ-s<{HN*2R$OPg@R~Z`c-ET*2}XB@9xvAjrK&hS=f|R8Gr9 zr|0TGOsI7RD+4+2{ZiwdVD@2zmg~g@^D--YL;6UYGSM8i$NbQr4!c7T9rg!8;TM0E zT#@?&S=t>GQm)*ua|?TLT2ktj#`|R<_*FAkOu2Pz$wEc%-=Y9V*$&dg+wIei3b*O8 z2|m$!jJG!J!ZGbbIa!(Af~oSyZV+~M1qGvelMzPNE_%5?c2>;MeeG2^N?JDKjFYCy z7SbPWH-$cWF9~fX%9~v99L!G(wi!PFp>rB!9xj7=Cv|F+7CsGNwY0Q_J%FID%C^CBZQfJ9K(HK%k31j~e#&?hQ zNuD6gRkVckU)v+53-fc} z7ZCzYN-5RG4H7;>>Hg?LU9&5_aua?A0)0dpew1#MMlu)LHe(M;OHjHIUl7|%%)YPo z0cBk;AOY00%Fe6heoN*$(b<)Cd#^8Iu;-2v@>cE-OB$icUF9EEoaC&q8z9}jMTT2I z8`9;jT%z0;dy4!8U;GW{i`)3!c6&oWY`J3669C!tM<5nQFFrFRglU8f)5Op$GtR-3 zn!+SPCw|04sv?%YZ(a7#L?vsdr7ss@WKAw&A*}-1S|9~cL%uA+E~>N6QklFE>8W|% zyX-qAUGTY1hQ-+um`2|&ji0cY*(qN!zp{YpDO-r>jPk*yuVSay<)cUt`t@&FPF_&$ zcHwu1(SQ`I-l8~vYyUxm@D1UEdFJ$f5Sw^HPH7b!9 zzYT3gKMF((N(v0#4f_jPfVZ=ApN^jQJe-X$`A?X+vWjLn_%31KXE*}5_}d8 zw_B1+a#6T1?>M{ronLbHIlEsMf93muJ7AH5h%;i99<~JX^;EAgEB1uHralD*!aJ@F zV2ruuFe9i2Q1C?^^kmVy921eb=tLDD43@-AgL^rQ3IO9%+vi_&R2^dpr}x{bCVPej z7G0-0o64uyWNtr*loIvslyo0%)KSDDKjfThe0hcqs)(C-MH1>bNGBDRTW~scy_{w} zp^aq8Qb!h9Lwielq%C1b8=?Z=&U)ST&PHbS)8Xzjh2DF?d{iAv)Eh)wsUnf>UtXN( zL7=$%YrZ#|^c{MYmhn!zV#t*(jdmYdCpwqpZ{v&L8KIuKn`@IIZfp!uo}c;7J57N` zAxyZ-uA4=Gzl~Ovycz%MW9ZL7N+nRo&1cfNn9(1H5eM;V_4Z_qVann7F>5f>%{rf= zPBZFaV@_Sobl?Fy&KXyzFDV*FIdhS5`Uc~S^Gjo)aiTHgn#<0C=9o-a-}@}xDor;D zZyZ|fvf;+=3MZd>SR1F^F`RJEZo+|MdyJYQAEauKu%WDol~ayrGU3zzbHKsnHKZ*z zFiwUkL@DZ>!*x05ql&EBq@_Vqv83&?@~q5?lVmffQZ+V-=qL+!u4Xs2Z2zdCQ3U7B&QR9_Iggy} z(om{Y9eU;IPe`+p1ifLx-XWh?wI)xU9ik+m#g&pGdB5Bi<`PR*?92lE0+TkRuXI)z z5LP!N2+tTc%cB6B1F-!fj#}>S!vnpgVU~3!*U1ej^)vjUH4s-bd^%B=ItQqDCGbrEzNQi(dJ`J}-U=2{7-d zK8k^Rlq2N#0G?9&1?HSle2vlkj^KWSBYTwx`2?9TU_DX#J+f+qLiZCqY1TXHFxXZqYMuD@RU$TgcnCC{_(vwZ-*uX)~go#%PK z@}2Km_5aQ~(<3cXeJN6|F8X_1@L%@xTzs}$_*E|a^_URF_qcF;Pfhoe?FTFwvjm1o z8onf@OY@jC2tVcMaZS;|T!Ks(wOgPpRzRnFS-^RZ4E!9dsnj9sFt609a|jJbb1Dt@ z<=Gal2jDEupxUSwWu6zp<<&RnAA;d&4gKVG0iu6g(DsST(4)z6R)zDpfaQ}v{5ARt zyhwvMtF%b-YazR5XLz+oh=mn;y-Mf2a8>7?2v8qX;19y?b>Z5laGHvzH;Nu9S`B8} zI)qN$GbXIQ1VL3lnof^6TS~rvPVg4V?Dl2Bb*K2z4E{5vy<(@@K_cN@U>R!>aUIRnb zL*)=787*cs#zb31zBC49x$`=fkQbMAef)L2$dR{)6BAz!t5U_B#1zZG`^neKSS22oJ#5B=gl%U=WeqL9REF2g zZnfCb0?quf?Ztj$VXvDSWoK`0L=Zxem2q}!XWLoT-kYMOx)!7fcgT35uC~0pySEme z`{wGWTkGr7>+Kb^n;W?BZH6ZP(9tQX%-7zF>vc2}LuWDI(9kh1G#7B99r4x6;_-V+k&c{nPUrR zAXJGRiMe~aup{0qzmLNjS_BC4cB#sXjckx{%_c&^xy{M61xEb>KW_AG5VFXUOjAG4 z^>Qlm9A#1N{4snY=(AmWzatb!ngqiqPbBZ7>Uhb3)dTkSGcL#&SH>iMO-IJBPua`u zo)LWZ>=NZLr758j{%(|uQuZ)pXq_4c!!>s|aDM9#`~1bzK3J1^^D#<2bNCccH7~-X}Ggi!pIIF>uFx%aPARGQsnC8ZQc8lrQ5o~smqOg>Ti^GNme94*w z)JZy{_{#$jxGQ&`M z!OMvZMHR>8*^>eS%o*6hJwn!l8VOOjZQJvh)@tnHVW&*GYPuxqXw}%M!(f-SQf`=L z5;=5w2;%82VMH6Xi&-K3W)o&K^+vJCepWZ-rW%+Dc6X3(){z$@4zjYxQ|}8UIojeC zYZpQ1dU{fy=oTr<4VX?$q)LP}IUmpiez^O&N3E_qPpchGTi5ZM6-2ScWlQq%V&R2Euz zO|Q0Hx>lY1Q1cW5xHv5!0OGU~PVEqSuy#fD72d#O`N!C;o=m+YioGu-wH2k6!t<~K zSr`E=W9)!g==~x9VV~-8{4ZN9{~-A9zJpRe%NGg$+MDuI-dH|b@BD)~>pPCGUNNzY zMDg||0@XGQgw`YCt5C&A{_+J}mvV9Wg{6V%2n#YSRN{AP#PY?1FF1#|vO_%e+#`|2*~wGAJaeRX6=IzFNeWhz6gJc8+(03Ph4y6ELAm=AkN7TOgMUEw*N{= z_)EIDQx5q22oUR+_b*tazu9+pX|n1c*IB-}{DqIj z-?E|ks{o3AGRNb;+iKcHkZvYJvFsW&83RAPs1Oh@IWy%l#5x2oUP6ZCtv+b|q>jsf zZ_9XO;V!>n`UxH1LvH8)L4?8raIvasEhkpQoJ`%!5rBs!0Tu(s_D{`4opB;57)pkX z4$A^8CsD3U5*!|bHIEqsn~{q+Ddj$ME@Gq4JXtgVz&7l{Ok!@?EA{B3P~NAqb9)4? zkQo30A^EbHfQ@87G5&EQTd`frrwL)&Yw?%-W@uy^Gn23%j?Y!Iea2xw<-f;esq zf%w5WN@E1}zyXtYv}}`U^B>W`>XPmdLj%4{P298|SisrE;7HvXX;A}Ffi8B#3Lr;1 zHt6zVb`8{#+e$*k?w8|O{Uh|&AG}|DG1PFo1i?Y*cQm$ZwtGcVgMwtBUDa{~L1KT-{jET4w60>{KZ27vXrHJ;fW{6| z=|Y4!&UX020wU1>1iRgB@Q#m~1^Z^9CG1LqDhYBrnx%IEdIty z!46iOoKlKs)c}newDG)rWUikD%j`)p z_w9Ph&e40=(2eBy;T!}*1p1f1SAUDP9iWy^u^Ubdj21Kn{46;GR+hwLO=4D11@c~V zI8x&(D({K~Df2E)Nx_yQvYfh4;MbMJ@Z}=Dt3_>iim~QZ*hZIlEs0mEb z_54+&*?wMD`2#vsQRN3KvoT>hWofI_Vf(^C1ff-Ike@h@saEf7g}<9T`W;HAne-Nd z>RR+&SP35w)xKn8^U$7))PsM!jKwYZ*RzEcG-OlTrX3}9a{q%#Un5E5W{{hp>w~;` zGky+3(vJvQyGwBo`tCpmo0mo((?nM8vf9aXrrY1Ve}~TuVkB(zeds^jEfI}xGBCM2 zL1|#tycSaWCurP+0MiActG3LCas@_@tao@(R1ANlwB$4K53egNE_;!&(%@Qo$>h`^1S_!hN6 z)vZtG$8fN!|BXBJ=SI>e(LAU(y(i*PHvgQ2llulxS8>qsimv7yL}0q_E5WiAz7)(f zC(ahFvG8&HN9+6^jGyLHM~$)7auppeWh_^zKk&C_MQ~8;N??OlyH~azgz5fe^>~7F zl3HnPN3z-kN)I$4@`CLCMQx3sG~V8hPS^}XDXZrQA>}mQPw%7&!sd(Pp^P=tgp-s^ zjl}1-KRPNWXgV_K^HkP__SR`S-|OF0bR-N5>I%ODj&1JUeAQ3$9i;B~$S6}*^tK?= z**%aCiH7y?xdY?{LgVP}S0HOh%0%LI$wRx;$T|~Y8R)Vdwa}kGWv8?SJVm^>r6+%I z#lj1aR94{@MP;t-scEYQWc#xFA30^}?|BeX*W#9OL;Q9#WqaaM546j5j29((^_8Nu z4uq}ESLr~r*O7E7$D{!k9W>`!SLoyA53i9QwRB{!pHe8um|aDE`Cg0O*{jmor)^t)3`>V>SWN-2VJcFmj^1?~tT=JrP`fVh*t zXHarp=8HEcR#vFe+1a%XXuK+)oFs`GDD}#Z+TJ}Ri`FvKO@ek2ayn}yaOi%(8p%2$ zpEu)v0Jym@f}U|-;}CbR=9{#<^z28PzkkTNvyKvJDZe+^VS2bES3N@Jq!-*}{oQlz z@8bgC_KnDnT4}d#&Cpr!%Yb?E!brx0!eVOw~;lLwUoz#Np%d$o%9scc3&zPm`%G((Le|6o1 zM(VhOw)!f84zG^)tZ1?Egv)d8cdNi+T${=5kV+j;Wf%2{3g@FHp^Gf*qO0q!u$=m9 zCaY`4mRqJ;FTH5`a$affE5dJrk~k`HTP_7nGTY@B9o9vvnbytaID;^b=Tzp7Q#DmD zC(XEN)Ktn39z5|G!wsVNnHi) z%^q94!lL|hF`IijA^9NR0F$@h7k5R^ljOW(;Td9grRN0Mb)l_l7##{2nPQ@?;VjXv zaLZG}yuf$r$<79rVPpXg?6iiieX|r#&`p#Con2i%S8*8F}(E) zI5E6c3tG*<;m~6>!&H!GJ6zEuhH7mkAzovdhLy;)q z{H2*8I^Pb}xC4s^6Y}6bJvMu=8>g&I)7!N!5QG$xseeU#CC?ZM-TbjsHwHgDGrsD= z{%f;@Sod+Ch66Ko2WF~;Ty)v>&x^aovCbCbD7>qF*!?BXmOV3(s|nxsb*Lx_2lpB7 zokUnzrk;P=T-&kUHO}td+Zdj!3n&NR?K~cRU zAXU!DCp?51{J4w^`cV#ye}(`SQhGQkkMu}O3M*BWt4UsC^jCFUy;wTINYmhD$AT;4 z?Xd{HaJjP`raZ39qAm;%beDbrLpbRf(mkKbANan7XsL>_pE2oo^$TgdidjRP!5-`% zv0d!|iKN$c0(T|L0C~XD0aS8t{*&#LnhE;1Kb<9&=c2B+9JeLvJr*AyyRh%@jHej=AetOMSlz^=!kxX>>B{2B1uIrQyfd8KjJ+DBy!h)~*(!|&L4^Q_07SQ~E zcemVP`{9CwFvPFu7pyVGCLhH?LhEVb2{7U+Z_>o25#+3<|8%1T^5dh}*4(kfJGry} zm%r#hU+__Z;;*4fMrX=Bkc@7|v^*B;HAl0((IBPPii%X9+u3DDF6%bI&6?Eu$8&aWVqHIM7mK6?Uvq$1|(-T|)IV<>e?!(rY zqkmO1MRaLeTR=)io(0GVtQT@s6rN%C6;nS3@eu;P#ry4q;^O@1ZKCJyp_Jo)Ty^QW z+vweTx_DLm{P-XSBj~Sl<%_b^$=}odJ!S2wAcxenmzFGX1t&Qp8Vxz2VT`uQsQYtdn&_0xVivIcxZ_hnrRtwq4cZSj1c-SG9 z7vHBCA=fd0O1<4*=lu$6pn~_pVKyL@ztw1swbZi0B?spLo56ZKu5;7ZeUml1Ws1?u zqMf1p{5myAzeX$lAi{jIUqo1g4!zWLMm9cfWcnw`k6*BR^?$2(&yW?>w;G$EmTA@a z6?y#K$C~ZT8+v{87n5Dm&H6Pb_EQ@V0IWmG9cG=O;(;5aMWWrIPzz4Q`mhK;qQp~a z+BbQrEQ+w{SeiuG-~Po5f=^EvlouB@_|4xQXH@A~KgpFHrwu%dwuCR)=B&C(y6J4J zvoGk9;lLs9%iA-IJGU#RgnZZR+@{5lYl8(e1h6&>Vc_mvg0d@);X zji4T|n#lB!>pfL|8tQYkw?U2bD`W{na&;*|znjmalA&f;*U++_aBYerq;&C8Kw7mI z7tsG*?7*5j&dU)Lje;^{D_h`%(dK|pB*A*1(Jj)w^mZ9HB|vGLkF1GEFhu&rH=r=8 zMxO42e{Si6$m+Zj`_mXb&w5Q(i|Yxyg?juUrY}78uo@~3v84|8dfgbPd0iQJRdMj< zncCNGdMEcsxu#o#B5+XD{tsg*;j-eF8`mp~K8O1J!Z0+>0=7O=4M}E?)H)ENE;P*F z$Ox?ril_^p0g7xhDUf(q652l|562VFlC8^r8?lQv;TMvn+*8I}&+hIQYh2 z1}uQQaag&!-+DZ@|C+C$bN6W;S-Z@)d1|en+XGvjbOxCa-qAF*LA=6s(Jg+g;82f$ z(Vb)8I)AH@cdjGFAR5Rqd0wiNCu!xtqWbcTx&5kslzTb^7A78~Xzw1($UV6S^VWiP zFd{Rimd-0CZC_Bu(WxBFW7+k{cOW7DxBBkJdJ;VsJ4Z@lERQr%3eVv&$%)b%<~ zCl^Y4NgO}js@u{|o~KTgH}>!* z_iDNqX2(As7T0xivMH|3SC1ivm8Q}6Ffcd7owUKN5lHAtzMM4<0v+ykUT!QiowO;`@%JGv+K$bBx@*S7C8GJVqQ_K>12}M`f_Ys=S zKFh}HM9#6Izb$Y{wYzItTy+l5U2oL%boCJn?R3?jP@n$zSIwlmyGq30Cw4QBO|14` zW5c);AN*J3&eMFAk$SR~2k|&+&Bc$e>s%c{`?d~85S-UWjA>DS5+;UKZ}5oVa5O(N zqqc@>)nee)+4MUjH?FGv%hm2{IlIF-QX}ym-7ok4Z9{V+ZHVZQl$A*x!(q%<2~iVv znUa+BX35&lCb#9VE-~Y^W_f;Xhl%vgjwdjzMy$FsSIj&ok}L+X`4>J=9BkN&nu^E*gbhj3(+D>C4E z@Fwq_=N)^bKFSHTzZk?-gNU$@l}r}dwGyh_fNi=9b|n}J>&;G!lzilbWF4B}BBq4f zYIOl?b)PSh#XTPp4IS5ZR_2C!E)Z`zH0OW%4;&~z7UAyA-X|sh9@~>cQW^COA9hV4 zXcA6qUo9P{bW1_2`eo6%hgbN%(G-F1xTvq!sc?4wN6Q4`e9Hku zFwvlAcRY?6h^Fj$R8zCNEDq8`=uZB8D-xn)tA<^bFFy}4$vA}Xq0jAsv1&5!h!yRA zU()KLJya5MQ`q&LKdH#fwq&(bNFS{sKlEh_{N%{XCGO+po#(+WCLmKW6&5iOHny>g z3*VFN?mx!16V5{zyuMWDVP8U*|BGT$(%IO|)?EF|OI*sq&RovH!N%=>i_c?K*A>>k zyg1+~++zY4Q)J;VWN0axhoIKx;l&G$gvj(#go^pZskEVj8^}is3Jw26LzYYVos0HX zRPvmK$dVxM8(Tc?pHFe0Z3uq){{#OK3i-ra#@+;*=ui8)y6hsRv z4Fxx1c1+fr!VI{L3DFMwXKrfl#Q8hfP@ajgEau&QMCxd{g#!T^;ATXW)nUg&$-n25 zruy3V!!;{?OTobo|0GAxe`Acn3GV@W=&n;~&9 zQM>NWW~R@OYORkJAo+eq1!4vzmf9K%plR4(tB@TR&FSbDoRgJ8qVcH#;7lQub*nq&?Z>7WM=oeEVjkaG zT#f)=o!M2DO5hLR+op>t0CixJCIeXH*+z{-XS|%jx)y(j&}Wo|3!l7{o)HU3m7LYyhv*xF&tq z%IN7N;D4raue&&hm0xM=`qv`+TK@;_xAcGKuK(2|75~ar2Yw)geNLSmVxV@x89bQu zpViVKKnlkwjS&&c|-X6`~xdnh}Ps)Hs z4VbUL^{XNLf7_|Oi>tA%?SG5zax}esF*FH3d(JH^Gvr7Rp*n=t7frH!U;!y1gJB^i zY_M$KL_}mW&XKaDEi9K-wZR|q*L32&m+2n_8lq$xRznJ7p8}V>w+d@?uB!eS3#u<} zIaqi!b!w}a2;_BfUUhGMy#4dPx>)_>yZ`ai?Rk`}d0>~ce-PfY-b?Csd(28yX22L% zI7XI>OjIHYTk_@Xk;Gu^F52^Gn6E1&+?4MxDS2G_#PQ&yXPXP^<-p|2nLTb@AAQEY zI*UQ9Pmm{Kat}wuazpjSyXCdnrD&|C1c5DIb1TnzF}f4KIV6D)CJ!?&l&{T)e4U%3HTSYqsQ zo@zWB1o}ceQSV)<4G<)jM|@@YpL+XHuWsr5AYh^Q{K=wSV99D~4RRU52FufmMBMmd z_H}L#qe(}|I9ZyPRD6kT>Ivj&2Y?qVZq<4bG_co_DP`sE*_Xw8D;+7QR$Uq(rr+u> z8bHUWbV19i#)@@G4bCco@Xb<8u~wVDz9S`#k@ciJtlu@uP1U0X?yov8v9U3VOig2t zL9?n$P3=1U_Emi$#slR>N5wH-=J&T=EdUHA}_Z zZIl3nvMP*AZS9{cDqFanrA~S5BqxtNm9tlu;^`)3X&V4tMAkJ4gEIPl= zoV!Gyx0N{3DpD@)pv^iS*dl2FwANu;1;%EDl}JQ7MbxLMAp>)UwNwe{=V}O-5C*>F zu?Ny+F64jZn<+fKjF01}8h5H_3pey|;%bI;SFg$w8;IC<8l|3#Lz2;mNNik6sVTG3 z+Su^rIE#40C4a-587$U~%KedEEw1%r6wdvoMwpmlXH$xPnNQN#f%Z7|p)nC>WsuO= z4zyqapLS<8(UJ~Qi9d|dQijb_xhA2)v>la)<1md5s^R1N&PiuA$^k|A<+2C?OiHbj z>Bn$~t)>Y(Zb`8hW7q9xQ=s>Rv81V+UiuZJc<23HplI88isqRCId89fb`Kt|CxVIg znWcwprwXnotO>3s&Oypkte^9yJjlUVVxSe%_xlzmje|mYOVPH^vjA=?6xd0vaj0Oz zwJ4OJNiFdnHJX3rw&inskjryukl`*fRQ#SMod5J|KroJRsVXa5_$q7whSQ{gOi*s0 z1LeCy|JBWRsDPn7jCb4s(p|JZiZ8+*ExC@Vj)MF|*Vp{B(ziccSn`G1Br9bV(v!C2 z6#?eqpJBc9o@lJ#^p-`-=`4i&wFe>2)nlPK1p9yPFzJCzBQbpkcR>={YtamIw)3nt z(QEF;+)4`>8^_LU)_Q3 zC5_7lgi_6y>U%m)m@}Ku4C}=l^J=<<7c;99ec3p{aR+v=diuJR7uZi%aQv$oP?dn?@6Yu_+*^>T0ptf(oobdL;6)N-I!TO`zg^Xbv3#L0I~sn@WGk-^SmPh5>W+LB<+1PU}AKa?FCWF|qMNELOgdxR{ zbqE7@jVe+FklzdcD$!(A$&}}H*HQFTJ+AOrJYnhh}Yvta(B zQ_bW4Rr;R~&6PAKwgLWXS{Bnln(vUI+~g#kl{r+_zbngT`Y3`^Qf=!PxN4IYX#iW4 zucW7@LLJA9Zh3(rj~&SyN_pjO8H&)|(v%!BnMWySBJV=eSkB3YSTCyIeJ{i;(oc%_hk{$_l;v>nWSB)oVeg+blh=HB5JSlG_r7@P z3q;aFoZjD_qS@zygYqCn=;Zxjo!?NK!%J$ z52lOP`8G3feEj+HTp@Tnn9X~nG=;tS+z}u{mQX_J0kxtr)O30YD%oo)L@wy`jpQYM z@M>Me=95k1p*FW~rHiV1CIfVc{K8r|#Kt(ApkXKsDG$_>76UGNhHExFCw#Ky9*B-z zNq2ga*xax!HMf_|Vp-86r{;~YgQKqu7%szk8$hpvi_2I`OVbG1doP(`gn}=W<8%Gn z%81#&WjkH4GV;4u43EtSW>K_Ta3Zj!XF?;SO3V#q=<=>Tc^@?A`i;&`-cYj|;^ zEo#Jl5zSr~_V-4}y8pnufXLa80vZY4z2ko7fj>DR)#z=wWuS1$$W!L?(y}YC+yQ|G z@L&`2upy3f>~*IquAjkVNU>}c10(fq#HdbK$~Q3l6|=@-eBbo>B9(6xV`*)sae58*f zym~RRVx;xoCG3`JV`xo z!lFw)=t2Hy)e!IFs?0~7osWk(d%^wxq&>_XD4+U#y&-VF%4z?XH^i4w`TxpF{`XhZ z%G}iEzf!T(l>g;W9<~K+)$g!{UvhW{E0Lis(S^%I8OF&%kr!gJ&fMOpM=&=Aj@wuL zBX?*6i51Qb$uhkwkFYkaD_UDE+)rh1c;(&Y=B$3)J&iJfQSx!1NGgPtK!$c9OtJuu zX(pV$bfuJpRR|K(dp@^j}i&HeJOh@|7lWo8^$*o~Xqo z5Sb+!EtJ&e@6F+h&+_1ETbg7LfP5GZjvIUIN3ibCOldAv z)>YdO|NH$x7AC8dr=<2ekiY1%fN*r~e5h6Yaw<{XIErujKV~tiyrvV_DV0AzEknC- zR^xKM3i<1UkvqBj3C{wDvytOd+YtDSGu!gEMg+!&|8BQrT*|p)(dwQLEy+ zMtMzij3zo40)CA!BKZF~yWg?#lWhqD3@qR)gh~D{uZaJO;{OWV8XZ_)J@r3=)T|kt zUS1pXr6-`!Z}w2QR7nP%d?ecf90;K_7C3d!UZ`N(TZoWNN^Q~RjVhQG{Y<%E1PpV^4 z-m-K+$A~-+VDABs^Q@U*)YvhY4Znn2^w>732H?NRK(5QSS$V@D7yz2BVX4)f5A04~$WbxGOam22>t&uD)JB8-~yiQW6ik;FGblY_I>SvB_z2?PS z*Qm&qbKI{H1V@YGWzpx`!v)WeLT02};JJo*#f$a*FH?IIad-^(;9XC#YTWN6;Z6+S zm4O1KH=#V@FJw7Pha0!9Vb%ZIM$)a`VRMoiN&C|$YA3~ZC*8ayZRY^fyuP6$n%2IU z$#XceYZeqLTXw(m$_z|33I$B4k~NZO>pP6)H_}R{E$i%USGy{l{-jOE;%CloYPEU+ zRFxOn4;7lIOh!7abb23YKD+_-?O z0FP9otcAh+oSj;=f#$&*ExUHpd&e#bSF%#8*&ItcL2H$Sa)?pt0Xtf+t)z$_u^wZi z44oE}r4kIZGy3!Mc8q$B&6JqtnHZ>Znn!Zh@6rgIu|yU+zG8q`q9%B18|T|oN3zMq z`l&D;U!OL~%>vo&q0>Y==~zLiCZk4v%s_7!9DxQ~id1LLE93gf*gg&2$|hB#j8;?3 z5v4S;oM6rT{Y;I+#FdmNw z){d%tNM<<#GN%n9ox7B=3#;u7unZ~tLB_vRZ52a&2=IM)2VkXm=L+Iqq~uk#Dug|x z>S84e+A7EiOY5lj*!q?6HDkNh~0g;0Jy(al!ZHHDtur9T$y-~)94HelX1NHjXWIM7UAe}$?jiz z9?P4`I0JM=G5K{3_%2jPLC^_Mlw?-kYYgb7`qGa3@dn|^1fRMwiyM@Ch z;CB&o7&&?c5e>h`IM;Wnha0QKnEp=$hA8TJgR-07N~U5(>9vJzeoFsSRBkDq=x(YgEMpb=l4TDD`2 zwVJpWGTA_u7}?ecW7s6%rUs&NXD3+n;jB86`X?8(l3MBo6)PdakI6V6a}22{)8ilT zM~T*mU}__xSy|6XSrJ^%lDAR3Lft%+yxC|ZUvSO_nqMX!_ul3;R#*{~4DA=h$bP)%8Yv9X zyp><|e8=_ttI}ZAwOd#dlnSjck#6%273{E$kJuCGu=I@O)&6ID{nWF5@gLb16sj|&Sb~+du4e4O_%_o`Ix4NRrAsyr1_}MuP94s>de8cH-OUkVPk3+K z&jW)It9QiU-ti~AuJkL`XMca8Oh4$SyJ=`-5WU<{cIh+XVH#e4d&zive_UHC!pN>W z3TB;Mn5i)9Qn)#6@lo4QpI3jFYc0~+jS)4AFz8fVC;lD^+idw^S~Qhq>Tg(!3$yLD zzktzoFrU@6s4wwCMz}edpF5i5Q1IMmEJQHzp(LAt)pgN3&O!&d?3W@6U4)I^2V{;- z6A(?zd93hS*uQmnh4T)nHnE{wVhh(=MMD(h(P4+^p83Om6t<*cUW>l(qJzr%5vp@K zN27ka(L{JX=1~e2^)F^i=TYj&;<7jyUUR2Bek^A8+3Up*&Xwc{)1nRR5CT8vG>ExV zHnF3UqXJOAno_?bnhCX-&kwI~Ti8t4`n0%Up>!U`ZvK^w2+0Cs-b9%w%4`$+To|k= zKtgc&l}P`*8IS>8DOe?EB84^kx4BQp3<7P{Pq}&p%xF_81pg!l2|u=&I{AuUgmF5n zJQCTLv}%}xbFGYtKfbba{CBo)lWW%Z>i(_NvLhoQZ*5-@2l&x>e+I~0Nld3UI9tdL zRzu8}i;X!h8LHVvN?C+|M81e>Jr38%&*9LYQec9Ax>?NN+9(_>XSRv&6hlCYB`>Qm z1&ygi{Y()OU4@D_jd_-7vDILR{>o|7-k)Sjdxkjgvi{@S>6GqiF|o`*Otr;P)kLHN zZkpts;0zw_6;?f(@4S1FN=m!4^mv~W+lJA`&7RH%2$)49z0A+8@0BCHtj|yH--AEL z0tW6G%X-+J+5a{5*WKaM0QDznf;V?L5&uQw+yegDNDP`hA;0XPYc6e0;Xv6|i|^F2WB)Z$LR|HR4 zTQsRAby9(^Z@yATyOgcfQw7cKyr^3Tz7lc7+JEwwzA7)|2x+PtEb>nD(tpxJQm)Kn zW9K_*r!L%~N*vS8<5T=iv|o!zTe9k_2jC_j*7ik^M_ zaf%k{WX{-;0*`t`G!&`eW;gChVXnJ-Rn)To8vW-?>>a%QU1v`ZC=U)f8iA@%JG0mZ zDqH;~mgBnrCP~1II<=V9;EBL)J+xzCoiRBaeH&J6rL!{4zIY8tZka?_FBeQeNO3q6 zyG_alW54Ba&wQf{&F1v-r1R6ID)PTsqjIBc+5MHkcW5Fnvi~{-FjKe)t1bl}Y;z@< z=!%zvpRua>>t_x}^}z0<7MI!H2v6|XAyR9!t50q-A)xk0nflgF4*OQlCGK==4S|wc zRMsSscNhRzHMBU8TdcHN!q^I}x0iXJ%uehac|Zs_B$p@CnF)HeXPpB_Za}F{<@6-4 zl%kml@}kHQ(ypD8FsPJ2=14xXJE|b20RUIgs!2|R3>LUMGF6X*B_I|$`Qg=;zm7C z{mEDy9dTmPbued7mlO@phdmAmJ7p@GR1bjCkMw6*G7#4+`k>fk1czdJUB!e@Q(~6# zwo%@p@V5RL0ABU2LH7Asq^quDUho@H>eTZH9f*no9fY0T zD_-9px3e}A!>>kv5wk91%C9R1J_Nh!*&Kk$J3KNxC}c_@zlgpJZ+5L)Nw|^p=2ue}CJtm;uj*Iqr)K})kA$xtNUEvX;4!Px*^&9T_`IN{D z{6~QY=Nau6EzpvufB^hflc#XIsSq0Y9(nf$d~6ZwK}fal92)fr%T3=q{0mP-EyP_G z)UR5h@IX}3Qll2b0oCAcBF>b*@Etu*aTLPU<%C>KoOrk=x?pN!#f_Og-w+;xbFgjQ zXp`et%lDBBh~OcFnMKMUoox0YwBNy`N0q~bSPh@+enQ=4RUw1) zpovN`QoV>vZ#5LvC;cl|6jPr}O5tu!Ipoyib8iXqy}TeJ;4+_7r<1kV0v5?Kv>fYp zg>9L`;XwXa&W7-jf|9~uP2iyF5`5AJ`Q~p4eBU$MCC00`rcSF>`&0fbd^_eqR+}mK z4n*PMMa&FOcc)vTUR zlDUAn-mh`ahi_`f`=39JYTNVjsTa_Y3b1GOIi)6dY)D}xeshB0T8Eov5%UhWd1)u}kjEQ|LDo{tqKKrYIfVz~@dp!! zMOnah@vp)%_-jDTUG09l+;{CkDCH|Q{NqX*uHa1YxFShy*1+;J`gywKaz|2Q{lG8x zP?KBur`}r`!WLKXY_K;C8$EWG>jY3UIh{+BLv0=2)KH%P}6xE2kg)%(-uA6lC?u8}{K(#P*c zE9C8t*u%j2r_{;Rpe1A{9nNXU;b_N0vNgyK!EZVut~}+R2rcbsHilqsOviYh-pYX= zHw@53nlmwYI5W5KP>&`dBZe0Jn?nAdC^HY1wlR6$u^PbpB#AS&5L6zqrXN&7*N2Q` z+Rae1EwS)H=aVSIkr8Ek^1jy2iS2o7mqm~Mr&g5=jjt7VxwglQ^`h#Mx+x2v|9ZAwE$i_9918MjJxTMr?n!bZ6n$}y11u8I9COTU`Z$Fi z!AeAQLMw^gp_{+0QTEJrhL424pVDp%wpku~XRlD3iv{vQ!lAf!_jyqd_h}+Tr1XG| z`*FT*NbPqvHCUsYAkFnM`@l4u_QH&bszpUK#M~XLJt{%?00GXY?u_{gj3Hvs!=N(I z(=AuWPijyoU!r?aFTsa8pLB&cx}$*%;K$e*XqF{~*rA-qn)h^!(-;e}O#B$|S~c+U zN4vyOK0vmtx$5K!?g*+J@G1NmlEI=pyZXZ69tAv=@`t%ag_Hk{LP~OH9iE)I= zaJ69b4kuCkV0V zo(M0#>phpQ_)@j;h%m{-a*LGi(72TP)ws2w*@4|C-3+;=5DmC4s7Lp95%n%@Ko zfdr3-a7m*dys9iIci$A=4NPJ`HfJ;hujLgU)ZRuJI`n;Pw|yksu!#LQnJ#dJysgNb z@@qwR^wrk(jbq4H?d!lNyy72~Dnn87KxsgQ!)|*m(DRM+eC$wh7KnS-mho3|KE)7h zK3k;qZ;K1Lj6uEXLYUYi)1FN}F@-xJ z@@3Hb84sl|j{4$3J}aTY@cbX@pzB_qM~APljrjju6P0tY{C@ zpUCOz_NFmALMv1*blCcwUD3?U6tYs+N%cmJ98D%3)%)Xu^uvzF zS5O!sc#X6?EwsYkvPo6A%O8&y8sCCQH<%f2togVwW&{M;PR!a(ZT_A+jVAbf{@5kL zB@Z(hb$3U{T_}SKA_CoQVU-;j>2J=L#lZ~aQCFg-d<9rzs$_gO&d5N6eFSc z1ml8)P*FSi+k@!^M9nDWR5e@ATD8oxtDu=36Iv2!;dZzidIS(PCtEuXAtlBb1;H%Z zwnC^Ek*D)EX4#Q>R$$WA2sxC_t(!!6Tr?C#@{3}n{<^o;9id1RA&-Pig1e-2B1XpG zliNjgmd3c&%A}s>qf{_j#!Z`fu0xIwm4L0)OF=u(OEmp;bLCIaZX$&J_^Z%4Sq4GZ zPn6sV_#+6pJmDN_lx@1;Zw6Md_p0w9h6mHtzpuIEwNn>OnuRSC2=>fP^Hqgc)xu^4 z<3!s`cORHJh#?!nKI`Et7{3C27+EuH)Gw1f)aoP|B3y?fuVfvpYYmmukx0ya-)TQX zR{ggy5cNf4X|g)nl#jC9p>7|09_S7>1D2GTRBUTW zAkQ=JMRogZqG#v;^=11O6@rPPwvJkr{bW-Qg8`q8GoD#K`&Y+S#%&B>SGRL>;ZunM@49!}Uy zN|bBCJ%sO;@3wl0>0gbl3L@1^O60ONObz8ZI7nder>(udj-jt`;yj^nTQ$L9`OU9W zX4alF#$|GiR47%x@s&LV>2Sz2R6?;2R~5k6V>)nz!o_*1Y!$p>BC5&?hJg_MiE6UBy>RkVZj`9UWbRkN-Hk!S`=BS3t3uyX6)7SF#)71*}`~Ogz z1rap5H6~dhBJ83;q-Y<5V35C2&F^JI-it(=5D#v!fAi9p#UwV~2tZQI+W(Dv?1t9? zfh*xpxxO{-(VGB>!Q&0%^YW_F!@aZS#ucP|YaD#>wd1Fv&Z*SR&mc;asi}1G) z_H>`!akh-Zxq9#io(7%;a$)w+{QH)Y$?UK1Dt^4)up!Szcxnu}kn$0afcfJL#IL+S z5gF_Y30j;{lNrG6m~$Ay?)*V9fZuU@3=kd40=LhazjFrau>(Y>SJNtOz>8x_X-BlA zIpl{i>OarVGj1v(4?^1`R}aQB&WCRQzS~;7R{tDZG=HhgrW@B`W|#cdyj%YBky)P= zpxuOZkW>S6%q7U{VsB#G(^FMsH5QuGXhb(sY+!-R8Bmv6Sx3WzSW<1MPPN1!&PurYky(@`bP9tz z52}LH9Q?+FF5jR6-;|+GVdRA!qtd;}*-h&iIw3Tq3qF9sDIb1FFxGbo&fbG5n8$3F zyY&PWL{ys^dTO}oZ#@sIX^BKW*bon=;te9j5k+T%wJ zNJtoN1~YVj4~YRrlZl)b&kJqp+Z`DqT!la$x&&IxgOQw#yZd-nBP3!7FijBXD|IsU8Zl^ zc6?MKpJQ+7ka|tZQLfchD$PD|;K(9FiLE|eUZX#EZxhG!S-63C$jWX1Yd!6-Yxi-u zjULIr|0-Q%D9jz}IF~S%>0(jOqZ(Ln<$9PxiySr&2Oic7vb<8q=46)Ln%Z|<*z5&> z3f~Zw@m;vR(bESB<=Jqkxn(=#hQw42l(7)h`vMQQTttz9XW6^|^8EK7qhju4r_c*b zJIi`)MB$w@9epwdIfnEBR+?~);yd6C(LeMC& zn&&N*?-g&BBJcV;8&UoZi4Lmxcj16ojlxR~zMrf=O_^i1wGb9X-0@6_rpjPYemIin zmJb+;lHe;Yp=8G)Q(L1bzH*}I>}uAqhj4;g)PlvD9_e_ScR{Ipq|$8NvAvLD8MYr}xl=bU~)f%B3E>r3Bu9_t|ThF3C5~BdOve zEbk^r&r#PT&?^V1cb{72yEWH}TXEE}w>t!cY~rA+hNOTK8FAtIEoszp!qqptS&;r$ zaYV-NX96-h$6aR@1xz6_E0^N49mU)-v#bwtGJm)ibygzJ8!7|WIrcb`$XH~^!a#s& z{Db-0IOTFq#9!^j!n_F}#Z_nX{YzBK8XLPVmc&X`fT7!@$U-@2KM9soGbmOSAmqV z{nr$L^MBo_u^Joyf0E^=eo{Rt0{{e$IFA(#*kP@SQd6lWT2-#>` zP1)7_@IO!9lk>Zt?#CU?cuhiLF&)+XEM9B)cS(gvQT!X3`wL*{fArTS;Ak`J<84du zALKPz4}3nlG8Fo^MH0L|oK2-4xIY!~Oux~1sw!+It)&D3p;+N8AgqKI`ld6v71wy8I!eP0o~=RVcFQR2Gr(eP_JbSytoQ$Yt}l*4r@A8Me94y z8cTDWhqlq^qoAhbOzGBXv^Wa4vUz$(7B!mX`T=x_ueKRRDfg&Uc-e1+z4x$jyW_Pm zp?U;-R#xt^Z8Ev~`m`iL4*c#65Nn)q#=Y0l1AuD&+{|8-Gsij3LUZXpM0Bx0u7WWm zH|%yE@-#XEph2}-$-thl+S;__ciBxSSzHveP%~v}5I%u!z_l_KoW{KRx2=eB33umE zIYFtu^5=wGU`Jab8#}cnYry@9p5UE#U|VVvx_4l49JQ;jQdp(uw=$^A$EA$LM%vmE zvdEOaIcp5qX8wX{mYf0;#51~imYYPn4=k&#DsKTxo{_Mg*;S495?OBY?#gv=edYC* z^O@-sd-qa+U24xvcbL0@C7_6o!$`)sVr-jSJE4XQUQ$?L7}2(}Eixqv;L8AdJAVqc zq}RPgpnDb@E_;?6K58r3h4-!4rT4Ab#rLHLX?eMOfluJk=3i1@Gt1i#iA=O`M0@x! z(HtJP9BMHXEzuD93m|B&woj0g6T?f#^)>J>|I4C5?Gam>n9!8CT%~aT;=oco5d6U8 zMXl(=W;$ND_8+DD*?|5bJ!;8ebESXMUKBAf7YBwNVJibGaJ*(2G`F%wx)grqVPjudiaq^Kl&g$8A2 zWMxMr@_$c}d+;_B`#kUX-t|4VKH&_f^^EP0&=DPLW)H)UzBG%%Tra*5 z%$kyZe3I&S#gfie^z5)!twG={3Cuh)FdeA!Kj<-9** zvT*5%Tb`|QbE!iW-XcOuy39>D3oe6x{>&<#E$o8Ac|j)wq#kQzz|ATd=Z0K!p2$QE zPu?jL8Lb^y3_CQE{*}sTDe!2!dtlFjq&YLY@2#4>XS`}v#PLrpvc4*@q^O{mmnr5D zmyJq~t?8>FWU5vZdE(%4cuZuao0GNjp3~Dt*SLaxI#g_u>hu@k&9Ho*#CZP~lFJHj z(e!SYlLigyc?&5-YxlE{uuk$9b&l6d`uIlpg_z15dPo*iU&|Khx2*A5Fp;8iK_bdP z?T6|^7@lcx2j0T@x>X7|kuuBSB7<^zeY~R~4McconTxA2flHC0_jFxmSTv-~?zVT| zG_|yDqa9lkF*B6_{j=T>=M8r<0s;@z#h)3BQ4NLl@`Xr__o7;~M&dL3J8fP&zLfDfy z);ckcTev{@OUlZ`bCo(-3? z1u1xD`PKgSg?RqeVVsF<1SLF;XYA@Bsa&cY!I48ZJn1V<3d!?s=St?TLo zC0cNr`qD*M#s6f~X>SCNVkva^9A2ZP>CoJ9bvgXe_c}WdX-)pHM5m7O zrHt#g$F0AO+nGA;7dSJ?)|Mo~cf{z2L)Rz!`fpi73Zv)H=a5K)*$5sf_IZypi($P5 zsPwUc4~P-J1@^3C6-r9{V-u0Z&Sl7vNfmuMY4yy*cL>_)BmQF!8Om9Dej%cHxbIzA zhtV0d{=%cr?;bpBPjt@4w=#<>k5ee=TiWAXM2~tUGfm z$s&!Dm0R^V$}fOR*B^kGaipi~rx~A2cS0;t&khV1a4u38*XRUP~f za!rZMtay8bsLt6yFYl@>-y^31(*P!L^^s@mslZy(SMsv9bVoX`O#yBgEcjCmGpyc* zeH$Dw6vB5P*;jor+JOX@;6K#+xc)Z9B8M=x2a@Wx-{snPGpRmOC$zpsqW*JCh@M2Y z#K+M(>=#d^>Of9C`))h<=Bsy)6zaMJ&x-t%&+UcpLjV`jo4R2025 zXaG8EA!0lQa)|dx-@{O)qP6`$rhCkoQqZ`^SW8g-kOwrwsK8 z3ms*AIcyj}-1x&A&vSq{r=QMyp3CHdWH35!sad#!Sm>^|-|afB+Q;|Iq@LFgqIp#Z zD1%H+3I?6RGnk&IFo|u+E0dCxXz4yI^1i!QTu7uvIEH>i3rR{srcST`LIRwdV1P;W z+%AN1NIf@xxvVLiSX`8ILA8MzNqE&7>%jMzGt9wm78bo9<;h*W84i29^w!>V>{N+S zd`5Zmz^G;f=icvoOZfK5#1ctx*~UwD=ab4DGQXehQ!XYnak*dee%YN$_ZPL%KZuz$ zD;$PpT;HM^$KwtQm@7uvT`i6>Hae1CoRVM2)NL<2-k2PiX=eAx+-6j#JI?M}(tuBW zkF%jjLR)O`gI2fcPBxF^HeI|DWwQWHVR!;;{BXXHskxh8F@BMDn`oEi-NHt;CLymW z=KSv5)3dyzec0T5B*`g-MQ<;gz=nIWKUi9ko<|4I(-E0k$QncH>E4l z**1w&#={&zv4Tvhgz#c29`m|;lU-jmaXFMC11 z*dlXDMEOG>VoLMc>!rApwOu2prKSi*!w%`yzGmS+k(zm*CsLK*wv{S_0WX^8A-rKy zbk^Gf_92^7iB_uUF)EE+ET4d|X|>d&mdN?x@vxKAQk`O+r4Qdu>XGy(a(19g;=jU} zFX{O*_NG>!$@jh!U369Lnc+D~qch3uT+_Amyi}*k#LAAwh}k8IPK5a-WZ81ufD>l> z$4cF}GSz>ce`3FAic}6W4Z7m9KGO?(eWqi@L|5Hq0@L|&2flN1PVl}XgQ2q*_n2s3 zt5KtowNkTYB5b;SVuoXA@i5irXO)A&%7?V`1@HGCB&)Wgk+l|^XXChq;u(nyPB}b3 zY>m5jkxpZgi)zfbgv&ec4Zqdvm+D<?Im*mXweS9H+V>)zF#Zp3)bhl$PbISY{5=_z!8&*Jv~NYtI-g!>fDs zmvL5O^U%!^VaKA9gvKw|5?-jk>~%CVGvctKmP$kpnpfN{D8@X*Aazi$txfa%vd-|E z>kYmV66W!lNekJPom29LdZ%(I+ZLZYTXzTg*to~m?7vp%{V<~>H+2}PQ?PPAq`36R z<%wR8v6UkS>Wt#hzGk#44W<%9S=nBfB);6clKwnxY}T*w21Qc3_?IJ@4gYzC7s;WP zVQNI(M=S=JT#xsZy7G`cR(BP9*je0bfeN8JN5~zY(DDs0t{LpHOIbN);?T-69Pf3R zSNe*&p2%AwXHL>__g+xd4Hlc_vu<25H?(`nafS%)3UPP7_4;gk-9ckt8SJRTv5v0M z_Hww`qPudL?ajIR&X*;$y-`<)6dxx1U~5eGS13CB!lX;3w7n&lDDiArbAhSycd}+b zya_3p@A`$kQy;|NJZ~s44Hqo7Hwt}X86NK=(ey>lgWTtGL6k@Gy;PbO!M%1~Wcn2k zUFP|*5d>t-X*RU8g%>|(wwj*~#l4z^Aatf^DWd1Wj#Q*AY0D^V@sC`M zjJc6qXu0I7Y*2;;gGu!plAFzG=J;1%eIOdn zQA>J&e05UN*7I5@yRhK|lbBSfJ+5Uq;!&HV@xfPZrgD}kE*1DSq^=%{o%|LChhl#0 zlMb<^a6ixzpd{kNZr|3jTGeEzuo}-eLT-)Q$#b{!vKx8Tg}swCni>{#%vDY$Ww$84 zew3c9BBovqb}_&BRo#^!G(1Eg((BScRZ}C)Oz?y`T5wOrv);)b^4XR8 zhJo7+<^7)qB>I;46!GySzdneZ>n_E1oWZY;kf94#)s)kWjuJN1c+wbVoNQcmnv}{> zN0pF+Sl3E}UQ$}slSZeLJrwT>Sr}#V(dVaezCQl2|4LN`7L7v&siYR|r7M(*JYfR$ zst3=YaDw$FSc{g}KHO&QiKxuhEzF{f%RJLKe3p*7=oo`WNP)M(9X1zIQPP0XHhY3c znrP{$4#Ol$A0s|4S7Gx2L23dv*Gv2o;h((XVn+9+$qvm}s%zi6nI-_s6?mG! zj{DV;qesJb&owKeEK?=J>UcAlYckA7Sl+I&IN=yasrZOkejir*kE@SN`fk<8Fgx*$ zy&fE6?}G)d_N`){P~U@1jRVA|2*69)KSe_}!~?+`Yb{Y=O~_+@!j<&oVQQMnhoIRU zA0CyF1OFfkK44n*JD~!2!SCPM;PRSk%1XL=0&rz00wxPs&-_eapJy#$h!eqY%nS0{ z!aGg58JIJPF3_ci%n)QSVpa2H`vIe$RD43;#IRfDV&Ibit z+?>HW4{2wOfC6Fw)}4x}i1maDxcE1qi@BS*qcxD2gE@h3#4cgU*D-&3z7D|tVZWt= z-Cy2+*Cm@P4GN_TPUtaVyVesbVDazF@)j8VJ4>XZv!f%}&eO1SvIgr}4`A*3#vat< z_MoByL(qW6L7SFZ#|Gc1fFN)L2PxY+{B8tJp+pxRyz*87)vXR}*=&ahXjBlQKguuf zX6x<<6fQulE^C*KH8~W%ptpaC0l?b=_{~*U4?5Vt;dgM4t_{&UZ1C2j?b>b+5}{IF_CUyvz-@QZPMlJ)r_tS$9kH%RPv#2_nMb zRLj5;chJ72*U`Z@Dqt4$@_+k$%|8m(HqLG!qT4P^DdfvGf&){gKnGCX#H0!;W=AGP zbA&Z`-__a)VTS}kKFjWGk z%|>yE?t*EJ!qeQ%dPk$;xIQ+P0;()PCBDgjJm6Buj{f^awNoVx+9<|lg3%-$G(*f) zll6oOkN|yamn1uyl2*N-lnqRI1cvs_JxLTeahEK=THV$Sz*gQhKNb*p0fNoda#-&F zB-qJgW^g}!TtM|0bS2QZekW7_tKu%GcJ!4?lObt0z_$mZ4rbQ0o=^curCs3bJK6sq z9fu-aW-l#>z~ca(B;4yv;2RZ?tGYAU)^)Kz{L|4oPj zdOf_?de|#yS)p2v8-N||+XL=O*%3+y)oI(HbM)Ds?q8~HPzIP(vs*G`iddbWq}! z(2!VjP&{Z1w+%eUq^ '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..1a25252 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'Benchmark' \ No newline at end of file diff --git a/src/main/java/ch/illgner/OrderCalculator.java b/src/main/java/ch/illgner/OrderCalculator.java new file mode 100644 index 0000000..747844e --- /dev/null +++ b/src/main/java/ch/illgner/OrderCalculator.java @@ -0,0 +1,18 @@ +package ch.illgner; + +public class OrderCalculator { + + /** + * Berechnet den Endpreis nach Rabatt und inklusive 19% MwSt. + * @param basePrice Der Nettopreis + * @param discountRate Der Rabatt (z.B. 0.1 für 10%) + * @return Der Endpreis gerundet auf 2 Nachkommastellen + */ + public double calculateFinalPrice(double basePrice, double discountRate) { + double discountedPrice = basePrice * discountRate; + + double priceWithTax = discountedPrice * 1.19; + + return priceWithTax; + } +} \ No newline at end of file diff --git a/src/test/java/ch/illgner/OrderCalculatorTest.java b/src/test/java/ch/illgner/OrderCalculatorTest.java new file mode 100644 index 0000000..f62acda --- /dev/null +++ b/src/test/java/ch/illgner/OrderCalculatorTest.java @@ -0,0 +1,32 @@ +package ch.illgner; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class OrderCalculatorTest { + + @Test + public void testCalculateFinalPrice_NormalScenario() { + OrderCalculator calculator = new OrderCalculator(); + + // Szenario: 100€ Basis, 10% Rabatt = 90€. + // 90€ + 19% MwSt (17.10€) = 107.10€ + double result = calculator.calculateFinalPrice(100.0, 0.10); + + assertEquals(107.10, result, "Der Endpreis für 100€ mit 10% Rabatt muss 107.10 sein."); + } + + @Test + public void testCalculateFinalPrice_Rounding() { + OrderCalculator calculator = new OrderCalculator(); + + // Szenario: 10.55€ Basis, 5% Rabatt = 10.0225€ + // 10.0225€ + 19% MwSt = 11.926775€ -> Gerundet: 11.93€ + double result = calculator.calculateFinalPrice(10.55, 0.05); + + assertEquals(11.93, result, "Der Preis muss korrekt auf 2 Stellen gerundet werden."); + } +} \ No newline at end of file From bb3f42b6bdf7519b1f29070c001d8d042199af3e Mon Sep 17 00:00:00 2001 From: fillg1 Date: Wed, 8 Apr 2026 21:50:51 +0200 Subject: [PATCH 03/26] remove comment --- src/main/java/ch/illgner/OrderCalculator.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/ch/illgner/OrderCalculator.java b/src/main/java/ch/illgner/OrderCalculator.java index 68fa8a9..091ac7a 100644 --- a/src/main/java/ch/illgner/OrderCalculator.java +++ b/src/main/java/ch/illgner/OrderCalculator.java @@ -4,18 +4,16 @@ public class OrderCalculator { /** * Berechnet den Endpreis nach Rabatt und inklusive 19% MwSt. - * @param basePrice Der Nettopreis + * + * @param basePrice Der Nettopreis * @param discountRate Der Rabatt (z.B. 0.1 für 10%) * @return Der Endpreis gerundet auf 2 Nachkommastellen */ public double calculateFinalPrice(double basePrice, double discountRate) { - // FEHLER 1: Logikfehler - berechnet nur den Rabattwert, nicht den neuen Preis double discountedPrice = basePrice * discountRate; - // FEHLER 2: MwSt wird auf den falschen Wert aufgeschlagen double priceWithTax = discountedPrice * 1.19; - // FEHLER 3: Keine Rundung auf 2 Nachkommastellen return priceWithTax; } } \ No newline at end of file From 1df3e2ad819daeef6a70e4677dde1bcf9a26275a Mon Sep 17 00:00:00 2001 From: fillg1 Date: Wed, 8 Apr 2026 21:50:51 +0200 Subject: [PATCH 04/26] remove comment --- src/main/java/ch/illgner/OrderCalculator.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/illgner/OrderCalculator.java b/src/main/java/ch/illgner/OrderCalculator.java index 747844e..091ac7a 100644 --- a/src/main/java/ch/illgner/OrderCalculator.java +++ b/src/main/java/ch/illgner/OrderCalculator.java @@ -4,7 +4,8 @@ public class OrderCalculator { /** * Berechnet den Endpreis nach Rabatt und inklusive 19% MwSt. - * @param basePrice Der Nettopreis + * + * @param basePrice Der Nettopreis * @param discountRate Der Rabatt (z.B. 0.1 für 10%) * @return Der Endpreis gerundet auf 2 Nachkommastellen */ From 72d74bcd629b5a55edd1fecd40e25eb9f2e5314b Mon Sep 17 00:00:00 2001 From: fillg1 Date: Thu, 9 Apr 2026 06:31:16 +0200 Subject: [PATCH 05/26] Initial commit --- .idea/.gitignore | 10 + .idea/.name | 1 + .idea/go.imports.xml | 10 + .idea/gradle.xml | 17 + .idea/misc.xml | 10 + .idea/vcs.xml | 6 + build.gradle | 9 +- gradle.properties | 1 + gradle/wrapper/gradle-wrapper.jar | Bin gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew.bat | 0 settings.gradle | 2 +- src/main/java/ch/illgner/OrderCalculator.java | 19 -- .../createzip/PostBoxZipFileWriter.java | 138 ++++++++ .../java/com/dswiss/createzip/ZipResult.java | 23 ++ .../encryption/IInputStreamEnhancer.java | 15 + .../encryption/IOutputStreamEnhancer.java | 18 ++ .../java/ch/illgner/OrderCalculatorTest.java | 32 -- .../service/PostBoxZipFileWriterTest.java | 306 ++++++++++++++++++ 19 files changed, 565 insertions(+), 54 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/go.imports.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml mode change 100644 => 100755 build.gradle create mode 100755 gradle.properties mode change 100644 => 100755 gradle/wrapper/gradle-wrapper.jar mode change 100644 => 100755 gradle/wrapper/gradle-wrapper.properties mode change 100644 => 100755 gradlew.bat mode change 100644 => 100755 settings.gradle delete mode 100644 src/main/java/ch/illgner/OrderCalculator.java create mode 100755 src/main/java/com/dswiss/createzip/PostBoxZipFileWriter.java create mode 100755 src/main/java/com/dswiss/createzip/ZipResult.java create mode 100755 src/main/java/com/dswiss/encryption/IInputStreamEnhancer.java create mode 100755 src/main/java/com/dswiss/encryption/IOutputStreamEnhancer.java delete mode 100644 src/test/java/ch/illgner/OrderCalculatorTest.java create mode 100755 src/test/java/com/dswiss/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..b26bca2 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +benchmark \ No newline at end of file diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml new file mode 100644 index 0000000..644cdf0 --- /dev/null +++ b/.idea/go.imports.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..b37fcda --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7dcc043 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle old mode 100644 new mode 100755 index 304e7e2..df545ab --- a/build.gradle +++ b/build.gradle @@ -10,11 +10,18 @@ repositories { } dependencies { + implementation group: 'commons-io', name: 'commons-io', version: '2.21.0' + implementation "org.apache.commons:commons-lang3:3.20.0" + implementation "org.slf4j:jul-to-slf4j:$sl4j_version" + implementation "org.slf4j:log4j-over-slf4j:$sl4j_version" + testImplementation platform('org.junit:junit-bom:6.0.0') testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation group: 'org.hamcrest', name: 'hamcrest-library', version: '3.0' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } test { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties new file mode 100755 index 0000000..28f4b38 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +sl4j_version=1.7.35 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar old mode 100644 new mode 100755 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties old mode 100644 new mode 100755 index 43d9bce..be2bdc2 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Wed Apr 08 21:42:31 CEST 2026 +#Wed Apr 08 22:08:17 CEST 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip diff --git a/gradlew.bat b/gradlew.bat old mode 100644 new mode 100755 diff --git a/settings.gradle b/settings.gradle old mode 100644 new mode 100755 index 1a25252..e3abb74 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'Benchmark' \ No newline at end of file +rootProject.name = 'benchmark' \ No newline at end of file diff --git a/src/main/java/ch/illgner/OrderCalculator.java b/src/main/java/ch/illgner/OrderCalculator.java deleted file mode 100644 index 091ac7a..0000000 --- a/src/main/java/ch/illgner/OrderCalculator.java +++ /dev/null @@ -1,19 +0,0 @@ -package ch.illgner; - -public class OrderCalculator { - - /** - * Berechnet den Endpreis nach Rabatt und inklusive 19% MwSt. - * - * @param basePrice Der Nettopreis - * @param discountRate Der Rabatt (z.B. 0.1 für 10%) - * @return Der Endpreis gerundet auf 2 Nachkommastellen - */ - public double calculateFinalPrice(double basePrice, double discountRate) { - double discountedPrice = basePrice * discountRate; - - double priceWithTax = discountedPrice * 1.19; - - return priceWithTax; - } -} \ No newline at end of file diff --git a/src/main/java/com/dswiss/createzip/PostBoxZipFileWriter.java b/src/main/java/com/dswiss/createzip/PostBoxZipFileWriter.java new file mode 100755 index 0000000..2cc4d52 --- /dev/null +++ b/src/main/java/com/dswiss/createzip/PostBoxZipFileWriter.java @@ -0,0 +1,138 @@ +package com.dswiss.createzip; + +import com.dswiss.encryption.IOutputStreamEnhancer; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.output.CountingOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.NoSuchPaddingException; +import java.io.BufferedOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.EnumSet; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static java.nio.file.attribute.PosixFilePermission.GROUP_READ; +import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; + +public class PostBoxZipFileWriter implements Closeable { + + private static final Logger LOG = LoggerFactory.getLogger(PostBoxZipFileWriter.class); + + private static final Set ZIP_FILE_PERMISSIONS = EnumSet.of(OWNER_READ, OWNER_WRITE, GROUP_READ, OTHERS_READ); + + private final Path destinationDir; + private final String baseName; + private final int chunkSize; + private final int bufferSize; + private final ZipResult zipResult; + IOutputStreamEnhancer outputStreamEnhancer; + + private ZipOutputStream zipOutputStream; + + private int fileCount = 1; + private long bytesWritten = 0; + private File actualZipFile; + private CountingOutputStream countingOutputStream; + + public PostBoxZipFileWriter(Path destinationDir, String baseName, int chunkSize, int bufferSize, IOutputStreamEnhancer outputStreamEnhancer) { + this.destinationDir = destinationDir; + this.baseName = baseName; + this.chunkSize = chunkSize; + this.bufferSize = bufferSize; + this.zipResult = new ZipResult(); + this.outputStreamEnhancer = outputStreamEnhancer; + } + + private ZipOutputStream createNewZipOutputStream() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, + NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + + String actualZipFilename = createOutputFilename(); + actualZipFile = destinationDir.resolve(actualZipFilename).toFile(); + + LOG.info("Creating zip part {}: {}", fileCount, actualZipFilename); + + countingOutputStream = new CountingOutputStream(outputStreamEnhancer.enhance(new BufferedOutputStream(new FileOutputStream(actualZipFile), bufferSize))); + return new ZipOutputStream(countingOutputStream); + } + + private String createOutputFilename() { + return String.format("%s_%02d.zip", baseName, fileCount); + } + + public void write(InputStream inputStream, String path) throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, + NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + + if (countingOutputStream == null) { + zipOutputStream = createNewZipOutputStream(); + } + if (bytesWritten >= chunkSize) { + switchOutputFile(); + } + LOG.info("Writing file {} to zip", path); + zipOutputStream.putNextEntry(new ZipEntry(path)); + bytesWritten += IOUtils.copyLarge(inputStream, zipOutputStream); + zipOutputStream.closeEntry(); + } + + private void switchOutputFile() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + LOG.info("Switching zip"); + zipOutputStream.flush(); + zipOutputStream.close(); + addZipToResult(); + fileCount++; + zipOutputStream = createNewZipOutputStream(); + bytesWritten = 0; + } + + public void close() throws IOException { + LOG.info("Closing zip {}", actualZipFile.getName()); + zipOutputStream.flush(); + zipOutputStream.close(); + addZipToResult(); + } + + public void writeReadMe() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, + NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + + if (zipOutputStream != null) { + LOG.info("Skip adding Readme"); + return; + } + + try (InputStream readmeStream = this.getClass().getClassLoader().getResourceAsStream("Readme.txt")) { + if (readmeStream == null) { + throw new IOException("Cannot open Readme"); + } + + LOG.info("Writing Readme"); + write(readmeStream, "Readme.txt"); + } catch (IOException e) { + throw new IOException("Could not write Readme", e); + } + } + + public ZipResult getZipResult() { + return this.zipResult; + } + + private void addZipToResult() throws IOException { + zipResult.add(actualZipFile.getName(), countingOutputStream.getCount()); + Files.setPosixFilePermissions(actualZipFile.toPath(), ZIP_FILE_PERMISSIONS); + } +} diff --git a/src/main/java/com/dswiss/createzip/ZipResult.java b/src/main/java/com/dswiss/createzip/ZipResult.java new file mode 100755 index 0000000..1e08df1 --- /dev/null +++ b/src/main/java/com/dswiss/createzip/ZipResult.java @@ -0,0 +1,23 @@ +package com.dswiss.createzip; + +import java.util.ArrayList; +import java.util.List; + +public class ZipResult { + + private final List zipFilenames = new ArrayList<>(); + private final List zipFileLengths = new ArrayList<>(); + + void add(String fileName, long bytesWritten) { + zipFilenames.add(fileName); + zipFileLengths.add(bytesWritten); + } + + public List getFileLengths() { + return zipFileLengths; + } + + public List getFilenames() { + return zipFilenames; + } +} diff --git a/src/main/java/com/dswiss/encryption/IInputStreamEnhancer.java b/src/main/java/com/dswiss/encryption/IInputStreamEnhancer.java new file mode 100755 index 0000000..24d804f --- /dev/null +++ b/src/main/java/com/dswiss/encryption/IInputStreamEnhancer.java @@ -0,0 +1,15 @@ +package com.dswiss.encryption; + +import javax.crypto.NoSuchPaddingException; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +public interface IInputStreamEnhancer { + + InputStream enhance(InputStream inputStream) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException, InvalidAlgorithmParameterException, NoSuchProviderException; + +} diff --git a/src/main/java/com/dswiss/encryption/IOutputStreamEnhancer.java b/src/main/java/com/dswiss/encryption/IOutputStreamEnhancer.java new file mode 100755 index 0000000..c1882a8 --- /dev/null +++ b/src/main/java/com/dswiss/encryption/IOutputStreamEnhancer.java @@ -0,0 +1,18 @@ +package com.dswiss.encryption; + +import javax.crypto.NoSuchPaddingException; +import java.io.IOException; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +public interface IOutputStreamEnhancer { + /** + * This method enhances the given input stream {@link OutputStream} wrapping with the additional functionality (in our case with encryption) + * @param outputStream the original stream {@link OutputStream} to be enhanced. + * @return A new {@link OutputStream} that wraps the functionality to input stream. + */ + OutputStream enhance(OutputStream outputStream) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException, InvalidAlgorithmParameterException, NoSuchProviderException; +} diff --git a/src/test/java/ch/illgner/OrderCalculatorTest.java b/src/test/java/ch/illgner/OrderCalculatorTest.java deleted file mode 100644 index f62acda..0000000 --- a/src/test/java/ch/illgner/OrderCalculatorTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package ch.illgner; - -import static org.junit.jupiter.api.Assertions.*; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class OrderCalculatorTest { - - @Test - public void testCalculateFinalPrice_NormalScenario() { - OrderCalculator calculator = new OrderCalculator(); - - // Szenario: 100€ Basis, 10% Rabatt = 90€. - // 90€ + 19% MwSt (17.10€) = 107.10€ - double result = calculator.calculateFinalPrice(100.0, 0.10); - - assertEquals(107.10, result, "Der Endpreis für 100€ mit 10% Rabatt muss 107.10 sein."); - } - - @Test - public void testCalculateFinalPrice_Rounding() { - OrderCalculator calculator = new OrderCalculator(); - - // Szenario: 10.55€ Basis, 5% Rabatt = 10.0225€ - // 10.0225€ + 19% MwSt = 11.926775€ -> Gerundet: 11.93€ - double result = calculator.calculateFinalPrice(10.55, 0.05); - - assertEquals(11.93, result, "Der Preis muss korrekt auf 2 Stellen gerundet werden."); - } -} \ No newline at end of file diff --git a/src/test/java/com/dswiss/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java b/src/test/java/com/dswiss/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java new file mode 100755 index 0000000..70da55e --- /dev/null +++ b/src/test/java/com/dswiss/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java @@ -0,0 +1,306 @@ +package com.dswiss.jobs.postboxbulk.service; + +import com.dswiss.createzip.PostBoxZipFileWriter; +import com.dswiss.createzip.ZipResult; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import javax.crypto.NoSuchPaddingException; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.Objects; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.io.FileMatchers.aFileNamed; +import static org.hamcrest.io.FileMatchers.anExistingFile; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PostBoxZipFileWriterTest { + + private static final Path DESTINATION_DIR = Paths.get("./destination"); + private static final int ONE_BYTE = 1024; + private static final int HALF_BYTE = ONE_BYTE / 2; + private static final int ZIP_BUFFER_SIZE_IN_BYTE = 2 * ONE_BYTE; + private static final int ZIP_CHUNK_SIZE_IN_BYTE = 3 * ONE_BYTE; + + @BeforeAll + static void setUp() throws IOException { + FileUtils.deleteDirectory(DESTINATION_DIR.toFile()); + boolean directoryCreated = DESTINATION_DIR.toFile().mkdir(); + assertTrue(directoryCreated); + } + + @AfterAll + static void cleanUp() throws IOException { + FileUtils.deleteDirectory(DESTINATION_DIR.toFile()); + boolean directoryCreated = DESTINATION_DIR.toFile().mkdir(); + assertTrue(directoryCreated); + } + + @Test + void singleFileSmallerThanChunkSizeToOneZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + byte[] fileSimpleContent = RandomStringUtils.randomAlphanumeric(2 * ONE_BYTE).getBytes(StandardCharsets.UTF_8); + ZipResult zipResult; + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + String inputFileName = "singleFileSmallerThanChunkSizeToOneZip.txt"; + PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); + try (postBoxZipFileWriter) { + postBoxZipFileWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); + } + String expectedZippedFileName = outputFileName + "_01.zip"; + zipResult = postBoxZipFileWriter.getZipResult(); + File createdZipFile = DESTINATION_DIR.resolve(expectedZippedFileName).toFile(); + + assertThat(zipResult.getFilenames(), hasSize(1)); + assertThat(zipResult.getFileLengths(), hasSize(1)); + assertThat(zipResult.getFilenames().get(0), is(equalTo(expectedZippedFileName))); + assertThat(zipResult.getFileLengths().get(0), is(equalTo(createdZipFile.length()))); + assertThat(createdZipFile, is(anExistingFile())); + assertThat(createdZipFile, is(aFileNamed(equalTo(expectedZippedFileName)))); + + // check zip content + try (ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(createdZipFile))) { + assertThat(Objects.requireNonNull(zipInputStream.getNextEntry()).getName(), is(equalTo(inputFileName))); + assertThat(IOUtils.toByteArray(zipInputStream), is(equalTo(fileSimpleContent))); + } + } + + @Test + void singleFileGreaterThanChunkSizeToOneZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + byte[] fileSimpleContent = RandomStringUtils.randomAlphanumeric(4 * ONE_BYTE).getBytes(StandardCharsets.UTF_8); + ZipResult zipResult; + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + String inputFileName = "singleFileGreaterThanChunkSizeToOneZip.txt"; + PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); + try (postBoxZipFileWriter) { + postBoxZipFileWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); + } + String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; + zipResult = postBoxZipFileWriter.getZipResult(); + assertThat(zipResult.getFilenames(), hasSize(1)); + assertThat(zipResult.getFileLengths(), hasSize(1)); + assertTrue(zipResult.getFilenames().contains(expectedZippedFileNamePartOne)); + + File createdZipOne = DESTINATION_DIR.resolve(expectedZippedFileNamePartOne).toFile(); + + assertThat(createdZipOne, is(anExistingFile())); + assertThat(createdZipOne, is(aFileNamed(equalTo(expectedZippedFileNamePartOne)))); + + // check zip content + extractSplitZipFiles(new File[]{createdZipOne}); + File extractedFile = new File(DESTINATION_DIR + "/" + inputFileName); + byte[] extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(fileSimpleContent))); + } + + @Test + void multipleFileSmallerThanChunkSizeToSingleZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + byte[] firstFile = RandomStringUtils.randomAlphanumeric(ONE_BYTE).getBytes(StandardCharsets.UTF_8); + byte[] secondFile = "uts öäü ÖÄÜ ß".getBytes(StandardCharsets.UTF_8); + + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + String inputFileNameOne = "multipleFileSmallerThanChunkSizeToSingleZip1.txt"; + String inputFileNameTwo = "multipleFileSmallerThanChunkSizeToSingleZip2.txt"; + + ZipResult zipResult; + PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); + try (postBoxZipFileWriter) { + postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); + postBoxZipFileWriter.write(new ByteArrayInputStream(secondFile), inputFileNameTwo); + } + + zipResult = postBoxZipFileWriter.getZipResult(); + String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; + File createdZip = DESTINATION_DIR.resolve(expectedZippedFileNamePartOne).toFile(); + + assertThat(zipResult.getFilenames(), hasSize(1)); + assertThat(zipResult.getFileLengths(), hasSize(1)); + assertThat(createdZip, is(anExistingFile())); + assertThat(createdZip, is(aFileNamed(equalTo(expectedZippedFileNamePartOne)))); + assertThat(zipResult.getFilenames().get(0), is(equalTo(expectedZippedFileNamePartOne))); + assertThat(zipResult.getFileLengths().get(0), is(equalTo(createdZip.length()))); + + // check zip content + try (ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(createdZip))) { + assertThat(Objects.requireNonNull(zipInputStream.getNextEntry()).getName(), is(equalTo(inputFileNameOne))); + assertThat(IOUtils.toByteArray(zipInputStream), is(equalTo(firstFile))); + assertThat(zipInputStream.getNextEntry().getName(), is(equalTo(inputFileNameTwo))); + assertThat(IOUtils.toByteArray(zipInputStream), is(equalTo(secondFile))); + } + } + + @Test + void multipleFilesTogetherGreaterThanChunkSizeToSingleZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + String text1 = RandomStringUtils.randomAlphanumeric(2 * ONE_BYTE + HALF_BYTE); + String text2 = RandomStringUtils.randomAlphanumeric(2 * ONE_BYTE + HALF_BYTE); + byte[] firstFile = text1.getBytes(StandardCharsets.UTF_8); + byte[] secondFile = text2.getBytes(StandardCharsets.UTF_8); + + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + String inputFileNameOne = "multipleFilesTogetherGreaterThanChunkSizeToSingleZip1.txt"; + String inputFileNameTwo = "multipleFilesTogetherGreaterThanChunkSizeToSingleZip2.txt"; + + ZipResult zipResult; + PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); + try (postBoxZipFileWriter) { + postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); + postBoxZipFileWriter.write(new ByteArrayInputStream(secondFile), inputFileNameTwo); + } finally { + zipResult = postBoxZipFileWriter.getZipResult(); + } + + String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; + + assertThat(zipResult.getFilenames(), hasSize(1)); + assertThat(zipResult.getFileLengths(), hasSize(1)); + assertTrue(zipResult.getFilenames().contains(expectedZippedFileNamePartOne)); + + File createdZipOne = DESTINATION_DIR.resolve(expectedZippedFileNamePartOne).toFile(); + + assertThat(createdZipOne, is(anExistingFile())); + assertThat(createdZipOne, is(aFileNamed(equalTo(expectedZippedFileNamePartOne)))); + + // check zip content + extractSplitZipFiles(new File[]{createdZipOne}); + File extractedFile = new File(DESTINATION_DIR + "/" + inputFileNameOne); + byte[] extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(firstFile))); + + extractedFile = new File(DESTINATION_DIR + "/" + inputFileNameTwo); + extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(secondFile))); + } + + @Test + void multipleBigFilesToMultipleZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + ZipResult zipResult; + int bigChuckSize = ONE_BYTE * 50; + int bigBufferSize = ONE_BYTE * 10; + // First two files will be zipped together and third file will be zipped in the second file. + byte[] firstFile = RandomStringUtils.randomAlphanumeric(40 * ONE_BYTE).getBytes(StandardCharsets.UTF_8); + byte[] secondFile = RandomStringUtils.randomAlphanumeric(70 * ONE_BYTE).getBytes(StandardCharsets.UTF_8); + byte[] thirdFile = RandomStringUtils.randomAlphanumeric(10 * ONE_BYTE).getBytes(StandardCharsets.UTF_8); + String[] originalFileNames = {"multipleBigFilesToMultipleZip1.txt", "multipleBigFilesToMultipleZip2.txt", "multipleBigFilesToMultipleZip3.txt"}; + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + + PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, outputFileName, bigChuckSize, bigBufferSize, (OutputStream outputStream) -> outputStream); + try (postBoxZipFileWriter) { + postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), originalFileNames[0]); + postBoxZipFileWriter.write(new ByteArrayInputStream(secondFile), originalFileNames[1]); + postBoxZipFileWriter.write(new ByteArrayInputStream(thirdFile), originalFileNames[2]); + } finally { + zipResult = postBoxZipFileWriter.getZipResult(); + } + + String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; + String expectedZippedFileNamePartTwo = outputFileName + "_02.zip"; + + assertThat(zipResult.getFilenames(), hasSize(2)); + assertThat(zipResult.getFileLengths(), hasSize(2)); + assertTrue(zipResult.getFilenames().contains(expectedZippedFileNamePartOne)); + assertTrue(zipResult.getFilenames().contains(expectedZippedFileNamePartTwo)); + + File part1 = DESTINATION_DIR.resolve(expectedZippedFileNamePartOne).toFile(); + File part2 = DESTINATION_DIR.resolve(expectedZippedFileNamePartTwo).toFile(); + + File[] zippedFiles = {part1, part2}; + + assertThat(part1, is(anExistingFile())); + assertThat(part1, is(aFileNamed(equalTo(expectedZippedFileNamePartOne)))); + assertThat(part2, is(anExistingFile())); + assertThat(part2, is(aFileNamed(equalTo(expectedZippedFileNamePartTwo)))); + + extractSplitZipFiles(zippedFiles); + + // check zip content + File extractedFile = new File(DESTINATION_DIR + "/" + originalFileNames[0]); + byte[] extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(firstFile))); + + extractedFile = new File(DESTINATION_DIR + "/" + originalFileNames[1]); + extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(secondFile))); + + extractedFile = new File(DESTINATION_DIR + "/" + originalFileNames[2]); + extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(thirdFile))); + } + + @Test + void createBigZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + // first implementation crashed with an OutOfMemoryException while copying an 50MB Stream, + // so we added a test for this + + try (PostBoxZipFileWriter zipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, "testBigMulti", 100 * 1000 * 1000, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream)) { + for (int i = 0; i < 10; i++) { + byte[] content = RandomStringUtils.randomAlphanumeric(50 * 1000 * 1000).getBytes(); + zipFileWriter.write(new ByteArrayInputStream(content), "test" + i); + } + } + } + + @Test + void addCompressibleFiles() throws InvalidAlgorithmParameterException, NoSuchPaddingException, IOException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + byte[] content1 = new byte[10000]; + byte[] content2 = new byte[10000]; + byte[] content3 = new byte[10000]; + + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + + ZipResult zipResult; + try (PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter(DESTINATION_DIR, outputFileName, 1000, 2048, (OutputStream outputStream) -> outputStream)) { + postBoxZipFileWriter.write(new ByteArrayInputStream(content1), "file1"); + postBoxZipFileWriter.write(new ByteArrayInputStream(content2), "file2"); + postBoxZipFileWriter.write(new ByteArrayInputStream(content3), "file3"); + zipResult = postBoxZipFileWriter.getZipResult(); + } + assertThat("compressible files only containing zeroes should be compressed into one file", zipResult.getFileLengths().size(), is(1)); + } + + public static void extractSplitZipFiles(File[] zipFiles) throws IOException { + byte[] buffer = new byte[ZIP_BUFFER_SIZE_IN_BYTE]; + for (File zipFilePath : zipFiles) { + try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFilePath))) { + ZipEntry zipEntry = zis.getNextEntry(); + + while (zipEntry != null) { + File newFile = new File(DESTINATION_DIR + "/" + zipEntry.getName()); + new File(newFile.getParent()).mkdirs(); // Create directories for nested files + try (FileOutputStream fos = new FileOutputStream(newFile, true)) { + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + } + zipEntry = zis.getNextEntry(); + } + } + } + } +} From 6447b1d90b87ea273acbac7ecc189ed005efe0e7 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Thu, 9 Apr 2026 06:31:16 +0200 Subject: [PATCH 06/26] Initial commit --- .idea/.gitignore | 10 + .idea/.name | 1 + .idea/go.imports.xml | 10 + .idea/gradle.xml | 17 + .idea/misc.xml | 10 + .idea/vcs.xml | 6 + build.gradle | 9 +- gradle.properties | 1 + gradle/wrapper/gradle-wrapper.jar | Bin gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew.bat | 0 settings.gradle | 2 +- src/main/java/ch/illgner/OrderCalculator.java | 19 -- .../ch/createzip/PostBoxZipFileWriter.java | 138 ++++++++ .../java/illgner/ch/createzip/ZipResult.java | 23 ++ .../ch/encryption/IInputStreamEnhancer.java | 15 + .../ch/encryption/IOutputStreamEnhancer.java | 18 ++ .../java/ch/illgner/OrderCalculatorTest.java | 32 -- .../service/PostBoxZipFileWriterTest.java | 306 ++++++++++++++++++ 19 files changed, 565 insertions(+), 54 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/go.imports.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml mode change 100644 => 100755 build.gradle create mode 100755 gradle.properties mode change 100644 => 100755 gradle/wrapper/gradle-wrapper.jar mode change 100644 => 100755 gradle/wrapper/gradle-wrapper.properties mode change 100644 => 100755 gradlew.bat mode change 100644 => 100755 settings.gradle delete mode 100644 src/main/java/ch/illgner/OrderCalculator.java create mode 100755 src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java create mode 100755 src/main/java/illgner/ch/createzip/ZipResult.java create mode 100755 src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java create mode 100755 src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java delete mode 100644 src/test/java/ch/illgner/OrderCalculatorTest.java create mode 100755 src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..b26bca2 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +benchmark \ No newline at end of file diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml new file mode 100644 index 0000000..644cdf0 --- /dev/null +++ b/.idea/go.imports.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..b37fcda --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7dcc043 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle old mode 100644 new mode 100755 index 304e7e2..df545ab --- a/build.gradle +++ b/build.gradle @@ -10,11 +10,18 @@ repositories { } dependencies { + implementation group: 'commons-io', name: 'commons-io', version: '2.21.0' + implementation "org.apache.commons:commons-lang3:3.20.0" + implementation "org.slf4j:jul-to-slf4j:$sl4j_version" + implementation "org.slf4j:log4j-over-slf4j:$sl4j_version" + testImplementation platform('org.junit:junit-bom:6.0.0') testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation group: 'org.hamcrest', name: 'hamcrest-library', version: '3.0' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } test { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties new file mode 100755 index 0000000..28f4b38 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +sl4j_version=1.7.35 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar old mode 100644 new mode 100755 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties old mode 100644 new mode 100755 index 43d9bce..be2bdc2 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Wed Apr 08 21:42:31 CEST 2026 +#Wed Apr 08 22:08:17 CEST 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip diff --git a/gradlew.bat b/gradlew.bat old mode 100644 new mode 100755 diff --git a/settings.gradle b/settings.gradle old mode 100644 new mode 100755 index 1a25252..e3abb74 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'Benchmark' \ No newline at end of file +rootProject.name = 'benchmark' \ No newline at end of file diff --git a/src/main/java/ch/illgner/OrderCalculator.java b/src/main/java/ch/illgner/OrderCalculator.java deleted file mode 100644 index 091ac7a..0000000 --- a/src/main/java/ch/illgner/OrderCalculator.java +++ /dev/null @@ -1,19 +0,0 @@ -package ch.illgner; - -public class OrderCalculator { - - /** - * Berechnet den Endpreis nach Rabatt und inklusive 19% MwSt. - * - * @param basePrice Der Nettopreis - * @param discountRate Der Rabatt (z.B. 0.1 für 10%) - * @return Der Endpreis gerundet auf 2 Nachkommastellen - */ - public double calculateFinalPrice(double basePrice, double discountRate) { - double discountedPrice = basePrice * discountRate; - - double priceWithTax = discountedPrice * 1.19; - - return priceWithTax; - } -} \ No newline at end of file diff --git a/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java b/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java new file mode 100755 index 0000000..ef46a0a --- /dev/null +++ b/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java @@ -0,0 +1,138 @@ +package illgner.ch.createzip; + +import illgner.ch.encryption.IOutputStreamEnhancer; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.output.CountingOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.NoSuchPaddingException; +import java.io.BufferedOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.EnumSet; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static java.nio.file.attribute.PosixFilePermission.GROUP_READ; +import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; + +public class PostBoxZipFileWriter implements Closeable { + + private static final Logger LOG = LoggerFactory.getLogger(PostBoxZipFileWriter.class); + + private static final Set ZIP_FILE_PERMISSIONS = EnumSet.of(OWNER_READ, OWNER_WRITE, GROUP_READ, OTHERS_READ); + + private final Path destinationDir; + private final String baseName; + private final int chunkSize; + private final int bufferSize; + private final ZipResult zipResult; + IOutputStreamEnhancer outputStreamEnhancer; + + private ZipOutputStream zipOutputStream; + + private int fileCount = 1; + private long bytesWritten = 0; + private File actualZipFile; + private CountingOutputStream countingOutputStream; + + public PostBoxZipFileWriter(Path destinationDir, String baseName, int chunkSize, int bufferSize, IOutputStreamEnhancer outputStreamEnhancer) { + this.destinationDir = destinationDir; + this.baseName = baseName; + this.chunkSize = chunkSize; + this.bufferSize = bufferSize; + this.zipResult = new ZipResult(); + this.outputStreamEnhancer = outputStreamEnhancer; + } + + private ZipOutputStream createNewZipOutputStream() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, + NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + + String actualZipFilename = createOutputFilename(); + actualZipFile = destinationDir.resolve(actualZipFilename).toFile(); + + LOG.info("Creating zip part {}: {}", fileCount, actualZipFilename); + + countingOutputStream = new CountingOutputStream(outputStreamEnhancer.enhance(new BufferedOutputStream(new FileOutputStream(actualZipFile), bufferSize))); + return new ZipOutputStream(countingOutputStream); + } + + private String createOutputFilename() { + return String.format("%s_%02d.zip", baseName, fileCount); + } + + public void write(InputStream inputStream, String path) throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, + NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + + if (countingOutputStream == null) { + zipOutputStream = createNewZipOutputStream(); + } + if (bytesWritten >= chunkSize) { + switchOutputFile(); + } + LOG.info("Writing file {} to zip", path); + zipOutputStream.putNextEntry(new ZipEntry(path)); + bytesWritten += IOUtils.copyLarge(inputStream, zipOutputStream); + zipOutputStream.closeEntry(); + } + + private void switchOutputFile() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + LOG.info("Switching zip"); + zipOutputStream.flush(); + zipOutputStream.close(); + addZipToResult(); + fileCount++; + zipOutputStream = createNewZipOutputStream(); + bytesWritten = 0; + } + + public void close() throws IOException { + LOG.info("Closing zip {}", actualZipFile.getName()); + zipOutputStream.flush(); + zipOutputStream.close(); + addZipToResult(); + } + + public void writeReadMe() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, + NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + + if (zipOutputStream != null) { + LOG.info("Skip adding Readme"); + return; + } + + try (InputStream readmeStream = this.getClass().getClassLoader().getResourceAsStream("Readme.txt")) { + if (readmeStream == null) { + throw new IOException("Cannot open Readme"); + } + + LOG.info("Writing Readme"); + write(readmeStream, "Readme.txt"); + } catch (IOException e) { + throw new IOException("Could not write Readme", e); + } + } + + public ZipResult getZipResult() { + return this.zipResult; + } + + private void addZipToResult() throws IOException { + zipResult.add(actualZipFile.getName(), countingOutputStream.getCount()); + Files.setPosixFilePermissions(actualZipFile.toPath(), ZIP_FILE_PERMISSIONS); + } +} diff --git a/src/main/java/illgner/ch/createzip/ZipResult.java b/src/main/java/illgner/ch/createzip/ZipResult.java new file mode 100755 index 0000000..947bf33 --- /dev/null +++ b/src/main/java/illgner/ch/createzip/ZipResult.java @@ -0,0 +1,23 @@ +package illgner.ch.createzip; + +import java.util.ArrayList; +import java.util.List; + +public class ZipResult { + + private final List zipFilenames = new ArrayList<>(); + private final List zipFileLengths = new ArrayList<>(); + + void add(String fileName, long bytesWritten) { + zipFilenames.add(fileName); + zipFileLengths.add(bytesWritten); + } + + public List getFileLengths() { + return zipFileLengths; + } + + public List getFilenames() { + return zipFilenames; + } +} diff --git a/src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java b/src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java new file mode 100755 index 0000000..94dd475 --- /dev/null +++ b/src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java @@ -0,0 +1,15 @@ +package illgner.ch.encryption; + +import javax.crypto.NoSuchPaddingException; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +public interface IInputStreamEnhancer { + + InputStream enhance(InputStream inputStream) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException, InvalidAlgorithmParameterException, NoSuchProviderException; + +} diff --git a/src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java b/src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java new file mode 100755 index 0000000..d17ad88 --- /dev/null +++ b/src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java @@ -0,0 +1,18 @@ +package illgner.ch.encryption; + +import javax.crypto.NoSuchPaddingException; +import java.io.IOException; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +public interface IOutputStreamEnhancer { + /** + * This method enhances the given input stream {@link OutputStream} wrapping with the additional functionality (in our case with encryption) + * @param outputStream the original stream {@link OutputStream} to be enhanced. + * @return A new {@link OutputStream} that wraps the functionality to input stream. + */ + OutputStream enhance(OutputStream outputStream) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException, InvalidAlgorithmParameterException, NoSuchProviderException; +} diff --git a/src/test/java/ch/illgner/OrderCalculatorTest.java b/src/test/java/ch/illgner/OrderCalculatorTest.java deleted file mode 100644 index f62acda..0000000 --- a/src/test/java/ch/illgner/OrderCalculatorTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package ch.illgner; - -import static org.junit.jupiter.api.Assertions.*; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class OrderCalculatorTest { - - @Test - public void testCalculateFinalPrice_NormalScenario() { - OrderCalculator calculator = new OrderCalculator(); - - // Szenario: 100€ Basis, 10% Rabatt = 90€. - // 90€ + 19% MwSt (17.10€) = 107.10€ - double result = calculator.calculateFinalPrice(100.0, 0.10); - - assertEquals(107.10, result, "Der Endpreis für 100€ mit 10% Rabatt muss 107.10 sein."); - } - - @Test - public void testCalculateFinalPrice_Rounding() { - OrderCalculator calculator = new OrderCalculator(); - - // Szenario: 10.55€ Basis, 5% Rabatt = 10.0225€ - // 10.0225€ + 19% MwSt = 11.926775€ -> Gerundet: 11.93€ - double result = calculator.calculateFinalPrice(10.55, 0.05); - - assertEquals(11.93, result, "Der Preis muss korrekt auf 2 Stellen gerundet werden."); - } -} \ No newline at end of file diff --git a/src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java b/src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java new file mode 100755 index 0000000..837ddd2 --- /dev/null +++ b/src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java @@ -0,0 +1,306 @@ +package illgner.ch.jobs.postboxbulk.service; + +import illgner.ch.createzip.PostBoxZipFileWriter; +import illgner.ch.createzip.ZipResult; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import javax.crypto.NoSuchPaddingException; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.Objects; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.io.FileMatchers.aFileNamed; +import static org.hamcrest.io.FileMatchers.anExistingFile; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PostBoxZipFileWriterTest { + + private static final Path DESTINATION_DIR = Paths.get("./destination"); + private static final int ONE_BYTE = 1024; + private static final int HALF_BYTE = ONE_BYTE / 2; + private static final int ZIP_BUFFER_SIZE_IN_BYTE = 2 * ONE_BYTE; + private static final int ZIP_CHUNK_SIZE_IN_BYTE = 3 * ONE_BYTE; + + @BeforeAll + static void setUp() throws IOException { + FileUtils.deleteDirectory(DESTINATION_DIR.toFile()); + boolean directoryCreated = DESTINATION_DIR.toFile().mkdir(); + assertTrue(directoryCreated); + } + + @AfterAll + static void cleanUp() throws IOException { + FileUtils.deleteDirectory(DESTINATION_DIR.toFile()); + boolean directoryCreated = DESTINATION_DIR.toFile().mkdir(); + assertTrue(directoryCreated); + } + + @Test + void singleFileSmallerThanChunkSizeToOneZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + byte[] fileSimpleContent = RandomStringUtils.randomAlphanumeric(2 * ONE_BYTE).getBytes(StandardCharsets.UTF_8); + ZipResult zipResult; + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + String inputFileName = "singleFileSmallerThanChunkSizeToOneZip.txt"; + PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); + try (postBoxZipFileWriter) { + postBoxZipFileWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); + } + String expectedZippedFileName = outputFileName + "_01.zip"; + zipResult = postBoxZipFileWriter.getZipResult(); + File createdZipFile = DESTINATION_DIR.resolve(expectedZippedFileName).toFile(); + + assertThat(zipResult.getFilenames(), hasSize(1)); + assertThat(zipResult.getFileLengths(), hasSize(1)); + assertThat(zipResult.getFilenames().get(0), is(equalTo(expectedZippedFileName))); + assertThat(zipResult.getFileLengths().get(0), is(equalTo(createdZipFile.length()))); + assertThat(createdZipFile, is(anExistingFile())); + assertThat(createdZipFile, is(aFileNamed(equalTo(expectedZippedFileName)))); + + // check zip content + try (ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(createdZipFile))) { + assertThat(Objects.requireNonNull(zipInputStream.getNextEntry()).getName(), is(equalTo(inputFileName))); + assertThat(IOUtils.toByteArray(zipInputStream), is(equalTo(fileSimpleContent))); + } + } + + @Test + void singleFileGreaterThanChunkSizeToOneZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + byte[] fileSimpleContent = RandomStringUtils.randomAlphanumeric(4 * ONE_BYTE).getBytes(StandardCharsets.UTF_8); + ZipResult zipResult; + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + String inputFileName = "singleFileGreaterThanChunkSizeToOneZip.txt"; + PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); + try (postBoxZipFileWriter) { + postBoxZipFileWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); + } + String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; + zipResult = postBoxZipFileWriter.getZipResult(); + assertThat(zipResult.getFilenames(), hasSize(1)); + assertThat(zipResult.getFileLengths(), hasSize(1)); + assertTrue(zipResult.getFilenames().contains(expectedZippedFileNamePartOne)); + + File createdZipOne = DESTINATION_DIR.resolve(expectedZippedFileNamePartOne).toFile(); + + assertThat(createdZipOne, is(anExistingFile())); + assertThat(createdZipOne, is(aFileNamed(equalTo(expectedZippedFileNamePartOne)))); + + // check zip content + extractSplitZipFiles(new File[]{createdZipOne}); + File extractedFile = new File(DESTINATION_DIR + "/" + inputFileName); + byte[] extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(fileSimpleContent))); + } + + @Test + void multipleFileSmallerThanChunkSizeToSingleZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + byte[] firstFile = RandomStringUtils.randomAlphanumeric(ONE_BYTE).getBytes(StandardCharsets.UTF_8); + byte[] secondFile = "uts öäü ÖÄÜ ß".getBytes(StandardCharsets.UTF_8); + + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + String inputFileNameOne = "multipleFileSmallerThanChunkSizeToSingleZip1.txt"; + String inputFileNameTwo = "multipleFileSmallerThanChunkSizeToSingleZip2.txt"; + + ZipResult zipResult; + PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); + try (postBoxZipFileWriter) { + postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); + postBoxZipFileWriter.write(new ByteArrayInputStream(secondFile), inputFileNameTwo); + } + + zipResult = postBoxZipFileWriter.getZipResult(); + String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; + File createdZip = DESTINATION_DIR.resolve(expectedZippedFileNamePartOne).toFile(); + + assertThat(zipResult.getFilenames(), hasSize(1)); + assertThat(zipResult.getFileLengths(), hasSize(1)); + assertThat(createdZip, is(anExistingFile())); + assertThat(createdZip, is(aFileNamed(equalTo(expectedZippedFileNamePartOne)))); + assertThat(zipResult.getFilenames().get(0), is(equalTo(expectedZippedFileNamePartOne))); + assertThat(zipResult.getFileLengths().get(0), is(equalTo(createdZip.length()))); + + // check zip content + try (ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(createdZip))) { + assertThat(Objects.requireNonNull(zipInputStream.getNextEntry()).getName(), is(equalTo(inputFileNameOne))); + assertThat(IOUtils.toByteArray(zipInputStream), is(equalTo(firstFile))); + assertThat(zipInputStream.getNextEntry().getName(), is(equalTo(inputFileNameTwo))); + assertThat(IOUtils.toByteArray(zipInputStream), is(equalTo(secondFile))); + } + } + + @Test + void multipleFilesTogetherGreaterThanChunkSizeToSingleZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + String text1 = RandomStringUtils.randomAlphanumeric(2 * ONE_BYTE + HALF_BYTE); + String text2 = RandomStringUtils.randomAlphanumeric(2 * ONE_BYTE + HALF_BYTE); + byte[] firstFile = text1.getBytes(StandardCharsets.UTF_8); + byte[] secondFile = text2.getBytes(StandardCharsets.UTF_8); + + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + String inputFileNameOne = "multipleFilesTogetherGreaterThanChunkSizeToSingleZip1.txt"; + String inputFileNameTwo = "multipleFilesTogetherGreaterThanChunkSizeToSingleZip2.txt"; + + ZipResult zipResult; + PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); + try (postBoxZipFileWriter) { + postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); + postBoxZipFileWriter.write(new ByteArrayInputStream(secondFile), inputFileNameTwo); + } finally { + zipResult = postBoxZipFileWriter.getZipResult(); + } + + String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; + + assertThat(zipResult.getFilenames(), hasSize(1)); + assertThat(zipResult.getFileLengths(), hasSize(1)); + assertTrue(zipResult.getFilenames().contains(expectedZippedFileNamePartOne)); + + File createdZipOne = DESTINATION_DIR.resolve(expectedZippedFileNamePartOne).toFile(); + + assertThat(createdZipOne, is(anExistingFile())); + assertThat(createdZipOne, is(aFileNamed(equalTo(expectedZippedFileNamePartOne)))); + + // check zip content + extractSplitZipFiles(new File[]{createdZipOne}); + File extractedFile = new File(DESTINATION_DIR + "/" + inputFileNameOne); + byte[] extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(firstFile))); + + extractedFile = new File(DESTINATION_DIR + "/" + inputFileNameTwo); + extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(secondFile))); + } + + @Test + void multipleBigFilesToMultipleZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + ZipResult zipResult; + int bigChuckSize = ONE_BYTE * 50; + int bigBufferSize = ONE_BYTE * 10; + // First two files will be zipped together and third file will be zipped in the second file. + byte[] firstFile = RandomStringUtils.randomAlphanumeric(40 * ONE_BYTE).getBytes(StandardCharsets.UTF_8); + byte[] secondFile = RandomStringUtils.randomAlphanumeric(70 * ONE_BYTE).getBytes(StandardCharsets.UTF_8); + byte[] thirdFile = RandomStringUtils.randomAlphanumeric(10 * ONE_BYTE).getBytes(StandardCharsets.UTF_8); + String[] originalFileNames = {"multipleBigFilesToMultipleZip1.txt", "multipleBigFilesToMultipleZip2.txt", "multipleBigFilesToMultipleZip3.txt"}; + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + + PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, outputFileName, bigChuckSize, bigBufferSize, (OutputStream outputStream) -> outputStream); + try (postBoxZipFileWriter) { + postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), originalFileNames[0]); + postBoxZipFileWriter.write(new ByteArrayInputStream(secondFile), originalFileNames[1]); + postBoxZipFileWriter.write(new ByteArrayInputStream(thirdFile), originalFileNames[2]); + } finally { + zipResult = postBoxZipFileWriter.getZipResult(); + } + + String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; + String expectedZippedFileNamePartTwo = outputFileName + "_02.zip"; + + assertThat(zipResult.getFilenames(), hasSize(2)); + assertThat(zipResult.getFileLengths(), hasSize(2)); + assertTrue(zipResult.getFilenames().contains(expectedZippedFileNamePartOne)); + assertTrue(zipResult.getFilenames().contains(expectedZippedFileNamePartTwo)); + + File part1 = DESTINATION_DIR.resolve(expectedZippedFileNamePartOne).toFile(); + File part2 = DESTINATION_DIR.resolve(expectedZippedFileNamePartTwo).toFile(); + + File[] zippedFiles = {part1, part2}; + + assertThat(part1, is(anExistingFile())); + assertThat(part1, is(aFileNamed(equalTo(expectedZippedFileNamePartOne)))); + assertThat(part2, is(anExistingFile())); + assertThat(part2, is(aFileNamed(equalTo(expectedZippedFileNamePartTwo)))); + + extractSplitZipFiles(zippedFiles); + + // check zip content + File extractedFile = new File(DESTINATION_DIR + "/" + originalFileNames[0]); + byte[] extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(firstFile))); + + extractedFile = new File(DESTINATION_DIR + "/" + originalFileNames[1]); + extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(secondFile))); + + extractedFile = new File(DESTINATION_DIR + "/" + originalFileNames[2]); + extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(thirdFile))); + } + + @Test + void createBigZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + // first implementation crashed with an OutOfMemoryException while copying an 50MB Stream, + // so we added a test for this + + try (PostBoxZipFileWriter zipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, "testBigMulti", 100 * 1000 * 1000, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream)) { + for (int i = 0; i < 10; i++) { + byte[] content = RandomStringUtils.randomAlphanumeric(50 * 1000 * 1000).getBytes(); + zipFileWriter.write(new ByteArrayInputStream(content), "test" + i); + } + } + } + + @Test + void addCompressibleFiles() throws InvalidAlgorithmParameterException, NoSuchPaddingException, IOException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + byte[] content1 = new byte[10000]; + byte[] content2 = new byte[10000]; + byte[] content3 = new byte[10000]; + + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + + ZipResult zipResult; + try (PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter(DESTINATION_DIR, outputFileName, 1000, 2048, (OutputStream outputStream) -> outputStream)) { + postBoxZipFileWriter.write(new ByteArrayInputStream(content1), "file1"); + postBoxZipFileWriter.write(new ByteArrayInputStream(content2), "file2"); + postBoxZipFileWriter.write(new ByteArrayInputStream(content3), "file3"); + zipResult = postBoxZipFileWriter.getZipResult(); + } + assertThat("compressible files only containing zeroes should be compressed into one file", zipResult.getFileLengths().size(), is(1)); + } + + public static void extractSplitZipFiles(File[] zipFiles) throws IOException { + byte[] buffer = new byte[ZIP_BUFFER_SIZE_IN_BYTE]; + for (File zipFilePath : zipFiles) { + try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFilePath))) { + ZipEntry zipEntry = zis.getNextEntry(); + + while (zipEntry != null) { + File newFile = new File(DESTINATION_DIR + "/" + zipEntry.getName()); + new File(newFile.getParent()).mkdirs(); // Create directories for nested files + try (FileOutputStream fos = new FileOutputStream(newFile, true)) { + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + } + zipEntry = zis.getNextEntry(); + } + } + } + } +} From 8d1737527107b1156831728f48f3494868f1f8e9 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Thu, 9 Apr 2026 06:31:55 +0200 Subject: [PATCH 07/26] Remove .idea files and update .gitignore --- .gitignore | 7 ++----- .idea/.gitignore | 10 ---------- .idea/.name | 1 - .idea/go.imports.xml | 10 ---------- .idea/gradle.xml | 17 ----------------- .idea/misc.xml | 10 ---------- .idea/vcs.xml | 6 ------ 7 files changed, 2 insertions(+), 59 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/.name delete mode 100644 .idea/go.imports.xml delete mode 100644 .idea/gradle.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore index 1fac4d5..8e3f7f9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,7 @@ build/ .kotlin ### IntelliJ IDEA ### -.idea/modules.xml -.idea/jarRepositories.xml -.idea/compiler.xml -.idea/libraries/ +.idea/ *.iws *.iml *.ipr @@ -40,4 +37,4 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store.idea/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 30cf57e..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index b26bca2..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -benchmark \ No newline at end of file diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml deleted file mode 100644 index 644cdf0..0000000 --- a/.idea/go.imports.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index b37fcda..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 7dcc043..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From fb626f8af5a86c7f21c2f9695ff6a04c02fc86eb Mon Sep 17 00:00:00 2001 From: fillg1 Date: Thu, 9 Apr 2026 06:31:55 +0200 Subject: [PATCH 08/26] Remove .idea files and update .gitignore --- .gitignore | 7 ++----- .idea/.gitignore | 10 ---------- .idea/.name | 1 - .idea/go.imports.xml | 10 ---------- .idea/gradle.xml | 17 ----------------- .idea/misc.xml | 10 ---------- .idea/vcs.xml | 6 ------ 7 files changed, 2 insertions(+), 59 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/.name delete mode 100644 .idea/go.imports.xml delete mode 100644 .idea/gradle.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore index 1fac4d5..8e3f7f9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,7 @@ build/ .kotlin ### IntelliJ IDEA ### -.idea/modules.xml -.idea/jarRepositories.xml -.idea/compiler.xml -.idea/libraries/ +.idea/ *.iws *.iml *.ipr @@ -40,4 +37,4 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store.idea/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 30cf57e..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index b26bca2..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -benchmark \ No newline at end of file diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml deleted file mode 100644 index 644cdf0..0000000 --- a/.idea/go.imports.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index b37fcda..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 7dcc043..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 0d861cbc1b8a6b243939a5d4ae512c91572e52c4 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Thu, 9 Apr 2026 06:35:28 +0200 Subject: [PATCH 09/26] Rename package com.dswiss to illgner.ch --- .../ch}/createzip/PostBoxZipFileWriter.java | 4 ++-- .../{com/dswiss => illgner/ch}/createzip/ZipResult.java | 2 +- .../ch}/encryption/IInputStreamEnhancer.java | 2 +- .../ch}/encryption/IOutputStreamEnhancer.java | 2 +- .../jobs/postboxbulk/service/PostBoxZipFileWriterTest.java | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) rename src/main/java/{com/dswiss => illgner/ch}/createzip/PostBoxZipFileWriter.java (98%) rename src/main/java/{com/dswiss => illgner/ch}/createzip/ZipResult.java (94%) rename src/main/java/{com/dswiss => illgner/ch}/encryption/IInputStreamEnhancer.java (94%) rename src/main/java/{com/dswiss => illgner/ch}/encryption/IOutputStreamEnhancer.java (96%) rename src/test/java/{com/dswiss => illgner/ch}/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java (99%) diff --git a/src/main/java/com/dswiss/createzip/PostBoxZipFileWriter.java b/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java similarity index 98% rename from src/main/java/com/dswiss/createzip/PostBoxZipFileWriter.java rename to src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java index 2cc4d52..621aede 100755 --- a/src/main/java/com/dswiss/createzip/PostBoxZipFileWriter.java +++ b/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java @@ -1,6 +1,6 @@ -package com.dswiss.createzip; +package illgner.ch.createzip; -import com.dswiss.encryption.IOutputStreamEnhancer; +import illgner.ch.encryption.IOutputStreamEnhancer; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.CountingOutputStream; import org.slf4j.Logger; diff --git a/src/main/java/com/dswiss/createzip/ZipResult.java b/src/main/java/illgner/ch/createzip/ZipResult.java similarity index 94% rename from src/main/java/com/dswiss/createzip/ZipResult.java rename to src/main/java/illgner/ch/createzip/ZipResult.java index 1e08df1..947bf33 100755 --- a/src/main/java/com/dswiss/createzip/ZipResult.java +++ b/src/main/java/illgner/ch/createzip/ZipResult.java @@ -1,4 +1,4 @@ -package com.dswiss.createzip; +package illgner.ch.createzip; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/dswiss/encryption/IInputStreamEnhancer.java b/src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java similarity index 94% rename from src/main/java/com/dswiss/encryption/IInputStreamEnhancer.java rename to src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java index 24d804f..94dd475 100755 --- a/src/main/java/com/dswiss/encryption/IInputStreamEnhancer.java +++ b/src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java @@ -1,4 +1,4 @@ -package com.dswiss.encryption; +package illgner.ch.encryption; import javax.crypto.NoSuchPaddingException; import java.io.IOException; diff --git a/src/main/java/com/dswiss/encryption/IOutputStreamEnhancer.java b/src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java similarity index 96% rename from src/main/java/com/dswiss/encryption/IOutputStreamEnhancer.java rename to src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java index c1882a8..d17ad88 100755 --- a/src/main/java/com/dswiss/encryption/IOutputStreamEnhancer.java +++ b/src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java @@ -1,4 +1,4 @@ -package com.dswiss.encryption; +package illgner.ch.encryption; import javax.crypto.NoSuchPaddingException; import java.io.IOException; diff --git a/src/test/java/com/dswiss/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java b/src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java similarity index 99% rename from src/test/java/com/dswiss/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java rename to src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java index 70da55e..837ddd2 100755 --- a/src/test/java/com/dswiss/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java +++ b/src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java @@ -1,7 +1,7 @@ -package com.dswiss.jobs.postboxbulk.service; +package illgner.ch.jobs.postboxbulk.service; -import com.dswiss.createzip.PostBoxZipFileWriter; -import com.dswiss.createzip.ZipResult; +import illgner.ch.createzip.PostBoxZipFileWriter; +import illgner.ch.createzip.ZipResult; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.RandomStringUtils; From 384617739abaf23830bfee6970db83953b048a1b Mon Sep 17 00:00:00 2001 From: fillg1 Date: Thu, 9 Apr 2026 06:35:28 +0200 Subject: [PATCH 10/26] Normalize package structure --- src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java b/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java index ef46a0a..621aede 100755 --- a/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java +++ b/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java @@ -1,4 +1,4 @@ -package illgner.ch.createzip; +package illgner.ch.createzip; import illgner.ch.encryption.IOutputStreamEnhancer; import org.apache.commons.io.IOUtils; From eea4baf065e12fa33ef0f6e1b043b1126ba3fc0e Mon Sep 17 00:00:00 2001 From: fillg1 Date: Sat, 11 Apr 2026 20:39:34 +0200 Subject: [PATCH 11/26] Refactor package structure and rename classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename encryption/ to stream/ (IOutputStreamEnhancer → OutputStreamEnhancer, IInputStreamEnhancer → InputStreamEnhancer) - Rename PostBoxZipFileWriter → SplitZipWriter - Simplify test package from illgner.ch.jobs.postboxbulk.service to illgner.ch.createzip --- ...ZipFileWriter.java => SplitZipWriter.java} | 10 +++++----- .../InputStreamEnhancer.java} | 6 ++---- .../OutputStreamEnhancer.java} | 9 ++------- .../SplitZipWriterTest.java} | 20 +++++++++---------- 4 files changed, 19 insertions(+), 26 deletions(-) rename src/main/java/illgner/ch/createzip/{PostBoxZipFileWriter.java => SplitZipWriter.java} (92%) rename src/main/java/illgner/ch/{encryption/IInputStreamEnhancer.java => stream/InputStreamEnhancer.java} (87%) mode change 100755 => 100644 rename src/main/java/illgner/ch/{encryption/IOutputStreamEnhancer.java => stream/OutputStreamEnhancer.java} (55%) mode change 100755 => 100644 rename src/test/java/illgner/ch/{jobs/postboxbulk/service/PostBoxZipFileWriterTest.java => createzip/SplitZipWriterTest.java} (95%) diff --git a/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java b/src/main/java/illgner/ch/createzip/SplitZipWriter.java similarity index 92% rename from src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java rename to src/main/java/illgner/ch/createzip/SplitZipWriter.java index 621aede..fc4afab 100755 --- a/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java +++ b/src/main/java/illgner/ch/createzip/SplitZipWriter.java @@ -1,6 +1,6 @@ package illgner.ch.createzip; -import illgner.ch.encryption.IOutputStreamEnhancer; +import illgner.ch.stream.OutputStreamEnhancer; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.CountingOutputStream; import org.slf4j.Logger; @@ -30,9 +30,9 @@ import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; -public class PostBoxZipFileWriter implements Closeable { +public class SplitZipWriter implements Closeable { - private static final Logger LOG = LoggerFactory.getLogger(PostBoxZipFileWriter.class); + private static final Logger LOG = LoggerFactory.getLogger(SplitZipWriter.class); private static final Set ZIP_FILE_PERMISSIONS = EnumSet.of(OWNER_READ, OWNER_WRITE, GROUP_READ, OTHERS_READ); @@ -41,7 +41,7 @@ public class PostBoxZipFileWriter implements Closeable { private final int chunkSize; private final int bufferSize; private final ZipResult zipResult; - IOutputStreamEnhancer outputStreamEnhancer; + OutputStreamEnhancer outputStreamEnhancer; private ZipOutputStream zipOutputStream; @@ -50,7 +50,7 @@ public class PostBoxZipFileWriter implements Closeable { private File actualZipFile; private CountingOutputStream countingOutputStream; - public PostBoxZipFileWriter(Path destinationDir, String baseName, int chunkSize, int bufferSize, IOutputStreamEnhancer outputStreamEnhancer) { + public SplitZipWriter(Path destinationDir, String baseName, int chunkSize, int bufferSize, OutputStreamEnhancer outputStreamEnhancer) { this.destinationDir = destinationDir; this.baseName = baseName; this.chunkSize = chunkSize; diff --git a/src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java b/src/main/java/illgner/ch/stream/InputStreamEnhancer.java old mode 100755 new mode 100644 similarity index 87% rename from src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java rename to src/main/java/illgner/ch/stream/InputStreamEnhancer.java index 94dd475..845b709 --- a/src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java +++ b/src/main/java/illgner/ch/stream/InputStreamEnhancer.java @@ -1,4 +1,4 @@ -package illgner.ch.encryption; +package illgner.ch.stream; import javax.crypto.NoSuchPaddingException; import java.io.IOException; @@ -8,8 +8,6 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; -public interface IInputStreamEnhancer { - +public interface InputStreamEnhancer { InputStream enhance(InputStream inputStream) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException, InvalidAlgorithmParameterException, NoSuchProviderException; - } diff --git a/src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java b/src/main/java/illgner/ch/stream/OutputStreamEnhancer.java old mode 100755 new mode 100644 similarity index 55% rename from src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java rename to src/main/java/illgner/ch/stream/OutputStreamEnhancer.java index d17ad88..7a25a42 --- a/src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java +++ b/src/main/java/illgner/ch/stream/OutputStreamEnhancer.java @@ -1,4 +1,4 @@ -package illgner.ch.encryption; +package illgner.ch.stream; import javax.crypto.NoSuchPaddingException; import java.io.IOException; @@ -8,11 +8,6 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; -public interface IOutputStreamEnhancer { - /** - * This method enhances the given input stream {@link OutputStream} wrapping with the additional functionality (in our case with encryption) - * @param outputStream the original stream {@link OutputStream} to be enhanced. - * @return A new {@link OutputStream} that wraps the functionality to input stream. - */ +public interface OutputStreamEnhancer { OutputStream enhance(OutputStream outputStream) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException, InvalidAlgorithmParameterException, NoSuchProviderException; } diff --git a/src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java b/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java similarity index 95% rename from src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java rename to src/test/java/illgner/ch/createzip/SplitZipWriterTest.java index 837ddd2..d391749 100755 --- a/src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java +++ b/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java @@ -1,6 +1,6 @@ -package illgner.ch.jobs.postboxbulk.service; +package illgner.ch.createzip; -import illgner.ch.createzip.PostBoxZipFileWriter; +import illgner.ch.createzip.SplitZipWriter; import illgner.ch.createzip.ZipResult; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; @@ -35,7 +35,7 @@ import static org.hamcrest.io.FileMatchers.aFileNamed; import static org.hamcrest.io.FileMatchers.anExistingFile; import static org.junit.jupiter.api.Assertions.assertTrue; -class PostBoxZipFileWriterTest { +class SplitZipWriterTest { private static final Path DESTINATION_DIR = Paths.get("./destination"); private static final int ONE_BYTE = 1024; @@ -63,7 +63,7 @@ class PostBoxZipFileWriterTest { ZipResult zipResult; String outputFileName = RandomStringUtils.randomAlphanumeric(10); String inputFileName = "singleFileSmallerThanChunkSizeToOneZip.txt"; - PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); try (postBoxZipFileWriter) { postBoxZipFileWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); @@ -92,7 +92,7 @@ class PostBoxZipFileWriterTest { ZipResult zipResult; String outputFileName = RandomStringUtils.randomAlphanumeric(10); String inputFileName = "singleFileGreaterThanChunkSizeToOneZip.txt"; - PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); try (postBoxZipFileWriter) { postBoxZipFileWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); @@ -125,7 +125,7 @@ class PostBoxZipFileWriterTest { String inputFileNameTwo = "multipleFileSmallerThanChunkSizeToSingleZip2.txt"; ZipResult zipResult; - PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); try (postBoxZipFileWriter) { postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); @@ -164,7 +164,7 @@ class PostBoxZipFileWriterTest { String inputFileNameTwo = "multipleFilesTogetherGreaterThanChunkSizeToSingleZip2.txt"; ZipResult zipResult; - PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); try (postBoxZipFileWriter) { postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); @@ -207,7 +207,7 @@ class PostBoxZipFileWriterTest { String[] originalFileNames = {"multipleBigFilesToMultipleZip1.txt", "multipleBigFilesToMultipleZip2.txt", "multipleBigFilesToMultipleZip3.txt"}; String outputFileName = RandomStringUtils.randomAlphanumeric(10); - PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, bigChuckSize, bigBufferSize, (OutputStream outputStream) -> outputStream); try (postBoxZipFileWriter) { postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), originalFileNames[0]); @@ -256,7 +256,7 @@ class PostBoxZipFileWriterTest { // first implementation crashed with an OutOfMemoryException while copying an 50MB Stream, // so we added a test for this - try (PostBoxZipFileWriter zipFileWriter = new PostBoxZipFileWriter( + try (SplitZipWriter zipFileWriter = new SplitZipWriter( DESTINATION_DIR, "testBigMulti", 100 * 1000 * 1000, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream)) { for (int i = 0; i < 10; i++) { byte[] content = RandomStringUtils.randomAlphanumeric(50 * 1000 * 1000).getBytes(); @@ -274,7 +274,7 @@ class PostBoxZipFileWriterTest { String outputFileName = RandomStringUtils.randomAlphanumeric(10); ZipResult zipResult; - try (PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter(DESTINATION_DIR, outputFileName, 1000, 2048, (OutputStream outputStream) -> outputStream)) { + try (SplitZipWriter postBoxZipFileWriter = new SplitZipWriter(DESTINATION_DIR, outputFileName, 1000, 2048, (OutputStream outputStream) -> outputStream)) { postBoxZipFileWriter.write(new ByteArrayInputStream(content1), "file1"); postBoxZipFileWriter.write(new ByteArrayInputStream(content2), "file2"); postBoxZipFileWriter.write(new ByteArrayInputStream(content3), "file3"); From d18c70a4e823347d6778cfcc706468c314c6ef64 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Sat, 11 Apr 2026 20:39:34 +0200 Subject: [PATCH 12/26] Refactor package structure and rename classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename encryption/ to stream/ (IOutputStreamEnhancer → OutputStreamEnhancer, IInputStreamEnhancer → InputStreamEnhancer) - Rename PostBoxZipFileWriter → SplitZipWriter - Simplify test package from illgner.ch.jobs.postboxbulk.service to illgner.ch.createzip --- ...ZipFileWriter.java => SplitZipWriter.java} | 10 +++++----- .../InputStreamEnhancer.java} | 6 ++---- .../OutputStreamEnhancer.java} | 9 ++------- .../SplitZipWriterTest.java} | 20 +++++++++---------- 4 files changed, 19 insertions(+), 26 deletions(-) rename src/main/java/illgner/ch/createzip/{PostBoxZipFileWriter.java => SplitZipWriter.java} (92%) rename src/main/java/illgner/ch/{encryption/IInputStreamEnhancer.java => stream/InputStreamEnhancer.java} (87%) mode change 100755 => 100644 rename src/main/java/illgner/ch/{encryption/IOutputStreamEnhancer.java => stream/OutputStreamEnhancer.java} (55%) mode change 100755 => 100644 rename src/test/java/illgner/ch/{jobs/postboxbulk/service/PostBoxZipFileWriterTest.java => createzip/SplitZipWriterTest.java} (95%) diff --git a/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java b/src/main/java/illgner/ch/createzip/SplitZipWriter.java similarity index 92% rename from src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java rename to src/main/java/illgner/ch/createzip/SplitZipWriter.java index 621aede..fc4afab 100755 --- a/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java +++ b/src/main/java/illgner/ch/createzip/SplitZipWriter.java @@ -1,6 +1,6 @@ package illgner.ch.createzip; -import illgner.ch.encryption.IOutputStreamEnhancer; +import illgner.ch.stream.OutputStreamEnhancer; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.CountingOutputStream; import org.slf4j.Logger; @@ -30,9 +30,9 @@ import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; -public class PostBoxZipFileWriter implements Closeable { +public class SplitZipWriter implements Closeable { - private static final Logger LOG = LoggerFactory.getLogger(PostBoxZipFileWriter.class); + private static final Logger LOG = LoggerFactory.getLogger(SplitZipWriter.class); private static final Set ZIP_FILE_PERMISSIONS = EnumSet.of(OWNER_READ, OWNER_WRITE, GROUP_READ, OTHERS_READ); @@ -41,7 +41,7 @@ public class PostBoxZipFileWriter implements Closeable { private final int chunkSize; private final int bufferSize; private final ZipResult zipResult; - IOutputStreamEnhancer outputStreamEnhancer; + OutputStreamEnhancer outputStreamEnhancer; private ZipOutputStream zipOutputStream; @@ -50,7 +50,7 @@ public class PostBoxZipFileWriter implements Closeable { private File actualZipFile; private CountingOutputStream countingOutputStream; - public PostBoxZipFileWriter(Path destinationDir, String baseName, int chunkSize, int bufferSize, IOutputStreamEnhancer outputStreamEnhancer) { + public SplitZipWriter(Path destinationDir, String baseName, int chunkSize, int bufferSize, OutputStreamEnhancer outputStreamEnhancer) { this.destinationDir = destinationDir; this.baseName = baseName; this.chunkSize = chunkSize; diff --git a/src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java b/src/main/java/illgner/ch/stream/InputStreamEnhancer.java old mode 100755 new mode 100644 similarity index 87% rename from src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java rename to src/main/java/illgner/ch/stream/InputStreamEnhancer.java index 94dd475..845b709 --- a/src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java +++ b/src/main/java/illgner/ch/stream/InputStreamEnhancer.java @@ -1,4 +1,4 @@ -package illgner.ch.encryption; +package illgner.ch.stream; import javax.crypto.NoSuchPaddingException; import java.io.IOException; @@ -8,8 +8,6 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; -public interface IInputStreamEnhancer { - +public interface InputStreamEnhancer { InputStream enhance(InputStream inputStream) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException, InvalidAlgorithmParameterException, NoSuchProviderException; - } diff --git a/src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java b/src/main/java/illgner/ch/stream/OutputStreamEnhancer.java old mode 100755 new mode 100644 similarity index 55% rename from src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java rename to src/main/java/illgner/ch/stream/OutputStreamEnhancer.java index d17ad88..7a25a42 --- a/src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java +++ b/src/main/java/illgner/ch/stream/OutputStreamEnhancer.java @@ -1,4 +1,4 @@ -package illgner.ch.encryption; +package illgner.ch.stream; import javax.crypto.NoSuchPaddingException; import java.io.IOException; @@ -8,11 +8,6 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; -public interface IOutputStreamEnhancer { - /** - * This method enhances the given input stream {@link OutputStream} wrapping with the additional functionality (in our case with encryption) - * @param outputStream the original stream {@link OutputStream} to be enhanced. - * @return A new {@link OutputStream} that wraps the functionality to input stream. - */ +public interface OutputStreamEnhancer { OutputStream enhance(OutputStream outputStream) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException, InvalidAlgorithmParameterException, NoSuchProviderException; } diff --git a/src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java b/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java similarity index 95% rename from src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java rename to src/test/java/illgner/ch/createzip/SplitZipWriterTest.java index 837ddd2..d391749 100755 --- a/src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java +++ b/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java @@ -1,6 +1,6 @@ -package illgner.ch.jobs.postboxbulk.service; +package illgner.ch.createzip; -import illgner.ch.createzip.PostBoxZipFileWriter; +import illgner.ch.createzip.SplitZipWriter; import illgner.ch.createzip.ZipResult; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; @@ -35,7 +35,7 @@ import static org.hamcrest.io.FileMatchers.aFileNamed; import static org.hamcrest.io.FileMatchers.anExistingFile; import static org.junit.jupiter.api.Assertions.assertTrue; -class PostBoxZipFileWriterTest { +class SplitZipWriterTest { private static final Path DESTINATION_DIR = Paths.get("./destination"); private static final int ONE_BYTE = 1024; @@ -63,7 +63,7 @@ class PostBoxZipFileWriterTest { ZipResult zipResult; String outputFileName = RandomStringUtils.randomAlphanumeric(10); String inputFileName = "singleFileSmallerThanChunkSizeToOneZip.txt"; - PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); try (postBoxZipFileWriter) { postBoxZipFileWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); @@ -92,7 +92,7 @@ class PostBoxZipFileWriterTest { ZipResult zipResult; String outputFileName = RandomStringUtils.randomAlphanumeric(10); String inputFileName = "singleFileGreaterThanChunkSizeToOneZip.txt"; - PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); try (postBoxZipFileWriter) { postBoxZipFileWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); @@ -125,7 +125,7 @@ class PostBoxZipFileWriterTest { String inputFileNameTwo = "multipleFileSmallerThanChunkSizeToSingleZip2.txt"; ZipResult zipResult; - PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); try (postBoxZipFileWriter) { postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); @@ -164,7 +164,7 @@ class PostBoxZipFileWriterTest { String inputFileNameTwo = "multipleFilesTogetherGreaterThanChunkSizeToSingleZip2.txt"; ZipResult zipResult; - PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); try (postBoxZipFileWriter) { postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); @@ -207,7 +207,7 @@ class PostBoxZipFileWriterTest { String[] originalFileNames = {"multipleBigFilesToMultipleZip1.txt", "multipleBigFilesToMultipleZip2.txt", "multipleBigFilesToMultipleZip3.txt"}; String outputFileName = RandomStringUtils.randomAlphanumeric(10); - PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, bigChuckSize, bigBufferSize, (OutputStream outputStream) -> outputStream); try (postBoxZipFileWriter) { postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), originalFileNames[0]); @@ -256,7 +256,7 @@ class PostBoxZipFileWriterTest { // first implementation crashed with an OutOfMemoryException while copying an 50MB Stream, // so we added a test for this - try (PostBoxZipFileWriter zipFileWriter = new PostBoxZipFileWriter( + try (SplitZipWriter zipFileWriter = new SplitZipWriter( DESTINATION_DIR, "testBigMulti", 100 * 1000 * 1000, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream)) { for (int i = 0; i < 10; i++) { byte[] content = RandomStringUtils.randomAlphanumeric(50 * 1000 * 1000).getBytes(); @@ -274,7 +274,7 @@ class PostBoxZipFileWriterTest { String outputFileName = RandomStringUtils.randomAlphanumeric(10); ZipResult zipResult; - try (PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter(DESTINATION_DIR, outputFileName, 1000, 2048, (OutputStream outputStream) -> outputStream)) { + try (SplitZipWriter postBoxZipFileWriter = new SplitZipWriter(DESTINATION_DIR, outputFileName, 1000, 2048, (OutputStream outputStream) -> outputStream)) { postBoxZipFileWriter.write(new ByteArrayInputStream(content1), "file1"); postBoxZipFileWriter.write(new ByteArrayInputStream(content2), "file2"); postBoxZipFileWriter.write(new ByteArrayInputStream(content3), "file3"); From bf0e53267bf371ddb5d3e21cbd480188ed574090 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Sat, 11 Apr 2026 20:55:07 +0200 Subject: [PATCH 13/26] removed unneeded method --- .../illgner/ch/createzip/SplitZipWriter.java | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/main/java/illgner/ch/createzip/SplitZipWriter.java b/src/main/java/illgner/ch/createzip/SplitZipWriter.java index fc4afab..d78456c 100755 --- a/src/main/java/illgner/ch/createzip/SplitZipWriter.java +++ b/src/main/java/illgner/ch/createzip/SplitZipWriter.java @@ -107,26 +107,6 @@ public class SplitZipWriter implements Closeable { addZipToResult(); } - public void writeReadMe() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, - NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { - - if (zipOutputStream != null) { - LOG.info("Skip adding Readme"); - return; - } - - try (InputStream readmeStream = this.getClass().getClassLoader().getResourceAsStream("Readme.txt")) { - if (readmeStream == null) { - throw new IOException("Cannot open Readme"); - } - - LOG.info("Writing Readme"); - write(readmeStream, "Readme.txt"); - } catch (IOException e) { - throw new IOException("Could not write Readme", e); - } - } - public ZipResult getZipResult() { return this.zipResult; } From 54a305a2d18a4b0eee20d5917ff63953d5e66de1 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Sat, 11 Apr 2026 20:55:07 +0200 Subject: [PATCH 14/26] removed unneeded method --- .../illgner/ch/createzip/SplitZipWriter.java | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/main/java/illgner/ch/createzip/SplitZipWriter.java b/src/main/java/illgner/ch/createzip/SplitZipWriter.java index fc4afab..d78456c 100755 --- a/src/main/java/illgner/ch/createzip/SplitZipWriter.java +++ b/src/main/java/illgner/ch/createzip/SplitZipWriter.java @@ -107,26 +107,6 @@ public class SplitZipWriter implements Closeable { addZipToResult(); } - public void writeReadMe() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, - NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { - - if (zipOutputStream != null) { - LOG.info("Skip adding Readme"); - return; - } - - try (InputStream readmeStream = this.getClass().getClassLoader().getResourceAsStream("Readme.txt")) { - if (readmeStream == null) { - throw new IOException("Cannot open Readme"); - } - - LOG.info("Writing Readme"); - write(readmeStream, "Readme.txt"); - } catch (IOException e) { - throw new IOException("Could not write Readme", e); - } - } - public ZipResult getZipResult() { return this.zipResult; } From e65ada39e78e03c8618bb3f905ee711e1505623d Mon Sep 17 00:00:00 2001 From: fillg1 Date: Sat, 11 Apr 2026 21:33:04 +0200 Subject: [PATCH 15/26] refactor: cleanup project configuration and fix naming inconsistencies --- build.gradle | 10 ++-- gradle.properties | 1 - .../illgner/ch/createzip/SplitZipWriter.java | 2 +- .../ch/createzip/SplitZipWriterTest.java | 58 +++++++++---------- 4 files changed, 35 insertions(+), 36 deletions(-) delete mode 100755 gradle.properties diff --git a/build.gradle b/build.gradle index df545ab..6e48de2 100755 --- a/build.gradle +++ b/build.gradle @@ -10,14 +10,14 @@ repositories { } dependencies { - implementation group: 'commons-io', name: 'commons-io', version: '2.21.0' - implementation "org.apache.commons:commons-lang3:3.20.0" - implementation "org.slf4j:jul-to-slf4j:$sl4j_version" - implementation "org.slf4j:log4j-over-slf4j:$sl4j_version" + implementation 'commons-io:commons-io:2.21.0' + implementation 'org.apache.commons:commons-lang3:3.20.0' + implementation 'org.slf4j:jul-to-slf4j:1.7.35' + implementation 'org.slf4j:log4j-over-slf4j:1.7.35' testImplementation platform('org.junit:junit-bom:6.0.0') testImplementation 'org.junit.jupiter:junit-jupiter' - testImplementation group: 'org.hamcrest', name: 'hamcrest-library', version: '3.0' + testImplementation 'org.hamcrest:hamcrest-library:3.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/gradle.properties b/gradle.properties deleted file mode 100755 index 28f4b38..0000000 --- a/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -sl4j_version=1.7.35 diff --git a/src/main/java/illgner/ch/createzip/SplitZipWriter.java b/src/main/java/illgner/ch/createzip/SplitZipWriter.java index d78456c..fe6349d 100755 --- a/src/main/java/illgner/ch/createzip/SplitZipWriter.java +++ b/src/main/java/illgner/ch/createzip/SplitZipWriter.java @@ -41,7 +41,7 @@ public class SplitZipWriter implements Closeable { private final int chunkSize; private final int bufferSize; private final ZipResult zipResult; - OutputStreamEnhancer outputStreamEnhancer; + private OutputStreamEnhancer outputStreamEnhancer; private ZipOutputStream zipOutputStream; diff --git a/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java b/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java index d391749..4935eec 100755 --- a/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java +++ b/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java @@ -63,13 +63,13 @@ class SplitZipWriterTest { ZipResult zipResult; String outputFileName = RandomStringUtils.randomAlphanumeric(10); String inputFileName = "singleFileSmallerThanChunkSizeToOneZip.txt"; - SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( + SplitZipWriter splitZipWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); - try (postBoxZipFileWriter) { - postBoxZipFileWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); + try (splitZipWriter) { + splitZipWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); } String expectedZippedFileName = outputFileName + "_01.zip"; - zipResult = postBoxZipFileWriter.getZipResult(); + zipResult = splitZipWriter.getZipResult(); File createdZipFile = DESTINATION_DIR.resolve(expectedZippedFileName).toFile(); assertThat(zipResult.getFilenames(), hasSize(1)); @@ -92,13 +92,13 @@ class SplitZipWriterTest { ZipResult zipResult; String outputFileName = RandomStringUtils.randomAlphanumeric(10); String inputFileName = "singleFileGreaterThanChunkSizeToOneZip.txt"; - SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( + SplitZipWriter splitZipWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); - try (postBoxZipFileWriter) { - postBoxZipFileWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); + try (splitZipWriter) { + splitZipWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); } String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; - zipResult = postBoxZipFileWriter.getZipResult(); + zipResult = splitZipWriter.getZipResult(); assertThat(zipResult.getFilenames(), hasSize(1)); assertThat(zipResult.getFileLengths(), hasSize(1)); assertTrue(zipResult.getFilenames().contains(expectedZippedFileNamePartOne)); @@ -125,14 +125,14 @@ class SplitZipWriterTest { String inputFileNameTwo = "multipleFileSmallerThanChunkSizeToSingleZip2.txt"; ZipResult zipResult; - SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( + SplitZipWriter splitZipWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); - try (postBoxZipFileWriter) { - postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); - postBoxZipFileWriter.write(new ByteArrayInputStream(secondFile), inputFileNameTwo); + try (splitZipWriter) { + splitZipWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); + splitZipWriter.write(new ByteArrayInputStream(secondFile), inputFileNameTwo); } - zipResult = postBoxZipFileWriter.getZipResult(); + zipResult = splitZipWriter.getZipResult(); String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; File createdZip = DESTINATION_DIR.resolve(expectedZippedFileNamePartOne).toFile(); @@ -164,13 +164,13 @@ class SplitZipWriterTest { String inputFileNameTwo = "multipleFilesTogetherGreaterThanChunkSizeToSingleZip2.txt"; ZipResult zipResult; - SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( + SplitZipWriter splitZipWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); - try (postBoxZipFileWriter) { - postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); - postBoxZipFileWriter.write(new ByteArrayInputStream(secondFile), inputFileNameTwo); + try (splitZipWriter) { + splitZipWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); + splitZipWriter.write(new ByteArrayInputStream(secondFile), inputFileNameTwo); } finally { - zipResult = postBoxZipFileWriter.getZipResult(); + zipResult = splitZipWriter.getZipResult(); } String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; @@ -207,14 +207,14 @@ class SplitZipWriterTest { String[] originalFileNames = {"multipleBigFilesToMultipleZip1.txt", "multipleBigFilesToMultipleZip2.txt", "multipleBigFilesToMultipleZip3.txt"}; String outputFileName = RandomStringUtils.randomAlphanumeric(10); - SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( + SplitZipWriter splitZipWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, bigChuckSize, bigBufferSize, (OutputStream outputStream) -> outputStream); - try (postBoxZipFileWriter) { - postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), originalFileNames[0]); - postBoxZipFileWriter.write(new ByteArrayInputStream(secondFile), originalFileNames[1]); - postBoxZipFileWriter.write(new ByteArrayInputStream(thirdFile), originalFileNames[2]); + try (splitZipWriter) { + splitZipWriter.write(new ByteArrayInputStream(firstFile), originalFileNames[0]); + splitZipWriter.write(new ByteArrayInputStream(secondFile), originalFileNames[1]); + splitZipWriter.write(new ByteArrayInputStream(thirdFile), originalFileNames[2]); } finally { - zipResult = postBoxZipFileWriter.getZipResult(); + zipResult = splitZipWriter.getZipResult(); } String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; @@ -274,11 +274,11 @@ class SplitZipWriterTest { String outputFileName = RandomStringUtils.randomAlphanumeric(10); ZipResult zipResult; - try (SplitZipWriter postBoxZipFileWriter = new SplitZipWriter(DESTINATION_DIR, outputFileName, 1000, 2048, (OutputStream outputStream) -> outputStream)) { - postBoxZipFileWriter.write(new ByteArrayInputStream(content1), "file1"); - postBoxZipFileWriter.write(new ByteArrayInputStream(content2), "file2"); - postBoxZipFileWriter.write(new ByteArrayInputStream(content3), "file3"); - zipResult = postBoxZipFileWriter.getZipResult(); + try (SplitZipWriter splitZipWriter = new SplitZipWriter(DESTINATION_DIR, outputFileName, 1000, 2048, (OutputStream outputStream) -> outputStream)) { + splitZipWriter.write(new ByteArrayInputStream(content1), "file1"); + splitZipWriter.write(new ByteArrayInputStream(content2), "file2"); + splitZipWriter.write(new ByteArrayInputStream(content3), "file3"); + zipResult = splitZipWriter.getZipResult(); } assertThat("compressible files only containing zeroes should be compressed into one file", zipResult.getFileLengths().size(), is(1)); } From fb056bb366da7916a4d2d354c26c49c33d9f826a Mon Sep 17 00:00:00 2001 From: fillg1 Date: Sat, 11 Apr 2026 21:33:04 +0200 Subject: [PATCH 16/26] refactor: cleanup project configuration and fix naming inconsistencies --- build.gradle | 10 ++-- gradle.properties | 1 - .../illgner/ch/createzip/SplitZipWriter.java | 2 +- .../ch/createzip/SplitZipWriterTest.java | 58 +++++++++---------- 4 files changed, 35 insertions(+), 36 deletions(-) delete mode 100755 gradle.properties diff --git a/build.gradle b/build.gradle index df545ab..6e48de2 100755 --- a/build.gradle +++ b/build.gradle @@ -10,14 +10,14 @@ repositories { } dependencies { - implementation group: 'commons-io', name: 'commons-io', version: '2.21.0' - implementation "org.apache.commons:commons-lang3:3.20.0" - implementation "org.slf4j:jul-to-slf4j:$sl4j_version" - implementation "org.slf4j:log4j-over-slf4j:$sl4j_version" + implementation 'commons-io:commons-io:2.21.0' + implementation 'org.apache.commons:commons-lang3:3.20.0' + implementation 'org.slf4j:jul-to-slf4j:1.7.35' + implementation 'org.slf4j:log4j-over-slf4j:1.7.35' testImplementation platform('org.junit:junit-bom:6.0.0') testImplementation 'org.junit.jupiter:junit-jupiter' - testImplementation group: 'org.hamcrest', name: 'hamcrest-library', version: '3.0' + testImplementation 'org.hamcrest:hamcrest-library:3.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/gradle.properties b/gradle.properties deleted file mode 100755 index 28f4b38..0000000 --- a/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -sl4j_version=1.7.35 diff --git a/src/main/java/illgner/ch/createzip/SplitZipWriter.java b/src/main/java/illgner/ch/createzip/SplitZipWriter.java index d78456c..fe6349d 100755 --- a/src/main/java/illgner/ch/createzip/SplitZipWriter.java +++ b/src/main/java/illgner/ch/createzip/SplitZipWriter.java @@ -41,7 +41,7 @@ public class SplitZipWriter implements Closeable { private final int chunkSize; private final int bufferSize; private final ZipResult zipResult; - OutputStreamEnhancer outputStreamEnhancer; + private OutputStreamEnhancer outputStreamEnhancer; private ZipOutputStream zipOutputStream; diff --git a/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java b/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java index d391749..4935eec 100755 --- a/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java +++ b/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java @@ -63,13 +63,13 @@ class SplitZipWriterTest { ZipResult zipResult; String outputFileName = RandomStringUtils.randomAlphanumeric(10); String inputFileName = "singleFileSmallerThanChunkSizeToOneZip.txt"; - SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( + SplitZipWriter splitZipWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); - try (postBoxZipFileWriter) { - postBoxZipFileWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); + try (splitZipWriter) { + splitZipWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); } String expectedZippedFileName = outputFileName + "_01.zip"; - zipResult = postBoxZipFileWriter.getZipResult(); + zipResult = splitZipWriter.getZipResult(); File createdZipFile = DESTINATION_DIR.resolve(expectedZippedFileName).toFile(); assertThat(zipResult.getFilenames(), hasSize(1)); @@ -92,13 +92,13 @@ class SplitZipWriterTest { ZipResult zipResult; String outputFileName = RandomStringUtils.randomAlphanumeric(10); String inputFileName = "singleFileGreaterThanChunkSizeToOneZip.txt"; - SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( + SplitZipWriter splitZipWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); - try (postBoxZipFileWriter) { - postBoxZipFileWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); + try (splitZipWriter) { + splitZipWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); } String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; - zipResult = postBoxZipFileWriter.getZipResult(); + zipResult = splitZipWriter.getZipResult(); assertThat(zipResult.getFilenames(), hasSize(1)); assertThat(zipResult.getFileLengths(), hasSize(1)); assertTrue(zipResult.getFilenames().contains(expectedZippedFileNamePartOne)); @@ -125,14 +125,14 @@ class SplitZipWriterTest { String inputFileNameTwo = "multipleFileSmallerThanChunkSizeToSingleZip2.txt"; ZipResult zipResult; - SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( + SplitZipWriter splitZipWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); - try (postBoxZipFileWriter) { - postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); - postBoxZipFileWriter.write(new ByteArrayInputStream(secondFile), inputFileNameTwo); + try (splitZipWriter) { + splitZipWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); + splitZipWriter.write(new ByteArrayInputStream(secondFile), inputFileNameTwo); } - zipResult = postBoxZipFileWriter.getZipResult(); + zipResult = splitZipWriter.getZipResult(); String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; File createdZip = DESTINATION_DIR.resolve(expectedZippedFileNamePartOne).toFile(); @@ -164,13 +164,13 @@ class SplitZipWriterTest { String inputFileNameTwo = "multipleFilesTogetherGreaterThanChunkSizeToSingleZip2.txt"; ZipResult zipResult; - SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( + SplitZipWriter splitZipWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); - try (postBoxZipFileWriter) { - postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); - postBoxZipFileWriter.write(new ByteArrayInputStream(secondFile), inputFileNameTwo); + try (splitZipWriter) { + splitZipWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); + splitZipWriter.write(new ByteArrayInputStream(secondFile), inputFileNameTwo); } finally { - zipResult = postBoxZipFileWriter.getZipResult(); + zipResult = splitZipWriter.getZipResult(); } String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; @@ -207,14 +207,14 @@ class SplitZipWriterTest { String[] originalFileNames = {"multipleBigFilesToMultipleZip1.txt", "multipleBigFilesToMultipleZip2.txt", "multipleBigFilesToMultipleZip3.txt"}; String outputFileName = RandomStringUtils.randomAlphanumeric(10); - SplitZipWriter postBoxZipFileWriter = new SplitZipWriter( + SplitZipWriter splitZipWriter = new SplitZipWriter( DESTINATION_DIR, outputFileName, bigChuckSize, bigBufferSize, (OutputStream outputStream) -> outputStream); - try (postBoxZipFileWriter) { - postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), originalFileNames[0]); - postBoxZipFileWriter.write(new ByteArrayInputStream(secondFile), originalFileNames[1]); - postBoxZipFileWriter.write(new ByteArrayInputStream(thirdFile), originalFileNames[2]); + try (splitZipWriter) { + splitZipWriter.write(new ByteArrayInputStream(firstFile), originalFileNames[0]); + splitZipWriter.write(new ByteArrayInputStream(secondFile), originalFileNames[1]); + splitZipWriter.write(new ByteArrayInputStream(thirdFile), originalFileNames[2]); } finally { - zipResult = postBoxZipFileWriter.getZipResult(); + zipResult = splitZipWriter.getZipResult(); } String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; @@ -274,11 +274,11 @@ class SplitZipWriterTest { String outputFileName = RandomStringUtils.randomAlphanumeric(10); ZipResult zipResult; - try (SplitZipWriter postBoxZipFileWriter = new SplitZipWriter(DESTINATION_DIR, outputFileName, 1000, 2048, (OutputStream outputStream) -> outputStream)) { - postBoxZipFileWriter.write(new ByteArrayInputStream(content1), "file1"); - postBoxZipFileWriter.write(new ByteArrayInputStream(content2), "file2"); - postBoxZipFileWriter.write(new ByteArrayInputStream(content3), "file3"); - zipResult = postBoxZipFileWriter.getZipResult(); + try (SplitZipWriter splitZipWriter = new SplitZipWriter(DESTINATION_DIR, outputFileName, 1000, 2048, (OutputStream outputStream) -> outputStream)) { + splitZipWriter.write(new ByteArrayInputStream(content1), "file1"); + splitZipWriter.write(new ByteArrayInputStream(content2), "file2"); + splitZipWriter.write(new ByteArrayInputStream(content3), "file3"); + zipResult = splitZipWriter.getZipResult(); } assertThat("compressible files only containing zeroes should be compressed into one file", zipResult.getFileLengths().size(), is(1)); } From 4caed67c998ade9ddc7965a31754f55dc995e2a4 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Fri, 22 May 2026 21:47:19 +0200 Subject: [PATCH 17/26] remove comment --- src/main/java/illgner/ch/createzip/SplitZipWriter.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/illgner/ch/createzip/SplitZipWriter.java b/src/main/java/illgner/ch/createzip/SplitZipWriter.java index fe6349d..c926962 100755 --- a/src/main/java/illgner/ch/createzip/SplitZipWriter.java +++ b/src/main/java/illgner/ch/createzip/SplitZipWriter.java @@ -85,8 +85,10 @@ public class SplitZipWriter implements Closeable { switchOutputFile(); } LOG.info("Writing file {} to zip", path); + long entryStartBytes = countingOutputStream.getCount(); zipOutputStream.putNextEntry(new ZipEntry(path)); - bytesWritten += IOUtils.copyLarge(inputStream, zipOutputStream); + IOUtils.copyLarge(inputStream, zipOutputStream); + bytesWritten += countingOutputStream.getCount() - entryStartBytes; zipOutputStream.closeEntry(); } From dfba3eab33570778ca5f3ab2ad67d10308913eb5 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Fri, 22 May 2026 21:51:35 +0200 Subject: [PATCH 18/26] remove comment --- src/main/java/illgner/ch/createzip/SplitZipWriter.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/illgner/ch/createzip/SplitZipWriter.java b/src/main/java/illgner/ch/createzip/SplitZipWriter.java index c926962..fe6349d 100755 --- a/src/main/java/illgner/ch/createzip/SplitZipWriter.java +++ b/src/main/java/illgner/ch/createzip/SplitZipWriter.java @@ -85,10 +85,8 @@ public class SplitZipWriter implements Closeable { switchOutputFile(); } LOG.info("Writing file {} to zip", path); - long entryStartBytes = countingOutputStream.getCount(); zipOutputStream.putNextEntry(new ZipEntry(path)); - IOUtils.copyLarge(inputStream, zipOutputStream); - bytesWritten += countingOutputStream.getCount() - entryStartBytes; + bytesWritten += IOUtils.copyLarge(inputStream, zipOutputStream); zipOutputStream.closeEntry(); } From 04ccc5f83accaafe9cc4c52eecffa32be63dc144 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Wed, 27 May 2026 21:25:09 +0200 Subject: [PATCH 19/26] disabled long running test --- src/test/java/illgner/ch/createzip/SplitZipWriterTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java b/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java index 4935eec..74e0798 100755 --- a/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java +++ b/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java @@ -7,6 +7,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import javax.crypto.NoSuchPaddingException; @@ -252,6 +253,7 @@ class SplitZipWriterTest { } @Test + @Disabled void createBigZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { // first implementation crashed with an OutOfMemoryException while copying an 50MB Stream, // so we added a test for this From f662275e203fc1b99abd0f9ba694cbe0bde5d013 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Wed, 27 May 2026 21:25:09 +0200 Subject: [PATCH 20/26] disabled long running test --- src/test/java/illgner/ch/createzip/SplitZipWriterTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java b/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java index 4935eec..74e0798 100755 --- a/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java +++ b/src/test/java/illgner/ch/createzip/SplitZipWriterTest.java @@ -7,6 +7,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import javax.crypto.NoSuchPaddingException; @@ -252,6 +253,7 @@ class SplitZipWriterTest { } @Test + @Disabled void createBigZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { // first implementation crashed with an OutOfMemoryException while copying an 50MB Stream, // so we added a test for this From 101070a0d3cfb3ff0db82a9b4aea89274dbc5d31 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Sun, 12 Jul 2026 15:45:44 +0200 Subject: [PATCH 21/26] docs(spec): Add Conway's Game of Life design specification for Canvas Edition --- .../specs/2026-07-12-gameoflife-design.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-12-gameoflife-design.md diff --git a/docs/superpowers/specs/2026-07-12-gameoflife-design.md b/docs/superpowers/specs/2026-07-12-gameoflife-design.md new file mode 100644 index 0000000..1d79682 --- /dev/null +++ b/docs/superpowers/specs/2026-07-12-gameoflife-design.md @@ -0,0 +1,38 @@ +# Conway's Game of Life Application Specification - Canvas Edition +## Date: 2026-07-12 + +## 🚀 Goal +To create a single-page web application capable of running Conway's Game of Life with specific features, including: start/stop functionality, state editing via mouse interaction, and initialization with the Glider Cannon pattern. The simulation must utilize a fixed 2D array for state management and be rendered efficiently using HTML Canvas to support large board sizes (starting at 100x100). + +## 🖼️ High-Level Architecture +The application is structured as a Single Page Application (SPA) contained entirely within `index.html`. The three core components interact to manage the simulation loop and output rendering: + +* **GameController:** The central state machine. It manages the internal grid representation (`grid[W][H]`), handles game logic execution (calculating next generations), maintains the current running status (running/paused/stopped), and provides methods for external state mutation. It is responsible for upholding the rules of Conway's Game of Life (under/overpopulation, death). +* **CanvasRenderer:** Responsible solely for visual output. It receives the current grid array from `GameController` periodically and draws the necessary pixels onto the HTML Canvas element. This component also manages aspect ratio and scaling related to the fixed cell size. +* **UIHandler:** The interface layer (Event Listener). This module listens for user inputs, such as button clicks (Start/Stop) and mouse events (editing state via clicking/dragging the canvas). It translates screen coordinates into array indices while accounting for viewport offset (scrolling), thus interfacing between the human user and the `GameController`. + +## 🔄 Data Flow and Implementation Details +### Initialization +1. The application starts by initializing the internal $W \times H$ grid array (e.g., $100 \times 100$). +2. This initial state is populated with a known pattern: the **Glider Cannon**, placed at specific pre-determined Gosper coordinates within the grid space. + +### Simulation Loop (The Tick) +The simulation runs asynchronously using `requestAnimationFrame` for smooth rendering and timing accuracy, rather than a simple fixed interval timer. +1. **State Calculation:** In each cycle, before modifying the main state, all neighbors are checked for every live cell in the current grid (`grid`). A temporary 'next\_grid' is used during calculation to ensure all neighbor checks rely *only* on the prior generation's state, preventing race conditions. +2. **State Transition:** The `GameController` applies Conway's Rules. Once the next state for every cell is determined and stored in `next_grid`, the main state array (`grid`) is atomically swapped with `next_grid`. +3. **Rendering:** After the swap, the new state is delivered to `CanvasRenderer`, which draws the board. + +### User Editing (Mutation) +When a user interacts with the UI (e.g., clicks on a cell they wish to toggle): +1. The `UIHandler` captures the screen position and determines the corresponding 2D array index. +2. It commands the `GameController` to *force* a change on that specific cell in the current grid, overriding the simulation rules for that moment. This action modifies the state locally but does not necessarily reset or trigger the entire generation sequence unless explicitly requested by a separate 'reset' action. + +## 📏 Constraints and Assumptions +* **Initial Dimensions:** The initial working dimensions are fixed at $100 \times 100$. Future growth beyond this (e.g., to $200\times200$) requires only scaling the rendering logic, not a fundamental change in data structure. +* **Scrolling Support:** A mechanism for handling views larger than the container size is implemented by mapping client-side scroll offsets back into virtual grid coordinates during editing and drawing stages. +* **Concurrency Control:** The critical requirement of preventing race conditions during next-state calculation is met by using a temporary buffer (`next_grid`) before committing the state change, ensuring atomicity between generations. + +## 🔎 Spec Self-Review Complete +The design is self-consistent; all components are related appropriately (GameController -> CanvasRenderer via shared grid state). The requirements for fixed array use and Glider Cannon initialization are met in the architecture description. No ambiguous concepts were found requiring further clarification. + +**Proposed Next Step:** Create the implementation plan using `writing-plans`. \ No newline at end of file From 55121af472002c95ee3e1334e19401dd6c2d6e87 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Sun, 12 Jul 2026 15:45:44 +0200 Subject: [PATCH 22/26] docs(spec): Add Conway's Game of Life design specification for Canvas Edition --- .../specs/2026-07-12-gameoflife-design.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-12-gameoflife-design.md diff --git a/docs/superpowers/specs/2026-07-12-gameoflife-design.md b/docs/superpowers/specs/2026-07-12-gameoflife-design.md new file mode 100644 index 0000000..1d79682 --- /dev/null +++ b/docs/superpowers/specs/2026-07-12-gameoflife-design.md @@ -0,0 +1,38 @@ +# Conway's Game of Life Application Specification - Canvas Edition +## Date: 2026-07-12 + +## 🚀 Goal +To create a single-page web application capable of running Conway's Game of Life with specific features, including: start/stop functionality, state editing via mouse interaction, and initialization with the Glider Cannon pattern. The simulation must utilize a fixed 2D array for state management and be rendered efficiently using HTML Canvas to support large board sizes (starting at 100x100). + +## 🖼️ High-Level Architecture +The application is structured as a Single Page Application (SPA) contained entirely within `index.html`. The three core components interact to manage the simulation loop and output rendering: + +* **GameController:** The central state machine. It manages the internal grid representation (`grid[W][H]`), handles game logic execution (calculating next generations), maintains the current running status (running/paused/stopped), and provides methods for external state mutation. It is responsible for upholding the rules of Conway's Game of Life (under/overpopulation, death). +* **CanvasRenderer:** Responsible solely for visual output. It receives the current grid array from `GameController` periodically and draws the necessary pixels onto the HTML Canvas element. This component also manages aspect ratio and scaling related to the fixed cell size. +* **UIHandler:** The interface layer (Event Listener). This module listens for user inputs, such as button clicks (Start/Stop) and mouse events (editing state via clicking/dragging the canvas). It translates screen coordinates into array indices while accounting for viewport offset (scrolling), thus interfacing between the human user and the `GameController`. + +## 🔄 Data Flow and Implementation Details +### Initialization +1. The application starts by initializing the internal $W \times H$ grid array (e.g., $100 \times 100$). +2. This initial state is populated with a known pattern: the **Glider Cannon**, placed at specific pre-determined Gosper coordinates within the grid space. + +### Simulation Loop (The Tick) +The simulation runs asynchronously using `requestAnimationFrame` for smooth rendering and timing accuracy, rather than a simple fixed interval timer. +1. **State Calculation:** In each cycle, before modifying the main state, all neighbors are checked for every live cell in the current grid (`grid`). A temporary 'next\_grid' is used during calculation to ensure all neighbor checks rely *only* on the prior generation's state, preventing race conditions. +2. **State Transition:** The `GameController` applies Conway's Rules. Once the next state for every cell is determined and stored in `next_grid`, the main state array (`grid`) is atomically swapped with `next_grid`. +3. **Rendering:** After the swap, the new state is delivered to `CanvasRenderer`, which draws the board. + +### User Editing (Mutation) +When a user interacts with the UI (e.g., clicks on a cell they wish to toggle): +1. The `UIHandler` captures the screen position and determines the corresponding 2D array index. +2. It commands the `GameController` to *force* a change on that specific cell in the current grid, overriding the simulation rules for that moment. This action modifies the state locally but does not necessarily reset or trigger the entire generation sequence unless explicitly requested by a separate 'reset' action. + +## 📏 Constraints and Assumptions +* **Initial Dimensions:** The initial working dimensions are fixed at $100 \times 100$. Future growth beyond this (e.g., to $200\times200$) requires only scaling the rendering logic, not a fundamental change in data structure. +* **Scrolling Support:** A mechanism for handling views larger than the container size is implemented by mapping client-side scroll offsets back into virtual grid coordinates during editing and drawing stages. +* **Concurrency Control:** The critical requirement of preventing race conditions during next-state calculation is met by using a temporary buffer (`next_grid`) before committing the state change, ensuring atomicity between generations. + +## 🔎 Spec Self-Review Complete +The design is self-consistent; all components are related appropriately (GameController -> CanvasRenderer via shared grid state). The requirements for fixed array use and Glider Cannon initialization are met in the architecture description. No ambiguous concepts were found requiring further clarification. + +**Proposed Next Step:** Create the implementation plan using `writing-plans`. \ No newline at end of file From e14a39d23538560552f6baa2feacbe56d917e5a5 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Sun, 23 Aug 2026 11:41:53 +0200 Subject: [PATCH 23/26] cleanup --- .../specs/2026-07-12-gameoflife-design.md | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 docs/superpowers/specs/2026-07-12-gameoflife-design.md diff --git a/docs/superpowers/specs/2026-07-12-gameoflife-design.md b/docs/superpowers/specs/2026-07-12-gameoflife-design.md deleted file mode 100644 index 1d79682..0000000 --- a/docs/superpowers/specs/2026-07-12-gameoflife-design.md +++ /dev/null @@ -1,38 +0,0 @@ -# Conway's Game of Life Application Specification - Canvas Edition -## Date: 2026-07-12 - -## 🚀 Goal -To create a single-page web application capable of running Conway's Game of Life with specific features, including: start/stop functionality, state editing via mouse interaction, and initialization with the Glider Cannon pattern. The simulation must utilize a fixed 2D array for state management and be rendered efficiently using HTML Canvas to support large board sizes (starting at 100x100). - -## 🖼️ High-Level Architecture -The application is structured as a Single Page Application (SPA) contained entirely within `index.html`. The three core components interact to manage the simulation loop and output rendering: - -* **GameController:** The central state machine. It manages the internal grid representation (`grid[W][H]`), handles game logic execution (calculating next generations), maintains the current running status (running/paused/stopped), and provides methods for external state mutation. It is responsible for upholding the rules of Conway's Game of Life (under/overpopulation, death). -* **CanvasRenderer:** Responsible solely for visual output. It receives the current grid array from `GameController` periodically and draws the necessary pixels onto the HTML Canvas element. This component also manages aspect ratio and scaling related to the fixed cell size. -* **UIHandler:** The interface layer (Event Listener). This module listens for user inputs, such as button clicks (Start/Stop) and mouse events (editing state via clicking/dragging the canvas). It translates screen coordinates into array indices while accounting for viewport offset (scrolling), thus interfacing between the human user and the `GameController`. - -## 🔄 Data Flow and Implementation Details -### Initialization -1. The application starts by initializing the internal $W \times H$ grid array (e.g., $100 \times 100$). -2. This initial state is populated with a known pattern: the **Glider Cannon**, placed at specific pre-determined Gosper coordinates within the grid space. - -### Simulation Loop (The Tick) -The simulation runs asynchronously using `requestAnimationFrame` for smooth rendering and timing accuracy, rather than a simple fixed interval timer. -1. **State Calculation:** In each cycle, before modifying the main state, all neighbors are checked for every live cell in the current grid (`grid`). A temporary 'next\_grid' is used during calculation to ensure all neighbor checks rely *only* on the prior generation's state, preventing race conditions. -2. **State Transition:** The `GameController` applies Conway's Rules. Once the next state for every cell is determined and stored in `next_grid`, the main state array (`grid`) is atomically swapped with `next_grid`. -3. **Rendering:** After the swap, the new state is delivered to `CanvasRenderer`, which draws the board. - -### User Editing (Mutation) -When a user interacts with the UI (e.g., clicks on a cell they wish to toggle): -1. The `UIHandler` captures the screen position and determines the corresponding 2D array index. -2. It commands the `GameController` to *force* a change on that specific cell in the current grid, overriding the simulation rules for that moment. This action modifies the state locally but does not necessarily reset or trigger the entire generation sequence unless explicitly requested by a separate 'reset' action. - -## 📏 Constraints and Assumptions -* **Initial Dimensions:** The initial working dimensions are fixed at $100 \times 100$. Future growth beyond this (e.g., to $200\times200$) requires only scaling the rendering logic, not a fundamental change in data structure. -* **Scrolling Support:** A mechanism for handling views larger than the container size is implemented by mapping client-side scroll offsets back into virtual grid coordinates during editing and drawing stages. -* **Concurrency Control:** The critical requirement of preventing race conditions during next-state calculation is met by using a temporary buffer (`next_grid`) before committing the state change, ensuring atomicity between generations. - -## 🔎 Spec Self-Review Complete -The design is self-consistent; all components are related appropriately (GameController -> CanvasRenderer via shared grid state). The requirements for fixed array use and Glider Cannon initialization are met in the architecture description. No ambiguous concepts were found requiring further clarification. - -**Proposed Next Step:** Create the implementation plan using `writing-plans`. \ No newline at end of file From 25e444764ca167a5f00de756d36988f06c026dea Mon Sep 17 00:00:00 2001 From: fillg1 Date: Sun, 23 Aug 2026 11:41:53 +0200 Subject: [PATCH 24/26] cleanup --- .../specs/2026-07-12-gameoflife-design.md | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 docs/superpowers/specs/2026-07-12-gameoflife-design.md diff --git a/docs/superpowers/specs/2026-07-12-gameoflife-design.md b/docs/superpowers/specs/2026-07-12-gameoflife-design.md deleted file mode 100644 index 1d79682..0000000 --- a/docs/superpowers/specs/2026-07-12-gameoflife-design.md +++ /dev/null @@ -1,38 +0,0 @@ -# Conway's Game of Life Application Specification - Canvas Edition -## Date: 2026-07-12 - -## 🚀 Goal -To create a single-page web application capable of running Conway's Game of Life with specific features, including: start/stop functionality, state editing via mouse interaction, and initialization with the Glider Cannon pattern. The simulation must utilize a fixed 2D array for state management and be rendered efficiently using HTML Canvas to support large board sizes (starting at 100x100). - -## 🖼️ High-Level Architecture -The application is structured as a Single Page Application (SPA) contained entirely within `index.html`. The three core components interact to manage the simulation loop and output rendering: - -* **GameController:** The central state machine. It manages the internal grid representation (`grid[W][H]`), handles game logic execution (calculating next generations), maintains the current running status (running/paused/stopped), and provides methods for external state mutation. It is responsible for upholding the rules of Conway's Game of Life (under/overpopulation, death). -* **CanvasRenderer:** Responsible solely for visual output. It receives the current grid array from `GameController` periodically and draws the necessary pixels onto the HTML Canvas element. This component also manages aspect ratio and scaling related to the fixed cell size. -* **UIHandler:** The interface layer (Event Listener). This module listens for user inputs, such as button clicks (Start/Stop) and mouse events (editing state via clicking/dragging the canvas). It translates screen coordinates into array indices while accounting for viewport offset (scrolling), thus interfacing between the human user and the `GameController`. - -## 🔄 Data Flow and Implementation Details -### Initialization -1. The application starts by initializing the internal $W \times H$ grid array (e.g., $100 \times 100$). -2. This initial state is populated with a known pattern: the **Glider Cannon**, placed at specific pre-determined Gosper coordinates within the grid space. - -### Simulation Loop (The Tick) -The simulation runs asynchronously using `requestAnimationFrame` for smooth rendering and timing accuracy, rather than a simple fixed interval timer. -1. **State Calculation:** In each cycle, before modifying the main state, all neighbors are checked for every live cell in the current grid (`grid`). A temporary 'next\_grid' is used during calculation to ensure all neighbor checks rely *only* on the prior generation's state, preventing race conditions. -2. **State Transition:** The `GameController` applies Conway's Rules. Once the next state for every cell is determined and stored in `next_grid`, the main state array (`grid`) is atomically swapped with `next_grid`. -3. **Rendering:** After the swap, the new state is delivered to `CanvasRenderer`, which draws the board. - -### User Editing (Mutation) -When a user interacts with the UI (e.g., clicks on a cell they wish to toggle): -1. The `UIHandler` captures the screen position and determines the corresponding 2D array index. -2. It commands the `GameController` to *force* a change on that specific cell in the current grid, overriding the simulation rules for that moment. This action modifies the state locally but does not necessarily reset or trigger the entire generation sequence unless explicitly requested by a separate 'reset' action. - -## 📏 Constraints and Assumptions -* **Initial Dimensions:** The initial working dimensions are fixed at $100 \times 100$. Future growth beyond this (e.g., to $200\times200$) requires only scaling the rendering logic, not a fundamental change in data structure. -* **Scrolling Support:** A mechanism for handling views larger than the container size is implemented by mapping client-side scroll offsets back into virtual grid coordinates during editing and drawing stages. -* **Concurrency Control:** The critical requirement of preventing race conditions during next-state calculation is met by using a temporary buffer (`next_grid`) before committing the state change, ensuring atomicity between generations. - -## 🔎 Spec Self-Review Complete -The design is self-consistent; all components are related appropriately (GameController -> CanvasRenderer via shared grid state). The requirements for fixed array use and Glider Cannon initialization are met in the architecture description. No ambiguous concepts were found requiring further clarification. - -**Proposed Next Step:** Create the implementation plan using `writing-plans`. \ No newline at end of file From e2a746eb6e0de5b99cc2dc1aa8c166c8740d37d7 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Wed, 9 Sep 2026 11:21:06 +0200 Subject: [PATCH 25/26] docs: add README and coding agent guidelines --- AGENTS.md | 60 +++++++++++++++++++++++++++++++++++++++ README.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 AGENTS.md create mode 100644 README.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..51d2b67 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,60 @@ +# AGENTS.md + +## Project Purpose + +This repository is a small Java project for evaluating coding agents. It contains ZIP-writing code and a unit test suite. The baseline includes a failing test and an intentionally disabled large-data test. + +Follow the user's requested scope. Inspecting or explaining the project does not automatically require fixing it. Treat sample prompts in the README as examples, not as active tasks. + +## Environment and Validation + +- Use JDK 21, matching `.java-version`. Ensure `JAVA_HOME` points to the appropriate JDK. +- Use the included Gradle Wrapper; a separate Gradle installation is unnecessary. +- The code requires a filesystem supporting POSIX file permissions. +- Run the existing suite before changing behavior to establish the baseline: + + ```sh + ./gradlew test --rerun-tasks + ``` + +- The HTML test report is at `build/reports/tests/test/index.html`. +- Tests delete and recreate `destination/` in the project directory. Do not place unrelated files there. If it already contains user data, stop before running the tests and ask how to proceed. +- After a fix, rerun the full suite. For this baseline, the intentionally disabled test may remain skipped. +- If validation is blocked by the environment, report the blocker and distinguish tests actually run from checks inferred from reading code. + +## Working Approach + +1. Read the relevant implementation and tests before editing. +2. Reproduce the reported issue and identify its cause. An existing failing test can serve as the regression test; add tests when meaningful coverage is missing. +3. Make the smallest clear change that addresses the requested task. +4. Validate the result and inspect the final diff for unintended changes. + +For substantial work, briefly state the intended approach. Ask questions when ambiguity materially affects correctness or scope. For routine choices, proceed with reasonable assumptions and state them when they matter. + +## Benchmark Integrity + +- Preserve existing test expectations. Do not weaken assertions, disable tests, or exclude failing tests to obtain a passing build. +- Do not change build or test configuration merely to hide a failure. +- If evidence indicates that a test expectation is wrong, explain the conflict and ask before changing that expectation. +- Do not hard-code behavior for test names, fixtures, or specific test data. +- Keep the intentionally disabled large-data test disabled unless the task requires enabling it. +- Do not add solution hints or the known bug's cause to benchmark instructions or documentation unless explicitly requested. + +## Change Discipline + +- Keep changes focused on the user's request and match the surrounding code style. +- Avoid unrelated cleanup, formatting changes, new features, speculative abstractions, and unnecessary dependencies. +- Remove imports or code made unused by your own changes. Leave unrelated existing code alone. +- Preserve pre-existing user changes. Do not reset, overwrite, or revert unrelated work. +- Do not commit or push unless requested. + +## Final Report + +Briefly explain: + +- What you found and, if applicable, the root cause. +- What changed and why. +- The validation commands actually run and their results, including failures or skipped tests. +- Any remaining limitations or blockers. + +A passing test suite is evidence of correctness, not a substitute for understanding the behavior and reviewing the diff. diff --git a/README.md b/README.md new file mode 100644 index 0000000..923c501 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# Coding Agent Benchmark + +A small Java project for comparing coding agents. Its compact codebase and existing test suite provide a starting point for observing how an agent understands unfamiliar code, investigates bugs, and makes focused changes. + +This project is intended as a benchmark and learning environment. It does not include a runnable application; start with the source code and unit tests. + +## The Example Project + +`SplitZipWriter` writes files from input streams into ZIP archives, creating multiple parts based on a size threshold. Archives are named `_01.zip`, `_02.zip`, and so on. Each input file is written entirely into one archive; switching archives happens between files. + +Other components: + +- `ZipResult` collects the names and sizes of the generated archives. +- `OutputStreamEnhancer` allows the output stream to be wrapped with additional behavior. +- `InputStreamEnhancer` defines the corresponding interface for input streams. +- `SplitZipWriterTest` checks archive splitting and file contents. + +## Requirements + +- JDK 21, matching the repository's `.java-version` +- A filesystem supporting POSIX file permissions, such as those typically used on Linux or macOS +- Internet access for the first build to download Gradle and dependencies + +The Gradle Wrapper is included, so no separate Gradle installation is required. `JAVA_HOME` should point to the JDK you want to use. + +## Quick Start + +Run all tests from the project directory: + +```sh +./gradlew test +``` + +Run a full build, including tests: + +```sh +./gradlew build +``` + +Force the tests to run again, even if Gradle considers them up to date: + +```sh +./gradlew test --rerun-tasks +``` + +The HTML test report is available at `build/reports/tests/test/index.html` after the test run. + +**Warning:** The tests delete and recreate the `destination/` directory inside the project directory. Do not store your own files there. + +## Using This Project as a Benchmark + +The baseline contains one failing unit test. Another test covering large amounts of data is disabled with `@Disabled`. A failing test run is therefore part of the benchmark's initial state. + +Example prompt for a coding agent: + +> Inspect the project and run the unit tests. Find the cause of the failing test and fix it with the smallest reasonable change. Preserve the existing test expectations and do not disable any tests. Explain the cause, your change, and how you validated it. + +For comparable runs: + +1. Start each agent from the same commit in a separate checkout. +2. Use the same prompt, Java version, and access permissions. +3. Record the model, agent version, settings, runtime, and token usage where available. +4. Review the resulting diff and run the tests again. + +Possible evaluation criteria include a correct root cause analysis, a small and understandable change, preserved test expectations, and successful validation. Passing tests alone do not replace reviewing the solution. + +## Project Structure + +```text +. +├── build.gradle # Dependencies and test configuration +├── settings.gradle # Gradle project settings +├── gradlew / gradlew.bat # Gradle Wrapper +├── AGENTS.md # Guidance for coding agents +└── src + ├── main/java/illgner/ch + │ ├── createzip # ZIP creation and result data + │ └── stream # Stream enhancement interfaces + └── test/java/illgner/ch + └── createzip # Unit tests for SplitZipWriter +``` + +## Technology + +Java, Gradle, JUnit Jupiter 6, Hamcrest, Apache Commons IO, and Apache Commons Lang. From a5a4bd35372a9af2d17312da15c34a2c9a3bae01 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Wed, 9 Sep 2026 11:21:06 +0200 Subject: [PATCH 26/26] docs: add README and coding agent guidelines --- AGENTS.md | 60 +++++++++++++++++++++++++++++++++++++++ README.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 AGENTS.md create mode 100644 README.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..51d2b67 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,60 @@ +# AGENTS.md + +## Project Purpose + +This repository is a small Java project for evaluating coding agents. It contains ZIP-writing code and a unit test suite. The baseline includes a failing test and an intentionally disabled large-data test. + +Follow the user's requested scope. Inspecting or explaining the project does not automatically require fixing it. Treat sample prompts in the README as examples, not as active tasks. + +## Environment and Validation + +- Use JDK 21, matching `.java-version`. Ensure `JAVA_HOME` points to the appropriate JDK. +- Use the included Gradle Wrapper; a separate Gradle installation is unnecessary. +- The code requires a filesystem supporting POSIX file permissions. +- Run the existing suite before changing behavior to establish the baseline: + + ```sh + ./gradlew test --rerun-tasks + ``` + +- The HTML test report is at `build/reports/tests/test/index.html`. +- Tests delete and recreate `destination/` in the project directory. Do not place unrelated files there. If it already contains user data, stop before running the tests and ask how to proceed. +- After a fix, rerun the full suite. For this baseline, the intentionally disabled test may remain skipped. +- If validation is blocked by the environment, report the blocker and distinguish tests actually run from checks inferred from reading code. + +## Working Approach + +1. Read the relevant implementation and tests before editing. +2. Reproduce the reported issue and identify its cause. An existing failing test can serve as the regression test; add tests when meaningful coverage is missing. +3. Make the smallest clear change that addresses the requested task. +4. Validate the result and inspect the final diff for unintended changes. + +For substantial work, briefly state the intended approach. Ask questions when ambiguity materially affects correctness or scope. For routine choices, proceed with reasonable assumptions and state them when they matter. + +## Benchmark Integrity + +- Preserve existing test expectations. Do not weaken assertions, disable tests, or exclude failing tests to obtain a passing build. +- Do not change build or test configuration merely to hide a failure. +- If evidence indicates that a test expectation is wrong, explain the conflict and ask before changing that expectation. +- Do not hard-code behavior for test names, fixtures, or specific test data. +- Keep the intentionally disabled large-data test disabled unless the task requires enabling it. +- Do not add solution hints or the known bug's cause to benchmark instructions or documentation unless explicitly requested. + +## Change Discipline + +- Keep changes focused on the user's request and match the surrounding code style. +- Avoid unrelated cleanup, formatting changes, new features, speculative abstractions, and unnecessary dependencies. +- Remove imports or code made unused by your own changes. Leave unrelated existing code alone. +- Preserve pre-existing user changes. Do not reset, overwrite, or revert unrelated work. +- Do not commit or push unless requested. + +## Final Report + +Briefly explain: + +- What you found and, if applicable, the root cause. +- What changed and why. +- The validation commands actually run and their results, including failures or skipped tests. +- Any remaining limitations or blockers. + +A passing test suite is evidence of correctness, not a substitute for understanding the behavior and reviewing the diff. diff --git a/README.md b/README.md new file mode 100644 index 0000000..923c501 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# Coding Agent Benchmark + +A small Java project for comparing coding agents. Its compact codebase and existing test suite provide a starting point for observing how an agent understands unfamiliar code, investigates bugs, and makes focused changes. + +This project is intended as a benchmark and learning environment. It does not include a runnable application; start with the source code and unit tests. + +## The Example Project + +`SplitZipWriter` writes files from input streams into ZIP archives, creating multiple parts based on a size threshold. Archives are named `_01.zip`, `_02.zip`, and so on. Each input file is written entirely into one archive; switching archives happens between files. + +Other components: + +- `ZipResult` collects the names and sizes of the generated archives. +- `OutputStreamEnhancer` allows the output stream to be wrapped with additional behavior. +- `InputStreamEnhancer` defines the corresponding interface for input streams. +- `SplitZipWriterTest` checks archive splitting and file contents. + +## Requirements + +- JDK 21, matching the repository's `.java-version` +- A filesystem supporting POSIX file permissions, such as those typically used on Linux or macOS +- Internet access for the first build to download Gradle and dependencies + +The Gradle Wrapper is included, so no separate Gradle installation is required. `JAVA_HOME` should point to the JDK you want to use. + +## Quick Start + +Run all tests from the project directory: + +```sh +./gradlew test +``` + +Run a full build, including tests: + +```sh +./gradlew build +``` + +Force the tests to run again, even if Gradle considers them up to date: + +```sh +./gradlew test --rerun-tasks +``` + +The HTML test report is available at `build/reports/tests/test/index.html` after the test run. + +**Warning:** The tests delete and recreate the `destination/` directory inside the project directory. Do not store your own files there. + +## Using This Project as a Benchmark + +The baseline contains one failing unit test. Another test covering large amounts of data is disabled with `@Disabled`. A failing test run is therefore part of the benchmark's initial state. + +Example prompt for a coding agent: + +> Inspect the project and run the unit tests. Find the cause of the failing test and fix it with the smallest reasonable change. Preserve the existing test expectations and do not disable any tests. Explain the cause, your change, and how you validated it. + +For comparable runs: + +1. Start each agent from the same commit in a separate checkout. +2. Use the same prompt, Java version, and access permissions. +3. Record the model, agent version, settings, runtime, and token usage where available. +4. Review the resulting diff and run the tests again. + +Possible evaluation criteria include a correct root cause analysis, a small and understandable change, preserved test expectations, and successful validation. Passing tests alone do not replace reviewing the solution. + +## Project Structure + +```text +. +├── build.gradle # Dependencies and test configuration +├── settings.gradle # Gradle project settings +├── gradlew / gradlew.bat # Gradle Wrapper +├── AGENTS.md # Guidance for coding agents +└── src + ├── main/java/illgner/ch + │ ├── createzip # ZIP creation and result data + │ └── stream # Stream enhancement interfaces + └── test/java/illgner/ch + └── createzip # Unit tests for SplitZipWriter +``` + +## Technology + +Java, Gradle, JUnit Jupiter 6, Hamcrest, Apache Commons IO, and Apache Commons Lang.