Gradleのファイル操作API

# ファイル操作の方法いろいろ紹介
以下、ファイル操作の種類。

- 単一ファイルの参照
- ファイルコレクションによる参照
- ファイルツリーによる参照


# 単一ファイルの参照
fileメソッドで単一ファイルを保持するFileクラスを返却し、操作を行う。

## fileメソッドの引数

- プロジェクトディレクトリを起点とする相対パスの文字列
- 絶対パスで指定したFileオブジェクト
- URL/URIオブジェクト
- Callableインターフェース
- クロージャ

```gradle
// 相対パス
File relative = file('<相対パス>')

// 絶対パス
File absolute = file(new File('<絶対パス>'))

// URL/URIオブジェクト
File url = new URL('<url形式のパス>')
File urlFile = file(url)
File uri = new URI('<rui形式のパス>')
File uriFile = file(uri)

// Callableインターフェース
import java.util.concurrent.Callable
File callable = file(new Callable<String>() {
    String call() {
        '<絶対パス>'
    }
})

// クロージャ
File closure = file { '<相対パス>' }

```

## PathValidationによるファイルの検証
file()でFileインスタンスの生成を行う際、第２引数にPathValidation列挙型を指定すると
生成対象の検証を行うことができる。

**PathValidationの定数一覧**

|定数|説明|
|:--|:--|
|DIRECTORY|生成対象がディレクトリでない場合、タスクを失敗させる。|
|FILE|生成対象がファイルでない場合、タスクを失敗させる。|
|EXISTS|生成対象が存在しない場合、タスクを失敗させる。|
|NONE|デフォルト値。特に検証を行わない。|

# ファイルコレクション

filesメソッドで複数ファイルを保持するコレクションである
ConfigurableFileCollectionインターフェースを実装し、親インターフェースである
FileCollectionインターフェースに定義されたメソッドで操作を行う。

## filesメソッドの引数

- fileメソッドが許容する引数(複数指定可能)
- コレクション、配列
- Callableインターフェース(後述)

```gradle
// fileメソッドが許容する引数(一部抜粋)
FileCollection collection = files('src/file1.txt', new File('src/file2.txt'))

List fileList = [new File('src/file1.txt'), new File('src/file2.txt')]
// コレクション
FileCollection list = files(fileList)
// 配列
FileCollection array = files(fileList as File[])
```

## ファイルコレクションの変換

ファイルコレクションは、Set、ListなどのコレクションやFileクラスの配列に変換できる。

```gradle
FileCollection collection = files('src/file1.txt', 'src/file2.txt')

// Groovyのas演算子による変換
List list = collection as List
Set set = collection as Set
File[] array = collection as File[]

// getFilesメソッド
Set set2 = collection.getFiles()
Set set3 = collection.files

// getSingleFileメソッド
// コレクションに複数ファイルが保持されている場合、例外が発生する。
// よって、演算子によってfile2.txtを減算(削除)しておく。
FileCollection collection2 = collection - files('src/file2.txt')
File file = collection.singleFile
```

## ファイルコレクションのCallableインターフェース

filesメソッドにCallableインターフェースを渡すと、遅延評価という動作を取る。
具体的には、callメソッドが呼ばれるタイミングがfileメソッドと異なるようになる。
fileメソッドではインスタンスの生成時に呼ばれるが、
filesメソッドでは生成したインスタンスにアクセスがあったタイミングで呼ばれる。

下記の例では、先にファイルコレクションのインスタンスの生成が行われているが、
実行結果で先に出力されているのは、
fileメソッドによるファイルインスタンス生成のログであることがわかる。

```gradle
task lateEvaluation {
    doLast {
        FileCollection argCollection = null
        FileCollection newCollection = files(new Callable<List<File>>() {
            List<File> call() {
                println '-- files#call --'
                argCollection as List
            }
        })
        // 1. ファイルインスタンスの生成
        File file = file(new Callable<String>() {
            String call() {
                println '-- file#call --'
                'src/file1.txt'
            }
        })
        // 2. ファイルコレクションへのアクセス
        argCollection = files('src/file1.txt')
        // 3. ファイルコレクションの内容
        println newCollection.singleFile
    }
}
```

```terminal
> Task :lateEvaluation
-- file#call --
-- files#call --
<ファイルの絶対パス>/src/file1.txt
```

## ファイルコレクションのフィルタリング
ファイルコレクションのfilterメソッドにクロージャを渡すと、
条件に一致するファイルのみを抽出することができる。

```gradle
FileCollection collection = files('src/file1.txt', 'src/file2.txt')
FileCollection filteredCollection = collection.filter { file ->
    file.name == 'file1.txt'  
}
println filteredCollection.singleFile
```

## その他の機能
その他、利用頻度の高いファイルコレクションのメソッド。

```gradle
FileCollection collection = files('src/file1.txt')

// getAsPath():コレクションの保持するファイルのフルパスを環境に応じたパスセパレータで取得する
println collection.asPath

// contains():コレクションに指定したファイルが含まれているかを判定する
File target = file('src/file1.txt')
println collection.contains(target)

// isEmpty():コレクションが空かどうかを判定する
println collection.isEmpty()

// stopExecutionIfEmpty():コレクションが空の場合、StopExecutionExceptionをスローする
// ※ 書籍に乗っていたので一応書いたが、現在は廃止されているメソッド
collection = collection - files('src/file1.txt')
collection.stopExecutionIfEmpty()
```

# ファイルツリーによる参照

fileTreeメソッドで複数ファイルを木構造のファイルツリーとして保持する
ConfigurableFileTreeインターフェースを実装し、
親インターフェースであるFileTreeインターフェースに定義されたメソッドで操作を行う。

## ファイルツリーの取得

ファイルツリーの取得にはルートとなるフォルダを指定すると、
その配下のフォルダが取得できる。
Antのパターンを使用し、取得対象のマッチングを行うことで対象の指定・除外を行い、
ファイルツリーの構造を変更することも可能。

### Antのパターン

|パターン|意味|
|:--|:--|
|*|0以上の任意の文字列|
|?|任意の１文字|
|**|任意の階層(パス)|
|/または¥で終わる文字列|指定した文字列の後ろに**がつくものとして取り扱われる。|

```gradle
// 基本のファイルツリーの取得
FileTree tree = fileTree('src')

// include/excludeメソッドによるマッチング
fileTree('src').include('**/*.java').exclude('**/Example1.java').each { println it.name }

// クロージャによるマッチング
FileTree closure = fileTree('src') {
    include '**/*.java'
    exclude '**/Example1.java'
}
closure.each { println it.name }

// マップによるマッチング
FileTree map = fileTree(dir: 'src', include: '**/*.java', exclude : 'Example1.java')
map.each { println it.name }

// matchingメソッドによるマッチング(上記の例とは異なり、新しくFileTreeを作成して返す)
FileTree tree2 = fileTree('src')
FileTree javaFiles = tree2.matching { include '**/*.java' }
javaFiles.each { println it.name }
FileTree nonJavaFiles = tree2.matching { exclude '**/*.java' }
nonJavaFiles.each { println it.name }
```

## ファイルツリーの探索
visitメソッドにより、起点から順に各ディレクトリやファイルの情報へとアクセスできる。
visitメソッドは引数として、
　・ FileVisitDetailsインターフェースを渡すクロージャ
　・ FileVisitorインターフェースの実装クラス
のいずれかをとる。

```
// FileVisitDetailsインターフェース
FileTree tree = fileTree('src')
tree.visit { fileDetails ->
    println 'name: ' + fileDetails.getName()
    if(fileDetails.isDirectory()) {
        println 'path: ' + fileDetails.getPath()
    } else {
        println 'file size: ' + fileDetails.getSize()
    }
}

// FileVisitorインターフェース
tree.visit ( new FileVisitor() {
    void visitDir(FileVisitDetails fileDetails) {
        println 'name: ' + fileDetails.getName()
        println 'path: ' + fileDetails.getPath()
    }
    void visitFile(FileVisitDetails fileDetails) {
       println 'name: ' + fileDetails.getName()
       println 'file size: ' + fileDetails.getSize()
    }
}
```

# ファイルのコピー

ファイルのコピーにはcopyメソッドを使用する。copyメソッドを使用したファイルのコピーは
copyメソッドに渡す引数であるCopySpecインターフェース側で行う。

```gradle
copy {
    // CopySpecインターフェースのメソッドを使用した処理
}
```

## CopySpecインターフェース

Gradleが提供するインターフェースの一つ。Copyタスクなど、ファイル操作を行う
組み込みタスクの内部ではCopySpecインターフェースを使用した処理が行われている。

## 単純なコピー

```gradle
copy {
    // コピー元の指定
    from '相対パス'
    // コピー先の指定
    into '相対パス'
    // コピー対象の指定(Antのパターンでマッチング)
    include '条件１', '条件２'...
    // コピー対象外の指定(Antのパターンでマッチング)
    exclude '条件１', '条件２'...
    // 空のフォルダをコピーするかの指定
    includeEmptyDirs = false
}

// 実装例
copy {
    from 'src'
    into 'target'
    include '**/*.java'
    exclude 'Example1.java'
    includeEmptyDirs = false
}
```

## ファイルのリネーム

ファイルコピー時、ファイル名をリネームしたい場合はrenameメソッドを使用する。
renameメソッドの引数には、クロージャもしくは正規表現を渡す。
※renameはCopySpecのメソッドなので、copyメソッドのクロージャ内でなければ使えない。

```gradle
copy {
    from 'src'
    into 'target'

    // クロージャによるリネーム
    rename { fileName ->
        if(fileName == 'file2.txt') {
            fileName.replace('file2', 'rename-file')
        }
    }

    // 正規表現によるリネーム(XXX1.xxxというファイルがrename1.xxxにリネームされる)
    rename '(.*)1.(.*)', 'rename1.$2'
}
```

## コピー時のファイル編集

コピー時、ファイルの内容を編集することができる。
編集には、filterメソッドとexpandメソッドの２通りの方法がある。

### filterメソッドを使用した編集

filterメソッドはクロージャもしくはFilterReaderのサブクラスを引数にとる。

```gradle
copy {
    from 'src'
    into 'target'
    include 'replace-by-filter.txt'
    // filterメソッドは１行づつファイルを読み込むため、複数行にまたがった編集は不可
    filter { line ->
        line.replaceAll 'EXAMPLE_STR', 'REPLACE_STR'
    }
}

copy {
    from 'src'
    into 'target'
    include 'replace-by-filter-reader.txt'
    // FilterReaderを継承したAntの提供するTabsToSpacesクラスを使用し、
    // タブを半角スペース4つに変換している
    filter org.apache.tools.ant.filters.TabsToSpaces, tablength: 4
}
```

### expandメソッドを使用した編集

expandメソッドは、コピー対象のファイルをGroovyのSimpleTemplateEngineクラスの
テンプレートファイルとして読み込み、テンプレートファイル内に定義された
テンプレート変数を、引数に渡した値でリプレースして編集することができる。

```java:template.java
package ${packageName}

/**
 * ${className}です。
 * Gradleで自動生成したクラスです。
 * /
public class ${className} {
<% fields.each { type, fieldName -> %>
    private ${type} ${fieldName};
<% } %>

    pubilc void ${className}() {}

<% fields.each { type, fieldName ->
   String methodName = fieldName.capitalize() %>
    public ${type} get${methodName}() {
        return ${fieldName};
    }

    public void set${methodName}(${type} ${fieldName}) {
        this.${fieldName} = ${fieldName};
    }
<% } %>
}
```

```gradle:build.gradle
String targetName = 'SampleBean'
copy {
    from 'template'
    into 'target'
    // ファイル内容の編集
    expand packageName:'com.example.bean',
           className:targetName,
           fields:['String':'name', 'int':'value']
    // 編集後のファイル名のリネーム
    rename 'template.java', "${targetName}.java"
}
```
## CopySpecの入れ子

CopySpecは、fromメソッドとintoメソッドにおいてさらに入れ子にすることができる。
入れ子構造になることで、コピー元とコピー先についてさらに細かな指定を行うことができる。

例えば、以下の構造のファイル群があったとする。

```text:コピー前のフォルダ構成
<PROJECT_ROOT>
├ original
│　├ internal
│　│　└ inner.txt
│　├ sub1
│　│　└ sub1.txt
│　└ sub2
│　 　└ sub2.txt
└ build.gradle
```

original配下の全てのファイルをreplicationにコピーしたいが、
sub1、sub2フォルダはコピーせず、全てinternalフォルダにまとめてコピーしたいとする。
そういった場合は、以下のように実装する。

```gradle
copy {
    // replication配下に
    into 'replication'
    // original配下のinternalのみコピーする
    from('original') {
        include 'internal/'
    }
    // replicationにコピーされたinternalの中にsub1、sub2配下のファイルをコピーする
    into('internal') {
        from 'original/sub1', 'original/sub2'
    }
}
```

```text:コピー後のフォルダ構成
<PROJECT_ROOT>
├ original
│　…省略…
│
├ replication
│　└ internal
│　 　├ inner.txt
│　 　├ sub1.txt
│　 　└ sub2.txt
└ build.gradle
```

# ファイルの削除

```gradle
delete 'deleted.txt'
```

# ディレクトリの作成

ディレクトリの作成にはmkdirメソッドを使用するが、指定したパスはデフォルトで
プロジェクトディレクトリからの相対パスとなっている。
指定したパスの親フォルダがない場合は、遡って必要な親フォルダも作成される。
また、プロジェクトファイルの範囲を外れた場所にもファイルを作成することができる。

```gradle
// parentフォルダが存在しない場合、parent、childフォルダも一緒に作成される
mkdir 'parent/child/grandchild'
// プロジェクトフォルダ外のフォルダも作成できる
mkdir '../outOfProjectFolder'
// 絶対パスを指定することも可能
mkdir '/tmp/absoluteFolder'
```
# 参照

この記事は以下の書籍を参考に執筆しています。

参考書籍：[Gradle徹底入門 次世代ビルドツールによる自動化基盤の構築](https://www.shoeisha.co.jp/book/detail/9784798136431)

# 感想
Gradleはビルドツールというのもあって、フォルダやファイルの操作は
実用する上では肝になるところなのかな？と感じた。

書籍で扱っているGradleのバージョンが2.X系とかなり前のバージョンなので、
たまーにサンプルコードに廃止されたものが混ざっている。
エラーが出るのでその調査に結構時間を取られた。。。

ただ、プログラム学習の上で何がどのタイミングで廃止、追加されたっていう
差分を調査する力は必須ではあるので、それもまた良い練習かなと思う。
調査のスピードを上げれるよう、バージョンによる差分の調べ方を見直してみる。

書籍はGradleが内部でどう動いているのかという根幹の話にも
多く紙面が割かれているので、なんとなくの理解で終わらずに済みそうで好感触です。
ツールの動きを理解すれば、ログの読み方も変わって調査も楽になるだろうという
希望を持って学習を続けます。