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.renderkit;
21  
22  import org.apache.myfaces.tobago.component.ClientBehaviors;
23  import org.apache.myfaces.tobago.context.TobagoContext;
24  import org.apache.myfaces.tobago.internal.behavior.EventBehavior;
25  import org.apache.myfaces.tobago.internal.component.AbstractUICommand;
26  import org.apache.myfaces.tobago.internal.component.AbstractUIEvent;
27  import org.apache.myfaces.tobago.internal.component.AbstractUIReload;
28  import org.apache.myfaces.tobago.internal.renderkit.Collapse;
29  import org.apache.myfaces.tobago.internal.renderkit.Command;
30  import org.apache.myfaces.tobago.internal.renderkit.CommandMap;
31  import org.apache.myfaces.tobago.internal.util.HtmlRendererUtils;
32  import org.apache.myfaces.tobago.internal.util.RenderUtils;
33  import org.apache.myfaces.tobago.internal.webapp.TobagoResponseWriterWrapper;
34  import org.apache.myfaces.tobago.renderkit.html.CustomAttributes;
35  import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
36  import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
37  import org.apache.myfaces.tobago.util.ComponentUtils;
38  import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  import javax.faces.component.EditableValueHolder;
43  import javax.faces.component.UIComponent;
44  import javax.faces.component.ValueHolder;
45  import javax.faces.component.behavior.AjaxBehavior;
46  import javax.faces.component.behavior.ClientBehavior;
47  import javax.faces.component.behavior.ClientBehaviorBase;
48  import javax.faces.component.behavior.ClientBehaviorContext;
49  import javax.faces.component.behavior.ClientBehaviorHolder;
50  import javax.faces.context.FacesContext;
51  import javax.faces.context.ResponseWriter;
52  import javax.faces.convert.Converter;
53  import javax.faces.convert.ConverterException;
54  import javax.faces.render.ClientBehaviorRenderer;
55  import javax.faces.render.Renderer;
56  import java.io.IOException;
57  import java.lang.invoke.MethodHandles;
58  import java.util.List;
59  import java.util.Map;
60  
61  public abstract class RendererBase<T extends UIComponent> extends Renderer {
62  
63    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
64  
65    private static final String FOCUS_KEY = HtmlRendererUtils.class.getName() + ".FocusId";
66  
67    // begin of "redefine" the method signatur (generics): UIComponent -> T
68  
69    @Override
70    public final void encodeBegin(FacesContext context, UIComponent component) throws IOException {
71      this.encodeBeginInternal(context, (T) component);
72    }
73  
74    public void encodeBeginInternal(FacesContext context, T component) throws IOException {
75      super.encodeBegin(context, component);
76    }
77  
78    @Override
79    public final void encodeChildren(FacesContext context, UIComponent component) throws IOException {
80      this.encodeChildrenInternal(context, (T) component);
81    }
82  
83    public void encodeChildrenInternal(FacesContext context, T component) throws IOException {
84      super.encodeChildren(context, component);
85    }
86  
87    @Override
88    public final void encodeEnd(FacesContext context, UIComponent component) throws IOException {
89      this.encodeEndInternal(context, (T) component);
90    }
91  
92    public void encodeEndInternal(FacesContext context, T component) throws IOException {
93      super.encodeEnd(context, component);
94    }
95  
96    @Override
97    public final void decode(FacesContext context, UIComponent component) {
98      this.decodeInternal(context, (T) component);
99    }
100 
101   public void decodeInternal(FacesContext context, T component) {
102     super.decode(context, component);
103   }
104 
105   @Override
106   public Object getConvertedValue(
107       final FacesContext facesContext, final UIComponent component, final Object submittedValue)
108       throws ConverterException {
109     return getConvertedValueInternal(facesContext, (T) component, submittedValue);
110   }
111 
112   public Object getConvertedValueInternal(
113       final FacesContext context, final T component, final Object submittedValue)
114       throws ConverterException {
115     if (!(submittedValue instanceof String)) {
116       return submittedValue;
117     }
118     final Converter converter = ComponentUtils.getConverter(context, component, submittedValue);
119     if (converter != null) {
120       return converter.getAsObject(context, component, (String) submittedValue);
121     } else {
122       return submittedValue;
123     }
124   }
125 
126 // end of "redefine"
127 
128   protected String getCurrentValue(final FacesContext facesContext, final T component) {
129 
130     if (component instanceof ValueHolder) {
131       final ValueHolder valueHolder = (ValueHolder) component;
132       if (valueHolder instanceof EditableValueHolder) {
133         final EditableValueHolder editableValueHolder = (EditableValueHolder) component;
134         final Object submittedValue = editableValueHolder.getSubmittedValue();
135         if (submittedValue != null || !editableValueHolder.isValid()) {
136           return (String) submittedValue;
137         }
138       }
139       String currentValue = null;
140       final Object result = ((ValueHolder) component).getValue();
141       if (result != null) {
142         currentValue = ComponentUtils.getFormattedValue(facesContext, component, result);
143       }
144       return currentValue;
145     } else {
146       return null;
147     }
148   }
149 
150   public static void renderFocus(
151       final String clientId, final boolean focus, final boolean error, final FacesContext facesContext,
152       final TobagoResponseWriter writer) throws IOException {
153     final Map<String, Object> requestMap = facesContext.getExternalContext().getRequestMap();
154     if (!requestMap.containsKey(FOCUS_KEY)
155         && (clientId.equals(TobagoContext.getInstance(facesContext).getFocusId()) || focus || error)) {
156       requestMap.put(FOCUS_KEY, Boolean.TRUE);
157       writer.writeAttribute(HtmlAttributes.AUTOFOCUS, true);
158     }
159   }
160 
161   protected TobagoResponseWriter getResponseWriter(final FacesContext facesContext) {
162     final ResponseWriter writer = facesContext.getResponseWriter();
163     if (writer instanceof TobagoResponseWriter) {
164       return (TobagoResponseWriter) writer;
165     } else {
166       return new TobagoResponseWriterWrapper(writer);
167     }
168   }
169 
170   protected void insideBegin(final FacesContext facesContext, final HtmlElements inside) {
171     facesContext.getAttributes().put(inside, Boolean.TRUE);
172   }
173 
174   protected void insideEnd(final FacesContext facesContext, final HtmlElements inside) {
175     facesContext.getAttributes().remove(inside);
176   }
177 
178   protected boolean isInside(final FacesContext facesContext, final HtmlElements inside) {
179     return facesContext.getAttributes().get(inside) != null;
180   }
181 
182   /**
183    * Special implementation for the reload facet (e.g. for tc:panel and tc:sheet).
184    */
185   public void encodeReload(FacesContext facesContext, AbstractUIReload reload) throws IOException {
186     final TobagoResponseWriter writer = getResponseWriter(facesContext);
187     writer.write("{\"reload\":{\"frequency\":" + reload.getFrequency() + "}}");
188   }
189 
190   /**
191    * Renders the tobago-behavior tag.
192    *
193    * @since 5.0
194    */
195   protected void encodeBehavior(
196       final TobagoResponseWriter writer, final FacesContext facesContext, final ClientBehaviorHolder holder)
197       throws IOException {
198     if (holder != null) {
199       final CommandMap behaviorCommands = getBehaviorCommands(facesContext, holder);
200       encodeBehavior(writer, behaviorCommands);
201     }
202   }
203 
204   /**
205    * Renders the tobago-behavior tag.
206    *
207    * @since 5.0
208    */
209   protected void encodeBehavior(
210       final TobagoResponseWriter writer, final CommandMap behaviorCommands)
211       throws IOException {
212     if (behaviorCommands != null) {
213       final Command click = behaviorCommands.getClick();
214       if (click != null) {
215         encodeBehavior(writer, ClientBehaviors.click, click);
216       }
217       final Map<ClientBehaviors, Command> other = behaviorCommands.getOther();
218       if (other != null) {
219         for (Map.Entry<ClientBehaviors, Command> entry : other.entrySet()) {
220           encodeBehavior(writer, entry.getKey(), entry.getValue());
221         }
222       }
223     }
224   }
225 
226   private void encodeBehavior(
227       final TobagoResponseWriter writer, final ClientBehaviors behaviors, final Command command)
228       throws IOException {
229     writer.startElement(HtmlElements.TOBAGO_BEHAVIOR);
230     writer.writeAttribute(CustomAttributes.EVENT, behaviors.name(), false);
231     writer.writeAttribute(HtmlAttributes.ACTION, command.getAction(), false); // tbd: rename to actionId?
232     writer.writeAttribute(CustomAttributes.EXECUTE, command.getExecute(), false);
233     writer.writeAttribute(CustomAttributes.RENDER, command.getRender(), false);
234     writer.writeAttribute(CustomAttributes.OMIT, command.getOmit());
235     writer.writeAttribute(CustomAttributes.CONFIRMATION, command.getConfirmation(), true);
236     writer.writeAttribute(CustomAttributes.DECOUPLED,
237         command.getTransition() != null ? command.getTransition() : false);
238     final Collapse collapse = command.getCollapse();
239     if (collapse != null) {
240       writer.writeAttribute(CustomAttributes.COLLAPSE_ACTION, collapse.getAction().name(), false);
241       writer.writeAttribute(CustomAttributes.COLLAPSE_TARGET, collapse.getFor(), false);
242     }
243     writer.writeAttribute(CustomAttributes.DELAY, command.getDelay());
244     writer.writeAttribute(CustomAttributes.FOCUS_ID, command.getFocus(), false);
245     writer.writeAttribute(HtmlAttributes.TARGET, command.getTarget(), true);
246 
247     // todo: all the other attributes
248     writer.endElement(HtmlElements.TOBAGO_BEHAVIOR);
249   }
250 
251   protected CommandMap getBehaviorCommands(
252       final FacesContext facesContext, final ClientBehaviorHolder clientBehaviorHolder) {
253     CommandMap commandMap = null;
254 
255     for (final Map.Entry<String, List<ClientBehavior>> entry : clientBehaviorHolder.getClientBehaviors().entrySet()) {
256       final String eventName = entry.getKey();
257       final ClientBehaviorContext clientBehaviorContext
258           = getClientBehaviorContext(facesContext, clientBehaviorHolder, eventName);
259 
260       for (final ClientBehavior clientBehavior : entry.getValue()) {
261         if (clientBehavior instanceof EventBehavior) {
262           final EventBehavior eventBehavior = (EventBehavior) clientBehavior;
263           final AbstractUIEvent abstractUIEvent
264               = RenderUtils.getAbstractUIEvent((UIComponent) clientBehaviorHolder, eventBehavior);
265 
266           if (abstractUIEvent != null && abstractUIEvent.isRendered() && !abstractUIEvent.isDisabled()) {
267             for (List<ClientBehavior> children : abstractUIEvent.getClientBehaviors().values()) {
268               for (ClientBehavior child : children) {
269                 final CommandMap childMap = getCommandMap(facesContext, clientBehaviorContext, child);
270                 commandMap = CommandMap.merge(commandMap, childMap);
271               }
272             }
273           }
274         }
275 
276         final CommandMap map = getCommandMap(facesContext, clientBehaviorContext, clientBehavior);
277         commandMap = CommandMap.merge(commandMap, map);
278       }
279     }
280 
281     // if there is no explicit behavior (with f:ajax or tc:event), use the command properties as default.
282     if ((commandMap == null || commandMap.isEmpty()) && clientBehaviorHolder instanceof AbstractUICommand) {
283       if (commandMap == null) {
284         commandMap = new CommandMap();
285       }
286       commandMap.addCommand(ClientBehaviors.click, new Command(facesContext, (AbstractUICommand) clientBehaviorHolder));
287     }
288 
289     return commandMap;
290   }
291 
292   private static ClientBehaviorContext getClientBehaviorContext(
293       final FacesContext facesContext, final ClientBehaviorHolder clientBehaviorHolder, final String eventName) {
294     final UIComponent component = (UIComponent) clientBehaviorHolder;
295     return ClientBehaviorContext.createClientBehaviorContext(facesContext, component, eventName,
296         component.getClientId(facesContext), null);
297   }
298 
299   private static CommandMap getCommandMap(
300       final FacesContext facesContext, final ClientBehaviorContext clientBehaviorContext,
301       final ClientBehavior clientBehavior) {
302     if (clientBehavior instanceof ClientBehaviorBase) {
303       String type = ((ClientBehaviorBase) clientBehavior).getRendererType();
304 
305       // this is to use a different renderer for Tobago components and other components.
306       if (type.equals(AjaxBehavior.BEHAVIOR_ID)) {
307         type = "org.apache.myfaces.tobago.behavior.Ajax";
308       }
309       final ClientBehaviorRenderer renderer = facesContext.getRenderKit().getClientBehaviorRenderer(type);
310       final String dummy = renderer.getScript(clientBehaviorContext, clientBehavior);
311       if (dummy != null) {
312         return CommandMap.restoreCommandMap(facesContext);
313       }
314     } else {
315       LOG.warn("Ignoring: '{}'", clientBehavior);
316     }
317     return null;
318   }
319 
320   protected void decodeClientBehaviors(final FacesContext facesContext, final T component) {
321     if (component instanceof ClientBehaviorHolder) {
322       final ClientBehaviorHolder clientBehaviorHolder = (ClientBehaviorHolder) component;
323       final Map<String, List<ClientBehavior>> clientBehaviors = clientBehaviorHolder.getClientBehaviors();
324       if (clientBehaviors != null && !clientBehaviors.isEmpty()) {
325         final Map<String, String> paramMap = facesContext.getExternalContext().getRequestParameterMap();
326         final String behaviorEventName = paramMap.get("javax.faces.behavior.event");
327         if (behaviorEventName != null) {
328           final List<ClientBehavior> clientBehaviorList = clientBehaviors.get(behaviorEventName);
329           if (clientBehaviorList != null && !clientBehaviorList.isEmpty()) {
330             final String clientId = paramMap.get("javax.faces.source");
331             if (component.getClientId(facesContext).equals(clientId)) {
332               for (final ClientBehavior clientBehavior : clientBehaviorList) {
333                 clientBehavior.decode(facesContext, component);
334               }
335             }
336           }
337         }
338       }
339     }
340   }
341 
342 }