vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Spring boot】サンプルを Doma + log4j2 + Gradle + PostgreSQLで置き換えてみた

はじめに

はじめてのSpring Bootをサンプルの写経をしながら読みました。

ここでは、そのときのサンプルを元に、前から気になっていた下記を使って置き換えてみたときのメモを残します。 * Doma * log4j2 * Gradle * PostgreSQL

Gradle

サンプルではMavenを使っていますが、今回はGradleを使うことにしました。
AndroidなどでもGradleを使いますし、多少こちらの方が慣れているかと思ったので。

※下記の説明に合わせて順番に試していく場合は一度に置き換えるのではなく、1つずつ追加・変更していってください。

build.gradle

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

sourceCompatibility = 1.8

buildscript {
    ext {
        springBootVersion = '1.5.1.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}
jar {
    baseName = 'loginlogout'
    version = '0.0.1-SNAPSHOT'
}
repositories {
    mavenCentral()
}
dependencies {
    compile('org.springframework.boot:spring-boot-starter')
    compile("org.springframework.boot:spring-boot-starter-web")
    compile ("org.springframework:spring-jdbc")
    runtime('org.postgresql:postgresql')
    compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16")
    compile "org.seasar.doma.boot:doma-spring-boot-starter:1.1.0"
    compileOnly "org.projectlombok:lombok:1.16.12"
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

後述するDomaやlog4j2、PostgreSQLも追加しています。

PostgreSQL

サンプルではDBとして組み込みのH2を使っていますが、今回はAmazon RDS上に作成したPostgreSQLのテーブルにアクセスすることにしました。

まずSoftware Design 2016年10月号の記事などを参考にRDSにPostgreSQLのDBを「customerdemo」という名前で作成します。

DBが使用可能になったら、GradleにPostgreSQL(runtime(‘org.postgresql:postgresql’))を追加し、src/main/resourcesにあるapplication.propertiesを下記のように変更します。
※あとでDomaを使用するときにConfigクラスに移動させます。

application.properties

spring.datasource.url=jdbc:log4jdbc:postgresql://DBのエンドポイント:DBのポート番号/customerdemo
spring.datasource.username=DB作成時に設定したユーザー名
spring.datasource.password=DB作成時に設定したパスワード
  • 「customerdemo」はDBの名前です。

このまま実行すると、テーブルが見つからないとエラーになるため、src/main/resourcesに「scheme.sql」を作成します。

scheme.sql

CREATE TABLE IF NOT EXISTS customers(
  id SERIAL NOT NULL,
  first_name VARCHAR (50),
  last_name VARCHAR (50)
);

scheme.sql」は起動時に自動で実行される、ということなので、テーブルが存在しなかった場合は追加するようにします。
実行して、エラーが出ることなくページが表示されればOKです。

log4j2

サンプルではログを取得するために「log4jdbc-remix」を使用しています。
しかし開発がストップしているとのこと。

ということで、「log4jdbc-log4j2」を使ってみます。

まずはGradleのlog4jdbcを変更します。

 compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16")

そしてapplication.propertiesを下記のように変更します。

application.properties

spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy

spring.datasource.url=jdbc:log4jdbc:postgresql://DBのエンドポイント:DBのポート番号/customerdemo
spring.datasource.username=DB作成時に設定したユーザー名
spring.datasource.password=DB作成時に設定したパスワード

logging.level.jdbc=OFF
logging.level.jdbc.sqltiming=DEBUG

また、application.propertiesと同じくsrc/main/resourcesに「log4jdbc.log4j2.properties」と「logback-spring.xml」を作成します。

log4jdbc.log4j2.properties

log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator

logback-spring.xml

< ?xml version="1.0" encoding="UTF-8"?>
< configuration>
    < include resource="org/springframework/boot/logging/logback/base.xml"/>
    < logger name="jdbc.sqlonly"        level="DEBUG"/>
    < logger name="jdbc.sqltiming"      level="INFO"/>
    < logger name="jdbc.audit"          level="INFO"/>
    < logger name="jdbc.resultset"      level="ERROR"/>
    < logger name="jdbc.resultsettable" level="ERROR"/>
    < logger name="jdbc.connection"     level="DEBUG"/>
< /configuration>

最後に、Configクラスを作成します。今回は「MainConfig」というクラスを作成しました。

MainConfig.java

package jp.example.config;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;

import javax.sql.DataSource;

@Configuration
public class MainConfig{
    @Bean
    @ConfigurationProperties("spring.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }
}

実行してエラーが出ることなくログが取得できればOKです。

Doma

Domaはデータベースにアクセスするためのフレームワークです(カンペを見ながら)。
「はじめてのSpring Boot」ではJPAを使って実装していた部分を置き換えます。

実は去年勉強会に出てからずっと気にはなっていたのですが、 触る機会がないままだったので、ここぞとばかりに試してみることにしました。

まずSpring Boot用のものを使うため、Gradleに「compile “org.seasar.doma.boot:doma-spring-boot-starter:1.1.0"」を追加します。

Domaを利用する上で最低限必要となるクラスが下記の3つです。

Configクラス

接続先のDBのURLなどを指定します。つまり先程application.propertiesに書いていた内容をこのクラスにまとめます。

MainConfig.java

package jp.example.config;

import org.seasar.doma.jdbc.Config;
import org.seasar.doma.jdbc.SimpleDataSource;
import org.seasar.doma.jdbc.dialect.Dialect;
import org.seasar.doma.jdbc.dialect.PostgresDialect;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;


@Configuration
public class MainConfig implements Config {
    private static final SimpleDataSource dataSource;
    private static final Dialect dialect = new PostgresDialect();

    static {
        dataSource = new SimpleDataSource();
        dataSource.setUrl("jdbc:log4jdbc:postgresql://DBのエンドポイント:DBのポート番号/customerdemo");
        dataSource.setUser(DB作成時に設定したユーザー名);
        dataSource.setPassword(DB作成時に設定したパスワード);
    }
    @Override
    public DataSource getDataSource(){
        return dataSource;
    }
    @Override
    public Dialect getDialect(){
        return dialect;
    }
    
    @Bean
    @ConfigurationProperties("spring.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }
    
}
  • グレーの部分は先程log4jdbc-log4j2で設定した部分です。
  • 本当であれば「spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy」などもこちらにまとめることができると思うのですが、まだ方法が分からなかったのでそのままapplication.propertiesに残しています。

Entityクラス

DBのテーブルのデータを持つためのクラスです。クラス名、変数はDBのテーブル、カラム名と同じである必要があります。
「はじめてのSpring Boot」のサンプルにおけるDomainクラスの役割だと考えています。
(DomaでもDomainクラスは使用するのですが、今回はスキップしています)

Customers.java

package jp.example.entity;

import org.seasar.doma.Column;
import org.seasar.doma.Entity;
import org.seasar.doma.GeneratedValue;
import org.seasar.doma.GenerationType;
import org.seasar.doma.Id;

@Entity
public class Customers {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    public long id;
    public String first_name;
    public String last_name;
}
  • 「@GeneratedValue(strategy = GenerationType.IDENTITY)」を付けることでInsert時に自動的に値を付与できます。
  • 「@Column(name = “id”)」でカラム名を指定できます。これがないと正しく「id」というカラム名を見つけられず、エラーになっていました。

Daoインターフェイス

InsertやUpdate、SelectのようにDBにアクセスするためのメソッドを持ちます。

CustomerDao.java

package jp.example.dao;

import jp.example.config.MainConfig;
import jp.example.entity.Customers;
import org.seasar.doma.Dao;
import org.seasar.doma.Insert;
import org.seasar.doma.Update;
import org.seasar.doma.Delete;
import org.seasar.doma.Select;
import org.seasar.doma.boot.ConfigAutowireable;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@ConfigAutowireable
@Dao(config = MainConfig.class)
public interface CustomerDao {
    @Insert
    @Transactional
    int insert(Customers entity);
    @Update
    @Transactional
    int update(Customers entity);
    @Delete
    @Transactional
    int delete(Customers entity);
    @Select
    List selectAll();
    @Select
    Customer selectById(Integer id);
}

呼び出し

例えばControllerでルートURLにアクセスした場合に全アイテムを表示する、という場合の呼び出しは以下のようにできます。

MainController.java

package jp.example.controller;

import jp.example.dao.CustomerDao;
import jp.example.entity.Customers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
public class MainController {
    private final CustomerDao customerDao;

    @Autowired
    public MainController(CustomerDao customerDao) {
        this.customerDao = customerDao;
    }

    @RequestMapping(path = "/")
    List selectAll(){
        // ルートURLにアクセスしたらアイテムを全て表示.
        return customerDao.selectAll();
    }
}
  • 今回は特に表示部分を作成していないため、検索結果がJson形式で表示されます。
  • IntelliJ IDEAではAutowiredの部分で「Could not autowire.~」とエラーとなりますが、実行自体は問題なくできます。

SQL

Select文については、実行するためのSQLファイルを作成する必要があります。

例えば「selectAll()」というメソッドを追加したい場合、以下の場所に「selectAll.sql」というファイルが必要です。

src/main/resources/META-INF/jp/example/dao/CustomerDao

※jp/example/daoの部分はCustomerDao.javaのパッケージに揃えます。

selectAll.sql

SELECT /*%expand*/* FROM customers

なお、「selectById(Integer id)」のように引数を持つ関数の場合は、SQLの中でも引数の値を使用しないとエラーとなります。

selectById.sql

SELECT /*%expand*/* FROM customers WHERE id = /* id */0

エラー内容

SQLファイル[META-INF/jp/example/dao/CustomerDao/selectById.sql]の妥当検査に失敗しました。メソッドのパラメータ[id]がSQLファイルで参照されていません。

SQLが見つからない

DAOインターフェースにSelect文を追加した時、SQLが見つからない、というエラーが発生しました。

ここでちょっとハマりました。

CustomerDao.java上では上記で正しくSQLを認識してくれるのですが、ビルドするとSQLが見つからないとエラーが発生します。

Error:(25, 20) java: [DOMA4019] ファイル[META-INF/jp/example/dao/CustomerDao/selectById.sql]がクラスパスから見つかりませんでした。ファイルの絶対パスは"~省略~loginlogout\build\classes\main\META-INF\jp\example\dao\CustomerDao\selectById.sql"です。

・・・なんかパスが違う?

対策を調べたところ、Project Structure > Modules > プロジェクト名_main > Paths > Compiler outputを、「Use module compile output path」ではなく、「Inherit project compile output path」に変更することでエラーが無くなりました。

「Rebuild Project」を実行すると元に戻ってしまったりするため、完全とは言えないのですがとりあえずこれでうまく動作するようになりました。

おわりに

とりあえず前から気になっていたものを使ってみる、ということで、正直ほとんどコピペで切って貼っただけ、というもやもやした内容となっています。

まぁ何にせよまず動く環境が手に入ったので、あれこれ試しつつそれぞれ突っ込んで調べてみたいと思います。

参考

Spring Boot

PostgreSQL

Amazon RDS

log4jdbc-log4j2

Doma

Groovyでテキストファイル操作

はじめに

やりたいこと:

  • ローカルに置いてあるテキストファイルに、一定時間ごとに書き込む。
  • 「q」と入力したらストップ。

ちょっと上記のような処理が必要になったため、入門がてらGroovyで書いてみることにしました。

書き込みとタイマー

ベラっと書きたかったのでベタ打ちでパス指定したテキストファイルに、2種類の内容を交互に書きます。

class WriterTask extends TimerTask{
    def targetFile = new File("test.txt")
    boolean flag = false
    void run(){
        targetFile.text = (flag)? "Test1": "Test2"
        flag = !flag
    }
}
Timer timer = new Timer()
timer.schedule(new WriterTask(), 0, 5000)

あまりJavaと変わらない気がするのは気のせいです。
(そもそもTimerやTimerTaskは、Javaのものを利用しているので仕方ないところがあります)

以下のimport文が不要なのは、Groovyではデフォルトでインポートされているから、ということでよりコード量が抑えられていますね。

import java.util.Timer
import java.util.TimerTask

さて、動作自体はこれで問題ないのですが、一度スタートさせると止まらない、という問題点があります。
特にGroovyConsoleで動かすとInterruptボタンも押せず、タスクマネージャーで無理やり止めたりしていました。

これはこれで気になるのですが、ここでは標準入力を受け取って、「q」が入力されたらストップさせる、ということにします。

入力を受けつける

ターミナルなどからの入力を受け付ける場合は、BufferedReader使うのが良さそうです。

class WriterTask extends TimerTask{
    def targetFile = new File("C:\\Users\\masanori\\test.txt")
    boolean flag = false
    void run(){
        targetFile.text = (flag)? "Test1": "Test2"
        flag = !flag
    }
}
def getInput(){
    def reader = new BufferedReader(new InputStreamReader(System.in))
    reader.lines().any{ line ->
        if(line == "q") {
            System.exit(1)
        } else {
            return true
        }
    }
}
Timer timer = new Timer();
timer.schedule(new WriterTask(), 0, 5000)
getInput()

おわりに

Twitterで話題になっていたこともあり挑戦したGroovyですが、今回のようにサッと書いて動かしたい、という場合に便利そうです。
(規模が大きくなる場合は、慣れの問題もあるとは思いますが、静的に型を持っている言語の方が今のところは良いかな、と思っています)

お手軽度で言えばGroovyConsoleで書いて実行、というのが良さそうですが、IntelliJの入力補完も便利で、慣れや処理の規模に合わせて選択するのも良さそうです。

あと個人的にGroovyに触れる機会が多いと思っているGradleについてももう少し調べてみたいところ。

ということで、のんびりペースにはなると思いますが、あれこれ試していきたいと思います。

参考

【Unity】Windows Store AppでUniRxのIObservableを使う

はじめに

ハマったのでメモ。

※今回の方法でとりあえず動作することは確認しましたが、正しい対処法かどうかは保証できませんのであしからず。

UniRxの一部のinterfaceは、.Net4.0以降のinterfaceと競合するものがあり、Windows Store App(以下WSA)用にビルドする場合にエラーとなる場合があります。

ここでは名前空間などを使用せずに、「UniRx.IObservable」なら「IObservable」としてね、とありました。

ところが、そのような方法をとっても「IObservable」が見つからないとエラーとなり、うまくビルドできませんでした(Unity5.5.0f3を使用)。

あれこれ試したところ、一応これでビルドができるようになりました。

.Net4.0との切り分け

まず.Net4.0の場合に、UniRxではなくSystem.IObservableが呼ばれるようにしてみます。

#if NETFX_CORE || NET_4_0
using System;
#endif

using UnityEngine;
using UniRx;
using UnityEngine.UI;

public class MainCtrl : MonoBehaviour
{
    public Button CompleteButton;
    private void Start ()
    {
        CompleteButton.onClick.AsObservable()
            .Subscribe(_ =>
            {
                // ボタンが押されたら一旦非表示.
                CompleteButton.gameObject.SetActive(false);
                ExecuteSomething().Subscribe(result => {},
                    () =>
                    {
                        // 一定時間経過後に再表示.
                        CompleteButton.gameObject.SetActive(true);
                    });
            });
    }
    private IObservable ExecuteSomething()
    {
        return Observable.Create(observer =>
        {
            Observable.Timer(System.TimeSpan.FromSeconds(2d))
                .Subscribe(tim => { }, () =>
                {
                    observer.OnNext(0);
                    observer.OnCompleted();
                });
            return new CompositeDisposable();
        });
    }
}
  • 「using System;」を分けているのは、「IObservable」の競合を避けるためです。

2017.01.31 7.21更新
ExcecuteSomethingの処理をNETFX_CORE、.Net4.0用に分けて書いていましたが、
内容が全く同じ(実行時に「UniRx.IObservable」と「System.IObservable」が自動で切り替わる)なため削除しました。

ただし、これだけだとまだエラーが発生していました。

で、あれこれ探っていたところ、下記のページを見つけました。

ここの中で、UniRxの「IObservable」を、「NETFX_CORE」環境では除外する提案がされています。
もしや…と思い、Assets/Plugins/UniRx/Scripts/System/IObservable.csを下記のように変更してみました。

IObservable.cs

// defined from .NET Framework 4.0 and NETFX_CORE

using System;

#if !(NETFX_CORE || ENABLE_MONO_BLEEDING_EDGE_EDITOR || ENABLE_MONO_BLEEDING_EDGE_STANDALONE
 || NET_4_0)

namespace UniRx
~省略~
  • 赤字の部分を追加しました。

おわりに

これでエラーが無くなり、無事ビルドすることができるようになりました。

ただしREADMEの内容を私が読み違えている恐れがあるため、これが正しい対処法かどうかは、分からない、という不安は残っています。

今後Unityが.Net4.0以上に対応するときには、この辺の問題も解決済みかもしれませんが、まぁ何かの役に立てば、ということでここに書き残しておきます。

参考

UniRxでObservableを作る・つなげる

はじめに

Observableは作れる!ということで(完全に思いつきで発言しています)、CreateやSelectManyを使ってみます。

なお、本来はRxJavaとUniRxの対応表を作ってみたい 1の続編にするつもりでしたが、今回はUniRxのみ扱います。

Observableを作る

例えば時間のかかる処理を行う関数で、戻り値としてIObservable<T>を返すことで、処理の完了を呼び出し側で受け取れるようにします。

private IObservable ExecuteSomething()
{
        return Observable.Create(observer =>
        {
            // 何らかの処理.
            observer.OnNext(0);
            observer.OnCompleted();
            return new CompositeDisposable();
        });
}

Observable.Create<T>{何らかの処理}でObservableを作ります。

  • <T>の型はintだけでなくstringやTexture2Dなども使用可能ですが、関数の戻り値IObservable<T>と、Observable.Create<T>、observer.OnNext(T)の型を合わせる必要があります。
     (というかOnNextで呼び出し元に値を渡すことができるので、そこで必要な値の型を指定します)
  • {何らかの処理}では戻り値としてDisposableを渡します。
     これは変数に入れて渡すことも可能ですが、Createのときに新規作成する必要があります  (Startなどで1回しか作成しないと、1回Completeした後Observable.Create直後に完了してしまうようになります)。
  • {何らかの処理}の中でobserver.OnNext(T)、observer.OnComplete()、observer.OnError(Exception)を呼び出すことで、呼び出し元に通知することができます。

呼び出し

このExecuteSomething()を呼び出すには、ExecuteSomething().Subscribe()を実行します。
Subscribeしないと{何らかの処理}の内容が実行されないので注意が必要です
(私はたまに忘れます\(^o^)/)。

ExecuteSomething()
    .Subscribe(
        result => {/* observer.OnNext実行時に呼ばれる(resultはOnNext(T)の引数) */},
        error => {/* observer.OnError実行時に呼ばれる(errorはOnError(Exception)の引数) */},
        () => {/* observer.OnComplete実行時に呼ばれる */});
  • 上記はOnNext、OnError、OnComplete全て書いていますが、不要な場合はOnError、OnCompleteを省略できます。

Error

{何らかの処理}の中でErrorが発生した時に呼び出し元に通知が欲しい場合はobserver.OnError(Exception)を実行します。

例えば下記の場合は引数としてFileNotFoundExceptionを渡します。

private IObservable LoadTexture(string path)
{
    return Observable.Create(observer =>
    {
        if (!File.Exists(path))
        {
            observer.OnError(new FileNotFoundException("ファイルがないアルヨ"));
            // OnErrorを実行しても処理自体は止まらないのでreturnで処理を中断.
            return new CompositeDisposable();
        }
~省略~
    });
}

なお、OnErrorを呼び出し元に返すためには呼び出し元、呼び出される側(ExecuteSomething、LoadTexture)の両方でOnErrorの処理を書いておく必要があり、どちらかが書かれていない場合は通常のErrorと同じように扱われます。

ここは注意が必要かもしれません。

private IObservable LoadTexture(string path)
    {
        return Observable.Create(observer =>
        {
            // パスから画像読み込み.
            var newTexture = new Texture2D(1, 1);
            newTexture.LoadImage(File.ReadAllBytes(path));
            observer.OnNext(newTexture);
            observer.OnCompleted();
            return new CompositeDisposable();
        });
    }

Observableをつなげる

例えば画像を読み込んで、それをMaterialのTextureにセットする処理を行う場合。

それぞれ下記のような関数を作ってみました。

private IObservable LoadTexture(string path)
{
    // パスから画像を読み込む.
    return Observable.Create(observer =>
    {
        if (!File.Exists(path))
        {
            observer.OnError(new FileNotFoundException("ファイルがないアルヨ"));
            return new CompositeDisposable();
        }
        var newTexture = new Texture2D(1, 1);
        newTexture.LoadImage(File.ReadAllBytes(path));
        observer.OnNext(newTexture);
        observer.OnCompleted();
        return new CompositeDisposable();
    });
}
private IObservable AttachMainTexture(Texture targetTexture)
{
    // Materialに画像をセットする.
    return Observable.Create(observer =>
    {
        TargetMaterial1.SetTexture("_MainTex", targetTexture);
        observer.OnCompleted();
        return new CompositeDisposable();
    });
}

これを実行する場合に、それぞれをSubscribeすることもできますが、 下記のように一纏めにすることができます。

// LoadTextureを呼び出して画像を読み込む.
LoadTexture(Application.dataPath + @"/files/image1.png")
    // LoadTextureのOnNextで渡された画像を使ってAttachMainTextureを呼び出す.
    .SelectMany(texture => AttachMainTexture(texture))
                .Subscribe(
                    result => Debug.Log("OnNext"),
                    () => Debug.Log("finished"));
  • あるObservableの処理が終わったあと、OnNextで渡された値を使ってそのまま別のObservableをSubscribeしたい場合、「SelectMany」を使うことができます。
  • 同じものを呼び出したい場合などは「Concat」を使います。
  • SubscribeのOnNext、OnError、OnCompleteは後から読んでいるAttachMainTextureのものが反映されるため、例えばLoadTextureのOnCompleteのタイミングで何かをしたい、ということであればSelectManyなどを使わず分割した方が良さそうです。

参考

【Unity】ScriptからMaterialにTextureを設定する

はじめに

UnityのScript(C#)からMaterialにNormalmapなどのTextureを設定する方法をメモっておきます。

MaterialにTextureをセットする

例えばMaterialにNormalmapのTextureをセットするには下記を実行します。

public Texture NormalmapTexture;
public Material TargetMaterial;

private void Start(){
    TargetMaterial.SetTexture("_BumpMap", NormalmapTexture);
}

「SetTexture」の第一引数にプロパティ名を、第二引数に設定するTextureを指定します。

他にもDiffuse(オブジェクトのベースカラー)を設定したい場合は「MainTex」、Specularなら「SpecGlossMap」(Materialの種類を「Standard(Specular setup)」などに変更する必要があります)で設定できます。
※Diffuseについては「Material.mainTexture」でも設定できます。

さて、このプロパティ名は何を参照すればよいでしょうか。

ShaderのEditorを開く

  1. まずInspector上で変更対象のMaterialを開き、Material名の上で右クリック -> Edit Shader を開きます。

f:id:mslGt:20170118073249j:plain

  1. 「Properties」から名前を探します(左側がプロパティ名です)。

f:id:mslGt:20170118073303j:plain

参考

2016 -> 2017

今週のお題「2017年にやりたいこと」

大晦日ハッカソンの休憩がてら今年の振り返りと来年の目標をば。

atnd.org

2016年の振り返りと2017年について

発表について

Twitterなどで書いていたかは覚えていませんが、2016年は発表の場にどんどん挑戦してみよう、と考えていました。

ブログでの記録を見ると7回程。

発表の機会をくれた関西モバイルアプリ研究会、Kansai.kt、umeda.apkの方々には改めて感謝するとともに、
来年もどんどん挑戦させてほしいなと考えています。

また、これまで話を聴いたり見ているだけだった勉強会にも、発表者として参加できるチャンスを狙っていきたいと思います。

一方で課題として、あまり処理の深い部分にたどり着けていないのでは、というところがあります。

Kansai.kt #2でDecompileに触れましたが、2017年は処理が裏でどのように動いているのか、なぜそうなっているのか、
という部分にも突っ込んでみたいと思います。

公開したアプリについて

これもブログ頼りですが、個人で作成したアプリをストアに公開したのも今年ですね。

【Android】リリースしました(和歌山トイレマップで遊んでみる 8) - vaguely

随分ほったらかしなので、これの更新もしなきゃですが、もう一つWebで何か公開してみたいと思っています。
(というか去年もWebやりたいとか言ってたような気はしますが...)

ただ、何を作るのかというのが決まっていないため、まずはそこからかなぁ。

Advent Calendarについて

2015年と比較して、結構たくさん参加させてもらいました。
ブログに書いたのは下記ですが、Twitterなどで参加した朝こざけ #asakzk Advent CalendarBeer Advent Calendarもあります。

日付の縛りがあるため大変なときもありますが、参加すると楽しいですね。

今年は前もって参加登録していたものもありますが、大部分は12月に入って空きがあって、
かつ自分が記事を書けそうなところに登録する、というスタンスで進めていました。

このやり方は来年も続けようかな。

そこそこ埋まっている(ただし空きはある)ようなところに参加して、「今日を埋めたら続くかな?」とちょっとドキドキしつつ書くのも楽しかったので。

2017年もよろしくお願いいたします

というわけで、相変わらず思いつくまま全力でぶつかっていきたいと思います。

このブログをご覧の皆様、2016年は大変お世話になりました。

そして2017年も何卒よろしくお願いいたしますm( )m

JavaFX + Apache POIでSpreadsheet操作 2

はじめに

前回の続きです。

今回は名前付きのセルから値を取得するなど、Apache POIを使ってSpreadsheetから値を取得したり、書き込んだりした内容をまとめます。

セルの名前からWorkbook上のセルの列番号を取得する

まずはWorkbookを開いて、セルの名前(「ID」)を元に対象セルの列番号を取得します。
さらに値が含まれる最大の行番号を取得して、IDの最大値を取得してみます。

FileInputStream fileStream = new FileInputStream(filePath);
// Workbookを開く.
Workbook workbook = WorkbookFactory.create(fileStream);
if(workbook == null){
    observer.onError(new Throwable("Workbookの取得に失敗しました。"));
}
// とりあえずSheet名は固定.
Sheet sheet = workbook.getSheet("Sheet1");
if(sheet == null){
    observer.onError(new Throwable("Sheetの取得に失敗しました。"));
}
// 「ID」という名前がつけられたセルを取得する.
Name name = targetWorkbook.getName("ID");
CellReference reference = new CellReference(name.getRefersToFormula());

//  対象セルの列番号を取得.
int idColumnNum = reference.getCol();
// 対象Sheetの値が入った最終行番号を取得する.
int lastRowNum = sheet.getLastRowNum();
int newId = 1;

// 0行目は項目名を入力しているため値は取得しない.
if(lastRowNum > 0){
    Row lastRow = sheet.getRow(lastRowNum);
    if(lastRow != null){
        Cell lastCell = lastRow.getCell(idColumnNum);
        // 数値を取得すると、「1.0」のような形になるため一旦float型で受取り、その後intに変換する.
        float lastId = NumParser.TryParseFloat(lastCell.toString());
        newId = (int)lastId + 1;
    }
}
else{
    // IDが未登録の場合は1を付与する.
    newId = 1;
}

セルの値を取得する

以前触れましたが、セルの値を取得するには「Cell.getStringCellValue」のように型を指定した上で行います。

この型が実際のセルの型と違っている場合はエラーが発生するため、「Cell.getCellType」で型を確認した上で値を取得していました。

しかし、POI ver.3.15ではこの「getCellType」がdeprecatedで表示されます。

ver.3系ではintが返るのですが、ver.4系ではこれが廃止され、enumが返る「getCellTypeEnum」が「getCellType」に改名される、という変更のためのようです。
とりあえずver.3.15では「getCellType」への対処方法が見つからなかったため、deprecatedでも気にせず使うか、上記のように「Cell.toString」とString型として一旦受け取るなどする必要がありそうです。

指定シート上のセルの名前からセルの列番号を取得する

上記のコードは、セルの名前がWorkbook全体に適用されている、またはWorkbook内に該当の名前が一つしかない場合は問題なく動作します。
しかし、セルの名前がSheet単位で登録されていて、Workbook全体で見ると同一の名前が複数登録されている場合は一番左にあるSheetの値が返ってしまいます。

CellReferenceがWorkbookではなくSheetから取得できれば良さそうですが、調べた限り難しいようでした。

あれこれ試した結果、Workbookに付与されている名前を一旦全て取得して、対象のシートのものだけを取り出すことで解決できました。
(もう少しスマートな方法があるような気はするのですが)

CellValueGetter.java

public class CellValueGetter {
    private int columnNum;
    public List getTargetCellValueList(Workbook workbook, String sheetName){
        // 指定されたSheet内のセルの名前を全て取得する.
        List targetNames = workbook.getAllNames();
        return targetNames.stream()
                .filter(filteredName -> filteredName.getSheetName().equals(sheetName))
                .map(object -> (Name)object)
                .collect(Collectors.toList());
    }
    public int getTargetCellColumnNum(List cellNameList, String targetCellName){
        // targetCellNameの名前が付けられたセルの列番号を返す.
        // 対象のNameを取得.
        Optional foundCellName = cellNameList.stream().filter(name -> name.getNameName().equals(targetCellName)).findFirst();

        // Nullでなければ列番号を取得.
        Optional foundColumnNum = foundCellName.map(cellName -> {
            CellReference reference = new CellReference(cellName.getRefersToFormula());
            if(reference == null){
                return -1;
            }
            int gotColmnNum = (int)reference.getCol();
            return gotColmnNum;
        });
        columnNum = -1;
        foundColumnNum.ifPresent(num -> columnNum = num);
        return columnNum;
    }
}

下記のように呼び出します。

// 指定したシート内の名前を全て取得する.
List allCellNameList = cellValueGetter.getTargetCellValueList(workbook, sheet.getSheetName());
// 「ID」と名付けられたセルの列番号を取得する.
int idColumnNum = cellValueGetter.getTargetCellColumnNum(allCellNameList, "ID");

セルの追加

取得したIDから次のIDを発行し、書き込みます。

// 現在値が入っている行番号最大値を取得する.
int lastRowNum = sheet.getLastRowNum();
int newId = 1;
if(lastRowNum > 0){
    Row lastRow = sheet.getRow(lastRowNum);
    if(lastRow != null){
        Cell lastCell = lastRow.getCell(idColumnNum);

        float lastId = NumParser.TryParseFloat(lastCell.toString());
        newId = (int)lastId + 1;
    }
}
else{
    // IDが未登録の場合は1を付与する.
    newId = 1;
}
// 行を追加する.
Row newRow = sheet.createRow(lastRowNum + 1);

if(newRow != null){
    // セルを追加する.
    newRow.createCell(idColumnNum).setCellValue(newId);
    newRow.createCell(titleColumnNum).setCellValue(title);

    // 現在の日付を取得.
    LocalDate currentDate = LocalDate.now();
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
    newRow.createCell(lastUpdateDateColumnNum).setCellValue(currentDate.format(formatter));

    FileOutputStream outputStream = new FileOutputStream(filePath);
    // Workbookへの書き込み.
    workbook.write(outputStream);
    outputStream.close();
}
  • 新しい行(値の入ったセルが一つもない行)、新しいセルに値を追加する場合はCreateを行う必要があります。
  • 今回更新日はString型でセットしていますが、POIでセルに値をセットする場合に扱うことのできる型にCalendarがあるため、日付型を使いたい場合はCalendar型にしてあげると良いかと思います。

終わりに

さて、これでID取得部分はできるようになりました。

次はこれを使ってもう一つのSpreadsheetに値を入れたり、シートの表示・非表示切り替えを行いたいと思います。

参考

Apache POI

Stream

Optional

Date