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: TextUnit.java 1569413 2014-02-18 16:28:38Z lu4242 $
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.size() == 0)
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.parse(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 
388         // NEW IMPLEMENTATION
389         if (true)
390         {
391 
392             this.flushTextBuffer(child);
393 
394             int size = this.instructionBuffer.size();
395             if (size > 0)
396             {
397                 try
398                 {
399                     String s = this.buffer.toString();
400                     if (child)
401                     {
402                         s = trimRight(s);
403                     }
404                     ELText txt = ELText.parse(s);
405                     if (txt != null)
406                     {
407                         if (compressSpaces)
408                         {
409                             // Use the logic behind the instructions to remove unnecessary instructions
410                             // containing only spaces, or recreating new ones containing only the necessary
411                             // spaces.
412                             size = compressSpaces(instructionBuffer, size);
413                         }
414                         Instruction[] instructions = (Instruction[]) this.instructionBuffer
415                                 .toArray(new Instruction[size]);
416                         this.children.add(new UIInstructionHandler(this.alias, this.id, instructions, txt));
417                         this.instructionBuffer.clear();
418                     }
419 
420                 }
421                 catch (ELException e)
422                 {
423                     if (this.tags.size() > 0)
424                     {
425                         throw new TagException((Tag) this.tags.peek(), e.getMessage());
426                     }
427                     else
428                     {
429                         throw new ELException(this.alias + ": " + e.getMessage(), e.getCause());
430                     }
431                 }
432             }
433 
434             // KEEP THESE SEPARATE SO LOGIC DOESN'T GET FUBARED
435         }
436         else if (this.buffer.length() > 0)
437         {
438             String s = this.buffer.toString();
439             if (s.trim().length() > 0)
440             {
441                 if (child)
442                 {
443                     s = trimRight(s);
444                 }
445                 if (s.length() > 0)
446                 {
447                     try
448                     {
449                         ELText txt = ELText.parse(s);
450                         if (txt != null)
451                         {
452                             if (txt.isLiteral())
453                             {
454                                 this.children.add(new UILiteralTextHandler(txt.toString()));
455                             }
456                             else
457                             {
458                                 this.children.add(new UITextHandler(this.alias, txt));
459                             }
460                         }
461                     }
462                     catch (ELException e)
463                     {
464                         if (this.tags.size() > 0)
465                         {
466                             throw new TagException((Tag) this.tags.peek(), e.getMessage());
467                         }
468                         else
469                         {
470                             throw new ELException(this.alias + ": " + e.getMessage(), e.getCause());
471                         }
472                     }
473                 }
474             }
475         }
476 
477         // ALWAYS CLEAR FOR BOTH IMPL
478         this.buffer.setLength(0);
479     }
480 
481     public boolean isClosed()
482     {
483         return this.tags.empty();
484     }
485 
486     private final static String trimRight(String s)
487     {
488         int i = s.length() - 1;
489         while (i >= 0 && Character.isWhitespace(s.charAt(i)))
490         {
491             i--;
492         }
493         if (i >= 0)
494         {
495             return s;
496         }
497         else
498         {
499             return "";
500         }
501         /*
502         if (i == s.length() - 1)
503         {
504             return s;
505         }
506         else
507         {
508             return s.substring(0, i + 1);
509         }*/
510     }
511     
512     final static String compressELText(String text)
513     {
514         //int firstCharLocation = getFirstTextCharLocationIgnoringSpacesTabsAndCarriageReturn(text);
515         int firstCharLocation = -1;
516         int leftChar = 0; // 0=first char on left 1=\n 2=\r 3=\r\n
517         int lenght = text.length();
518         String leftText = null;
519         for (int j = 0; j < lenght; j++)
520         {
521             char c = text.charAt(j);
522             if (leftChar == 0)
523             {
524                 if (c == '\r')
525                 {
526                     leftChar = 2;
527                     if (j+1 < lenght)
528                     {
529                         if (text.charAt(j+1) == '\n')
530                         {
531                             leftChar = 3;
532                         }
533                     }
534                 }
535                 if (c == '\n')
536                 {
537                     leftChar = 1;
538                 }
539             }
540             if (Character.isWhitespace(c))
541             {
542                 continue;
543             }
544             else
545             {
546                 firstCharLocation = j;
547                 break;
548             }
549         }
550         if (firstCharLocation == -1)
551         {
552             firstCharLocation = lenght;
553         }
554         // Define the character on the left
555         if (firstCharLocation > 0)
556         {
557             switch (leftChar)
558             {
559                 case 1:
560                     leftText = "\n";
561                     break;
562                 case 2:
563                     leftText = "\r";
564                     break;
565                 case 3:
566                     leftText = "\r\n";
567                     break;
568                 default:
569                     leftText = (lenght > 1) ? text.substring(0,1) : text;
570                     break;
571             }                
572         }
573         else
574         {
575             leftText = "";
576         }
577                 
578         int lastCharLocation = getLastTextCharLocationIgnoringSpacesTabsAndCarriageReturn(text);
579         if (firstCharLocation == 0 && lastCharLocation == text.length()-1)
580         {
581             return text;
582         }
583         else
584         {
585             if (lastCharLocation+1 < text.length())
586             {
587                 lastCharLocation = lastCharLocation+1;
588             }
589             if (firstCharLocation == 0)
590             {
591                 return text.substring(firstCharLocation, lastCharLocation+1);
592             }
593             else
594             {
595                 return leftText+text.substring(firstCharLocation, lastCharLocation+1);
596             }
597         }
598     }
599     
600     /**
601      * Compress spaces around a list of instructions, following these rules:
602      * 
603      * - The first instruction that is on the left usually make contact with a component.
604      * 
605      * @param instructionBuffer
606      * @param size
607      * @return 
608      */
609     final static int compressSpaces(List<Instruction> instructionBuffer, int size)
610     {
611         boolean addleftspace = true;
612         boolean addrightspace = false;
613         boolean skipnext = false;
614         for (int i = 0; i < size; i++)
615         {
616             String text = null;
617             String newText = null;
618             int instructionType = 0;
619             if (skipnext)
620             {
621                 skipnext = false;
622                 continue;
623             }
624             Instruction ins = instructionBuffer.get(i);
625             if (i+1 == size)
626             {
627                 addrightspace = true;
628             }
629             
630             if (ins instanceof LiteralTextInstruction)
631             {
632                 text = ((LiteralTextInstruction)ins).getText();
633                 instructionType = 1;
634             }
635             else if (ins instanceof LiteralNonExcapedTextInstruction)
636             {
637                 text = ((LiteralTextInstruction)ins).getText();
638                 instructionType = 2;
639             }
640             else if (ins instanceof LiteralXMLInstruction)
641             {
642                 skipnext = true;
643                 continue;
644             }
645             
646             if (text != null && text.length() > 0)
647             {
648                 int firstCharLocation = -1;
649                 int leftChar = 0; // 0=first char on left 1=\n 2=\r 3=\r\n
650                 int lenght = text.length();
651                 String leftText = null;
652                 for (int j = 0; j < lenght; j++)
653                 {
654                     char c = text.charAt(j);
655                     if (leftChar == 0)
656                     {
657                         if (c == '\r')
658                         {
659                             leftChar = 2;
660                             if (j+1 < lenght)
661                             {
662                                 if (text.charAt(j+1) == '\n')
663                                 {
664                                     leftChar = 3;
665                                 }
666                             }
667                         }
668                         if (c == '\n')
669                         {
670                             leftChar = 1;
671                         }
672                     }
673                     if (Character.isWhitespace(c))
674                     {
675                         continue;
676                     }
677                     else
678                     {
679                         firstCharLocation = j;
680                         break;
681                     }
682                 }
683                 if (firstCharLocation == -1)
684                 {
685                     firstCharLocation = lenght;
686                 }
687                 // Define the character on the left
688                 if (firstCharLocation > 0)
689                 {
690                     switch (leftChar)
691                     {
692                         case 1:
693                             leftText = "\n";
694                             break;
695                         case 2:
696                             leftText = "\r";
697                             break;
698                         case 3:
699                             leftText = "\r\n";
700                             break;
701                         default:
702                             leftText = (lenght > 1) ? text.substring(0,1) : text;
703                             break;
704                     }                
705                 }
706                 else
707                 {
708                     leftText = "";
709                 }
710                 
711                 if (firstCharLocation == lenght && lenght > 1)
712                 {
713                     // All the instruction is space, replace with an instruction 
714                     // with only one space
715                     if (addleftspace || addrightspace)
716                     {
717                         newText = leftText;
718                     }
719                     else
720                     {
721                         instructionBuffer.remove(i);
722                         i--;
723                         size--;
724                     }
725                 }
726                 else
727                 {
728                     int lastCharLocation = getLastTextCharLocationIgnoringSpacesTabsAndCarriageReturn(text);
729                     // If right space, increment in 1
730                     if (lastCharLocation+1 < text.length())
731                     {
732                         lastCharLocation = lastCharLocation+1;
733                     }
734                     if (firstCharLocation > 0)
735                     {
736                         newText = leftText+
737                             text.substring(firstCharLocation, lastCharLocation+1);
738                     }
739                     else
740                     {
741                         newText = text.substring(firstCharLocation, lastCharLocation+1);
742                     }
743                 }
744                 
745                 if (newText != null)
746                 {
747                     if (instructionType == 1)
748                     {
749                         instructionBuffer.set(i, new LiteralTextInstruction(newText));
750                     }
751                     else if (instructionType == 2)
752                     {
753                         instructionBuffer.set(i, new LiteralNonExcapedTextInstruction(newText));
754                     }
755                 }
756             }
757             addleftspace = false;
758         }
759         return size;
760     }
761 
762     private static int getFirstTextCharLocationIgnoringSpacesTabsAndCarriageReturn(String text)
763     {
764         for (int i = 0; i < text.length(); i++)
765         {
766             if (Character.isWhitespace(text.charAt(i)))
767             {
768                 continue;
769             }
770             else
771             {
772                 return i;
773             }
774         }
775         return text.length();
776     }
777     
778     private static int getLastTextCharLocationIgnoringSpacesTabsAndCarriageReturn(String text)
779     {
780         for (int i = text.length()-1; i >= 0; i--)
781         {
782             if (Character.isWhitespace(text.charAt(i)))
783             {
784                 continue;
785             }
786             else
787             {
788                 return i;
789             }
790         }
791         return 0;
792     }
793 
794     public String toString()
795     {
796         return "TextUnit[" + this.children.size() + "]";
797     }
798     
799     public void addMessage(FacesMessage.Severity severity, String summary, String detail)
800     {
801         this.messages.add(new Object[]{severity, summary, detail});
802     }
803 }