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.view.facelets.tag.jsf.html;
20  
21  import java.util.Arrays;
22  import javax.faces.render.Renderer;
23  import javax.faces.view.facelets.FaceletException;
24  import javax.faces.view.facelets.Tag;
25  import javax.faces.view.facelets.TagAttribute;
26  import javax.faces.view.facelets.TagAttributes;
27  import javax.faces.view.facelets.TagDecorator;
28  import org.apache.myfaces.view.facelets.tag.TagAttributeImpl;
29  import org.apache.myfaces.view.facelets.tag.TagAttributesImpl;
30  import org.apache.myfaces.view.facelets.tag.jsf.JsfLibrary;
31  import org.apache.myfaces.view.facelets.tag.jsf.PassThroughLibrary;
32  import org.apache.myfaces.view.facelets.tag.jsf.core.CoreLibrary;
33  
34  /**
35   * Default implementation of TagDecorator as described in JSF 2.2 javadoc of
36   * javax.faces.view.facelets.TagDecorator
37   * 
38   * @since 2.2
39   * @author Leonardo Uribe
40   */
41  public class DefaultTagDecorator implements TagDecorator
42  {
43      public final static String XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
44      public final static String JSF_NAMESPACE = JsfLibrary.NAMESPACE;
45      public final static String JSF_ALIAS_NAMESPACE = JsfLibrary.ALIAS_NAMESPACE;
46      public final static String PASS_THROUGH_NAMESPACE = PassThroughLibrary.NAMESPACE;
47      public final static String PASS_THROUGH_ALIAS_NAMESPACE = PassThroughLibrary.ALIAS_NAMESPACE;
48      private final static String EMPTY_NAMESPACE = "";
49      
50      private final static String P_ELEMENTNAME = "p:"+Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY;
51      
52      /**
53       * Fast array for lookup of local names to be inspected. 
54       */
55      static private final Object[][] LOCAL_NAME_ARR = new Object[256][];
56      
57      static private final Object[] A_NAMES = new Object[]
58      {
59        "a", new Object[]
60        {
61          new TagSelectorImpl("jsf:action", "h:commandLink"),
62          new TagSelectorImpl("jsf:actionListener", "h:commandLink"),
63          new TagSelectorImpl("jsf:value", "h:outputLink"),
64          new TagSelectorImpl("jsf:outcome", "h:link")
65        }
66      };
67  
68      static private final Object[] B_NAMES = new Object[]
69      {
70        "body",   new Object[]{new TagSelectorImpl(null, "h:body")},
71        "button", new Object[]{
72            new TagSelectorImpl("jsf:outcome", "h:button"),
73            new TagSelectorImpl(null, "h:commandButton")
74        }
75      };
76      
77      static private final Object[] F_NAMES = new Object[]
78      {
79        "form", new Object[]{new TagSelectorImpl(null, "h:form")}
80      };
81      
82      static private final Object[] H_NAMES = new Object[]
83      {
84        "head", new Object[]{new TagSelectorImpl(null, "h:head")}
85      };
86      
87      static private final Object[] I_NAMES = new Object[]
88      {
89        "img", new Object[]{new TagSelectorImpl(null, "h:graphicImage")},
90        // We can optimize this part, but note the decoration step is done at
91        // compile time, so at the end it does not matter. The important 
92        // optimization is the outer one.      
93        "input", new Object[]{
94            new TagSelectorImpl("type=\"button\"", "h:commandButton"),
95            new TagSelectorImpl("type=\"checkbox\"", "h:selectBooleanCheckbox"),          
96            
97            new TagSelectorImpl("type=\"color\"", "h:inputText"),
98            new TagSelectorImpl("type=\"date\"", "h:inputText"),
99            new TagSelectorImpl("type=\"datetime\"", "h:inputText"),
100           new TagSelectorImpl("type=\"datetime-local\"", "h:inputText"),          
101           new TagSelectorImpl("type=\"email\"", "h:inputText"),
102           new TagSelectorImpl("type=\"month\"", "h:inputText"),
103           new TagSelectorImpl("type=\"number\"", "h:inputText"),
104           new TagSelectorImpl("type=\"range\"", "h:inputText"),
105           new TagSelectorImpl("type=\"search\"", "h:inputText"),
106           new TagSelectorImpl("type=\"time\"", "h:inputText"),
107           new TagSelectorImpl("type=\"url\"", "h:inputText"),
108           new TagSelectorImpl("type=\"week\"", "h:inputText"),
109           
110           new TagSelectorImpl("type=\"file\"", "h:inputFile"),
111           new TagSelectorImpl("type=\"hidden\"", "h:inputHidden"),
112           new TagSelectorImpl("type=\"password\"", "h:inputSecret"),
113           new TagSelectorImpl("type=\"reset\"", "h:commandButton"),
114           new TagSelectorImpl("type=\"submit\"", "h:commandButton"),
115           new TagSelectorImpl("type=\"*\"", "h:inputText")
116       }
117     };
118     
119     static private final Object[] L_NAMES = new Object[]
120     {
121       "label", new Object[]{new TagSelectorImpl(null, "h:outputLabel")},
122       "link",  new Object[]{new TagSelectorImpl(null, "h:outputStylesheet")}
123     };
124 
125     static private final Object[] S_NAMES = new Object[]
126     {
127       "script", new Object[]{new TagSelectorImpl(null, "h:outputScript")},
128       "select", new Object[]
129       {
130         new TagSelectorImpl("multiple=\"*\"", "h:selectManyListbox"),
131         new TagSelectorImpl(null, "h:selectOneListbox")
132       }
133     };
134     
135     static private final Object[] T_NAMES = new Object[]
136     {
137       "textarea", new Object[]{new TagSelectorImpl(null, "h:inputTextarea")}
138     };    
139 
140     static
141     {
142       LOCAL_NAME_ARR['a'] = A_NAMES;
143       LOCAL_NAME_ARR['A'] = A_NAMES;
144       LOCAL_NAME_ARR['b'] = B_NAMES;
145       LOCAL_NAME_ARR['B'] = B_NAMES;
146       LOCAL_NAME_ARR['f'] = F_NAMES;
147       LOCAL_NAME_ARR['F'] = F_NAMES;
148       LOCAL_NAME_ARR['h'] = H_NAMES;
149       LOCAL_NAME_ARR['H'] = H_NAMES;
150       LOCAL_NAME_ARR['i'] = I_NAMES;
151       LOCAL_NAME_ARR['I'] = I_NAMES;
152       LOCAL_NAME_ARR['l'] = L_NAMES;
153       LOCAL_NAME_ARR['L'] = L_NAMES;
154       LOCAL_NAME_ARR['s'] = S_NAMES;
155       LOCAL_NAME_ARR['S'] = S_NAMES;
156       LOCAL_NAME_ARR['t'] = T_NAMES;
157       LOCAL_NAME_ARR['T'] = T_NAMES;
158     }
159     
160     static private final String[][] RESERVED_JSF_ATTRS_ARR =  new String[256][];
161     
162     static private final String[] JSF_ATTRS_B_NAMES = {"binding"};
163 
164     static private final String[] JSF_ATTRS_I_NAMES = {"id"};
165     
166     static private final String[] JSF_ATTRS_R_NAMES = {"rendered"};
167     
168     static private final String[] JSF_ATTRS_T_NAMES = {"transient"};
169 
170     static 
171     {
172       RESERVED_JSF_ATTRS_ARR['b'] = JSF_ATTRS_B_NAMES;
173       RESERVED_JSF_ATTRS_ARR['B'] = JSF_ATTRS_B_NAMES;
174       RESERVED_JSF_ATTRS_ARR['i'] = JSF_ATTRS_I_NAMES;
175       RESERVED_JSF_ATTRS_ARR['I'] = JSF_ATTRS_I_NAMES;
176       RESERVED_JSF_ATTRS_ARR['r'] = JSF_ATTRS_R_NAMES;
177       RESERVED_JSF_ATTRS_ARR['R'] = JSF_ATTRS_R_NAMES;
178       RESERVED_JSF_ATTRS_ARR['t'] = JSF_ATTRS_T_NAMES;
179       RESERVED_JSF_ATTRS_ARR['T'] = JSF_ATTRS_T_NAMES;
180     }
181     
182     private static final TagDecoratorExecutor NO_MATCH_SELECTOR = new TagSelectorImpl(null, "jsf:element");
183     
184     @Override
185     public Tag decorate(Tag tag)
186     {
187         boolean jsfNamespaceFound = false;
188 
189         for (String namespace : tag.getAttributes().getNamespaces())
190         {
191             if (JSF_NAMESPACE.equals(namespace) || JSF_ALIAS_NAMESPACE.equals(namespace))
192             {
193                 jsfNamespaceFound = true;
194                 break;
195             }
196         }
197         if (!jsfNamespaceFound)
198         {
199             // Return null, so the outer CompositeTagDecorator can process the tag.
200             return null;
201         }
202         
203         // One or many attributes has the JSF_NAMESPACE attribute set. Check empty or
204         // xhtml namespace
205         if (EMPTY_NAMESPACE.equals(tag.getNamespace()) ||
206             XHTML_NAMESPACE.equals(tag.getNamespace()))
207         {
208             String localName = tag.getLocalName();
209             boolean processed = false;
210             if (isLocalNameDecorated(localName))
211             {
212                 Object[] array = LOCAL_NAME_ARR[localName.charAt(0)];
213                 int localNameIndex = -1;
214                 if (array != null)
215                 {
216                     for (int i = array.length - 2; i >= 0; i-=2)
217                     {
218                         if (localName.equalsIgnoreCase((String)array[i]))
219                         {
220                             localNameIndex = i;
221                             break;
222                         }
223                     }
224                     if (localNameIndex >= 0)
225                     {
226                         Object[] tagSelectorArray = (Object[]) array[localNameIndex+1];
227 
228                         for (int i = 0; i < tagSelectorArray.length; i++)
229                         {
230                             TagSelector tagSelector = (TagSelector) tagSelectorArray[i];
231                             TagDecoratorExecutor executor = tagSelector.getExecutorIfApplies(tag);
232 
233                             if (executor != null)
234                             {
235                                 return executor.decorate(tag, convertTagAttributes(tag));
236                             }
237                         }
238                     }
239                 }
240             }
241             if (!processed)
242             {
243                 //If no matching entry is found, let jsf:element be the value of targetTag
244                 return NO_MATCH_SELECTOR.decorate(tag, convertTagAttributes(tag));
245             }
246             return null;
247         }
248         else
249         {
250             throw new FaceletException("Attributes under "+JSF_NAMESPACE+
251                 " can only be used for tags under "+ XHTML_NAMESPACE +" or tags with no namespace defined" );
252         }
253     }
254     
255     private TagAttributes convertTagAttributes(Tag tag)
256     {        
257         TagAttribute[] sourceTagAttributes = tag.getAttributes().getAll();
258         
259         String elementNameTagLocalName = tag.getLocalName();
260 
261         TagAttribute elementNameTagAttribute = new TagAttributeImpl(
262             tag.getLocation(), PASS_THROUGH_NAMESPACE , Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY,
263             P_ELEMENTNAME, elementNameTagLocalName );
264         
265         // 1. Count how many attributes requires to be duplicated
266         int duplicateCount = 0;
267         
268         TagAttribute[] convertedTagAttributes = new TagAttribute[
269             sourceTagAttributes.length+1+duplicateCount];
270         boolean elementNameTagAttributeSet = false;
271         int j = 0;
272 
273         for (int i = 0; i < sourceTagAttributes.length; i++)
274         {
275             TagAttribute tagAttribute = sourceTagAttributes[i];
276             String convertedNamespace;
277             String qname;
278             String namespace = tagAttribute.getNamespace();
279             
280             /*
281                 -= Leonardo Uribe =- After check the javadoc and compare it with the code and try some
282                 examples with the implementation done in the RI, we found that the javadoc of 
283                 TagDecorator has some bugs. Below is the description of the implementation done, which
284                 resembles the behavior found on the RI.
285 
286                 "...
287                 For each of argument tag's attributes obtain a reference to a TagAttribute 
288                 with the following characteristics. For discussion let such an attribute be 
289                 convertedTagAttribute.
290 
291                     * convertedTagAttribute's location: from the argument tag's location.
292 
293                     * If the current attribute's namespace is http://xmlns.jcp.org/jsf, 
294                         convertedTagAttribute's qualified name must be the current attribute's 
295                         local name and convertedTagAttribute's namespace must be the empty string. 
296                         This will have the effect of setting the current attribute as a proper 
297                         property on the UIComponent instance represented by this markup.
298 
299                     * If the current attribute's namespace is empty, assume the current 
300                         attribute's namespace is http://xmlns.jcp.org/jsf/passthrough. 
301                         ConvertedTagAttribute's qualified name is the current attribute's 
302                         local name prefixed by "p:". convertedTagAttribute's namespace must be 
303                         http://xmlns.jcp.org/jsf/passthrough.
304 
305                     * Otherwise, if the current attribute's namespace is not empty, let 
306                         the current attribute be convertedTagAttribute. This will have the 
307                         effect of let the attribute be processed by the meta rules defined
308                         by the TagHandler instance associated with the generated target 
309                         component.
310                 ..."        
311             */            
312             if (JSF_NAMESPACE.equals(namespace) || JSF_ALIAS_NAMESPACE.equals(namespace))
313             {
314                 // "... If the current attribute's namespace is http://xmlns.jcp.org/jsf, convertedTagAttribute's 
315                 //  qualified name must be the current attribute's local name and convertedTagAttribute's 
316                 // namespace must be the empty string. This will have the effect of setting the current 
317                 // attribute as a proper property on the UIComponent instance represented by this markup.
318                 convertedNamespace = "";
319                 qname = tagAttribute.getLocalName();
320                 
321                 convertedTagAttributes[j] = new TagAttributeImpl(tagAttribute.getLocation(), 
322                     convertedNamespace, tagAttribute.getLocalName(), qname, tagAttribute.getValue());
323             }
324             else if (namespace == null)
325             {
326                 // should not happen, but let it because org.xml.sax.Attributes considers it
327                 // -= Leonardo Uribe =- after conversation with Frank Caputo, who was the main contributor for
328                 // this feature in JSF 2.2, he said that if the namespace is empty the intention is pass the
329                 // attribute to the passthrough attribute map, so there is an error in the spec documentation.
330                 //convertedTagAttributes[j] = tagAttribute;
331                 
332                 convertedNamespace = PASS_THROUGH_NAMESPACE;
333                 qname = "p:"+tagAttribute.getLocalName();
334                 
335                 convertedTagAttributes[j] = new TagAttributeImpl(tagAttribute.getLocation(), 
336                     convertedNamespace, tagAttribute.getLocalName(), qname, tagAttribute.getValue());
337             }
338             else if (tagAttribute.getNamespace().length() == 0)
339             {
340                 // "... If the current attribute's namespace is empty 
341                 // let the current attribute be convertedTagAttribute. ..."
342                 // -= Leonardo Uribe =- after conversation with Frank Caputo, who was the main contributor for
343                 // this feature in JSF 2.2, he said that if the namespace is empty the intention is pass the
344                 // attribute to the passthrough attribute map, so there is an error in the spec documentation.
345                 //convertedTagAttributes[j] = tagAttribute;
346                 
347                 convertedNamespace = PASS_THROUGH_NAMESPACE;
348                 qname = "p:"+tagAttribute.getLocalName();
349                 
350                 convertedTagAttributes[j] = new TagAttributeImpl(tagAttribute.getLocation(), 
351                     convertedNamespace, tagAttribute.getLocalName(), qname, tagAttribute.getValue());
352             }
353             else /*if (!tag.getNamespace().equals(tagAttribute.getNamespace()))*/
354             {
355                 // "... or different from the argument tag's namespace, 
356                 // let the current attribute be convertedTagAttribute. ..."
357                 convertedTagAttributes[j] = tagAttribute;
358             }
359             
360             if (Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY.equals(convertedTagAttributes[j].getLocalName()) && (
361                 PASS_THROUGH_NAMESPACE.equals(convertedTagAttributes[j].getNamespace()) || 
362                 PASS_THROUGH_ALIAS_NAMESPACE.equals(convertedTagAttributes[j].getNamespace()) ) )
363             {
364                 elementNameTagAttributeSet = true;
365             }
366             j++;
367         }
368         
369         if (elementNameTagAttributeSet)
370         {
371             // This is unlikely, but theorically possible.
372             return new TagAttributesImpl(Arrays.copyOf(convertedTagAttributes, convertedTagAttributes.length-1));
373         }
374         else
375         {
376             convertedTagAttributes[convertedTagAttributes.length-1] = elementNameTagAttribute;
377             return new TagAttributesImpl(convertedTagAttributes);
378         }
379     }
380     
381     private boolean isLocalNameDecorated(String elem)
382     {
383         Object[] array = LOCAL_NAME_ARR[elem.charAt(0)];
384         if (array != null)
385         {
386             for (int i = array.length - 2; i >= 0; i-=2)
387             {
388                 if (elem.equalsIgnoreCase((String)array[i]))
389                 {
390                     return true;
391                 }
392             }
393         }
394         return false;
395     }
396 
397     private boolean isReservedJSFAttribute(String attr)
398     {
399         String[] array = RESERVED_JSF_ATTRS_ARR[attr.charAt(0)];
400         if (array != null)
401         {
402             for (int i = array.length - 1; i >= 0; i-=1)
403             {
404                 if (attr.equalsIgnoreCase((String)array[i]))
405                 {
406                     return true;
407                 }
408             }
409         }
410         return false;
411     }
412     
413     private static interface TagDecoratorExecutor
414     {
415         public Tag decorate(Tag orig, TagAttributes attributes);
416         
417     }
418     
419     private static abstract class TagSelector
420     {
421         public abstract TagDecoratorExecutor getExecutorIfApplies(Tag tag);
422     }
423     
424     private static class TagSelectorImpl extends TagSelector implements TagDecoratorExecutor
425     {
426         //private String selector;
427         private String attributeQName;
428         private String attributeLocalName;
429         private String attributePrefix;
430         private final String attributeNamespace;
431         private final String attributeAliasNamespace;
432         private String matchValue;
433         
434         private String targetQName;
435         private String targetNamespace;
436         private String targetLocalName;
437         
438         public TagSelectorImpl(String selector, String targetQName)
439         {
440             // The idea in this constructor is do the parsing step of the selector
441             // just once, so the check can be done quickly.
442             //this.selector = selector;
443             if (selector != null)
444             {
445                 int i = selector.indexOf('=');
446                 if (i >= 0)
447                 {
448                     this.attributeQName = selector.substring(0,i);
449                     String value = selector.substring(i+1);
450                     int s = value.indexOf('"');
451                     int t = value.lastIndexOf('"');
452                     if (s >= 0 && t >= 0 && t > s)
453                     {
454                         this.matchValue = value.substring(s+1,t);
455                     }
456                     else
457                     {
458                         this.matchValue = value;
459                     }
460                 }
461                 else
462                 {
463                     this.attributeQName = selector;
464                     this.matchValue = null;
465                 }
466                 
467                 int j = attributeQName.indexOf(':');
468                 this.attributeLocalName = (j >= 0) ? attributeQName.substring(j+1) : attributeQName;
469                 this.attributePrefix = (j >= 0) ? attributeQName.substring(0, j) : null;
470                 this.attributeNamespace = resolveSelectorNamespace(this.attributePrefix);
471                 this.attributeAliasNamespace = resolveAliasSelectorNamespace(this.attributePrefix);
472             }
473             else
474             {
475                 this.attributeQName = null;
476                 this.matchValue = null;
477                 this.attributeLocalName = null;
478                 this.attributePrefix = null;
479                 this.attributeNamespace = "";
480                 this.attributeAliasNamespace = null;
481             }
482             
483             this.targetQName = targetQName;
484             if (targetQName != null)
485             {
486                 int j = targetQName.indexOf(':');
487                 if (j >= 0)
488                 {
489                     //this.
490                     if (j == 1 && targetQName.charAt(0) == 'h')
491                     {
492                         this.targetNamespace = HtmlLibrary.NAMESPACE;
493                         this.targetLocalName = targetQName.substring(j+1);
494                     }
495                     else if (j == 3 && targetQName.startsWith("jsf"))
496                     {
497                         this.targetNamespace = JsfLibrary.ALIAS_NAMESPACE;
498                         this.targetLocalName = targetQName.substring(j+1);
499                     }
500                 }
501                 else
502                 {
503                     this.targetLocalName = targetQName;
504                 }
505             }
506         }
507 
508         @Override
509         public TagDecoratorExecutor getExecutorIfApplies(Tag tag)
510         {
511             if (attributeQName != null)
512             {
513                  if (matchValue != null)
514                  {
515                      String attributeNS = attributeNamespace;
516                      TagAttribute attr = tag.getAttributes().get(attributeNS, attributeLocalName);
517                      if (attr == null && attributeAliasNamespace.length() > 0)
518                      {
519                          attributeNS = attributeAliasNamespace;
520                          attr = tag.getAttributes().get(attributeAliasNamespace, attributeLocalName);
521                      }
522                      if (attr != null)
523                      {
524                          if (attributeNS.equals(attr.getNamespace()) )
525                          {
526                             // if namespace is the same match
527                              if (matchValue.equals(attr.getValue()))
528                              {
529                                 return this;
530                              }
531                              else if ("*".equals(matchValue) && attr.getValue() != null)
532                              {
533                                  return this;
534                              }
535                          }
536                          else if (attributeNS == "" && attr.getNamespace() == null)
537                          {
538                              // if namespace is empty match
539                              if (matchValue.equals(attr.getValue()))
540                              {
541                                  return this;
542                              }
543                              else if ("*".equals(matchValue) && attr.getValue() != null)
544                              {
545                                  return this;
546                              }
547                          }
548                      }
549                  }
550                  else
551                  {
552                      String attributeNS = attributeNamespace;
553                      TagAttribute attr = tag.getAttributes().get(attributeNS, attributeLocalName);
554                      if (attr == null)
555                      {
556                          attributeNS = attributeAliasNamespace;
557                          attr = tag.getAttributes().get(attributeNS, attributeLocalName);
558                      }
559                      if (attr != null)
560                      {
561                          if (attributeNS.equals(attr.getNamespace()))
562                          {
563                              // if namespace is the same match
564                              return this;
565                          }
566                          else if (attributeNS == "" && attr.getNamespace() == null)
567                          {
568                              // if namespace is empty match
569                              return this;
570                          }
571                      }
572                  }
573                 return null;
574             }
575             else
576             {
577                 return this;
578             }
579         }
580         
581         @Override
582         public Tag decorate(Tag orig, TagAttributes attributes)
583         {
584             return new Tag(orig.getLocation(), this.targetNamespace, 
585                 this.targetLocalName, this.targetQName, attributes);
586         }
587     }
588 
589     private static String resolveSelectorNamespace(String prefix)
590     {
591         if ("jsf".equals(prefix))
592         {
593             return JsfLibrary.NAMESPACE;
594         }
595         else if ("h".equals(prefix))
596         {
597             return HtmlLibrary.NAMESPACE;
598         }
599         else if ("f".equals(prefix))
600         {
601             return CoreLibrary.NAMESPACE;
602         }
603         return "";
604     }
605 
606     private static String resolveAliasSelectorNamespace(String prefix)
607     {
608         if ("jsf".equals(prefix))
609         {
610             return JsfLibrary.ALIAS_NAMESPACE;
611         }
612         else if ("h".equals(prefix))
613         {
614             return HtmlLibrary.ALIAS_NAMESPACE;
615         }
616         else if ("f".equals(prefix))
617         {
618             return CoreLibrary.ALIAS_NAMESPACE;
619         }
620         return "";
621     }
622 }