com.sun.jsftemplating.layout.template.TemplateParser Maven / Gradle / Ivy
/*
* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the license at
* https://jsftemplating.dev.java.net/cddl1.html or
* jsftemplating/cddl1.txt.
* See the License for the specific language governing
* permissions and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at jsftemplating/cddl1.txt.
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* you own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Copyright 2006 Sun Microsystems, Inc. All rights reserved.
*/
package com.sun.jsftemplating.layout.template;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import com.sun.jsftemplating.layout.SyntaxException;
import com.sun.jsftemplating.layout.descriptors.handler.OutputTypeManager;
import com.sun.jsftemplating.util.IncludeInputStream;
import com.sun.jsftemplating.util.LogUtil;
/**
* This class is responsible for the actual parsing of a template.
*
* This class is intended to read the template one time. Often it may be
* useful to cache the result as it would be inefficient to reread a
* template multiple times. Templates that are generated from this class
* are intended to be static and safe to share. However, this class
* itself is not thread safe.
*
* @author Ken Paulsen ([email protected])
*/
public class TemplateParser {
/**
* Constructor.
*
* @param url URL
pointing to the template.
*/
public TemplateParser(URL url) {
_url = url;
}
/**
* Constructor which accepts a InputStream
.
*
* @param stream InputStream
for the template.
*/
public TemplateParser(InputStream stream) {
_inputStream = stream;
}
/**
* Accessor for the URL.
*/
public URL getURL() {
return _url;
}
/**
* Accessor for the InputStream
. This either comes from
* the supplied URL
, or simply from the supplied
* InputStream
.
*/
public InputStream getInputStream() throws IOException {
if ((_inputStream == null) && (_url != null)) {
_inputStream = getURL().openStream();
}
return _inputStream;
}
/**
* The init method opens the given URL
pointing to a
* template and prepares to parses it.
*
* @throws IOException
*/
public void open() throws IOException {
if (_reader != null) {
// Generally this should not happen, but just in case... start over
close();
}
// FIXME: It is possible while evaluating the file an #include may need to log a message to the screen! Provide a callback mechanism to do this in a Template-specific way
// Create the reader from the stream
_reader = new BufferedReader(
new InputStreamReader(
new IncludeInputStream(
new BufferedInputStream(getInputStream()))));
// Initialize the queue we will use to push values back
_stack = new Stack();
}
/**
* This method closes the stream if it is open. It doesn't throw an
* exception, instead it logs any exceptions at the CONFIG level.
*/
public void close() {
try {
if (_reader != null) {
_reader.close();
}
} catch (Exception ex) {
if (LogUtil.configEnabled(this)) {
LogUtil.config("Exception while closing stream for url: '"
+ getURL() + "'.", ex);
}
}
}
/**
* This method returns the next character.
*/
public int nextChar() throws IOException {
if (!_stack.empty()) {
// We have values in the queue
return _stack.pop().charValue();
}
return _reader.read();
}
/**
* This method pushes a character on the read queue so that it will
* be read next.
*/
public void unread(int ch) {
_stack.push(new Character((char) ch));
}
/**
* This method reads a "Name Value Pair" from the stream. For the
* purposes of this method, a "Name Value Pair" may look like like one
* of these formats:
*
*
* - keyName="keyValue"
* - keyName='keyValue'
* - "keyValue" (only if defName is supplied)
* - 'keyValue' (only if defName is supplied)
* - keyName=>$attribute{attributeKey}
* - keyName=>$session{sessionKey}
* keyName=>$page{pageSessionKey}
* keyName=>$pageSession{pageSessionKey}
*
*
* In the first two formats, keyName
must consist of
* letters, numbers, or the underscore '_' character.
* keyValue
must be wrapped in single or double quotes.
* The backslash '\' character may be used to escape characters, this
* may be useful if a backslash, single, or double quote exists in
* the string.
*
* The last four formats are only used for mapping return values.
* This is necessary when a handler returns a value so that the value
* can be stored somewhere. keyName
in these cases is
* the name of the return value to map. The value after the dollar
* '$' character (which is either "attribute", "page", "pageSession",
* or "session") specifies the type of storage the value should be
* saved. The value inside the curly braces "{}" specifies the key
* that should be used when saving the value as a request, page, or
* session attribute.
*
* The return value is of type {@link NameValuePair}. This object
* contains the necessary information to interpret this NVP.
*
* @param defName The default name to use if ommitted. If
* null
, no default will be used -- a
* {@link SyntaxException} will be generated.
*
* @return A {@link NameValuePair} object containing the NVP info.
*/
public NameValuePair getNVP(String defName) throws IOException {
return getNVP(defName, true);
}
/**
* This method behaves the same as {@link #getNVP(String)}, however,
* it adds the ability to make quotes around the value optional. This
* is done by passing in false
for
* requireQuotes
. This is used by some special commands
* which only take a single argument with no property name. In this
* case, the value will be read until a '>' is encountered (if
* "/>" is encountered, it will stop before the '/').
*
* Also, in cases where quotes are optional, output NVPs will not be
* allowed. The rationale is that the "=>$...{...}" syntax did
* not require quotes already, and use cases which allow for omitting
* quotes do not use output mappings.
*
* @param defName The default name to use if ommitted. If
* null
, no default will be used -- a
* {@link SyntaxException} will be generated.
*
* @param requireQuotes Flag indicating whether enforce the use of
* quotes or not.
*
* @return A {@link NameValuePair} object containing the NVP info.
*
* @throws {@link SyntaxException} if the syntax is not correct.
*/
public NameValuePair getNVP(String defName, boolean requireQuotes) throws IOException {
return getNVP(defName, requireQuotes, "_.");
}
/**
* This method behaves the same as {@link #getNVP(String, boolean)},
* however, it adds the ability to specify the valid characters which
* may appear in the parameter name (via otherChars
).
*
* @param defName The default name to use if ommitted. If
* null
, no default will be used -- a
* {@link SyntaxException} will be generated.
*
* @param requireQuotes Flag indicating whether enforce the use of
* quotes or not.
*
* @param otherChars Other valid characters.
*
* @return A {@link NameValuePair} object containing the NVP info.
*
* @throws {@link SyntaxException} if the syntax is not correct.
*/
public NameValuePair getNVP(String defName, boolean requireQuotes, String otherChars) throws IOException {
// Read the name
String name = readToken(otherChars);
Object value = null;
// Check for empty name
if ((name.length() == 0) && (defName != null)) {
name = defName; // Use default name
unread('='); // Add '=' character
}
// Skip White Space
skipCommentsAndWhiteSpace(SIMPLE_WHITE_SPACE);
// Ensure next character is '='
int next = nextChar();
if ((next != '=') && (next != ':')) {
if (!requireQuotes && !name.equals(defName)) {
// This is the case where there is no property name and no
// quotes, the whole string is the value.
value = name;
name = defName;
// Add a flag to ensure the next switch goes to the "default" case
unread(next);
unread('f');
} else {
throw new SyntaxException(
"'=' or ':' missing for Name Value Pair: '" + name + "'!");
}
}
// Skip whitespace...
skipCommentsAndWhiteSpace(SIMPLE_WHITE_SPACE);
// Check for '>' character (means we're mapping an output value)
String target = null;
int endingChar = -1;
next = nextChar();
switch (next) {
case '>':
if (!requireQuotes) {
// This means output mappings are not allowed, this must
// be the end of the input (meaning there was no input
// since we're at the beginning also)
unread(next);
value = "";
break;
}
// We are mapping an output value, this should look like:
// keyName => $attribute{attKey}
// keyName => $application{appKey}
// keyName => $session{sessionKey}
// keyName => $pageSession{pageSessionKey}
// First skip any whitespace after the '>'
skipCommentsAndWhiteSpace(SIMPLE_WHITE_SPACE);
// Next Make sure we have a '$' character
next = nextChar();
if (next != '$') {
throw new SyntaxException(
"'$' missing for Name Value Pair named: '" + name
+ "=>'! This NVP appears to be a mapping expression, "
+ "therefor requires a format similar to:\n\t" + name
+ " => $attribute{attKey}\nor:\n\t" + name
+ " => $application{applicationKey}\nor:\n\t" + name
+ " => $session{sessionKey}\nor:\n\t" + name
+ " => $pageSession{pageSessionKey}");
}
// Next look for valid type...
target = readToken();
OutputTypeManager otm = OutputTypeManager.getInstance();
if (otm.getOutputType(null, target) == null) {
throw new SyntaxException(
"Invalid OutputType ('" + target + "') for Name Value "
+ "Pair named: '" + name + "=>$" + target + "{...}'! "
+ "This NVP appears to be a mapping expression, "
+ "therefor requires a format similar to:\n\t" + name
+ " => $attribute{attKey}\nor:\n\t" + name
+ " => $application{applicationKey}\nor:\n\t" + name
+ " => $session{sessionKey}\nor:\n\t" + name
+ " => $pageSession{pageSessionKey}");
}
// Skip whitespace again...
skipCommentsAndWhiteSpace(SIMPLE_WHITE_SPACE);
// Now look for '{'
next = nextChar();
if (next != '{') {
throw new SyntaxException(
"'{' missing for Name Value Pair: '" + name
+ "=>$" + target
+ "'! The format must resemble the following:\n\t"
+ name + " => $" + target + "{key}");
}
endingChar = '}';
break;
case '{':
// NVP w/ a List as its value
value = parseList('}');
break;
case '[':
// NVP w/ an array as its value
value = parseList(']').toArray();
break;
case '"':
case '\'':
// Regular NVP...
// Set the ending character to the same type of quote
endingChar = next;
break;
case 'f':
if ((value != null) && (value.toString().length() > 0)) {
// We have the case where the whole string is the value
// Get the next character so we can fall through w/ it
// to the default case
next = nextChar();
}
// Don't break here, fall through...
default:
// See if we require quotes around the value...
if (!requireQuotes) {
unread(next); // Include "next" when getting the value
// Read the value until '>'
String strVal = readUntil('>', true);
// Unread the '>'
unread('>');
// See if we also need put back a '/'...
if (strVal.endsWith("/")) {
// Remove the '/' and place back in the read buffer
strVal = strVal.substring(0, strVal.length() - 1).trim();
unread('/');
}
value = (value == null) ? strVal : (value.toString() + strVal);
break;
}
// This isn't legal, throw an exception
throw new SyntaxException("Name Value Pair named '"
+ name + "' is missing single or double quotes enclosing "
+ "its value. It must follow one of these formats:\n\t"
+ name + "=\"value\"\nor:\n\t" + name + "='value'");
}
// Read the value
if (endingChar != -1) {
value = readUntil(endingChar, false);
}
// Create the NVP object and return it
return new NameValuePair(name, value, target);
}
/**
* This method processes lists of String values in the format:
*
* "value1", "value2", ...}
*
* The content inside the Strings can be anything. The double quotes
* can also be single quotes. The separators can be spaces, tabs, new
* lines, commas, semi-colons, or colons. The terminating character
* is whatever is passed in for endChar
(shown as '}'
* above).
*/
protected List parseList(int endChar) throws IOException {
List list = new ArrayList();
skipCommentsAndWhiteSpace(SIMPLE_WHITE_SPACE);
int next = nextChar();
while (next != endChar) {
// We should start w/ a single or double quote
if ((next != '\'') && (next != '"')) {
throw new IllegalArgumentException(
"A List or array is missing a single or double quotes "
+ "enclosing one or more of its values. It must "
+ "follow:\n\tname={\"value\", ...}\nor:\n\tname={'value',"
+ "...}\n\n[]'s may be used in place of {}'s to specify "
+ "an array instead of a List.");
}
// Read everything inside the quotes
list.add(readUntil(next, false));
// Skip white space (including the seperators ",:;");
skipCommentsAndWhiteSpace(SIMPLE_WHITE_SPACE + ",:;");
next = nextChar();
}
return list;
}
/**
* This method reads while the stream contains letters, numbers, the
* colon character ':', a dot '.', or the underscore '_' character,
* and returns the result.
*/
public String readToken() throws IOException {
return readToken("_:.");
}
/**
* This method reads while the stream contains letters or numbers and
* returns the result.
*
* It also allows any charcters specified by otherChars
* to be considered as part of the token. This allows tokens with
* additional valid characters to be read. otherChars
* may be null if no additional chars are valid.
*
* @param otherChars Other valid characters.
*/
public String readToken(String otherChars) throws IOException {
if (otherChars == null) {
otherChars = "";
}
StringBuffer buf = new StringBuffer();
int next = nextChar();
while (Character.isLetterOrDigit(next) ||
(otherChars.indexOf(next) != -1)) {
buf.append((char) next);
next = nextChar();
}
unread(next);
// Return the result
return buf.toString();
}
/**
* This method returns a String
of characters from the
* current position in the file until the given character (or end of
* file) is encountered. It will not leave the given character in the
* buffer, so the next character to be read will be the character
* following the given character.
*
* @param skipComments true
to strip comments.
*/
public String readUntil(int endingChar, boolean skipComments) throws IOException {
if (skipComments) {
// In case we start on a comment and should skip it...
skipCommentsAndWhiteSpace("");
}
int tmpch;
int next = nextChar();
StringBuffer buf = new StringBuffer();
while ((next != endingChar) && (next != -1)) {
switch (next) {
case '\'' :
case '\"' :
if ((skipComments) && (next != endingChar)) {
// In this case, we want to make sure no comments are
// skipped when inside a quote
//
// NOTE: Also means endingChar will not be found in
// a quote.
buf.append((char) next);
buf.append(readUntil(next, false));
buf.append((char) next);
} else {
buf.append((char) next);
}
break;
case '#' :
case '/' :
case '<' :
// When reading we want to ignore comments, don't skip
// whitespace, though...
if (skipComments) {
unread(next);
skipCommentsAndWhiteSpace("");
// If same char, read next to prevent infinite loop
// We don't have to go through switch again b/c its
// not the ending char and its not escaped -- so it is
// safe to add.
tmpch = nextChar();
if (next == tmpch) {
buf.append((char) next);
} else {
// We're somewhere different, unread
unread(tmpch);
}
} else {
buf.append((char) next);
}
break;
case '\\' :
// Escape Character...
next = nextChar();
if (next == 'n') {
// Special case, insert a '\n' character.
buf.append('\n');
} else if (next == 't') {
// Special case, insert a '\t' character.
buf.append('\t');
} else if (next != '\n') {
// add the next char unless it's a return char
buf.append((char) next);
}
break;
default:
buf.append((char) next);
break;
}
next = nextChar();
}
// Return the result
return buf.toString();
}
/**
* This method returns a String
of characters from the
* current position in the file until the given String (or end of
* file) is encountered. It will not leave the given String in the
* buffer, so the next character to be read will be the character
* following the given character.
*
* @param endingStr The terminating String
.
* @param skipComments true
to ignore comments.
*/
public String readUntil(String endingStr, boolean skipComments) throws IOException {
// Sanity Check
if ((endingStr == null) || (endingStr.length() == 0)) {
return "";
}
// Break String into characters
char arr[] = endingStr.toCharArray();
int arrlen = arr.length;
StringBuffer buf = new StringBuffer("");
int ch = nextChar(); // Read a char to unread
int idx = 1;
do {
// We didn't find the end, push read values back on queue
unread(ch);
for (int cnt = idx-1; cnt > 0; cnt--) {
unread(arr[cnt]);
}
// Read until the beginning of the end (maybe)
buf.append(readUntil(arr[0], skipComments));
buf.append(arr[0]); // readUntil reads but doesn't return this char
// Check to see if we are at the end
for (idx = 1; idx < arrlen; idx++) {
ch = nextChar();
if (ch != arr[idx]) {
// This is not the end!
break;
}
}
} while ((ch != -1) && (idx < arrlen));
// Append the remaining characters (use idx in case we hit eof)...
for (int cnt = 1; cnt < idx; cnt++) {
buf.append(arr[cnt]);
}
if (arrlen != idx) {
// Didn't find it!
throw new SyntaxException("Unable to find: '" + endingStr
+ "'. Read to EOF and gave up. Read: \n" + buf.toString());
}
// Return the result
return buf.toString();
}
/**
* This method skips the given String of characters (usually used to
* skip white space. The contents of the String that is skipped is
* lost. Often you may wish to skip comments as well, use
* {@link TemplateParser#skipCommentsAndWhiteSpace(String)} in this
* case.
*
* @param skipChars The white space characters to skip.
*
* @see TemplateParser#skipCommentsAndWhiteSpace(String)
*/
public void skipWhiteSpace(String skipChars) throws IOException {
int next = nextChar();
while ((next != -1) && (skipChars.indexOf(next) != -1)) {
// Skip...
next = nextChar();
}
// This will skip one too many
unread(next);
}
/**
* Normally you don't just want to skip white space, you also want to
* skip comments. This method allows you to do that. It skips
* comments of the following types:
*
*
* - // - Comment extends to the rest of the line.
* - # - Comment extends to the rest of the line.
* - /* - Comment extends until closing '*' and '/'.
* - <!-- - Comment extends until closing -->.
*
*
* @param skipChars The white space characters to skip
*
* @see TemplateParser#skipWhiteSpace(String)
*/
public void skipCommentsAndWhiteSpace(String skipChars) throws IOException {
int ch = 0;
while (ch != -1) {
ch = nextChar();
switch (ch) {
case '#' :
// Skip rest of line
readLine();
break;
case '/' :
ch = nextChar();
if (ch == '/') {
// Skip rest of line
readLine();
} else if (ch == '*') {
// Throw away everything until '*' & '/'.
readUntil("*/", false);
} else {
// Not a comment, don't read
unread(ch);
unread('/');
ch = -1; // Exit loop
}
break;
case '<' :
ch = nextChar(); // !
if (ch == '!') {
ch = nextChar(); // -
if (ch == '-') {
ch = nextChar(); // -
if (ch == '-') {
// Ignore HTML-style comment
readUntil("-->", false);
} else {
// Not a comment, probably a mistake... lets
// throw an exception
unread(ch);
unread('-');
unread('!');
unread('<');
throw new IllegalArgumentException("Invalid "
+ "comment! Expected comment to begin "
+ "with \"