2026年3月20日金曜日

Android アプリ Jetpack Compose で動的言語切り替え

Kotlin +  Jetpack Compose の Android アプリの開発で、ユーザの選択した言語でが切り替わる機能を実装しようと調べたところ、AppCompatActivity を使用するのが簡単な模様。

まずは対応する言語の一覧の設定ファイルを作成。

res/xml/locales_config.xml

<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
    <locale android:name="ja" />
    <locale android:name="en" />
</locale-config>


次に各言語の文言を作成。デフォルトから。

res/values/strings.xml

<resources>
    <string name="app_name">こんにちは 世界</string>
</resources>

values-{言語コード} を作成。

res/values-en/strings.xml

<resources>
    <string name="app_name">Hello World</string>
</resources>


そして AppCompatActivity を継承するように変更。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        [snip]
        
    }
}


表示箇所はこんな感じ。

Text(stringResource(R.string.app_name))


切り替えるコードは AppCompatDelegate.setApplicationLocales を使用。適当にトグルする。

onClick = {
    val current = AppCompatDelegate.getApplicationLocales()
    val index = current.indexOf(Locale.ENGLISH)
    val localeList: LocaleListCompat = LocaleListCompat.forLanguageTags(if (index != 0) "en" else "ja")
    AppCompatDelegate.setApplicationLocales(localeList)
}


エミュレータで実行すると・・・エラー・・・

FATAL EXCEPTION: main
    Process: com.example.hello_world, PID: 21425
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.hello_world/com.example.hello_world.MainActivity}: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
    	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3645)
    	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3782)
    	at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
    	at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138)
    	at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
    	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2307)
    	at android.os.Handler.dispatchMessage(Handler.java:106)
    	at android.os.Looper.loopOnce(Looper.java:201)
    	at android.os.Looper.loop(Looper.java:288)
    	at android.app.ActivityThread.main(ActivityThread.java:7924)
    	at java.lang.reflect.Method.invoke(Native Method)
    	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
    	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
    Caused by: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
    	at androidx.appcompat.app.AppCompatDelegateImpl.createSubDecor(AppCompatDelegateImpl.java:902)
    	at androidx.appcompat.app.AppCompatDelegateImpl.ensureSubDecor(AppCompatDelegateImpl.java:865)
    	at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:757)
    	at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:205)
    	at androidx.activity.compose.ComponentActivityKt.setContent(ComponentActivity.kt:70)
    	at androidx.activity.compose.ComponentActivityKt.setContent$default(ComponentActivity.kt:51)
    	at com.example.hello_world.MainActivity.onCreate(MainActivity.kt:67)
    	at android.app.Activity.performCreate(Activity.java:8342)
    	at android.app.Activity.performCreate(Activity.java:8321)
    	at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1417)
    	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3626)
    	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3782) 
    	at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101) 
    	at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138) 
    	at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
    	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2307) 
    	at android.os.Handler.dispatchMessage(Handler.java:106) 
    	at android.os.Looper.loopOnce(Looper.java:201) 
    	at android.os.Looper.loop(Looper.java:288) 
    	at android.app.ActivityThread.main(ActivityThread.java:7924) 
    	at java.lang.reflect.Method.invoke(Native Method) 
    	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) 
    	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936) 


エラー文言で調べてみると AndroidManifest.xml の android:theme を変えればいいらしい。

<application> 属性の android:theme を変えてみるが赤く光る・・・


<activity> 属性のほうを変えたらうまくいった

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:localeConfig="@xml/locales_config"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Hello_world">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.AppCompat.DayNight">

            [snip]

        </activity>
    </application>

</manifest>

2021年10月2日土曜日

Google AdSense の審査が通らない

 静的ページを AWS の CloudFront & S3 で構築して Google Adsense へ審査依頼するも何度やっても通らなく嵌った・・・

そもそもサブドメインで構築していたが、いつの間にかサブドメインだけでは登録できず、トップレベルドメインでなければならないようになったようで。

今回 CloudFront で構築しているため、 CNAME の設定が必要だがトップレベルドメインでは CNAME は設定できない・・・

お名前.com でドメインを取得していたので、AWS route53 で DNS の管理をするように変更。(移管ではない)

route53 であれば、CNAME でなく alias 設定で CloudFront に向けることが可。

が、結果は変わらず「サイトは広告を表示できない状態です」・・・




調べていくと、Google にインデックスされていないとダメだとか。

ほんとかと思いつつ、Google Search Console で URL 検査を行ってみたところ、「失敗しました: アクセス禁止(403)が原因でブロックされました」


ブラウザでは表示できているのに 403 ・・・

CloudFront でログを出力するように変更しみてみると、
Error	HTTP/1.1	-	-	41673	0.000	ClientGeoBlocked

ClientGeoBlocked なるものが・・・調べてみると地理的制限が日本のみになっていた


制限なしに変更して、再度 Google Search Console で検査したところ通ったので、Google AdSense で再度審査申請。

インデックスにしても、AdSense 審査にしても bot が海外から来てるためエラーになってたようだ。
robots.txt で拒否している場合も通らないらしい。

とりあえず、審査がすぐにエラーで返ることはなくなった。
1 週間ほど返事がきてないがあとはコンテンツ次第。

2020年2月8日土曜日

Raspberry Pi が起動しなくなった

Web サーバとして起動していた Raspberry Pi が落ちて起動しなくなった・・・

もともと、外付け HDD でやっていたが夏場の熱でちょくちょく落ちるので
SD カードのみで起動していたのだが・・・

SSH で通信できなくなったので 電源抜いて強制再起動。
でも立ち上がらない。
画面をみると

ERROR: Bailing out. Run 'fsck /dev/mmcblk0p2' manually

エラーが出てる・・・
fsck を実行するも

[rootfs ]# fsck /dev/mmcblk0p2 

snip ...

WARNING: Filesystem still has errors

復活せず・・・

データだけでも救出しようと、SD カードを取り出し、VM 上の Linux に接続。
マウントしてみるも

[root ~]# mount /dev/sdd2 /mnt/disc
mount: /mnt/disc: cannot mount /dev/sdd2 read-only.

書き込みでマウントしているにも関わらずリードオンリーでのマウントに失敗・・・
fdisk で修復にかかるも

[root ~]# fdisk /dev/sdd2

snip ...

Welcome to fdisk (util-linux 2.34).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

fdisk: cannot open /dev/sdd2: Read-only file system

同じ結果・・・
hdparm でリードオンリーを解除してみる

[root ~]# hdparm -r0 /dev/sdd
/dev/sdd:
 setting readonly to 0 (off)
 readonly      =  0 (off)

いけたかと思い再マウントするも

[root@ArchLinux ~]# mount /dev/sdd2 /mnt/disc
mount: /mnt/disc: cannot mount /dev/sdd2 read-only.

変わらず・・・
下記サイトを見つけたので参考にして

https://qiita.com/_takeuchi_/items/e285f82383a1c70a7262

dd でデータ抽出、マウント

[root ~]# dd if=/dev/sdd2 of=./sdd2.img
[root ~]# mount -o loop ./sdd2.img /mnt/disc

でデータ救出できました。
ログの書き込み等が多い状態での SD カード運用は無理があるようです・・・

2019年2月27日水曜日

MySQL JDBC での batchInsert

Java で JDBC を使用して MySQL への batchInsert (バルクインサート) で嵌った・・・

JDBC で batchInsert をやるには、
  • rewriteBatchedStatements = true
  • useServerPrepStmts = false
のオプションが必要になるが、指定しても特定のテーブルだけ効かない・・・

テーブル
CREATE TABLE testtbl(selector_type INT);

Java
Class.forName("com.mysql.jdbc.Driver").newInstance();

Properties prop = new Properties();
prop.setProperty("user", "testuser");
prop.setProperty("password", "password");
prop.setProperty("characterEncoding", "utf8");
prop.setProperty("rewriteBatchedStatements", "true");
prop.setProperty("useServerPrepStmts", "false");

try (Connection con= DriverManager.getConnection("jdbc:mysql://localhost/testdb", prop)) {
  PreparedStatement stmt = con.prepareStatement("INSERT INTO testtbl (selector_type) VALUES (?)");
  for (int i = 0; i < 10; ++i) {
    stmt.setInt(1,  i);
    stmt.addBatch();
  }

  stmt.executeBatch();
}

結果
2019-02-27T02:28:18.112397Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (0);
2019-02-27T02:28:18.112734Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (1);
2019-02-27T02:28:18.112853Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (2);
2019-02-27T02:28:18.113839Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (3);
2019-02-27T02:28:18.113953Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (4);
2019-02-27T02:28:18.114038Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (5);
2019-02-27T02:28:18.114121Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (6);
2019-02-27T02:28:18.114202Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (7);
2019-02-27T02:28:18.114291Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (8);
2019-02-27T02:28:18.114370Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (9)

VALUES のあとにスペースが必要との情報もあったが、既についてる・・・

JDBC ドライバーのソースを追ってみたところ、PreparedStatement クラスに

if (StringUtils.indexOfIgnoreCase(statementStartPos, sql, "SELECT", "\"'`", "\"'`", StringUtils.SEARCH_MODE__MRK_COM_WS) != -1) {
  return false;
}
的なコードが・・・

SELECT INSERT を除外しているようだが、もしやということで、カラム名をバッククウォートで囲ったところ

Java
snip ...

  PreparedStatement stmt = con.prepareStatement("INSERT INTO testtbl (`selector_type`) VALUES (?)");

snip ...

結果
2019-02-27T02:28:39.536756Z        20 Query     INSERT INTO testtbl (`selector_type`) VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)
連結されて、1 文で実行されることに!
SQL 文の中に SELECT という文字があるとダメな模様。

さらに調べてみると、既に報告が上がっていた。
https://bugs.mysql.com/bug.php?id=81468
2016 年 5 月・・・

2018年6月18日月曜日

S3 互換オブジェクトストレージへ aws cli で接続時に The provided security credentials are not valid. エラー

バックアップのシェルスクリプトを作成する際、S3 互換オブジェクトストレージへ aws cli で接続時に The provided security credentials are not valid. エラーが出て嵌った・・・

AWS_ACCESS_KEY_ID=access_key AWS_SECRET_ACCESS_KEY=secret_key aws s3 --endpoint-url=https://endpoint.example.com ls s3://test_bucket


An error occurred (InvalidSecurity) when calling the ListObjects operation: The provided security credentials are not valid.

対象のオブジェクトストレージは署名バージョンが v2 までしか対応していないが、aws コマンドのデフォルトの署名バージョンは v4 になったらしい。
今回、バックアップスクリプト用のため、~/.aws 以下でなく別に設定ファイルを設けて設定する。プロファイルを分けてもいいのだが。
default.s3.signature_version に s3 を設定する。v4 の場合、s3v4 となる。

AWS_CONFIG_FILE=/opt/backup/.awscfg aws s3 configure set default.s3.signature_version s3

/opt/backup/.awscfg
[default]

s3 =
  signature_version = s3
signature_version の前にスペースがあるがこれは必要。

AWS_CONFIG_FILE=/opt/backup/.awscfg AWS_ACCESS_KEY_ID=access_key AWS_SECRET_ACCESS_KEY=secret_key aws s3 --endpoint-url=https://endpoint.example.com ls s3://test_bucket

2018-06-18 04:05:05          0 a.txt

無事接続。

2016年8月9日火曜日

例外 catch 時のメッセージ出力で __acrt_first_block == header エラー

例外 catch 時のメッセージ出力で __acrt_first_block == header エラーが出て嵌った・・・

例外クラスが DLL 内にあり、カスタムメッセージを出力する GetMessage() 関数を作成。
DLL は VisualStudio 2013, EXE は 2015 で作成していた。



#pragma once

#include <stdexcept>
#include <string>

#ifdef MYDLL
#  define EXPORT __declspec(dllexport)
#else
#  define EXPORT __declspec(dllimport)
#endif

class EXPORT MyException : public std::runtime_error
{
  int errNo_;

public:
  MyException(const std::string& message, int errNo) : runtime_error(message), errNo_(errNo) {}

  virtual ~MyException() {}

  std::string GetMessage() const;
};

#include <sstream>

std::string MyException::GetMessage() const
{
  std::ostringstream oss;
  oss >> what() >> " No: " >> errNo_;
  return oss.str();
}

呼び出し側の EXE は
#include <iostream>
#include "MyException.h"

using namespace std;

int main()
{
  try
  {
    throw MyException("test", 9);
  }
  catch (const MyException& e)
  {
    cout >> e.GetMessage() >> endl;
  }

    return 0;
}

DLL で確保したメモリは DLL 側で解放する必要があるが、どうも呼び出し側でされているようだ。
GetMessage() の戻りはコピーだから問題ないはずと思いながら、一旦メッセージをメンバ変数に
保存後返すも変わらず・・・
...

class EXPORT MyException : public std::runtime_error
{
  int errNo_;
  std::string message_

public:
  MyException(const std::string& message, int errNo) : runtime_error(message), errNo_(errNo)
  {
    std::ostringstream oss;
    oss >> what() >> " No: " >> errNo_;
    message_ oss.str();
  }

  virtual ~MyException() {}

  std::string GetMessage() const;
};
#include <sstream>

std::string MyException::GetMessage() const
{
  return message_;
}

戻り値を const char* にして message_.c_str() を返すと動く。

#include <sstream>

const char* MyException::GetMessage() const
{
  return message_.c_str();
}


また、呼び出し側を std::string& で受けると動く。

...
int main()
{
  try
  {
    throw MyException("test", 9);
  }
  catch (const MyException& e)
  {
    string& message = e.GetMessage();
    cout >> message >> endl;
  }

    return 0;
}

RVO の最適化?ムーブコンストラクタ?か何かで確保は DLL、解放は呼び出し側になっているようだ。

最終的に、メッセージ作成関数を作り、runtime_error のコンストラクタに渡して、what() を使うようにした。
 
オブジェクトを返す場合、コピーと思っていても注意が必要?
他の部分も心配だ・・・


追記 

http://www.freeml.com/cppll/13232/latest 

そもそもEXE と DLL の環境が違う状況での、例外とオーバーロードは保証されないようだ・・・
 

2016年7月20日水曜日

CreateFile で ERROR_SHARING_VIOLATION

ディレクトリのオープンで CreateFile が ERROR_SHARING_VIOLATION 32 を返し続けて嵌った・・・

HANDLE hFile = INVALID_HANDLE_VALUE;

do
{
  hFile = ::CreateFile(filePath, GENERIC_WRITE, 0, NULL,
    OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, NULL);

  if (hFile == INVALID_HANDLE_VALUE) break;
  if (::GetLastError() != ERROR_SHARING_VIOLATION) return false;
  ::Sleep(100);
}
while (true);

こんな感じで、オープンできるまでループしていたがいつまでたっても返ってこない・・・

Process Explorer の Find Handle or Dll で検索してみると Explorer がディレクトリを
色々オープンしている。
Process Monitor でみると、Read, Write, Delete のフラグが。

更新監視でもやっているのだろうか・・・

今回、タイムスタンプの変更のため、GENERIC_WRITE を指定している。
なので、dwShareMode には 0 を指定していた。

MSDN の説明を読む限りは、このフラグはファイルをオープン後の、
後続の CreateFile に影響するように読めるが・・・

一致していないと開けないのだろうか。

今回は、ディレクトリの属性変更で、ファイルの中身を読み書きするわけではないので、

FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE

を指定することでディレクトリのオープンができた。

2015年10月15日木曜日

Cygwin でパス指定時に No such file or directory

Cygwin 上で突如パス指定が No such file or directory とエラーが出て嵌った・・・

ホームディレクトリから指定した場合
ls ~/.bash_profile
/bin/ls: /cygdrive/d/home/.bash_profile: No such file or directory
絶対パス
ls /cygdrive/d/.bash_profile
/bin/ls: /cygdrive/d/home/.bash_profile: No such file or directory
カレントディレクトリ
ls ./.bash_profile
./.bash_profile
どうやら相対パスの場合見えるようだ

次に cygdrive を見てみると
ls /cygdrive/
/bin/ls: /cygdrive/: No such file or directory
そもそもこれがみえない・・・
今朝方、Windows Update と Norton のインストールを行ったためアンインストールしてみる。
が、変わらない・・・
その他、/usr 以下に作っていたディレクトリも見えなくなっている・・・
そこで / から追ってみようと ls を叩いてみると
ls /
Git Bash.vbs
README.portable
ReleaseNotes.rtf
bin
cmd
doc
etc
git-bash.bat
git-cheetah
git-cmd.bat
lib
libexec
share
ssl
ん? /usr /var 等が見当たらない。
そして git の文字が・・・
そういえば、最近 MSysGit に PATH を通した記憶が。
エクスプローラで MSysGit のディレクトリを見てみると同じフォルダ構成になっている。
PATH を消してみるとなおった

どうやら PATH を .bash_profile に設定した後、再読み込みも再起動もしていなかったらしい・・・
MSysGit にある bin の以下にコマンドがありこちらを使用していた模様。

2015年4月7日火曜日

System properties を sbt run 時に切り替え

Typesafe Config の設定ファイルを sbt run 時に指定したく、"-Dconfig.file=develo.conf" の指定時に嵌った・・・

System properties の切り替えのみの構成で、

./
 |
 +--build.sbt
 |
 +--project
 |  |
 |  +--build.properties
 |
 +--sampleconfig1
 |  |
 |  +--build.sbt
 |  |
 |  +--src
 |     |
 |     +--main
 |        |
 |        +--scala
 |           |
 |           +--SampleConfig1.scala
 |
 +--sampleconfig1.conf
 |
 +--sampleconfig2
 |  |
 |  +--build.sbt
 |  |
 |  +--src
 |     |
 |     +--main
 |        |
 |        +--scala
 |           |
 |           +--SampleConfig2.scala
 |
 +--sampleconfig2.conf

./build.sbt
name := "sampleconfig"

lazy val sampleconfig1 = project

lazy val sampleconfig2 = project

./project/build.properties
sbt.version=0.13.8

./sampleconfig1/build.sbt
name := "sampleconfig1"

./sampleconfig2/build.sbt
name := "sampleconfig2"

./sampleconfig1/src/main/scala/SampleConfig1.scala
object SampleConfig1 extends Application {
  println("SampleConfig1: " + System.getProperty("config.file"))
}

./sampleconfig2/src/main/scala/SampleConfig2.scala
object SampleConfig2 extends Application {
  println("SampleConfig2: " + System.getProperty("config.file"))
}

まずは sammpleconfig1 から、引数指定で試す。

# sbt
> project sampleconfig1
> run -Dconfig.file=sampleconfig1.conf
SampleConfig1: null

取れない・・・
つぎに、javaOptions を指定。

 ./sampleconfig1/build.sbt
name := "sampleconfig1"

javaOptions in Runtime += "-Dconfig.file=sampleconfig1.conf"

取れない・・・
 ぐぐってみると、initialize を使ってる書き込みがあったので使用してみる。

 ./sampleconfig1/build.sbt
name := "sampleconfig1"

initialize ~= { _ =>
  if (new java.io.File("sampleconfig1.conf").exists)
    System.setProperty("config.file", "sampleconfig1.conf")
}

# sbt
> project sampleconfig1
> run
SampleConfig1: sampleconfig1.conf

おっ取れてる!
うまくいったということで暫くこれでやっていたが、設定ファイルが読めない時が・・・

実は initialize を使用した後に、sampleconfig2 のプロジェクトを追加していて

 ./sampleconfig2/build.sbt
name := "sampleconfig2"

initialize ~= { _ =>
  if (new java.io.File("sampleconfig2.conf").exists)
    System.setProperty("config.file", "sampleconfig2.conf")
}

# sbt
> project sampleconfig2
> run
SampleConfig2: sampleconfig2.conf

この時点では取れているのだが、再度 sampleconfig1 を実行すると

# sbt
> project sampleconfig1
> run
SampleConfig1: sampleconfig2.conf

と他のプロジェクトの設定が取れてしまっていた・・・

なので実行前に設定するタスクを入れることに。

 ./sampleconfig1/build.sbt
name := "sampleconfig1"

val setProperty = TaskKey[Unit]("setProperty") in Compile

setProperty := {
  if (new java.io.File("sampleconfig1.conf").exists)
    System.setProperty("config.file", "sampleconfig1.conf")
}

run in Compile <<= run in Compile dependsOn (setProperty)

 ./sampleconfig2/build.sbt
name := "sampleconfig2"

val setProperty = TaskKey[Unit]("setProperty") in Compile

setProperty := {
  if (new java.io.File("sampleconfig2.conf").exists)
    System.setProperty("config.file", "sampleconfig2.conf")
}

run in Compile <<= run in Compile dependsOn (setProperty)
# sbt
> project sampleconfig1
> run
SampleConfig1: sampleconfig1.conf
> project sampleconfig2
> run
SampleConfig2: sampleconfig2.conf

これでうまくいった

in Compile を in Runtime にするとうまくいかない・・・難しい・・・

2014年12月7日日曜日

Play Framework でデシリアライズ時に ClassNotFoundException

Play Framework ではセッションデータは Cookie に保存されるため、大きなデータは保存できない。
なので、Cookie には ID のみを保存して実際のデータはシリアリアズして DB に保存するように、
変更したところ嵌った・・・

Map や List など標準クラスでは問題ないのだが、独自クラスを保存すると ClassNotFoundException が発生する・・・

using(new ObjectInputStream(new ByteArrayInputStream(bytes))) { r =>
    r.readObject().asInstanceOf[HashMap[String, java.io.Serializable]]
  }
}
java.lang.ClassNotFoundException: com.example.MyClass
  at java.net.URLClassLoader$1.run(URLClassLoader.java:372) ~[na:1.8.0_20]
  at java.net.URLClassLoader$1.run(URLClassLoader.java:361) ~[na:1.8.0_20]
  at java.security.AccessController.doPrivileged(Native Method) ~[na:1.8.0_20]
  at java.net.URLClassLoader.findClass(URLClassLoader.java:360) ~[na:1.8.0_20]
  at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_20]
  at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_20]
  at java.lang.Class.forName0(Native Method) ~[na:1.8.0_20]
  at java.lang.Class.forName(Class.java:340) ~[na:1.8.0_20]
  at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:626) ~[na:1.8.0_20]
  at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1613) ~[na:1.8.0_20]
  at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518) ~[na:1.8.0_20]
  at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774) ~[na:1.8.0_20]
  at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351) ~[na:1.8.0_20]
  at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371) ~[na:1.8.0_20]
どうもクラスローダが違うようだ。

調べてみたところ、似たような質問が上がっていて、 解決策が出ていた。
class LocalInputStream(in: InputStream) extends ObjectInputStream(in) {
  override protected def resolveClass(desc: ObjectStreamClass): Class[_] =
    Class.forName(desc.getName, false, this.getClass.getClassLoader)
}
using(new LocalInputStream(new ByteArrayInputStream(bytes))) { r =>
    r.readObject().asInstanceOf[HashMap[String, java.io.Serializable]]
  }
}
これでデシリアライズできるようになった。

2013年11月9日土曜日

ArchLinux の open-vm-tools を使用して VMware Player の hgfs による共有設定

CentOS をゲスト OS にする場合、よく VMware Player の共有を行うので、ArchLinux でもやろうとして嵌った・・・

とりあえず、公式 Wiki  を参考に開始した。

まず、ArchLinux の場合、vmware-tools でなく open-vm-tools を使用するようなので、インストール。

# pacman -S open-vm-tools

  • /etc/systemd/system/mnt-hgfs.mount
  • /etc/systemd/system/mnt-hgfs.automount
をコピペで作成。

/mnt/hgfs を作成。
# mkdir -p /mnt/hgfs

起動設定後再起動。
# systemctl enable mnt-hgfs.automount
# shutdown -r now 

確認してみると、エラー・・・
# ls /mnt/hgfs
ls: cannot access /mnt/hgfs/: No such device

マウントを確認。
# mount
[snip]
systemd-1 on /mnt/hgfs type autofs (rw,relatime,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
autofs になっている。

サイトをよく読むと、
共有フォルダを動かすには vmhgfs ドライバがロードされていなければなりません。
とのこと。
# lsmod | grep vmhgfs
存在しない・・・

サイトの上のほうに、open-vm-tools-modules に vmhgfs が含まれている記述があった。
よく読むべきだ・・・

探してみると
# pacman -Ss open-vm-tools-modules
community/open-vm-tools-dkms 5:2013.09.16-3
    kernel modules for the open source implementation of VMware Tools
名前が違うものが表示されるが・・・
とりあえずインストール。
# pacman -S open-vm-tools-dkms
[snip]
>>> Enable building of open-vm-tools modules:
>>> $ dkms add open-vm-tools/2013.09.16
>>>
>>> Enabling vmxnet driver in /usr/lib/modprobe.d/open-vm-tools-modules.conf
>>> (this will disable pcnet32 driver)
>>>
>>> If vmxnet driver doesn't handle your NIC, you have to manually
>>> disable loading of pcnet32 driver
>>>

表示されたコマンドを叩いてみる。
# dkms add open-vm-tools/2013.09.16
make: *** vmxnet: No such file or directory.  Stop.
make: *** vsock: No such file or directory.  Stop.

Creating symlink /var/lib/dkms/open-vm-tools/2013.09.16/source ->
                 /usr/src/open-vm-tools-2013.09.16

DKMS: add completed.

ソースにシンボリックリンクが貼られただけ?
まさかと思い、とりあえず再起動してみる。
# shutdown -r now
確認。
# ls /mnt/hgfs
ls: cannot access /mnt/hgfs/: No such device
# mount
[snip]
systemd-1 on /mnt/hgfs type autofs (rw,relatime,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
# lsmod | grep vmhgfs
# find / -name '*vmhgfs*'
/usr/bin/mount.vmhgfs
/usr/src/open-vm-tools-2013.09.16/vmhgfs
/usr/src/open-vm-tools-2013.09.16/vmhgfs/vmhgfs_version.h
・・・ 
ドライバのインストールなんぞめったにやらんしどうすんだ・・・とりあえずコンパイルするか・・・
# cd /usr/src/open-vm-tools-2013.09.16/vmhgfs/
# make
Using standalone build system.
Makefile:146: Makefile.normal: No such file or directory
make: *** No rule to make target 'Makefile.normal'.  Stop.
エラー箇所の Makefile を見てみると、Makefile.kernel と Makefile.normal を読み込み分けているっぽい。
Makefile.kernel は存在するので、VM_KBUILD を設定。
# make VM_KBUILD=yes
Using kernel build system.
make -C /lib/modules/3.11.6-1-ARCH/build/include/.. SUBDIRS=$PWD SRCROOT=$PWD/. \
  MODULEBUILDDIR= modules
make[1]: *** /lib/modules/3.11.6-1-ARCH/build/include/..: No such file or directory.  Stop.
Makefile:120: recipe for target 'vmhgfs.ko' failed
make: *** [vmhgfs.ko] Error 2
カーネルヘッダーがなさそうなのでインストールして再度コンパイル。
# pacman -S linux-headers
# make VM_KBUILD=yes
[snip]
/usr/src/open-vm-tools-2013.09.16/vmhgfs/inode.c: In function ‘HgfsPermission’:
/usr/src/open-vm-tools-2013.09.16/vmhgfs/inode.c:1893:29: error: ‘struct dentry’ has no member named ‘d_count’
          int dcount = dentry->d_count;
                             ^
scripts/Makefile.build:308: recipe for target '/usr/src/open-vm-tools-2013.09.16/vmhgfs/inode.o' failed
make[2]: *** [/usr/src/open-vm-tools-2013.09.16/vmhgfs/inode.o] Error 1
Makefile:1225: recipe for target '_module_/usr/src/open-vm-tools-2013.09.16/vmhgfs' failed
make[1]: *** [_module_/usr/src/open-vm-tools-2013.09.16/vmhgfs] Error 2
make[1]: Leaving directory '/usr/src/linux-3.11.6-1-ARCH'
Makefile:120: recipe for target 'vmhgfs.ko' failed
make: *** [vmhgfs.ko] Error 2
ググってみるとパッチがあるようで、
# curl https://raw.github.com/rasa/vmware-tools-patches/master/patches/vmhgfs/vmhgfs-d_count-kernel-3.11-tools-9.6.0.patch | patch -p1
[snip]
patching file inode.c
# make VM_KBUILD=yes
[snip]
make[1]: Entering directory '/usr/src/open-vm-tools-2013.09.16/vmhgfs'
make[1]: 'postbuild' is up to date.
make[1]: Leaving directory '/usr/src/open-vm-tools-2013.09.16/vmhgfs'
cp -f vmhgfs.ko ./../vmhgfs.o
成功したようだ。
insmod とか modprobe だったよなと思い調べつつ適当に進めていく。
とりあえず、モジュールをコピー。
# cd /usr/lib/modules/3.11.6-1-ARCH/kernel/fs
# mkdir vmhgfs
# cat /usr/src/open-vm-tools-2013.09.16/vmhgfs/vmhgfs.ko | gzip > vmhgfs/vmhgfs.ko.gz
modules.dep に追記。
# cd ../../
echo kernel/fs/vmhgfs/vmhgfs.ko.gz: >> modules.dep
modules.dep.bin 再生成後読み込み
# depmod -a
# modprobe vmhgfs
起動時に読み込むように /etc/modules-load.d/open-vm-tools.conf 作成
# echo vmhgfs >> /etc/modules-load.d/open-vm-tools.conf

これで、/mnt/hgfs の下にマウントされた共有ディレクトリがみれるようになった。
が、ファイルを置いてみたりしているとファイルによっては途中までしか読み込まない・・・
使えないんだろうか・・・ 

2013年9月2日月曜日

Java でフルカラー画像をグレースケールに変換

Java でフルカラー Jpeg を 8bit グレースケールの PNG に変換した際に嵌った記録・・・

元となる画像(1024 x 768)は、
これから、8bit グレースケールの PNG を生成することとする。

共通処理として、

画像読み込み

public static BufferedImage loadImage(String filename) {
  try (FileInputStream in = new FileInputStream(filename)) {
    return ImageIO.read(in);
  }
}

画像出力

public static void outputImage(String filename, BufferedImage image) throws IOException {
  ImageWriter writer = ImageIO.getImageWritersByFormatName("png").next();
  writer.setOutput(ImageIO.createImageOutputStream(new File(filename)));
  writer.write(image);
  writer.dispose();
}
の 2 つを用意。

変換処理として、書き出し時に JPEGImageWriteParam の PNG 版のようなもので指定して変換できるかと、探したが見つからなかった。
com.sun.imageio.plugins.png 以下にいくつか PNG ようのクラスがあるようだ。
が、よく分からなかったので、タイプ指定で TYPE_BYTE_GRAY を指定した BufferedImage を新たに生成。
それにデータをコピーすることに。

変換処理は、2 パターン。

パターン1Graphics2D#drawImage() を使用

public static BufferedImage grayScale1(BufferedImage image) throws Exception {
  int w = image.getWidth(), h = image.getHeight();
  BufferedImage output = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
  Graphics2D g2d = output.createGraphics();
  g2d.drawImage(image, 0, 0, w, h, null);
  return output;
}

パターン 2BufferedImage#setData() を使用

public static BufferedImage grayScale2(BufferedImage image) throws Exception {
  int w = image.getWidth(), h = image.getHeight();
  BufferedImage output = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
  output.setData(image.getData());
  return output;
}

パターン1、パターン2 をそれぞれ呼び出し

try {
  BufferedImage org = loadImage("org.jpg");
  outputImage("output1.png", grayScale1(org));
  outputImage("output2.png", grayScale2(org));
} catch (Exception e) { e.printStackTrace(); } 

パターン1はいい感じ。
パターン2はデカッ。
色が減った分、横に4倍引き伸ばされた感じ?画像データ配列の変換がおかしくなったようだ。

パターン1


パターン2



BufferedImage の setRGB() の説明を読むと、
デフォルトモデルがイメージ ColorModel と一致しない場合は色変換が行われます。
と書かれていたのでこちらを使うようにしてみた。


パターン 3BufferedImage#setRGB() を使用

public static BufferedImage grayScale3(BufferedImage image) throws Exception {
  int w = image.getWidth(), h = image.getHeight();
  BufferedImage output = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
  output.setRGB(0, 0, w, h, image.getRGB(0, 0, w, h, null, 0, w), 0, w);
  return output;
} 

パターン1にくらべかなり黒っぽくなった。

パターン3


今回は、パターン1がよさそうだ。

実は、実際嵌った時の画像では、パターン1の場合、元画像の白の部分がグレーがかってしまいパターン3で出力したほうがはっきりしたものになった。
元画像によって変えるのも大変だが・・・

Graphics2D の場合、パラメータを色々指定できるのでそのあたりをいじってみても変わるかもしれない。

2013年8月7日水曜日

Googlebot のクロール時の jsessionid

とあるシステムでアクセスログを見ていたら、Googlebot が jsessionid 付の URL でアクセスしていた・・・

初回アクセス時や、Cookie 未対応の端末の場合、jsessionid を付ける設定になっている。
セキュリティー上よろしくないのだが、ガラケー対応のサイトのため、
アプリケーションサーバ Tomcat の設定で止めることができない。

ログイン必須のページとそうでないページがあり、Googlebot がログインするわけでもないので、
無視することもできるが、SEO 的によろしくない気がする。何より気持ち悪い・・・
というわけで対応することに。

ググってみると、URL Rewrite Filter なるものがあるらしい。
mod_rewrite のフィルターバージョンとのこと。
mod_rewrite にリンク等を書き換える機能があったっけと思いつつみてみると、
<%= response.encodeURL("/world.jsp?country=usa&amp;city=nyc") %>
や
<c:url value="/world.jsp?country=${country}&amp;city=${city}" />

のように指定した URL が正規表現で指定した値に置換されるようだ。

SAStruts や mobylet を使用しているので、c:url はほとんど使っていないが、
フレームワークの内部的には、response.encodeURL を使っているだろうと思われるので問題はなさそう。

ただ、jsessionid を付加しておいて置換で消すというのは冗長な気がする・・・
ソースを読んだわけではないので実装がどうなっているかは分からないが・・・
最初から付加しなければいいはず。

Tomcat の HttpServletResponse.encodeURL は、
HttpServletRequest.isRequestedSessionIdFromCookie を見ていた気がする、ということで、
Bot の UserAgent だった場合、HttpServletRequestWrapper を作成し、
isRequestedSessionIdFromCookie で常に true を返すことに。

public class BotFilter implements Filter {
  private static class BotRequesteWrapper extends HttpServletRequestWrapper {
    public CrawlerRequestWrapper(HttpServletRequest request) {
      super(request);
    }

    @Override
    public boolean isRequestedSessionIdFromCookie() {
      return true;
    }

    @Override
    public boolean isRequestedSessionIdFromURL() {
      return false;
    }
  }


  private static final Pattern PAT_BOT =
    Pattern.compile("Googlebot|Y!J|YahooSeeker|msnbot|bingbot");


  @Override
  public void init(FilterConfig filterConfig) throws ServletException {}

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws IOException, ServletException {
    HttpServletRequest req = (HttpServletRequest)request);
    chain.doFilter(PAT_BOT.matcher(req.getHeader("User-Agent")).find() ? new BotRequestWrapper(req) : request, response);
  }

  @Override
  public void destroy() {}
}

Google, Yahoo, MSN, Bing 等の場合、isRequestedSessionIdFromCookie で常に true、
念のために、isRequestedSessionIdFromURL は 常に false を返す。
そして、フィルターを設定。
<filter>
  <filter-name>botFilter</filter-name>
  <filter-class>com.example.BotFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>botFilter</filter-name>
  <url-pattern>/*</url-pattern>
  <dispatcher>REQUEST</dispatcher>
  <dispatcher>FORWARD</dispatcher>
  <dispatcher>INCLUDE</dispatcher>
  <dispatcher>ERROR</dispatcher>
</filter-mapping> 
UserAgent を Bot のものにして、Cookie を削除後アクセスすると、ものの見事に
jsessionid が付加されている・・・

何故・・・と言う事で追ってみると、どうも ServletResponse 内に ServletRequest を保持しており、
 HttpServletRequestWrapper とは別物のよう・・・

仕方がないので、HttpServletResponseWrapper を作成し、直接 encodeURL を変更することに。
public class CrawlerFilter implements Filter {
  private static class BotResponseWrapper extends HttpServletResponseWrapper {
    public BotResponseWrapper(HttpServletResponse response) {
      super(response);
    }


    @Override
    public String encodeRedirectURL(String url) {
      return url;
    }

    @Override
    public String encodeURL(String url) {
      return url;
    }
  }


  private static final Pattern PAT_BOT =
    Pattern.compile("Googlebot|Y!J|YahooSeeker|msnbot|bingbot");


  @Override
  public void init(FilterConfig filterConfig) throws ServletException {}

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws IOException, ServletException {
    chain.doFilter(request,
      PAT_BOT.matcher(((HttpServletRequest)request).getHeader("User-Agent")).matches() ?
        new BotResponseWrapper((HttpServletResponse)response) : response);
    }

    @Override
    public void destroy() {}
}
encodeUR(), encodeRedirectURL() ともに渡された URL を返すだけにする。

これで、jsessionid が付加されなくなった。

2013年1月17日木曜日

財布経過

財布購入から早一年。
購入した GANZO の財布がどうなったかとういことで。


基本、バッグか上着の内ポケットに入れて持ち歩いているが、角のほうが若干へたってきている。
落としたり、水をこぼしたりしてしまったので、内側外側共に細かい傷がいっぱいついている。内側には若干染みになっている部分も。

エイジング的にはありだが、コードバンの場合最初のきれいな状態を保ちたいという感じもしてしまう・・・

手入れは、GANZO のサイトに、「乳化性タイプ無色 成分はろう・油脂の栄養補給を目的としたクリーム」でアフターケアをと書かれてたので、M.モゥブレィシュークリームという靴用のクリーム購入。

これでたまに磨いている。
外側の折る背の部分が若干カサつく。折れる部分なので仕方ないんだろう。念入りクリームを塗っている。

硬さは 1 年前に比べて大分柔らかくなり、360度開くことも可能になった。
小銭部分はマチがないのであまり多いと取り出し難い。
カードも取り出しやすくなった。伸びてガバガバになっているわけでもなくいい感じ。
 さらに使い込んでいこうと思う。

2012年12月27日木曜日

sbt で設定ファイルを本番用に切り替え

Scala の勉強をかなりさぼっていたわけで・・・
構文も大分忘れている状態なんですが、またちょっとはじめることに。
というか実践。

sbt を使ってちょっとしたバッチを開発時に jar に含める設定ファイル(ログレベル設定等)を、開発、ステージング、本番用に切り替えたくなった。そして嵌った・・・約 2 日・・・

とりあえず設定ファイルの構成は、Java の時にやっていた構成で、
+ src
  + main (開発)
    + scala
    + resources
      + logback.xml
      + ...
  + product (本番)
    + resources
      + logback.xml
  + staging (ステージング)
    + resources
      + logback.xml
の構成で、product/resources, staging/resources には上書きするもののみを置くこととする。

始める sbt のスコープ軸を見ると、タスク毎に設定が出来そう。ということで、最初のアプローチは、pack-staging, pack-product のタスクを生成して、タスクのスコープで unmanagedResourceDirectory を追加しようとした。が、そこから呼び出す copy-resources は copy-resources in Compile になってしまい挫折・・・これに 1 日以上費やしたわけだが・・・結局わからなかった・・・

で、maven の profile 的なことができないかと検索してみると参考になるサイトがあった。
ここを参考に、
case class Profile(name: String)

object Profile {
 val staging = Profile("staging")
 val product = Profile("product")

 implicit def toOption(profile: Profile): Option[Profile] = Option(profile)
}
として、
  unmanagedResourceDirectories in Compile <<= (sourceDirectory, selectedProfile, unmanagedResourceDirectories in Compile)
    ((dir, env, res) => env.map((dir / _.name / "resources")).toSeq ++ res),
  unmanagedResources in Compile~= (_.groupBy((_: java.io.File).getName).map(_._2.head).toSeq),
リソースディレクトリの設定時に振り分け。さらに、リソースファイル設定時に一意になるようグループ化しているようなしていないような・・・いまいち何をやっているかわからないが完成・・・ 

記号がさっぱりわからん・・・Perl とまでは言わんが・・・演算子でなくメソッドなので色々できるわけだが、C++ みたいに標準的な演算子のみオーバーライドできればいいのに・・・ ライブラリ毎に覚えるのは大変だ・・・ 標準的な意味合いの記号があるのだろうか。

 とりあえず sbt の API ドキュメントより、Setting[T] の <<= メソッドを見てみると trait DefinableSetting[S] に定義があり、
final def <<=(app: Initialize[T]): Setting[T]
なるほど、Initialize[T] を受け取る。 unmanagedResourceDirectories は SettingKey[Seq[File]] なので、右辺の
(sourceDirectory, selectedProfile, unmanagedResourceDirectories in Compile)
    ((dir, env, res) => env.map((dir / _.name / "resources")).toSeq ++ res)
は、Initialize[Seq[File]] を返すのであろう。

で、右辺部分だが、
(sourceDirectory, selectedProfile, unmanagedResourceDirectories in Compile)(なんかの処理)
つまり、
(タプル).apply(なんかの処理)
のように見えるが、タプルに対してそんなことできるのか・・・おそらくは暗黙の型変換・・・
というわけで、ソースコードを implisit で grep すると、Structure.scala で
implicit def t3ToApp3[A,B,C](t3: (Initialize[A], Initialize[B], Initialize[C]) ): Apply3[A,B,C] = new Apply3(t3)
というのを発見。さらに、Apply3 をみると、
def apply[T](z: (A,B,C) => T) = Project.app( k3(t3) )( hf3(z) )
となっている。さらに、object Project の API ドキュメントに、Init[Scope] に app の定義があり
def app[HL <: HList, T](inputs: KList[Initialize, HL])(f: (HL) ⇒ T): Initialize[T]
なので、Initialize[Seq[File]] が返りそう。なんか予想が多いが・・・

次に、なんかの処理部分は、Apply3 の apply の引き数なので、
(A,B,C) => T
最初のタプルの設定の値がそれぞれ渡り、T つまり、Seq[File] を返す関数を渡せばよさそう。
unmanagedResourceDirectories が SettingKey[Seq[File]] なので、res は Seq[File] とおもわれる。
そして、生成したパスと res を連結したものが返ると。

最終的に返った、Initialize[Seq[File]] で unmanagedResourceDirectories in Compile を設定しているようだ。
とりあえず、今回はここまで。
追うのだけでも大変・・・自分でカスタマイズ出来る気がしない・・・Scala は嫌いじゃないけど慣れないと、読むのも困難だな・・・

2012年10月19日金曜日

Java による画像縮小 lanczos 編

JDK を使った縮小の結果に納得行かなかったので、GIMP なんかにもある lanczos による縮小を実装してみることに。
元画像は Jpeg なので、RGB の 3 色のみを扱う。

こちらの Ruby バージョンをとりあえず Java にポーティング。
今回縮小のみなので、拡大部分を削除。実行してみると 6 秒前後かかった・・・

もう少し早くならないかなと、さらにコメントのリンク先を読んでみる。

何をやっているかというと、中心を 1 として距離によって重みを付けるということのようだ。
| 0.3 | 0.5 | 0.3 |
|-----+-----+-----|
| 0.5 | 1.0 | 0.5 |
|-----+-----+-----|
| 0.3 | 0.5 | 0.3 |

といった具合に。
重みを計算するのが lanczos 関数。
lanczos2 や lanczos3 の数字は距離で、縮小画像のあるピクセルを計算するために、対応する元画像ピクセルからどれくらいの範囲を参照するかを決めるものっぽい。

となると、縮小率と距離の数字が決まれば、対象のピクセルがどこであろうと、重みは一緒?
というわけで、先に上記のような重みの表を作成しておき、各ピクセル計算時はそれを使用すれば計算量は減るだろう。

中心からのオフセットと重みを保持するクラス

public class Weight {
  public final int offsetX;
  public final int offsetY;
  public final double weight;

  public Weight(int x, int y, double w) {
    offsetX = x;
    offsetY = y;
    weight = w;
  }
}

重みの一覧と合計を保持するクラス

public class WeightFilter {
  public final double weightTotal;
  public final Weight[] weightList;


  public WeightFilter(double sumw, Weight[] weights) {
    weightTotal = sumw;
    weightList = weights;
  }
}

lanczos 関数

public static double lanczos(double x, double n) {
  if (x == 0.0) return 1.0;
  if (Math.abs(x) >= n) return 0.0;
  return Math.sin(Math.PI * x) / Math.PI / x * (Math.sin(Math.PI * x / n) / Math.PI / x * n);
}

縮小率と距離で重み一覧作成

縦・横別でなく斜めも距離を計算してみる
public static WeightFilter getReduceFilter(double scale, int distance) {
  double bdx0 = 0 + 0.5, bdy0 = 0 + 0.5;
  int ss = (int)((bdx0 - distance) / scale), se = (int)((bdx0 + distance) / scale);
  double sumw = 0.0;
  List<Weight> weightList = new ArrayList<>((se - ss) * (se - ss));

  for (int sy = ss; sy <= se; ++sy) {
    for (int sx = ss; sx <= se; ++sx) {
      double xl = (sx + 0.5) * scale - bdx0;
      double yl = (sy + 0.5) * scale - bdy0;
      double w = lanczos(Math.sqrt(xl * xl + yl * yl), distance);
      if (w == 0.0) continue;
      weightList.add(new Weight(sx, sy, w));
      sumw += w;
    }
  }
  return new WeightFilter(sumw, weightList.toArray(new Weight[0]));
}


ユーティリティ関数も作成。

指定座標の RGB 値取得

範囲外は鏡像として取得。
public static int getRGB(int[] image, int x, int y, int w, int h) {
  x = Math.abs(x); y = Math.abs(y);
  if (x >= w) x = w + w - x - 1;
  if (y >= h) y = h + h - y - 1;
  return image[y * w + x];
}

0 - 255 の範囲で色値取得

public static int getColor(double val, double sumw)
{
  if (sumw != 0.0) val /= sumw;
  if (val < 0.0) return 0;
  if (val > 255.0) return 255;
  return (int)val;
}

縮小処理

public static BufferedImage reduce3(BufferedImage image, int n, int dw, int dh)
  int sw = image.getWidth(), sh = image.getHeight();

  double scale = (double)dw / sw;
  BufferedImage thumb = new BufferedImage(dw, dh, image.getType());
  WeightFilter filter = getFilter(scale, n);
  double sumw = filter.weightTotal;
  int[] colors = image.getRGB(0, 0, sw, sh, null, 0, sw);

  for (int dy = 0; dy < dh; ++dy) {
    for (int dx = 0; dx < dw; ++dx) {
      double dr = 0.0, db = 0.0, dg = 0.0;
      int bsx = (int)(dx / scale), bsy = (int)(dy / scale);
      for (Weight weight : filter.weightList) {
        int rgb = getRGB(colors, bsx + weight.offsetX, bsy + weight.offsetY, sw, sh);
        dr += ((rgb >> 16) & 0xFF) * weight.weight;
        dg += ((rgb >> 8) & 0xFF) * weight.weight;
        db += (rgb & 0xFF) * weight.weight;
      }
      thumb.setRGB(dx, dy, (getColor(dr, sumw) << 16) | (getColor(dg, sumw) << 8) | getColor(db, sumw));
    }
  }
  return thumb;
}

呼び出し

lanczos2 と lanczos3 で縮小。
long start3_1 = System.currentTimeMillis();
BufferedImage reduce3_1 = reduce3(base, 2, 120, 90);
System.out.println("reduce3_1: " + (System.currentTimeMillis() - start3_1));
outputImage("reduce3_1.jpg", reduce3_1);

long start3_2 = System.currentTimeMillis();
BufferedImage reduce3_2 = reduce3(base, 3, 120, 90);
System.out.println("reduce3_2: " + (System.currentTimeMillis() - start3_2));
outputImage("reduce3_2.jpg", reduce3_2);

結果

lanczos2: 126.3ms
lanczos3: 233.0ms

元画像と左から、パターン1 3段階、パターン2 3段階、lanczos2、lanczos3




大分早くなった。JDK に比べると遅いが画質はいい。
一度作成してた後は、キャッシュするようにすればいいので、満足。

Java による画像縮小 JDK 編

忙しい日々が続いて、色々嵌っているが書けていない・・・
また同じ事で嵌まることになるだろう・・・

半年以上前だが、Java で画像を縮小してサムネイルを作成する際に、なるべく画質を落としたくなかったので色々試したのでその記録を。
結局ブログ書くために再調査したわけだけが・・・

元となる画像(1024 x 768)は、
これから 120 x 90 のサムネイルを作成することとする。


共通処理として、

画像読み込み

public static BufferedImage loadImage(String filename) {
  try (FileInputStream in = new FileInputStream(filename)) {
    return ImageIO.read(in);
  }
}

画像出力

// 品質 95 Jpeg
public static void outputImage(String filename, BufferedImage image) throws IOException {
  JPEGImageWriteParam param = new JPEGImageWriteParam(Locale.getDefault());
  param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
  param.setCompressionQuality(0.95f);

  ImageWriter writer = ImageIO.getImageWritersByFormatName("jpg").next();
  writer.setOutput(ImageIO.createImageOutputStream(new File(filename)));
  writer.write(null, new IIOImage(image, null, null), param);
  writer.dispose();
}
の 2 つを用意。

縮小処理は、2 パターン。

パターン1 BufferedImage#getScaledInstance() を使用

public static BufferedImage reduce1(BufferedImage image, int dw, int dh) {
  BufferedImage thumb = new BufferedImage(dw, dh, image.getType());
  thumb.getGraphics().drawImage(image.getScaledInstance(dw, dh, Image.SCALE_AREA_AVERAGING), 0, 0, dw, dh, null);
  return thumb;
}

パターン 2 Graphics2D#drawImage() を使用

public static BufferedImage reduce2(BufferedImage image, int dw, int dh) {
  BufferedImage thumb = new BufferedImage(dw, dh, image.getType());
  Graphics2D g2d = thumb.createGraphics();
  g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
  g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
  g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
  g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
  g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
  g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
  g2d.drawImage(image, 0, 0, dw, dh, null);
  return thumb;
}

パターン1、パターン2 をそれぞれ呼び出し

try {
  BufferedImage base = loadImage("base.jpg");
  int dw = 120, dh= 90;

  long start1_1 = System.currentTimeMillis();
  BufferedImage reduce1_1 = reduce1(base, dw, dh);
  System.out.println("reduce1_1: " + (System.currentTimeMillis() - start1_1));
  outputImage("reduce1_1.jpg", reduce1_1);

  long start2_1 = System.currentTimeMillis();
  BufferedImage reduce2_1 = reduce2(base, dw, dh);
  System.out.println("reduce2_1: " + (System.currentTimeMillis() - start2_1));
  outputImage("reduce2_1.jpg", reduce2_1);
} catch (Exception e) { e.printStackTrace(); } 

パターン1はぼやけた感じ、パターン2はざらついた感じ。
連続で 4 回実行し、1 回目を除いた 3 回の平均時間を計測。
速度はパターン2が早いが、今回は作成した画像をキャッシュするので速度より画質。

パターン1:  80.3ms


パターン2: 0.7ms


段階的に縮小

近傍縮小法だと、大きな画像から一気に縮小すると色情報の欠落が大きくなるので、段階的に縮小してみる。
  • 1024x768 -> 640x480 -> 120x90 の 2 段階
  • 1024x768 -> 800x600 -> 480x360 -> 120x90 の 3 段階
の 2 つそれぞれのパターンで実行。
int dw2_1 = 640, dh2_1 = 480;
int dw3_1 = 800, dh3_1 = 600, dw3_2 = 480, dh3_2 = 360;

long start1_2 = System.currentTimeMillis();
outputImage("reduce1_2.jpg", reduce1(reduce1(base, dw2_1, dh2_1), dw, dh));
System.out.println("reduce1_2: " + (System.currentTimeMillis() - start1_2));

long start2_2 = System.currentTimeMillis();
outputImage("reduce2_2.jpg", reduce2(reduce2(base, dw2_1, dh2_1), dw, dh));
System.out.println("reduce2_2: " + (System.currentTimeMillis() - start2_2));

long start1_3 = System.currentTimeMillis();
outputImage("reduce1_3.jpg", reduce1(reduce1(reduce1(base, dw3_1, dh3_1), dw3_2, dh3_2), dw, dh));
System.out.println("reduce1_3: " + (System.currentTimeMillis() - start1_3));

long start2_3 = System.currentTimeMillis();
outputImage("reduce2_3.jpg", reduce2(reduce2(reduce2(base, dw3_1, dh3_1), dw3_2, dh3_2), dw, dh));
System.out.println("reduce2_3: " + (System.currentTimeMillis() - start2_3));
パターン1 2段階: 141.7ms


パターン1 3段階: 215ms


パターン2 2段階: 7.3ms


パターン2 3段階: 16.3ms


多少マシになったが、ちょっと納得いかないレベル・・・
続く

2012年4月22日日曜日

Apache + Tomcat のレスポンス

ここのところ忙しく、全然更新していなかったが、実際には色々嵌りまくりな日々・・・

その中でも最近一番大きかったのが、Apache + Tomcat の構成でレスポンスがおかしい現象が発生。
しばらく使っていると、別のリクエストのレスポンスが返ってくるようになる。
別のユーザのユーザ情報が見えたり・・・セッションがおかしくなったり・・・

Apache または、Tomcat のいずれかを再起動すると普通に戻るがしばらくするとまた発生し始める。
プログラムのせいではないことを祈り調査。

接続は mod_proxy_ajpを使用して mod_proxy_balancer で分散していた。
分散していないテスト環境でも発生していたため、mod_proxy_balancer は調査から除外。
mod_proxy_ajp を mod_proxy_http に変更したところ発生しなくなった。
過去に何度も Apache + Tomcat, mod_proxy_ajp 構成をやったことはあるが初めての現象。

次に、Apache 2.4 を使っていたため、2.2 にダウングレード。
mod_proxy_ajp で接続して問題は発生しなかった。

つまり、Apache 2.4 の mod_proxy_ajp が問題だったと思われる。
Apache 2.4 は今回初めて使用したが、まだ早かったんだろうか・・・

2012年2月12日日曜日

Pacemaker で仮想 IP の切り替え

Pacemaker でまず仮想 IP の切り替えを行う時に嵌まったこと。

OS は CentOS 5.7 でやってみる。インストールは、LINUX-HA JAPAN より、ローカルリポジトリ + yum を使用。

クラスタ制御部には、Corosync を使用することにする。
/etc/corosync/corosync.conf を作成。
# Please read the corosync.conf.5 manual page
compatibility: whitetank

aisexec {
        user: root
        group: root
}

service {
    name: pacemaker
    ver: 0
    use_mgmtd: yes
}

totem {
    version: 2
    secauth: off
    threads: 0
    rrp_mode: active
    clear_node_high_bit: yes
    token: 4000
    consensus: 10000
    rrp_problem_count_timeout: 3000
    interface {
        ringnumber: 0
        bindnetaddr: 192.168.234.0
        mcastaddr: 226.94.1.1
        mcastport: 5405
        ttl: 1
    }
}

logging {
    fileline: on
    to_stderr: no
    to_logfile: yes
    to_syslog: no
    logfile: /var/log/cluster/corosync.log
    debug: off
    timestamp: on
}
bindnetaddr にはインターフェースの IP でなくネットワークアドレスを指定する。
ログをファイルに出力するように指定。

/etc/init.d/corosync start で起動して、crm_mon を実行して確認する。
Attempting connection to the cluster.........................
帰ってこない・・・

一般ユーザで実行したからっぽい。グループに、haclient を追加したら帰ってきた。
============
Last updated: Sun Feb 12 00:54:09 2012
Stack: openais
Current DC: localhost.localdomain - partition WITHOUT quorum
Version: 1.0.11-1554a83db0d3c3e546cfd3aaff6af1184f79ee87
1 Nodes configured, 2 expected votes
0 Resources configured.
============

Online: [ localhost.localdomain ]

次に、 crm configure help でコマンドを確認しようとするも、
cibadmin not available, check your installation
となる、sudo でやってもダメだった。仕方ないので root になって行う。
# crm configure property no-quorum-policy="ignore" stonith-enabled="false"
# crm configure rsc_defaults resource-stickiness="INFINITY" migration-threshold="1"
# crm configure primitive vip ocf:heartbeat:IPaddr2 \
params ip="192.168.234.51" nic="eth0" cidr_netmask="24" op monitor interval="10s"
eth0 に 192.168.234.51 の仮想 IP を作成する設定。

ifconfig で確認するも設定したアドレスが見当たらない。
eth0 にはそもそも実インターフェースの IP が割当たっているからだ。
ip addr show eth0 で確認できる。
inet 192.168.234.51/24 brd 192.168.234.255 scope global secondary eth0

最初、CentOS 6 を使用して普通に yum でインストールして開始したわけだが、こっちのほうが嵌まった。
Pacemaker が 1.1 になっており、クラスタ部に heartbeat が使えなかったり、corosync.conf の service の ver を 1 にしないとだったり、vip 指定時に nic に eth0:1 などを指定すると警告が出たりとかなり面倒だった。

結局、CentOS5.7 を使うことになり、落としたわけだが・・・

2012年1月11日水曜日

SAStruts + Maven

久々、Java での Web アプリケーションを作成をすることに。
最近のフレームワークが分からないため、今更 SAStruts を選択。最近は何がメジャーなんだろうか・・・

とりあえず開発環境を最新版でそろえていく。
  • JDK 1.7.0_02
  • Maven 3.0.3
  • Eclipse 3.7 64bit(Pleiades のプラグイン + Maven プラグイン)
  • Tomcat 7.0.23 -> 6.0.35
Maven で SAStruts のプロジェクトを作成。
# mvn archetype:generate -DarchetypeRepository=https://www.seasar.org/maven/maven2/ \
  -DarchetypeGroupId=org.seasar.sastruts \
  -DarchetypeArtifactId=sa-struts-archetype -DarchetypeVersion=1.0.4-sp9.0 \
  -DgroupId=com.example.sample.sas -DartifactId=sas -Dversion=1.0.0 
Eclipse 用プロジェクト作成。
# cd sas
# mvn eclipse:eclipse -Dwtpversion=1.5
Eclise で「インポート」->「一般」->「既存プロジェクトをワークスペースへ」でインポート。が、ここで問題発生。
プロジェクトを右クリックしても Maven のメニューが出てこない・・・ Maven プラグインは入れたはずなのに。 昔はこれでいけてた気がしたが、バージョンが上がって変わったのだろうか。

色々検索したり、Maven のバージョン変えたりプラグインを再インストールしたりしてみたがうまくいかない。で、かなりして Eclipse のインポートメニューに Maven の項目を発見。
 mvn archietype:generate でプロジェクト作成後、mvn eclipse:eclipse は行わずに、Eclipse の「インポート」->「Maven」->「Existing Maven Project」でインポート。途中、「maven-antrun-plugin:1.3:run (1 errors)」 とエラーが出るがかまわず完了。これで、プロジェクトの右クリックメニューに Maven の項目が出た。

取り込み後、pom.xml に警告とエラーが出ているので潰していく。
まず、<project> タグで警告がでるため、クイック・フィックスで修正。

次に、maven-antrun-plugin の「Plugin execution not covered by lifecycle configuration: org.apache.maven.plugins:maven-antrun-plugin:1.3:run (execution: delete-lib-dir, phase: initialize)」もクイック・フィックスの「Permanently mark goal run in pom.xml as ignored in Eclipse build」で修正する。

最後の、maven-dependency-plugin の「maven-dependency-plugin (goals "copyff-dependencies", "unpack") is not supported by m2e.」は、上記修正で追加された、pluginManagement/plugins/plugin/configuration/lifecycleMappingMetadata/pluginExecutions に設定を追加する。
<pluginExecution>
  <pluginExecutionFilter>
    <groupId>
      org.apache.maven.plugins
    </groupId>
    <artifactId>
      maven-dependency-plugin
    </artifactId>
    <versionRange>[1.3,)</versionRange>
    <goals>
      <goal>copy-dependencies</goal>
    </goals>
  </pluginExecutionFilter>
  <action>
    <ignore></ignore>
  </action>
</pluginExecution>
ついでに、build/plugins/plugin/artifactId が maven-compiler-plugin の source と target を 1.7 に変更。

プロジェクトを右クリックして 「Maven」 -> 「プロジェクト構成の更新」を実行して反映すればエラーと警告が消えた。

次に、WTP の設定。
プロジェクトを右クリックしてプロパティを開き、プロジェクト・ファセットを選択。
Convert to faceted from... をクリック。動的 Web モジュールにチェックを入れて、バージョンを確認すると、3.0 になっている。SAStruts は 3.0 で動くのだろうか・・・調べてみるとどうもダメっぽいので、2.5 に変更。よって Tomcat も 6 に落とす。
プロジェクト・ファセットの設定がなぜだか、1 回目だと失敗する。2 回やると通ったのでそのまま続けることにする・・・
次に、プロパティダイアログの Deployment Assembly を選択。/WebContent の行を削除。追加をクリックして、 「フォルダー」より src/main/webapp を追加。/WebConent のディレクトリは物理的に削除して大丈夫だと思われる。さらに、追加をクリックして、「Java Build Path Entries」より Maven 依存関係を追加。

src/main/webapp/WEB-INF/web.xml のバージョンが 2.4 になっているので 2.5 に変更。
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
  id="WebApp_ID" version="2.5">
dependencies/dependency の geronimo-jsp_2.0_spec を geronimo-jsp_2.1_spec に、バージョンを 1.0 から 1.0.1 に変更。
同じく、geronimo-servlet_2.4_spec を geronimo-servlet_2.5_spec に、バージョンを 1.0 から 1.2 に変更。

プロジェクトを右クリックして、「実行」->「サーバで実行」でサーバを起動すると特にエラーもなく無事起動。
だが、ブラウザで http://localhost:8080/sas/ にアクセスすると、「org.apache.jasper.JasperException: /WEB-INF/view/index.jsp(1,1) /WEB-INF/view/common/common.jsp(1,61) JARファイル "file:/ワークスペースのパス/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/sas/WEB-INF/lib/standard-1.1.2.jar" からTLD "META-INF/c.tld" を読み込めません: org.apache.jasper.JasperException: TagLibraryValidatorクラスのロード又はインスタンス化に失敗しました: org.apache.taglibs.standard.tlv.JstlCoreTLV 」と 500 エラーになる。 

これも結構嵌まったが、どうも pom.xml の provided の jar を読み込んでいる模様。
WTP 実行時に、pom.xml の povided を認識しないらしい。

検索してみると m2e-wtp なるものを発見。インストールしてみる。
「ヘルプ」->「Eclipse マーケットプレース」検索してインストール。
プロジェクトを右クリックして 「Maven」 -> 「プロジェクト構成の更新」を実行後、起動すると無事「Hello!」が表示された。

プラグインをインストールしたことでかなり飛ばせる手順がありそうだが、次回もこの手順でやれば同じ結果になるだろう・・・