Много раз мне было трудно поддерживать шаблоны xhtml, поэтому в недавнем проекте я решил попробовать создать сложные деревья компонентов JSF из кода Java. Эта статья даст вам краткий обзор методов, преимуществ и недостатков этого подхода.
Xhtml и EL не являются Java-кодом (ну, спасибо, Капитан Очевидность), и поэтому им требуется дополнительная поддержка IDE для правильного рефакторинга, поиска, навигации, автозаполнения и безопасности типов. IntelliJ IDEA дает вам очень хороший вариант, и есть JBoss Tools для Eclipse (хотя я пока что считаю его очень медленным и нестабильным). Тем не менее, я считаю, что использование чистого Java-кода лучше во многих случаях.
Другой момент — это декларативные шаблоны JSF2 и Facelets. Мне просто это не нравится. После определенной сложности Java-код становится проще для чтения, навигации и рефакторинга. Когда вы используете конструкцию процедурного пользовательского интерфейса, состав, агрегация и другие вещи выражаются как обычные конструкции языка Java, и вы имеете над ними процедурный контроль.
Создание компонентов
Итак, давайте попробуем. Я не пробовал создавать целые представления JSF из кода Java, фреймы все еще находятся в формате xhtml. Когда html-контента больше, чем компонентов, проще писать xhtml, но когда есть больше компонентов, я обращаюсь к коду Java. Я просто вставляю компонент «заполнитель» в мой шаблон xhtml:
<h:panelGroup binding="#{myBacking.panel1}"/>
class MyBacking {
private HtmlPanelGroup panel1;
@PostConstruct
public void init() {
panel1 = new HtmlPanelGroup();
panel1.setId("panel1");
HtmlOutputLabel label = new HtmlOutputLabel();
label.setValue("hello world");
panel1.getChildren().add(label);
}
public void getPanel1() {
return panel1;
}
public void setPanel1(HtmlPanelGroup panel1) {
// do nothing
}
}
Привязка значений
Привязки значений могут быть созданы из строк EL в Java:
/**
* @param el EL string WITH the #{} marks
*/
public static ValueExpression createFromEL(String el) {
FacesContext context = FacesContext.getCurrentInstance();
ELContext elContext = context.getELContext();
return FacesContext.getCurrentInstance().getApplication().getExpressionFactory()
.createValueExpression(elContext, el, Object.class);
}
Но в общем случае это не тот путь, потому что строки EL в коде Java не лучше. Класс ValueExpression можно довольно легко расширить, чтобы обеспечить некоторые основные функциональные возможности.
Привязка к статическим или локальным значениям
import javax.el.ELContext;
import javax.el.ValueExpression;
public abstract class DirectValueExpression<Type> extends ValueExpression {
private Class<Type> type;
public static <T> DirectValueExpression<T> of(final T value) {
return new DirectValueExpression<T>((Class)value.getClass()) {
@Override
public T getValue() {
return value;
}
};
}
public DirectValueExpression(Class<Type> type) {
this.type = type;
}
public abstract Type getValue();
public void setValue(Type value) {
// do nothing
}
@Override
public Object getValue(ELContext context) {
return getValue();
}
@Override
public void setValue(ELContext context, Object value) {
setValue((Type) value);
}
@Override
public boolean isReadOnly(ELContext context) {
return true;
}
@Override
public Class<?> getType(ELContext context) {
return type;
}
@Override
public Class<?> getExpectedType() {
return type;
}
@Override
public String getExpressionString() {
return null;
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public int hashCode() {
return 0;
}
@Override
public boolean isLiteralText() {
return false;
}
}
Вы можете использовать статические из метода для создания привязок констант:
behavior.setValueExpression("process", DirectValueExpression.of("panel"));
И вы можете привязать к полю в вашем классе:
new DirectValueExpression<String>(String.class) {
public String getValue() {
return name;
}
@Override
public void setValue(String value) {
name = value;
}
};
Привязка к свойствам объектов контекста
Если вы хотите связать со свойствами поддержки, которые недоступны из метода, в котором вы находитесь, для этого тоже есть удобный способ.
public abstract class BasicValueExpression<Root, Type> extends ValueExpression {
private Class<Type> valueClass;
private String rootName;
private Class<Root> rootClass;
private ELContext currentContext;
public BasicValueExpression(Class<Root> backingClass, Class<Type> valueClass) {
this.rootName = backingClass.getSimpleName().substring(0, 1).toLowerCase()
+ backingClass.getSimpleName().substring(1);
this.rootClass = backingClass;
this.valueClass = valueClass;
}
public BasicValueExpression(String rootName, Class<Root> rootClass, Class<Type> valueClass) {
this.rootName = rootName;
this.rootClass = rootClass;
this.valueClass = valueClass;
}
public BasicValueExpression(UIData uiData, Class<Root> rootClass, Class<Type> valueClass) {
this(uiData.getVar(), rootClass, valueClass);
}
public BasicValueExpression(UIRepeat uiRepeat, Class<Root> rootClass, Class<Type> valueClass) {
this(uiRepeat.getVar(), rootClass, valueClass);
}
protected abstract Type getFromRoot(Root root);
@Override
public Object getValue(ELContext context) {
currentContext = context;
Root root = getRoot(context);
return getFromRoot(root);
}
public <T> T resolve(String name, Class<T> cls) {
ValueExpression rootExpression = FacesContext.getCurrentInstance().getApplication().getExpressionFactory()
.createValueExpression(currentContext, "#{" + name + "}", cls);
return (T) rootExpression.getValue(currentContext);
}
private Root getRoot(ELContext context) {
ValueExpression rootExpression = FacesContext.getCurrentInstance().getApplication().getExpressionFactory()
.createValueExpression(context, "#{" + rootName + "}", rootClass);
Root root = (Root) rootExpression.getValue(context);
return root;
}
protected void setOnRoot(Root root, Type value) {
// do nothing silently, if not overriden
}
@Override
public void setValue(ELContext context, Object value) {
Root root = getRoot(context);
setOnRoot(root, (Type) value);
}
@Override
public boolean isReadOnly(ELContext context) {
return true;
}
@Override
public Class<?> getType(ELContext context) {
return valueClass;
}
@Override
public Class<?> getExpectedType() {
return valueClass;
}
@Override
public String getExpressionString() {
return null;
}
@Override
public boolean equals(Object obj) {
return false;
}
@Override
public int hashCode() {
return 0;
}
@Override
public boolean isLiteralText() {
return false;
}
public ELContext getCurrentContext() {
return currentContext;
}
}
С этим классом привязка начинается с определенного корневого выражения типа Root и, наконец, преобразуется в целевое выражение типа Type. Вы также должны определить имя корневого объекта в корневом контексте. Для поддержки мы используем соглашение об именах, заключающееся в том, что первая буква имени класса поддержки должна быть строчной. Внутри повторяющихся компонентов имя корневого объекта обычно находится в свойстве «var» повторяемого компонента. Например:
ValueExpression titleBinding = new BasicValueExpression<MyBacking, String>(MyBacking.class,
String.class) {
@Override
protected String getFromRoot(MyBacking root) {
return root.getDialogTitle();
}
};
Другие способы
Well, this is a way more code than writing «#{myBacking.dialogTitle}» — that’s for sure. But this is type-safe, you can bravely refactor your code and discover property usage with your plain Java IDE.
We have another method to more easily, still type-safely bind values in JSF, but that will be for another article some weeks later.
EL also supports automagically processing Bean Validation annotations. If we write our own value expressions, we lose this functionality. We can quite easily add bean validation to our bindings too, but I leave that topic also to the forementioned future article.
Action Listeners
In EL, you would create method bindings. In Java code, you can just implement the ActionListener interface as you would do in Swing. I haven’t yet met a case when actually creating an EL method binding from Java code was necessary.
So adding behaviour to buttons is simple:
button.addActionListener(new ActionListener() {
@Override
public void processAction(ActionEvent event) throws AbortProcessingException {
System.out.println("button clicked");
}
});
Special xhtml Elements
Some xhtml elements don’t have JSF component equivalents, they are special instructions. When the xhtml engine finished constructing and composing the view source, it runs tag handlers to create the component tree in Java objects. The tag f:attribute for example is a special tag that is handled by com.sun.faces.facelets.tag.jsf.core.AttributeHandler.
Static Text
Facelets converts static html content to a special component named UIInstructions, but those classes are private for facelets. But we’re not lost, a very simple component will do the same for us:
public class TextLiteral extends UIComponentBase {
private String text;
public TextLiteral(String text) {
this.text = text;
}
@Override
public String getFamily() {
return "textLiteral";
}
@Override
public void encodeEnd(FacesContext context) throws IOException {
context.getResponseWriter().append(text);
}
}
Adding ajax Behaviour
The tags f:ajax and p:ajax are also special tags. Their handlers add AjaxBehaviours to their parent components. To add PrimeFaces AjaxBehavior (that is similar to the standard JSF AjaxBehavior) to a component, just do the following:
AjaxBehavior behavior = (AjaxBehavior) FacesContext.getCurrentInstance().getApplication().createBehavior(AjaxBehavior.BEHAVIOR_ID);
behavior.setValueExpression("update", DirectValueExpression.of("@form"));
behavior.setValueExpression("process", DirectValueExpression.of("@form"));
calendar.addClientBehavior("dateSelect", behavior);
Adding standard JSF ajax behaviour is similar.
Summary
When we started using these techniques, the code first looked quite bloated indeed. But I think that with a few tricks, you can make writing JSF in Java quite convenient, and it turned out in a larger project at Doctusoft Ltd that after a specific complexity, having most of the UI stuff in Java really makes templating, code reuse and maintenance a lot easier.