/* 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.AccessList; import com.qumasoft.qvcslib.ArchiveAttributes; import com.qumasoft.qvcslib.ArchiveDirManagerInterface; import com.qumasoft.qvcslib.ArchiveInfoInterface; import com.qumasoft.qvcslib.LabelInfo; import com.qumasoft.qvcslib.LogFileHeader; import com.qumasoft.qvcslib.LogFileHeaderInfo; import com.qumasoft.qvcslib.LogFileInterface; import com.qumasoft.qvcslib.LogfileInfo; import com.qumasoft.qvcslib.LogfileListenerInterface; import com.qumasoft.qvcslib.MajorMinorRevisionPair; import com.qumasoft.qvcslib.QVCSConstants; import com.qumasoft.qvcslib.QVCSException; import com.qumasoft.qvcslib.RemoteViewProperties; import com.qumasoft.qvcslib.RevisionDescriptor; import com.qumasoft.qvcslib.RevisionHeader; import com.qumasoft.qvcslib.RevisionInformation; import com.qumasoft.qvcslib.Utility; import com.qumasoft.qvcslib.commandargs.CheckInCommandArgs; import com.qumasoft.qvcslib.commandargs.CheckOutCommandArgs; import com.qumasoft.qvcslib.commandargs.GetRevisionCommandArgs; import com.qumasoft.qvcslib.commandargs.LabelRevisionCommandArgs; import com.qumasoft.qvcslib.commandargs.LockRevisionCommandArgs; import com.qumasoft.qvcslib.commandargs.SetRevisionDescriptionCommandArgs; import com.qumasoft.qvcslib.commandargs.UnLabelRevisionCommandArgs; import com.qumasoft.qvcslib.commandargs.UnlockRevisionCommandArgs; import com.qumasoft.qvcslib.logfileaction.ActionType; import com.qumasoft.qvcslib.logfileaction.SetAttributes; import com.qumasoft.server.dataaccess.PromotionCandidateDAO; import com.qumasoft.server.dataaccess.impl.PromotionCandidateDAOImpl; import com.qumasoft.server.datamodel.PromotionCandidate; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; /** * Archive info for translucent branch. * @author Jim Voris */ public final class ArchiveInfoForTranslucentBranch implements ArchiveInfoInterface, LogFileInterface, LogfileListenerInterface { // Create our logger object private static final Logger LOGGER = Logger.getLogger("com.qumasoft.server"); private String shortWorkfileName; private Date dateOfLastCheckIn; private String lastEditBy; private String defaultRevisionString; private byte[] defaultRevisionDigest; private final ArchiveAttributes archiveAttributes; private LogfileInfo logfileInfo; private int logFileFileID = -1; private final String projectName; private final String viewName; private final String branchLabel; private int revisionCount = -1; private List<LogfileListenerInterface> logfileListeners; private String fullLogfileName; private final ProjectView projectView; private String workfileInLocation; private Map<String, RevisionHeader> revisionHeaderMap; private boolean isOverlapFlag; /** * Creates a new instance of ArchiveInfoForReadOnlyDateBasedView. * @param shortName the short workfile name. * @param logFile the LogFile instance from which we build the archive info for the translucent branch. * @param remoteViewProperties the project properties. */ ArchiveInfoForTranslucentBranch(String shortName, LogFile logFile, RemoteViewProperties remoteViewProperties) { this.shortWorkfileName = shortName; this.projectName = remoteViewProperties.getProjectName(); this.viewName = remoteViewProperties.getViewName(); this.projectView = ViewManager.getInstance().getView(this.projectName, this.viewName); this.branchLabel = this.projectView.getTranslucentBranchLabel(); this.logfileInfo = buildLogfileInfo(logFile); this.defaultRevisionDigest = ArchiveDigestManager.getInstance().getArchiveDigest(logFile, getDefaultRevisionString()); this.archiveAttributes = new ArchiveAttributes(logFile.getAttributes()); // No lock checking on a translucent branch. this.archiveAttributes.setIsCheckLock(false); this.logFileFileID = logFile.getFileID(); } /** * Get the short workfile name. * * @return the short workfile name. */ @Override public String getShortWorkfileName() { return shortWorkfileName; } /** * Set the short workfile name. * * @param newShortWorkfileName the new short workfile name. */ public void setShortWorkfileName(String newShortWorkfileName) { this.shortWorkfileName = newShortWorkfileName; } /** * Return the lock count. * * @return the lock count. */ @Override public int getLockCount() { return 0; } /** * Get the locked by string. Since we don't support locks on a translucent branch, this is always null; * * @return an empty string. */ @Override public String getLockedByString() { return ""; } /** * Get the last checkin date. * * @return the last checkin date. */ @Override public synchronized Date getLastCheckInDate() { return dateOfLastCheckIn; } /** * Get the workfile in location. We figure this out when we build object... see the deduceWorkfileLocation method. * * @return the workfile in location. */ @Override public synchronized String getWorkfileInLocation() { return this.workfileInLocation; } /** * Get the last edit by string. * * @return the last edit by string. */ @Override public synchronized String getLastEditBy() { return this.lastEditBy; } /** * Get the default revision digest. * * @return the default revision digest. */ @Override public synchronized byte[] getDefaultRevisionDigest() { return this.defaultRevisionDigest; } /** * Get the attributes. * * @return the attributes. */ @Override public ArchiveAttributes getAttributes() { return this.archiveAttributes; } /** * Get the logfile info. * * @return the logfile info. */ @Override public synchronized LogfileInfo getLogfileInfo() { return this.logfileInfo; } /** * Get the revision description for the given revision string. * * @param revisionString the revision string of the revision we are interested in. * @return that revision's revision description. */ @Override public synchronized String getRevisionDescription(final String revisionString) { String returnValue = null; try { RevisionHeader revisionHeader = this.revisionHeaderMap.get(revisionString); if (revisionString != null) { returnValue = revisionHeader.getRevisionDescription(); } } catch (ClassCastException | NullPointerException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } return returnValue; } /** * Get a given revision as a byte array. * * @param revisionString the revision to fetch. * @return an in memory byte[] that is the non-keyword expanded bytes for the given revision. */ @Override public byte[] getRevisionAsByteArray(String revisionString) { byte[] retVal = null; try { retVal = getCurrentLogFile().getRevisionAsByteArray(revisionString); } catch (QVCSException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } return retVal; } /** * Get the locked revision string for the given user. * * @param userName the user. * @return null, since locks are not allowed on translucent branches. */ @Override public String getLockedRevisionString(String userName) { return null; } /** * Get the default revision string. * * @return the default revision string. */ @Override public synchronized String getDefaultRevisionString() { return this.defaultRevisionString; } /** * Get the requested revision to the requested file. * * @param commandLineArgs the command arguments that identify what revision to get. * @param fetchToFileName the file that the revision is written to. * @return true if things work as expected. * @throws QVCSException if something goes wrong. */ @Override public boolean getRevision(GetRevisionCommandArgs commandLineArgs, String fetchToFileName) throws QVCSException { boolean returnValue = false; try { // We need to fill in the revision string. if ((commandLineArgs.getRevisionString() != null) && (commandLineArgs.getRevisionString().length() > 0)) { if (0 == commandLineArgs.getRevisionString().compareTo(QVCSConstants.QVCS_DEFAULT_REVISION)) { commandLineArgs.setRevisionString(getDefaultRevisionString()); } } returnValue = getCurrentLogFile().getRevision(commandLineArgs, fetchToFileName); } catch (QVCSException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } return returnValue; } /** * Get for visual compare. * * @param commandLineArgs command arguments to identify what revision to fetch. * @param outputFileName where the revision is fetched to. * @return true if things work as expected. * @throws QVCSException if something goes wrong. */ @Override public boolean getForVisualCompare(GetRevisionCommandArgs commandLineArgs, String outputFileName) throws QVCSException { boolean returnValue = false; try { // We need to fill in the revision string. if ((commandLineArgs.getRevisionString() != null) && (commandLineArgs.getRevisionString().length() > 0)) { if (0 == commandLineArgs.getRevisionString().compareTo(QVCSConstants.QVCS_DEFAULT_REVISION)) { commandLineArgs.setRevisionString(getDefaultRevisionString()); } } returnValue = getCurrentLogFile().getForVisualCompare(commandLineArgs, outputFileName); } catch (QVCSException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } return returnValue; } /** * Check out a revision. This is not allowed on a translucent branch. * * @param commandLineArgs the command line arguments. * @param fetchToFileName the file to fetch to (not used). * @return false. * @throws QVCSException to satisfy the interface signature. */ @Override public boolean checkOutRevision(CheckOutCommandArgs commandLineArgs, String fetchToFileName) throws QVCSException { // Check outs are not allowed on translucent branches. return false; } /** * Check in a revision. * * @param commandArgs the command arguments. * @param checkInFilename the local file containing the new revision. * @param ignoreLocksToEnableBranchCheckInFlag flag (that we ignore because we always set it to true). * @return true if things work as expected. * @throws QVCSException if something goes wrong. */ @Override public boolean checkInRevision(CheckInCommandArgs commandArgs, String checkInFilename, boolean ignoreLocksToEnableBranchCheckInFlag) throws QVCSException { boolean retFlag; boolean firstRevisionOnBranchFlag = false; commandArgs.setApplyLabelFlag(true); commandArgs.setLabel(getProjectView().getTranslucentBranchLabel()); commandArgs.setLockedRevisionString(getDefaultRevisionString()); LogFile logfile = getCurrentLogFile(); if (logfile.getLogfileInfo().getLogFileHeaderInfo().hasLabel(getBranchLabel())) { commandArgs.setForceBranchFlag(false); commandArgs.setReuseLabelFlag(true); } else { commandArgs.setForceBranchFlag(true); firstRevisionOnBranchFlag = true; } retFlag = logfile.checkInRevision(commandArgs, checkInFilename, true); // If things worked, and this is the first time we've checked in on this branch, // record the record in the promotion candidate table. if (retFlag && firstRevisionOnBranchFlag) { capturePromotionCandidate(); } return retFlag; } void capturePromotionCandidate() throws QVCSException { Integer projectId = DatabaseCache.getInstance().getProjectId(getProjectName()); Integer branchId = DatabaseCache.getInstance().getBranchId(projectId, getViewName()); PromotionCandidate promotionCandidate = new PromotionCandidate(getFileID(), branchId); PromotionCandidateDAO promotionCandidateDAO = new PromotionCandidateDAOImpl(); try { promotionCandidateDAO.insertIfMissing(promotionCandidate); } catch (SQLException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); throw new QVCSException("Failed to create promotion candidate record for [" + getShortWorkfileName() + "]"); } } /** * Lock a revision. Locks are not allowed on a translucent branch. * * @param commandArgs command line arguments. * @return false. * @throws QVCSException to satisfy the interface signature. */ @Override public boolean lockRevision(LockRevisionCommandArgs commandArgs) throws QVCSException { // Locks are not allowed on a translucent branch. return false; } /** * Do the work needed to delete an archive. At the logfile level, this involves creating a new revision on the file branch * associated with the translucent branch * * @param userName the user name. * @param appendedPath the current appended path. * @param shortName the short workfile name. * @param date the date of this transaction. This should be taken from the enclosing transaction. * @return true if the work was completed successfully. * @throws QVCSException if something goes wrong. */ public boolean deleteArchive(String userName, String appendedPath, String shortName, final Date date) throws QVCSException { boolean retVal; boolean firstRevisionOnBranchFlag = false; String branchTipRevisionString = getBranchTipRevisionString(); if (branchTipRevisionString.length() > 0) { LogFile logfile = getCurrentLogFile(); if (!logfile.getLogfileInfo().getLogFileHeaderInfo().hasLabel(getBranchLabel())) { firstRevisionOnBranchFlag = true; } if (logfile.deleteArchiveOnTranslucentBranch(userName, appendedPath, shortName, date, getBranchLabel(), this)) { retVal = true; if (firstRevisionOnBranchFlag) { capturePromotionCandidate(); } } else { retVal = false; } } else { retVal = false; } return retVal; } /** * Resolve the conflict from the parent branch. <i>All</i> we do here is remove the branch label from the archive file. The * other work for resolving the conflict is performed elsewhere. The <i>only</i> responsibility that this class has is to make * those changes to the archive file that are required for this operation. Period. * * @param userName the user name. * @param date the transaction timestamp. * @return true if things work as expected. * @throws QVCSException if something goes wrong. */ public boolean resolveConflictFromParentBranch(String userName, final Date date) throws QVCSException { boolean retVal = false; LogFile logfile = getCurrentLogFile(); if (logfile.hasLabel(getBranchLabel())) { UnLabelRevisionCommandArgs commandArgs = new UnLabelRevisionCommandArgs(); commandArgs.setShortWorkfileName(getShortWorkfileName()); commandArgs.setUserName(userName); commandArgs.setLabelString(getBranchLabel()); retVal = logfile.unLabelRevision(commandArgs); } else { throw new QVCSException("Invalid attempt to resolve conflict from parent branch. Missing translucent branch label: [" + getBranchLabel() + "] for file: [" + getShortWorkfileName() + "]"); } return retVal; } /** * Promote the file. <i>All</i> we do here is remove the branch label from the archive file. The other work for promoting the * file is performed elsewhere. The <i>only</i> responsibility that this class has is to make those changes to the archive file * that are required for this operation. Period. * * @param userName the user name. * @param date the transaction timestamp. * @return true if things work as expected. * @throws QVCSException if something goes wrong. */ public boolean promoteFile(String userName, final Date date) throws QVCSException { boolean retVal = false; LogFile logfile = getCurrentLogFile(); if (logfile.hasLabel(getBranchLabel())) { UnLabelRevisionCommandArgs commandArgs = new UnLabelRevisionCommandArgs(); commandArgs.setShortWorkfileName(getShortWorkfileName()); commandArgs.setUserName(userName); commandArgs.setLabelString(getBranchLabel()); retVal = logfile.unLabelRevision(commandArgs); } else { throw new QVCSException("Invalid attempt to promote a file. Missing translucent branch label: [" + getBranchLabel() + "] for file: [" + getShortWorkfileName() + "]"); } return retVal; } /** * Do the work needed for a move. At the logfile level, this involves creating a new revision on the file branch associated with * the translucent branch * * @param userName the user name. * @param appendedPath the current appended path. * @param targetArchiveDirManagerInterface the destination directory (this should be an instance of a translucent branch * directory manager). * @param shortName the short workfile name. * @param date the date of this transaction. This should be taken from the enclosing transaction. * @return true if the work was completed successfully. * @throws QVCSException if something goes wrong. */ public boolean moveArchive(String userName, String appendedPath, ArchiveDirManagerInterface targetArchiveDirManagerInterface, String shortName, final Date date) throws QVCSException { boolean retVal; boolean firstRevisionOnBranchFlag = false; String branchTipRevisionString = getBranchTipRevisionString(); if (branchTipRevisionString.length() > 0) { LogFile logfile = getCurrentLogFile(); if (!logfile.getLogfileInfo().getLogFileHeaderInfo().hasLabel(getBranchLabel())) { firstRevisionOnBranchFlag = true; } if (targetArchiveDirManagerInterface instanceof ArchiveDirManagerForTranslucentBranch) { ArchiveDirManagerForTranslucentBranch targetArchiveDirManager = (ArchiveDirManagerForTranslucentBranch) targetArchiveDirManagerInterface; if (logfile.moveArchiveOnTranslucentBranch(userName, appendedPath, targetArchiveDirManager, shortName, date, getBranchLabel(), this)) { retVal = true; if (firstRevisionOnBranchFlag) { capturePromotionCandidate(); } } else { retVal = false; } } else { retVal = false; } } else { retVal = false; } return retVal; } /** * Do the work needed for a move. At the logfile level, this involves creating a new revision on the file branch associated with * the translucent branch * * @param userName the user name. * @param appendedPath the current appended path. * @param oldShortWorkfileName the current short workfile name. * @param newShortWorkfileName the new short workfile name. * @param date the date of this transaction. This should be taken from the enclosing transaction. * @return true if the work was completed successfully. * @throws QVCSException if something goes wrong. */ public boolean renameArchive(String userName, String appendedPath, String oldShortWorkfileName, String newShortWorkfileName, final Date date) throws QVCSException { boolean retVal; boolean firstRevisionOnBranchFlag = false; String branchTipRevisionString = getBranchTipRevisionString(); if (branchTipRevisionString.length() > 0) { LogFile logfile = getCurrentLogFile(); if (!logfile.getLogfileInfo().getLogFileHeaderInfo().hasLabel(getBranchLabel())) { firstRevisionOnBranchFlag = true; } if (logfile.renameArchiveOnTranslucentBranch(userName, appendedPath, oldShortWorkfileName, newShortWorkfileName, date, getBranchLabel(), this)) { retVal = true; if (firstRevisionOnBranchFlag) { capturePromotionCandidate(); } } else { retVal = false; } } else { retVal = false; } return retVal; } @Override public boolean unlockRevision(UnlockRevisionCommandArgs commandArgs) throws QVCSException { return false; } @Override public boolean breakLock(UnlockRevisionCommandArgs commandArgs) throws QVCSException { throw new QVCSException("Unexpected call to breakLock method."); } @Override public boolean labelRevision(LabelRevisionCommandArgs commandArgs) throws QVCSException { boolean retVal = false; // A label should only be allowed to be applied to the tip of the branch. String labelRevisionString = getBranchTipRevisionString(); if (labelRevisionString.length() > 0) { commandArgs.setRevisionFlag(true); commandArgs.setRevisionString(labelRevisionString); commandArgs.setReuseLabelFlag(true); commandArgs.setFloatingFlag(false); commandArgs.setDuplicateFlag(false); LogFile logfile = getCurrentLogFile(); if (logfile.labelRevision(commandArgs)) { retVal = true; } } return retVal; } @Override public boolean unLabelRevision(UnLabelRevisionCommandArgs commandArgs) throws QVCSException { LogFile logfile = getCurrentLogFile(); return logfile.unLabelRevision(commandArgs); } @Override public boolean getIsObsolete() { return false; } @Override public boolean setIsObsolete(String userName, boolean flag) throws QVCSException { return false; } @Override public boolean setAttributes(String userName, ArchiveAttributes attributes) throws QVCSException { return false; } @Override public boolean setCommentPrefix(String userName, String commentPrefix) throws QVCSException { return false; } @Override public boolean setModuleDescription(String userName, String moduleDescription) throws QVCSException { return false; } @Override public boolean setRevisionDescription(SetRevisionDescriptionCommandArgs commandArgs) throws QVCSException { LogFile logfile = getCurrentLogFile(); return logfile.setRevisionDescription(commandArgs); } /** * Figure out the tip revision for this branch. * * If this file has been branched already for this translucent branch, then the tip will be labeled with the branch's label. If, * however, the file has not yet been branched for this branch, then we need to figure out the branch's <i>parent</i> tip * revision and return that, since for translucent branches, the parent branch's tip is the same as our tip (unless we have * branched from the parent). * * @param logFile the trunk's archive file. * @return the revision header for this branch's tip revision. */ private int deduceBranchTipRevisionIndex(LogFile logFile, ProjectView view) throws QVCSException { int branchTipRevisionIndex = -1; String parentBranchName = view.getRemoteViewProperties().getBranchParent(); if (logFile.hasLabel(getBranchLabel())) { String revisionStringForLabel = getRevisionStringFromLabel(getBranchLabel(), logFile); int localRevisionCount = logFile.getRevisionCount(); RevisionInformation revisionInformation = logFile.getRevisionInformation(); for (int i = 0; i < localRevisionCount; i++) { if (0 == revisionInformation.getRevisionHeader(i).getRevisionString().compareTo(revisionStringForLabel)) { branchTipRevisionIndex = i; break; } } } else { if (0 == parentBranchName.compareTo(QVCSConstants.QVCS_TRUNK_VIEW)) { branchTipRevisionIndex = 0; } else { ProjectView parentView = ViewManager.getInstance().getView(getProjectName(), parentBranchName); branchTipRevisionIndex = deduceBranchTipRevisionIndex(logFile, parentView); } } return branchTipRevisionIndex; } /** * Figure out the default depth. For a logfile that hasn't branched (file level branch) for this translucent (project level) * branch, then the depth will be 0; for others, the depth can be deduced based on the default revision string (which * <b>MUST</b> have been figured out before this method is called). * * @return the default depth for this archive info. */ private int deduceDefaultDepth() { // We should just be able to count the number of revision segments minus 2. String localDefaultRevisionString = getDefaultRevisionString(); String[] revisionStringSegments = localDefaultRevisionString.split("\\."); int depth = (revisionStringSegments.length - 2) / 2; return depth; } /** * If there is a lock on the trunk's tip revision, and the branch is still viewing the trunk's tip revision as the branch's tip * revision then we can display the trunk's "workfile in" value. Alternately, if we are branched then there is no workfile * location since we do not support locks on a translucent branch. * * @param logFile the trunk's archive file. * @return a string that shows where the workfile is checked out. */ private String deduceWorkfileInLocation(LogFile logFile) { String localWorkfileInLocation = ""; if (0 == logFile.getDefaultRevisionString().compareTo(getDefaultRevisionString())) { if (logFile.getLockCount() > 0) { localWorkfileInLocation = logFile.getWorkfileInLocation(); } } return localWorkfileInLocation; } private String getProjectName() { return this.projectName; } private String getViewName() { return this.viewName; } @Override public synchronized String getFullArchiveFilename() { return this.fullLogfileName; } @Override public int getFileID() { return this.logFileFileID; } /** * Get the LogFile instance associated with this translucent archive info. * @return the LogFile instance associated with this translucent archive info. * @throws QVCSException if there are problems finding the associated LogFile. */ public LogFile getCurrentLogFile() throws QVCSException { FileIDInfo fileIDInfo = FileIDDictionary.getInstance().lookupFileIDInfo(getProjectName(), QVCSConstants.QVCS_TRUNK_VIEW, this.logFileFileID); int directoryID = fileIDInfo.getDirectoryID(); // Lookup the archiveDirManager for the file's current location... ArchiveDirManager archiveDirManager = DirectoryIDDictionary.getInstance().lookupArchiveDirManager(getProjectName(), directoryID, null, true); String keyToFile = fileIDInfo.getShortFilename(); boolean ignoreCaseFlag = archiveDirManager.getProjectProperties().getIgnoreCaseFlag(); if (ignoreCaseFlag) { keyToFile = keyToFile.toLowerCase(); } // Get the file's current archiveInfo... return (LogFile) archiveDirManager.getArchiveInfo(keyToFile); } private synchronized LogfileInfo buildLogfileInfo(LogFile logFile) { LogfileInfo localLogfileInfo = null; this.fullLogfileName = logFile.getFullArchiveFilename(); RevisionInformation branchRevisionInformation = buildBranchRevisionInformation(logFile); if (branchRevisionInformation != null) { this.defaultRevisionString = getBranchTipRevisionString(logFile); LogFileHeaderInfo branchLogFileHeaderInfo = buildBranchLogFileHeaderInfo(logFile, branchRevisionInformation); localLogfileInfo = new LogfileInfo(branchLogFileHeaderInfo, branchRevisionInformation, logFile.getFileID(), this.fullLogfileName); dateOfLastCheckIn = branchRevisionInformation.getRevisionHeader(0).getCheckInDate(); // Figure out what we should display for workfile in location... this.workfileInLocation = deduceWorkfileInLocation(logFile); // Figure out who made the last edit. int lastEditByIndex = branchRevisionInformation.getRevisionHeader(0).getCreatorIndex(); AccessList modifierList = branchRevisionInformation.getModifierList(); this.lastEditBy = modifierList.indexToUser(lastEditByIndex); } return localLogfileInfo; } private synchronized RevisionInformation buildBranchRevisionInformation(LogFile logFile) { RevisionInformation branchRevisionInformation = null; try { // Need to create the set of revisions that are visible for this view... int branchTipRevisionIndex = deduceBranchTipRevisionIndex(logFile, getProjectView()); RevisionInformation logFileRevisionInformation = logFile.getRevisionInformation(); assert (branchTipRevisionIndex >= 0); isOverlapFlag = false; int[] branchIndexes = deduceBranchRevisions(branchTipRevisionIndex, logFile.getLogfileInfo()); AccessList accessList = new AccessList(logFile.getLogFileHeaderInfo().getAccessList()); branchRevisionInformation = new RevisionInformation(branchIndexes.length, accessList, accessList); this.revisionHeaderMap = new TreeMap<>(); for (int i = 0; i < branchIndexes.length; i++) { RevisionHeader branchRevisionHeader = new RevisionHeader(logFileRevisionInformation.getRevisionHeader(branchIndexes[i])); // We do not allow locks on the branch. branchRevisionHeader.setIsLocked(false); branchRevisionInformation.updateRevision(i, branchRevisionHeader); this.revisionHeaderMap.put(branchRevisionHeader.getRevisionString(), branchRevisionHeader); } this.revisionCount = this.revisionHeaderMap.size(); } catch (QVCSException e) { LOGGER.log(Level.SEVERE, Utility.expandStackTraceToString(e), e); } return branchRevisionInformation; } /** * This method builds an ordered list of revision indexes that are on this branch and figures out whether we have overlap. * * @param branchTipRevisionIndex The index of the branch's tip revision. * @param info the logfile info of the logfile. * @return an ordered array of the revision indexes that appear on this branch. * @throws java.io.IOException */ private synchronized int[] deduceBranchRevisions(int branchTipRevisionIndex, LogfileInfo info) { RevisionInformation revisionInformation = info.getRevisionInformation(); RevisionHeader branchTipRevisionHeader = revisionInformation.getRevisionHeader(branchTipRevisionIndex); assert (branchTipRevisionHeader.isTip()); int localRevisionCount = info.getLogFileHeaderInfo().getRevisionCount(); Map<RevisionDescriptor, Integer> revisions = Collections.synchronizedMap(new TreeMap<RevisionDescriptor, Integer>()); // The idea is to walk the set of revisions looking for those that belong on this branch. // given that the branch tip revision is the one passed in. if (branchTipRevisionHeader.getDepth() == 0) { // We are dealing with the trunk tip revision. This is the easy case. for (int i = branchTipRevisionIndex; i < localRevisionCount; i++) { RevisionHeader currentRevision = revisionInformation.getRevisionHeader(i); if (currentRevision.getDepth() == 0) { revisions.put(currentRevision.getRevisionDescriptor(), Integer.valueOf(i)); } } } else { // For branch revisions, we need to look through all revisions, // since some of the revisions we show may be located at a lower // revision index than the one we are expanding the log for. for (int i = 0; i < localRevisionCount; i++) { RevisionHeader currentRevision = revisionInformation.getRevisionHeader(i); if (isAncestor(currentRevision, branchTipRevisionHeader)) { revisions.put(currentRevision.getRevisionDescriptor(), Integer.valueOf(i)); } else { // A non ancestor.... if any non-ancestor exists, and has a lower // revision index than the index of the branchTipRevisionIndex, then // we have overlap -- meaning that there have been edits to the file // after we the branch point for this translucent branch. if (i < branchTipRevisionIndex) { isOverlapFlag = true; } } } } // Bundle the result in a nice package. int[] returnedIndexes = new int[revisions.size()]; int maxIndex = revisions.size() - 1; Iterator iterator = revisions.values().iterator(); for (int i = 0; iterator.hasNext(); i++) { Integer integerIndex = (Integer) iterator.next(); returnedIndexes[maxIndex - i] = integerIndex.intValue(); } return returnedIndexes; } /** * (Cloned from QVCSKeywordManager). Determine whether a revision is an ancestor of the descendant revision. * * @param ancestorCandidate the prospective ancestor. * @param descendantRevision the descendant revision. * @return true if the prospective ancestor <i>is</i> an ancestor of the descendant. */ private boolean isAncestor(RevisionHeader ancestorCandidate, RevisionHeader descendantRevision) { boolean retVal; if (ancestorCandidate.getDepth() > descendantRevision.getDepth()) { // There is no way for a revision that is deeper than the decendant // to be an ancestor of that decendant. retVal = false; } else if (ancestorCandidate.getDepth() == descendantRevision.getDepth()) { // If the depths are equal, then all elements except the final minor // number must match; the final minor number must be less than the // minor number of the decendant. int i; retVal = true; for (i = 0; retVal && (i < ancestorCandidate.getDepth()); i++) { MajorMinorRevisionPair ancestorPair = ancestorCandidate.getRevisionDescriptor().getRevisionPairs()[i]; MajorMinorRevisionPair decendantPair = descendantRevision.getRevisionDescriptor().getRevisionPairs()[i]; if (ancestorPair.getMajorNumber() == decendantPair.getMajorNumber() && ancestorPair.getMinorNumber() == decendantPair.getMinorNumber()) { continue; } else { retVal = false; } } // Look at the last pair... if (retVal) { MajorMinorRevisionPair ancestorPair = ancestorCandidate.getRevisionDescriptor().getRevisionPairs()[i]; MajorMinorRevisionPair decendantPair = descendantRevision.getRevisionDescriptor().getRevisionPairs()[i]; if (ancestorPair.getMajorNumber() == decendantPair.getMajorNumber()) { if (ancestorPair.getMinorNumber() > decendantPair.getMinorNumber()) { retVal = false; } } else { retVal = false; } } } else { // If the candidate depth is less than the decendant, then // all the major/minor pairs of the candidate must match the // decendant's. int i; retVal = true; for (i = ancestorCandidate.getDepth(); retVal && (i > 0); i--) { MajorMinorRevisionPair ancestorPair = ancestorCandidate.getRevisionDescriptor().getRevisionPairs()[i]; MajorMinorRevisionPair decendantPair = descendantRevision.getRevisionDescriptor().getRevisionPairs()[i]; if (ancestorPair.getMajorNumber() == decendantPair.getMajorNumber() && ancestorPair.getMinorNumber() == decendantPair.getMinorNumber()) { continue; } else { retVal = false; } } // For trunk revisions, the major number must be <=, and the minor number must be lower if (retVal) { MajorMinorRevisionPair ancestorPair = ancestorCandidate.getRevisionDescriptor().getRevisionPairs()[0]; MajorMinorRevisionPair decendantPair = descendantRevision.getRevisionDescriptor().getRevisionPairs()[0]; if (ancestorPair.getMajorNumber() > decendantPair.getMajorNumber()) { retVal = false; } else { if (ancestorPair.getMinorNumber() > decendantPair.getMinorNumber()) { retVal = false; } } } } return retVal; } private LogFileHeaderInfo buildBranchLogFileHeaderInfo(LogFile logFile, RevisionInformation branchRevisionInformation) { LogFileHeaderInfo logFileHeaderInfo; LabelInfo[] labelInfo = buildBranchLabelInfo(logFile, branchRevisionInformation); LogFileHeader logFileHeader = buildBranchLogFileHeader(logFile, branchRevisionInformation); logFileHeaderInfo = new LogFileHeaderInfo(logFileHeader); logFileHeaderInfo.setAccessList(logFile.getLogFileHeaderInfo().getAccessList()); logFileHeaderInfo.setCommentPrefix(logFile.getLogFileHeaderInfo().getCommentPrefix()); logFileHeaderInfo.setModifierList(logFile.getLogFileHeaderInfo().getModifierList()); logFileHeaderInfo.setModuleDescription(logFile.getLogFileHeaderInfo().getModuleDescription()); logFileHeaderInfo.setOwner(logFile.getLogFileHeaderInfo().getOwner()); logFileHeaderInfo.setWorkfileName(logFile.getLogFileHeaderInfo().getWorkfileName()); logFileHeaderInfo.setLabelInfo(labelInfo); logFileHeaderInfo.setDefaultRevisionDescriptor(branchRevisionInformation.getRevisionHeader(0).getRevisionDescriptor()); return logFileHeaderInfo; } private LogFileHeader buildBranchLogFileHeader(LogFile logFile, RevisionInformation branchRevisionInformation) { LogFileHeader branchLogFileHeader = new LogFileHeader(); LogFileHeader existingLogFileHeader = logFile.getLogFileHeaderInfo().getLogFileHeader(); RevisionHeader tipRevisionHeader = branchRevisionInformation.getRevisionHeader(0); branchLogFileHeader.getQVCSVersion().setValue((short) QVCSConstants.QVCS_ARCHIVE_VERSION); branchLogFileHeader.getMajorNumber().setValue((short) tipRevisionHeader.getMajorNumber()); branchLogFileHeader.getMinorNumber().setValue((short) tipRevisionHeader.getMinorNumber()); branchLogFileHeader.getAttributeBits().setValue((short) existingLogFileHeader.attributes().getAttributesAsInt()); branchLogFileHeader.getVersionCount().setValue((short) 0); branchLogFileHeader.getRevisionCount().setValue((short) getRevisionCount()); branchLogFileHeader.getDefaultDepth().setValue((short) deduceDefaultDepth()); branchLogFileHeader.getLockCount().setValue((short) 0); branchLogFileHeader.getAccessSize().setValue((short) 0); branchLogFileHeader.getModifierSize().setValue((short) 0); branchLogFileHeader.getCommentSize().setValue((short) 0); branchLogFileHeader.getOwnerSize().setValue((short) 0); branchLogFileHeader.getDescSize().setValue((short) 0); branchLogFileHeader.getSupplementalInfoSize().setValue((short) 0); // Lock checking is not allowed on the translucent branch. ArchiveAttributes branchHeaderAttributes = new ArchiveAttributes(branchLogFileHeader.getAttributeBits().getValue()); branchHeaderAttributes.setIsCheckLock(false); branchLogFileHeader.setAttributes(branchHeaderAttributes); branchLogFileHeader.getCheckSum().setValue(branchLogFileHeader.computeCheckSum()); return branchLogFileHeader; } @Override public synchronized RevisionInformation getRevisionInformation() { return this.logfileInfo.getRevisionInformation(); } private LabelInfo[] buildBranchLabelInfo(LogFile logFile, RevisionInformation viewRevisionInformation) { LabelInfo[] viewLabelInfo = null; if (logFile.getLogfileInfo().getLogFileHeaderInfo().getLogFileHeader().getVersionCount().getValue() > 0) { // Build the set of revision strings visible for this view. Only include // a label if the label's revision string is in that set. Set<String> viewRevisionStringSet = new HashSet<>(); for (int i = 0; i < getRevisionCount(); i++) { viewRevisionStringSet.add(viewRevisionInformation.getRevisionHeader(i).getRevisionString()); } ArrayList<LabelInfo> viewLabelArray = new ArrayList<>(); LabelInfo[] currentLogFileLabelInfo = logFile.getLogFileHeaderInfo().getLabelInfo(); for (LabelInfo currentLogFileLabelInfo1 : currentLogFileLabelInfo) { String labelRevisionString = currentLogFileLabelInfo1.getLabelRevisionString(); if (viewRevisionStringSet.contains(labelRevisionString)) { // Make sure that we do NOT include the viewLabel... it's for // internal use only. if (0 != currentLogFileLabelInfo1.getLabelString().compareToIgnoreCase(getProjectView().getTranslucentBranchLabel())) { viewLabelArray.add(currentLogFileLabelInfo1); } } } if (viewLabelArray.size() > 0) { viewLabelInfo = new LabelInfo[viewLabelArray.size()]; for (int i = 0; i < viewLabelArray.size(); i++) { viewLabelInfo[i] = viewLabelArray.get(i); } } } return viewLabelInfo; } @Override public synchronized int getRevisionCount() { return this.revisionCount; } private ProjectView getProjectView() { return this.projectView; } /** * Get the branch label associated with the translucent branch that this archive info is associated with. * @return the branch label associated with the translucent branch that this archive info is associated with. */ public String getBranchLabel() { return this.branchLabel; } /** * Figure out the tip revision string for this branch. * * @return the branch tip revision string. */ public String getBranchTipRevisionString() { String branchTipRevisionString = ""; try { // We have to go to the original logfile to get this info, since we don't // include the branchLabel in the labelInfo that we keep here. LogFile logfile = getCurrentLogFile(); if (logfile.getLogfileInfo().getLogFileHeaderInfo().hasLabel(getBranchLabel())) { branchTipRevisionString = logfile.getLogfileInfo().getLogFileHeaderInfo().getRevisionStringForLabel(getBranchLabel()); } else { branchTipRevisionString = logfile.getDefaultRevisionString(); } } catch (QVCSException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } return branchTipRevisionString; } /** * Figure out the tip revision string for this branch. */ private String getBranchTipRevisionString(LogFile logfile) { String branchRevisionString; // We have to go to the original logfile to get this info, since we don't // include the viewLabel in the labelInfo that we keep here. if (logfile.getLogfileInfo().getLogFileHeaderInfo().hasLabel(getBranchLabel())) { branchRevisionString = logfile.getLogfileInfo().getLogFileHeaderInfo().getRevisionStringForLabel(getBranchLabel()); } else { branchRevisionString = logfile.getDefaultRevisionString(); } return branchRevisionString; } /** * Add a logfile listener. * @param l the logfile listener to add. */ public synchronized void addListener(LogfileListenerInterface l) { if (this.logfileListeners == null) { this.logfileListeners = new ArrayList<>(); } this.logfileListeners.add(l); } /** * Remove a logfile listener. * @param l the listener to remove. */ public synchronized void removeListener(LogfileListenerInterface l) { if (this.logfileListeners == null) { this.logfileListeners = new ArrayList<>(); } else { this.logfileListeners.remove(l); } } private synchronized void notifyLogfileListeners(ActionType action) { if (this.logfileListeners != null) { // Make a copy of the listener array so we can avoid concurrent modification exceptions // which happen when we handle the delete notification (which removes itself as a listener). ArrayList<LogfileListenerInterface> listenersCopy = new ArrayList<>(this.logfileListeners); Iterator<LogfileListenerInterface> i = listenersCopy.iterator(); while (i.hasNext()) { LogfileListenerInterface listener = i.next(); listener.notifyLogfileListener(this, action); } } } /** * We receive notifications here directly from the LogFile object. We'll forward this notification on to our listeners if the * change resulted in a meaningful change. * * @param subject the LogFile object that has changed. * @param action the type of change made to the LogFile. */ @Override public synchronized void notifyLogfileListener(ArchiveInfoInterface subject, ActionType action) { LOGGER.log(Level.INFO, "Received notification [" + action.getActionType() + "] on translucent branch for: [" + subject.getShortWorkfileName() + "]"); int oldRevisionCount = getRevisionCount(); int oldAttributes = getAttributes().getAttributesAsInt(); int oldLabelCount = getLogfileInfo().getLogFileHeaderInfo().getLogFileHeader().versionCount(); boolean oldOverlapFlag = getIsOverlap(); if (subject instanceof LogFile) { LogFile logfile = (LogFile) subject; this.logfileInfo = buildLogfileInfo(logfile); this.defaultRevisionDigest = ArchiveDigestManager.getInstance().getArchiveDigest(logfile, getDefaultRevisionString()); int newLabelCount = getLogfileInfo().getLogFileHeaderInfo().getLogFileHeader().versionCount(); // If things changed enough to matter, then let our listeners know about the change. if ((oldRevisionCount != getRevisionCount()) || (oldAttributes != getAttributes().getAttributesAsInt()) || (oldLabelCount != newLabelCount) || (oldOverlapFlag != getIsOverlap()) || (action.getAction() == ActionType.CHANGE_HEADER) || (action.getAction() == ActionType.CHANGE_REVHEADER) || (action.getAction() == ActionType.SET_REVISION_DESCRIPTION) || (action.getAction() == ActionType.SET_MODULE_DESCRIPTION) || // Include checkins since that could cause creation of overlap/conflict (action.getAction() == ActionType.CHECKIN)) { // If the logfile has revisions associated with this branch, and this is a remove // operation, then we need to 'eat' the remove notification, and turn it into // a change header type notification, since we do not want to remove this // file from the branch's directory manager. if (action.getAction() == ActionType.REMOVE) { if (logfile.hasLabel(getBranchLabel())) { action = new SetAttributes(getAttributes()); LOGGER.log(Level.INFO, "\ttranslated to notification: [" + action.getActionType() + "] on translucent branch for: [" + subject.getShortWorkfileName() + "]"); } } notifyLogfileListeners(action); } if (action.getAction() == ActionType.REMOVE) { handleRemoveNotification(logfile); } } } private void handleRemoveNotification(LogFile logFile) { // If the logfile hasn't had any changes for this branch, then we are not // interested in it any more, since it will be in the cemetery and will not // represent any file that we are interested in on the branch. if (!logFile.hasLabel(getBranchLabel())) { logFile.removeListener(this); } } private String getRevisionStringFromLabel(final java.lang.String labelString, LogFile logFile) throws QVCSException { String revisionStringForLabel = null; // 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()) { throw new QVCSException("Internal Error. Found floating label for a branch!!"); } else { revisionStringForLabel = labelInfo1.getLabelRevisionString(); } break; } } } } return revisionStringForLabel; } @Override public synchronized boolean getIsOverlap() { return isOverlapFlag; } }