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  package org.apache.myfaces.trinidad.webapp;
20  
21  import java.text.DateFormat;
22  import java.text.ParseException;
23  import java.text.SimpleDateFormat;
24  
25  import java.util.Calendar;
26  import java.util.Date;
27  import java.util.Map;
28  import java.util.TimeZone;
29  import java.util.concurrent.atomic.AtomicInteger;
30  
31  import java.util.logging.Level;
32  
33  import javax.el.MethodExpression;
34  import javax.el.ValueExpression;
35  
36  import javax.faces.component.UIComponent;
37  import javax.faces.component.UIViewRoot;
38  import javax.faces.context.FacesContext;
39  import javax.faces.webapp.UIComponentClassicTagBase;
40  import javax.faces.webapp.UIComponentELTag;
41  
42  import javax.servlet.jsp.JspException;
43  
44  import org.apache.myfaces.trinidad.bean.FacesBean;
45  import org.apache.myfaces.trinidad.bean.PropertyKey;
46  import org.apache.myfaces.trinidad.component.UIXComponent;
47  import org.apache.myfaces.trinidad.context.RequestContext;
48  import org.apache.myfaces.trinidad.logging.TrinidadLogger;
49  import org.apache.myfaces.trinidad.util.ComponentUtils;
50  
51  
52  /**
53   * Subclass of UIComponentTag to add convenience methods,
54   * and optimize where appropriate.
55   */
56  abstract public class UIXComponentELTag extends UIComponentELTag
57  {
58    /**
59     * @see #checkChildTagExecution
60     */
61    protected enum CheckExecutionResult
62    {
63      /** Execute the child tag and its body */
64      ACCEPT,
65  
66      /** Skip both the creation of the component, and do not execute the child's body */
67      REJECT
68    }
69  
70    public UIXComponentELTag()
71    {
72    }
73  
74    public void setAttributeChangeListener(MethodExpression attributeChangeListener)
75    {
76      _attributeChangeListener = attributeChangeListener;
77    }
78  
79    @Override
80    public int doStartTag() throws JspException
81    {
82      FacesContext context = getFacesContext();
83      Map<String, Object> reqMap = context.getExternalContext().getRequestMap();
84  
85      Map<Object, Object> facesContextAttributes = getFacesContext().getAttributes();
86  
87      // Only support skipping the body of a tag when not iterating. This is due to the fact that
88      // skipping tag execution usually relies on component attribute state. Since stamping causes
89      // component state to change per-stamp, it is not reliable enough to try to determine if
90      // a component tag should process its body based on the non-stamped component state. As such,
91      // we always execute the tag body when inside a stamping component tag
92      if (!_isProcessingStampingComponentTag(facesContextAttributes))
93      {
94        UIComponentClassicTagBase parentTagBase = getParentUIComponentClassicTagBase(pageContext);
95        if (parentTagBase instanceof UIXComponentELTag)
96        {
97          UIXComponentELTag parentTag = (UIXComponentELTag)parentTagBase;
98          String facetName = getFacetName();
99  
100         // Check if the component should be created
101         if (parentTag.checkChildTagExecution(this, facetName) ==
102           CheckExecutionResult.REJECT)
103         {
104           _skipEndTagSuperCall = true;
105           return SKIP_BODY;
106         }
107       }
108     }
109 
110     _skipEndTagSuperCall = false;
111 
112     int retVal;
113     try
114     {
115       retVal = super.doStartTag();
116     }
117     catch (RuntimeException rte)
118     {
119       _logSevereTagProcessingError(context, rte);
120       throw rte;
121     }
122     catch (JspException jspe)
123     {
124       _logSevereTagProcessingError(context, jspe);
125       throw jspe;
126     }
127 
128     // There could have been some validation error during property setting
129     // on the bean, this is the closest opportunity to burst out.
130     if (_validationError != null)
131       throw new JspException(_validationError);
132 
133     _checkStartingStampingTag(facesContextAttributes);
134 
135     return retVal;
136   }
137 
138   @Override
139   public int doEndTag()
140     throws JspException
141   {
142     if (_skipEndTagSuperCall)
143     {
144       _skipEndTagSuperCall = false;
145       return EVAL_PAGE;
146     }
147     else
148     {
149       _checkEndingStampingTag();
150       return super.doEndTag();
151     }
152   }
153 
154   /**
155    * Check if a tag that creates components that stamp their children (tag that creates a component
156    * that stamps out its contents, potentially rendering the children multiple times)
157    * is currently executing (between doStartTag and doEndTag).
158    */
159   protected boolean isProcessingStampingComponentTag()
160   {
161     return _isProcessingStampingComponentTag(getFacesContext().getAttributes());
162   }
163 
164   /**
165    * Check if this tag is a stamping tag (tag that creates a component that stamps out its
166    * contents, potentially rendering the children multiple times).
167    */
168   protected boolean isStampingTag()
169   {
170     return false;
171   }
172 
173   /**
174    * Check if a child component tag should execute or not. Allows the parent tag to control if
175    * child components are created for optimization purposes. Only called if the current tag is
176    * not in a stamping tag.
177    * <p>
178    *   Called from the doStartTag of the child tag to see if the parent tag wishes to prevent
179    *   the execution of the child tag. This is called before the child tag creates its component.
180    * </p>
181    * <p>
182    *   This may be overridden by a tag to check if a child tag's body should be executed. The
183    *   framework will call this method when the child tag is executing.
184    * </p>
185    * <p>
186    *   If inside of a stamping container this code is not executed as component state
187    *   may change per stamp and therefore the tag will not have access to that state since the
188    *   component does not stamp during tag execution. Therefore, it is best to avoid trying to
189    *   defer child execution when component state is not known.
190    * </p>
191    * <p>
192    *   This method is called by the framework where the {@link #checkChildTagExecution(UIComponent)}
193    *   function is called by sub-classes of {@link UIXComponentELTag}.
194    * </p>
195    *
196    * @param childTag The child tag
197    * @param facetName The facet, if any, for the child tag
198    * @return if the child tag body should be executed or not
199    */
200   protected CheckExecutionResult checkChildTagExecution(
201     @SuppressWarnings("unused") UIComponentELTag childTag,
202     @SuppressWarnings("unused") String           facetName)
203   {
204     return CheckExecutionResult.ACCEPT;
205   }
206 
207   /**
208    * When within a component binding context, the component bindings (stored in backing bean)
209    * will be cleared so a new component instance can be created.
210    * @param context FacesContext instance
211    * @param newId id for the component
212    */
213   @Override
214   protected UIComponent createComponent(FacesContext context, String newId)
215     throws JspException
216   {
217     UIComponent component = null;
218     if (RequestContext.isInComponentBindingContext(context) && hasBinding())
219     {
220      // null out component in binding; this forces a new component to be created.
221      ValueExpression binding = _getBinding();
222      binding.setValue(getELContext(), null);
223     }
224 
225     component = super.createComponent(context, newId);
226     // if the component was pulled out of a component binding during createComponent() it is likely
227     // to be attached to some component in the tree - and thus the (severe) error is justified
228     if (component != null && component.getParent() != null)
229       _logStaleParent(context, component, component.getParent());
230 
231     return component;
232   }
233 
234   @Override
235   public void release()
236   {
237     this._binding = null;
238     super.release();
239   }
240 
241   /**
242    * Allows a child tag to check if its children should be executed based on the grand-parent.
243    * Used for components where a parent-child relationship has been established. For example,
244    * allows the show detail item to ask the parent panelTabbed tag if the show detail item should
245    * allow children components of the show detail item to be created.
246    * <p>
247    *   This method is not called by the framework, but may be called by a child tag on the
248    *   parent tag. The parent tag should override this method to determine if a child tag should
249    *   execute its children tags. In the above example, the show detail item tag should call this
250    *   method on the panelTabbed tag to see if the show detail item's children tags should be
251    *   executed.
252    * </p>
253    * <p>
254    *   This method is called by sub-classes and is not called by the framework at any time.
255    *   The framework will invoke the {@link #checkChildTagExecution(UIComponentELTag, String)}
256    *   method during the execution of the start tag.
257    * </p>
258    *
259    * @param childComponent The child component
260    * @return if the children tags of the child component should execute their tag bodies
261    */
262   public CheckExecutionResult checkChildTagExecution(
263     @SuppressWarnings("unused") UIComponent childComponent)
264   {
265     return CheckExecutionResult.ACCEPT;
266   }
267 
268   protected final void setProperties(UIComponent component)
269   {
270     if (component instanceof UIViewRoot)
271     {
272       throw new IllegalStateException(
273          "<f:view> was not present on this page; tag " + this +
274          "encountered without an <f:view> being processed.");
275     }
276 
277     super.setProperties(component);
278 
279     UIXComponent uixComponent = (UIXComponent) component;
280 
281     if (_attributeChangeListener != null)
282     {
283       uixComponent.setAttributeChangeListener(_attributeChangeListener);
284     }
285 
286     setProperties(uixComponent.getFacesBean());
287   }
288 
289   protected void setProperty(
290     FacesBean   bean,
291     PropertyKey key,
292     ValueExpression expression)
293   {
294     if (expression == null)
295       return;
296 
297     if (expression.isLiteralText())
298     {
299       bean.setProperty(key, expression.getValue(FacesContext.getCurrentInstance().getELContext()));
300     }
301     else
302     {
303       bean.setValueExpression(key, expression);
304     }
305   }
306 
307   /**
308    * Set a property of type java.lang.String[].  If the value
309    * is an EL expression, it will be stored as a ValueExpression.
310    * Otherwise, it will parsed as a whitespace-separated series
311    * of strings.
312    * Null values are ignored.
313    */
314   protected void setStringArrayProperty(
315     FacesBean       bean,
316     PropertyKey     key,
317     ValueExpression expression)
318   {
319     if (expression == null)
320       return;
321 
322     if (expression.isLiteralText())
323     {
324       bean.setProperty(key, TagUtils.parseNameTokens(
325         expression.getValue(FacesContext.getCurrentInstance().getELContext())));
326     }
327     else
328     {
329       // Support coercion from a string to a string array
330       expression = new StringArrayValueExpression(expression);
331       bean.setValueExpression(key, expression);
332     }
333   }
334 
335   /**
336    * Set a property of type java.util.List&lt;java.lang.String>.  If the value
337    * is an EL expression, it will be stored as a ValueExpression.
338    * Otherwise, it will parsed as a whitespace-separated series
339    * of strings.
340    * Null values are ignored.
341    */
342   protected void setStringListProperty(
343     FacesBean       bean,
344     PropertyKey     key,
345     ValueExpression expression)
346   {
347     if (expression == null)
348       return;
349 
350     if (expression.isLiteralText())
351     {
352       bean.setProperty(key, TagUtils.parseNameTokensAsList(
353         expression.getValue(FacesContext.getCurrentInstance().getELContext())));
354     }
355     else
356     {
357       bean.setValueExpression(key, expression);
358     }
359   }
360 
361   /**
362    * Set a property of type java.util.Set&lt;java.lang.String>.  If the value
363    * is an EL expression, it will be stored as a ValueExpression.
364    * Otherwise, it will parsed as a whitespace-separated series
365    * of strings.
366    * Null values are ignored.
367    */
368   protected void setStringSetProperty(
369     FacesBean       bean,
370     PropertyKey     key,
371     ValueExpression expression)
372   {
373     if (expression == null)
374       return;
375 
376     if (expression.isLiteralText())
377     {
378       bean.setProperty(key, TagUtils.parseNameTokensAsSet(
379         expression.getValue(FacesContext.getCurrentInstance().getELContext())));
380     }
381     else
382     {
383       bean.setValueExpression(key, expression);
384     }
385   }
386 
387   /**
388    * Set a property of type java.lang.Number.  If the value
389    * is an EL expression, it will be stored as a ValueBinding.
390    * Otherwise, it will parsed with Integer.valueOf() or Double.valueOf() .
391    * Null values are ignored.
392    */
393   protected void setNumberProperty(
394     FacesBean   bean,
395     PropertyKey key,
396     ValueExpression expression)
397   {
398     if (expression == null)
399       return;
400 
401     if (expression.isLiteralText())
402     {
403       Object value = expression.getValue(FacesContext.getCurrentInstance().getELContext());
404       if (value != null)
405       {
406         if (value instanceof Number)
407         {
408           bean.setProperty(key, value);
409         }
410         else
411         {
412           String valueStr = value.toString();
413           if(valueStr.indexOf('.') == -1)
414             bean.setProperty(key, Integer.valueOf(valueStr));
415           else
416             bean.setProperty(key, Double.valueOf(valueStr));
417         }
418       }
419     }
420     else
421     {
422       bean.setValueExpression(key, expression);
423     }
424   }
425 
426   /**
427    * Set a property of type int[].  If the value
428    * is an EL expression, it will be stored as a ValueExpression.
429    * Otherwise, it will parsed as a whitespace-separated series
430    * of ints.
431    * Null values are ignored.
432    */
433   protected void setIntArrayProperty(
434     FacesBean   bean,
435     PropertyKey key,
436     ValueExpression expression)
437   {
438     if (expression == null)
439       return;
440 
441     if (expression.isLiteralText())
442     {
443       Object value = expression.getValue(FacesContext.getCurrentInstance().getELContext());
444       if (value != null)
445       {
446         String[] strings = TagUtils.parseNameTokens(value);
447         final int[] ints;
448         if (strings != null)
449         {
450           try
451           {
452             ints = new int[strings.length];
453             for(int i=0; i<strings.length; i++)
454             {
455               int j = Integer.parseInt(strings[i]);
456               ints[i] = j;
457             }
458           }
459           catch (NumberFormatException e)
460           {
461             _LOG.severe("CANNOT_CONVERT_INTO_INT_ARRAY",value);
462             _LOG.severe(e);
463             return;
464           }
465         }
466       }
467     }
468     else
469     {
470       bean.setValueExpression(key, expression);
471     }
472   }
473 
474   @Override
475   public void setBinding(ValueExpression valueExpression)
476     throws JspException
477   {
478     this._binding = valueExpression;
479     super.setBinding(valueExpression);
480   }
481 
482   private ValueExpression _getBinding()
483   {
484     return _binding;
485   }
486 
487 
488   /**
489    * Set a property of type java.util.Date.  If the value
490    * is an EL expression, it will be stored as a ValueExpression.
491    * Otherwise, it will parsed as an ISO 8601 date (yyyy-MM-dd).
492    * Null values are ignored.
493    */
494   protected void setDateProperty(
495     FacesBean   bean,
496     PropertyKey key,
497     ValueExpression expression)
498   {
499     if (expression == null)
500       return;
501 
502     if (expression.isLiteralText())
503     {
504       bean.setProperty(key, _parseISODate(
505         expression.getValue(FacesContext.getCurrentInstance().getELContext())));
506     }
507     else
508     {
509       bean.setValueExpression(key, expression);
510     }
511   }
512 
513   /**
514    * Set a property of type java.util.Date.  If the value
515    * is an EL expression, it will be stored as a ValueBinding.
516    * Otherwise, it will parsed as an ISO 8601 date (yyyy-MM-dd)
517    * and the time components (hour, min, second, millisecond) maximized.
518    * Null values are ignored.
519    */
520     protected void setMaxDateProperty(
521     FacesBean   bean,
522     PropertyKey key,
523     ValueExpression expression)
524   {
525     if (expression == null)
526       return;
527 
528     if (expression.isLiteralText())
529     {
530       Date d = _parseISODate(expression.getValue(FacesContext.getCurrentInstance().getELContext()));
531       Calendar c = Calendar.getInstance();
532       TimeZone tz = RequestContext.getCurrentInstance().getTimeZone();
533       if (tz != null)
534         c.setTimeZone(tz);
535       c.setTime(d);
536       // Original value had 00:00:00 for hours,mins, seconds now maximize those
537       // to get the latest time value for the date supplied.
538       c.set (Calendar.HOUR_OF_DAY, 23);
539       c.set (Calendar.MINUTE, 59);
540       c.set (Calendar.SECOND, 59);
541       c.set (Calendar.MILLISECOND, 999);
542       bean.setProperty(key, c.getTime());
543     }
544     else
545     {
546       bean.setValueExpression(key, expression);
547     }
548   }
549 
550   protected void setProperties(
551     @SuppressWarnings("unused")
552     FacesBean bean)
553   {
554     // Could be abstract, but it's easier to *always* call super.setProperties(),
555     // and perhaps we'll have something generic in here, esp. if we take
556     // over "rendered" from UIComponentTag
557   }
558 
559   /**
560    * Sets any fatal validation error that could have happened during property
561    *  setting. If this is set, tag execution aborts with a JspException at the
562    *  end of doStartTag().
563    * @param validationError
564    */
565   protected void setValidationError(String validationError)
566   {
567     _validationError = validationError;
568   }
569 
570   /**
571    * Checks if the current tag is a stamping tag. If so, a counter is incremented so that it can
572    * be tracked when we are outside all stamping tags again.
573    *
574    * @see #_checkEndingStampingTag()
575    * @see #isStampingTag()
576    * @param facesContextAttributes
577    */
578   private void _checkStartingStampingTag(
579     Map<Object, Object> facesContextAttributes)
580   {
581     if (isStampingTag())
582     {
583       AtomicInteger count = (AtomicInteger)facesContextAttributes.get(_STAMPING_COUNT_KEY);
584       if (count == null)
585       {
586         // Use an atomic integer here so that we can increment and decrement the value without
587         // having to store a new integer value into the map each time. This avoids the overhead
588         // of the map.put operation.
589         facesContextAttributes.put(_STAMPING_COUNT_KEY, new AtomicInteger(1));
590       }
591       else
592       {
593         // Only used on one thread, so use the safe methods for performance (only using the
594         // atomic integer for higher performance than boxing int to Integer)
595         count.set(count.get() + 1);
596       }
597     }
598   }
599 
600   /**
601    * Decrement the counter if this tag returns true from {@link #isStampingTag()}. Allows the code
602    * to check if we are outside of all stamping component tags.
603    *
604    * @see #_checkStartingStampingTag(Map)
605    */
606   private void _checkEndingStampingTag()
607   {
608     if (isStampingTag())
609     {
610       Map<Object, Object> facesContextAttributes = getFacesContext().getAttributes();
611       AtomicInteger count = (AtomicInteger)facesContextAttributes.get(_STAMPING_COUNT_KEY);
612       if (count.get() == 1)
613       {
614         facesContextAttributes.remove(_STAMPING_COUNT_KEY);
615       }
616       else
617       {
618         count.set(count.get() - 1);
619       }
620     }
621   }
622 
623   /**
624    * Check if an iterating tag is currently executing (between doStartTag and doEndTag).
625    * This is used to always process the tag body when stamping. This prevents issues when component
626    * state is used to determine if a tag should execute and that state differs per stamp. Since
627    * stamp state is not available during JSP tag execution, it is best to always execute the tag
628    * body when inside a stamping tag.
629    */
630   private boolean _isProcessingStampingComponentTag(
631     Map<Object, Object> facesContextAttributes)
632   {
633     return facesContextAttributes.containsKey(_STAMPING_COUNT_KEY);
634   }
635 
636   /**
637     * Logs a severe warning when an exception is thrown during component tag processing.
638     * @param methodName name of the method throwing the exception
639     * @param e Throwable
640     */
641    private void _logSevereTagProcessingError(FacesContext context, Throwable e)
642    {
643      UIViewRoot viewRoot = context.getViewRoot();
644      UIComponent component = this.getComponentInstance();
645      String scopedId = _getScopedId(component, viewRoot);
646      String parentScopedId = _getParentScopedId(viewRoot);
647 
648      String message = _LOG.getMessage("ERROR_PARSING_COMPONENT_TAG",
649                                       new Object[] {scopedId, parentScopedId});
650      _LOG.severe(message, e);
651    }
652 
653    private String _getScopedId(UIComponent component, UIViewRoot viewRoot)
654    {
655      if (component == null)
656      {
657        // use tag id if component was not created
658        return this.getId();
659      }
660      else
661      {
662        return ComponentUtils.getScopedIdForComponent(component, viewRoot);
663      }
664    }
665 
666   /**
667    * Logs a message when a stale component is detected during create component.
668    * @param context FacesContext
669    * @param child the UIComponent being created
670    * @param oldParent the parent UIComponent
671    */
672   private void _logStaleParent(FacesContext context,
673                                UIComponent child,
674                                UIComponent oldParent)
675   {
676     _logStaleParentAtLevel(context, child, oldParent, Level.INFO);
677   }
678 
679    /**
680     * Logs a message at a specified level when a stale component is detected during create component.
681     * @param context FacesContext
682     * @param child the UIComponent being created
683     * @param oldParent the parent UIComponent
684     * @param level the level at which to log the message
685     */
686    private void _logStaleParentAtLevel(FacesContext context,
687                                        UIComponent child,
688                                        UIComponent oldParent,
689                                        Level level)
690    {
691      if (_LOG.isLoggable(level))
692      {
693        UIViewRoot viewRoot = context.getViewRoot();
694 
695        String scopedId = ComponentUtils.getScopedIdForComponent(child, viewRoot);
696        String oldParentScopedId = ComponentUtils.getScopedIdForComponent(oldParent, viewRoot);
697        String newParentScopedId = _getParentScopedId(viewRoot);
698 
699        String bindingEL = _getBindingExpression();
700 
701        _LOG.log(level, "ERROR_CREATE_COMPONENT_STALE",
702                    new Object[] {scopedId, oldParentScopedId, newParentScopedId, bindingEL});
703      }
704    }
705 
706    /**
707     * Returns the expression set for the component's binding attribute or null.
708     * @return String
709     */
710    private String _getBindingExpression()
711    {
712      if (_getBinding() != null)
713      {
714        if (_bindingExpression == null)
715          _bindingExpression = _getBinding().getExpressionString();
716 
717        return _bindingExpression;
718      }
719 
720      return null;
721    }
722 
723    /**
724     * Gets the scopedId of the parent component of the current tag's parent.
725     * @param viewRoot UIViewRoot instance
726     * @return String
727     */
728    private String _getParentScopedId(UIViewRoot viewRoot)
729    {
730      UIComponentClassicTagBase parentTag =
731        UIComponentClassicTagBase.getParentUIComponentClassicTagBase(pageContext);
732      if (parentTag != null)
733      {
734        UIComponent parent = parentTag.getComponentInstance();
735        return ComponentUtils.getScopedIdForComponent(parent, viewRoot);
736      }
737 
738      return null;
739    }
740 
741   /**
742    * Parse a string into a java.util.Date object.  The
743    * string must be in ISO 9601 format (yyyy-MM-dd).
744    */
745   static private final Date _parseISODate(Object o)
746   {
747     if (o == null)
748       return null;
749 
750     String stringValue = o.toString();
751     try
752     {
753       return _getDateFormat().parse(stringValue);
754     }
755     catch (ParseException pe)
756     {
757       _LOG.info("CANNOT_PARSE_VALUE_INTO_DATE", stringValue);
758       return null;
759     }
760   }
761 
762   // We rely strictly on ISO 8601 formats
763   private static DateFormat _getDateFormat()
764   {
765     SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
766     TimeZone tz = RequestContext.getCurrentInstance().getTimeZone();
767     if (tz != null)
768       sdf.setTimeZone(tz);
769     return sdf;
770   }
771 
772   private static final TrinidadLogger _LOG =
773     TrinidadLogger.createTrinidadLogger(UIXComponentELTag.class);
774 
775   /** @deprecated Not used any more in the session state manager */
776   @Deprecated
777   public static final String DOCUMENT_CREATED_KEY = "org.apache.myfaces.trinidad.DOCUMENTCREATED";
778 
779   private final static String _STAMPING_COUNT_KEY = UIXComponentELTag.class.getName() + ".STAMPING";
780 
781   private MethodExpression _attributeChangeListener;
782   private String           _validationError;
783   private String           _bindingExpression;
784   private boolean          _skipEndTagSuperCall = false;
785   /*
786    * <p>The value binding expression (if any) used to wire up this component
787    * to a {@link UIComponent} property of a JavaBean class.</p>
788    */
789   private ValueExpression _binding;
790 }