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.util;
21  
22  import org.apache.myfaces.tobago.component.Attributes;
23  import org.apache.myfaces.tobago.component.Facets;
24  import org.apache.myfaces.tobago.component.RendererTypes;
25  import org.apache.myfaces.tobago.component.Visual;
26  import org.apache.myfaces.tobago.context.Markup;
27  import org.apache.myfaces.tobago.context.TransientStateHolder;
28  import org.apache.myfaces.tobago.internal.component.AbstractUIForm;
29  import org.apache.myfaces.tobago.internal.component.AbstractUIFormBase;
30  import org.apache.myfaces.tobago.internal.component.AbstractUIInput;
31  import org.apache.myfaces.tobago.internal.component.AbstractUIPage;
32  import org.apache.myfaces.tobago.internal.component.AbstractUIPopup;
33  import org.apache.myfaces.tobago.internal.component.AbstractUIReload;
34  import org.apache.myfaces.tobago.internal.component.AbstractUISheet;
35  import org.apache.myfaces.tobago.internal.util.StringUtils;
36  import org.apache.myfaces.tobago.renderkit.RendererBase;
37  import org.apache.myfaces.tobago.renderkit.html.DataAttributes;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  import javax.el.ValueExpression;
42  import javax.faces.FactoryFinder;
43  import javax.faces.application.FacesMessage;
44  import javax.faces.component.NamingContainer;
45  import javax.faces.component.UIComponent;
46  import javax.faces.component.UIInput;
47  import javax.faces.component.UINamingContainer;
48  import javax.faces.component.UIParameter;
49  import javax.faces.component.UISelectMany;
50  import javax.faces.component.UIViewRoot;
51  import javax.faces.component.ValueHolder;
52  import javax.faces.context.FacesContext;
53  import javax.faces.convert.Converter;
54  import javax.faces.convert.ConverterException;
55  import javax.faces.event.ActionEvent;
56  import javax.faces.event.ValueChangeEvent;
57  import javax.faces.render.RenderKit;
58  import javax.faces.render.RenderKitFactory;
59  import javax.faces.render.Renderer;
60  import javax.faces.view.facelets.FaceletContext;
61  import java.lang.invoke.MethodHandles;
62  import java.util.ArrayList;
63  import java.util.Collection;
64  import java.util.HashMap;
65  import java.util.Iterator;
66  import java.util.List;
67  import java.util.Map;
68  
69  public final class ComponentUtils {
70  
71    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
72  
73    public static final String SUB_SEPARATOR = "::";
74  
75    private static final String RENDER_KEY_PREFIX
76        = "org.apache.myfaces.tobago.util.ComponentUtils.RendererKeyPrefix_";
77  
78    private static final String PAGE_KEY = "org.apache.myfaces.tobago.Page.Key";
79  
80    public static final Class[] ACTION_ARGS = {};
81    public static final Class[] ACTION_LISTENER_ARGS = {ActionEvent.class};
82    public static final Class[] VALUE_CHANGE_LISTENER_ARGS = {ValueChangeEvent.class};
83    public static final Class[] VALIDATOR_ARGS = {FacesContext.class, UIComponent.class, Object.class};
84    public static final String LIST_SEPARATOR_CHARS = ", ";
85  
86    /**
87     * Name of the map for data attributes in components. New in JSF 2.2.
88     *
89     * @since 2.0.0
90     */
91    public static final String DATA_ATTRIBUTES_KEY = "javax.faces.component.DATA_ATTRIBUTES_KEY";
92  
93    private ComponentUtils() {
94    }
95  
96    /**
97     * @deprecated since 3.0.1
98     */
99    @Deprecated
100   public static boolean hasErrorMessages(final FacesContext context) {
101     for (final FacesMessage message : (Iterable<FacesMessage>) context::getMessages) {
102       if (FacesMessage.SEVERITY_ERROR.compareTo(message.getSeverity()) <= 0) {
103         return true;
104       }
105     }
106     return false;
107   }
108 
109   public static String getFacesMessageAsString(final FacesContext facesContext, final UIComponent component) {
110     final Iterator messages = facesContext.getMessages(
111         component.getClientId(facesContext));
112     final StringBuilder stringBuffer = new StringBuilder();
113     while (messages.hasNext()) {
114       final FacesMessage message = (FacesMessage) messages.next();
115       stringBuffer.append(message.getDetail());
116     }
117     if (stringBuffer.length() > 0) {
118       return stringBuffer.toString();
119     } else {
120       return null;
121     }
122   }
123 
124   /**
125    * @deprecated since 3.0.1
126    */
127   @Deprecated
128   public static boolean isInPopup(final UIComponent component) {
129     UIComponent c = component;
130     while (c != null) {
131       if (c instanceof AbstractUIPopup) {
132         return true;
133       }
134       c = c.getParent();
135     }
136     return false;
137   }
138 
139   /**
140    * @deprecated since 3.0.1
141    */
142   @Deprecated
143   public static void resetPage(final FacesContext context) {
144     final UIViewRoot view = context.getViewRoot();
145     if (view != null) {
146       view.getAttributes().remove(PAGE_KEY);
147     }
148   }
149 
150   /**
151    * Tries to walk up the parents to find the UIViewRoot, if not found, then go to FaceletContext's FacesContext for the
152    * view root.
153    */
154   public static UIViewRoot findViewRoot(final FaceletContext faceletContext, final UIComponent component) {
155     final UIViewRoot viewRoot = findAncestor(component, UIViewRoot.class);
156     if (viewRoot != null) {
157       return viewRoot;
158     } else {
159       return faceletContext.getFacesContext().getViewRoot();
160     }
161   }
162 
163   public static AbstractUIPage findPage(final FacesContext context, final UIComponent component) {
164     final UIViewRoot view = context.getViewRoot();
165     if (view != null) {
166       TransientStateHolder stateHolder = (TransientStateHolder) view.getAttributes().get(PAGE_KEY);
167       if (stateHolder == null || stateHolder.isEmpty()) {
168         final AbstractUIPage page = findPage(component);
169         stateHolder = new TransientStateHolder(page);
170         context.getViewRoot().getAttributes().put(PAGE_KEY, stateHolder);
171       }
172       return (AbstractUIPage) stateHolder.get();
173     } else {
174       return findPage(component);
175     }
176   }
177 
178   public static AbstractUIPage findPage(final UIComponent component) {
179     UIComponent c = component;
180     if (c instanceof UIViewRoot) {
181       return findPageBreadthFirst(c);
182     } else {
183       while (c != null) {
184         if (c instanceof AbstractUIPage) {
185           return (AbstractUIPage) c;
186         }
187         c = c.getParent();
188       }
189       return null;
190     }
191   }
192 
193   public static AbstractUIPage findPage(final FacesContext facesContext) {
194     return findPageBreadthFirst(facesContext.getViewRoot());
195   }
196 
197   private static AbstractUIPage findPageBreadthFirst(final UIComponent component) {
198     for (final UIComponent child : component.getChildren()) {
199       if (child instanceof AbstractUIPage) {
200         return (AbstractUIPage) child;
201       }
202     }
203     for (final UIComponent child : component.getChildren()) {
204       final AbstractUIPage result = findPageBreadthFirst(child);
205       if (result != null) {
206         return result;
207       }
208     }
209     return null;
210   }
211 
212   public static AbstractUIFormBase findForm(final UIComponent component) {
213     UIComponent c = component;
214     while (c != null) {
215       if (c instanceof AbstractUIFormBase) {
216         return (AbstractUIFormBase) c;
217       }
218       c = c.getParent();
219     }
220     return null;
221   }
222 
223   public static <T> T findAncestor(final UIComponent component, final Class<T> type) {
224     UIComponent c = component;
225     while (c != null) {
226       if (type.isAssignableFrom(c.getClass())) {
227         return (T) c;
228       }
229       c = c.getParent();
230     }
231     return null;
232   }
233 
234   /**
235    * Find all sub forms of a component, and collects it. It does not find sub forms of sub forms.
236    */
237   public static List<AbstractUIForm> findSubForms(final UIComponent component) {
238     final List<AbstractUIForm> collect = new ArrayList<>();
239     findSubForms(collect, component);
240     return collect;
241   }
242 
243   @SuppressWarnings("unchecked")
244   private static void findSubForms(final List<AbstractUIForm> collect, final UIComponent component) {
245     final Iterator<UIComponent> kids = component.getFacetsAndChildren();
246     while (kids.hasNext()) {
247       final UIComponent child = kids.next();
248       if (child instanceof AbstractUIForm) {
249         collect.add((AbstractUIForm) child);
250       } else {
251         findSubForms(collect, child);
252       }
253     }
254   }
255 
256   /**
257    * Searches the component tree beneath the component and return the first component matching the type.
258    */
259   public static <T extends UIComponent> T findDescendant(final UIComponent component, final Class<T> type) {
260 
261     for (final UIComponent child : component.getChildren()) {
262       if (type.isAssignableFrom(child.getClass())) {
263         return (T) child;
264       }
265       final T descendant = findDescendant(child, type);
266       if (descendant != null) {
267         return descendant;
268       }
269     }
270     return null;
271   }
272 
273   /**
274    * Searches the component tree beneath the component and return the first component matching the type.
275    */
276   public static <T extends UIComponent> T findFacetDescendant(
277       final UIComponent component, final Facets facet, final Class<T> type) {
278 
279     final UIComponent facetComponent = component.getFacet(facet.name());
280     if (facetComponent != null) {
281       if (type.isAssignableFrom(facetComponent.getClass())) {
282         return (T) facetComponent;
283       } else {
284         return findDescendant(facetComponent, type);
285       }
286     } else {
287       return null;
288     }
289   }
290 
291   /**
292    * Searches the children beneath the component and return the first component matching the type.
293    */
294   public static <T extends UIComponent> T findChild(final UIComponent component, final Class<T> type) {
295 
296     for (final UIComponent child : component.getChildren()) {
297       if (type.isAssignableFrom(child.getClass())) {
298         return (T) child;
299       }
300     }
301     return null;
302   }
303 
304   /**
305    * Searches the component tree beneath the component and return all component matching the type.
306    */
307   public static <T extends UIComponent> List<T> findDescendantList(final UIComponent component, final Class<T> type) {
308 
309     final List<T> result = new ArrayList<>();
310 
311     for (final UIComponent child : component.getChildren()) {
312       if (type.isAssignableFrom(child.getClass())) {
313         result.add((T) child);
314       }
315       result.addAll(findDescendantList(child, type));
316     }
317     return result;
318   }
319 
320   /**
321    * Looks for the attribute "for" in the component. If there is any search for the component which is referenced by the
322    * "for" attribute, and return their clientId. If there is no "for" attribute, return the "clientId" of the parent (if
323    * it has a parent). This is useful for labels.
324    */
325   public static String findClientIdFor(final UIComponent component, final FacesContext facesContext) {
326     final UIComponent forComponent = findFor(component);
327     if (forComponent != null) {
328       final String clientId = forComponent.getClientId(facesContext);
329       if (LOG.isDebugEnabled()) {
330         LOG.debug("found clientId: '" + clientId + "'");
331       }
332       return clientId;
333     }
334     if (LOG.isDebugEnabled()) {
335       LOG.debug("found no clientId");
336     }
337     return null;
338   }
339 
340   public static UIComponent findFor(final UIComponent component) {
341     final String forValue = getStringAttribute(component, Attributes.forValue);
342     if (forValue == null) {
343       return component.getParent();
344     }
345     return ComponentUtils.findComponent(component, forValue);
346   }
347 
348   /**
349    * Looks for the attribute "for" of the component. In case that the value is equals to "@auto" the children of the
350    * parent will be checked if they are of the type of the parameter clazz. The "id" of the first one will be used to
351    * reset the "for" attribute of the component.
352    */
353   public static void evaluateAutoFor(final UIComponent component, final Class<? extends UIComponent> clazz) {
354     final String forComponent = getStringAttribute(component, Attributes.forValue);
355     if (LOG.isDebugEnabled()) {
356       LOG.debug("for = '" + forComponent + "'");
357     }
358     if ("@auto".equals(forComponent)) {
359       // parent
360       for (final UIComponent child : component.getParent().getChildren()) {
361         if (setForToInput(component, child, clazz, component instanceof NamingContainer)) {
362           return;
363         }
364       }
365       // grand parent
366       for (final UIComponent child : component.getParent().getParent().getChildren()) {
367         if (setForToInput(component, child, clazz, component.getParent() instanceof NamingContainer)) {
368           return;
369         }
370       }
371     }
372   }
373 
374   private static boolean setForToInput(
375       final UIComponent component, final UIComponent child, final Class<? extends UIComponent> clazz,
376       final boolean namingContainer) {
377     if (clazz.isAssignableFrom(child.getClass())) { // find the matching component
378       final String forComponent;
379       if (namingContainer) {
380         forComponent = ":::" + child.getId();
381       } else {
382         forComponent = child.getId();
383       }
384       ComponentUtils.setAttribute(component, Attributes.forValue, forComponent);
385       return true;
386     }
387     return false;
388   }
389 
390   /**
391    * @deprecated since 4.0.0
392    */
393   @Deprecated
394   public static boolean isInActiveForm(final UIComponent component) {
395     UIComponent c = component;
396     while (c != null) {
397       if (c instanceof AbstractUIFormBase) {
398         final AbstractUIFormBase form = (AbstractUIFormBase) c;
399         if (form.isSubmitted()) {
400           return true;
401         }
402       }
403       c = c.getParent();
404     }
405     return false;
406   }
407 
408   public static FacesMessage.Severity getMaximumSeverity(final UIComponent component) {
409     final FacesContext facesContext = FacesContext.getCurrentInstance();
410     final List<FacesMessage> messages = facesContext.getMessageList(component.getClientId(facesContext));
411     final FacesMessage.Severity maximumSeverity = getMaximumSeverity(messages);
412 
413     final boolean invalid = component instanceof UIInput && !((UIInput) component).isValid();
414 
415     return invalid
416         && (maximumSeverity == null || FacesMessage.SEVERITY_ERROR.getOrdinal() > maximumSeverity.getOrdinal())
417         ? FacesMessage.SEVERITY_ERROR : maximumSeverity;
418   }
419 
420   public static FacesMessage.Severity getMaximumSeverity(final List<FacesMessage> messages) {
421     FacesMessage.Severity max = null;
422     for (final FacesMessage message : messages) {
423       if (max == null || message.getSeverity().getOrdinal() > max.getOrdinal()) {
424         max = message.getSeverity();
425       }
426     }
427     return max;
428   }
429 
430   /**
431    * @deprecated since 5.0.0
432    */
433   @Deprecated
434   public static boolean isError(final UIInput uiInput) {
435     final FacesContext facesContext = FacesContext.getCurrentInstance();
436     return !uiInput.isValid()
437         || facesContext.getMessages(uiInput.getClientId(facesContext)).hasNext();
438   }
439 
440   /**
441    * @deprecated since 5.0.0
442    */
443   @Deprecated
444   public static boolean isError(final UIComponent component) {
445     if (component instanceof AbstractUIInput) {
446       return isError((AbstractUIInput) component);
447     }
448     return false;
449   }
450 
451   /**
452    * @deprecated since 5.0.0
453    */
454   @Deprecated
455   public static boolean isOutputOnly(final UIComponent component) {
456     return getBooleanAttribute(component, Attributes.disabled)
457         || getBooleanAttribute(component, Attributes.readonly);
458   }
459 
460   public static Object getAttribute(final UIComponent component, final Attributes name) {
461     return component.getAttributes().get(name.getName());
462   }
463 
464   public static boolean getBooleanAttribute(final UIComponent component, final Attributes name) {
465     return getBooleanAttribute(component, name, false);
466   }
467 
468   public static boolean getBooleanAttribute(
469       final UIComponent component, final Attributes name, final boolean defaultValue) {
470 
471     final Object bool = component.getAttributes().get(name.getName());
472     if (bool == null) {
473       return defaultValue;
474     } else if (bool instanceof Boolean) {
475       return (Boolean) bool;
476     } else {
477       return Boolean.valueOf(bool.toString());
478     }
479   }
480 
481   public static String getStringAttribute(final UIComponent component, final Attributes name) {
482     return getStringAttribute(component, name, null);
483   }
484 
485   public static String getStringAttribute(
486       final UIComponent component, final Attributes name, final String defaultValue) {
487     final String result = (String) getAttribute(component, name);
488     return result != null ? result : defaultValue;
489   }
490 
491   public static int getIntAttribute(final UIComponent component, final Attributes name) {
492     return getIntAttribute(component, name, 0);
493   }
494 
495   public static int getIntAttribute(final UIComponent component, final Attributes name, final int defaultValue) {
496     final Object integer = component.getAttributes().get(name.getName());
497     if (integer instanceof Number) {
498       return ((Number) integer).intValue();
499     } else if (integer instanceof String) {
500       try {
501         return Integer.parseInt((String) integer);
502       } catch (final NumberFormatException e) {
503         LOG.warn("Can't parse number from string : \"" + integer + "\"!");
504         return defaultValue;
505       }
506     } else if (integer == null) {
507       return defaultValue;
508     } else {
509       LOG.warn("Unknown type '" + integer.getClass().getName()
510           + "' for integer attribute: " + name + " comp: " + component);
511       return defaultValue;
512     }
513   }
514 
515   public static Character getCharacterAttribute(final UIComponent component, final Attributes name) {
516     final Object character = getAttribute(component, name);
517     if (character == null) {
518       return null;
519     } else if (character instanceof Character) {
520       return (Character) character;
521     } else if (character instanceof String) {
522       final String asString = (String) character;
523       return asString.length() > 0 ? asString.charAt(0) : null;
524     } else {
525       LOG.warn("Unknown type '" + character.getClass().getName()
526           + "' for integer attribute: " + name + " comp: " + component);
527       return null;
528     }
529   }
530 
531   public static void setAttribute(final UIComponent component, final Attributes name, final Object value) {
532     component.getAttributes().put(name.getName(), value);
533   }
534 
535   public static void removeAttribute(final UIComponent component, final Attributes name) {
536     component.getAttributes().remove(name.getName());
537   }
538 
539   public static UIComponent getFacet(final UIComponent component, final Facets facet) {
540     return component.getFacet(facet.name());
541   }
542 
543   public static void setFacet(final UIComponent component, final Facets facet, final UIComponent child) {
544     component.getFacets().put(facet.name(), child);
545   }
546 
547   public static void removeFacet(final UIComponent component, final Facets facet) {
548     component.getFacets().remove(facet.name());
549   }
550 
551   public static AbstractUIReload getReloadFacet(final UIComponent component) {
552     final UIComponent facet = getFacet(component, Facets.reload);
553     if (facet == null) {
554       return null;
555     } else if (facet instanceof AbstractUIReload) {
556       return (AbstractUIReload) facet;
557     } else {
558       LOG.warn("Content of a reload facet must be {} but found {} in component with id '{}'",
559           AbstractUIReload.class.getName(), facet.getClass().getName(), component.getClientId());
560       return null;
561     }
562   }
563 
564   public static boolean isFacetOf(final UIComponent component, final UIComponent parent) {
565     for (final Object o : parent.getFacets().keySet()) {
566       final UIComponent facet = parent.getFacet((String) o);
567       if (component.equals(facet)) {
568         return true;
569       }
570     }
571     return false;
572   }
573 
574   public static RendererBase getRenderer(final FacesContext facesContext, final UIComponent component) {
575     return getRenderer(facesContext, component.getFamily(), component.getRendererType());
576   }
577 
578   public static RendererBase getRenderer(final FacesContext facesContext, final String family,
579       final String rendererType) {
580     if (rendererType == null) {
581       return null;
582     }
583 
584     final Map<String, Object> requestMap = facesContext.getExternalContext().getRequestMap();
585     final StringBuilder key = new StringBuilder(RENDER_KEY_PREFIX);
586     key.append(rendererType);
587     RendererBase renderer = (RendererBase) requestMap.get(key.toString());
588 
589     if (renderer == null) {
590       final Renderer myRenderer = getRendererInternal(facesContext, family, rendererType);
591       if (myRenderer instanceof RendererBase) {
592         requestMap.put(key.toString(), myRenderer);
593         renderer = (RendererBase) myRenderer;
594       } else {
595         return null;
596       }
597     }
598     return renderer;
599   }
600 
601 
602   private static Renderer getRendererInternal(
603       final FacesContext facesContext, final String family, final String rendererType) {
604     final RenderKitFactory rkFactory = (RenderKitFactory) FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
605     final RenderKit renderKit = rkFactory.getRenderKit(facesContext, facesContext.getViewRoot().getRenderKitId());
606     final Renderer myRenderer = renderKit.getRenderer(family, rendererType);
607     return myRenderer;
608   }
609 
610   public static Object findParameter(final UIComponent component, final String name) {
611     for (final UIComponent child : component.getChildren()) {
612       if (child instanceof UIParameter) {
613         final UIParameter parameter = (UIParameter) child;
614         if (LOG.isDebugEnabled()) {
615           LOG.debug("Select name='" + parameter.getName() + "'");
616           LOG.debug("Select value='" + parameter.getValue() + "'");
617         }
618         if (name.equals(parameter.getName())) {
619           return parameter.getValue();
620         }
621       }
622     }
623     return null;
624   }
625 
626   /**
627    * <p>
628    * The search depends on the number of prefixed colons in the relativeId:
629    * </p>
630    * <dl>
631    * <dd>number of prefixed colons == 0</dd>
632    * <dt>fully relative</dt>
633    * <dd>number of prefixed colons == 1</dd>
634    * <dt>absolute (still normal findComponent syntax)</dt>
635    * <dd>number of prefixed colons == 2</dd>
636    * <dt>search in the current naming container (same as 0 colons)</dt>
637    * <dd>number of prefixed colons == 3</dd>
638    * <dt>search in the parent naming container of the current naming container</dt>
639    * <dd>number of prefixed colons &gt; 3</dd>
640    * <dt>go to the next parent naming container for each additional colon</dt>
641    * </dl>
642    * <p>
643    * If a literal is specified: to use more than one identifier the identifiers must be space delimited.
644    * </p>
645    */
646   public static UIComponent findComponent(final UIComponent from, final String relativeId) {
647     UIComponent from1 = from;
648     String relativeId1 = relativeId;
649     final int idLength = relativeId1.length();
650     if (idLength > 0
651         && relativeId1.charAt(0) == '@'
652         && "@this".equals(relativeId1)) {
653       return from1;
654     }
655 
656     // Figure out how many colons
657     int colonCount = 0;
658     while (colonCount < idLength) {
659       if (relativeId1.charAt(colonCount) != UINamingContainer.getSeparatorChar(FacesContext.getCurrentInstance())) {
660         break;
661       }
662       colonCount++;
663     }
664 
665     // colonCount == 0: fully relative
666     // colonCount == 1: absolute (still normal findComponent syntax)
667     // colonCount > 1: for each extra colon after 1, go up a naming container
668     // (to the view root, if naming containers run out)
669     if (colonCount > 1) {
670       relativeId1 = relativeId1.substring(colonCount);
671       for (int j = 1; j < colonCount; j++) {
672         while (from1.getParent() != null) {
673           from1 = from1.getParent();
674           if (from1 instanceof NamingContainer) {
675             break;
676           }
677         }
678       }
679     }
680     return from1.findComponent(relativeId1);
681   }
682 
683   /**
684    * Resolves the real clientIds.
685    */
686   public static String evaluateClientIds(
687       final FacesContext context, final UIComponent component, final String[] componentIds) {
688     final List<String> result = new ArrayList<>(componentIds.length);
689     for (final String id : componentIds) {
690       if (!StringUtils.isBlank(id)) {
691         final String clientId = evaluateClientId(context, component, id);
692         if (clientId != null) {
693           result.add(clientId);
694         }
695       }
696     }
697     if (result.isEmpty()) {
698       return null;
699     } else {
700       return StringUtils.join(result, ' ');
701     }
702   }
703 
704   /**
705    * Resolves the real clientId.
706    */
707   public static String evaluateClientId(
708       final FacesContext context, final UIComponent component, final String componentId) {
709     final UIComponent partiallyComponent = ComponentUtils.findComponent(component, componentId);
710     if (partiallyComponent != null) {
711       final String clientId = partiallyComponent.getClientId(context);
712       if (partiallyComponent instanceof AbstractUISheet) {
713         final int rowIndex = ((AbstractUISheet) partiallyComponent).getRowIndex();
714         if (rowIndex >= 0 && clientId.endsWith(Integer.toString(rowIndex))) {
715           return clientId.substring(0, clientId.lastIndexOf(UINamingContainer.getSeparatorChar(context)));
716         }
717       }
718       return clientId;
719     }
720     LOG.error("No component found for id='{}', search base component is '{}'",
721         componentId, component != null ? component.getClientId(context) : "<null>");
722     return null;
723   }
724 
725   public static String[] splitList(final String renderers) {
726     return StringUtils.split(renderers, LIST_SEPARATOR_CHARS);
727   }
728 
729   public static Object getConvertedValue(
730       final FacesContext facesContext, final UIComponent component, final String stringValue) {
731     try {
732       final Renderer renderer = getRenderer(facesContext, component);
733       if (renderer != null) {
734         if (component instanceof UISelectMany) {
735           final Object converted = renderer.getConvertedValue(facesContext, component, new String[]{stringValue});
736           if (converted instanceof Object[]) {
737             return ((Object[]) converted)[0];
738           } else if (converted instanceof List) {
739             return ((List) converted).get(0);
740           } else if (converted instanceof Collection) {
741             return ((Collection) converted).iterator().next();
742           } else {
743             return null;
744           }
745         } else {
746           return renderer.getConvertedValue(facesContext, component, stringValue);
747         }
748       } else if (component instanceof ValueHolder) {
749         Converter converter = ((ValueHolder) component).getConverter();
750         if (converter == null) {
751           //Try to find out by value expression
752           final ValueExpression expression = component.getValueExpression("value");
753           if (expression != null) {
754             final Class valueType = expression.getType(facesContext.getELContext());
755             if (valueType != null) {
756               converter = facesContext.getApplication().createConverter(valueType);
757             }
758           }
759         }
760         if (converter != null) {
761           converter.getAsObject(facesContext, component, stringValue);
762         }
763       }
764     } catch (final Exception e) {
765       LOG.warn("Can't convert string value '" + stringValue + "'", e);
766     }
767     return stringValue;
768   }
769 
770   public static Markup markupOfSeverity(final FacesMessage.Severity maximumSeverity) {
771     if (FacesMessage.SEVERITY_FATAL.equals(maximumSeverity)) {
772       return Markup.FATAL;
773     } else if (FacesMessage.SEVERITY_ERROR.equals(maximumSeverity)) {
774       return Markup.ERROR;
775     } else if (FacesMessage.SEVERITY_WARN.equals(maximumSeverity)) {
776       return Markup.WARN;
777     } else if (FacesMessage.SEVERITY_INFO.equals(maximumSeverity)) {
778       return Markup.INFO;
779     }
780     return null;
781   }
782 
783   public static FacesMessage.Severity getMaximumSeverityOfChildrenMessages(
784       final FacesContext facesContext, final NamingContainer container) {
785     if (container instanceof UIComponent) {
786       final String clientId = ((UIComponent) container).getClientId(facesContext);
787       FacesMessage.Severity max = null;
788       for (final String id : (Iterable<String>) facesContext::getClientIdsWithMessages) {
789         if (id != null && id.startsWith(clientId)) {
790           final Iterator messages = facesContext.getMessages(id);
791           while (messages.hasNext()) {
792             final FacesMessage message = (FacesMessage) messages.next();
793             if (max == null || message.getSeverity().getOrdinal() > max.getOrdinal()) {
794               max = message.getSeverity();
795             }
796           }
797         }
798       }
799       return max;
800     }
801     return null;
802   }
803 
804   /**
805    * Adding a data attribute to the component. The name must start with "data-", e. g. "data-tobago-foo" or "data-bar"
806    */
807   public static void putDataAttributeWithPrefix(
808       final UIComponent component, final DataAttributes name, final Object value) {
809     if (name.getValue().startsWith("data-")) {
810       putDataAttribute(component, name.getValue().substring(5), value);
811     } else {
812       LOG.error("The name must start with 'data-' but it doesn't: '" + name + "'");
813     }
814   }
815 
816   /**
817    * Adding a data attribute to the component. The name should not start with "data-", e. g. "tobago-foo" or "bar"
818    */
819   public static void putDataAttribute(final UIComponent component, final Object name, final Object value) {
820     Map<Object, Object> map = getDataAttributes(component);
821     if (map == null) {
822       map = new HashMap<>();
823       component.getAttributes().put(DATA_ATTRIBUTES_KEY, map);
824     }
825     if (map.containsKey(name)) {
826       LOG.warn("Data attribute '{}' is already set for component '{}' (old value='{}', new value='{}')!",
827           name, component.getClientId(), map.get(name), value);
828     }
829     map.put(name, value);
830   }
831 
832   @SuppressWarnings("unchecked")
833   public static Map<Object, Object> getDataAttributes(final UIComponent component) {
834     return (Map<Object, Object>) component.getAttributes().get(DATA_ATTRIBUTES_KEY);
835   }
836 
837   public static Object getDataAttribute(final UIComponent component, final String name) {
838     final Map<Object, Object> map = getDataAttributes(component);
839     return map != null ? map.get(name) : null;
840   }
841 
842   /**
843    * May return null, if no converter can be find.
844    */
845   public static Converter getConverter(
846       final FacesContext facesContext, final UIComponent component, final Object value) {
847 
848     Converter converter = null;
849     if (component instanceof ValueHolder) {
850       converter = ((ValueHolder) component).getConverter();
851     }
852 
853     if (converter == null) {
854       final ValueExpression valueExpression = component.getValueExpression("value");
855       if (valueExpression != null) {
856         Class converterType = null;
857         try {
858           converterType = valueExpression.getType(facesContext.getELContext());
859         } catch (final Exception e) {
860           // ignore, seems not to be possible, when EL is a function like #{bean.getName(item.id)}
861         }
862         if (converterType == null) {
863           if (value != null) {
864             converterType = value.getClass();
865           }
866         }
867         if (converterType != null && converterType != Object.class) {
868           converter = facesContext.getApplication().createConverter(converterType);
869         }
870       }
871     }
872 
873     return converter;
874   }
875 
876   public static String getFormattedValue(
877       final FacesContext facesContext, final UIComponent component, final Object currentValue)
878       throws ConverterException {
879 
880     if (currentValue == null) {
881       return "";
882     }
883 
884     final Converter converter = ComponentUtils.getConverter(facesContext, component, currentValue);
885     if (converter != null) {
886       return converter.getAsString(facesContext, component, currentValue);
887     } else {
888       return currentValue.toString();
889     }
890   }
891 
892   public static UIComponent createComponent(
893       final FacesContext facesContext, final String componentType, final RendererTypes rendererType,
894       final String clientId) {
895     final UIComponent component = facesContext.getApplication().createComponent(componentType);
896     if (rendererType != null) {
897       component.setRendererType(rendererType.name());
898     }
899     component.setId(clientId);
900     return component;
901   }
902 
903   public static List<UIComponent> findLayoutChildren(final UIComponent container) {
904     final List<UIComponent> result = new ArrayList<>();
905     addLayoutChildren(container, result);
906     return result;
907   }
908 
909   private static void addLayoutChildren(final UIComponent component, final List<UIComponent> result) {
910     for (final UIComponent child : component.getChildren()) {
911       if (child instanceof Visual && !((Visual) child).isPlain()
912           || (UIComponent.isCompositeComponent(child) && !child.isRendered())) {
913         result.add(child);
914       } else {
915         // Child seems to be transparent for layout, like UIForm with "plain" set.
916         // So we try to add the inner components.
917         addLayoutChildren(child, result);
918       }
919     }
920 
921     final UIComponent child = component.getFacet(UIComponent.COMPOSITE_FACET_NAME);
922     if (child instanceof Visual && !((Visual) child).isPlain()) {
923       result.add(child);
924     } else if (child != null) {
925       // Child seems to be transparent for layout, like UIForm with "plain" set.
926       // So we try to add the inner components.
927       addLayoutChildren(child, result);
928     }
929   }
930 
931   /**
932    * returns the "confirmation" attribute or the value of the "confirmation" facet of a component.
933    *
934    * @since Tobago 4.4.0
935    */
936   public static String getConfirmation(final UIComponent component) {
937     final String confirmation = getStringAttribute(component, Attributes.confirmation, null);
938     if (confirmation != null) {
939       return confirmation;
940     }
941     final UIComponent facet = ComponentUtils.getFacet(component, Facets.confirmation);
942     if (facet instanceof ValueHolder && ((UIComponent) facet).isRendered()) {
943       final ValueHolder valueHolder = (ValueHolder) facet;
944       return "" + valueHolder.getValue();
945     } else if (facet != null && !(facet instanceof ValueHolder)) {
946       LOG.warn("The content of a confirmation facet must be a ValueHolder. Use e. g. <tc:out>.");
947     }
948     return null;
949   }
950 }