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.compiler;
20  
21  import java.util.ArrayList;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.Stack;
25  
26  import javax.el.ELException;
27  import javax.faces.application.FacesMessage;
28  import javax.faces.view.facelets.CompositeFaceletHandler;
29  import javax.faces.view.facelets.FaceletHandler;
30  import javax.faces.view.facelets.Tag;
31  import javax.faces.view.facelets.TagAttribute;
32  import javax.faces.view.facelets.TagException;
33  
34  import org.apache.myfaces.shared.renderkit.html.HTML;
35  import org.apache.myfaces.view.facelets.el.ELText;
36  
37  /**
38   * 
39   * @author Jacob Hookom
40   * @version $Id$
41   */
42  final class TextUnit extends CompilationUnit
43  {
44  
45      private final StringBuffer buffer;
46  
47      private final StringBuffer textBuffer;
48  
49      private final List<Instruction> instructionBuffer;
50  
51      private final Stack<Tag> tags;
52  
53      private final List<Object> children;
54  
55      private boolean startTagOpen;
56  
57      private final String alias;
58  
59      private final String id;
60      
61      private final List<Object> messages;
62  
63      private final boolean escapeInlineText;
64  
65      private final boolean compressSpaces;
66  
67      public TextUnit(String alias, String id)
68      {
69          this(alias,id,true);
70      }
71      
72      public TextUnit(String alias, String id, boolean escapeInlineText)
73      {
74          this(alias,id,escapeInlineText,false);
75      }
76      
77      public TextUnit(String alias, String id, boolean escapeInlineText, boolean compressSpaces)
78      {
79          this.alias = alias;
80          this.id = id;
81          this.buffer = new StringBuffer();
82          this.textBuffer = new StringBuffer();
83          this.instructionBuffer = new ArrayList<Instruction>();
84          this.tags = new Stack<Tag>();
85          this.children = new ArrayList<Object>();
86          this.startTagOpen = false;
87          this.messages = new ArrayList<Object>(4);
88          this.escapeInlineText = escapeInlineText;
89          this.compressSpaces = compressSpaces;
90      }
91  
92      public FaceletHandler createFaceletHandler()
93      {
94          this.flushBufferToConfig(true);
95  
96          if (this.children.isEmpty())
97          {
98              return LEAF;
99          }
100 
101         FaceletHandler[] h = new FaceletHandler[this.children.size()];
102         Object obj;
103         for (int i = 0; i < h.length; i++)
104         {
105             obj = this.children.get(i);
106             if (obj instanceof FaceletHandler)
107             {
108                 h[i] = (FaceletHandler) obj;
109             }
110             else
111             {
112                 h[i] = ((CompilationUnit) obj).createFaceletHandler();
113             }
114         }
115         if (h.length == 1)
116         {
117             return h[0];
118         }
119         return new CompositeFaceletHandler(h);
120     }
121 
122     private void addInstruction(Instruction instruction)
123     {
124         this.flushTextBuffer(false);
125         this.instructionBuffer.add(instruction);
126     }
127 
128     private void flushTextBuffer(boolean child)
129     {
130         if (this.textBuffer.length() > 0)
131         {
132             String s = this.textBuffer.toString();
133 
134             if (child)
135             {
136                 s = trimRight(s);
137             }
138             if (s.length() > 0)
139             {
140                 if (!compressSpaces)
141                 {
142                     //Do it as usual.
143                     ELText txt = ELText.parse(s);
144                     if (txt != null)
145                     {
146                         if (txt.isLiteral())
147                         {
148                             if (escapeInlineText)
149                             {
150                                 this.instructionBuffer.add(new LiteralTextInstruction(txt.toString()));
151                             }
152                             else
153                             {
154                                 this.instructionBuffer.add(new LiteralNonExcapedTextInstruction(txt.toString()));
155                             }
156                         }
157                         else
158                         {
159                             if (escapeInlineText)
160                             {
161                                 this.instructionBuffer.add(new TextInstruction(this.alias, txt ));
162                             }
163                             else
164                             {
165                                 // When escape inline text is disabled (jspx case) we have to split the EL and add
166                                 // separate instructions, so it can be properly escaped.
167                                 ELText[] splitText = ELText.parseAsArray(s);
168                                 if (splitText.length > 1)
169                                 {
170                                     Instruction[] array = new Instruction[splitText.length];
171                                     for (int i = 0; i < splitText.length; i++)
172                                     {
173                                         ELText selText = splitText[i];
174                                         if (selText.isLiteral())
175                                         {
176                                             array[i] = new LiteralNonExcapedTextInstruction(selText.toString());
177                                         }
178                                         else
179                                         {
180                                             array[i] = new TextInstruction(this.alias, selText );
181                                         }
182                                     }
183                                     this.instructionBuffer.add(new CompositeTextInstruction(array));
184                                 }
185                                 else
186                                 {
187                                     this.instructionBuffer.add(new TextInstruction(this.alias, txt ));
188                                 }
189                             }
190                         }
191                     }
192                 }
193                 else
194                 {
195                     // First check if the text contains EL before build something, and if contains 
196                     // an EL expression, compress it before build the ELText.
197                     if (s != null && s.length() > 0)
198                     {
199                         if (ELText.isLiteral(s))
200                         {
201                             if (escapeInlineText)
202                             {
203                                 this.instructionBuffer.add(new LiteralTextInstruction(s));
204                             }
205                             else
206                             {
207                                 this.instructionBuffer.add(new LiteralNonExcapedTextInstruction(s));
208                             }
209                         }
210                         else
211                         {
212                             if (instructionBuffer.size() > 0 && 
213                                 !(instructionBuffer.get(instructionBuffer.size()-1) instanceof LiteralXMLInstruction))
214                             {
215                                 s = compressELText(s);
216                             }
217                             // When escape inline text is disabled (jspx case) we have to split the EL and add
218                             // separate instructions, so it can be properly escaped.
219                             ELText[] splitText = ELText.parseAsArray(s);
220                             if (splitText.length > 1)
221                             {
222                                 Instruction[] array = new Instruction[splitText.length];
223                                 for (int i = 0; i < splitText.length; i++)
224                                 {
225                                     ELText selText = splitText[i];
226                                     if (selText.isLiteral())
227                                     {
228                                         array[i] = new LiteralNonExcapedTextInstruction(selText.toString());
229                                     }
230                                     else
231                                     {
232                                         array[i] = new TextInstruction(this.alias, selText );
233                                     }
234                                 }
235                                 this.instructionBuffer.add(new CompositeTextInstruction(array));
236                             }
237                             else
238                             {
239                                 this.instructionBuffer.add(new TextInstruction(this.alias, ELText.parse(s)));
240                             }
241                         }
242                     }
243                 }
244             }
245 
246         }
247         this.textBuffer.setLength(0);
248     }
249 
250     public void write(String text)
251     {
252         this.finishStartTag();
253         this.textBuffer.append(text);
254         this.buffer.append(text);
255     }
256 
257     public void writeInstruction(String text)
258     {
259         this.finishStartTag();
260         ELText el = ELText.parse(text);
261         if (el.isLiteral())
262         {
263             this.addInstruction(new LiteralXMLInstruction(text));
264         }
265         else
266         {
267             this.addInstruction(new XMLInstruction(el));
268         }
269         this.buffer.append(text);
270     }
271 
272     public void writeComment(String text)
273     {
274         this.finishStartTag();
275 
276         ELText el = ELText.parse(text);
277         if (el.isLiteral())
278         {
279             this.addInstruction(new LiteralCommentInstruction(text));
280         }
281         else
282         {
283             this.addInstruction(new CommentInstruction(el));
284         }
285 
286         this.buffer.append("<!--" + text + "-->");
287     }
288 
289     public void startTag(Tag tag)
290     {
291 
292         // finish any previously written tags
293         this.finishStartTag();
294 
295         // push this tag onto the stack
296         this.tags.push(tag);
297 
298         // write it out
299         this.buffer.append('<');
300         this.buffer.append(tag.getQName());
301 
302         this.addInstruction(new StartElementInstruction(tag.getQName()));
303 
304         TagAttribute[] attrs = tag.getAttributes().getAll();
305         if (attrs.length > 0)
306         {
307             for (int i = 0; i < attrs.length; i++)
308             {
309                 String qname = attrs[i].getQName();
310                 String value = attrs[i].getValue();
311                 this.buffer.append(' ').append(qname).append("=\"").append(value).append("\"");
312 
313                 ELText txt = ELText.parseAllowEmptyString(value);
314                 if (txt != null)
315                 {
316                     if (txt.isLiteral())
317                     {
318                         this.addInstruction(new LiteralAttributeInstruction(qname, txt.toString()));
319                     }
320                     else
321                     {
322                         this.addInstruction(new AttributeInstruction(this.alias, qname, txt));
323                     }
324                 }
325             }
326         }
327         
328         if (!messages.isEmpty())
329         {
330             for (Iterator<Object> it = messages.iterator(); it.hasNext();)
331             {
332                 Object[] message = (Object[])it.next();
333                 this.addInstruction(new AddFacesMessageInstruction((FacesMessage.Severity) message[0],
334                                                                    (String)message[1], (String)message[2]));
335                 it.remove();
336             }
337         }
338 
339         // notify that we have an open tag
340         this.startTagOpen = true;
341     }
342 
343     private void finishStartTag()
344     {
345         if (this.tags.size() > 0 && this.startTagOpen)
346         {
347             this.buffer.append(">");
348             this.startTagOpen = false;
349         }
350     }
351 
352     public void endTag()
353     {
354         Tag tag = (Tag) this.tags.pop();
355 
356         if (HTML.BODY_ELEM.equalsIgnoreCase(tag.getQName()))
357         {
358             this.addInstruction(new BodyEndElementInstruction(tag.getQName()));
359         }
360         else
361         {
362             this.addInstruction(new EndElementInstruction(tag.getQName()));            
363         }
364 
365         if (this.startTagOpen)
366         {
367             this.buffer.append("/>");
368             this.startTagOpen = false;
369         }
370         else
371         {
372             this.buffer.append("</").append(tag.getQName()).append('>');
373         }
374     }
375 
376     public void addChild(CompilationUnit unit)
377     {
378         // if we are adding some other kind of unit
379         // then we need to capture our buffer into a UITextHandler
380         this.finishStartTag();
381         this.flushBufferToConfig(true);
382         this.children.add(unit);
383     }
384 
385     protected void flushBufferToConfig(boolean child)
386     {
387         this.flushTextBuffer(child);
388 
389         int size = this.instructionBuffer.size();
390         if (size > 0)
391         {
392             try
393             {
394                 String s = this.buffer.toString();
395                 if (child)
396                 {
397                     s = trimRight(s);
398                 }
399                 ELText txt = ELText.parse(s);
400                 if (txt != null)
401                 {
402                     if (compressSpaces)
403                     {
404                         // Use the logic behind the instructions to remove unnecessary instructions
405                         // containing only spaces, or recreating new ones containing only the necessary
406                         // spaces.
407                         size = compressSpaces(instructionBuffer, size);
408                     }
409                     Instruction[] instructions = (Instruction[]) this.instructionBuffer
410                             .toArray(new Instruction[size]);
411                     this.children.add(new UIInstructionHandler(this.alias, this.id, instructions, txt));
412                     this.instructionBuffer.clear();
413                 }
414 
415             }
416             catch (ELException e)
417             {
418                 if (this.tags.size() > 0)
419                 {
420                     throw new TagException((Tag) this.tags.peek(), e.getMessage());
421                 }
422                 else
423                 {
424                     throw new ELException(this.alias + ": " + e.getMessage(), e.getCause());
425                 }
426             }
427         }
428 
429         // ALWAYS CLEAR FOR BOTH IMPL
430         this.buffer.setLength(0);
431     }
432 
433     public boolean isClosed()
434     {
435         return this.tags.empty();
436     }
437 
438     private final static String trimRight(String s)
439     {
440         int i = s.length() - 1;
441         while (i >= 0 && Character.isWhitespace(s.charAt(i)))
442         {
443             i--;
444         }
445         if (i >= 0)
446         {
447             return s;
448         }
449         else
450         {
451             return "";
452         }
453         /*
454         if (i == s.length() - 1)
455         {
456             return s;
457         }
458         else
459         {
460             return s.substring(0, i + 1);
461         }*/
462     }
463     
464     final static String compressELText(String text)
465     {
466         //int firstCharLocation = getFirstTextCharLocationIgnoringSpacesTabsAndCarriageReturn(text);
467         int firstCharLocation = -1;
468         int leftChar = 0; // 0=first char on left 1=\n 2=\r 3=\r\n
469         int lenght = text.length();
470         String leftText = null;
471         for (int j = 0; j < lenght; j++)
472         {
473             char c = text.charAt(j);
474             if (leftChar == 0)
475             {
476                 if (c == '\r')
477                 {
478                     leftChar = 2;
479                     if (j+1 < lenght)
480                     {
481                         if (text.charAt(j+1) == '\n')
482                         {
483                             leftChar = 3;
484                         }
485                     }
486                 }
487                 if (c == '\n')
488                 {
489                     leftChar = 1;
490                 }
491             }
492             if (Character.isWhitespace(c))
493             {
494                 continue;
495             }
496             else
497             {
498                 firstCharLocation = j;
499                 break;
500             }
501         }
502         if (firstCharLocation == -1)
503         {
504             firstCharLocation = lenght;
505         }
506         // Define the character on the left
507         if (firstCharLocation > 0)
508         {
509             switch (leftChar)
510             {
511                 case 1:
512                     leftText = "\n";
513                     break;
514                 case 2:
515                     leftText = "\r";
516                     break;
517                 case 3:
518                     leftText = "\r\n";
519                     break;
520                 default:
521                     leftText = (lenght > 1) ? text.substring(0,1) : text;
522                     break;
523             }                
524         }
525         else
526         {
527             leftText = "";
528         }
529                 
530         int lastCharLocation = getLastTextCharLocationIgnoringSpacesTabsAndCarriageReturn(text);
531         if (firstCharLocation == 0 && lastCharLocation == text.length()-1)
532         {
533             return text;
534         }
535         else
536         {
537             if (lastCharLocation+1 < text.length())
538             {
539                 lastCharLocation = lastCharLocation+1;
540             }
541             if (firstCharLocation == 0)
542             {
543                 return text.substring(firstCharLocation, lastCharLocation+1);
544             }
545             else
546             {
547                 return leftText+text.substring(firstCharLocation, lastCharLocation+1);
548             }
549         }
550     }
551     
552     /**
553      * Compress spaces around a list of instructions, following these rules:
554      * 
555      * - The first instruction that is on the left usually make contact with a component.
556      * 
557      * @param instructionBuffer
558      * @param size
559      * @return 
560      */
561     final static int compressSpaces(List<Instruction> instructionBuffer, int size)
562     {
563         boolean addleftspace = true;
564         boolean addrightspace = false;
565         boolean skipnext = false;
566         for (int i = 0; i < size; i++)
567         {
568             String text = null;
569             String newText = null;
570             int instructionType = 0;
571             if (skipnext)
572             {
573                 skipnext = false;
574                 continue;
575             }
576             Instruction ins = instructionBuffer.get(i);
577             if (i+1 == size)
578             {
579                 addrightspace = true;
580             }
581             
582             if (ins instanceof LiteralTextInstruction)
583             {
584                 text = ((LiteralTextInstruction)ins).getText();
585                 instructionType = 1;
586             }
587             else if (ins instanceof LiteralNonExcapedTextInstruction)
588             {
589                 text = ((LiteralTextInstruction)ins).getText();
590                 instructionType = 2;
591             }
592             else if (ins instanceof LiteralXMLInstruction)
593             {
594                 skipnext = true;
595                 continue;
596             }
597             
598             if (text != null && text.length() > 0)
599             {
600                 int firstCharLocation = -1;
601                 int leftChar = 0; // 0=first char on left 1=\n 2=\r 3=\r\n
602                 int lenght = text.length();
603                 String leftText = null;
604                 for (int j = 0; j < lenght; j++)
605                 {
606                     char c = text.charAt(j);
607                     if (leftChar == 0)
608                     {
609                         if (c == '\r')
610                         {
611                             leftChar = 2;
612                             if (j+1 < lenght)
613                             {
614                                 if (text.charAt(j+1) == '\n')
615                                 {
616                                     leftChar = 3;
617                                 }
618                             }
619                         }
620                         if (c == '\n')
621                         {
622                             leftChar = 1;
623                         }
624                     }
625                     if (Character.isWhitespace(c))
626                     {
627                         continue;
628                     }
629                     else
630                     {
631                         firstCharLocation = j;
632                         break;
633                     }
634                 }
635                 if (firstCharLocation == -1)
636                 {
637                     firstCharLocation = lenght;
638                 }
639                 // Define the character on the left
640                 if (firstCharLocation > 0)
641                 {
642                     switch (leftChar)
643                     {
644                         case 1:
645                             leftText = "\n";
646                             break;
647                         case 2:
648                             leftText = "\r";
649                             break;
650                         case 3:
651                             leftText = "\r\n";
652                             break;
653                         default:
654                             leftText = (lenght > 1) ? text.substring(0,1) : text;
655                             break;
656                     }                
657                 }
658                 else
659                 {
660                     leftText = "";
661                 }
662                 
663                 if (firstCharLocation == lenght && lenght > 1)
664                 {
665                     // All the instruction is space, replace with an instruction 
666                     // with only one space
667                     if (addleftspace || addrightspace)
668                     {
669                         newText = leftText;
670                     }
671                     else
672                     {
673                         instructionBuffer.remove(i);
674                         i--;
675                         size--;
676                     }
677                 }
678                 else
679                 {
680                     int lastCharLocation = getLastTextCharLocationIgnoringSpacesTabsAndCarriageReturn(text);
681                     // If right space, increment in 1
682                     if (lastCharLocation+1 < text.length())
683                     {
684                         lastCharLocation = lastCharLocation+1;
685                     }
686                     if (firstCharLocation > 0)
687                     {
688                         newText = leftText+
689                             text.substring(firstCharLocation, lastCharLocation+1);
690                     }
691                     else
692                     {
693                         newText = text.substring(firstCharLocation, lastCharLocation+1);
694                     }
695                 }
696                 
697                 if (newText != null)
698                 {
699                     if (instructionType == 1)
700                     {
701                         instructionBuffer.set(i, new LiteralTextInstruction(newText));
702                     }
703                     else if (instructionType == 2)
704                     {
705                         instructionBuffer.set(i, new LiteralNonExcapedTextInstruction(newText));
706                     }
707                 }
708             }
709             addleftspace = false;
710         }
711         return size;
712     }
713     
714     private static int getLastTextCharLocationIgnoringSpacesTabsAndCarriageReturn(String text)
715     {
716         for (int i = text.length()-1; i >= 0; i--)
717         {
718             if (Character.isWhitespace(text.charAt(i)))
719             {
720                 continue;
721             }
722             else
723             {
724                 return i;
725             }
726         }
727         return 0;
728     }
729 
730     public String toString()
731     {
732         return "TextUnit[" + this.children.size() + "]";
733     }
734     
735     public void addMessage(FacesMessage.Severity severity, String summary, String detail)
736     {
737         this.messages.add(new Object[]{severity, summary, detail});
738     }
739 }