Java tutorial
/* * $Id: DbLogService.java 327 2014-01-27 13:07:13Z blaser $ * * Copyright (C) 2006 by Bundesamt fr Justiz, Fachstelle fr Rechtsinformatik * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ package ch.admin.suis.msghandler.log; import ch.admin.suis.msghandler.util.DateUtils; import org.apache.commons.dbutils.DbUtils; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.ArrayListHandler; import org.apache.commons.dbutils.handlers.ScalarHandler; import java.io.*; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Timestamp; import java.text.MessageFormat; import java.util.*; /** * <p> The implementation of the * <code>LogService</code> interface that uses the HSQL to store the status of the messages. The DB files are located in * the directory specified by the * <code>base</code> property. The DB entries are periodically cleaned up. The number of days the entries are held in * the DB is determined by the * <code>maxAge</code> property. </p> * <p> * <p> This class is not thread-safe, but intended to be used as a singleton. So, additional measures need to be taken * if the synchronozation is required. </p> * * @author Alexander Nikiforov * @author $Author: blaser $ * @version $Revision: 327 $ */ public class DbLogService implements LogService { /** * logger */ private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger .getLogger(DbLogService.class.getName()); private static final String DB_FILE_NAME_PREFIX = "msghandler_log"; private static final int POS_PARTICIPANT_ID = 0; private static final int POS_FILENAME = 1; private static final int POS_MESSAGE_ID = 2; private static final int POS_SENT_DATE = 3; private static final int POS_RECEIVED_DATE = 4; private static final int POS_LOG_STATUS = 5; private static final int POS_MESSAGE_SOURCE = 6; private Connection connection; private QueryRunner runner; private String base; private int maxAge; private boolean resend; private static final String CREATE_TABLE_STATEMENT = "CREATE CACHED TABLE status (" + "participant_id VARCHAR(255) NOT NULL, " + "filename VARCHAR(255) NOT NULL, " + "message_id VARCHAR(255), " + "sent_date DATETIME, " + "received_date DATETIME, " + "status TINYINT NOT NULL, " + "source TINYINT NOT NULL, " + "UNIQUE (participant_id, filename)" + ")"; private static final String SELECT_STATEMENT = "SELECT status FROM status WHERE participant_id = ? AND filename = ?"; private static final String SELECT_SENT_DATE_STATEMENT = "SELECT sent_date FROM status WHERE message_id = ?"; private static final String SELECT_SOURCE_STATEMENT = "SELECT source FROM status WHERE message_id = ?"; private static final String SELECT_MESSAGE_ID_ALL_STATEMENT = "SELECT DISTINCT message_id FROM status WHERE status = ?"; private static final String SELECT_FILENAME_STATEMENT = "SELECT DISTINCT filename FROM status WHERE message_id = ?"; private static final String SELECT_ALL_STATEMENT = "SELECT * FROM status"; private static final String INSERT_STATEMENT = "INSERT INTO status(participant_id, filename, status, source) " + "VALUES(?, ?, ?, ?)"; private static final String UPDATE_SENT_STATEMENT = "UPDATE status SET " + "status = ?, sent_date = ?, received_date = NULL, message_id = ?, source = ? WHERE participant_id = ? AND filename = ?"; private static final String UPDATE_RECEIVED_STATEMENT = "UPDATE status SET " + "status = ?, received_date = ? WHERE message_id = ?"; private static final String CLEANUP_STATEMENT = "DELETE FROM status WHERE " + "(received_date IS NULL AND DATEDIFF('dd', sent_date, CURRENT_TIMESTAMP) >= ?) OR " + "DATEDIFF('dd', received_date, CURRENT_TIMESTAMP) >= ?"; private static final String SELECT_AGED_STATEMENT = "SELECT message_id FROM status WHERE " + "(received_date IS NULL AND DATEDIFF('dd', sent_date, CURRENT_TIMESTAMP) >= ?) OR " + "DATEDIFF('dd', received_date, CURRENT_TIMESTAMP) >= ?"; private static final String DUMP_LINE = "{0},{1},{2},{3},{4},{5}" + System.getProperty("line.separator"); private static final String CHECKPOINT_DEFRAG_STATEMENT = "CHECKPOINT DEFRAG"; private static final String SET_LOGSIZE_STATEMENT = "SET LOGSIZE 1"; private static final String DB_ERROR_MESSAGE = "DB error while querying the status table: "; /** * Sets the path location where the database files are located. This is the absolute path. * * @param logBase db location */ public void setBase(String logBase) { base = logBase; } /** * Sets the number of days, the log database entries are held in the file before they are deleted. * * @param maxAge number of days in the DB */ public void setMaxAge(int maxAge) { this.maxAge = maxAge; } /** * <p> Sets the flag whether the once sent files can be sent again. By default, if this method was never called, the * files are not resend. * <p> * <p> MANTIS 3301 * * @param resend <code>true</code> if the files are allowed to be sent again and * <code>false</code> otherwise */ public void setResend(boolean resend) { this.resend = resend; } /** * Initializes the log service by creating the database and the log table and getting the SQL connection. This method * loads the HSQL JDBC driver and gets the connection to the database. After that an attempt is made to clean up the * old records. If the DB table does not exist, it is created. If the JDBC driver cannot be loaded, this method logs * atr fatal level and returns. * * @throws SQLException if the JDBC connection or the DB table cannot be created */ public void init() throws SQLException { // load the driver try { Class.forName("org.hsqldb.jdbcDriver"); } catch (ClassNotFoundException e) { LOG.fatal("cannot find the HSQL driver class : " + e); return; } // get the connection from the manager connection = DriverManager.getConnection("jdbc:hsqldb:" + base + File.separator + DB_FILE_NAME_PREFIX, // filenames "sa", // username ""); // password runner = new QueryRunner(); // initialize the log size to 1MB to possibly prevent a very slow start if the MH was stopped before // a DB checkpoint had been reached (default is 200 MB which is too much) runner.update(connection, SET_LOGSIZE_STATEMENT); // check if the status table already exists try { // we run the cleanup statement; if it fails, then the database file does not probably exist cleanup(); } catch (SQLException e) { LOG.error(e); // create the table if it does not exist LOG.debug("Creating the status table..."); runner.update(connection, CREATE_TABLE_STATEMENT); LOG.info("The status table is created."); } } /** * Removes the aged records. * * @throws SQLException if the operation cannot be performed. */ private void cleanup() throws SQLException { LOG.info("starting to remove aged records from the log table"); int count = runner.update(connection, CLEANUP_STATEMENT, new Object[] { maxAge, maxAge }); LOG.info(count + " aged records removed from the log table while performing cleanup"); // compact the DB LOG.info("compacting DB files"); runner.update(connection, CHECKPOINT_DEFRAG_STATEMENT); LOG.info("DB files compacted"); } /* * (non-Javadoc) @see ch.admin.suis.msghandler.log.LogService#removeAged() */ @Override public Collection<String> removeAged() { try { ArrayList<Object[]> rowSet = (ArrayList<Object[]>) runner.query(connection, SELECT_AGED_STATEMENT, new Object[] { maxAge, maxAge }, new ArrayListHandler()); List<String> result = new ArrayList<>(rowSet.size()); // remove the records cleanup(); // add the message ids for (Object[] row : rowSet) { // we have only one element in the array - the sent date and it is a // java.lang.String result.add((String) row[0]); } return result; } catch (SQLException e) { LOG.error("cannot remove the aged records from the log table", e); return Collections.emptyList(); } } @Override public boolean setSending(Mode source, List<String> participantIds, String filename) throws LogServiceException { boolean retVal = false; for (String participantId : participantIds) { retVal = setSending(source, participantId, filename); } return retVal; } /* * (non-Javadoc) * * @see ch.admin.suis.msghandler.log.LogService#setSending(java.lang.String, java.lang.String) */ @Override public boolean setSending(Mode source, String participantId, String filename) throws LogServiceException { try { // select to see if we have this record already // we receive a java.lang.Integer here because // the status is defined as TINYINT Integer value = (Integer) runner.query(connection, SELECT_STATEMENT, new Object[] { participantId, filename }, new ScalarHandler()); // insert, if not if (null == value) { runner.update(connection, INSERT_STATEMENT, new Object[] { participantId, filename, LogStatus.SENDING.getCode(), source.getCode() }); } else { // some record is already there if (LogStatus.ERROR.getCode() == value) { // rewrite with the status "sending" runner.update(connection, UPDATE_SENT_STATEMENT, new Object[] { LogStatus.SENDING.getCode(), new Timestamp(System.currentTimeMillis()), null, source.getCode(), participantId, filename }); } else { // cannot change the status if the special flag is not set return resend; } } } catch (SQLException e) { LOG.error(DB_ERROR_MESSAGE + e.getMessage()); throw new LogServiceException(e); } return true; } @Override public void setForwarded(Mode source, List<String> participantIds, String filename, String messageId) throws LogServiceException { for (String participantId : participantIds) { setForwarded(source, participantId, filename, messageId); } } /** * Sets the message status to * <code>FORWARDED</code>. * * @see ch.admin.suis.msghandler.log.LogService#setForwarded(Mode, java.lang.String, java.lang.String, * java.lang.String) */ @Override public void setForwarded(Mode source, String participantId, String filename, String messageId) throws LogServiceException { try { // update with the new status runner.update(connection, UPDATE_SENT_STATEMENT, new Object[] { LogStatus.FORWARDED.getCode(), new Timestamp(System.currentTimeMillis()), messageId, source.getCode(), participantId, filename }); } catch (SQLException e) { LOG.error(DB_ERROR_MESSAGE + e.getMessage()); throw new LogServiceException(e); } } /* * (non-Javadoc) @see ch.admin.suis.msghandler.log.LogService#getSentDate(java.lang.String) */ @SuppressWarnings("unchecked") @Override public Date getSentDate(String messageId) throws LogServiceException { try { ArrayList<Object[]> rowSet = (ArrayList<Object[]>) runner.query(connection, SELECT_SENT_DATE_STATEMENT, new Object[] { messageId }, new ArrayListHandler()); List<Date> result = new ArrayList<>(rowSet.size()); for (Object[] row : rowSet) { // we have only one element in the array - the sent date and it is a // java.util.Date result.add((Date) row[0]); } if (result.isEmpty()) { // nothing found return null; } else { // otherwise return the first encountered value return result.get(0); } } catch (SQLException e) { throw new LogServiceException(e); } } /** * Closes the SQL connection and shuts down the HBSQL database. This method MUST be called before quitting the * program. */ public void destroy() { try { if (null != runner) { runner.update(connection, "SHUTDOWN"); LOG.debug("shutdown statement issued for the log service"); } DbUtils.close(connection); LOG.debug("log service stopped"); } catch (SQLException e) { // ignore LOG.error("cannot close the DB connection while stopping the log service: " + e); } } /** * Creates the CSV-file in the directory in which the database files are located. If the service was not initialized, * this method silently returns. */ @SuppressWarnings("unchecked") public void dump() { if (null == connection) { // this service was not initialized LOG.error("cannot create the DB dump file: the log service was not initialized"); return; } File dumpFile = new File(base, "dump.csv"); // TODO make this configurable try (BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(dumpFile), "ISO-8859-1"))) { // read all the available records from the DB ArrayList<Object[]> rowSet = (ArrayList<Object[]>) runner.query(connection, "select * from status", new ArrayListHandler()); for (Object[] row : rowSet) { for (int i = 0; i < row.length; i++) { // replace the null values with empty strings row[i] = null == row[i] ? "" : row[i]; } writer.write(MessageFormat.format(DUMP_LINE, row)); } } catch (UnsupportedEncodingException e) { LOG.fatal(e); } catch (FileNotFoundException e) { LOG.fatal("cannot create the DB dump file " + dumpFile.getAbsolutePath() + ". Error : " + e); } catch (SQLException e) { LOG.fatal("cannot read from the status table: " + e); } catch (IOException e) { LOG.fatal("cannot write to the dump file: " + e); } } /* * (non-Javadoc) @see ch.admin.suis.msghandler.log.LogService#getSentMessages() */ @Override public List<String> getSentMessages() throws LogServiceException { List<String> result = new ArrayList<>(); result.addAll(getMessages(LogStatus.SENT)); result.addAll(getMessages(LogStatus.FORWARDED)); return result; } /* * (non-Javadoc) * * @see ch.admin.suis.msghandler.log.LogService#setReceived(java.lang.String, java.util.Date) */ @Override public void setStatusChange(String messageid, Date changeDate, LogStatus status) throws LogServiceException { try { // update with the new status runner.update(connection, UPDATE_RECEIVED_STATEMENT, new Object[] { status.getCode(), new Timestamp(changeDate.getTime()), messageid }); } catch (SQLException e) { LOG.error(DB_ERROR_MESSAGE + e.getMessage()); throw new LogServiceException(e); } } /* * (non-Javadoc) * * @see ch.admin.suis.msghandler.log.LogService#getFiles(java.lang.String) */ @Override public List<String> getFiles(String messageId) throws LogServiceException { try { ArrayList<Object[]> rowSet = (ArrayList<Object[]>) runner.query(connection, SELECT_FILENAME_STATEMENT, new Object[] { messageId }, new ArrayListHandler()); List<String> result = new ArrayList<>(rowSet.size()); for (Object[] row : rowSet) { // we have only one element in the array - the filename and it is a // string result.add((String) row[0]); } return result; } catch (SQLException e) { throw new LogServiceException(e); } } /** * {@inheritDoc } */ @Override public List<DBLogEntry> getAllEntries() throws LogServiceException { try { ArrayList<Object[]> rowSet = (ArrayList<Object[]>) runner.query(connection, SELECT_ALL_STATEMENT, new Object[] {}, new ArrayListHandler()); List<DBLogEntry> results = new ArrayList<>(rowSet.size()); for (Object[] row : rowSet) { DBLogEntry logEntry = parseDBLogEntry(row); results.add(logEntry); } return results; } catch (SQLException e) { throw new LogServiceException(e); } } private DBLogEntry parseDBLogEntry(Object[] row) { DBLogEntry logEntry = new DBLogEntry(); if (row[POS_PARTICIPANT_ID] != null) { logEntry.setRecipientId(row[POS_PARTICIPANT_ID].toString()); } if (row[POS_FILENAME] != null) { logEntry.setFilename(row[POS_FILENAME].toString()); } if (row[POS_MESSAGE_ID] != null) { logEntry.setMessageId(row[POS_MESSAGE_ID].toString()); } if (row[POS_SENT_DATE] != null) { logEntry.setSentDate(DateUtils.dateToXsdDateTime(new Date(((Timestamp) row[POS_SENT_DATE]).getTime()))); } if (row[POS_RECEIVED_DATE] != null) { logEntry.setReceivedDate( DateUtils.dateToXsdDateTime(new Date(((Timestamp) row[POS_RECEIVED_DATE]).getTime()))); } if (row[POS_LOG_STATUS] != null) { logEntry.setState(LogStatus.fromInt((Integer) row[POS_LOG_STATUS])); } if (row[POS_MESSAGE_SOURCE] != null) { logEntry.setMode(Mode.fromInt((Integer) row[POS_MESSAGE_SOURCE])); } return logEntry; } /* * (non-Javadoc) @see ch.admin.suis.msghandler.log.LogService#getMessages(ch.admin.suis.msghandler.log.LogStatus) */ @Override public List<String> getMessages(LogStatus status) throws LogServiceException { try { ArrayList<Object[]> rowSet = (ArrayList<Object[]>) runner.query(connection, SELECT_MESSAGE_ID_ALL_STATEMENT, new Object[] { status.getCode() }, new ArrayListHandler()); List<String> result = new ArrayList<>(rowSet.size()); for (Object[] row : rowSet) { // we have only one element in the array - the message ID and it is a // string result.add((String) row[0]); } return result; } catch (SQLException e) { throw new LogServiceException(e); } } /* * (non-Javadoc) @see ch.admin.suis.msghandler.log.LogService#isTransparent(java.lang.String) */ @Override public boolean isTransparent(String messageId) throws LogServiceException { try { ArrayList<Object[]> rowSet = (ArrayList<Object[]>) runner.query(connection, SELECT_SOURCE_STATEMENT, new Object[] { messageId }, new ArrayListHandler()); List<Integer> result = new ArrayList<>(rowSet.size()); for (Object[] row : rowSet) { // we have only one element in the array - the message ID and it is a // string result.add((Integer) row[0]); } // nothing found, otherwise return the first encountered value return result.isEmpty() || Mode.TRANSP.getCode() == result.get(0); } catch (SQLException e) { throw new LogServiceException(e); } } }