| /* |
| JSPWiki - a JSP-based WikiWiki clone. |
| |
| 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.wiki.ui.migrator; |
| |
| import java.util.*; |
| |
| /** |
| * Parser that reads JSP document and constructs a {@link JspDocument} with the |
| * results. |
| */ |
| public class JspParser |
| { |
| private static final Parser TEXT_PARSER = new TextParser(); |
| |
| private static final Parser TAG_PARSER = new TagParser(); |
| |
| private static final AttributeParser ATTRIBUTE_PARSER = new AttributeParser(); |
| |
| private static final DynamicAttributeParser DYNAMIC_ATTRIBUTE_PARSER = new DynamicAttributeParser(); |
| |
| /** |
| * Parses dynamic attributes in a Tag declaration. |
| */ |
| public static class DynamicAttributeParser implements Parser |
| { |
| /** |
| * {@inheritDoc} |
| */ |
| public void beginStage() |
| { |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void endStage() |
| { |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void handle( char ch ) |
| { |
| ParseContext ctx = ParseContext.currentContext(); |
| int leftAngleBrackets = 1; |
| boolean increment = false; |
| do |
| { |
| // Get character at current position |
| // Increment position |
| if ( increment ) |
| { |
| ctx.incrementPosition(); |
| } |
| ch = ctx.getSource().charAt( ctx.position() ); |
| |
| switch (ch) |
| { |
| case ('<'): |
| { |
| leftAngleBrackets++; |
| break; |
| } |
| case ('>'): |
| { |
| leftAngleBrackets--; |
| break; |
| } |
| default: |
| { |
| // Ignore any other character |
| } |
| increment = true; |
| } |
| |
| } while ( leftAngleBrackets != 0 ); |
| |
| // Set the end position, name and value. |
| Attribute attribute = (Attribute)ctx.getNode(); |
| ctx.setEndPosition( attribute ); |
| attribute.setType( NodeType.DYNAMIC_ATTRIBUTE ); |
| attribute.setValue( ctx.getSource().substring( ctx.getMarkerForStage( Stage.CODE_OR_COMMENT), attribute.getEnd() ) ); |
| |
| // Add to parent |
| Tag parent = (Tag)ctx.getParentContext().getNode(); |
| parent.addAttribute( attribute ); |
| |
| // Pop the ParseContext and exit |
| ParseContext.pop(); |
| } |
| } |
| |
| public static class AttributeParser implements Parser |
| { |
| /** |
| * {@inheritDoc} |
| */ |
| public void beginStage() |
| { |
| ParseContext ctx = ParseContext.currentContext(); |
| Node attribute = new Attribute( ctx.getDocument() ); |
| ctx.setNode( attribute ); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void endStage() |
| { |
| ParseContext ctx = ParseContext.currentContext(); |
| Node attribute = ctx.getNode(); |
| attribute.setEnd( ctx.position() ); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void handle( char ch ) |
| { |
| ParseContext ctx = ParseContext.currentContext(); |
| int pos = ctx.position(); |
| |
| switch( ctx.getStage() ) |
| { |
| // Characters that name an attribute, after whitespace but |
| // before equals (=). |
| case NAME: { |
| if( ch == '=' ) |
| { |
| // Set the attribute name |
| Attribute attribute = (Attribute) ctx.getNode(); |
| attribute.setName( ctx.getSource().substring( attribute.getStart(), pos ) ); |
| |
| // The next character will be a single/double quote |
| ctx.incrementPosition(); |
| char delimiter = ctx.getSource().charAt( ctx.position() ); |
| attribute.setAttributeDelimiter( delimiter ); |
| |
| // Start a new ParseContext, and Text node in position |
| // after quote |
| ctx = ParseContext.push(); |
| ctx.setStartPosition( ctx.getNode(), ctx.position() + 1 ); |
| } |
| break; |
| } |
| |
| case ATTRIBUTE_END: { |
| // Add the finished attribute to the parent |
| Attribute attribute = (Attribute) ctx.getNode(); |
| Tag parent = (Tag) attribute.getParent(); |
| parent.addAttribute( attribute ); |
| |
| // Retrieve the Tag ParseContext & fire the handler method |
| ctx = ParseContext.pop(); |
| Parser parser = ctx.getParser(); |
| parser.handle( ch ); |
| break; |
| } |
| } |
| } |
| |
| } |
| |
| public static class TagParser implements Parser |
| { |
| /** |
| * Factory method that initializes a supplied Node. When initialized, |
| * the node's start position, line number, column number and level are |
| * set automatically based on JspDocument's internal cache of |
| * line-breaks and nodes. Note that the new node is not actually added |
| * to the internal node tree until the method |
| * {@link #finalizeNode(Node, int)} is called. |
| * |
| * @param type the node type |
| * @param the stage to set at the end of the initialization |
| */ |
| private void initNode( Node node, Stage stage ) |
| { |
| ParseContext ctx = ParseContext.currentContext(); |
| ctx.setNode( node ); |
| |
| // Skip ahead until we get to the first name character |
| int pos = ctx.position(); |
| int increment = node.getType().getTagStart().length(); |
| String source = ctx.getSource(); |
| while ( pos + increment < source.length() && Character.isWhitespace( source.charAt( pos + increment ) ) ) |
| { |
| increment++; |
| } |
| |
| // Walk backwards one position |
| increment--; |
| |
| for( int i = 0; i < increment; i++ ) |
| { |
| ctx.incrementPosition(); |
| } |
| |
| // Set the new stage and mark it at the next character |
| ctx.setStage( stage, 1 ); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void beginStage() |
| { |
| ParseContext ctx = ParseContext.currentContext(); |
| |
| // Figure out what this tag is |
| String lookahead = ctx.lookahead( 10 ); |
| JspDocument doc = ctx.getDocument(); |
| |
| // <%- means hidden JSP comment |
| if( lookahead.startsWith( NodeType.JSP_COMMENT.getTagStart() ) ) |
| { |
| initNode( new Text( doc, NodeType.JSP_COMMENT ), Stage.CODE_OR_COMMENT ); |
| } |
| |
| // <%! means JSP declaration |
| else if( lookahead.startsWith( NodeType.JSP_DECLARATION.getTagStart() ) ) |
| { |
| initNode( new Text( doc, NodeType.JSP_DECLARATION ), Stage.CODE_OR_COMMENT ); |
| } |
| |
| // <%= means JSP expression |
| else if( lookahead.startsWith( NodeType.JSP_EXPRESSION.getTagStart() ) ) |
| { |
| initNode( new Text( doc, NodeType.JSP_EXPRESSION ), Stage.CODE_OR_COMMENT ); |
| } |
| |
| // <%@ + optional space means JSP directive |
| else if( lookahead.startsWith( NodeType.JSP_DIRECTIVE.getTagStart() ) ) |
| { |
| initNode( new Tag( doc, NodeType.JSP_DIRECTIVE ), Stage.NAME ); |
| } |
| |
| // <!-- means HTML comment |
| else if( lookahead.startsWith( NodeType.COMMENT.getTagStart() ) ) |
| { |
| initNode( new Text( doc, NodeType.COMMENT ), Stage.CODE_OR_COMMENT ); |
| } |
| |
| // Whitespace after <% means |
| // scriptlet |
| else if( lookahead.startsWith( NodeType.SCRIPTLET.getTagStart() ) ) |
| { |
| if( lookahead.length() >= 3 && Character.isWhitespace( lookahead.charAt( 2 ) ) ) |
| { |
| initNode( new Text( doc, NodeType.SCRIPTLET ), Stage.CODE_OR_COMMENT ); |
| } |
| } |
| |
| // <? means declaration, such as XML declaration |
| else if ( lookahead.startsWith( NodeType.DECLARATION.getTagStart() ) ) |
| { |
| initNode( new Tag( doc, NodeType.DECLARATION ), Stage.NAME ); |
| } |
| |
| // <!DOCTYPE means DOCTYPE |
| else if ( lookahead.startsWith( NodeType.DOCTYPE.getTagStart() ) ) |
| { |
| initNode( new Text( doc, NodeType.DOCTYPE ), Stage.CODE_OR_COMMENT ); |
| } |
| |
| // <![CDATA[ means CDATA |
| else if( lookahead.startsWith( NodeType.CDATA.getTagStart() ) ) |
| { |
| initNode( new Text( doc, NodeType.CDATA ), Stage.CODE_OR_COMMENT ); |
| } |
| |
| // If </, it's an HTML end tag |
| else if( lookahead.startsWith( NodeType.END_TAG.getTagStart() ) ) |
| { |
| initNode( new Tag( doc, NodeType.END_TAG ), Stage.NAME ); |
| } |
| |
| // Any other char means its HTML start tag |
| // or combined tag |
| else |
| { |
| initNode( new Tag( doc, NodeType.UNRESOLVED_TAG ), Stage.NAME ); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void endStage() |
| { |
| ParseContext ctx = ParseContext.currentContext(); |
| Node node = ctx.getNode(); |
| |
| if( node != null ) |
| { |
| // Set the end position |
| ctx.setEndPosition( node ); |
| |
| // If node length is > 0, add it to the parent |
| if( node.getEnd() > node.getStart() ) |
| { |
| node.getParent().addChild( node ); |
| } |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void handle( char ch ) |
| { |
| ParseContext ctx = ParseContext.currentContext(); |
| Node node = ctx.getNode(); |
| |
| switch( ctx.getStage() ) |
| { |
| // Characters that supply the tag name; immediately after < |
| case NAME: { |
| switch( ch ) |
| { |
| // Whitespace == end of the name |
| case (' '): { |
| int nameStart = ctx.getMarkerForStage( Stage.NAME ); |
| int nameEnd = ctx.position(); |
| node.setName( ctx.getSource().substring( nameStart, nameEnd ) ); |
| ctx.setStage( Stage.WHITESPACE, 0 ); |
| break; |
| } |
| |
| // Right angle bracket == end of the node |
| case ('>'): { |
| handleTagEnd(); |
| break; |
| } |
| } |
| break; |
| } |
| |
| // Whitespace between <%! and name, or between attributes. |
| case WHITESPACE: { |
| switch( ch ) |
| { |
| case ('/'): |
| case (' '): |
| case ('?'): |
| case ('%'): { |
| break; |
| } |
| |
| // It's the start of a new dynamic attribute |
| case ('<'): { |
| ctx = ParseContext.push(); |
| ctx.setParser( DYNAMIC_ATTRIBUTE_PARSER, Stage.CODE_OR_COMMENT, 0 ); |
| ctx.setNode( new Attribute( ctx.getDocument() ) ); |
| break; |
| } |
| |
| case ('>'): { |
| handleTagEnd(); |
| break; |
| } |
| |
| default: { |
| // It's the start of a new attribute |
| ctx = ParseContext.push(); |
| ctx.setParser( ATTRIBUTE_PARSER, Stage.NAME, 0 ); |
| } |
| } |
| break; |
| } |
| |
| case CODE_OR_COMMENT: { |
| switch( ch ) |
| { |
| // Terminating %>, > or --> means the end of the comment (or DOCTYPE) |
| case ('>'): { |
| String tagEnd = node.getType().getTagEnd(); |
| String lookbehind = ctx.lookbehind( tagEnd.length() ); |
| if( lookbehind.equals( tagEnd ) ) |
| { |
| // Set the end position |
| node.setEnd( ctx.position() + 1 ); |
| |
| // Set the value |
| NodeType type = node.getType(); |
| node.setValue( ctx.getSource().substring( node.getStart() + type.getTagStart().length(), |
| node.getEnd() - type.getTagEnd().length() ) ); |
| |
| ctx.setParser( TEXT_PARSER, Stage.TEXT, 1 ); |
| } |
| break; |
| } |
| } |
| break; |
| } |
| } |
| } |
| |
| /** |
| * <p> |
| * Finishes up an HTML or JSP tag when > is detected by the handler |
| * method {@link #handle(char)}, but before {@link #endStage()} |
| * executes. Because HTML parsing can be messy, this method resolves |
| * several edge cases: |
| * </p> |
| * <ul> |
| * <li>If the tag's type has not been determined (that is, its current |
| * type is {@link NodeType#UNRESOLVED_TAG}, its type is resolved |
| * to either {@link NodeType#EMPTY_ELEMENT_TAG} or |
| * {@link NodeType#START_TAG}, depending on whether the last |
| * character was /. |
| * <li> |
| * <li>If the tag's name has not been set, because the parser has not |
| * encountered whitespace that delimits attributes, its name is set.</li> |
| * <li>If the tag is of type {@link NodeType#START_TAG} or |
| * {@link NodeType#START_TAG}, the current ParseContext is pushed |
| * on, or popped off, of the stack. In addition, if the tag is an end |
| * tag, its parent is re-wired to the same parent as the start tag. This |
| * makes the start and end tag logical peers, as they should be.</li> |
| * </ul> |
| * <p> |
| * After these cases are resolved, the current node is re-set to a Text |
| * node, in the character position starting after the right angle |
| * bracket (>). |
| * </p> |
| */ |
| private void handleTagEnd() |
| { |
| ParseContext ctx = ParseContext.currentContext(); |
| Node node = ctx.getNode(); |
| |
| // Special case if we have a META or LINK tag |
| if ( "LINK".equals( node.getName() ) ) |
| { |
| node.setType( NodeType.LINK ); |
| } |
| else if ( "META".equals( node.getName() ) ) |
| { |
| node.setType( NodeType.META ); |
| } |
| |
| // Resolve tag type if not set |
| if( node.getType() == NodeType.UNRESOLVED_TAG ) |
| { |
| String lookbehind = ctx.lookbehind( 2 ); |
| if( NodeType.EMPTY_ELEMENT_TAG.getTagEnd().equals( lookbehind ) ) |
| { |
| node.setType( NodeType.EMPTY_ELEMENT_TAG ); |
| } |
| else |
| { |
| node.setType( NodeType.START_TAG ); |
| } |
| } |
| |
| // Set the name if not set |
| if( node.getName() == null ) |
| { |
| int nameStart = ctx.getMarkerForStage( Stage.NAME ); |
| int nameEnd = ctx.position() + 1 - node.getType().getTagEnd().length(); |
| node.setName( ctx.getSource().substring( nameStart, nameEnd ) ); |
| } |
| |
| // If start or end tag, push/pop as needed; |
| // otherwise start new Text |
| switch( node.getType() ) |
| { |
| case START_TAG: { |
| // Add the start tag to parent, and push it onto stack |
| endStage(); |
| ctx = ParseContext.push(); |
| break; |
| } |
| case END_TAG: { |
| // Make end tag the peer of the start tag |
| Node startTag = node.getParent(); |
| node.setParent( startTag.getParent() ); |
| |
| // Does the end tag match the start tag? |
| if ( !node.getName().equals( startTag.getName() ) ) |
| { |
| if ( startTag.equals( ctx.getDocument().getRoot() ) ) |
| { |
| throw new IllegalStateException( "Encountered end tag </" |
| + node.getName() + "> (" + node.getLine() + "," + node.getColumn() + " char " + node.getStart() |
| + ") that did have a matching start tag." ); |
| } |
| throw new IllegalStateException( "Encountered end tag </" |
| + node.getName() + "> (" + node.getLine() + "," + node.getColumn() + " char " + node.getStart() |
| + ") that did not match start tag <" |
| + startTag.getName() + "> (" + startTag.getLine() + "," + startTag.getColumn() + " char " + startTag.getEnd() + ")" |
| ); |
| } |
| |
| // Get rid of the current node in the parent context |
| ctx = ParseContext.pop(); |
| ctx.setNode( null ); |
| break; |
| } |
| } |
| |
| // Start new Text node after the > |
| ctx.setParser( TEXT_PARSER, Stage.TEXT, 1 ); |
| } |
| } |
| |
| public interface Parser |
| { |
| public abstract void beginStage(); |
| |
| public abstract void endStage(); |
| |
| public abstract void handle( char ch ); |
| } |
| |
| public static class TextParser implements Parser |
| { |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void beginStage() |
| { |
| // Create Text node and set start at next character |
| ParseContext ctx = ParseContext.currentContext(); |
| Text text = new Text( ctx.getDocument() ); |
| ctx.setNode( text ); |
| ctx.setStartPosition( ctx.getNode(), ctx.position() + 1 ); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void endStage() |
| { |
| // Finalize current node |
| ParseContext ctx = ParseContext.currentContext(); |
| if( ctx.position() > 0 ) |
| { |
| Node node = ctx.getNode(); |
| |
| // Set the end position |
| node.setEnd( ctx.position() ); |
| |
| // Set the node value |
| node.setValue( ctx.getSource().substring( node.getStart(), node.getEnd() ) ); |
| |
| // If node length is > 0, add it to the parent |
| if( node.getEnd() > node.getStart() ) |
| { |
| node.getParent().addChild( node ); |
| } |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void handle( char ch ) |
| { |
| ParseContext ctx = ParseContext.currentContext(); |
| switch( ctx.getStage() ) |
| { |
| case TEXT: { |
| switch( ch ) |
| { |
| // If we see a quote, check to see if it's a part of a |
| // parent attribute |
| case ('\''): |
| case ('"'): { |
| Node node = ctx.getNode(); |
| Node parent = node.getParent(); |
| if( parent instanceof Attribute ) |
| { |
| Attribute attribute = (Attribute) parent; |
| if( ch == attribute.getAttributeDelimiter() ) |
| { |
| ctx = ParseContext.pop(); |
| ctx.setStage( Stage.ATTRIBUTE_END, 0 ); |
| } |
| } |
| break; |
| } |
| case ('<'): { |
| // Valid XML or JSP tag start? |
| String lookahead = ctx.lookahead( 2 ); |
| if( lookahead.length() == 2 ) |
| { |
| char nextChar = lookahead.charAt( 1 ); |
| if( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!/%_:?".indexOf( nextChar ) != -1 ) |
| { |
| ctx.setParser( TAG_PARSER, Stage.TAG_START, 0 ); |
| } |
| } |
| break; |
| } |
| } |
| break; |
| } |
| } |
| |
| } |
| |
| } |
| |
| private static final class Counter |
| { |
| private int m_pos; |
| |
| private final List<Integer> lineBreaks = new ArrayList<Integer>(); |
| |
| Counter() |
| { |
| m_pos = 0; |
| } |
| |
| public void increment() |
| { |
| m_pos++; |
| } |
| |
| public int position() |
| { |
| return m_pos; |
| } |
| } |
| |
| private enum Stage |
| { |
| /** |
| * Parsing the left-angle bracket that starts the node (<). |
| */ |
| TAG_START, |
| /** Parsing any text outside of a tag or element. */ |
| TEXT, |
| /** |
| * Characters that name a node. For tags, the node name begins after the |
| * opening left bracket (<) and ends before the first whitespace |
| * character. For attributes, the name begins after any whitespace and |
| * ends before the equals (=) character. |
| */ |
| NAME, |
| /** |
| * Whitespace between the node or directive name and the first |
| * attribute, and between attributes. |
| */ |
| WHITESPACE, |
| /** Any text inside of a scriptlet, JSP comment, or JSP declaration. */ |
| CODE_OR_COMMENT, |
| /** Finishing an attribute. */ |
| ATTRIBUTE_END |
| } |
| |
| /** |
| * Encapsulates the current state of document parsing. |
| */ |
| private static class ParseContext |
| { |
| private static final Stack<ParseContext> CONTEXT_STACK = new Stack<ParseContext>(); |
| |
| private static ParseContext CURRENT_CONTEXT; |
| |
| public static ParseContext currentContext() |
| { |
| return CURRENT_CONTEXT; |
| } |
| |
| public static ParseContext initialContext( JspDocument doc, String source ) |
| { |
| CONTEXT_STACK.clear(); |
| ParseContext context = new ParseContext( doc, source, new Counter() ); |
| CURRENT_CONTEXT = context; |
| |
| // Set the default stage to TEXT |
| Text text = new Text( doc ); |
| context.setNode( text ); |
| context.setStartPosition( text, 0 ); |
| context.m_stage = Stage.TEXT; |
| context.m_parser = TEXT_PARSER; |
| |
| // Return the init'ed context |
| return context; |
| } |
| |
| private Parser m_parser = null; |
| |
| private Node m_node = null; |
| |
| private final Counter m_counter; |
| |
| private final String m_source; |
| |
| private Stage m_stage = Stage.TEXT; |
| |
| private JspDocument m_doc; |
| |
| private Map<Stage, Integer> m_markers = new HashMap<Stage, Integer>(); |
| |
| private ParseContext( JspDocument doc, String source, Counter counter ) |
| { |
| super(); |
| m_doc = doc; |
| m_source = source; |
| m_counter = counter; |
| } |
| |
| /** |
| * Returns an arbitrary number of characters ahead of the current |
| * position, starting with the current character. For example, if the |
| * current position returned by {@link #position()} is 70, |
| * <code>lookahead(3)</code> returns the characters at positions 70, |
| * 71 and 72. If the supplied length causes the lookahead position to |
| * exceed the length of the source string, the substring is truncated |
| * appropriately. |
| * |
| * @param length the number of characters to return |
| * @return the substring |
| */ |
| public String lookahead( int length ) |
| { |
| int startPos = position(); |
| int endPos = startPos + length > m_source.length() ? endPos = m_source.length() : startPos + length; |
| return m_source.substring( startPos, endPos ); |
| } |
| |
| /** |
| * Returns an arbitrary number of characters behind the current |
| * position, starting with the current character. For example, if the |
| * current position returned by {@link #position()} is 70, |
| * <code>lookbehind(3)</code> returns the characters at positions 68, |
| * 69 and 70. If the supplied length causes the lookbehind position to |
| * exceed the length of the source string, the substring is truncated |
| * appropriately. |
| * |
| * @param length the number of characters to return |
| * @return the substring |
| */ |
| public String lookbehind( int length ) |
| { |
| int endPos = position() + 1; |
| int startPos = endPos - length < 0 ? 0 : endPos - length; |
| return m_source.substring( startPos, endPos ); |
| } |
| |
| public Counter getCounter() |
| { |
| return m_counter; |
| } |
| |
| public JspDocument getDocument() |
| { |
| return m_doc; |
| } |
| |
| /** |
| * Retrieves the position of the marker set for the current stage (as |
| * set by {@link #mark()}). If no marker was set, this method returns |
| * {@link Node#POSITION_NOT_SET}. |
| * |
| * @param stage the stage for which the marker position is desired |
| * @return the position of the marker. |
| */ |
| public int getMarkerForStage( Stage stage ) |
| { |
| Integer mark = m_markers.get( stage ); |
| return mark == null ? Node.POSITION_NOT_SET : mark.intValue(); |
| } |
| |
| public Node getNode() |
| { |
| return m_node; |
| } |
| |
| /** |
| * Sets the active Parser without changing the stage or incrementing the |
| * position. Calling this method does <em>not</em> execute either the |
| * {@link Parser#beginStage()} or {@link Parser#endStage()} methods. |
| * |
| * @param parser the parser to set |
| */ |
| public void setParser( Parser parser ) |
| { |
| m_parser = parser; |
| } |
| |
| public ParseContext getParentContext() |
| { |
| return CONTEXT_STACK.size() == 0 ? null : CONTEXT_STACK.peek(); |
| } |
| |
| public Parser getParser() |
| { |
| return m_parser; |
| } |
| |
| public String getSource() |
| { |
| return m_source; |
| } |
| |
| public Stage getStage() |
| { |
| return m_stage; |
| } |
| |
| /** |
| * Returns <code>true</code> if one or more ParseContexts have been |
| * pushed onto the stack (via {@link #push()}); <code>false</code> |
| * otherwise. |
| * |
| * @return the result |
| */ |
| public boolean hasParentContext() |
| { |
| return CONTEXT_STACK.size() > 0; |
| } |
| |
| public void incrementPosition() |
| { |
| // Reset the line/column counters if we encounter linebreaks |
| int pos = position(); |
| char currentChar = m_source.charAt( pos ); |
| char nextChar = ( pos +1 ) < m_source.length() ? m_source.charAt( pos + 1 ) : currentChar; |
| if( currentChar == '\r' && nextChar == '\n' ) |
| { |
| m_counter.lineBreaks.add( pos ); |
| m_counter.increment(); |
| } |
| else if ( currentChar == '\r' || currentChar == '\n' ) |
| { |
| m_counter.lineBreaks.add( pos ); |
| } |
| m_counter.increment(); |
| |
| } |
| |
| /** |
| * Pops the topmost ParseContext from the stack and replaces the |
| * JspParser's current ParseContext with it. Before popping the |
| * ParseContext, the current stage's {@link Parser#endStage()} method is |
| * executed. |
| */ |
| public static ParseContext pop() |
| { |
| // Run the endStage method for the current stage |
| ParseContext ctx = ParseContext.currentContext(); |
| ctx.getParser().endStage(); |
| |
| ctx = CONTEXT_STACK.pop(); |
| CURRENT_CONTEXT = ctx; |
| return ctx; |
| } |
| |
| public int position() |
| { |
| return m_counter.position(); |
| } |
| |
| /** |
| * Pushes this ParseContext onto the stack and replaces the JspParser's |
| * current ParseContext with a new one. This method does <em>not</em> |
| * call the current Parser's {@link Parser#endStage()} method. |
| * |
| * @return the new ParseContext |
| */ |
| public static ParseContext push() |
| { |
| ParseContext oldCtx = currentContext(); |
| CONTEXT_STACK.push( oldCtx ); |
| ParseContext ctx = new ParseContext( oldCtx.m_doc, oldCtx.m_source, oldCtx.m_counter ); |
| CURRENT_CONTEXT = ctx; |
| |
| // Set the default stage to TEXT |
| Node text = new Text( ctx.m_doc ); |
| ctx.setNode( text ); |
| ctx.m_stage = Stage.TEXT; |
| ctx.m_parser = TEXT_PARSER; |
| return ctx; |
| } |
| |
| /** |
| * Sets the current Node to a supplied node. If the start position is |
| * not set, it set to the current position. If not already set, the |
| * parent is set to the parent ParseContext's node, or else the |
| * JspDocument's root node. |
| * |
| * @param node the node to set. If <code>null</code>, the current |
| * node is removed |
| */ |
| public void setNode( Node node ) |
| { |
| m_node = node; |
| if( node == null ) |
| { |
| return; |
| } |
| |
| // If start not set, set it now |
| if( node.getStart() == Node.POSITION_NOT_SET ) |
| { |
| setStartPosition( node, position() ); |
| } |
| |
| // Set the parent relationship |
| if( node.getParent() == null ) |
| { |
| if( getParentContext() == null ) |
| { |
| node.setParent( m_doc.getRoot() ); |
| } |
| else |
| { |
| node.setParent( getParentContext().getNode() ); |
| } |
| } |
| } |
| |
| /** |
| * Sets the current lifecycle stage and mark its position, without |
| * resetting the current parser. The "marker" is set relative to the |
| * current position (as reported by {@link #position()}. The marker can |
| * be retrieved later via |
| * {@link #getMarkerForStage(org.apache.wiki.ui.stripes.JspParser.Stage)}. |
| * |
| * @param stage |
| * @param increment the number of characters ahead of the current |
| * position to set the marker |
| */ |
| public void setStage( Stage stage, int increment ) |
| { |
| // Set the new stage. |
| m_stage = stage; |
| m_markers.put( getStage(), position() + increment ); |
| } |
| |
| /** |
| * <p> |
| * Ends the current {@link Stage} and starts another, and sets a marker |
| * for the next character position. When this method is called, the |
| * active {@link Parser} is finalized for the previous stage by calling |
| * its {@link Parser#endStage()} method. Then, the current text parser |
| * is replaced with the one that corresponds to the correct one for the |
| * new stage, and a marker is set at the next position. Finally, the new |
| * stage is initialized by calling the new parser's |
| * {@link Parser #beginStage()} method. |
| * </p> |
| * |
| * @param parser the parser to set |
| * @param stage the stage to set |
| * @increment the number of characters ahead to set the marker for the |
| * new stage. Must be zero or higher. |
| */ |
| public void setParser( Parser parser, Stage stage, int increment ) |
| { |
| // Finish the parser's current stage |
| if( parser != null && m_parser != null ) |
| { |
| m_parser.endStage(); |
| } |
| |
| // Set the new stage and set a marker at the next position |
| m_stage = stage; |
| m_markers.put( stage, position() + increment ); |
| |
| // Replace the parser and start it up |
| if( parser != null ) |
| { |
| m_parser = parser; |
| m_parser.beginStage(); |
| } |
| } |
| |
| /** |
| * Sets the start line/column positions for a supplied node, based on |
| * the position in the ParseContext. |
| * |
| * @param node the node to set |
| * @param the position to set |
| */ |
| private void setStartPosition( Node node, int pos ) |
| { |
| // Set the start, end, linebreak |
| node.setStart( pos ); |
| int lastLineBreakPos = m_counter.lineBreaks.size() == 0 ? Node.POSITION_NOT_SET : m_counter.lineBreaks.get( m_counter.lineBreaks.size() - 1 ); |
| node.setLine( m_counter.lineBreaks.size() + 1 ); |
| node.setColumn( pos - lastLineBreakPos ); |
| } |
| |
| /** |
| * Sets the end position for a supplied node, based on the current |
| * position in the ParseContext plus one. |
| * |
| * @param node to set |
| */ |
| private void setEndPosition( Node node ) |
| { |
| ParseContext ctx = ParseContext.currentContext(); |
| int pos = ctx.position() + 1; |
| if( pos > ctx.getSource().length() ) |
| { |
| pos = ctx.getSource().length(); |
| } |
| node.setEnd( pos ); |
| } |
| } |
| |
| /** |
| * Constructs a new JspDocument. |
| */ |
| public JspParser() |
| { |
| super(); |
| } |
| |
| /** |
| * Parses a JSP file, supplied as a String, into Nodes. |
| * |
| * @param source the JSP file contents |
| */ |
| public JspDocument parse( String source ) |
| { |
| // Initialize the cached document, m_source, and stack variables |
| JspDocument doc = new JspDocument(); |
| |
| // Create new parse context and put it on the stack |
| ParseContext ctx = ParseContext.initialContext( doc, source ); |
| |
| // Parse the file, character by character |
| int pos = ctx.position(); |
| while ( pos < source.length() ) |
| { |
| ctx = ParseContext.currentContext(); |
| char currentChar = source.charAt( pos ); |
| |
| // Is the current character whitespace? |
| boolean isWhitespace = Character.isWhitespace( currentChar ); |
| char ch = isWhitespace ? ' ' : currentChar; // For case statements |
| |
| // Handle the current character |
| Parser parser = ctx.getParser(); |
| parser.handle( ch ); |
| |
| // Increment the character position |
| ctx.incrementPosition(); |
| pos = ctx.position(); |
| } |
| |
| // Finalize the last node and return the parsed JSP |
| Node node = ctx.getNode(); |
| node.setEnd( ctx.position() ); |
| if( node.getType() == NodeType.TEXT ) |
| { |
| ctx.getParser().endStage(); |
| } |
| |
| return doc; |
| } |
| |
| } |