Converterのところで変なバグが出てて、1時間半くらいハマってたので、その記録。
先に結論を書くと、これ。
getAsStringのvalueがnullだった場合、nullを返さないといけないっぽい
— もちだでしたが、衰退しました (@mike_neck) 2015, 3月 20
まず、選択肢用にenum
を用意します。
package sample; import javax.faces.model.SelectItem; import java.util.NoSuchElementException; import java.util.stream.Stream; public enum Event { Track100m(1, "100m", true), Track400m(2, "400m", true), Track1500m(4, "1500m", true), Track5000m(5, "5000m", true), Hurdle400m(6, "400mH", true), HighJump(7, "HJ", false), LongJump(8, "LJ", false), ShotPut(9, "SP", false), DiscusThrow(10, "DT", false); private final int id; private final String displayName; private final boolean track; Event(int id, String display, boolean track) { this.id = id; this.displayName = display; this.track = track; } public int getId() { return id; } public String getName() { return displayName; } public boolean isTrack() { return track; } public boolean isField() { return !track; } private SelectItem toSelectItem() { return new SelectItem(this, displayName); } public static SelectItem[] tracks() { return Stream.of(values()) .filter(Event::isTrack) .map(Event::toSelectItem) .toArray(SelectItem[]::new); } public static SelectItem[] fields() { return Stream.of(values()) .filter(Event::isField) .map(Event::toSelectItem) .toArray(SelectItem[]::new); } public static Event of(int id) { for (Event e : values()) { if (e.id == id) return e; } throw new NoSuchElementException(); } }
もう一個、選択肢を準備します。
package sample; import javax.faces.model.SelectItem; import java.util.NoSuchElementException; public enum Sex { 男子(0), 女子(1); private final int id; Sex(int id) { this.id = id; } public int getId() { return id; } public static SelectItem[] asSelectItems() { Sex[] sexes = values(); SelectItem[] items = new SelectItem[sexes.length]; for (int i = 0; i < sexes.length; i++) { items[i] = new SelectItem(sexes[i], sexes[i].toString()); } return items; } public static Sex of(int id) { if (id < 0 || id > 1) throw new NoSuchElementException(); for (Sex sex : values()) { if(sex.id == id) return sex; } throw new NoSuchElementException(); } }
これらを選択させる画面を作ります。
<h:outputText value="性別"/> <h:selectOneRadio label="性別" id="sex" required="true" value="#{entry.sex}"> <f:selectItems value="#{entry.sexes}"/> <f:converter converterId="eventConverter"/> </h:selectOneRadio> <h:outputText value="種目"/> <h:selectManyCheckbox label="種目" id="events" required="true" value="#{entry.events}"> <f:selectItems value="#{entry.trackEvents}"/> <f:selectItems value="#{entry.fieldsEvents}"/> <f:converter converterId="eventConverter"/> </h:selectManyCheckbox>
あとコンバーターも準備します。
package sample.converter; import sample.Event; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.convert.Converter; import javax.faces.convert.ConverterException; import javax.faces.convert.FacesConverter; import java.util.Objects; import java.util.regex.Pattern; @FacesConverter(value = "eventConverter", forClass = Event.class) public class EventConverter implements Converter { @Override public Object getAsObject(FacesContext context, UIComponent component, String value) { Objects.requireNonNull(context); Objects.requireNonNull(component); String v = Objects.requireNonNull(value); if (!Pattern.compile("^\\d+$").matcher(v.trim()).matches()) { throw new ConverterException(v + " is not integer"); } return Event.of(Integer.parseInt(v)); } @Override public String getAsString(FacesContext context, UIComponent component, Object value) { Objects.requireNonNull(context); Objects.requireNonNull(component); Objects.requireNonNull(value); if (!(value instanceof Event)) throw new IllegalArgumentException(value + " is not Event. [class : " + value.getClass().getName() + "]"); Event e = (Event) value; return String.format("%d", e.getId()); } }
まず最初に、Converter
に注目します。
Converter<T>
ではなく、Converter
なので、型がわかりません。しかたなくgetAsString
にはEvent
ではなくObject
の形で渡されます。また、逆方向のgetAsObject
でも? extends Object
が返されればよいので、forClass = Event.class
としているけど、String
でも、List<String>
でもはたまたint
でも返せます。
型がわからないということは何が起こるかというと、実行時に例外が発生します。Javaは型安全ではなかったんや!
次にnull
が渡される問題。
@Named("entry")
でアノテートされたオブジェクトのインスタンス化された後は、その保持しているEvent
のインスタンスがnull
です。したがって、JSFのEL式value="#{entry.events}"
はnull
なので、Converter#getAsString
にはnull
が渡されます。
さて、不勉強な僕はnull
が渡されるなんてこれっぽっちも思わず(どれっぽっち?)、陽気にObjects.requireNonNull(value)
なんてやっています。ドキュメントには
Model object to be converted (may be
null
)
って書いてありますけどね。発生するのはNullPointerException
です。画面にアクセスしてから落ちます。ドキュメントにはこうも書いてあります。
a zero-length String if value is
null
, otherwise the result of the conversion
いや、それならワンクッション入れてくれ。そのように不勉強な僕は思うわけです。特定のデータに対して特定の戻り値を要求するなら、そのような実装になるようにプログラムを制限してくれ、頼むから。つまりnull
は氏ね。
最後にConverter
の指定が型安全ではない問題。
IntelliJ IDEA様もxmlの属性値についてコード補完してくれますけど、残念ながらConverter
は何に対するConverter
か型情報を持たないので、f:converter
に対して型推論を元にした補完ができません。したがって、不勉強な僕は型Sex
に対してEvent
のConverter
を適用しているわけです。当然ながら実行時例外(ClassCastException
など)が発生します。せっかくJava5で導入されたジェネリクスがもったいないです。
以上、闇の現場からの報告でした