/**
* Copyright (c) 2010 DITA for Publishers
*/
package net.sourceforge.dita4publishers.util.xml;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.commons.logging.Log;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.ErrorHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.ext.DefaultHandler2;
import org.xml.sax.ext.Locator2Impl;
import org.xml.sax.helpers.AttributesImpl;
/**
* Handles errors and maintains location information for the current element
* context in order be able to report it with the error reports. The validation
* report is an XML document.
*/
public class ContextTrackingErrorHandler extends DefaultHandler2 implements
ContentHandler, ErrorHandler {
/**
*
*/
public static final int CONTENT_TRUNCATION_LENGTH = 60;
/**
*
*/
public class ParsingContext {
private String uri;
private String localName;
private String qName;
private Attributes atts;
private StringBuilder charbuf = new StringBuilder();
private Locator locator;
private List<ParsingContext> children = new ArrayList<ParsingContext>();
private List<SAXParseException> pendingErrors = new ArrayList<SAXParseException>();
/**
* @param uri
* @param localName
* @param qName
* @param atts
* @param locator
*/
public ParsingContext(String uri, String localName, String qName,
Attributes atts, Locator locator) {
this.uri = uri;
this.localName = localName;
this.qName = qName;
this.atts = new AttributesImpl(atts);
this.locator = locator;
}
/**
*
*/
public ParsingContext() {
}
/**
* @param ch
*/
public void append(char[] ch) {
this.charbuf.append(ch);
}
/**
* @return
*/
public int getLineNumber() {
if (this.locator != null)
return this.locator.getLineNumber();
return -1;
}
/**
* @return
*/
public int getColumnNumber() {
if (this.locator != null)
return this.locator.getColumnNumber();
return -1;
}
/**
* @return
*/
public String getLocalName() {
return this.localName;
}
/**
* @return
*/
public Attributes getAttributes() {
return this.atts;
}
/**
* @return
*/
public String getCharacters() {
return this.charbuf.toString();
}
/**
* @param c
*/
public void append(char c) {
this.charbuf.append(c);
}
/**
* @param context
*/
public void addChild(ParsingContext context) {
this.children .add(context);
}
/**
* @return
*/
public List<ParsingContext> getChildren() {
return this.children;
}
/**
* @param pendingErrors
*/
public void setPendingErrors(List<SAXParseException> pendingErrors) {
this.pendingErrors .addAll(pendingErrors);
}
/**
* @return
*/
public List<SAXParseException> getPendingErrors() {
return this.pendingErrors;
}
}
private Log log;
private StringBuilder charbuf = new StringBuilder();
private Stack<ParsingContext> contextStack = new Stack<ParsingContext>();
private ParsingContext context = new ParsingContext();
private Locator locator;
private boolean isValid = true;
private Document validationReport;
private Element currentElement;
// Holds errors issued before the start tag is handled.
private List<SAXParseException> pendingErrors = new ArrayList<SAXParseException>();
/**
* @param log
* @throws Exception
*/
public ContextTrackingErrorHandler(Log log) throws Exception {
if (log.isDebugEnabled())
log.debug("Constructing new ContextTrackingErrorHandler with a log");
this.log = log;
this.validationReport = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
this.currentElement = this.validationReport.createElement("validationReport");
this.validationReport.appendChild(currentElement);
}
public void characters(char[] ch, int start, int length) throws SAXException {
if (log.isDebugEnabled())
log.debug("characters(): " + start + ", " + length);
for (int i = start;i < start + length;i++) {
context.append(ch[i]);
}
}
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
if (log.isDebugEnabled())
log.debug("startElement(): " + localName + ", qName=" + qName);
contextStack.push(this.context);
Locator locator = new Locator2Impl(this.locator);
this.context = new ParsingContext(uri, localName, qName, atts, locator);
contextStack.peek().addChild(this.context);
if (this.pendingErrors.size() > 0) {
this.context.setPendingErrors(this.pendingErrors);
this.pendingErrors.clear();
}
}
public void endElement(String uri, String localName, String qName) throws SAXException{
if (log.isDebugEnabled())
log.debug("endElement(): " + localName + ", qName=" + qName);
for (SAXParseException e : this.context.getPendingErrors()) {
reportParseException(e, e.getLineNumber(), e.getColumnNumber(), localName);
}
this.context = contextStack.pop();
}
public void setDocumentLocator(Locator locator) {
if (log.isDebugEnabled())
log.debug("setDocumentLocator(): " + locator);
this.locator = locator;
}
public void warning(SAXParseException e) throws SAXException {
log.warn(context.getLineNumber() + ":" + context.getColumnNumber() + " - " + e.getMessage());
// FIXME: Construct context element in result DOM.
}
public void error(SAXParseException e) throws SAXException {
int lineNum = context.getLineNumber();
int colNum = context.getColumnNumber();
if (lineNum < 1) lineNum = e.getLineNumber();
if (colNum < 1) colNum = e.getColumnNumber();
log.error(lineNum + ":" + colNum + " - " + e.getMessage());
String localName = context.getLocalName();
this.isValid = false;
if (localName == null) {
// Must be an issue with the document element's
// start tag.
this.pendingErrors .add(e);
return;
}
reportParseException(e, lineNum, colNum, localName);
}
protected void reportParseException(SAXParseException e, int lineNum,
int colNum, String localName) {
Element elem = createErrorElement(e, lineNum, colNum);
// FIXME: Ignoring namespaces for now since DITA doesn't use namespaces except
// for foreign elements.
Element child = this.validationReport.createElement("context");
elem.appendChild(child);
elem = child;
child = this.validationReport.createElement(localName);
elem.appendChild(child);
Attributes atts = context.getAttributes();
for (int i = 0; i < atts.getLength(); i++) {
child.setAttribute(atts.getLocalName(i), atts.getValue(i));
}
String chars = this.context.getCharacters();
if (chars.length() > CONTENT_TRUNCATION_LENGTH)
chars = chars.substring(0, CONTENT_TRUNCATION_LENGTH) + "...";
child.setTextContent(chars);
for (ParsingContext childContext : this.context.getChildren()) {
Element subChild = this.validationReport.createElement(childContext.getLocalName());
String content = childContext.getCharacters();
if (content.length() > CONTENT_TRUNCATION_LENGTH)
content = content.substring(0, CONTENT_TRUNCATION_LENGTH) + "...";
subChild.setTextContent(content);
child.appendChild(subChild);
}
}
protected Element createErrorElement(SAXParseException e, int lineNum,
int colNum) {
Element elem = this.validationReport.createElement("error");
elem.setAttribute("line", String.valueOf(lineNum));
elem.setAttribute("col", String.valueOf(colNum));
elem.setAttribute("message", e.getMessage());
elem.setAttribute("docuri", e.getSystemId());
this.currentElement.appendChild(elem);
return elem;
}
public void fatalError(SAXParseException e) throws SAXException {
int lineNum = context.getLineNumber();
int colNum = context.getColumnNumber();
if (lineNum < 1) lineNum = e.getLineNumber();
if (colNum < 1) colNum = e.getColumnNumber();
log.error(lineNum + ":" + colNum + " - " + e.getMessage());
this.isValid = false;
if (context.getLocalName() == null) {
// Must be an error in the prolog
Element elem = createErrorElement(e, lineNum, colNum);
Element child = this.validationReport.createElement("context");
elem.appendChild(child);
elem = child;
child = this.validationReport.createElement("errorInProlog");
child.setTextContent("Error is before the root element start tag (in the document prolog) or after the end tag");
elem.appendChild(child);
} else {
reportParseException(e, lineNum, colNum, context.getLocalName());
}
}
public boolean isValid() {
return this.isValid;
}
/**
* @return
*/
public Document getValidationReportDocument() {
return this.validationReport;
}
}