bokusui について

ソフトウェアハウスでのPG・SEから始まり、10年近く勤めた金融系企業の社内SEを数年前にやめ、フリーランス時代を経たのち法人成りしました。システム開発の全工程をこじんまりとやり続けています。

ラズパイ4で各センサーを試してAWS Iotにpublishしてみた

 前回の続きで、今度はKEYESTUDIOスターターキットに含まれていた各センサーを試してみます。
 まず、温度湿度センサーですが、キット付属のセンサーはDHT11というモノ、サイトのコードはパット見pythonに変換するのが面倒なので、下記を参考にしました。
[blogcard url=”https://github.com/szazo/DHT11_Python”]
 簡単に温度と湿度の値が取れましたが、たまにゼロのケースがあるようなので、正しく計測するにはちょっと考慮が必要ですね。

 次は土壌水分センサーです。キットのサイトを参考に、I2Cを有効にし、Cのソースをそのままpythonに置き換えてすぐに値が取れるようになりました。

import wiringpi

Address = 0x48
BASE = 64
A0 = BASE + 0
A1 = BASE + 1
A2 = BASE + 2
A3 = BASE + 3

wiringpi.wiringPiSetup()
wiringpi.pcf8591Setup(BASE, Address)
while True:
    value = wiringpi.analogRead(A1)
    print("A1:" + str(value))
    wiringpi.delay(500)

 キット付属の照度センサーとかも指定するpinを変えるだけで取れました。ただ、照度センサーの場合はキット付属の3種の抵抗から1つを使用するのですが、3種の違いが見た目で解らず、配線が厄介です。
 
 使えそうなセンサーの値が取れるようになったので、これらを定期的にAWS Iotにpublishする事で最適な水やりタイミングを分析出来るようにしてみました。データはCSV形式でこんな感じです。

2020-04-19 00:05:11.97121800,19.3,49.0,134,254

 データは、日時、温度、湿度、土壌水位、照度をAWS Athenaのクエリーで取れるようにしておきます。

 Athenaでは、日時をtimestamp型にする場合、ミリ秒を8桁にしないとデータとして認識しないようなので、無理やりゼロを付けてます。ただ、AthenaではタイムゾーンをJSTとして扱うのが面倒な感じなので、無理にtimestamp型にしなくてもよかったかも?

なお、AWS IOTからS3バケットへの格納するキーは、

${clientid()}/${parse_time("yyyyMM", timestamp(), "Asia/Tokyo")}/${parse_time("dd", timestamp(), "Asia/Tokyo")}/${timestamp()}.csv

のように年月と日付をディレクトリにして、あとでログ管理をしやすくしておきます。ま、この程度ならS3もAthenaも当面コストほとんどゼロでいけるでしょう。

 これで各センサーの値によって水やりを制御する形が大体出来てきました。が、最後に肝心な水やり制御方法をどうするかについては、まだ検討中です。。

ラズパイ4でKEYESTUDIOスターターキットを試してみた。

 自動水やりを目指して購入したRaspberryPi4ですが、今回の目的に使えそうな下記のキットを購入しました。
[blogcard url=”https://www.amazon.co.jp/dp/B0798DYZQW”]
使う予定なのは、サーボと温度・湿度、土壌湿度センサーでしょうか。ラズパイも電子工作も素人なので、まずは各パーツを試していきます。


 LABISTS Raspberry4のケースには合わないですが、とりあえず、GPIO-PCF8591 Shieldを取り付けて、使用するパーツを指してみます。

 最初に、サーボを試してみます。KEYSTUDIOのサイトですが、C言語ですね。。お久しぶりです。pythonでやりたいところですが、まずは動作確認でキットのサイトにあるコードをそのままやってみます。下記はMakefileです。


Servo:Servo.o
gcc Servo.c -o Servo -lwiringPi

 Makefileのタブが曲者ですね。


pi@raspberrypi:~/lesson/19 $ ls
Makefile Servo.c
pi@raspberrypi:~/lesson/19 $ make
cc -c -o Servo.o Servo.c
gcc Servo.c -o Servo -lwiringPi
pi@raspberrypi:~/lesson/19 $ ls
Makefile Servo Servo.c Servo.o

 コンパイル出来たようです。


pi@raspberrypi:~/lesson/19 $ sudo ./Servo

 、、、うんともすんとも動きませんね。。。


pi@raspberrypi:~/lesson/19 $ gpio readall
Oops - unable to determine board type... model: 17

 gpioが使えていないようです。インストールはされているようですが、


pi@raspberrypi:~/lesson/19 $ dpkg -l wiringpi
要望=(U)不明/(I)インストール/(R)削除/(P)完全削除/(H)保持
| 状態=(N)無/(I)インストール済/(C)設定/(U)展開/(F)設定失敗/(H)半インストール/(W)
|/ エラー?=(空欄)無/(R)要再インストール (状態,エラーの大文字=異常)
||/ 名前 バージョン アーキテクチ 説明
+++-==============-============-============-===================================
ii wiringpi 2.50 armhf The wiringPi libraries, headers and

 どうやら、バージョンが対応していないようです。コチラを参考にさせて頂き、バージョンアップしてみます。


pi@raspberrypi:~/lesson/19 $ gpio readall
+-----+-----+---------+------+---+---Pi 4B--+---+------+---------+-----+-----+
| BCM | wPi | Name | Mode | V | Physical | V | Mode | Name | wPi | BCM |
+-----+-----+---------+------+---+----++----+---+------+---------+-----+-----+
| | | 3.3v | | | 1 || 2 | | | 5v | | |
| 2 | 8 | SDA.1 | IN | 1 | 3 || 4 | | | 5v | | |
| 3 | 9 | SCL.1 | IN | 1 | 5 || 6 | | | 0v | | |
| 4 | 7 | GPIO. 7 | IN | 1 | 7 || 8 | 1 | IN | TxD | 15 | 14 |
| | | 0v | | | 9 || 10 | 1 | IN | RxD | 16 | 15 |
| 17 | 0 | GPIO. 0 | IN | 0 | 11 || 12 | 0 | IN | GPIO. 1 | 1 | 18 |
| 27 | 2 | GPIO. 2 | IN | 0 | 13 || 14 | | | 0v | | |
| 22 | 3 | GPIO. 3 | IN | 0 | 15 || 16 | 0 | IN | GPIO. 4 | 4 | 23 |
| | | 3.3v | | | 17 || 18 | 0 | IN | GPIO. 5 | 5 | 24 |
| 10 | 12 | MOSI | IN | 0 | 19 || 20 | | | 0v | | |
| 9 | 13 | MISO | IN | 0 | 21 || 22 | 0 | IN | GPIO. 6 | 6 | 25 |
| 11 | 14 | SCLK | IN | 0 | 23 || 24 | 1 | IN | CE0 | 10 | 8 |
| | | 0v | | | 25 || 26 | 1 | IN | CE1 | 11 | 7 |
| 0 | 30 | SDA.0 | IN | 1 | 27 || 28 | 1 | IN | SCL.0 | 31 | 1 |
| 5 | 21 | GPIO.21 | IN | 1 | 29 || 30 | | | 0v | | |
| 6 | 22 | GPIO.22 | IN | 1 | 31 || 32 | 0 | IN | GPIO.26 | 26 | 12 |
| 13 | 23 | GPIO.23 | IN | 0 | 33 || 34 | | | 0v | | |
| 19 | 24 | GPIO.24 | IN | 0 | 35 || 36 | 0 | IN | GPIO.27 | 27 | 16 |
| 26 | 25 | GPIO.25 | IN | 0 | 37 || 38 | 0 | IN | GPIO.28 | 28 | 20 |
| | | 0v | | | 39 || 40 | 0 | IN | GPIO.29 | 29 | 21 |
+-----+-----+---------+------+---+----++----+---+------+---------+-----+-----+
| BCM | wPi | Name | Mode | V | Physical | V | Mode | Name | wPi | BCM |
+-----+-----+---------+------+---+---Pi 4B--+---+------+---------+-----+-----+

 Cのサンプルが動くようになりました。

 Cのコードをそのまま、pythonに書き換えます。

import time
import wiringpi as w

#Define
INPUT=0
OUTPUT=1
HIGH=1
LOW=0

w.wiringPiSetup()
w.pinMode(1,OUTPUT)

while 1:
  for i in range(50):
    w.digitalWrite(1,HIGH)
    time.sleep(1000 / 1000000.)
    w.digitalWrite(1,LOW)
    time.sleep(19 / 1000.)

  time.sleep(1)

  for i in range(50):
    w.digitalWrite(1,HIGH)
    time.sleep(2000 / 1000000.)
    w.digitalWrite(1,LOW)
    time.sleep(18 / 1000.)

  time.sleep(1)


pi@raspberrypi:~ $ sudo python3 Servo.py
Traceback (most recent call last):
File "Servo.py", line 2, in
import wiringpi as w
ModuleNotFoundError: No module named 'wiringpi'

 ライブラリが足りてませんでした。。


pi@raspberrypi:~ $ pip3 install wiringpi
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting wiringpi
Downloading https://files.pythonhosted.org/packages/06/bf/7c4ec17172f72917707dddeacfa02eae80b56ad3b7b5674a4258e62b2f5a/wiringpi-2.60.0-cp37-cp37m-linux_armv7l.whl (285kB)
100% |????????????????????????????????| 286kB 495kB/s
Installing collected packages: wiringpi
Successfully installed wiringpi-2.60.0

 実行してみます

pi@raspberrypi:~ $ python3 Servo.py

 お、カチャカチャと動きだしました。細かい制御は難しそうなので後回しに、、

 次は温度・湿度センサーです。

ラズパイ4のモバイルバッテリー稼働時間を計測してみた

 ここ数年、ベランダで育てている野菜等を今年こそまともに収穫したいなあと、水やり自動化を検討していました。
 いろいろネットをみていると、arduinoやRaspberry Piで実現している例が多数あり、拡張性が高そうだし、仕事ではあまり使う機会が無いPythonの学習にもなるかとRaspberry Pi4でやってみようと、こちらがちょっと安くなっていたので買ってみました。

[blogcard url=”https://www.amazon.co.jp/dp/B07ZNTX1R5″]

 到着後、ボードをケースにねじ止めし、ヒートシンクを張り、OS入りのSDメモリを指し、USBキーボードを接続、有線LANを指し、HDMIでテレビに接続、電源を入れただけで簡単にデスクトップが表示されました。

 普通にサクサク動くパソコンです。ただ、ファンのピンの指し方が間違っているのか壊れているのか動きません。。
 
 今回の目的としてはベランダにラズパイを置く事になるのですが、残念ながらベランダにコンセントはありません。そこで、まずモバイルバッテリーでどの程度稼働するのかを確認してみました。デスクトップから無線LANの設定をして、BluetoothはOff、デスクトップは使わないのでCLIのみに設定し、VNCサーバは起動しないようにして、以後は別のPCからSSHでログインして操作する形にします。

 適当に10分おきにログ出力するpythonを書いて、何年も使っているダンボーのモバイルバッテリー 10400mAh で確認した結果、750分=12時間30分でした。HDMI出力オフ(tvserviceコマンド)すると、さらに100分持ちましたが、これではどうにもなりません。水やりが自動化されても、毎日バッテリー変えていたら本末転倒ですし、、

 色々調べましたが、モバイルバッテリーでラズパイをほぼ常時動作させるのは、ほぼ無理っぽいです。素直にベランダへコンセントを通すしか手は無いですね。

eLTAXから住民税の特別徴収を納税してみた

 既に殆どの納税がeTax等でネットから出来るようになっているのですが、一つだけ、出来ていなかったのが住民税の特別徴収でした。
 これは法人が従業員の代わりに市区町村へ毎月納税するものなのですが、毎月するものなので、小さい法人にとってはこれが一番面倒な納税です。
 基本的に振込金額は1年で決められており、市区町村から1年分纏めて送られてくる毎月分の振込用紙を使い、毎月の納付期限付近に金融機関に行って振り込んだりしていたのですが、去年からやっとeLTAXで納税がしやすくなったようですので、そろそろeLTAXから納税する事にしました。

 eLTAXのPC用アプリであるPCdeskから納税するのですが、筆者は既にPCdeskを使っているので、今回はアップデートだけです。以前は本当に使い勝手の悪いアプリだったのですが、だいぶ良くなったようです。

 まず金融機関の登録が必要なので、PCdeskの納税メニューから口座情報の登録を行います。っと言っても、口座情報を画面に入力すると申込書を印刷して捺印し郵送という形です。郵送後、1週間くらい経ってPCdesk宛に登録通知メッセージが届きます。これで準備完了です。

 で、実際の納税ですが、納付情報発行依頼という画面から、届いている振込用紙を元に金額等を入力し、登録。その後、納税操作を行うという内容で、即時口座から引き落としされてました。

 これで納税の為に銀行に行く手間が無くなり便利になりました。が、このPCdeskの画面に出てくる「納付情報発行依頼」って言葉、なんでこんな表記なんですかね?裏で何をやっているのか解らないですが、もう少し利用者目線の表記にしてくれませんかね?

Springboot Securityのログイン画面がいい感じになっていた

 Springbootで作ったアプリを、そろそろユーザーに渡そうかというタイミングになりましたが、Springbootのバージョンが結構更新されているので、今更ながら最新に入れ替えてみました。
 それまでフレームワークは、spring-boot-starter-parentの2.0.1.RELEASEを指定していましたが、今見ると2.1系の次に2.2系が既に出ています。
 コードに影響があれば、そこまで最新にしなくてもいいかととりあえず2.2.4を試したところ、特に問題が無かったのでチェンジ。
 今回のアプリはsecurityのデフォルトログイン画面を使っていたのですが、更新したところ画面が変わりました。どうやら2.1から既に変わっていたようです。

2.0系はこれ

2.1系overはこれ

2.0系の見ためがしょぼいのでカスタマイズしようか考えましたが、中央寄せのいい感じのUIに変わってました。これならそのままでも違和感無いですね。

アプリの動作確認は終了、最後に、バージョンアップでこんな警告が出るようになったみたいなので、
2020-02-27 14:09:12 WARN JpaBaseConfiguration$JpaWebConfiguration spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning

出ないようにapplication.ymlに追加

spring:
   jpa:
     open-in-view: false

受け渡し準備OKです。
でもjarのファイルサイズはどんどん肥大化してます。。

Mavenインストールが失敗するようになった

SpiringBootで作成中のツールにちょっと機能が漏れていたので、pom.xmlにライブラリを追加したところ、ダウンロード出来ませんとの事。

[INFO] Scanning for projects...
[INFO] 
[INFO] Using the builder org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder with a thread count of 1
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building api-tool 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] Downloading: http://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-security/2.0.1.RELEASE/spring-boot-starter-security-2.0.1.RELEASE.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.814 s
[INFO] Finished at: 2020-01-29T10:56:41+09:00
[INFO] Final Memory: 15M/159M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal on project api-tool: Could not resolve dependencies for project api-tool:api-tool:jar:0.0.1-SNAPSHOT: Failed to collect dependencies at org.springframework.boot:spring-boot-starter-security:jar:2.0.1.RELEASE: Failed to read artifact descriptor for org.springframework.boot:spring-boot-starter-security:jar:2.0.1.RELEASE: Could not transfer artifact org.springframework.boot:spring-boot-starter-security:pom:2.0.1.RELEASE from/to central (http://repo.maven.apache.org/maven2): Failed to transfer http://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-security/2.0.1.RELEASE/spring-boot-starter-security-2.0.1.RELEASE.pom. Error code 501, HTTPS Required -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/DependencyResolutionException

mavenリポジトリから501が応答されているのですが、何か変わったのでしょうか?
試しに、ブラウザ越しに上記のDownload URLをたたくと下記の応答

501 HTTPS Required.
Use https://repo.maven.apache.org/maven2/
More information at https://links.sonatype.com/central/501-https-required

httpsにしなくちゃいけないって事?

ちょっと調べると下記を発見。
[blogcard url=”https://arimodoki.mydns.jp/promenade/s_mavenpom.html”]

今時当たり前なのですが、Mavenリポジトリがhttpsのみに変わったようです。

Mavenツールが古いからかな?とまずeclipse(marsを今でも使用中)から更新の確認を実行してもダメ。
インストールの詳細からm2e Eclipse用Maven統合 を更新してもダメでした。

上記リンクに、pom.xmlで設定するのは例が書かれていますので、有難くこれで一旦解決には出来ますが、プロジェクト個別設定でなくて全体的にどうにかならないの?と、.m2に置いてあるsetting.xmlの設定で対応してみます。

結果、リポジトリ設定ではうまくいかず、ミラーの設定をしてみると、ちゃんとライブラリをダウンロードしてくれるようになりましたが、
初回はミラーから全てのライブラリをダウンロードしようとするようなので、とりあえずはpom.xmlで個別に対応した方がいいかもしれません。

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
  <mirrors>
    <mirror>
      <id>repo.maven.apache.org</id>
      <name>https maven2 </name>
      <url>https://repo.maven.apache.org/maven2</url>
      <mirrorOf>central</mirrorOf>
    </mirror>
  </mirrors> 
</settings>

後日、別環境で使っているeclipseで確認したところ、比較的新しいeclipseでは特に問題が無かったようです。どうやら、古いバージョンのeclipseだけ対応出来ないようです。

カテゴリー: Java

redmineでライフログ的記録を取る

 毎年1月になると法人として法定調書を提出する必要があるのですが、年に1回だけのイベントなので、翌年になるとどのように手続きしたかをすっかり忘れて困った事になってしまいます。。
そこで、数年前から後で困らないようにとredmineで備忘録的に記録する事にしました。

今の所、決算期毎にプロジェクト分け、「タスク」という簡単なトラッカーを用意し、期日管理しつつ記録を取っていくという感じです。備忘録が目的なので進捗管理とかは考えていません。

これで、毎回その時が来る度に調べたり考えたりしないでもスムーズに進める事が最近は出来てきたように思います。

で、他にもこういう記録を取っておきたいな、と思ったのが本を読んだ記録です。何を読んで、どうだったかが全く記憶に残せないクチなので、ちょっとしたメモレベルでよいので記録を取る事にしました。

これで、ちょっとは思い出せそうですが、最近たいして読んでないなとも気付かされます。
他にも株式投資をする時にこの銘柄がいいと思ったのは何だったかを検討開始時や購入時に記録しておくとか、ベランダ植物の栽培記録とかもしてましたが、将来的なメリットを感じられないでただ記録しているような内容だとあまり続かないですね。栽培記録は最初だけで止まってます。。

個人的なredmineサイトなら、こんな感じのテーマ事に分類された手動ライフログツールとしても使えるので重宝してます。

w2uiでテーブルメンテナンス画面を作ってみる

SpringBootで作っているアプリに、テーブルメンテナンス画面が必要になったので、w2uiで作ってみる事にしました。
[blogcard url=”http://w2ui.com/web”]
テーブルメンテナンス画面といえば、一覧表示があって、選択したら登録フォームが表示されて、とか基本的な構成はどのテーブルも似たようなものですよね。w2uiのグリッドとフォームを使い、使いまわし出来るようjavascriptに機能を纏め、html側にテーブル固有の個別設定を寄せる事で、いろんなテーブルに対応出来るような形にしていきます。
w2uiのformは、urlを指定するとthis.save()でフォームをPOSTしてくれるようなのですが、ちょっと扱いにくいのでsaveを使わずにjsonでPOSTするようにしました。

eclipse上のリソース構成はこんな感じです。

javascriptです。ちょっと変更後のUIの動きがアバウトではありますが。。

//編集表示
function showEditForm(isEdit){
   $('#data-form').show(); 
   $('#data-grid').hide(); 
	w2ui['data_form'].clear();
	w2ui['data_form'].header = uiName;
	
	if(isEdit){
		var sel = w2ui['data_grid'].getSelection();
		if (sel.length == 1) {
        	var record = w2ui['data_grid'].get(sel[0]);
        	w2ui['data_form'].record = record;
		}
	}
    w2ui['data_form'].refresh();
}

//一覧表示
function showGrid(){
   $('#data-grid').show(); 
   $('#data-form').hide(); 
   $.ajax({
      headers:{ "Accept":"text/html,application/xhtml+xml,application/xml,application/json",},
      url:gridURL})
      .fail((jqXHR, textStatus, errorThrown) => {
         if( jqXHR.status == 404 ){
            w2alert('該当するデータはありません'+ jqXHR.responseText, uiName);
         }else{
            w2alert('データが取得出来ませんでした。 ' + jqXHR.status + jqXHR.responseText, uiName);
         }
      })
      .done((data, textStatus, jqXHR) => {
         if(data == ""){
            w2alert('該当する情報はありません', uiName);
            return;
         }
         var json=JSON.parse(data);
         var idx = 0;
         if(!Array.isArray(json)){
            json = JSON.parse('[' + JSON.stringify(json) + ']');
         }
         for(var item of json){
            idx++;
            item['recid']=idx;
         }
         w2ui['data_grid'].clear();
         w2ui['data_grid'].records = json;
         w2ui['data_grid'].refresh();
       });
}

w2utils.locale({
    "phrases" : {
        "Save": "閉じる",
        "Hide": "隠す",
        "Add New": "追加",
        "Edit": "編集",
        "Delete": "削除",
        "Confirmation": "確認",
        "Yes": "はい",
        "No": "いいえ",
        "Reload data in the list": "一覧を再表示します",
        "Edit selected record": "選択した設定を編集します",
        "Required field": "入力必須項目です",
        "Add new record": "新規に設定を追加します",
        "Delete selected records": "選択した設定を削除します",
        "Are you sure you want to delete selected records?": "選択した設定を削除します。よろしいですか?"
    }
});

//更新要求
function ajaxRequest(url, method, JSON, msg){
	var deferred = new $.Deferred();
 	w2confirm( msg +'します。よろしいですか?')
	.yes(function () { 
		$.ajax({
	        type: method,
	        url: url,
	        data: JSON,
	        contentType: 'application/JSON',
	        dataType : 'JSON',
	        scriptCharset: 'utf-8',
	        })
	        .fail((jqXHR, textStatus, errorThrown) => {
	           w2alert(msg + 'に失敗しました' + jqXHR.status + jqXHR.responseText, uiName).done(function () {
		        	deferred.resolve();
	           });
	        })
	        .done((data, textStatus, jqXHR) => {
	           w2alert(msg + 'しました', uiName).done(function () {
		        	deferred.resolve();
	           });
	        });
	});
 	return deferred;
}

$('#data-grid').w2grid({
    header: uiName,
    show: { header: true, toolbar: true, toolbarEdit: true, toolbarAdd: true, toolbarDelete: true, toolbarReload: true, toolbarColumns: false, toolbarSearch: false },
    name: 'data_grid',
    multiSelect : false,
    onReload: function(event) {
    	showGrid();
    },
    onAdd: function (event) {
    	showEditForm(false);
    },
    onEdit: function (event) {
    	showEditForm(true);
    },
    onDelete: function (event) {
    	event.preventDefault();
       	var record = this.get(this.getSelection());
    	var deleteJSON = JSON.stringify(record[0]);
    	var deferred = ajaxRequest(formURL, 'delete', deleteJSON, '削除');
    	deferred.done(function(){
    		setTimeout(function(){showGrid()},500);
    	});
    },
    columns: gridColumns,
});

$('#data-form').w2form({ 
    name  : 'data_form',
    header: uiName,
//    url   : formURL,
    fields: formColumns,
    actions: {
        '更新': function (event) {
        	if( this.validate(true).length == 0 ){
      	    	var postData = this.record;
      	    	var postJSON = JSON.stringify(postData);
      	    	ajaxRequest(formURL, 'post', postJSON, '更新');
        	}
        },
        'キャンセル': function (event) {
            showGrid();
            this.clear();
        }
    }
});

htmlにはjsで定義済の要素とか変数を載せます。メンテ対象のテーブルが増えた場合を考慮しツールバーも入れてます。なお、項目の設定はw2uiのドキュメントを見ながら調整していきますが、結構融通が利きますね。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Customer API Tool</title>
<script src="javascript/jquery-3.4.1.min.js"></script>
<script type="text/javascript" src="javascript/w2ui-1.5.rc1.min.js"></script>
<link rel="stylesheet" type="text/css" href="css/w2ui-1.5.rc1.min.css" />
</head>
<body>
<div id="toolbar" style="padding: 4px; border: 1px solid #dfdfdf; border-radius: 3px"></div>
<div id="data-grid" style="width: 100%; height: 500px;"></div>
<div id="data-form" style="width: 100%; height: 350px;"></div>
<script type="text/javascript">
//w2ui-tablemaintenance.js グリッド設定
var uiName = "顧客設定";  // 画面表示名
var gridURL = location.href + 'customers';  //グリッド表示データ取得URL
var gridColumns = [  // グリッド項目
                   { field: 'customerid', caption: '顧客ID', size: '35%' ,sortable: true, info: true},
                   { field: 'customerCode', caption: '顧客契約CD', size: '35%' ,sortable: true},
                   { field: 'customerName', caption: '顧客名 ', size: '130%' ,sortable: true},
                   { field: 'apiUrl', caption: 'API URL', size: '100%' ,sortable: true},
                   { field: 'user', caption: 'ユーザーID', size: '50%' ,sortable: true},
                   { field: 'password', caption: 'パスワード', size: '40%', render: 'password', sortable: true},
                   { field: 'mailadr', caption: '通知先メールアドレス', size: '100%' ,sortable: true},
                   { field: 'insdate', caption: '作成日時', size: '60%' ,sortable: true},
                   { field: 'updddate', caption: '更新日時', size: '60%' ,sortable: true},
                   ];  

//w2ui-tablemaintenance.js フォーム設定
var formURL = location.href + 'customers';  //フォーム更新URL
var formColumns = [ //フォーム項目
                   { field: 'customerid', type: 'text', required: true, html:{caption: '顧客ID', attr: 'size=5 maxlength=4'}},
                   { field: 'customerCode', type: 'text', required: true, html:{caption: '顧客契約CD', attr: 'size=10 maxlengh=8'}},
                   { field: 'customerName', type: 'text', required: true, html:{caption: '顧客名', attr: 'size=60 maxlengh=60' }},
                   { field: 'apiUrl', type: 'text', required: true, html:{caption: 'API URL', attr: 'size=60 maxlengh=60' }},
                   { field: 'user', caption: 'login ID', type: 'text', required: true, html:{caption: 'login ID', attr: 'size=20 maxlengh=20' }},
                   { field: 'password', caption: 'password', type: 'password', required: true, html:{caption: 'password', attr: 'size=20 maxlengh=20' }},
                   { field: 'mailadr', caption: '通知先メールアドレス', size: '100%' ,sortable: true, html:{caption: '通知先メールアドレス', attr: 'size=40 maxlengh=40' }},
                   ];

//ツールバー
$('#toolbar').w2toolbar({
    name: 'toolbar',
    tooltip: 'right',
    items: [{ type: 'break' },{ type: 'html', html: '<strong><font color="red">Customer API Tool</font></strong>'},
        { type: 'button', id: 'customerBT', group: '1',text: '顧客設定', icon: 'fa-wrench', tooltip: '顧客設定を参照・編集します',
        	onClick: function (event) { showGrid();}},
        { type: 'break' },
    ]
});

$(document).ready(function(){
	showGrid();
});

</script>
<script type="text/javascript" src="javascript/w2ui-tablemaintenance.js"></script>
</body>
</html>

最後にというか、本来なら最初にですがSpringBoot側は、w2uiの項目名と一致するエンティティクラス(ここではCustomer)と、CrudRepositoryを継承したインターフェースCustomerRepositoryを用意し、RESTコントローラーからアクセスさせるようにします。

@CrossOrigin
@RestController
public class CustomerController {
	private CustomerRepository repository;
	
	@Autowired
    public CustomerController(CustomerRepository repository) {
        this.repository = repository;
    }
	/**
	 * 顧客情報取得
	 * */
	@RequestMapping(value="/customers",method = RequestMethod.GET)
    private String getCustomers() throws Exception{
		
		ObjectMapper jsonMapper = new ObjectMapper();
		List<Customer> list = new ArrayList<>();
		StreamSupport.stream(Spliterators.spliteratorUnknownSize(repository.findAll().iterator(), 0),false).forEach(o -> list.add(o));
		return jsonMapper.writeValueAsString(list);
	}
	/**
	 * 顧客情報取得
	 * @param id customer id
	 * */
	@RequestMapping(value="/customers/{id}",method = RequestMethod.GET)
    private String getCustomers(@PathVariable(name = "id", required = false) String id
    		) throws Exception{
		ObjectMapper jsonMapper = new ObjectMapper();
		
		return jsonMapper.writeValueAsString(repository.findById(id));
	}
	/**
	 * 顧客情報更新
	 * */
	@RequestMapping(value="/customers",method = RequestMethod.POST)
	private Customer setCustomers(@RequestBody Customer customer) throws Exception{
		if(customer.getInsdate()== null){
			customer.setInsdate(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
		}
		customer.setUpdddate(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
		return repository.save(customer);
	}
	/**
	 * 顧客情報削除
	 * */
	@RequestMapping(value="/customers",method = RequestMethod.DELETE)
    private String deleteCustomers(@RequestBody Customer customer) throws Exception{
		repository.deleteById(customer.getCustomerid());
		return customer.getCustomerid();
	}
}

で、出来た画面です。

なお、今回セキュリティ要件は考慮していません。。

msg形式のメールファイルをemlファイルに変換する

 数年前にjavamailで作ったメールを受信した後の業務を自動化するツールを、今でもいろいろ機能追加をしているのですが、機能を追加・変更する時に、利用者から対象のメールをemlで提供してもらってテストしています。
 でも、outlookユーザーはmsgでしか提供出来ませんというので、emlへ変換したいのですが、ネットに転がってるツールを使ってもイマイチ変換できません。。
 なら作ろうと調べた所、javaならjotlmsgというのがあったのですが、日本語が文字化けします。
 結局、apache poiでoutlookのmsgを扱えるようなので、msgファイルを読み込んでemlファイルへ出力するという形にしました。

 実行時の引数を、msgファイルを入れたパス、emlファイルを吐き出すパスで指定します。

 何件かのmsgファイルを試した所、htmlで取れるケースが無かった。。メソッド的にはあるはずなのに。。
 その為、完全にemlファイルで再現出来るというレベルにはなりませんでしたが、本文テキストと添付ファイルはemlファイルに出力できているので、まあここまででよいかと。

package jp.esoro.mail.main;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;

import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;

import org.apache.commons.lang3.StringUtils;
import org.apache.poi.hsmf.MAPIMessage;
import org.apache.poi.hsmf.datatypes.AttachmentChunks;
import org.apache.poi.hsmf.exceptions.ChunkNotFoundException;

public class MsgtoEml {
	public static void main(String[] args)  {
		String msgdir = null;
		String emldir = null;
		for(String arg : args){å
			if(msgdir == null){
				msgdir = arg;
			}
			else{
				emldir = arg;
			}
		}
		if(msgdir == null || emldir == null){
			System.out.println("first arg is msg-file path. second arg is export-eml path");
			System.exit(100);
		}
    	File dir = new File(msgdir);
    	File[] files = dir.listFiles();  
    	
        Properties prop = new Properties();
	    Session session = Session.getDefaultInstance(prop, null);
	    
		for(File file: files){
			if( file.isDirectory() ) {
				continue;
			}
			if(StringUtils.endsWithIgnoreCase(file.getName(),"msg")){
				try {
					FileOutputStream fos = new FileOutputStream(emldir + "/" + StringUtils.replaceIgnoreCase(file.getName(), "msg", "eml"));
					
					MAPIMessage  msg = new MAPIMessage(file);
					MimeMessage eml = new MimeMessage(session);
					String headerValue = "";
					String headerKey = "";
					for(String head: msg.getHeaders()){
						if(head.startsWith(" ")){
							headerValue = headerValue + System.lineSeparator() +head;
						}
						else{
							if(headerKey.length() > 0){
								eml.setHeader(headerKey, headerValue);
							}
							String[] h = head.split(":");
							if(h.length > 1){
								headerKey = head.split(":")[0];
								headerValue = head.split(":")[1];
							}
						}
					}
					eml.setSubject(msg.getSubject(), "UTF-8");
					if(msg.getAttachmentFiles() != null){
						MimeBodyPart mbody = new MimeBodyPart();
						try {
							mbody.setContent(msg.getHtmlBody(),"text/html; charset=UTF-8");
						} catch (ChunkNotFoundException e) {
							mbody.setContent(msg.getTextBody(),"text/plain; charset=UTF-8");
						}
						Multipart mp = new MimeMultipart();
						mp.addBodyPart(mbody);
						for (AttachmentChunks chunks : msg.getAttachmentFiles()){
							MimeBodyPart mbAttachment = new MimeBodyPart();
							mbAttachment.setContent(chunks.getAttachData().getValue(), "application/octet-stream");
							mbAttachment.setFileName(chunks.getAttachFileName().getValue());
							mp.addBodyPart(mbAttachment);
						}
						eml.setContent(mp);
					}
					else{
						try {
						eml.setContent(msg.getHtmlBody(),"text/html; charset=UTF-8");
						} catch (ChunkNotFoundException e) {
							eml.setContent(msg.getTextBody(),"text/plain; charset=UTF-8");
						}
					}
					eml.setSentDate(msg.getMessageDate().getTime());
					eml.writeTo(fos);
					msg.close();
					fos.close();
					System.out.println("["+file.getName() + "] msg exchanged to eml");
					
				} catch (IOException | MessagingException | ChunkNotFoundException e) {
					System.out.println("[" +file.getName() + "] msg export to eml is fail ");
					e.printStackTrace();
				}
			}
		}
	}
}
カテゴリー: Java

weblogic 12cR2にバージョンアップしたら、commons-vfs2でFTPが失敗する

weblogicバージョンアップに伴うトラブルで前回の続き、JAX-RS周りの問題が解消したと思ったら、別のエラーが発生。
commons-vfs2でftp送信している箇所が動かなくなりました。commons-vfs2では他にファイル取得等も実装してますが、そっちの方は何も問題はありません。
ログは下記のように何が原因か解らない状況。


Caused by: org.apache.commons.vfs2.FileSystemException: Could not copy "/tmp/report/upload.txt" to "ftp://username:***@ftphost/report/upload.txt".
Caused by: org.apache.commons.vfs2.FileSystemException: Could not create folder "ftp://username:***@ftphost/report" because it already exists and is a file.

結果的に、pom.xmlに下記を追加しただけで問題が解消しました。

    <!-- https://mvnrepository.com/artifact/commons-net/commons-net -->
	<dependency>
	    <groupId>commons-net</groupId>
	    <artifactId>commons-net</artifactId>
	    <version>2.2</version>
	</dependency>

アプリケーションサーバの機能が多いと、かえって何処が依存しているか解らなくなりがちですね。