package jp.agentec.abook.abv.bl.download;

import net.lingala.zip4j.core.ZipFile;
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.unzip.UnzipUtil;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import jp.agentec.abook.abv.bl.acms.client.json.content.ContentJSON;
import jp.agentec.abook.abv.bl.common.ABVEnvironment;
import jp.agentec.abook.abv.bl.common.exception.ABVException;
import jp.agentec.abook.abv.bl.common.exception.ABVExceptionCode;
import jp.agentec.abook.abv.bl.common.log.Logger;
import jp.agentec.abook.abv.bl.common.util.ContentFileUtil;
import jp.agentec.abook.abv.bl.common.util.SecurityUtil;
import jp.agentec.abook.abv.bl.data.dao.AbstractDao;
import jp.agentec.abook.abv.bl.data.dao.ContentDao;
import jp.agentec.abook.abv.bl.data.dao.ContentMarkingDao;
import jp.agentec.abook.abv.bl.data.dao.ContentMemoDao;
import jp.agentec.abook.abv.bl.data.dao.ContentOldPdfDao;
import jp.agentec.abook.abv.bl.data.dao.ContentPageDao;
import jp.agentec.abook.abv.bl.data.dao.ContentResourceDao;
import jp.agentec.abook.abv.bl.dto.AbstractDto;
import jp.agentec.abook.abv.bl.dto.ContentDto;
import jp.agentec.abook.abv.bl.dto.ContentMemoDto;
import jp.agentec.abook.abv.bl.dto.ContentOldPdfDto;
import jp.agentec.abook.abv.bl.dto.ContentPageDto;
import jp.agentec.abook.abv.bl.dto.ContentResourceDto;
import jp.agentec.abook.abv.bl.logic.AbstractLogic;
import jp.agentec.abook.abv.bl.logic.ContentLogic;
import jp.agentec.adf.util.FileUtil;
import jp.agentec.adf.util.RuntimeUtil;
import jp.agentec.adf.util.StringUtil;

/**
 * コンテンツファイルの解凍を主に扱うクラス
 * (ContentDownloaderから分離）
 * 
 */
public class ContentFileExtractor {
	private static final String TAG = "ContentFileExtractor";

    private static final String COMPLETE_FILE = "complete.file";
	public static final String GUIDE_PDF_FILE_NAME = "ABookCheckManual.pdf";
	private static ContentFileExtractor instance;
	
	private ContentDao contentDao = AbstractDao.getDao(ContentDao.class);
	private ContentResourceDao contentResourceDao = AbstractDao.getDao(ContentResourceDao.class);
	private ContentOldPdfDao contentOldPdfDao = AbstractDao.getDao(ContentOldPdfDao.class);

	public static ContentFileExtractor getInstance() {
		if (instance == null) {
			synchronized (ContentFileExtractor.class) {
				if (instance == null) {
					instance = new ContentFileExtractor();
				}
			}
		}
		
		return instance;
	}
	
	private ContentFileExtractor() {
	}

	public void initializeContent(long contentId, String resourceZipPath, String imageZipPath, ContentDto contentDto) throws Exception {
		Logger.v(TAG, "initializeContent contentId=%s resourceZipPath=%s imageZipPath=%s", contentId, resourceZipPath, imageZipPath);
		String contentPath = ABVEnvironment.getInstance().getContentResourcesDirectoryPath(contentDto.contentId, false);
		File contentDir = new File(contentPath);

		FileUtil.delete(ABVEnvironment.getInstance().getContentCacheDirectory(contentId));
		FileUtil.delete(contentPath);

		//	resourceの解凍
		extractZipFile(contentId, resourceZipPath, contentPath);
		extractResourceZipFile(contentId, contentPath, contentDir);
		doContentResourceFile(contentId, contentDir);

		String cacheDir = getContentCacheDirWithExtract(contentId);
		Logger.i(TAG, "extract content files to cache dir. contentId=%s, cacheDir=%s", contentId, cacheDir);

	}

	private void extractResourceZipFile(long contentId, String contentPath, File contentDir) throws ZipException, NoSuchAlgorithmException, IOException, ABVException {
		//  3d解凍
		File content3DFile = new File(ABVEnvironment.getInstance().getContent3DzipName(contentPath));
		//存在チェック
		if (content3DFile.exists()) {
			extractZipFile(contentId, ABVEnvironment.getInstance().getContent3DzipName(contentPath), contentPath); // 3dディレクトリは作らない
		}
		FilenameFilter fileFilter = new FilenameFilter() {
			@Override
			public boolean accept(File dir, String name) {
				return name.endsWith(".zip");
			}
		};

		File panoImageFile = new File(ABVEnvironment.getInstance().getPanoImageName(contentPath));
		//存在チェック
		if (panoImageFile.exists()) {
			// panoImage.zipを解凍して、中身のzipを確認する
			String panoImageDirPath = ABVEnvironment.getInstance().getPanoImageDirName(contentPath);
			extractZipFile(contentId, panoImageFile.getPath(), panoImageDirPath);
			File panoImageDir = new File(panoImageDirPath);

			File[] zipFiles = panoImageDir.listFiles(fileFilter);
			if (zipFiles != null) {
				for (File zipFile : zipFiles) {
					String folderPath = zipFile.getPath().replace(".zip", "");
					extractZipFile(contentId, zipFile.getPath(), folderPath);
				}
			}
		}

		//PDFプロジェクト
		File pdfImageFile = new File(ABVEnvironment.getInstance().getTaskPdfFileName(contentPath));
		//存在チェック
		if (pdfImageFile.exists()) {
			// panoImage.zipを解凍して、中身のzipを確認する
			String taskPdfDirPath = ABVEnvironment.getInstance().getTaskPdfDirName(contentPath);
			extractZipFile(contentId, pdfImageFile.getPath(), taskPdfDirPath);
			File panoImageDir = new File(taskPdfDirPath);

			File[] zipFiles = panoImageDir.listFiles(fileFilter);
			if (zipFiles != null) {
				for (File zipFile : zipFiles) {
					String folderPath = zipFile.getPath().replace(".zip", "");
					extractZipFile(contentId, zipFile.getPath(), folderPath);
				}
			}
		}

		File taskListFile = new File(ABVEnvironment.getInstance().getTaskListName(contentPath));
		//存在チェック
		if (taskListFile.exists()) {
			// taskList.zipを解凍して、中身のzipを確認する
			String taskListDirPath = ABVEnvironment.getInstance().getTaskListDirName(contentPath);
			extractZipFile(contentId, taskListFile.getPath(), taskListDirPath);
			File taskListDir = new File(taskListDirPath);

			File[] zipFiles = taskListDir.listFiles(fileFilter);
			if (zipFiles != null) {
				for (File zipFile : zipFiles) {
					String folderPath = zipFile.getPath().replace(".zip", "");
					extractZipFile(contentId, zipFile.getPath(), folderPath);
				}
			}
		}

		
		// それ以外のzip
		ContentDto contentDto = contentDao.getContent(contentId);
        if (contentDto == null || contentDto.contentType == null) {
            Logger.w(TAG, "No Data ContentDto or contentType:" + contentId);
            return;
        }
		if (!ContentJSON.KEY_OTHER_TYPE.equals(contentDto.contentType.toLowerCase(Locale.US))) { // その他タイプの場合無視

			//html 또는 앙케이트의 Zip파일을 압축해제한다.
			File[] zipFiles = contentDir.listFiles(fileFilter);
			if (zipFiles!=null) {
				for (int i=0; i<zipFiles.length; i++) {
					String folderPath = contentDir.getPath()+"/"+zipFiles[i].getName();
					folderPath = folderPath.replace(".zip", "");

					extractZipFile(contentId, zipFiles[i].getPath(), folderPath);
					if (zipFiles[i].getName().endsWith("ENQUETE.zip")) {
						postToGet(folderPath);
					}
				}
			}
		}
	}

	private void postToGet(String path) throws IOException{
		Logger.d(TAG, "HTML 폴더 패스 : %s", path);
		String directoryPath = path;
		String htmlPath = directoryPath + "/index.html";
		String htmlsPath = directoryPath + "/index_s.html";
		
        //HTMl 불러와서 post -> get으로 변경
		File htmlFile = new File(htmlPath);
		File htmlsFile = new File(htmlsPath);
		if(htmlFile.exists()){
			postToGetFile(htmlPath, htmlFile);
		}
		if(htmlsFile.exists()){
			postToGetFile(htmlsPath, htmlsFile);
		}
	}
	
	private void postToGetFile(String htmlPath, File htmlFile) throws IOException {
		FileInputStream fis;
		BufferedReader br;
		BufferedInputStream bis = null;
		BufferedOutputStream bos = null;
        
		try {
			fis = new FileInputStream(htmlFile);
			br = new BufferedReader(new InputStreamReader(fis));
			
			StringBuilder sb = new StringBuilder();
			
			int bufferSize = 1024*1024;
			char readBuf[]= new char[bufferSize];
			int resultSize;
			
			while ((resultSize=br.read(readBuf)) != -1) {
				if (resultSize==bufferSize) {
					sb.append(readBuf);
				}else{
					for (int i=0; i<resultSize; i++) {
						sb.append(readBuf[i]);
					}
				}
			}
			
			br.close();

	        String viewUrl = new String(sb.toString());
	        viewUrl = viewUrl.replace("method=\"post\"", "method=\"get\"");
	        
			bis = new BufferedInputStream(new ByteArrayInputStream(viewUrl.toString().getBytes("UTF-8")), 1024);
			bos = new BufferedOutputStream(new FileOutputStream(htmlPath));
			
			int len;
			byte[] buf = new byte[1024];
			while ((len=bis.read(buf,0,1024))!=-1) {
				bos.write(buf,0,len);
			}
			
			bos.flush();
		} 
		finally {
			if (bis != null) {
				try { bis.close(); } catch (IOException e) { }
			}
			if (bos != null) {
				try { bos.close(); } catch (IOException e) { }
			}
		}
	}

	private void doContentResourceFile(long contentId, File contentDir) throws IOException {
		//	ページのテキストを保存
		saveContentPage(contentId, contentDir);
		
		ContentResourceDao resourceDao = AbstractDao.getDao(ContentResourceDao.class);
		
		try {
			resourceDao.beginTransaction();
			
			List<AbstractDto> resourceList = new ArrayList<AbstractDto>();
			if (ABVEnvironment.getInstance().isContentProtected) {
				// 暗号化オプションがONの場合、暗号化する。（現在、パッケージ版iPadの最新バージョンこのオプションに対応していない。よって、Android版も今後対応とし、常に暗号化することにする）
				String encryptKey = ABVEnvironment.getInstance().encryptKey;
				if (StringUtil.isNullOrWhiteSpace(encryptKey)) {
					throw new IllegalArgumentException("encryptKey is null or empty.");
				}
				encryptContentResource(contentId, contentDir, contentDir, encryptKey, resourceList);
				//	リソース情報を保存
				resourceDao.deleteContentResources(contentId);
				resourceDao.insertContentResource(contentId, resourceList);
			}
			else {
				registerContentResource(contentId, contentDir, contentDir, resourceList);
			}
			
			resourceDao.commit();
		} catch (Exception e) {
			Logger.e(TAG, "insert or update contentResources failed.", e);
			resourceDao.rollback();
			//	暗号化に失敗するとダウンロードしたファイルを削除する。
			FileUtil.delete(contentDir);
			throw new IOException(e);
		}
	}
	
	public void doContentResource(long contentId, String contentResourcePath) throws ZipException, NoSuchAlgorithmException, IOException, ABVException {
		// 指定ファイル以外を移動
        File[] files = new File(contentResourcePath).getParentFile().listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isFile()) {
                    if (!file.getName().equals("json.txt") && !file.getName().contains("__THM") && !file.getName().contains("__THUMBIG")) {
                        file.renameTo(new File(contentResourcePath + "/" + file.getName()));
                    }
                }
            }
        }
		
		File contentDir = new File(contentResourcePath);
		extractResourceZipFile(contentId, contentResourcePath, contentDir);
		doContentResourceFile(contentId, contentDir);
	}

	private void registerContentResource(long contentId, File f, File resourceRootDir, List<AbstractDto> resourceList) {
		if (f.isDirectory()) {
            File[] files = f.listFiles();
            if (files != null) {
                for (File file : files) {
                    registerContentResource(contentId, file, resourceRootDir, resourceList);
                }
            }
		} else {
			ContentResourceDto dto = new ContentResourceDto();
			dto.contentId = contentId;
			dto.resourceFileName = f.getName();
			dto.subDirectory = f.getParent().replace(resourceRootDir.getPath(), StringUtil.Empty);
			resourceList.add(dto);
		}
	}

	private void encryptContentResource(long contentId, File resourceRootDir, File f, String keyString, List<AbstractDto> list) throws IOException {
		if (f.isDirectory()) {
            File[] files = f.listFiles();
            if (files != null) {
                for (File file : files) {
                    encryptContentResource(contentId, resourceRootDir, file, keyString, list);
                }
            }
		} else {
			//	暗号化
			ContentResourceDto dto = encryptResourceFile(contentId, resourceRootDir, f, keyString);
			if (dto != null) {
				list.add(dto);
			}
		}
	}
	
	/**
	 * コンテンツのリソースファイルを複合化します。<br>
	 * ファイル名をAESアルゴリズムで複合化し、ファイルの先頭から {@link ABVEnvironment#ContentHeaderBytes} byte（これをヘッダーと言います。）に元のbyteを埋めます。
	 * @param dto リソースファイル情報を格納したDtoです。暗号化したファイル名とヘッダーもここに格納されます。
	 * @param resourceRootDir リソースのルートディレクトリです。普段/.../ABook/contents/{contentId}/resourcesとなります。
	 * @param destDir 複合化したファイルの保存先のフルパスです。
	 * @return 複合化が成功するとtrueを返します。
	 * @since 1.0.0
	 */
	public boolean decryptResourceFile(ContentResourceDto dto, File resourceRootDir, File destDir) {
		boolean result = false;
        if (dto != null
				&& resourceRootDir != null && resourceRootDir.exists() && resourceRootDir.isDirectory()
				&& destDir != null) {
            Logger.d(TAG, "[decryptResourceFile]: dto=%s, from=%s, to:%s", dto.resourceFileName, resourceRootDir.getAbsolutePath(), destDir.getAbsolutePath());
			File source = new File(resourceRootDir.getPath() + dto.subDirectory, dto.hashedFileName);
			File dest = new File(destDir.getPath() + dto.subDirectory, dto.resourceFileName);
			
			if (source.exists() && source.isFile()) {
				try {
					if (FileUtil.copy(source, dest, true)) {
						if (dto.hashedFileHeader != null && dto.hashedFileHeader.length > 0) {
							RandomAccessFile hashedFile = null;
							
							try {
								hashedFile = new RandomAccessFile(dest, "rw");
								hashedFile.seek(0);
								
								hashedFile.write(dto.hashedFileHeader, 0, dto.hashedFileHeader.length);
								
								RuntimeUtil.exec("chmod 750 "+ dest);
								
								result = true;
							} catch (Exception e) {
                                Logger.e(TAG, "[decryptResourceFile]: chmod fail." + dto.resourceFileName, e);
                                decryptResourceFile(dto, resourceRootDir, destDir);
								//throw e;
							} finally {
								if (hashedFile != null) {
									hashedFile.close();
								}
							}
						}
						
						result = true;
					} else {
						Logger.e(TAG, "Copy a resource file has failed on decryptResourceFile.");
					}
				} catch (Exception e) {
					Logger.e("Copy a resource file has failed on decryptResourceFile.", e);
					FileUtil.delete(dest);
				}
			}
		}
		
		return result;
	}
	
	/**
	 * コンテンツのリソースファイルを暗号化します。<br>
	 * ファイル名をAESアルゴリズムで暗号化し、ファイルの先頭から {@link ABVEnvironment#ContentHeaderBytes} byte（これをヘッダーと言います。）をnullに埋めます。
	 * @param contentId コンテンツIDです。
	 * @param resourceRootDir リソースのルートディレクトリです。普段/.../ABook/contents/{contentId}/resourcesとなります。
	 * @param resourceFile 暗号化するファイルです。
	 * @param keyString AESのキー文字列です。
	 * @return 暗号化が成功するとその情報を格納した {@link ContentResourceDto} クラスのインスタンスを返します。失敗するとnullを返します。
	 * @throws IOException 
	 * @since 1.0.0
	 */
	private ContentResourceDto encryptResourceFile(long contentId, File resourceRootDir, File resourceFile, String keyString) throws IOException {
		ContentResourceDto dto = null;
		RandomAccessFile hashedFile = null;
		File dest = null;

		try {
			String hashedFileName = new String(new BigInteger(SecurityUtil.encrypt(resourceFile.getName(), keyString)).toString(16));
			dest = new File(resourceFile.getParent(), hashedFileName);

			if (FileUtil.move(resourceFile, dest, true)) {
				dto = new ContentResourceDto();
				dto.contentId = contentId;
				dto.resourceFileName = resourceFile.getName();
				dto.subDirectory = resourceFile.getParent().replace(resourceRootDir.getPath(), StringUtil.Empty);
				dto.hashedFileName = hashedFileName;

				hashedFile = new RandomAccessFile(dest, "rw");
				hashedFile.seek(0);

                int byteSize = (int) (hashedFile.length() < ABVEnvironment.ContentHeaderBytes ? hashedFile.length() : ABVEnvironment.ContentHeaderBytes);
                
                byte[] buffer = new byte[byteSize];
                int len = hashedFile.read(buffer, 0, byteSize);

				if (len > 0) {
					dto.hashedFileHeader = buffer;
					hashedFile.seek(0);

					for (int i = 0; i < len; i++) {
						hashedFile.write(0x00);
					}
				}
			} else {
				Logger.e(TAG, "Rename a resource file has failed on encryptResourceFileName.");
				throw new IOException("File move failed. " + resourceFile);
			}
		} catch (IOException e) {
			Logger.e("Rename a resource file has failed on encryptResourceFileName.", e);
			try {
                FileUtil.delete(dest);
            } catch (Exception e2) { // ignore
                Logger.e("SecurityUtil", "file delete failed. dest=" + dest + " : " + e2.toString());
            }
			throw e;
		} finally {
			if (hashedFile != null) {
				try {
					hashedFile.close();
				} catch (IOException e) { // ignore
				}
			}
		}
		
		return dto;
	}
	
	private void saveContentPage(long contentId, File contentPath) {
		ContentPageDao contentPageDao = AbstractDao.getDao(ContentPageDao.class);
		
		//	ページテキストファイル（pageText_n.txt）のみ取得
		File[] pageTexts = ContentFileUtil.getPageTexts(contentPath);
		List<ContentPageDto> detailPages = contentPageDao.getContentPages(contentId);
		
		if (pageTexts != null && pageTexts.length > 0) {
			try {
				contentPageDao.beginTransaction();
				
				for (int i = 0; i < pageTexts.length; i++) {
					int pageNum = ContentFileUtil.getPageNumFromFileName(FileUtil.getFilenameWithoutExt(pageTexts[i].getName()));
					String pageText = ContentFileUtil.getStringFromPageTextFile(pageTexts[i]);
					boolean exist = false;
					
					if (detailPages != null && detailPages.size() > 0) {
						for (ContentPageDto dto : detailPages) {
							if (dto.pageNum == pageNum) {
								dto.pageText = pageText;
								//	update
								detailPages.remove(dto);
								exist = true;
								break;
							}
						}
					}
					
					if (!exist) {
						ContentPageDto dto = new ContentPageDto();
						dto.contentId = contentId;
						dto.pageNum = pageNum;
						dto.pageText = pageText;
						
						//Logger.d(LogTag, "insert page" + dto.toString());
						
						contentPageDao.insertContentPages(dto);
					}
				}

				if (detailPages != null && detailPages.size() > 0) {
					for (ContentPageDto contentPageDto : detailPages) {
						contentPageDao.deleteContentPage(contentPageDto.contentId, contentPageDto.pageNum);
					}
				}
				
				contentPageDao.commit();
				
				for (File f : pageTexts) {
					try {
						f.delete();
					} catch (Exception e) { // ignore
					}
				}
			} catch (Exception e) {
				contentPageDao.rollback();
				throw new RuntimeException(e);
			} finally {
			}
		}
	}
		
	public void deleteFile(String resourceZipPath, String imageZipPath) {
		//	ダウンロードしていたファイルの削除
		if (!StringUtil.isNullOrWhiteSpace(resourceZipPath)) {
			FileUtil.delete(resourceZipPath);
		}
		
		if (!StringUtil.isNullOrWhiteSpace(imageZipPath)) {
			FileUtil.delete(imageZipPath);
		}
	}
	

	/**
	 * abkファイルのパスワードをチェックする
	 * 
	 * @param zipFilePath
	 * @param extractPath
	 * @param password
	 * @return
	 * @throws NoSuchAlgorithmException
	 * @throws ABVException
	 * @throws ZipException
	 * @throws IOException
	 */
	public boolean checkPassword(String zipFilePath, String extractPath, String password) throws NoSuchAlgorithmException, ABVException, ZipException, IOException {
		ZipFile zip = createZipFileInstance(zipFilePath, extractPath, password);
		
		try {
			// zip.extractFile("meta.json", extractPath)⇒これではうまくいかない
			extractZipFile(extractPath, zip, zip.getFileHeader("meta.json"));
			return FileUtil.exists(extractPath + "meta.json");
		} catch (IOException e) {
			if (e.getMessage() != null && e.getMessage().toLowerCase().contains("password")) {
				return false;
			}
			throw e;
		} catch (ZipException e) {
			if (e.getCode() == ZipExceptionConstants.WRONG_PASSWORD) {
				 return false;
             }
			 throw e;
		}
		finally {
			try {
				FileUtil.delete(extractPath + "meta.json");
			} catch (Exception e2) {} // ignore
		}
	}

	/**
	 * ZipFileのインスタンスを生成
	 * 
	 * @param zipFilePath
	 * @param extractPath
	 * @param password
	 * @return
	 * @throws ZipException
	 * @throws NoSuchAlgorithmException
	 * @throws ABVException
	 */
	private ZipFile createZipFileInstance(String zipFilePath, String extractPath, String password) throws ZipException, NoSuchAlgorithmException, ABVException {
		ZipFile zip = new ZipFile(zipFilePath);
		zip.setRunInThread(true);
		if (zip.isEncrypted()) {
			if (StringUtil.isNullOrEmpty(password)) {
				password = ABVEnvironment.AbkDefaultZipPassword; // デフォルトパスワードを指定
			}
			zip.setPassword(SecurityUtil.getZipPassword(ABVEnvironment.ZipPasswordPrefixForReader, password));
		}
		
		if (!FileUtil.createNewDirectory(extractPath)) { // 出力先ディレクトリもついでに作成
			throw new ABVException(ABVExceptionCode.C_E_SYSTEM_0101, "Can't create content directory " + extractPath);
		}
		
		return zip;
	}
	
	
	public void extractZipFile(long contentId, String zipFilePath, String extractPath) throws ZipException, NoSuchAlgorithmException, IOException, ABVException {
		extractZipFile(contentId, zipFilePath, extractPath, SecurityUtil.getZipPassword(contentId, ABVEnvironment.ZipPasswordPrefix), true);
	}

	public void extractZipFile(long contentId, String zipFilePath, String extractPath, String password, boolean deleteZip) throws ZipException, NoSuchAlgorithmException, IOException, ABVException {
		ZipFile zip = createZipFileInstance(zipFilePath, extractPath, password);
		
		@SuppressWarnings("unchecked")
		List<FileHeader> headerList = zip.getFileHeaders();
		
		for (FileHeader header : headerList) {
			if (header != null) {
				if (header.getFileName().endsWith("Thumbs.db") || header.getFileName().endsWith("thumbs.db")) {
					continue;
				}
				
				extractZipFile(extractPath, zip, header);
			}
		}
			
		if (!ABVEnvironment.DebugContentDownload && deleteZip) {
			FileUtil.delete(zipFilePath);
		}
	}
	
	private void extractZipFile(String extractPath, ZipFile zip, FileHeader header) throws ZipException, IOException {
		String outputFilePath = extractPath + File.separator + header.getFileName();

		File outputFile = new File(outputFilePath);

		//현재 조건이 디렉토리 체크를 제대로 하지 못함. header로 수정
		//if (outputFile.isDirectory()) {
		if (header.isDirectory()) {
			outputFile.mkdirs();
			return;
		}

		File parentDir = outputFile.getParentFile();

		if (!parentDir.exists()) {
			parentDir.mkdirs();
		}
		
		ZipInputStream is = null;
		BufferedOutputStream os = null;
		try {
			is = zip.getInputStream(header);
			os = new BufferedOutputStream(new FileOutputStream(outputFile));
			int len;
			byte[] buff = new byte[1024];
			while ((len = is.read(buff)) != -1) {
				os.write(buff, 0, len);
			}
			os.flush();
		} finally {
			if (os != null) {
				try {
					os.close();
				} catch (Exception e) {
					// igrnore
				}
			}

			if (is != null) {
				try {
					is.close(true);
				} catch (Exception e) {
					// igrnore
				}
			}
		}

		UnzipUtil.applyFileAttributes(header, outputFile);

		Logger.d("unzip", "done extracting : %s", header.getFileName());
	}
	
	/**
	 * コンテンツのキャッシュディレクトリを返す。<br>
	 * コンテンツの素材ファイルの全てをキャッシュディレクトリにコピーします。<br>
	 * @param contentId
	 * @return
	 */
	public String getContentCacheDirWithExtract(long contentId) {
        String result = null;

        if (contentDao.isExist(contentId) && (ABVEnvironment.getInstance().isContentProtected)) {
            Logger.d(TAG, "[getContentCacheDirWithExtract]:ProtectedContentId=" + contentId);
            File cacheDir = ABVEnvironment.getInstance().getContentCacheDirectory(contentId);
            String jsonPath = cacheDir.getPath() + "/" + contentId + ".json";
            if (cacheDir.exists() && FileUtil.exists(jsonPath)) {
                //	コンテンツのキャッシュが存在するのでそのまま使う。
                result = cacheDir.getPath();
                cleanContentCacheDirectory(cacheDir, false);
            } else {
                File resourceDir = new File(ABVEnvironment.getInstance().getContentResourcesDirectoryPath(contentId, false));
                cleanContentCacheDirectory(cacheDir, true);
                boolean decrypted = true;
                List<ContentResourceDto> resources = contentResourceDao.getContentResources(contentId);
                for (ContentResourceDto contentResourceDto : resources) {
                    decrypted = decryptResourceFile(contentResourceDto, resourceDir, cacheDir);
                    if (!decrypted) {
                        Logger.e(TAG, "copy to " + cacheDir + contentResourceDto.subDirectory + File.separator + contentResourceDto.resourceFileName
                                + " from " + resourceDir + contentResourceDto.subDirectory + File.separator + contentResourceDto.hashedFileName + " failed.");
                        break;
                    }
                    Logger.d(TAG, "copy to %s%s/%s from %s%s/%s successful.", cacheDir, contentResourceDto.subDirectory, contentResourceDto.resourceFileName,
                            resourceDir, contentResourceDto.subDirectory, contentResourceDto.hashedFileName);
                }
                try {
                    File completeFile = new File(cacheDir.getPath(), COMPLETE_FILE);
                    if (!completeFile.exists()) {
                        FileUtil.createNewFile(completeFile.getAbsolutePath());
                        Logger.i(TAG, "Create complete.file :" + contentId);
                    }
                } catch (IOException e) {
                    Logger.e(TAG, "Create complete.file failed. dir=" + cacheDir.getPath() + " exists=" + FileUtil.exists(cacheDir.getPath()));
                    throw new RuntimeException(e);
                }
                if (decrypted) {
                    result = cacheDir.getPath();
                }
                chmodFile(cacheDir);
            }
        } else{
            result = ABVEnvironment.getInstance().getContentResourcesDirectoryPath(contentId, false);
        }
        return result;
	}

    private void chmodFile(File cacheDir) {
        Logger.d(TAG, "[chmodFile]: cacheDir=" + cacheDir.getAbsolutePath());
        try {
            RuntimeUtil.exec("chmod 755 " + cacheDir.getAbsolutePath());
            File[] files = FileUtil.getChildFiles(cacheDir);
            if (files != null) {
                for (File file : files) {
                    if (file.getPath().toLowerCase().endsWith(".mp4")) {
                        RuntimeUtil.exec("chmod 755 " + file.getAbsolutePath());
                    }
                }
            }
        } catch (IOException e) {
            Logger.d(TAG, "chmod error. " + cacheDir.getAbsolutePath(), e);
        }
    }

    /**
     * 指定コンテンツIDのZIPの展開が完了したか否かの確認
     * @param contentId 展開したか確認するコンテンツID
     * @return boolean 完了可否
     */
    public boolean isCompleteContentExtract(long contentId) {
        String contentPath = ABVEnvironment.getInstance().getContentCacheDirectoryPath(contentId);
        return isCompleteContentExtract(contentPath);
    }

    /**
     * 指定コンテンツフォルダにZIPの展開が完了したか否かの確認
     * @param contentFolderPath コンテンツを展開したフォルダ
     * @return boolean 完了可否
     */
    public boolean isCompleteContentExtract(String contentFolderPath) {
        String path = contentFolderPath + "/" + ContentFileExtractor.COMPLETE_FILE;
        return FileUtil.exists(path);
    }
	
	private void cleanContentCacheDirectory(File cacheDir, boolean deleteSelf) {
        int cacheSize = ABVEnvironment.getInstance().cacheSize;
		if (cacheSize <= 0) { // 0以下の場合削除しない
			return;
		}
        Logger.d(TAG, "[cleanContentCacheDirectory]: cacheDir=%s, cacheSize=%s", cacheDir.getAbsolutePath(), cacheSize);
		File[] caches = FileUtil.getChildDirectories(cacheDir.getParentFile());
		
		//	キャッシュが五つ以上存在すると一番古いキャッシュを削除する。
		if (caches != null && caches.length >= cacheSize) {
			File oldestCache = null;
			
			for (File file : caches) {
				if (oldestCache == null || oldestCache.lastModified() > file.lastModified()) {
					if (file.equals(cacheDir) && !deleteSelf) {
						continue;
					}
					
					oldestCache = file;
				}
			}
			
			if (oldestCache != null) {
				FileUtil.delete(oldestCache);
			}
		}
	}
	
	public void removeContentCash(long contentId) {
		if (!ABVEnvironment.getInstance().isContentProtected) { // 非暗号化の場合、getContentCacheDirectoryがコンテンツディレクトリを見るので何もしない
			return;
		}
		File cacheDir = ABVEnvironment.getInstance().getContentCacheDirectory(contentId);
		FileUtil.delete(cacheDir); // 何等か通知が必要? ⇒不要
	}
	
	public boolean removeAllContentCash() {
		if (!ABVEnvironment.getInstance().isContentProtected) { // 非暗号化の場合、getContentCacheDirectoryがコンテンツディレクトリを見るので何もしない
			return true;
		}
		String cacheDirPath;
		cacheDirPath = ABVEnvironment.getInstance().cacheDirectory;
		return FileUtil.deleteChilds(cacheDirPath);
	}

	public String getPdfPath(long contentId) throws IOException {
		String contentDir = getContentCacheDirWithExtract(contentId);
		if (ABVEnvironment.getInstance().isContentProtected) {
			// cacheフォルダのパーミッションの修正
			RuntimeUtil.exec("chmod 751 "+ contentDir);
		}

		ContentJSON contentJSON = AbstractLogic.getLogic(ContentLogic.class).getContentInfoJson(contentDir, contentId);
		if (contentJSON != null && contentJSON.isPdf()) { // 一般PDFコンテンツの場合
			String path = contentDir + "/" + contentJSON.getPdfFileName();
			if (!FileUtil.exists(path)) {
				Logger.e(TAG, "getPdfPath: File not found. filepath=" + path);
				throw new FileNotFoundException();
			}
			return path;
		}
		return null;
	}


	public List<String> getMoviePathList(long contentId) throws IOException {
		String contentDir = getContentCacheDirWithExtract(contentId);
		if (ABVEnvironment.getInstance().isContentProtected) {
			// cacheフォルダのパーミッションの修正
			RuntimeUtil.exec("chmod 751 "+ contentDir);
		}
		
		List<String> moviePathList = new ArrayList<String>();
        String[] contentDirList = new File(contentDir).list();
        if (contentDirList != null) {
            for (String path : contentDirList) {
                if (path.endsWith(".mp4") || path.endsWith(".mov") || path.endsWith(".m4v")) {
                    moviePathList.add(contentDir + "/" + path);
                }
            }
        }
		
		return moviePathList;
	}

	public void configureContent(long contentId) {
		ContentDto contentDto = contentDao.getContent(contentId);
		
		if (contentDto != null) {
			if (contentDto.downloadedFlg) {
				// コンテンツの更新時のメモ、マーキング、ブックマークなどの更新処理
				ContentOldPdfDto oldPdfDto = contentOldPdfDao.getContentOldPdf(contentId);
				
				//oldPdfDtoにデータがある場合のみ
				if (oldPdfDto!=null) {
					ContentDto newPdfDto = contentDao.getContent(contentId);
					int oldPage = oldPdfDto.allPageNum;
					int newPage = newPdfDto.allPageNum;
					
					// PDFが差し替わったらページサムネイル削除
					FileUtil.deleteChilds(ABVEnvironment.getInstance().getPdfThumbnailDirectory(contentId));

					// ページ数が少ない場合マーキング削除　（向きが変わる場合はマーキング削除しない。既存のマーキングははみ出ることなく枠の中に納まるように縮小される） v1.5.2で仕様変更
					if (newPage < oldPage) {
						File markingDir = ABVEnvironment.getInstance().getContentMarkingDirectory(contentId, false);
						for (int pageNum = newPage; pageNum < oldPage; pageNum++) {
							String filePath = markingDir.getAbsolutePath() + File.separator + pageNum + ".png";
							FileUtil.delete(filePath);
							AbstractDao.getDao(ContentMarkingDao.class).deleteMarking(contentId, pageNum);
						}
					}

					// ページ数が少なくなる場合、メモを最後のページに移動
					if (newPage < oldPage) {
						ContentMemoDao contentMemoDao = AbstractDao.getDao(ContentMemoDao.class);
						List<ContentMemoDto> list = contentMemoDao.getMemoList(newPdfDto.contentId);
						if (list!=null) { 
							for (int i=0; i < list.size(); i++) {
								if ((list.get(i).pageNum) > newPage-1) {
									Logger.v(TAG, "memo update : %s", list.get(i));
									contentMemoDao.moveMemo(list.get(i), newPage-1);
								}
							}
						}
					}
				}	
			}	
			
			//PDF情報をt_content_old_pdfに移動する。(メソッド内部でinsert or updateする)
			contentOldPdfDao.insertContentOldPdf(contentId, contentDto.allPageNum, contentDto.orientation);
			
			contentDto.downloadedFlg = true;
			contentDto.downloadingFlg = false;
			contentDto.updatedFlg = false;
			contentDto.newFlg = false;
			contentDao.updateContent(contentDto, false);
		}
	}
	
	public String getGuidePDFFilePath(long contentId) {
		String contentDir = ABVEnvironment.getInstance().getContentCacheDirectoryPath(contentId);
		return contentDir + "/" + GUIDE_PDF_FILE_NAME;
	}
}
