// 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.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.jrcs.diff.AddDelta;
import org.apache.commons.jrcs.diff.ChangeDelta;
import org.apache.commons.jrcs.diff.DeleteDelta;
import org.apache.commons.jrcs.diff.Delta;
import org.apache.commons.jrcs.diff.Diff;
import org.apache.commons.jrcs.diff.DifferentiationFailedException;
import org.apache.commons.jrcs.diff.Revision;
/**
* <p>
* Wrap the apache difference algorithm so it produces results that are useful.</p>
* <p>
* The CompareFilesWithApacheDiff class implements the QVCSOperation interface. It compares two files and creates an output file that contains a list of the differences between the
* two files.</p>
* <p>
* The argument array consists of just three arguments: the first input filename, the second input filename, and the output filename. The argument array may be set in the
* constructor to the CompareFiles object, or via a version of the execute command that takes an argument array.</p>
* <p>
* If the argument array isn't specified before the execute() method is called, or if the argument array doesn't have the necessary number of elements in it, the object will throw
* a QVCSCommandException.</p>
*
* @see QVCSOperation
* @author Jim Voris
*/
public class CompareFilesWithApacheDiff implements QVCSOperation {
// Create our logger object
private static final Logger LOGGER = Logger.getLogger("com.qumasoft.qvcslib");
private static final String UTF8 = "UTF-8";
/** The number of arguments that we need. */
protected static final int COMMAND_ARG_COUNT = 3;
private String[] args = null;
private boolean compareAttemptedFlag = false;
private boolean comparisonResultFlag = false;
private boolean ignoreAllWhitespaceFlag = false;
private boolean ignoreLeadingWhitespaceFlag = false;
private boolean ignoreCaseFlag = false;
private boolean ignoreEOLChangesFlag = false;
private File inFileA;
private File inFileB;
private File outFile;
private int file1LineCount = 0;
private int file2LineCount = 0;
/**
* Constructor with arguments defined.
* @param arguments the arguments that define what we should compare.
*/
public CompareFilesWithApacheDiff(String[] arguments) {
this.args = arguments;
}
/**
* Default constructor.
*/
public CompareFilesWithApacheDiff() {
}
/**
* Get the number of lines in file 1.
* @return the number of lines in file 1.
*/
int getFile1LineCount() {
return this.file1LineCount;
}
private void setFile1LineCount(int file1LineCnt) {
this.file1LineCount = file1LineCnt;
}
/**
* Get the number of lines in file 2.
* @return the number of lines in file 2.
*/
int getFile2LineCount() {
return this.file2LineCount;
}
/**
* Get the arguments.
* @return the arguments.
*/
public String[] getArgs() {
return this.args;
}
private void setFile2LineCount(int file2LineCnt) {
this.file2LineCount = file2LineCnt;
}
/**
* Return true if the two files are equal; false if they are not equal. Throws QVCSCommandException if the comparison hasn't yet been made.
*
* @return true if the two files are equal; false otherwise.
* @throws com.qumasoft.qvcslib.QVCSOperationException Throws this exception if the command arguments haven't yet been supplied.
*/
public boolean isEqual() throws QVCSOperationException {
if (compareAttemptedFlag) {
return comparisonResultFlag;
} else {
throw new QVCSOperationException("CompareFiles -- no results available; comparison not yet attempted!");
}
}
/**
* Get the ignore all white space flag.
* @return the ignore all white space flag.
*/
public boolean getIgnoreAllWhiteSpace() {
return ignoreAllWhitespaceFlag;
}
/**
* Set the ignore all white space flag.
* @param flag the ignore all white space flag.
*/
public void setIgnoreAllWhiteSpace(boolean flag) {
ignoreAllWhitespaceFlag = flag;
}
/**
* Get the ignore leading white space flag.
* @return the ignore leading white space flag.
*/
public boolean getIgnoreLeadingWhiteSpace() {
return ignoreLeadingWhitespaceFlag;
}
/**
* Set the ignore leading white space flag.
* @param flag the ignore leading white space flag.
*/
public void setIgnoreLeadingWhiteSpace(boolean flag) {
ignoreLeadingWhitespaceFlag = flag;
}
/**
* Get the ignore case flag.
* @return the ignore case flag.
*/
public boolean getIgnoreCaseFlag() {
return ignoreCaseFlag;
}
/**
* Set the ignore case flag.
* @param flag the ignore case flag.
*/
public void setIgnoreCaseFlag(boolean flag) {
ignoreCaseFlag = flag;
}
/**
* Get the ignore end-of-line changes flag.
* @return the ignore end-of-line changes flag.
*/
public boolean getIgnoreEOLChangesFlag() {
return ignoreEOLChangesFlag;
}
/**
* Set the ignore end-of-line changes flag.
* @param flag the ignore end-of-line changes flag.
*/
public void setIgnoreEOLChangesFlag(boolean flag) {
ignoreEOLChangesFlag = flag;
}
/**
* {@inheritDoc}
*/
@Override
public boolean execute(String[] arguments) throws QVCSOperationException {
this.args = arguments;
return execute();
}
/**
* {@inheritDoc}
*/
@Override
public boolean execute() throws QVCSOperationException {
boolean returnValue = true;
if (this.args.length < COMMAND_ARG_COUNT) {
throw new QVCSOperationException("Compare Files -- invalid arguments!");
}
// Make file objects for the files we've been asked to compare
inFileA = new File(args[0]);
inFileB = new File(args[1]);
outFile = new File(args[2]);
// Make sure we can read/write these as needed
if (!canReadWriteNecessaryFiles()) {
returnValue = false;
}
// Do the compare
if (returnValue) {
setCompareAttempted(true);
try {
returnValue = compareFiles();
} catch (DifferentiationFailedException | IOException | QVCSOperationException e) {
LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e));
outFile.delete();
returnValue = false;
}
}
return returnValue;
}
private boolean canReadWriteNecessaryFiles() {
boolean returnValue = true;
try {
if (!inFileA.canRead()
|| !inFileB.canRead()
|| (outFile.exists() && !outFile.canWrite())) {
returnValue = false;
}
} catch (SecurityException e) {
returnValue = false;
}
return returnValue;
}
private boolean compareFiles() throws DifferentiationFailedException, IOException, QVCSOperationException {
CompareLineInfo[] fileA = buildLinesFromFile(inFileA);
setFile1LineCount(fileA.length);
CompareLineInfo[] fileB = buildLinesFromFile(inFileB);
setFile2LineCount(fileB.length);
Revision apacheRevision = Diff.diff(fileA, fileB);
if (apacheRevision.size() == 0) {
// The files are identical.
comparisonResultFlag = true;
}
// Create the edit script that describes the changes we found.
writeEditScript(apacheRevision, fileA, fileB);
return true;
}
private CompareLineInfo[] buildLinesFromFile(File inFile) throws IOException {
List<CompareLineInfo> lineInfoList = new ArrayList<>();
RandomAccessFile randomAccessFile = new RandomAccessFile(inFile, "r");
long fileLength = randomAccessFile.length();
int startOfLineSeekPosition = 0;
int currentSeekPosition = 0;
byte character;
while (currentSeekPosition < fileLength) {
character = randomAccessFile.readByte();
currentSeekPosition = (int) randomAccessFile.getFilePointer();
if (character == '\n') {
int endOfLine = (int) randomAccessFile.getFilePointer();
byte[] buffer = new byte[endOfLine - startOfLineSeekPosition];
randomAccessFile.seek(startOfLineSeekPosition);
randomAccessFile.readFully(buffer);
String line = new String(buffer, UTF8);
CompareLineInfo lineInfo = new CompareLineInfo(startOfLineSeekPosition, createCompareLine(line));
lineInfoList.add(lineInfo);
startOfLineSeekPosition = endOfLine;
}
}
// Add the final line which can happen if it doesn't end in a newline.
if ((fileLength - startOfLineSeekPosition) > 0L) {
byte[] buffer = new byte[(int) fileLength - startOfLineSeekPosition];
randomAccessFile.seek(startOfLineSeekPosition);
randomAccessFile.readFully(buffer);
String line = new String(buffer, UTF8);
CompareLineInfo lineInfo = new CompareLineInfo(startOfLineSeekPosition, createCompareLine(line));
lineInfoList.add(lineInfo);
}
CompareLineInfo[] lineInfoArray = new CompareLineInfo[lineInfoList.size()];
int i = 0;
for (CompareLineInfo lineInfo : lineInfoList) {
lineInfoArray[i++] = lineInfo;
}
return lineInfoArray;
}
protected void writeEditScript(Revision apacheRevision, CompareLineInfo[] fileA, CompareLineInfo[] fileB) throws QVCSOperationException {
DataOutputStream outStream = null;
try {
outStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(outFile)));
// Write the header
CompareFilesEditHeader editHeader = new CompareFilesEditHeader();
editHeader.setBaseFileSize(new Common32Long((int) inFileA.length()));
editHeader.setTimeOfTarget(new CommonTime());
editHeader.write(outStream);
int count = apacheRevision.size();
for (int index = 0; index < count; index++) {
Delta delta = apacheRevision.getDelta(index);
formatEditScript(delta, outStream, fileA);
}
} catch (IOException e) {
throw new QVCSOperationException("IO Exception in writeEditScript() " + e.getLocalizedMessage());
} finally {
if (outStream != null) {
try {
outStream.close();
} catch (IOException ex) {
Logger.getLogger(CompareFilesWithApacheDiff.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
private void formatEditScript(Delta delta, DataOutputStream outStream, CompareLineInfo[] fileA) throws QVCSOperationException {
try {
short editType;
int seekPosition = -1;
int deletedByteCount = 0;
int insertedByteCount = 0;
byte[] secondFileByteBuffer = null;
if (delta instanceof ChangeDelta) {
CompareLineInfo originalStartingLine = fileA[delta.getOriginal().anchor()];
seekPosition = originalStartingLine.getLineSeekPosition();
editType = CompareFilesEditInformation.QVCS_EDIT_REPLACE;
deletedByteCount = computeDeletedByteCount(delta);
insertedByteCount = computeInsertedByteCount(delta);
secondFileByteBuffer = computeSecondFileByteBufferForInsert(delta, insertedByteCount);
} else if (delta instanceof AddDelta) {
int anchor = delta.getOriginal().anchor();
if (anchor == 0) {
CompareLineInfo originalStartingLine = fileA[delta.getOriginal().anchor()];
seekPosition = originalStartingLine.getLineSeekPosition();
} else {
CompareLineInfo originalStartingLine = fileA[delta.getOriginal().anchor() - 1];
byte[] lineAsByteArray = originalStartingLine.getLineString().getBytes(UTF8);
seekPosition = originalStartingLine.getLineSeekPosition() + lineAsByteArray.length;
}
editType = CompareFilesEditInformation.QVCS_EDIT_INSERT;
insertedByteCount = computeInsertedByteCount(delta);
AddDelta insertDelta = (AddDelta) delta;
secondFileByteBuffer = computeSecondFileByteBufferForInsert(insertDelta, insertedByteCount);
} else if (delta instanceof DeleteDelta) {
CompareLineInfo originalStartingLine = fileA[delta.getOriginal().anchor()];
seekPosition = originalStartingLine.getLineSeekPosition();
editType = CompareFilesEditInformation.QVCS_EDIT_DELETE;
deletedByteCount = computeDeletedByteCount(delta);
} else {
throw new QVCSOperationException("Internal error -- invalid edit type");
}
CompareFilesEditInformation editInfo = new CompareFilesEditInformation(editType, seekPosition, deletedByteCount, insertedByteCount);
switch (editType) {
case CompareFilesEditInformation.QVCS_EDIT_DELETE:
editInfo.write(outStream);
break;
case CompareFilesEditInformation.QVCS_EDIT_INSERT:
case CompareFilesEditInformation.QVCS_EDIT_REPLACE:
editInfo.write(outStream);
outStream.write(secondFileByteBuffer, 0, insertedByteCount);
break;
default:
throw new QVCSOperationException("Internal error -- invalid edit type");
}
} catch (IOException e) {
throw new QVCSOperationException("IOException in formatEditScript() " + e.getLocalizedMessage());
}
}
private int computeDeletedByteCount(Delta delta) throws UnsupportedEncodingException {
// This should be the byte count of the original chunk.
@SuppressWarnings("unchecked")
List<CompareLineInfo> originalChunk = delta.getOriginal().chunk();
int seekPosition = originalChunk.get(0).getLineSeekPosition();
CompareLineInfo lastLine = originalChunk.get(originalChunk.size() - 1);
int lastLineSeekStart = lastLine.getLineSeekPosition();
byte[] lastLineAsByteArray = lastLine.getLineString().getBytes(UTF8);
int lastLineEnd = lastLineSeekStart + lastLineAsByteArray.length;
return lastLineEnd - seekPosition;
}
private int computeInsertedByteCount(Delta replaceDelta) throws UnsupportedEncodingException {
// This should be the byte count of the revised chunk.
@SuppressWarnings("unchecked")
List<CompareLineInfo> revisedChunk = replaceDelta.getRevised().chunk();
int seekPosition = revisedChunk.get(0).getLineSeekPosition();
CompareLineInfo lastLine = revisedChunk.get(revisedChunk.size() - 1);
int lastLineSeekStart = lastLine.getLineSeekPosition();
byte[] lastLineAsByteArray = lastLine.getLineString().getBytes(UTF8);
int lastLineEnd = lastLineSeekStart + lastLineAsByteArray.length;
return lastLineEnd - seekPosition;
}
private byte[] computeSecondFileByteBufferForInsert(Delta delta, int insertedByteCount) throws UnsupportedEncodingException {
byte[] insertedBytes = new byte[insertedByteCount];
@SuppressWarnings("unchecked")
List<CompareLineInfo> insertedChunk = delta.getRevised().chunk();
int insertionIndex = 0;
for (CompareLineInfo lineInfo : insertedChunk) {
byte[] chunkBytes = lineInfo.getLineString().getBytes(UTF8);
for (int i = 0; i < chunkBytes.length; i++) {
insertedBytes[insertionIndex++] = chunkBytes[i];
}
}
assert (insertionIndex == insertedByteCount);
if (insertionIndex != insertedByteCount) {
System.out.println("Oops");
throw new RuntimeException("Error in compare with apache.");
}
return insertedBytes;
}
/**
* Set the compare attempted flag.
* @param flag true if the compare has been attempted; false if not yet attempted.
*/
public void setCompareAttempted(boolean flag) {
compareAttemptedFlag = true;
}
private String createCompareLine(String line) {
String alteredLine = line;
if (getIgnoreCaseFlag()) {
alteredLine = line.toUpperCase();
}
if (getIgnoreEOLChangesFlag()) {
if (alteredLine.endsWith("\r\n")) {
alteredLine = alteredLine.substring(0, alteredLine.length() - 2);
} else if (alteredLine.endsWith("\n")) {
alteredLine = alteredLine.substring(0, alteredLine.length() - 1);
}
}
if (getIgnoreAllWhiteSpace()) {
String[] segments = alteredLine.split("\t| ");
StringBuilder stringBuilder = new StringBuilder();
for (String segment : segments) {
stringBuilder.append(segment);
}
alteredLine = stringBuilder.toString();
} else if (getIgnoreLeadingWhiteSpace()) {
alteredLine = alteredLine.trim();
}
return alteredLine;
}
}