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 {}
}