View Javadoc
1   /*
2    * This file is part of Waarp Project (named also Waarp or GG).
3    *
4    *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
5    *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
6    * individual contributors.
7    *
8    *  All Waarp Project is free software: you can redistribute it and/or
9    * modify it under the terms of the GNU General Public License as published by
10   * the Free Software Foundation, either version 3 of the License, or (at your
11   * option) any later version.
12   *
13   * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
14   * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15   * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16   *
17   *  You should have received a copy of the GNU General Public License along with
18   * Waarp . If not, see <http://www.gnu.org/licenses/>.
19   */
20  package org.waarp.openr66.context.task;
21  
22  import org.waarp.common.logging.WaarpLogger;
23  import org.waarp.common.logging.WaarpLoggerFactory;
24  import org.waarp.openr66.context.ErrorCode;
25  import org.waarp.openr66.context.R66Result;
26  import org.waarp.openr66.context.R66Session;
27  import org.waarp.openr66.context.task.exception.OpenR66RunnerErrorException;
28  import org.waarp.openr66.database.data.DbTaskRunner;
29  
30  import java.sql.Timestamp;
31  import java.util.Calendar;
32  import java.util.Date;
33  import java.util.Map;
34  
35  /**
36   * Reschedule Transfer task to a time delayed by the specified number of
37   * milliseconds, if the error code is
38   * one of the specified codes and the optional intervals of date are compatible
39   * with the new time schedule<br>
40   * <br>
41   * <p>
42   * Result of arguments will be as following options (the two first are
43   * mandatory):<br>
44   * <br>
45   * <p>
46   * "-delay ms" where ms is the added number of ms on current time before retry
47   * on schedule<br>
48   * <br>
49   * "-case errorCode,errorCode,..." where errorCode is one of the following error
50   * of the current transfer
51   * (either literal or code in 1 character:<br>
52   * ConnectionImpossible(C), ServerOverloaded(l), BadAuthent(A), ExternalOp(E),
53   * TransferError(T), MD5Error(M),
54   * Disconnection(D), RemoteShutdown(r), FinalOp(F), Unimplemented(U),
55   * Shutdown(S), RemoteError(R),
56   * Internal(I), StoppedTransfer(H), CanceledTransfer(K), Warning(W), Unknown(-),
57   * QueryAlreadyFinished(Q),
58   * QueryStillRunning(s), NotKnownHost(N), QueryRemotelyUnknown(u),
59   * FileNotFound(f), CommandNotFound(c),
60   * PassThroughMode(p)<br>
61   * <br>
62   * "-between start;end" and/or "-notbetween start;end" (multiple times are
63   * allowed, start or end can be not
64   * set) and where start and stop are in the following format:<br>
65   * Yn:Mn:Dn:Hn:mn:Sn where n is a number for each time specification, each
66   * specification is optional, as
67   * Y=Year, M=Month, D=Day, H=Hour, m=minute, s=second.<br>
68   * Format can be X+n, X-n, X=n or Xn where X+-n means adding/subtracting n to
69   * current date value, while X=n or
70   * Xn means setting exact value<br>
71   * If one time specification is not set, it is based on the current date.<br>
72   * <br>
73   * "-count limit" will be the limit of retry. The current value limit is taken
74   * from the "transferInfo"
75   * internal code (not any more the "information of transfer")and not from the
76   * rule as "{"CPTLIMIT": limit}" as
77   * JSON code. Each time this function is called, the limit value will be
78   * replaced as newlimit = limit - 1 in
79   * the "transferInfo" as "{"CPTLIMIT": limit}" as JSON code.<br>
80   * To ensure correctness, the value must be in the "transferInfo" internal code
81   * since this value will be
82   * changed statically in the "transferInfo". If taken from the rule, it will be
83   * wrong since the value will
84   * never decrease. However, a value must be setup in the rule in order to reset
85   * the value when the count reach
86   * 0. <br>
87   * So in the rule, "-count resetlimit" must be present, where resetlimit will be
88   * the new value set when the
89   * limit reach 0, and in the "transferInfo" internal code, "{"CPTLIMIT": limit}"
90   * as JSON code must be present.
91   * If one is missing, the condition is not applied. <br>
92   * <br>
93   * If "-notbetween" is specified, the planned date must not be in the area.<br>
94   * If "-between" is specified, the planned date must be found in any such
95   * specified areas (could be in any of
96   * the occurrence). If not specified, it only depends on "-notbetween".<br>
97   * If none is specified, the planned date is always valid.<br>
98   * <br>
99   * <p>
100  * Note that if a previous called to a reschedule was done for this attempt and
101  * was successful, the following
102  * calls will be ignored.<br>
103  * <br>
104  * <B>Important note: any subsequent task will be ignored and not executed once
105  * the reschedule is
106  * accepted.</B><br>
107  * <br>
108  * In case start > end, end will be +1 day<br>
109  * In case start and end < current planned date, both will have +1 day.<br>
110  * <br>
111  * <p>
112  * Example: -delay 3600000 -case ConnectionImpossible,ServerOverloaded,Shutdown
113  * -notbetween H7:m0:S0;H19:m0:S0
114  * -notbetween H1:m0:S0;H=3:m0:S0<br>
115  * means retry in case of error during initialization of connection in 1 hour if
116  * not between 7AM to 7PM and
117  * not between 1AM to 3AM.<br>
118  */
119 public class RescheduleTransferTask extends AbstractTask {
120   /**
121    * Internal Logger
122    */
123   private static final WaarpLogger logger =
124       WaarpLoggerFactory.getLogger(RescheduleTransferTask.class);
125 
126   /**
127    * Delimiter for -count option in Reschedule to be placed in the transfer
128    * info
129    * of transfer as {"CPTLIMIT":
130    * limit} where limit is an integer.
131    */
132   public static final String CPTLIMIT = "CPTLIMIT";
133   public static final String CPTTOTAL = "CPTTOTAL";
134 
135   protected long newdate;
136 
137   protected Calendar newDate;
138 
139   protected boolean countUsed;
140 
141   protected int limitCount = -1;
142 
143   protected int totalCount;
144 
145   protected int resetCount = -1;
146 
147   protected DbTaskRunner runner;
148 
149   /**
150    * @param argRule
151    * @param delay
152    * @param argTransfer
153    * @param session
154    */
155   public RescheduleTransferTask(final String argRule, final int delay,
156                                 final String argTransfer,
157                                 final R66Session session) {
158     super(TaskType.RESCHEDULE, delay, argRule, argTransfer, session);
159   }
160 
161   @Override
162   public final void run() {
163     logger.info("Reschedule with {}:{} and {}", argRule, argTransfer, session);
164     runner = session.getRunner();
165     if (runner == null) {
166       futureCompletion.setFailure(
167           new OpenR66RunnerErrorException("No valid runner in Reschedule"));
168       return;
169     }
170     if (runner.isRescheduledTransfer()) {
171       // Already rescheduled so ignore
172       final R66Result result =
173           new R66Result(session, false, ErrorCode.Warning, runner);
174       futureCompletion.setResult(result);
175       logger.warn("Transfer already Rescheduled: " + runner.toShortString());
176       futureCompletion.setSuccess();
177       return;
178     }
179     if (runner.isRequestOnRequested()) {
180       // Self Requested Request so reschedule is ignored
181       final R66Result result =
182           new R66Result(session, false, ErrorCode.LoopSelfRequestedHost,
183                         runner);
184       futureCompletion.setResult(result);
185       futureCompletion.setFailure(new OpenR66RunnerErrorException(
186           "No valid runner in Reschedule since Self Requested"));
187       return;
188     }
189     String finalname = argRule;
190     finalname = getReplacedValue(finalname, BLANK.split(argTransfer));
191     final String[] args = BLANK.split(finalname);
192     if (args.length < 4) {
193       final R66Result result =
194           new R66Result(session, false, ErrorCode.Warning, runner);
195       futureCompletion.setResult(result);
196       logger.warn(
197           "Not enough argument in Reschedule: " + runner.toShortString());
198       futureCompletion.setSuccess();
199       return;
200     }
201     if (!validateArgs(args)) {
202       final R66Result result =
203           new R66Result(session, false, ErrorCode.Warning, runner);
204       futureCompletion.setResult(result);
205       logger.warn(
206           "Reschedule unallowed due to argument: " + runner.toShortString());
207       futureCompletion.setSuccess();
208       return;
209     }
210     if (countUsed) {
211       limitCount--;
212       if (limitCount >= 0) {
213         // restart is allowed so resetting to new limitCount
214         resetCount = limitCount;
215       }
216       resetInformation(resetCount);
217       if (limitCount < 0) {
218         // Must not reschedule since limit is reached
219         try {
220           runner.saveStatus();
221         } catch (final OpenR66RunnerErrorException ignored) {
222           // nothing
223         }
224         final R66Result result =
225             new R66Result(session, false, ErrorCode.Warning, runner);
226         futureCompletion.setResult(result);
227         logger.warn("Reschedule unallowed due to limit reached: " +
228                     runner.toShortString());
229         futureCompletion.setSuccess();
230         return;
231       }
232     }
233     final Timestamp start = new Timestamp(newdate);
234     try {
235       runner.setStart(start);
236       if (runner.restart(true)) {
237         runner.saveStatus();
238       }
239     } catch (final OpenR66RunnerErrorException e) {
240       logger.error(
241           "Prepare transfer in     FAILURE      " + runner.toShortString() +
242           "     <AT>" + new Date(newdate) + "</AT>", e);
243       futureCompletion.setFailure(new OpenR66RunnerErrorException(
244           "Reschedule failed: " + e.getMessage(), e));
245       return;
246     }
247     runner.setRescheduledTransfer();
248     final R66Result result =
249         new R66Result(session, false, ErrorCode.Warning, runner);
250     futureCompletion.setResult(result);
251     logger.warn(
252         "Reschedule transfer in     SUCCESS     " + runner.toShortString() +
253         "     <AT>" + new Date(newdate) + "</AT>");
254     futureCompletion.setSuccess();
255   }
256 
257   protected final void resetInformation(final int value) {
258     final Map<String, Object> root = runner.getTransferMap();
259     root.put(CPTLIMIT, value);
260     try {
261       totalCount = (Integer) root.get(CPTTOTAL);
262       totalCount++;
263       root.put(CPTTOTAL, totalCount);
264     } catch (final Exception e) {
265       totalCount = 1;
266       root.put(CPTTOTAL, totalCount);
267     }
268     runner.setTransferMap(root);
269   }
270 
271   protected final boolean validateArgs(final String[] args) {
272     boolean validCode = false;
273     for (int i = 0; i < args.length; i++) {
274       if ("-delay".equalsIgnoreCase(args[i])) {
275         i++;
276         try {
277           newdate = Long.parseLong(args[i]);
278         } catch (final NumberFormatException e) {
279           logger.warn("Bad Long format: args[i]");
280           return false;
281         }
282       } else if ("-case".equalsIgnoreCase(args[i])) {
283         i++;
284         if (!validCode) {
285           final String[] codes = args[i].split(",");
286           for (final String code2 : codes) {
287             final ErrorCode code = ErrorCode.getFromCode(code2);
288             if (session.getLocalChannelReference().getCurrentCode() == code) {
289               logger.debug("Code valid: {}", code);
290               validCode = true;
291             }
292           }
293         }
294       } else if ("-count".equalsIgnoreCase(args[i])) {
295         i++;
296         try {
297           resetCount = Integer.parseInt(args[i]);
298         } catch (final NumberFormatException e) {
299           logger.warn("ResetLimit is not an integer: " + args[i]);
300           countUsed = false;
301           return false;
302         }
303         final Map<String, Object> root = runner.getTransferMap();
304         final Integer limit;
305         try {
306           limit = (Integer) root.get(CPTLIMIT);
307         } catch (final Exception e) {
308           logger.warn("Bad Long format: CPTLIMIT" + " : {}", e.getMessage());
309           return false;
310         }
311         if (limit != null) {
312           limitCount = limit;
313         } else {
314           limitCount = resetCount;
315           root.put(CPTLIMIT, limitCount);
316         }
317         countUsed = true;
318       }
319     }
320     // now we have new delay plus code
321     if (!validCode) {
322       logger.warn("No valid Code found");
323       return false;
324     }
325     if (newdate <= 0) {
326       logger.warn("Delay is negative: " + newdate);
327       return false;
328     }
329     newdate += System.currentTimeMillis();
330     newDate = Calendar.getInstance();
331     newDate.setTimeInMillis(newdate);
332     boolean betweenTest = false;
333     boolean betweenResult = false;
334     for (int i = 0; i < args.length; i++) {
335       if ("-notbetween".equalsIgnoreCase(args[i])) {
336         i++;
337         final String[] elmts = args[i].split(";");
338         boolean startModified = false;
339         String[] values = elmts[0].split(":");
340         Calendar start = getCalendar(values);
341         if (start != null) {
342           startModified = true;
343         } else {
344           start = Calendar.getInstance();
345         }
346         boolean stopModified = false;
347         values = elmts[1].split(":");
348         Calendar stop = getCalendar(values);
349         if (stop != null) {
350           stopModified = true;
351         } else {
352           stop = Calendar.getInstance();
353         }
354         logger.debug("Dates before check: Not between {} and {}",
355                      start.getTime(), stop.getTime());
356         // Check that start < stop
357         if (start.compareTo(stop) > 0) {
358           // no so add 24H to stop
359           stop.add(Calendar.DAY_OF_MONTH, 1);
360         }
361         // Check that start and stop > newDate (only start is checked since start <= stop)
362         if (start.compareTo(newDate) < 0) {
363           start.add(Calendar.DAY_OF_MONTH, 1);
364           stop.add(Calendar.DAY_OF_MONTH, 1);
365         }
366         logger.debug("Dates after check: Not between {} and {}",
367                      start.getTime(), stop.getTime());
368         if (!startModified) {
369           if (newDate.compareTo(stop) < 0) {
370             logger.debug("newDate: {} Should not be between {} and {}",
371                          newDate.getTime(), start.getTime(), stop.getTime());
372             return false;
373           }
374         } else if (start.compareTo(newDate) < 0) {
375           if (!stopModified || newDate.compareTo(stop) < 0) {
376             logger.debug("newDate: {} Should not be between {} and {}",
377                          newDate.getTime(), start.getTime(), stop.getTime());
378             return false;
379           }
380         }
381       } else if ("-between".equalsIgnoreCase(args[i])) {
382         i++;
383         betweenTest = true;
384         final String[] elmts = args[i].split(";");
385         boolean startModified = false;
386         String[] values = elmts[0].split(":");
387         Calendar start = getCalendar(values);
388         if (start != null) {
389           startModified = true;
390         } else {
391           start = Calendar.getInstance();
392         }
393         boolean stopModified = false;
394         values = elmts[1].split(":");
395         Calendar stop = getCalendar(values);
396         if (stop != null) {
397           stopModified = true;
398         } else {
399           stop = Calendar.getInstance();
400         }
401         logger.debug("Dates before check: Between {} and {}", start.getTime(),
402                      stop.getTime());
403         // Check that start < stop
404         if (start.compareTo(stop) > 0) {
405           // no so add 24H to stop
406           stop.add(Calendar.DAY_OF_MONTH, 1);
407         }
408         // Check that start and stop > newDate (only start is checked since start <= stop)
409         if (start.compareTo(newDate) < 0) {
410           start.add(Calendar.DAY_OF_MONTH, 1);
411           stop.add(Calendar.DAY_OF_MONTH, 1);
412         }
413         logger.debug("Dates before check: Between {} and {}", start.getTime(),
414                      stop.getTime());
415         if (!startModified) {
416           if (newDate.compareTo(stop) < 0) {
417             logger.debug("newDate: {} is between {} and {}", newDate.getTime(),
418                          start.getTime(), stop.getTime());
419             betweenResult = true;
420           }
421         } else if (start.compareTo(newDate) < 0) {
422           if (!stopModified || newDate.compareTo(stop) < 0) {
423             logger.debug("newDate: {} is between {} and {}", newDate.getTime(),
424                          start.getTime(), stop.getTime());
425             betweenResult = true;
426           }
427         }
428       }
429     }
430     if (betweenTest) {
431       logger.debug(
432           "Since between is specified, do we found newDate: {} Result: {}",
433           newDate.getTime(), betweenResult);
434       return betweenResult;
435     }
436     logger.debug("newDate: {} rescheduled", newDate.getTime());
437     return true;
438   }
439 
440   /**
441    * @param values as X+n or X-n or X=n or Xn where X=Y/M/D/H/m/s,
442    *     n=number
443    *     and +/- meaning adding/subtracting
444    *     from current date and = meaning specific set value
445    *
446    * @return the Calendar if any specification, or null if no calendar
447    *     specified
448    */
449   private Calendar getCalendar(final String[] values) {
450     final Calendar newcal = Calendar.getInstance();
451     boolean isModified = false;
452     for (final String value2 : values) {
453       if (value2.length() > 1) {
454         int addvalue = 0; // will be different of 0
455         int value = -1; // will be >= 0
456         switch (value2.charAt(0)) {
457           case '+':
458             try {
459               addvalue = Integer.parseInt(value2.substring(2));
460             } catch (final NumberFormatException e) {
461               continue;
462             }
463             break;
464           case '-':
465             try {
466               addvalue = Integer.parseInt(value2.substring(1));
467             } catch (final NumberFormatException e) {
468               continue;
469             }
470             break;
471           case '=':
472             try {
473               value = Integer.parseInt(value2.substring(2));
474             } catch (final NumberFormatException e) {
475               continue;
476             }
477             break;
478           default: // no sign
479             try {
480               value = Integer.parseInt(value2.substring(1));
481             } catch (final NumberFormatException e) {
482               continue;
483             }
484         }
485         switch (value2.charAt(0)) {
486           case 'Y':
487             if (value >= 0) {
488               newcal.set(Calendar.YEAR, value);
489             } else {
490               newcal.add(Calendar.YEAR, addvalue);
491             }
492             isModified = true;
493             break;
494           case 'M':
495             if (value >= 0) {
496               newcal.set(Calendar.MONTH, value);
497             } else {
498               newcal.add(Calendar.MONTH, addvalue);
499             }
500             isModified = true;
501             break;
502           case 'D':
503             if (value >= 0) {
504               newcal.set(Calendar.DAY_OF_MONTH, value);
505             } else {
506               newcal.add(Calendar.DAY_OF_MONTH, addvalue);
507             }
508             isModified = true;
509             break;
510           case 'H':
511             if (value >= 0) {
512               newcal.set(Calendar.HOUR_OF_DAY, value);
513             } else {
514               newcal.add(Calendar.HOUR_OF_DAY, addvalue);
515             }
516             isModified = true;
517             break;
518           case 'm':
519             if (value >= 0) {
520               newcal.set(Calendar.MINUTE, value);
521             } else {
522               newcal.add(Calendar.MINUTE, addvalue);
523             }
524             isModified = true;
525             break;
526           case 'S':
527             if (value >= 0) {
528               newcal.set(Calendar.SECOND, value);
529             } else {
530               newcal.add(Calendar.SECOND, addvalue);
531             }
532             isModified = true;
533             break;
534           default:
535             // nothing
536         }
537       }
538     }
539     if (isModified) {
540       return newcal;
541     }
542     return null;
543   }
544 }