package net.rubygrapefruit.docs.docbook; import net.rubygrapefruit.docs.model.Component; import net.rubygrapefruit.docs.model.buildable.*; import net.rubygrapefruit.docs.parser.Parser; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.io.File; import java.io.Reader; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; /** * Builds a document for some Docbook input. */ public class DocbookParser extends Parser { private final NoOpElementHandler noOpElementHandler = new NoOpElementHandler(); private final LinkHandlerFactory linkHandlerFactory = new LinkHandlerFactory(); private final UlinkHandlerFactory ulinkHandlerFactory = new UlinkHandlerFactory(); private final InlineHandlerFactory inlineHandlerFactory = new InlineHandlerFactory(); private final BlockHandlerFactory blockHandlerFactory = new BlockHandlerFactory(); private final XrefHandler xrefHandler = new XrefHandler(); private final BookHandler bookHandler = new BookHandler(); private final TitleHandler titleHandler = new TitleHandler(); private final BookInfoHandler bookInfoHandler = new BookInfoHandler(); private final PartHandler partHandler = new PartHandler(); private final ChapterHandler chapterHandler = new ChapterHandler(); private final AppendixHandler appendixHandler = new AppendixHandler(); private final ParaHandler paraHandler = new ParaHandler(); private final SectionHandler sectionHandler = new SectionHandler(); private final CodeHandler codeHandler = new CodeHandler(); private final LiteralHandler literalHandler = new LiteralHandler(); private final EmphasisHandler emphasisHandler = new EmphasisHandler(); private final ClassNameHandler classNameHandler = new ClassNameHandler(); private final ExampleHandler exampleHandler = new ExampleHandler(); @Override protected void doParse(Reader input, String fileName, BuildableDocument document) throws Exception { final LinkedList<ElementHandler> handlerStack = new LinkedList<ElementHandler>(); final LinkedList<String> elementNameStack = new LinkedList<String>(); int pos = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf(File.separatorChar)); if (pos >= 0) { fileName = fileName.substring(pos + 1); } final DefaultContext context = new DefaultContext(fileName); context.push(document); handlerStack.add(new RootHandler()); SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); saxParserFactory.setNamespaceAware(true); saxParserFactory.setXIncludeAware(true); saxParserFactory.setValidating(false); SAXParser saxParser = saxParserFactory.newSAXParser(); saxParser.getXMLReader().setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); saxParser.getXMLReader().setFeature("http://xml.org/sax/features/validation", false); saxParser.getXMLReader().setContentHandler(new DefaultHandler() { @Override public void setDocumentLocator(Locator locator) { context.location = locator; } @Override public void startElement(String uri, String elementName, String qName, Attributes attributes) throws SAXException { ElementHandler childHandler = handlerStack.getLast().pushChild(elementName, attributes, context); elementNameStack.add(elementName); handlerStack.add(childHandler); context.setElementName(elementName); childHandler.start(elementName, attributes, context); } @Override public void characters(char[] chars, int start, int length) throws SAXException { handlerStack.getLast().handleText(new String(chars, start, length), context); } @Override public void endElement(String uri, String elementName, String qName) throws SAXException { handlerStack.removeLast().finish(context); elementNameStack.removeLast(); context.setElementName(elementNameStack.isEmpty() ? null : elementNameStack.getLast()); } }); saxParser.getXMLReader().parse(new InputSource(input)); } private static String normalise(String value) { if (value == null) { return null; } String normalised = value.trim(); if (normalised.isEmpty()) { return null; } return normalised; } private interface Context { String getFileName(); int getLineNumber(); int getColumnNumber(); BuildableComponent currentComponent(); BuildableBlockContainer ownerContainer(); BuildableBlockContainer currentContainer(); BuildableTitledBlockContainer currentTitled(); BuildableInlineContainer currentInline(); void push(Object element); void pop(); Map<String, Component> getComponentsById(); String getElementName(); } private static class DefaultContext implements Context { final String fileName; final LinkedList<Object> stack = new LinkedList<Object>(); final Map<String, Component> componentsById = new HashMap<String, Component>(); Locator location; private String elementName; private DefaultContext(String fileName) { this.fileName = fileName; } public String getFileName() { return fileName; } public int getLineNumber() { return location.getLineNumber(); } public int getColumnNumber() { return location.getColumnNumber(); } public Map<String, Component> getComponentsById() { return componentsById; } public BuildableBlockContainer ownerContainer() { for (Object element : stack) { if (element instanceof BuildableBlockContainer) { return (BuildableBlockContainer) element; } } throw new IllegalStateException("No container available."); } public BuildableComponent currentComponent() { return (BuildableComponent) stack.getFirst(); } public BuildableBlockContainer currentContainer() { return (BuildableBlockContainer) stack.getFirst(); } public BuildableInlineContainer currentInline() { return (BuildableInlineContainer) stack.getFirst(); } public BuildableTitledBlockContainer currentTitled() { return (BuildableTitledBlockContainer) stack.getFirst(); } public void push(Object element) { stack.addFirst(element); } public void pop() { stack.removeFirst(); } public String getElementName() { return elementName; } public void setElementName(String elementName) { this.elementName = elementName; } } private interface ElementHandler { void start(String name, Attributes attributes, Context context); ElementHandler pushChild(String name, Attributes attributes, Context context); void handleText(String text, Context context); void finish(Context context); } private static class NoOpElementHandler implements ElementHandler { public ElementHandler pushChild(String name, Attributes attributes, Context context) { return this; } public void handleText(String text, Context context) { } public void start(String name, Attributes attributes, Context context) { } public void finish(Context context) { } } private static abstract class AbstractElementHandler implements ElementHandler { protected BuildableInlineContainer attachCrossReference(Context context, final String target) { final Map<String, Component> components = context.getComponentsById(); final String fileName = context.getFileName(); final int lineNumber = context.getLineNumber(); final int columnNumber = context.getColumnNumber(); final String elementName = context.getElementName(); return context.currentInline().addCrossReference(new LinkResolver() { public void resolve(LinkResolverContext resolverContext) { Component component = components.get(target); if (component == null) { String message = String.format("<%s>unknown linkend \"%s\" in %s, line %s, column %s</%s>", elementName, target, fileName, lineNumber, columnNumber, elementName); resolverContext.error(message); } else { resolverContext.crossReference(component); } } }); } protected BuildableInlineContainer attachLink(Context context, final String target) { final String fileName = context.getFileName(); final int lineNumber = context.getLineNumber(); final int columnNumber = context.getColumnNumber(); final String elementName = context.getElementName(); return context.currentInline().addCrossReference(new LinkResolver() { public void resolve(LinkResolverContext resolverContext) { URI url; try { url = new URI(target); } catch (URISyntaxException e) { String message = String.format("<%s>badly formed URL \"%s\" specified in %s, line %s, column %s</%s>", elementName, target, fileName, lineNumber, columnNumber, elementName); resolverContext.error(message); return; } resolverContext.url(url); } }); } } private class DefaultElementHandler extends AbstractElementHandler { public void start(String name, Attributes attributes, Context context) { } public ElementHandler pushChild(String name, Attributes attributes, Context context) { String message = String.format("<%s>%s, line %s, column %s</%s>", name, context.getFileName(), context.getLineNumber(), context.getColumnNumber(), name); context.ownerContainer().addError(message); return noOpElementHandler; } public void handleText(String text, Context context) { if (text.matches("\\s+")) { return; } String message = String.format("(text %s, line %s, column %s)", context.getFileName(), context.getLineNumber(), context.getColumnNumber()); context.ownerContainer().addError(message); } public void finish(Context context) { } } private class RootHandler extends DefaultElementHandler { @Override public ElementHandler pushChild(String name, Attributes attributes, Context context) { if (name.equals("book")) { return bookHandler; } return super.pushChild(name, attributes, context); } } private abstract class ComponentHandler extends DefaultElementHandler { protected abstract BuildableComponent create(Context context); @Override public void start(String name, Attributes attributes, Context context) { BuildableComponent container = create(context); String id = attributes.getValue("id"); if (id != null) { container.setId(id); context.getComponentsById().put(id, container); } context.push(container); } @Override public ElementHandler pushChild(String name, Attributes attributes, Context context) { if (name.equals("title")) { return titleHandler; } return super.pushChild(name, attributes, context); } @Override public void finish(Context context) { context.pop(); } } private class BookHandler extends ComponentHandler { @Override protected BuildableComponent create(Context context) { return context.currentComponent(); } @Override public ElementHandler pushChild(String name, Attributes attributes, Context context) { if (name.equals("bookinfo")) { return bookInfoHandler; } if (name.equals("part")) { return partHandler; } if (name.equals("chapter")) { return chapterHandler; } if (name.equals("appendix")) { return appendixHandler; } return super.pushChild(name, attributes, context); } } private class BookInfoHandler extends DefaultElementHandler { @Override public ElementHandler pushChild(String name, Attributes attributes, Context context) { if (name.equals("title")) { return titleHandler; } return super.pushChild(name, attributes, context); } } private class PartHandler extends ComponentHandler { @Override protected BuildableComponent create(Context context) { return context.currentComponent().addPart(); } @Override public ElementHandler pushChild(String name, Attributes attributes, Context context) { if (name.equals("chapter")) { return chapterHandler; } if (name.equals("appendix")) { return appendixHandler; } return super.pushChild(name, attributes, context); } } private class BlockHandlerFactory { public ElementHandler createHandler(String name, Attributes attributes, Context context) { if (name.equals("para")) { return paraHandler; } if (name.equals("itemizedlist")) { return new ItemizedListHandler(); } if (name.equals("orderedlist")) { return new OrderedListHandler(); } if (name.equals("programlisting")) { return new ProgramListingHandler(); } return null; } } private class ContainerHandler extends DefaultElementHandler { public ElementHandler pushChild(String name, Attributes attributes, Context context) { ElementHandler handler = blockHandlerFactory.createHandler(name, attributes, context); if (handler != null) { return handler; } if (name.equals("example")) { return exampleHandler; } return super.pushChild(name, attributes, context); } } private class SectionHandler extends ContainerHandler { protected BuildableComponent create(Context context) { return context.currentComponent().addSection(); } @Override public void start(String name, Attributes attributes, Context context) { BuildableComponent container = create(context); String id = attributes.getValue("id"); if (id != null) { container.setId(id); context.getComponentsById().put(id, container); } context.push(container); } @Override public ElementHandler pushChild(String name, Attributes attributes, Context context) { if (name.equals("title")) { return titleHandler; } if (name.equals("section")) { return sectionHandler; } return super.pushChild(name, attributes, context); } @Override public void finish(Context context) { context.pop(); } } private class ChapterHandler extends SectionHandler { @Override protected BuildableComponent create(Context context) { return context.currentComponent().addChapter(); } } private class AppendixHandler extends SectionHandler { @Override protected BuildableComponent create(Context context) { return context.currentComponent().addAppendix(); } } private abstract class ListHandler extends DefaultElementHandler { private BuildableList list; abstract BuildableList create(Context context); @Override public void start(String name, Attributes attributes, Context context) { list = create(context); } @Override public ElementHandler pushChild(String name, Attributes attributes, Context context) { if (name.equals("listitem")) { return new ListItemHandler(list); } return super.pushChild(name, attributes, context); } } private class ItemizedListHandler extends ListHandler { @Override BuildableList create(Context context) { return context.currentContainer().addItemisedList(); } } private class OrderedListHandler extends ListHandler { @Override BuildableList create(Context context) { return context.currentContainer().addOrderedList(); } } private class ListItemHandler extends ContainerHandler { private final BuildableList list; public ListItemHandler(BuildableList list) { this.list = list; } @Override public void start(String name, Attributes attributes, Context context) { context.push(list.addItem()); } @Override public void finish(Context context) { context.pop(); } } private class ExampleHandler extends DefaultElementHandler { @Override public void start(String name, Attributes attributes, Context context) { context.push(context.currentContainer().addExample()); } @Override public ElementHandler pushChild(String name, Attributes attributes, Context context) { ElementHandler handler = blockHandlerFactory.createHandler(name, attributes, context); if (handler != null) { return handler; } if (name.equals("title")) { return titleHandler; } return super.pushChild(name, attributes, context); } @Override public void finish(Context context) { context.pop(); } } private class ProgramListingHandler extends DefaultElementHandler { private BuildableProgramListing programListing; @Override public void start(String name, Attributes attributes, Context context) { programListing = context.currentContainer().addProgramListing(); } @Override public void handleText(String text, Context context) { programListing.append(text); } } private class DefaultInlineHandler extends AbstractElementHandler { public void start(String name, Attributes attributes, Context context) { } public ElementHandler pushChild(String name, Attributes attributes, Context context) { String message = String.format("<%s>%s, line %s, column %s</%s>", name, context.getFileName(), context.getLineNumber(), context.getColumnNumber(), name); context.currentInline().addError(message); return noOpElementHandler; } public void handleText(String text, Context context) { context.currentInline().append(text); } public void finish(Context context) { } } private interface ElementHandlerFactory { ElementHandler createHandler(String name, Attributes attributes, Context context); } private class InlineHandlerFactory { ElementHandler createHandler(String name, Attributes attributes, Context context) { if (name.equals("code")) { return codeHandler; } if (name.equals("literal")) { return literalHandler; } if (name.equals("emphasis")) { return emphasisHandler; } if (name.equals("classname")) { return classNameHandler; } return null; } } private class InlineContainerHandler extends DefaultInlineHandler { public ElementHandler pushChild(String name, Attributes attributes, Context context) { ElementHandler handler = inlineHandlerFactory.createHandler(name, attributes, context); if (handler != null) { return handler; } if (name.equals("xref")) { return xrefHandler; } if (name.equals("link")) { return linkHandlerFactory.createHandler(name, attributes, context); } if (name.equals("ulink")) { return ulinkHandlerFactory.createHandler(name, attributes, context); } return super.pushChild(name, attributes, context); } } private abstract class InlineHandler extends DefaultInlineHandler { abstract BuildableInlineContainer create(Context context, Attributes attributes); @Override public void start(String name, Attributes attributes, Context context) { context.push(create(context, attributes)); } @Override public void finish(Context context) { context.pop(); } @Override public ElementHandler pushChild(String name, Attributes attributes, Context context) { ElementHandler handler = inlineHandlerFactory.createHandler(name, attributes, context); if (handler != null) { return handler; } return super.pushChild(name, attributes, context); } } private class XrefHandler extends DefaultElementHandler { @Override public void start(String name, Attributes attributes, final Context context) { final String target = normalise(attributes.getValue("linkend")); if (target != null) { attachCrossReference(context, target); return; } String message = String.format("<xref>no \"linkend\" attribute specified in %s, line %s, column %s</xref>", context.getFileName(), context.getLineNumber(), context.getColumnNumber()); context.currentInline().addError(String.format(message)); } } private class LinkHandlerFactory implements ElementHandlerFactory { public ElementHandler createHandler(String name, Attributes attributes, Context context) { String linkend = normalise(attributes.getValue("linkend")); String href = normalise(attributes.getValue("http://www.w3.org/1999/xlink", "href")); if (linkend == null && href == null) { String message = String.format( "<link>no \"linkend\" or \"href\" attribute specified in %s, line %s, column %s</link>", context.getFileName(), context.getLineNumber(), context.getColumnNumber()); context.currentInline().addError(String.format(message)); return new NoOpElementHandler(); } if (linkend != null && href != null) { String message = String.format( "<link>both \"linkend\" and \"href\" attribute specified in %s, line %s, column %s</link>", context.getFileName(), context.getLineNumber(), context.getColumnNumber()); context.currentInline().addError(String.format(message)); return new NoOpElementHandler(); } if (linkend != null) { return new CrossReferenceHandler(linkend); } return new LinkHandler(href); } } private class CrossReferenceHandler extends InlineHandler { private final String linkend; public CrossReferenceHandler(String linkend) { this.linkend = linkend; } @Override BuildableInlineContainer create(Context context, Attributes attributes) { return attachCrossReference(context, linkend); } } private class UlinkHandlerFactory implements ElementHandlerFactory { public ElementHandler createHandler(String name, Attributes attributes, Context context) { String href = normalise(attributes.getValue("url")); if (href != null) { return new LinkHandler(href); } String message = String.format("<ulink>no \"url\" attribute specified in %s, line %s, column %s</ulink>", context.getFileName(), context.getLineNumber(), context.getColumnNumber()); context.currentInline().addError(String.format(message)); return noOpElementHandler; } } private class LinkHandler extends InlineHandler { private final String href; public LinkHandler(String href) { this.href = href; } @Override BuildableInlineContainer create(Context context, Attributes attributes) { return attachLink(context, href); } } private class CodeHandler extends InlineHandler { @Override BuildableInlineContainer create(Context context, Attributes attributes) { return context.currentInline().addCode(); } } private class LiteralHandler extends InlineHandler { @Override BuildableInlineContainer create(Context context, Attributes attributes) { return context.currentInline().addLiteral(); } } private class EmphasisHandler extends InlineHandler { @Override BuildableInlineContainer create(Context context, Attributes attributes) { return context.currentInline().addEmphasis(); } } private class ClassNameHandler extends InlineHandler { @Override BuildableInlineContainer create(Context context, Attributes attributes) { return context.currentInline().addClassName(); } } private class ParaHandler extends InlineContainerHandler { @Override public void start(String name, Attributes attributes, Context context) { context.push(context.currentContainer().addParagraph()); } @Override public void finish(Context context) { context.pop(); } } private class TitleHandler extends InlineContainerHandler { @Override public void start(String name, Attributes attributes, Context context) { context.push(context.currentTitled().getTitle()); } @Override public void finish(Context context) { context.pop(); } } }