/* 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.ArchiveDirManagerInterface;
import com.qumasoft.qvcslib.ArchiveDirManagerReadWriteViewInterface;
import com.qumasoft.qvcslib.ArchiveInfoInterface;
import com.qumasoft.qvcslib.DirectoryCoordinate;
import com.qumasoft.qvcslib.DirectoryManagerInterface;
import com.qumasoft.qvcslib.LogfileListenerInterface;
import com.qumasoft.qvcslib.QVCSConstants;
import com.qumasoft.qvcslib.QVCSException;
import com.qumasoft.qvcslib.RemoteViewProperties;
import com.qumasoft.qvcslib.ServerResponseFactoryInterface;
import com.qumasoft.qvcslib.Utility;
import com.qumasoft.qvcslib.commandargs.CreateArchiveCommandArgs;
import com.qumasoft.qvcslib.logfileaction.ActionType;
import com.qumasoft.qvcslib.logfileaction.Create;
import com.qumasoft.qvcslib.logfileaction.MoveFile;
import com.qumasoft.qvcslib.logfileaction.Remove;
import com.qumasoft.qvcslib.logfileaction.Rename;
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.ServerNotificationRenameArchive;
import com.qumasoft.qvcslib.notifications.ServerNotificationSetRevisionDescription;
import com.qumasoft.qvcslib.notifications.ServerNotificationUnlock;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.event.ChangeListener;
/**
* Archive directory manager for a translucent branch directory.
*
* @author Jim Voris
*/
public class ArchiveDirManagerForTranslucentBranch implements ArchiveDirManagerInterface, ArchiveDirManagerReadWriteViewInterface, LogfileListenerInterface {
// Create our logger object.
private static final Logger LOGGER = Logger.getLogger("com.qumasoft.server");
private final String viewName;
private final String projectName;
private final String appendedPath;
private final String userName;
private final String branchParent;
private int directoryID = -1;
private final ProjectView projectView;
private final String branchLabel;
private final RemoteViewProperties remoteViewProperties;
private final Map<String, ArchiveInfoInterface> archiveInfoMap = Collections.synchronizedMap(new TreeMap<String, ArchiveInfoInterface>());
/**
* Keep track of oldest revision for this manager.
*/
private long oldestRevision = Long.MAX_VALUE;
// Remote listeners for changes to this directory.
private final ArrayList<ServerResponseFactoryInterface> logfileListeners;
/**
* Constructor.
*
* @param bParent the name of this branch's parent branch (typically the Trunk, but other parents are supported, e.g. other
* branches can serve as the parent to this branch).
* @param rvProperties the remote view properties.
* @param view the name of this branch.
* @param path the appended path for this directory.
* @param user the user name.
* @param response identifies the client.
*/
public ArchiveDirManagerForTranslucentBranch(String bParent, RemoteViewProperties rvProperties, String view, String path, String user,
ServerResponseFactoryInterface response) {
this.logfileListeners = new ArrayList<>();
this.branchParent = bParent;
this.viewName = view;
this.appendedPath = path;
this.remoteViewProperties = rvProperties;
this.userName = user;
this.projectName = rvProperties.getProjectName();
this.projectView = ViewManager.getInstance().getView(projectName, viewName);
this.branchLabel = projectView.getTranslucentBranchLabel();
populateCollection(response);
}
/**
* This is not used on the server.
* @return null, since this is not used on the server.
*/
@Override
public Date getMostRecentActivityDate() {
return null;
}
/**
* Set the directory manager. This is only used on the client.
*
* @param directoryManager the directory manager.
*/
@Override
public void setDirectoryManager(DirectoryManagerInterface directoryManager) {
// We don't need to do anything.
}
/**
* Get the appended path.
*
* @return the appended path.
*/
@Override
public String getAppendedPath() {
return appendedPath;
}
/**
* Get the project name.
*
* @return the project name.
*/
@Override
public String getProjectName() {
return projectName;
}
/**
* Get the view name.
*
* @return the view name.
*/
@Override
public String getViewName() {
return viewName;
}
/**
* Get the user name.
*
* @return the user name.
*/
@Override
public String getUserName() {
return userName;
}
/**
* Get the name of this branch's parent branch.
*
* @return the name of this branch's parent branch.
*/
public String getBranchParent() {
return branchParent;
}
/**
* Get the branch label associated with this translucent branch.
*
* @return the branch label associated with this translucent branch.
*/
public String getBranchLabel() {
return branchLabel;
}
/**
* Get the project properties.
*
* @return the project properties.
*/
@Override
public AbstractProjectProperties getProjectProperties() {
return remoteViewProperties;
}
private ServerNotificationInterface buildLogfileNotification(ArchiveInfoInterface subject, ActionType action) {
ServerNotificationInterface serverNotification = ArchiveDirManagerHelper.buildLogfileNotification(this, subject, action);
if (subject instanceof LogFile) {
// We need to turn off lock checking in the attributes.
switch (action.getAction()) {
case ActionType.CHECKOUT:
ServerNotificationCheckOut serverNotificationCheckOut = (ServerNotificationCheckOut) serverNotification;
serverNotificationCheckOut.getSkinnyLogfileInfo().getAttributes().setIsCheckLock(false);
break;
case ActionType.CHECKIN:
ServerNotificationCheckIn serverNotificationCheckIn = (ServerNotificationCheckIn) serverNotification;
serverNotificationCheckIn.getSkinnyLogfileInfo().getAttributes().setIsCheckLock(false);
break;
case ActionType.LOCK:
ServerNotificationLock serverNotificationLock = (ServerNotificationLock) serverNotification;
serverNotificationLock.getSkinnyLogfileInfo().getAttributes().setIsCheckLock(false);
break;
case ActionType.CREATE:
ServerNotificationCreateArchive serverNotificationCreateArchive = (ServerNotificationCreateArchive) serverNotification;
serverNotificationCreateArchive.getSkinnyLogfileInfo().getAttributes().setIsCheckLock(false);
break;
case ActionType.MOVE_FILE:
ServerNotificationMoveArchive serverNotificationMoveArchive = (ServerNotificationMoveArchive) serverNotification;
serverNotificationMoveArchive.getSkinnyLogfileInfo().getAttributes().setIsCheckLock(false);
break;
case ActionType.UNLOCK:
ServerNotificationUnlock serverNotificationUnlock = (ServerNotificationUnlock) serverNotification;
serverNotificationUnlock.getSkinnyLogfileInfo().getAttributes().setIsCheckLock(false);
break;
case ActionType.SET_REVISION_DESCRIPTION:
ServerNotificationSetRevisionDescription serverNotificationSetRevisionDescription = (ServerNotificationSetRevisionDescription) serverNotification;
serverNotificationSetRevisionDescription.getSkinnyLogfileInfo().getAttributes().setIsCheckLock(false);
break;
case ActionType.REMOVE:
break;
case ActionType.RENAME:
ServerNotificationRenameArchive serverNotificationRenameArchive = (ServerNotificationRenameArchive) serverNotification;
serverNotificationRenameArchive.getSkinnyLogfileInfo().getAttributes().setIsCheckLock(false);
break;
case ActionType.CHANGE_ON_BRANCH:
if (serverNotification instanceof ServerNotificationHeaderChange) {
ServerNotificationHeaderChange serverNotificationHeaderChange = (ServerNotificationHeaderChange) serverNotification;
serverNotificationHeaderChange.getSkinnyLogfileInfo().getAttributes().setIsCheckLock(false);
}
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 = (ServerNotificationHeaderChange) serverNotification;
serverNotificationHeaderChange.getSkinnyLogfileInfo().getAttributes().setIsCheckLock(false);
break;
}
}
return serverNotification;
}
private RemoteViewProperties getRemoteViewProperties() {
return remoteViewProperties;
}
/**
* Get the archive info for the given short workfile name.
*
* @param shortWorkfileName the name of the workfile to lookup.
* @return the archive info (on this branch) for the given workfile, or null if the file was not found.
*/
@Override
public ArchiveInfoInterface getArchiveInfo(String shortWorkfileName) {
if (getProjectProperties().getIgnoreCaseFlag()) {
shortWorkfileName = shortWorkfileName.toLowerCase();
}
return archiveInfoMap.get(shortWorkfileName);
}
/**
* Create an archive file for this branched directory. We need to create the archive file in the branch archives directory
* instead of on the trunk, since the file doesn't exist on the trunk.
*
* @param commandLineArgs the command line args to create the file.
* @param inputFileName the input file name.
* @param response where the create request came from.
* @return true if we created the archive successfully.
* @throws java.io.IOException if there are IO problems.
* @throws com.qumasoft.qvcslib.QVCSException if there was a QVCS problem.
*/
@Override
public boolean createArchive(CreateArchiveCommandArgs commandLineArgs, String inputFileName, ServerResponseFactoryInterface response)
throws IOException, QVCSException {
String shortWorkfileName = Utility.convertWorkfileNameToShortWorkfileName(commandLineArgs.getWorkfileName());
verifyCreateIsAllowed(shortWorkfileName);
boolean retVal = false;
ArchiveDirManagerInterface branchArchiveDirManagerInterface = ServerUtility.getBranchArchiveDirManager(getProjectName(), response);
ArchiveDirManager branchArchiveDirManager = (ArchiveDirManager) branchArchiveDirManagerInterface;
if (!branchArchiveDirManager.directoryExists()) {
if (!branchArchiveDirManager.createDirectory()) {
LOGGER.log(Level.WARNING, "Failed to create archive directory for branch archive appended path: [" + branchArchiveDirManager.getAppendedPath() + "]");
return false;
}
}
int fileID;
String branchArchiveDirectoryShortWorkfileName;
boolean createSuccessFlag;
// We have to synchronize on the FileIDManager so that the fileID we get will be correct. Absent this synchronization block, another thread could
// come along after we figure out what the next fileID will be and grab that next fileID, making our notion of the fileID incorrect.
synchronized (FileIDManager.getInstance()) {
fileID = 1 + FileIDManager.getInstance().getCurrentMaximumFileID();
String branchArchiveDirectoryShortArchiveFileName = Utility.createBranchShortArchiveName(1 + FileIDManager.getInstance().getCurrentMaximumFileID());
branchArchiveDirectoryShortWorkfileName = Utility.convertArchiveNameToShortWorkfileName(branchArchiveDirectoryShortArchiveFileName);
commandLineArgs.setWorkfileName(branchArchiveDirectoryShortWorkfileName);
createSuccessFlag = branchArchiveDirManager.createArchive(commandLineArgs, inputFileName, response);
}
if (createSuccessFlag) {
// The archive has been created in the branch archive directory with a filename based on the fileID.
// We need to create an entry in the translucent branch's DirectoryContents object to point to that
// archive file, and we need to populate our own collection with info based on the actual
// archive file, etc.
String keyToFile = branchArchiveDirectoryShortWorkfileName;
boolean ignoreCaseFlag = branchArchiveDirManager.getProjectProperties().getIgnoreCaseFlag();
if (ignoreCaseFlag) {
keyToFile = keyToFile.toLowerCase();
}
// Get the file's current archiveInfo...
LogFile archiveInfo = (LogFile) branchArchiveDirManager.getArchiveInfo(keyToFile);
// Create the translucent branch archiveInfo.
ArchiveInfoForTranslucentBranch archiveInfoForTranslucentBranch = new ArchiveInfoForTranslucentBranch(shortWorkfileName, archiveInfo, getRemoteViewProperties());
archiveInfo.addListener(archiveInfoForTranslucentBranch);
archiveInfoForTranslucentBranch.capturePromotionCandidate();
String keyToOurFile = shortWorkfileName;
if (ignoreCaseFlag) {
keyToOurFile = keyToOurFile.toLowerCase();
}
// And store in our map...
synchronized (archiveInfoMap) {
archiveInfoMap.put(keyToOurFile, archiveInfoForTranslucentBranch);
}
// Capture the change to the directory contents...
try {
DirectoryContentsManagerFactory.getInstance().getDirectoryContentsManager(getProjectName()).addFileToTranslucentBranch(getViewName(), getDirectoryID(), fileID,
shortWorkfileName, 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(), fileID, getAppendedPath(), shortWorkfileName, getDirectoryID());
// Listen for changes to the info object (which itself listens
// for changes to the LogFile from which it is built).
archiveInfoForTranslucentBranch.addListener(this);
// Notify the clients of the move.
Create logfileActionCreate = new Create(commandLineArgs);
notifyLogfileListener(archiveInfoForTranslucentBranch, logfileActionCreate);
retVal = true;
}
return retVal;
}
@Override
public void createReferenceCopy(AbstractProjectProperties projectProperties, ArchiveInfoInterface logfile, byte[] buffer) {
// We don't need to do anything.
}
@Override
public void deleteReferenceCopy(AbstractProjectProperties projectProperties, ArchiveInfoInterface logfile) {
// We don't need to do anything.
}
@Override
public boolean moveArchive(String user, String shortWorkfileName, ArchiveDirManagerInterface targetArchiveDirManager, ServerResponseFactoryInterface response)
throws IOException, QVCSException {
boolean retVal = false;
// Make sure the target directory manager is of the correct type.
if (!(targetArchiveDirManager instanceof ArchiveDirManagerForTranslucentBranch)) {
String errorMessage = "#### INTERNAL ERROR: Attempt to move a file on a translucent branch to wrong type of target directory manager.";
LOGGER.log(Level.WARNING, errorMessage);
throw new QVCSException(errorMessage);
}
// 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 (ArchiveDirManagerForTranslucentBranch.class) {
String containerKeyValue = shortWorkfileName;
if (getProjectProperties().getIgnoreCaseFlag()) {
containerKeyValue = shortWorkfileName.toLowerCase();
}
int fileID = getArchiveInfo(shortWorkfileName).getFileID();
// Verify that the move is allowed.
verifyMoveIsAllowed(shortWorkfileName, targetArchiveDirManager);
// Create the new revision in the archive file that documents the move. This new revision must be
// on the file branch associated with this translucent branch.
ArchiveInfoForTranslucentBranch translucentBrancharchiveInfo = (ArchiveInfoForTranslucentBranch) getArchiveInfo(shortWorkfileName);
Date date = ServerTransactionManager.getInstance().getTransactionTimeStamp(response);
if (translucentBrancharchiveInfo.moveArchive(user, getAppendedPath(), targetArchiveDirManager, shortWorkfileName, date)) {
// Remove the archive info from our collection.
ArchiveInfoInterface archiveInfo = getArchiveInfoCollection().remove(containerKeyValue);
ArchiveInfoForTranslucentBranch archiveInfoForTranslucentBranch = (ArchiveInfoForTranslucentBranch) archiveInfo;
archiveInfoForTranslucentBranch.removeListener(this);
// Add it to the target directory's collection...
targetArchiveDirManager.getArchiveInfoCollection().put(containerKeyValue, archiveInfo);
ArchiveDirManagerForTranslucentBranch targetDirManager = (ArchiveDirManagerForTranslucentBranch) targetArchiveDirManager;
archiveInfoForTranslucentBranch.addListener(targetDirManager);
// Capture the change to the directory contents...
DirectoryContentsManager directoryContentsManager = DirectoryContentsManagerFactory.getInstance().getDirectoryContentsManager(getProjectName());
directoryContentsManager.moveFileOnTranslucentBranch(getViewName(), getDirectoryID(), targetArchiveDirManager.getDirectoryID(), fileID, response);
// Capture the change in association of this file to this directory.
FileIDDictionary.getInstance().saveFileIDInfo(getProjectName(), getViewName(), fileID, targetArchiveDirManager.getAppendedPath(), shortWorkfileName,
targetArchiveDirManager.getDirectoryID());
// Notify the clients of the move.
MoveFile logfileActionMoveFile = new MoveFile(getAppendedPath(), targetDirManager.getAppendedPath());
notifyLogfileListener(archiveInfoForTranslucentBranch, logfileActionMoveFile);
retVal = true;
}
}
return retVal;
}
@Override
public boolean renameArchive(String user, String oldShortWorkfileName, String newShortWorkfileName, ServerResponseFactoryInterface response) throws IOException,
QVCSException {
boolean returnValue = false;
String oldContainerKeyValue = oldShortWorkfileName;
String newContainerKeyValue = newShortWorkfileName;
if (getProjectProperties().getIgnoreCaseFlag()) {
oldContainerKeyValue = oldShortWorkfileName.toLowerCase();
newContainerKeyValue = newShortWorkfileName.toLowerCase();
}
// Step 1. Verify that the rename is allowed.
verifyRenameIsAllowed(oldShortWorkfileName, newShortWorkfileName);
// Only allow one rename per directory at a time.
synchronized (this) {
// Create the new revision in the archive file that documents the rename. This new revision must be
// on the file branch associated with this translucent branch.
ArchiveInfoForTranslucentBranch translucentBrancharchiveInfo = (ArchiveInfoForTranslucentBranch) getArchiveInfo(oldShortWorkfileName);
Date date = ServerTransactionManager.getInstance().getTransactionTimeStamp(response);
if (translucentBrancharchiveInfo.renameArchive(user, getAppendedPath(), oldShortWorkfileName, newShortWorkfileName, date)) {
// Remove the entry for the old name...
getArchiveInfoCollection().remove(oldContainerKeyValue);
// Add an entry for the new name...
translucentBrancharchiveInfo.setShortWorkfileName(newShortWorkfileName);
getArchiveInfoCollection().put(newContainerKeyValue, translucentBrancharchiveInfo);
// Step 3. Update the DirectoryContents object to create a new revision there that has the new name.
int fileID = getArchiveInfo(newShortWorkfileName).getFileID();
DirectoryContentsManagerFactory.getInstance().getDirectoryContentsManager(getProjectName()).renameFileOnTranslucentBranch(getViewName(), fileID,
oldShortWorkfileName,
newShortWorkfileName, response);
// Capture the change in association of this file to this directory.
FileIDDictionary.getInstance().saveFileIDInfo(getProjectName(), getViewName(), fileID, getAppendedPath(), newShortWorkfileName, getDirectoryID());
// Create a notification message to let everyone know about the 'new' file.
Rename logfileActionRename = new Rename(oldShortWorkfileName);
notifyLogfileListener(translucentBrancharchiveInfo, logfileActionRename);
returnValue = true;
}
}
return returnValue;
}
@Override
public boolean deleteArchive(String user, String shortWorkfileName, ServerResponseFactoryInterface response) throws IOException, QVCSException {
boolean retVal = false;
// We need to synchronize on the class object -- only one delete at at time
// is allowed on the whole server.
synchronized (ArchiveDirManagerForTranslucentBranch.class) {
String containerKeyValue = shortWorkfileName;
if (getProjectProperties().getIgnoreCaseFlag()) {
containerKeyValue = shortWorkfileName.toLowerCase();
}
int fileID = getArchiveInfo(shortWorkfileName).getFileID();
// Create the new revision in the archive file that documents the delete. This new revision must be
// on the file branch associated with this translucent branch.
ArchiveInfoForTranslucentBranch translucentBrancharchiveInfo = (ArchiveInfoForTranslucentBranch) getArchiveInfo(shortWorkfileName);
Date date = ServerTransactionManager.getInstance().getTransactionTimeStamp(response);
if (translucentBrancharchiveInfo.deleteArchive(user, getAppendedPath(), shortWorkfileName, date)) {
// Remove the archive info from our collection.
ArchiveInfoInterface archiveInfo = getArchiveInfoCollection().remove(containerKeyValue);
ArchiveInfoForTranslucentBranch archiveInfoForTranslucentBranch = (ArchiveInfoForTranslucentBranch) archiveInfo;
archiveInfoForTranslucentBranch.removeListener(this);
// Add it to the cemetery directory's collection...
DirectoryCoordinate directoryCoordinate = new DirectoryCoordinate(getProjectName(), getViewName(), QVCSConstants.QVCS_CEMETERY_DIRECTORY);
ArchiveDirManagerInterface cemeteryDirManager = ArchiveDirManagerFactoryForServer.getInstance().getDirectoryManager(QVCSConstants.QVCS_SERVER_SERVER_NAME,
directoryCoordinate, QVCSConstants.QVCS_SERVED_PROJECT_TYPE, QVCSConstants.QVCS_SERVER_USER, response, true);
String shortArchiveFilename = Utility.createCemeteryShortArchiveName(archiveInfo.getFileID());
String cemeteryWorkfileName = Utility.convertArchiveNameToShortWorkfileName(shortArchiveFilename);
String cemeteryKeyValue = cemeteryWorkfileName;
if (getProjectProperties().getIgnoreCaseFlag()) {
cemeteryKeyValue = cemeteryKeyValue.toLowerCase();
}
// Notify the clients of the delete.
Remove logfileActionRemove = new Remove();
notifyLogfileListener(archiveInfoForTranslucentBranch, logfileActionRemove);
archiveInfoForTranslucentBranch.setShortWorkfileName(cemeteryWorkfileName);
cemeteryDirManager.getArchiveInfoCollection().put(cemeteryKeyValue, archiveInfoForTranslucentBranch);
ArchiveDirManagerForTranslucentBranchCemetery targetCemeteryDirManager = (ArchiveDirManagerForTranslucentBranchCemetery) cemeteryDirManager;
archiveInfoForTranslucentBranch.addListener(targetCemeteryDirManager);
// Capture the change to the directory contents...
DirectoryContentsManager directoryContentsManager = DirectoryContentsManagerFactory.getInstance().getDirectoryContentsManager(getProjectName());
directoryContentsManager.deleteFileFromTranslucentBranch(getViewName(), getDirectoryID(), -1, fileID, shortWorkfileName, response);
// Capture the change in association of this file to this directory.
FileIDDictionary.getInstance().saveFileIDInfo(getProjectName(), getViewName(), fileID, cemeteryDirManager.getAppendedPath(), cemeteryWorkfileName,
cemeteryDirManager.getDirectoryID());
// Notify any cemetery listeners of the change.
targetCemeteryDirManager.notifyLogfileListener(archiveInfoForTranslucentBranch, new Create());
retVal = true;
}
}
return retVal;
}
@Override
public boolean unDeleteArchive(String user, String shortWorkfileName, ServerResponseFactoryInterface response) throws IOException, QVCSException {
// We cannot undelete a file from a non-cemetery directory.
return false;
}
/**
* There is nothing to do here, since the archive files are really stored elsewhere.
*
* @return true indicating that creation of the archive directory was successful.
*/
@Override
public boolean createDirectory() {
// TODO -- do we need to update the DirectoryContents for this directory to add the sub-directory?
return true;
}
/**
* Add change listener. Not supported on the server.
*
* @param listener the listener to add.
*/
@Override
public void addChangeListener(ChangeListener listener) {
// Do not need to support this. It is used solely on the client side.
throw new UnsupportedOperationException("Not supported on the server!");
}
@Override
public void removeChangeListener(ChangeListener listener) {
// Do not need to support this. It is used solely on the client side.
throw new UnsupportedOperationException("Not supported on the server!");
}
@Override
public void startDirectoryManager() {
// We don't need to do anything.
}
@Override
public void notifyListeners() {
// Do not need to support this. It is used solely on the client side.
throw new UnsupportedOperationException("Not supported on the server!");
}
@Override
public void setFastNotify(boolean flag) {
// Do not need to support this. It is used solely on the client side.
}
@Override
public boolean getFastNotify() {
return false;
}
@Override
public Map<String, ArchiveInfoInterface> getArchiveInfoCollection() {
return archiveInfoMap;
}
@Override
public long getOldestRevision() {
return oldestRevision;
}
private void setOldestRevision(long revisionCheckInTime) {
if (revisionCheckInTime < oldestRevision) {
oldestRevision = revisionCheckInTime;
}
}
@Override
public int getDirectoryID() {
return directoryID;
}
void setDirectoryID(int dirID) {
this.directoryID = dirID;
}
@Override
public void addLogFileListener(ServerResponseFactoryInterface logfileListener) {
synchronized (logfileListeners) {
logfileListeners.add(logfileListener);
}
}
@Override
public void removeLogFileListener(ServerResponseFactoryInterface logfileListener) {
synchronized (logfileListeners) {
logfileListeners.remove(logfileListener);
}
}
/**
* We receive notifications here from 2 sources. Most of the notifications here arrive from instances of
* ArchiveInfoForTranslucentBranch objects. The other source of notifications is when a new archive is created on the trunk, and
* the trunk ArchiveDirManager sends a create notification here, passing the actual LogFile object as the subject. Note that we
* are <i>not</i> listeners of LogFile objects here... the create notification is a special case.
*
* @param subject the object that has changed.
* @param action the kind of change made to the object.
*/
@Override
public void notifyLogfileListener(ArchiveInfoInterface subject, ActionType action) {
boolean continueFlag = true;
// Handle create notifications...
if (action.getAction() == ActionType.CREATE) {
// We may have to just absorb the create notification...
continueFlag = handleCreateNotification(subject);
}
if (continueFlag) {
if (action.getAction() == ActionType.REMOVE) {
handleRemoveNotification(subject);
}
// 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 (logfileListeners) {
Iterator<ServerResponseFactoryInterface> it = logfileListeners.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 boolean handleCreateNotification(ArchiveInfoInterface subject) {
boolean continueFlag = true;
if (subject instanceof LogFile) {
LogFile logFile = (LogFile) subject;
// Only pay attention to create notifications if the logfile is not branched for this translucent branch.
// This can happen in the case of a rename, or a file move, where the file is renamed or moved on the
// trunk.... If that same file has already been branched on the translucent branch, then we need to
// ignore the create, since that same file is already represented elsewhere in the translucent branch's
// directory tree.
if (!logFile.hasLabel(getBranchLabel())) {
String filenameForBranch = logFile.getShortWorkfileName();
// Create the translucent branch archiveInfo.
ArchiveInfoForTranslucentBranch archiveInfoForTranslucentBranch = new ArchiveInfoForTranslucentBranch(filenameForBranch, logFile, getRemoteViewProperties());
logFile.addListener(archiveInfoForTranslucentBranch);
String keyToOurFile = filenameForBranch;
boolean ignoreOurCaseFlag = getProjectProperties().getIgnoreCaseFlag();
if (ignoreOurCaseFlag) {
keyToOurFile = keyToOurFile.toLowerCase();
}
// And store in our map...
archiveInfoMap.put(keyToOurFile, archiveInfoForTranslucentBranch);
// And listen for changes to the info object (which itself listens
// for changes to the LogFile from which it is built).
archiveInfoForTranslucentBranch.addListener(this);
LOGGER.log(Level.INFO, "Adding file id: [" + logFile.getFileID() + "] filename: [" + filenameForBranch + "]");
} else {
continueFlag = false;
}
}
return continueFlag;
}
private void handleRemoveNotification(ArchiveInfoInterface subject) {
String filenameForBranch = subject.getShortWorkfileName();
String keyToOurFile = filenameForBranch;
boolean ignoreOurCaseFlag = getProjectProperties().getIgnoreCaseFlag();
if (ignoreOurCaseFlag) {
keyToOurFile = keyToOurFile.toLowerCase();
}
// And remove from our map...
ArchiveInfoInterface archiveInfo = archiveInfoMap.remove(keyToOurFile);
if (archiveInfo != null) {
// And we don't need to listen to this anymore.
ArchiveInfoForTranslucentBranch listener = (ArchiveInfoForTranslucentBranch) archiveInfo;
listener.removeListener(this);
}
LOGGER.log(Level.INFO, "Removing from translucent branch [" + getViewName() + "] file id: [" + subject.getFileID() + "] filename: [" + filenameForBranch + "]");
}
private void populateCollection(ServerResponseFactoryInterface response) {
try {
String[] segments = Utility.convertToStandardPath(getAppendedPath()).split("/");
// Work our way up from the root of the project until we get to this
// directory...
DirectoryCoordinate directoryCoordinate = new DirectoryCoordinate(getProjectName(), QVCSConstants.QVCS_TRUNK_VIEW, "");
ArchiveDirManagerInterface projectRootArchiveDirManager = ArchiveDirManagerFactoryForServer.getInstance().getDirectoryManager(QVCSConstants.QVCS_SERVER_SERVER_NAME,
directoryCoordinate, QVCSConstants.QVCS_SERVED_PROJECT_TYPE, getUserName(), response, true);
int projectRootDirectoryID = projectRootArchiveDirManager.getDirectoryID();
ProjectView projView = ViewManager.getInstance().getView(getProjectName(), getViewName());
DirectoryContentsManager directoryContentsManager = DirectoryContentsManagerFactory.getInstance().getDirectoryContentsManager(getProjectName());
DirectoryContents projectRootDirectoryContents;
// Get the root directory contents for this branch....
projectRootDirectoryContents = directoryContentsManager.getDirectoryContentsForTranslucentBranch(projView, "", projectRootDirectoryID, response);
// 'Navigate' to the current 'directory' so we can get its contents.
int segmentIndex;
if (getAppendedPath().length() > 0) {
segmentIndex = 0;
} else {
segmentIndex = 1;
}
DirectoryContents directoryContents = projectRootDirectoryContents;
Integer dirID = projectRootDirectoryID;
boolean foundFlag = false;
while (segmentIndex < segments.length) {
// Look through the child directories for the one that matches
// this directory segment...
Map<Integer, String> childDirectories = directoryContents.getChildDirectories();
Iterator<Map.Entry<Integer, String>> entrySetIt = childDirectories.entrySet().iterator();
while (entrySetIt.hasNext()) {
Map.Entry<Integer, String> directoryEntry = entrySetIt.next();
dirID = directoryEntry.getKey();
String directoryName = directoryEntry.getValue();
if (0 == directoryName.compareTo(segments[segmentIndex])) {
DirectoryContents childDirectoryContents = directoryContentsManager.getDirectoryContentsForTranslucentBranch(projView, getAppendedPath(),
dirID.intValue(), response);
if (childDirectoryContents != null) {
childDirectoryContents.setParentDirectoryID(directoryContents.getDirectoryID());
directoryContents = childDirectoryContents;
LOGGER.log(Level.INFO, "Found directory contents for: [" + getAppendedPath() + "]");
foundFlag = true;
}
break;
}
}
segmentIndex++;
}
boolean ignoreOurCaseFlag = getProjectProperties().getIgnoreCaseFlag();
// Ok. We have 'navigated' to the requested directory. Now we need
// to build the collection of archiveInfoInterface objects to represent
// the files present in the directory for the given branch.
if (((directoryContents != null) && (getAppendedPath().length() == 0)) || ((getAppendedPath().length() > 0) && (directoryContents != null) && foundFlag)) {
// This is the directory ID of the Trunk that is most closely associated with this branch directory.
// TODO -- what about the case where this branch's directory does not exist on the trunk? Will that
// code path even go through here? Won't the directoryContents object be null in that case?
setDirectoryID(dirID.intValue());
// Lookup the archiveDirManager for the file's current location so we can add ourselves
// as a create listener (so we'll get notifications when an archive is created in the
// trunk's directory).
ArchiveDirManager trunkDirManager = DirectoryIDDictionary.getInstance().lookupArchiveDirManager(getProjectName(), dirID.intValue(), response, true);
trunkDirManager.addCreateListener(this);
Map<Integer, String> files = directoryContents.getFiles();
// Now, iterate over the directory contents files, and create the
// objects that populate this object's container.
Iterator<Integer> it = files.keySet().iterator();
while (it.hasNext()) {
int fileID = it.next().intValue();
FileIDInfo fileIDInfo = FileIDDictionary.getInstance().lookupFileIDInfo(getProjectName(), QVCSConstants.QVCS_TRUNK_VIEW, fileID);
int directoryIDForFile = fileIDInfo.getDirectoryID();
String filenameForBranch = files.get(Integer.valueOf(fileID));
// Lookup the archiveDirManager for the file's current location...
ArchiveDirManager archiveDirManager = DirectoryIDDictionary.getInstance().lookupArchiveDirManager(getProjectName(), directoryIDForFile, response, true);
String keyToFile = fileIDInfo.getShortFilename();
boolean ignoreCaseFlag = archiveDirManager.getProjectProperties().getIgnoreCaseFlag();
if (ignoreCaseFlag) {
keyToFile = keyToFile.toLowerCase();
}
// Get the file's current archiveInfo...
LogFile archiveInfo = (LogFile) archiveDirManager.getArchiveInfo(keyToFile);
// Create the translucent branch archiveInfo.
ArchiveInfoForTranslucentBranch archiveInfoForTranslucentBranch = new ArchiveInfoForTranslucentBranch(filenameForBranch, archiveInfo,
getRemoteViewProperties());
archiveInfo.addListener(archiveInfoForTranslucentBranch);
String keyToOurFile = filenameForBranch;
if (ignoreOurCaseFlag) {
keyToOurFile = keyToOurFile.toLowerCase();
}
// Save the timestamp of the oldest revision in this logfile.
setOldestRevision(archiveInfoForTranslucentBranch.getRevisionInformation().getRevisionHeader(archiveInfoForTranslucentBranch.getRevisionCount() - 1)
.getCheckInDate().getTime());
// And store in our map...
archiveInfoMap.put(keyToOurFile, archiveInfoForTranslucentBranch);
// And listen for changes to the info object (which itself listens
// for changes to the LogFile from which it is built).
archiveInfoForTranslucentBranch.addListener(this);
LOGGER.log(Level.FINEST, "Adding file id: [" + fileID + "] filename: [" + filenameForBranch + "]");
}
} else {
LOGGER.log(Level.INFO, "Found empty terminal directory: [" + getAppendedPath() + "]");
}
} catch (QVCSException e) {
LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e));
}
}
/**
* Verify that we can create the archive for the given workfile.
*
* @param shortWorkfileName the short workfile name.
* @throws com.qumasoft.qvcslib.QVCSException if the create is not allowed.
*/
private void verifyCreateIsAllowed(String shortWorkfileName) throws QVCSException {
// Make sure the file does not already exist in the destination directory.
if (null != getArchiveInfo(shortWorkfileName)) {
throw new QVCSException("Cannot create archive for file. File [" + shortWorkfileName + "] already exists.");
}
}
/**
* Verify that a file move is allowed.
*
* @param userName the user name.
* @param shortWorkfileName the short workfile name.
* @param targetArchiveDirManager the destination directory.
* @param response response object identifying the client.
* @throws com.qumasoft.qvcslib.QVCSException if the move is not allowed.
*/
private void verifyMoveIsAllowed(String shortWorkfileName, ArchiveDirManagerInterface targetArchiveDirManager) throws QVCSException {
// Make sure the file does not already exist in the destination directory.
if (null != targetArchiveDirManager.getArchiveInfo(shortWorkfileName)) {
throw new QVCSException("Cannot move file to " + targetArchiveDirManager.getAppendedPath() + " directory. File " + shortWorkfileName + " already exists.");
}
// Make sure there is an archive file.
if (null == getArchiveInfo(shortWorkfileName)) {
throw new QVCSException("Archive not found for '" + shortWorkfileName + "'. Archive file cannot be moved since archive does not exist.");
}
// Make sure there are no locks.
if (getArchiveInfo(shortWorkfileName).getLockCount() > 0) {
throw new QVCSException("Cannot move an archive that is locked");
}
}
/**
* Verify that the rename is allowed.
*
* @param shortWorkfileName the current short workfile name.
* @param newShortWorkfileName prospective new file name.
* @throws com.qumasoft.qvcslib.QVCSException if the rename is not allowed.
*/
private void verifyRenameIsAllowed(String shortWorkfileName, String newShortWorkfileName) throws QVCSException {
// Make sure the file does not already exist in this directory.
if (null != getArchiveInfo(newShortWorkfileName)) {
throw new QVCSException("Cannot rename file. File " + newShortWorkfileName + " already exists.");
}
// Make sure there is an archive file.
if (null == getArchiveInfo(shortWorkfileName)) {
throw new QVCSException("Archive not found for '" + shortWorkfileName + "'. Archive file cannot be renamed since archive does not exist.");
}
// Make sure there are no locks.
if (getArchiveInfo(shortWorkfileName).getLockCount() > 0) {
throw new QVCSException("Cannot rename an archive that is locked");
}
}
}