/** * Copyright 2015 Santhosh Kumar Tekuri * * The JLibs authors license this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package jlibs.xml.sax.async; import jlibs.nbp.Chars; import jlibs.nbp.NBChannel; import jlibs.nbp.NBHandler; import jlibs.nbp.ReadableCharChannel; import jlibs.xml.ClarkName; import org.apache.xerces.util.XMLChar; import org.xml.sax.*; import org.xml.sax.ext.DeclHandler; import org.xml.sax.ext.LexicalHandler; import org.xml.sax.ext.Locator2; import org.xml.sax.helpers.AttributesImpl; import java.io.CharArrayReader; import java.io.IOException; import java.util.*; import static javax.xml.XMLConstants.*; import static jlibs.xml.sax.SAXFeatures.*; import static jlibs.xml.sax.SAXProperties.*; /** * @author Santhosh Kumar T */ @SuppressWarnings({"ThrowableInstanceNeverThrown"}) public final class AsyncXMLReader implements XMLReader, NBHandler<SAXException>, Locator2{ private static Map<String, char[]> defaultEntities = new HashMap<String, char[]>(); static{ defaultEntities.put("amp", new char[]{ '&' }); defaultEntities.put("lt", new char[]{ '<' }); defaultEntities.put("gt", new char[]{ '>' }); defaultEntities.put("apos", new char[]{ '\'' }); defaultEntities.put("quot", new char[]{ '"' }); } public AsyncXMLReader(){ xmlScanner.coalesceNewLines = true; encoding = null; elements[0] = elem; namespaces[0] = XMLNS_ATTRIBUTE; namespaces[1] = XMLNS_ATTRIBUTE_NS_URI; namespaces[2] = XML_NS_PREFIX; namespaces[3] = XML_NS_URI; elem.defaultNamespace = ""; } private boolean strict = false; public boolean isStrict(){ return strict; } public void setStrict(boolean strict){ this.strict = strict; } private ContentHandler contentHandler; @Override public void setContentHandler(ContentHandler contentHandler){ this.contentHandler = contentHandler; } @Override public ContentHandler getContentHandler(){ return contentHandler; } private ErrorHandler errorHandler; @Override public void setErrorHandler(ErrorHandler errorHandler){ this.errorHandler = errorHandler; } @Override public ErrorHandler getErrorHandler(){ return errorHandler; } private EntityResolver entityResolver; @Override public void setEntityResolver(EntityResolver entityResolver){ this.entityResolver = entityResolver; } @Override public EntityResolver getEntityResolver(){ return entityResolver; } private DTDHandler dtdHandler; @Override public void setDTDHandler(DTDHandler dtdHandler){ this.dtdHandler = dtdHandler; } @Override public DTDHandler getDTDHandler(){ return dtdHandler; } private LexicalHandler lexicalHandler; private DeclHandler declHandler; @Override public boolean getFeature(String name) throws SAXNotRecognizedException, SAXNotSupportedException{ if(NAMESPACES.equals(name) || EXTERNAL_GENERAL_ENTITIES.equals(name) || EXTERNAL_PARAMETER_ENTITIES.equals(name)) return true; throw new SAXNotSupportedException(); } @Override public void setFeature(String name, boolean value) throws SAXNotRecognizedException{ if((NAMESPACES.equals(name) || EXTERNAL_GENERAL_ENTITIES.equals(name) || EXTERNAL_PARAMETER_ENTITIES.equals(name)) && value) return; throw new SAXNotRecognizedException(); } @Override public Object getProperty(String name) throws SAXNotRecognizedException, SAXNotSupportedException{ if(LEXICAL_HANDLER.equals(name) || LEXICAL_HANDLER_ALT.equals(name)) return lexicalHandler; if(DECL_HANDLER.equals(name) || DECL_HANDLER_ALT.equals(name)) return declHandler; throw new SAXNotRecognizedException(); } @Override public void setProperty(String name, Object value) throws SAXNotRecognizedException, SAXNotSupportedException{ if(LEXICAL_HANDLER.equals(name) || LEXICAL_HANDLER_ALT.equals(name)){ if(value==null || value instanceof LexicalHandler) lexicalHandler = (LexicalHandler)value; else throw new SAXNotSupportedException("value must implement "+LexicalHandler.class); }else if(DECL_HANDLER.equals(name) || DECL_HANDLER_ALT.equals(name)){ if(value==null || value instanceof DeclHandler) declHandler = (DeclHandler)value; else throw new SAXNotSupportedException("value must implement "+DeclHandler.class); }else throw new SAXNotRecognizedException(); } private XMLScanner xmlScanner = new XMLScanner(this, XMLScanner.RULE_DOCUMENT); private XMLScanner declScanner = new XMLScanner(this, XMLScanner.RULE_XDECL); private XMLFeeder xmlFeeder, feeder; void setFeeder(XMLFeeder feeder) throws IOException{ if(this.feeder.getParent()==feeder){ if(this.feeder.postAction!=null){ try{ this.feeder.postAction.run(); }catch(Exception ex){ throw this.feeder.parser.ioError(ex.getMessage()); } } } this.feeder = feeder; elemLock = feeder.elemDepth; } public XMLFeeder createFeeder(InputSource inputSource) throws IOException, SAXException{ xmlScanner.reset(); declScanner.reset(XMLScanner.RULE_XDECL); if(xmlFeeder==null) xmlFeeder = new XMLFeeder(this, xmlScanner, inputSource, declScanner); else xmlFeeder.init(inputSource, declScanner); feeder = xmlFeeder; documentStart(); return feeder; } @Override public void parse(InputSource input) throws IOException, SAXException{ if(createFeeder(input).feed()!=null) throw new IOException("parse(...) shouldn't be used on non-blocking IO"); } @Override public void parse(String systemID) throws IOException, SAXException{ parse(new InputSource(systemID)); } /*-------------------------------------------------[ Locator ]---------------------------------------------------*/ @Override public String getPublicId(){ return feeder.publicID; } @Override public String getSystemId(){ return feeder.systemID; } @Override public int getLineNumber(){ return feeder.parser.getLineNumber(); } @Override public int getColumnNumber(){ return feeder.parser.getColumnNumber(); } public int getCharacterOffset(){ return feeder.parser.getCharacterOffset(); } @Override public String getXMLVersion(){ return "1.0"; } @Override public String getEncoding(){ ReadableCharChannel channel = feeder.channel(); if(channel instanceof NBChannel){ NBChannel nbChannel = (NBChannel)channel; if(nbChannel.decoder()!=null) return nbChannel.decoder().charset().name(); } return "UTF-8"; } /*-------------------------------------------------[ Document ]---------------------------------------------------*/ void documentStart() throws SAXException{ encoding = null; standalone = null; prefixLength = 0; value.setLength(0); valueStarted = false; entityValue = false; elem = elements[0]; elemLock = elemDepth = 0; nsFree = 4; systemID = null; publicID = null; entities.clear(); entityStack.clear(); paramEntities.clear(); paramEntityStack.clear(); peReferenceOutsideMarkup = false; dtd = null; _dtd.reset(); dtdElement = null; attributeList = null; dtdAttribute = null; if(contentHandler!=null){ contentHandler.setDocumentLocator(this); contentHandler.startDocument(); } } /*-------------------------------------------------[ XML Decleration ]---------------------------------------------------*/ void version(Chars data) throws SAXException{ if(!"1.0".contentEquals(data)) throw fatalError("Unsupported XML Version: "+data); } String encoding; void encoding(Chars data) throws SAXException{ encoding = data.toString(); } private Boolean standalone; void standalone(Chars data){ standalone = "yes".contentEquals(data); } void xdeclEnd(){ feeder.setDeclaredEncoding(encoding); encoding = null; } /*-------------------------------------------------[ QName ]---------------------------------------------------*/ private final QNamePool qnamePool = new QNamePool(); private QName curQName; private int prefixLength = 0; void prefix(Chars data){ prefixLength = data.length(); } void qname(Chars data) throws SAXException{ curQName = qnamePool.add(prefixLength, data.array(), data.offset(), data.length()); prefixLength = 0; } /*-------------------------------------------------[ Value ]---------------------------------------------------*/ private StringBuilder value = new StringBuilder(); private boolean valueStarted = true; private boolean entityValue = false; void valueStart(){ value.setLength(0); valueStarted = true; entityValue = false; } void entityValue(){ entityValue = true; } void rawValue(Chars data){ char[] chars = data.array(); int offset = data.offset(); int length = data.length(); if(!entityValue){ for(int i=offset; i<length; i++){ char ch = chars[i]; if(ch=='\n' || ch=='\r' || ch=='\t') chars[i] = ' '; } } value.append(chars, offset, length); } private int radix; void hexCode() throws SAXException{ radix = 16; } void asciiCode() throws SAXException{ radix = 10; } void charReference(Chars data) throws SAXException{ int cp = Integer.parseInt(data.toString(), radix); if(XMLChar.isValid(cp)){ if(valueStarted) value.appendCodePoint(cp); else if(contentHandler!=null){ char chars[] = Character.toChars(cp); contentHandler.characters(chars, 0, chars.length); } }else throw fatalError("invalid xml character"); } private ArrayDeque<String> entityStack = new ArrayDeque<String>(); @SuppressWarnings({"ConstantConditions"}) void entityReference(Chars data) throws SAXException, IOException{ if(entityValue){ value.append('&').append(data).append(';'); return; } String entity = data.toString(); char[] entityContent = defaultEntities.get(entity); if(entityContent!=null){ if(valueStarted) value.append(entityContent); else if(contentHandler!=null) contentHandler.characters(entityContent, 0, entityContent.length); }else{ EntityValue entityValue = entities.get(entity); if(entityValue==null) throw fatalError("The entity \""+entity+"\" was referenced, but not declared."); if(entityValue.unparsed) throw fatalError("The unparsed entity reference \"&"+entity+";\" is not permitted"); if(standalone==Boolean.TRUE && entityValue.externalDefinition) throw fatalError("The external entity reference \"&"+entity+";\" is not permitted in standalone document"); if(standalone==Boolean.TRUE && entityValue.externalValue) throw fatalError("The reference to entity \""+entity+"\" declared in an external parsed entity is not permitted in a standalone document"); checkRecursion(entityStack, entity, "entity"); int rule; if(valueStarted){ if(entityValue.externalValue) throw fatalError("The external entity reference \"&"+entity+";\" is not permitted in an attribute value."); rule = XMLScanner.RULE_INT_VALUE; }else{ rule = XMLScanner.RULE_ELEM_CONTENT; } entityStack.push(entity); try{ XMLFeeder childFeeder = entityValue.parse(rule); childFeeder.elemDepth = elemDepth; childFeeder.postAction = new Runnable(){ @Override public void run(){ entityStack.pop(); if(elemDepth>AsyncXMLReader.this.feeder.elemDepth) throw new RuntimeException("expected </"+elem.qname.name+">"); } }; }catch(IOException ex){ throw new RuntimeException(ex); } } } private ArrayDeque<String> paramEntityStack = new ArrayDeque<String>(); private boolean peReferenceOutsideMarkup = false; void peReferenceOutsideMarkup(){ peReferenceOutsideMarkup = true; } @SuppressWarnings({"ConstantConditions"}) void peReference(Chars data) throws Exception{ String param = data.toString(); final EntityValue entityValue = paramEntities.get(param); if(entityValue==null){ if(strict) throw fatalError("The param entity \""+param+"\" was referenced, but not declared."); else return; } if(standalone==Boolean.TRUE && entityValue.externalValue) throw fatalError("The reference to param entity \""+param+"\" declared in an external parsed entity is not permitted in a standalone document"); checkRecursion(paramEntityStack, param, "parameter entity"); if(valueStarted){ if(feeder.parser==xmlScanner && feeder.getParent()==null) throw fatalError("The parameter entity reference \"%"+data+";\" cannot occur within markup in the internal subset of the DTD."); if(entityValue.content!=null) value.append(entityValue.content); else{ entityValue.parse(XMLScanner.RULE_EXTERNAL_ENTITY_VALUE).postAction = new Runnable(){ @Override public void run(){ value.append(externalEntityValue); } }; } }else{ if(peReferenceOutsideMarkup){ peReferenceOutsideMarkup = false; paramEntityStack.push(param); try{ entityValue.parse(XMLScanner.RULE_EXT_SUBSET_DECL).postAction = new Runnable(){ @Override public void run(){ paramEntityStack.pop(); } }; }catch(IOException ex){ throw new RuntimeException(ex); } }else{ if(feeder.parser==xmlScanner && feeder.getParent()==null) throw fatalError("The parameter entity reference \"%"+data+";\" cannot occur within markup in the internal subset of the DTD."); feeder.setChild(new XMLFeeder(this, feeder.parser, entityValue.inputSource(true), entityValue.prologParser())); } } } void valueEnd(){ valueStarted = false; entityValue = false; } /*-------------------------------------------------[ Start Element ]---------------------------------------------------*/ private String namespaces[] = new String[20]; private int nsFree; public String getNamespaceURI(String prefix){ for(int i=nsFree-2; i>=0; i-=2){ if(namespaces[i].equals(prefix)) return namespaces[i+1]; } return null; } private final DTD _dtd = new DTD(this); private DTD dtd ; private final AttributesImpl attrs = new AttributesImpl(); private Element elem = new Element(); private Element elements[] = new Element[10]; private int elemDepth = 0; private int elemLock = 0; private boolean resolveAttributePrefixes; void attributesStart() throws SAXException{ if(curQName.prefix.equals("xmlns")) throw fatalError("Element \""+curQName.name+"\" cannot have \"xmlns\" as its prefix"); if(attrs.getLength()>0) attrs.clear(); resolveAttributePrefixes = false; if(elemDepth==elements.length-1) elements = Arrays.copyOf(elements, elemDepth<<1); String defaultNamespace = elem.defaultNamespace; elem = elements[++elemDepth]; if(elem==null) elements[elemDepth] = elem = new Element(); elem.init(curQName, nsFree, defaultNamespace); } void attributeEnd() throws SAXException{ String attrName = curQName.name; String type, attrValue; if(dtd==null){ type = "CDATA"; attrValue = value.toString(); }else{ AttributeType attrType = dtd.attributeType(elem.qname.name, attrName); type = attrType.name(); attrValue = attrType.normalize(value.toString()); } String attrLocalName = curQName.localName; if(attrName.startsWith("xmlns", 0)){ String nsPrefix = null; if(attrName.length()==5){ nsPrefix = ""; elem.defaultNamespace = attrValue; }else if(attrName.charAt(5)==':'){ if(attrLocalName.equals(XML_NS_PREFIX)){ if(!attrValue.equals(XML_NS_URI)) throw fatalError("prefix "+ XML_NS_PREFIX+" must refer to "+ XML_NS_URI); return; }else if(attrLocalName.equals(XMLNS_ATTRIBUTE)) throw fatalError("prefix "+ XMLNS_ATTRIBUTE+" must not be declared"); else if(attrValue.length()==0) throw fatalError("No Prefix Undeclaring: "+attrLocalName); else nsPrefix = attrLocalName; } if(nsPrefix!=null){ if(attrValue.equals(XML_NS_URI)) throw fatalError(XML_NS_URI+" must be bound to "+ XML_NS_PREFIX); else if(attrValue.equals(XMLNS_ATTRIBUTE_NS_URI)) throw fatalError(XMLNS_ATTRIBUTE_NS_URI+" must be bound to "+ XMLNS_ATTRIBUTE); if(nsFree+2>namespaces.length) namespaces = Arrays.copyOf(namespaces, nsFree<<1); namespaces[nsFree] = nsPrefix; namespaces[nsFree+1] = attrValue; nsFree += 2; if(contentHandler!=null) contentHandler.startPrefixMapping(nsPrefix, attrValue); return; } } String prefix = curQName.prefix; if(prefix.length()>0) resolveAttributePrefixes = true; attrs.addAttribute(prefix, attrLocalName, attrName, type, attrValue); } void attributesEnd() throws SAXException{ int attrCount = attrs.getLength(); if(resolveAttributePrefixes){ for(int i=0; i<attrCount; i++){ String prefix = attrs.getURI(i); if(prefix.length()>0){ String uri = getNamespaceURI(prefix); if(uri==null) throw fatalError("Unbound prefix: "+prefix); attrs.setURI(i, uri); } } } if(attrCount>1){ for(int i=1; i<attrCount; i++){ if(attrs.getIndex(attrs.getURI(i), attrs.getLocalName(i))<i) throw fatalError("Attribute \""+ ClarkName.valueOf(attrs.getURI(i), attrs.getLocalName(i))+"\" was already specified for element \""+elem.qname.name+"\""); } } QName elemQName = elem.qname; if(dtd!=null) dtd.addMissingAttributes(elemQName.name, attrs); String uri; if(elemQName.prefix.length()==0) uri = elem.defaultNamespace; else{ uri = getNamespaceURI(elemQName.prefix); if(uri==null) throw fatalError("Unbound prefix: "+elemQName.prefix); } elem.uri = uri; if(contentHandler!=null) contentHandler.startElement(uri, elemQName.localName, elemQName.name, attrs); } void endingElem(){ feeder.parser.dynamicStringToBeMatched = elem.qname.chars; } void elementEnd() throws SAXException{ if(elemDepth==elemLock) throw fatalError("The element \""+elem.qname.name+"\" must start and end within the same entity"); if(contentHandler!=null){ contentHandler.endElement(elem.uri, elem.qname.localName, elem.qname.name); for(int i=elem.nsStart; i<nsFree; i+=2) contentHandler.endPrefixMapping(namespaces[i]); } nsFree = elem.nsStart; elem = elements[--elemDepth]; if(elemDepth==0) feeder.parser.pop = true; } void rootElementEnd() throws SAXException{ if(elemDepth>0) throw fatalError("expected </"+elem.qname.name+">"); } /*-------------------------------------------------[ PI ]---------------------------------------------------*/ private String piTarget; void piTarget(Chars data) throws SAXException{ piTarget = data.toString(); if(piTarget.equalsIgnoreCase("xml")) throw fatalError("The processing instruction target matching \"[xX][mM][lL]\" is not allowed"); } void piData(Chars piData) throws SAXException{ if(contentHandler!=null) contentHandler.processingInstruction(piTarget, piData.length()>0 ? piData.toString() : ""); } void piData() throws SAXException{ if(contentHandler!=null) contentHandler.processingInstruction(piTarget, ""); } /*-------------------------------------------------[ Misc ]---------------------------------------------------*/ private boolean isWhitespace(Chars data){ char chars[] = data.array(); int end = data.offset()+data.length(); for(int i=data.offset(); i<end; i++){ if(!XMLChar.isSpace(chars[i])) return false; } return true; } void characters(Chars data) throws SAXException{ if(contentHandler!=null){ int len = data.length(); if(len>0){ if(dtd!=null && dtd.nonMixedElements.contains(elem.qname.name) && isWhitespace(data)) contentHandler.ignorableWhitespace(data.array(), data.offset(), len); else contentHandler.characters(data.array(), data.offset(), len); } } } void cdata(Chars data) throws SAXException{ if(lexicalHandler!=null) lexicalHandler.startCDATA(); if(contentHandler!=null) contentHandler.characters(data.array(), data.offset(), data.length()); if(lexicalHandler!=null) lexicalHandler.endCDATA(); } void comment(Chars data) throws SAXException{ if(lexicalHandler!=null) lexicalHandler.comment(data.array(), data.offset(), data.length()); } @Override public SAXException fatalError(String message){ return fatalError(new SAXParseException(message, this)); } public SAXException fatalError(SAXParseException ex){ try{ try{ if(errorHandler!=null) errorHandler.fatalError(ex); return ex; }finally{ if(contentHandler!=null) contentHandler.endDocument(); if(XMLScanner.SHOW_STATS) xmlScanner.printStats(); } }catch(SAXException e){ return ex; } } @Override public void onSuccessful() throws SAXException{ if(feeder.getParent()==null){ if(contentHandler!=null) contentHandler.endDocument(); if(XMLScanner.SHOW_STATS) xmlScanner.printStats(); } } /*-------------------------------------------------[ DTD ]---------------------------------------------------*/ void dtdRoot(Chars data){ dtd = _dtd; dtd.root = data.toString(); } private String systemID; void systemID(Chars data){ systemID = data.toString(); } private String publicID; void publicID(Chars data){ publicID = AttributeType.toPublicID(data.toString()); } void dtdStart() throws SAXException, IOException{ if(lexicalHandler!=null) lexicalHandler.startDTD(dtd.root, publicID, systemID); if(publicID!=null || systemID!=null){ dtd.externalDTD = feeder.resolve(publicID, systemID); publicID = systemID = null; } } private String notationName; void notationName(Chars data){ notationName = data.toString(); } void notationEnd() throws SAXException, IOException{ systemID = feeder.resolve(systemID); if(dtdHandler!=null) dtdHandler.notationDecl(notationName, publicID, systemID); notationName = null; publicID = this.systemID = null; } private String dtdElement; void dtdElement(Chars data){ dtdElement = data.toString(); } void notMixed(){ dtd.nonMixedElements.add(dtdElement); } public void dtdEnd() throws SAXException, IOException{ if(dtd.externalDTD!=null){ InputSource inputSource = dtd.externalDTD; dtd.externalDTD = null; InputSource is = null; if(entityResolver!=null) is = entityResolver.resolveEntity(inputSource.getPublicId(), inputSource.getSystemId()); XMLScanner dtdScanner = new XMLScanner(this, XMLScanner.RULE_EXT_SUBSET_DECL); dtdScanner.coalesceNewLines = true; encoding = null; declScanner.reset(XMLScanner.RULE_TEXT_DECL); feeder.setChild(new XMLFeeder(this, dtdScanner, is==null?inputSource:is, declScanner)); } if(lexicalHandler!=null) lexicalHandler.endDTD(); } /*-------------------------------------------------[ Entity Definition ]---------------------------------------------------*/ private String entityName; void entityName(Chars data){ entityName = data.toString(); } private boolean unparsedEntity; void notationReference(Chars data) throws IOException, SAXException{ if(dtdHandler!=null) dtdHandler.unparsedEntityDecl(entityName, publicID, feeder.resolve(systemID) , data.toString()); unparsedEntity = true; } private Map<String, EntityValue> entities = new HashMap<String, EntityValue>(); void entityEnd() throws SAXException, IOException{ // entities may be declared more than once, with the first declaration being the binding one if(!entities.containsKey(entityName)) entities.put(entityName, new EntityValue()); unparsedEntity = false; value.setLength(0); publicID = systemID = null; } private char[] externalEntityValue; void externalEntityValue(Chars data){ externalEntityValue = Arrays.copyOfRange(data.array(), data.offset(), data.offset()+data.length()); } class EntityValue{ String entityName; char[] content; boolean externalDefinition; boolean unparsed; boolean externalValue; InputSource inputSource; public EntityValue() throws IOException, SAXException{ entityName = AsyncXMLReader.this.entityName; externalDefinition = feeder.getParent()!=null; unparsed = unparsedEntity; if(systemID==null && publicID==null) content = value.toString().toCharArray(); else{ externalValue = true; inputSource = feeder.resolve(publicID, systemID); } } public InputSource inputSource(boolean wrapWithSpace) throws IOException, SAXException{ if(inputSource==null){ InputSource is = new InputSource(getSystemId()); is.setPublicId(getPublicId()); is.setCharacterStream(wrapWithSpace ? new SpaceWrappedReader(content) : new CharArrayReader(content)); return is; }else{ InputSource is = null; if(entityResolver!=null) is = entityResolver.resolveEntity(inputSource.getPublicId(), inputSource.getSystemId()); return is!=null ? is : inputSource; } } public XMLScanner prologParser(){ XMLScanner prologParser = null; if(externalValue){ prologParser = declScanner; prologParser.reset(XMLScanner.RULE_TEXT_DECL); encoding = null; } return prologParser; } public XMLFeeder parse(int rule) throws IOException, SAXException{ XMLScanner scanner = new XMLScanner(AsyncXMLReader.this, rule); XMLScanner prologParser = prologParser(); scanner.coalesceNewLines = externalValue; XMLFeeder childFeeder = new XMLFeeder(AsyncXMLReader.this, scanner, inputSource(false), prologParser); feeder.setChild(childFeeder); return childFeeder; } } public void checkRecursion(Deque<String> stack, String current, String type) throws SAXException{ if(stack.contains(current)){ StringBuilder message = new StringBuilder("Recursive ").append(type).append(" reference "); message.append('"').append(current).append('"').append(". (Reference path: "); boolean first = true; Iterator<String> iter = stack.descendingIterator(); while(iter.hasNext()){ if(first) first = false; else message.append(" -> "); message.append(iter.next()); } message.append(" -> ").append(current); message.append(')'); throw fatalError(message.toString()); } } /*-------------------------------------------------[ Param Entity Definition ]---------------------------------------------------*/ private String paramEntityName; void paramEntityName(Chars data){ paramEntityName = data.toString(); } private Map<String, EntityValue> paramEntities = new HashMap<String, EntityValue>(); void paramEntityEnd() throws SAXException, IOException{ if(!paramEntities.containsKey(paramEntityName)) paramEntities.put(paramEntityName, new EntityValue()); unparsedEntity = false; value.setLength(0); publicID = systemID = null; } /*-------------------------------------------------[ DTD Attributes ]---------------------------------------------------*/ private Map<String, DTDAttribute> attributeList; private DTDAttribute dtdAttribute; void dtdAttributesStart(Chars data){ String dtdElementName = data.toString(); attributeList = dtd.attributes.get(dtdElementName); if(attributeList==null) dtd.attributes.put(dtdElementName, attributeList=new HashMap<String, DTDAttribute>()); } void dtdAttribute(Chars data){ String attributeName = data.toString(); if(attributeList.get(attributeName)==null){ dtdAttribute = new DTDAttribute(); dtdAttribute.name = attributeName; attributeList.put(attributeName, dtdAttribute); }else dtdAttribute = null; } void cdataAttribute(){ if(dtdAttribute!=null) dtdAttribute.type = AttributeType.CDATA; } void idAttribute(){ if(dtdAttribute!=null) dtdAttribute.type = AttributeType.ID; } void idRefAttribute(){ if(dtdAttribute!=null) dtdAttribute.type = AttributeType.IDREF; } void idRefsAttribute(){ if(dtdAttribute!=null) dtdAttribute.type = AttributeType.IDREFS; } void nmtokenAttribute(){ if(dtdAttribute!=null) dtdAttribute.type = AttributeType.NMTOKEN; } void nmtokensAttribute(){ if(dtdAttribute!=null) dtdAttribute.type = AttributeType.NMTOKENS; } void entityAttribute(){ if(dtdAttribute!=null) dtdAttribute.type = AttributeType.ENTITY; } void entitiesAttribute(){ if(dtdAttribute!=null) dtdAttribute.type = AttributeType.ENTITIES; } void enumerationAttribute(){ if(dtdAttribute!=null){ dtdAttribute.type = AttributeType.ENUMERATION; dtdAttribute.validValues = new ArrayList<String>(); } } void notationAttribute(){ if(dtdAttribute!=null){ dtdAttribute.type = AttributeType.NOTATION; dtdAttribute.validValues = new ArrayList<String>(); } } void attributeEnumValue(Chars data){ if(dtdAttribute!=null) dtdAttribute.validValues.add(data.toString()); } void attributeNotationValue(Chars data){ if(dtdAttribute!=null) dtdAttribute.validValues.add(data.toString()); } void attributeDefaultValue() throws SAXException{ if(dtdAttribute!=null){ dtdAttribute.valueType = AttributeValueType.DEFAULT; dtdAttribute.value = dtdAttribute.type.normalize(value.toString()); dtdAttribute.fire(declHandler); } value.setLength(0); } void attributeRequired() throws SAXException{ if(dtdAttribute!=null){ dtdAttribute.valueType = AttributeValueType.REQUIRED; dtdAttribute.fire(declHandler); } } void attributeImplied() throws SAXException{ if(dtdAttribute!=null){ dtdAttribute.valueType = AttributeValueType.IMPLIED; dtdAttribute.fire(declHandler); } } void attributeFixedValue() throws SAXException{ if(dtdAttribute!=null){ dtdAttribute.valueType = AttributeValueType.FIXED; dtdAttribute.value = dtdAttribute.type.normalize(value.toString()); dtdAttribute.fire(declHandler); } value.setLength(0); } void dtdAttributesEnd(){} }