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.internal.util;
21  
22  import org.apache.myfaces.tobago.component.ClientBehaviors;
23  import org.apache.myfaces.tobago.internal.behavior.EventBehavior;
24  import org.apache.myfaces.tobago.internal.component.AbstractUICommand;
25  import org.apache.myfaces.tobago.internal.component.AbstractUICommandBase;
26  import org.apache.myfaces.tobago.internal.component.AbstractUIData;
27  import org.apache.myfaces.tobago.internal.component.AbstractUIEvent;
28  import org.apache.myfaces.tobago.internal.component.AbstractUITreeNodeBase;
29  import org.apache.myfaces.tobago.internal.renderkit.Command;
30  import org.apache.myfaces.tobago.internal.renderkit.CommandMap;
31  import org.apache.myfaces.tobago.model.ExpandedState;
32  import org.apache.myfaces.tobago.model.SelectedState;
33  import org.apache.myfaces.tobago.model.TreePath;
34  import org.apache.myfaces.tobago.util.ComponentUtils;
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  
38  import javax.faces.application.ViewHandler;
39  import javax.faces.component.EditableValueHolder;
40  import javax.faces.component.UIComponent;
41  import javax.faces.component.UIPanel;
42  import javax.faces.component.UIParameter;
43  import javax.faces.component.ValueHolder;
44  import javax.faces.component.behavior.AjaxBehavior;
45  import javax.faces.component.behavior.ClientBehavior;
46  import javax.faces.component.behavior.ClientBehaviorBase;
47  import javax.faces.component.behavior.ClientBehaviorContext;
48  import javax.faces.component.behavior.ClientBehaviorHolder;
49  import javax.faces.context.ExternalContext;
50  import javax.faces.context.FacesContext;
51  import javax.faces.render.ClientBehaviorRenderer;
52  import java.io.IOException;
53  import java.io.UnsupportedEncodingException;
54  import java.lang.invoke.MethodHandles;
55  import java.net.URLEncoder;
56  import java.util.Collections;
57  import java.util.List;
58  import java.util.Map;
59  import java.util.Objects;
60  
61  public final class RenderUtils {
62  
63    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
64  
65    private RenderUtils() {
66      // to prevent instantiation
67    }
68  
69    /**
70     * @deprecated since 4.0.0. Use {@link ArrayUtils#contains(Object[], Object)}
71     */
72    @Deprecated
73    public static boolean contains(final Object[] list, final Object value) {
74      return ArrayUtils.contains(list, value);
75    }
76  
77    /**
78     * @deprecated since 4.0.0. Use {@link UIComponent#encodeChildren(FacesContext)}
79     */
80    @Deprecated
81    public static void encodeChildren(final FacesContext facesContext, final UIComponent panel) throws IOException {
82      for (final UIComponent child : panel.getChildren()) {
83        child.encodeAll(facesContext);
84      }
85    }
86  
87    /**
88     * @deprecated since 4.0.0. Use {@link UIComponent#encodeAll(FacesContext)}
89     */
90    @Deprecated
91    public static void encode(final FacesContext facesContext, final UIComponent component) throws IOException {
92      component.encodeAll(facesContext);
93    }
94  
95    /**
96     * @deprecated since 4.0.0. Use {@link UIComponent#encodeAll(FacesContext)}
97     */
98    @Deprecated
99    public static void encode(
100       final FacesContext facesContext, final UIComponent component,
101       final List<? extends Class<? extends UIComponent>> only)
102       throws IOException {
103 
104     if (only != null && !matchFilter(component, only)) {
105       return;
106     }
107 
108     if (component.isRendered()) {
109       if (LOG.isDebugEnabled()) {
110         LOG.debug("rendering " + component.getRendererType() + " " + component);
111       }
112       component.encodeBegin(facesContext);
113       if (component.getRendersChildren()) {
114         component.encodeChildren(facesContext);
115       } else {
116         for (final UIComponent child : component.getChildren()) {
117           encode(facesContext, child, only);
118         }
119       }
120       component.encodeEnd(facesContext);
121     }
122   }
123 
124   /**
125    * @deprecated since 4.0.0
126    */
127   @Deprecated
128   private static boolean matchFilter(
129       final UIComponent component, final List<? extends Class<? extends UIComponent>> only) {
130     for (final Class<? extends UIComponent> clazz : only) {
131       if (clazz.isAssignableFrom(component.getClass())) {
132         return true;
133       }
134     }
135     return false;
136   }
137 
138   public static String currentValue(final UIComponent component) {
139     String currentValue = null;
140     if (component instanceof ValueHolder) {
141       Object value;
142       if (component instanceof EditableValueHolder) {
143         value = ((EditableValueHolder) component).getSubmittedValue();
144         if (value != null) {
145           return (String) value;
146         }
147       }
148 
149       value = ((ValueHolder) component).getValue();
150       if (value != null) {
151         currentValue = ComponentUtils.getFormattedValue(FacesContext.getCurrentInstance(), component, value);
152       }
153     }
154     return currentValue;
155   }
156 
157   public static void decodedStateOfTreeData(final FacesContext facesContext, final AbstractUIData data) {
158 
159     if (!data.isTreeModel()) {
160       return;
161     }
162 
163     // selected
164     final List<Integer> selectedIndices = decodeIndices(facesContext, data, AbstractUIData.SUFFIX_SELECTED);
165 
166     // expanded
167     final List<Integer> expandedIndices = decodeIndices(facesContext, data, AbstractUIData.SUFFIX_EXPANDED);
168 
169     final int last = data.isRowsUnlimited() ? Integer.MAX_VALUE : data.getFirst() + data.getRows();
170     for (int rowIndex = data.getFirst(); rowIndex < last; rowIndex++) {
171       data.setRowIndex(rowIndex);
172       if (!data.isRowAvailable()) {
173         break;
174       }
175 
176       // if the node is not rendered, the state must not be evaluated.
177       boolean skip = false;
178       for (final UIComponent uiComponent : data.getChildren()) {
179         if (uiComponent instanceof AbstractUITreeNodeBase && !uiComponent.isRendered()) {
180           skip = true;
181           break;
182         }
183       }
184       if (skip) {
185         continue;
186       }
187 
188       final TreePath path = data.getPath();
189 
190       // selected
191       if (selectedIndices != null) {
192         final SelectedState selectedState = data.getSelectedState();
193         final boolean oldSelected = selectedState.isSelected(path);
194         final boolean newSelected = selectedIndices.contains(rowIndex);
195         if (newSelected != oldSelected) {
196           if (newSelected) {
197             selectedState.select(path);
198           } else {
199             selectedState.unselect(path);
200           }
201         }
202       }
203 
204       // expanded
205       if (expandedIndices != null) {
206         final ExpandedState expandedState = data.getExpandedState();
207         final boolean oldExpanded = expandedState.isExpanded(path);
208         final boolean newExpanded = expandedIndices.contains(rowIndex);
209         if (newExpanded != oldExpanded) {
210           if (newExpanded) {
211             expandedState.expand(path);
212           } else {
213             expandedState.collapse(path);
214           }
215         }
216       }
217 
218     }
219     data.setRowIndex(-1);
220   }
221 
222   private static List<Integer> decodeIndices(
223       final FacesContext facesContext, final AbstractUIData data, final String suffix) {
224     String string = null;
225     final String key = data.getClientId(facesContext) + ComponentUtils.SUB_SEPARATOR + suffix;
226     try {
227       string = facesContext.getExternalContext().getRequestParameterMap().get(key);
228       return JsonUtils.decodeIntegerArray(string);
229     } catch (final Exception e) {
230       // should not happen
231       LOG.warn("Can't parse " + suffix + ": '" + string + "' from parameter '" + key + "'", e);
232     }
233     return null;
234   }
235 
236   public static String generateUrl(final FacesContext facesContext, final AbstractUICommandBase component) {
237 
238     final ExternalContext externalContext = facesContext.getExternalContext();
239     final String outcome = component.getOutcome();
240     final String link = component.getLink();
241 
242     String url = null;
243 
244     if (outcome != null) {
245       final ViewHandler viewHandler = facesContext.getApplication().getViewHandler();
246       url = viewHandler.getBookmarkableURL(
247           facesContext,
248           outcome,
249           null,
250           true);
251     } else if (link != null) {
252       if (StringUtils.isUrl(link)) { // external link
253         url = link;
254       } else { // internal link
255         url = externalContext.encodeResourceURL(link);
256       }
257     }
258 
259     if (link != null || outcome != null) {
260       final String characterEncoding = facesContext.getResponseWriter().getCharacterEncoding();
261       final StringBuilder builder = new StringBuilder(url);
262       boolean firstParameter = !url.contains("?");
263       for (final UIComponent child : component.getChildren()) {
264         if (child instanceof UIParameter) {
265           final UIParameter parameter = (UIParameter) child;
266           if (firstParameter) {
267             builder.append("?");
268             firstParameter = false;
269           } else {
270             builder.append("&");
271           }
272           appendUrlEncoded(builder, parameter.getName(), characterEncoding);
273           builder.append("=");
274           appendUrlEncoded(builder, parameter.getValue(), characterEncoding);
275         }
276       }
277 
278       final String fragment = component.getFragment();
279       if (StringUtils.isNotBlank(fragment)) {
280         builder.append("#");
281         appendUrlEncoded(builder, fragment.trim(), characterEncoding);
282       }
283 
284       url = builder.toString();
285     }
286 
287     return url;
288   }
289 
290   private static void appendUrlEncoded(
291       final StringBuilder builder, final Object value, final String characterEncoding) {
292 
293     if (value != null) {
294       try {
295         final String encode = URLEncoder.encode(value.toString(), characterEncoding);
296         // URLEncoder.encode may return + instead of %20 for a space, but this is not good in some cases, e.g. mailto:
297         builder.append(encode.replace("+", "%20"));
298       } catch (final UnsupportedEncodingException e) {
299         LOG.error("string='" + value + "'", e);
300       }
301     }
302   }
303 
304   /**
305    * @deprecated since 5.0.0
306    */
307   @Deprecated
308   public static CommandMap getBehaviorCommands(final FacesContext facesContext,
309       final ClientBehaviorHolder clientBehaviorHolder) {
310     CommandMap commandMap = null;
311 
312     for (final Map.Entry<String, List<ClientBehavior>> entry : clientBehaviorHolder.getClientBehaviors().entrySet()) {
313       final String eventName = entry.getKey();
314       final ClientBehaviorContext clientBehaviorContext
315           = getClientBehaviorContext(facesContext, clientBehaviorHolder, eventName);
316 
317       for (final ClientBehavior clientBehavior : entry.getValue()) {
318         if (clientBehavior instanceof EventBehavior) {
319           final EventBehavior eventBehavior = (EventBehavior) clientBehavior;
320           final AbstractUIEvent abstractUIEvent = getAbstractUIEvent((UIComponent) clientBehaviorHolder, eventBehavior);
321 
322           if (abstractUIEvent != null && abstractUIEvent.isRendered() && !abstractUIEvent.isDisabled()) {
323             for (List<ClientBehavior> children : abstractUIEvent.getClientBehaviors().values()) {
324               for (ClientBehavior child : children) {
325                 final CommandMap childMap = getCommandMap(facesContext, clientBehaviorContext, child);
326                 commandMap = CommandMap.merge(commandMap, childMap);
327               }
328             }
329           }
330         }
331 
332         final CommandMap map = getCommandMap(facesContext, clientBehaviorContext, clientBehavior);
333         commandMap = CommandMap.merge(commandMap, map);
334       }
335     }
336 
337     // if there is no explicit behavior (with f:ajax or tc:event), use the command properties as default.
338     if ((commandMap == null || commandMap.isEmpty()) && clientBehaviorHolder instanceof AbstractUICommand) {
339       if (commandMap == null) {
340         commandMap = new CommandMap();
341       }
342       commandMap.addCommand(ClientBehaviors.click, new Command(facesContext, (AbstractUICommand) clientBehaviorHolder));
343     }
344 
345     return commandMap;
346   }
347 
348   /**
349    * @deprecated since 5.0.0
350    */
351   @Deprecated
352   private static ClientBehaviorContext getClientBehaviorContext(final FacesContext facesContext,
353       final ClientBehaviorHolder clientBehaviorHolder, final String eventName) {
354     UIComponent component = (UIComponent) clientBehaviorHolder;
355     return ClientBehaviorContext.createClientBehaviorContext(facesContext, component, eventName,
356         component.getClientId(facesContext), null);
357   }
358 
359   public static AbstractUIEvent getAbstractUIEvent(final UIComponent parent,
360       final EventBehavior eventBehavior) {
361     return (AbstractUIEvent) parent.getChildren().stream()
362         .filter(child -> child instanceof AbstractUIEvent)
363         .filter(child -> Objects.equals(child.getId(), eventBehavior.getFor()))
364         .findFirst().orElse(null);
365   }
366 
367   /**
368    * @deprecated since 5.0.0
369    */
370   @Deprecated
371   private static CommandMap getCommandMap(final FacesContext facesContext,
372       final ClientBehaviorContext clientBehaviorContext, final ClientBehavior clientBehavior) {
373     if (clientBehavior instanceof ClientBehaviorBase) {
374       String type = ((ClientBehaviorBase) clientBehavior).getRendererType();
375 
376       // this is to use a different renderer for Tobago components and other components.
377       if (type.equals(AjaxBehavior.BEHAVIOR_ID)) {
378         type = "org.apache.myfaces.tobago.behavior.Ajax";
379       }
380       final ClientBehaviorRenderer renderer = facesContext.getRenderKit().getClientBehaviorRenderer(type);
381       final String dummy = renderer.getScript(clientBehaviorContext, clientBehavior);
382       if (dummy != null) {
383         return CommandMap.restoreCommandMap(facesContext);
384       }
385     } else {
386       LOG.warn("Ignoring: '{}'", clientBehavior);
387     }
388     return null;
389   }
390 
391   public static void decodeClientBehaviors(final FacesContext facesContext, final UIComponent component) {
392     if (component instanceof ClientBehaviorHolder) {
393       final ClientBehaviorHolder clientBehaviorHolder = (ClientBehaviorHolder) component;
394       final Map<String, List<ClientBehavior>> clientBehaviors = clientBehaviorHolder.getClientBehaviors();
395       if (clientBehaviors != null && !clientBehaviors.isEmpty()) {
396         final Map<String, String> paramMap = facesContext.getExternalContext().getRequestParameterMap();
397         final String behaviorEventName = paramMap.get("javax.faces.behavior.event");
398         if (behaviorEventName != null) {
399           final List<ClientBehavior> clientBehaviorList = clientBehaviors.get(behaviorEventName);
400           if (clientBehaviorList != null && !clientBehaviorList.isEmpty()) {
401             final String clientId = paramMap.get("javax.faces.source");
402             if (component.getClientId(facesContext).equals(clientId)) {
403               for (final ClientBehavior clientBehavior : clientBehaviorList) {
404                 clientBehavior.decode(facesContext, component);
405               }
406             }
407           }
408         }
409       }
410     }
411   }
412 
413   public static List<UIComponent> getFacetChildren(UIComponent facet) {
414     if (facet instanceof UIPanel) {
415       return facet.getChildren();
416     } else {
417       return Collections.singletonList(facet);
418     }
419   }
420 }