OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 149 | Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
144 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.erp.core.sales.pos.model;
15
 
16
import org.openconcerto.erp.core.sales.pos.POSConfiguration;
17
import org.openconcerto.erp.core.sales.pos.model.RegisterLog.EventType;
18
import org.openconcerto.erp.core.sales.pos.model.RegisterLogEntry.ReceiptEntry;
19
import org.openconcerto.erp.core.sales.pos.model.RegisterState.Status;
20
import org.openconcerto.utils.CompareUtils;
21
import org.openconcerto.utils.FileUtils;
22
import org.openconcerto.utils.MessageDigestUtils;
23
import org.openconcerto.utils.StringUtils;
24
import org.openconcerto.utils.cc.ExnTransformer;
25
import org.openconcerto.utils.checks.ValidState;
26
 
27
import java.io.BufferedInputStream;
28
import java.io.IOException;
29
import java.io.InputStream;
30
import java.io.RandomAccessFile;
31
import java.nio.file.DirectoryStream;
32
import java.nio.file.FileVisitResult;
33
import java.nio.file.Files;
34
import java.nio.file.LinkOption;
35
import java.nio.file.Path;
36
import java.nio.file.SimpleFileVisitor;
37
import java.nio.file.StandardCopyOption;
38
import java.nio.file.attribute.BasicFileAttributes;
39
import java.security.DigestInputStream;
40
import java.security.DigestOutputStream;
41
import java.sql.SQLException;
42
import java.text.ParseException;
43
import java.util.ArrayList;
44
import java.util.Arrays;
45
import java.util.Calendar;
46
import java.util.Collections;
47
import java.util.Comparator;
48
import java.util.Date;
49
import java.util.List;
50
import java.util.SortedSet;
51
import java.util.TreeSet;
52
import java.util.logging.Level;
53
import java.util.regex.Pattern;
54
 
55
import org.jdom2.Document;
56
import org.jdom2.Element;
57
import org.jdom2.JDOMException;
58
import org.jdom2.input.SAXBuilder;
59
import org.jdom2.output.Format;
60
import org.jdom2.output.XMLOutputter;
61
 
62
/**
63
 * <pre>
64
lockFile
65
2017
66
  12
67
    18/current or if not there previous
68
       log.xml
69
       log.xml.hash
70
       0218121700001.xml
71
       0218121700001.xml.hash
72
 * </pre>
73
 */
74
public class RegisterFiles {
75
 
76
    private static final String REGISTER_DIRNAME = "register";
77
    public static final String STRUCT_VERSION_2013 = "v20131206";
78
    public static final String STRUCT_VERSION = "v20171220";
79
 
80
    private static final String LOG_FILENAME = "log.xml";
81
    static final String HASH_SUFFIX = ".hash";
82
    private static final String LOG_HASH_FILENAME = LOG_FILENAME + HASH_SUFFIX;
83
 
84
    static private final Comparator<Path> FILENAME_COMPARATOR = new Comparator<Path>() {
85
        @Override
86
        public int compare(Path p1, Path p2) {
87
            return p1.getFileName().toString().compareTo(p2.getFileName().toString());
88
        }
89
    };
90
    static private final Comparator<Path> PATH_COMPARATOR = new Comparator<Path>() {
91
        @Override
92
        public int compare(Path p1, Path p2) {
93
            return p1.toString().compareTo(p2.toString());
94
        }
95
    };
96
 
97
    static private final Path getGreatestSubDir(final Path dir) throws IOException {
98
        Path res = null;
99
        if (dir != null && Files.exists(dir, LinkOption.NOFOLLOW_LINKS)) {
100
            try (final DirectoryStream<Path> subdirs = Files.newDirectoryStream(dir, FileUtils.DIR_PATH_FILTER)) {
101
                for (final Path subdir : subdirs) {
102
                    if (res == null || FILENAME_COMPARATOR.compare(subdir, res) > 0) {
103
                        res = subdir;
104
                    }
105
                }
106
            }
107
        }
108
        return res;
109
    }
110
 
111
    static private final ValidState canReadFile(final Path f, final String missingString, final String missingPermString) throws IOException {
112
        if (!Files.isRegularFile(f))
113
            return ValidState.createCached(false, missingString);
114
        if (!Files.isReadable(f))
115
            return ValidState.createCached(false, missingPermString);
116
        return ValidState.getTrueInstance();
117
    }
118
 
119
    static public final byte[] save(final Document doc, final Path f) throws IOException {
120
        final XMLOutputter out = new XMLOutputter(Format.getPrettyFormat());
121
        final byte[] res;
122
        try (final DigestOutputStream digestStream = new DigestOutputStream(Files.newOutputStream(f), MessageDigestUtils.getSHA256())) {
123
            out.output(doc, digestStream);
124
            res = digestStream.getMessageDigest().digest();
125
        }
126
        Files.write(f.resolveSibling(f.getFileName() + HASH_SUFFIX), MessageDigestUtils.asHex(res).getBytes(StringUtils.UTF8));
127
        return res;
128
    }
129
 
130
    static public final class HashMode {
131
 
132
        static public final HashMode NOT_REQUIRED = new HashMode(false, null);
133
        static public final HashMode REQUIRED = new HashMode(true, null);
134
 
135
        static public final HashMode equalTo(final String hashRequired) {
136
            return new HashMode(true, hashRequired);
137
        }
138
 
139
        private final boolean hashFileRequired;
140
        private final String hashRequired;
141
 
142
        private HashMode(boolean hashFileRequired, String hashRequired) {
143
            super();
144
            this.hashFileRequired = hashFileRequired;
145
            this.hashRequired = hashRequired;
146
        }
147
    }
148
 
149
    static public final Document parse(final Path f) throws IOException, JDOMException {
150
        return parse(f, HashMode.REQUIRED);
151
    }
152
 
153
    static public final Document parse(final Path f, final HashMode hashMode) throws IOException, JDOMException {
154
        final byte[] hash;
155
        final Path logHashFile = f.resolveSibling(f.getFileName() + HASH_SUFFIX);
156
        if (Files.isRegularFile(logHashFile)) {
157
            final String hashString = Files.readAllLines(logHashFile, StringUtils.UTF8).get(0);
158
            if (hashMode.hashRequired != null && !hashString.equals(hashMode.hashRequired))
159
                throw new IllegalStateException("Required hash doesn't match recorded hash");
160
            hash = MessageDigestUtils.fromHex(hashString);
161
            assert hash != null;
162
        } else if (hashMode.hashFileRequired) {
163
            throw new IllegalStateException("Missing required hash file for " + f);
164
        } else {
165
            hash = null;
166
        }
167
        final Document doc;
168
        try (final InputStream ins = new BufferedInputStream(Files.newInputStream(f, LinkOption.NOFOLLOW_LINKS));
169
                final DigestInputStream dIns = new DigestInputStream(ins, MessageDigestUtils.getSHA256())) {
170
            doc = new SAXBuilder().build(dIns);
171
            if (hash != null && !Arrays.equals(hash, dIns.getMessageDigest().digest()))
172
                throw new IOException("File hash doesn't match recorded hash for " + f);
173
        }
174
        return doc;
175
    }
176
 
177
    static private final Pattern DIGITS_PATTERN = Pattern.compile("[0-9]+");
178
 
179
    static public final List<RegisterFiles> scan(final Path rootDir) throws IOException {
180
        final Path registersDir = rootDir.resolve(REGISTER_DIRNAME);
181
        if (!Files.exists(registersDir))
182
            return Collections.emptyList();
183
        final List<RegisterFiles> res = new ArrayList<>();
184
        try (final DirectoryStream<Path> stream = Files.newDirectoryStream(registersDir, new DirectoryStream.Filter<Path>() {
185
            @Override
186
            public boolean accept(Path entry) throws IOException {
187
                return DIGITS_PATTERN.matcher(entry.getFileName().toString()).matches();
188
            }
189
        })) {
190
            for (final Path registerDir : stream) {
191
                if (Files.isDirectory(registerDir.resolve(STRUCT_VERSION))) {
192
                    res.add(new RegisterFiles(rootDir, true, Integer.parseInt(registerDir.getFileName().toString())));
193
                }
194
            }
195
        }
196
        return res;
197
    }
198
 
199
    // under that year/month/day
200
    private final Path rootDir;
201
    private final boolean useHardLinks;
202
    private final int posID;
203
    private final ThreadLocal<Boolean> hasLock = new ThreadLocal<Boolean>() {
204
        protected Boolean initialValue() {
205
            return Boolean.FALSE;
206
        };
207
    };
208
 
209
    public RegisterFiles(final Path rootDir, final boolean useHardLinks, final int caisse) {
210
        super();
211
        this.rootDir = rootDir.resolve(REGISTER_DIRNAME).resolve(Integer.toString(caisse));
212
        this.useHardLinks = useHardLinks;
213
        this.posID = caisse;
214
    }
215
 
216
    private final Path getRootDir() {
217
        return this.rootDir;
218
    }
219
 
220
    public final Path getVersionDir() {
221
        return this.getRootDir().resolve(STRUCT_VERSION);
222
    }
223
 
224
    public final Path getDayDir(final Calendar day, final boolean create) {
225
        return ReceiptCode.getDayDir(getVersionDir().toFile(), day, create).toPath();
226
    }
227
 
228
    public final Path getReceiptFile(final ReceiptCode code) throws IOException {
229
        final Path dayDirToUse = getDayDirToUse(getDayDir(code.getDay(), false));
230
        return dayDirToUse == null ? null : dayDirToUse.resolve(code.getFileName());
231
    }
232
 
233
    public final int getPosID() {
234
        return this.posID;
235
    }
236
 
237
    public <T, Exn extends Exception> T doWithLock(final ExnTransformer<RegisterFiles, T, Exn> transf) throws IOException, Exn {
238
        if (this.hasLock.get())
239
            throw new IllegalStateException("Already locked");
240
        this.hasLock.set(Boolean.TRUE);
241
        try {
242
            return FileUtils.doWithLock(this.getRootDir().resolve("lockFile").toFile(), new ExnTransformer<RandomAccessFile, T, Exn>() {
243
                @Override
244
                public T transformChecked(RandomAccessFile input) throws Exn {
245
                    return transf.transformChecked(RegisterFiles.this);
246
                }
247
            });
248
        } finally {
249
            this.hasLock.set(Boolean.FALSE);
250
        }
251
    }
252
 
253
    public final RegisterLog getLastLog() throws IOException, JDOMException {
254
        final Path lastLogFile = this.findLastLogFile();
255
        if (lastLogFile == null)
256
            return null;
257
        return new RegisterLog(lastLogFile).parse();
258
    }
259
 
260
    public final Path findLastLogFile() throws IOException {
261
        final Path versionDir = this.getVersionDir();
262
 
263
        // first quick search
264
        final Path yearDir = getGreatestSubDir(versionDir);
265
        final Path monthDir = getGreatestSubDir(yearDir);
266
        if (monthDir != null) {
267
            final SortedSet<Path> sortedDays = new TreeSet<>(Collections.reverseOrder(FILENAME_COMPARATOR));
268
            try (final DirectoryStream<Path> dayDirs = Files.newDirectoryStream(monthDir, FileUtils.DIR_PATH_FILTER)) {
269
                for (final Path dayDir : dayDirs) {
270
                    sortedDays.add(dayDir);
271
                }
272
            }
273
            Path logToUse = getLogToUse(sortedDays);
274
            if (logToUse != null)
275
                return logToUse;
276
 
277
            // then walk the whole tree before giving up
278
            logToUse = getLogToUse(getSortedDays(false));
279
            if (logToUse != null)
280
                return logToUse;
281
        }
282
 
283
        return null;
284
    }
285
 
286
    private SortedSet<Path> getSortedDays(final boolean chronological) throws IOException {
287
        final Path versionDir = this.getVersionDir();
288
        final SortedSet<Path> sortedPaths = new TreeSet<>(chronological ? PATH_COMPARATOR : Collections.reverseOrder(PATH_COMPARATOR));
289
        if (Files.exists(versionDir)) {
290
            Files.walkFileTree(versionDir, new SimpleFileVisitor<Path>() {
291
                @Override
292
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
293
                    final Path dateDir = versionDir.relativize(dir);
294
                    if (dateDir.getNameCount() == 3) {
295
                        sortedPaths.add(dir);
296
                        return FileVisitResult.SKIP_SUBTREE;
297
                    } else {
298
                        return FileVisitResult.CONTINUE;
299
                    }
300
                }
301
            });
302
        }
303
        return sortedPaths;
304
    }
305
 
306
    private Path getLogToUse(final SortedSet<Path> sortedDays) throws IOException {
307
        for (final Path dayDir : sortedDays) {
308
            final Path dayDirToUse = getDayDirToUse(dayDir);
309
            if (dayDirToUse != null)
310
                return dayDirToUse.resolve(LOG_FILENAME);
311
        }
312
        return null;
313
    }
314
 
315
    public final List<Path> findLogFiles() throws IOException {
316
        final List<Path> res = new ArrayList<>();
317
        for (final Path dayDir : getSortedDays(true)) {
318
            final Path dayDirToUse = getDayDirToUse(dayDir);
319
            if (dayDirToUse != null)
320
                res.add(dayDirToUse.resolve(LOG_FILENAME));
321
        }
322
        return res;
323
    }
324
 
325
    private final Path getDayDirToUse(final Path dayDir) throws IOException {
326
        for (final Path subdir : new Path[] { dayDir.resolve("current"), dayDir.resolve("previous") }) {
327
            if (Files.exists(subdir)) {
328
                final ValidState validity = getDayDirValidity(subdir);
329
                if (validity.isValid())
330
                    return subdir;
331
                else
332
                    throw new IOException("Invalid " + subdir + " : " + validity.getValidationText());
333
            }
334
        }
335
        return null;
336
    }
337
 
338
    private final ValidState getDayDirValidity(final Path dayDir) throws IOException {
339
        if (!Files.isDirectory(dayDir))
340
            return ValidState.createCached(false, "Not a directory");
341
        final ValidState canReadLog = canReadFile(dayDir.resolve(LOG_FILENAME), "Missing log file", "Unreadable log file");
342
        if (!canReadLog.isValid())
343
            return canReadLog;
344
        final ValidState canReadLogHash = canReadFile(dayDir.resolve(LOG_HASH_FILENAME), "Missing log hash file", "Unreadable log hash file");
345
        if (!canReadLogHash.isValid())
346
            return canReadLogHash;
347
        return ValidState.getTrueInstance();
348
    }
349
 
350
    public final RegisterLog open(final int userID, final DBState dbState) throws IOException {
351
        if (!this.hasLock.get())
352
            throw new IllegalStateException("Not locked");
353
        return createOpen(userID, null, dbState).transformChecked(this);
354
    }
355
 
356
    public final RegisterLog open(final int userID, final RegisterDB registerDB) throws IOException {
357
        return this.doWithLock(createOpen(userID, registerDB, null));
358
    }
359
 
360
    static private final ExnTransformer<RegisterFiles, RegisterLog, IOException> createOpen(final int userID, final RegisterDB registerDB, final DBState passedDBState) throws IOException {
361
        // TODO use UpdateDir like save() and close()
362
        return new ExnTransformer<RegisterFiles, RegisterLog, IOException>() {
363
            @Override
364
            public RegisterLog transformChecked(RegisterFiles input) throws IOException {
365
                POSConfiguration.getLogger().log(Level.FINE, "Begin opening of FS state for register {0}", input.getPosID());
366
                POSConfiguration.checkRegisterID(input.getPosID(), registerDB.getPosID());
367
                final RegisterLog lastLog = input.checkStatus(true);
368
                final String lastLocalHash;
369
                if (lastLog == null) {
370
                    lastLocalHash = null;
371
                } else {
372
                    try {
373
                        lastLocalHash = lastLog.getLastReceiptHash();
374
                    } catch (ParseException e) {
375
                        throw new IOException("Couldn't parse last receipt of log", e);
376
                    }
377
                }
378
 
379
                final DBState dbState;
380
                if (passedDBState == null) {
381
                    try {
382
                        dbState = registerDB.open(lastLocalHash, userID);
383
                    } catch (SQLException e) {
384
                        throw new IOException("Couldn't open the register in the DB", e);
385
                    }
386
                } else {
387
                    dbState = passedDBState;
388
                }
389
                if (dbState.getRegisterState().getStatus() != Status.OPEN)
390
                    throw new IllegalArgumentException("DB not open : " + dbState);
391
 
392
                final Calendar cal = dbState.getLastEntry().getDate("DATE");
393
 
394
                // e.g. 2017/12/21/
395
                final Path dayDir = input.getDayDir(cal, true);
396
                // e.g. 2017/12/21/current/
397
                final Path dayDirToUse = input.getDayDirToUse(dayDir);
398
                if (dayDirToUse != null)
399
                    throw new IllegalStateException(cal.getTime() + " already open");
400
 
401
                final Path stagingDir = dayDir.resolve("staging");
402
                final Path currentDir = stagingDir.resolveSibling("current");
403
                final Path prevDir = stagingDir.resolveSibling("previous");
404
                FileUtils.rm_R(stagingDir);
405
                FileUtils.rm_R(currentDir);
406
                FileUtils.rm_R(prevDir);
407
                Files.createDirectory(stagingDir);
408
 
409
                final Element rootElem = new Element("registerLog");
410
                rootElem.addContent(new RegisterLogEntry.RegisterEntry(EventType.REGISTER_OPENING, cal.getTime(), userID, input.getPosID(), lastLocalHash).toXML());
411
                save(new Document(rootElem), stagingDir.resolve(LOG_FILENAME));
412
 
413
                Files.move(stagingDir, currentDir, StandardCopyOption.ATOMIC_MOVE);
414
 
415
                POSConfiguration.getLogger().log(Level.INFO, "Finished opening of FS state for register {0}", registerDB);
416
 
417
                // TODO parse and validate before moving into place
418
                try {
419
                    return new RegisterLog(currentDir.resolve(LOG_FILENAME)).parse();
420
                } catch (JDOMException e) {
421
                    throw new IOException("Couldn't parse new log");
422
                }
423
            }
424
        };
425
    }
426
 
427
    private abstract class UpdateDir<I, T> extends ExnTransformer<RegisterFiles, T, IOException> {
428
 
429
        private final String logMsg;
430
 
431
        public UpdateDir(final String logMsg) {
432
            this.logMsg = logMsg;
433
        }
434
 
435
        @Override
436
        public T transformChecked(RegisterFiles input) throws IOException {
437
            POSConfiguration.getLogger().log(Level.FINE, "Begin " + this.logMsg + " for register {0}", input.getPosID());
438
            final RegisterLog lastLog = checkStatus(needsClosed());
439
 
440
            // e.g. 2017/12/21/current/
441
            final Path toUse = lastLog.getLogFile().getParent();
442
            final Path stagingDir = toUse.resolveSibling("staging");
443
            final Path currentDir = stagingDir.resolveSibling("current");
444
            final Path prevDir = stagingDir.resolveSibling("previous");
445
 
446
            FileUtils.rm_R(stagingDir);
447
            FileUtils.copyDirectory(toUse, stagingDir, input.useHardLinks, StandardCopyOption.COPY_ATTRIBUTES);
448
 
449
            final I intermediateRes = updateDir(stagingDir, lastLog);
450
 
451
            if (Files.exists(currentDir)) {
452
                FileUtils.rm_R(prevDir);
453
                Files.move(currentDir, prevDir, StandardCopyOption.ATOMIC_MOVE);
454
            }
455
            assert !Files.exists(currentDir);
456
 
457
            Files.move(stagingDir, currentDir, StandardCopyOption.ATOMIC_MOVE);
458
 
459
            POSConfiguration.getLogger().log(Level.INFO, "Finished " + this.logMsg + " for register {0}", input.getPosID());
460
 
461
            assert Files.isDirectory(currentDir);
462
            try {
463
                FileUtils.rm_R(prevDir);
464
            } catch (Exception e) {
465
                // OK to leave behind some small files
466
                e.printStackTrace();
467
            }
468
 
469
            return createResult(currentDir, intermediateRes);
470
        }
471
 
472
        protected abstract boolean needsClosed();
473
 
474
        protected abstract I updateDir(final Path stagingDir, final RegisterLog lastLog) throws IOException;
475
 
476
        protected abstract T createResult(final Path currentDir, final I intermediateRes) throws IOException;
477
 
478
    }
479
 
480
    private final RegisterLog checkStatus(final boolean needsClosed) throws IOException {
481
        final RegisterLog lastLog;
482
        try {
483
            lastLog = getLastLog();
484
            final boolean closed = lastLog == null || lastLog.getLastRegisterEvent().getType() != EventType.REGISTER_OPENING;
485
            if (closed != needsClosed)
486
                throw new IllegalStateException(needsClosed ? "Not closed" : "Not open");
487
        } catch (JDOMException | ParseException e) {
488
            throw new IOException(e);
489
        }
490
        return lastLog;
491
    }
492
 
493
    public final RegisterLog close(final int userID) throws IOException {
494
        return this.doWithLock(new UpdateDir<Object, RegisterLog>("closure of FS state") {
495
            @Override
496
            protected boolean needsClosed() {
497
                return false;
498
            }
499
 
500
            @Override
501
            protected Object updateDir(final Path stagingDir, final RegisterLog lastLog) throws IOException {
502
                final String lastHash;
503
                try {
504
                    lastHash = lastLog.getLastReceiptHash();
505
                } catch (ParseException e) {
506
                    throw new IOException("Couldn't find last receipt hash", e);
507
                }
508
                // TODO verify that receipts' files match the log's content
509
                final Document doc = lastLog.getDocument().clone();
510
                doc.getRootElement().addContent(new RegisterLogEntry.RegisterEntry(EventType.REGISTER_CLOSURE, new Date(), userID, getPosID(), lastHash).toXML());
511
                save(doc, stagingDir.resolve(LOG_FILENAME));
512
                return null;
513
            }
514
 
515
            @Override
516
            protected RegisterLog createResult(final Path currentDir, final Object intermediateRes) throws IOException {
517
                // TODO parse and validate before moving into place
518
                try {
519
                    return new RegisterLog(currentDir.resolve(LOG_FILENAME)).parse();
520
                } catch (JDOMException e) {
521
                    throw new IOException("Couldn't parse new log");
522
                }
523
            }
524
        });
525
    }
526
 
527
    public final String save(final Ticket t) throws IOException, SQLException {
528
        return this.doWithLock(new UpdateDir<String, String>("saving receipt") {
529
            @Override
530
            protected boolean needsClosed() {
531
                return false;
532
            }
533
 
534
            @Override
535
            protected String updateDir(final Path stagingDir, final RegisterLog lastLog) throws IOException {
536
                try {
537
                    final ReceiptEntry lastReceipt = lastLog.getLastReceiptCreationEvent();
538
                    final int expectedIndex;
539
                    final String expectedHash = lastLog.getLastReceiptHash();
540
                    if (lastReceipt == null) {
541
                        expectedIndex = 1;
542
                    } else {
543
                        expectedIndex = lastReceipt.getCode().getDayIndex() + 1;
544
                    }
545
                    if (t.getReceiptCode().getDayIndex() != expectedIndex)
546
                        throw new IllegalStateException("Non consecutive number");
547
                    if (!CompareUtils.equals(expectedHash, t.getPreviousHash()))
548
                        throw new IllegalStateException("Previous hash mismatch, expected " + expectedHash + " but previous of receipt was " + t.getPreviousHash());
549
                } catch (ParseException e) {
550
                    throw new IOException("Couldn't parse last receipt of log", e);
551
                }
552
                // save receipt
553
                final String fileHash = MessageDigestUtils.asHex(t.saveToFile(stagingDir.resolve(t.getReceiptCode().getFileName())));
554
                // update log
555
                final Document doc = lastLog.getDocument().clone();
556
                doc.getRootElement().addContent(new RegisterLogEntry.ReceiptEntry(t.getCreationDate(), t.getReceiptCode(), fileHash).toXML());
557
                save(doc, stagingDir.resolve(LOG_FILENAME));
558
                return fileHash;
559
            }
560
 
561
            @Override
562
            protected String createResult(final Path currentDir, final String intermediateRes) {
563
                return intermediateRes;
564
            }
565
        });
566
    }
567
}