/*
* Copyright 2010 Srikanth Reddy Lingala  
* 
* 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 net.lingala.zip4j.core;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.exception.ZipExceptionConstants;
import net.lingala.zip4j.io.ZipInputStream;
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.model.UnzipParameters;
import net.lingala.zip4j.model.ZipModel;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.progress.ProgressMonitor;
import net.lingala.zip4j.unzip.Unzip;
import net.lingala.zip4j.util.ArchiveMaintainer;
import net.lingala.zip4j.util.InternalZipConstants;
import net.lingala.zip4j.util.Zip4jUtil;
import net.lingala.zip4j.zip.ZipEngine;

/**
 * Base class to handle zip files. Some of the operations supported
 * in this class are:<br>
 * <ul>
 * 		<li>Create Zip File</li>
 * 		<li>Add files to zip file</li>
 * 		<li>Add folder to zip file</li>
 * 		<li>Extract files from zip files</li>
 * 		<li>Remove files from zip file</li>
 * </ul>
 *
 */

public class ZipFile {
	
	private String file;
	private int mode;
	private ZipModel zipModel;
	private boolean isEncrypted;
	private ProgressMonitor progressMonitor;
	private boolean runInThread;
	private String fileNameCharset;
	
	/**
	 * Creates a new Zip File Object with the given zip file path.
	 * If the zip file does not exist, it is not created at this point. 
	 * @param zipFile
	 * @throws ZipException
	 */
	public ZipFile(String zipFile) throws ZipException {
		this(new File(zipFile));
	}
	
	/**
	 * Creates a new Zip File Object with the input file.
	 * If the zip file does not exist, it is not created at this point.
	 * @param zipFile
	 * @throws ZipException
	 */
	public ZipFile(File zipFile) throws ZipException {
		if (zipFile == null) {
			throw new ZipException("Input zip file parameter is not null", 
					ZipExceptionConstants.inputZipParamIsNull);
		}
		
		this.file = zipFile.getPath();
		this.mode = InternalZipConstants.MODE_UNZIP;
		this.progressMonitor = new ProgressMonitor();
		this.runInThread = false;
	}
	
	/**
	 * Creates a zip file and adds the source file to the zip file. If the zip file
	 * exists then this method throws an exception. Parameters such as compression type, etc
	 * can be set in the input parameters
	 * @param sourceFile - File to be added to the zip file
	 * @param parameters - parameters to create the zip file
	 * @throws ZipException
	 */
	public void createZipFile(File sourceFile, ZipParameters parameters) throws ZipException {
		ArrayList sourceFileList = new ArrayList();
		sourceFileList.add(sourceFile);
		createZipFile(sourceFileList, parameters, false, -1);
	}
	
	/**
	 * Creates a zip file and adds the source file to the zip file. If the zip file
	 * exists then this method throws an exception. Parameters such as compression type, etc
	 * can be set in the input parameters. While the method addFile/addFiles also creates the 
	 * zip file if it does not exist, the main functionality of this method is to create a split
	 * zip file. To create a split zip file, set the splitArchive parameter to true with a valid
	 * splitLength. Split Length has to be more than 65536 bytes
	 * @param sourceFile - File to be added to the zip file
	 * @param parameters - parameters to create the zip file
	 * @param splitArchive - if archive has to be split or not
	 * @param splitLength - if archive has to be split, then length in bytes at which it has to be split
	 * @throws ZipException
	 */
	public void createZipFile(File sourceFile, ZipParameters parameters, 
			boolean splitArchive, long splitLength) throws ZipException {
		
		ArrayList sourceFileList = new ArrayList();
		sourceFileList.add(sourceFile);
		createZipFile(sourceFileList, parameters, splitArchive, splitLength);
	}
	
	/**
	 * Creates a zip file and adds the list of source file(s) to the zip file. If the zip file
	 * exists then this method throws an exception. Parameters such as compression type, etc
	 * can be set in the input parameters
	 * @param sourceFileList - File to be added to the zip file
	 * @param parameters - parameters to create the zip file
	 * @throws ZipException
	 */
	public void createZipFile(ArrayList sourceFileList, 
			ZipParameters parameters) throws ZipException {
		createZipFile(sourceFileList, parameters, false, -1);
	}
	
	/**
	 * Creates a zip file and adds the list of source file(s) to the zip file. If the zip file
	 * exists then this method throws an exception. Parameters such as compression type, etc
	 * can be set in the input parameters. While the method addFile/addFiles also creates the 
	 * zip file if it does not exist, the main functionality of this method is to create a split
	 * zip file. To create a split zip file, set the splitArchive parameter to true with a valid
	 * splitLength. Split Length has to be more than 65536 bytes
	 * @param sourceFileList - File to be added to the zip file
	 * @param parameters - zip parameters for this file list
	 * @param splitArchive - if archive has to be split or not
	 * @param splitLength - if archive has to be split, then length in bytes at which it has to be split
	 * @throws ZipException
	 */
	public void createZipFile(ArrayList sourceFileList, ZipParameters parameters, 
			boolean splitArchive, long splitLength) throws ZipException {
		
		if (!Zip4jUtil.isStringNotNullAndNotEmpty(file)) {
			throw new ZipException("zip file path is empty");
		}
		
		if (Zip4jUtil.checkFileExists(file)) {
			throw new ZipException("zip file: " + file + " already exists. To add files to existing zip file use addFile method");
		}
		
		if (sourceFileList == null) {
			throw new ZipException("input file ArrayList is null, cannot create zip file");
		}
		
		if (!Zip4jUtil.checkArrayListTypes(sourceFileList, InternalZipConstants.LIST_TYPE_FILE)) {
			throw new ZipException("One or more elements in the input ArrayList is not of type File");
		}
		
		createNewZipModel();
		this.zipModel.setSplitArchive(splitArchive);
		this.zipModel.setSplitLength(splitLength);
		addFiles(sourceFileList, parameters);
	}
	
	/**
	 * Creates a zip file and adds the files/folders from the specified folder to the zip file.
	 * This method does the same functionality as in addFolder method except that this method
	 * can also create split zip files when adding a folder. To create a split zip file, set the 
	 * splitArchive parameter to true and specify the splitLength. Split length has to be more than
	 * or equal to 65536 bytes. Note that this method throws an exception if the zip file already 
	 * exists.
	 * @param folderToAdd
	 * @param parameters
	 * @param splitArchive
	 * @param splitLength
	 * @throws ZipException
	 */
	public void createZipFileFromFolder(String folderToAdd, ZipParameters parameters, 
			boolean splitArchive, long splitLength) throws ZipException {
		
		if (!Zip4jUtil.isStringNotNullAndNotEmpty(folderToAdd)) {
			throw new ZipException("folderToAdd is empty or null, cannot create Zip File from folder");
		}
		
		createZipFileFromFolder(new File(folderToAdd), parameters, splitArchive, splitLength);
		
	}
	
	/**
	 * Creates a zip file and adds the files/folders from the specified folder to the zip file.
	 * This method does the same functionality as in addFolder method except that this method
	 * can also create split zip files when adding a folder. To create a split zip file, set the 
	 * splitArchive parameter to true and specify the splitLength. Split length has to be more than
	 * or equal to 65536 bytes. Note that this method throws an exception if the zip file already 
	 * exists.
	 * @param folderToAdd
	 * @param parameters
	 * @param splitArchive
	 * @param splitLength
	 * @throws ZipException
	 */
	public void createZipFileFromFolder(File folderToAdd, ZipParameters parameters, 
			boolean splitArchive, long splitLength) throws ZipException {
		
		if (folderToAdd == null) {
			throw new ZipException("folderToAdd is null, cannot create zip file from folder");
		}
		
		if (parameters == null) {
			throw new ZipException("input parameters are null, cannot create zip file from folder");
		}
		
		if (Zip4jUtil.checkFileExists(file)) {
			throw new ZipException("zip file: " + file + " already exists. To add files to existing zip file use addFolder method");
		}
		
		createNewZipModel();
		this.zipModel.setSplitArchive(splitArchive);
		if (splitArchive)
			this.zipModel.setSplitLength(splitLength);
		
		addFolder(folderToAdd, parameters, false);
	}
	
	/**
	 * Adds input source file to the zip file. If zip file does not exist, then 
	 * this method creates a new zip file. Parameters such as compression type, etc
	 * can be set in the input parameters.
	 * @param sourceFile - File to tbe added to the zip file
	 * @param parameters - zip parameters for this file
	 * @throws ZipException
	 */
	public void addFile(File sourceFile, ZipParameters parameters) throws ZipException {
		ArrayList sourceFileList = new ArrayList();
		sourceFileList.add(sourceFile);
		addFiles(sourceFileList, parameters);
	}
	
	/**
	 * Adds the list of input files to the zip file. If zip file does not exist, then 
	 * this method creates a new zip file. Parameters such as compression type, etc
	 * can be set in the input parameters.
	 * @param sourceFileList
	 * @param parameters
	 * @throws ZipException
	 */
	public void addFiles(ArrayList sourceFileList, ZipParameters parameters) throws ZipException {
		
		checkZipModel();
		
		if (this.zipModel == null) {
			throw new ZipException("internal error: zip model is null");
		}
		
		if (sourceFileList == null) {
			throw new ZipException("input file ArrayList is null, cannot add files");
		}
		
		if (!Zip4jUtil.checkArrayListTypes(sourceFileList, InternalZipConstants.LIST_TYPE_FILE)) {
			throw new ZipException("One or more elements in the input ArrayList is not of type File");
		}
		
		if (parameters == null) {
			throw new ZipException("input parameters are null, cannot add files to zip");
		}
		
		if (progressMonitor.getState() == ProgressMonitor.STATE_BUSY) {
			throw new ZipException("invalid operation - Zip4j is in busy state");
		}
		
		if (Zip4jUtil.checkFileExists(file)) {
			if (zipModel.isSplitArchive()) {
				throw new ZipException("Zip file already exists. Zip file format does not allow updating split/spanned files");
			}
		}
		
		ZipEngine zipEngine = new ZipEngine(zipModel);
		zipEngine.addFiles(sourceFileList, parameters, progressMonitor, runInThread);
	}
	
	/**
	 * Adds the folder in the given path to the zip file. If zip file does not exist, 
	 * then a new zip file is created. If input folder path is invalid then an exception
	 * is thrown. Zip parameters for the files in the folder to be added can be set in
	 * the input parameters
	 * @param path
	 * @param parameters
	 * @throws ZipException
	 */
	public void addFolder(String path, ZipParameters parameters) throws ZipException {
		if (!Zip4jUtil.isStringNotNullAndNotEmpty(path)) {
			throw new ZipException("input path is null or empty, cannot add folder to zip file");
		}
		
		addFolder(new File(path), parameters);
	}
	
	/**
	 * Adds the folder in the given file object to the zip file. If zip file does not exist, 
	 * then a new zip file is created. If input folder is invalid then an exception
	 * is thrown. Zip parameters for the files in the folder to be added can be set in
	 * the input parameters
	 * @param path
	 * @param parameters
	 * @throws ZipException
	 */
	public void addFolder(File path, ZipParameters parameters) throws ZipException {
		if (path == null) {
			throw new ZipException("input path is null, cannot add folder to zip file");
		}
		
		if (parameters == null) {
			throw new ZipException("input parameters are null, cannot add folder to zip file");
		}
		
		addFolder(path, parameters, true);
	}
	
	/**
	 * Internal method to add a folder to the zip file.
	 * @param path
	 * @param parameters
	 * @param checkSplitArchive
	 * @throws ZipException
	 */
	private void addFolder(File path, ZipParameters parameters, 
			boolean checkSplitArchive) throws ZipException {
		
		checkZipModel();
		
		if (this.zipModel == null) {
			throw new ZipException("internal error: zip model is null");
		}
		
		if (checkSplitArchive) {
			if (this.zipModel.isSplitArchive()) {
				throw new ZipException("This is a split archive. Zip file format does not allow updating split/spanned files");
			}
		}
		
		ZipEngine zipEngine = new ZipEngine(zipModel);
		zipEngine.addFolderToZip(path, parameters, progressMonitor, runInThread);
		
	}
	
	/**
	 * Creates a new entry in the zip file and adds the content of the inputstream to the
	 * zip file. ZipParameters.isSourceExternalStream and ZipParameters.fileNameInZip have to be
	 * set before in the input parameters. If the file name ends with / or \, this method treats the
	 * content as a directory. Setting the flag ProgressMonitor.setRunInThread to true will have
	 * no effect for this method and hence this method cannot be used to add content to zip in
	 * thread mode
	 * @param inputStream
	 * @param parameters
	 * @throws ZipException
	 */
	public void addStream(InputStream inputStream, ZipParameters parameters) throws ZipException {
		if (inputStream == null) {
			throw new ZipException("inputstream is null, cannot add file to zip");
		}
		
		if (parameters == null) {
			throw new ZipException("zip parameters are null");
		}
		
		this.setRunInThread(false);
		
		checkZipModel();
		
		if (this.zipModel == null) {
			throw new ZipException("internal error: zip model is null");
		}
		
		if (Zip4jUtil.checkFileExists(file)) {
			if (zipModel.isSplitArchive()) {
				throw new ZipException("Zip file already exists. Zip file format does not allow updating split/spanned files");
			}
		}
		
		ZipEngine zipEngine = new ZipEngine(zipModel);
		zipEngine.addStreamToZip(inputStream, parameters);
	}
	
	/**
	 * Reads the zip header information for this zip file. If the zip file
	 * does not exist, then this method throws an exception.<br><br>
	 * <b>Note:</b> This method does not read local file header information
	 * @throws ZipException
	 */
	private void readZipInfo() throws ZipException {
		
		if (!Zip4jUtil.checkFileExists(file)) {
			throw new ZipException("zip file does not exist");
		}
		
		if (!Zip4jUtil.checkFileReadAccess(this.file)) {
			throw new ZipException("no read access for the input zip file");
		}
		
		if (this.mode != InternalZipConstants.MODE_UNZIP) {
			throw new ZipException("Invalid mode");
		}
		
		RandomAccessFile raf = null;
		try {
			raf = new RandomAccessFile(new File(file), InternalZipConstants.READ_MODE);
			
			if (zipModel == null) {
				
				HeaderReader headerReader = new HeaderReader(raf);
				zipModel = headerReader.readAllHeaders(this.fileNameCharset);
				if (zipModel != null) {
					zipModel.setZipFile(file);
				}
			}
		} catch (FileNotFoundException e) {
			throw new ZipException(e);
		} finally {
			if (raf != null) {
				try {
					raf.close();
				} catch (IOException e) {
					//ignore
				}
			}
		}
	}
	
	/**
	 * Extracts all the files in the given zip file to the input destination path.
	 * If zip file does not exist or destination path is invalid then an 
	 * exception is thrown. 
	 * @param destPath
	 * @throws ZipException
	 */
	public void extractAll(String destPath) throws ZipException {
		extractAll(destPath, null);
		
	}
	
	/**
	 * Extracts all the files in the given zip file to the input destination path.
	 * If zip file does not exist or destination path is invalid then an 
	 * exception is thrown.
	 * @param destPath
	 * @param unzipParameters
	 * @throws ZipException
	 */
	public void extractAll(String destPath, 
			UnzipParameters unzipParameters) throws ZipException {
		
		if (!Zip4jUtil.isStringNotNullAndNotEmpty(destPath)) {
			throw new ZipException("output path is null or invalid");
		}
		
		if (!Zip4jUtil.checkOutputFolder(destPath)) {
			throw new ZipException("invalid output path");
		}
		
		if (zipModel == null) {
			readZipInfo();
		}
		
		// Throw an exception if zipModel is still null
		if (zipModel == null) {
			throw new ZipException("Internal error occurred when extracting zip file");
		}
		
		if (progressMonitor.getState() == ProgressMonitor.STATE_BUSY) {
			throw new ZipException("invalid operation - Zip4j is in busy state");
		}
		
		Unzip unzip = new Unzip(zipModel);
		unzip.extractAll(unzipParameters, destPath, progressMonitor, runInThread);
		
	}
	
	/**
	 * Extracts a specific file from the zip file to the destination path.
	 * If destination path is invalid, then this method throws an exception.
	 * @param fileHeader
	 * @param destPath
	 * @throws ZipException
	 */
	public void extractFile(FileHeader fileHeader, String destPath) throws ZipException {
		extractFile(fileHeader, destPath, null);
	}
	
	/**
	 * Extracts a specific file from the zip file to the destination path.
	 * If destination path is invalid, then this method throws an exception.
	 * <br><br>
	 * If newFileName is not null or empty, newly created file name will be replaced by 
	 * the value in newFileName. If this value is null, then the file name will be the 
	 * value in FileHeader.getFileName
	 * @param fileHeader
	 * @param destPath
	 * @param unzipParameters
	 * @throws ZipException
	 */
	public void extractFile(FileHeader fileHeader, 
			String destPath, UnzipParameters unzipParameters) throws ZipException {
		extractFile(fileHeader, destPath, unzipParameters, null);
	}
	
	/**
	 * Extracts a specific file from the zip file to the destination path.
	 * If destination path is invalid, then this method throws an exception.
	 * @param fileHeader
	 * @param destPath
	 * @param unzipParameters
	 * @param newFileName
	 * @throws ZipException
	 */
	public void extractFile(FileHeader fileHeader, String destPath, 
			UnzipParameters unzipParameters, String newFileName) throws ZipException {
		
		if (fileHeader == null) {
			throw new ZipException("input file header is null, cannot extract file");
		}
		
		if (!Zip4jUtil.isStringNotNullAndNotEmpty(destPath)) {
			throw new ZipException("destination path is empty or null, cannot extract file");
		}
		
		readZipInfo();
		
		if (progressMonitor.getState() == ProgressMonitor.STATE_BUSY) {
			throw new ZipException("invalid operation - Zip4j is in busy state");
		}
		
		fileHeader.extractFile(zipModel, destPath, unzipParameters, newFileName, progressMonitor, runInThread);
		
	}
	
	/**
	 * Extracts a specific file from the zip file to the destination path. 
	 * This method first finds the necessary file header from the input file name.
	 * <br><br>
	 * File name is relative file name in the zip file. For example if a zip file contains
	 * a file "a.txt", then to extract this file, input file name has to be "a.txt". Another
	 * example is if there is a file "b.txt" in a folder "abc" in the zip file, then the
	 * input file name has to be abc/b.txt
	 * <br><br>
	 * Throws an exception if file header could not be found for the given file name or if 
	 * the destination path is invalid
	 * @param fileName
	 * @param destPath
	 * @throws ZipException
	 */
	public void extractFile(String fileName, String destPath) throws ZipException {
		extractFile(fileName, destPath, null);
	}
	
	/**
	 * Extracts a specific file from the zip file to the destination path. 
	 * This method first finds the necessary file header from the input file name.
	 * <br><br>
	 * File name is relative file name in the zip file. For example if a zip file contains
	 * a file "a.txt", then to extract this file, input file name has to be "a.txt". Another
	 * example is if there is a file "b.txt" in a folder "abc" in the zip file, then the
	 * input file name has to be abc/b.txt
	 * <br><br>
	 * Throws an exception if file header could not be found for the given file name or if 
	 * the destination path is invalid
	 * @param fileName
	 * @param destPath
	 * @param unzipParameters
	 * @throws ZipException
	 */
	public void extractFile(String fileName, 
			String destPath, UnzipParameters unzipParameters) throws ZipException {
		extractFile(fileName, destPath, unzipParameters, null);
	}
	
	/**
	 * Extracts a specific file from the zip file to the destination path. 
	 * This method first finds the necessary file header from the input file name.
	 * <br><br>
	 * File name is relative file name in the zip file. For example if a zip file contains
	 * a file "a.txt", then to extract this file, input file name has to be "a.txt". Another
	 * example is if there is a file "b.txt" in a folder "abc" in the zip file, then the
	 * input file name has to be abc/b.txt
	 * <br><br>
	 * If newFileName is not null or empty, newly created file name will be replaced by 
	 * the value in newFileName. If this value is null, then the file name will be the 
	 * value in FileHeader.getFileName
	 * <br><br>
	 * Throws an exception if file header could not be found for the given file name or if 
	 * the destination path is invalid
	 * @param fileName
	 * @param destPath
	 * @param unzipParameters
	 * @param newFileName
	 * @throws ZipException
	 */
	public void extractFile(String fileName, String destPath, 
			UnzipParameters unzipParameters, String newFileName) throws ZipException {
		
		if (!Zip4jUtil.isStringNotNullAndNotEmpty(fileName)) {
			throw new ZipException("file to extract is null or empty, cannot extract file");
		}
		
		if (!Zip4jUtil.isStringNotNullAndNotEmpty(destPath)) {
			throw new ZipException("destination string path is empty or null, cannot extract file");
		}
		
		readZipInfo();
		
		FileHeader fileHeader = Zip4jUtil.getFileHeader(zipModel, fileName);
		
		if (fileHeader == null) {
			throw new ZipException("file header not found for given file name, cannot extract file");
		}
		
		if (progressMonitor.getState() == ProgressMonitor.STATE_BUSY) {
			throw new ZipException("invalid operation - Zip4j is in busy state");
		}
		
		fileHeader.extractFile(zipModel, destPath, unzipParameters, newFileName, progressMonitor, runInThread);
		
	}
	
	/**
	 * Sets the password for the zip file.<br>
	 * <b>Note</b>: For security reasons, usage of this method is discouraged. Use 
	 * setPassword(char[]) instead. As strings are immutable, they cannot be wiped
	 * out from memory explicitly after usage. Therefore, usage of Strings to store 
	 * passwords is discouraged. More info here: 
	 * http://docs.oracle.com/javase/1.5.0/docs/guide/security/jce/JCERefGuide.html#PBEEx
	 * @param password
	 * @throws ZipException
	 */
	public void setPassword(String password) throws ZipException {
		if (!Zip4jUtil.isStringNotNullAndNotEmpty(password)) {
			throw new NullPointerException();
		}
		setPassword(password.toCharArray());
	}
	
	/**
	 * Sets the password for the zip file
	 * @param password
	 * @throws ZipException
	 */
	public void setPassword(char[] password) throws ZipException {
		if (zipModel == null) {
			readZipInfo();
			if (zipModel == null) {
				throw new ZipException("Zip Model is null");
			}
		}
		
		if (zipModel.getCentralDirectory() == null || zipModel.getCentralDirectory().getFileHeaders() == null) {
			throw new ZipException("invalid zip file");
		}
		
		for (int i = 0; i < zipModel.getCentralDirectory().getFileHeaders().size(); i++) {
			if (zipModel.getCentralDirectory().getFileHeaders().get(i) != null) {
				if (((FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(i)).isEncrypted()) {
					((FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(i)).setPassword(password);
				}
			}
		}
	}
	
	/**
	 * Returns the list of file headers in the zip file. Throws an exception if the 
	 * zip file does not exist
	 * @return list of file headers
	 * @throws ZipException
	 */
	public List getFileHeaders() throws ZipException {
		readZipInfo();
		if (zipModel == null || zipModel.getCentralDirectory() == null) {
			return null;
		}
		return zipModel.getCentralDirectory().getFileHeaders();
	}
	
	/**
	 * Returns FileHeader if a file header with the given fileHeader 
	 * string exists in the zip model: If not returns null
	 * @param fileName
	 * @return FileHeader
	 * @throws ZipException
	 */
	public FileHeader getFileHeader(String fileName) throws ZipException {
		if (!Zip4jUtil.isStringNotNullAndNotEmpty(fileName)) {
			throw new ZipException("input file name is emtpy or null, cannot get FileHeader");
		}
		
		readZipInfo();
		if (zipModel == null || zipModel.getCentralDirectory() == null) {
			return null;
		}
		
		return Zip4jUtil.getFileHeader(zipModel, fileName);
	}
	
	/**
	 * Checks to see if the zip file is encrypted
	 * @return true if encrypted, false if not
	 * @throws ZipException
	 */
	public boolean isEncrypted() throws ZipException {
		if (zipModel == null) {
			readZipInfo();
			if (zipModel == null) {
				throw new ZipException("Zip Model is null");
			}
		}
		
		if (zipModel.getCentralDirectory() == null || zipModel.getCentralDirectory().getFileHeaders() == null) {
			throw new ZipException("invalid zip file");
		}
		
		ArrayList fileHeaderList = zipModel.getCentralDirectory().getFileHeaders();
		for (int i = 0; i < fileHeaderList.size(); i++) {
			FileHeader fileHeader = (FileHeader)fileHeaderList.get(i);
			if (fileHeader != null) {
				if (fileHeader.isEncrypted()) {
					isEncrypted = true;
					break;
				}
			}
		}
		
		return isEncrypted;
	}
	
	/**
	 * Checks if the zip file is a split archive
	 * @return true if split archive, false if not
	 * @throws ZipException
	 */
	public boolean isSplitArchive() throws ZipException {

		if (zipModel == null) {
			readZipInfo();
			if (zipModel == null) {
				throw new ZipException("Zip Model is null");
			}
		}
		
		return zipModel.isSplitArchive();
	
	}
	
	/**
	 * Removes the file provided in the input paramters from the zip file.
	 * This method first finds the file header and then removes the file.
	 * If file does not exist, then this method throws an exception.
	 * If zip file is a split zip file, then this method throws an exception as
	 * zip specification does not allow for updating split zip archives.
	 * @param fileName
	 * @throws ZipException
	 */
	public void removeFile(String fileName) throws ZipException {
		
		if (!Zip4jUtil.isStringNotNullAndNotEmpty(fileName)) {
			throw new ZipException("file name is empty or null, cannot remove file");
		}
		
		if (zipModel == null) {
			if (Zip4jUtil.checkFileExists(file)) {
				readZipInfo();
			}
		}
		
		if (zipModel.isSplitArchive()) {
			throw new ZipException("Zip file format does not allow updating split/spanned files");
		}
		
		FileHeader fileHeader = Zip4jUtil.getFileHeader(zipModel, fileName);
		if (fileHeader == null) {
			throw new ZipException("could not find file header for file: " + fileName);
		}
		
		removeFile(fileHeader);
	}
	
	/**
	 * Removes the file provided in the input file header from the zip file.
	 * If zip file is a split zip file, then this method throws an exception as
	 * zip specification does not allow for updating split zip archives.
	 * @param fileHeader
	 * @throws ZipException
	 */
	public void removeFile(FileHeader fileHeader) throws ZipException {
		if (fileHeader == null) {
			throw new ZipException("file header is null, cannot remove file");
		}
		
		if (zipModel == null) {
			if (Zip4jUtil.checkFileExists(file)) {
				readZipInfo();
			}
		}
		
		if (zipModel.isSplitArchive()) {
			throw new ZipException("Zip file format does not allow updating split/spanned files");
		}
		
		ArchiveMaintainer archiveMaintainer = new ArchiveMaintainer();
		archiveMaintainer.initProgressMonitorForRemoveOp(zipModel, fileHeader, progressMonitor);
		archiveMaintainer.removeZipFile(zipModel, fileHeader, progressMonitor, runInThread);
	}
	
	/**
	 * Merges split zip files into a single zip file without the need to extract the
	 * files in the archive
	 * @param outputZipFile
	 * @throws ZipException
	 */
	public void mergeSplitFiles(File outputZipFile) throws ZipException {
		if (outputZipFile == null) {
			throw new ZipException("outputZipFile is null, cannot merge split files");
		}
		
		if (outputZipFile.exists()) {
			throw new ZipException("output Zip File already exists");
		}
		
		checkZipModel();
		
		if (this.zipModel == null) {
			throw new ZipException("zip model is null, corrupt zip file?");
		}
		
		ArchiveMaintainer archiveMaintainer = new ArchiveMaintainer();
		archiveMaintainer.initProgressMonitorForMergeOp(zipModel, progressMonitor);
		archiveMaintainer.mergeSplitZipFiles(zipModel, outputZipFile, progressMonitor, runInThread);
	}
	
	/**
	 * Sets comment for the Zip file
	 * @param comment
	 * @throws ZipException
	 */
	public void setComment(String comment) throws ZipException {
		if (comment == null) {
			throw new ZipException("input comment is null, cannot update zip file");
		}
		
		if (!Zip4jUtil.checkFileExists(file)) {
			throw new ZipException("zip file does not exist, cannot set comment for zip file");
		}
		
		readZipInfo();
		
		if (this.zipModel == null) {
			throw new ZipException("zipModel is null, cannot update zip file");
		}
		
		if (zipModel.getEndCentralDirRecord() == null) {
			throw new ZipException("end of central directory is null, cannot set comment");
		}
		
		ArchiveMaintainer archiveMaintainer = new ArchiveMaintainer();
		archiveMaintainer.setComment(zipModel, comment);
	}
	
	/**
	 * Returns the comment set for the Zip file
	 * @return String
	 * @throws ZipException
	 */
	public String getComment() throws ZipException {
		return getComment(null);
	}
	
	/**
	 * Returns the comment set for the Zip file in the input encoding
	 * @param encoding
	 * @return String
	 * @throws ZipException
	 */
	public String getComment(String encoding) throws ZipException {
		if (encoding == null) {
			if (Zip4jUtil.isSupportedCharset(InternalZipConstants.CHARSET_COMMENTS_DEFAULT)) {
				encoding = InternalZipConstants.CHARSET_COMMENTS_DEFAULT;
			} else {
				encoding = InternalZipConstants.CHARSET_DEFAULT;
			}
		}
		
		if (Zip4jUtil.checkFileExists(file)) {
			checkZipModel();
		} else {
			throw new ZipException("zip file does not exist, cannot read comment");
		}
		
		if (this.zipModel == null) {
			throw new ZipException("zip model is null, cannot read comment");
		}
		
		if (this.zipModel.getEndCentralDirRecord() == null) {
			throw new ZipException("end of central directory record is null, cannot read comment");
		}
		
		if (this.zipModel.getEndCentralDirRecord().getCommentBytes() == null || 
				this.zipModel.getEndCentralDirRecord().getCommentBytes().length <= 0) {
			return null;
		}
		
		try {
			return new String(this.zipModel.getEndCentralDirRecord().getCommentBytes(), encoding);
		} catch (UnsupportedEncodingException e) {
			throw new ZipException(e);
		}
	}
	
	/**
	 * Loads the zip model if zip model is null and if zip file exists.
	 * @throws ZipException
	 */
	private void checkZipModel() throws ZipException {
		if (this.zipModel == null) {
			if (Zip4jUtil.checkFileExists(file)) {
				readZipInfo();
			} else {
				createNewZipModel();
			}
		}
	}
	
	/**
	 * Creates a new instance of zip model
	 * @throws ZipException
	 */
	private void createNewZipModel() {
		zipModel = new ZipModel();
		zipModel.setZipFile(file);
		zipModel.setFileNameCharset(fileNameCharset);
	}
	
	/**
	 * Zip4j will encode all the file names with the input charset. This method throws
	 * an exception if the Charset is not supported
	 * @param charsetName
	 * @throws ZipException
	 */
	public void setFileNameCharset(String charsetName) throws ZipException {
		if (!Zip4jUtil.isStringNotNullAndNotEmpty(charsetName)) {
			throw new ZipException("null or empty charset name");
		}
		
		if (!Zip4jUtil.isSupportedCharset(charsetName)) {
			throw new ZipException("unsupported charset: " + charsetName);
		}
		
		this.fileNameCharset = charsetName;
	}
	
	/**
	 * Returns an input stream for reading the contents of the Zip file corresponding
	 * to the input FileHeader. Throws an exception if the FileHeader does not exist
	 * in the ZipFile
	 * @param fileHeader
	 * @return ZipInputStream
	 * @throws ZipException
	 */
