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.checksum;
024
025import java.io.BufferedReader;
026import java.io.File;
027import java.io.FileReader;
028import java.io.FileWriter;
029import java.io.IOException;
030import java.io.InputStream;
031import java.util.Collections;
032import java.util.Date;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Map;
036
037import org.apache.commons.io.IOUtils;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041import dk.netarkivet.archive.ArchiveSettings;
042import dk.netarkivet.archive.arcrepositoryadmin.AdminData;
043import dk.netarkivet.common.CommonSettings;
044import dk.netarkivet.common.distribute.RemoteFile;
045import dk.netarkivet.common.distribute.arcrepository.ReplicaStoreState;
046import dk.netarkivet.common.exceptions.ArgumentNotValid;
047import dk.netarkivet.common.exceptions.IOFailure;
048import dk.netarkivet.common.exceptions.IllegalState;
049import dk.netarkivet.common.utils.ChecksumCalculator;
050import dk.netarkivet.common.utils.FileUtils;
051import dk.netarkivet.common.utils.KeyValuePair;
052import dk.netarkivet.common.utils.Settings;
053import dk.netarkivet.common.utils.batch.ChecksumJob;
054
055/**
056 * A checksum archive in the form of a file (as alternative to a database).<br>
057 * <p>
058 * Each entry in the file is on its own line, thus the number of lines is the number of entries.<br>
059 * The entries on a line is in the format of a ChecksumJob: <br>
060 * <b>'filename' + ## + 'checksum'</b> <br>
061 * The lines are not sorted.
062 * <p>
063 * If no file exists when the class is instantiated then it will be created, and if an 'admin.data' file exists, then it
064 * will be loaded and put into the archive file.
065 */
066@SuppressWarnings({"deprecation"})
067public final class FileChecksumArchive implements ChecksumArchive {
068
069    /** The character sequence for separating the filename from the checksum. */
070    private static final String CHECKSUM_SEPARATOR = ChecksumJob.STRING_FILENAME_SEPARATOR;
071
072    /** The prefix to the filename. */
073    private static final String FILENAME_PREFIX = "checksum_";
074    /** The suffix to the filename. */
075    private static final String FILENAME_SUFFIX = ".md5";
076    /** The suffix of the filename of the recreation file. */
077    private static final String RECREATE_PREFIX = "recreate_";
078    /** The suffix of the filename of the recreation file. */
079    private static final String RECREATE_SUFFIX = ".checksum";
080    /** The prefix to the removedEntryFile. */
081    private static final String WRONG_FILENAME_PREFIX = "removed_";
082    /** The suffix to the removedEntryFile. */
083    private static final String WRONG_FILENAME_SUFFIX = ".checksum";
084
085    /** The logger used by this class. */
086    private static final Logger log = LoggerFactory.getLogger(FileChecksumArchive.class);
087
088    /** The current instance of this class. */
089    private static FileChecksumArchive instance;
090
091    /**
092     * The file to store the checksum. Each line should contain the following: arc-filename + ## + checksum.
093     */
094    private File checksumFile;
095
096    /**
097     * The file for storing all the deleted entries. Each entry should be: 'date :' + 'wrongEntry'.
098     */
099    private File wrongEntryFile;
100
101    /**
102     * The last modified date for the checksum file. This variable is used for determining whether to reload the archive
103     * from the checksum file, when they are synchronized. This has to be updated whenever the checksum file is changed.
104     */
105    private long lastModifiedChecksumFile;
106
107    /**
108     * This map consists of the archive loaded into the memory. It is faster to use a memory archive than the the
109     * checksum file, though all entries must exist both in the file and the memory.
110     * <p>
111     * Map(file -> checksum).
112     */
113    private Map<String, String> checksumArchive = Collections.synchronizedMap(new HashMap<String, String>());
114
115    /** The minimum space left. */
116    private long minSpaceLeft;
117
118    /**
119     * Method for obtaining the current singleton instance of this class. If the instance of this class has not yet been
120     * constructed, then it will be initialised.
121     *
122     * @return The current instance of this class.
123     */
124    public static synchronized FileChecksumArchive getInstance() {
125        if (instance == null) {
126            instance = new FileChecksumArchive();
127        }
128        return instance;
129    }
130
131    /**
132     * Constructor. Retrieves the minimum space left variable, and ensures the existence of the archive file. If the
133     * file does not exist, then it is created.
134     *
135     * @throws ArgumentNotValid If the variable minimum space left is smaller than zero.
136     * @throws IOFailure If the checksum file cannot be created.
137     */
138    private FileChecksumArchive() throws IOFailure, ArgumentNotValid {
139        super();
140
141        // Get the minimum space left setting.
142        minSpaceLeft = Settings.getLong(ArchiveSettings.CHECKSUM_MIN_SPACE_LEFT);
143        // make sure, that minSpaceLeft is non-negative.
144        if (minSpaceLeft < 0) {
145            String msg = "Wrong setting of minSpaceRequired read from " + "Settings: int " + minSpaceLeft;
146            log.warn(msg);
147            throw new ArgumentNotValid(msg);
148        }
149
150        // Initialize the archive and bad-entry files.
151        initializeFiles();
152    }
153
154    /**
155     * Method for retrieving the name of the checksum file.
156     *
157     * @return The checksum file name.
158     */
159    public String getFileName() {
160        return checksumFile.getPath();
161    }
162
163    /**
164     * Method for retrieving the name of the wrongEntryFile.
165     *
166     * @return The wrong entry file name.
167     */
168    public String getWrongEntryFilename() {
169        return wrongEntryFile.getPath();
170    }
171
172    /**
173     * Method for testing where there is enough space left on local drive.
174     *
175     * @return Whether there is enough space left.
176     */
177    public boolean hasEnoughSpace() {
178        // The file must be valid and have enough space.
179        if (checkArchiveFile(checksumFile) && (FileUtils.getBytesFree(checksumFile) > minSpaceLeft)) {
180            return true;
181        }
182        return false;
183    }
184
185    /**
186     * Method for testing whether there is enough left on the local drive for recreating the checksum file.
187     *
188     * @return False only if there is not enough space left.
189     */
190    private boolean hasEnoughSpaceForRecreate() {
191        // check if the checksum file is larger than space left and the minimum
192        // space left.
193        if (checksumFile.length() + minSpaceLeft > FileUtils.getBytesFree(checksumFile)) {
194            return false;
195        }
196
197        return true;
198    }
199
200    /**
201     * Method for initializing the files. Starts by initializing the removedEntryFile before initializing the
202     * checksumFile. If the checksum file already exists, then it is loaded into memory.
203     */
204    private void initializeFiles() {
205        // Extract the dir-name and create the dir (if it does not yet exist).
206        File checksumDir = new File(Settings.get(ArchiveSettings.CHECKSUM_BASEDIR));
207        if (!checksumDir.exists()) {
208            checksumDir.mkdir();
209        }
210
211        // Get the name and initialise the wrong entry file.
212        wrongEntryFile = new File(checksumDir, makeWrongEntryFileName());
213
214        // ensure that the file exists.
215        if (!wrongEntryFile.exists()) {
216            try {
217                wrongEntryFile.createNewFile();
218            } catch (IOException e) {
219                String msg = "Cannot create 'wrongEntryFile'!";
220                log.error(msg);
221                throw new IOFailure(msg, e);
222            }
223        }
224
225        // get the name of the file and initialise it.
226        checksumFile = new File(checksumDir, makeChecksumFileName());
227
228        // Create file is checksumFile does not exist.
229        if (!checksumFile.exists()) {
230            try {
231                checksumFile.createNewFile();
232                lastModifiedChecksumFile = checksumFile.lastModified();
233            } catch (IOException e) {
234                String msg = "Cannot create checksum archive file!";
235                log.error(msg);
236                throw new IOFailure(msg, e);
237            }
238        } else {
239            // If the archive file already exists, then it must consist of the
240            // archive for this replica. It must therefore be loaded into the
241            // memory.
242            loadFile();
243        }
244
245        // If the archive is new or otherwise empty, then try to load admin.data
246        if (checksumArchive.isEmpty()) {
247            loadAdminData();
248        }
249    }
250
251    /**
252     * Loads an existing checksum archive file into the memory. This will go through every line, and if the line is
253     * valid, then it is loaded into the checksumArchive map in the memory. If the line is invalid then a warning is
254     * issued and the line is put into the wrongEntryFile.
255     * <p>
256     * If a bad entry is found, then the archive file has to be recreated afterwards, since the bad entry otherwise
257     * still would be in the archive file.
258     */
259    private void loadFile() {
260        // Checks whether a bad entry was found, to decide whether the archive
261        // file should be recreated.
262        boolean recreate = false;
263
264        // extract all the data from the file.
265        List<String> entries;
266
267        // This should be synchronized to prevent reading the file while it is
268        // being written.
269        synchronized (checksumFile) {
270            entries = FileUtils.readListFromFile(checksumFile);
271        }
272
273        String filename;
274        String checksum;
275
276        // go through all entries and extract their filename and checksum.
277        for (String record : entries) {
278            try {
279                KeyValuePair<String, String> entry = ChecksumJob.parseLine(record);
280                // extract the filename and checksum
281                filename = entry.getKey();
282                checksum = entry.getValue();
283                // If their are extracted correct, then they will be put
284                // into the archive.
285                checksumArchive.put(filename, checksum);
286            } catch (IllegalState e) {
287                log.warn("An invalid entry in the loaded file: '{}' This will be put in the wrong entry file.", record,
288                        e);
289                // put into wrongEntryFile!
290                appendWrongRecordToWrongEntryFile(record);
291                recreate = true;
292            }
293        }
294
295        // If a bad entry is found, then the archive file should be recreated.
296        // Otherwise the bad entries might still be in the archive file next
297        // time the FileChecksumArchive is initialized/restarted.
298        if (recreate) {
299            recreateArchiveFile();
300        }
301
302        // retrieve the 'last modified' from the checksum file.
303        lastModifiedChecksumFile = checksumFile.lastModified();
304    }
305
306    /**
307     * This function is made for the converting the checksum part of admin.data to an actual checksum replica. If no
308     * usable admin.data file is found, then we start with an empty archive.
309     */
310    private void loadAdminData() {
311        log.debug("Empty archive, trying to load an admin.data file");
312
313        File adminFile = new File("admin.data");
314
315        if (!adminFile.exists() || !adminFile.isFile()) {
316            log.info("No admin.data file found, starts with empty archive.");
317            return;
318        }
319        if (!adminFile.canRead()) {
320            log.warn("Cannot read admin.data. Starts with empty archive.");
321            return;
322        }
323
324        // line length;
325        final int lineLength = 4;
326        boolean recreate = false;
327
328        BufferedReader in = null;
329        try {
330            try {
331                in = new BufferedReader(new FileReader(adminFile));
332                String line = in.readLine();
333                if (line == null) {
334                    return;
335                }
336                if (!line.contains(AdminData.VERSION_NUMBER)) {
337                    log.warn("The first line in Admin.data tells the version. Expected '{}', but got: {}. "
338                            + "Continuing anyway.", AdminData.VERSION_NUMBER, line);
339                } else {
340                    log.debug("Admin.data version: {}", line);
341                }
342
343                // go through the lines, parse them and put them in the archive.
344                while ((line = in.readLine()) != null) {
345                    // Retrieve the basic entry data.
346                    String[] entryData = line.split(" ");
347
348                    // Check if enough elements
349                    if (entryData.length < lineLength) {
350                        log.warn("bad line in admin data: {}", line);
351                        continue;
352                    }
353
354                    String filename = entryData[0];
355                    String checksum = entryData[1];
356                    String uploadState = entryData[2];
357
358                    if (uploadState.equals(ReplicaStoreState.UPLOAD_COMPLETED.toString())) {
359                        if (checksumArchive.containsKey(filename)) {
360                            recreate = true;
361                        }
362                        checksumArchive.put(filename, checksum);
363                        appendEntryToFile(filename, checksum);
364                        log.debug("AdminData line inserted: {}", line);
365                    } else {
366                        log.trace("AdminData line ignored: {}", line);
367                    }
368                }
369            } finally {
370                if (in != null) {
371                    in.close();
372                }
373            }
374        } catch (IOException e) {
375            String msg = "An error occurred during reading the admin data file " + adminFile.getAbsolutePath();
376            throw new IOFailure(msg, e);
377        }
378
379        // If a entry have been written twice, then recreate the archive file.
380        if (recreate) {
381            recreateArchiveFile();
382        }
383
384        log.info("Finished loading admin data.");
385    }
386
387    /**
388     * Recreates the archive file from the memory. Makes a new file which contains the entire archive, and then move the
389     * new archive file on top of the old one. This is used when to recreate the archive file, when an record has been
390     * removed.
391     *
392     * @throws IOFailure If a problem occur when writing the new file.
393     */
394    private void recreateArchiveFile() throws IOFailure {
395        try {
396            // Handle the case, when there is not enough space left for
397            // recreating the
398            if (!hasEnoughSpaceForRecreate()) {
399                log.error("Not enough space left to recreate the checksum file.");
400                throw new IOFailure("Not enough space left to recreate the checksum file.");
401            }
402
403            // This should be synchronized, so no new entries can be made
404            // while recreating the archive file.
405            synchronized (checksumFile) {
406                // initialize and create the file.
407                File recreateFile = new File(checksumFile.getParentFile(), makeRecreateFileName());
408                if (!recreateFile.createNewFile()) {
409                    log.warn("Cannot create new file. The recreate checksum file did already exist.");
410                }
411
412                // put the archive into the file.
413                FileWriter fw = new FileWriter(recreateFile);
414                try {
415                    for (Map.Entry<String, String> entry : checksumArchive.entrySet()) {
416                        String record = entry.getKey() + CHECKSUM_SEPARATOR + entry.getValue();
417                        fw.append(record + "\n");
418                    }
419                } finally {
420                    fw.flush();
421                    fw.close();
422                }
423
424                // Move the file.
425                FileUtils.moveFile(recreateFile, checksumFile);
426            }
427        } catch (IOException e) {
428            String errMsg = "The checksum file has not been recreated as attempted. "
429                    + "The archive in memory and the one on file are no longer identical.";
430            log.error(errMsg, e);
431            throw new IOFailure(errMsg, e);
432        }
433    }
434
435    /**
436     * Creates the string for the name of the checksum file. E.g. checksum_REPLICA.md5.
437     *
438     * @return The name of the file.
439     */
440    private String makeChecksumFileName() {
441        return FILENAME_PREFIX + Settings.get(CommonSettings.USE_REPLICA_ID) + FILENAME_SUFFIX;
442    }
443
444    /**
445     * Creates the string for the name of the recreate file. E.g. recreate_REPLICA.checksum.
446     *
447     * @return The name of the file for recreating the checksum file.
448     */
449    private String makeRecreateFileName() {
450        return RECREATE_PREFIX + Settings.get(CommonSettings.USE_REPLICA_ID) + RECREATE_SUFFIX;
451    }
452
453    /**
454     * Creates the string for the name of the wrongEntryFile. E.g. removed_REPLICA.checksum
455     *
456     * @return The name of the wrongEntryFile.
457     */
458    private String makeWrongEntryFileName() {
459        return WRONG_FILENAME_PREFIX + Settings.get(CommonSettings.USE_REPLICA_ID) + WRONG_FILENAME_SUFFIX;
460    }
461
462    /**
463     * Method for validating a file for use as checksum file. This basically checks whether the file exists, whether it
464     * is a directory instead of a file, and whether it is writable.
465     * <p>
466     * It has to exist and be writable, but it may not be a directory.
467     *
468     * @param file The file to validate.
469     * @return Whether the file is valid.
470     */
471    private boolean checkArchiveFile(File file) {
472        // The file must exist.
473        if (!file.isFile()) {
474            log.warn("The file '{}' is not a valid file.", file.getAbsolutePath());
475            return false;
476        }
477        // It must be writable.
478        if (!file.canWrite()) {
479            log.warn("The file '{}' is not writable", file.getAbsolutePath());
480            return false;
481        }
482        return true;
483    }
484
485    /**
486     * Appending an checksum archive entry to the checksum file. The record string is created and appended to the file.
487     *
488     * @param filename The name of the file to add.
489     * @param checksum The checksum of the file to add.
490     * @throws IOFailure If something is wrong when writing to the file.
491     */
492    private synchronized void appendEntryToFile(String filename, String checksum) throws IOFailure {
493        // initialise the record.
494        String record = filename + CHECKSUM_SEPARATOR + checksum + "\n";
495
496        // get a filewriter for the checksum file, and append the record.
497        boolean appendToFile = true;
498
499        // Synchronize to ensure that the file is not overridden during the
500        // appending of the new entry.
501        synchronized (checksumFile) {
502            try {
503                FileWriter fwrite = new FileWriter(checksumFile, appendToFile);
504                try {
505                    fwrite.append(record);
506                } finally {
507                    // close fileWriter.
508                    fwrite.flush();
509                    fwrite.close();
510                }
511            } catch (IOException e) {
512                throw new IOFailure("An error occurred while appending an entry to the archive file.", e);
513            }
514
515            // The checksum file has been updated and so has its timestamp.
516            // Thus update the last modified date for the checksum file.
517            lastModifiedChecksumFile = checksumFile.lastModified();
518        }
519    }
520
521    /**
522     * Method for appending a 'wrong' entry in the wrongEntryFile. It will be written when the wrong entry was appended:
523     * date + " : " + wrongRecord.
524     *
525     * @param wrongRecord The record to append.
526     * @throws IOFailure If the wrong record cannot be appended correctly.
527     */
528    private synchronized void appendWrongRecordToWrongEntryFile(String wrongRecord) throws IOFailure {
529        try {
530            // Create the string to append: date + 'wrong record'.
531            String entry = new Date().toString() + " : " + wrongRecord + "\n";
532
533            // get a filewriter for the checksum file, and append the record.
534            boolean appendToFile = true;
535            FileWriter fwrite = new FileWriter(wrongEntryFile, appendToFile);
536            fwrite.append(entry);
537
538            // close fileWriter.
539            fwrite.flush();
540            fwrite.close();
541        } catch (IOException e) {
542            log.warn("Cannot put a bad record to the 'wrongEntryFile'.", e);
543            throw new IOFailure("Cannot put a bad record to the 'wrongEntryFile'.", e);
544        }
545    }
546
547    /**
548     * The method for uploading a file to the archive.
549     *
550     * @param file The remote file containing the file to be uploaded.
551     * @param filename The name of the arcFile.
552     * @throws ArgumentNotValid If the RemoteFile is null or if the filename is not valid.
553     * @throws IllegalState If the file already within the archive but with a different checksum.
554     */
555    public void upload(RemoteFile file, String filename) throws ArgumentNotValid, IllegalState {
556        // Validate arguments.
557        ArgumentNotValid.checkNotNull(file, "RemoteFile file");
558        ArgumentNotValid.checkNotNullOrEmpty(filename, "String filename");
559        
560        InputStream input = null;
561
562        try {
563            input = file.getInputStream();
564            synchronizeMemoryWithFile();
565            String checksum = calculateChecksum(input);
566
567            if (checksumArchive.containsKey(filename)) {
568                if (checksumArchive.get(filename).equals(checksum)) {
569                    log.warn("Cannot upload arcfile '{}', it is already archived with the same checksum: '{}",
570                            filename, checksum);
571                } else {
572                    throw new IllegalState("Cannot upload arcfile '" + filename
573                            + "', it is already archived with different checksum." + " Archive checksum: '"
574                            + checksumArchive.get(filename) + "' and the uploaded file has: '" + checksum + "'.");
575                }
576
577                // It is considered a success that it already is within the archive,
578                // thus do not throw an exception.
579                return;
580            }
581
582            // otherwise put the file into memory and file.
583            appendEntryToFile(filename, checksum);
584            checksumArchive.put(filename, checksum);
585        } finally {
586            if (input != null) {
587                IOUtils.closeQuietly(input);
588            }
589        }
590
591    }
592    
593    public void upload(String checksum, String filename) throws ArgumentNotValid, IllegalState {
594        // Validate arguments.
595        ArgumentNotValid.checkNotNull(checksum, "String checksum");
596        ArgumentNotValid.checkNotNullOrEmpty(filename, "String filename");
597
598        synchronizeMemoryWithFile();
599        if (checksumArchive.containsKey(filename)) {
600                if (checksumArchive.get(filename).equals(checksum)) {
601                        log.warn("Cannot upload arcfile '{}', it is already archived with the same checksum: '{}",
602                                        filename, checksum);
603                } else {
604                        throw new IllegalState("Cannot upload arcfile '" + filename
605                                        + "', it is already archived with different checksum." + " Archive checksum: '"
606                                        + checksumArchive.get(filename) + "' and the uploaded file has: '" + checksum + "'.");
607                }
608
609                // It is considered a success that it already is within the archive,
610                // thus do not throw an exception.
611                return;
612        }
613
614        // otherwise put the file into memory and file.
615        appendEntryToFile(filename, checksum);
616        checksumArchive.put(filename, checksum);
617    }
618
619    /**
620     * Method for retrieving the checksum of a record, based on the filename.
621     *
622     * @param filename The name of the file to have recorded in the archive.
623     * @return The checksum of a record, or null if it was not found.
624     * @throws ArgumentNotValid If the filename is not valid (null or empty).
625     */
626    @Override
627    public String getChecksum(String filename) throws ArgumentNotValid {
628        // validate the argument
629        ArgumentNotValid.checkNotNullOrEmpty(filename, "String filename");
630
631        synchronizeMemoryWithFile();
632
633        // Return the checksum of the record.
634        return checksumArchive.get(filename);
635    }
636
637    /**
638     * Method for checking whether an entry exists within the archive.
639     *
640     * @param filename The name of the file whose entry in the archive should be determined.
641     * @return Whether an entry with the filename was found.
642     */
643    @Override
644    public boolean hasEntry(String filename) {
645        ArgumentNotValid.checkNotNullOrEmpty(filename, "String filename");
646
647        // Return whether the archive contains an entry with the filename.
648        return checksumArchive.containsKey(filename);
649    }
650
651    /**
652     * Method for calculating the checksum of a file.
653     *
654     * @param f The file to calculate the checksum of.
655     * @return The checksum of the file.
656     * @throws IOFailure If a IOException is caught during the calculation of the MD5-checksum.
657     */
658    @Override
659    public String calculateChecksum(File f) throws IOFailure {
660        return ChecksumCalculator.calculateMd5(f);
661    }
662
663    /**
664     * Method for calculating the checksum of a inputstream.
665     *
666     * @param is The inputstream to calculate the checksum of.
667     * @return The checksum of the inputstream.
668     * @throws IOFailure If a error occurs during the generation of the MD5 checksum.
669     */
670    @Override
671    public String calculateChecksum(InputStream is) throws IOFailure {
672        return ChecksumCalculator.calculateMd5(is);
673    }
674
675    /**
676     * Method for correcting a bad entry from the archive. The current incorrect entry is put into the wrongEntryFile.
677     * Then it calculates the checksum and corrects the entry for the file, and then the checksum file is recreated from
678     * the archive in the memory.
679     *
680     * @param filename The name of the file whose record should be removed.
681     * @param correctFile The file that should replace the current entry
682     * @return A file containing the removed entry.
683     * @throws ArgumentNotValid If one of the arguments are not valid.
684     * @throws IOFailure If the entry cannot be corrected. Either the bad entry cannot be stored, or the new checksum
685     * file cannot be created. Or if a file for the removed entry cannot be created.
686     * @throws IllegalState If no such entry exists to be corrected, or if the entry has a different checksum than the
687     * incorrectChecksum.
688     */
689    @Override
690    public File correct(String filename, File correctFile) throws IOFailure, ArgumentNotValid, IllegalState {
691        ArgumentNotValid.checkNotNullOrEmpty(filename, "String filename");
692        ArgumentNotValid.checkNotNull(correctFile, "File correctFile");
693
694        // synchronize the memory.
695        synchronizeMemoryWithFile();
696
697        // If no file entry exists, then IllegalState
698        if (!checksumArchive.containsKey(filename)) {
699            String errMsg = "No file entry for file '" + filename + "'.";
700            log.error(errMsg);
701            throw new IllegalState(errMsg);
702        }
703
704        // retrieve the checksum
705        String currentChecksum = checksumArchive.get(filename);
706
707        // Calculate the new checksum and verify that it is different.
708        String newChecksum = calculateChecksum(correctFile);
709        if (newChecksum.equals(currentChecksum)) {
710            // This should never occur.
711            throw new IllegalState("The checksum of the old 'bad' entry is "
712                    + " the same as the checksum of the new correcting entry");
713        }
714
715        // Make entry in the wrongEntryFile.
716        String badEntry = ChecksumJob.makeLine(filename, currentChecksum);
717        appendWrongRecordToWrongEntryFile(badEntry);
718
719        // Correct the bad entry, by changing the value to the newChecksum.'
720        // Since the checksumArchive is a hashmap, then putting an existing
721        // entry with a new value will override the existing one.
722        checksumArchive.put(filename, newChecksum);
723
724        // Recreate the archive file.
725        recreateArchiveFile();
726
727        // Make the file containing the bad entry be returned in the
728        // CorrectMessage.
729        File removedEntryFile;
730        try {
731            // Initialise file and writer.
732            removedEntryFile = File.createTempFile(filename, "tmp", FileUtils.getTempDir());
733            FileWriter fw = new FileWriter(removedEntryFile);
734
735            // Write the bad entry.
736            fw.write(badEntry);
737
738            // flush and close.
739            fw.flush();
740            fw.close();
741        } catch (IOException e) {
742            throw new IOFailure("Unable to create return file for CorrectMessage", e);
743        }
744
745        // Return the file containing the removed entry.
746        return removedEntryFile;
747    }
748
749    /**
750     * Method for retrieving the archive as a temporary file containing the checksum entries. Each line should contain
751     * one checksum entry in the format produced by the ChecksumJob.
752     *
753     * @return A temporary checksum file, which is a copy of the archive file.
754     * @throws IOFailure If problems occurs during the creation of the file.
755     */
756    @Override
757    public File getArchiveAsFile() throws IOFailure {
758        synchronizeMemoryWithFile();
759
760        try {
761            // create new temporary file of the archive.
762            File tempFile = File.createTempFile("tmp", "tmp", FileUtils.getTempDir());
763            synchronized (checksumFile) {
764                FileUtils.copyFile(checksumFile, tempFile);
765            }
766
767            return tempFile;
768        } catch (IOException e) {
769            String msg = "Cannot create the output file containing all the entries of this archive.";
770            log.warn(msg);
771            throw new IOFailure(msg);
772        }
773    }
774
775    /**
776     * Method for retrieving the names of all the files within the archive as a temporary file.
777     *
778     * @return A temporary file containing the list of all the filenames. This file has one filename per line.
779     * @throws IOFailure If problems occurs during the creation of the file.
780     */
781    @Override
782    public File getAllFilenames() throws IOFailure {
783        synchronizeMemoryWithFile();
784
785        try {
786            File tempFile = File.createTempFile("tmp", "tmp", FileUtils.getTempDir());
787            FileWriter fw = new FileWriter(tempFile);
788
789            try {
790                // put the content into the file.
791                for (String filename : checksumArchive.keySet()) {
792                    fw.append(filename);
793                    fw.append("\n");
794                }
795
796            } finally {
797                // flush and close the file, before returning it.
798                fw.flush();
799                fw.close();
800            }
801            return tempFile;
802        } catch (IOException e) {
803            String msg = "Cannot create the output file containing the filenames of all the entries of this archive.";
804            log.warn(msg);
805            throw new IOFailure(msg);
806        }
807    }
808
809    /**
810     * Ensures that the file and memory archives are identical.
811     * <p>
812     * The timestamp of last communication with the file (read/write) will be checked whether it corresponds the 'last
813     * modified' date of the file. If they are different, then the memory archive is reloaded from the file.
814     */
815    private synchronized void synchronizeMemoryWithFile() {
816        log.debug("Synchronizing memory archive with file archive.");
817
818        // Check if the checksum file has changed since last access.
819        if (checksumFile.lastModified() > lastModifiedChecksumFile) {
820            log.warn("Archive in memory out of sync with archive in file.");
821
822            // The archive is then reloaded by clearing the current memory
823            // archive and loading the file again.
824            checksumArchive.clear();
825            // The 'last modified' is reset during loading.
826            loadFile();
827        }
828    }
829
830    /**
831     * The method for cleaning up when done. It sets the checksum file and the instance to null.
832     */
833    @Override
834    public void cleanup() {
835        checksumFile = null;
836        instance = null;
837        if (checksumArchive != null) {
838            checksumArchive.clear();
839        }
840    }
841
842}