/*
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 1999 The Apache Software Foundation.  All rights 
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer. 
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:  
 *       "This product includes software developed by the 
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written 
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */ 


package org.apache.jasper.compiler;

import java.io.InputStreamReader;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.Reader;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.File;
import java.util.Hashtable;
import java.util.Vector;
import java.util.Stack;

import org.apache.jasper.Constants;
import org.apache.jasper.JspCompilationContext;


/**
 * JspReader is an input buffer for the JSP parser. It should allow
 * unlimited lookahead and pushback. It also has a bunch of parsing
 * utility methods for understanding htmlesque thingies.
 *
 * @author Anil K. Vijendran
 * @author Anselm Baird-Smith
 * @author Harish Prabandham
 * @author Rajiv Mordani
 * @author Mandar Raje
 */
public class JspReader {
    protected Mark current  = null;
    String master = null;

    Vector sourceFiles = new Vector();
    int currFileId = 0;
    int size = 0;
    
    private JspCompilationContext context;
    
    public String getFile(int fileid) {
        return (String) sourceFiles.elementAt(fileid);
    }

    /**
     * Register a new source file.
     * This method is used to implement file inclusion. Each included file
     * gets a uniq identifier (which is the index in the array of source files).
     * @return The index of the now registered file.
     */
    protected int registerSourceFile(String file) {
        if (sourceFiles.contains(file))
            return -1;
        sourceFiles.addElement(file);
        this.size++;
        return sourceFiles.size() - 1;
    }
    

    /**
     * Unregister the source file.
     * This method is used to implement file inclusion. Each included file
     * gets a uniq identifier (which is the index in the array of source files).
     * @return The index of the now registered file.
     */
    protected int unregisterSourceFile(String file) {
        if (!sourceFiles.contains(file))
            return -1;
        sourceFiles.removeElement(file);
        this.size--;
        return sourceFiles.size() - 1;
    }

    /**
     * Push a new file onto the stack.
     * The name of the file is resolved to a fully qualified filename.
     * @param name The name of the file.
     * @param encoding The optional encoding for the file.
     */
    public void pushFile(String name, String encoding) 
        throws ParseException, FileNotFoundException
    {
        String parent = master == null ?
            null : master.substring(0, master.lastIndexOf('/') + 1);
        boolean isAbsolute = name.startsWith("/");

        if (parent == null || isAbsolute) {
            master = name;
            pushFile(new File(name), encoding);
        } else {
            master = parent + name;
            pushFile(new File(master), encoding);
        }
    }

    /**
     * Push a new file to be parsed onto the stack.
     * @param inputFile The fully qualified path of the file.
     * @param encoding Optional encoding to read the file.
     */
    private void pushFile(File file, String encoding) 
        throws ParseException, FileNotFoundException 
    {
        // Default encoding if needed:
        if (encoding == null) {
            encoding = "8859_1";
            // XXX - longer term, this should really be:
            //   System.getProperty("file.encoding", "8859_1");
            // but this doesn't work right now, so we stick with ASCII
        }

        // Register the file, and read its content:
        String longName = (context == null)
            ? file.getAbsolutePath()
            : context.getRealPath(file.toString());

 	if (longName == null)
 	    throw new FileNotFoundException(file.toString());
 
        int fileid = registerSourceFile(longName);
        
        if (fileid == -1)
            throw new ParseException(Constants.getString("jsp.error.file.already.registered",
                                                         new Object[] { 
                                                             file 
                                                         }));
        currFileId = fileid;
                                     
        InputStreamReader reader = null;
        try {
            if (context == null)
                reader = new InputStreamReader(new FileInputStream(file),
                                               encoding);
            else {
                String fileName = context.getRealPath(file.toString());
                InputStream in = context.getResourceAsStream(file.toString());
                if (in == null)
                    throw new FileNotFoundException(fileName);
                
                try {
                    reader = new InputStreamReader(in, encoding);
                } catch (Throwable ex) {
                    throw new FileNotFoundException(fileName + ": "+ ex.getMessage());
                }
            }
            
            CharArrayWriter caw   = new CharArrayWriter();
            char            buf[] = new char[1024];
            for (int i = 0 ; (i = reader.read(buf)) != -1 ; )
                caw.write(buf, 0, i);
            caw.close();
            if (current == null) {
                current = new Mark( this, caw.toCharArray(), fileid, getFile(fileid),
                                    master, encoding );
            } else {
                current.pushStream( caw.toCharArray(), fileid, getFile(fileid),
                                    master, encoding );
            }

        } catch (FileNotFoundException fnfe) {
            throw fnfe;
        } catch (Throwable ex) {
            Constants.warning("Exception parsing file " + file + ": "+ex);
            // Pop state being constructed:
            popFile();
            throw new ParseException(Constants.getString("jsp.error.file.cannot.read",
                                                        new Object[] { file }));
        } finally {
            if ( reader != null ) {
                try { reader.close(); } catch (Exception any) {}
            }
        }
    }

    public boolean popFile() throws ParseException {
        // Is stack created ? (will happen if the Jsp file we'r looking at is
        // missing.
        if (current == null) 
                return false;

        // Restore parser state:
        //size--;
        String fName = getFile(currFileId);
        currFileId = unregisterSourceFile(fName);
        if (currFileId < -1)
            throw new ParseException
                (Constants.getString("jsp.error.file.not.registered",
                                     new Object[] {fName}));

        boolean r = current.popStream();
        if (r)
                master = current.baseDir;
        return r;
    }
        
    protected JspReader(String file, JspCompilationContext ctx, String encoding) 
        throws ParseException, FileNotFoundException
    {
        this.context = ctx;
        pushFile(file, encoding);
    }

    public static JspReader createJspReader(String file, JspCompilationContext ctx, String encoding) 
        throws ParseException, FileNotFoundException
    {
        return new JspReader(file, ctx, encoding);
    }

    public boolean hasMoreInput() throws ParseException {
        if (current.cursor >= current.stream.length) {
            while (popFile()) {
                if (current.cursor < current.stream.length) return true;
            }
            return false;
        }
        return true;
    }
    
    public int nextChar() throws ParseException {
        if (!hasMoreInput())
            return -1;
        
        int ch = current.stream[current.cursor];

        current.cursor++;
        
        if (ch == '\n') {
            current.line++;
            current.col = 0;
        } else {
            current.col++;
        }
        return ch;
    }

    /**
     * Gets Content until the next potential JSP element.  Because all elements
     * begin with a '&lt;' we can just move until we see the next one.
     */
    String nextContent() {
        int cur_cursor = current.cursor;
        int len = current.stream.length;
 	char ch;

        if (peekChar() == '\n') {
            current.line++;
            current.col = 0;
        }
        else current.col++;
        
        // pure obsfuscated genius!
        while ((++current.cursor < len) && 
            ((ch = current.stream[current.cursor]) != '<')) {

            if (ch == '\n') {
                current.line++;
                current.col = 0;
            } else {
  		current.col++;
            }
        }

        return new String(current.stream, cur_cursor, current.cursor-cur_cursor);
    }

    char[] getChars(Mark start, Mark stop) throws ParseException {
        Mark oldstart = mark();
        reset(start);
        CharArrayWriter caw = new CharArrayWriter();
        while (!stop.equals(mark()))
            caw.write(nextChar());
        caw.close();
        reset(oldstart);
        return caw.toCharArray();
    }

    public int peekChar() {
        return current.stream[current.cursor];
    }

    public Mark mark() {
        return new Mark(current);
    }

    public void reset(Mark mark) {
        current = new Mark(mark);
    }

    public boolean matchesIgnoreCase(String string) throws ParseException {
        Mark mark = mark();
        int ch = 0;
        int i = 0;
        do {
            ch = nextChar();
            if (Character.toLowerCase((char) ch) != string.charAt(i++)) {
                reset(mark);
                return false;
            }
        } while (i < string.length());
        reset(mark);
        return true;
    }

    public boolean matches(String string) throws ParseException {
        Mark mark = mark();
        int ch = 0;
        int i = 0;
        do {
            ch = nextChar();
            if (((char) ch) != string.charAt(i++)) {
                reset(mark);
                return false;
            }
        } while (i < string.length());
        reset(mark);
        return true;
    }
    
    public void advance(int n) throws ParseException {
        while (--n >= 0)
            nextChar();
    }

    public int skipSpaces() throws ParseException {
        int i = 0;
        while (isSpace()) {
            i++;
            nextChar();
        }
        return i;
    }

    /**
     * Skip until the given string is matched in the stream.
     * When returned, the context is positioned past the end of the match.
     * @param s The String to match.
     * @return A non-null <code>Mark</code> instance if found,
     * <strong>null</strong> otherwise.
     */
    public Mark skipUntil(String limit)
    throws ParseException {
        Mark ret = null;
        int limlen = limit.length();
        int ch;
        
    skip:
        for (ret = mark(), ch = nextChar() ; ch != -1 ; ret = mark(), ch = nextChar()) {
            
            if ( ch == limit.charAt(0) ) {
                for (int i = 1 ; i < limlen ; i++) {
                    if (Character.toLowerCase((char) nextChar()) != limit.charAt(i))
                        continue skip;
                }
                return ret;
            }
        }
        return null;
    }
    
    final boolean isSpace() {
        return peekChar() <= ' ';
    }

    /**
     * Parse a space delimited token.
     * If quoted the token will consume all characters up to a matching quote,
     * otherwise, it consumes up to the first delimiter character.
     * @param quoted If <strong>true</strong> accept quoted strings.
     */

    public String parseToken(boolean quoted) 
        throws ParseException
    {
        StringBuffer stringBuffer = new StringBuffer();
        skipSpaces();
        stringBuffer.setLength(0);
        
        int ch = peekChar();
        
        if (quoted) {
            if ( ch == '"' || ch == '\'') {

                char endQuote = ch == '"' ? '"' : '\'';
                // Consume the open quote: 
                ch = nextChar();
                for(ch = nextChar(); ch != -1 && ch != endQuote; ch = nextChar()) {
                    if (ch == '\\') 
                        ch = nextChar();
                    stringBuffer.append((char) ch);
                }
                // Check end of quote, skip closing quote:
                if ( ch == -1 ) 
                    throw new ParseException(mark(), 
                                Constants.getString("jsp.error.quotes.unterminated"));
            }
            else throw new ParseException(mark(),
                                Constants.getString("jsp.error.attr.quoted"));
        } else {
            if (!isDelimiter())
                // Read value until delimiter is found:
                do {
                    ch = nextChar();
                    // Take care of the quoting here.
                    if (ch == '\\') {
                        if (peekChar() == '"' || peekChar() == '\'' ||
                               peekChar() == '>' || peekChar() == '%')
                            ch = nextChar();
                    }
                    stringBuffer.append((char) ch);
                } while ( !isDelimiter() );
        }
        return stringBuffer.toString();
    }

    /**
     * Parse an attribute/value pair, and store it in provided hash table.
     * The attribute/value pair is defined by:
     * <pre>
     * av := spaces token spaces '=' spaces token spaces
     * </pre>
     * Where <em>token</em> is defined by <code>parseToken</code> and
     * <em>spaces</em> is defined by <code>skipSpaces</code>.
     * The name is always considered case insensitive, hence stored in its
     * lower case version.
     * @param into The Hashtable instance to save the result to.
     */

    private void parseAttributeValue(Hashtable into)
        throws ParseException
    {
        // Get the attribute name:
        skipSpaces();
        String name = parseToken(false);
        // Check for an equal sign:
        skipSpaces();
        if ( peekChar() != '=' ) 
            throw new ParseException(mark(), Constants.getString("jsp.error.attr.novalue",
                                                new Object[] { name }));
        char ch = (char) nextChar();
        // Get the attribute value:
        skipSpaces();
        String value = parseToken(true);
        skipSpaces();
        // Add the binding to the provided hashtable:
        into.put(name, value);
        return;
    }

    /**
     * Parse some tag attributes for Beans.
     * The stream is assumed to be positioned right after the tag name. The
     * syntax recognized is:
     * <pre>
     * tag-attrs := empty | attr-list ("&gt;" | "--&gt;" | %&gt;)
     * attr-list := empty | av spaces attr-list
     * empty     := spaces 
     * </pre>
     * Where <em>av</em> is defined by <code>parseAttributeValue</code>.
     * @return A Hashtable mapping String instances (variable names) into
     * String instances (variable values).
     */

    public Hashtable parseTagAttributesBean() 
        throws ParseException
    {
      Hashtable values = new Hashtable(11);
        while ( true ) {
            skipSpaces();
            int ch = peekChar();
            if ( ch == '>' ) {
                // End of the useBean tag.
                return values;

            } else if ( ch == '/' ) {
                Mark mark = mark();
                nextChar();
                // XMLesque Close tags 
                try {
                    if ( nextChar() == '>' )
                      return values;
                } finally {
                    reset(mark);
                }
            }
            if ( ch == -1 )
                break;
            // Parse as an attribute=value:
            parseAttributeValue(values);
        }
        // Reached EOF:
        throw new ParseException(mark(),
                        Constants.getString("jsp.error.tag.attr.unterminated"));
    }

    /**
     * Parse some tag attributes.
     * The stream is assumed to be positioned right after the tag name. The
     * syntax recognized is:
     * <pre>
     * tag-attrs := empty | attr-list ("&gt;" | "--&gt;" | %&gt;)
     * attr-list := empty | av spaces attr-list
     * empty     := spaces 
     * </pre>
     * Where <em>av</em> is defined by <code>parseAttributeValue</code>.
     * @return A Hashtable mapping String instances (variable names) into
     * String instances (variable values).
     */

    public Hashtable parseTagAttributes() 
        throws ParseException
    {
        Hashtable values = new Hashtable(11);
        while ( true ) {
            skipSpaces();
            int ch = peekChar();
            if ( ch == '>' ) {
                return values;
            }
            if ( ch == '-' ) {
                Mark mark = mark();
                nextChar();
                // Close NCSA like attributes "->"
                try {
                    if ( nextChar() == '-' && nextChar() == '>' )
                        return values;
                } finally {
                    reset(mark);
                }
            } else if ( ch == '%' ) {
                Mark mark = mark();
                nextChar();
                // Close variable like attributes "%>"
                try {
                    if ( nextChar() == '>' )
                        return values;
                } finally {
                    reset(mark);
                }
            } else if ( ch == '/' ) {
                Mark mark = mark();
                nextChar();
                // XMLesque Close tags 
                try {
                    if ( nextChar() == '>' )
                        return values;
                } finally {
                    reset(mark);
                }
            }
            if ( ch == -1 )
                break;
            // Parse as an attribute=value:
            parseAttributeValue(values);
        }
        // Reached EOF:
        throw new ParseException(mark(),
                        Constants.getString("jsp.error.tag.attr.unterminated"));
    }

    /**
     * Parse PARAM tag attributes into the given hashtable.
     * Parses the PARAM tag as defined by:
     * <pre>
     * &lt;PARAM tag-attributes %gt;
     * </pre>
     * Two special tag attributes are recognized here:
     * <ol>
     * <li>The <strong>name</strong> attribute,
     * <li>The <strong>value</strong> attribute.
     * </ol>
     * The resulting name, value pair is stored in the provided hash table.
     * @param into Storage for parameter values.
     */
    public void parseParamTag(Hashtable into) 
        throws ParseException
    {
        // Really check for a param tag:
        if ( matches("param") ) {
            advance(6);
            parseParams (into);
        } else {
            // False alarm, just skip it
        }
    }

    /**
     * Parse jsp:param tag attributes into the given hashtable.
     * Parses the jsp:param tag as defined by:
     * <pre>
     * &lt;jsp:param tag-attributes %gt;
     * </pre>
     * Two special tag attributes are recognized here:
     * <ol>
     * <li>The <strong>name</strong> attribute,
     * <li>The <strong>value</strong> attribute.
     * </ol>
     * The resulting name, value pair is stored in the provided hash table.
     * @param into Storage for parameter values.
     */
    public void parsePluginParamTag(Hashtable into) 
        throws ParseException
    {
        // Really check for a param tag:
        if ( matches("<jsp:param") ) {
            advance(11);
            parseParams (into);
        } else {
            // False alarm, just skip it
        }
    }

    private void parseParams (Hashtable into) 
        throws ParseException
    {
        Hashtable attrs = parseTagAttributes();
        // Check attributes (name and value):
        String name  = (String) attrs.get("name");
        String value = (String) attrs.get("value");
        if ( name == null )
 	    throw new ParseException(mark(), Constants.getString("jsp.error.param.noname"));
        if ( value == null )
 	    throw new ParseException(mark(), Constants.getString("jsp.error.param.novalue"));
        // Put that new binding into the params hashatble:
        String oldval[] = (String[]) into.get(name);
        if ( oldval == null ) {
            String newval[] = new String[1];
            newval[0] = value;
            into.put(name, newval);
        } else {
            String newval[] = new String[oldval.length+1];
            System.arraycopy(oldval, 0, newval, 0, oldval.length);
            newval[oldval.length] = value;
            into.put(name, newval);
        }
    }

    /**
     * Parse utils - Is current character a token delimiter ?
     * Delimiters are currently defined to be =, &gt;, &lt;, ", and ' or any
     * any space character as defined by <code>isSpace</code>.
     * @return A boolean.
     */
    private boolean isDelimiter() throws ParseException {
        if ( ! isSpace() ) {
            int ch = peekChar();
            // Look for a single-char work delimiter:
            if ( ch == '=' || ch == '>' || ch == '"' || ch == '\'' || ch == '/') 
                return true;
            // Look for an end-of-comment or end-of-tag:		
            if ( ch == '-' ) {
                Mark mark = mark();
                if ( ((ch = nextChar()) == '>')
                     || ((ch == '-') && (nextChar() == '>')) ) {
                    reset(mark);
                    return true;
                } else {
                    reset(mark);
                    return false;
                }
            }
            return false;
        } else {
            return true;
        }
    }
}

