/* 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.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.commandargs.CreateArchiveCommandArgs;
import com.qumasoft.qvcslib.logfileaction.ActionType;
import com.qumasoft.qvcslib.logfileaction.MoveFile;
import com.qumasoft.qvcslib.notifications.ServerNotificationInterface;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
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's cemetery.
*
* @author Jim Voris
*/
public class ArchiveDirManagerForTranslucentBranchCemetery implements ArchiveDirManagerInterface, ArchiveDirManagerReadWriteViewInterface, LogfileListenerInterface {
// Create our logger object.
private static final Logger LOGGER = Logger.getLogger("com.qumasoft.server");
/**
* The project name
*/
private final String projectName;
/**
* The branch name
*/
private final String branchName;
/**
* Keep track of oldest revision for this manager.
*/
private long oldestRevision;
private final RemoteViewProperties remoteViewProperties;
/**
* The collection of archive info objects for this cemetery
*/
private final Map<String, ArchiveInfoInterface> archiveInfoMap = Collections.synchronizedMap(new TreeMap<String, ArchiveInfoInterface>());
// Remote listeners for changes to this directory.
private final ArrayList<ServerResponseFactoryInterface> logfileListeners;
/**
* Constructor that takes the project name, and the branch name.
*
* @param project the name of the project.
* @param branch the name of the branch.
* @param rvProperties the project's properties.
* @param response the response object.
* @throws IOException for IO problems.
* @throws QVCSException for QVCS specific problems.
*/
public ArchiveDirManagerForTranslucentBranchCemetery(String project, String branch, RemoteViewProperties rvProperties,
ServerResponseFactoryInterface response) throws IOException, QVCSException {
this.oldestRevision = Long.MAX_VALUE;
this.logfileListeners = new ArrayList<>();
this.projectName = project;
this.branchName = branch;
this.remoteViewProperties = rvProperties;
// Get the directory contents object for this cemetery.
DirectoryContentsManager directoryContentsManager = DirectoryContentsManagerFactory.getInstance().getDirectoryContentsManager(project);
DirectoryContents cemeteryContents = directoryContentsManager.getDirectoryContentsForTranslucentBranchCemetery(branch, response);
// 2. populate our archive info collection based on the directory contents object.
Map<Integer, String> files = cemeteryContents.getFiles();
Set<Integer> fileIdSet = cemeteryContents.getFiles().keySet();
for (Integer fileId : fileIdSet) {
// Use the file id to lookup the archive that we can use to create the branch's archive info object.
int fileID = fileId;
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, rvProperties);
archiveInfo.addListener(archiveInfoForTranslucentBranch);
String keyToOurFile = filenameForBranch;
if (rvProperties.getIgnoreCaseFlag()) {
keyToOurFile = keyToOurFile.toLowerCase();
}
// And store in our map...
archiveInfoMap.put(keyToOurFile, archiveInfoForTranslucentBranch);
// Save the timestamp of the oldest revision in this archiveInfo.
setOldestRevision(archiveInfoForTranslucentBranch.getRevisionInformation().getRevisionHeader(archiveInfoForTranslucentBranch.getRevisionCount() - 1)
.getCheckInDate().getTime());
// 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.FINE, "Adding file id: [" + fileID + "] filename: [" + filenameForBranch + "]");
}
// Add the logfile listener for the cemetery.
addLogFileListener(response);
}
/**
* This is not used on the server.
* @return null, since this is not used on the server.
*/
@Override
public Date getMostRecentActivityDate() {
return null;
}
@Override
public void setDirectoryManager(DirectoryManagerInterface directoryManager) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public String getAppendedPath() {
return QVCSConstants.QVCS_CEMETERY_DIRECTORY;
}
@Override
public final String getProjectName() {
return projectName;
}
@Override
public final String getViewName() {
return branchName;
}
@Override
public String getUserName() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public AbstractProjectProperties getProjectProperties() {
return remoteViewProperties;
}
@Override
public ArchiveInfoInterface getArchiveInfo(String shortWorkfileName) {
if (getProjectProperties().getIgnoreCaseFlag()) {
shortWorkfileName = shortWorkfileName.toLowerCase();
}
return archiveInfoMap.get(shortWorkfileName);
}
@Override
public boolean createArchive(CreateArchiveCommandArgs commandLineArgs, String fullWorkfilename, ServerResponseFactoryInterface response) throws IOException,
QVCSException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void createReferenceCopy(AbstractProjectProperties projectProperties, ArchiveInfoInterface logfile, byte[] buffer) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void deleteReferenceCopy(AbstractProjectProperties projectProperties, ArchiveInfoInterface logfile) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public boolean moveArchive(String userName, 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 target's 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(userName, 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.moveFileFromTranslucentBranchCemetery(getViewName(), targetArchiveDirManager.getDirectoryID(), fileID, shortWorkfileName, response);
// 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 userName, String oldShortWorkfileName, String newShortWorkfileName, ServerResponseFactoryInterface response) throws IOException,
QVCSException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public boolean deleteArchive(String userName, String shortWorkfileName, ServerResponseFactoryInterface response) throws IOException, QVCSException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public boolean unDeleteArchive(String userName, String shortWorkfileName, ServerResponseFactoryInterface response) throws IOException, QVCSException {
UnDeleteArchiveForTranslucentBranchOperation unDeleteArchiveOperation = new UnDeleteArchiveForTranslucentBranchOperation(this, userName, shortWorkfileName, response);
return unDeleteArchiveOperation.execute();
}
@Override
public boolean createDirectory() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void addChangeListener(ChangeListener listener) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void removeChangeListener(ChangeListener listener) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void startDirectoryManager() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void notifyListeners() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void setFastNotify(boolean flag) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public boolean getFastNotify() {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* Get the collection of archive info objects for this branch cemetery.
*
* @return the collection of archive info objects for this branch cemetery.
*/
@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 -1;
}
@Override
public final void addLogFileListener(ServerResponseFactoryInterface logfileListener) {
synchronized (logfileListeners) {
logfileListeners.add(logfileListener);
}
}
@Override
public void removeLogFileListener(ServerResponseFactoryInterface logfileListener) {
synchronized (logfileListeners) {
logfileListeners.remove(logfileListener);
}
}
@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 (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);
}
}
}
}
/**
* 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.");
}
}
private ServerNotificationInterface buildLogfileNotification(ArchiveInfoInterface subject, ActionType action) {
return ArchiveDirManagerHelper.buildLogfileNotification(this, subject, action);
}
}