001/*
002 * #%L
003 * Netarchivesuite - archive
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
012 * 
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023package dk.netarkivet.archive.arcrepositoryadmin;
024
025import java.sql.Connection;
026import java.sql.Date;
027import java.sql.PreparedStatement;
028import java.sql.ResultSet;
029import java.sql.SQLException;
030import java.sql.Statement;
031import java.util.ArrayList;
032import java.util.Calendar;
033import java.util.HashMap;
034import java.util.HashSet;
035import java.util.List;
036import java.util.Map;
037import java.util.Set;
038
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042import dk.netarkivet.common.distribute.arcrepository.Replica;
043import dk.netarkivet.common.distribute.arcrepository.ReplicaStoreState;
044import dk.netarkivet.common.distribute.arcrepository.ReplicaType;
045import dk.netarkivet.common.exceptions.IOFailure;
046import dk.netarkivet.common.exceptions.IllegalState;
047import dk.netarkivet.common.utils.DBUtils;
048import dk.netarkivet.common.utils.NotificationType;
049import dk.netarkivet.common.utils.NotificationsFactory;
050
051/**
052 * Helper methods used by {@link ReplicaCacheDatabase}.
053 */
054public final class ReplicaCacheHelpers {
055
056    /** The log. */
057    protected static Logger log = LoggerFactory.getLogger(ReplicaCacheHelpers.class);
058
059    /** Private constructor to avoid instantiation. */
060    private ReplicaCacheHelpers() {
061    }
062
063    /**
064     * Method for checking whether a replicafileinfo is in the database.
065     *
066     * @param fileid The id of the file.
067     * @param replicaID The id of the replica.
068     * @param con An open connection to the archive database
069     * @return Whether the replicafileinfo was there or not.
070     * @throws IllegalState If more than one copy of the replicafileinfo is placed in the database.
071     */
072    protected static boolean existsReplicaFileInfoInDB(long fileid, String replicaID, Connection con)
073            throws IllegalState {
074        // retrieve the amount of times this replicafileinfo
075        // is within the database.
076        String sql = "SELECT COUNT(*) FROM replicafileinfo WHERE file_id = ? AND replica_id = ?";
077        int count = DBUtils.selectIntValue(con, sql, fileid, replicaID);
078
079        // Handle the different cases for count.
080        switch (count) {
081        case 0:
082            return false;
083        case 1:
084            return true;
085        default:
086            throw new IllegalState("Cannot handle " + count + " replicafileinfo entries with the id '" + fileid + "'.");
087        }
088    }
089
090    /**
091     * Method for inserting a Replica into the replica table. The replica_guid is automatically given by the database,
092     * and the values in the fields replica_id, replica_name and replica_type is created from the replica argument.
093     *
094     * @param rep The Replica to insert into the replica table.
095     * @param con An open connection to the archive database
096     * @throws IOFailure If a SQLException is caught.
097     */
098    protected static void insertReplicaIntoDB(Replica rep, Connection con) throws IOFailure {
099        PreparedStatement statement = null;
100        try {
101            // Make the SQL statement for putting the replica into the database
102            // and insert the variables for the entry to the replica table.
103            statement = con.prepareStatement("INSERT INTO replica (replica_id, replica_name, replica_type) "
104                    + "(SELECT ?,?,? from replica WHERE replica_id=? HAVING count(*) = 0)");
105            statement.setString(1, rep.getId());
106            statement.setString(2, rep.getName());
107            statement.setInt(3, rep.getType().ordinal());
108            statement.setString(4, rep.getId());
109            log.debug("Executing insert, conditional on {} not already existing in the database.", rep.getId());
110            int result = statement.executeUpdate();
111            log.debug("Insert statement for {} returned {}", rep.getId(), result);
112            con.commit();
113        } catch (SQLException e) {
114            throw new IOFailure("Cannot add replica '" + rep + "'to the database.", e);
115        } finally {
116            DBUtils.closeStatementIfOpen(statement);
117        }
118    }
119
120    /**
121     * Method to create a new entry in the file table in the database. The file_id is automatically created by the
122     * database, and the argument is used for the filename for this new entry to the table. This will also create a
123     * replicafileinfo entry for each replica.
124     *
125     * @param filename The filename for the new entry in the file table.
126     * @param connection An open connection to the archive database
127     * @return created file_id for the new entry.
128     * @throws IllegalState If the file cannot be inserted into the database.
129     */
130    protected static long insertFileIntoDB(String filename, Connection connection) throws IllegalState {
131        log.debug("Insert file '{}' into database", filename);
132        PreparedStatement statement = null;
133        try {
134            // Make the SQL statement for putting the replica into the database
135            // and insert the variables for the entry to the replica table.
136            statement = connection.prepareStatement("INSERT INTO file (filename) VALUES ( ? )",
137                    Statement.RETURN_GENERATED_KEYS);
138            statement.setString(1, filename);
139
140            // execute the SQL statement
141            statement.executeUpdate();
142            // Retrieve the fileId for the just inserted file.
143            ResultSet resultset = statement.getGeneratedKeys();
144            resultset.next();
145            long fileId = resultset.getLong(1);
146            connection.commit();
147
148            // Create replicafileinfo for each replica.
149            createReplicaFileInfoEntriesInDB(fileId, connection);
150            log.debug("Insert file '{}' into database completed. Assigned fileID={}", filename, fileId);
151            return fileId;
152        } catch (SQLException e) {
153            throw new IllegalState("Cannot add file '" + filename + "' to the database.", e);
154        } finally {
155            DBUtils.closeStatementIfOpen(statement);
156        }
157    }
158
159    /**
160     * When a new file is inserted into the database, each replica gets a new entry in the replicafileinfo table for
161     * this file. The fields for this new entry are set to the following: - file_id = argument. - replica_id = The id of
162     * the current replica. - filelist_status = NO_FILELIST_STATUS. - checksum_status = UNKNOWN. - upload_status =
163     * NO_UPLOAD_STATUS.
164     * <p>
165     * The replicafileinfo_guid is automatically created by the database, and the dates are set to null.
166     *
167     * @param fileId The id for the file.
168     * @param con An open connection to the archive database
169     * @throws IllegalState If the file could not be entered into the database.
170     */
171    protected static void createReplicaFileInfoEntriesInDB(long fileId, Connection con) throws IllegalState {
172        PreparedStatement statement = null;
173        try {
174            // init variables
175            List<String> repIds = ReplicaCacheHelpers.retrieveIdsFromReplicaTable(con);
176
177            // Make a entry for each replica.
178            for (String repId : repIds) {
179                // create if it does not exists already.
180                if (!existsReplicaFileInfoInDB(fileId, repId, con)) {
181                    // Insert with the known values (no dates).
182                    statement = DBUtils.prepareStatement(con,
183                            "INSERT INTO replicafileinfo (file_id, replica_id, filelist_status, checksum_status, "
184                                    + "upload_status ) VALUES ( ?, ?, ?, ?, ? )", fileId, repId,
185                            FileListStatus.NO_FILELIST_STATUS.ordinal(), ChecksumStatus.UNKNOWN.ordinal(),
186                            ReplicaStoreState.UNKNOWN_UPLOAD_STATE.ordinal());
187
188                    // execute the SQL statement
189                    statement.executeUpdate();
190                    con.commit();
191                    statement.close(); // Important to cleanup!
192                }
193            }
194        } catch (SQLException e) {
195            throw new IllegalState("Cannot add replicafileinfo to the database.", e);
196        } finally {
197            DBUtils.closeStatementIfOpen(statement);
198        }
199    }
200
201    /**
202     * Method for retrieving the replica IDs within the database.
203     *
204     * @param con An open connection to the archive database
205     * @return The list of replicaIds from the replica table in the database.
206     */
207    protected static List<String> retrieveIdsFromReplicaTable(Connection con) {
208        // Make SQL statement for retrieving the replica_ids in the
209        // replica table.
210        final String sql = "SELECT replica_id FROM replica";
211
212        // execute the SQL statement and return the results.
213        return DBUtils.selectStringList(con, sql);
214    }
215
216    /**
217     * Method for retrieving all the file IDs within the database.
218     *
219     * @param con An open connection to the archive database
220     * @return The list of fileIds from the file table in the database.
221     */
222    protected static List<String> retrieveIdsFromFileTable(Connection con) {
223        // Make SQL statement for retrieving the file_ids in the
224        // file table.
225        final String sql = "SELECT file_id FROM file";
226
227        // execute the SQL statement and return the results.
228        return DBUtils.selectStringList(con, sql);
229    }
230
231    /**
232     * Method for retrieving all the ReplicaFileInfo GUIDs within the database.
233     *
234     * @param con An open connection to the archive database
235     * @return A list of the replicafileinfo_guid for all entries in the replicafileinfo table.
236     */
237    protected static List<String> retrieveIdsFromReplicaFileInfoTable(Connection con) {
238        // Make SQL statement for retrieving the replicafileinfo_guids in the
239        // replicafileinfo table.
240        final String sql = "SELECT replicafileinfo_guid FROM replicafileinfo";
241
242        // execute the SQL statement and return the results.
243        return DBUtils.selectStringList(con, sql);
244    }
245
246    /**
247     * Retrieves the file_id for the corresponding filename. An error is thrown if no such file_id is found in the file
248     * table, and if more than one instance with the given name is found, then a warning is issued. If more than one is
249     * found, then it is logged and only the first is returned.
250     *
251     * @param filename The entry in the filename list where the corresponding file_id should be found.
252     * @param con An open connection to the archive database
253     * @return The file_id for the file, or -1 if the file was not found.
254     */
255    protected static long retrieveIdForFile(String filename, Connection con) {
256        // retrieve the file_id of the entry in the file table with the
257        // filename filename.
258        final String sql = "SELECT file_id FROM file WHERE filename = ?";
259        List<Long> files = DBUtils.selectLongList(con, sql, filename);
260
261        switch (files.size()) {
262        // if no such file within the database, then return negative value.
263        case 0:
264            return -1;
265        case 1:
266            return files.get(0);
267            // if more than one file, then log it and return the first found.
268        default:
269            log.warn("Only one entry in the file table for the name '{}' was expected, but {} was found. "
270                    + "The first element is returned.", filename, files.size());
271            return files.get(0);
272        }
273    }
274
275    /**
276     * Method for retrieving the replicafileinfo_guid for a specific instance defined from the fileId and the replicaId.
277     * If more than one is found, then it is logged and only the first is returned.
278     *
279     * @param fileId The identifier for the file.
280     * @param replicaId The identifier for the replica.
281     * @param con An open connection to the archive database
282     * @return The identifier for the replicafileinfo, or -1 if not found.
283     */
284    protected static long retrieveReplicaFileInfoGuid(long fileId, String replicaId, Connection con) {
285        // sql for retrieving the replicafileinfo_guid.
286        final String sql = "SELECT replicafileinfo_guid FROM replicafileinfo WHERE file_id = ? AND replica_id = ?";
287        List<Long> result = DBUtils.selectLongList(con, sql, fileId, replicaId);
288
289        // Handle the different cases for count.
290        switch (result.size()) {
291        case 0:
292            return -1;
293        case 1:
294            return result.get(0);
295        default:
296            log.warn(
297                    "More than one replicafileinfo with the file id '{}' from replica '{}': {}. The first result returned.",
298                    fileId, replicaId, result);
299            return result.get(0);
300        }
301    }
302
303    /**
304     * Method for retrieving the list of all the replicafileinfo_guids for a specific replica.
305     *
306     * @param replicaId The id for the replica to contain the files.
307     * @param con An open connection to the archiveDatabase.
308     * @return The list of all the replicafileinfo_guid.
309     */
310    protected static Set<Long> retrieveReplicaFileInfoGuidsForReplica(String replicaId, Connection con) {
311        // sql for retrieving the replicafileinfo_guids for the replica.
312        final String sql = "SELECT replicafileinfo_guid FROM replicafileinfo "
313                + "WHERE replica_id = ? ORDER BY replicafileinfo_guid";
314        return DBUtils.selectLongSet(con, sql, replicaId);
315    }
316
317    /**
318     * Method for retrieving the replica type for a specific replica.
319     *
320     * @param replicaId The id of the replica.
321     * @param con An open connection to the archiveDatabase.
322     * @return The type of the replica.
323     */
324    protected static ReplicaType retrieveReplicaType(String replicaId, Connection con) {
325        // The SQL statement for retrieving the replica_type of a replica with
326        // the given replica id.
327        final String sql = "SELECT replica_type FROM replica WHERE replica_id = ?";
328        return ReplicaType.fromOrdinal(DBUtils.selectIntValue(con, sql, replicaId));
329    }
330
331    /**
332     * Method for retrieving the list of replica, where the file with the given name has the checksum_status 'OK'.
333     *
334     * @param filename The name of the file.
335     * @param con An open connection to the archive database
336     * @return The list of replicas where the status for the checksum of the file is OK.
337     */
338    protected static List<String> retrieveReplicaIdsWithOKChecksumStatus(String filename, Connection con) {
339        // The SQL statement to retrieve the replica_id for the entries in the
340        // replicafileinfo table for the given fileId and checksum_status = OK
341        // FIXME Use joins
342        final String sql = "SELECT replica_id FROM replicafileinfo, file "
343                + "WHERE replicafileinfo.file_id = file.file_id AND file.filename = ? AND checksum_status = ?";
344        return DBUtils.selectStringList(con, sql, filename, ChecksumStatus.OK.ordinal());
345    }
346
347    /**
348     * Method for retrieving the filename from the entry in the file table which has the fileId as file_id.
349     *
350     * @param fileId The file_id of the entry in the file table for which to retrieve the filename.
351     * @param con An open connection to the archive database
352     * @return The filename corresponding to the fileId in the file table.
353     */
354    protected static String retrieveFilenameForFileId(long fileId, Connection con) {
355        // The SQL statement to retrieve the filename for a given file_id
356        final String sql = "SELECT filename FROM file WHERE file_id = ?";
357        return DBUtils.selectStringValue(con, sql, fileId);
358    }
359
360    /**
361     * Method for retrieving the filelist_status for the entry in the replicafileinfo table associated with the given
362     * filename for the replica identified with a given id.
363     *
364     * @param filename the filename of the file for which you want a status.
365     * @param replicaId The identifier of the replica
366     * @param con An open connection to the archive database
367     * @return The above mentioned filelist_status of the file
368     */
369    protected static int retrieveFileListStatusFromReplicaFileInfo(String filename, String replicaId, Connection con) {
370        // The SQL statement to retrieve the filelist_status for the given
371        // entry in the replica fileinfo table.
372        // FIXME Use joins
373        final String sql = "SELECT filelist_status FROM replicafileinfo, file "
374                + "WHERE file.file_id = replicafileinfo.file_id AND file.filename=? AND replica_id=?";
375        return DBUtils.selectIntValue(con, sql, filename, replicaId);
376    }
377
378    /**
379     * This is used for updating a replicafileinfo instance based on the results of a checksumjob. Updates the following
380     * fields for the entry in the replicafileinfo: <br/>
381     * - checksum = checksum argument. <br/>
382     * - upload_status = completed. <br/>
383     * - filelist_status = ok. <br/>
384     * - checksum_status = UNKNOWN. <br/>
385     * - checksum_checkdatetime = now. <br/>
386     * - filelist_checkdatetime = now.
387     *
388     * @param replicafileinfoId The unique id for the replicafileinfo.
389     * @param checksum The new checksum for the entry.
390     * @param con An open connection to the archive database
391     */
392    protected static void updateReplicaFileInfoChecksum(long replicafileinfoId, String checksum, Connection con) {
393        PreparedStatement statement = null;
394        try {
395            // The SQL statement
396            final String sql = "UPDATE replicafileinfo SET checksum = ?, upload_status = ?, filelist_status = ?,"
397                    + " checksum_status = ?, checksum_checkdatetime = ?, filelist_checkdatetime = ? "
398                    + "WHERE replicafileinfo_guid = ?";
399
400            Date now = new Date(Calendar.getInstance().getTimeInMillis());
401
402            // complete the SQL statement.
403            statement = DBUtils.prepareStatement(con, sql, checksum, ReplicaStoreState.UPLOAD_COMPLETED.ordinal(),
404                    FileListStatus.OK.ordinal(), ChecksumStatus.UNKNOWN.ordinal(), now, now, replicafileinfoId);
405
406            // execute the SQL statement
407            statement.executeUpdate();
408            con.commit();
409        } catch (Exception e) {
410            String msg = "Problems updating the replicafileinfo.";
411            log.warn(msg);
412            throw new IOFailure(msg, e);
413        } finally {
414            DBUtils.closeStatementIfOpen(statement);
415        }
416    }
417
418    /**
419     * Method for updating the filelist of a replicafileinfo instance. Updates the following fields for the entry in the
420     * replicafileinfo: <br/>
421     * filelist_status = OK. <br/>
422     * filelist_checkdatetime = current time.
423     *
424     * @param replicafileinfoId The id of the replicafileinfo.
425     * @param con An open connection to the archive database
426     */
427    protected static void updateReplicaFileInfoFilelist(long replicafileinfoId, Connection con) {
428        PreparedStatement statement = null;
429        try {
430            // The SQL statement
431            final String sql = "UPDATE replicafileinfo SET filelist_status = ?, filelist_checkdatetime = ? "
432                    + "WHERE replicafileinfo_guid = ?";
433
434            Date now = new Date(Calendar.getInstance().getTimeInMillis());
435
436            // complete the SQL statement.
437            statement = DBUtils.prepareStatement(con, sql, FileListStatus.OK.ordinal(), now, replicafileinfoId);
438
439            // execute the SQL statement
440            statement.executeUpdate();
441            con.commit();
442        } catch (Exception e) {
443            String msg = "Problems updating the replicafileinfo.";
444            log.warn(msg);
445            throw new IOFailure(msg, e);
446        } finally {
447            DBUtils.closeStatementIfOpen(statement);
448        }
449    }
450
451    /**
452     * Method for updating the filelist of a replicafileinfo instance. Updates the following fields for the entry in the
453     * replicafileinfo: <br/>
454     * filelist_status = missing. <br/>
455     * filelist_checkdatetime = current time.
456     * <p>
457     * The replicafileinfo is in the filelist.
458     *
459     * @param replicafileinfoId The id of the replicafileinfo.
460     * @param con An open connection to the archive database
461     */
462    protected static void updateReplicaFileInfoMissingFromFilelist(long replicafileinfoId, Connection con) {
463        PreparedStatement statement = null;
464        try {
465            // The SQL statement
466            final String sql = "UPDATE replicafileinfo "
467                    + "SET filelist_status = ?, filelist_checkdatetime = ?, upload_status = ? "
468                    + "WHERE replicafileinfo_guid = ?";
469
470            Date now = new Date(Calendar.getInstance().getTimeInMillis());
471
472            // complete the SQL statement.
473            statement = DBUtils.prepareStatement(con, sql, FileListStatus.MISSING.ordinal(), now,
474                    ReplicaStoreState.UPLOAD_FAILED.ordinal(), replicafileinfoId);
475
476            // execute the SQL statement
477            statement.executeUpdate();
478            con.commit();
479        } catch (Exception e) {
480            String msg = "Problems updating the replicafileinfo.";
481            log.warn(msg);
482            throw new IOFailure(msg, e);
483        } finally {
484            DBUtils.closeStatementIfOpen(statement);
485        }
486    }
487
488    /**
489     * Method for updating the checksum status of a replicafileinfo instance. Updates the following fields for the entry
490     * in the replicafileinfo: <br/>
491     * checksum_status = CORRUPT. <br/>
492     * checksum_checkdatetime = current time.
493     * <p>
494     * The replicafileinfo is in the filelist.
495     *
496     * @param replicafileinfoId The id of the replicafileinfo.
497     * @param con An open connection to the archive database
498     */
499    protected static void updateReplicaFileInfoChecksumCorrupt(long replicafileinfoId, Connection con) {
500        PreparedStatement statement = null;
501        try {
502            // The SQL statement
503            final String sql = "UPDATE replicafileinfo "
504                    + "SET checksum_status = ?, checksum_checkdatetime = ?, upload_status = ? "
505                    + "WHERE replicafileinfo_guid = ?";
506
507            Date now = new Date(Calendar.getInstance().getTimeInMillis());
508
509            // complete the SQL statement.
510            statement = DBUtils.prepareStatement(con, sql, ChecksumStatus.CORRUPT.ordinal(), now,
511                    ReplicaStoreState.UPLOAD_FAILED.ordinal(), replicafileinfoId);
512
513            // execute the SQL statement
514            statement.executeUpdate();
515            con.commit();
516        } catch (Exception e) {
517            String msg = "Problems updating the replicafileinfo.";
518            log.warn(msg);
519            throw new IOFailure(msg, e);
520        } finally {
521            DBUtils.closeStatementIfOpen(statement);
522        }
523    }
524
525    /**
526     * Retrieve the guid stored for a filename on a given replica.
527     *
528     * @param filename a given filename
529     * @param replicaId An identifier for a replica.
530     * @param con An open connection to the archive database
531     * @return the abovementioned guid.
532     */
533    protected static long retrieveGuidForFilenameOnReplica(String filename, String replicaId, Connection con) {
534        // sql for retrieving the replicafileinfo_guid.
535        // FIXME Use joins
536        final String sql = "SELECT replicafileinfo_guid FROM replicafileinfo, file "
537                + "WHERE replicafileinfo.file_id = file.file_id AND file.filename = ? AND replica_id = ?";
538        List<Long> result = DBUtils.selectLongList(con, sql, filename, replicaId);
539        return result.get(0);
540    }
541
542    /**
543     * Method for updating the checksum status of a replicafileinfo instance. Updates the following fields for the entry
544     * in the replicafileinfo: <br/>
545     * checksum_status = UNKNOWN. <br/>
546     * checksum_checkdatetime = current time.
547     * <p>
548     * The replicafileinfo is in the filelist.
549     *
550     * @param replicafileinfoId The id of the replicafileinfo.
551     * @param con An open connection to the archive database
552     */
553    protected static void updateReplicaFileInfoChecksumUnknown(long replicafileinfoId, Connection con) {
554        PreparedStatement statement = null;
555        try {
556            // The SQL statement
557            final String sql = "UPDATE replicafileinfo SET checksum_status = ?, checksum_checkdatetime = ? "
558                    + "WHERE replicafileinfo_guid = ?";
559
560            Date now = new Date(Calendar.getInstance().getTimeInMillis());
561
562            // complete the SQL statement.
563            statement = DBUtils.prepareStatement(con, sql, ChecksumStatus.UNKNOWN.ordinal(), now, replicafileinfoId);
564
565            // execute the SQL statement
566            statement.executeUpdate();
567            con.commit();
568        } catch (Exception e) {
569            String msg = "Problems updating the replicafileinfo.";
570            log.warn(msg);
571            throw new IOFailure(msg, e);
572        } finally {
573            DBUtils.closeStatementIfOpen(statement);
574        }
575    }
576
577    /**
578     * Method for updating the checksum status of a replicafileinfo instance. Updates the following fields for the entry
579     * in the replicafileinfo: <br/>
580     * checksum_status = OK. <br/>
581     * upload_status = UPLOAD_COMLPETE. <br/>
582     * checksum_checkdatetime = current time. <br/>
583     * <br/>
584     * The file is required to exist in the replica.
585     *
586     * @param replicafileinfoId The id of the replicafileinfo.
587     * @param con An open connection to the archive database
588     */
589    protected static void updateReplicaFileInfoChecksumOk(long replicafileinfoId, Connection con) {
590        PreparedStatement statement = null;
591        try {
592            // The SQL statement
593            final String sql = "UPDATE replicafileinfo "
594                    + "SET checksum_status = ?, checksum_checkdatetime = ?, upload_status = ? "
595                    + "WHERE replicafileinfo_guid = ?";
596            Date now = new Date(Calendar.getInstance().getTimeInMillis());
597
598            // complete the SQL statement.
599            statement = DBUtils.prepareStatement(con, sql, ChecksumStatus.OK.ordinal(), now,
600                    ReplicaStoreState.UPLOAD_COMPLETED.ordinal(), replicafileinfoId);
601
602            // execute the SQL statement
603            statement.executeUpdate();
604            con.commit();
605        } catch (Exception e) {
606            String msg = "Problems updating the replicafileinfo.";
607            log.warn(msg);
608            throw new IOFailure(msg, e);
609        } finally {
610            DBUtils.closeStatementIfOpen(statement);
611        }
612    }
613
614    /**
615     * Method for updating the checksum_updated field for a given replica in the replica table. This is called when a
616     * checksum_job has been handled.
617     * <p>
618     * The following fields for the entry in the replica table: <br/>
619     * checksum_updated = now.
620     *
621     * @param rep The replica which has just been updated.
622     * @param con An open connection to the archive database
623     */
624    protected static void updateChecksumDateForReplica(Replica rep, Connection con) {
625        PreparedStatement statement = null;
626        try {
627            Date now = new Date(Calendar.getInstance().getTimeInMillis());
628            final String sql = "UPDATE replica SET checksum_updated = ? WHERE replica_id = ?";
629            statement = DBUtils.prepareStatement(con, sql, now, rep.getId());
630            statement.executeUpdate();
631            con.commit();
632        } catch (Exception e) {
633            String msg = "Cannot update the checksum_updated for replica '" + rep + "'.";
634            log.warn(msg);
635            throw new IOFailure(msg, e);
636        } finally {
637            DBUtils.closeStatementIfOpen(statement);
638        }
639    }
640
641    /**
642     * Method for updating the filelist_updated field for a given replica in the replica table. This is called when a
643     * filelist_job or a checksum_job has been handled.
644     * <p>
645     * The following fields for the entry in the replica table: <br/>
646     * filelist_updated = now.
647     *
648     * @param rep The replica which has just been updated.
649     * @param connection An open connection to the archive database
650     */
651    protected static void updateFilelistDateForReplica(Replica rep, Connection connection) {
652        PreparedStatement statement = null;
653        try {
654            Date now = new Date(Calendar.getInstance().getTimeInMillis());
655            final String sql = "UPDATE replica SET filelist_updated = ? WHERE replica_id = ?";
656            statement = DBUtils.prepareStatement(connection, sql, now, rep.getId());
657            statement.executeUpdate();
658            connection.commit();
659        } catch (Exception e) {
660            String msg = "Cannot update the filelist_updated for replica '" + rep + "'.";
661            log.warn(msg);
662            throw new IOFailure(msg, e);
663        } finally {
664            DBUtils.closeStatementIfOpen(statement);
665        }
666    }
667
668    /**
669     * Method for setting the filelist_updated field for a given replica in the replica table to a specified value. This
670     * is only called when the admin.data is converted.
671     * <p>
672     * The following fields for the entry in the replica table: <br/>
673     * filelist_updated = date.
674     *
675     * @param rep The replica which has just been updated.
676     * @param date The date for the last filelist update.
677     * @param con An open connection to the archive database
678     */
679    protected static void setFilelistDateForReplica(Replica rep, Date date, Connection con) {
680        PreparedStatement statement = null;
681        try {
682            final String sql = "UPDATE replica SET filelist_updated = ? WHERE replica_id = ?";
683            statement = DBUtils.prepareStatement(con, sql, date, rep.getId());
684            statement.executeUpdate();
685            con.commit();
686        } catch (Exception e) {
687            String msg = "Cannot update the filelist_updated for replica '" + rep + "'.";
688            log.warn(msg);
689            throw new IOFailure(msg, e);
690        } finally {
691            DBUtils.closeStatementIfOpen(statement);
692        }
693    }
694
695    /**
696     * Method for setting the checksum_updated field for a given replica in the replica table to a specified value. This
697     * is only called when the admin.data is converted.
698     * <p>
699     * The following fields for the entry in the replica table: <br/>
700     * checksum_updated = date.
701     *
702     * @param rep The replica which has just been updated.
703     * @param date The date for the last checksum update.
704     * @param con An open connection to the archive database
705     */
706    protected static void setChecksumlistDateForReplica(Replica rep, Date date, Connection con) {
707        PreparedStatement statement = null;
708        try {
709            final String sql = "UPDATE replica SET checksum_updated = ? WHERE replica_id = ?";
710            statement = DBUtils.prepareStatement(con, sql, date, rep.getId());
711            statement.executeUpdate();
712            con.commit();
713        } catch (Exception e) {
714            String msg = "Cannot update the filelist_updated for replica '" + rep + "'.";
715            log.warn(msg);
716            throw new IOFailure(msg, e);
717        } finally {
718            DBUtils.closeStatementIfOpen(statement);
719        }
720    }
721
722    /**
723     * Method for testing whether a replica already is within the database.
724     *
725     * @param rep The replica to find in the database.
726     * @param con An open connection to the archive database
727     * @return Whether the replica is found in the database.
728     */
729    protected static boolean existsReplicaInDB(Replica rep, Connection con) {
730        // retrieve the amount of times this replica is within the database.
731        final String sql = "SELECT COUNT(*) FROM replica WHERE replica_id = ?";
732        int count = DBUtils.selectIntValue(con, sql, rep.getId());
733
734        // Handle the different cases for count.
735        switch (count) {
736        case 0:
737            return false;
738        case 1:
739            return true;
740        default:
741            throw new IOFailure("Cannot handle " + count + " replicas " + "with id '" + rep.getId() + "'.");
742        }
743    }
744
745    /**
746     * Method for retrieving ReplicaFileInfo entry in the database.
747     *
748     * @param replicaFileInfoGuid The guid for the specific replicafileinfo.
749     * @param con An open connection to the archive database
750     * @return The replicafileinfo.
751     */
752    protected static ReplicaFileInfo getReplicaFileInfo(long replicaFileInfoGuid, Connection con) {
753        // retrieve all
754        final String sql = "SELECT replicafileinfo_guid, replica_id, file_id, segment_id, checksum, upload_status, "
755                + "filelist_status, checksum_status, filelist_checkdatetime, checksum_checkdatetime "
756                + "FROM replicafileinfo WHERE replicafileinfo_guid = ?";
757
758        PreparedStatement s = null;
759
760        try {
761            s = DBUtils.prepareStatement(con, sql, replicaFileInfoGuid);
762            ResultSet res = s.executeQuery();
763            res.next();
764
765            // return the corresponding replica file info.
766            return new ReplicaFileInfo(res);
767        } catch (SQLException e) {
768            final String message = "SQL error while selecting ResultsSet by executing statement '" + sql + "'.";
769            log.warn(message, e);
770            throw new IOFailure(message, e);
771        } finally {
772            DBUtils.closeStatementIfOpen(s);
773        }
774    }
775
776    /**
777     * Method for retrieving the data for the wanted entries in the replicafileinfo table. All the replicafileinfo
778     * entries with no checksum defined is ignored.
779     *
780     * @param rfiGuids The list of guids for the entries in the replicafileinfo table which is wanted.
781     * @param con An open connection to the archive database
782     * @return The complete data for these entries in the replicafileinfo table.
783     */
784    protected static List<ReplicaFileInfo> retrieveReplicaFileInfosWithChecksum(List<Long> rfiGuids, Connection con) {
785        ArrayList<ReplicaFileInfo> result = new ArrayList<ReplicaFileInfo>();
786
787        // Extract all the replicafileinfos, but only put the entries with a
788        // non-empty checksum into the result list.
789        for (long rfiGuid : rfiGuids) {
790            ReplicaFileInfo rfi = getReplicaFileInfo(rfiGuid, con);
791            if (rfi.getChecksum() != null && !rfi.getChecksum().isEmpty()) {
792                result.add(rfi);
793            }
794        }
795
796        return result;
797    }
798
799    /**
800     * Method for updating an entry in the replicafileinfo table. This method does not update the
801     * 'checksum_checkdatetime' and 'filelist_checkdatetime'.
802     *
803     * @param replicafileinfoGuid The guid to update.
804     * @param checksum The new checksum for the entry.
805     * @param state The state for the upload.
806     * @param con An open connection to the archive database
807     * @throws IOFailure If an error occurs in the database connection.
808     */
809    protected static void updateReplicaFileInfo(long replicafileinfoGuid, String checksum, ReplicaStoreState state,
810            Connection con) throws IOFailure {
811        PreparedStatement statement = null;
812        try {
813            final String sql = "UPDATE replicafileinfo "
814                    + "SET checksum = ?, upload_status = ?, filelist_status = ?, checksum_status = ? "
815                    + "WHERE replicafileinfo_guid = ?";
816
817            FileListStatus fls;
818            ChecksumStatus cs;
819
820            if (state == ReplicaStoreState.UPLOAD_COMPLETED) {
821                fls = FileListStatus.OK;
822                cs = ChecksumStatus.OK;
823            } else if (state == ReplicaStoreState.UPLOAD_FAILED) {
824                fls = FileListStatus.MISSING;
825                cs = ChecksumStatus.UNKNOWN;
826            } else {
827                fls = FileListStatus.NO_FILELIST_STATUS;
828                cs = ChecksumStatus.UNKNOWN;
829            }
830
831            // complete the SQL statement.
832            statement = DBUtils.prepareStatement(con, sql, checksum, state.ordinal(), fls.ordinal(), cs.ordinal(),
833                    replicafileinfoGuid);
834            statement.executeUpdate();
835            con.commit();
836        } catch (Exception e) {
837            String errMsg = "Problems with updating a ReplicaFileInfo";
838            log.warn(errMsg);
839            throw new IOFailure(errMsg, e);
840        } finally {
841            DBUtils.closeStatementIfOpen(statement);
842        }
843    }
844
845    /**
846     * Method for updating an entry in the replicafileinfo table. This method updates the 'checksum_checkdatetime' and
847     * 'filelist_checkdatetime' with the given date argument.
848     *
849     * @param replicafileinfoGuid The guid to update.
850     * @param checksum The new checksum for the entry.
851     * @param date The date for the update.
852     * @param state The status for the upload.
853     * @param con An open connection to the archive database
854     * @throws IOFailure If an error occurs in the connection to the database.
855     */
856    protected static void updateReplicaFileInfo(long replicafileinfoGuid, String checksum, Date date,
857            ReplicaStoreState state, Connection con) throws IOFailure {
858        PreparedStatement statement = null;
859        try {
860            final String sql = "UPDATE replicafileinfo "
861                    + "SET checksum = ?, upload_status = ?, filelist_status = ?, checksum_status = ?, "
862                    + "checksum_checkdatetime = ?, " + "filelist_checkdatetime = ? WHERE replicafileinfo_guid = ?";
863
864            FileListStatus fls;
865            ChecksumStatus cs;
866
867            if (state == ReplicaStoreState.UPLOAD_COMPLETED) {
868                fls = FileListStatus.OK;
869                cs = ChecksumStatus.OK;
870            } else if (state == ReplicaStoreState.UPLOAD_FAILED) {
871                fls = FileListStatus.MISSING;
872                cs = ChecksumStatus.UNKNOWN;
873            } else {
874                fls = FileListStatus.NO_FILELIST_STATUS;
875                cs = ChecksumStatus.UNKNOWN;
876            }
877
878            // complete the SQL statement.
879            statement = DBUtils.prepareStatement(con, sql, checksum, state.ordinal(), fls.ordinal(), cs.ordinal(),
880                    date, date, replicafileinfoGuid);
881            statement.executeUpdate();
882            con.commit();
883        } catch (Throwable e) {
884            String errMsg = "Problems with updating a ReplicaFileInfo";
885            log.warn(errMsg);
886            throw new IOFailure(errMsg);
887        } finally {
888            DBUtils.closeStatementIfOpen(statement);
889        }
890    }
891
892    /**
893     * Retrieves the UploadStatus for a specific entry in the replicafileinfo table identified by the file guid and the
894     * replica id.
895     *
896     * @param fileGuid The id of the file.
897     * @param repId The id of the replica.
898     * @param con An open connection to the archive database
899     * @return The upload status of the corresponding replicafileinfo entry.
900     */
901    protected static ReplicaStoreState retrieveUploadStatus(long fileGuid, String repId, Connection con) {
902        // sql query for retrieval of upload status for a specific entry.
903        final String sql = "SELECT upload_status FROM replicafileinfo WHERE file_id = ? AND replica_id = ?";
904        int us = DBUtils.selectIntValue(con, sql, fileGuid, repId);
905        return ReplicaStoreState.fromOrdinal(us);
906    }
907
908    /**
909     * Retrieves the checksum for a specific entry in the replicafileinfo table identified by the file guid and the
910     * replica id.
911     *
912     * @param fileGuid The guid of the file in the file table.
913     * @param repId The id of the replica.
914     * @param con An open connection to the archive database
915     * @return The checksum of the corresponding replicafileinfo entry.
916     */
917    protected static String retrieveChecksumForReplicaFileInfoEntry(long fileGuid, String repId, Connection con) {
918        // sql query for retrieval of checksum value for an specific entry.
919        final String sql = "SELECT checksum FROM replicafileinfo WHERE file_id = ? AND replica_id = ?";
920        return DBUtils.selectStringValue(con, sql, fileGuid, repId);
921    }
922
923    /**
924     * Retrieves the checksum status for a specific entry in the replicafileinfo table identified by the file guid and
925     * the replica id.
926     *
927     * @param fileGuid The guid of the file in the file table.
928     * @param repId The id of the replica.
929     * @param con An open connection to the archive database
930     * @return The checksum status of the corresponding replicafileinfo entry.
931     */
932    protected static ChecksumStatus retrieveChecksumStatusForReplicaFileInfoEntry(long fileGuid, String repId,
933            Connection con) {
934        // sql query for retrieval of checksum value for an specific entry.
935        String sql = "SELECT checksum_status FROM replicafileinfo WHERE file_id = ? AND replica_id = ?";
936        // retrieve the ordinal for the checksum status.
937        int statusOrdinal = DBUtils.selectIntValue(con, sql, fileGuid, repId);
938        // return the checksum corresponding to the ordinal.
939        return ChecksumStatus.fromOrdinal(statusOrdinal);
940    }
941
942    /**
943     * Method for finding the checksum which is present most times in the list.
944     *
945     * @param checksums The list of checksum to vote about.
946     * @return The most common checksum, or null if several exists.
947     */
948    protected static String vote(List<String> checksums) {
949        log.debug("voting for checksums: {}", checksums.toString());
950
951        // count the occurrences of each unique checksum.
952        Map<String, Integer> csMap = new HashMap<String, Integer>();
953        for (String cs : checksums) {
954            if (csMap.containsKey(cs)) {
955                // count one more!
956                Integer count = csMap.get(cs) + 1;
957                csMap.put(cs, count);
958            } else {
959                csMap.put(cs, 1);
960            }
961        }
962
963        // find the checksum with the largest count.
964        int largestCount = -1;
965        boolean unique = false;
966        String checksum = null;
967        for (Map.Entry<String, Integer> entry : csMap.entrySet()) {
968            if (entry.getValue() > largestCount) {
969                largestCount = entry.getValue();
970                checksum = entry.getKey();
971                unique = true;
972            } else if (entry.getValue() == largestCount) {
973                unique = false;
974            }
975        }
976
977        // if not unique, then log an error and return null!
978        if (!unique) {
979            log.error("No checksum has the most occurrences in '{}'. A null has been returned!", csMap);
980            return null;
981        }
982
983        return checksum;
984    }
985
986    /**
987     * The method for voting about the checksum of a file. <br/>
988     * Each entry in the replicafileinfo table containing the file is retrieved. All the unique checksums are retrieved,
989     * e.g. if a checksum is found more than one, then it is ignored. <br/>
990     * If only one unique checksum is found, then if must be the correct one, and all the replicas with this file will
991     * have their checksum_status set to 'OK'. <br/>
992     * If more than one checksum is found, then a vote for the correct checksum is performed. This is done by counting
993     * the amount of time each of the unique checksum is found among the replicafileinfo entries for the current file.
994     * The checksum with most votes is chosen as the correct one, and the checksum_status for all the replicafileinfo
995     * entries with this checksum is set to 'OK', whereas the replicafileinfo entries with a different checksum is set
996     * to 'CORRUPT'. <br/>
997     * If no winner is found then a warning and a notification is issued, and the checksum_status for all the
998     * replicafileinfo entries with for the current file is set to 'UNKNOWN'. <br/>
999     *
1000     * @param fileId The id for the file to vote about.
1001     * @param con An open connection to the archive database
1002     */
1003    protected static void fileChecksumVote(long fileId, Connection con) {
1004        // Get all the replicafileinfo instances for the fileid, though
1005        // only the ones which have a valid checksum.
1006        // Check the checksums against each other if they differ,
1007        // then set to CORRUPT.
1008        final String sql = "SELECT replicafileinfo_guid FROM replicafileinfo WHERE file_id = ?";
1009        List<Long> rfiGuids = DBUtils.selectLongList(con, sql, fileId);
1010
1011        List<ReplicaFileInfo> rfis = retrieveReplicaFileInfosWithChecksum(rfiGuids, con);
1012
1013        // handle the case, when no replicas has a checksum of the file.
1014        if (rfis.size() == 0) {
1015            // issue a warning.
1016            log.warn("No replicas contains a valid version of the file '{}'.", retrieveFilenameForFileId(fileId, con));
1017
1018            return;
1019        }
1020
1021        // Put all the checksums into a hash set to obtain a set of
1022        // unique checksums.
1023        Set<String> hs = new HashSet<String>(rfis.size());
1024        for (ReplicaFileInfo rfi : rfis) {
1025            // only accept those files which can be found.
1026            if (rfi.getFileListState() == FileListStatus.OK) {
1027                hs.add(rfi.getChecksum());
1028            }
1029        }
1030
1031        // handle the unlikely case, where the file is missing from everywhere!
1032        if (hs.size() == 0) {
1033            String errorMsg = "The file '" + retrieveFilenameForFileId(fileId, con) + "' is missing in all replicas";
1034            log.warn(errorMsg);
1035            NotificationsFactory.getInstance().notify(errorMsg, NotificationType.WARNING);
1036
1037            return;
1038        }
1039
1040        // if at exactly one unique checksum is found, then no irregularities
1041        // among the checksums are found.
1042        if (hs.size() == 1) {
1043            log.trace("No irregularities found for the file with id '{}'.", fileId);
1044
1045            // Tell all the replicafileinfo entries that their checksum
1046            // is ok
1047            for (ReplicaFileInfo rfi : rfis) {
1048                // only set OK for those replica where the file is.
1049                if (rfi.getFileListState() == FileListStatus.OK) {
1050                    updateReplicaFileInfoChecksumOk(rfi.getGuid(), con);
1051                }
1052            }
1053
1054            // go to next entry in the file table.
1055            return;
1056        }
1057
1058        // Make a list of the checksums for voting.
1059        List<String> checksums = new ArrayList<String>();
1060        for (ReplicaFileInfo rfi : rfis) {
1061            if (rfi.getFileListState() == FileListStatus.OK) {
1062                checksums.add(rfi.getChecksum());
1063            }
1064        }
1065
1066        // vote to find the unique most common checksum (null if no unique).
1067        String uniqueChecksum = vote(checksums);
1068
1069        if (uniqueChecksum != null) {
1070            // change checksum_status to CORRUPT for the replicafileinfo
1071            // which
1072            // does not have the chosen checksum.
1073            // Set the others replicafileinfo entries to OK.
1074            for (ReplicaFileInfo rfi : rfis) {
1075                if (!rfi.getChecksum().equals(uniqueChecksum)) {
1076                    updateReplicaFileInfoChecksumCorrupt(rfi.getGuid(), con);
1077                } else {
1078                    updateReplicaFileInfoChecksumOk(rfi.getGuid(), con);
1079                }
1080            }
1081        } else {
1082            // Handle the case, when no checksum has most votes.
1083            String errMsg = "There is no winner of the votes between the replicas for the checksum of file '"
1084                    + retrieveFilenameForFileId(fileId, con) + "'.";
1085            log.warn(errMsg);
1086
1087            // send a notification
1088            NotificationsFactory.getInstance().notify(errMsg, NotificationType.WARNING);
1089
1090            // set all replicafileinfo entries to unknown
1091            for (ReplicaFileInfo rfi : rfis) {
1092                updateReplicaFileInfoChecksumUnknown(rfi.getGuid(), con);
1093            }
1094        }
1095    }
1096
1097    /**
1098     * Add information about one file in a given replica.
1099     *
1100     * @param file The name of a file
1101     * @param replica A replica
1102     * @param con An open connection to the ArchiveDatabase
1103     * @return the ReplicaFileInfo ID for the given filename and replica in the database
1104     */
1105    protected static long addFileInformation(String file, Replica replica, Connection con) {
1106        // retrieve the file_id for the file.
1107        long fileId = ReplicaCacheHelpers.retrieveIdForFile(file, con);
1108        // If not found, log and create the file in the database.
1109        if (fileId < 0) {
1110            log.info("The file '{}' was not found in the database. Thus creating entry for the file.", file);
1111            // insert the file and retrieve its file_id.
1112            fileId = ReplicaCacheHelpers.insertFileIntoDB(file, con);
1113        }
1114
1115        // retrieve the replicafileinfo_guid for this entry.
1116        long rfiId = ReplicaCacheHelpers.retrieveReplicaFileInfoGuid(fileId, replica.getId(), con);
1117        // if not found log and create the replicafileinfo in the database.
1118        if (rfiId < 0) {
1119            log.warn("Cannot find the file '{}' for replica '{}'. Thus creating missing entry before updating.", file,
1120                    replica.getId());
1121            ReplicaCacheHelpers.createReplicaFileInfoEntriesInDB(fileId, con);
1122        }
1123
1124        // update the replicafileinfo of this file:
1125        // filelist_checkdate, filelist_status, upload_status
1126        ReplicaCacheHelpers.updateReplicaFileInfoFilelist(rfiId, con);
1127
1128        return rfiId;
1129    }
1130
1131    /**
1132     * Process checksum information about one file in a given replica. and update the database accordingly.
1133     *
1134     * @param filename The name of a file
1135     * @param checksum The checksum of that file.
1136     * @param replica A replica
1137     * @param con An open connection to the ArchiveDatabase
1138     * @return the ReplicaFileInfo ID for the given filename and replica in the database
1139     */
1140    public static long processChecksumline(String filename, String checksum, Replica replica, Connection con) {
1141
1142        // The ID for the file.
1143        long fileid = -1;
1144
1145        // If the file is not within DB, then insert it.
1146        int count = DBUtils.selectIntValue(con, "SELECT COUNT(*) FROM file WHERE filename = ?", filename);
1147
1148        if (count == 0) {
1149            log.info("Inserting the file '{}' into the database.", filename);
1150            fileid = ReplicaCacheHelpers.insertFileIntoDB(filename, con);
1151        } else {
1152            fileid = ReplicaCacheHelpers.retrieveIdForFile(filename, con);
1153        }
1154
1155        // If the file does not already exists in the database, create it
1156        // and retrieve the new ID.
1157        if (fileid < 0) {
1158            log.warn("Inserting the file '{}' into the database, again: This should never happen!!!", filename);
1159            fileid = ReplicaCacheHelpers.insertFileIntoDB(filename, con);
1160        }
1161
1162        // Retrieve the replicafileinfo for the file at the replica.
1163        long rfiId = ReplicaCacheHelpers.retrieveReplicaFileInfoGuid(fileid, replica.getId(), con);
1164
1165        // Check if there already is an entry in the replicafileinfo table.
1166        // rfiId is negative if no entry was found.
1167        if (rfiId < 0) {
1168            // insert the file into the table.
1169            ReplicaCacheHelpers.createReplicaFileInfoEntriesInDB(fileid, con);
1170            log.info("Inserted file '{}' for replica '{}' into replicafileinfo.", filename, replica.toString());
1171        }
1172
1173        // Update this table
1174        ReplicaCacheHelpers.updateReplicaFileInfoChecksum(rfiId, checksum, con);
1175        log.trace("Updated file '{}' for replica '{}' into replicafileinfo.", filename, replica.toString());
1176
1177        return rfiId;
1178    }
1179
1180}