Pages

Monday, December 21, 2009

JSF PhaseListeners With Annotations

Although JSF 2 moved most of it's configuration to annotations still some must be made on xml's, like the phase listeners.
The most straight forward way to implement a mechanism that would enable defining them with annotations are treating phase listeners as singletons. Than we could simply annotate static methods with after phase, before phase annotations.
A second and more elegant/useful way would be to treat phases as cdi events and observe these events.
Annotating static methods
I have created @BeforePhase and @ActionPhase annotations that take phase id as parameter and mark static methods with that :
1:  public class ActionContext extends AbstractThreadLocalMapContext {
2:
3: private static ActionContext instance = new ActionContext();
4: ...
5: @BeforePhase(CycleId.RESTORE_VIEW)
6: public static void activate() {
7: instance.setActive(true);
8: instance.setBeanStore(new ConcurrentHashMapBeanStore());
9: }
10:
11: @AfterPhase(CycleId.RENDER_RESPONSE)
12: public static void deActivate() {
13: instance.destroy();
14: instance.setActive(false);
15: instance.setBeanStore(null);
16: }
17:
18: @AfterPhase(CycleId.INVOKE_APPLICATION)
19: public static void reActivate() {
20: instance.deActivate();
21: instance.activate();
22: }
23:
24: }
Now we need a phase listener that would scan for these annotations and act as a proxy for these methods. I used the Scannotation to scan for the annotations and proxy phase listener still must be defined in faces-config.xml:
1:  public class ProxyPhaseListener
2: implements PhaseListener {
3:
4: private Map<Integer, Set<Target>> afterregistry;
5: private Map<Integer, Set<Target>> beforeregistry;
6:
7: public ProxyPhaseListener() {
8: try {
9: URL ucp = new URL(ClasspathUrlFinder.findClassBase(getClass()).toString().replaceAll("%20", " "));
10: AnnotationDB db = new AnnotationDB();
11: db.setScanClassAnnotations(false);
12: db.setScanFieldAnnotations(false);
13: db.setScanParameterAnnotations(false);
14: db.scanArchives(ucp);
15: afterregistry = new HashMap<Integer, Set<Target>>();
16: beforeregistry = new HashMap<Integer, Set<Target>>();
17: Map<String, Set<String>> map = db.getAnnotationIndex();
18: registerMethods(map, afterregistry, new AfterPhaseRegisterer());
19: registerMethods(map, beforeregistry, new BeforePhaseRegisterer());
20: } catch (Exception ex) {
21: throw new RuntimeException(ex);
22: }
23: }
24:
25: public void afterPhase(PhaseEvent event) {
26: invoke(event, afterregistry);
27: }
28:
29: private void invoke(PhaseEvent event,Map<Integer, Set<Target>> registry) {
30: Set<Target> targets = registry.get(event.getPhaseId().getOrdinal());
31: if (targets != null) {
32: for (Target target : targets) {
33: target.invoke();
34: }
35: }
36: }
37:
38: public void beforePhase(PhaseEvent event) {
39: invoke(event, beforeregistry);
40: }
41:
42: public PhaseId getPhaseId() {
43: return PhaseId.ANY_PHASE;
44: }
45:
46: private void registerMethods(Map<String, Set<String>> annotationIndex,
47: Map<Integer, Set<Target>> registry,
48: PhaseRegisterer phaseRegisterer) throws ClassNotFoundException, SecurityException {
49: Set<String> classes = annotationIndex.get(phaseRegisterer.getPhaseMetaData().getName());
50: if(classes != null) {
51: for (String c : classes) {
52: Class clazz = Class.forName(c);
53: Method[] ms = clazz.getDeclaredMethods();
54: for (Method m : ms) {
55: for (Annotation an : m.getDeclaredAnnotations()) {
56: Class antype = an.annotationType();
57: if (antype.equals(phaseRegisterer.getPhaseMetaData())) {
58: phaseRegisterer.register(an, registry, m, clazz);
59: }
60: }
61: }
62: }
63: }
64:
65: }
66:
67: protected class AfterPhaseRegisterer implements PhaseRegisterer {
68: public void register(Annotation an, Map<Integer, Set<Target>> registry, Method m, Class clazz) {
69: AfterPhase ap = (AfterPhase) an;
70: Integer key = ap.value().ordinal();
71: Set<Target> value = registry.get(key);
72: if (value == null) {
73: value = new HashSet<Target>();
74: }
75: value.add(new Target(m, clazz));
76: registry.put(key, value);
77: }
78:
79: public Class<? extends Annotation> getPhaseMetaData() {
80: return AfterPhase.class;
81: }
82: }
83:
84: protected class BeforePhaseRegisterer implements PhaseRegisterer {
85: ...
86: }
87:
88: interface PhaseRegisterer {
89: void register(Annotation an, Map<Integer, Set<Target>> registry, Method m, Class clazz);
90: public Class<? extends Annotation> getPhaseMetaData();
91: }
92:
93: protected class Target {
94: private Method method;
95: private Class clazz;
96:
97: public Target(Method method, Class clazz) {
98: this.method = method;
99: this.clazz = clazz;
100: }
101:
102: private void invoke() {
103: try {
104: method.invoke(null);
105: } catch (Exception ex) {
106: throw new RuntimeException(ex);
107: }
108: }
109: ...
110: }
111:
112: }
113:
I believe this may improved, so that it could use singleton beans instead of static methods. Also more work has to be done to pass the phase event as a parameter.
An easier way to implement this with non-static methods would be using the CDIs event producer/observer api.
Producer/Observer
Here is my client bean looks like :
1:  @ApplicationScoped
2: @Named
3: public class TestBean implements Serializable {
4:
5: public void afterRestoreView(@Observes
6: @PhaseEventDefinition(value=CycleId.RESTORE_VIEW, when=AfterBeforeEnum.AFTER)
7: PhaseEventHolder holder) {
8: System.out.println("after restore view : " + holder.getEvent().getPhaseId());
9: }
10:
11: }
@Observers annotation states that this bean will observe events and the @PhaseEventDefinition tells which phase we will listen to. PhaseEventHolder object holds the PhaseEvent parameters that's missing on the previous example. Again we will need a phase listener that would listen to all phases and forward them as events, wrapping a jsf PhaseEvent with our PhaseEventHolder.
1:  public class ProxyPhaseListener
2: implements PhaseListener {
3:
4: private PhaseProducer phaseProducer;
5:
6: public void afterPhase(PhaseEvent event) {
7: fireEvent(event,AfterBeforeEnum.AFTER);
8: }
9:
10:
11: public void beforePhase(PhaseEvent event) {
12: fireEvent(event,AfterBeforeEnum.BEFORE);
13: }
14:
15: private void fireEvent(PhaseEvent event, AfterBeforeEnum when) {
16: final int ordinal = event.getPhaseId().getOrdinal();
17: final CycleId[] cycles = CycleId.values();
18: PhaseEventHolder phaseEventHolder = new PhaseEventHolder(event);
19: AnnotationLiteral<PhaseEventDefinition> annotationLiteral =
20: new PhaseLiteral(cycles[ordinal], when);
21: getPhaseProducer().fireEvent(phaseEventHolder,annotationLiteral);
22: }
23:
24: private PhaseProducer getPhaseProducer() {
25: if(phaseProducer == null) {
26: FacesContext context = FacesContext.getCurrentInstance();
27: phaseProducer = (PhaseProducer) context.getApplication()
28: .getELResolver().getValue(context.getELContext(),null,"phaseProducer");
29: }
30: return phaseProducer;
31: }
32:
33: public PhaseId getPhaseId() {
34: return PhaseId.ANY_PHASE;
35: }
36: }
37:
PhaseProducer is the bean that fires the phase holders as events:
1:  @ApplicationScoped
2: @Named
3: public class PhaseProducer implements Serializable {
4:
5: @Inject @Any
6: private Event<PhaseEventHolder> phaseEvent;
7:
8:
9: public void fireEvent(PhaseEventHolder phaseEventHolder, AnnotationLiteral<PhaseEventDefinition> annotationLiteral) {
10: phaseEvent.select(annotationLiteral).fire(phaseEventHolder);
11: }
12: }
This approach is much simpler, usefull and clearer than the first one. Only thing I am not happy with it is that I had use EL to access the phaseProducer object.
I keep the source here so, chek it out. It should run in a glassfish-v3 environment.

No comments:

Post a Comment

Post a Comment