複数のWebサイトからpostgreSQLのテーブルを一部だけ分離する

 postgreSQLを使っているphpのWebアプリを2つのサイトにして、テーブルの一部を共有し、その他は分離したいという要件があったので、Oracle歴が長かった筆者にとっては、別ユーザー作って、シノニム作ればいいじゃんと思いきや、postgreSQLにはシノニムは無い模様。。

 既にpublicとしてテーブルが作成されている状態なので、別ユーザーとそのユーザーのスキーマを作成し、分離したいテーブルのみをそのスキーマに作成、既存のテーブルを別ユーザーに対しアクセス権を付与するという形で何とかなりました。

まず、別ユーザーを作成し、DBに対する権限を付与します
CREATE USER anotheruser WITH PASSWORD ‘******’;
GRANT CONNECT ON DATABASE 既存DB TO anotheruser;
GRANT CREATE ON DATABASE 既存DB TO anotheruser;

共有するテーブル全てにアクセス権を付与します
GRANT ALL ON TABLE common_table1 TO anotheruser;
GRANT ALL ON TABLE common_table2 TO anotheruser;
・・・

作成したユーザーでログインし、ユーザー名と同名のスキーマを作成します
create schema anotheruser AUTHORIZATION anotheruser;
分離するテーブルをこのスキーマ上に作成します
create table anotheruser.devide_table
(id serial, ~

で、片方のWebサイト側DB設定を別ユーザーに変えて、要件通りの動きになりました。
postgreSQLにはサーチパス指定等があるようですが、スキーマ名とユーザー名が同一であれば、勝手に優先してくれるみたいでした。
という事はアクセス権付与はテーブル個別でなくALLでもよかったかも?

VisualStudio2017でビルドしたDLLが動かない

ちょっと前に作られたBeckyPluginDLLの追加開発をしていましたが、それまでVisualStudio2012で開発されていたものを、今回 VisualStudioCommunity2017に変えたところ、Pluginが動作する端末と動かない端末が出てきて困ったことに。。
動かない端末環境との相違点がパッと解らないので、下記ツールでDLL依存を確認。
http://www.dependencywalker.com/

元のDLLがこれで

VS2017でビルドしたのがこれ

確かにだいぶ変わってしまったようです。

 しょうがないので過去のVisualStudioをインストールしたところ、VS2017のデバッグ→プロジェクトのプロパティで「プラットフォームツールセット」を変える事が出来るようになり、それでビルドしたところ、依存しているDLLが変わり動くようになりました。

 結果的にVS2017にした影響で、再配布可能パッケージが必要になってしまっていたようです。
再配布可能パッケージを各端末に導入してもらうのも厄介な話なので、この形でビルドして配布する事にしました。
 久しぶりにVisualStudioを使いましたが、こういう環境依存箇所は昔から相変わらず厄介なままですが、それ以外は使い込めば結構使えそうな感じですね。

ZendFramework1でREST APIを試してみた

既存の業務アプリ内にある機能を外部から利用出来たらなあ・・・のような要件があり、RestAPIで実装を検討しました。対象のアプリは、今となっては・・・ですが、ZendFramework1が使われています。

こちらにいろいろ書いていますが、Zend_Rest_Controllerを使えばいいっぽい。

開発環境はEclipse + php + XAMPP
XAMPP内のphp/pearの中に、ダウンロードしたZendFramework1.12.20のZIPからlibrary内のZendを配置、XAMPP内のapache/conf/extra/httpd-xammp.confに該当プロジェクトのAliasをセットして環境ができました。

    Alias /testapp "C:/xampp/htdocs/TestAPP/html/"
    
        AllowOverride All
         Require all granted
    

テスト用のEclipceプロジェクトはこんな感じで最少構成。

.htaccess でindex.phpに仕向けます。

RewriteEngine on
RewriteBase /testapp/
RewriteRule !\.(js|ico|gif|jpg|png|css)$ index.php

index.phpでdispatchします。

<?php
require_once 'Zend/Controller/Front.php';

$controller = Zend_Controller_Front::getInstance();
$controller->setControllerDirectory('../application/controllers');
$controller->setParam('noViewRenderer', true);

try {
	$controller->dispatch();
} catch (Exception $e) {
}

ControllerにZend_Rest_Controllerとして必要なメソッドを追加していきます。

<?php
require_once 'Zend/Rest/Controller.php';

class TestController extends Zend_Rest_Controller  {

    public function indexAction() {
		$this->getResponse()->appendBody("hello world " . $_REQUEST[param1]);
    }

    public function headAction(){
//TODO
    }
    public function getAction() {
//TODO
    }
    public function postAction() {
//TODO
    }
    public function putAction() {
//TODO
    }
    public function deleteAction() {
//TODO
    }
}

で、ブラウザから動作確認

URLをいろいろ意識すると難しそうですが、これなら簡単ですね。

Windows10でHTAがエラーになる

 HTA&VBSの構成で社内利用向けに作ったツール(社内端末はwin7)について、今後のWindows10導入へ向けて稼働確認をしたところ、エラーメッセージが出て動作しませんでした。

で、ネットでいろいろ調べたところ、
https://msdn.microsoft.com/ja-jp/library/cc817574.aspx
等を見ながら、

<meta http-equiv="x-ua-compatible" content="IE=9"/>

を入れてみたところ、エラーは無くなったのですが、画面レイアウトが崩れる結果に・・・
 社内ではこれからの新しい端末はWin10に、既存は当面Win7という方針なので、ツールとしてはどちらも同じように動作して欲しいところです。

 で、上記metaタグを外して結局何がエラーなの?と調べていくと、WMIのExecQueryを使った箇所がWin10ではエラーとなる事が判明。どうもセキュリティがらみの変更っぽいですが、詳細はよくわかりませんでした。。

 今回は別命令で置き換えが可能だったので、それに置き換えて問題無く動作するようになりましたが、画面だけは、フォントサイズの影響があったので微調整しておきました。
 そろそろHTAを止めたいのですが、Windowsマシン前提で簡易な画面ツールを作るとなると、まだ代替え手段が見つかりません。。

Apache CommonsのDateUtilsに機能を拡張する

 よくあるケースと思いますが、メジャーなライブラリにもうちょっと機能が欲しい時、そのライブラリを拡張して共通ライブラリとして使いまわしたりしますよね?
筆者がJavaを使う場合は、だいぶ前からApache CommonsのDateUtilsを拡張し、String・Dateの相互変換メソッドとかを追加していろんな開発案件で使いまわしてましたが、今回、文字列から日時型に変換するけど、どんな文字列パターンになるかが不明確、、という要件があったので、拡張していたクラスに機能を追加し、一般的に日時として使われる文字列からのDate変換機能を追加してみました。
※2021.04 長文からの日時文字列取得で問題があったので修正しています。

package jp.esoro.common.utils;

import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.time.DateFormatUtils;

/**
 * 日付処理クラス
 * */
public class DateUtils extends org.apache.commons.lang3.time.DateUtils {

	/**
	 * 日付表示パターン(YYYY/MM/DD hh:mm)
	 */
	public static final String FORMAT_YYYY_MM_DD_HH_MM = "yyyy/MM/dd HH:mm";

	public static final String FORMAT_YYYY_MM_DD = "yyyy/MM/dd";
	public static final String FORMAT_YYYYMMDD = "yyyyMMdd";
	public static final String FORMAT_YYMMDD = "yyMMdd";
	public static final String FORMAT_JPN_YYYY_MM_DD = "yyyy年MM月dd日";

	public static final String FORMAT_YYYY_MM_DD_HH_MM_SS = "yyyy/MM/dd HH:mm:ss";

	public static final String FORMAT_MYSQL_DATE = "yyyy-MM-dd";
	public static final String FORMAT_MYSQL_DATETIME = "yyyy-MM-dd HH:mm:ss";
	public static final String FORMAT_YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
	public static final String FORMAT_YYYYMMDDHHMMSSmi = "yyyyMMddHHmmssSSS";
	public static final String FORMAT_YYMMDDHHMMSS = "yyMMddHHmmss";
	public static final String FORMAT_YYMMDDHHMMSSmi = "yyMMddHHmmssSSS";

	/**
	 * 日付(Calendar)を文字列に編集する
	 * @param date
	 * @param pattern
	 * @return
	 */
	public static String format(Calendar date, String pattern) {
		if (date == null) {
			return null;
		}
		return DateFormatUtils.format(date, pattern);
	}

	/**
	 * 日付(Date)を文字列に編集する
	 * @param date
	 * @param pattern
	 * @return
	 */
	public static String format(Date date, String pattern) {
		if (date == null) {
			return null;
		}
		Calendar cal = Calendar.getInstance();
		cal.setTime(date);
		return format(cal, pattern);
	}

	/**
	 * システム日付を取得する
	 * @return Date
	 */
	public static Date getDateTime() {
		return Calendar.getInstance().getTime();
	}

	/**
	 * 文字列をDate型へ変換する
	 * @param value(yyyy_mm_dd)
	 * @return
	 */
	public static Date stringToDate(String value) {
	    return stringToDate(value , FORMAT_YYYY_MM_DD);
	}

	/**
	 * 文字列をDate型へ変換する
	 * @param value
	 * @param pattern 
	 * @return
	 */
	public static Date stringToDate(String value ,String pattern) {
	    SimpleDateFormat sdf = new SimpleDateFormat(pattern);
	    if (value == null) {
	    	return null;
	    }
	    try {
	        return sdf.parse(value);
	    } catch (ParseException e) {
	        return null;
	    }
	}
//ここまで以前から、以降を追加
	/**
	 * 日時文字列をDate変換する
	 * @param str Date変換したい文字列
	 * @return Date 取得出来ない場合はnull
	 * @see あらかじめ文字列パターンが判明している場合は、stringToDateを使用してください)
	 */
	public static Date extractDate(String str) {
		final String[] regexs = {"([0-9]{2,4})(/|-|¥¥.|年)([01]?[0-9])(/|-|¥¥.|月)([0123]?[0-9])(日|日¥¥s+|¥¥s|_)([0-9]{1,2})(:|-|¥¥.|時)([0-9]{1,2})(:|-|¥¥.|分)([0-9]{1,2})",
				"([0-9]{2,4})(/|-|¥¥.|年)([01]?[0-9])(/|-|¥¥.|月)([0123]?[0-9])(日|日¥¥s+|¥¥s|_)([0-9]{1,2})(:|-|¥¥.|時)([0-9]{1,2})",
				"([0-9]{2,4})(/|-|¥¥.|年)([01]?[0-9])(/|-|¥¥.|月)([0123]?[0-9])(日|日¥¥s+|¥¥s|_)([0-9]{1,2})(:|-|¥¥.|時)",
				"([0-9]{2,4})(/|-|¥¥.|年)([01]?[0-9])(/|-|¥¥.|月)([0123]?[0-9])",
				};
		Date ret = null;
		String normStr = Normalizer.normalize(str, Form.NFKC);
		try{
			String chkStr = StringUtils.removePattern(normStr, "am|AM|Am|pm|PM|Pm|午前|午後");
			
			for(String regex : regexs){
				Pattern pattern = Pattern.compile(regex, Pattern.DOTALL|Pattern.CASE_INSENSITIVE);
				Matcher matcher = pattern.matcher(chkStr);
				
				if(matcher.find()){
					if(matcher.groupCount() >= 5){
						boolean isPm = false;
						if(normStr.indexOf(matcher.group()) < 0 && matcher.groupCount() >= 7){
							if( normStr.matches(".*"+matcher.group(1)+matcher.group(2)+matcher.group(3)+matcher.group(4)+matcher.group(5)+matcher.group(6)+"(pm|PM|pm|午後).*")){
								isPm = true;
							}
						}
						
						if(matcher.group(1).length() == 2){
							ret = DateUtils.setYears(getDateTime(), Integer.valueOf("20" + matcher.group(1)));
						}
						else{
							ret = DateUtils.setYears(getDateTime(), Integer.valueOf(matcher.group(1)));
						}
						ret = DateUtils.setDays(ret, 1);
						//Month is 0 -11
						ret = DateUtils.setMonths(ret, Integer.valueOf(matcher.group(3))-1);
						ret = DateUtils.setDays(ret, Integer.valueOf(matcher.group(5)));
						
						if(matcher.groupCount() >= 7){
							int hour = Integer.valueOf(matcher.group(7));
							ret = DateUtils.setHours(ret, hour + (hour<12 && isPm?12:0));
						}
						else{
							ret = DateUtils.setHours(ret, 0);
						}
						if(matcher.groupCount() >= 9){
							ret = DateUtils.setMinutes(ret, Integer.valueOf(matcher.group(9)));
						}
						else{
							ret = DateUtils.setMinutes(ret, 0);
						}
						if(matcher.groupCount() == 11){
							ret = DateUtils.setSeconds(ret, Integer.valueOf(matcher.group(11)));
						}
						else{
							ret = DateUtils.setSeconds(ret, 0);
						}
					}
					return ret;
				}
			}
			if(ret == null){
				SimpleDateFormat sdf1 = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy",Locale.ENGLISH);
		        ret = sdf1.parse(chkStr);
			}
		}catch(Exception e){
	        return null;
		}
		return ret;
	}
}

で、テストです。

package test;

import jp.esoro.common.utils.DateUtils;

public class Datetest {
	static String[] dateStrs = {
			"20170131",
			"2017-9-2 15:00:00",
			"2017/9/2 15:25:00",
			"2017年9月2日8時34分51秒",
			"2017年1月2日8時32分",
			"2017年12月31日午後11時集合",
			"2017年12月2日8時",
			"2017.12.2 PM8:00",
			"2017.12.3 AM8:25",
			"2017.01.31",
			"2017.1.32"	};

	public static void main(String[] args) {
		for(String str : dateStrs){
			System.out.println(str + " is " + DateUtils.format(DateUtils.extractDate(str),DateUtils.FORMAT_YYYY_MM_DD_HH_MM_SS));
		}
	}
}

テスト結果
20170131 is null
2017-9-2 15:00:00 is 2017/09/02 15:00:00
2017/9/2 15:25:00 is 2017/09/02 15:25:00
2017年9月2日8時34分51秒 is 2017/09/02 08:34:51
2017年1月2日8時32分 is 2017/01/02 08:32:00
2017年12月31日午後11時集合 is 2017/12/31 23:00:00
2017年12月2日8時 is 2017/12/02 08:00:00
2017.12.2 PM8:00 is 2017/12/02 20:00:00
2017.12.3 AM8:25 is 2017/12/03 08:25:00
2017.01.31 is 2017/01/31 00:00:00
2017.1.32 is null

YYYYMMDDってのは、ここでは対象外です。だって、ただの数字の羅列は文字として日付とは言えないし。

2年たってバグってた事が判明。1日に戻すのを追加しました。。

カテゴリー: Java

redmineでたまにInternalErrorが出るようになった。

 筆者が通っているユーザーさんでは、redmineに独自開発含めプラグインを10個くらい入れて使っているのですが、何時からかInternalErrorが出るようになり、その調査を依頼されました。
 状況的にはチケットの一覧や詳細を表示する時、たまにInternalErrorとなる事があり、ただ再表示すれば普通に表示されるという何とも解りにくい状況。。
 redmineのログには下記InternalError発生時の内容が出力されています。

Completed 500 Internal Server Error in 6169.9ms

ActionView::Template::Error (undefined method `deep_symbolize_keys' for #<String:0x007f34da039088>):
    50: <p id="start_date_area">
    51:   <%= f.text_field(:start_date, :size => 10, :disabled => !@issue.leaf?,
    52:                    :required => @issue.required_attribute?('start_date')) %>
    53:   <%= calendar_for('issue_start_date') if @issue.leaf? %>
    54: </p>
    55: <% end %>
    56: 
  lib/redmine/i18n.rb:131:in `store_translations'
  lib/redmine/i18n.rb:152:in `init_translations'
  lib/redmine/i18n.rb:166:in `lookup'
  lib/redmine/i18n.rb:30:in `l'
  app/helpers/application_helper.rb:1180:in `block in include_calendar_headers_tags'
  app/helpers/application_helper.rb:1167:in `include_calendar_headers_tags'
  app/helpers/application_helper.rb:1159:in `calendar_for'
以下省略

i18nが関係しているようですが、たまにしか起きないし、それ以外に問題箇所を特定できそうな出力は読み取れません。
いろいろ試行錯誤してみますが原因は不明。。で、国際化対応が絡んでいるという事で、ログインユーザーの言語設定を英語に変えてみた所、相変わらずInternalErrorがたまに出るものの、ログの出力内容が変わりました。

Completed 500 Internal Server Error in 2011.0ms

NoMethodError (undefined method `deep_symbolize_keys' for #<String:0x007f8f44c7d8c0>):
  lib/redmine/i18n.rb:131:in `store_translations'
  lib/redmine/i18n.rb:152:in `init_translations'
  lib/redmine/i18n.rb:166:in `lookup'
  lib/redmine/i18n.rb:27:in `l'
  app/models/issue_query.rb:172:in `initialize_available_filters'
以下省略

で、エラーが出始めた時期とかを考慮すると、独自プラグインを入れた後なので、そのソースを眺めていると、en.ymlに不備を発見。直して入れてみると問題が解消したようです。

weblogic管理画面のパスワードを忘れた。。

 2年前くらいからOEPEを入れて開発に使っているwindows端末で、webアプリをweblogicで動かしてテストしたいなと思い、weblogicの管理画面からwarファイルをデプロイしようとした所、管理画面のログインパスワードを完全に忘れていました。。
デフォルトユーザーはweblogicだったのは覚えてましたが、心当たりがあるパスワードを入れても全くダメ。で、どうやってリセット出来るか?をググっても何も解らず、困っていた所、下記を発見。
https://docs.oracle.com/cd/E28613_01/web.1211/b65928/weblogicserver.htm#i1013039
開発端末なので既存のドメインにログイン出来なくなっても、ドメインを新規で作ればとりあえず動かせると言う事で。。本番環境では致命的ですけど。

ManagedBeanが非推奨になっていた

JavaEEでJSF2とJAX-RSを使った簡単な業務Webアプリを作っていたんですが、それまで作っていた開発環境のままWebLogicでローカルテストしてある程度動作するようになった後に、さすがにこの規模のAPにWebLogicを用意するのは無理!という事になったので、前にちょっと別件で使ってみたglashfish実装のpayaramicroで動くように変更してみました。
 まず、普通にweblogicで動かしていたwarをpayaraにデプロイすると下記エラーで失敗します・・

[[FATAL] No injection source found for a parameter of type public javax.ws.rs.core.Response

 正直あまり意識してなかったのですが、、やっぱりweblogicになると内包しているライブラリに依存しやすく、ちゃんと内部を理解していないと何が作用して動作しているかが解りにくいです。。
 エラーを見る限り、jax-rs関連でエラーになっている模様。
いろいろライブラリを置き換えていくと、既にManagedBeanが非推奨になっているようだ。。
結果pom.xmlは下記になり

<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.faces/javax.faces-api -->
<dependency>
    <groupId>javax.faces</groupId>
    <artifactId>javax.faces-api</artifactId>
    <version>2.3</version>
    <scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.enterprise/cdi-api -->
<dependency>
    <groupId>javax.enterprise</groupId>
    <artifactId>cdi-api</artifactId>
    <version>2.0</version>
    <scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.containers/jersey-container-servlet -->
<dependency>
    <groupId>org.glassfish.jersey.containers</groupId>
    <artifactId>jersey-container-servlet</artifactId>
    <version>2.25.1</version>
</dependency>

@ManagedBeanを@Namedに置き換え、SessionScopedをjavax.enterprise.contextに変えたりして、ようやくpayaraで動くようになりました。

 で、とりあえず動かすことはできたとは言え、今やWebアプリを積極的にJavaEEで実装する理由が少なくなっている気がします。Wabアプリならphpとかruby on Railsでの大規模サイトの構築事例がたくさんあります。速度的なアドバンテージも今となってはキャッシュにより殆ど感じられないですし、その実装対象のシステムに何等かの制約やレガシーな理由が絡んでないと敢えてJavaEEを選択する理由は何だろう?JavaEEがいまいち普及しないのは結局の所、各実装はベンダー次第となっている事が結果的にアプリケーションサーバの実装次第となってしまい、Javaとして最大の魅力である(と筆者は思っている)OSを超えた一貫性というものが、各APサーバが内包しているライブラリがバラバラである事により、消え失せてしまっているような気がします。

JavaMailで送信日時が取得できないケースがある

 メールを受信する業務用ツールをJavaMailで実装して、かれこれ一年近く普通に使っていたのですが、新しい要件に沿った機能を追加してテストしていたところ、メールの送信日時を取得
javax.mail.Message.getSentDate()
でnullを想定しておらずエラーに。。
これまで何も問題がなかったのですが、どうもメールによっては取れないケースがある模様。
メールをemlファイルにして中身をエディタで見たところ、

問題無いケース Date: Fri, 23 Jun 2017 11:23:10 +0900
問題有るケース Date: Tue Jun 6 12:31:19 2017

 で、試しにemlファイルのDateヘッダを問題無いケースに書き換えてやってみたところ、普通にJavaMailで送信日時が取れた、、という事は?

 細かくは調べてませんが、問題となるメールはDate書式がMIMEの仕様に沿っていないようです。
仕様に沿っていないのか、仕様が明確でないのか、もしかして、仕様が明確になる前からあるSMTPサーバなのか・・?

 OutLook等の一般的なメーラーで見る限り、普通に送信日時として表示されます。
こういう場合、メールを受信する側からすれば、細かい箇所が仕様に沿っていなくても、問題無く処理できるものを用意しなくてはなりません。
とはいえ、なかなか事前に想定するのは難しく、メールみたいに昔からあるものは特に注意しなければいけませんね。。

カテゴリー: Java

curlでパラメータを指定してWebページ上のファイルを取得してみる

 とあるWebサイトからファイルをダウンロードする作業を自動化したいという話があり、Linux上で動作するものという要件でしたので
とりあえずcurlでコマンドを作ってみました。対象サイトの認証はログイン画面からIDとパスワードを入力するという一般的な内容ですが、画面以外の対象サイトにおける認証の仕様については、何も情報が無くどうやらWebアプリ独自仕様のようです。
 ログイン画面でどんなやり取りがされているかと、FireFoxの開発ツールで確認し、POSTするパラメータを指定するコマンドを作成していきます。クッキーも指定してやってみますが、まだダメ。。

 今度はhttpヘッダの情報も開発ツールで確認しながら、必要そうなヘッダもcrulコマンドに指定すると、うまく動作しました。下記のような感じです。

ログイン画面

curl -X POST -L -b my.cookie -c my.cookie --header "Content-Type: application/x-www-form-urlencoded" -d "log=****&pwd=****" "https://targethost/login"

ダウンロードするファイルのGET

curl -L -b my.cookie -c my.cookie -o getout --header "Referer:https://targethost/aaaaa" https://targethost/files/targetfile.zip

 出来る出来ないはWebサイト側の作り次第かもしれませんが、とりあえずcurlでアクセスする事が出来そうです。
 とは言え、対象サイトの認証パスワードは定期更新が必要だし、他にもいろいろ対処が必要なので別の手段も考えた方がいいかもしれません。。。