package com.jclark.xml.parse;

import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.net.URL;
import java.util.Enumeration;
import java.util.Vector;
import java.util.ResourceBundle;
import java.util.MissingResourceException;
import java.util.Locale;
import java.text.MessageFormat;

import com.jclark.util.Hashtable;

import com.jclark.xml.parse.base.Application;
import com.jclark.xml.tok.*;

/**
 * Parses a single entity.
 * @version $Revision: 1.31 $ $Date: 1998/12/28 08:12:30 $
 */
class EntityParser
extends ContentToken
implements StartElementEvent, EndElementEvent, CharacterDataEvent,
  ProcessingInstructionEvent, EndPrologEvent,
  CommentEvent, StartCdataSectionEvent, EndCdataSectionEvent,
  StartEntityReferenceEvent, EndEntityReferenceEvent,
  ParseLocation {

  private static final class EntityImpl implements Entity {
    byte[] text;
    String systemId;
    String publicId;
    URL baseURL;
    String notationName;
    boolean open;
    public String getSystemId() { return systemId; }
    public URL getBase() { return baseURL; }
    public String getPublicId() { return publicId; }
    public String getNotationName() { return notationName; }
    public String getReplacementText() {
      if (text == null)
	return null;
      StringBuffer buf = new StringBuffer(text.length >> 1);
      for (int i = 0; i < text.length; i += 2)
	buf.append((char)(((text[i] & 0xFF) << 8)
			  | (text[i + 1] & 0xFF)));
      return buf.toString();
    }
  }

  private static final class ElementTypeImpl implements ElementType {
    /* defaultIndex is equal to ID_DEFAULT_INDEX if the attribute
       is an ID attribute; this relies on the fact that ID attributes
       can't have defaults */
    static final int ID_DEFAULT_INDEX = -2;

    private static class Attribute implements AttributeDefinition {
      int defaultIndex = -1;
      boolean required = false;
      byte type = UNDECLARED;
      Vector values = null;
      String name = null;
      String value = null;
      String unnormalizedValue = null;

      public String getDefaultValue() {	return value; }
      public String getDefaultUnnormalizedValue() {
	return unnormalizedValue;
      }
      public boolean isRequired() { return required; }
      public byte getType() { return type; }
      public Enumeration allowedValues() {
	if (values == null)
	  return null;
	return values.elements();
      }
    }

    ElementTypeImpl() { }

    void setContentType(byte n) {
      contentType = n;
    }

    public byte getContentType() {
      return contentType;
    }

    void setContentSpec(String s) {
      contentSpec = s;
    }

    public String getContentSpec() {
      return contentSpec;
    }

    int getDefaultAttributeCount() {
      return nDefaultAtts;
    }

    int getAttributeDefaultIndex(String name) {
      Attribute att = (Attribute)attTable.get(name);
      return att == null ? -1 : att.defaultIndex;
    }

    String getDefaultAttributeValue(int i) {
      if (i >= nDefaultAtts)
	throw new IndexOutOfBoundsException();
      return defaultAtts[i].value;
    }

    String getDefaultAttributeUnnormalizedValue(int i) {
      if (i >= nDefaultAtts)
	throw new IndexOutOfBoundsException();
      return defaultAtts[i].unnormalizedValue;
    }

    String getDefaultAttributeName(int i) {
      if (i >= nDefaultAtts)
	throw new IndexOutOfBoundsException();
      return defaultAtts[i].name;
    }

    boolean isAttributeCdata(String name) {
      Attribute att = (Attribute)attTable.get(name);
      return att == null || att.type <= 0;
    }

    public Enumeration attributeNames() {
      return attTable.keys();
    }

    public AttributeDefinition getAttributeDefinition(String name) {
      return (AttributeDefinition)attTable.get(name);
    }

    /* Value may be null if the default is #IMPLIED or #REQUIRED. */
    boolean appendAttribute(String name,
			    String value,
			    String unnormalizedValue,
			    boolean required,
			    byte attributeType,
			    Vector allowedValues) {
      if (attTable.get(name) != null)
	return false;
      Attribute att = new Attribute();
      attTable.put(name, att);
      att.name = name;
      att.value = value;
      att.unnormalizedValue = unnormalizedValue;
      if (value == null)
	att.defaultIndex = -1;
      else {
	if (nDefaultAtts == defaultAtts.length) {
	  Attribute[] tem = defaultAtts;
	  defaultAtts = new Attribute[tem.length << 1];
	  System.arraycopy(tem, 0, defaultAtts, 0, tem.length);
	}
	defaultAtts[nDefaultAtts] = att;
	att.defaultIndex = nDefaultAtts++;
      }
      att.required = required;
      att.type = attributeType;
      if (attributeType == AttributeDefinition.ID && value == null)
	att.defaultIndex = ElementTypeImpl.ID_DEFAULT_INDEX;
      att.values = allowedValues;
      return true;
    }

    private final Hashtable attTable = new Hashtable();
    private int nDefaultAtts = 0;
    private Attribute[] defaultAtts = new Attribute[4];
    private byte contentType = ElementType.UNDECLARED_CONTENT;
    private String contentSpec = null;
  }

  private static class DTDImpl implements DTD {
    String name = null;
    EntityImpl externalSubset = null;
    URL baseURL = null;
    Hashtable elementTypeTable = new Hashtable();
    Hashtable generalEntityTable = new Hashtable();
    Hashtable paramEntityTable = new Hashtable();
    Hashtable notationTable = new Hashtable();
    boolean complete = true;
    boolean standalone = false;
    boolean hasInternalSubset = false;

    DTDImpl(URL baseURL) {
      this.baseURL = baseURL;
    }

    public String getDocumentTypeName() {
      return name;
    }

    public boolean isComplete() {
      return complete;
    }

    public boolean isStandalone() {
      return standalone;
    }

    public Enumeration entityNames(byte entityType) {
      switch (entityType) {
      case GENERAL_ENTITY:
	return generalEntityTable.keys();
      case PARAMETER_ENTITY:
	return paramEntityTable.keys();
      case NOTATION:
	return notationTable.keys();
      }
      throw new IllegalArgumentException();
    }

    public Entity getEntity(byte entityType, String entityName) {
      switch (entityType) {
      case GENERAL_ENTITY:
	return (Entity)generalEntityTable.get(entityName);
      case PARAMETER_ENTITY:
	if (entityName.equals(DTD.EXTERNAL_SUBSET_NAME)) {
	  if (externalSubset.systemId == null)
	    return null;
	  else
	    return externalSubset;
	}
	return (Entity)paramEntityTable.get(entityName);
      case NOTATION:
	return (Entity)notationTable.get(entityName);

      }
      throw new IllegalArgumentException();
    }

    public Enumeration elementTypeNames() {
      return elementTypeTable.keys();
    }

    public ElementType getElementType(String name) {
      return (ElementType)elementTypeTable.get(name);
    }
  }

  private static final boolean forceStandalone = false;

  private static final int READSIZE = 1024*8;

  private static class StartExternalSubsetEvent
  implements StartEntityReferenceEvent {
    public String getName() { return DTD.EXTERNAL_SUBSET_NAME; }
  }

  private static final StartEntityReferenceEvent startExternalSubsetEvent
    = new StartExternalSubsetEvent();

  private EntityParser parent;
  private String internalEntityName;
  private boolean isParameterEntity;
  private byte[] buf;
  private int bufStart;
  private int bufEnd;
  private int currentTokenStart;
  private InputStream in;
  private URL baseURL;
  private String location;
  private Position pos = new Position();
  // The offset in buffer corresponding to pos.
  private int posOff = 0;
  private long bufEndStreamOffset = 0;
  private Encoding enc;
  // True if the encoding in the XML declaration should be ignored.
  private boolean ignoreDeclEnc;
  private /* final */ int minBPC;
  private int fixBPC;
  private StringConversionCache stringCache;
  private Encoding internalEnc;
  private StringConversionCache internalStringCache;
  private Application app;
  private DTDImpl dtd;
  private EntityManager entityManager;
  private Locale locale;

  private int nameStart;
  // Some temporary buffers
  private Buffer valueBuf;
  private char[] data;
  private static final int INIT_DATA_BUF_SIZE = 65;
  private int dataLength;
  private char[] dataBuf;
  private boolean dataIsRef = false;
  private String[] attValues;
  private String[] attNames;
  private int nAttributes;
  private int idAttributeIndex;
  private boolean[] defaultSpecified;

  EntityParser(OpenEntity entity, EntityManager entityManager, Application app,
	       Locale locale, EntityParser parent) throws IOException {
    this.in = entity.getInputStream();
    this.app = app;
    this.locale = locale;
    this.baseURL = entity.getBase();
    this.location = entity.getLocation();
    this.entityManager = entityManager;
    buf = new byte[READSIZE * 2];
    currentTokenStart = bufStart = bufEnd = 0;
    while (bufEnd - bufStart < 4 && fill())
      ;
    enc = Encoding.getInitialEncoding(buf, bufStart, bufEnd, this);
    currentTokenStart = bufStart = getTokenEnd();
    posOff = bufStart; // ignore the byte order mark in computing columns
    if (enc == null)
      fatal(MessageId.BAD_INITIAL_BYTES);
    String encName = entity.getEncoding();
    if (encName != null) {
      ignoreDeclEnc = true;
      enc = enc.getEncoding(encName);
      if (enc == null)
	fatal(MessageId.UNSUPPORTED_ENCODING);
    }
    minBPC = enc.getMinBytesPerChar();
    fixBPC = enc.getFixedBytesPerChar();
    stringCache = new StringConversionCache(enc);
    valueBuf = new Buffer();
    dataBuf = new char[INIT_DATA_BUF_SIZE];
    internalEnc = Encoding.getInternalEncoding();
    internalStringCache = new StringConversionCache(internalEnc);
    if (parent != null)
      dtd = parent.dtd;
    else
      dtd = new DTDImpl(baseURL);
  }

  private EntityParser(byte[] buf, String entityName, boolean isParameterEntity, EntityParser parent) {
    this.internalEntityName = entityName;
    this.isParameterEntity = isParameterEntity;
    this.buf = buf;
    this.parent = parent;
    baseURL = parent.baseURL;
    entityManager = parent.entityManager;
    currentTokenStart = bufStart = 0;
    bufEnd = buf.length;
    app = parent.app;
    locale = parent.locale;
    enc = internalEnc = parent.internalEnc;
    stringCache = internalStringCache = parent.internalStringCache;
    minBPC = enc.getMinBytesPerChar();
    fixBPC = enc.getFixedBytesPerChar();
    dtd = parent.dtd;
    valueBuf = parent.valueBuf;
    dataBuf = parent.dataBuf;
  }

  void parseDocumentEntity() throws IOException, ApplicationException {
    try {

      try {
	app.startDocument();
      }
      catch (RuntimeException e) {
	throw e;
      }
      catch (Exception e) {
	throw new ApplicationException(e);
      }
      parseDecls(PrologParser.PROLOG);
      parseContent(true);
      parseMisc();

      try {
	app.endDocument();
      }
      catch (RuntimeException e) {
	throw e;
      }
      catch (Exception e) {
	throw new ApplicationException(e);
      }
    }
    finally {
      if (in != null) {
	in.close();
	in = null;
      }
    }
  }

  private void parseExternalTextEntity() throws IOException, ApplicationException {
    try {
      for (;;) {
	try {
	  if (enc.tokenizeContent(buf, bufStart, bufEnd, this)
	      == Encoding.TOK_XML_DECL) {
	    currentTokenStart = bufStart;
	    bufStart = getTokenEnd();
	    handleXmlDecl(true);
	  }
	  break;
	}
	catch (InvalidTokenException e) {
	  break;
	}
	catch (TokenException e) {
	  if (!fill())
	    break;
	}
      }
      parseContent(false);
    }
    finally {
      if (in != null) {
	in.close();
	in = null;
      }
    }
  }

  private void handleXmlDecl(boolean notDocumentEntity) throws NotWellFormedException {
    try {
      TextDecl textDecl;
      if (notDocumentEntity)
	textDecl = new TextDecl(enc, buf, currentTokenStart, bufStart);
      else {
	XmlDecl xmlDecl = new XmlDecl(enc, buf, currentTokenStart, bufStart);
	dtd.standalone = xmlDecl.isStandalone();
	textDecl = xmlDecl;
      }
      if (!ignoreDeclEnc) {
	enc = enc.getEncoding(textDecl.getEncoding());
	if (enc == null)
	  fatal(MessageId.UNSUPPORTED_ENCODING);
	if (enc.getMinBytesPerChar() != minBPC)
	  fatal(MessageId.BAD_DECL_ENCODING);
	stringCache.setEncoding(enc);
	fixBPC = enc.getFixedBytesPerChar();
      }
    }
    catch (InvalidTokenException e) {
      currentTokenStart = e.getOffset();
      fatal(MessageId.INVALID_XML_DECLARATION);
    }
  }

  static class DeclState implements
  MarkupDeclarationEvent, StartDocumentTypeDeclarationEvent,
    EndDocumentTypeDeclarationEvent {
    DeclState(byte type, DTD dtd) {
      this.type = type;
      this.dtd = dtd;
    }
    public DTD getDTD() { return dtd; }
    public int getType() { return declType; }
    public String getName() { return declName; }
    public String getAttributeName() {
      if (declType != ATTRIBUTE)
	return null;
      return attributeName;
    }
    final byte type;
    final DTD dtd;
    EntityImpl entity;
    ElementTypeImpl elementType;
    String attributeName;
    byte attributeType;
    StringBuffer contentSpec = new StringBuffer();
    Vector allowedValues;
    String declName;
    int declType = -1;
  }

  private void parseDecls(byte type) throws IOException, ApplicationException {
    PrologParser pp = new PrologParser(type);
    DeclState declState = new DeclState(type, dtd);
    try {
      for (;;) {
	int tok;
	try {
	  tok = tokenizeProlog();
	}
	catch (EndOfPrologException e) {
	  if (type != PrologParser.PROLOG)
	    fatal(MessageId.SYNTAX_ERROR);
	  pp.end();
	  break;
	}
	catch (EmptyTokenException e) {
	  if (type == PrologParser.PROLOG) {
	    currentTokenStart = bufStart;
	    fatal(MessageId.NO_DOCUMENT_ELEMENT);
	  }
	  pp.end();
	  break;
	}
	prologAction(pp.action(tok, buf, currentTokenStart, bufStart, enc),
		     pp,
		     declState);
      }
    }
    catch (PrologSyntaxException e) {
      fatal(MessageId.SYNTAX_ERROR);
    }
    finally {
      if (type == PrologParser.EXTERNAL_ENTITY && in != null) {
	in.close();
	in = null;
      }
    }
    if (type == PrologParser.PROLOG) {
      try {
	app.endProlog(this);
      }
      catch (RuntimeException e) {
	throw e;
      }
      catch (Exception e) {
	throw new ApplicationException(e);
      }
    }
  }

  void parseInnerParamEntity(PrologParser pp, DeclState declState) throws IOException, ApplicationException {
    int groupLevel = pp.getGroupLevel();
    try {
      for (;;) {
	int tok = tokenizeProlog();
	prologAction(pp.action(tok, buf, currentTokenStart, bufStart, enc),
		     pp,
		     declState);
	if (tok == Encoding.TOK_DECL_CLOSE)
	  fatal(MessageId.PE_DECL_NESTING);
      }
    }
    catch (EndOfPrologException e) {
      fatal(MessageId.SYNTAX_ERROR);
    }
    catch (PrologSyntaxException e) {
      fatal(MessageId.SYNTAX_ERROR);
    }
    catch (EmptyTokenException e) { }
    if (pp.getGroupLevel() != groupLevel)
      fatal(MessageId.PE_GROUP_NESTING);
  }

  void prologAction(int action, PrologParser pp, DeclState declState) throws IOException, ApplicationException {
    String name;
    switch (action) {
    case PrologParser.ACTION_XML_DECL:
      handleXmlDecl(false);
      break;
    case PrologParser.ACTION_TEXT_DECL:
      handleXmlDecl(true);
      break;
    case PrologParser.ACTION_ENTITY_PUBLIC_ID:
    case PrologParser.ACTION_DOCTYPE_PUBLIC_ID:
    case PrologParser.ACTION_NOTATION_PUBLIC_ID:
      try {
	String id = enc.getPublicId(buf, currentTokenStart, bufStart);
	declState.entity.publicId = id;
      }
      catch (InvalidTokenException e) {
	currentTokenStart = e.getOffset();
	fatal(MessageId.PUBID_CHAR);
      }
      break;
    case PrologParser.ACTION_DOCTYPE_NAME:
      dtd.name = stringCache.convert(buf, currentTokenStart, bufStart, true);
      dtd.externalSubset = new EntityImpl();
      declState.entity = dtd.externalSubset;
      break;
    case PrologParser.ACTION_NOTATION_NAME:
      declState.declType = MarkupDeclarationEvent.NOTATION;
      startEntityDecl(dtd.notationTable, declState);
      break;
    case PrologParser.ACTION_GENERAL_ENTITY_NAME:
      declState.declType = MarkupDeclarationEvent.GENERAL_ENTITY;
      startEntityDecl(dtd.generalEntityTable, declState);
      break;
    case PrologParser.ACTION_PARAM_ENTITY_NAME:
      declState.declType = MarkupDeclarationEvent.PARAMETER_ENTITY;
      startEntityDecl(dtd.paramEntityTable, declState);
      break;
    case PrologParser.ACTION_ENTITY_VALUE_NO_PEREFS:
    case PrologParser.ACTION_ENTITY_VALUE_WITH_PEREFS:
      byte[] text = makeReplacementText(action == PrologParser.ACTION_ENTITY_VALUE_WITH_PEREFS);
      if (declState.entity != null)
	declState.entity.text = text;
      break;
    case PrologParser.ACTION_NOTATION_SYSTEM_ID:
    case PrologParser.ACTION_ENTITY_SYSTEM_ID:
    case PrologParser.ACTION_DOCTYPE_SYSTEM_ID:
      if (declState.entity != null) {
	declState.entity.systemId
	  = stringCache.convert(buf,
				currentTokenStart + minBPC,
				bufStart - minBPC,
				false);
	declState.entity.baseURL = baseURL;
      }
      break;
    case PrologParser.ACTION_ENTITY_NOTATION_NAME:
      if (declState.entity != null)
	declState.entity.notationName
	  = stringCache.convert(buf, currentTokenStart, bufStart, true);
      break;
    case PrologParser.ACTION_DOCTYPE_SUBSET:
      dtd.hasInternalSubset = true;
      reportStartDocumentTypeDeclaration(declState);
      break;
    case PrologParser.ACTION_DOCTYPE_CLOSE:
      if (!dtd.hasInternalSubset)
	reportStartDocumentTypeDeclaration(declState);
      if (dtd.externalSubset != null
	  && dtd.externalSubset.systemId != null) {
	if (!dtd.standalone && !forceStandalone) {
	  OpenEntity openEntity
	    = entityManager.open(dtd.externalSubset.systemId,
				 baseURL,
				 dtd.externalSubset.publicId);
	  if (openEntity != null) {
	    try {
	      app.startEntityReference(startExternalSubsetEvent);
	    }
	    catch (RuntimeException e) {
	      throw e;
	    }
	    catch (Exception e) {
	      throw new ApplicationException(e);
	    }
	    new EntityParser(openEntity, entityManager, app, locale, this)
	      .parseDecls(PrologParser.EXTERNAL_ENTITY);
	    reportEndEntityReference();
	    reportEndDocumentTypeDeclaration(declState);
	    return;
	  }
	}
	dtd.complete = false;
      }
      reportEndDocumentTypeDeclaration(declState);
      break;
    case PrologParser.ACTION_INNER_PARAM_ENTITY_REF:
    case PrologParser.ACTION_OUTER_PARAM_ENTITY_REF:
      nameStart = currentTokenStart + minBPC;
      name = stringCache.convert(buf,
				 nameStart,
				 getNameEnd(),
				 true);
      EntityImpl entity = (EntityImpl)dtd.paramEntityTable.get(name);
      if (entity == null) {
	if (dtd.complete)
	  fatal(MessageId.UNDEF_PEREF, name);
	break;
      }
      EntityParser parser = makeParserForEntity(entity, name, true);
      if (parser == null || dtd.standalone || forceStandalone) {
	dtd.complete = false;
	break;
      }
      entity.open = true;
      if (action == PrologParser.ACTION_OUTER_PARAM_ENTITY_REF) {
	reportStartEntityReference();
	parser.parseDecls(entity.text != null
			  ? PrologParser.INTERNAL_ENTITY
			  : PrologParser.EXTERNAL_ENTITY);
	reportEndEntityReference();
      }
      else
	parser.parseInnerParamEntity(pp, declState);
      entity.open = false;
      break;
      /* Default attribute processing. */
    case PrologParser.ACTION_ATTLIST_ELEMENT_NAME:
      String gi = stringCache.convert(buf,
				      currentTokenStart,
				      bufStart,
				      true);
      declState.declType = MarkupDeclarationEvent.ATTRIBUTE;
      declState.declName = gi;
      declState.elementType = (ElementTypeImpl)dtd.elementTypeTable.get(gi);
      if (declState.elementType == null) {
	declState.elementType = new ElementTypeImpl();
	dtd.elementTypeTable.put(gi, declState.elementType);
      }
      break;
    case PrologParser.ACTION_ATTRIBUTE_NAME:
      declState.attributeName = stringCache.convert(buf,
						    currentTokenStart,
						    bufStart,
						    true);
      declState.allowedValues = null;
      break;
    case PrologParser.ACTION_ATTRIBUTE_TYPE_CDATA:
      declState.attributeType = AttributeDefinition.CDATA;
      break;
    case PrologParser.ACTION_ATTRIBUTE_TYPE_ID:
      declState.attributeType = AttributeDefinition.ID;
      break;
    case PrologParser.ACTION_ATTRIBUTE_TYPE_IDREF:
      declState.attributeType = AttributeDefinition.IDREF;
      break;
    case PrologParser.ACTION_ATTRIBUTE_TYPE_IDREFS:
      declState.attributeType = AttributeDefinition.IDREFS;
      break;
    case PrologParser.ACTION_ATTRIBUTE_TYPE_ENTITY:
      declState.attributeType = AttributeDefinition.ENTITY;
      break;
    case PrologParser.ACTION_ATTRIBUTE_TYPE_ENTITIES:
      declState.attributeType = AttributeDefinition.ENTITIES;
      break;
    case PrologParser.ACTION_ATTRIBUTE_TYPE_NMTOKEN:
      declState.attributeType = AttributeDefinition.NMTOKEN;
      break;
    case PrologParser.ACTION_ATTRIBUTE_TYPE_NMTOKENS:
      declState.attributeType = AttributeDefinition.NMTOKENS;
      break;
    case PrologParser.ACTION_IMPLIED_ATTRIBUTE_VALUE:
    case PrologParser.ACTION_REQUIRED_ATTRIBUTE_VALUE:
      if (declState
	  .elementType
	  .appendAttribute(declState.attributeName,
			   null,
			   null,
			   action
			   == PrologParser.ACTION_REQUIRED_ATTRIBUTE_VALUE,
			   declState.attributeType,
			   declState.allowedValues))
	reportMarkupDeclaration(declState);
      break;
    case PrologParser.ACTION_DEFAULT_ATTRIBUTE_VALUE:
    case PrologParser.ACTION_FIXED_ATTRIBUTE_VALUE:
      if (declState
	  .elementType
	  .appendAttribute(declState.attributeName,
			   makeAttributeValue(declState.attributeType
					      == AttributeDefinition.CDATA,
					      buf,
					      currentTokenStart + minBPC,
					      bufStart - minBPC),
			   normalizeNewlines(stringCache.convert(buf,
								 currentTokenStart + minBPC,
								 bufStart - minBPC,
								 false)),
			   action == PrologParser.ACTION_FIXED_ATTRIBUTE_VALUE,
			   declState.attributeType,
			   declState.allowedValues))
	reportMarkupDeclaration(declState);
      break;
    case PrologParser.ACTION_ATTRIBUTE_ENUM_VALUE:
    case PrologParser.ACTION_ATTRIBUTE_NOTATION_VALUE:
      if (action == PrologParser.ACTION_ATTRIBUTE_NOTATION_VALUE)
	declState.attributeType = AttributeDefinition.NOTATION;
      else
	declState.attributeType = AttributeDefinition.ENUM;
      if (declState.allowedValues == null)
	declState.allowedValues = new Vector();
      declState.allowedValues.addElement(stringCache.convert(buf,
							     currentTokenStart,
							     bufStart,
							     true));
      break;
    case PrologParser.ACTION_PI:
      nameStart = currentTokenStart + minBPC + minBPC;
      reportProcessingInstruction();
      break;
    case PrologParser.ACTION_COMMENT:
      reportComment();
      break;
    case PrologParser.ACTION_IGNORE_SECT:
      skipIgnoreSect();
      break;
    case PrologParser.ACTION_ELEMENT_NAME:
      name = stringCache.convert(buf, currentTokenStart, bufStart, true);
      declState.elementType = (ElementTypeImpl)dtd.elementTypeTable.get(name);
      if (declState.elementType == null) {
	declState.elementType = new ElementTypeImpl();
	dtd.elementTypeTable.put(name, declState.elementType);
      }
      declState.declName = name;
      declState.declType = MarkupDeclarationEvent.ELEMENT;
      declState.contentSpec.setLength(0);
      declState.elementType.setContentType(ElementType.ELEMENT_CONTENT);
      break;
    case PrologParser.ACTION_CONTENT_ANY:
      declState.elementType.setContentType(ElementType.ANY_CONTENT);
      declState.elementType.setContentSpec("ANY");
      break;
    case PrologParser.ACTION_CONTENT_EMPTY:
      declState.elementType.setContentType(ElementType.EMPTY_CONTENT);
      declState.elementType.setContentSpec("EMPTY");
      break;
    case PrologParser.ACTION_CONTENT_PCDATA:
      declState.elementType.setContentType(ElementType.MIXED_CONTENT);
      declState.contentSpec.append("#PCDATA");
      break;
    case PrologParser.ACTION_GROUP_OPEN:
    case PrologParser.ACTION_GROUP_CHOICE:
    case PrologParser.ACTION_GROUP_SEQUENCE:
    case PrologParser.ACTION_CONTENT_ELEMENT:
    case PrologParser.ACTION_CONTENT_ELEMENT_REP:
    case PrologParser.ACTION_CONTENT_ELEMENT_OPT:
    case PrologParser.ACTION_CONTENT_ELEMENT_PLUS:
      declState.contentSpec.append(stringCache.convert(buf,
						       currentTokenStart,
						       bufStart,
						       false));
      break;
    case PrologParser.ACTION_GROUP_CLOSE:
    case PrologParser.ACTION_GROUP_CLOSE_REP:
    case PrologParser.ACTION_GROUP_CLOSE_OPT:
    case PrologParser.ACTION_GROUP_CLOSE_PLUS:
      declState.contentSpec.append(stringCache.convert(buf,
						       currentTokenStart,
						       bufStart,
						       false));
      if (pp.getGroupLevel() == 0)
	declState.elementType.setContentSpec(declState.contentSpec.toString());
      break;
    case PrologParser.ACTION_DECL_CLOSE:
      if (declState.declType >= 0
	  && declState.declType != MarkupDeclarationEvent.ATTRIBUTE)
	reportMarkupDeclaration(declState);
      declState.declType = -1;
      break;
    }
  }

  private final void startEntityDecl(Hashtable table, DeclState declState) {
    String name = stringCache.convert(buf, currentTokenStart, bufStart, true);
    declState.entity = (EntityImpl)table.get(name);
    if (declState.entity == null) {
      declState.entity = new EntityImpl();
      table.put(name, declState.entity);
      declState.declName = name;
    }
    else {
      declState.entity = null;
      declState.declType = -1;
    }
  }

  private final void skipIgnoreSect() throws IOException {
    for (;;) {
      try {
	bufStart = enc.skipIgnoreSect(buf, bufStart, bufEnd);
	return;
      }
      catch (PartialTokenException e) {
	if (!fill()) {
	  currentTokenStart = bufStart;
	  fatal(MessageId.UNCLOSED_CONDITIONAL_SECTION);
	}
      }
      catch (InvalidTokenException e) {
	currentTokenStart = e.getOffset();
	fatal(MessageId.IGNORE_SECT_CHAR);
      }
    }
  }

  private final void parseContent(boolean oneElement) throws IOException, ApplicationException {
    byte[] buf = this.buf;
    int bufEnd = this.bufEnd;
    int bufStart = this.bufStart;
    Encoding enc = this.enc;
    int nOpenElements = 0;
    byte[] openElementNameBuf = new byte[64];
    // Indexed by nOpenElements
    int[] openElementNameStart = new int[8];
    openElementNameStart[0] = 0;
    for (;;) {
      try {
	switch (enc.tokenizeContent(buf, bufStart, bufEnd, this)) {
	case Encoding.TOK_START_TAG_WITH_ATTS:
	  storeAtts();
	  /* fall through */
	case Encoding.TOK_START_TAG_NO_ATTS:
	  if (nOpenElements + 1 >= openElementNameStart.length)
	    openElementNameStart = grow(openElementNameStart);
	  nameStart = bufStart + minBPC;
	  nAttributes = -1;
	  /* Update currentTokenStart so that getLocation works. */
	  currentTokenStart = bufStart;
	  try {
	    app.startElement(this);
	  }
	  catch (RuntimeException e) {
	    throw e;
	  }
	  catch (Exception e) {
	    throw new ApplicationException(e);
	  }
	  int nameLength = getNameEnd() - nameStart;
	  int nameBufEnd = openElementNameStart[nOpenElements];
	  if ((openElementNameStart[nOpenElements + 1]
	       = nameBufEnd + nameLength)
	      > openElementNameBuf.length) {
	    byte[] tem
	      = new byte[(openElementNameBuf.length << 1) + nameLength];
	    System.arraycopy(openElementNameBuf,
			     0,
			     tem,
			     0,
			     openElementNameStart[nOpenElements]);
	    openElementNameBuf = tem;
	  }
	  copyBytes(buf, nameStart, openElementNameBuf, nameBufEnd, nameLength);
	  nOpenElements++;
	  break;
	case Encoding.TOK_EMPTY_ELEMENT_WITH_ATTS:
	  storeAtts();
	  /* fall through */
	case Encoding.TOK_EMPTY_ELEMENT_NO_ATTS:
	  nameStart = bufStart + minBPC;
	  nAttributes = -1;

	  /* Update currentTokenStart so that getLocation works. */
	  currentTokenStart = bufStart;
	  try {
	    app.startElement(this);
	    app.endElement(this);
	  }
	  catch (RuntimeException e) {
	    throw e;
	  }
	  catch (Exception e) {
	    throw new ApplicationException(e);
	  }
	  if (oneElement && nOpenElements == 0) {
	    this.bufStart = getTokenEnd();
	    return;
	  }
	  break;
	case Encoding.TOK_END_TAG:
	  if (nOpenElements == 0) {
	    currentTokenStart = bufStart;
	    fatal(MessageId.INVALID_END_TAG);
	  }
	  --nOpenElements;
	  nameStart = bufStart + 2*minBPC;
	  if (!bytesEqual(openElementNameBuf,
			  openElementNameStart[nOpenElements],
			  openElementNameStart[nOpenElements + 1],
			  buf,
			  nameStart,
			  getNameEnd())) {
	    String expected
	      = stringCache.convert(openElementNameBuf,
				    openElementNameStart[nOpenElements],
				    openElementNameStart[nOpenElements + 1],
				    false);
	    String got = stringCache.convert(buf,
					     nameStart,
					     getNameEnd(),
					     false);
	    currentTokenStart = bufStart;
	    fatal(MessageId.MISMATCHED_END_TAG, got, expected);
	  }
	  try {
	    app.endElement(this);
	  }
	  catch (RuntimeException e) {
	    throw e;
	  }
	  catch (Exception e) {
	    throw new ApplicationException(e);
	  }
	  if (oneElement && nOpenElements == 0) {
	    this.bufStart = getTokenEnd();
	    return;
	  }
	  break;
	case Encoding.TOK_DATA_CHARS:
	  data = null;
	  this.bufStart = bufStart;
	  reportCharacterData();
	  break;
	case Encoding.TOK_DATA_NEWLINE:
	  dataBuf[0] = '\n';
	  dataLength = 1;
	  data = dataBuf;
	  reportCharacterData();
	  break;
	case Encoding.TOK_MAGIC_ENTITY_REF:
	case Encoding.TOK_CHAR_REF:
	  dataBuf[0] = getRefChar();
	  dataLength = 1;
	  data = dataBuf;
	  dataIsRef = true;
	  reportCharacterData();
	  dataIsRef = false;
	  break;
	case Encoding.TOK_CHAR_PAIR_REF:
	  getRefCharPair(dataBuf, 0);
	  data = dataBuf;
	  dataLength = 2;
	  dataIsRef = true;
	  reportCharacterData();
	  dataIsRef = false;
	  break;
	case Encoding.TOK_CDATA_SECT_OPEN:
	  currentTokenStart = bufStart;
	  try {
	    app.startCdataSection(this);
	  }
	  catch (RuntimeException e) {
	    throw e;
	  }
	  catch (Exception e) {
	    throw new ApplicationException(e);
	  }
	  this.bufStart = getTokenEnd();
	  parseCdataSection();
	  buf = this.buf;
	  bufStart = this.bufStart;
	  bufEnd = this.bufEnd;
	  break;
	case Encoding.TOK_ENTITY_REF:
	  {
	    nameStart = bufStart + minBPC;
	    String name = stringCache.convert(buf,
					      nameStart,
					      getNameEnd(),
					      true);
	    EntityImpl entity = (EntityImpl)dtd.generalEntityTable.get(name);
	    if (entity == null) {
	      if (dtd.complete || dtd.standalone) {
		currentTokenStart = bufStart;
		fatal(MessageId.UNDEF_REF, name);
	      }
	      break;
	    }
	    EntityParser parser = makeParserForEntity(entity, name, false);
	    if (parser == null)
	      break;
	    reportStartEntityReference();
	    entity.open = true;
	    if (entity.text != null) {
	      currentTokenStart = this.bufStart = bufStart;
	      parser.parseContent(false);
	    }
	    else
	      parser.parseExternalTextEntity();
	    reportEndEntityReference();
	    entity.open = false;
	    break;
	  }
	case Encoding.TOK_PI:
	  nameStart = bufStart + minBPC*2;
	  currentTokenStart = bufStart;
	  reportProcessingInstruction();
	  break;
	case Encoding.TOK_COMMENT:
	  currentTokenStart = bufStart;
	  reportComment();
	  break;
	case Encoding.TOK_XML_DECL:
	  currentTokenStart = bufStart;
	  fatal(MessageId.MISPLACED_XML_DECL);
	}
	bufStart = getTokenEnd();
      }
      catch (EmptyTokenException e) {
	this.bufStart = bufStart;
	if (!fill()) {
	  if (oneElement || nOpenElements > 0) {
	    currentTokenStart = this.bufStart;
	    fatal(MessageId.MISSING_END_TAG);
	  }
	  return;
	}
	buf = this.buf;
	bufStart = this.bufStart;
	bufEnd = this.bufEnd;
      }
      catch (PartialTokenException e) {
	this.bufStart = bufStart;
	if (!fill()) {
	  currentTokenStart = this.bufStart;
	  fatal(MessageId.UNCLOSED_TOKEN);
	}
	buf = this.buf;
	bufStart = this.bufStart;
	bufEnd = this.bufEnd;
      }
      catch (ExtensibleTokenException e) {
	this.bufStart = bufStart;
	if (!fill()) {
	  if (oneElement || nOpenElements > 0) {
	    currentTokenStart = this.bufStart;
	    fatal(MessageId.MISSING_END_TAG);
	  }
	  switch (e.getTokenType()) {
	  case Encoding.TOK_DATA_NEWLINE:
	    dataBuf[0] = '\n';
	    dataLength = 1;
	    data = dataBuf;
	    reportCharacterData();
	    break;
	  case Encoding.TOK_DATA_CHARS:
	    data = null;
	    setTokenEnd(this.bufEnd);
	    reportCharacterData();
	    break;
	  default:
	    throw new Error("extensible token botch");
	  }
	  return;
	}
	buf = this.buf;
	bufStart = this.bufStart;
	bufEnd = this.bufEnd;
      }
      catch (InvalidTokenException e) {
	currentTokenStart = e.getOffset();
	reportInvalidToken(e);
      }
    }
  }

  private final void parseCdataSection() throws IOException, InvalidTokenException, ApplicationException {
    for (;;) {
      try {
	switch (enc.tokenizeCdataSection(buf, bufStart, bufEnd, this)) {
	case Encoding.TOK_DATA_CHARS:
	  data = null;
	  reportCharacterData();
	  break;
	case Encoding.TOK_DATA_NEWLINE:
	  dataBuf[0] = '\n';
	  dataLength = 1;
	  data = dataBuf;
	  reportCharacterData();
	  break;
	case Encoding.TOK_CDATA_SECT_CLOSE:
	  currentTokenStart = bufStart;
	  try {
	    app.endCdataSection(this);
	  }
	  catch (RuntimeException e) {
	    throw e;
	  }
	  catch (Exception e) {
	    throw new ApplicationException(e);
	  }
	  return;
	}
	bufStart = getTokenEnd();
      }
      catch (InvalidTokenException e) {
	throw e;
      }
      catch (TokenException e) {
	if (!fill()) {
	  currentTokenStart = this.bufStart;
	  fatal(MessageId.UNCLOSED_CDATA_SECTION);
	}
      }
    }
  }

  private EntityParser makeParserForEntity(EntityImpl entity, String name, boolean isParameter) throws IOException {
    if (entity.open)
      fatal(MessageId.RECURSION);
    if (entity.notationName != null)
      fatal(MessageId.UNPARSED_REF);
    if (entity.text != null)
      return new EntityParser(entity.text, name, isParameter, this);
    OpenEntity openEntity
      = entityManager.open(entity.systemId, entity.baseURL, entity.publicId);
    if (openEntity == null)
      return null;
    return new EntityParser(openEntity, entityManager, app, locale, this);
  }

  private final void storeAtts() throws NotWellFormedException {
    int i = getAttributeSpecifiedCount();
    ElementTypeImpl elementType = null;
    boolean gotElementType = false;
    while (i != 0) {
      --i;
      if (!isAttributeNormalized(i)) {
	valueBuf.clear();
	if (!gotElementType)
	  elementType
	    = (ElementTypeImpl)dtd.elementTypeTable
	                          .get(stringCache.convert(buf,
							   bufStart + minBPC,
							   getNameEnd(),
							   true));
	boolean isCdata;
	if (elementType != null) {
	  String attName
	    = stringCache.convert(buf,
				  getAttributeNameStart(i),
				  getAttributeNameEnd(i),
				  true);
	  isCdata = elementType.isAttributeCdata(attName);
	}
	else
	  isCdata = true;
	String val = makeAttributeValue(isCdata,
					buf,
					getAttributeValueStart(i),
					getAttributeValueEnd(i));
	setAttributeValue(i, val);
      }
    }
  }

  private String makeAttributeValue(boolean isCdata,
				    byte[] buf,
				    int start,
				    int end) throws NotWellFormedException {
 				      
    /* appendAttributeValue will trash currentTokenStart. */
    int saveCurrentTokenStart = currentTokenStart;
    int saveNameEnd = getNameEnd();
    valueBuf.clear();
    appendAttributeValue(isCdata, start, end, valueBuf);
    if (!isCdata
	&& valueBuf.length() > 0
	&& valueBuf.charAt(valueBuf.length() - 1) == ' ')
      valueBuf.chop();
    currentTokenStart = saveCurrentTokenStart;
    setNameEnd(saveNameEnd);
    return valueBuf.toString();
  }

  private void appendAttributeValue(boolean isCdata,
				    int start,
				    int end,
				    Buffer valueBuf) throws NotWellFormedException {
    Token t = new Token();
    try {
      for (;;) {
	int tok;
	int nextStart;
	try {
	  tok = enc.tokenizeAttributeValue(buf, start, end, t);
	  nextStart = t.getTokenEnd();
	}
	catch (ExtensibleTokenException e) {
	  tok = e.getTokenType();
	  nextStart = end;
	}
	currentTokenStart = start;
	switch (tok) {
	case Encoding.TOK_DATA_CHARS:
	  valueBuf.append(enc, buf, start, t.getTokenEnd());
	  break;
	case Encoding.TOK_MAGIC_ENTITY_REF:
	case Encoding.TOK_CHAR_REF:
	  if (isCdata
	      || t.getRefChar() != ' '
	      || (valueBuf.length() > 0
		  && valueBuf.charAt(valueBuf.length() - 1) != ' '))
	      valueBuf.append(t.getRefChar());
	  break;
	case Encoding.TOK_CHAR_PAIR_REF:
	  valueBuf.appendRefCharPair(t);
	  break;
	case Encoding.TOK_ATTRIBUTE_VALUE_S:
	case Encoding.TOK_DATA_NEWLINE:
	  if (isCdata
	      || (valueBuf.length() > 0
		  && valueBuf.charAt(valueBuf.length() - 1) != ' '))
	    valueBuf.append(' ');
	  break;
	case Encoding.TOK_ENTITY_REF:
	  String name = stringCache.convert(buf,
					    start + minBPC,
					    t.getTokenEnd() - minBPC,
					    true);
	  EntityImpl entity = (EntityImpl)dtd.generalEntityTable.get(name);
	  if (entity == null) {
	    if (dtd.complete || dtd.standalone)
	      fatal(MessageId.UNDEF_REF, name);
	    break;
	  }
	  if (entity.systemId != null)
	    fatal(MessageId.EXTERN_REF_ATTVAL);
	  try {
	    EntityParser parser = makeParserForEntity(entity, name, false);
	    entity.open = true;
	    parser.appendAttributeValue(isCdata, 0, parser.bufEnd, valueBuf);
	    entity.open = false;
	  }
	  catch (NotWellFormedException e) {
	    throw e;
	  }
	  catch (IOException e) {
	    // Shouldn't happen since the entity is internal.
	    throw new Error("unexpected IOException");
	  }
	  break;
	default:
	  throw new Error("attribute value botch");
	}
	start = nextStart;
      }
    }
    catch (PartialTokenException e) {
      currentTokenStart = end;
      fatal(MessageId.NOT_WELL_FORMED);
    }
    catch (InvalidTokenException e) {
      currentTokenStart = e.getOffset();
      reportInvalidToken(e);
    }
    catch (EmptyTokenException e) { }
  }

  /*
   * Make the replacement text for an entity out of the literal in the
   * current token.
   */
  private byte[] makeReplacementText(boolean allowPerefs) throws IOException {
    valueBuf.clear();
    Token t = new Token();
    int start = currentTokenStart + minBPC;
    final int end = bufStart - minBPC;
    try {
      for (;;) {
	int tok;
	int nextStart;
	try {
	  tok = enc.tokenizeEntityValue(buf, start, end, t);
	  nextStart = t.getTokenEnd();
	}
	catch (ExtensibleTokenException e) {
	  tok = e.getTokenType();
	  nextStart = end;
	}
	if (tok == Encoding.TOK_PARAM_ENTITY_REF && !allowPerefs) {
	  currentTokenStart = start;
	  fatal(MessageId.INTERNAL_PEREF_ENTVAL);
	}
	handleEntityValueToken(valueBuf, tok, start, nextStart, t);
	start = nextStart;
      }
    }
    catch (PartialTokenException e) {
      currentTokenStart = end;
      fatal(MessageId.NOT_WELL_FORMED);
    }
    catch (InvalidTokenException e) {
      currentTokenStart = e.getOffset();
      reportInvalidToken(e);
    }
    catch (EmptyTokenException e) { }

    return valueBuf.getBytes();
  }

  private void parseEntityValue(Buffer value) throws IOException {
    final Token t = new Token();
    for (;;) {
      int tok;
      for (;;) {
	try {
	  tok = enc.tokenizeEntityValue(buf, bufStart, bufEnd, t);
	  currentTokenStart = bufStart;
	  bufStart = t.getTokenEnd();
	  break;
	}
	catch (EmptyTokenException e) {
	  if (!fill())
	    return;
	}
	catch (PartialTokenException e) {
	  if (!fill()) {
	    currentTokenStart = bufStart;
	    bufStart = bufEnd;
	    fatal(MessageId.UNCLOSED_TOKEN);
	  }
	}
	catch (ExtensibleTokenException e) {
	  if (!fill()) {
	    currentTokenStart = bufStart;
	    bufStart = bufEnd;
	    tok = e.getTokenType();
	    break;
	  }
	}
	catch (InvalidTokenException e) {
	  currentTokenStart = e.getOffset();
	  reportInvalidToken(e);
	}
      }
      handleEntityValueToken(value, tok, currentTokenStart, bufStart, t);
    }
  }

  private void handleEntityValueToken(Buffer value, int tok, int start, int end, Token t) throws IOException {
    switch (tok) {
    case Encoding.TOK_DATA_CHARS:
    case Encoding.TOK_ENTITY_REF:
    case Encoding.TOK_MAGIC_ENTITY_REF:
      value.append(enc, buf, start, end);
      break;
    case Encoding.TOK_CHAR_REF:
      value.append(t.getRefChar());
      break;
    case Encoding.TOK_CHAR_PAIR_REF:
      value.appendRefCharPair(t);
      break;
    case Encoding.TOK_DATA_NEWLINE:
      value.append('\n');
      break;
    case Encoding.TOK_PARAM_ENTITY_REF:
      String name = stringCache.convert(buf,
					start + minBPC,
					end - minBPC,
					true);
      EntityImpl entity = (EntityImpl)dtd.paramEntityTable.get(name);
      if (entity == null) {
	if (dtd.complete)
	  fatal(MessageId.UNDEF_PEREF, name);
	break;
      }
      EntityParser parser = makeParserForEntity(entity, name, true);
      if (parser != null) {
	entity.open = true;
	parser.parseEntityValue(value);
	entity.open = false;
      }
      break;
    default:
      throw new Error("replacement text botch");
    }
  }

  private void parseMisc() throws IOException, ApplicationException {
    try {
      for (;;) {
	switch (tokenizeProlog()) {
	case Encoding.TOK_PI:
	  nameStart = currentTokenStart + minBPC + minBPC;
	  reportProcessingInstruction();
	  break;
	case Encoding.TOK_COMMENT:
	  reportComment();
	  break;
	case Encoding.TOK_PROLOG_S:
	  break;
	default:
	  fatal(MessageId.EPILOG_JUNK);
	}
      }
    }
    catch (EndOfPrologException e) {
      currentTokenStart = bufStart;
      fatal(MessageId.ELEMENT_AFTER_DOCUMENT_ELEMENT);
    }
    catch (EmptyTokenException e) { }
  }

  private final int tokenizeProlog()
       throws IOException, EmptyTokenException, EndOfPrologException {
    for (;;) {
      try {
	int tok = enc.tokenizeProlog(buf, bufStart, bufEnd, this);
	currentTokenStart = bufStart;
	bufStart = getTokenEnd();
	return tok;
      }
      catch (EmptyTokenException e) {
	if (!fill())
	  throw e;
      }
      catch (PartialTokenException e) {
	if (!fill()) {
	  currentTokenStart = bufStart;
	  bufStart = bufEnd;
	  fatal(MessageId.UNCLOSED_TOKEN);
	}
      }
      catch (ExtensibleTokenException e) {
	if (!fill()) {
	  currentTokenStart = bufStart;
	  bufStart = bufEnd;
	  return e.getTokenType();
	}
      }
      catch (InvalidTokenException e) {
	bufStart = currentTokenStart = e.getOffset();
	reportInvalidToken(e);
      }
    }
  }

  private static final int[] grow(int[] v) {
    int[] tem = v;
    v = new int[tem.length << 1];
    System.arraycopy(tem, 0, v, 0, tem.length);
    return v;
  }

  private long getEntityByteIndex(int off) {
    return bufEndStreamOffset - (bufEnd - off);
  }

  /* The size of the buffer is always a multiple of READSIZE.
     We do reads so that a complete read would end at the
     end of the buffer.  Unless there has been an incomplete
     read, we always read in multiples of READSIZE. */
  private boolean fill() throws IOException {
    if (in == null)
      return false;
    if (bufEnd == buf.length) {
      enc.movePosition(buf, posOff, bufStart, pos);
      /* The last read was complete. */
      int keep = bufEnd - bufStart;
      if (keep == 0)
	bufEnd = 0;
      else if (keep + READSIZE <= buf.length) {
	/*
	 * There is space in the buffer for at least READSIZE bytes.
	 * Choose bufEnd so that it is the least non-negative integer
	 * greater than or equal to <code>keep</code>, such
	 * <code>bufLength - keep</code> is a multiple of READSIZE.
	 */
	bufEnd = buf.length - (((buf.length - keep)/READSIZE) * READSIZE);
	for (int i = 0; i < keep; i++)
	  buf[bufEnd - keep + i] = buf[bufStart + i];
      }
      else {
	byte newBuf[] = new byte[buf.length << 1];
	bufEnd = buf.length;
	System.arraycopy(buf, bufStart, newBuf, bufEnd - keep, keep);
	buf = newBuf;
      }
      bufStart = bufEnd - keep;
      posOff = bufStart;
    }
    int nBytes = in.read(buf, bufEnd, buf.length - bufEnd);
    if (nBytes < 0) {
      in.close();
      in = null;
      return false;
    }
    bufEnd += nBytes;
    bufEndStreamOffset += nBytes;
    return true;
  }

  private void reportInvalidToken(InvalidTokenException e) throws NotWellFormedException {
    switch (e.getType()) {
    case InvalidTokenException.DUPLICATE_ATTRIBUTE:
      fatal(MessageId.DUPLICATE_ATTRIBUTE);
    case InvalidTokenException.XML_TARGET:
      fatal(MessageId.XML_TARGET);
    }
    fatal(MessageId.ILLEGAL_CHAR);
  }

  private void fatal(String message) throws NotWellFormedException {
    doFatal(message, null);
  }

  private void fatal(String message, Object arg) throws NotWellFormedException {
    doFatal(message, new Object[] { arg });
  }

  private void fatal(String message, Object arg1, Object arg2) throws NotWellFormedException {
    doFatal(message, new Object[] { arg1, arg2 });
  }

  private void doFatal(String id, Object[] args) throws NotWellFormedException {
    if (parent != null)
      parent.doFatal(id, args);
    if (posOff > currentTokenStart)
      throw new Error("positioning botch");
    if (enc != null)
      enc.movePosition(buf, posOff, currentTokenStart, pos);
    posOff = currentTokenStart;
    String desc = id;
    String message = null;
    try {
      ResourceBundle resources
	= ResourceBundle.getBundle("com.jclark.xml.parse.Messages", locale);
      desc = resources.getString(id);
      if (args != null)
	desc = MessageFormat.format(desc, args);
      Object[] msgArgs = new Object[] { desc,
					  location,
					  new Integer(pos.getLineNumber()),
					  new Integer(pos.getColumnNumber()),
					  new Long(getEntityByteIndex(currentTokenStart))
					  };
      message = MessageFormat.format(resources
				     .getString(MessageId.MESSAGE_FORMAT),
				     msgArgs);
    }
    catch (MissingResourceException e) { 
      message = desc;
    }
    catch (IllegalArgumentException e) {
      message = desc;
    }
    throw new NotWellFormedException(message,
				     desc,
				     location,
				     baseURL,
				     pos.getLineNumber(),
				     pos.getColumnNumber(),
				     getEntityByteIndex(currentTokenStart));
  }

  private static final
  boolean bytesEqual(byte[] buf1, int start1, int end1,
		     byte[] buf2, int start2, int end2) {
    int len = end1 - start1;
    if (end2 - start2 != len)
      return false;
    for (; len > 0; --len)
      if (buf1[start1++] != buf2[start2++])
	return false;
    return true;
  }

  private final static
  void copyBytes(byte[] from, int fromOff, byte[] to, int toOff, int len) {
    while (--len >= 0) {
      to[toOff++] = from[fromOff++];
    }
  }

  public ParseLocation getLocation() {
    if (parent != null)
      return parent.getLocation();
    if (posOff > currentTokenStart)
      throw new Error("positioning botch");
    if (enc != null)
      enc.movePosition(buf, posOff, currentTokenStart, pos);
    posOff = currentTokenStart;
    return this;
  }

  public String getEntityLocation() {
    return location;
  }

  public URL getEntityBase() {
    return baseURL;
  }

  public long getByteIndex() {
    return getEntityByteIndex(currentTokenStart);
  }

  public int getLineNumber() {
    return pos.getLineNumber();
  }

  public int getColumnNumber() {
    return pos.getColumnNumber();
  }

  public String getName() {
    return stringCache.convert(buf, nameStart, getNameEnd(), true);
  }

  public DTD getDTD() {
    return dtd;
  }

  public int getLength() {
    if (data == null) {
      if (fixBPC != 0)
	return (getTokenEnd() - bufStart)/fixBPC;
      convertData(bufStart, getTokenEnd());
    }
    return dataLength;
  }

  public boolean isReference() {
    return dataIsRef;
  }

  public int getLengthMax() {
    if (data != null)
      return dataLength;
    else
      return (getTokenEnd() - bufStart)/minBPC;
  }

  public int copyChars(char[] cbuf, int off) {
    if (data != null) {
      System.arraycopy(data, 0, cbuf, off, dataLength);
      return dataLength;
    }
    else
      return enc.convert(buf, bufStart, getTokenEnd(), cbuf, off);
  }

  public void writeChars(Writer writer) throws IOException {
    if (data == null)
      convertData(bufStart, getTokenEnd());
    writer.write(data, 0, dataLength);
  }

  private void convertData(int start, int end) {
    if (dataBuf == null || dataBuf.length * minBPC < end - start)
      dataBuf = new char[(end - start)/minBPC];
    dataLength = enc.convert(buf, start, end, dataBuf, 0);
    data = dataBuf;
  }

  private final void setAttributeValue(int index, String value) {
    if (attValues == null)
      attValues = new String[index + 10];
    else if (index >= attValues.length) {
      String[] tem = new String[index << 1];
      System.arraycopy(attValues, 0, tem, 0, attValues.length);
      attValues = tem;
    }
    attValues[index] = value;
  }

  public final int getAttributeCount() {
    if (nAttributes < 0)
      buildAttributes();
    return nAttributes;
  }
  
  public final int getIdAttributeIndex() {
    if (nAttributes < 0)
      buildAttributes();
    return idAttributeIndex;
  }

  public final String getAttributeName(int i) {
    if (nAttributes < 0)
      buildAttributes();
    if (i >= nAttributes)
      throw new IndexOutOfBoundsException();
    return attNames[i];
  }

  public final String getAttributeValue(int i) {
    if (nAttributes < 0)
      buildAttributes();
    if (i < getAttributeSpecifiedCount()) {
      if (isAttributeNormalized(i))
	return stringCache.convert(buf,
				   getAttributeValueStart(i),
				   getAttributeValueEnd(i),
				   false);
    }
    else if (i >= nAttributes)
      throw new IndexOutOfBoundsException();
    return attValues[i];
  }

  public final String getAttributeUnnormalizedValue(int i) {
    if (i >= getAttributeSpecifiedCount() || i < 0)
      throw new IndexOutOfBoundsException();
    return normalizeNewlines(stringCache.convert(buf,
						 getAttributeValueStart(i),
						 getAttributeValueEnd(i),
						 false));
  }

  public final String getAttributeValue(String name) {
    if (nAttributes < 0)
      buildAttributes();
    for (int i = 0; i < nAttributes; i++) {
      if (attNames[i].equals(name)) {
	if (i < getAttributeSpecifiedCount() && isAttributeNormalized(i))
	  return stringCache.convert(buf,
				     getAttributeValueStart(i),
				     getAttributeValueEnd(i),
				     false);
	else
	  return attValues[i];
      }
    }
    return null;
  }

  private void buildAttributes() {
    ElementTypeImpl elementType
      = (ElementTypeImpl)dtd.elementTypeTable.get(getName());
    int nSpecAtts = getAttributeSpecifiedCount();

    {
      int totalAtts = nSpecAtts;
      if (elementType != null)
	totalAtts += elementType.getDefaultAttributeCount();
      if (attNames == null || totalAtts > attNames.length)
	attNames = new String[totalAtts];
    }
    for (int i = nSpecAtts; --i >= 0;)
      attNames[i] = stringCache.convert(buf,
					getAttributeNameStart(i),
					getAttributeNameEnd(i),
					true);
    nAttributes = nSpecAtts;
    idAttributeIndex = -1;
    if (elementType != null) {
      int nDefaults = elementType.getDefaultAttributeCount();
      if (defaultSpecified == null
	  || nDefaults > defaultSpecified.length)
	defaultSpecified = new boolean[nDefaults];
      else {
	for (int i = 0; i < nDefaults; i++)
	  defaultSpecified[i] = false;
      }
      for (int i = nSpecAtts; --i >= 0;) {
	int di = elementType.getAttributeDefaultIndex(attNames[i]);
	if (di >= 0)
	  defaultSpecified[di] = true;
	else if (di == ElementTypeImpl.ID_DEFAULT_INDEX)
	  idAttributeIndex = i;
      }
      for (int i = 0; i < nDefaults; i++) {
	if (!defaultSpecified[i]) {
	  setAttributeValue(nAttributes,
			    elementType.getDefaultAttributeValue(i));
	  attNames[nAttributes] = elementType.getDefaultAttributeName(i);
	  ++nAttributes;
	}
      }
    }
  }

  public final String getComment() {
    return normalizeNewlines(stringCache.convert(buf,
						 currentTokenStart + 4*minBPC,
						 getTokenEnd() - 3*minBPC,
						 false));
  }

  public final String getInstruction() {
    return normalizeNewlines(stringCache.convert(buf,
						 enc.skipS(buf,
							   getNameEnd(),
							   getTokenEnd()),
						 getTokenEnd() - 2*minBPC,
						 false));
  }

  private final String normalizeNewlines(String str) {
    int i = str.indexOf('\r');
    if (i < 0)
      return str;
    StringBuffer buf = new StringBuffer();
    for (i = 0; i < str.length(); i++) {
      char c = str.charAt(i);
      if (c == '\r') {
	buf.append('\n');
	if (i + 1 < str.length() && str.charAt(i + 1) == '\n')
	  i++;
      }
      else
	buf.append(c);
    }
    return buf.toString();
  }

  private final void reportCharacterData() throws ApplicationException {
    try {
      app.characterData(this);
    }
    catch (RuntimeException e) {
      throw e;
    }
    catch (Exception e) {
      throw new ApplicationException(e);
    }
  }

  private final void reportProcessingInstruction() throws ApplicationException {
    try {
      app.processingInstruction(this);
    }
    catch (RuntimeException e) {
      throw e;
    }
    catch (Exception e) {
      throw new ApplicationException(e);
    }
  }

  private final void reportComment() throws ApplicationException {
    try {
      app.comment(this);
    }
    catch (RuntimeException e) {
      throw e;
    }
    catch (Exception e) {
      throw new ApplicationException(e);
    }
  }

  private final void reportStartEntityReference() throws ApplicationException {
    try {
      app.startEntityReference(this);
    }
    catch (RuntimeException e) {
      throw e;
    }
    catch (Exception e) {
      throw new ApplicationException(e);
    }
  }

  private final void reportEndEntityReference() throws ApplicationException {
    try {
      app.endEntityReference(this);
    }
    catch (RuntimeException e) {
      throw e;
    }
    catch (Exception e) {
      throw new ApplicationException(e);
    }
  }

  private final void reportMarkupDeclaration(DeclState declState) throws ApplicationException {
    try {
      app.markupDeclaration(declState);
    }
    catch (RuntimeException e) {
      throw e;
    }
    catch (Exception e) {
      throw new ApplicationException(e);
    }
  }

  private final void reportStartDocumentTypeDeclaration(DeclState declState) throws ApplicationException {
    try {
      app.startDocumentTypeDeclaration(declState);
    }
    catch (RuntimeException e) {
      throw e;
    }
    catch (Exception e) {
      throw new ApplicationException(e);
    }
  }
      
  private final void reportEndDocumentTypeDeclaration(DeclState declState) throws ApplicationException {
    try {
      app.endDocumentTypeDeclaration(declState);
    }
    catch (RuntimeException e) {
      throw e;
    }
    catch (Exception e) {
      throw new ApplicationException(e);
    }
  }
}
