/* Copyright 2004-2014 Jim Voris * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.qumasoft.server; import com.qumasoft.qvcslib.QVCSConstants; import com.qumasoft.qvcslib.QVCSException; import com.qumasoft.qvcslib.TimerManager; import com.qumasoft.qvcslib.Utility; import com.qumasoft.qvcslib.commandargs.GetRevisionCommandArgs; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.TimerTask; import java.util.logging.Level; import java.util.logging.Logger; /** * A class to manage the collection of digests associated with a given file revision. It is a singleton. * * @author Jim Voris */ public final class ArchiveDigestManager { // Create our logger object private static final Logger LOGGER = Logger.getLogger("com.qumasoft.server"); /** * Wait 2 seconds before saving the latest archive digest store. */ private static final long SAVE_ARCHIVE_DIGEST_DELAY = 1000L * 2L; private static final ArchiveDigestManager ARCHIVE_DIGEST_MANAGER = new ArchiveDigestManager(); private boolean isInitializedFlag; private String storeName; private String oldStoreName; private ArchiveDigestDictionaryStore store; private MessageDigest messageDigest; private final Object messageDigestSyncObject = new Object(); private SaveArchiveDigestStoreTimerTask saveArchiveDigestStoreTimerTask; /** * Creates a new instance of WorkfileDigestDictionary. */ private ArchiveDigestManager() { try { messageDigest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { LOGGER.log(Level.SEVERE, "Failed to create MD5 digest instance! " + e.getClass().toString() + " " + e.getLocalizedMessage()); } } /** * Get the server archive digest manager singleton. * @return the server archive digest manager singleton. */ public static ArchiveDigestManager getInstance() { return ARCHIVE_DIGEST_MANAGER; } /** * Initialize the server archive digest manager singleton. * @param type the type of project. * @return true if things initialized successfully; false otherwise. */ public boolean initialize(String type) { if (!isInitializedFlag) { storeName = System.getProperty("user.dir") + File.separator + QVCSConstants.QVCS_META_DATA_DIRECTORY + File.separator + QVCSConstants.QVCS_ARCHIVE_DIGEST_STORE_NAME + type + ".dat"; oldStoreName = storeName + ".old"; loadStore(); isInitializedFlag = true; } return isInitializedFlag; } byte[] getArchiveDigest(LogFile logfile, String revisionString) { byte[] retVal = store.lookupArchiveDigest(logfile, revisionString); if (retVal == null) { // There's no digest yet. retVal = computeDigest(logfile, revisionString); store.addDigest(logfile, revisionString, retVal); } return retVal; } synchronized byte[] addRevision(LogFileImpl logfile, String revisionString, byte[] buffer) { scheduleSaveOfArchiveDigestStore(); byte[] digest; synchronized (messageDigestSyncObject) { messageDigest.reset(); digest = messageDigest.digest(buffer); store.addDigest(logfile, revisionString, digest); } return digest; } /** * Add and return the digest for the given revision string. * @param logfile the logfile from which we'll compute the digest. * @param revisionString the revision string for which we need to compute and add the digest. * @return the computed digest. */ public synchronized byte[] addRevision(LogFile logfile, String revisionString) { scheduleSaveOfArchiveDigestStore(); byte[] digest = computeDigest(logfile, revisionString); store.addDigest(logfile, revisionString, digest); return digest; } private byte[] computeDigest(LogFile logfile, String revisionString) { byte[] digest = null; FileInputStream inStream = null; File tempFile = null; try { synchronized (messageDigestSyncObject) { messageDigest.reset(); tempFile = File.createTempFile("QVCS", "tmp"); GetRevisionCommandArgs commandArgs = new GetRevisionCommandArgs(); commandArgs.setOutputFileName(tempFile.getAbsolutePath()); commandArgs.setRevisionString(revisionString); commandArgs.setShortWorkfileName(tempFile.getAbsolutePath()); if (logfile.getRevision(commandArgs, tempFile.getAbsolutePath())) { inStream = new FileInputStream(tempFile); byte[] buffer = new byte[(int) tempFile.length()]; Utility.readDataFromStream(buffer, inStream); LOGGER.log(Level.FINEST, "computing digest on buffer of size: " + buffer.length + " for file: " + logfile.getShortWorkfileName()); digest = messageDigest.digest(buffer); } } } catch (QVCSException | IOException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } finally { if (inStream != null) { try { inStream.close(); } catch (IOException e) { LOGGER.log(Level.WARNING, "caught exception: " + e.getClass().toString() + " " + e.getLocalizedMessage()); LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } if (tempFile != null) { tempFile.delete(); } } } return digest; } private void loadStore() { File storeFile; FileInputStream fileStream = null; try { storeFile = new File(storeName); fileStream = new FileInputStream(storeFile); ObjectInputStream inStream = new ObjectInputStream(fileStream); store = (ArchiveDigestDictionaryStore) inStream.readObject(); } catch (FileNotFoundException e) { // The file doesn't exist yet. Create a default store. store = new ArchiveDigestDictionaryStore(); writeStore(); } catch (ClassNotFoundException | IOException e) { // Serialization failed. Create a default store. store = new ArchiveDigestDictionaryStore(); writeStore(); } finally { if (fileStream != null) { try { fileStream.close(); } catch (IOException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } } } } synchronized void writeStore() { FileOutputStream fileStream = null; try { File storeFile = new File(storeName); File oldStoreFile = new File(oldStoreName); if (oldStoreFile.exists()) { oldStoreFile.delete(); } if (storeFile.exists()) { storeFile.renameTo(oldStoreFile); } File newStoreFile = new File(storeName); // Make sure the needed directories exists if (!newStoreFile.getParentFile().exists()) { newStoreFile.getParentFile().mkdirs(); } fileStream = new FileOutputStream(newStoreFile); ObjectOutputStream outStream = new ObjectOutputStream(fileStream); outStream.writeObject(store); } catch (IOException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } finally { if (fileStream != null) { try { fileStream.close(); } catch (IOException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } } } } /** * Schedule the save of the archive digest store. We want to save the archive digest store after things are quiet for the * SAVE_ARCHIVE_DIGEST_DELAY amount of time so that the archive digest values will have been preserved in the case of a crash. */ private void scheduleSaveOfArchiveDigestStore() { if (saveArchiveDigestStoreTimerTask != null) { saveArchiveDigestStoreTimerTask.cancel(); saveArchiveDigestStoreTimerTask = null; } saveArchiveDigestStoreTimerTask = new SaveArchiveDigestStoreTimerTask(); TimerManager.getInstance().getTimer().schedule(saveArchiveDigestStoreTimerTask, SAVE_ARCHIVE_DIGEST_DELAY); } /** * Use a timer to write the digest store after a while so it will have been saved before a crash. */ class SaveArchiveDigestStoreTimerTask extends TimerTask { @Override public void run() { LOGGER.log(Level.INFO, "Performing scheduled save of archive digest store."); writeStore(); } } }