[Spring][Maven]実行可能jarを生成するときの落とし穴 その2

fatjarを生成すると当然jarの署名も変わります。なのでchecksumを検証するような行儀の良いjarを含んでいると実行時にSecurityExceptionが吹っ飛んできます。

回避策は以下の通り。署名のマニフェストをjarから除外するのです。

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>jp.co.tsr_net.omega.pdf.Main</mainClass>
</transformer>

<!-- Springの各jarにこれらのファイルが存在するため、マージする。 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlers</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemas</resource>
</transformer>

<!-- jarを作りなおすため、署名は無効にする -->
<transformer implementation="org.apache.maven.plugins.shade.resource.DontIncludeResourceTransformer">
<resource>.RSA</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.DontIncludeResourceTransformer">
<resource>.DSA</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.DontIncludeResourceTransformer">
<resource>*.SF</resource>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>

[Spring][Maven] 実行可能jarを生成するときの落とし穴

どうやらSpringは各jarにspring.handlersやspring.schemasがあるため、maven-assembly-pluginでfatjarを生成しようとすると、ファイルがコンフリクトしてしまいます。

なのでmaven-shade-pluginを使うのが正解。

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>net.nagaseyasuhito.sandbox.Main</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlers</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemas</resource>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>

RelativeDateLabel

時刻を「36秒前」とか「15分前」とか「8時間前」のように相対的な経過時間で表示するLabel。24時間以上前は絶対時刻で表示。
自分はだいたいIConverterでやってしまうのだけど、Modelでも出来るよね。どう使い分けるのがいいのかね。

package net.nagaseyasuhito.sandbox;

import java.util.Date;
import java.util.Locale;

import org.apache.wicket.Application;
import org.apache.wicket.Localizer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.util.convert.IConverter;
import org.apache.wicket.util.convert.converters.AbstractConverter;

public class RelativeDateLabel extends Label {
	private static final long serialVersionUID = 1L;

	public RelativeDateLabel(String id) {
		super(id);
	}

	public RelativeDateLabel(String id, IModel<?> model) {
		super(id, model);
	}

	public RelativeDateLabel(String id, String label) {
		super(id, label);
	}

	@Override
	public IConverter getConverter(Class<?> type) {
		return new AbstractConverter() {
			private static final long serialVersionUID = 1L;

			@Override
			public Object convertToObject(String value, Locale locale) {
				return null;
			}

			@Override
			public String convertToString(Object value, Locale locale) {
				long duration = (System.currentTimeMillis() - ((Date) value).getTime()) / 1000;

				Localizer localizer = RelativeDateLabel.this.getLocalizer();

				if (duration < 60) {
					return localizer.getString("second", RelativeDateLabel.this, Model.of(duration));
				}

				duration /= 60;
				if (duration < 60) {
					return localizer.getString("minute", RelativeDateLabel.this, Model.of(duration));
				}

				duration /= 24;
				if (duration < 24) {
					return localizer.getString("hour", RelativeDateLabel.this, Model.of(duration));
				}

				return Application.get().getConverterLocator().getConverter(value.getClass()).convertToString(value, locale);
			}

			@Override
			protected Class<?> getTargetType() {
				return Date.class;
			}
		};
	}
}

RelativeDateLabel.propertiesに

second = ${}秒前
minute = ${}分前
hour = ${}時間前

って記述もお忘れなく。

cronで任意の件数をバックアップ

例えばデータベースのバックアップを7日間ごとにやるなら曜日を利用して、

/usr/bin/pg_dump database > database-`/bin/date +\%A`.dump

と書けばよかったけれど、任意の日付間隔の場合どうしようかなあと考えた結果がこれ。

/usr/bin/pg_dump database > database-`expr \`date +%-s\` / 60 / 60 / 24 % [バックアップを取りたい件数]`.dump

1970/01/01からの秒数を日数に直した値の剰余をとればdatabase-0.dumpから連番でローテーションして生成されます。

例えば最新10日分なら

/usr/bin/pg_dump database > database-`expr \`date +%-s\` / 60 / 60 / 24 % 10`.dump

こんな感じにするとdatabase-0.dumpからdatabase-9.dumpが生成されます。どれが最新かはファイル名からは判別しづらいけど、まぁそこは生成された日付をみてください、ということで。

Galileoのミラーアップデートサイト

Galileoのアップデートサイト重すぎ!という訳でミラーサイトに変更。
[Window]->[Preferences]->[Install/Update]->[Available Software Sites]
LocationのURLの
http://download.eclipse.org/

http://ftp.yz.yamagata-u.ac.jp/pub/eclipse/
に置換するだけ。サクサクアップデート動きます。

Place Holder(Input Prompt)を表示するIBehavior

TextFieldやTextArea用のPlace Holder(入力がない場合はグレーの文字でヒントが出るやつ)のIBehaviorを書いてみました。 書いた、といってもSimpleAttributeModifierと大して変わらないですが。

jQueryjQuery.placeholderをこのIBehaviorと同じpackageにおいて使ってください。

使い方は至って単純。 Place Holderを適応したいコンポーネントにPlaceHolderBehaviorをaddしてください。

this.add(new TextField<String>("mailAddress").add(new PlaceHolderBehavior(new Model<String>("メールアドレス"))));

こんな感じです。

package net.nagaseyasuhito.sandbox.behavior;

import org.apache.wicket.Component;
import org.apache.wicket.ResourceReference;
import org.apache.wicket.behavior.AbstractBehavior;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.html.IHeaderResponse;
import org.apache.wicket.model.IModel;

public class PlaceHolderBehavior extends AbstractBehavior {
	private static final ResourceReference JQUERY_JS = new ResourceReference(PlaceHolderBehavior.class, "jquery-1.3.2.min.js");
	private static final ResourceReference PLACEHOLDER_JS = new ResourceReference(PlaceHolderBehavior.class, "jquery.placeholder.js");

	private static final long serialVersionUID = 1L;

	private IModel<String> value;

	public PlaceHolderBehavior(IModel<String> value) {
		this.value = value;
	}

	@Override
	public void onComponentTag(final Component component, final ComponentTag tag) {
		if (this.isEnabled(component)) {
			tag.getAttributes().put("placeholder", this.value.getObject());
		}
	}

	@Override
	public void renderHead(IHeaderResponse response) {
		super.renderHead(response);

		response.renderJavascriptReference(PlaceHolderBehavior.JQUERY_JS);
		response.renderJavascriptReference(PlaceHolderBehavior.PLACEHOLDER_JS);
	}
}

enhanceされたclassにも対応するIConverterLocator

HibernateなどORMでentityがenhanceされているとWicketのConverterLocatorはObject.toString()の値を返してしまいます。

Application.newConverterLocator()は、内部にclass名とIConverterのMapを持ち、IConverterLocator.getConverter(Class type)は単純にMap.get(String key)の値を返します。 そのためenhanceされたclassだとDefaultConverterが使われ、Object.toString()の値が返ってくるのです。

サクっとoverrideしてやりたいところですがConverterLocator.getConverter(Class type)がfinalなため、仕方なくコピペ。
IConverterLocator.getConverter(Class type)内でMapに合致するIConverterが無かった場合superclassを辿って探す実装にしています。

package net.nagaseyasuhito.sandbox;

import java.lang.ref.WeakReference;
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import org.apache.wicket.IConverterLocator;
import org.apache.wicket.util.convert.ConversionException;
import org.apache.wicket.util.convert.IConverter;
import org.apache.wicket.util.convert.converters.BigDecimalConverter;
import org.apache.wicket.util.convert.converters.BooleanConverter;
import org.apache.wicket.util.convert.converters.ByteConverter;
import org.apache.wicket.util.convert.converters.CharacterConverter;
import org.apache.wicket.util.convert.converters.DateConverter;
import org.apache.wicket.util.convert.converters.DoubleConverter;
import org.apache.wicket.util.convert.converters.FloatConverter;
import org.apache.wicket.util.convert.converters.IntegerConverter;
import org.apache.wicket.util.convert.converters.LongConverter;
import org.apache.wicket.util.convert.converters.ShortConverter;
import org.apache.wicket.util.convert.converters.SqlDateConverter;
import org.apache.wicket.util.convert.converters.SqlTimeConverter;
import org.apache.wicket.util.convert.converters.SqlTimestampConverter;
import org.apache.wicket.util.lang.Objects;

public class ConverterLocator implements IConverterLocator {
	private class DefaultConverter implements IConverter {
		private static final long serialVersionUID = 1L;

		private final WeakReference<Class<?>> type;

		public DefaultConverter(Class<?> type) {
			this.type = new WeakReference<Class<?>>(type);
		}

		public Object convertToObject(String value, Locale locale) {
			if (value == null) {
				return null;
			}
			if ("".equals(value)) {
				if (((Class<?>) this.type.get()) == String.class) {
					return "";
				}
				return null;
			}

			try {
				return Objects.convertValue(value, this.type.get());
			} catch (Exception e) {
				throw new ConversionException(e.getMessage(), e).setSourceValue(value);
			}
		}

		public String convertToString(Object value, Locale locale) {
			if (value == null || "".equals(value)) {
				return "";
			}

			return (String) Objects.convertValue(value, String.class);
		}
	}

	private static final long serialVersionUID = 1L;

	private final Map<String, IConverter> classToConverter = new HashMap<String, IConverter>();

	public ConverterLocator() {
		this.set(Boolean.TYPE, BooleanConverter.INSTANCE);
		this.set(Boolean.class, BooleanConverter.INSTANCE);
		this.set(Byte.TYPE, ByteConverter.INSTANCE);
		this.set(Byte.class, ByteConverter.INSTANCE);
		this.set(Character.TYPE, CharacterConverter.INSTANCE);
		this.set(Character.class, CharacterConverter.INSTANCE);
		this.set(Double.TYPE, DoubleConverter.INSTANCE);
		this.set(Double.class, DoubleConverter.INSTANCE);
		this.set(Float.TYPE, FloatConverter.INSTANCE);
		this.set(Float.class, FloatConverter.INSTANCE);
		this.set(Integer.TYPE, IntegerConverter.INSTANCE);
		this.set(Integer.class, IntegerConverter.INSTANCE);
		this.set(Long.TYPE, LongConverter.INSTANCE);
		this.set(Long.class, LongConverter.INSTANCE);
		this.set(Short.TYPE, ShortConverter.INSTANCE);
		this.set(Short.class, ShortConverter.INSTANCE);
		this.set(Date.class, new DateConverter());
		this.set(java.sql.Date.class, new SqlDateConverter());
		this.set(java.sql.Time.class, new SqlTimeConverter());
		this.set(java.sql.Timestamp.class, new SqlTimestampConverter());
		this.set(BigDecimal.class, new BigDecimalConverter());
	}

	public final IConverter get(Class<?> c) {
		while (c != null) {
			IConverter converter = this.classToConverter.get(c.getName());

			if (converter != null) {
				return converter;
			}

			c = c.getSuperclass();
		}

		return null;
	}

	@SuppressWarnings("unchecked")
	public final IConverter getConverter(Class type) {
		if (type == null) {
			return new DefaultConverter(String.class);
		}

		IConverter converter = this.get(type);
		while (null == (converter = this.get(type))) {
			if (type.getSuperclass() != null) {
				type = type.getSuperclass();
			} else {
				return new DefaultConverter(type);
			}
		}
		return converter;
	}

	public final IConverter remove(Class<?> c) {
		return this.classToConverter.remove(c.getName());
	}

	public final IConverter set(final Class<?> c, final IConverter converter) {
		if (converter == null) {
			throw new IllegalArgumentException("CoverterLocator cannot be null");
		}
		if (c == null) {
			throw new IllegalArgumentException("Class cannot be null");
		}
		return this.classToConverter.put(c.getName(), converter);
	}
}

Application.newConverterLocator()をoverrideし、

ConverterLocator converterLocator = new ConverterLocator();
converterLocator.set(User.class, new UserConverter());
return converterLocator;

のように使ってください。