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       if (selectedIndices != null) {
414         final SelectedState selectedState = data.getSelectedState();
415         final boolean oldSelected = selectedState.isSelected(path);
416         final boolean newSelected = selectedIndices.contains(rowIndex);
417         if (newSelected != oldSelected) {
418           if (newSelected) {
419             selectedState.select(path);
420           } else {
421             selectedState.unselect(path);
422           }
423         }
424       }
425 
426       // expanded
427       if (expandedIndices != null) {
428         if (expandedIndices != null) {
429           final ExpandedState expandedState = data.getExpandedState();
430           final boolean oldExpanded = expandedState.isExpanded(path);
431           final boolean newExpanded = expandedIndices.contains(rowIndex);
432           if (newExpanded != oldExpanded) {
433             if (newExpanded) {
434               expandedState.expand(path);
435             } else {
436               expandedState.collapse(path);
437             }
438           }
439         }
440       }
441 
442     }
443     data.setRowIndex(-1);
444   }
445 
446   private static List<Integer> decodeIndices(
447       final FacesContext facesContext, final AbstractUIData data, final String suffix) {
448     String string = null;
449     final String key = data.getClientId(facesContext) + ComponentUtils.SUB_SEPARATOR + suffix;
450     try {
451       string = facesContext.getExternalContext().getRequestParameterMap().get(key);
452       if (string != null) {
453         return StringUtils.parseIntegerList(string);
454       }
455     } catch (final Exception e) {
456       // should not happen
457       LOG.warn("Can't parse " + suffix + ": '" + string + "' from parameter '" + key + "'", e);
458     }
459     return null;
460   }
461 
462   public static void writeScrollPosition(
463       final FacesContext facesContext, final TobagoResponseWriter writer, final UIComponent component)
464       throws IOException {
465     Integer[] scrollPosition = (Integer[]) component.getAttributes().get(Attributes.SCROLL_POSITION);
466     if (scrollPosition == null) {
467       final String key = component.getClientId(facesContext) + SCROLL_POSTFIX;
468       scrollPosition = parseScrollPosition(facesContext.getExternalContext().getRequestParameterMap().get(key));
469     }
470     writeScrollPosition(facesContext, writer, component, scrollPosition);
471   }
472 
473   public static void writeScrollPosition(
474       final FacesContext facesContext, final TobagoResponseWriter writer, final UIComponent component,
475       final Integer[] scrollPosition)
476       throws IOException {
477     final String clientId = component.getClientId(facesContext);
478     writer.startElement(HtmlElements.INPUT, null);
479     writer.writeIdAttribute(clientId + SCROLL_POSTFIX);
480     writer.writeNameAttribute(clientId + SCROLL_POSTFIX);
481     writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN, false);
482     final String scrollPositionString = scrollPosition != null ? scrollPosition[0] + ";" + scrollPosition[1] : "";
483     writer.writeAttribute(HtmlAttributes.VALUE, scrollPositionString, false);
484     writer.writeAttribute("data-tobago-scroll-position", "true", true);
485     writer.endElement(HtmlElements.INPUT);
486   }
487 
488   public static void decodeScrollPosition(final FacesContext facesContext, final UIComponent component) {
489     final String key = component.getClientId(facesContext) + SCROLL_POSTFIX;
490     final String value = facesContext.getExternalContext().getRequestParameterMap().get(key);
491     if (value != null) {
492       final Integer[] scrollPosition = parseScrollPosition(value);
493       if (scrollPosition != null) {
494         //noinspection unchecked
495         component.getAttributes().put(Attributes.SCROLL_POSITION, scrollPosition);
496       }
497     }
498   }
499 
500   public static Integer[] parseScrollPosition(final String value) {
501     Integer[] position = null;
502     if (!StringUtils.isBlank(value)) {
503       final int sep = value.indexOf(";");
504       if (sep == -1) {
505         LOG.warn("Can't parse: '{}'", value);
506         return null;
507       }
508       final int left = Integer.parseInt(value.substring(0, sep));
509       final int top = Integer.parseInt(value.substring(sep + 1));
510       position = new Integer[2];
511       position[0] = left;
512       position[1] = top;
513     }
514     return position;
515   }
516 
517   public static String generateUrl(final FacesContext facesContext, final AbstractUICommand component) {
518 
519     final Application application = facesContext.getApplication();
520     final ViewHandler viewHandler = application.getViewHandler();
521     final ExternalContext externalContext = facesContext.getExternalContext();
522 
523     String url = null;
524 
525     if (component.getResource() != null) {
526       final boolean jsfResource = component.isJsfResource();
527       url = ResourceManagerUtils.getPageWithoutContextPath(facesContext, component.getResource());
528       if (url != null) {
529         if (jsfResource) {
530           url = viewHandler.getActionURL(facesContext, url);
531           url = externalContext.encodeActionURL(url);
532         } else {
533           url = viewHandler.getResourceURL(facesContext, url);
534           url = externalContext.encodeResourceURL(url);
535         }
536       } else {
537         url = "";
538       }
539     } else if (component.getLink() != null) {
540 
541       final String link = component.getLink();
542       if (link.startsWith("/")) { // internal absolute link
543         url = externalContext.encodeResourceURL(externalContext.getRequestContextPath() + link);
544       } else if (StringUtils.isUrl(link)) { // external link
545         url = link;
546       } else { // internal relative link
547         url = externalContext.encodeResourceURL(link);
548       }
549 
550       final StringBuilder builder = new StringBuilder(url);
551       boolean firstParameter = !url.contains("?");
552       for (final UIComponent child : component.getChildren()) {
553         if (child instanceof UIParameter) {
554           final UIParameter parameter = (UIParameter) child;
555           if (firstParameter) {
556             builder.append("?");
557             firstParameter = false;
558           } else {
559             builder.append("&");
560           }
561           builder.append(parameter.getName());
562           builder.append("=");
563           final Object value = parameter.getValue();
564           // TODO encoding
565           builder.append(value != null ? URLDecoder.decode(value.toString()) : null);
566         }
567       }
568       url = builder.toString();
569     }
570 
571     return url;
572   }
573 
574 }