blob: a25f200d893e990af0bbb8a86ea987e347feefe8 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.myfaces.view.facelets.compiler;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import javax.el.ELException;
import javax.faces.application.FacesMessage;
import javax.faces.view.facelets.CompositeFaceletHandler;
import javax.faces.view.facelets.FaceletHandler;
import javax.faces.view.facelets.Tag;
import javax.faces.view.facelets.TagAttribute;
import javax.faces.view.facelets.TagException;
import org.apache.myfaces.renderkit.html.util.HTML;
import org.apache.myfaces.view.facelets.el.ELText;
/**
*
* @author Jacob Hookom
* @version $Id$
*/
final class TextUnit extends CompilationUnit
{
private final StringBuffer buffer;
private final StringBuffer textBuffer;
private final List<Instruction> instructionBuffer;
private final Stack<Tag> tags;
private final List<Object> children;
private boolean startTagOpen;
private final String alias;
private final String id;
private final List<Object> messages;
private final boolean escapeInlineText;
private final boolean compressSpaces;
public TextUnit(String alias, String id)
{
this(alias,id,true);
}
public TextUnit(String alias, String id, boolean escapeInlineText)
{
this(alias,id,escapeInlineText,false);
}
public TextUnit(String alias, String id, boolean escapeInlineText, boolean compressSpaces)
{
this.alias = alias;
this.id = id;
this.buffer = new StringBuffer();
this.textBuffer = new StringBuffer();
this.instructionBuffer = new ArrayList<Instruction>();
this.tags = new Stack<Tag>();
this.children = new ArrayList<Object>();
this.startTagOpen = false;
this.messages = new ArrayList<Object>(4);
this.escapeInlineText = escapeInlineText;
this.compressSpaces = compressSpaces;
}
@Override
public FaceletHandler createFaceletHandler()
{
this.flushBufferToConfig(true);
if (this.children.isEmpty())
{
return LEAF;
}
FaceletHandler[] h = new FaceletHandler[this.children.size()];
Object obj;
for (int i = 0; i < h.length; i++)
{
obj = this.children.get(i);
if (obj instanceof FaceletHandler)
{
h[i] = (FaceletHandler) obj;
}
else
{
h[i] = ((CompilationUnit) obj).createFaceletHandler();
}
}
if (h.length == 1)
{
return h[0];
}
return new CompositeFaceletHandler(h);
}
private void addInstruction(Instruction instruction)
{
this.flushTextBuffer(false);
this.instructionBuffer.add(instruction);
}
private void flushTextBuffer(boolean child)
{
if (this.textBuffer.length() > 0)
{
String s = this.textBuffer.toString();
if (child)
{
s = trimRight(s);
}
if (s.length() > 0)
{
if (!compressSpaces)
{
//Do it as usual.
ELText txt = ELText.parse(s);
if (txt != null)
{
if (txt.isLiteral())
{
if (escapeInlineText)
{
this.instructionBuffer.add(new LiteralTextInstruction(txt.toString()));
}
else
{
this.instructionBuffer.add(new LiteralNonExcapedTextInstruction(txt.toString()));
}
}
else
{
if (escapeInlineText)
{
this.instructionBuffer.add(new TextInstruction(this.alias, txt ));
}
else
{
// When escape inline text is disabled (jspx case) we have to split the EL and add
// separate instructions, so it can be properly escaped.
ELText[] splitText = ELText.parseAsArray(s);
if (splitText.length > 1)
{
Instruction[] array = new Instruction[splitText.length];
for (int i = 0; i < splitText.length; i++)
{
ELText selText = splitText[i];
if (selText.isLiteral())
{
array[i] = new LiteralNonExcapedTextInstruction(selText.toString());
}
else
{
array[i] = new TextInstruction(this.alias, selText );
}
}
this.instructionBuffer.add(new CompositeTextInstruction(array));
}
else
{
this.instructionBuffer.add(new TextInstruction(this.alias, txt ));
}
}
}
}
}
else
{
// First check if the text contains EL before build something, and if contains
// an EL expression, compress it before build the ELText.
if (s != null && s.length() > 0)
{
if (ELText.isLiteral(s))
{
if (escapeInlineText)
{
this.instructionBuffer.add(new LiteralTextInstruction(s));
}
else
{
this.instructionBuffer.add(new LiteralNonExcapedTextInstruction(s));
}
}
else
{
if (instructionBuffer.size() > 0 &&
!(instructionBuffer.get(instructionBuffer.size()-1) instanceof LiteralXMLInstruction))
{
s = compressELText(s);
}
// When escape inline text is disabled (jspx case) we have to split the EL and add
// separate instructions, so it can be properly escaped.
ELText[] splitText = ELText.parseAsArray(s);
if (splitText.length > 1)
{
Instruction[] array = new Instruction[splitText.length];
for (int i = 0; i < splitText.length; i++)
{
ELText selText = splitText[i];
if (selText.isLiteral())
{
array[i] = new LiteralNonExcapedTextInstruction(selText.toString());
}
else
{
array[i] = new TextInstruction(this.alias, selText );
}
}
this.instructionBuffer.add(new CompositeTextInstruction(array));
}
else
{
this.instructionBuffer.add(new TextInstruction(this.alias, ELText.parse(s)));
}
}
}
}
}
}
this.textBuffer.setLength(0);
}
public void write(String text)
{
this.finishStartTag();
this.textBuffer.append(text);
this.buffer.append(text);
}
public void writeInstruction(String text)
{
this.finishStartTag();
ELText el = ELText.parse(text);
if (el.isLiteral())
{
this.addInstruction(new LiteralXMLInstruction(text));
}
else
{
this.addInstruction(new XMLInstruction(el));
}
this.buffer.append(text);
}
public void writeComment(String text)
{
this.finishStartTag();
ELText el = ELText.parse(text);
if (el.isLiteral())
{
this.addInstruction(new LiteralCommentInstruction(text));
}
else
{
this.addInstruction(new CommentInstruction(el));
}
this.buffer.append("<!--").append(text).append("-->");
}
public void startTag(Tag tag)
{
// finish any previously written tags
this.finishStartTag();
// push this tag onto the stack
this.tags.push(tag);
// write it out
this.buffer.append('<');
this.buffer.append(tag.getQName());
this.addInstruction(new StartElementInstruction(tag.getQName()));
TagAttribute[] attrs = tag.getAttributes().getAll();
if (attrs.length > 0)
{
for (TagAttribute attr : attrs)
{
String qname = attr.getQName();
String value = attr.getValue();
this.buffer.append(' ').append(qname).append("=\"").append(value).append('"');
ELText txt = ELText.parseAllowEmptyString(value);
if (txt != null)
{
if (txt.isLiteral())
{
this.addInstruction(new LiteralAttributeInstruction(qname, txt.toString()));
}
else
{
this.addInstruction(new AttributeInstruction(this.alias, qname, txt));
}
}
}
}
if (!messages.isEmpty())
{
for (Iterator<Object> it = messages.iterator(); it.hasNext();)
{
Object[] message = (Object[])it.next();
this.addInstruction(new AddFacesMessageInstruction((FacesMessage.Severity) message[0],
(String)message[1], (String)message[2]));
it.remove();
}
}
// notify that we have an open tag
this.startTagOpen = true;
}
private void finishStartTag()
{
if (this.tags.size() > 0 && this.startTagOpen)
{
this.buffer.append('>');
this.startTagOpen = false;
}
}
public void endTag()
{
Tag tag = (Tag) this.tags.pop();
if (HTML.BODY_ELEM.equalsIgnoreCase(tag.getQName()))
{
this.addInstruction(new BodyEndElementInstruction(tag.getQName()));
}
else
{
this.addInstruction(new EndElementInstruction(tag.getQName()));
}
if (this.startTagOpen)
{
this.buffer.append("/>");
this.startTagOpen = false;
}
else
{
this.buffer.append("</").append(tag.getQName()).append('>');
}
}
@Override
public void addChild(CompilationUnit unit)
{
// if we are adding some other kind of unit
// then we need to capture our buffer into a UITextHandler
this.finishStartTag();
this.flushBufferToConfig(true);
this.children.add(unit);
}
protected void flushBufferToConfig(boolean child)
{
this.flushTextBuffer(child);
int size = this.instructionBuffer.size();
if (size > 0)
{
try
{
String s = this.buffer.toString();
if (child)
{
s = trimRight(s);
}
ELText txt = ELText.parse(s);
if (txt != null)
{
if (compressSpaces)
{
// Use the logic behind the instructions to remove unnecessary instructions
// containing only spaces, or recreating new ones containing only the necessary
// spaces.
size = compressSpaces(instructionBuffer, size);
}
Instruction[] instructions = (Instruction[]) this.instructionBuffer
.toArray(new Instruction[size]);
this.children.add(new UIInstructionHandler(this.alias, this.id, instructions, txt));
this.instructionBuffer.clear();
}
}
catch (ELException e)
{
if (this.tags.size() > 0)
{
throw new TagException((Tag) this.tags.peek(), e.getMessage());
}
else
{
throw new ELException(this.alias + ": " + e.getMessage(), e.getCause());
}
}
}
// ALWAYS CLEAR FOR BOTH IMPL
this.buffer.setLength(0);
}
public boolean isClosed()
{
return this.tags.empty();
}
private final static String trimRight(String s)
{
int i = s.length() - 1;
while (i >= 0 && Character.isWhitespace(s.charAt(i)))
{
i--;
}
if (i >= 0)
{
return s;
}
else
{
return "";
}
/*
if (i == s.length() - 1)
{
return s;
}
else
{
return s.substring(0, i + 1);
}*/
}
final static String compressELText(String text)
{
//int firstCharLocation = getFirstTextCharLocationIgnoringSpacesTabsAndCarriageReturn(text);
int firstCharLocation = -1;
int leftChar = 0; // 0=first char on left 1=\n 2=\r 3=\r\n
int lenght = text.length();
String leftText = null;
for (int j = 0; j < lenght; j++)
{
char c = text.charAt(j);
if (leftChar == 0)
{
if (c == '\r')
{
leftChar = 2;
if (j+1 < lenght)
{
if (text.charAt(j+1) == '\n')
{
leftChar = 3;
}
}
}
if (c == '\n')
{
leftChar = 1;
}
}
if (Character.isWhitespace(c))
{
continue;
}
else
{
firstCharLocation = j;
break;
}
}
if (firstCharLocation == -1)
{
firstCharLocation = lenght;
}
// Define the character on the left
if (firstCharLocation > 0)
{
switch (leftChar)
{
case 1:
leftText = "\n";
break;
case 2:
leftText = "\r";
break;
case 3:
leftText = "\r\n";
break;
default:
leftText = (lenght > 1) ? text.substring(0,1) : text;
break;
}
}
else
{
leftText = "";
}
int lastCharLocation = getLastTextCharLocationIgnoringSpacesTabsAndCarriageReturn(text);
if (firstCharLocation == 0 && lastCharLocation == text.length()-1)
{
return text;
}
else
{
if (lastCharLocation+1 < text.length())
{
lastCharLocation = lastCharLocation+1;
}
if (firstCharLocation == 0)
{
return text.substring(firstCharLocation, lastCharLocation+1);
}
else
{
return leftText+text.substring(firstCharLocation, lastCharLocation+1);
}
}
}
/**
* Compress spaces around a list of instructions, following these rules:
*
* - The first instruction that is on the left usually make contact with a component.
*
* @param instructionBuffer
* @param size
* @return
*/
final static int compressSpaces(List<Instruction> instructionBuffer, int size)
{
boolean addleftspace = true;
boolean addrightspace = false;
boolean skipnext = false;
for (int i = 0; i < size; i++)
{
String text = null;
String newText = null;
int instructionType = 0;
if (skipnext)
{
skipnext = false;
continue;
}
Instruction ins = instructionBuffer.get(i);
if (i+1 == size)
{
addrightspace = true;
}
if (ins instanceof LiteralTextInstruction)
{
text = ((LiteralTextInstruction)ins).getText();
instructionType = 1;
}
else if (ins instanceof LiteralNonExcapedTextInstruction)
{
text = ((LiteralTextInstruction)ins).getText();
instructionType = 2;
}
else if (ins instanceof LiteralXMLInstruction)
{
skipnext = true;
continue;
}
if (text != null && text.length() > 0)
{
int firstCharLocation = -1;
int leftChar = 0; // 0=first char on left 1=\n 2=\r 3=\r\n
int lenght = text.length();
String leftText = null;
for (int j = 0; j < lenght; j++)
{
char c = text.charAt(j);
if (leftChar == 0)
{
if (c == '\r')
{
leftChar = 2;
if (j+1 < lenght)
{
if (text.charAt(j+1) == '\n')
{
leftChar = 3;
}
}
}
if (c == '\n')
{
leftChar = 1;
}
}
if (Character.isWhitespace(c))
{
continue;
}
else
{
firstCharLocation = j;
break;
}
}
if (firstCharLocation == -1)
{
firstCharLocation = lenght;
}
// Define the character on the left
if (firstCharLocation > 0)
{
switch (leftChar)
{
case 1:
leftText = "\n";
break;
case 2:
leftText = "\r";
break;
case 3:
leftText = "\r\n";
break;
default:
leftText = (lenght > 1) ? text.substring(0,1) : text;
break;
}
}
else
{
leftText = "";
}
if (firstCharLocation == lenght && lenght > 1)
{
// All the instruction is space, replace with an instruction
// with only one space
if (addleftspace || addrightspace)
{
newText = leftText;
}
else
{
instructionBuffer.remove(i);
i--;
size--;
}
}
else
{
int lastCharLocation = getLastTextCharLocationIgnoringSpacesTabsAndCarriageReturn(text);
// If right space, increment in 1
if (lastCharLocation+1 < text.length())
{
lastCharLocation = lastCharLocation+1;
}
if (firstCharLocation > 0)
{
newText = leftText+
text.substring(firstCharLocation, lastCharLocation+1);
}
else
{
newText = text.substring(firstCharLocation, lastCharLocation+1);
}
}
if (newText != null)
{
if (instructionType == 1)
{
instructionBuffer.set(i, new LiteralTextInstruction(newText));
}
else if (instructionType == 2)
{
instructionBuffer.set(i, new LiteralNonExcapedTextInstruction(newText));
}
}
}
addleftspace = false;
}
return size;
}
private static int getLastTextCharLocationIgnoringSpacesTabsAndCarriageReturn(String text)
{
for (int i = text.length()-1; i >= 0; i--)
{
if (Character.isWhitespace(text.charAt(i)))
{
continue;
}
else
{
return i;
}
}
return 0;
}
@Override
public String toString()
{
return "TextUnit[" + this.children.size() + ']';
}
public void addMessage(FacesMessage.Severity severity, String summary, String detail)
{
this.messages.add(new Object[]{severity, summary, detail});
}
}