/* 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.AbstractProjectProperties; import com.qumasoft.qvcslib.ArchiveDirManagerBase; import com.qumasoft.qvcslib.ArchiveDirManagerInterface; import com.qumasoft.qvcslib.ArchiveDirManagerReadWriteViewInterface; import com.qumasoft.qvcslib.ArchiveInfoInterface; import com.qumasoft.qvcslib.DirectoryCoordinate; import com.qumasoft.qvcslib.LogfileListenerInterface; import com.qumasoft.qvcslib.QVCSConstants; import com.qumasoft.qvcslib.QVCSException; import com.qumasoft.qvcslib.ServerResponseFactoryInterface; import com.qumasoft.qvcslib.SkinnyLogfileInfo; import com.qumasoft.qvcslib.Utility; import com.qumasoft.qvcslib.commandargs.CheckOutCommandArgs; import com.qumasoft.qvcslib.commandargs.CreateArchiveCommandArgs; import com.qumasoft.qvcslib.commandargs.LockRevisionCommandArgs; import com.qumasoft.qvcslib.commandargs.SetRevisionDescriptionCommandArgs; import com.qumasoft.qvcslib.commandargs.UnlockRevisionCommandArgs; import com.qumasoft.qvcslib.logfileaction.ActionType; import com.qumasoft.qvcslib.logfileaction.CheckOut; import com.qumasoft.qvcslib.logfileaction.Create; import com.qumasoft.qvcslib.logfileaction.Lock; import com.qumasoft.qvcslib.logfileaction.MoveFile; import com.qumasoft.qvcslib.logfileaction.Rename; import com.qumasoft.qvcslib.logfileaction.SetRevisionDescription; import com.qumasoft.qvcslib.logfileaction.Unlock; import com.qumasoft.qvcslib.notifications.ServerNotificationCheckIn; import com.qumasoft.qvcslib.notifications.ServerNotificationCheckOut; import com.qumasoft.qvcslib.notifications.ServerNotificationCreateArchive; import com.qumasoft.qvcslib.notifications.ServerNotificationHeaderChange; import com.qumasoft.qvcslib.notifications.ServerNotificationInterface; import com.qumasoft.qvcslib.notifications.ServerNotificationLock; import com.qumasoft.qvcslib.notifications.ServerNotificationMoveArchive; import com.qumasoft.qvcslib.notifications.ServerNotificationRemoveArchive; import com.qumasoft.qvcslib.notifications.ServerNotificationRenameArchive; import com.qumasoft.qvcslib.notifications.ServerNotificationSetRevisionDescription; import com.qumasoft.qvcslib.notifications.ServerNotificationUnlock; import com.qumasoft.server.dataaccess.BranchDAO; import com.qumasoft.server.dataaccess.DirectoryDAO; import com.qumasoft.server.dataaccess.ProjectDAO; import com.qumasoft.server.dataaccess.impl.BranchDAOImpl; import com.qumasoft.server.dataaccess.impl.DirectoryDAOImpl; import com.qumasoft.server.dataaccess.impl.ProjectDAOImpl; import com.qumasoft.server.datamodel.Branch; import com.qumasoft.server.datamodel.Directory; import com.qumasoft.server.datamodel.Project; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * Archive directory manager. There is one instance per archive directory. This class manages the archive files for a given directory. * * @author Jim Voris */ public class ArchiveDirManager extends ArchiveDirManagerBase implements ArchiveDirManagerReadWriteViewInterface, LogfileListenerInterface { private static final Logger LOGGER = Logger.getLogger("com.qumasoft.qvcslib"); /** * Remote listeners for changes to this directory. */ private final Set<ServerResponseFactoryInterface> instanceLogfileListeners = new HashSet<>(); private final Set<LogfileListenerInterface> instanceCreateListeners = new HashSet<>(); /** * Keep track of oldest revision for this manager. */ private long instanceOldestRevision = Long.MAX_VALUE; // Set the default value of the directoryID to -1. private int instanceDirectoryID = -1; // This directory's parent private ArchiveDirManager instanceParentArchiveDirManager; // Keep track of obsolete file count... so we can know whether to 'delete' them. // We do this so we can migrate old (pre-2.1) directories to new (post 2.1). private int instanceObsoleteFileCount = 0; private Date mostRecentActivityDate = new Date(0L); /** * Creates a new instance of ArchiveDirManager. * * @param projectProperties project properties. * @param view the name of the view. * @param path the appended path. * @param user user name. * @param response response so we know where to send status updates, etc. * @param discardObsoleteFilesFlag whether to move obsolete files to the cemetery. */ public ArchiveDirManager(AbstractProjectProperties projectProperties, String view, String path, String user, ServerResponseFactoryInterface response, boolean discardObsoleteFilesFlag) { super(projectProperties, view, path, user); initArchiveDirectory(); initParent(response, discardObsoleteFilesFlag); } /** * Start the directory manager. Used only on the client. */ @Override public void startDirectoryManager() { // We don't need to do anything. } /** * Get the archive directory manager for this directory's parent directory. Parent, in this context means closer to root directory, i.e. we're navigating up the directory tree. * * @return the parent directory's archive directory manager, or null if this is the root directory. */ public ArchiveDirManager getParent() { return instanceParentArchiveDirManager; } /** * Get the archive directory manager for the root directory of this directory tree. * * @return the root directory's archive directory manager. */ public ArchiveDirManager getProjectRootArchiveDirManager() { ArchiveDirManager projectRootArchiveDirManager = this; while (true) { if (projectRootArchiveDirManager.getParent() == null) { break; } projectRootArchiveDirManager = projectRootArchiveDirManager.getParent(); } return projectRootArchiveDirManager; } private void notifyCreateListeners(LogFile logfile) { Create createAction = new Create(); synchronized (instanceCreateListeners) { Iterator<LogfileListenerInterface> it = instanceCreateListeners.iterator(); while (it.hasNext()) { LogfileListenerInterface listener = it.next(); listener.notifyLogfileListener(logfile, createAction); } } } /** * Add a create listener. * * @param dirManager a listener who wishes to be notified of file create actions that occur within this archive directory manager. */ public void addCreateListener(LogfileListenerInterface dirManager) { synchronized (instanceCreateListeners) { instanceCreateListeners.add(dirManager); } } /** * Remove a create listener. * * @param dirManager a listener who no longer wished to be notified of file create actions that occur within this archive directory manager. */ public void removeCreateListener(ArchiveDirManagerInterface dirManager) { synchronized (instanceCreateListeners) { if (dirManager instanceof LogfileListenerInterface) { LogfileListenerInterface logfileListenerInterface = (LogfileListenerInterface) dirManager; instanceCreateListeners.remove(logfileListenerInterface); } } } /** * Figure out the parent archive directory manager for this archive directory manager. * * @param response an object that identifies the client. * @param discardObsoleteFilesFlag a flag indicating whether to discard obsolete files. */ private void initParent(ServerResponseFactoryInterface response, boolean discardObsoleteFilesFlag) { String parentAppendedPath = getParentAppendedPath(); if (parentAppendedPath != null) { try { DirectoryCoordinate directoryCoordinate = new DirectoryCoordinate(getProjectName(), getViewName(), parentAppendedPath); instanceParentArchiveDirManager = (ArchiveDirManager) ArchiveDirManagerFactoryForServer.getInstance().getDirectoryManager(QVCSConstants.QVCS_SERVER_SERVER_NAME, directoryCoordinate, QVCSConstants.QVCS_SERVED_PROJECT_TYPE, QVCSConstants.QVCS_SERVER_USER, response, discardObsoleteFilesFlag); } catch (QVCSException e) { LOGGER.log(Level.WARNING, "Caught exception when trying to initialize parent directory manager for: [" + getAppendedPath() + "]"); LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } } else { instanceParentArchiveDirManager = null; } } private void initArchiveDirectory() { File directory = new File(getArchiveDirectoryName()); File[] fileList = directory.listFiles(); File directoryIDFile = new File(getArchiveDirectoryName() + File.separator + QVCSConstants.QVCS_DIRECTORYID_FILENAME); // We need to init this first!! initDirectoryID(directoryIDFile); if (fileList == null) { return; } for (File fileList1 : fileList) { if (fileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_CACHE_NAME) == 0) { // Get rid of the cache file, since we may be changing things here that // would make the cache out of date. if (fileList1.delete()) { LOGGER.log(Level.INFO, "Deleting [" + QVCSConstants.QVCS_CACHE_NAME + "] file from directory: [" + directory.getAbsolutePath() + "]"); } continue; } if (fileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_JOURNAL_NAME) == 0) { continue; } if (fileList1.isDirectory()) { continue; } if (fileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_DIRECTORYID_FILENAME) == 0) { continue; } if (fileList1.getName().endsWith(QVCSConstants.QVCS_ARCHIVE_TEMPFILE_SUFFIX)) { continue; } if (fileList1.getName().endsWith(QVCSConstants.QVCS_ARCHIVE_OLDFILE_SUFFIX)) { continue; } if (Utility.isMacintosh() && fileList1.getName().compareToIgnoreCase(QVCSConstants.QVCS_MAC_DS_STORE_FILENAME) == 0) { continue; } LogFile logfile = new LogFile(fileList1.getPath()); if (logfile.readInformation()) { String shortWorkfileName = logfile.getShortWorkfileName(); synchronized (getArchiveInfoCollection()) { getArchiveInfoCollection().put(Utility.getArchiveKey(getProjectProperties(), shortWorkfileName), logfile); } logfile.addListener(this); // Capture the association of this file to this directory. FileIDDictionary.getInstance().saveFileIDInfo(getProjectName(), getViewName(), logfile.getFileID(), getAppendedPath(), logfile.getShortWorkfileName(), getDirectoryID()); // Save the timestamp of the oldest revision in this logfile. setOldestRevision(logfile.getRevisionInformation().getRevisionHeader(logfile.getRevisionCount() - 1).getCheckInDate().getTime()); // If the file is obsolete, then delete it, i.e. move to the cemetery. if (logfile.getIsObsolete()) { instanceObsoleteFileCount++; } } else { LOGGER.log(Level.WARNING, "Failed to read logfile information for: [" + fileList1.getPath() + "]"); } } } void deleteObsoleteFiles(String userName, ServerResponseFactoryInterface response) { if (instanceObsoleteFileCount > 0) { Object[] keys = getArchiveInfoCollection().keySet().toArray(); for (Object key : keys) { String shortWorkfileName = (String) key; try { LogFile logfile = (LogFile) getArchiveInfoCollection().get(shortWorkfileName); if (logfile.getIsObsolete()) { // It's not obsolete anymore because we're moving it to the cemetery. logfile.setIsObsolete(userName, false); deleteArchive(userName, shortWorkfileName, response); instanceObsoleteFileCount--; } } catch (QVCSException | IOException e) { LOGGER.log(Level.WARNING, "Failed to delete obsolete file for: [" + getAppendedPath() + File.separator + shortWorkfileName + "]"); LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } } } } private void setOldestRevision(long revisionCheckInTime) { if (revisionCheckInTime < instanceOldestRevision) { instanceOldestRevision = revisionCheckInTime; } } /** * Get the time of the oldest revision in this directory. (Used for license enforcement). * * @return the number of milliseconds past the epoch for the oldest revision in this directory. */ @Override public long getOldestRevision() { return instanceOldestRevision; } /** * Does the archive directory exist. * * @return true if the directory already exists; false otherwise. */ public boolean directoryExists() { File directory = new File(getArchiveDirectoryName()); return directory.exists(); } /** * Create the archive directory. * * @return true if the create succeeds; false otherwise. */ @Override public boolean createDirectory() { boolean createdDirectory = false; if (!directoryExists()) { File directory = new File(getArchiveDirectoryName()); if (!directory.exists()) { if (!directory.mkdirs()) { LOGGER.log(Level.WARNING, "Failed to create archive directory: [" + directory.getAbsolutePath() + "]"); } else { ProjectDAO projectDAO = new ProjectDAOImpl(); Project project = projectDAO.findByProjectName(getProjectName()); if (project != null) { BranchDAO branchDAO = new BranchDAOImpl(); Branch branch = branchDAO.findByProjectIdAndBranchName(project.getProjectId(), QVCSConstants.QVCS_TRUNK_VIEW); if (branch != null) { // Add the new directory to the database. DirectoryDAO directoryDAO = new DirectoryDAOImpl(); Directory newDirectory = new Directory(); newDirectory.setDirectoryId(getDirectoryID()); newDirectory.setAppendedPath(getAppendedPath()); newDirectory.setBranchId(branch.getBranchId()); if (getParent() != null) { newDirectory.setParentDirectoryId(getParent().getDirectoryID()); } newDirectory.setRootDirectoryId(getProjectRootArchiveDirManager().getDirectoryID()); try { directoryDAO.insert(newDirectory); createdDirectory = true; } catch (SQLException e) { LOGGER.log(Level.SEVERE, Utility.expandStackTraceToString(e)); } } } } } } else { createdDirectory = true; } return createdDirectory; } /** * Create an archive file. * * @param commandLineArgs the command line arguments that define the archive to create. * @param inputFileName the name of the file that will be inserted into the archive file as the 1st revision. * @param response the object that identifies the client. * @return true if things work; false otherwise. * @throws IOException if there is a QVCS specific problem. * @throws QVCSException if there is an IO exception. */ @Override public boolean createArchive(CreateArchiveCommandArgs commandLineArgs, String inputFileName, ServerResponseFactoryInterface response) throws IOException, QVCSException { boolean retVal = false; // Make sure the archive directory exists. File directory = new File(getArchiveDirectoryName()); if (!directory.exists()) { if (!directory.mkdirs()) { LOGGER.log(Level.WARNING, "Failed to create archive directory: [" + directory.getAbsolutePath() + "]"); retVal = false; } } else { String shortArchiveFilename = Utility.convertWorkfileNameToShortArchiveName(commandLineArgs.getWorkfileName()); String fullArchiveFilename = getArchiveDirectoryName() + File.separator + shortArchiveFilename; LogFile logfile = new LogFile(fullArchiveFilename); // Set the timestamp to be the one from the transaction... Date date = ServerTransactionManager.getInstance().getTransactionTimeStamp(response); commandLineArgs.setCheckInTimestamp(date); logfile.addListener(this); if (logfile.createArchive(commandLineArgs, getProjectProperties(), inputFileName)) { if (logfile.readInformation()) { String keyShortWorkfileName = logfile.getShortWorkfileName(); synchronized (getArchiveInfoCollection()) { getArchiveInfoCollection().put(Utility.getArchiveKey(getProjectProperties(), keyShortWorkfileName), logfile); } retVal = true; // Capture the change to the directory contents... try { DirectoryContentsManagerFactory.getInstance().getDirectoryContentsManager(getProjectName()).addFileToTrunk(getDirectoryID(), logfile.getFileID(), logfile.getShortWorkfileName(), response); } catch (SQLException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); throw new QVCSException("Caught SQLException: " + e.getLocalizedMessage()); } // Capture the association of this file to this directory. FileIDDictionary.getInstance().saveFileIDInfo(getProjectName(), getViewName(), logfile.getFileID(), getAppendedPath(), logfile.getShortWorkfileName(), getDirectoryID()); } notifyCreateListeners(logfile); } } return retVal; } /** * Rename a file within this directory. This needs to be synchronized so only one rename occurs at a time within this directory * * @param userName user name * @param oldShortWorkfileName the old short workfile name * @param newShortWorkfileName the new short workfile name * @param response object used to capture who we're working for. * @return true if things work okay; false otherwise. * @throws IOException if we have an IO exception. * @throws QVCSException if we have a QVCS related exception. */ @Override public boolean renameArchive(String userName, String oldShortWorkfileName, String newShortWorkfileName, ServerResponseFactoryInterface response) throws IOException, QVCSException { LogFile originalLogfile; boolean retVal = false; String containerKeyValue = Utility.getArchiveKey(getProjectProperties(), oldShortWorkfileName); synchronized (getArchiveInfoCollection()) { // Lookup the existing LogFile object. originalLogfile = (LogFile) getArchiveInfoCollection().get(containerKeyValue); Date date = ServerTransactionManager.getInstance().getTransactionTimeStamp(response); if (originalLogfile != null) { if (originalLogfile.getLockCount() == 0) { if (originalLogfile.renameArchive(userName, getAppendedPath(), oldShortWorkfileName, newShortWorkfileName, date)) { // Throw away the old logfile.... we can't use it anymore. getArchiveInfoCollection().remove(containerKeyValue); // Delete the reference copy if we need to... AbstractProjectProperties projectProperties = getProjectProperties(); if (projectProperties.getCreateReferenceCopyFlag()) { deleteReferenceCopy(projectProperties, originalLogfile); } String shortArchiveFilename = Utility.convertWorkfileNameToShortArchiveName(newShortWorkfileName); String fullArchiveFilename = getArchiveDirectoryName() + File.separator + shortArchiveFilename; LogFile newLogfile = new LogFile(fullArchiveFilename); if (newLogfile.readInformation()) { // Capture the change to the directory contents... DirectoryContentsManagerFactory.getInstance().getDirectoryContentsManager(getProjectName()).renameFileOnTrunk(getDirectoryID(), originalLogfile.getFileID(), oldShortWorkfileName, newShortWorkfileName, response); // Add the new name to our container. String shortWorkfileName = newLogfile.getShortWorkfileName(); getArchiveInfoCollection().put(Utility.getArchiveKey(getProjectProperties(), shortWorkfileName), newLogfile); newLogfile.addListener(this); // Capture the association of this file to this directory. FileIDDictionary.getInstance().saveFileIDInfo(getProjectName(), getViewName(), originalLogfile.getFileID(), getAppendedPath(), newLogfile.getShortWorkfileName(), getDirectoryID()); // Create a notification message to let everyone know about the // 'new' file. Rename logfileActionRename = new Rename(oldShortWorkfileName); notifyLogfileListener(newLogfile, logfileActionRename); // Notify any translucent branches about the 'new' file. notifyCreateListeners(newLogfile); retVal = true; // Create a new reference copy if we need to... if (projectProperties.getCreateReferenceCopyFlag()) { byte[] buffer = newLogfile.getRevisionAsByteArray(newLogfile.getDefaultRevisionString()); createReferenceCopy(projectProperties, newLogfile, buffer); } } } } else { LOGGER.log(Level.WARNING, "Rename not allowed for locked file: [" + originalLogfile.getShortWorkfileName() + "]"); } } } return retVal; } /** * Move an archive file from one directory to another. * * @param userName the user requesting the move. * @param shortWorkfileName the short name of the file. * @param targetArchiveDirManagerInterface the destination directory. * @param response identify the client. * @return true if the move worked; false if it did not. * @throws IOException for any IOException * @throws QVCSException for QVCS problems. */ @Override public boolean moveArchive(String userName, String shortWorkfileName, final ArchiveDirManagerInterface targetArchiveDirManagerInterface, ServerResponseFactoryInterface response) throws IOException, QVCSException { LogFile logfile; boolean retVal = false; String containerKeyValue = Utility.getArchiveKey(getProjectProperties(), shortWorkfileName); // We need to synchronize on the class object -- only one move at at time // is allowed on the whole server. We need to do this to avoid a possible // deadlock situation that would occur if user A moved a file from directory // A to directory B at the same time as user B moving a file from // directory B to directory A. synchronized (ArchiveDirManager.class) { // Lookup the existing LogFile object. logfile = (LogFile) getArchiveInfoCollection().get(containerKeyValue); // Lookup the date we'll use.. Date date = ServerTransactionManager.getInstance().getTransactionTimeStamp(response); if ((logfile != null) && logfile.moveArchive(userName, getAppendedPath(), targetArchiveDirManagerInterface, shortWorkfileName, date)) { // Throw away the old logfile.... we can't use it anymore. getArchiveInfoCollection().remove(containerKeyValue); // Delete the reference copy if we need to... AbstractProjectProperties projectProperties = getProjectProperties(); if (projectProperties.getCreateReferenceCopyFlag()) { deleteReferenceCopy(projectProperties, logfile); } // Save the listeners so we can restore them to the new LogFile // object. List<LogfileListenerInterface> logfileListeners = logfile.getLogfileListeners(); // Cast the target to an actual ArchiveDirManager, since that is // what it MUST be here... if (targetArchiveDirManagerInterface instanceof ArchiveDirManager) { ArchiveDirManager targetArchiveDirManager = (ArchiveDirManager) targetArchiveDirManagerInterface; // Add the moved archive to the target archive directory manager. String shortArchiveFilename = Utility.convertWorkfileNameToShortArchiveName(shortWorkfileName); String fullTargetArchiveFilename = targetArchiveDirManager.getArchiveDirectoryName() + File.separator + shortArchiveFilename; LogFile targetLogfile = new LogFile(fullTargetArchiveFilename); targetLogfile.addListener(targetArchiveDirManager); if (targetLogfile.readInformation()) { synchronized (targetArchiveDirManager.getArchiveInfoCollection()) { targetArchiveDirManager.getArchiveInfoCollection().put(containerKeyValue, targetLogfile); } // Capture the change to the directories' contents DirectoryContentsManagerFactory.getInstance().getDirectoryContentsManager(getProjectName()).moveFileOnTrunk(QVCSConstants.QVCS_TRUNK_VIEW, getDirectoryID(), targetArchiveDirManager.getDirectoryID(), logfile.getFileID(), response); // Capture the change in association of this file to this directory. FileIDDictionary.getInstance().saveFileIDInfo(getProjectName(), getViewName(), logfile.getFileID(), targetArchiveDirManager.getAppendedPath(), shortWorkfileName, targetArchiveDirManager.getDirectoryID()); // Add any view listeners back to the new LogFile object. if (logfileListeners != null) { for (LogfileListenerInterface listener : logfileListeners) { if (listener instanceof ArchiveInfoInterface) { targetLogfile.addListener(listener); } } // Discard any listeners on the old logfile so it can // get garbage collected. logfile.clearLogfileListeners(); } // Create the reference copy if we need to... if (projectProperties.getCreateReferenceCopyFlag()) { byte[] buffer = targetLogfile.getRevisionAsByteArray(targetLogfile.getDefaultRevisionString()); targetArchiveDirManager.createReferenceCopy(projectProperties, targetLogfile, buffer); } // Create a notification message to let everyone know about the 'new' file. Create logfileActionCreate = new Create(); targetArchiveDirManager.notifyLogfileListener(targetLogfile, logfileActionCreate); // Notify any translucent branches about the 'new' file. targetArchiveDirManager.notifyCreateListeners(targetLogfile); retVal = true; } } else { throw new QVCSException("Internal error."); } } } return retVal; } @Override public boolean deleteArchive(String userName, String shortWorkfileName, ServerResponseFactoryInterface response) throws IOException, QVCSException { LogFile logfile; boolean retVal = false; String containerKeyValue = Utility.getArchiveKey(getProjectProperties(), shortWorkfileName); // We need to synchronize on the class object -- only one delete at at time // is allowed on the whole server. We need to do this to avoid a possible // deadlock situation that would occur if user A moved a file from directory // A to directory B at the same time as user B moving a file from // directory B to directory A. synchronized (ArchiveDirManager.class) { // Lookup the existing LogFile object. logfile = (LogFile) getArchiveInfoCollection().get(containerKeyValue); // Lookup the date we'll use.. Date date = ServerTransactionManager.getInstance().getTransactionTimeStamp(response); // Get the cemetery archive directory manager ArchiveDirManagerInterface cemeteryArchiveDirManagerInterface = ServerUtility.getCemeteryArchiveDirManager(getProjectName(), response); if (logfile != null) { int fileID = logfile.getFileID(); if (logfile.deleteArchive(userName, getAppendedPath(), cemeteryArchiveDirManagerInterface, shortWorkfileName, date)) { // Throw away the old logfile.... we can't use it anymore. getArchiveInfoCollection().remove(containerKeyValue); // Save the listeners so we can restore them to the new LogFile // object. List<LogfileListenerInterface> logfileListeners = logfile.getLogfileListeners(); // Cast the target to an actual ArchiveDirManager, since that is // what it MUST be here... ArchiveDirManager cemeteryArchiveDirManager = (ArchiveDirManager) cemeteryArchiveDirManagerInterface; // Add the moved archive to the target archive directory manager. String shortArchiveFilename = Utility.createCemeteryShortArchiveName(logfile.getFileID()); String cemeteryWorkfileName = Utility.convertArchiveNameToShortWorkfileName(shortArchiveFilename); String cemeteryKeyValue = Utility.getArchiveKey(getProjectProperties(), cemeteryWorkfileName); String fullTargetArchiveFilename = cemeteryArchiveDirManager.getArchiveDirectoryName() + File.separator + shortArchiveFilename; LogFile targetLogfile = new LogFile(fullTargetArchiveFilename); targetLogfile.addListener(cemeteryArchiveDirManager); if (targetLogfile.readInformation()) { synchronized (cemeteryArchiveDirManager.getArchiveInfoCollection()) { cemeteryArchiveDirManager.getArchiveInfoCollection().put(cemeteryKeyValue, targetLogfile); } // Capture the change to the directories' contents DirectoryContentsManagerFactory.getInstance().getDirectoryContentsManager(getProjectName()).deleteFileFromTrunk(getAppendedPath(), getDirectoryID(), cemeteryArchiveDirManager.getDirectoryID(), fileID, shortWorkfileName, response); // Capture the change in association of this file to this directory. FileIDDictionary.getInstance().saveFileIDInfo(getProjectName(), getViewName(), fileID, cemeteryArchiveDirManager.getAppendedPath(), cemeteryWorkfileName, cemeteryArchiveDirManager.getDirectoryID()); // Add any view listeners back to the new LogFile object. if (logfileListeners != null) { for (LogfileListenerInterface listener : logfileListeners) { if (listener instanceof ArchiveInfoInterface) { targetLogfile.addListener(listener); } } // Discard any listeners on the old logfile so it can // get garbage collected. logfile.clearLogfileListeners(); } // Notify any cemetery listeners of the change. cemeteryArchiveDirManager.notifyLogfileListener(targetLogfile, new Create()); retVal = true; } } } } return retVal; } @Override public boolean unDeleteArchive(String userName, String shortWorkfileName, ServerResponseFactoryInterface response) throws IOException, QVCSException { UnDeleteArchiveOperation unDeleteArchiveOperation = new UnDeleteArchiveOperation(this, userName, shortWorkfileName, response); return unDeleteArchiveOperation.execute(); } @Override public void addLogFileListener(ServerResponseFactoryInterface logfileListener) { synchronized (instanceLogfileListeners) { instanceLogfileListeners.add(logfileListener); } } @Override public void removeLogFileListener(ServerResponseFactoryInterface logfileListener) { synchronized (instanceLogfileListeners) { instanceLogfileListeners.remove(logfileListener); } } /** * Read this directory's directory ID from the directory ID file... */ @SuppressWarnings("LoggerStringConcat") private void initDirectoryID(File directoryIDFile) { DataInputStream dataInputStream = null; try { dataInputStream = new DataInputStream(new FileInputStream(directoryIDFile)); setDirectoryID(dataInputStream.readInt()); } catch (FileNotFoundException e) { LOGGER.log(Level.INFO, "Unable to find directory ID file for: [" + getArchiveDirectoryName() + "]"); instanceDirectoryID = -1; } catch (IOException e) { LOGGER.log(Level.WARNING, "Unable to read directory ID file for: [" + getArchiveDirectoryName() + "]"); instanceDirectoryID = -1; } finally { try { if (dataInputStream != null) { dataInputStream.close(); } } catch (IOException e) { LOGGER.log(Level.WARNING, "IOException when closing data input stream for: [" + getArchiveDirectoryName() + "]: " + e.getLocalizedMessage()); } } } /** * Save this directory's directory ID to the directory ID file... */ @SuppressWarnings("LoggerStringConcat") private void saveDirectoryID() { DataOutputStream dataOutputStream = null; try { File directory = new File(getArchiveDirectoryName()); if (!directory.exists()) { directory.mkdirs(); } File directoryIDFile = new File(getArchiveDirectoryName() + File.separator + QVCSConstants.QVCS_DIRECTORYID_FILENAME); dataOutputStream = new DataOutputStream(new FileOutputStream(directoryIDFile)); dataOutputStream.writeInt(instanceDirectoryID); } catch (IOException e) { LOGGER.log(Level.WARNING, "Unable to write directory ID file for: [" + getArchiveDirectoryName() + "]"); LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } finally { if (dataOutputStream != null) { try { dataOutputStream.close(); } catch (IOException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } } } } @Override public int getDirectoryID() { if (instanceDirectoryID == -1) { setDirectoryID(DirectoryIDManager.getInstance().getNewDirectoryID()); saveDirectoryID(); } return instanceDirectoryID; } /** * Set the directory id. * @param directoryID the directory id. */ @SuppressWarnings("LoggerStringConcat") public void setDirectoryID(int directoryID) { LOGGER.log(Level.INFO, "Setting directory id for [" + getAppendedPath() + "] to: [" + directoryID + "]"); instanceDirectoryID = directoryID; DirectoryIDDictionary.getInstance().put(directoryID, this); } /** * Return a Collection of LogFile objects that are associated with this directory for the given label string. * * @param label the label to search for. * @return A Collection of archive files (LogFile objects) that each have the given label. */ public synchronized Collection<LogFile> getArchiveCollectionByLabel(final String label) { ArrayList<LogFile> arrayList = new ArrayList<>(); Iterator it = getArchiveInfoCollection().values().iterator(); while (it.hasNext()) { LogFile logFile = (LogFile) it.next(); if (logFile.hasLabel(label)) { arrayList.add(logFile); } } return arrayList; } /** * Return a Collection of LogFile objects that are associated with this directory for the given Date. * * @param date date that defines the archive files that should compose the returned Collection. * @return A Collection of archive files (LogFile objects). */ public synchronized Collection<LogFile> getArchiveCollectionByDate(final java.util.Date date) { ArrayList<LogFile> arrayList = new ArrayList<>(); Iterator it = getArchiveInfoCollection().values().iterator(); while (it.hasNext()) { LogFile logFile = (LogFile) it.next(); int revisionCount = logFile.getRevisionCount(); // If the 1st file revision was created before the requested date, then // include it in the returned collection. if (logFile.getRevisionInformation().getRevisionHeader(revisionCount - 1).getCheckInDate().getTime() < date.getTime()) { arrayList.add(logFile); } } return arrayList; } @Override public void notifyLogfileListener(ArchiveInfoInterface subject, ActionType action) { // Build the information we need to send to the listeners. ServerNotificationInterface info = buildLogfileNotification(subject, action); // Let any remote users know about the logfile change. if (info != null) { synchronized (instanceLogfileListeners) { Iterator<ServerResponseFactoryInterface> it = instanceLogfileListeners.iterator(); while (it.hasNext()) { // Get who we'll send the information to. ServerResponseFactoryInterface serverResponseFactory = it.next(); // Set the server name on the notification message. info.setServerName(serverResponseFactory.getServerName()); // And send the info. serverResponseFactory.createServerResponse(info); } } } } private ServerNotificationInterface buildLogfileNotification(ArchiveInfoInterface subject, ActionType action) { ServerNotificationInterface info = null; byte[] digest = subject.getDefaultRevisionDigest(); switch (action.getAction()) { case ActionType.CHECKOUT: ServerNotificationCheckOut serverNotificationCheckOut = new ServerNotificationCheckOut(); if (action instanceof CheckOut) { CheckOut checkOutAction = (CheckOut) action; CheckOutCommandArgs commandArgs = checkOutAction.getCommandArgs(); serverNotificationCheckOut.setProjectName(getProjectName()); serverNotificationCheckOut.setViewName(getViewName()); serverNotificationCheckOut.setAppendedPath(getAppendedPath()); serverNotificationCheckOut.setShortWorkfileName(commandArgs.getShortWorkfileName()); serverNotificationCheckOut.setClientWorkfileName(commandArgs.getOutputFileName()); serverNotificationCheckOut.setSkinnyLogfileInfo(new SkinnyLogfileInfo(subject.getLogfileInfo(), File.separator, subject.getIsObsolete(), digest, subject.getShortWorkfileName(), subject.getIsOverlap())); serverNotificationCheckOut.setRevisionString(commandArgs.getRevisionString()); info = serverNotificationCheckOut; } break; case ActionType.CHECKIN: ServerNotificationCheckIn serverNotificationCheckIn = new ServerNotificationCheckIn(); serverNotificationCheckIn.setProjectName(getProjectName()); serverNotificationCheckIn.setViewName(getViewName()); serverNotificationCheckIn.setAppendedPath(getAppendedPath()); serverNotificationCheckIn.setShortWorkfileName(subject.getShortWorkfileName()); serverNotificationCheckIn.setSkinnyLogfileInfo(new SkinnyLogfileInfo(subject.getLogfileInfo(), File.separator, subject.getIsObsolete(), digest, subject.getShortWorkfileName(), subject.getIsOverlap())); info = serverNotificationCheckIn; break; case ActionType.LOCK: if (action instanceof Lock) { ServerNotificationLock serverNotificationLock = new ServerNotificationLock(); Lock lockAction = (Lock) action; LockRevisionCommandArgs commandArgs = lockAction.getCommandArgs(); serverNotificationLock.setProjectName(getProjectName()); serverNotificationLock.setViewName(getViewName()); serverNotificationLock.setAppendedPath(getAppendedPath()); serverNotificationLock.setShortWorkfileName(commandArgs.getShortWorkfileName()); serverNotificationLock.setClientWorkfileName(commandArgs.getOutputFileName()); serverNotificationLock.setRevisionString(commandArgs.getRevisionString()); serverNotificationLock.setSkinnyLogfileInfo(new SkinnyLogfileInfo(subject.getLogfileInfo(), File.separator, subject.getIsObsolete(), digest, subject.getShortWorkfileName(), subject.getIsOverlap())); info = serverNotificationLock; } break; case ActionType.CREATE: ServerNotificationCreateArchive serverNotificationCreateArchive = new ServerNotificationCreateArchive(); serverNotificationCreateArchive.setProjectName(getProjectName()); serverNotificationCreateArchive.setViewName(getViewName()); serverNotificationCreateArchive.setAppendedPath(getAppendedPath()); serverNotificationCreateArchive.setShortWorkfileName(subject.getShortWorkfileName()); serverNotificationCreateArchive.setSkinnyLogfileInfo(new SkinnyLogfileInfo(subject.getLogfileInfo(), File.separator, subject.getIsObsolete(), digest, subject.getShortWorkfileName(), subject.getIsOverlap())); info = serverNotificationCreateArchive; break; case ActionType.MOVE_FILE: if (action instanceof MoveFile) { MoveFile moveFileAction = (MoveFile) action; ServerNotificationMoveArchive serverNotificationMoveArchive = new ServerNotificationMoveArchive(); serverNotificationMoveArchive.setShortWorkfileName(subject.getShortWorkfileName()); serverNotificationMoveArchive.setOriginAppendedPath(moveFileAction.getOriginAppendedPath()); serverNotificationMoveArchive.setDestinationAppendedPath(moveFileAction.getDestinationAppendedPath()); serverNotificationMoveArchive.setProjectName(getProjectName()); serverNotificationMoveArchive.setViewName(getViewName()); serverNotificationMoveArchive.setProjectProperties(getProjectProperties().getProjectProperties()); serverNotificationMoveArchive.setSkinnyLogfileInfo(new SkinnyLogfileInfo(subject.getLogfileInfo(), File.separator, subject.getIsObsolete(), digest, subject.getShortWorkfileName(), subject.getIsOverlap())); info = serverNotificationMoveArchive; } break; case ActionType.UNLOCK: if (action instanceof Unlock) { Unlock unlockAction = (Unlock) action; ServerNotificationUnlock serverNotificationUnlock = new ServerNotificationUnlock(); UnlockRevisionCommandArgs commandArgs = unlockAction.getCommandArgs(); serverNotificationUnlock.setProjectName(getProjectName()); serverNotificationUnlock.setViewName(getViewName()); serverNotificationUnlock.setAppendedPath(getAppendedPath()); serverNotificationUnlock.setShortWorkfileName(commandArgs.getShortWorkfileName()); serverNotificationUnlock.setClientWorkfileName(commandArgs.getOutputFileName()); serverNotificationUnlock.setRevisionString(commandArgs.getRevisionString()); serverNotificationUnlock.setSkinnyLogfileInfo(new SkinnyLogfileInfo(subject.getLogfileInfo(), File.separator, subject.getIsObsolete(), digest, subject.getShortWorkfileName(), subject.getIsOverlap())); info = serverNotificationUnlock; } break; case ActionType.SET_REVISION_DESCRIPTION: if (action instanceof SetRevisionDescription) { SetRevisionDescription setRevisionDescriptionAction = (SetRevisionDescription) action; ServerNotificationSetRevisionDescription serverNotificationSetRevisionDescription = new ServerNotificationSetRevisionDescription(); SetRevisionDescriptionCommandArgs commandArgs = setRevisionDescriptionAction.getCommandArgs(); serverNotificationSetRevisionDescription.setProjectName(getProjectName()); serverNotificationSetRevisionDescription.setViewName(getViewName()); serverNotificationSetRevisionDescription.setAppendedPath(getAppendedPath()); serverNotificationSetRevisionDescription.setShortWorkfileName(commandArgs.getShortWorkfileName()); serverNotificationSetRevisionDescription.setRevisionDescription(commandArgs.getRevisionDescription()); serverNotificationSetRevisionDescription.setRevisionString(commandArgs.getRevisionString()); serverNotificationSetRevisionDescription.setSkinnyLogfileInfo(new SkinnyLogfileInfo(subject.getLogfileInfo(), File.separator, subject.getIsObsolete(), digest, subject.getShortWorkfileName(), subject.getIsOverlap())); info = serverNotificationSetRevisionDescription; } break; case ActionType.REMOVE: ServerNotificationRemoveArchive serverNotificationRemoveArchive = new ServerNotificationRemoveArchive(); serverNotificationRemoveArchive.setProjectName(getProjectName()); serverNotificationRemoveArchive.setViewName(getViewName()); serverNotificationRemoveArchive.setAppendedPath(getAppendedPath()); serverNotificationRemoveArchive.setShortWorkfileName(subject.getShortWorkfileName()); info = serverNotificationRemoveArchive; break; case ActionType.RENAME: if (action instanceof Rename) { Rename renameAction = (Rename) action; ServerNotificationRenameArchive serverNotificationRenameArchive = new ServerNotificationRenameArchive(); serverNotificationRenameArchive.setProjectName(getProjectName()); serverNotificationRenameArchive.setViewName(getViewName()); serverNotificationRenameArchive.setAppendedPath(getAppendedPath()); serverNotificationRenameArchive.setNewShortWorkfileName(subject.getShortWorkfileName()); serverNotificationRenameArchive.setOldShortWorkfileName(renameAction.getOldShortWorkfileName()); serverNotificationRenameArchive.setSkinnyLogfileInfo(new SkinnyLogfileInfo(subject.getLogfileInfo(), File.separator, subject.getIsObsolete(), digest, subject.getShortWorkfileName(), subject.getIsOverlap())); info = serverNotificationRenameArchive; } break; case ActionType.SET_OBSOLETE: case ActionType.LABEL: case ActionType.UNLABEL: case ActionType.CHANGE_HEADER: case ActionType.CHANGE_REVHEADER: case ActionType.SET_ATTRIBUTES: case ActionType.SET_COMMENT_PREFIX: case ActionType.SET_MODULE_DESCRIPTION: default: ServerNotificationHeaderChange serverNotificationHeaderChange = new ServerNotificationHeaderChange(); serverNotificationHeaderChange.setProjectName(getProjectName()); serverNotificationHeaderChange.setViewName(getViewName()); serverNotificationHeaderChange.setAppendedPath(getAppendedPath()); serverNotificationHeaderChange.setShortWorkfileName(subject.getShortWorkfileName()); serverNotificationHeaderChange.setSkinnyLogfileInfo(new SkinnyLogfileInfo(subject.getLogfileInfo(), File.separator, subject.getIsObsolete(), digest, subject.getShortWorkfileName(), subject.getIsOverlap())); info = serverNotificationHeaderChange; break; } return info; } /** * Update the most recent activity date. * @param activityDate the new activity date. */ public void updateMostRecentActivityDate(Date activityDate) { if (activityDate.after(this.mostRecentActivityDate)) { this.mostRecentActivityDate = activityDate; if (getParent() != null) { getParent().updateMostRecentActivityDate(activityDate); } } } @Override public Date getMostRecentActivityDate() { return new Date(this.mostRecentActivityDate.getTime()); } }