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.util;
21  
22  import org.apache.myfaces.tobago.component.Attributes;
23  import org.apache.myfaces.tobago.config.Configurable;
24  import org.apache.myfaces.tobago.context.ResourceManagerUtils;
25  import org.apache.myfaces.tobago.internal.component.AbstractUICommand;
26  import org.apache.myfaces.tobago.internal.component.AbstractUIData;
27  import org.apache.myfaces.tobago.internal.util.StringUtils;
28  import org.apache.myfaces.tobago.layout.Measure;
29  import org.apache.myfaces.tobago.model.ExpandedState;
30  import org.apache.myfaces.tobago.model.SelectedState;
31  import org.apache.myfaces.tobago.model.TreePath;
32  import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
33  import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
34  import org.apache.myfaces.tobago.renderkit.html.HtmlInputTypes;
35  import org.apache.myfaces.tobago.util.ComponentUtils;
36  import org.apache.myfaces.tobago.util.DebugUtils;
37  import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  import javax.faces.application.Application;
42  import javax.faces.application.ViewHandler;
43  import javax.faces.component.EditableValueHolder;
44  import javax.faces.component.UIComponent;
45  import javax.faces.component.UIParameter;
46  import javax.faces.component.UISelectItem;
47  import javax.faces.component.UISelectItems;
48  import javax.faces.component.ValueHolder;
49  import javax.faces.context.ExternalContext;
50  import javax.faces.context.FacesContext;
51  import javax.faces.convert.Converter;
52  import javax.faces.convert.ConverterException;
53  import javax.faces.model.SelectItem;
54  import java.io.IOException;
55  import java.net.URLDecoder;
56  import java.util.ArrayList;
57  import java.util.Arrays;
58  import java.util.Collection;
59  import java.util.List;
60  import java.util.Map;
61  
62  public class RenderUtils {
63  
64    private static final Logger LOG = LoggerFactory.getLogger(RenderUtils.class);
65  
66    /**
67     * @deprecated since 2.0.0
68     */
69    @Deprecated
70    public static final String COMPONENT_IN_REQUEST = "org.apache.myfaces.tobago.component";
71  
72    public static final String SCROLL_POSTFIX = ComponentUtils.SUB_SEPARATOR + "scrollPosition";
73  
74    private RenderUtils() {
75      // to prevent instantiation
76    }
77  
78    public static boolean contains(final Object[] list, final Object value) {
79      if (list == null) {
80        return false;
81      }
82      for (final Object aList : list) {
83        if (aList != null && aList.equals(value)) {
84          return true;
85        }
86      }
87      return false;
88    }
89  
90    public static void encodeChildren(final FacesContext facesContext, final UIComponent panel) throws IOException {
91      for (final UIComponent child : panel.getChildren()) {
92        encode(facesContext, child);
93      }
94    }
95  
96    public static void encode(final FacesContext facesContext, final UIComponent component) throws IOException {
97      encode(facesContext, component, null);
98    }
99  
100   public static void encode(
101       final FacesContext facesContext, final UIComponent component,
102       final List<? extends Class<? extends UIComponent>> only)
103       throws IOException {
104 
105     if (only != null && !matchFilter(component, only)) {
106       return;
107     }
108 
109     if (component.isRendered()) {
110       if (LOG.isDebugEnabled()) {
111         LOG.debug("rendering " + component.getRendererType() + " " + component);
112       }
113       component.encodeBegin(facesContext);
114       if (component.getRendersChildren()) {
115         component.encodeChildren(facesContext);
116       } else {
117         for (final UIComponent child : component.getChildren()) {
118           encode(facesContext, child, only);
119         }
120       }
121       component.encodeEnd(facesContext);
122     }
123   }
124 
125   private static boolean matchFilter(
126       final UIComponent component, final List<? extends Class<? extends UIComponent>> only) {
127     for (final Class<? extends UIComponent> clazz : only) {
128       if (clazz.isAssignableFrom(component.getClass())) {
129         return true;
130       }
131     }
132     return false;
133   }
134 
135   /**
136    * @deprecated since 2.0.0, please use EncodeUtils.prepareRendererAll()
137    */
138   @Deprecated
139   public static void prepareRendererAll(final FacesContext facesContext, final UIComponent component)
140       throws IOException {
141     EncodeUtils.prepareRendererAll(facesContext, component);
142   }
143 
144   public static String getFormattedValue(final FacesContext facesContext, final UIComponent component) {
145     Object value = null;
146     if (component instanceof ValueHolder) {
147       value = ((ValueHolder) component).getLocalValue();
148       if (value == null) {
149         value = ((ValueHolder) component).getValue();
150       }
151     }
152     return getFormattedValue(facesContext, component, value);
153   }
154 
155   // Copy from RendererBase
156   public static String getFormattedValue(
157       final FacesContext context, final UIComponent component, final Object currentValue)
158       throws ConverterException {
159 
160     if (currentValue == null) {
161       return "";
162     }
163 
164     if (!(component instanceof ValueHolder)) {
165       return currentValue.toString();
166     }
167 
168     Converter converter = ((ValueHolder) component).getConverter();
169 
170     if (converter == null) {
171       if (currentValue instanceof String) {
172         return (String) currentValue;
173       }
174       final Class converterType = currentValue.getClass();
175       converter = context.getApplication().createConverter(converterType);
176     }
177 
178     if (converter == null) {
179       return currentValue.toString();
180     } else {
181       return converter.getAsString(context, component, currentValue);
182     }
183   }
184 
185   public static Measure calculateStringWidth(
186       final FacesContext facesContext, final UIComponent component, final String text) {
187     return calculateStringWidth(facesContext, (Configurable) component, text, "tobago.font.widths");
188   }
189 
190   public static Measure calculateStringWidth2(
191       final FacesContext facesContext, final UIComponent component, final String text) {
192     return calculateStringWidth(facesContext, (Configurable) component, text, "tobago.font2.widths");
193   }
194 
195   private static Measure calculateStringWidth(
196       final FacesContext facesContext, final Configurable component, final String text, final String type) {
197     if (text == null) {
198       return Measure.ZERO;
199     }
200     int width = 0;
201     int defaultCharWidth = 10;
202     try {
203       defaultCharWidth = ResourceManagerUtils.getThemeMeasure(facesContext, component, "fontWidth").getPixel();
204     } catch (final NullPointerException e) {
205       LOG.warn("no value for 'fontWidth' for type '" + component.getRendererType() + "' found in theme-config");
206     }
207 
208     final String fontWidths = ResourceManagerUtils.getProperty(facesContext, "tobago", type);
209 
210     for (final char c : text.toCharArray()) {
211       if (c >= 32 && c < 128) { // "normal" char in precomputed range
212         final int begin = (c - 32) * 2;
213         width += Integer.parseInt(fontWidths.substring(begin, begin + 2), 16);
214       } else {
215         width += defaultCharWidth;
216       }
217     }
218 
219     width += text.length(); // fixes the problem, that sometime some browsers add some pixels
220 
221     return Measure.valueOf(width);
222   }
223 
224   /**
225    * @deprecated Since Tobago 2.0.0. Please use SelectItemUtils
226    */
227   @Deprecated
228   public static List<SelectItem> getItemsToRender(final javax.faces.component.UISelectOne component) {
229     return getItems(component);
230   }
231 
232   /**
233    * @deprecated Since Tobago 2.0.0. Please use SelectItemUtils
234    */
235   @Deprecated
236   public static List<SelectItem> getItemsToRender(final javax.faces.component.UISelectMany component) {
237     return getItems(component);
238   }
239 
240   /**
241    * @deprecated Since Tobago 2.0.0. Please use SelectItemUtils
242    */
243   @Deprecated
244   public static List<SelectItem> getItems(final javax.faces.component.UIInput component) {
245 
246     final List<SelectItem> selectItems = getSelectItems(component);
247 
248     String renderRange = (String) component.getAttributes().get(Attributes.RENDER_RANGE_EXTERN);
249     if (renderRange == null) {
250       renderRange = (String) component.getAttributes().get(Attributes.RENDER_RANGE);
251     }
252     if (renderRange == null) {
253       return selectItems;
254     }
255 
256     final int[] indices = StringUtils.getIndices(renderRange);
257     final List<SelectItem> items = new ArrayList<SelectItem>(indices.length);
258 
259     if (selectItems.size() != 0) {
260       for (final int index : indices) {
261         items.add(selectItems.get(index));
262       }
263     } else {
264       LOG.warn("No items found! rendering dummies instead!");
265       for (int i = 0; i < indices.length; i++) {
266         items.add(new SelectItem(Integer.toString(i), "Item " + i, ""));
267       }
268     }
269     return items;
270   }
271 
272   public static String currentValue(final UIComponent component) {
273     String currentValue = null;
274     if (component instanceof ValueHolder) {
275       Object value;
276       if (component instanceof EditableValueHolder) {
277         value = ((EditableValueHolder) component).getSubmittedValue();
278         if (value != null) {
279           return (String) value;
280         }
281       }
282 
283       value = ((ValueHolder) component).getValue();
284       if (value != null) {
285         Converter converter = ((ValueHolder) component).getConverter();
286         if (converter == null) {
287           final FacesContext context = FacesContext.getCurrentInstance();
288           converter = context.getApplication().createConverter(value.getClass());
289         }
290         if (converter != null) {
291           currentValue =
292               converter.getAsString(FacesContext.getCurrentInstance(),
293                   component, value);
294         } else {
295           currentValue = value.toString();
296         }
297       }
298     }
299     return currentValue;
300   }
301 
302   /**
303    * @deprecated Since Tobago 2.0.0. Please use SelectItemUtils
304    */
305   @Deprecated
306   public static List<SelectItem> getSelectItems(final UIComponent component) {
307 
308     final ArrayList<SelectItem> list = new ArrayList<SelectItem>();
309 
310     for (final UIComponent child : component.getChildren()) {
311       if (LOG.isDebugEnabled()) {
312         LOG.debug("kid " + child);
313         LOG.debug("kid " + child.getClass().getName());
314       }
315       if (child instanceof UISelectItem) {
316         final Object value = ((UISelectItem) child).getValue();
317         if (value == null) {
318           final UISelectItem item = (UISelectItem) child;
319           if (child instanceof org.apache.myfaces.tobago.component.UISelectItem) {
320             list.add(getSelectItem(
321                 (org.apache.myfaces.tobago.component.UISelectItem) child));
322           } else {
323             list.add(new SelectItem(item.getItemValue() == null ? "" : item.getItemValue(),
324                 item.getItemLabel() != null ? item.getItemLabel() : item.getItemValue().toString(),
325                 item.getItemDescription()));
326           }
327         } else if (value instanceof SelectItem) {
328           list.add((SelectItem) value);
329         } else {
330           final String message
331               = "TYPE ERROR: value NOT instanceof SelectItem. type="
332               + value.getClass().getName() + " value=" + value;
333           LOG.error(message);
334           DebugUtils.addDevelopmentMessage(FacesContext.getCurrentInstance(), message);
335         }
336       } else if (child instanceof UISelectItems) {
337         final Object value = ((UISelectItems) child).getValue();
338         if (LOG.isDebugEnabled()) {
339           LOG.debug("value " + value);
340           if (value != null) {
341             LOG.debug("value " + value.getClass().getName());
342           }
343         }
344         if (value == null) {
345           if (LOG.isDebugEnabled()) {
346             LOG.debug("value is null");
347           }
348         } else if (value instanceof SelectItem) {
349           list.add((SelectItem) value);
350         } else if (value instanceof SelectItem[]) {
351           final SelectItem[] items = (SelectItem[]) value;
352           list.addAll(Arrays.asList(items));
353         } else if (value instanceof Collection) {
354           for (final Object o : ((Collection) value)) {
355             list.add((SelectItem) o);
356           }
357         } else if (value instanceof Map) {
358           for (final Object key : ((Map) value).keySet()) {
359             if (key != null) {
360               final Object val = ((Map) value).get(key);
361               if (val != null) {
362                 list.add(new SelectItem(val.toString(), key.toString(), null));
363               }
364             }
365           }
366         } else {
367           final String message
368               = "TYPE ERROR: value NOT instanceof SelectItem, SelectItem[], Collection, Map. type="
369               + value.getClass().getName() + " value=" + value;
370           LOG.error(message);
371           DebugUtils.addDevelopmentMessage(FacesContext.getCurrentInstance(), message);
372         }
373       }
374     }
375 
376     return list;
377   }
378 
379   /**
380    * @deprecated Since Tobago 2.0.0. Please use SelectItemUtils
381    */
382   @Deprecated
383   private static SelectItem getSelectItem(final org.apache.myfaces.tobago.component.UISelectItem component) {
384     return
385         new org.apache.myfaces.tobago.model.SelectItem(component.getItemValue() == null ? "" : component.getItemValue(),
386             component.getItemLabel(), component.getItemDescription(),
387             component.isItemDisabled(), component.getItemImage(), component.getMarkup());
388   }
389 
390 
391   public static void decodedStateOfTreeData(final FacesContext facesContext, final AbstractUIData data) {
392 
393     if (!data.isTreeModel()) {
394       return;
395     }
396 
397     // selected
398     final List<Integer> selectedIndices = decodeIndices(facesContext, data, AbstractUIData.SUFFIX_SELECTED);
399 
400     // expanded
401     final List<Integer> expandedIndices = decodeIndices(facesContext, data, AbstractUIData.SUFFIX_EXPANDED);
402 
403     final int last = data.isRowsUnlimited() ? Integer.MAX_VALUE : data.getFirst() + data.getRows();
404     for (int rowIndex = data.getFirst(); rowIndex < last; rowIndex++) {
405       data.setRowIndex(rowIndex);
406       if (!data.isRowAvailable()) {
407         break;
408       }
409 
410       final TreePath path = data.getPath();
411 
412       // selected
413       final SelectedState selectedState = data.getSelectedState();
414       final boolean oldSelected = selectedState.isSelected(path);
415       final boolean newSelected = selectedIndices.contains(rowIndex);
416       if (newSelected != oldSelected) {
417         if (newSelected) {
418           selectedState.select(path);
419         } else {
420           selectedState.unselect(path);
421         }
422       }
423 
424       // expanded
425       if (expandedIndices != null) {
426         final ExpandedState expandedState = data.getExpandedState();
427         final boolean oldExpanded = expandedState.isExpanded(path);
428         final boolean newExpanded = expandedIndices.contains(rowIndex);
429         if (newExpanded != oldExpanded) {
430           if (newExpanded) {
431             expandedState.expand(path);
432           } else {
433             expandedState.collapse(path);
434           }
435         }
436       }
437 
438     }
439     data.setRowIndex(-1);
440   }
441 
442   private static List<Integer> decodeIndices(
443       final FacesContext facesContext, final AbstractUIData data, final String suffix) {
444     String string = null;
445     final String key = data.getClientId(facesContext) + ComponentUtils.SUB_SEPARATOR + suffix;
446     try {
447       string = facesContext.getExternalContext().getRequestParameterMap().get(key);
448       if (string != null) {
449         return StringUtils.parseIntegerList(string);
450       }
451     } catch (final Exception e) {
452       // should not happen
453       LOG.warn("Can't parse " + suffix + ": '" + string + "' from parameter '" + key + "'", e);
454     }
455     return null;
456   }
457 
458   public static void writeScrollPosition(
459       final FacesContext facesContext, final TobagoResponseWriter writer, final UIComponent component)
460       throws IOException {
461     Integer[] scrollPosition = (Integer[]) component.getAttributes().get(Attributes.SCROLL_POSITION);
462     if (scrollPosition == null) {
463       final String key = component.getClientId(facesContext) + SCROLL_POSTFIX;
464       scrollPosition = parseScrollPosition(facesContext.getExternalContext().getRequestParameterMap().get(key));
465     }
466     writeScrollPosition(facesContext, writer, component, scrollPosition);
467   }
468 
469   public static void writeScrollPosition(
470       final FacesContext facesContext, final TobagoResponseWriter writer, final UIComponent component,
471       final Integer[] scrollPosition)
472       throws IOException {
473     final String clientId = component.getClientId(facesContext);
474     writer.startElement(HtmlElements.INPUT, null);
475     writer.writeIdAttribute(clientId + SCROLL_POSTFIX);
476     writer.writeNameAttribute(clientId + SCROLL_POSTFIX);
477     writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN, false);
478     final String scrollPositionString = scrollPosition != null ? scrollPosition[0] + ";" + scrollPosition[1] : "";
479     writer.writeAttribute(HtmlAttributes.VALUE, scrollPositionString, false);
480     writer.writeAttribute("data-tobago-scroll-position", "true", true);
481     writer.endElement(HtmlElements.INPUT);
482   }
483 
484   public static void decodeScrollPosition(final FacesContext facesContext, final UIComponent component) {
485     final String key = component.getClientId(facesContext) + SCROLL_POSTFIX;
486     final String value = facesContext.getExternalContext().getRequestParameterMap().get(key);
487     if (value != null) {
488       final Integer[] scrollPosition = parseScrollPosition(value);
489       if (scrollPosition != null) {
490         //noinspection unchecked
491         component.getAttributes().put(Attributes.SCROLL_POSITION, scrollPosition);
492       }
493     }
494   }
495 
496   public static Integer[] parseScrollPosition(final String value) {
497     Integer[] position = null;
498     if (!StringUtils.isBlank(value)) {
499       final int sep = value.indexOf(";");
500       if (sep == -1) {
501         LOG.warn("Can't parse: '{}'", value);
502         return null;
503       }
504       final int left = Integer.parseInt(value.substring(0, sep));
505       final int top = Integer.parseInt(value.substring(sep + 1));
506       position = new Integer[2];
507       position[0] = left;
508       position[1] = top;
509     }
510     return position;
511   }
512 
513   public static String generateUrl(final FacesContext facesContext, final AbstractUICommand component) {
514 
515     final Application application = facesContext.getApplication();
516     final ViewHandler viewHandler = application.getViewHandler();
517     final ExternalContext externalContext = facesContext.getExternalContext();
518 
519     String url = null;
520 
521     if (component.getResource() != null) {
522       final boolean jsfResource = component.isJsfResource();
523       url = ResourceManagerUtils.getPageWithoutContextPath(facesContext, component.getResource());
524       if (url != null) {
525         if (jsfResource) {
526           url = viewHandler.getActionURL(facesContext, url);
527           url = externalContext.encodeActionURL(url);
528         } else {
529           url = viewHandler.getResourceURL(facesContext, url);
530           url = externalContext.encodeResourceURL(url);
531         }
532       } else {
533         url = "";
534       }
535     } else if (component.getLink() != null) {
536 
537       final String link = component.getLink();
538       if (link.startsWith("/")) { // internal absolute link
539         url = externalContext.encodeResourceURL(externalContext.getRequestContextPath() + link);
540       } else if (StringUtils.isUrl(link)) { // external link
541         url = link;
542       } else { // internal relative link
543         url = externalContext.encodeResourceURL(link);
544       }
545 
546       final StringBuilder builder = new StringBuilder(url);
547       boolean firstParameter = !url.contains("?");
548       for (final UIComponent child : component.getChildren()) {
549         if (child instanceof UIParameter) {
550           final UIParameter parameter = (UIParameter) child;
551           if (firstParameter) {
552             builder.append("?");
553             firstParameter = false;
554           } else {
555             builder.append("&");
556           }
557           builder.append(parameter.getName());
558           builder.append("=");
559           final Object value = parameter.getValue();
560           // TODO encoding
561           builder.append(value != null ? URLDecoder.decode(value.toString()) : null);
562         }
563       }
564       url = builder.toString();
565     }
566 
567     return url;
568   }
569 
570 }