View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.tobago.facelets;
21  
22  import org.apache.myfaces.tobago.component.Attributes;
23  import org.apache.myfaces.tobago.component.ClientBehaviors;
24  import org.apache.myfaces.tobago.internal.behavior.EventBehavior;
25  import org.apache.myfaces.tobago.internal.component.AbstractUIEvent;
26  
27  import javax.el.MethodExpression;
28  import javax.faces.component.PartialStateHolder;
29  import javax.faces.component.UIComponent;
30  import javax.faces.component.behavior.ClientBehaviorHolder;
31  import javax.faces.context.FacesContext;
32  import javax.faces.event.AbortProcessingException;
33  import javax.faces.event.AjaxBehaviorEvent;
34  import javax.faces.event.AjaxBehaviorListener;
35  import javax.faces.view.BehaviorHolderAttachedObjectHandler;
36  import javax.faces.view.facelets.ComponentConfig;
37  import javax.faces.view.facelets.ComponentHandler;
38  import javax.faces.view.facelets.FaceletContext;
39  import javax.faces.view.facelets.TagAttribute;
40  import javax.faces.view.facelets.TagAttributeException;
41  import javax.faces.view.facelets.TagException;
42  import java.io.IOException;
43  
44  /**
45   * This tag creates an instance of AjaxBehavior, and associates it with the nearest
46   * parent UIComponent that implements ClientBehaviorHolder interface. This tag can
47   * be used on single or composite components.
48   * <p>
49   * Unless otherwise specified, all attributes accept static values or EL expressions.
50   * </p>
51   * <p>
52   * According to the documentation, the tag handler implementing this tag should meet
53   * the following conditions:
54   * </p>
55   * <ul>
56   * <li>Since this tag attach objects to UIComponent instances, and those instances
57   * implements Behavior interface, this component should implement
58   * BehaviorHolderAttachedObjectHandler interface.</li>
59   * <li>f:ajax does not support binding property. In theory we should do something similar
60   * to f:convertDateTime tag does: extends from ConverterHandler and override setAttributes
61   * method, but in this case BehaviorTagHandlerDelegate has binding property defined, so
62   * if we extend from BehaviorHandler we add binding support to f:ajax.</li>
63   * <li>This tag works as a attached object handler, but note on the api there is no component
64   * to define a target for a behavior. See comment inside apply() method.</li>
65   * </ul>
66   *
67   * @since 3.0.0
68   */
69  public class EventHandler extends TobagoComponentHandler implements BehaviorHolderAttachedObjectHandler {
70  
71    public static final Class<?>[] AJAX_BEHAVIOR_LISTENER_SIG = new Class<?>[]{AjaxBehaviorEvent.class};
72  
73    private final TagAttribute event;
74  
75  // todo (see original AjaxHandler impl)  private final boolean _wrapMode;
76  
77    public EventHandler(final ComponentConfig config) {
78      super(config);
79      event = getAttribute(Attributes.event.getName());
80    }
81  
82    @Override
83    public void apply(final FaceletContext ctx, final UIComponent parent)
84        throws IOException {
85  
86      super.apply(ctx, parent);
87  
88      //Apply only if we are creating a new component
89      if (!ComponentHandler.isNew(parent)) {
90        return;
91      }
92      if (parent instanceof ClientBehaviorHolder) {
93        //Apply this handler directly over the parent
94        applyAttachedObject(ctx.getFacesContext(), parent);
95  //todo      } else if (UIComponent.isCompositeComponent(parent)) {
96  //todo        FaceletCompositionContext mctx = FaceletCompositionContext.getCurrentInstance(ctx);
97        // It is supposed that for composite components, this tag should
98        // add itself as a target, but note that on whole api does not exists
99        // some tag that expose client behaviors as targets for composite
100       // components. In RI, there exists a tag called composite:clientBehavior,
101       // but does not appear on spec or javadoc, maybe because this could be
102       // understand as an implementation detail, after all there exists a key
103       // called AttachedObjectTarget.ATTACHED_OBJECT_TARGETS_KEY that could be
104       // used to create a tag outside jsf implementation to attach targets.
105 //todo        mctx.addAttachedObjectHandler(parent, this);
106     } else {
107       throw new TagException(this.tag,
108           "Parent is not composite component or of type ClientBehaviorHolder, type is: "
109               + parent);
110     }
111   }
112 
113   /**
114    * ViewDeclarationLanguage.retargetAttachedObjects uses it to check
115    * if the the target to be processed is applicable for this handler
116    */
117   @Override
118   public String getEventName() {
119     if (event == null) {
120       return null;
121     } else {
122       return event.getValue();
123     }
124   }
125 
126   /**
127    * This method should create an AjaxBehavior object and attach it to the
128    * parent component.
129    * <p>
130    * Also, it should check if the parent can apply the selected AjaxBehavior
131    * to the selected component through ClientBehaviorHolder.getEventNames() or
132    * ClientBehaviorHolder.getDefaultEventName()
133    */
134   @Override
135   public void applyAttachedObject(final FacesContext context, final UIComponent parent) {
136     // Retrieve the current FaceletContext from FacesContext object
137     final FaceletContext faceletContext = (FaceletContext) context
138         .getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);
139 
140     final ClientBehaviorHolder clientBehaviorHolder = (ClientBehaviorHolder) parent;
141     final UIComponent lastChild = parent.getChildren().stream().skip(parent.getChildCount() - 1)
142         .findFirst().orElse(null);
143     final AbstractUIEvent abstractUIEvent = lastChild instanceof AbstractUIEvent ? (AbstractUIEvent) lastChild : null;
144 
145     if (abstractUIEvent != null) {
146       String eventName = getEventName();
147       if (eventName == null) {
148         eventName = clientBehaviorHolder.getDefaultEventName();
149         if (eventName == null) {
150           throw new TagAttributeException(event, "eventName could not be defined for f:ajax tag with no wrap mode.");
151         }
152       } else if (!clientBehaviorHolder.getEventNames().contains(eventName)) {
153         throw new TagAttributeException(event, "event it is not a valid eventName defined for this component");
154       }
155 
156       final EventBehavior eventBehavior = createBehavior(context);
157       eventBehavior.setFor(abstractUIEvent.getId());
158 
159       clientBehaviorHolder.addClientBehavior(eventName, eventBehavior);
160     }
161   }
162 
163   protected EventBehavior createBehavior(final FacesContext context) {
164     return (EventBehavior) context.getApplication().createBehavior(EventBehavior.BEHAVIOR_ID);
165   }
166 
167   @Override
168   public void onComponentCreated(
169       final FaceletContext faceletContext, final UIComponent component, final UIComponent parent) {
170     super.onComponentCreated(faceletContext, component, parent);
171 
172     final AbstractUIEvent uiEvent = (AbstractUIEvent) component;
173     if (uiEvent.getEvent() == null) {
174       final ClientBehaviorHolder holder = (ClientBehaviorHolder) parent;
175       uiEvent.setEvent(ClientBehaviors.valueOf(holder.getDefaultEventName()));
176     }
177   }
178 
179   /**
180    * The documentation says this attribute should not be used since it is not
181    * taken into account. Instead, getEventName is used on
182    * ViewDeclarationLanguage.retargetAttachedObjects.
183    */
184   @Override
185   public String getFor() {
186     return null;
187   }
188 
189   /**
190    * Wraps a method expression in a AjaxBehaviorListener
191    */
192   public static final class AjaxBehaviorListenerImpl implements
193       AjaxBehaviorListener, PartialStateHolder {
194     private MethodExpression expression;
195     private boolean transientBoolean;
196     private boolean initialStateMarked;
197 
198     public AjaxBehaviorListenerImpl() {
199     }
200 
201     public AjaxBehaviorListenerImpl(final MethodExpression expr) {
202       expression = expr;
203     }
204 
205     @Override
206     public void processAjaxBehavior(final AjaxBehaviorEvent event)
207         throws AbortProcessingException {
208       expression.invoke(FacesContext.getCurrentInstance().getELContext(),
209           new Object[]{event});
210     }
211 
212     @Override
213     public boolean isTransient() {
214       return transientBoolean;
215     }
216 
217     @Override
218     public void restoreState(final FacesContext context, final Object state) {
219       if (state == null) {
220         return;
221       }
222       expression = (MethodExpression) state;
223     }
224 
225     @Override
226     public Object saveState(final FacesContext context) {
227       if (initialStateMarked()) {
228         return null;
229       }
230       return expression;
231     }
232 
233     @Override
234     public void setTransient(final boolean newTransientValue) {
235       transientBoolean = newTransientValue;
236     }
237 
238     @Override
239     public void clearInitialState() {
240       initialStateMarked = false;
241     }
242 
243     @Override
244     public boolean initialStateMarked() {
245       return initialStateMarked;
246     }
247 
248     @Override
249     public void markInitialState() {
250       initialStateMarked = true;
251     }
252   }
253 }