// 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.qvcslib; import java.io.IOException; import java.io.RandomAccessFile; import java.text.DateFormat; import java.util.Date; import java.util.logging.Level; import java.util.logging.Logger; /** * Revision Header. Wrap the revision header that is stored for each revision within a QVCS archive file. * @author Jim Voris */ public final class RevisionHeader implements java.io.Serializable { private static final long serialVersionUID = -3233746302839526442L; /** Create our logger object */ private static final Logger LOGGER = Logger.getLogger("com.qumasoft.qvcslib"); // Revision header info that is stored in the archive file. private final AccessList accessList; private final AccessList modifierList; private RevisionDescriptor revisionDescriptor; private RevisionCompressionHeader compressionInformation; private String revisionDescription; private CommonShort descriptionSize = new CommonShort(); private CommonShort creatorIndex = new CommonShort(); private CommonShort lockerIndex = new CommonShort(); private Common32Long revisionSize = new Common32Long(); private CommonTime editDate = new CommonTime(); private CommonTime checkInDate = new CommonTime(); private byte newLineCharacter; private byte newLineFlag; private byte compressFlag; private byte isTipFlag; private CommonShort depth = new CommonShort(); private CommonShort childCount = new CommonShort(); private CommonShort lockFlag = new CommonShort(); private CommonShort minorNumber = new CommonShort(); private CommonShort majorNumber = new CommonShort(); // Where in the archive to find this revision. private transient long revisionHeaderStartPosition; private transient long revisionDataStartPosition; private transient int revisionIndex; private transient RevisionHeader parentRevisionHeader = null; private static final long MILLI_SECONDS_PER_SECOND = 1000L; /** * Create a revision header. * @param accessLst the access list. * @param modifierLst the modifier list. */ public RevisionHeader(AccessList accessLst, AccessList modifierLst) { revisionHeaderStartPosition = -1; revisionDataStartPosition = -1; this.accessList = accessLst; this.modifierList = modifierLst; } /** * A copy constructor. * @param copyThisRevisionHeader the revision header to copy. */ public RevisionHeader(RevisionHeader copyThisRevisionHeader) { this.accessList = new AccessList(copyThisRevisionHeader.accessList.getAccessListAsCommaSeparatedString()); this.modifierList = new AccessList(copyThisRevisionHeader.modifierList.getAccessListAsCommaSeparatedString()); this.revisionDescriptor = new RevisionDescriptor(copyThisRevisionHeader.revisionDescriptor); this.compressFlag = copyThisRevisionHeader.compressFlag; if (compressFlag != 0) { this.compressionInformation = new RevisionCompressionHeader(copyThisRevisionHeader.compressionInformation); } else { this.compressionInformation = null; } this.revisionDescription = copyThisRevisionHeader.revisionDescription; this.descriptionSize = new CommonShort(copyThisRevisionHeader.descriptionSize.getValue()); this.creatorIndex = new CommonShort(copyThisRevisionHeader.creatorIndex.getValue()); this.lockerIndex = new CommonShort(copyThisRevisionHeader.lockerIndex.getValue()); this.revisionSize = new Common32Long(copyThisRevisionHeader.revisionSize.getValue()); this.editDate = new CommonTime((int) copyThisRevisionHeader.editDate.getValue()); this.checkInDate = new CommonTime((int) copyThisRevisionHeader.checkInDate.getValue()); this.newLineCharacter = copyThisRevisionHeader.newLineCharacter; this.newLineFlag = copyThisRevisionHeader.newLineFlag; this.isTipFlag = copyThisRevisionHeader.isTipFlag; this.depth = new CommonShort(copyThisRevisionHeader.depth.getValue()); this.childCount = new CommonShort(copyThisRevisionHeader.childCount.getValue()); this.lockFlag = new CommonShort(copyThisRevisionHeader.lockFlag.getValue()); this.minorNumber = new CommonShort(copyThisRevisionHeader.minorNumber.getValue()); this.majorNumber = new CommonShort(copyThisRevisionHeader.majorNumber.getValue()); this.revisionHeaderStartPosition = copyThisRevisionHeader.revisionHeaderStartPosition; this.revisionDataStartPosition = copyThisRevisionHeader.revisionDataStartPosition; this.revisionIndex = copyThisRevisionHeader.revisionIndex; this.parentRevisionHeader = null; } /** * Convenience implementation of toString. * @return convenient String representation of this revision header. */ @Override public String toString() { StringBuilder returnString = new StringBuilder(); // Report the revision returnString.append("Revision "); returnString.append(revisionDescriptor); returnString.append(" created by "); returnString.append(modifierList.indexToUser(creatorIndex.getValue())); Date localEditDate = new Date(MILLI_SECONDS_PER_SECOND * this.editDate.getValue()); returnString.append("\nLast File edit: "); returnString.append(DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(localEditDate)); Date localCheckInDate = new Date(MILLI_SECONDS_PER_SECOND * this.checkInDate.getValue()); returnString.append("\nCheck-in date: "); returnString.append(DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(localCheckInDate)); if (compressFlag != 0) { returnString.append("\nRevision storage compressed from: "); returnString.append(compressionInformation.getInputSize()); returnString.append(" bytes to: "); returnString.append(compressionInformation.getCompressedSize()); returnString.append(" bytes\n"); } else { returnString.append("\nRevision storage requires: "); returnString.append(revisionSize.getValue()); returnString.append(" bytes\n"); } returnString.append("Revision description:\n"); returnString.append(revisionDescription).append("\n"); return returnString.toString(); } /** * Get the revision string. * @return the revision string. */ public String getRevisionString() { return revisionDescriptor.toString(); } /** * Get the revision size. The is the number of bytes it takes to store this revision on disk. * @return the revision size. The is the number of bytes it takes to store this revision on disk. */ public int getRevisionSize() { return revisionSize.getValue(); } /** * Set the revision size. * @param size the revision size. */ public void setRevisionSize(int size) { revisionSize.setValue(size); } /** * Get the 'depth' of this revision. A depth of 0 means this is a trunk revision; a depth of 1, means it's a branch revision; 2 means it a deeper branch, etc. * @return the 'depth' of this revision. */ public int getDepth() { return depth.getValue(); } /** * Set the 'depth' of this revision. * @param inDepth the 'depth' of this revision. */ public void setDepth(int inDepth) { this.depth.setValue(inDepth); } /** * Update only those portions of the revision header that are invariant in size. This method cannot be used to change the revision description since that could * change the size of the revision header stuff. This method is meant to be used to lock/unlock a revision. It could also be used to change the revision author; that's all. * @param outStream the stream to write the update to. * @return true if the update was successful. */ public boolean updateInPlace(RandomAccessFile outStream) { boolean retVal; try { outStream.seek(revisionHeaderStartPosition); retVal = writeConstantLengthPortion(outStream); } catch (IOException e) { retVal = false; } return retVal; } /** * Write this revision header to the the output stream. This method assumes that the output stream is positioned correctly. * @param outStream the stream to write to. * @return true if the write was successful; false otherwise. */ public boolean write(RandomAccessFile outStream) { boolean retVal; try { retVal = writeConstantLengthPortion(outStream); if (retVal) { // Write the revision description if (descriptionSize.getValue() > 0) { outStream.write(revisionDescription.getBytes()); outStream.writeByte(0); } } } catch (IOException e) { retVal = false; } return retVal; } /** * Read the revision header from disk. * @param inStream the stream to read from. * @param majorMinorArray array of revision pairs used to figure out the revision descriptor for this revision. * @return true if the read was successful; false otherwise. */ public boolean read(RandomAccessFile inStream, MajorMinorRevisionPair[] majorMinorArray) { boolean returnValue = true; try { // Save where this revision starts. revisionHeaderStartPosition = inStream.getFilePointer(); majorNumber.read(inStream); minorNumber.read(inStream); lockFlag.read(inStream); childCount.read(inStream); // Get the information we need to figure out // the revision descriptor for this revision depth.read(inStream); majorMinorArray[depth.getValue()] = new MajorMinorRevisionPair(majorNumber.getValue(), minorNumber.getValue()); revisionDescriptor = new RevisionDescriptor(depth.getValue() + 1, majorMinorArray); isTipFlag = inStream.readByte(); compressFlag = inStream.readByte(); newLineFlag = inStream.readByte(); newLineCharacter = inStream.readByte(); checkInDate.read(inStream); editDate.read(inStream); revisionSize.read(inStream); lockerIndex.read(inStream); creatorIndex.read(inStream); descriptionSize.read(inStream); // Read the revision description if (descriptionSize.getValue() > 0) { byte[] localRevisionDescription = new byte[descriptionSize.getValue()]; int bytesRead = inStream.read(localRevisionDescription); this.revisionDescription = new String(localRevisionDescription, 0, localRevisionDescription.length - 1); } // If this revision is compressed, read in the compression information // and reposition the archive back to the start of the revision. if (compressFlag != 0) { compressionInformation = new RevisionCompressionHeader(); if (revisionSize.getValue() > 0) { long currentFilePosition = inStream.getFilePointer(); compressionInformation.read(inStream); // Seek back to the start of this revision inStream.seek(currentFilePosition); } } // Save the location where the data of the revision starts. revisionDataStartPosition = inStream.getFilePointer(); // Skip forward to the next revision. inStream.skipBytes(revisionSize.getValue()); } catch (IOException ioProblem) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(ioProblem)); returnValue = false; } return returnValue; } /** * Write the revision header. */ private boolean writeConstantLengthPortion(RandomAccessFile outStream) { boolean retVal = true; try { majorNumber.write(outStream); minorNumber.write(outStream); lockFlag.write(outStream); childCount.write(outStream); depth.write(outStream); outStream.writeByte(isTipFlag); outStream.writeByte(compressFlag); outStream.writeByte(newLineFlag); outStream.writeByte(newLineCharacter); checkInDate.write(outStream); editDate.write(outStream); revisionSize.write(outStream); lockerIndex.write(outStream); creatorIndex.write(outStream); descriptionSize.write(outStream); } catch (IOException e) { retVal = false; } return retVal; } /** * Set the parent revision header. * @param parent the parent revision header. */ public void setParentRevisionHeader(RevisionHeader parent) { parentRevisionHeader = parent; } /** * Get the parent revision header. * @return the parent revision header. */ public RevisionHeader getParentRevisionHeader() { return parentRevisionHeader; } /** * Get the revision start position. This is the seek position for the start of this revision header within the archive file. * @return the revision start position. */ public long getRevisionStartPosition() { return revisionHeaderStartPosition; } /** * Get the revision data start position. This is the seek position for the start of the content data associated with this revision header... i.e. that that that is the * raw bytes of the file under version control, or the series of deltas that represent the edit script that can be used to reconstitute the given revision. * @return the revision data start position. */ public long getRevisionDataStartPosition() { return revisionDataStartPosition; } /** * Get the revision compression header. Meaningful only if the revision is compressed. * @return the revision compression header. */ public RevisionCompressionHeader getCompressionHeader() { return compressionInformation; } /** * Flag to indicate if this is a tip revision. * @return true if this is a tip revision; false otherwise. */ public boolean isTip() { boolean isTip = false; if (isTipFlag != 0) { isTip = true; } return isTip; } /** * Set the 'is tip' flag. * @param flag the 'is tip' flag. */ public void setIsTip(boolean flag) { if (flag) { isTipFlag = 1; } else { isTipFlag = 0; } } /** * Flag to indicate if the revision is compressed. * @return true if this revision is compressed; false otherwise. */ public boolean isCompressed() { boolean isCompressed = false; if (compressFlag != 0) { isCompressed = true; } return isCompressed; } /** * Set the 'is compressed' flag. * @param flag the 'is compressed' flag. */ public void setIsCompressed(boolean flag) { if (flag) { compressFlag = 1; } else { compressFlag = 0; } } /** * Flag to indicate if the revision is locked. * @return true if the revision is locked; false otherwise. */ public boolean isLocked() { boolean isLocked = false; if (lockFlag.getValue() != 0) { isLocked = true; } return isLocked; } /** * Set the 'is locked' flag. * @param flag the 'is locked' flag. */ public void setIsLocked(boolean flag) { if (flag) { lockFlag.setValue(1); } else { lockFlag.setValue(0); } } /** * Get the locker index. This is the index within the modifier list of the user holding the lock on this revision. * @return the locker index. */ public int getLockerIndex() { return lockerIndex.getValue(); } /** * Set the locker index. * @param index the locker index. */ public void setLockerIndex(int index) { this.lockerIndex.setValue(index); } /** * Set the locker. * @param locker the user name of the locker. * @throws QVCSException if the locker is not a valid user. */ public void setLocker(String locker) throws QVCSException { int localLockerIndex = modifierList.userToIndex(locker); if (localLockerIndex < 0) { throw new QVCSException("Invalid revision locker: " + locker); } else { setLockerIndex(localLockerIndex); } } /** * Get the creator index. This is the index within the modifier list of the user who created this revision. * @return the creator index. */ public int getCreatorIndex() { return creatorIndex.getValue(); } /** * Get the number of branch revisions... i.e. the number of branches that are anchored by this revision. * @return the number of branch revisions anchored by this revision. */ public int getChildCount() { return childCount.getValue(); } /** * Increment the child count. */ public void incrementChildCount() { childCount.setValue(childCount.getValue() + 1); } /** * Decrement the child count. */ public void decrementChildCount() { if (childCount.getValue() > 0) { childCount.setValue(childCount.getValue() - 1); } } /** * Set the creator. * @param creator the QVCS user name of the creator of this revision. * @throws QVCSException if the creator is not a valid QVCS user name. */ public void setCreator(String creator) throws QVCSException { int localCreatorIndex = modifierList.userToIndex(creator); if (localCreatorIndex < 0) { throw new QVCSException("Invalid revision creator: " + creator); } else { this.creatorIndex.setValue(localCreatorIndex); } } /** * Get the checkin data for this revision. * @return the checkin data for this revision. */ public java.util.Date getCheckInDate() { return new java.util.Date(MILLI_SECONDS_PER_SECOND * checkInDate.getValue()); } /** * Set the checkin data for this revision. * @param time the checkin data for this revision. */ public void setCheckInDate(java.util.Date time) { int dateTime = (int) (time.getTime() / MILLI_SECONDS_PER_SECOND); checkInDate.setValue(dateTime); } /** * Get the edit date for this revision. This is the last edit time of the workfile used to create this revision. * @return the edit date for this revision. */ public java.util.Date getEditDate() { return new java.util.Date(MILLI_SECONDS_PER_SECOND * editDate.getValue()); } /** * Set the edit date for this revision. * @param time the edit date for this revision. */ public void setEditDate(java.util.Date time) { int dateTime = (int) (time.getTime() / MILLI_SECONDS_PER_SECOND); editDate.setValue(dateTime); } /** * Get this revision's RevisionDescriptor. * @return this revision's RevisionDescriptor. */ public RevisionDescriptor getRevisionDescriptor() { return revisionDescriptor; } /** * Set this revision's RevisionDescriptor. * @param revDescriptor this revision's RevisionDescriptor. */ public void setRevisionDescriptor(RevisionDescriptor revDescriptor) { this.revisionDescriptor = revDescriptor; } /** * Get the revision description (i.e. the checkin comment). * @return the revision description (i.e. the checkin comment). */ public String getRevisionDescription() { return revisionDescription; } /** * Set the revision description (i.e. the checkin comment). * @param description the revision description (i.e. the checkin comment). */ public void setRevisionDescription(String description) { revisionDescription = description; descriptionSize.setValue(1 + description.getBytes().length); } /** * Get the revision's major revision number. * @return the revision's major revision number. */ public int getMajorNumber() { return majorNumber.getValue(); } /** * Set the revision's major revision number. * @param number the revision's major revision number. */ public void setMajorNumber(int number) { majorNumber.setValue(number); } /** * Get the revision's minor revision number. * @return the revision's minor revision number. */ public int getMinorNumber() { return minorNumber.getValue(); } /** * Set the revision's minor revision number. * @param number the revision's minor revision number. */ public void setMinorNumber(int number) { minorNumber.setValue(number); } /** * Get the revision's revision index... i.e. its position within the QVCS archive file. An index of 0 is the newest revision. * @return the revison's revision index. */ public int getRevisionIndex() { return this.revisionIndex; } /** * Set the revision's revision index. * @param index the revision's revision index. */ public void setRevisionIndex(int index) { this.revisionIndex = index; } }