All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 \"