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 com.fasterxml.jackson.core.JsonParseException;
23  import com.fasterxml.jackson.databind.JsonMappingException;
24  import com.fasterxml.jackson.databind.node.ArrayNode;
25  import com.fasterxml.jackson.databind.node.ObjectNode;
26  import org.waarp.common.digest.FilesystemBasedDigest;
27  import org.waarp.common.filemonitor.FileMonitor.FileItem;
28  import org.waarp.common.filemonitor.FileMonitor.FileMonitorInformation;
29  import org.waarp.common.json.JsonHandler;
30  import org.waarp.common.logging.WaarpLogger;
31  import org.waarp.common.logging.WaarpLoggerFactory;
32  import org.waarp.common.utility.ParametersChecker;
33  import org.waarp.common.utility.WaarpStringUtils;
34  import org.waarp.openr66.client.SpooledDirectoryTransfer;
35  import org.waarp.openr66.protocol.configuration.Configuration;
36  import org.waarp.openr66.protocol.configuration.Messages;
37  import org.waarp.openr66.protocol.exception.OpenR66ProtocolPacketException;
38  import org.waarp.openr66.protocol.localhandler.packet.BusinessRequestPacket;
39  import org.waarp.openr66.protocol.utils.ChannelUtils;
40  
41  import java.io.File;
42  import java.io.IOException;
43  import java.text.DateFormat;
44  import java.util.Date;
45  import java.util.Set;
46  import java.util.TreeMap;
47  
48  import static org.waarp.common.database.DbConstant.*;
49  
50  /**
51   * Java Task for SpooledDirectory information to the Waarp Server
52   */
53  public class SpooledInformTask extends AbstractExecJavaTask {
54  
55    private static final String TD_TD = "</TD><TD>";
56  
57    private static final String TH_TH = "</TH><TH>";
58  
59    /**
60     * Internal Logger
61     */
62    private static final WaarpLogger logger =
63        WaarpLoggerFactory.getLogger(SpooledInformTask.class);
64  
65    static final TreeMap<String, SpooledInformation> spooledInformationMap =
66        new TreeMap<String, SpooledInformation>();
67  
68    public static class SpooledInformation {
69      public final String host;
70      public FileMonitorInformation fileMonitorInformation;
71      public Date lastUpdate = new Date();
72  
73      /**
74       * @param host
75       * @param fileMonitorInformation
76       */
77      private SpooledInformation(final String host,
78                                 final FileMonitorInformation fileMonitorInformation) {
79        this.host = host;
80        this.fileMonitorInformation = fileMonitorInformation;
81      }
82    }
83  
84    @Override
85    public final void run() {
86      if (callFromBusiness) {
87        // Business Request to validate?
88        String validated = SpooledDirectoryTransfer.PARTIALOK;
89        if (isToValidate) {
90          try {
91            final FileMonitorInformation fileMonitorInformation =
92                JsonHandler.mapper.readValue(fullarg,
93                                             FileMonitorInformation.class);
94            logger.info(
95                "Receive SpooledInform of size: " + fullarg.length() + " (" +
96                fileMonitorInformation.fileItems.size() + ", " +
97                (fileMonitorInformation.removedFileItems != null?
98                    fileMonitorInformation.removedFileItems.size() : -1) + ')');
99            final String host = session.getAuth().getUser();
100           synchronized (spooledInformationMap) {
101             if (fileMonitorInformation.removedFileItems == null ||
102                 fileMonitorInformation.removedFileItems.isEmpty()) {
103               final SpooledInformation old =
104                   spooledInformationMap.put(fileMonitorInformation.name,
105                                             new SpooledInformation(host,
106                                                                    fileMonitorInformation));
107               if (old != null && old.fileMonitorInformation != null) {
108                 if (old.fileMonitorInformation.directories != null) {
109                   old.fileMonitorInformation.directories.clear();
110                 }
111                 if (old.fileMonitorInformation.fileItems != null) {
112                   old.fileMonitorInformation.fileItems.clear();
113                 }
114                 old.fileMonitorInformation = null;
115               }
116             } else {
117               // partial update
118               final SpooledInformation update =
119                   spooledInformationMap.get(fileMonitorInformation.name);
120               if (update == null) {
121                 // Issue since update is not existing so full update is needed next time
122                 spooledInformationMap.put(fileMonitorInformation.name,
123                                           new SpooledInformation(host,
124                                                                  fileMonitorInformation));
125                 validated = SpooledDirectoryTransfer.NEEDFULL;
126               } else {
127                 for (final String item : fileMonitorInformation.removedFileItems) {
128                   update.fileMonitorInformation.fileItems.remove(item);
129                 }
130                 update.fileMonitorInformation.fileItems.putAll(
131                     fileMonitorInformation.fileItems);
132                 update.lastUpdate = new Date();
133               }
134             }
135           }
136         } catch (final JsonParseException e1) {
137           logger.warn("Cannot parse SpooledInformation: " + fullarg + ' ' +
138                       e1.getMessage());
139         } catch (final JsonMappingException e1) {
140           logger.warn("Cannot parse SpooledInformation: " + fullarg + ' ' +
141                       e1.getMessage());
142         } catch (final IOException e1) {
143           logger.warn("Cannot parse SpooledInformation: " + e1.getMessage());
144         }
145         final BusinessRequestPacket packet =
146             new BusinessRequestPacket(getClass().getName() + " informed", 0);
147         if (!validate(packet)) {
148           // No Validation therefore send the new packet back
149           try {
150             ChannelUtils.writeAbstractLocalPacket(
151                 session.getLocalChannelReference(), packet, true);
152           } catch (final OpenR66ProtocolPacketException ignored) {
153             // nothing
154           }
155         }
156         status = 0;
157       }
158       finalValidate(validated);
159     } else {
160       // unallowed
161       logger.warn("SpooledInformTask not allowed as Java Task: " + fullarg);
162       invalid();
163     }
164   }
165 
166   /**
167    * @param detailed
168    * @param status 1 for ok, -1 for ko, 0 for all
169    * @param uri
170    *
171    * @return the StringBuilder containing the HTML format as a Table of the
172    *     current Spooled information
173    */
174   public static StringBuilder buildSpooledTable(final boolean detailed,
175                                                 final int status,
176                                                 final String uri) {
177     final StringBuilder builder = beginSpooledTable(detailed, uri);
178     // get current information
179     synchronized (spooledInformationMap) {
180       final Set<String> names = spooledInformationMap.keySet();
181       for (final String name : names) {
182         // per Name
183         buildSpooledTableElement(detailed, status, builder, name);
184       }
185     }
186     endSpooledTable(builder);
187     return builder;
188   }
189 
190   /**
191    * @param name
192    * @param uri
193    *
194    * @return the StringBuilder containing the HTML format as a Table of the
195    *     current Spooled information
196    */
197   public static StringBuilder buildSpooledUniqueTable(final String uri,
198                                                       final String name) {
199     final StringBuilder builder = beginSpooledTable(false, uri);
200     // get current information
201     synchronized (spooledInformationMap) {
202       // per Name
203       final SpooledInformation inform =
204           buildSpooledTableElement(false, 0, builder, name);
205       endSpooledTable(builder);
206       builder.append("<BR>");
207       if (inform != null) {
208         buildSpooledTableFiles(builder, inform);
209       }
210     }
211     return builder;
212   }
213 
214   /**
215    * @param builder
216    */
217   private static void endSpooledTable(final StringBuilder builder) {
218     builder.append("</TBODY></TABLE></small>");
219   }
220 
221   /**
222    * @param detailed
223    * @param uri
224    *
225    * @return the associated StringBuilder as temporary result
226    */
227   private static StringBuilder beginSpooledTable(final boolean detailed,
228                                                  final String uri) {
229     final StringBuilder builder = new StringBuilder();
230     builder.append(
231         "<small><TABLE class='table table-condensed table-bordered' BORDER=1><CAPTION><A HREF=");
232     builder.append(uri);
233     if (detailed) {
234       builder.append(
235           Messages.getString("SpooledInformTask.TitleDetailed")); //$NON-NLS-1$
236     } else {
237       builder.append(
238           Messages.getString("SpooledInformTask.TitleNormal")); //$NON-NLS-1$
239     }
240     // title first
241     builder.append("<THEAD><TR><TH>")
242            .append(Messages.getString("SpooledInformTask.0")) //$NON-NLS-1$
243            .append(TH_TH)
244            .append(Messages.getString("SpooledInformTask.1")) //$NON-NLS-1$
245            .append(TH_TH)
246            .append(Messages.getString("SpooledInformTask.2")) //$NON-NLS-1$
247            .append(TH_TH)
248            .append(Messages.getString("SpooledInformTask.3")) //$NON-NLS-1$
249            .append(TH_TH)
250            .append(Messages.getString("SpooledInformTask.4")) //$NON-NLS-1$
251            .append(TH_TH)
252            .append(Messages.getString("SpooledInformTask.5")) //$NON-NLS-1$
253            .append(TH_TH)
254            .append(Messages.getString("SpooledInformTask.6")) //$NON-NLS-1$
255            .append(TH_TH)
256            .append(Messages.getString("SpooledInformTask.7")) //$NON-NLS-1$
257            .append(TH_TH)
258            .append(Messages.getString("SpooledInformTask.8")) //$NON-NLS-1$
259            .append(TH_TH)
260            .append(Messages.getString("SpooledInformTask.9")) //$NON-NLS-1$
261            .append("</TH></TR></THEAD><TBODY>");
262     return builder;
263   }
264 
265   /**
266    * @param detailed
267    * @param status
268    * @param builder
269    * @param name
270    */
271   private static SpooledInformation buildSpooledTableElement(
272       final boolean detailed, final int status, final StringBuilder builder,
273       final String name) {
274     final SpooledInformation inform = spooledInformationMap.get(name);
275     if (inform == null) {
276       return null;
277     }
278     final long time = inform.lastUpdate.getTime() +
279                       Configuration.configuration.getTimeoutCon();
280     final long curtime = System.currentTimeMillis();
281     if (time + Configuration.configuration.getTimeoutCon() < curtime) {
282       if (status > 0) {
283         return inform;
284       }
285     } else {
286       if (status < 0) {
287         return inform;
288       }
289     }
290     builder.append("<TR><TH>").append(name.replace(',', ' '))
291            .append("</TH><TD>").append(inform.host).append("</TD>");
292     if (time + Configuration.configuration.getTimeoutCon() < curtime) {
293       builder.append("<TD bgcolor=Red>");
294     } else if (time < curtime) {
295       builder.append("<TD bgcolor=Orange>");
296     } else {
297       builder.append("<TD bgcolor=LightGreen>");
298     }
299     final DateFormat dateFormat =
300         DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.LONG);
301     builder.append(dateFormat.format(inform.lastUpdate)).append("</TD>");
302     if (inform.fileMonitorInformation != null) {
303       builder.append(
304                  Messages.getString("SpooledInformTask.AllOk")) //$NON-NLS-1$
305              .append(inform.fileMonitorInformation.globalok).append(
306                  Messages.getString("SpooledInformTask.AllError")) //$NON-NLS-1$
307              .append(inform.fileMonitorInformation.globalerror).append(
308                  Messages.getString("SpooledInformTask.TodayOk")) //$NON-NLS-1$
309              .append(inform.fileMonitorInformation.todayok).append(
310                  Messages.getString("SpooledInformTask.TodayError")) //$NON-NLS-1$
311              .append(inform.fileMonitorInformation.todayerror).append(TD_TD)
312              .append(inform.fileMonitorInformation.elapseTime).append(TD_TD)
313              .append(inform.fileMonitorInformation.stopFile).append(TD_TD)
314              .append(inform.fileMonitorInformation.statusFile).append(TD_TD)
315              .append(inform.fileMonitorInformation.scanSubDir).append("</TD>");
316       final StringBuilder dirs =
317           new StringBuilder("<ul class='list-unstyled'>");
318       for (final File dir : inform.fileMonitorInformation.directories) {
319         dirs.append("<li>").append(dir).append("</li>");
320       }
321       dirs.append("</ul>");
322       builder.append("<TD>").append(dirs).append(TD_TD);
323       if (detailed && inform.fileMonitorInformation.fileItems != null) {
324         buildSpooledTableFiles(builder, inform);
325       } else {
326         // simply print number of files
327         if (inform.fileMonitorInformation.fileItems != null) {
328           builder.append(inform.fileMonitorInformation.fileItems.size());
329         } else {
330           builder.append(0);
331         }
332         // Form GET to ensure encoding
333         builder.append(
334                    "<FORM class='form-inline' name='DETAIL' method='GET' action='/SpooledDetailed.html'><input type=hidden name='name' value='")
335                .append(name).append(
336                    "'/><INPUT type='submit' class='btn btn-info btn-sm' value='DETAIL'/></FORM>");
337       }
338     }
339     builder.append("</TD></TR>");
340     return inform;
341   }
342 
343   /**
344    * @param builder
345    * @param inform
346    */
347   private static void buildSpooledTableFiles(final StringBuilder builder,
348                                              final SpooledInformation inform) {
349     builder.append(
350                "<small><TABLE class='table table-condensed table-bordered' BORDER=1><THEAD><TR><TH>") //$NON-NLS-1$
351            .append(Messages.getString("SpooledInformTask.10")).append(TH_TH)
352            .append(Messages.getString("SpooledInformTask.11")) //$NON-NLS-2$
353            .append(TH_TH)
354            .append(Messages.getString("SpooledInformTask.12")) //$NON-NLS-1$
355            .append(TH_TH)
356            .append(Messages.getString("SpooledInformTask.13")) //$NON-NLS-1$
357            .append(TH_TH)
358            .append(Messages.getString("SpooledInformTask.14")) //$NON-NLS-1$
359            .append(TH_TH)
360            .append(Messages.getString("SpooledInformTask.15")) //$NON-NLS-1$
361            .append("</TH></TR></THEAD><TBODY>");
362     final DateFormat dateFormat =
363         DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.LONG);
364     for (final FileItem fileItem : inform.fileMonitorInformation.fileItems.values()) {
365       builder.append("<TR><TD>").append(fileItem.file).append(TD_TD);
366       if (fileItem.hash != null) {
367         builder.append(FilesystemBasedDigest.getHex(fileItem.hash));
368       }
369       builder.append(TD_TD);
370       if (fileItem.lastTime > 0) {
371         builder.append(dateFormat.format(new Date(fileItem.lastTime)));
372       }
373       builder.append(TD_TD);
374       if (fileItem.timeUsed > 0) {
375         builder.append(dateFormat.format(new Date(fileItem.timeUsed)));
376       }
377       builder.append(TD_TD).append(fileItem.used).append(TD_TD)
378              .append(fileItem.specialId).append("</TD></TR>");
379     }
380     builder.append("</TBODY></TABLE></small>");
381   }
382 
383   /**
384    * @param detailed
385    * @param status 1 for ok, -1 for ko, 0 for all
386    * @param uri
387    *
388    * @return the String containing the JSON format of the current Spooled
389    *     information
390    */
391   public static String buildSpooledJson(final boolean detailed,
392                                         final int status, final String uri) {
393     final ArrayNode array = JsonHandler.createArrayNode();
394     // get current information
395     synchronized (spooledInformationMap) {
396       final Set<String> names = spooledInformationMap.keySet();
397       for (final String name : names) {
398         // per Name
399         buildSpooledJsonElement(detailed, status, array, name, false);
400       }
401     }
402     return WaarpStringUtils.cleanJsonForHtml(JsonHandler.writeAsString(array));
403   }
404 
405   /**
406    * @param name
407    * @param uri
408    *
409    * @return the String containing the JSON format of the current Spooled
410    *     information
411    */
412   public static String buildSpooledUniqueJson(final String uri,
413                                               final String name) {
414     final ArrayNode array = JsonHandler.createArrayNode();
415     // get current information
416     synchronized (spooledInformationMap) {
417       // per Name
418       buildSpooledJsonElement(true, 0, array, name, false);
419     }
420     return WaarpStringUtils.cleanJsonForHtml(JsonHandler.writeAsString(array));
421   }
422 
423   /**
424    * For REST V2 service
425    *
426    * @param status if 0, get all Spooled, 1 active ones, -1 inactive ones
427    * @param name if null or empty, will get all Spooled
428    *
429    * @return the total number of valid "Entries" from request arguments of
430    *     SpooledInformation
431    */
432   public static int getSpooledJsonEntriesNumber(final int status,
433                                                 final String name) {
434     int nb = 0;
435     if (ParametersChecker.isEmpty(name)) {
436       synchronized (spooledInformationMap) {
437         final Set<String> names = spooledInformationMap.keySet();
438         for (final String nameInternal : names) {
439           // per Name
440           if (checkValidEntrySpooled(status, nameInternal) != null) {
441             nb++;
442           }
443         }
444       }
445     } else {
446       synchronized (spooledInformationMap) {
447         // per Name
448         nb = checkValidEntrySpooled(status, name) != null? 1 : 0;
449       }
450     }
451     return nb;
452   }
453 
454   /**
455    * For REST V2 service
456    *
457    * @param array the ArrayNode to fill with the results
458    * @param status if 0, get all Spooled, 1 active ones, -1 inactive ones
459    * @param name if null or empty, will get all Spooled
460    *
461    * @return the total number of "files" from request arguments of
462    *     SpooledInformation
463    */
464   public static int buildSpooledJson(final ArrayNode array, final int status,
465                                      final String name) {
466     int nb = 0;
467     if (ParametersChecker.isEmpty(name)) {
468       synchronized (spooledInformationMap) {
469         final Set<String> names = spooledInformationMap.keySet();
470         for (final String nameInternal : names) {
471           // per Name
472           nb +=
473               buildSpooledJsonElement(true, status, array, nameInternal, true);
474         }
475       }
476     } else {
477       synchronized (spooledInformationMap) {
478         // per Name
479         nb += buildSpooledJsonElement(true, status, array, name, true);
480       }
481     }
482     return nb;
483   }
484 
485   /**
486    * Check if this entry is valid according to status and time
487    *
488    * @param status
489    * @param name
490    *
491    * @return the SpooledInformation if OK, else null
492    */
493   private static SpooledInformation checkValidEntrySpooled(final int status,
494                                                            final String name) {
495     final SpooledInformation inform = spooledInformationMap.get(name);
496     if (inform == null) {
497       return null;
498     }
499     final long time = inform.lastUpdate.getTime() +
500                       Configuration.configuration.getTimeoutCon();
501     final long curtime = System.currentTimeMillis();
502     if (status != 0) {
503       if (time + Configuration.configuration.getTimeoutCon() < curtime) {
504         if (status < 0) {
505           return null;
506         }
507       } else if (status > 0) {
508         return null;
509       }
510     }
511     return inform;
512   }
513 
514   /**
515    * @param detailed
516    * @param status
517    * @param array
518    * @param name
519    * @param strict
520    *
521    * @return number of files within
522    */
523   private static int buildSpooledJsonElement(final boolean detailed,
524                                              final int status,
525                                              final ArrayNode array,
526                                              final String name,
527                                              final boolean strict) {
528     final SpooledInformation inform = checkValidEntrySpooled(status, name);
529     if (inform == null) {
530       return 0;
531     }
532     final long time = inform.lastUpdate.getTime() +
533                       Configuration.configuration.getTimeoutCon();
534     final long curtime = System.currentTimeMillis();
535     int nb = 0;
536     if (array == null) {
537       if (inform.fileMonitorInformation != null &&
538           inform.fileMonitorInformation.fileItems != null) {
539         nb = inform.fileMonitorInformation.fileItems.size();
540       }
541     } else {
542       final ObjectNode elt = JsonHandler.createObjectNode();
543       elt.put("NAME", name.replace(',', ' '));
544       elt.put("HOST", inform.host);
545       if (strict) {
546         elt.put("LAST_UPDATE", inform.lastUpdate.getTime());
547       } else {
548         final String val;
549         if (time + Configuration.configuration.getTimeoutCon() < curtime) {
550           val = "bg-danger";
551         } else if (time < curtime) {
552           val = "bg-warning";
553         } else {
554           val = "bg-success";
555         }
556         elt.put("LAST_UPDATE", val + ' ' + inform.lastUpdate.getTime());
557       }
558       if (inform.fileMonitorInformation != null) {
559         elt.put("GLOBALOK", inform.fileMonitorInformation.globalok.get());
560         elt.put("GLOBALERROR", inform.fileMonitorInformation.globalerror.get());
561         elt.put("TODAYOK", inform.fileMonitorInformation.todayok.get());
562         elt.put("TODAYERROR", inform.fileMonitorInformation.todayerror.get());
563         elt.put("INTERVAL", inform.fileMonitorInformation.elapseTime);
564         elt.put("STOPFILE", inform.fileMonitorInformation.stopFile.getPath());
565         elt.put("STATUSFILE",
566                 inform.fileMonitorInformation.statusFile.getPath());
567         elt.put("SUBDIRS", inform.fileMonitorInformation.scanSubDir);
568         final StringBuilder dirs = new StringBuilder();
569         final StringBuilder dirs2 = new StringBuilder();
570         int i = 0;
571         for (final File dir : inform.fileMonitorInformation.directories) {
572           i++;
573           dirs.append(dir).append('(').append(i).append(") ");
574           dirs2.append(dir).append(' ');
575         }
576         elt.put("DIRECTORIES", dirs.toString());
577         if (detailed && inform.fileMonitorInformation.fileItems != null) {
578           buildSpooledJsonFiles(elt, inform, dirs2.toString().split(" "),
579                                 strict);
580         } else {
581           // simply print number of files
582           if (inform.fileMonitorInformation.fileItems != null) {
583             elt.putArray("FILES")
584                .add(inform.fileMonitorInformation.fileItems.size());
585           } else {
586             elt.putArray("FILES").add(0);
587           }
588         }
589         nb = inform.fileMonitorInformation.fileItems.size();
590       }
591       array.add(elt);
592     }
593     return nb;
594   }
595 
596   /**
597    * @param node
598    * @param inform
599    * @param dirs
600    */
601   private static void buildSpooledJsonFiles(final ObjectNode node,
602                                             final SpooledInformation inform,
603                                             final String[] dirs,
604                                             final boolean strict) {
605     final ArrayNode array = node.putArray("FILES");
606     if (inform.fileMonitorInformation.fileItems.isEmpty()) {
607       array.add(0);
608       return;
609     }
610     if (!strict) {
611       final ArrayNode header = JsonHandler.createArrayNode();
612       header.add("FILE");
613       header.add("HASH");
614       header.add("LASTTIME");
615       header.add("USEDTIME");
616       header.add("USED");
617       header.add("ID");
618       array.add(header);
619     }
620     for (final FileItem fileItem : inform.fileMonitorInformation.fileItems.values()) {
621       final ObjectNode elt = JsonHandler.createObjectNode();
622       int i = 0;
623       String path = fileItem.file.getPath();
624       final String sep = path.lastIndexOf('/') >= 0? "/" : "\\";
625       for (final String dir : dirs) {
626         i++;
627         if (path.startsWith(dir + sep)) {
628           path = "(" + i + ')' + path.substring(dir.length());
629           break;
630         }
631       }
632       elt.put("FILE", path);
633       if (fileItem.hash != null) {
634         elt.put("HASH", FilesystemBasedDigest.getHex(fileItem.hash));
635       } else {
636         elt.putNull("HASH");
637       }
638       if (fileItem.lastTime > 0) {
639         elt.put("LASTTIME", fileItem.lastTime);
640       } else {
641         elt.putNull("LASTTIME");
642       }
643       if (fileItem.timeUsed > 0) {
644         elt.put("USEDTIME", fileItem.timeUsed);
645       } else {
646         elt.putNull("USEDTIME");
647       }
648       elt.put("USED", fileItem.used);
649       if (fileItem.specialId == ILLEGALVALUE) {
650         elt.put("ID", "");
651       } else {
652         elt.put("ID", Long.toString(fileItem.specialId));
653       }
654       array.add(elt);
655     }
656   }
657 }