org.fife.rsta.ac.xml.XmlParser Maven / Gradle / Ivy
/*
* 04/07/2012
*
* Copyright (C) 2012 Robert Futrell
* robert_futrell at users.sourceforge.net
* http://fifesoft.com/rsyntaxtextarea
*
* This library is distributed under a modified BSD license. See the included
* RSTALanguageSupport.License.txt file for details.
*/
package org.fife.rsta.ac.xml;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.Segment;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import org.fife.io.DocumentReader;
import org.fife.rsta.ac.xml.tree.XmlTreeNode;
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
import org.fife.ui.rsyntaxtextarea.parser.AbstractParser;
import org.fife.ui.rsyntaxtextarea.parser.DefaultParseResult;
import org.fife.ui.rsyntaxtextarea.parser.DefaultParserNotice;
import org.fife.ui.rsyntaxtextarea.parser.ParseResult;
import org.fife.ui.rsyntaxtextarea.parser.ParserNotice;
/**
* Parses XML code in an RSyntaxTextArea
.
*
* Like all RSTA Parser
s, an XmlParser
instance is
* notified when the RSTA's text content changes. After a small delay, it will
* parse the content as XML, building an AST and looking for any errors. When
* parsing is complete, a property change event of type {@link #PROPERTY_AST}
* is fired. Listeners can check the new value of the property for an
* {@link XmlTreeNode} that represents the root of a tree structure modeling
* the XML content in the text area. Note that the XmlTreeNode
* may be incomplete if there were parsing/syntax errors (it will usually be
* complete "up to" the error in the content).
*
* This parser cannot be shared amongst multiple instances of
* RSyntaxTextArea
.
*
* @author Robert Futrell
* @version 1.0
*/
public class XmlParser extends AbstractParser {
/**
* The property change event that's fired when the document is re-parsed.
* Applications can listen for this property change and update themselves
* accordingly. The property's "new value" will be an {@link XmlTreeNode}
* representing the root of a tree modeling the XML content. The "old
* value" is always null
.
*/
public static final String PROPERTY_AST = "XmlAST";
private XmlLanguageSupport xls;
private PropertyChangeSupport support;
private XmlTreeNode curElem;
private XmlTreeNode root;
private Locator locator;
private SAXParserFactory spf;
private SAXParser sp;
private ValidationConfig validationConfig;
public XmlParser(XmlLanguageSupport xls) {
this.xls = xls;
support = new PropertyChangeSupport(this);
try {
spf = SAXParserFactory.newInstance();
} catch (FactoryConfigurationError fce) {
fce.printStackTrace();
}
}
/**
* Adds a listener to this parser. Typically you'd want to register a
* listener on the {@link #PROPERTY_AST} property.
*
* @param prop The property to listen for changes on.
* @param l The listener itself.
* @see #removePropertyChangeListener(String, PropertyChangeListener)
*/
public void addPropertyChangeListener(String prop, PropertyChangeListener l) {
support.addPropertyChangeListener(prop, l);
}
/**
* Returns the XML model from the last time it was parsed.
*
* @return The root node of the XML model, or null
if it has
* not yet been parsed or an error occurred while parsing.
*/
public XmlTreeNode getAst() {
return root;
}
/**
* Returns a string representing the "main" attribute for an element.
*
* @param attributes The attributes of an element. Calling code should
* have already verified this has length > 0.
* @return The "main" attribute.
*/
private String getMainAttribute(Attributes attributes) {
int nameIndex = -1;
int idIndex = -1;
for (int i=0; inull
, no validation will be done.
*/
public void setValidationConfig(ValidationConfig config) {
this.validationConfig = config;
if (validationConfig!=null) {
validationConfig.configureParser(this);
}
sp = null; // Force recreation for possible new validation params
}
/**
* Callback for events when we're parsing the XML in the editor. Creates
* our model and records any parsing errors for squiggle underlining.
*/
public class Handler extends DefaultHandler {
private DefaultParseResult result;
private RSyntaxDocument doc;
private Segment s;
private EntityResolver entityResolver;
public Handler(RSyntaxDocument doc, DefaultParseResult result) {
this.doc = doc;
this.result = result;
s = new Segment();
}
private void doError(SAXParseException e, ParserNotice.Level level) {
if (!xls.getShowSyntaxErrors()) {
return;
}
int line = e.getLineNumber() - 1;
Element root = doc.getDefaultRootElement();
Element elem = root.getElement(line);
int offs = elem.getStartOffset();
int len = elem.getEndOffset() - offs;
if (line==root.getElementCount()-1) {
len++;
}
DefaultParserNotice pn = new DefaultParserNotice(XmlParser.this,
e.getMessage(), line, offs, len);
pn.setLevel(level);
result.addNotice(pn);
//System.err.println(">>> " + offs + "-" + len + " -> "+ pn);
}
@Override
public void endElement(String uri, String localName, String qName) {
curElem = (XmlTreeNode)curElem.getParent();
}
@Override
public void error(SAXParseException e) throws SAXException {
doError(e, ParserNotice.Level.ERROR);
}
@Override
public void fatalError(SAXParseException e) throws SAXException {
doError(e, ParserNotice.Level.ERROR);
}
private int getTagStart(int end) {
Element root = doc.getDefaultRootElement();
int line = root.getElementIndex(end);
Element elem = root.getElement(line);
int start = elem.getStartOffset();
int lastCharOffs = -1;
try {
while (line>=0) {
doc.getText(start, end-start, s);
for (int i=s.offset+s.count-1; i>=s.offset; i--) {
char ch = s.array[i];
if (ch=='<') {
return lastCharOffs;
}
else if (Character.isLetterOrDigit(ch)) {
lastCharOffs = start + i - s.offset;
}
}
if (--line>=0) {
elem = root.getElement(line);
start = elem.getStartOffset();
end = elem.getEndOffset();
}
}
} catch (BadLocationException ble) {
ble.printStackTrace();
}
return -1;
}
@Override
public InputSource resolveEntity(String publicId, String systemId)
throws IOException, SAXException {
if (entityResolver!=null) {
return entityResolver.resolveEntity(publicId, systemId);
}
// Override default behavior and return empty DTD contents
return new InputSource(new java.io.StringReader(" "));
//return super.resolveEntity(publicId, systemId);
}
@Override
public void setDocumentLocator(Locator l) {
locator = l;
}
public void setEntityResolver(EntityResolver resolver) {
this.entityResolver = resolver;
}
@Override
public void startElement(String uri, String localName, String qName,
Attributes attributes) {
XmlTreeNode newElem = new XmlTreeNode(qName);
if (attributes.getLength()>0) {
newElem.setMainAttribute(getMainAttribute(attributes));
}
if (locator!=null) {
int line = locator.getLineNumber();
if (line!=-1) {
int offs = doc.getDefaultRootElement().
getElement(line-1).getStartOffset();
int col = locator.getColumnNumber();
if (col!=-1) {
offs += col - 1;
}
// "offs" is now the end of the tag. Find the beginning of it.
offs = getTagStart(offs);
try {
newElem.setStartOffset(doc.createPosition(offs));
int endOffs = offs + qName.length();
newElem.setEndOffset(doc.createPosition(endOffs));
} catch (BadLocationException ble) {
ble.printStackTrace();
}
}
}
curElem.add(newElem);
curElem = newElem;
}
@Override
public void warning(SAXParseException e) throws SAXException {
doError(e, ParserNotice.Level.WARNING);
}
}
}