Gradle と Querydsl Scala を使った Querydsl JPA のコード生成

Gradle と Querydsl Scala を使って Querydsl JPAScala 用コード生成を試してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150727/

はじめに

Gradle を使った Querydsl JPA のコード生成」 ではアノテーションプロセッサを使って Querydsl JPA のコードを生成しましたが、Scala の場合は com.querydsl.codegen.GenericExporter クラスを使うようです。

GenericExporter でコード生成するには JPA のエンティティクラスをロードできなければなりません。 (つまり、エンティティクラスを事前にコンパイルしておく必要あり)

Gradle ビルド定義

エンティティクラスを Querydsl のコード生成前にコンパイルするため、今回は以下のようにエンティティクラスだけをコンパイルするタスク modelCompile と Querydsl 用のコードを生成するタスク generate を追加しました。

番号 概要 タスク名
(1) エンティティクラスをコンパイル modelCompile
(2) (1) のエンティティクラスを使って Querydsl JPAScala 用コードを生成 generate
(3) (2) で生成したソースをビルド compileScala

(1) では src/main/scala-model へ配置したエンティティクラスのソース (Scala) をビルドして build/classes/main へ出力します。

(2) では com.querydsl.codegen.GenericExporter を使って Scala 用の Querydsl JPA コードを src/main/qdsl-generated へ生成します。

(3) で (2) の生成したソースをビルドできるように sourceSets.main.scala.srcDirsrc/main/qdsl-generated を追加しています。

なお、(2) で (1) のクラスをロードできるように buildscriptclasspathbuild/classes/main を追加しているのですが、これが原因で初回実行時や clean 直後は (1) と (2) を別々に実行する必要があります。

これは、build/classes/main へクラスファイルが配置されていない状態 ((1) の実施前) で Gradle を実行すると given scan urls are empty. set urls in the configuration とメッセージが出力され、以降のタスクで build/classes/main をクラスパスとして認識しない事が原因です。

build.gradle
apply plugin: 'scala'

// スキャン対象の JPA エンティティクラスのパッケージ名
ext.modelPackage = 'sample.model'
// JPA エンティティクラスのソースディレクトリ
ext.modelSourceDir = 'src/main/scala-model'
// Querydsl のソース生成先ディレクトリ
ext.qdslDestDir = 'src/main/qdsl-generated'

buildscript {
    // JPA エンティティクラスのビルド結果の出力先ディレクトリ
    // buildscript の classpath へ設定する必要があるため、ここで定義している
    ext.destDir = "$buildDir/classes/main"

    repositories {
        jcenter()
    }

    dependencies {
        classpath 'com.querydsl:querydsl-codegen:4.0.2'
        classpath 'com.querydsl:querydsl-scala:4.0.2'

        classpath 'org.scala-lang:scala-library:2.11.7'

        classpath 'javax:javaee-api:7.0'
        // コード生成時に JPA エンティティクラスをロードさせるための設定
        classpath files(destDir)
    }
}

repositories {
    jcenter()
}

dependencies {
    compile 'com.querydsl:querydsl-jpa:4.0.2'
    compile 'com.querydsl:querydsl-scala:4.0.2'

    compile 'org.scala-lang:scala-library:2.11.7'

    compile 'org.apache.commons:commons-dbcp2:2.1'
    compile 'javax:javaee-api:7.0'
}

// (1) JPA エンティティクラスをコンパイル
task modelCompile(type: ScalaCompile) {
    // ソースディレクトリ
    source = modelSourceDir
    // クラスパスの設定 (buildscript のクラスパスを設定)
    classpath = buildscript.configurations.classpath
    // クラスファイルの出力先
    destinationDir = file(destDir)

    // 以下が必須 (ファイル名やパスは何でも良さそう)
    scalaCompileOptions.incrementalOptions.analysisFile = file("${buildDir}/tmp/scala/compilerAnalysis/compileCustomScala.analysis")
}

// (2) Querydsl JPA の Scala 用コードを生成
task generate(dependsOn: 'modelCompile') {
    def exporter = new com.querydsl.codegen.GenericExporter()
    // コード生成先ディレクトリの設定
    exporter.targetFolder = file(qdslDestDir)

    exporter.serializerClass = com.querydsl.scala.ScalaEntitySerializer
    exporter.typeMappingsClass = com.querydsl.scala.ScalaTypeMappings

    exporter.entityAnnotation = javax.persistence.Entity
    exporter.embeddableAnnotation = javax.persistence.Embeddable
    exporter.embeddedAnnotation = javax.persistence.Embedded
    exporter.skipAnnotation = javax.persistence.Transient
    exporter.supertypeAnnotation = javax.persistence.MappedSuperclass
    // Scala ソースの出力
    exporter.createScalaSources = true
    // コード生成の実施
    exporter.export(modelPackage)
}

// (3) ソースをビルド
compileScala {
    // generate タスクとの依存設定
    dependsOn generate
    // Querydsl のコード生成先ディレクトリを追加
    sourceSets.main.scala.srcDir qdslDestDir
}

clean {
    delete qdslDestDir
}

サンプルアプリケーション

それでは簡単なサンプルアプリケーションを作成し実行してみます。

ビルド定義

先程の build.gradle へ少しだけ手を加え、EclipseLink と MySQL を使ったサンプルアプリケーション sample.SampleApp を実行するようにしました。

build.gradle
apply plugin: 'scala'
apply plugin: 'application'

ext.modelPackage = 'sample.model'
ext.modelSourceDir = 'src/main/scala-model'
ext.qdslDestDir = 'src/main/qdsl-generated'
// 実行クラス
mainClassName = 'sample.SampleApp'

buildscript {
    ・・・
}
・・・
dependencies {
    compile 'com.querydsl:querydsl-scala:4.0.2'
    compile 'com.querydsl:querydsl-jpa:4.0.2'
    compile 'org.scala-lang:scala-library:2.11.7'
    compile 'org.apache.commons:commons-dbcp2:2.1'
    compile 'javax:javaee-api:7.0'

    // 実行用の依存ライブラリ
    runtime 'org.eclipse.persistence:eclipselink:2.6.1-RC1'
    runtime 'mysql:mysql-connector-java:5.1.36'
    runtime 'org.slf4j:slf4j-nop:1.7.12'
}
・・・

JPA エンティティクラス

JPA における一対多のリレーションシップ - EclipseLink」 で使った JPA エンティティクラスを Scala で実装し直しました。

src/main/scala-model/sample/model/Product.scala
package sample.model

import javax.persistence._

import java.util.ArrayList
import java.util.List
import java.math.BigDecimal

@Entity
class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = _
    var name: String = _
    var price: BigDecimal = _

    @OneToMany(fetch = FetchType.EAGER, cascade= Array(CascadeType.ALL))
    @JoinColumn(name = "product_id")
    val variationList: List[ProductVariation] = new ArrayList()

    override def toString = s"Product(id: ${id}, name: ${name}, price: ${price}, variationList: ${variationList})"
}
src/main/scala-model/sample/model/ProductVariation.scala
package sample.model

import javax.persistence._

@Entity
@Table(name = "product_variation")
class ProductVariation {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = _
    var color: String = _
    var size: String = _

    override def toString = s"ProductVariation(id: ${id}, color: ${color}, size: ${size})"
}

実行クラス

Querydsl JPA を使った単純な検索処理を行います。

src/main/scala/sample/SampleApp.scala
package sample

import sample.model.Product
import sample.model.ProductVariation
import sample.model.QProduct

import com.querydsl.jpa.impl.JPAQuery

import javax.persistence.Persistence
import java.math.BigDecimal

import scala.collection.JavaConversions._

object SampleApp extends App{
    def product(name: String, price: BigDecimal, variationList: ProductVariation*) = {
        val res = new Product()
        res.name = name
        res.price = price
        variationList.foreach(res.variationList.add)
        res
    }

    def variation(color: String, size: String) = {
        val res = new ProductVariation()
        res.color = color
        res.size = size
        res
    }

    val emf = Persistence.createEntityManagerFactory("jpa")
    val em = emf.createEntityManager()

    val tx = em.getTransaction()
    tx.begin()

    val p1 = product(
        "sample" + System.currentTimeMillis(), 
        new BigDecimal(1250),
        variation("White", "L"),
        variation("Black", "M")
    )

    em.persist(p1)

    tx.commit()

    val p = QProduct as "p"

    val query = new JPAQuery[Product](em)

    // Querydsl JPA による検索
    val res = query.from(p).where(p.name.startsWith("sample")).fetch()
    // 結果の出力
    res.foreach(println)

    em.close()
}

実行

JPA における一対多のリレーションシップ - EclipseLink」 で使った DB や JPA 設定ファイルを使って実行します。

src/main/resources/META-INF/persistence.xml
<persistence
    xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
    version="2.0">

    <persistence-unit name="jpa">
        <class>sample.model.Product</class>
        <class>sample.model.ProductVariation</class>
        <properties>
            <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/jpa_sample" />
            <property name="javax.persistence.jdbc.user" value="root" />

            <property name="eclipselink.logging.level" value="FINE" />
        </properties>
    </persistence-unit>
</persistence>

初回実行時や clean 直後は、modelCompilegenerate 以降のタスクを分けて実行する必要があります。 (上の方でも書きましたが buildscriptclasspathbuild/classes/main を設定している事が原因です)

エンティティクラスのコンパイル (modelCompile タスクの実行)
> gradle modelCompile

given scan urls are empty. set urls in the configuration
:modelCompile

以下のファイルが生成されます。

  • src/main/qdsl-generated/sample/model/QProduct.scala
  • src/main/qdsl-generated/sample/model/QProductVariation.scala
実行結果 (run タスクの実行)
> gradle run

:compileJava UP-TO-DATE
:modelCompile
:generate
:compileScala
:processResources
:classes
:run
・・・
Product(id: 3, name: sample1, price: 100, variationList: [ProductVariation(id: 4, color: Black, size: M), ProductVariation(id: 5, color: White, size: L)])
Product(id: 4, name: sample1437821487341, price: 1250, variationList: [ProductVariation(id: 6, color: White, size: L), ProductVariation(id: 7, color: Black, size: M)])

JPA における一対多のリレーションシップ - EclipseLink

EclipseLink 2.6.1 RC1 を使って JPA の一対多リレーションシップを下記 2通りで試し、SQL の実行内容などを調査してみました。

  • (a) 単方向: @OneToMany + @JoinColumn
  • (b) 双方向: @OneToMany + @ManyToOne

簡単にまとめると次の通りです。

タイプ 使用したアノテーション one側のデータ登録SQL many側のデータ登録SQL
(a) 単方向 @OneToMany, @JoinColumn insert insert, update
(b) 双方向 @OneToMany, @ManyToOne insert insert

(a) の場合に insert だけでは無く update も実施していました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150720/

はじめに

テーブル構成

今回使用したテーブル構成は以下の通りです。

product と product_variation が一対多の関係になっています。 (product_variation.product_id で product を参照)

product テーブル
id name price
1 test1 100
2 test2 200
product_variation テーブル
id product_id color size
1 1 Green F
2 1 Blue S
3 2 Red S

DDL 文は以下の通りです。

DDL
CREATE TABLE `product` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(30) NOT NULL,
  `price` decimal(10,0) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `product_variation` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `product_id` bigint(20) NOT NULL DEFAULT 0,
  `color` varchar(10) NOT NULL,
  `size` varchar(10) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

(a) の単方向の場合、データ登録時に下記のような挙動となるため、product_id へ外部キー制約を付けたりすると不都合が生じます。 (product_id のデフォルト値を 0 としているのもそのためです)

  • (1) product_id を指定せずに product_variation へ insert 文を実行
  • (2) update 文で product_id を設定

ビルド定義ファイル

Gradle 用のビルド定義ファイルを以下のようにしました。 動作確認に SampleApp クラスを実行するようになっています。

また、コードを簡素化するため lombok を使っています。

build.gradle
apply plugin: 'application'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

mainClassName = 'sample.SampleApp'

repositories {
    jcenter()
}

dependencies {
    compile 'javax:javaee-api:7.0'
    compile 'org.projectlombok:lombok:1.16.4'

    runtime 'org.eclipse.persistence:eclipselink:2.6.1-RC1'
    runtime 'mysql:mysql-connector-java:5.1.36'
    runtime 'org.slf4j:slf4j-nop:1.7.12'
}

JPA 設定ファイル

JPA の設定ファイルは以下のようにしました。

src/main/resources/META-INF/persistence.xml
<persistence
    xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
    version="2.0">

    <persistence-unit name="jpa">
        <class>sample.model.Product</class>
        <class>sample.model.ProductVariation</class>
        <properties>
            <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/jpa_sample" />
            <property name="javax.persistence.jdbc.user" value="root" />

            <!-- SQL の内容をログ出力するための設定 -->
            <property name="eclipselink.logging.level" value="FINE" />
        </properties>
    </persistence-unit>
</persistence>

SQL の内容をログ出力 (標準出力) するため eclipselink.logging.levelFINE を設定しています。

(a) 単方向: @OneToMany + @JoinColumn

まずは、@OneToMany@JoinColumn を使った単方向の一対多の関連を実現します。

エンティティクラス

product テーブル用のエンティティクラス Product 内で @OneToMany@JoinColumn を使います。

@JoinColumnname を使って join するテーブル (product_variation) の外部キー項目 (product_id) を指定します。

ちなみに、FetchType の指定は必須ではありません。 (今回は FetchType.EAGER 時の SQL 内容を確認するため指定しました)

src/main/java/sample/model/Product.java
package sample.model;

import lombok.AccessLevel;
import lombok.Data;
import lombok.Setter;
import javax.persistence.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

@Data
@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private BigDecimal price;

    // setter メソッドの未定義化
    @Setter(AccessLevel.NONE)
    // 一対多の関連
    @OneToMany(fetch = FetchType.EAGER, cascade= CascadeType.ALL)
    @JoinColumn(name = "product_id")
    private List<ProductVariation> variationList = new ArrayList<>();
}

lombok の @Data を使うと各フィールドの getter・setter メソッドを自動的に定義してくれますが、variationList の setter メソッドは不要なので @Setter(AccessLevel.NONE) を使って無効化しています。

また、product_variation テーブルのエンティティクラス ProductVariation に対する特別な設定は不要です。

src/main/java/sample/model/ProductVariation.java
package sample.model;

import lombok.Data;
import javax.persistence.*;

@Data
@Entity
@Table(name = "product_variation")
public class ProductVariation {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String color;
    private String size;
}

実行クラス

動作確認のための実行クラスです。

2つのバリエーションを追加した商品データを永続化した後、全件検索するようにしてみました。

src/main/java/sample/SampleApp.java
package sample;

import lombok.val;
import sample.model.Product;
import sample.model.ProductVariation;
import javax.persistence.Persistence;
import java.math.BigDecimal;
import java.util.List;

public class SampleApp {
    public static void main(String... args) throws Exception {
        val emf = Persistence.createEntityManagerFactory("jpa");
        val em = emf.createEntityManager();

        val tx = em.getTransaction();
        tx.begin();

        val p1 = product(
            "sample1", "50", 
            variation("White", "L"), 
            variation("Black", "M")
        );

        // 永続化
        em.persist(p1);

        tx.commit();

        val cq = em.getCriteriaBuilder().createQuery(Product.class);
        // 全件検索
        List<Product> res = em.createQuery(cq).getResultList();
        // 結果出力
        res.forEach(System.out::println);

        em.close();
    }
    // Product の作成
    private static Product product(String name, String price, ProductVariation... variations) {
        val res = new Product();

        res.setName(name);
        res.setPrice(new BigDecimal(price));

        for (val v : variations) {
            // バリエーションの追加
            res.getVariationList().add(v);
        }

        return res;
    }
    // ProductVariation の作成
    private static ProductVariation variation(String color, String size) {
        val res = new ProductVariation();

        res.setColor(color);
        res.setSize(size);

        return res;
    }
}

実行

Gradle による実行結果です。

実行結果
> gradle run
・・・
Product(id=1, name=test1, price=100, variationList=[ProductVariation(id=1, color=Green, size=F), ProductVariation(id=2, color=Blue, size=S)])
Product(id=2, name=test2, price=200, variationList=[ProductVariation(id=3, color=Red, size=S)])
Product(id=3, name=sample1, price=50, variationList=[ProductVariation(id=5, color=White, size=L), ProductVariation(id=4, color=Black, size=M)])

SQL の実行内容です。

SQL の実行内容
INSERT INTO PRODUCT (NAME, PRICE) VALUES (?, ?)
    bind => [sample1, 50]

SELECT LAST_INSERT_ID()

INSERT INTO product_variation (COLOR, SIZE) VALUES (?, ?)
    bind => [Black, M]

SELECT LAST_INSERT_ID()

INSERT INTO product_variation (COLOR, SIZE) VALUES (?, ?)
    bind => [White, L]

SELECT LAST_INSERT_ID()

UPDATE product_variation SET product_id = ? WHERE (ID = ?)
    bind => [3, 5]

UPDATE product_variation SET product_id = ? WHERE (ID = ?)
    bind => [3, 4]

----------

SELECT ID, NAME, PRICE FROM PRODUCT

SELECT ID, COLOR, SIZE FROM product_variation WHERE (product_id = ?)
    bind => [1]

SELECT ID, COLOR, SIZE FROM product_variation WHERE (product_id = ?)
    bind => [2]

特徴は次の通りです。

  • product_variation を insert してから update している (update 時に product_id を設定)
  • product_variation 1件毎に 1回 update 文を実行している
  • product_variation を product_id 毎に検索している
  • 永続化した p1 に対しては SQL を実行していない

(b) 双方向: @OneToMany + @ManyToOne

次に@OneToMany@ManyToOne を使った双方向の一対多の関連を実現します。

エンティティクラス

Product@OneToManyProductVariation@ManyToOne を用います。

@OneToManymappedBy を使って @ManyToOne を付与したフィールド名を指定します。

src/main/java/sample/model/Product.java
package sample.model;

import lombok.AccessLevel;
import lombok.Data;
import lombok.Setter;
import javax.persistence.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

@Data
@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private BigDecimal price;

    @Setter(AccessLevel.NONE)
    // 一対多の関連
    @OneToMany(mappedBy = "product", 
        fetch = FetchType.EAGER, cascade= CascadeType.ALL)
    private List<ProductVariation> variationList = new ArrayList<>();
}
src/main/java/sample/model/ProductVariation.java
package sample.model;

import lombok.Data;
import lombok.ToString;
import javax.persistence.*;

@Data
@ToString(exclude = "product") // Product.toString() を呼び出すのを防止
@Entity
@Table(name = "product_variation")
public class ProductVariation {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String color;
    private String size;

    // 多対一の関連
    @ManyToOne
    private Product product;
}

ここで、toString の内容に product フィールドが含まれると (@Data によって適用されてしまう) ProductProductVariationtoString を交互に呼び出し続ける事になってしまうので、@ToString(exclude = "product") としています。

実行クラス

基本的に (a) と同じですが、ProductVariationProduct を設定しなければならない点が異なります。

mappedBy で指定しているのだから、自動で設定してくれても良さそうな気がするのですが。 (もしかすると何か方法があるのかもしれません)

src/main/java/sample/SampleApp.java
・・・
public class SampleApp {
    public static void main(String... args) throws Exception {
        ・・・
    }

    private static Product product(String name, String price, ProductVariation... variations) {
        val res = new Product();

        res.setName(name);
        res.setPrice(new BigDecimal(price));

        for (val v : variations) {
            res.getVariationList().add(v);

            // 下記の設定が必要な点が (a) との違い
            v.setProduct(res);
        }

        return res;
    }
    ・・・
}

実行

Gradle による実行結果です。

実行結果
> gradle run
・・・
Product(id=1, name=test1, price=100, variationList=[ProductVariation(id=1, color=Green, size=F), ProductVariation(id=2, color=Blue, size=S)])
Product(id=2, name=test2, price=200, variationList=[ProductVariation(id=3, color=Red, size=S)])
Product(id=3, name=sample1, price=50, variationList=[ProductVariation(id=5, color=White, size=L), ProductVariation(id=4, color=Black, size=M)])

SQL の実行内容です。

SQL の実行内容
INSERT INTO PRODUCT (NAME, PRICE) VALUES (?, ?)
    bind => [sample1, 50]

SELECT LAST_INSERT_ID()

INSERT INTO product_variation (COLOR, SIZE, PRODUCT_ID) VALUES (?, ?, ?)
    bind => [Black, M, 3]

SELECT LAST_INSERT_ID()

INSERT INTO product_variation (COLOR, SIZE, PRODUCT_ID) VALUES (?, ?, ?)
    bind => [White, L, 3]

SELECT LAST_INSERT_ID()

-----

SELECT ID, NAME, PRICE FROM PRODUCT

SELECT ID, COLOR, SIZE, PRODUCT_ID FROM product_variation WHERE (PRODUCT_ID = ?)
    bind => [1]

SELECT ID, COLOR, SIZE, PRODUCT_ID FROM product_variation WHERE (PRODUCT_ID = ?)
    bind => [2]

(a) とは違って product_variation の insert 時に product_id も設定するようになっています。

Google スプレッドシートを REST API で操作

Google スプレッドシートREST API で操作します。

API の利用には 前回 と同様にリフレッシュトークンを使います。

はじめに

Google スプレッドシートAPI を使うには、Google アカウントで API の利用を承認する際に scopehttps://spreadsheets.google.com/feeds/ と指定します。 (手順は前回を参照)

アクセストークンの取得

リフレッシュトークンからアクセストークンを REST API で取得するには、 https://www.googleapis.com/oauth2/v3/tokenclient_id=<クライアントID>&client_secret=<クライアントシークレット>&grant_type=refresh_token&refresh_token=<リフレッシュトークン> を POST します。

実行例(cURL
$ curl -d "client_id=xxxxx.apps.googleusercontent.com&client_secret=SarzR・・・&grant_type=refresh_token&refresh_token=1/iiM・・・" https://www.googleapis.com/oauth2/v3/token

{
 "access_token": "ya26.pw・・・",
 "token_type": "Bearer",
 "expires_in": 3600
}

取得したアクセストークンは HTTP ヘッダーで指定します。

アクセストークンの指定例 (HTTP ヘッダー)
Authorization: Bearer ya26.pw・・・

(1) スプレッドシートの一覧を取得

スプレッドシートの一覧を取得するには https://spreadsheets.google.com/feeds/spreadsheets/private/full へ GET します。

スプレッドシート取得例(cURL
$ curl -H "Authorization: Bearer ya26.pw・・・" https://spreadsheets.google.com/feeds/spreadsheets/private/full

<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns='http://www.w3.org/2005/Atom'xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'><id>https://spreadsheets.google.com/feeds/spreadsheets/private/full</id>・・・

処理結果(XML)の内容は以下の通りです。

XML 結果例

<feed>
  ・・・
  <entry>
    <id>https://spreadsheets.google.com/feeds/spreadsheets/private/full/1E0R・・・</id>
    <updated>2015-07-01T16:30:20.027Z</updated>
    <category scheme="http://schemas.google.com/spreadsheets/2006" term="http://schemas.google.com/spreadsheets/2006#spreadsheet"/>
    <title type="text">sample</title>
    <content type="text">sample</content>
    <link rel="http://schemas.google.com/spreadsheets/2006#worksheetsfeed" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/worksheets/1E0R・・・/private/full"/>
    <link rel="alternate" type="text/html" href="https://docs.google.com/spreadsheets/d/1E0R・・・/edit"/>
    <link rel="self" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/spreadsheets/private/full/1E0R・・・"/>
    <author>
      ・・・
    </author>
  </entry>
  ・・・
</feed>

(2) スプレッドシート内のシートの一覧を取得

シートの一覧を取得するには https://spreadsheets.google.com/feeds/worksheets/${key}/private/full へ GET します。

${key} の値は (1) で取得した XML の entry/id 要素から取得できます。 (https://spreadsheets.google.com/feeds/spreadsheets/private/full/${key}

シート取得例(cURL
$ curl -H "Authorization: Bearer ya26.pw・・・" https://spreadsheets.google.com/feeds/worksheets/1E0R・・・/private/full

<?xml version='1.0' encoding='UTF-8'?><feed xmlns='http://www.w3.org/2005/Atom'
xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:gs='http://schemas.google.com/spreadsheets/2006'><id>https://spreadsheets.google.com/feeds/worksheets/1E0R・・・/private/full</id>・・・

処理結果(XML)の内容は以下の通りです。

XML 結果例

<feed>
  ・・・
  <entry>
    <id>https://spreadsheets.google.com/feeds/worksheets/1E0R・・・/private/full/od6</id>
    <updated>2015-07-01T16:30:20.010Z</updated>
    <category scheme="http://schemas.google.com/spreadsheets/2006" term="http://schemas.google.com/spreadsheets/2006#worksheet"/>
    <title type="text">シート1</title>
    <content type="text">シート1</content>
    <link rel="http://schemas.google.com/spreadsheets/2006#listfeed" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/list/1E0R・・・/od6/private/full"/>
    <link rel="http://schemas.google.com/spreadsheets/2006#cellsfeed" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full"/>
    <link rel="http://schemas.google.com/visualization/2008#visualizationApi" type="application/atom+xml" href="https://docs.google.com/spreadsheets/d/1E0R・・・/gviz/tq?gid=0"/>
    <link rel="http://schemas.google.com/spreadsheets/2006#exportcsv" type="text/csv" href="https://docs.google.com/spreadsheets/d/1E0R・・・/export?gid=0&format=csv"/>
    <link rel="self" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/worksheets/1E0R・・・/private/full/od6"/>
    <link rel="edit" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/worksheets/1E0R・・・/private/full/od6/6cl・・・"/>
    <gs:colCount>26</gs:colCount>
    <gs:rowCount>1000</gs:rowCount>
  </entry>
  ・・・
</feed>

なお、シートの一覧を取得する URL は、(1) の XML から以下のような XPath 式で取り出す事も可能です。

XPath
//entry[title = '<スプレッドシートのタイトル>']/link[@rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']/@href

(3) シート内のセルを取得

セルを取得するには https://spreadsheets.google.com/feeds/cells/${key}/${worksheetId}/private/full へ GET します。

${worksheetId} は (2) で取得した XML の entry/id 要素から取得できます。 (https://spreadsheets.google.com/feeds/worksheets/${key}/private/full/${worksheetId}

セル取得例(cURL
$ curl -H "Authorization: Bearer ya26.pw・・・" https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full

<?xml version='1.0' encoding='UTF-8'?><feed xmlns='http://www.w3.org/2005/Atom'
xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:batch='http://schemas.google.com/gdata/batch' xmlns:gs='http://schemas.google.com/spreadsheets/2006'><id>https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full</id>・・・

処理結果(XML)の内容は以下の通りです。

XML 結果例

<feed>
  ・・・
  <entry>
    <id>https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C1</id>
    <updated>2015-07-01T16:30:20.010Z</updated>
    <category scheme="http://schemas.google.com/spreadsheets/2006" term="http://schemas.google.com/spreadsheets/2006#cell"/>
    <title type="text">A1</title>
    <content type="text">aaa</content>
    <link rel="self" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C1"/>
    <link rel="edit" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C1/kgy・・・"/>
    <gs:cell row="1" col="1" inputValue="aaa">aaa</gs:cell>
  </entry>
  ・・・
</feed>

なお、セルの一覧を取得する URL は、(2) の XML から以下のような XPath 式で取り出す事も可能です。

XPath
//entry[title = '<シートのタイトル>']/link[@rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']/@href

(4) セルを登録・更新

https://spreadsheets.google.com/feeds/cells/${key}/${worksheetId}/private/full へ以下のようなフォーマットの XML を POST する事でセルを登録 (更新も可) できます。

注意点として、Content-Type を application/atom+xml と指定する必要があります。

セル登録 XML フォーマット例
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:gs="http://schemas.google.com/spreadsheets/2006">
  <id>https://spreadsheets.google.com/feeds/cells/${key}/${worksheetId}/private/full/R${行}C${列}</id>
  <link rel="edit" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/cells/${key}/${worksheetId}/private/full/R${行}C${列}"/>
  <gs:cell row="${行}" col="${列}" inputValue="${値}"/>
</entry>

今回は以下の XML ファイルを使って E1 セルへ sample123 という文字を登録してみます。

e1.xml
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:gs="http://schemas.google.com/spreadsheets/2006">
  <id>https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C5</id>
  <link rel="edit" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C5" />
  <gs:cell row="1" col="5" inputValue="sample123" />
</entry>
セル登録例(cURL
$ curl -X POST -H "Authorization: Bearer ya26.pw・・・" -H "Content-Type: application/atom+xml" -d @e1.xml https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full

<?xml version='1.0' encoding='UTF-8'?><entry xmlns='http://www.w3.org/2005/Atom' xmlns:batch='http://schemas.google.com/gdata/batch' xmlns:gs='http://schemas.google.com/spreadsheets/2006'><id>https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C5</id>・・・

処理結果(XML)の内容は以下の通りです。

XML 結果例

<entry>
  <id>https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C5</id>
  <updated>2015-07-01T16:30:20.010Z</updated>
  <category scheme="http://schemas.google.com/spreadsheets/2006" term="http://schemas.google.com/spreadsheets/2006#cell"/>
  <title type="text">E1</title>
  <content type="text">sample123</content>
  <link rel="self" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C5"/>
  <link rel="edit" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C5/jikzt4"/>
  <gs:cell row="1" col="5" inputValue="sample123">sample123</gs:cell>
</entry>

link[@rel='edit']/@hrefjikzt4 はそのセルのバージョン番号です。

Google アカウントで Google API を利用 - google-api-services-gmail

前回はサービスアカウントを使う方法を試しましたが、今回は Google アカウントを使って Google API を利用してみます。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150621/

はじめに

API 利用までの手順は次の通りです。

  • (1) クライアント ID を発行
  • (2) Google アカウントで API の利用を承認し、コードを取得
  • (3) (2) で取得したコードを使ってリフレッシュトークンを取得
  • (4) (3) で取得したリフレッシュトークンからアクセストークンを取得し API を利用

基本的に (2) と (3) は (API を利用する) Google アカウント毎に 1回だけ実施します。

(3) でアクセストークンも取得できますが、アクセストークンには有効期限があるため、通常は (4) でリフレッシュトークンからアクセストークンを取得し直します。

(1) クライアント ID を発行

Google Developers Console へログインし、ネイティブ アプリケーション用のクライアント ID を発行します。

  1. プロジェクトを選択
  2. APIと認証」の「認証情報」をクリック
  3. 「新しいクライアントIDを作成」をクリック
  4. 「インストールされているアプリケーション」を選択し、「クライアントIDを作成」 をクリック (アプリケーションの種類は "その他")
  5. JSON をダウンロード」をクリックしファイルを保存

保存した JSON ファイルは後の処理で使用します。

サービスアカウントで取得した JSON ファイルとは中身が異なりますのでご注意ください。

JSON ファイルの例
{
    "installed":{
        "auth_uri":"https://accounts.google.com/o/oauth2/auth",
        "client_secret":"・・・",
        "token_uri":"https://accounts.google.com/o/oauth2/token",
        "client_email":"",
        "redirect_uris":["urn:ietf:wg:oauth:2.0:oob","oob"],
        "client_x509_cert_url":"",
        "client_id":"・・・.apps.googleusercontent.com",
        "auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"
    }
}

(2) Google アカウントで API 利用を承認し、コードを取得

この作業は基本的に Web 画面を使って行います。

https://accounts.google.com/o/oauth2/auth?redirect_uri=<リダイレクト先URL>&response_type=code&client_id=<クライアントID>&scope=<利用するAPIのURL> へアクセスし、Google アカウントでログインして API の承認画面で 「承認」 ボタンをクリックします。

scope には承認する API の URL を %20 (半角スペースを URLエンコードしたもの) 区切りで指定します。

以下のように PhantomJS 等を利用し UI レスで実施する事も一応可能でしたが、Captcha の入力があった場合等には使えませんので実用性は低めだと思います。

approve_api.groovy
@Grab('org.gebish:geb-core:0.10.0')
@Grab('com.codeborne:phantomjsdriver:1.2.1')
import geb.Browser
import org.openqa.selenium.phantomjs.PhantomJSDriver
import org.openqa.selenium.remote.DesiredCapabilities

import groovy.json.JsonSlurper

def json = new JsonSlurper()
def conf = json.parse(new File(args[0])).installed

def userId = args[1]
def password = args[2]
// Gmail の API を承認するためのスコープ設定
def scope = [
    'https://mail.google.com/',
    'https://www.googleapis.com/auth/gmail.compose',
    'https://www.googleapis.com/auth/gmail.modify'
].join('%20')

def code = null

Browser.drive {
    setDriver(new PhantomJSDriver(new DesiredCapabilities()))

    def url = "${conf.auth_uri}?redirect_uri=${conf.redirect_uris[0]}&response_type=code&client_id=${conf.client_id}&scope=${scope}"

    go url
    // メールアドレス入力
    $('input[name="Email"]').value(userId)
    $('input[type="submit"]').click()

    // パスワード入力画面に変わるまで待機
    waitFor(30) { $('div.second div.slide-in').isDisplayed() }

    // パスワード入力
    $('input[name="Passwd"]').value(password)
    $('div.second input[type="submit"]').click()

    // API の承認ボタンが有効になるまで待機
    waitFor(30) { $('button[id="submit_approve_access"]').isDisabled() == false }

    // 承認ボタンをクリック
    $('button[id="submit_approve_access"]').click()

    def codeInput = waitFor(30) { $('input[id="code"]') }

    // コードを取得
    code = codeInput.value()

    quit()
}
println code

PhantomJS を実行できるように環境変数 PATH を設定し実行します。

コマンドライン引数の第1引数には (1) で保存した JSON ファイルを指定します。

実行例
> groovy approve_api.groovy client_secret.json xxxx@gmail.com ****
・・・

4/3vJ9・・・

(3) リフレッシュトークンの取得

(2) で取得したコードを使ってリフレッシュトークンを取得します。

https://www.googleapis.com/oauth2/v3/token へコードなどの情報を POST すれば、JSON データとしてアクセストークンとリフレッシュトークンを取得できます。

client_id, client_secret, redirect_uri の値には (1) で保存した JSON ファイル内の情報を使っています。

get_refresh-token.groovy
@Grab("org.apache.httpcomponents:httpclient:4.5")
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.client.methods.HttpPost
import org.apache.http.impl.client.HttpClientBuilder
import org.apache.http.message.BasicNameValuePair

import groovy.json.JsonSlurper

def json = new JsonSlurper()
def conf = json.parse(new File(args[0])).installed

def code = args[1]

def param = { name, value -> new BasicNameValuePair(name, value) }

def client = HttpClientBuilder.create().build()

def post = new HttpPost('https://www.googleapis.com/oauth2/v3/token')

post.entity = new UrlEncodedFormEntity([
    param('code', code),
    param('client_id', conf.client_id),
    param('client_secret', conf.client_secret),
    param('grant_type', 'authorization_code'),
    param('redirect_uri', conf.redirect_uris[0]) //urn:ietf:wg:oauth:2.0:oob
])

def res = client.execute(post)

// 実行結果(JSON)の出力
res.entity.writeTo(System.out)

実行結果は以下の通りです。

実行例
> groovy get_refresh-token.groovy client_secret.json 4/3vJ9・・・ > token.json

成功すると以下のような JSON を取得できます。

処理結果
{
 "access_token": "・・・",
 "token_type": "Bearer",
 "expires_in": 3600,
 "refresh_token": "・・・"
}

(4) アクセストークンの取得と Gmail API 利用

最後に GmailAPI を使ってメールの情報を取得してみます。

ここでは以下のモジュールを使いました。

リフレッシュトークンを GoogleCredentialsetRefreshToken で設定しておけばアクセストークンを自動的に取得して処理してくれるようです。

gmail_list.groovy
@Grab('com.google.apis:google-api-services-gmail:v1-rev31-1.20.0')
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
import com.google.api.client.googleapis.util.Utils
import com.google.api.services.gmail.Gmail

import groovy.json.JsonSlurper

def json = new JsonSlurper()

def conf = json.parse(new File(args[0])).installed
def token = json.parse(new File(args[1]))

def credential = new GoogleCredential.Builder()
    .setTransport(Utils.getDefaultTransport())
    .setJsonFactory(Utils.getDefaultJsonFactory())
    .setClientSecrets(conf.client_id, conf.client_secret)
    .build()
    .setRefreshToken(token.refresh_token) //リフレッシュトークンの設定

def gmail = new Gmail.Builder(
    Utils.getDefaultTransport(), 
    Utils.getDefaultJsonFactory(), 
    credential
).setApplicationName('sample').build()

// メールの情報を最大 3件取得
gmail.users().messages().list('me').setMaxResults(3).execute().messages.each {
    // メールの内容を minimal フォーマットで取得
    def msg = gmail.users().messages().get('me', it.id).setFormat('minimal').execute()
    println msg
}

実行結果は以下の通りです。

実行例
> groovy gmail_list.groovy client_secret.json token.json

[historyId:1000, ・・・ snippet:詳細な管理によって Google アカウントを保護・・・]
・・・

Google API 用のアクセストークンをサービスアカウントで取得 - Google API Client Library for Java

Google の各種 API を使うためのアクセストークンをサービスアカウントを使って取得してみました。

ライブラリは Google API Client Library for Java を使います。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150608/

はじめに

今のところ Google API を使用するには以下のような方法があるようで、 今回は (1) の方法で行います。

番号 方法 用途 アクセストークンの取得方法 Google アカウントで API 利用
(1) サービスアカウント利用 サーバーアプリケーション JSON Web Tokens (JWTs) 制限あり
(2) Google アカウントで承認 Webサーバーアプリケーション, 組み込みアプリケーション Google アカウントで API 利用を承認 or リフレッシュトークン 可能

(1) の方法はログイン等を行わずに API を利用できる反面、API を自分の Google アカウントで利用したい場合には以下のような事をしなければならないようです。

どちらにしても、ドメインgmail.comGmail アカウント等へ成り代わるのは無理そうですし、サービスアカウントで使える API は限られるかもしれません。

※ 同じドメインに所属する全てのユーザーへ成り代わる権限を
   サービスアカウントへ付与する機能

(2) の方法は自分のアカウントで API を利用できますが、基本的に Web 画面でログインして API の利用を承認する必要があります。(ただし、初回のみ)

承認するとアクセストークン(有効期限あり)とリフレッシュトークン(有効期限なし)を取得できるので、次からはリフレッシュトークンでアクセストークンを取得し直す事ができます。

サービスアカウントの発行

まずは Google Developers Console へログインしサービスアカウントを発行します。

  1. プロジェクトを選択
  2. APIと認証」の「認証情報」をクリック
  3. 「新しいクライアントIDを作成」をクリック
  4. 「サービスアカウント」を選択し、「クライアントIDを作成」 をクリック (キータイプは JSON
  5. キーファイルを保存

JSON キーファイルの方が実装するコードが少なくなるので、今回は JSON キータイプ (デフォルト) を選んでいます。

アクセストークンの取得

JSON キーファイルを使ってアクセストークンを取得し、出力する処理は以下のようになります。

SampleApp.java
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;

import java.io.FileInputStream;
import java.util.Arrays;

public class SampleApp {
    public static void main(String... args) throws Exception {
        FileInputStream jsonKeyFile = new FileInputStream(args[0]);

        GoogleCredential credential = GoogleCredential.fromStream(jsonKeyFile)
                .createScoped(Arrays.asList("https://spreadsheets.google.com/feeds/"));
        // アクセストークンの取得
        credential.refreshToken();

        System.out.println("access token: " + credential.getAccessToken());
    }
}

createScoped を使ってアクセストークンで利用する API の URL を指定しています。 (今回は Google スプレッドシートAPI を指定)

refreshToken メソッドを実行する事でアクセストークンを取得します。

なお、refreshToken の実行前に getAccessToken メソッドを実行すると null を返します。

JSON キーファイルの代わりに P12 キーファイルを使う場合は GoogleCredential.Builder 等を使う事になります。

実行

Gradle による実行結果は以下の通りです。 (JSON キーファイルは sample.json です)

実行結果
> gradle -q run -Pargs=sample.json

access token:  yb17.iwR01J2W8Left-・・・

Gradle のビルド定義ファイルは以下の通りです。

build.gradle
apply plugin: 'application'

repositories {
    jcenter()
}

dependencies {
    compile 'com.google.api-client:google-api-client-java6:1.20.0'
}

mainClassName = 'SampleApp'

run {
    if (project.hasProperty('args')) {
        args project.args.split(' ')
    }
}

Google スプレッドシートAPI 利用 - REST

次は、アクセストークンを取得して Google スプレッドシートの単純な API を呼び出してみます。

スプレッドシート用のライブラリは使わず、Apache の HttpClient を使って https://spreadsheets.google.com/feeds/spreadsheets/private/full を GET しました。

アクセストークンは HTTP ヘッダー (Authorization) へ設定します。

SampleApp2.java
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;

import java.io.FileInputStream;
import java.util.Arrays;

public class SampleApp2 {
    private static final String SPREADSHEETS_FEED_BASE = 
            "https://spreadsheets.google.com/feeds/";
    private static final String SPREADSHEETS_FEED_FULL =
            SPREADSHEETS_FEED_BASE + "spreadsheets/private/full";

    public static void main(String... args) throws Exception {
        FileInputStream jsonKeyFile = new FileInputStream(args[0]);

        GoogleCredential credential = GoogleCredential.fromStream(jsonKeyFile)
                .createScoped(Arrays.asList(SPREADSHEETS_FEED_BASE));

        // アクセストークン取得
        credential.refreshToken();

        HttpClient client = HttpClientBuilder.create().build();

        HttpGet get = new HttpGet(SPREADSHEETS_FEED_FULL);
        // アクセストークンをヘッダーへ設定
        get.addHeader("Authorization", "Bearer " + credential.getAccessToken());

        HttpResponse res = client.execute(get);

        // 結果の出力
        res.getEntity().writeTo(System.out);
        System.out.println("");
    }
}

実行

実行結果は以下の通りです。

最初はサービスアカウントで使えるスプレッドシートが存在していないので以下のようになると思います。

実行結果1 (出力結果は見易いように加工しています)
> gradle -q run -Pargs=sample.json

<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'>
  <id>https://spreadsheets.google.com/feeds/spreadsheets/private/full</id>
  <updated>2015-06-07T11:58:26.141Z</updated>
  <category scheme='http://schemas.google.com/spreadsheets/2006' term='http://schemas.google.com/spreadsheets/2006#spreadsheet'/>
  <title type='text'>Available Spreadsheets - ・・・</title>
  <link rel='alternate' type='text/html' href='http://docs.google.com'/>
  <link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='https://spreadsheets.google.com/feeds/spreadsheets/private/full'/>
  <link rel='self' type='application/atom+xml' href='https://spreadsheets.google.com/feeds/spreadsheets/private/full'/>
  <openSearch:totalResults>0</openSearch:totalResults>
  <openSearch:startIndex>1</openSearch:startIndex>
</feed>

Google スプレッドシートスプレッドシートを開き、「共有」をクリックして JSON キーファイルの client_email の値 (サービスアカウントのメールアドレス) を登録した後、再度実行してみると以下のようになりました。

実行結果2 (出力結果は見易いように加工しています)
> gradle -q run -Pargs=sample.json

<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' ・・・>
  ・・・
  <entry>
    <id>https://spreadsheets.google.com/feeds/spreadsheets/private/full/・・・</id>
    <updated>2015-06-07T11:46:41.314Z</updated>
    <category scheme='http://schemas.google.com/spreadsheets/2006' ・・・ />
    <title type='text'>sample</title>
    <content type='text'>sample</content>
    ・・・
  </entry>
</feed>

Gradle のビルド定義ファイルは以下の通りです。

build.gradle
apply plugin: 'application'

repositories {
    jcenter()
}

dependencies {
    compile 'com.google.api-client:google-api-client-java6:1.20.0'
    compile 'org.apache.httpcomponents:httpclient:4.5'
}

mainClassName = 'SampleApp2'

run {
    if (project.hasProperty('args')) {
        args project.args.split(' ')
    }
}

Gradle の起動スクリプト自動生成を無効化する - application プラグイン

Gradle の application プラグインにおける起動スクリプトの自動生成を無効化する方法です。

  • Gradle 2.4

ソースは http://github.com/fits/try_samples/tree/master/blog/20150607/

はじめに

application プラグインを使用して gradle build を実行すると、下記のような構成の zip と tar ファイルが build/distributions へ生成されます。

  • bin へ起動スクリプトを自動生成 (bat と bash 用)
  • lib へ各種 JAR ファイル ※ を配置
  • src/dist 内のディレクトリやファイルをそのままの構成で配置
※ ビルドによって生成された JAR と依存モジュールの JAR

例えば zip ファイルの中身は以下のようになります。

zip ファイル内の構成例
  • sample_app.zip
    • bin
      • sample_app
      • sample_app.bat
    • lib
      • sample_app.jar

起動スクリプトを自動生成してくれるのは非常に便利なのですが、自前で起動スクリプトを用意したい場合もあると思います。

src/dist/bin へ同名の起動スクリプトを用意する方法では、zip・tar ファイル内の bin に同名ファイルが 2重に登録されてしまいました。 (自動生成の方が後に追加される)

これでは不便なので自動生成を無効化する方法を探ってみました。

起動スクリプトの自動生成を無効化

結果的に startScripts.enabled へ false を設定すれば無効化できました。

build.gradle
apply plugin: 'application'

// 起動スクリプトの自動生成を無効化
startScripts.enabled = false
・・・

Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現3

Javaアノテーションプロセッサを使って下記と同等の機能を実現する試みの第三弾です。

  • Haskell の do 記法
  • Scala の for 内包表記
  • F# のコンピュテーション式

前回 のものを改良し、ようやく下記のような構文を実現しました。

Optional<String> res = opt$do -> {
    let a = Optional.of("a");
    let b = Optional.of("b");
    let c = opt$do -> {
        let c1 = Optional.of("c1");
        let c2 = Optional.of("c2");
        return c1 + "-" + c2;
    };
    return a + b + "/" + c;
};

ソースは http://github.com/fits/try_samples/tree/master/blog/20150516/

はじめに

環境

下記のような環境を使ってビルド・実行しています。

  • JavaSE Development Kit 8u45 (1.8.0_45)
  • Gradle 2.4

構文

前回 からの変更点は以下の通りです。

  • 対象のラムダ式JCLambda)を完全に置換し、Supplier を不要にした
  • let で入れ子に対応

対象のラムダ式を別の式 (JCMethodInvocation) で完全に置換し、Supplier を無くした事でまともな構文になったと思います。

変換前 (アノテーションプロセッサ処理前)
Optional<String> res = opt$do -> {
    let a = Optional.of("a");
    let b = Optional.of("b");
    let c = opt$do -> {
        let c1 = Optional.of("c1");
        let c2 = Optional.of("c2");
        return c1 + "-" + c2;
    };
    return a + b + "/" + c;
};
変換後 (アノテーションプロセッサ処理後)
Optional<String> res = opt.bind(
    Optional.of("a"), 
    (a) -> opt.bind(
        Optional.of("b"), 
        (b) -> opt.bind(
            opt.bind(
                Optional.of("c1"), 
                (c1) -> opt.bind(
                    Optional.of("c2"), 
                    (c2) -> opt.unit(c1 + "-" + c2)
                )
            ), 
            (c) -> opt2.unit(a + b + "/" + c)
        )
    )
);

また、変数への代入だけではなく、メソッドの引数にも上記構文を使えるようにしました。

メソッド引数としての使用例
System.out.println(opt$do -> {
    let a = Optional.of("a");
    let b = Optional.of("b");
    return "***" + b + a;
});

アノテーションプロセッサの実装

Processor の実装

前回 とほぼ同じですが、DoExprVisitor の extends 元を com.sun.tools.javac.tree.TreeScanner へ変えたので、accept メソッドの呼び出し箇所が多少変わっています。

なお、JCTree へキャストしていますが、JCCompilationUnit へキャストしても問題ありません。

src/main/java/sample/DoExprProcessor.java
package sample;

import java.util.Set;
import javax.annotation.processing.*;

import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.util.Trees;
import com.sun.source.util.TreePath;

import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.util.Context;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("*")
public class DoExprProcessor extends AbstractProcessor {
    private Trees trees;
    private Context context;

    @Override
    public void init(ProcessingEnvironment procEnv) {
        trees = Trees.instance(procEnv);
        context = ((JavacProcessingEnvironment)procEnv).getContext();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        roundEnv.getRootElements().stream().map(this::toUnit).forEach(this::processUnit);
        return false;
    }

    private CompilationUnitTree toUnit(Element el) {
        TreePath path = trees.getPath(el);
        return path.getCompilationUnit();
    }

    private void processUnit(CompilationUnitTree cu) {
        if (cu instanceof JCTree) {
            ((JCTree)cu).accept(new DoExprVisitor(context));
            // 変換内容を出力
            System.out.println(cu);
        }
    }
}

TreeVisitor の実装

前回 からの変更点は以下の通りです。

  • (a) コード生成部分を別クラス化
  • (b) 対象のラムダ式JCLambda) を全置換
  • (c) メソッド引数への対応
  • (d) extends 元を com.sun.tools.javac.tree.TreeScanner へ変更 (前回までは com.sun.source.util.TreeScanner

(b) を実現するため changeNode へ置換処理 (JCLambdaJCMethodInvocation へ差し替える事になる) を設定するようにしました。

主な処理内容は次のようになっています。

  • (1) 変数定義(JCVariableDecl)やメソッド実行(JCMethodInvocation)の箇所で該当部分を差し替えるための処理を changeNode へ設定
  • (2) ラムダの内容からソースコードを生成 (対象外なら何もしない)
  • (3) ソースコードJCExpression へパースして (実体は JCMethodInvocationpos の値を調整
  • (4) changeNode を実行しラムダ箇所を差し替え
src/main/java/sample/DoExprVisitor.java
package sample;

import com.sun.tools.javac.parser.ParserFactory;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.tree.TreeScanner;
import com.sun.tools.javac.util.Context;

import java.util.function.BiConsumer;
import java.util.stream.Stream;

public class DoExprVisitor extends TreeScanner {
    private ParserFactory parserFactory;
    private BiConsumer<JCLambda, JCExpression> changeNode = (lm, ne) -> {};
    private DoExprBuilder builder = new DoExprBuilder();

    public DoExprVisitor(Context context) {
        parserFactory = ParserFactory.instance(context);
    }

    @Override
    public void visitVarDef(JCVariableDecl node) {
        if (node.init != null) {
            // (b) (1)
            changeNode = (lm, ne) -> {
                // 変数への代入式を置換
                if (node.init == lm) {
                    node.init = ne;
                }
            };
        }
        super.visitVarDef(node);
    }

    // (c)
    @Override
    public void visitApply(JCMethodInvocation node) {
        if (node.args != null && node.args.size() > 0) {
            // (b) (1)
            changeNode = (lm, ne) -> {
                // メソッドの引数部分を置換
                if (node.args.contains(lm)) {
                    Stream<JCExpression> newArgs = node.args.stream().map(a -> (a == lm)? ne: a);
                    node.args = com.sun.tools.javac.util.List.from(newArgs::iterator);
                }
            };
        }
        super.visitApply(node);
    }

    @Override
    public void visitLambda(JCLambda node) {
        // (a) (2)
        builder.build(node).ifPresent(expr -> {
            // (3)
            JCExpression ne = parseExpression(expr);
            fixPos(ne, node.pos);

            // (b) (4) ラムダ部分を差し替え
            changeNode.accept(node, ne);
        });

        super.visitLambda(node);
    }

    // pos 値の修正
    private void fixPos(JCExpression ne, final int basePos) {
        ne.accept(new TreeScanner() {
            @Override
            public void scan(JCTree tree) {
                if(tree != null) {
                    tree.pos += basePos;
                    super.scan(tree);
                }
            }
        });
    }

    // 生成したソースコードをパース
    private JCExpression parseExpression(String doExpr) {
        return parserFactory.newParser(doExpr, false, false, false).parseExpression();
    }
}

コード生成処理の実装

該当のラムダ式を変換したソースコードを生成する処理です。

src/main/java/sample/DoExprBuilder.java
package sample;

import com.sun.tools.javac.tree.JCTree.*;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class DoExprBuilder {
    private static final String DO_TYPE = "$do";
    private static final String VAR_PREFIX = "#{";
    private static final String VAR_SUFFIX = "}";
    // let 用のコードテンプレート
    private static final String LET_CODE = "#{var}.bind(#{rExpr}, #{lExpr} -> #{body})";
    // return 用のコードテンプレート
    private static final String RETURN_CODE = "#{var}.unit( #{expr} )";

    private Map<Class<? extends JCStatement>, CodeGenerator<JCStatement>> builderMap = new HashMap<>();

    public DoExprBuilder() {
        // let 用のコード生成
        builderMap.put(JCVariableDecl.class, (n, v, b) -> generateCodeForLet(cast(n), v, b));
        // return 用のコード生成
        builderMap.put(JCReturn.class, (n, v, b) -> generateCodeForReturn(cast(n), v, b));
    }

    public Optional<String> build(JCLambda node) {
        return getDoVar(node).map(var -> createExpression((JCBlock)node.body, var));
    }

    private String createExpression(JCBlock block, String var) {
        String res = "";

        for (JCStatement st : block.stats.reverse()) {
            res = builderMap.getOrDefault(st.getClass(), this::generateNoneCode).generate(st, var, res);
        }
        return res;
    }

    private String generateNoneCode(JCStatement node, String var, String body) {
        return body;
    }

    // let 用のソースコード生成
    private String generateCodeForLet(JCVariableDecl node, String var, String body) {
        String res = body;

        if ("let".equals(node.vartype.toString())) {
            Map<String, String> params = createParams(var);
            params.put("body", res);
            params.put("lExpr", node.name.toString());
            params.put("rExpr", node.init.toString());

            // 入れ子への対応
            if (node.init instanceof JCLambda) {
                JCLambda lm = cast(node.init);

                getDoVar(lm).ifPresent(childVar ->
                        params.put("rExpr", createExpression((JCBlock) lm.body, childVar)));
            }
            res = buildTemplate(LET_CODE, params);
        }

        return res;
    }

    // return 用のソースコード生成
    private String generateCodeForReturn(JCReturn node, String var, String body) {
        Map<String, String> params = createParams(var);
        params.put("expr", node.expr.toString());

        return buildTemplate(RETURN_CODE, params);
    }

    // 処理変数名の抽出
    private Optional<String> getDoVar(JCLambda node) {
        if (node.params.size() == 1) {
            String name = node.params.get(0).name.toString();

            if (name.endsWith(DO_TYPE)) {
                return Optional.of(name.replace(DO_TYPE, ""));
            }
        }
        return Optional.empty();
    }

    private Map<String, String> createParams(String var) {
        Map<String, String> params = new HashMap<>();

        params.put("var", var);

        return params;
    }

    // テンプレート処理
    private String buildTemplate(String template, Map<String, String> params) {
        String res = template;

        for(Map.Entry<String, String> param : params.entrySet()) {
            res = res.replace(VAR_PREFIX + param.getKey() + VAR_SUFFIX, param.getValue());
        }
        return res;
    }

    @SuppressWarnings("unchecked")
    private <S, T> T cast(S obj) {
        return (T)obj;
    }

    private interface CodeGenerator<T> {
        String generate(T node, String var, String body);
    }
}

Service Provider 設定ファイルやビルド定義は 前回 と同じものです。

Service Provider 設定ファイル

src/main/resources/META-INF/services/javax.annotation.processing.Processor
sample.DoExprProcessor
build.gradle
apply plugin: 'java'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

dependencies {
    compile files("${System.properties['java.home']}/../lib/tools.jar")
}

ビルド

ビルド実行
> gradle build

:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

ビルド結果として build/libs/java_do_expr.jar3 が生成されます。

動作確認

下記のサンプルコードを使ってアノテーションプロセッサの動作確認を行います。

example/DoExprSample.java
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.Optional;

public class DoExprSample {
    public static void main(String... args) {
        Optional<Integer> o1 = Optional.of(2);
        Optional<Integer> o2 = Optional.of(3);

        Opt<Integer> opt = new Opt<>();

        Optional<Integer> res = opt$do -> {
            let a = o1;
            let b = o2;
            let c = Optional.of(4);
            return a + b + c * 2;
        };
        // Optional[13]
        System.out.println(res);

        Opt<String> opt2 = new Opt<>();

        Optional<String> res2 = opt2$do -> {
            let a = Optional.of("a");
            let b = Optional.of("b");
            let c = opt2$do -> {
                let c1 = Optional.of("c1");
                let c2 = Optional.of("c2");
                return c1 + "-" + c2;
            };
            return a + b + "/" + c;
        };
        // Optional[ab/c1-c2]
        System.out.println(res2);

        // Optional[***ba]
        System.out.println(opt2$do -> {
            let a = Optional.of("a");
            let b = Optional.of("b");
            return "***" + b + a;
        });
    }

    static class Opt<T> {
        public Optional<T> bind(Optional<T> x, Function<T, Optional<T>> f) {
            return x.flatMap(f);
        }

        public Optional<T> unit(T v) {
            return Optional.ofNullable(v);
        }
    }
}

java_do_expr3.jar を使って上記ソースファイルをコンパイルします。

出力内容(変換後のソースコード)を見る限りは変換できているようです。

コンパイル
> javac -cp ../build/libs/java_do_expr3.jar DoExprSample.java

・・・
public class DoExprSample {
    ・・・
    public static void main(String... args) {
        Optional<Integer> o1 = Optional.of(2);
        Optional<Integer> o2 = Optional.of(3);
        Opt<Integer> opt = new Opt<>();
        Optional<Integer> res = opt.bind(o1, (a)->opt.bind(o2, (b)->opt.bind(Optional.of(4), (c)->opt.unit(a + b + c * 2))));
        System.out.println(res);
        Opt<String> opt2 = new Opt<>();
        Optional<String> res2 = opt2.bind(Optional.of("a"), (a)->opt2.bind(Optional.of("b"), (b)->opt2.bind(opt2.bind(Optional.of("c1"), (c1)->opt2.bind(Optional.of("c2"), (c2)->opt2.unit(c1 + "-" + c2))), (c)->opt2.unit(a + b + "/" + c))));
        System.out.println(res2);
        System.out.println(opt2.bind(Optional.of("a"), (a)->opt2.bind(Optional.of("b"), (b)->opt2.unit("***" + b + a))));
    }
    ・・・
}

DoExprSample を実行すると正常に動作しました。

実行結果
> java DoExprSample

Optional[13]
Optional[ab/c1-c2]
Optional[***ba]