Dépôt officiel du code source de l'ERP OpenConcerto
Rev 151 | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
*
* The contents of this file are subject to the terms of the GNU General Public License Version 3
* only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
* copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each file.
*/
package org.openconcerto.erp.core.sales.pos.model;
import org.openconcerto.erp.config.ComptaPropsConfiguration;
import org.openconcerto.erp.core.sales.pos.POSConfiguration;
import org.openconcerto.erp.core.sales.pos.model.RegisterLog.EventType;
import org.openconcerto.erp.core.sales.pos.model.RegisterLogEntry.ReceiptEntry;
import org.openconcerto.erp.core.sales.pos.model.RegisterLogEntry.RegisterEntry;
import org.openconcerto.erp.core.sales.pos.model.RegisterState.Status;
import org.openconcerto.sql.model.Order;
import org.openconcerto.sql.model.SQLRow;
import org.openconcerto.sql.model.SQLRowListRSH;
import org.openconcerto.sql.model.SQLRowValues;
import org.openconcerto.sql.model.SQLSelect;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.TableRef;
import org.openconcerto.sql.model.Where;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.Value;
import java.io.IOException;
import java.nio.file.Path;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import org.jdom2.JDOMException;
public class CheckIntegrity {
public static void main(String[] args) throws JDOMException, IOException {
final POSConfiguration posConf = POSConfiguration.setInstance();
final ComptaPropsConfiguration conf = posConf.createConnexion();
try {
for (final RegisterFiles files : RegisterFiles.scan(posConf.getRootDir())) {
final RegisterDB registerDB = new RegisterDB(conf.getDirectory(), conf.getProductInfo(), files.getPosID());
checkRegisterFiles(registerDB, files);
checkRegisterRow(registerDB, files);
}
System.out.println("\n\nAll done");
} catch (Throwable e) {
e.printStackTrace();
} finally {
posConf.closeConnexion();
}
}
private static void checkRegisterRow(final RegisterDB registerDB, final RegisterFiles files) throws IOException, JDOMException, ParseException, SQLException {
final RegisterState localRegisterState = files.getLastLog().getRegisterState();
final DBState dbState = registerDB.fetchRegisterState();
final RegisterState remoteRegisterState = dbState.getRegisterState();
if (!localRegisterState.equals(remoteRegisterState))
System.out.println("WARNING FS and DB state not equal (this may be fixed by launching the application) :\n" + localRegisterState + "\n" + remoteRegisterState);
final SQLTable logT = registerDB.getLogElement().getTable();
final SQLSelect sel = new SQLSelect();
sel.addSelect(logT.getKey());
setWhereAndOrder(sel, registerDB, logT);
final Number lastLogID = (Number) logT.getDBSystemRoot().getDataSource().executeScalar(sel.asString());
final SQLRowValues registerLastEntry = dbState.getLastEntry();
final Number registerLastEntryID = registerLastEntry == null ? null : registerLastEntry.getIDNumber();
if (!Objects.equals(registerLastEntryID, lastLogID))
throw new IllegalStateException("Last log entry referenced by the register " + registerLastEntryID + " isn't the last in the log table " + lastLogID);
final SQLTable closureT = registerDB.getClosureElement().getTable();
final SQLSelect selClosure = new SQLSelect();
selClosure.addSelect(closureT.getKey());
final TableRef entryRef = selClosure.addJoin("INNER", closureT.getField("ID_ENTREE_JOURNAL")).getJoinedTable();
setWhereAndOrder(selClosure, registerDB, entryRef);
final Number lastClosureID = (Number) logT.getDBSystemRoot().getDataSource().executeScalar(selClosure.asString());
final SQLRowValues registerLastClosure = dbState.getLastClosure();
final Number registerLastClosureID = registerLastClosure == null ? null : registerLastClosure.getIDNumber();
if (!Objects.equals(registerLastClosureID, lastClosureID))
throw new IllegalStateException("Last closure referenced by the register " + registerLastClosureID + " isn't the last in its table " + lastClosureID);
}
private static void setWhereAndOrder(final SQLSelect sel, final RegisterDB registerDB, final TableRef entryRef) {
sel.setWhere(new Where(entryRef.getField("ID_CAISSE"), "=", registerDB.getPosID()));
sel.addFieldOrder(entryRef.getField("DATE"), Order.desc());
sel.setLimit(1);
}
private static void checkRegisterFiles(final RegisterDB registerDB, final RegisterFiles files) throws IOException, JDOMException, ParseException {
Value<RegisterLog> lastLog = Value.getNone();
for (final Path logFile : files.findLogFiles()) {
System.out.println("Checking " + logFile);
final RegisterLog log = new RegisterLog(logFile).parse();
try {
checkOneLog(files, lastLog, log, registerDB);
System.out.println("OK for " + logFile);
} catch (Exception e) {
// keep checking other files
System.err.println("Error for " + logFile);
e.printStackTrace();
}
lastLog = Value.getSome(log);
}
}
private static void checkOneLog(final RegisterFiles files, final Value<RegisterLog> previousLog, final RegisterLog log, final RegisterDB registerDB)
throws IOException, JDOMException, ParseException {
if (log.getFirstRegisterEvent().getRegisterID() != files.getPosID())
throw new IllegalStateException("Opening register ID mismatch");
final SQLTable logT = registerDB.getLogElement().getTable();
Date cal;
// this checks this log is chained to the previous
if (previousLog.hasValue()) {
final RegisterLogEntry lastEntry = previousLog.getValue().getLastEvent();
if (lastEntry.getType() != EventType.REGISTER_CLOSURE)
throw new IllegalStateException("Previous log isn't closed");
final RegisterEntry previousLogClosure = (RegisterEntry) lastEntry;
if (!Objects.equals(previousLogClosure.getLastReceiptHash(), log.getFirstRegisterEvent().getLastReceiptHash()))
throw new IllegalStateException("Register opening hash mismatch, chain broken");
if (RegisterFiles.isNotChronological(previousLogClosure.getDate(), log.getFirstRegisterEvent().getDate()))
throw new IllegalStateException("Register opening before previous closure");
cal = previousLogClosure.getDate();
} else {
cal = null;
}
final Date expectedPreviousDate;
// getPreviousDate() was added in version 2
if (log.getVersion() < 2 || !previousLog.hasValue()) {
expectedPreviousDate = null;
} else {
expectedPreviousDate = previousLog.getValue().getFirstRegisterEvent().getDate();
}
if (!Objects.equals(expectedPreviousDate, log.getFirstRegisterEvent().getPreviousDate()))
throw new IllegalStateException("Previous opening date of this log (" + log.getFirstRegisterEvent() + ") doesn't match the previous log " + expectedPreviousDate);
final List<ReceiptEntry> receiptEvents = log.getReceiptEvents();
// check all event dates are chronological
final List<RegisterLogEntry> allEvents = log.getAllEvents();
for (final RegisterLogEntry e : allEvents) {
final Date newDate = e.getDate();
if (cal != null && RegisterFiles.isNotChronological(cal, newDate))
throw new IllegalStateException("Later event before last one");
cal = newDate;
}
// check log opening was stored in the DB
checkLogTable(logT, log.getFirstRegisterEvent());
// this checks the hash chain and the coherence of the log and the receipts
final List<Ticket> receipts = log.parseReceipts();
// this checks the closure
if (log.getRegisterState().getStatus() == Status.CLOSED) {
final ReceiptEntry lastReceiptCreationEvent = log.getLastReceiptCreationEvent();
assert receipts.isEmpty() == (lastReceiptCreationEvent == null);
// parseReceipts() has already checked that lastReceiptCreationEvent.getFileHash()
// matches the receipt
// if there's no receipt in this log, opening and closure should match
// the opening has already been checked above to match the previous log
final String lastReceiptHash = receipts.isEmpty() ? log.getFirstRegisterEvent().getLastReceiptHash() : lastReceiptCreationEvent.getFileHash();
final RegisterEntry lastRegisterEvent = log.getLastRegisterEvent();
if (!CompareUtils.equals(lastRegisterEvent.getLastReceiptHash(), lastReceiptHash))
throw new IllegalStateException("Closure receipt hash mismatch, recorded " + lastRegisterEvent.getLastReceiptHash() + " but was " + lastReceiptHash);
if (lastRegisterEvent.getRegisterID() != files.getPosID())
throw new IllegalStateException("Closure register ID mismatch");
// check log closure was stored in the DB
final Number closureEntryID = checkLogTable(logT, lastRegisterEvent);
final SQLRow closureRow = getClosureRow(registerDB.getClosureElement().getTable(), lastRegisterEvent, closureEntryID);
// TODO this doesn't check dates
final SQLRowValues expected = RegisterDB.fillRow(new SQLRowValues(closureRow.getTable()), receiptEvents, receipts);
for (final String expectedField : expected.getFields()) {
final Object expectedVal = expected.getObject(expectedField);
final Object dbVal = closureRow.getObject(expectedField);
final boolean equals;
// needed for BigDecimal
if (expectedVal instanceof Comparable) {
equals = CompareUtils.compare(expectedVal, dbVal) == 0;
} else {
equals = Objects.equals(expectedVal, dbVal);
}
if (!equals)
throw new IllegalStateException("Closure row data doesn't match log for " + expectedField + " : " + expectedVal + " " + dbVal);
}
}
final SQLTable registerT = registerDB.getReceiptElement().getTable();
final SQLSelect selReceipts = new SQLSelect();
selReceipts.addSelectStar(registerT);
selReceipts.setWhere(new Where(registerT.getField("ID_CAISSE"), "=", files.getPosID()));
final Date lowerBound = log.getFirstRegisterEvent().getDate();
final Where dateWhere;
final int expectedRowsCount;
if (log.getRegisterState().getStatus() == Status.CLOSED) {
final Date upperBound = log.getLastEvent().getDate();
dateWhere = new Where(registerT.getField("DATE"), lowerBound, upperBound);
expectedRowsCount = receipts.size();
} else {
dateWhere = new Where(registerT.getField("DATE"), ">=", lowerBound);
// not yet in the DB
expectedRowsCount = 0;
}
selReceipts.andWhere(dateWhere);
selReceipts.addFieldOrder(registerT.getField("DATE"));
final List<SQLRow> receiptRows = SQLRowListRSH.execute(selReceipts);
if (receiptRows.size() != expectedRowsCount)
throw new IllegalStateException("Receipts count in the DB (" + receiptRows.size() + ") doesn't match log (" + expectedRowsCount + ")");
if (expectedRowsCount > 0) {
final Iterator<SQLRow> iter = receiptRows.iterator();
final Iterator<ReceiptEntry> receiptEventsIter = receiptEvents.iterator();
for (final Ticket receipt : receipts) {
final SQLRow row = iter.next();
final ReceiptEntry receiptEvent = receiptEventsIter.next();
try {
if (!row.getString("NUMERO").equals(receipt.getCode()))
throw new IllegalStateException("Code in the DB doesn't match log");
if (row.getDate("DATE").compareTo(receipt.getCreationCal()) != 0)
throw new IllegalStateException("Date in the DB doesn't match log");
if (!row.getString("FILE_HASH").equals(receiptEvent.getFileHash()))
throw new IllegalStateException("File hash in the DB doesn't match log : " + row.getString("FILE_HASH") + " != " + receiptEvent.getFileHash());
if (!Objects.equals(row.getString("FILE_HASH_PREVIOUS"), receipt.getPreviousHash()))
throw new IllegalStateException("Previous file hash in the DB doesn't match log");
if (row.getLong("TOTAL_TTC") != receipt.getTotalInCents())
throw new IllegalStateException("TTC in the DB " + row.getLong("TOTAL_TTC") + " doesn't match log " + receipt.getTotalInCents());
// ATTN the paid amount isn't stored in the DB by
// POSConfiguration.importReceipts() so we can't check it exactly
if (row.getLong("TOTAL_TTC") > receipt.getPaidTotal())
throw new IllegalStateException("Paid amount in the log (" + receipt.getPaidTotal() + ") is less than total in the DB " + row.getLong("TOTAL_TTC"));
} catch (Exception exn) {
throw new IllegalStateException("Error while checking " + row + " against " + receipt + " in " + log, exn);
}
}
assert !iter.hasNext() && !receiptEventsIter.hasNext();
}
}
// check log event was stored in the DB
private static final Number checkLogTable(final SQLTable logT, final RegisterEntry entry) {
final SQLSelect sel = new SQLSelect();
sel.addSelect(logT.getKey());
sel.setWhere(new Where(logT.getField("ID_CAISSE"), "=", entry.getRegisterID()));
sel.andWhere(new Where(logT.getField("EVT"), "=", (entry.getType() == EventType.REGISTER_OPENING ? Status.OPEN : Status.CLOSED).name()));
sel.andWhere(new Where(logT.getField("DATE"), "=", entry.getDate()));
final List<?> ids = logT.getDBSystemRoot().getDataSource().executeCol(sel.asString());
if (ids.size() != 1)
throw new IllegalStateException("Not found in the DB : " + entry);
return (Number) ids.get(0);
}
private static final SQLRow getClosureRow(final SQLTable closureT, final RegisterEntry entry, final Number closureEntryID) {
final SQLSelect sel = new SQLSelect();
sel.addSelectStar(closureT);
sel.setWhere(new Where(closureT.getField("ID_ENTREE_JOURNAL"), "=", closureEntryID));
final List<SQLRow> rows = SQLRowListRSH.execute(sel);
if (rows.size() != 1)
throw new IllegalStateException("Closure row not found for " + entry);
return rows.get(0);
}
}