Gradle と Querydsl Scala を使った Querydsl JPA のコード生成
Gradle と Querydsl Scala を使って Querydsl JPA の Scala 用コード生成を試してみました。
ソースは 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 JPA の Scala 用コードを生成 | 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.srcDir
へ src/main/qdsl-generated
を追加しています。
なお、(2) で (1) のクラスをロードできるように buildscript
の classpath
へ build/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 直後は、modelCompile
と generate
以降のタスクを分けて実行する必要があります。 (上の方でも書きましたが buildscript
の classpath
へ build/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.level
へ FINE
を設定しています。
(a) 単方向: @OneToMany + @JoinColumn
まずは、@OneToMany
と @JoinColumn
を使った単方向の一対多の関連を実現します。
エンティティクラス
product テーブル用のエンティティクラス Product
内で @OneToMany
と @JoinColumn
を使います。
@JoinColumn
の name
を使って 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
で @OneToMany
、ProductVariation
で @ManyToOne
を用います。
@OneToMany
の mappedBy
を使って @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
によって適用されてしまう) Product
と ProductVariation
の toString
を交互に呼び出し続ける事になってしまうので、@ToString(exclude = "product")
としています。
実行クラス
基本的に (a) と同じですが、ProductVariation
へ Product
を設定しなければならない点が異なります。
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 の利用を承認する際に scope
で https://spreadsheets.google.com/feeds/
と指定します。 (手順は前回を参照)
アクセストークンの取得
リフレッシュトークンからアクセストークンを REST API で取得するには、
https://www.googleapis.com/oauth2/v3/token
へ client_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']/@href
の jikzt4
はそのセルのバージョン番号です。
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 を発行します。
- プロジェクトを選択
- 「APIと認証」の「認証情報」をクリック
- 「新しいクライアントIDを作成」をクリック
- 「インストールされているアプリケーション」を選択し、「クライアントIDを作成」 をクリック (アプリケーションの種類は "その他")
- 「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 利用
最後に Gmail の API を使ってメールの情報を取得してみます。
ここでは以下のモジュールを使いました。
リフレッシュトークンを GoogleCredential
へ setRefreshToken
で設定しておけばアクセストークンを自動的に取得して処理してくれるようです。
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 アカウントで利用したい場合には以下のような事をしなければならないようです。
- Google Apps ドメインの成り代わり機能 ※ を使う
どちらにしても、ドメインが gmail.com
の Gmail アカウント等へ成り代わるのは無理そうですし、サービスアカウントで使える API は限られるかもしれません。
※ 同じドメインに所属する全てのユーザーへ成り代わる権限を サービスアカウントへ付与する機能
(2) の方法は自分のアカウントで API を利用できますが、基本的に Web 画面でログインして API の利用を承認する必要があります。(ただし、初回のみ)
承認するとアクセストークン(有効期限あり)とリフレッシュトークン(有効期限なし)を取得できるので、次からはリフレッシュトークンでアクセストークンを取得し直す事ができます。
サービスアカウントの発行
まずは Google Developers Console へログインしサービスアカウントを発行します。
- プロジェクトを選択
- 「APIと認証」の「認証情報」をクリック
- 「新しいクライアントIDを作成」をクリック
- 「サービスアカウント」を選択し、「クライアントIDを作成」 をクリック (キータイプは JSON)
- キーファイルを保存
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 へ生成されます。
※ ビルドによって生成された JAR と依存モジュールの JAR
例えば zip ファイルの中身は以下のようになります。
zip ファイル内の構成例
- sample_app.zip
- bin
- sample_app
- sample_app.bat
- lib
- sample_app.jar
- bin
起動スクリプトを自動生成してくれるのは非常に便利なのですが、自前で起動スクリプトを用意したい場合もあると思います。
src/dist/bin へ同名の起動スクリプトを用意する方法では、zip・tar ファイル内の bin に同名ファイルが 2重に登録されてしまいました。 (自動生成の方が後に追加される)
これでは不便なので自動生成を無効化する方法を探ってみました。
起動スクリプトの自動生成を無効化
結果的に startScripts.enabled
へ false を設定すれば無効化できました。
build.gradle
apply plugin: 'application' // 起動スクリプトの自動生成を無効化 startScripts.enabled = false ・・・
Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現3
Java のアノテーションプロセッサを使って下記と同等の機能を実現する試みの第三弾です。
前回 のものを改良し、ようやく下記のような構文を実現しました。
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
へ置換処理 (JCLambda
を JCMethodInvocation
へ差し替える事になる) を設定するようにしました。
主な処理内容は次のようになっています。
- (1) 変数定義(
JCVariableDecl
)やメソッド実行(JCMethodInvocation
)の箇所で該当部分を差し替えるための処理をchangeNode
へ設定 - (2) ラムダの内容からソースコードを生成 (対象外なら何もしない)
- (3) ソースコードを
JCExpression
へパースして (実体はJCMethodInvocation
)pos
の値を調整 - (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]