2020
2121import java .io .File ;
2222import java .net .URL ;
23+ import java .time .LocalDate ;
24+ import java .time .ZoneId ;
25+ import java .time .format .DateTimeParseException ;
26+ import java .util .Date ;
27+ import java .util .LinkedHashMap ;
2328import java .util .LinkedHashSet ;
2429import java .util .Locale ;
30+ import java .util .Map ;
2531import java .util .Properties ;
2632import java .util .Set ;
2733import org .kopi .ebics .interfaces .EbicsBank ;
34+ import org .kopi .ebics .interfaces .EbicsOrderType ;
2835import org .kopi .ebics .interfaces .EbicsPartner ;
2936import org .kopi .ebics .interfaces .PasswordCallback ;
3037import org .kopi .ebics .session .DefaultConfiguration ;
@@ -41,9 +48,16 @@ public final class ParameterizedEbicsClientLauncher {
4148 "--ini" ,
4249 "--hia" ,
4350 "--hpb" ,
44- "--help"
51+ "--help" ,
52+ "--btd"
4553 );
4654
55+ /**
56+ * EBICS 3.0 business transaction downloads always use the admin order type {@code BTD}; the
57+ * business order is carried by the service parameters instead of the 3-letter code.
58+ */
59+ private static final EbicsOrderType BTD_ORDER_TYPE = () -> "BTD" ;
60+
4761 private ParameterizedEbicsClientLauncher () {
4862 }
4963
@@ -117,6 +131,20 @@ public static void main(String[] args) throws Exception {
117131 client .sendHPBRequest (user , product );
118132 }
119133
134+ if (parsedArguments .hasFlag ("--btd" )) {
135+ EbicsDownloadParams downloadParams = btdDownloadParams (parsedArguments );
136+ client .fetchFile (
137+ new File (requireOutputPath (parsedArguments )),
138+ user ,
139+ product ,
140+ BTD_ORDER_TYPE ,
141+ downloadParams ,
142+ Boolean .parseBoolean (env ("EBICS_TEST_MODE" , "false" ))
143+ );
144+ client .quit ();
145+ return ;
146+ }
147+
120148 String orderFlag = parsedArguments .firstOrderFlag ();
121149 if (orderFlag != null ) {
122150 OrderType orderType = OrderType .valueOf (orderFlag .substring (2 ).toUpperCase (Locale .ROOT ));
@@ -129,16 +157,15 @@ public static void main(String[] args) throws Exception {
129157 defaultUploadParams (user , orderType )
130158 );
131159 } else if (parsedArguments .outputPath () != null ) {
132- if (parsedArguments .startDate () != null || parsedArguments .endDate () != null ) {
133- System .err .println (
134- "Date range arguments are ignored in parameterized mode for this order type."
135- );
136- }
137160 client .fetchFile (
138161 new File (parsedArguments .outputPath ()),
139162 user ,
140163 product ,
141164 orderType ,
165+ EbicsDownloadParams .dateRangeOnly (
166+ parseDate (parsedArguments .startDate (), "--start" ),
167+ parseDate (parsedArguments .endDate (), "--end" )
168+ ),
142169 Boolean .parseBoolean (env ("EBICS_TEST_MODE" , "false" ))
143170 );
144171 }
@@ -149,12 +176,67 @@ public static void main(String[] args) throws Exception {
149176
150177 private static void printUsage () {
151178 String usage = "Usage: ParameterizedEbicsClientLauncher [--create] [--ini] [--hia] [--hpb]"
152- + " [--<order>] [-i inputFile] [-o outputFile]\n "
179+ + " [--<order>] [-i inputFile] [-o outputFile] [-s start] [-e end]\n "
180+ + "EBICS 3.0 download: --btd --service <NAME> --scope <CC> --msg-name <name>"
181+ + " --msg-version <vv> --container <XML|ZIP|SVC>"
182+ + " [--option <OPT>] [-s YYYY-MM-DD] [-e YYYY-MM-DD] -o <file>\n "
183+ + " e.g. --btd --service EOP --scope CH --msg-name camt.053 --msg-version 08"
184+ + " --container ZIP -o statement.zip\n "
153185 + "Required environment variables: EBICS_PASSWORD, EBICS_USER_ID, EBICS_PARTNER_ID,"
154186 + " EBICS_HOST_ID, EBICS_BANK_URL" ;
155187 System .out .println (usage );
156188 }
157189
190+ /**
191+ * Builds the EBICS 3.0 service parameters for {@code --btd}. Fails fast on a missing mandatory
192+ * value, so a half-filled order is never sent to the bank.
193+ */
194+ static EbicsDownloadParams btdDownloadParams (ParsedArguments parsedArguments ) {
195+ // A half date range would be dropped silently further down, which is exactly how a
196+ // catch-up run loses the days it was supposed to fetch.
197+ if ((parsedArguments .startDate () == null ) != (parsedArguments .endDate () == null )) {
198+ throw new IllegalArgumentException (
199+ "Options --start and --end must be given together, a single one is ignored"
200+ + " by the bank request" );
201+ }
202+ return new EbicsDownloadParams (
203+ requireOption (parsedArguments .serviceName (), "--service" ),
204+ requireOption (parsedArguments .scope (), "--scope" ),
205+ parsedArguments .option (),
206+ requireOption (parsedArguments .messageName (), "--msg-name" ),
207+ requireOption (parsedArguments .messageVersion (), "--msg-version" ),
208+ requireOption (parsedArguments .containerType (), "--container" ),
209+ parseDate (parsedArguments .startDate (), "--start" ),
210+ parseDate (parsedArguments .endDate (), "--end" )
211+ );
212+ }
213+
214+ static String requireOutputPath (ParsedArguments parsedArguments ) {
215+ return requireOption (parsedArguments .outputPath (), "-o" );
216+ }
217+
218+ private static String requireOption (String value , String option ) {
219+ String normalized = normalize (value );
220+ if (normalized == null ) {
221+ throw new IllegalArgumentException ("Missing required option " + option + " for --btd" );
222+ }
223+ return normalized ;
224+ }
225+
226+ private static Date parseDate (String value , String option ) {
227+ String normalized = normalize (value );
228+ if (normalized == null ) {
229+ return null ;
230+ }
231+ try {
232+ return Date .from (LocalDate .parse (normalized )
233+ .atStartOfDay (ZoneId .systemDefault ()).toInstant ());
234+ } catch (DateTimeParseException e ) {
235+ throw new IllegalArgumentException (
236+ "Option " + option + " expects a date as YYYY-MM-DD but was: " + normalized );
237+ }
238+ }
239+
158240 private static EbicsUploadParams defaultUploadParams (User user , OrderType orderType ) {
159241 if (orderType == OrderType .XE2 ) {
160242 var orderParams = new EbicsUploadParams .OrderParams (
@@ -290,27 +372,41 @@ static String normalize(String value) {
290372
291373 static final class ParsedArguments {
292374 private final Set <String > flags = new LinkedHashSet <>();
375+ private final Map <String , String > values ;
293376 private final String inputPath ;
294377 private final String outputPath ;
295378 private final String startDate ;
296379 private final String endDate ;
297380
298381 private ParsedArguments (
299382 Set <String > flags ,
383+ Map <String , String > values ,
300384 String inputPath ,
301385 String outputPath ,
302386 String startDate ,
303387 String endDate
304388 ) {
305389 this .flags .addAll (flags );
390+ this .values = Map .copyOf (values );
306391 this .inputPath = inputPath ;
307392 this .outputPath = outputPath ;
308393 this .startDate = startDate ;
309394 this .endDate = endDate ;
310395 }
311396
397+ /** Value options of the EBICS 3.0 service block; each consumes the following argument. */
398+ private static final Set <String > VALUE_OPTIONS = Set .of (
399+ "--service" ,
400+ "--scope" ,
401+ "--option" ,
402+ "--msg-name" ,
403+ "--msg-version" ,
404+ "--container"
405+ );
406+
312407 static ParsedArguments parse (String [] args ) {
313408 Set <String > flags = new LinkedHashSet <>();
409+ Map <String , String > values = new LinkedHashMap <>();
314410 String inputPath = null ;
315411 String outputPath = null ;
316412 String startDate = null ;
@@ -337,12 +433,17 @@ static ParsedArguments parse(String[] args) {
337433 endDate = requireValue (args , ++index , arg );
338434 continue ;
339435 }
436+ String lowered = arg .toLowerCase (Locale .ROOT );
437+ if (VALUE_OPTIONS .contains (lowered )) {
438+ values .put (lowered , requireValue (args , ++index , arg ));
439+ continue ;
440+ }
340441 if (arg .startsWith ("--" )) {
341- flags .add (arg . toLowerCase ( Locale . ROOT ) );
442+ flags .add (lowered );
342443 }
343444 }
344445 }
345- return new ParsedArguments (flags , inputPath , outputPath , startDate , endDate );
446+ return new ParsedArguments (flags , values , inputPath , outputPath , startDate , endDate );
346447 }
347448
348449 private static String requireValue (String [] args , int index , String option ) {
@@ -393,5 +494,29 @@ String startDate() {
393494 String endDate () {
394495 return endDate ;
395496 }
497+
498+ String serviceName () {
499+ return values .get ("--service" );
500+ }
501+
502+ String scope () {
503+ return values .get ("--scope" );
504+ }
505+
506+ String option () {
507+ return values .get ("--option" );
508+ }
509+
510+ String messageName () {
511+ return values .get ("--msg-name" );
512+ }
513+
514+ String messageVersion () {
515+ return values .get ("--msg-version" );
516+ }
517+
518+ String containerType () {
519+ return values .get ("--container" );
520+ }
396521 }
397522}
0 commit comments