package runtime; import java.io.PrintStream; import java.text.MessageFormat; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.kohsuke.validatelet.Validatelet; import org.relaxng.datatype.ValidationContext; 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; /** * Validator implemented as SAX ContentHandler. * * If you are looking for a high-level abstraction, look * at {@link org.kohsuke.validatelet.ValidateletVerifier} * * @author Kohsuke Kawaguchi (kk@kohsuke.org) */ public class ValidateletImpl implements Validatelet { private final Schema schema; private State currentState = State.emptySet; /** Cached value of <code>currentState.getTextSensitivity()</code>. */ private int textSensitivity; private StateFactory factory = new StateFactory(); /** Stores unprocessed text. */ private StringBuffer textBuffer = new StringBuffer(); /** True if the text in the text buffer is ignorable. */ private boolean isTextBufferIgnorable = true; // private Schema.StringPair stringPair = new Schema.StringPair("",""); // // attributes // // array is used in the reverse order. attribute[depth] always // point to the top-most active attributes. private AttributesSet[] attributes; private int depth; private Locator locator; public ValidateletImpl( Schema schema ) { this.schema = schema; // prepare empty objects attributes = new AttributesSet[16]; for( int i=0; i<attributes.length; i++ ) attributes[i] = new AttributesSet(); // initialize nameCode map for (Iterator itr = schema.nameLiterals.entrySet().iterator(); itr.hasNext();) { Map.Entry e = (Map.Entry) itr.next(); Schema.StringPair key = (Schema.StringPair)e.getKey(); Integer value = (Integer)e.getValue(); nameCodes.put(key.uri, key.local, value.intValue()); } defaultNameCode = new NameCodeMap.Entry( 0,Schema.WILDCARD,Schema.WILDCARD, schema.defaultNameCode, null ); } /** * Sets the current state to the given state. * * @param newState * The caller needs to addRef. */ private final void setCurrentState( State newState ) { currentState = newState; textSensitivity = newState.textSensitivity; } public void startDocument() throws SAXException { setCurrentState(schema.initialState); context = initialContext; depth=attributes.length-1; } private final AttributesSet pushAttributes( Attributes atts ) { if( depth==0 ) { int len = attributes.length; AttributesSet[] newBuf = new AttributesSet[len*2]; System.arraycopy(attributes,0,newBuf,len,len); for( int i=len-1; i>=0; i-- ) newBuf[i] = new AttributesSet(); attributes = newBuf; depth = len; } depth--; AttributesSet r = attributes[depth]; r.reset(this,atts,context); return r; } // // // validation context // // /** * Immutable ValidationContext implementation. * Because we don't process attribute transitions when we see a start element, * ValidationContext implementation needs to be immutable. */ private final static class Context implements ValidationContext { Context( Context previous, String prefix, String uri ) { this.previous = previous; this.prefix = prefix; this.uri = uri; } private final Context previous; private final String prefix,uri; public String resolveNamespacePrefix(String prefix) { for( Context c = this; c!=null; c=c.previous ) { if( prefix.equals(c.prefix) ) return c.uri; } return null; } public String getBaseUri() { return null; } public boolean isNotation(String name) { return true; } public boolean isUnparsedEntity(String name) { return true; } } /** The initial context at the beginning of the validation. */ private static final Context initialContext = new Context(null,"xml","http://www.w3.org/XML/1998/namespace"); private Context context=null; public void startPrefixMapping(String prefix, String uri) throws SAXException { context = new Context( context, prefix, uri ); } public void endPrefixMapping(String prefix) throws SAXException { context = context.previous; } // // // SAX events handling // // public void characters(char[] buf, int start, int len) throws SAXException { int ts = textSensitivity; if( ts == State.TEXT_IGNORABLE ) return; // no need to accumulate text if( ts == State.TEXT_SENSITIVE ) textBuffer.append(buf,start,len); // make sure that the added text is still ignorable if(isTextBufferIgnorable) { for( int i=len-1; i>=0; i-- ) { char ch = buf[i+start]; if( ch==' ' || ch=='\t' || ch=='\r' || ch=='\n' ) continue; else { isTextBufferIgnorable = false; return; } } } } public void ignorableWhitespace(char[] buf, int start, int len) throws SAXException { // we know that the string consists entirely from whitespaces. if( textSensitivity==State.TEXT_SENSITIVE ) textBuffer.append(buf,start,len); } private void resetTextBuffer() { // if(textBuffer.length()<1024) textBuffer.setLength(0); // else textBuffer = new StringBuffer(); textBuffer.setLength(0); isTextBufferIgnorable = true; } private void processText() throws SAXException { switch(textSensitivity) { case State.TEXT_WHITESPACE_ONLY: if( !isTextBufferIgnorable ) { error(); isTextBufferIgnorable = true; } // fall through next block case State.TEXT_IGNORABLE: if( textBuffer.length()!=0 || !isTextBufferIgnorable) // no text should have been accumulated. throw new InternalError("assertion failure"); // no need to resetTextBuffer() as the buffer is empty. return; case State.TEXT_SENSITIVE: String value = textBuffer.toString(); // if(debug!=null) { // String trimmed = value.trim(); // printIndent(); // debug.println(MessageFormat.format("text \"{0}\"",new Object[]{trimmed})); // } // handle ignorable texts State newState = currentState.text( value, isTextBufferIgnorable, context, attributes[depth], State.emptySet, factory ); // if(debug!=null) { // if( value.trim().length()!=0) { // printIndent(); // debug.println(MessageFormat.format("{0} => {1}",new Object[]{currentState,newState})); // } // } if(newState==State.emptySet) error(); setCurrentState(newState); resetTextBuffer(); break; default: throw new InternalError("assertion failure"); } } // private HashMap startTagCache = new HashMap(); private final NameCodeMap nameCodes = new NameCodeMap(); private final NameCodeMap.Entry defaultNameCode; public void startElement( String nsUri, String localName, String qname, Attributes atts) throws SAXException { if( !isTextBufferIgnorable ) processText(); else resetTextBuffer(); // look up name code table NameCodeMap.Entry nameCode = getNameCode(nsUri,localName); if(debug!=null) { printIndent(); debug.println(MessageFormat.format("<{0}> (name code:{1})", new Object[]{qname,Integer.toString(nameCode.nameCode)})); indent++; } AttributesSet curAtts = pushAttributes(atts); State newState; if( atts.getLength()==0 ) { // HashMap m = (HashMap)startTagCache.get(nameCode); // if(m==null) startTagCache.put(nameCode,m=new HashMap()); // newState = (State)nameCode.startTagCache.get(currentState); if(newState==null) { newState = currentState.startElement( nameCode.nameCode, curAtts, State.emptySet, factory ); nameCode.startTagCache.put( currentState, newState ); } } else { newState = currentState.startElement( nameCode.nameCode, curAtts, State.emptySet, factory ); } if(debug!=null) { printIndent(); debug.println(MessageFormat.format("{0} => {1}",new Object[]{currentState,newState})); } if(newState==State.emptySet) error(); setCurrentState(newState); } public void endElement(String nsUri, String localName, String qname) throws SAXException { processText(); if(debug!=null) { indent--; printIndent(); debug.println(MessageFormat.format("</{0}>",new Object[]{qname})); } depth++; // pop attribute AttributesSet atts = attributes[depth]; State newState; if( atts.size()==0 ) { // optimization newState = currentState.endElementFast(factory); } else { // normal newState = currentState.endElement( atts, State.emptySet, factory ); } if(debug!=null) { printIndent(); debug.println(MessageFormat.format("{0} => {1}",new Object[]{currentState,newState})); } if(newState==State.emptySet) error(); setCurrentState(newState); } public void setDocumentLocator(Locator locator) { this.locator = locator; } public Locator getLocator() { return locator; } public void endDocument() throws SAXException { setCurrentState(State.emptySet); locator = null; } private ErrorHandler errorHandler; public void setErrorHandler( ErrorHandler newHandler ) { this.errorHandler = newHandler; } public ErrorHandler getErrorHandler() { return errorHandler; } public void error() throws SAXException { SAXParseException e = new SAXParseException("validation error",locator); if(errorHandler!=null) errorHandler.fatalError(e); throw e; // don't support error recovery } protected final NameCodeMap.Entry getNameCode( String uri, String localName ) { NameCodeMap.Entry e; e = nameCodes.get(uri,localName); if(e!=null) return e; e = nameCodes.get(uri,Schema.WILDCARD); if(e!=null) return e; return defaultNameCode; } // // // debug support // // /** debug messages will be sent to this object if set to non-null. */ private static final PrintStream debug = System.getProperty("DEBUG_BALI")!=null?System.out:null; private int indent=0; private void printIndent() { for( int i=0; i<indent; i++ ) debug.print(" "); } // unhandled call back public void processingInstruction(String target, String data) throws SAXException {} public void skippedEntity(String name) throws SAXException {} }