/* 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.ArchiveDirManagerInterface;
import com.qumasoft.qvcslib.ArchiveInfoInterface;
import com.qumasoft.qvcslib.DirectoryCoordinate;
import com.qumasoft.qvcslib.FileMerge;
import com.qumasoft.qvcslib.LabelInfo;
import com.qumasoft.qvcslib.MutableByteArray;
import com.qumasoft.qvcslib.QVCSConstants;
import com.qumasoft.qvcslib.QVCSException;
import com.qumasoft.qvcslib.QVCSOperationException;
import com.qumasoft.qvcslib.RevisionHeader;
import com.qumasoft.qvcslib.RevisionInformation;
import com.qumasoft.qvcslib.ServedProjectProperties;
import com.qumasoft.qvcslib.ServerResponseFactoryInterface;
import com.qumasoft.qvcslib.response.ServerResponseGetRevision;
import com.qumasoft.qvcslib.Utility;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Server utility. Holds some server side utility methods.
* @author Jim Voris
*/
public final class ServerUtility {
// Create our logger object
private static final Logger LOGGER = Logger.getLogger("com.qumasoft.server");
/** Map (keyed by project name) of the cemetery archive directory managers */
private static final Map<String, ArchiveDirManagerInterface> CEMETERY_ARCHIVE_DIR_MANAGER_MAP = Collections.synchronizedMap(new HashMap<String, ArchiveDirManagerInterface>());
/** Map (keyed by project name) of the branch archive directory managers */
private static final Map<String, ArchiveDirManagerInterface> BRANCH_ARCHIVE_DIR_MANAGER_MAP = Collections.synchronizedMap(new HashMap<String, ArchiveDirManagerInterface>());
/** Hide the constructor. */
private ServerUtility() {
}
static String deduceAppendedPath(File directory, ServedProjectProperties servedProjectProperties) {
String appendedPath = null;
String projectBaseDirectory = servedProjectProperties.getArchiveLocation();
String directoryPath;
String standardDirectoryPath;
try {
directoryPath = directory.getCanonicalPath();
standardDirectoryPath = Utility.convertToStandardPath(directoryPath);
if (projectBaseDirectory.length() == standardDirectoryPath.length()) {
appendedPath = "";
} else {
appendedPath = standardDirectoryPath.substring(1 + projectBaseDirectory.length());
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e));
}
return appendedPath;
}
/**
* Figure out what timestamp should get applied to the workfile and put that into the server response.
* @param logfile the logfile.
* @param serverResponse the server response.
* @param timestampBehavior identify the type of timestamp behavior that has been requested.
*/
public static void setTimestampData(ArchiveInfoInterface logfile, ServerResponseGetRevision serverResponse, Utility.TimestampBehavior timestampBehavior) {
assert (serverResponse.getRevisionString() != null);
assert (serverResponse.getRevisionString().length() > 0);
if (timestampBehavior == Utility.TimestampBehavior.SET_TIMESTAMP_TO_NOW) {
// Nothing to do here.
return;
}
// Get the revision header for the revision that is getting fetched.
int revisionIndex = logfile.getLogfileInfo().getRevisionInformation().getRevisionIndex(serverResponse.getRevisionString());
RevisionHeader revisionHeader = logfile.getLogfileInfo().getRevisionInformation().getRevisionHeader(revisionIndex);
long timestampTime;
switch (timestampBehavior) {
case SET_TIMESTAMP_TO_EDIT_TIME:
timestampTime = revisionHeader.getEditDate().getTime();
break;
default:
case SET_TIMESTAMP_TO_CHECKIN_TIME:
timestampTime = revisionHeader.getCheckInDate().getTime();
break;
}
serverResponse.setTimestamp(timestampTime);
}
/**
* Get the cemetery archive directory manager for a given project.
*
* @param projectName the name of the project.
* @param response the response object.
* @return the archive dir manager for the project's cemetery.
* @throws QVCSException if we have a problem.
*/
public static ArchiveDirManagerInterface getCemeteryArchiveDirManager(String projectName, ServerResponseFactoryInterface response) throws QVCSException {
ArchiveDirManagerInterface cemeteryArchiveDirManager;
synchronized (CEMETERY_ARCHIVE_DIR_MANAGER_MAP) {
cemeteryArchiveDirManager = CEMETERY_ARCHIVE_DIR_MANAGER_MAP.get(projectName);
if (cemeteryArchiveDirManager == null) {
DirectoryCoordinate directoryCoordinate = new DirectoryCoordinate(projectName, QVCSConstants.QVCS_TRUNK_VIEW, QVCSConstants.QVCS_CEMETERY_DIRECTORY);
cemeteryArchiveDirManager = ArchiveDirManagerFactoryForServer.getInstance().getDirectoryManager(QVCSConstants.QVCS_SERVER_SERVER_NAME, directoryCoordinate,
QVCSConstants.QVCS_SERVED_PROJECT_TYPE, QVCSConstants.QVCS_SERVER_USER, response, true);
CEMETERY_ARCHIVE_DIR_MANAGER_MAP.put(projectName, cemeteryArchiveDirManager);
}
}
return cemeteryArchiveDirManager;
}
/**
* Get the branch archive directory manager. This is the directory manager for archive files that exist only on the branch -- i.e. someone has created a new file on a
* branch, so the archive file does not exist on the trunk. We have to put that archive some where, so it gets created in this directory -- the branch archive directory.
* @param projectName the project name.
* @param response a link to the client.
* @return the branch archive directory manager.
* @throws QVCSException for QVCS problems.
*/
public static ArchiveDirManagerInterface getBranchArchiveDirManager(String projectName, ServerResponseFactoryInterface response) throws QVCSException {
ArchiveDirManagerInterface branchArchiveDirManager;
synchronized (BRANCH_ARCHIVE_DIR_MANAGER_MAP) {
branchArchiveDirManager = BRANCH_ARCHIVE_DIR_MANAGER_MAP.get(projectName);
if (branchArchiveDirManager == null) {
DirectoryCoordinate directoryCoordinate = new DirectoryCoordinate(projectName, QVCSConstants.QVCS_TRUNK_VIEW, QVCSConstants.QVCS_BRANCH_ARCHIVES_DIRECTORY);
branchArchiveDirManager = ArchiveDirManagerFactoryForServer.getInstance().getDirectoryManager(QVCSConstants.QVCS_SERVER_SERVER_NAME, directoryCoordinate,
QVCSConstants.QVCS_SERVED_PROJECT_TYPE, QVCSConstants.QVCS_SERVER_USER, response, true);
BRANCH_ARCHIVE_DIR_MANAGER_MAP.put(projectName, branchArchiveDirManager);
}
}
return branchArchiveDirManager;
}
/**
* Given an appended path, return the parent appended path.
*
* @param appendedPath the appended path.
* @return the parent appended path; null if there is no parent.
*/
public static String getParentAppendedPath(String appendedPath) {
String parentAppendedPath = null;
if (appendedPath.length() > 0) {
int lastForwardSlashIndex = appendedPath.lastIndexOf("/");
int lastBackSlashIndex = appendedPath.lastIndexOf("\\");
int maxIndex;
if (lastForwardSlashIndex > lastBackSlashIndex) {
maxIndex = lastForwardSlashIndex;
} else {
maxIndex = lastBackSlashIndex;
}
if (maxIndex > 0) {
parentAppendedPath = appendedPath.substring(0, maxIndex);
} else {
parentAppendedPath = "";
}
}
return parentAppendedPath;
}
static String getSortableRevisionStringForChosenLabel(final String labelString, final LogFile logFile) {
String revisionStringForLabel = null;
String sortableRevisionStringForLabel = null;
// This is a 'get by label' request. Find the label, if we can.
if ((labelString != null) && (logFile != null)) {
LabelInfo[] labelInfo = logFile.getLogFileHeaderInfo().getLabelInfo();
if (labelInfo != null) {
for (LabelInfo labelInfo1 : labelInfo) {
if (labelString.equals(labelInfo1.getLabelString())) {
// If it is a floating label, we have to figure out the
// revision string...
if (labelInfo1.isFloatingLabel()) {
RevisionInformation revisionInformation = logFile.getRevisionInformation();
int revisionCount = logFile.getRevisionCount();
for (int j = 0; j < revisionCount; j++) {
RevisionHeader revHeader = revisionInformation.getRevisionHeader(j);
if (revHeader.getDepth() == labelInfo1.getDepth()) {
if (revHeader.isTip()) {
String labelRevisionString = labelInfo1.getLabelRevisionString();
String revisionString = revHeader.getRevisionString();
if (revisionString.startsWith(labelRevisionString)) {
revisionStringForLabel = revisionString;
break;
}
}
}
}
} else {
revisionStringForLabel = labelInfo1.getLabelRevisionString();
}
break;
}
}
}
if (revisionStringForLabel != null) {
int revisionIndex = logFile.getRevisionInformation().getRevisionIndex(revisionStringForLabel);
RevisionHeader revisionHeader = logFile.getRevisionInformation().getRevisionHeader(revisionIndex);
sortableRevisionStringForLabel = revisionHeader.getRevisionDescriptor().toSortableString();
}
}
return sortableRevisionStringForLabel;
}
/**
* Copy one file to another.
*
* @param fromFile the file to copy from.
* @param toFile the file to copy to.
* @throws IOException if there is an IO problem.
*/
public static void copyFile(java.io.File fromFile, java.io.File toFile) throws IOException {
FileChannel toChannel;
try (FileChannel fromChannel = new FileInputStream(fromFile).getChannel()) {
toChannel = new FileOutputStream(toFile).getChannel();
toChannel.transferFrom(fromChannel, 0L, fromChannel.size());
}
toChannel.close();
}
/**
* Create a merged result buffer.
* @param archiveInfoForTranslucentBranch the archive info for the translucent branch file.
* @param commonAncestorBuffer the common ancestor buffer.
* @param branchTipRevisionBuffer the branch tip revision buffer.
* @param branchParentTipRevisionBuffer the branch parent tip revision buffer.
* @return the merged buffer.
* @throws QVCSException for a QVCS problem.
* @throws IOException for an IO problem.
*/
public static byte[] createMergedResultBuffer(ArchiveInfoForTranslucentBranch archiveInfoForTranslucentBranch,
MutableByteArray commonAncestorBuffer, MutableByteArray branchTipRevisionBuffer, MutableByteArray branchParentTipRevisionBuffer) throws QVCSException, IOException {
byte[] mergedResultBuffer = null;
String branchTipRevisionString = archiveInfoForTranslucentBranch.getBranchTipRevisionString();
String[] branchTipRevisionElements = branchTipRevisionString.split("\\.");
StringBuilder commonAncestorStringBuffer = new StringBuilder();
for (int i = 0; i < branchTipRevisionElements.length - 2; i++) {
if (i > 0) {
commonAncestorStringBuffer.append(".");
}
commonAncestorStringBuffer.append(branchTipRevisionElements[i]);
}
String commonAncestorRevisionString = commonAncestorStringBuffer.toString();
commonAncestorBuffer.setValue(archiveInfoForTranslucentBranch.getCurrentLogFile().getRevisionAsByteArray(commonAncestorRevisionString));
branchTipRevisionBuffer.setValue(archiveInfoForTranslucentBranch.getCurrentLogFile().getRevisionAsByteArray(archiveInfoForTranslucentBranch.getBranchTipRevisionString()));
String branchParentTipRevision = findBranchParentTipRevision(archiveInfoForTranslucentBranch, commonAncestorRevisionString);
branchParentTipRevisionBuffer.setValue(archiveInfoForTranslucentBranch.getCurrentLogFile().getRevisionAsByteArray(branchParentTipRevision));
File commonAncestorTempFile = createTempFileFromBuffer("commonAncestor", commonAncestorBuffer.getValue());
File branchTipTempFile = createTempFileFromBuffer("branchTip", branchTipRevisionBuffer.getValue());
File branchParentTipFile = createTempFileFromBuffer("branchParentTip", branchParentTipRevisionBuffer.getValue());
FileInputStream fileInputStream = null;
try {
// <editor-fold>
String[] args = new String[4];
args[0] = commonAncestorTempFile.getCanonicalPath();
args[1] = branchParentTipFile.getCanonicalPath();
args[2] = branchTipTempFile.getCanonicalPath();
args[3] = File.createTempFile("mergeResult", null).getCanonicalPath();
// </editor-fold>
FileMerge fileMerge = new FileMerge(args);
if (fileMerge.execute()) {
// <editor-fold>
File mergedResult = new File(args[3]);
// </editor-fold>
fileInputStream = new FileInputStream(mergedResult);
mergedResultBuffer = new byte[(int) mergedResult.length()];
fileInputStream.read(mergedResultBuffer);
mergedResult.delete();
}
} catch (QVCSOperationException e) {
// This gets thrown if the merge cannot be done 'automatically'. We just eat it here, since we don't want to allow the exception
// to be thrown to our caller... we just want the mergedResult to be null.
LOGGER.log(Level.INFO, "Failed to create merged buffer. Merge must be done manually. QVCSOperation exception: " + e.getLocalizedMessage());
} finally {
commonAncestorTempFile.delete();
branchParentTipFile.delete();
branchTipTempFile.delete();
if (fileInputStream != null) {
fileInputStream.close();
}
}
return mergedResultBuffer;
}
/**
* Find the branch parent's tip revision. If the branch's parent branch is the trunk, walk toward a lower index to find the
* parent branch's tip revision. If the branch's parent branch is another branch, it's stored as a forward delta, so the tip
* revision will be at a higher index.
*
* @param archiveInfoForTranslucentBranch archive info for the translucent branch.
* @param commonAncestorRevisionString the common ancestor revision string.
* @return the revision string of the parent branch's tip revision.
* @throws QVCSException if we can't find the parent branch's tip revision.
*/
private static String findBranchParentTipRevision(ArchiveInfoForTranslucentBranch archiveInfoForTranslucentBranch, String commonAncestorRevisionString) throws QVCSException {
String branchParentTipRevisionString = null;
LogFile logfile = archiveInfoForTranslucentBranch.getCurrentLogFile();
int commonAncestorRevisionIndex = logfile.getRevisionInformation().getRevisionIndex(commonAncestorRevisionString);
RevisionHeader commonAncestorRevisionHeader = logfile.getRevisionInformation().getRevisionHeader(commonAncestorRevisionIndex);
int indexIncrement = -1;
if (commonAncestorRevisionHeader.getDepth() > 0) {
indexIncrement = 1;
}
int index = commonAncestorRevisionIndex;
while (index >= 0) {
RevisionHeader revisionHeader = logfile.getRevisionInformation().getRevisionHeader(index);
if (revisionHeader != null) {
if (revisionHeader.getDepth() == commonAncestorRevisionHeader.getDepth()) {
if (revisionHeader.isTip()) {
branchParentTipRevisionString = revisionHeader.getRevisionString();
break;
}
}
} else {
throw new QVCSException("Failed to find branch parent tip revision");
}
// Handle the increment this way so that this method will work for both trunk based branches, and
// for branches where the parent branch is another branch.
index += indexIncrement;
}
return branchParentTipRevisionString;
}
/**
* Create a temp file from the buffer. We write the buffer to a temp file that we create. The name of the temp file uses the
* name parameter to help identify the role that the temp file plays here. We close the file before returning.
*
* @param name the prefix name we'll use for the temp file.
* @param buffer the buffer that gets written to the temp file.
* @return the File for the temp file that we write to.
* @throws IOException if there is an IO problem.
*/
private static File createTempFileFromBuffer(String name, byte[] buffer) throws IOException {
FileOutputStream outStream = null;
File tempFile = null;
try {
tempFile = File.createTempFile(name, null);
tempFile.deleteOnExit();
outStream = new java.io.FileOutputStream(tempFile);
outStream.write(buffer);
} finally {
if (outStream != null) {
try {
outStream.close();
} catch (IOException e) {
throw e;
}
}
}
return tempFile;
}
}