/**
* This file Copyright (c) 2005-2008 Aptana, Inc. This program is
* dual-licensed under both the Aptana Public License and the GNU General
* Public license. You may elect to use one or the other of these licenses.
*
* This program is distributed in the hope that it will be useful, but
* AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or
* NONINFRINGEMENT. Redistribution, except as permitted by whichever of
* the GPL or APL you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or modify this
* program under the terms of the GNU General Public License,
* Version 3, as published by the Free Software Foundation. You should
* have received a copy of the GNU General Public License, Version 3 along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Aptana provides a special exception to allow redistribution of this file
* with certain other free and open source software ("FOSS") code and certain additional terms
* pursuant to Section 7 of the GPL. You may view the exception and these
* terms on the web at http://www.aptana.com/legal/gpl/.
*
* 2. For the Aptana Public License (APL), this program and the
* accompanying materials are made available under the terms of the APL
* v1.0 which accompanies this distribution, and is available at
* http://www.aptana.com/legal/apl/.
*
* You may view the GPL, Aptana's exception and additional terms, and the
* APL in the file titled license.html at the root of the corresponding
* plugin containing this source file.
*
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.ide.editor.xml.parsing;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Stack;
import com.aptana.ide.editor.xml.lexing.XMLTokenTypes;
import com.aptana.ide.editor.xml.parsing.nodes.XMLDeclarationNode;
import com.aptana.ide.editor.xml.parsing.nodes.XMLElementNode;
import com.aptana.ide.editor.xml.parsing.nodes.XMLParseNode;
import com.aptana.ide.editor.xml.parsing.nodes.XMLParseNodeTypes;
import com.aptana.ide.lexer.ILexer;
import com.aptana.ide.lexer.Lexeme;
import com.aptana.ide.lexer.LexerException;
import com.aptana.ide.lexer.TokenCategories;
import com.aptana.ide.parsing.ParserInitializationException;
import com.aptana.ide.parsing.nodes.IParseNode;
import com.aptana.ide.parsing.nodes.IParseNodeAttribute;
import com.aptana.ide.parsing.nodes.ParseRootNode;
import com.aptana.ide.parsing.nodes.QuoteType;
/**
* @author Kevin Lindsey
*/
public class XMLParser extends XMLParserBase
{
private static final int[] elementEndSet = new int[] { XMLTokenTypes.GREATER_THAN, XMLTokenTypes.SLASH_GREATER_THAN };
private Stack<IParseNode> _elementStack;
static
{
// make sure all of our sets are sorted so that inSet will work properly
// (that method uses a binary search to test existence of members in the
// set)
Arrays.sort(elementEndSet);
}
/**
* Create a new instance of XMLParser
*
* @throws ParserInitializationException
*/
public XMLParser() throws ParserInitializationException
{
this(XMLMimeType.MimeType);
}
/**
* Create a new instance of XMLParser
* @param mimeType
*
* @throws ParserInitializationException
*/
public XMLParser(String mimeType) throws ParserInitializationException
{
super(mimeType);
this._elementStack = new Stack<IParseNode>();
}
/**
* Close the element that is on the top of the stack
*/
private void closeElement()
{
if (this._currentElement != null)
{
this._currentElement.includeLexemeInRange(this.currentLexeme);
}
if (this._elementStack.size() > 0)
{
this._currentElement = this._elementStack.pop();
}
else
{
this._currentElement = null;
}
}
/**
* createNode
*
* @param type
* @param startingLexeme
* @return HTMLParseNode
*/
private XMLParseNode createNode(int type, Lexeme startingLexeme)
{
return (XMLParseNode) this.getParseNodeFactory().createParseNode(type, startingLexeme);
}
/**
* Push the currently active element onto the stack and set the specified element as the new active element
*
* @param element
*/
private void openElement(XMLElementNode element)
{
// add the new parent as a child of the current parent
if (this._currentElement != null)
{
this._currentElement.appendChild(element);
}
this._elementStack.push(this._currentElement);
this._currentElement = element;
}
/**
* @see com.aptana.ide.parsing.AbstractParser#parseAll(com.aptana.ide.parsing.nodes.IParseNode)
*/
public synchronized void parseAll(IParseNode parentNode) throws LexerException
{
this._elementStack.clear();
this._currentElement = parentNode;
ILexer lexer = this.getLexer();
lexer.setLanguageAndGroup(this.getLanguage(), DEFAULT_GROUP);
//this.advance();
try
{
this.parseText(false);
}
catch (ParseException e)
{
// reset group
lexer.setGroup(DEFAULT_GROUP);
}
while (this.isEOS() == false)
{
try
{
switch (this.currentLexeme.typeIndex)
{
case XMLTokenTypes.CDATA_START:
this.parseCDATASection();
break;
case XMLTokenTypes.COMMENT:
this.parseText(false);
break;
case XMLTokenTypes.DOCTYPE_DECL:
this.parseDocTypeDeclaration();
break;
case XMLTokenTypes.END_TAG:
this.parseEndTag();
break;
case XMLTokenTypes.PI_OPEN:
this.parsePI();
this.parseText(false);
break;
case XMLTokenTypes.START_TAG:
this.parseStartTag();
break;
case XMLTokenTypes.XML_DECL:
this.parseXMLDeclaration();
break;
default:
this.advance();
}
}
catch (ParseException e)
{
// reset group
lexer.setGroup(DEFAULT_GROUP);
}
}
}
/**
* parseException
*
* @throws ParseException
* @throws LexerException
*/
private void parseAttribute() throws ParseException, LexerException
{
// assume we have a valid attribute
String name = this.currentLexeme.getText();
// advance over attribute name
this.assertAndAdvance(XMLTokenTypes.NAME, "error.attribute"); //$NON-NLS-1$
// advance over '='
this.assertAndAdvance(XMLTokenTypes.EQUAL, "error.attribute.equal"); //$NON-NLS-1$
// advance over value
this.assertType(XMLTokenTypes.STRING, "error.attribute.value"); //$NON-NLS-1$
if (this.currentLexeme.getCategoryIndex() != TokenCategories.ERROR)
{
// grab attribute value
String value = this.currentLexeme.getText();
// remove quotes, if needed
char firstChar = value.charAt(0);
int quoteType = QuoteType.NONE;
if (firstChar == '"')
{
value = value.substring(1, value.length() - 1);
quoteType = QuoteType.DOUBLE_QUOTE;
}
else if (firstChar == '\'')
{
value = value.substring(1, value.length() - 1);
quoteType = QuoteType.SINGLE_QUOTE;
}
// add attribute to element node
this._currentElement.setAttribute(name, value);
// set quote type
IParseNodeAttribute attr = this._currentElement.getAttributeNode(name);
attr.setQuoteType(quoteType);
}
this.advance();
}
/**
* parseCDATASection
*
* @throws ParseException
* @throws LexerException
*/
private void parseCDATASection() throws LexerException, ParseException
{
// get lexer
ILexer lexer = this.getLexer();
// switch to cdata-section group
lexer.setGroup(CDATA_SECTION_GROUP);
this.assertAndAdvance(XMLTokenTypes.CDATA_START, "error.cdata"); //$NON-NLS-1$
// grab text
this.assertAndAdvance(XMLTokenTypes.CDATA_END, "error.cdata.close"); //$NON-NLS-1$
}
/**
* parseEndTag
*
* @throws ParseException
* @throws LexerException
*/
private void parseEndTag() throws LexerException, ParseException
{
this.assertAndAdvance(XMLTokenTypes.END_TAG, "error.tag.end"); //$NON-NLS-1$
this.closeElement();
this.parseText(true);
}
/**
* parsePI
*
* @throws LexerException
* @throws ParseException
*/
private void parsePI() throws LexerException, ParseException
{
// get lexer
ILexer lexer = this.getLexer();
// switch to cdata-section group
lexer.setGroup(PROCESSING_INSTRUCTION_GROUP);
this.assertAndAdvance(XMLTokenTypes.PI_OPEN, "error.pi"); //$NON-NLS-1$
// grab text
this.assertAndAdvance(XMLTokenTypes.CDATA_END, "error.pi.close"); //$NON-NLS-1$
}
/**
* parseStartTag
*
* @throws ParseException
* @throws LexerException
*/
private void parseStartTag() throws ParseException, LexerException
{
// make sure we're currently on a start tag
this.assertType(XMLTokenTypes.START_TAG, "error.tag.start"); //$NON-NLS-1$
// create the new element
XMLElementNode element = (XMLElementNode) this.createNode(XMLParseNodeTypes.ELEMENT, this.currentLexeme);
// push the element onto our stack
this.openElement(element);
// advance over beginning of element
this.advance();
// process any attributes
while (this.isEOS() == false && this.inSet(elementEndSet) == false)
{
this.parseAttribute();
}
switch (this.currentLexeme.typeIndex)
{
case XMLTokenTypes.GREATER_THAN:
break;
case XMLTokenTypes.SLASH_GREATER_THAN:
this.closeElement();
break;
default:
throwParseError("error.tag.start.unclosed"); //$NON-NLS-1$
}
// handle possible inner or trailing text
parseText(false);
}
/**
* Parse XML declaration
*
* @throws LexerException
* @throws ParseException
*/
private XMLDeclarationNode parseXMLDeclaration() throws LexerException, ParseException
{
// switch to XML declaration
this.getLexer().setGroup(XML_DECLARATION_GROUP);
XMLDeclarationNode decl = (XMLDeclarationNode) this.createNode(XMLParseNodeTypes.DECLARATION, this.currentLexeme);
// advance over '<?xml'
this.assertAndAdvance(XMLTokenTypes.XML_DECL, "error.xml.declaration"); //$NON-NLS-1$
// set version
decl.setVersion(this.currentLexeme.getText());
// always root
if (this._currentElement instanceof ParseRootNode)
{
this._currentElement.appendChild(decl);
}
// parse declaration
this.assertAndAdvance(XMLTokenTypes.VERSION, "error.xml.declaration.version"); //$NON-NLS-1$
if (this.isType(XMLTokenTypes.ENCODING))
{
decl.setEncoding(this.currentLexeme.getText());
this.advance();
}
if (this.isType(XMLTokenTypes.STANDALONE))
{
decl.setStandalone(this.currentLexeme.getText());
this.advance();
}
this.getLexer().setGroup(DEFAULT_GROUP);
this.assertAndAdvance(XMLTokenTypes.QUESTION_GREATER_THAN, "error.xml.declaration.close"); //$NON-NLS-1$
return decl;
}
}