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}