edu.clemson.lph.civet.CVIFileController.java Source code

Java tutorial

Introduction

Here is the source code for edu.clemson.lph.civet.CVIFileController.java

Source

package edu.clemson.lph.civet;

/*
Copyright 2014 Michael K Martin
    
This file is part of Civet.
    
Civet is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
    
Civet is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
    
You should have received a copy of the Lesser GNU General Public License
along with Civet.  If not, see <http://www.gnu.org/licenses/>.
*/
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;

import org.apache.log4j.Logger;
import org.jpedal.PdfDecoder;

import com.itextpdf.text.DocumentException;
import com.itextpdf.text.pdf.PdfCopy;
import com.itextpdf.text.pdf.PdfImportedPage;
import com.itextpdf.text.pdf.PdfReader;

import edu.clemson.lph.civet.xml.StdeCviXml;
import edu.clemson.lph.dialogs.MessageDialog;
import edu.clemson.lph.pdfgen.PDFUtils;

/**
 * This class handles keeping track of Files and Pages open in the CivetEditDialog.
 * Big task because there are a number of invisible "modes" that must be tracked.
 * @author mmarti5
 *
 */
public class CVIFileController {
    public static final Logger logger = Logger.getLogger(Civet.class.getName());

    CivetEditDialog dlg;
    /** Data defining PDF and Index state **/
    /**name of current PDF file*/
    String currentFilePath = null;
    String currentFileName = null;
    private File currentFile = null;
    private File currentFiles[] = null;
    /** File and Page Numbers are 1 based.  PageNo used as is in pdfDecoder.decodePage()
     *  iFileNo is one more than the array index!!!  **/
    private int iFileNo = 1;
    private int iFiles = 0;
    private int iPageNo = 1;
    private int iPages = 0;
    ArrayList<Integer> aPagesInCurrent = new ArrayList<Integer>();
    private HashMap<String, ArrayList<Integer>> mPagesComplete = new HashMap<String, ArrayList<Integer>>();
    private HashMap<String, Integer> mFilePages = new HashMap<String, Integer>();
    private byte rawPdfBytes[];

    private File fLastSavedFile = null;
    private boolean bLastXFA = false;

    private StdeCviXml stdXml = null;

    public CVIFileController(CivetEditDialog parent) {
        dlg = parent;
    }

    public void setCurrentFiles(File files[], boolean bViewOnly) {
        currentFiles = files;
        if (currentFiles == null) {
            logger.error("setCurrentFiles called with null", new Exception("Civet Error"));
            return;
        }
        iFileNo = 1;
        iFiles = currentFiles.length;
        dlg.setFile(iFileNo);
        dlg.setFiles(iFiles);
        if (currentFiles != null && currentFiles.length > 0) {
            currentFile = currentFiles[0];
            currentFilePath = currentFile.getAbsolutePath();
            currentFileName = currentFile.getName();
            OpenFileThread thread = new OpenFileThread(dlg, currentFilePath);
            thread.setReadOnly(bViewOnly);
            thread.start();
        }
    }

    public void setCurrentPdfBytes(byte bytes[]) {
        rawPdfBytes = bytes;
    }

    public byte[] getCurrentPdfBytes() {
        return rawPdfBytes;
    }

    public File getCurrentFile() {
        return currentFile;
    }

    public String getCurrentFilePath() {
        return currentFilePath;
    }

    public String getCurrentFileName() {
        return currentFileName;
    }

    public int getCurrentFileNo() {
        return iFileNo;
    }

    public int getNumberOfFiles() {
        return currentFiles.length;
    }

    public int getCurrentPageNo() {
        return iPageNo;
    }

    public void setNumberOfPages(int iPages) {
        this.iPages = iPages;
    }

    public int getNumberOfPages() {
        return iPages;
    }

    /**
     * Get an array of complete files from the list of currentFiles
     * @return File[]
     */
    public File[] getCompleteFiles() {
        ArrayList<File> completeFiles = new ArrayList<File>();
        File aOut[] = new File[0];
        for (File fThis : currentFiles) {
            if (isFileComplete(fThis)) {
                completeFiles.add(fThis);
            }
        }
        return (File[]) completeFiles.toArray(aOut);
    }

    /**
     * Essentially answers the question whether every page in File fFile is 
     * included in the array of pages marked complete in the mPagesComplete map.
     * @param fFile
     * @return
     */
    public boolean isFileComplete(File fFile) {
        boolean bRet = true;
        String sFilePath = fFile.getAbsolutePath();
        Integer iPagesInFile = mFilePages.get(sFilePath);
        // NOT an error.  iPagesInFile is set when file opened.  If this is null, it means the file
        // was never opened so it can never be complete.
        if (iPagesInFile == null) {
            return false;
        }
        ArrayList<Integer> aPagesComplete = mPagesComplete.get(sFilePath);
        if (aPagesComplete == null) {
            return false;
        }
        for (int i = 1; i <= iPagesInFile; i++) {
            if (!aPagesComplete.contains(i)) {
                bRet = false;
                break;
            }
        }
        return bRet;
    }

    /**
     * Move to specified page in currentFile
     * @param iPage Both page and file numbers are 1 based indexes they are converted to zero based
     * as needed inside each method.
     */
    public void setPage(int iPage) {
        if (isPageable() && iPage > 0 && iPage <= iPages) {
            iPageNo = iPage;
            try {
                dlg.getPdfDecoder().decodePage(iPageNo);
                dlg.updatePdfDisplay();
                dlg.setupForm(currentFileName, iPageNo, iPages, iFileNo, iFiles, isPageComplete(iPageNo));
            } catch (Exception e1) {
                logger.error(e1.getMessage() + "\nError moving forward one page");
            }
        }
    }

    /**
     * Are there pages forward, complete or not
     * @return true of false
     */
    public boolean morePagesForward() {
        return (iPageNo < iPages);
    }

    /**
     * Are there pages backward, complete or not
     * @return true of false
     */
    public boolean morePagesBack() {
        return (iPageNo > 1);
    }

    /**
     * Are there files forward, complete or not
     * @return true of false
     */
    public boolean moreFilesForward() {
        return currentFiles != null && iFiles > 0 && iFileNo < iFiles;
    }

    /**
     * Are there files backward, complete or not
     * @return true of false
     */
    public boolean moreFilesBack() {
        return currentFiles != null && currentFiles.length > 0 && iFileNo > 1;
    }

    /**
     * Move backward one page
     */
    public void pageBack() {
        // First Try Paging Back
        if (iPageNo > 1) {
            iPageNo--;
            try {
                dlg.getPdfDecoder().decodePage(iPageNo);
                dlg.updatePdfDisplay();
            } catch (Exception e1) {
                logger.error(e1.getMessage() + "\nError moving back one page");
                iPageNo++;
                // Now what?  Another try?
            }
        } else if (iFileNo > 1) {
            iFileNo--;
            currentFile = currentFiles[iFileNo - 1];
            currentFilePath = currentFile.getAbsolutePath();
            currentFileName = currentFile.getName();
            // Would have a race condition if we didn't perform this set BEFORE starting thread
            iPageNo = iPages;
            OpenFileThread thread = new OpenFileThread(dlg, currentFilePath);
            thread.start();
        }
        dlg.setupForm(currentFileName, iPageNo, iPages, iFileNo, iFiles, isPageComplete(iPageNo));
    }

    /**
     * Move forward one page
     */
    public void pageForward() {
        if (iPageNo < iPages) {
            iPageNo++;
            try {
                dlg.getPdfDecoder().decodePage(iPageNo);
                dlg.updatePdfDisplay();
                dlg.setupForm(currentFileName, iPageNo, iPages, iFileNo, iFiles, isPageComplete(iPageNo));
            } catch (Exception e1) {
                logger.error(e1.getMessage() + "\nError moving forward one page");
                iPageNo--;
                // Now what?  Another try?
            }
        } else if (iFileNo < currentFiles.length - 1) {
            iFileNo++;
            currentFile = currentFiles[iFileNo - 1];
            currentFilePath = currentFile.getAbsolutePath();
            currentFileName = currentFile.getName();
            // Would have a race condition if we didn't perform this set BEFORE starting thread
            iPageNo = 1;
            OpenFileThread thread = new OpenFileThread(dlg, currentFile);
            thread.start();
        }
    }

    /**
     * Move back one file
     */
    public void fileBackward() {
        if (iFileNo > 1) {
            iFileNo--;
            currentFile = currentFiles[iFileNo - 1];
            currentFilePath = currentFile.getAbsolutePath();
            currentFileName = currentFile.getName();
            iPageNo = 1;
            OpenFileThread thread = new OpenFileThread(dlg, currentFile);
            thread.start();
        }
    }

    /**
     * Move forward one file
     */
    public void fileForward() {
        if (iFileNo < iFiles) {
            iFileNo++;
            currentFile = currentFiles[iFileNo - 1];
            currentFilePath = currentFile.getAbsolutePath();
            currentFileName = currentFile.getName();
            iPageNo = 1;
            OpenFileThread thread = new OpenFileThread(dlg, currentFilePath);
            thread.start();
        }
    }

    /**
     * Are there any pages ahead of this one either in this file or a later file
     * that have not yet been marked complete?
     * @return true or false
     */
    public boolean moreIncompleteForward() {
        int iNextPage = iPageNo + 1;
        File fThis;
        if (currentFiles == null)
            return false;
        for (int iFile = iFileNo; iFile <= iFiles; iFile++) {
            fThis = currentFiles[iFile - 1];
            ArrayList<Integer> aPagesComplete = mPagesComplete.get(fThis.getAbsolutePath());
            if (aPagesComplete == null)
                return true; // This file has no pages complete
            else {
                Integer iPagesInFile = mFilePages.get(fThis.getAbsolutePath());
                if (iPagesInFile == null)
                    return true; // File has not been read so not complete.
                for (int iPage = iNextPage; iPage <= iPagesInFile; iPage++) {
                    if (!aPagesComplete.contains(iPage))
                        return true;
                }
            }
            iNextPage = 1;
        }
        return false;
    }

    /**
     * Are there any pages behind of this one either in this file or a later file
     * that have not yet been marked complete?
     * @return true or false
     */
    public boolean moreIncompleteBack() {
        if (currentFiles == null)
            return false;
        File fThis;
        for (int iFile = iFileNo; iFile > 0; iFile--) {
            fThis = currentFiles[iFile - 1];
            ArrayList<Integer> aPagesComplete = mPagesComplete.get(fThis.getAbsolutePath());
            if (aPagesComplete == null)
                return true; // This file has no pages complete
            else {
                Integer iPagesInFile = mFilePages.get(fThis.getAbsolutePath());
                if (iPagesInFile == null)
                    return true; // File has not been read so not complete. Should not happen backwards.
                for (int iPage = iPagesInFile; iPage > 0; iPage--) {
                    if (!aPagesComplete.contains(iPage))
                        return true;
                }
            }
        }
        return false;
    }

    /**
     * Move to the last page that is not marked complete either in this file or a previous one.
     * This is only called if we had skipped a page somewhere and have hit the end so that
     * we need to move back to find incomplete pages.
     * Complexity here comes in needing to start at the END of previous files and move
     * backward looking for incomplete pages.  Would the user even care?  
     */
    public void moveToPreviousIncompletePage() {
        Integer iNextPage = iPageNo - 1; // Start with PREVIOUS page
        // For every file from this one back...
        for (int iThisFile = iFileNo; iThisFile > 0; iThisFile--) {
            // Get information on the current file
            ArrayList<Integer> aPagesComplete = mPagesComplete.get(currentFiles[iThisFile - 1].getAbsolutePath());
            Integer iPagesInFile = mFilePages.get(currentFiles[iThisFile - 1].getAbsolutePath());
            if (iPagesInFile == null) {
                logger.error(new Exception("Reached pageIncompleteBack (File with no page length recorded"));
                iPagesInFile = 1;
            }
            // Use the last page in the file unless we are on the current file
            if (iNextPage == null)
                iNextPage = iPagesInFile - 1;
            // For every page from the previous one back...
            for (int iThisPage = iNextPage; iThisPage >= 0; iThisPage--) {
                if (aPagesComplete == null || !aPagesComplete.contains(iThisPage)) {
                    // Found an unsaved page
                    iPageNo = iThisPage;
                    if (iFileNo != iThisFile) {
                        iFileNo = iThisFile;
                        currentFile = currentFiles[iFileNo - 1];
                        currentFilePath = currentFile.getAbsolutePath();
                        currentFileName = currentFile.getName();
                        OpenFileThread thread = new OpenFileThread(dlg, currentFile);
                        thread.start();
                        // Successfully tried to open file
                        return;
                    } else {
                        try {
                            dlg.getPdfDecoder().decodePage(iPageNo);
                            dlg.updatePdfDisplay();
                            dlg.setupForm(currentFileName, iPageNo, iPages, iFileNo, iFiles,
                                    isPageComplete(iPageNo));
                            // Successfully read page
                            return;
                        } catch (Exception e1) {
                            logger.error(e1.getMessage() + "\nError moving forward one pageIncomplete");
                        }
                    }
                }
            }
            iNextPage = null; // reset so we use the last
        }
        logger.error(new Exception("Called pageIncompleteBack() but found none"));
    }

    /**
     * Move to the next page that is not marked complete either in this file or a later one.
     * This is the most used movement, called after each page is saved.
     */
    void moveToNextIncompletePage() {
        Integer iNextPage = iPageNo + 1; // Start with NEXT page
        // For every file from this one back...
        for (int iThisFile = iFileNo; iThisFile <= currentFiles.length; iThisFile++) {
            // Get information on the current file
            ArrayList<Integer> aPagesComplete = mPagesComplete.get(currentFiles[iThisFile - 1].getAbsolutePath());
            Integer iPagesInFile = mFilePages.get(currentFiles[iThisFile - 1].getAbsolutePath());
            if (iPagesInFile == null) {
                // Not an error just the first time we've touched this file so won't need to look past first.
                // just don't trust this count for anything beyond picking the page.
                iPagesInFile = 1;
            }
            // Use the first page in the file unless we are on the current file
            if (iNextPage == null)
                iNextPage = 1;
            // For every page from the next one on ...
            for (int iThisPage = iNextPage; iThisPage <= iPagesInFile; iThisPage++) {
                if (aPagesComplete == null || !aPagesComplete.contains(iThisPage)) {
                    // Found an unsaved page
                    iPageNo = iThisPage;
                    if (iFileNo != iThisFile) {
                        iFileNo = iThisFile;
                        currentFile = currentFiles[iFileNo - 1];
                        currentFilePath = currentFile.getAbsolutePath();
                        currentFileName = currentFile.getName();
                        OpenFileThread thread = new OpenFileThread(dlg, currentFile);
                        thread.start();
                        // Successfully tried to open file
                        return;
                    } else {
                        try {
                            dlg.getPdfDecoder().decodePage(iPageNo);
                            dlg.updatePdfDisplay();
                            dlg.setupForm(currentFileName, iPageNo, iPages, iFileNo, iFiles,
                                    isPageComplete(iPageNo));
                            // Successfully read page
                            return;
                        } catch (Exception e1) {
                            logger.error(e1.getMessage() + "\nError moving forward one pageIncomplete");
                        }
                    }
                }
            }
            iNextPage = null; // reset so we use the last
        }
        logger.error(new Exception("Called pageIncompleteForward() but found none"));
    }

    /**
     * Used to setup appropriate buttons, etc., 
     * This should be the logic that says whether we should even attempt to cut a file into pages
     * or navigated page to page.
     * This combined with !isXFADocument() tells if the file can be cut into multiple CVIs.
     * @return true of false.
     */
    public boolean isPageable() {
        return dlg.getPdfDecoder() != null && dlg.getPdfDecoder().isOpen()
                && currentFilePath.toLowerCase().endsWith(".pdf");
    }

    /**
     * Used internally to check specific page agains mPagesComplete
     * @param iPageNumber Both page and file numbers are 1 based indexes they are converted to zero based
     * as needed inside each method.
     * @return
     */
    private boolean isPageComplete(Integer iPageNumber) {
        ArrayList<Integer> aPagesComplete = mPagesComplete.get(currentFilePath);
        if (aPagesComplete == null)
            return false;
        else
            return aPagesComplete.contains(iPageNumber);
    }

    /**
     * Set the specified page complete.
     * @param iPageNumber Both page and file numbers are 1 based indexes they are converted to zero based
     * as needed inside each method.
     */
    public void setPageComplete(Integer iPageNumber) {
        ArrayList<Integer> aPagesComplete = mPagesComplete.get(currentFilePath);
        if (aPagesComplete == null) {
            logger.error("null aPagesComplete in setPageComplete for file (" + currentFilePath
                    + ") Should have been set in setupFile", new Exception("Null aPagesComplete"));
            aPagesComplete = new ArrayList<Integer>();
            mPagesComplete.put(currentFilePath, aPagesComplete);
        }
        if (!aPagesComplete.contains(iPageNumber)) {
            aPagesComplete.add(iPageNumber);
        }
    }

    /**
     * Add the current page to the current CVI
     */
    public void addCurrentPage() {
        if (!aPagesInCurrent.contains(iPageNo))
            aPagesInCurrent.add(iPageNo);
    }

    /**
     * Clear the current pages list
     */
    public void clearCurrentPages() {
        aPagesInCurrent.clear();
    }

    /**
     * @return if this is an XFA PDF and we can read it with JPedalXFA, return true, else false.
     */
    public boolean isXFADocument() {
        boolean bRet = false;
        if (currentFileName.toLowerCase().endsWith(".pdf")) {
            try {
                bRet = PDFUtils.isXFA(rawPdfBytes);
            } catch (Exception e) {
                bRet = false;
            }
        }
        return bRet;
    }

    /**
     * Get the appropriate byte array to include in standard XML.  This will be read from file for Images or XFAPDF files
     * and extracted pages from other PDF files.
     * @return byte array or null if not available (why not?)
     */
    public byte[] getPDFBytesFromCurrent() {
        byte[] pdfBytes = null;
        if (isPageable() && !isXFADocument()) {
            pdfBytes = extractPagesToNewPDF();
        } else {
            long len = currentFile.length();
            FileInputStream r;
            try {
                r = new FileInputStream(currentFile);
                pdfBytes = new byte[(int) len];
                int iRead = r.read(pdfBytes);
                r.close();
                if (iRead != len) {
                    throw new IOException("Array length " + iRead + " does not match file length " + len);
                }
            } catch (IOException e) {
                MessageDialog.showMessage(dlg, "Civet: Error",
                        "Error Reading File " + currentFile.getAbsolutePath());
            }
        }
        return pdfBytes;
    }

    /**
     * From the current pdfDecoder, extract the page(s) in aPagesInCurrent to a new pdfData buffer as output stream.
     * @param aPages int[]
     * @return byte[]
     */
    public byte[] extractPagesToNewPDF() {
        ByteArrayOutputStream baOut = new ByteArrayOutputStream();
        try {
            byte[] pdfDataIn = rawPdfBytes;
            PdfReader reader = new PdfReader(pdfDataIn);
            com.itextpdf.text.Document document = new com.itextpdf.text.Document();
            PdfCopy writer = new PdfCopy(document, baOut);
            document.open();
            for (Integer iPage : aPagesInCurrent) {
                PdfImportedPage pip = writer.getImportedPage(reader, iPage.intValue());
                writer.addPage(pip);
            }
            document.close();
        } catch (IOException ioe) {
            logger.info("IO error extracting pages to byte array", ioe);
            return rawPdfBytes;
        } catch (DocumentException de) {
            logger.info(de.getMessage() + "\nDocument error extracting pages to byte array");
            return rawPdfBytes;
        }
        return baOut.toByteArray();
    }// End decode pages to new PDF

    /**
     * Set all pages in the list of current pages as complete.
     */
    public void setCurrentPagesComplete() {
        if (isXFADocument()) {
            bLastXFA = true;
            Integer iPagesInFile = mFilePages.get(currentFilePath);
            if (iPagesInFile == null) {
                logger.error("Null iPagesInFile in setCurrentPagesComplete(" + currentFileName + ")",
                        new Exception("Null iPages"));
                return;
            }
            for (int i = 1; i <= iPagesInFile; i++)
                setPageComplete(i);
        } else {
            for (Integer iPage : aPagesInCurrent)
                setPageComplete(iPage);
        }
    }

    /**
     * Callback from open file thread.
     */
    public void setupFile() {
        PdfDecoder pdfDecoder = dlg.getPdfDecoder();
        try {
            pdfDecoder.decodePage(iPageNo);
            pdfDecoder.setPageParameters(dlg.getScale(), iPageNo, dlg.getRotation()); //values scaling (1=100%). page number
            int iPagesInFile = pdfDecoder.getPageCount();
            setNumberOfPages(iPagesInFile);
            // Pages in this file
            mFilePages.put(currentFilePath, iPagesInFile);
            // None of them complete unless reopened. But every open file will have a valid array object
            // in mPagesComplete keyed by its file path.
            ArrayList<Integer> aPagesComplete = mPagesComplete.get(currentFilePath);
            if (aPagesComplete == null)
                mPagesComplete.put(currentFilePath, new ArrayList<Integer>());
            aPagesInCurrent.clear();
            if (isXFADocument()) {
                dlg.setRotation(0); // Always rotate 180 (actually 0) since we know they are right that way
                dlg.populateFromPDF();
            } else {
                dlg.setRotation(CivetConfig.getRotation());
            }
            dlg.setupForm(currentFileName, iPageNo, iPagesInFile, iFileNo, iFiles, isPageComplete(iPageNo));
            dlg.setVisible(true);
            dlg.updatePdfDisplay();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            logger.error(e);
        }
    }

    /**
     * One of a series of methods that allow us to revisit the last file saved 
     * These are used to add pages, edit last, etc.
     * @param fLastSavedFile
     */
    public void setLastSavedFile(File fLastSavedFile) {
        this.fLastSavedFile = fLastSavedFile;
    }

    /**
     * One of a series of methods that allow us to revisit the last file saved 
     * These are used to add pages, edit last, etc.
     * @return File we last saved
     */
    public File getLastSavedFile() {
        return fLastSavedFile;
    }

    /**
     * One of a series of methods that allow us to revisit the last file saved 
     * These are used to add pages, edit last, etc.
     * Don't offer to add pages or edit last if there is none!
     * @return true or false
     */
    public boolean hasLastSaved() {
        return (fLastSavedFile != null);
    }

    /**
     * One of a series of methods that allow us to revisit the last file saved 
     * These are used to add pages, edit last, etc.
     * We need to reopen XFA documents differently than others.
     * @return true or false
     */
    public boolean isLastSavedXFA() {
        return bLastXFA;
    }

    /**
     * Assign an existing StdeCviXml object to the current file.
     * Used when opening an existing Civet "binary" file.
     * This needs to be much more robust to ensure consistent state even with 
     * null currentFile, currentFiles[], etc.
     * Never called except by CivetEditDialog.openStdXml() which is never called.
     * @param xStd
     */
    public void setStdXml(StdeCviXml xStd) {
        this.stdXml = xStd;
    }

    /**
     * Get the StdeCviXml object for the current file.
     * This is a key method used by save to extract the original attachment for resaving.
     * @return
     */
    public StdeCviXml getStdXml() {
        return stdXml;
    }

}// End class CVIFileController