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

法人住民税の中間納付を忘れてました。。

 そろそろ法人2期目の申告をする時期なんですが、いろいろ書類を整理していた所、
「法人県民税・事業税・地方特別税の予告申告について」
という黄色い書類が出てきて、あれ?中間納付って3月にしたよなあ?と銀行の通帳とかを調べたら、やっぱりどこにも該当が無い。。
 国税の中間申告だけ対処していて、県民地方税の納付がされていない事が判明。。
 今月中に決算申告しなきゃいけないので、ヤバいと思い、管轄の県税事務所に電話した所、とりあえず、そのまま申告して下さいとの事。
 この辺の納付って一元化して欲しいなあ。って、国税管轄の地方法人税もあり、県民地方税とか解りにくいし。。

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

映画「メッセージ」を見て思う事

 昨日、近所の映画館で「メッセージ」を見てきました。何となく柿の種のような(ばかうけの方が正しかったみたいですが・・)宇宙船がちょっと気になった程度で見て見てみようと思っただけなのですが、なかなか、色々な示唆に富んだ映画でした。
 基本的には、未知の宇宙人とのコミュニケーションを取る過程が殆どなんですが、時間軸的に行ったり来たりするので、結構解りにくく、しかしそれがそもそもこの映画の主題なんだろうと思うのですが、それ以上に相手から聞くという事の重要性について考えました。
 システムの業界って、こんな事を実現したいというクライアントの言っている事を、開発者がよく理解出来ない時に、大抵、よくない事が現実化します。それをクライアントが避けたい場合には、高い金を払ってコンサルタントを雇い、その間に入れる事もあります。同じ言葉を使う相手でさえもこんな事は日常茶飯事なのですから、映画のように全く相手に言葉(=WORD)が通じないとしたら、その苦労は大変なものです。
 お互いが相手を理解する為には、少なくともどちらか一方が一旦は聞き役になる必要があります。伝える側は相手を理解すると言う事を目的にはしていないので、聞き役しか相手を理解する事は出来ません。聞き役は「あなたが伝えたい事はこのことですね?」と理解している事を相手に伝え、信用を得た上で次のステップに進む事が出来ます。映画のラストでは主人公が重要人物に伝えたい事を伝える為に、相手を理解していると言う事も同時に伝える事で信用を得ていますが、これは、その重要人物からそれを聞いた事に起因しています。未来に、ですが。
 結局のところ、人と人(または人以外の何か)の関係は、信用しているか?疑っているか?無関心か?の3つの状態を互いに9通りを時間軸で行ったり来たりしているだけと言えば、だけなのかもしれませんが、最低でもどちらか一方が聞くスタンスを持ち、多少なりとも聞く為の技術を持っていれば、信用しているか?という要素がいつのまに何処にも無くなっていたという状況は避けられると思います。
 もうひとつ、この映画に出てくる象徴的な言葉として「ノンゼロサムゲーム」がありました。宇宙人がなぜ地球に来たのかはネタバレなので避けますが、それは数千年後に起きる出来事に関してなので、直接的な利害関係は全く無いようなものです。利害関係が無いのに、なぜ相手を信用出来ないのか?逆に言うと相手を不信に思う時は、ただ単純に相手が嫌いなだけ(信用していない側の問題)か、何らかの「ゼロサムゲーム」的な、勝ち負け、損得の要素が相手との関係の中に生じたと言う事なんでしょう。

消費税の届け出に関する書類が来た

 先日、税務署から「消費税の届出のお尋ね」という書類が来ていました。
あれ?確か、起業から2年は免税じゃなかったっけ?と思いながらも、書類を読んでも「課税期間」という言葉がピンとこないので、税務署の方に電話してみました。なお、筆者の法人はあと2か月で2期目終了となります。
 税務署の電話口では、企業名を言うとすぐに過去の申告内容が検索できるようになってるらしく、前期の売上が1千万超えているから提出が必要ですとの事。ただ、2年間免税なのはその通りで問題無く、課税期間=3期目が始まる今のタイミングで「消費税課税事業者届出書」を提出する必要があるようです。

 もうちょっと調べていくと、消費税には、「原則課税」と中小事業者向けの特例「簡易課税」の2つの計算方法あり、「簡易課税」を選択する場合は、こちらも事前に「消費税簡易課税制度選択届出書」という書類を提出しておく必要があるとの事。
 となると、どちらの計算方法を選択すべきかという事になるのですが、簡易課税を選択すると業種によって「みなし仕入れ率」が決まっており、筆者のような業種だとサービス業に入るので50%で、売上額*消費税率*みなし仕入れ率で、おおよそ消費税率に対し半分が消費税納付額になる模様。一方の「原則課税」は、仕入れ関係の帳簿や請求書等を確実に保存する必要がある、と届いた書類に記載されているので、その辺りの管理が厄介そう。

 計算や帳簿管理が簡単だし、仕入が少ない業種は簡易課税の方がお得になりそうなので、こちらの届出も一緒に提出することにします。

特別徴収税額通知書がきた

 先日、市役所の方から「特別徴収税額通知書」なる書類が送られて来てました。
 
特別って、何??と思い中を開けると、どうやら源泉徴収の住民税版のようなものらしいのですが、いろいろ調べてみると、従業員が自分で住んでいる自治体に支払う「普通徴収」と、会社(事業所)の方が、従業員が住んでいる自治体に支払う「特別徴収」の2通りがあるようで、知らないうちに特別徴収義務者になっていたらしい。。

 封筒の中には、「特別徴収税額の決定通知書」と、6月分から来年5月分までの納付書が毎月分入っており、6月分は7月10日期限となっています。源泉徴収のように半年に一回支払いすれば済むものでは無いようなのですが、納付書があるのでまとめて先払いしちゃえばそれほど手間がかかるものでは無さそうです。とは言え、給料を払う前に税を払うという事は、経理上、源泉徴収とは逆の対処が必要そうなので、どうやるのかは別途調べないといけませんね。。

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でアクセスする事が出来そうです。
 とは言え、対象サイトの認証パスワードは定期更新が必要だし、他にもいろいろ対処が必要なので別の手段も考えた方がいいかもしれません。。。