AndroidManifest.xmlやJavaのクラスをmvel2で生成する黒魔術

## 目の前にあった課題

1. AndroidManifest.xmlのintent-filterの指定とJavaのコードの整合性を取りたい
 - schemeやhostやpathPatternをstrings.xmlに追い出す手も無くはない
2. intent-filterのタグをいっぱい書いてるとよくわからなくなってくる
 - ワイルドカードで書いてもいいけどアプリの仕様によってはそうもいかない
3. パスの中に複数のkey項目があるとき、keyを取り出す処理が煩雑なのをどうにかしたい
 - 欲しい物が1つめだったり2つめだったりしてJava側のコードでゴリゴリ書いてると案の定間違える

## この課題へのアプローチ
- schemeやhostやpathPatternの定義を一箇所に固める
- ビルド時にそれらの定義からファイルを生成する
 - AndroidManifest.xml
 - Javaのenumクラス

## どうやるか
ファイルを生成するスクリプトを強引にgradleのビルドプロセスに割り込ませます。
生成するファイルは[mvel2](https://en.wikisource.org/wiki/MVEL_Language_Guide)というテンプレートからファイルを作れるエンジンを使用します。

[サンプルプロジェクトはこちら](https://github.com/cattaka/GenerateCodeExample)

app/build.gralde [(ソース)](https://github.com/cattaka/GenerateCodeExample/blob/master/app/build.gradle)

```groovy
apply plugin: 'com.android.application'

android {
    /* 省略 */
}

dependencies {
    /* 省略 */
}

// ↓の一行を追加
apply from: 'tasks.gradle'
```

app/tasks.gradle [(ソース)](https://github.com/cattaka/GenerateCodeExample/blob/master/app/tasks.gradle)

```groovy
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'org.mvel:mvel2:2.2.8.Final'
    }
}

import org.mvel2.templates.TemplateRuntime

def placeHolderValues = evaluate(new File(projectDir.absolutePath, "templates/values.gradle"))

// AndroidManifest.xmlを生成する
def manifestTemplate = new File(projectDir.absolutePath, 'templates/AndroidManifest.mvel.xml').text;
def manifestFile = new File(projectDir.absolutePath, 'src/main/AndroidManifest.xml');
manifestFile.text = String.valueOf(TemplateRuntime.eval(manifestTemplate, placeHolderValues));

// Javaのenumクラスを生成する
def helperTemplate = new File(projectDir.absolutePath, 'templates/IntentFilterHelper.mvel.java').text;
def helperFile = new File(projectDir.absolutePath, 'src/main/java/net/cattaka/generatecodeexample/generated/IntentFilterHelper.java');
helperFile.text = String.valueOf(TemplateRuntime.eval(helperTemplate, placeHolderValues));
```

templates/values.gralde [(ソース)](https://github.com/cattaka/GenerateCodeExample/blob/master/app/templates/values.gradle)

```groovy
return [
        metaDataEntries: [
                hoge_key1: "hoge_value1",
                hoge_key2: "hoge_value2",
                hoge_key3: "hoge_value3",
                hoge_key4: "hoge_value4",
        ],
        intentFilters  : [
                [name: "HOGE", scheme: "https", host: "www.cattaka.net", pathPattern: "/hoge/([^/]+)", idIndex: 1],
                [name: "FUGA_FOO", scheme: "https", host: "www.cattaka.net", pathPattern: "/fuga/([^/]+)/foo/([^/]+)", idIndex: 2],
                [name: "BAR", scheme: "https", host: "www.cattaka.net", pathPattern: "/bar/([^/]+)", idIndex: 1],
                [name: "SPAM_HAM", scheme: "https", host: "www.cattaka.net", pathPattern: "/spam/([^/]+)/ham/([^/]+)", idIndex: 2],
        ]
]
```


templates/AndroidManifest.mvel.xml [(ソース)](https://github.com/cattaka/GenerateCodeExample/blob/master/app/templates/AndroidManifest.mvel.xml)

```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.cattaka.generatecodeexample">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            @foreach{entry:intentFilters}
            @code{cPathPattern=entry.pathPattern.replaceAll("\\([^\\)]+\\)",".*")}
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data
                    android:host="@{entry.host}"
                    android:pathPattern="@{cPathPattern}"
                    android:scheme="@{entry.scheme}"/>
            </intent-filter>
            @end{}
        </activity>

        @foreach{entry:metaDataEntries.entrySet}
        <meta-data android:name="@{entry.key}" android:value="@{entry.value}" />
        @end{}
    </application>
</manifest>
```

templates/IntentFilterHelper.mvel.java [(ソース)](https://github.com/cattaka/GenerateCodeExample/blob/master/app/templates/IntentFilterHelper.mvel.java)

```java
package net.cattaka.generatecodeexample.generated;

import android.net.Uri;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by cattaka on 16/05/22.
 */
public enum IntentFilterHelper {
    @foreach{entry:intentFilters}
    @{entry.name}("@{entry.scheme}", "@{entry.host}", "@{entry.pathPattern}", @{entry.idIndex}),@comment{
    }@end{}
    ;
    private final String scheme;
    private final String host;
    private final String pathPattern;
    private final int idIndex;

    IntentFilterHelper(String scheme, String host, String pathPattern, int idIndex) {
        this.scheme = scheme;
        this.host = host;
        this.pathPattern = pathPattern;
        this.idIndex = idIndex;
    }

    public static IntentFilterHelper findHelper(Uri uri) {
        for (IntentFilterHelper helper:values()) {
            if (helper.matches(uri)) {
                return helper;
            }
        }
        return null;
    }

    public boolean matches(Uri uri) {
        return uri != null
                && scheme.equals(uri.getScheme())
                && host.equals(uri.getHost())
                && uri.getPath().matches(pathPattern);
    }

    public String pickKey(Uri uri) {
        Matcher matcher = Pattern.compile(pathPattern).matcher(uri.getPath());
        if (matcher.find()) {
            return matcher.group(idIndex);
        } else {
            return null;
        }
    }
}
```

ところどころ入っている@forech{]や@{entry.scheme}などはmvel2の記法です。

これにより1.と2.の問題は解決しました。
3.の問題はこれだけでは解決しないのですが、この例ではintent-filterについての情報をIntentFilterHelperクラスの中に引き釣り混んでいるので、IntentFilterHelperクラス内に便利メソッドを実装してあげれば対応できます。
たとえば上に挙げたIntentFilterHelperのfindHelperメソッドとmatchesメソッドを組み合わせると、
次のようにしてKeyを取り出すことができます。

MainActivity.javaより抜粋 [(ソース)](https://github.com/cattaka/GenerateCodeExample/blob/master/app/src/main/java/net/cattaka/generatecodeexample/MainActivity.java#L41)

```java
Uri uri = getIntent().getData();
IntentFilterHelper helper = IntentFilterHelper.findHelper(uri);
String key = (helper != null) ? helper.pickKey(uri) : null;

mDataText.setText(String.valueOf(uri));
mKeyText.setText(String.valueOf(key));
```

課題は解決したけど黒魔術には代わりが無いのでご利用は計画的にです。

## サンプルプロジェクト
[サンプルプロジェクトはこちら](https://github.com/cattaka/GenerateCodeExample)