com.qumasoft.qvcslib.CompareFilesWithApacheDiff.java Source code

Java tutorial

Introduction

Here is the source code for com.qumasoft.qvcslib.CompareFilesWithApacheDiff.java

Source

//   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;
    }
}