package org.freemarker.docgen; import static org.freemarker.docgen.DocBook5Constants.AV_CONFORMANCE_DOCGEN; import static org.freemarker.docgen.DocBook5Constants.A_CONFORMANCE; import static org.freemarker.docgen.DocBook5Constants.A_LANGUAGE; import static org.freemarker.docgen.DocBook5Constants.A_ROLE; import static org.freemarker.docgen.DocBook5Constants.A_XML_ID; import static org.freemarker.docgen.DocBook5Constants.A_XREFLABEL; import static org.freemarker.docgen.DocBook5Constants.E_ANCHOR; import static org.freemarker.docgen.DocBook5Constants.E_APPENDIX; import static org.freemarker.docgen.DocBook5Constants.E_ARTICLE; import static org.freemarker.docgen.DocBook5Constants.E_BOOK; import static org.freemarker.docgen.DocBook5Constants.E_CHAPTER; import static org.freemarker.docgen.DocBook5Constants.E_COL; import static org.freemarker.docgen.DocBook5Constants.E_COLGROUP; import static org.freemarker.docgen.DocBook5Constants.E_FOOTNOTE; import static org.freemarker.docgen.DocBook5Constants.E_GLOSSARY; import static org.freemarker.docgen.DocBook5Constants.E_GLOSSENTRY; import static org.freemarker.docgen.DocBook5Constants.E_INDEX; import static org.freemarker.docgen.DocBook5Constants.E_INDEXTERM; import static org.freemarker.docgen.DocBook5Constants.E_INFO; import static org.freemarker.docgen.DocBook5Constants.E_INFORMALTABLE; import static org.freemarker.docgen.DocBook5Constants.E_ITEMIZEDLIST; import static org.freemarker.docgen.DocBook5Constants.E_LINK; import static org.freemarker.docgen.DocBook5Constants.E_LISTITEM; import static org.freemarker.docgen.DocBook5Constants.E_MEDIAOBJECT; import static org.freemarker.docgen.DocBook5Constants.E_NOTE; import static org.freemarker.docgen.DocBook5Constants.E_OLINK; import static org.freemarker.docgen.DocBook5Constants.E_ORDEREDLIST; import static org.freemarker.docgen.DocBook5Constants.E_PARA; import static org.freemarker.docgen.DocBook5Constants.E_PART; import static org.freemarker.docgen.DocBook5Constants.E_PREFACE; import static org.freemarker.docgen.DocBook5Constants.E_PRIMARY; import static org.freemarker.docgen.DocBook5Constants.E_PRODUCTNAME; import static org.freemarker.docgen.DocBook5Constants.E_PROGRAMLISTING; import static org.freemarker.docgen.DocBook5Constants.E_QUANDAENTRY; import static org.freemarker.docgen.DocBook5Constants.E_SECONDARY; import static org.freemarker.docgen.DocBook5Constants.E_SECTION; import static org.freemarker.docgen.DocBook5Constants.E_SIMPLESECT; import static org.freemarker.docgen.DocBook5Constants.E_SUBTITLE; import static org.freemarker.docgen.DocBook5Constants.E_TABLE; import static org.freemarker.docgen.DocBook5Constants.E_TBODY; import static org.freemarker.docgen.DocBook5Constants.E_TD; import static org.freemarker.docgen.DocBook5Constants.E_TFOOT; import static org.freemarker.docgen.DocBook5Constants.E_TH; import static org.freemarker.docgen.DocBook5Constants.E_THEAD; import static org.freemarker.docgen.DocBook5Constants.E_TITLE; import static org.freemarker.docgen.DocBook5Constants.E_TITLEABBREV; import static org.freemarker.docgen.DocBook5Constants.E_TR; import static org.freemarker.docgen.DocBook5Constants.E_WARNING; import static org.freemarker.docgen.DocBook5Constants.XMLNS_DOCBOOK5; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.Set; import java.util.TreeSet; 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; /** * Adds Docgen-specific restrictions to an already existing DocBook 5 validator. */ class DocgenRestrictionsValidator implements ContentHandler { public static final int MAX_SECTION_NESTING_LEVEL = 3; private static final Set<String> SUPPORTED_ELEMENTS; static { Set<String> supportedElements = new TreeSet<String>(); supportedElements.add(E_ANCHOR); supportedElements.add("answer"); supportedElements.add(E_APPENDIX); supportedElements.add(E_ARTICLE); supportedElements.add(E_BOOK); supportedElements.add(E_CHAPTER); supportedElements.add("classname"); supportedElements.add(E_COL); supportedElements.add(E_COLGROUP); supportedElements.add("emphasis"); supportedElements.add("entry"); supportedElements.add(E_FOOTNOTE); supportedElements.add(E_GLOSSARY); supportedElements.add("glossdef"); supportedElements.add(E_GLOSSENTRY); supportedElements.add("glosssee"); supportedElements.add("glossseealso"); supportedElements.add("glossterm"); supportedElements.add("imagedata"); supportedElements.add("imageobject"); supportedElements.add(E_INDEX); supportedElements.add(E_INDEXTERM); supportedElements.add(E_INFO); supportedElements.add(E_INFORMALTABLE); supportedElements.add(E_ITEMIZEDLIST); supportedElements.add(E_LINK); supportedElements.add(E_LISTITEM); supportedElements.add("literal"); supportedElements.add(E_MEDIAOBJECT); supportedElements.add("methodname"); supportedElements.add(E_NOTE); supportedElements.add(E_OLINK); supportedElements.add(E_ORDEREDLIST); supportedElements.add("package"); supportedElements.add(E_PARA); supportedElements.add(E_PART); supportedElements.add("phrase"); supportedElements.add(E_PREFACE); supportedElements.add(E_PRIMARY); supportedElements.add(E_PRODUCTNAME); supportedElements.add(E_PROGRAMLISTING); supportedElements.add(E_QUANDAENTRY); supportedElements.add("qandaset"); supportedElements.add("question"); supportedElements.add("quote"); supportedElements.add("remark"); supportedElements.add("replaceable"); supportedElements.add(E_SECONDARY); supportedElements.add(E_SECTION); supportedElements.add(E_SIMPLESECT); supportedElements.add(E_SUBTITLE); supportedElements.add(E_TBODY); supportedElements.add(E_TD); supportedElements.add(E_TFOOT); supportedElements.add(E_TH); supportedElements.add(E_THEAD); supportedElements.add(E_TR); supportedElements.add(E_TITLE); supportedElements.add(E_TITLEABBREV); supportedElements.add(E_WARNING); supportedElements.add("xref"); SUPPORTED_ELEMENTS = Collections.unmodifiableSet(supportedElements); } private static final Set<String> ELEMENTS_ALLOW_ID; static { // Attention! When adding entries here, be sure that the corresponding // element indeed generates HTML anchors (or is an output-file element). Set<String> elementsAllowId = new TreeSet<String>(); elementsAllowId.add(E_PART); elementsAllowId.add(E_APPENDIX); elementsAllowId.add(E_CHAPTER); elementsAllowId.add(E_SECTION); elementsAllowId.add(E_SIMPLESECT); elementsAllowId.add(E_PREFACE); elementsAllowId.add(E_INDEX); elementsAllowId.add(E_GLOSSARY); elementsAllowId.add(E_PARA); elementsAllowId.add(E_MEDIAOBJECT); elementsAllowId.add(E_INFORMALTABLE); elementsAllowId.add(E_PROGRAMLISTING); elementsAllowId.add(E_ITEMIZEDLIST); elementsAllowId.add(E_ORDEREDLIST); elementsAllowId.add(E_LISTITEM); elementsAllowId.add(E_GLOSSENTRY); elementsAllowId.add(E_QUANDAENTRY); elementsAllowId.add(E_ANCHOR); ELEMENTS_ALLOW_ID = Collections.unmodifiableSet(elementsAllowId); } private final ContentHandler docbook5Validator; private final ErrorHandler errorHandler; private final MessageStreamActivityMonitor errorMessageMonitor; private final DocgenValidationOptions options; private Locator locator; private String documentElementName; private int sectionNestingLevel; private int paraNestingLevel; private LinkedList<Integer> paraNestingLevelsHiddenByFootnote = new LinkedList<Integer>(); private LinkedList<Integer> programlistingNestingLevelsHiddenByFootnote = new LinkedList<Integer>(); private LinkedList<Integer> programlistingLineLengthHiddenByFootnote = new LinkedList<Integer>(); private ArrayList<Boolean> hadClosedPara = new ArrayList<Boolean>(); private ArrayList<String> elemPath = new ArrayList<String>(); private int programlistingNestingLevel; private int invisibleElementNestingLevel; private int programlistingLineLength; /** * @param errorMessageMonitor Used for preventing reporting a violation * that was also a DocBook 5 violation. */ DocgenRestrictionsValidator( ContentHandler docbook5Validator, ErrorHandler errorHandler, MessageStreamActivityMonitor errorMessageMonitor, DocgenValidationOptions options) { if (docbook5Validator == null) { throw new IllegalArgumentException( "\"docbook5Validator\" can't be null"); } this.docbook5Validator = docbook5Validator; if (errorHandler == null) { throw new IllegalArgumentException( "\"errorHandler\" can't be null"); } this.errorHandler = errorHandler; if (errorMessageMonitor == null) { throw new IllegalArgumentException( "\"messageMonitor\" can't be null"); } this.errorMessageMonitor = errorMessageMonitor; if (options == null) { throw new IllegalArgumentException("\"options\" can't be null"); } this.options = options; } public void startElement(String uri, final String localName, String name, Attributes atts) throws SAXException { boolean xmlnsOK = uri.equals(XMLNS_DOCBOOK5); if (xmlnsOK) { hadClosedPara.add(false); elemPath.add(localName); } errorMessageMonitor.reset(); docbook5Validator.startElement(uri, localName, name, atts); if (!errorMessageMonitor.hadNewErrorMessage()) { if (!xmlnsOK) { errorHandler.error(newSAXException( "Unsupported element namespace: " + uri)); } else if (!SUPPORTED_ELEMENTS.contains(localName)) { if (localName.equals("sect1") || localName.equals("sect2") || localName.equals("sect3") || localName.equals("sect4") || localName.equals("sect5")) { errorHandler.error(newSAXException( "The \"" + localName + "\" element and other such " + "numbered \"sect\"-s are not allowed; " + "use \"" + E_SECTION + "\"-s instead.")); } else { errorHandler.error(newSAXException( "Unsupported element: " + localName)); } } else { startSupportedDocbook5Element(localName, atts); } } } private void startSupportedDocbook5Element( String localName, Attributes atts) throws SAXException { boolean isDocumentElem; if (documentElementName == null) { documentElementName = localName; isDocumentElem = true; } else { isDocumentElem = false; } if (localName.equals(E_SECTION)) { sectionNestingLevel++; if (sectionNestingLevel > MAX_SECTION_NESTING_LEVEL) { errorHandler.error(newSAXException( "\"" + localName + "\" element nesting too deep. " + "The maximum supported is " + MAX_SECTION_NESTING_LEVEL + " levels. Hint: Use \"" + E_SIMPLESECT + "\" instead.")); } } else if (localName.equals(E_PARA)) { paraNestingLevel++; } else if (localName.equals(E_ITEMIZEDLIST) || localName.equals(E_ORDEREDLIST) || localName.equals(E_PROGRAMLISTING) || localName.equals(E_MEDIAOBJECT)) { checkNotInAPara(localName); if (localName.equals(E_PROGRAMLISTING)) { if (options.getProgramlistingRequiresLanguage() && atts.getValue("", A_LANGUAGE) == null) { errorHandler.error(newSAXException( "In this book, \"" + localName + "\" elements must have a \"" + A_LANGUAGE + "\" attribute. Hint: If the language is so " + "marginal that will not ever have syntax " + "highlighter anyway, use \"unknown\" as the " + "attribute value.")); } if (options.getProgramlistingRequiresRole() && atts.getValue("", A_ROLE) == null) { errorHandler.error(newSAXException("In this book, " + "\"" + localName + "\" elements " + "must have a \"" + A_ROLE + "\" attribute. " + "Hint: If none of the avialble roles fit, " + "use \"unspecified\" as the attribute value." )); } checkHasPrecedingParaInListitem(localName); programlistingLineLength = 0; programlistingNestingLevel++; } } else if (localName.equals(E_INFORMALTABLE) || localName.equals(E_TABLE)) { checkNotInAPara(localName); checkHasPrecedingParaInListitem(localName); } else if (localName.equals(E_FOOTNOTE)) { if (!paraNestingLevelsHiddenByFootnote.isEmpty()) { errorHandler.error(newSAXException("\"" + localName + "\" inside another \"" + localName + "\" is not allowed.")); } if (programlistingNestingLevel != 0) { errorHandler.error(newSAXException("\"" + localName + "\" inside a \"" + E_PROGRAMLISTING + "\" is not allowed.")); } paraNestingLevelsHiddenByFootnote.add(paraNestingLevel); paraNestingLevel = 0; programlistingNestingLevelsHiddenByFootnote.add( programlistingNestingLevel); programlistingNestingLevel = 0; programlistingLineLengthHiddenByFootnote.add( programlistingLineLength); programlistingLineLength = 0; } else if (localName.equals(E_ANCHOR) || localName.equals(E_INDEXTERM)) { invisibleElementNestingLevel++; } else if (isDocumentElem) { String conformance = atts.getValue("", A_CONFORMANCE); if (conformance == null) { errorHandler.error(newSAXException("The \"" + localName + "\" element must have a \"" + A_CONFORMANCE + "\" attribute. Hint: " + "Add the attribute with value \"" + AV_CONFORMANCE_DOCGEN + "\".")); } else if (!conformance.equals(AV_CONFORMANCE_DOCGEN)) { errorHandler.error(newSAXException("The value of the \"" + A_CONFORMANCE + "\" attribute must be \"" + AV_CONFORMANCE_DOCGEN + "\".")); } } if (atts.getIndex(A_XML_ID) != -1 && !ELEMENTS_ALLOW_ID.contains(localName)) { errorHandler.error(newSAXException("The \"" + localName + "\" element can't have an \"" + A_XML_ID + "\" " + "attribute (" + A_XML_ID + "=\"" + atts.getValue("xml:id") + "\"). (Hint: " + (localName.equals(E_TITLE) ? "Move the " + A_XML_ID + " over into the " + "element whose \"" + E_TITLE + "\" the element is." : "Try moving the " + A_XML_ID + " higher in the element hierarchy.") + ")")); } if (atts.getIndex(A_XREFLABEL) != -1 && !ELEMENTS_ALLOW_ID.contains(localName)) { errorHandler.error(newSAXException("The \"" + localName + "\" element can't have an \"" + A_XREFLABEL + "\" attribute, because it couldn't have a \"" + A_XML_ID + "\" attribute either, and hence it " + "couldn't be the target of a link. (Hint: " + (localName.equals(E_TITLE) ? "Move the \"" + A_XREFLABEL + "\" attribute " + "over into the element whose \"" + E_TITLE + "\" the element is." : "Try moving the " + A_XREFLABEL + " higher in the element hierarchy.") + ")")); } } private void checkNotInAPara(String localName) throws SAXException { if (paraNestingLevel > 0) { errorHandler.error(newSAXException("It's not allowed to " + "put a(n) \"" + localName + "\" inside a \"" + E_PARA + "\". Hint: Simply split the containing " + "\"" + E_PARA + "\" into two parts, or move the " + "element after the \"" + E_PARA + "\".")); } } private void checkHasPrecedingParaInListitem(String elemName) throws SAXException { if (elemPath.get(elemPath.size() - 2).equals(E_LISTITEM) && !hadClosedPara.get(hadClosedPara.size() - 2)) { // This restriction exists as otherwise there are problems // with rendering it under most browsers if the element is // implemented as a HTML table. errorHandler.error(newSAXException("A(n) \"" + elemName + "\" in a " + "\"" + E_LISTITEM + "\" must be preceded by a \"" + E_PARA + "\".")); } } public void endElement(String uri, String localName, String name) throws SAXException { boolean xmlnsOK = uri.equals(XMLNS_DOCBOOK5); try { docbook5Validator.endElement(uri, localName, name); if (xmlnsOK) { if (localName.equals(E_SECTION)) { sectionNestingLevel--; } else if (localName.equals(E_PARA)) { paraNestingLevel--; hadClosedPara.set(hadClosedPara.size() - 2, true); } else if (localName.equals(E_FOOTNOTE)) { paraNestingLevel = paraNestingLevelsHiddenByFootnote.remove(); programlistingNestingLevel = programlistingNestingLevelsHiddenByFootnote .remove(); programlistingLineLength = programlistingLineLengthHiddenByFootnote.remove(); } else if (localName.equals(E_PROGRAMLISTING)) { programlistingNestingLevel--; } else if (localName.equals(E_ANCHOR) || localName.equals(E_INDEXTERM)) { invisibleElementNestingLevel--; } } } finally { if (xmlnsOK) { elemPath.remove(elemPath.size() - 1); hadClosedPara.remove(hadClosedPara.size() - 1); } } } public void characters(char[] ch, int start, int length) throws SAXException { if (invisibleElementNestingLevel == 0 && programlistingNestingLevel > 0) { int end = start + length; for (int i = start; i < end; i++) { char c = ch[i]; if (c == 0x0A || c == 0x0D) { programlistingLineLength = 0; } else { if (c == 0x09) { // Assuming tab-width 8: programlistingLineLength = ((programlistingLineLength / 8) + 1) * 8; errorHandler.error(newSAXException( "Tab character is not allowed in " + "programlistings. (Hint: Use spaces instead.)" )); } else { programlistingLineLength++; } } if (programlistingLineLength == options.getMaximumProgramlistingWidth() + 1) { errorHandler.error(newSAXException( "Line length in the programlisting exceeded " + options.getMaximumProgramlistingWidth() + ", which was set as the maximum in the " + "(Related Docgen setting: \"" + Transform.SETTING_VALIDATION + "\" per \"" + Transform .SETTING_VALIDATION_MAXIMUM_PROGRAMLISTING_WIDTH + "\")")); } } } docbook5Validator.characters(ch, start, length); } public void endDocument() throws SAXException { docbook5Validator.endDocument(); } public void endPrefixMapping(String prefix) throws SAXException { docbook5Validator.endPrefixMapping(prefix); } public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { docbook5Validator.ignorableWhitespace(ch, start, length); } public void processingInstruction(String target, String data) throws SAXException { docbook5Validator.processingInstruction(target, data); } public void setDocumentLocator(Locator locator) { docbook5Validator.setDocumentLocator(locator); this.locator = locator; } public void skippedEntity(String name) throws SAXException { docbook5Validator.skippedEntity(name); } public void startDocument() throws SAXException { docbook5Validator.startDocument(); } private SAXParseException newSAXException(String message) { return new SAXParseException( "Docgen-specific DocBook restriction violated: " + message, locator); } public void startPrefixMapping(String prefix, String uri) throws SAXException { docbook5Validator.startPrefixMapping(prefix, uri); } }