Javaでファイルシステムのファイル、Jar(zip)内のファイル、リソースのファイルを同時に扱う方法 

この記事では下記の環境を利用しています。
- Eclipse：Mars.2 Release (4.5.2)
- JavaSE-1.8
- Windwos10

## ファイルを扱うクラスを用意
・ファイルシステム内にあるファイル
・jar(zip)内にあるファイル
・リソースにあるファイル
これらのファイルの読込やコピーをワンアクションで行えるようにするため
下記のようなクラスを用意して、様々なファイルを扱ってみました。

### ターゲットとなるファイルの指定方法
|                    |Windows                                 |UNIX/Linux                              |
|:-------------------|:---------------------------------------|:---------------------------------------|
|ファイルシステムのパス|C:\temp\sample.txt                      |/temp/sample.txt                        |
|jar内ファイルのURL   |jar:file:/C:\temp\sample.jar!/sample.txt|jar:file:/temp/sample.jar!/sample.txt|
|zip内ファイルのURL   |jar:file:/C:\temp\sample.zip!/sample.txt|jar:file:/temp/sample.zip!/sample.txt|

|                    |パターン1                                |パターン2                                |
|:-------------------|:---------------------------------------|:---------------------------------------|
|リソースのパス　　　　|temp/sample.txt                         |/temp/sample.txt                        |


```java:FileAccessUtil
package sample;

import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * ファイルシステムのファイル,jar(zip)内のファイル、リソースのファイルを扱うクラスです。 対象とするファイルは下記のように指定出来ます。
 * <dl>
 * <dt>ファイルシステムのパス</dt>
 * <dd>(Windows) C:\temp\sample.txt</dd>
 * <dd>(UNIX/Linux)/temp/sample.txt</dd>
 * <dt>jar(zip)内ファイルのURL</dt>
 * <dd>(Windows) jar:file:/C:\temp\sample.zip!/sample.txt</dd>
 * <dd>(UNIX/Linux)jar:file:/temp/sample.zip!/sample.txt</dd>
 * <dt>リソース内のパス</dt>
 * <dd>/samlpe/sample.txt
 * </dl>
 */
public class FileAccessUtil {

	/**
	 * 通常のファイルシステム内のファイル、jar（zip）内のファイル、リソース内のファイルをコピーします。
	 *
	 * @param src
	 *            ファイルのパス、Jar内のファイルへのURL、リソースのパス
	 * @param dst
	 *            コピー先のパス
	 * @return コピー先のPath
	 */
	public Path copy(String src, String dst) {
		Path path = null;
		try {
			if (isInFileSystem(src) || isResourceInFileSystem(src)) {
				path = Files.copy(Paths.get(getURI(src)), Paths.get(dst), StandardCopyOption.REPLACE_EXISTING);
			} else if (isInJarFile(src) || isResourceInJarFile(src)) {
				path = copyFromInJar(getURI(src), dst);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return path;
	}

	/**
	 * jar(zip)内にあるファイルをファイルシステムにコピーします。
	 *
	 * @param uri
	 *            コピー元となるjar(zip)内のファイルを指定したURI
	 * @param dst
	 *            コピー先のパス
	 * @return コピー先のPath
	 * @throws IOException
	 */
	private Path copyFromInJar(URI uri, String dst) throws IOException {
		Path path = null;
		Map<String, String> env = new HashMap<>();
		env.put("create", "true");
		try (FileSystem jarFs = FileSystems.newFileSystem(URI.create(uri.toString().split("!")[0]), env)) {
			Path pathInJarFile = jarFs.getPath(uri.toString().split("!")[1]);
			path = Files.copy(pathInJarFile, Paths.get(dst), StandardCopyOption.REPLACE_EXISTING);
		}
		return path;
	}

	/**
	 * 通常のファイルシステム内のファイル、jar（zip）内のファイル、リソース内のファイルからbyte[]を読み込みます。
	 *
	 * @param src
	 *            ファイルのパス、Jar内のファイルへのURL、リソースのパス
	 */
	public byte[] getReadAllBytes(String src) {
		byte[] bytes = null;
		try {
			if (isInFileSystem(src) || isResourceInFileSystem(src)) {
				bytes = Files.readAllBytes(Paths.get(getURI(src)));
			} else if (isInJarFile(src) || isResourceInJarFile(src)) {
				bytes = getReadAllBytesInJar(getURI(src));
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return bytes;
	}

	/**
	 * jar(zip)内にあるファイルからbyte[]を読み込みます。
	 *
	 * @param uri
	 *            Jar(zip)内のファイルを指定したURI
	 * @throws IOException
	 */
	private byte[] getReadAllBytesInJar(URI uri) throws IOException {

		Map<String, String> env = new HashMap<>();
		env.put("create", "true");
		try (FileSystem jarfs = FileSystems.newFileSystem(URI.create(uri.toString().split("!")[0]), env)) {
			return Files.readAllBytes(jarfs.getPath(uri.toString().split("!")[1]));
		}
	}

	/**
	 * 通常のファイルシステム内のファイル、jar（zip）内のファイル、リソース内のファイルから行を一行ずつ取得したListを取得します。
	 *
	 * @param src
	 *            ファイルのパス、Jar内のファイルへのURL、リソースのパス
	 */
	public List<String> getReadAllLines(String src) {
		List<String> lines = new ArrayList<String>();
		try {
			if (isInFileSystem(src) || isResourceInFileSystem(src)) {
				lines = Files.readAllLines(Paths.get(getURI(src)));
			} else if (isInJarFile(src) || isResourceInJarFile(src)) {
				lines = getReadAllLinesInJarFile(getURI(src));
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return lines;
	}

	/**
	 * ファイル・システム、jar(zip)、リソース内のファイルいずれかのURLを取得します。
	 *
	 * @param src
	 *            ファイルシステムのパス、jar()内ファイルのURL、リソースのパスのいずれか
	 * @return
	 */
	private URL getURL(String src) {
		if (isInFileSystem(src)) {
			return getURLInFileSystem(src);
		} else if (isInJarFile(src)) {
			return getURLInJarFile(src);
		} else if (isResource(src)) {
			return getURLInResource(src);
		}
		return null;
	}

	/**
	 * jar(zip)内にあるファイルから読み込んだ行のListを取得します。
	 *
	 * @param uri
	 *            Jar(zip)内のファイルを指定したURI
	 * @return ファイルを一行ずつ読み込んだList
	 * @throws IOException
	 */
	private List<String> getReadAllLinesInJarFile(URI uri) throws IOException {
		Map<String, String> env = new HashMap<>();
		env.put("create", "true");
		try (FileSystem Jarfs = FileSystems.newFileSystem(URI.create(uri.toString().split("!")[0]), env)) {
			return Files.readAllLines(Jarfs.getPath(uri.toString().split("!")[1]));
		}
	}

	/**
	 * URIを作成します。
	 *
	 * @param src
	 *            ファイルシステムのパス、jar()内ファイルのURL、リソースのパスのいずれか
	 * @return
	 */
	private URI getURI(String src) {
		// URLにバックスラッシュが含まれている場合は/に置換
		// jar内にあるURLを利用する場合にurisyntaxexceptionが発生するため。
		URI uri = URI.create(getURL(src).toString().replace(File.separator, "/"));
		return uri;
	}

	/**
	 * jar(zip)内にあるファイルを確認し、ファイルが存在する場合はそのURLを取得します。
	 *
	 * @param src
	 *            jar(zip)内のファイルを示すURL
	 * @return jar(zip)内にあるファイルのURLを返します。 {@code null} ファイルが見つからなかった場合
	 */
	private URL getURLInJarFile(String src) {
		try {
			URL url = new URL(src);
			JarURLConnection connection = (JarURLConnection) url.openConnection();
			connection.connect();
			return url;
			// jar(zip)内のURLを指定していない場合はClassCastExceptionが発生する
		} catch (IOException | ClassCastException e) {
			return null;
		}
	}

	/**
	 * ファイルシステム内にあるファイルのURLを取得します。
	 *
	 * @param src
	 *            ファイルシステムのパス
	 * @return ファイルシステムのURL {@code null} ファイルが見つからなかった場合
	 */
	private URL getURLInFileSystem(String src) {
		File file = new File(src);
		try {
			return file.toURI().toURL();
		} catch (IOException e) {
			return null;
		}
	}

	/**
	 * リソースのURLを取得します。
	 *
	 * @param src
	 *            リソース内のパスを指定します。 <br>
	 *            例) /samlpe/sample.txt<br>
	 *            例) sample.txtt
	 * @return リソースのURL {@code null} ファイルが見つからなかった場合
	 */
	private URL getURLInResource(String src) {
		// "."から始まっているとjarにパッケージした際にうまく取得出来ないため"."から始まる場合は頭の"."を削除
		if (src.startsWith("." + File.separator)) {
			src = src.substring(1);
		}
		// "/"で始まっているファイル場合と、そうでない場合に備えてgetResourceは2パターン使う
		URL url = this.getClass().getResource(src);
		if (!Optional.ofNullable(url).isPresent()) {
			url = this.getClass().getClassLoader().getResource(src);
		}
		return url;
	}

	/**
	 * ファイルシステムに存在するかを確認します。
	 *
	 * @param src
	 *            ファイルシステムのパス
	 * @return 存在する場合は true
	 */
	private boolean isInFileSystem(String src) {
		File file = new File(src);
		{
			if (file.exists()) {
				return true;
			}
		}
		return false;
	}

	/**
	 * jar(zip)内にファイルが存在するかを確認します。
	 *
	 * @param src
	 *            jar(zip)内のファイルを示すURL
	 * @return 存在する場合は true
	 */
	private boolean isInJarFile(String src) {
		if (Optional.ofNullable(getURLInJarFile(src)).isPresent()) {
			return true;
		}
		return false;
	}

	/**
	 * リソースにファイルが存在するかを確認します。
	 *
	 * @param src
	 *            リソースのパス
	 * @return 存在する場合は true
	 */
	private boolean isResource(String src) {
		if (Optional.ofNullable(getURLInResource(src)).isPresent()) {
			return true;
		}
		return false;
	}

	/**
	 * リソースのファイルがファイルシステムに存在するかを確認します。
	 *
	 * @param src
	 *            リソースのパス
	 * @return 存在する場合は true
	 */
	private boolean isResourceInFileSystem(String src) {
		if (isResource(src)) {
			if (!isInJarFile(getURLInResource(src).toString())) {
				return true;
			}
		}
		return false;
	}

	/**
	 * リソースのファイルがjar内に存在するかを確認します。
	 *
	 * @param src
	 *            リソースのパス
	 * @return 存在する場合は true
	 */
	private boolean isResourceInJarFile(String src) {
		if (isResource(src)) {
			if (isInJarFile(getURLInResource(src).toString())) {
				return true;
			}
		}
		return false;
	}

}
```

## ファイルを読み込んでコピーしてみる

sampleとして以下のファイルを用意。
```text:sample.txt
hoge fuga
```
下記を実行してみます。

```java:FileAccessUtilTest
import java.nio.file.Path;

public class FileAccessUtilTest {

	public static void main(String[] args) {
		String src = args[0];
		String dst = args[1];
		FileAccessUtil util = new FileAccessUtil();
		System.out.println("--- 出力開始 ---");
		for (String line : util.getReadAllLines(src)) {
			System.out.println(line);
		}
		System.out.println("--- 出力終了 ---");

		System.out.println("--- コピー開始 ---");
		Path path = util.copy(src, dst);
		System.out.println("コピー先のパスは[" + path.toString()+"]です。");
		System.out.println("--- コピー終了 ---");
	}
}
```

1. ファイルシステムのファイル  
第1引数に"C:\temp\sample.txt"を指定
2. jar(zip)内のファイル  
第1引数に"jar:file:/C:\temp\sample.zip!/sample.txt"を指定
3. リソースのファイル（Eclipseから実行）  
第1引数に"sample.txt"を指定
4. リソースのファイル（jarから実行）  
第1引数に"sample.txt"を指定  

第2引数に”C:\temp¥sample2.txt”を指定

それぞれ、下記のように出力され、無事にファイルの読み込みとコピーができました。
```
--- 出力開始 ---
hoge fuga
--- 出力終了 ---
--- コピー開始 ---
コピー先のパスは[C:\temp\sample2.txt]です。
--- コピー終了 ---
```

## 詰まった点
### jar(zip)内にあるファイルを指定したURLからURIへの変換がうまくいかない。
- 原因
URL.toURI()を使って変換しようとしたときに、バックスラッシュがURLに含まれていると「URISyntaxException」が発生する

- 回避策
    - URIを作成する際はURI.create(url)を利用して作成する。
    - URLに含まれているセパレータにバックスラッシュが含まれている場合を考慮し、スラッシュに変換する。  
例）URI.create(url.toString().replace(File.separator, "/"));

### リソース内のURLを取得する際にパスが"."から始まっているとjar内のリソースを取得出来ない。
- 原因  
クラスローダでの getResourceはすべてのピリオド (.) をスラッシュ (/) に変換しているので"./~"のようなパスを指定すると"//"となってしまいうまく取れないらしい。
- 回避策  
  getResource(name)に渡すパスがピリオドで始まっている場合はあらかじめ消去しておく

###  jar(zip)内のファイルは通常のファイル・システムからは扱えない。
- 原因  
デフォルトのファイル・システムではjar(zip)内のファイルを直接参照出来ないため。
- 回避策  
jar(zip)用のファイル・システムを用意し、そのファイルシステムを利用して操作を行う
- 参考URL  
http://docs.oracle.com/javase/jp/7/technotes/guides/io/fsp/zipfilesystemprovider.html