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