OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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