Quantcast
Channel: システム開発メモ
Viewing all 100 articles
Browse latest View live

tomcatとapacheを連携させる時の、8080ポート停止設定をコマンドのみで行う

$
0
0

tomcatのserver.xml、apacheのhttpd.conf、httpd-proxy.confの編集をvi等で行うことなく、コマンドの実行のみで行う。

###ajp###
sed -ie '/<Connector port="8080"/,/\/>/s/^\(.*\)$/<!--\1-->/g' /opt/tomcat/apache-tomcat-8.0.26/conf/server.xml

sed -ie 's/#LoadModule proxy_module modules\/mod_proxy.so/LoadModule proxy_module modules\/mod_proxy.so/' /etc/httpd/conf/httpd.conf
sed -ie 's/#LoadModule proxy_ajp_module modules\/mod_proxy_ajp.so/LoadModule proxy_ajp_module modules\/mod_proxy_ajp.so/' /etc/httpd/conf/httpd.conf
echo Include /etc/httpd/conf/extra/httpd-proxy.conf >> /etc/httpd/conf/httpd.conf
cd /etc/httpd/conf/
mkdir extra
cd extra
echo ProxyPass /examples/ ajp://localhost:8009/examples/ > httpd-proxy.conf

/opt/tomcat/apache-tomcat-8.0.26/bin/startup.sh
service httpd start

簡単な解説

1つ目のコードは、sedの条件式「行数,行数⇒ 指定した行数間の文字列を処理する」アドレス指定の応用。
/はじまり/ と /おわり/ がそれぞれ行数を返しているので、結果的に「はじまり」が含まれる行から「おわり」が含まれる行までを返す。

###ajp###
sed -ie '/<Connector port="8080"/,/\/>/s/^\(.*\)$/<!--\1-->/g' /opt/tomcat/apache-tomcat-8.0.26/conf/server.xml

参考:http://taichiw.hatenablog.com/entry/2012/04/06/102650

その他の行は単純なsed -ieによる書き換えやechoとリダイレクトによるファイルの新規作成のみ。


cronでの環境変数設定

$
0
0

cronでは環境変数が設定されないので、実行スクリプトかcron自身で環境変数が設定されるようにしなければいけない。

実行スクリプト内で対応するには以下の2通り
・スクリプトのshebangで、ログインシェルとして動くように

#!/bin/bash -l
とする
・スクリプトの冒頭で
source /home/username/.bash_profile
を実行する

cronで対応するには以下の2通り

bash -l -c 'コマンド'
とする。-lでログインシェルとして動くようになる。
source ~/.bash_profile
を書いてから目的のコマンドを記載する。
00 00 * * * postgres bash -l -c 'psql -p 5432 dbname -c "VACUUM ANALYZE"'
00 00 * * * postgres source ~/.bash_profile;psql -p 5432 dbname -c "VACUUM ANALYZE"

スクリプトを作らず直接cronにコマンドを書くことを考慮すると、cronで対応するように統一するのが良い。

Linuxサーバでのファイルの入れ替え

$
0
0

サーバにローカル端末で更新した設定ファイル等をアップしてからmvで所定のファイルに移動させると、権限や所有者が変わってしまうことがある。
cpコマンドを–preserveオプションや–no-preserveオプションをつけずに実行し、上書きすると、timestampは更新されるものの、権限や所有者は元のファイルのまま保たれる。

つまり、ファイル入れ替えにおいてmvは使用してはいけない。

### /usr/local/bin/test.shを~/test.shと入れ替える。
ORIGINAL=/usr/local/bin/test.sh
UPDATED=~/test.sh
BACKUP=/tmp/test.sh

NGパターン

cp -p $ORIGINAL $BACKUP
mv -f $UPDATED $ORIGINAL

OKパターン

cp -p $ORIGINAL $BACKUP
\cp -f $UPDATED $ORIGINAL
# \cpでcpのalias(cp -i)が使われないようにし、-fオプションが有効になるようにしている。

特別に何かほかの情報を更新したい、あるいは更新したくないという場合は、–preserveオプション、–no-preserveオプションで対応すればいい。

最終更新日時と最新アクセス日時 timestamps
所有者と所有グループ/td> ownership
アクセス権(パーミッション)/td> mode
SELinuxの「コンテキスト」と呼ばれる情報/td> context
ディレクトリ内に存在するハードリンク/td> links
ファイルシステムの拡張属性/td> xattr

参考:cpコマンドでファイルやディレクトリをコピーした際に保持される情報/属性について

ちなみにcpではなくmvを使う場合、無理やり権限等を正そうとすると、chmodとchownのreferenceオプションを使って権限やオーナー・グループを元のファイルからコピーする必要がある。

# backup
cp -p $ORIGINAL $BACKUP
# 権限をコピー
chmod --reference=$ORIGINAL $UPDATED
# オーナー・グループをコピー
chown --reference=$ORIGINAL $UPDATED
# 入れ替え
mv -f $UPDATED $ORIGINAL

サーバのファイルをteratermマクロを使って端末に保存する方法

$
0
0

teratermではscpが使用できるが、teratermマクロのscp(受信)コマンド scprecv で同機能が使える。
通常はバッチファイルからSFTPを自動で実行するで書いたWinSCPを使用してファイルダウンロードする方が簡単かつメリットがあるが、teratermマクロの柔軟な機能を使用したい時などは、teratermマクロ + WinSCPよりもteratermマクロだけで完結させた方がシンプルになる。
※WinSCP側のメリット
・teratermと違って受信機能が同期で動くため、受信完了のタイミングを考えなくてもよい。
・サーバへのログインパスワードがWinSCP側で保存でき、平文でスクリプトファイルに書く必要がない。

サンプルとして、サーバの/tmp/sample.csvを端末のC:\work\にダウンロードするteratermマクロを、バッチから呼び出す。

callscpresv.bat

set TTLEXE="C:\Program Files\teraterm\ttpmacro.exe"
:必ずフルパスでファイル指定する
set TTLFILE=C:\work\rack_remove_comp.ttl

%TTLEXE% %TTLFILE%

scprecv.ttl

hostname = 'HOST名もしくはIPアドレス'
portnum = '22'
username = 'ユーザ名'
userpasswd = 'パスワード'

localdir = 'C:\work\'
remotedir = '/tmp/'
filename = 'sample.csv'
; ローカルファイルフルパス
localpath = localdir
strconcat localpath filename
; リモートファイルフルパス
remotepath = remotedir
strconcat remotepath filename


; ログインコマンド組立て
msg = hostname
strconcat msg ':portnum /ssh /auth=password /user='
strconcat msg username
strconcat msg ' /passwd='
strconcat msg userpasswd

; 接続
connect msg

; 接続判定1(接続出来ない場合はメッセージを表示しマクロ終了)
if result <> 2 then
    messagebox 'It could not be connected.' 'Connection Error'
    end
endif

; 接続判定2(10秒以内にプロンプトが表示されない場合TeraTerm終了)
timeout = 10
wait '$' '#'
if result=0 then
    disconnect 0
    end
endif

; 受信ファイルが存在する場合削除
filesearch localpath
if result then
    filedelete localpath
endif

; ファイル受信
scprecv remotepath localpath
;受信完了確認(scprecvが非同期処理のため)
; 参考 http://qiita.com/KurokoSin/items/b4d2d0a81c8d05f110ef
; 1秒ごとに端末のファイルサイズを確認し、変動がなくなれば完了としている
mpause 1100
sizeBef = 0
:confirm_start
    filestat localpath sizeNow
    if sizeNow = sizeBef then
        goto confirm_end
    else
        mpause 1100
    endif
    sizeBef = sizeNow
    goto confirm_start
:confirm_end

; マクロ終了
sendln 'exit'
end

Windows端末とLinuxサーバのファイルの送受信投稿一覧

バッチファイルからSFTPを自動で実行する
サーバのファイルをteratermマクロを使って端末に保存する方法
サーバのファイルをteratermマクロを使って端末に保存する方法(その2)
端末のファイルをteratermマクロを使ってサーバに保存する方法

バッチとPowerShellで昨日の日付を取得する

$
0
0

Windowsバッチで昨日の日付を簡単に取得できる方法はない。
PowerShellを使えば簡単に求められるので、スクリプトの全体をバッチで書いていた場合、PowerShellで呼び出してあげる必要がある。

まず、PowerShellで昨日の日付を求めるのはワンライナーで書ける。

[DateTime]::Today.AddDays(-1).ToString('yyyyMMdd')

これをコマンドラインから実行するには

powershell [DateTime]::Today.AddDays(-1).ToString('yyyyMMdd')

※コマンドラインから実行する場合は、yyyyMMddを囲うのはシングルクォーテーションのみ可能。
 ダブルクォーテーションを使用していた場合は\でエスケープが必要。

実行結果を変数に入れるにはFORコマンドを使用しなければいけない。
※こんなことはできない。

SET yesterday=powershell [DateTime]::Today.AddDays(-1).ToString('yyyyMMdd')

FORコマンドのコマンド実行部(で囲まれたところ)では、()等はダブルクォーテーションで囲うか^でエスケープが必要となるため、書き方は2通り。
FOR /F "usebackq" %a IN (`powershell [DateTime]::Today.AddDays"("-1")".ToString"("'yyyyMMdd'")"`) DO SET yesterday=%a

あるいは

FOR /F "usebackq" %a IN (`powershell [DateTime]::Today.AddDays^(-1^).ToString^('yyyyMMdd'^)`) DO SET yesterday=%a

FORコマンドの参考:http://www.atmarkit.co.jp/ait/articles/0106/23/news004_2.html

さらにこれをバッチファイルで実行するには%aが展開されてaになるので、%を2つつける必要がある。

FOR /F "usebackq" %%a IN (`powershell [DateTime]::Today.AddDays"("-1")".ToString"("'yyyyMMdd'")"`) DO SET yesterday=%%a

echo %yesterday%
を実行すると以下のように表示される。
20160123

ファイル名の一括変換方法

$
0
0

複数のファイルの名前をあるルールで一括して変換する方法。
(動作確認 RedHat6.2)

簡単なルールであればrenameコマンドを使用すればいい。

rename from to files
# files:ワイルドカード使用可能

しかし正規表現を使用しなければいけないようなルール、たとえば末尾に.csvをつけるといったようなことはできない。
ネットではsedのように

's/regex/regex/'
形式で指定できると書いてあったが、RedHat6.2に入っているrenameコマンドは対応していなかった。
Linuxには2つのrenameコマンドが存在し、正規表現が使えるのはperlで作られたDebian系のrenameの方であり、RedHat系はutil-linuxに含まれるrenameコマンドなので、複雑なルールには不向き。

findの結果をxargsで受けて、一つずつ処理するのが汎用的に使える。

【要件】test_で始まる複数のファイル名の末尾に.csvをつける。
【方法】

find -name 'test_*' -printf '%f¥n' | xargs -I % mv % %.csv

【説明】find -printf ‘%f\n’でtest_で始まるファイルのファイル名部分だけを抜き出し、ファイル名を一つずつmvする。

mvを最終的に行う前に、必要に応じてsed等を挟めば、より柔軟な変換が可能になる。

Windowsのコマンドプロンプトで簡単にSSHやSCPする方法

$
0
0

デフォルトでWindowsにはSSHクライアントが入っていないため、SSHコマンドを簡単に使用できない。
Git for Windowsを入れるとついてくるGit Bashを使うと、Linuxと同じコマンドでSSHやSCPができる。

Git Bashを立ち上げて、Git Bashのコマンドライン上でSSHコマンドを打つのはLinuxと同じなので問題ないとして、Windowsの標準コマンドプロンプトから実行したい場合はGit Bash.exeに引数を渡して実行する必要がある。

"C:\Program Files\Git\bin\bash.exe" --login -i -c "コマンド"

–login でログインシェルとして動き、
-i でインタラクティブシェルとして動く。インタラクティブシェルなのでパスワード等を聞かれたときには、コマンドプロンプトに入力すればよい。
-c で実行したいコマンドを渡せる。

たとえばリモートサーバのホームディレクトリにあるsample.sqlをSCPでWindows端末に取得したい場合は次のようなコマンドになる。

C:\Users\admin>"C:\Program Files\Git\bin\bash.exe" --login -i -c "scp user@remotehost:~/sample.sql ."
user@remotehost's password:
sample.sql                                     100%  966     0.9KB/s   00:00

サーバのファイルをteratermマクロを使って端末に保存する方法(その2)

$
0
0

サーバのファイルをteratermマクロを使って端末に保存する方法で書いた別解。

前回は1秒ごとに端末のファイルサイズを確認し、変動がなくなれば完了とするというロジックだったが、今回はscp中のサーバのプロセスを確認し、プロセスが消えれば完了としている。
端末がファイルを受信するscprecvコマンドを実行すると、サーバでは

scp -f scprecvの第一引数(remotepath)
というプロセスが動く。ps -efをgrepしてマッチした行数をgrepの-cオプションで表示し、表示が「0」になったらプロセスが消えたと判断する。

scprecv.ttlのファイル受信部分のみ以下に変更

; ファイル受信
scprecv remotepath localpath
;受信完了確認(scprecvが非同期処理のため)
do
    mpause 2000
    sendln 'ps -ef | grep -v grep | grep -c "scp -f ' remotepath '"'
    recvln ;上のコマンドを受信
    recvln ;上のコマンドの実行結果である標準出力を受信
    strcompare inputstr '0' ;受信した標準出力が'0'と等しければresult=0
loop while result != 0;

Windows端末とLinuxサーバのファイルの送受信投稿一覧

バッチファイルからSFTPを自動で実行する
サーバのファイルをteratermマクロを使って端末に保存する方法
サーバのファイルをteratermマクロを使って端末に保存する方法(その2)
端末のファイルをteratermマクロを使ってサーバに保存する方法


端末のファイルをteratermマクロを使ってサーバに保存する方法

$
0
0

サーバのファイルをteratermマクロを使って端末に保存する方法(その2)でteratermマクロのscpを利用した受信を書いたので、今回は送信について書く。

ロジックは前回と同様にサーバのプロセスが消えれば処理完了と判断するというもの。
送信用コマンドscpsendを実行すると、サーバでは

scp -t scpsendの第二引数(remotepath)
というプロセスが動く。
; ファイル送信
scpsend localpath remotepath
do
    mpause 2000
    sendln 'ps -ef | grep -v grep | grep -c "scp -t ' remotepath '"'
    recvln ;上のコマンドを受信
    recvln ;上のコマンドの実行結果である標準出力を受信
    strcompare inputstr '0' ;受信した標準出力が'0'と等しければresult=0
loop while result != 0;

Windows端末とLinuxサーバのファイルの送受信投稿一覧

バッチファイルからSFTPを自動で実行する
サーバのファイルをteratermマクロを使って端末に保存する方法
サーバのファイルをteratermマクロを使って端末に保存する方法(その2)
端末のファイルをteratermマクロを使ってサーバに保存する方法

ログファイルをgrepした結果をOutlookでメール送信するサンプルスクリプト

$
0
0

ログファイルをgrepした結果をOutlookでメール送信するサンプルスクリプトをVBSで作成。
ポイントは以下の2点。
・Windowsでgrepをどのように実現するか
・メール本文にログファイル全文をどう書くか

Windowsでgrepする方法はfindstrコマンドで実現可能で、正規表現も検索文字列に指定できる。

findstr [パラメータ] 検索文字列 [ファイル名群]

しかしfindstrはVBSの関数ではないため、

CreateObject("WScript.Shell")
してから
Exec("%ComSpec% /c (コマンド)")
しなければいけない。

2点目の問題と同じなのだが、Execでコマンド実行した結果が標準出力に出て、それを取得しようとしたときに出力サイズによっては問題が発生する。

標準出力に4KB(4096byte)以上出力しバッファがいっぱいになっている状態では、Exec.StdOutで読み込みを行おうとしてもデッドロックがかかってしまい、スクリプトが止まってしまう。

StdOut, StdErrストリームは、4KBのバッファを共有しています。また、WshScriptExecオブジェクトは、単にこのストリームを同期的に読み込む操作を行うだけです。
同期読み込み操作は、呼び出したスクリプトの読み込みと、子プロセスの書き込みに依存しています。これがデッドロック状態を起こしうるのです。
子プロセスのストリームを読み込むとき、子プロセスに依存した形になります。子プロセスがストリームに書き込むか、ストリームを閉じるまで、呼び出しは完了しません。
一方、子プロセスがストリームのバッファ(4KB)をいっぱいにしてしまうとき、親プロセスの読み込みに依存しています。
親プロセスがバッファから完全に読み込むか、ストリームを閉じるまで、子プロセスの書き込み操作は待ち状態に入って完了しません。
このようにして、スクリプトと子プロセスがお互いに読み書きを待つ状態に陥ると、デッドロック(フリーズ)が生じます。

引用:http://p2pquake.ddo.jp/mskb/archives/261

findstrの結果だけなら問題ないことも多いが、ログファイル全文となると簡単に4KBを超えてしまうので、 以下のようにEndOfStreamに到達するまで常にReadLineする必要がある。

Do While Not(Exec.StdOut.AtEndOfStream )
  stdout = stdout & Exec.StdOut.ReadLine & kaigyo
Loop

ログファイルの内容自体は、テキストファイルなどの内容を標準出力に表示するコマンドtypeを使用している。

サンプルスクリプト

mail.vbs

Option Explicit
' 引数 logname: ログファイル名(拡張子.logは含めない。logname*.logが処理される)
'      grepStr: grepする文字列
' 使用例:cscript mail.vbs logsample エラー
' 仕様:%TMP%\logname.logファイルについて、grepStrを検索する。
'       検索できれば、ログ全文を本文に、【失敗】lognameを件名にセットしてメール送信する。
'       検索できなければ、本文は空で、【成功】lognameを件名にセットしてメール送信する。
'       ログファイルはC:\logbackup\に日付と時分をファイル名に付与して退避する。
'
'*************************************************************
Dim outlook, item, mailBody, mailSubject, mailAddress, mailTo, mailCc, kaigyo

'*************************************
'* 個人情報
'*************************************
' 自分のメールアドレス
mailAddress = "me@example.com"
' 送信先:TO
mailTo = "you@example.com"
' 送信先:CC
mailCc = ""

'*************************************
'* 初期処理
'*************************************
Set outlook = CreateObject("Outlook.Application")
Set item = outlook.CreateItem(0)

' 改行コード
kaigyo = vbCrLf

'*************************************
'* コマンドライン引数(パラメータ)の取得
'*************************************
Dim oParam
Set oParam = WScript.Arguments
Dim logName, grepStr
logName = oParam(0)
grepStr = oParam(1)

'*************************************
'* メール本文
'*************************************
Dim WShell, Exec
Set WShell = CreateObject("WScript.Shell")
Set Exec = WShell.Exec("%ComSpec% /c (type %TMP%\" & logName & "*.log | findstr " & grepStr & ")")
Dim stdout
Do While Not(Exec.StdOut.AtEndOfStream )
  stdout = stdout & Exec.StdOut.ReadLine
Loop

If Len(stdout) <> 0 Then
    Set Exec = WShell.Exec("%ComSpec% /c (type %TMP%\" & logName & "*.log)")
    Do While Not(Exec.StdOut.AtEndOfStream )
        mailBody = mailBody & Exec.StdOut.ReadLine & kaigyo
    Loop
End IF

'*************************************
'* メール件名
'*************************************
mailSubject = logName
If Len(stdout) = 0 Then
    mailSubject = "【成功】" & mailSubject
Else
    mailSubject = "【失敗】" & mailSubject
End If

'*************************************
'* メール内の設定
'*************************************
item.To = mailTo
item.Cc = mailCc
item.Subject = mailSubject
item.Body = mailBody
item.Display

'*************************************
'* 送信処理
'*************************************
item.Send

'*************************************
'* 終了処理
'*************************************
'ファイルの退避
WShell.Exec("%ComSpec% /c (move %TMP%\" & logName & "*.log C:\logbackup\" & logName _
    & Year(Now) & Right("0" & Month(Now), 2) & Right("0" & Day(Now), 2) & Right("0" & Hour(Now), 2) & Right("0" & Minute(Now), 2) _
    & ".log)")

Set item = Nothing
Set outlook = Nothing
WScript.Quit 0

Spring BootとMyBatisで複数のDBを扱うためのapplication.properties作成方法

$
0
0

Spring Bootで複数のDataSourceを扱う方法は、使用するORMによってだけでなく、Springのconfigurationをapplication.properties/application.ymlで行うかJavaConfigで行うかによっても変わってくる。
ここではMyBatisとapplication.propertiesでの方法を書く。
ちなみにMyBatisを選んだのは直接SQLを書きたいからで、application.propertiesを選んだのは(ymlでもいいが)JavaConfigより設定が一か所にまとまり、さらに.javaファイルよりも「このファイルは設定である」と一目でわかるのが簡潔に思うから。

検証version
・Spring Boot 1.3.3
・MyBatis 3.3.1
・MyBatis-Spring 1.2.4

目次

今回の設定における前提
pom.xml
ディレクトリ構造
ソースコード解説

今回の設定における前提

・DBは二つ接続したい。
 一つは自システム専用のDBで、もう一つは他システムのDB。
 それぞれownとotherというDB名とする。
・ownにはCustomerテーブル、otherにはOrderテーブルがある。

pom.xml

Spring BootにMyBatisを組み込むためのライブラリの設定。
pom.xml

<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>1.0.1</version>
</dependency>

mybatis-spring-boot-starterが提供されているので、これだけでMyBatis本体とMyBatis-Springが入ってくれる。

ディレクトリ構造

DB接続部分のディレクトリ構造の抜粋

src/main/java/jp/co/sample
├ domain
│   ├ Customer.java
│   └ Order.java
├ mapper
│   ├ handler
│   ├ other
│   │    └ OrderMapper.java
│   └ own
│        └ CustomerMapper.java
├ service
│   ├ CustomerService.java
│   └ OrderService.java
├ system/config/db
│        ├ DataSource.java
│        ├ OtherDataSourceConfiguration.java
│        ├ OtherDBProperties.java
│        ├ OwnDataSourceConfiguration.java
│        └ OwnDBProperties.java
├ web
└ Application.java

src/main/resources
├ jp/co/sample
│        ├ other
│        │    └ OrderMapper.xml
│        └ own
│             └ CustomerMapper.xml
├ application.properties
├ data_other.sql
├ data_own.sql
├ mybatis-config.xml
├ schema_other.sql
└ schema_own.sql

ディレクトリごとの役割解説

ディレクトリ 役割
domain 検索結果に相当するJavaBeansをおく。
mapper 直下にDBごとのディレクトリを作成し、SQLを記載したxmlを呼び出すためのinterfaceをおく。
ディレクトリを分けるということ以外、Javaソース上でDBの違いは意識しなくて済む。
service mapper interfaceを呼び出したり、ビジネスロジックを書く。DBの違いはここでは意識しなくて済む。
system/config/db DBに関係する設定。パラメータ設定をapplication.propertiesで行えるようにするためのコードが書かれている。

ソースコード解説

ディレクトリごとの役割解説で記載した通り、DBごとの違いが現れるのはsystem/config/dbのソースだけであり、あとは単一DBで使用するときと変わらない。
system/config/dbの各クラスについて全文掲載していく。

DataSource.java

import lombok.Getter;
import lombok.Setter;

public class DataSource extends org.apache.tomcat.jdbc.pool.DataSource {

	/**
	 * Schema (DDL) script resource reference.
	 */
	@Getter @Setter
	private String schema;

	/**
	 * Data (DML) script resource reference.
	 */
	@Getter @Setter
	private String data;

}

Tomcatのorg.apache.tomcat.jdbc.pool.DataSourceを独自拡張している。
Spring Bootでは/src/main/resourcesにschema.sqlとdata.sqlを配置することで、アプリの起動時に自動でSQLを実行してくれる機能がある。また以下のようにファイル名を指定することで、デフォルトのファイル名から変更することもできる。

spring.datasource.schema=classpath:schema_for_development.sql
spring.datasource.data=classpath:data_for_development.sql

しかしorg.apache.tomcat.jdbc.pool.DataSourceには同パラメータがないため、schemaとdataというフィールドを持つように拡張している。

OwnDBProperties.java

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;

import lombok.Getter;
import lombok.Setter;

@ConfigurationProperties("spring.own")
public class OwnDBProperties {

	@Getter @Setter
	@NestedConfigurationProperty
	private DataSource datasource;
}

ownDB用にapplication.properiesでspring.ownをkeyに各種設定をできるようにしている。
@ConfigurationPropertiesをクラスに付与すると、@ConfigurationPropertiesの引数で指定した値がapplication.properiesでkeyとして使える。
DataSourceクラスで定義されているものを、各種keyとして使用したいので、@NestedConfigurationPropertyをDataSourceに付与することで、spring.own.datasourceというprefixで各種項目にアクセスできるようになる。

OtherDBProperties.java

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;

import lombok.Getter;
import lombok.Setter;

@ConfigurationProperties("spring.other")
public class OtherProperties {

	@Getter @Setter
	@NestedConfigurationProperty
	private DataSource datasource;
}

spring.otherをapplication.propertiesで使用できるようにする。基本はOwnDBPropertiesと同じ。

OwnDataSourceConfiguration.java

import javax.sql.DataSource;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;

@Configuration
@MapperScan(basePackages = "jp.co.sample.mapper.own", sqlSessionTemplateRef = "ownSqlSessionTemplate")
public class OwnDataSourceConfiguration {

	@Autowired
	private OwnDBProperties ownDBProperties;

	@Autowired
	private ApplicationContext context;

	@Bean(name = { "dataSource", "ownDataSource" })
	@ConfigurationProperties(prefix = "spring.own.datasource")
	@Primary
	public DataSource dataSource() {
		return ownDBProperties.getDatasource();
	}

	@Bean(name = "ownSqlSessionTemplate")
	public SqlSessionTemplate sqlSessionTemplate(@Qualifier("ownDataSource") DataSource dataSource) throws Exception {
		SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
		bean.setDataSource(dataSource);
		// MyBatis のコンフィグレーションファイル
		bean.setConfigLocation(context.getResource(context.getEnvironment().getProperty("mybatis.config")));
		return new SqlSessionTemplate(bean.getObject());
	}

	@Bean(name = "ownDataSourceInitializer")
	public DataSourceInitializer dataSourceInitializer(@Qualifier("ownDataSource") DataSource dataSource) {
		DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
		dataSourceInitializer.setDataSource(dataSource);
		ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
		databasePopulator.addScripts(
				context.getResource(ownDBProperties.getDatasource().getSchema()),
				context.getResource(ownDBProperties.getDatasource().getData()));
		dataSourceInitializer.setDatabasePopulator(databasePopulator);
		dataSourceInitializer.setEnabled(true);

		return dataSourceInitializer;
	}

}

ownDB用に以下のクラスを構築するJavaConfig。
・DataSource
・MyBatisが使用するSqlSessionTemplate
・Spring Boot起動時のSQLファイル自動実行用のDataSourceInitializer

JavaConfigなので、@Configurationをクラスに付与する。
また@MapperScanで、ownDBにSQLアクセスするmapper interface群が格納されているpackageの指定と、SqlSessionTemplateがどのインスタンスを参照すればいいかを指定する。

OtherDataSourceConfiguration.java

import javax.sql.DataSource;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;

@Configuration
@MapperScan(basePackages = "jp.co.sample.mapper.other", sqlSessionTemplateRef = "otherSqlSessionTemplate")
public class OtherDataSourceConfiguration {

	@Autowired
	private OtherProperties otherProperties;

	@Autowired
	private ApplicationContext context;

	@Bean(name = "otherDataSource")
	@ConfigurationProperties(prefix = "spring.other.datasource")
	public DataSource dataSource() {
		return otherProperties.getDatasource();
	}

	@Bean(name = "otherSqlSessionTemplate")
	public SqlSessionTemplate sqlSessionTemplate(@Qualifier("otherDataSource") DataSource dataSource) throws Exception {
		SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
		bean.setDataSource(dataSource);
		// MyBatis のコンフィグレーションファイル
		bean.setConfigLocation(context.getResource(context.getEnvironment().getProperty("mybatis.config")));
		return new SqlSessionTemplate(bean.getObject());
	}

	@Bean(name = "otherDataSourceInitializer")
	public DataSourceInitializer dataSourceInitializer(@Qualifier("otherDataSource") DataSource dataSource) {
		DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
		dataSourceInitializer.setDataSource(dataSource);
		ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
		databasePopulator.addScripts(
				context.getResource(otherProperties.getDatasource().getSchema()),
				context.getResource(otherProperties.getDatasource().getData()));
		dataSourceInitializer.setDatabasePopulator(databasePopulator);
		dataSourceInitializer.setEnabled(true);

		return dataSourceInitializer;
	}
}

otherDB用。各種名称、参照がownからotherになっている以外はOwnDataSourceConfiguration.javaと同じ。

以上で、application.propertiesで複数DataSourceの設定が行えるようになったので、以降では/src/main/resourcesを見ていく。

CustomerMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="jp.co.sample.mapper.own.CustomerMapper">

	<select id="selectAll" resultMap="Customer">
		SELECT *
		FROM customer
	</select>

</mapper>

xmlファイルで注意する点はが指定するmapper interfaceがどのディレクトリにあるかということだけ。
jp.co.sample.mapper.own.CustomerMapperが正解で、jp.co.sample.mapper.CustomerMapperやjp.co.sample.mapper.other.CustomerMapperというように格納されているディレクトリ名をコピペで処理して間違えないようにしなければいけない。

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<settings>
		<setting name="mapUnderscoreToCamelCase" value="true" />
	</settings>
	<typeAliases>
		<package name="jp.co.sample.domain" />
	</typeAliases>
	<typeHandlers>
		<typeHandler handler="jp.co.sample.mapper.handler.LocalDateTypeHandler" />
		<typeHandler handler="jp.co.sample.mapper.handler.LocalDateTimeTypeHandler" />
	</typeHandlers>

</configuration>

mybatis-config.xmlは複数DBになったことによる影響はない。理由は今回についてはdomainディレクトリ以下をownとotherで分けなかったため、typeAliasesのpackage nameに影響がなかったから。
domain以下もディレクトリを分けるのであれば、mybatis-config-own.xmlとmybatis-config-other.xmlの二つに分けるのが良いと思う。そしてmybatis-configのファイルパス指定をapplication.propertiesで行うのではなく、OwnDataSourceConfiguration、OtherDataSourceConfigurationで直指定すればいい。

application.properties

#接続情報
spring.own.datasource.url=jdbc:h2:mem:own;
#初期設定
spring.own.datasource.schema=classpath:schema_own.sql
spring.own.datasource.data=classpath:data_own.sql

#コネクションプール設定
spring.own.datasource.max-active=100
spring.own.datasource.max-idle=8
spring.own.datasource.min-idle=8
spring.own.datasource.initial-size=8
#コネクションを利用する際に検証を行う。DBが再起動していてもこの処理を挟むことでtomcatを再起動しなくても済む
spring.own.datasource.test-on-borrow=true
spring.own.datasource.validation-query=SELECT 1
#コミットされずに残ったコネクションは60秒後に破棄される。
spring.own.datasource.remove-abandoned=true
spring.own.datasource.remove-abandoned-timeout=60
#auto commit
spring.own.datasource.default-auto-commit=true


#接続情報
spring.other.datasource.url=jdbc:h2:mem:other;
#初期設定
spring.other.datasource.schema=classpath:schema_other.sql
spring.other.datasource.data=classpath:data_other.sql

#コネクションプール設定
spring.other.datasource.max-active=100
spring.other.datasource.max-idle=8
spring.other.datasource.min-idle=8
spring.other.datasource.initial-size=8
#コネクションを利用する際に検証を行う。DBが再起動していてもこの処理を挟むことでtomcatを再起動しなくても済む
spring.other.datasource.test-on-borrow=true
spring.other.datasource.validation-query=SELECT 1
#コミットされずに残ったコネクションは60秒後に破棄される。
spring.other.datasource.remove-abandoned=true
spring.other.datasource.remove-abandoned-timeout=60
#auto commit
spring.other.datasource.default-auto-commit=true

Spring Boot + Spring Security使用時のSessionTimeout対応

$
0
0

Spring BootにSpring Securityを入れた時のSessionTimeoutのデフォルト挙動は、ログイン画面への自動遷移になる。
一般的な要件として、ログイン画面に遷移したときに「タイムアウトしました。」などのメッセージを表示しなければいけないような時の対応方法を記載する。
※関連ページ:Spring Boot + Spring Security使用時のCSRFとSessionTimeoutの問題

検証version
・Spring Boot 1.3.3
・Spring Security 4.0.3
※参考までにView側技術
 ・Thymeleaf 2.1.4
 ・Bootstrap 3.3.6

org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPointというExceptionが発生したときのログイン画面へのリダイレクト用クラスをSpring Securityが用意していて、これを拡張してすることで実現する。
LoginUrlAuthenticationEntryPoint

Used by the ExceptionTranslationFilter to commence a form login authentication via the UsernamePasswordAuthenticationFilter.
Holds the location of the login form in the loginFormUrl property, and uses that to build a redirect URL to the login page.

拡張方法は、リダイレクトURLを決定するメソッドbuildRedirectUrlToLoginPageをOverrideする。

@Override
	protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {

		String redirectUrl = super.buildRedirectUrlToLoginPage(request, response, authException);
		if (isRequestedSessionInvalid(request)) {
			redirectUrl += redirectUrl.contains("?") ? "&" : "?";
			redirectUrl += "timeout";
		}
		return redirectUrl;
	}

	private boolean isRequestedSessionInvalid(HttpServletRequest request) {
		return request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid();
	}

処理の内容は、Sessionが無効状態になっているとき、デフォルトのリダイレクトURLの後ろにリクエストパラメータ”timeout”を付与する。
これにより、/login?timeoutというURLになる。

しかしAjaxリクエストの場合はリダイレクトが動かないため、リクエストがAjaxかどうかを判定して、Ajaxの場合はHTTP STATUS:401 Unauthorizedを返すのみにして、JavaScript側でリダイレクトするように対応する必要がある。
まずは、リクエストがAjaxかどうかを判定するために、LoginUrlAuthenticationEntryPoint#commenceをOverrideする。

@Override
	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

		if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
			response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
			return;
		}

		super.commence(request, response, authException);
	}

JavaScript側はjQueryを用いて書くと以下のようになる。

var ajaxUrl ="/ajax?id=1";
$.ajax({
	type : "GET",
	url : ajaxUrl,
	statusCode: {
		401: function() {
			window.location.href = /*[[@{/login?timeout}]]*/"";
		}
	}
}).done(
		function(json) {
			// 通常処理
		});

login.htmlでは、

th:if="${param.timeout}"
という条件式を設定して、リクエストパラメータにtimeoutが存在するときのみ「タイムアウトしました。」という文言を表示するようにしている。
以下はlogin.htmlのBODY部。
<body>
	<div class="container" layout:fragment="content">
		<div th:include="common/pageheader :: pageheader('ログイン画面')"></div>

		<div th:if="${param.error}" id="information" class="alert alert-danger alert-dismissible">
			<button type="button" class="close" data-dismiss="alert" aria-label="閉じる"><span aria-hidden="true">×</span></button>
			ユーザ名かパスワードに誤りがあります。
		</div>
		<div th:if="${param.logout}" id="information" class="alert alert-success alert-dismissible">
			<button type="button" class="close" data-dismiss="alert" aria-label="閉じる"><span aria-hidden="true">×</span></button>
			ログアウトしました。
		</div>
		<div th:if="${param.timeout}" id="information" class="alert alert-info alert-dismissible">
			<button type="button" class="close" data-dismiss="alert" aria-label="閉じる"><span aria-hidden="true">×</span></button>
			タイムアウトしました。
		</div>
		<form th:action="@{/login}" method="post">
			<div class="form-group col-sm-2"><label class="control-label">ユーザ名</label><input type="text" class="form-control input-sm" id="username" name="username" style="ime-mode: disabled;"/></div>
			<div class="form-group col-sm-2"><label class="control-label">パスワード</label><input type="password" class="form-control input-sm" id="password" name="password"/></div>
			<div class="form-group col-sm-1"><label></label><input type="submit" class="btn btn-primary" id="login" value="ログイン"/></div>
        </form>
	</div>

</body>

あとは、拡張したクラスをSpring Securityに認識させることで、SessionTimeout時のリダイレクトとリダイレクト先での文言表示が実現できる。

まず、拡張クラスSessionExpiredDetectingLoginUrlAuthenticationEntryPointの全文を以下に掲載する。

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;

public class SessionExpiredDetectingLoginUrlAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {

	public SessionExpiredDetectingLoginUrlAuthenticationEntryPoint(String loginFormUrl) {
		super(loginFormUrl);
	}

	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

		if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
			response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
			return;
		}

		super.commence(request, response, authException);
	}

	@Override
	protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {

		String redirectUrl = super.buildRedirectUrlToLoginPage(request, response, authException);
		if (isRequestedSessionInvalid(request)) {
			redirectUrl += redirectUrl.contains("?") ? "&" : "?";
			redirectUrl += "timeout";
		}
		return redirectUrl;
	}

	private boolean isRequestedSessionInvalid(HttpServletRequest request) {
		return request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid();
	}
}

SessionExpiredDetectingLoginUrlAuthenticationEntryPointをSpring SecurityのJavaConfigで設定する。

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.AuthenticationEntryPoint;

import jp.co.sample.service.UserDetailsServiceImpl;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	UserDetailsServiceImpl userDetailsService;

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
				.antMatchers("/css/**").permitAll()
				.antMatchers("/js/**").permitAll()
				.antMatchers("/fonts/**").permitAll()
				.antMatchers("/login", "/login?**").permitAll()
				.antMatchers("/logout").permitAll()
				.anyRequest().authenticated()
		.and().formLogin()
		.and().logout()
		.and().exceptionHandling()
				// 通常のRequestとAjaxを両方対応するSessionTimeout用
				.authenticationEntryPoint(authenticationEntryPoint())
		;
	}

	@Bean
	AuthenticationEntryPoint authenticationEntryPoint() {
		return new SessionExpiredDetectingLoginUrlAuthenticationEntryPoint("/login");
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService);
	}

}

WebSecurityConfigクラスでのポイントは2点。

  • exceptionHandling().authenticationEntryPoint()に拡張したSessionExpiredDetectingLoginUrlAuthenticationEntryPointを設定する
  • /login?timeoutを認証不要のURLと認識させるため、authorizeRequests().antMatchers(“/login”, “/login?**”).permitAll()を設定する。
    よくあるSpring Securityのサンプルでは、 以下のようになっていることが多いが、authorizeRequests().antMatchers(“/login”, “/login?**”).permitAll()を設定するため、formLogin()とlogout()の箇所でpermitAll()をせず、authorizeRequests()でpermitAll()する。
.and().formLogin()
		.permitAll()
.and().logout()
		.permitAll()

以上でSpring Boot + Spring Security使用時のSessionTimeout対応は完了。
ただし、CSRF対策が有効の場合、POST時にSessionTimeoutしているとHTTP Status:403 Forbiddenが発生してしまう問題がある。
CSRFとSessionTimeout問題は別ページで対応策を記載する。
Spring Boot + Spring Security使用時のCSRFとSessionTimeoutの問題

Spring Boot + Spring Security使用時のCSRFとSessionTimeoutの問題

$
0
0

Spring Boot + Spring Security使用時のSessionTimeout対応の最後に、「CSRF対策が有効の場合、POST時にSessionTimeoutしているとHTTP Status:403 Forbiddenが発生してしまう問題がある。」と記載した。

今回はこの問題の対応方法を記載し、Spring SecurityのJavaConfigの完成形を作る。

まずこの問題が起こる原因は、CSRF対策の仕組みが、リクエストパラメータで送られるCSRF TokenとSessionに保存されたCSRF Tokenを比較するというロジックであり、Sessionに依存しているから。
SessionがTimeoutによって消滅しているときにCSRF Tokenをリクエストパラメータで送っても、Sessionは既に存在していないから必ずTokenが違うということになる。

対策したソースを記載する。

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.csrf.MissingCsrfTokenException;

import jp.co.sample.service.UserDetailsServiceImpl;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	UserDetailsServiceImpl userDetailsService;

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
				.antMatchers("/css/**").permitAll()
				.antMatchers("/js/**").permitAll()
				.antMatchers("/fonts/**").permitAll()
				.antMatchers("/login", "/login?**").permitAll()
				.antMatchers("/logout").permitAll()
				.anyRequest().authenticated()
		.and().formLogin()
		.and().logout()
		.and().exceptionHandling()
				// 通常のRequestとAjaxを両方対応するSessionTimeout用
				.authenticationEntryPoint(authenticationEntryPoint())
				// csrfはsessionがないと動かない。SessionTimeout時にPOSTすると403 Forbiddenを必ず返してしまうため、
				// MissingCsrfTokenExceptionの時はリダイレクトを、それ以外の時は通常の扱いとする。
				.accessDeniedHandler(accessDeniedHandler())
		;
	}

	@Bean
	AuthenticationEntryPoint authenticationEntryPoint() {
		return new SessionExpiredDetectingLoginUrlAuthenticationEntryPoint("/login");
	}

	@Bean
	AccessDeniedHandler accessDeniedHandler() {
		return new AccessDeniedHandler() {
			@Override
			public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
				if (accessDeniedException instanceof MissingCsrfTokenException) {
					authenticationEntryPoint().commence(request, response, null);
				} else {
					new AccessDeniedHandlerImpl().handle(request, response, accessDeniedException);
				}
			}
		};
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService);
	}

}

前回との差分は1点。

  • exceptionHandling().accessDeniedHandler()にAccessDeniedExceptionが発生したときの処理を書いた無名クラスを設定した。

発生したExceptionが、CSRF Tokenがない場合に発生するMissingCsrfTokenExceptionだった場合、SessionTimeoutであると判断してSessionExpiredDetectingLoginUrlAuthenticationEntryPointを実行する。
SessionExpiredDetectingLoginUrlAuthenticationEntryPointではSessionが当然Invalidであると判断するので、/login?timeoutにリダイレクトしてくれる。

Spring Bootで日本語ファイル名のファイルダウンロード

$
0
0

日本語ファイル名のファイルをダウンロードするときのポイントは3点。
・引数でHttpServletResponseを受け取る。
・レスポンスヘッダにContent-Dispositionをつける。この時、URLエンコードしたファイル名の前に「filename*=UTF-8”」をつける。
・OutputStreamにファイルを書き込む。

@RequestMapping(value = "/download", method = RequestMethod.GET)
public String download(HttpServletResponse response) throws IOException {
	File file = new File("filepath");
	response.addHeader("Content-Disposition", "attachment; filename*=UTF-8''" + URLEncoder.encode(file.getName(), StandardCharsets.UTF_8.name()));

	Files.copy(file.toPath(), response.getOutputStream());
	return null;
}

Java標準ライブラリでTupleを使う。

$
0
0

Javaの標準ライブラリには、ScalaのようなTuple1~22が定義されていないが、Tuple2相当のものであれば存在する。
scala-libraryを入れられない場合で、Tuple2のみ使えればいい場合は、AbstractMap.SimpleEntryを使用する。

//import java.util.AbstractMap.SimpleEntry;

SimpleEntry<String, Integer> tuple2 = new SimpleEntry<>("_1", 2);
String t1 = tuple2.getKey();
Integer t2 = tuple2.getValue();

ただし、swap等は使用できない。

ちなみにscala-libraryが入れられる場合は、以下のようにJavaから呼び出せる。

//import scala.Tuple2;

Tuple2<String, Integer> tuple2 = new Tuple2<>("_1", 2);
String t1 = tuple2._1;
Integer t2 = tuple2._2;


Seleniumでファイルダウンロードして、その後検証する方法

$
0
0

version: Selenium WebDriver 2.53.0、JUnit4

Seleniumでダウンロードダイアログを出さずに強制的にファイルダウンロードして、その後ファイルを検証したい。
ダウンロードする場所は、時間が経ったら消えてくれるように、Tempディレクトリ以下にする。
また、ダウンロードした後にファイルを簡単に取得できるようにTempディレクトリの下にSelenium起動時刻がついたディレクトリを作成して、その直下にダウンロードする。

WebDriver driver;

Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"), "seleniumtest", Long.toString(System.currentTimeMillis()));

@SneakyThrows(IOException.class)
@Before
public void setup() {
	Files.createDirectories(tempDir);

	FirefoxProfile profile = new FirefoxProfile();
	profile.setPreference("browser.download.folderList", 2);
	profile.setPreference("browser.download.dir", tempDir.toString());
	profile.setPreference("browser.download.useDownloadDir", true);
	profile.setPreference("browser.helperApps.neverAsk.saveToDisk", "text/html, text/plain, application/vnd.ms-excel, text/csv, application/zip, text/comma-separated-values, application/octet-stream");
	driver = new FirefoxDriver(profile);
}

@SneakyThrows(InterruptedException.class)
private void download() {
	driver.findElement(By.id("btnDownload")).click();
	TimeUnit.SECONDS.sleep(3);
}

@Test
public void ダウンロード() {
	// ダウンロードページまでの遷移は略
	download();

	File file = Arrays.stream(tempDir.toFile().listFiles())
					.sorted(Comparator.comparing(File::lastModified).reversed())
					.findFirst()
					.get();
	assertEquals("expectedFileName", file.getName());
}}

シェルスクリプトでパスワードを変更する

$
0
0

passwdコマンドで対話的にパスワードを変更するのではなく、コマンドでワンライナーあるいはシェルスクリプト実行で変更したい場合、chpasswdを使用する。
一人分のパスワード変更であれば以下のように書ける。

echo "username:password" | chpasswd

複数人のパスワードを一回で変更したければ、ファイルに同様の書式で、各行に各人の”username:password”を書けばいい。尚、空行が入らないように注意すること。

chpasswd < ファイル名

自分一人の使い捨てスクリプトであれば上記対応で問題ないが、サーバの初期構築で固定のユーザを一括で登録するなどの目的であれば、パスワードが平文でファイルやコマンドに記載されているのは、セキュリティ上好ましくない。

chpasswdは暗号化されたパスワードも受け取ってくれるので、ファイルに”username:encrypted_password”の書式で書くことにする。
encrypted_passwordの部分の書式は、/etc/shadowでも使用されているパスワードファイル用の形式と同じ。

$<ハッシュ方式>$<salt>$<ハッシュ後のパスワード>

この書式をPerlで作成する。

perl -e 'print "username:", crypt("plain_password", "\$6\$" . join ("", map { (a..z,A..Z)[rand 52] } 1..8)), "\n"' >> /tmp/passwordfile

・ $6$はSHA512でハッシュ化することを示している。

join ("", map { (a..z,A..Z)[rand 52] } 1..8))
は8桁のSaltをランダムに生成している。

簡略化すると以下のコマンドになる。

perl -e 'print "username:", crypt("plain_password", "\$6\$salt"), "\n"'

出力結果は以下のようになる。

username:$6$rYlftLJE$x.ucNeKnapdsGsX5KEHhgwnlhg1XV9nj7cF5ihl9lQHQlX2C8iUjMJf4VcZwjZhl5yyHBKU2.U1Lbjgbo4l.G1

/tmp/passwordfileに”username:encrypted_password”が出力されたので、これをchgpasswdに渡してあげれば良いが、暗号化されたパスワードをchgpasswdに渡す場合は、-e オプションが必要となる。

chgpasswd -e < /tmp/passwordfile

Apache POIでExcelに複数の画像を連続で貼り付ける

$
0
0

画面キャプチャをSeleniumでとって、ExcelにApache POIで画像を複数枚連続で貼り付けるときに使用したコード。
今回はエビデンス取得目的の全画面キャプチャなので、画像の大きさはすべて同じで、貼り付け方は1シート目のA1から下方向に連続で貼り付けるという仕様とする。

Workbook book = new HSSFWorkbook();
Sheet sheet = book.createSheet();
// 1画像あたり縦横何個のセルを使用するか(この範囲に合わせてリサイズする)
int picRows = 29;
int picCols = 13;

for (int i = 0; i < files.size(); i++) {
	try (ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream()) {
		BufferedImage img = ImageIO.read(file.get(i));
		ImageIO.write(img, "png", byteArrayOut);
	
		Drawing drawing = sheet.createDrawingPatriarch();
		// 画像の貼り付け位置指定
		// dx1 = 0, dy1 = 100 : 貼り付け開始位置(col1, row1)からの指定した分だけずれて貼り付けされる。
		// dx2 = 0, dy2 =   0 : 貼り付け終了位置(col2, row2)からの指定した分だけずれて貼り付けされる。
		// dy1に値をセットしているのは、連続で貼り付けた時に上下の画像が完全にくっついてしまうのが見づらいため。
		// col1 = 0, row1 = i * picRows : 貼り付け開始位置。
		// col2 = picCols, row2 = (i + 1) * picRows : 貼り付け終了位置。
		ClientAnchor anchor = drawing.createAnchor(0, 100, 0, 0, 0, i * picRows, picCols, (i + 1) * picRows);
		int picIndex = book.addPicture(byteArrayOut.toByteArray(), Workbook.PICTURE_TYPE_PNG);
		drawing.createPicture(anchor, picIndex);
	}
}
try (OutputStream out = new FileOutputStream(Paths.get(System.getProperty("java.io.tmpdir"), "エビデンス.xls").toFile())) {
	book.write(out);
}

自作プログラム起動用batファイルでフルパスを環境によらずに指定する方法

$
0
0

自作プログラムの起動用batファイルを作成し、プログラム本体とbatファイルを同一フォルダにまとめておくとする。
たとえば以下のような構成の時、各プログラムのフルパス指定等をどのように書けばいいか。

MyPGFolder
├ execMyPG.bat
├ Dependency
│   └ OtherPG.exe
└ MyPG.jar

execMyPG.bat

set MYPG=???
set DEPEND_PG=???
set OUT_DIR=C:\Users\%username%\Desktop
:: 引数0 -> 依存プログラム, 引数1 -> 出力ディレクトリ
java -jar %MYPG% %DEPEND_PG% %OUT_DIR%

???部分に相対パスとして

set MYPG=MyPG.jar
,
set DEPEND_PG=Dependency\OtherPG.exe
のように書いてしまうと、batファイルをダブルクリックして起動するときには問題ないが、別の場所からコマンドプロンプトで実行したりタスクスケジューラから実行したりすると、パス指定がおかしくなり動かない。

フォルダの配置場所をCドライブ直下等に決め打ちして、

set MYPG=C:\MyPGFolder\MyPG.jar
とすると、場所が変わった時に動かない汎用性のないものになってしまう。

対策法は、batファイル起動中に、batファイル自身がいる場所をコマンドにより取得して、パス指定の時はすべてその場所を前につけること。

%~dp0
でbatファイル自身がいる場所を取得できる。
コマンドの意味は、http://pentan.info/server/windows/cmd/dp0.htmlに詳細に説明されている。

%0
実行されているファイルのパスです。
~
“(ダブルクオート)を除く
d
ドライブ文字だけに展開する
p
ファイル名を除くパスの部分に展開する

つまり%~dp0 は、『実行されているファイルが置かれているカレントディレクトリ』を表します。

オプション構文はcallのヘルプで詳しく説明されています。
C:\>call /?

execMyPG.bat

set MYDIR=%~dp0
set MYPG=%MYDIR%MyPG.jar
set DEPEND_PG=%MYDIR%Dependency\OtherPG.exe
set OUT_DIR=C:\Users\%username%\Desktop
:: 引数0 -> 依存プログラム, 引数1 -> 出力ディレクトリ
java -jar %MYPG% %DEPEND_PG% %OUT_DIR%

ちなみに、batファイルの初めに

cd /d %~dp0
をして、各パスはフルパスではなく相対パスで設定する方法もあるが、batファイルが終了したときのカレントディレクトリが変わってしまうのでお勧めできない。

参考:Linuxで同じことをするには

$(dirname $(readlink -f $0))

シェルスクリプト自身のディレクトリの絶対パスを取得する

sbtで実行可能なfat jarを作る方法と依存ライブラリを含めて一まとめにzip化する方法の比較

$
0
0

前提version: Scala 2.11.7, sbt 0.13

目次

fat jar, zipのメリットデメリット
build.sbt
fat jarの作成方法
zipの作成方法

fat jar, zipのメリットデメリット

Scalaで作成したプログラムをsbtを使用してjarファイルにする方法は

sbt package
だが、これで作成されるjarファイルは依存ライブラリを含まない、自分で作成したソース部分のみとなる。
プログラムの配布等を考えると、fat jarか依存ライブラリを含めて一まとめにしたzipを出力したい。

fat jarは単一のファイルとして動くので解凍の必要がなく、OSによってダブルクリックで起動できる等様々な利点があるが、依存ライブラリ全てと作成プログラムをマージすることになるのでfat jarを作成する時間が長くなるという欠点がある。

対してzip化の場合は、fat jarほどユーザフレンドリーではないという欠点があるが、依存ライブラリをivyのcacheからコピーしてくるだけでなので作成時間が短い。また作成プログラムに修正を加えた場合、解凍したzipのうち作成したプログラムのjarだけを

sbt package
により再作成して入れ替えればいいので、修正に伴うjar作成のコストが圧倒的に低くなる。

現在作成している依存ライブラリが個のプログラムでpackageとzip、fat jarにかかる時間を比べると、それぞれ1秒、7秒、62秒となった。
これに修正時のjar化コストを考慮すると、「初回zip + 次回以降package」と「fat jarを毎回」の差はどんどん開いていく。

それぞれ利点欠点があるのでどちらの方法でもjarをsbtで簡単に作成できるようにしたい。

build.sbt

今回使用するbuild.sbtの雛形。配置場所はSampleProject直下。

build.sbt

scalaVersion := "2.11.7"

libraryDependencies ++= Seq(
		"org.apache.poi" % "poi" % "3.14"
		, "org.apache.poi" % "poi-ooxml" % "3.14"
		, "org.apache.poi" % "poi-ooxml-schemas" % "3.14"
		, "org.apache.poi" % "poi-scratchpad" % "3.14")

fat jarの作成方法

fat jarを作成するためにはsbt-assemblyというプラグインが必要になる。
プラグインを追加するために以下のファイルを.sbt/0.13/plugins/に配置する。

plugins.sbt

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.2")

fat jar作成の実行コマンドは

sbt assembly
となる。

zipの作成方法

zipを作成するためにはsbt-native-packagerというプラグインが必要になる。
プラグインを追加するために以下のファイルを.sbt/0.13/plugins/に配置する。

plugins.sbt

addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.1.0-RC1")

プラグインを加えた後、build.sbtに

enablePlugins(JavaAppPackaging)
を加えてあげる必要がある。この意味は、公式サイトに書かれている通り、package方法とarchetypeを指定する仕組みを提供しているからで、今回はサーバに配置するようなものではないコマンドラインで動かすjarを作るという目的なので、”the basic Java Application Archetype”、つまりJavaAppPackagingを使うようにしている。サーバに配置する用のJavaServerAppPackaging等もあるので、目的に合わせて設定する。

Native packager provides packaging format plugins and archetype plugins to separate configuration and actual packaging. To get started we use the basic Java Application Archetype.

build.sbt

scalaVersion := "2.11.7"
enablePlugins(JavaAppPackaging)

libraryDependencies ++= Seq(
		"org.apache.poi" % "poi" % "3.14"
		, "org.apache.poi" % "poi-ooxml" % "3.14"
		, "org.apache.poi" % "poi-ooxml-schemas" % "3.14"
		, "org.apache.poi" % "poi-scratchpad" % "3.14")

zip作成の実行コマンドは

sbt universal:packageBin
となる。
universalの部分がWindowsでもLinuxでも使えるpackage方法として、普遍的なzipを指定していることになる。
Viewing all 100 articles
Browse latest View live