/*
* @(#)$Id: Verifier.java,v 1.35 2001/11/19 19:57:24 kk122374 Exp $
*
* Copyright 2001 Sun Microsystems, Inc. All Rights Reserved.
*
* This software is the proprietary information of Sun Microsystems, Inc.
* Use is subject to license terms.
*
*/
package com.sun.msv.verifier;
import org.xml.sax.*;
import org.xml.sax.helpers.NamespaceSupport;
import org.relaxng.datatype.Datatype;
import java.util.Set;
import java.util.Map;
import java.util.Iterator;
import com.sun.msv.datatype.xsd.StringType;
import com.sun.msv.grammar.IDContextProvider;
import com.sun.msv.util.StringRef;
import com.sun.msv.util.StringPair;
import com.sun.msv.util.StartTagInfo;
import com.sun.msv.util.DatatypeRef;
/**
* SAX ContentHandler that verifies incoming SAX event stream.
*
* This object can be reused to validate multiple documents.
* Just be careful NOT to use the same object to validate more than one
* documents <b>at the same time</b>.
*
* @author <a href="mailto:kohsuke.kawaguchi@eng.sun.com">Kohsuke KAWAGUCHI</a>
*/
public class Verifier extends AbstractVerifier implements IVerifier {
protected Acceptor current;
private static final class Context {
final Context previous;
final Acceptor acceptor;
final int stringCareLevel;
int panicLevel;
Context( Context prev, Acceptor acc, int scl, int plv )
{ previous=prev; acceptor=acc; stringCareLevel=scl; panicLevel=plv; }
};
/** context stack */
Context stack = null;
/** current string care level. See Acceptor.getStringCareLevel */
private int stringCareLevel = Acceptor.STRING_STRICT;
/** characters that were read (but not processed) */
private StringBuffer text = new StringBuffer();
/** Error handler */
protected ErrorHandler errorHandler;
public final ErrorHandler getErrorHandler() { return errorHandler; }
public final void setErrorHandler( ErrorHandler handler ) {
this.errorHandler = handler;
}
/** This flag will be set to true if an error is found */
protected boolean hadError;
/** This flag will be set to true after endDocument method is called. */
private boolean isFinished;
/** An object used to store start tag information.
* the same object is reused. */
private final StartTagInfo sti = new StartTagInfo(null,null,null,null,null);
public final boolean isValid() { return !hadError && isFinished; }
/** Schema object against which the validation will be done */
protected final DocumentDeclaration docDecl;
/**
* Panic level.
*
* If the level is non-zero, createChildAcceptors will silently recover
* from error. This effectively suppresses spurious error messages.
*
* This value is set to INITIAL_PANIC_LEVEL when first an error is encountered,
* and is decreased by successful stepForward and createChildAcceptor.
* This value is also propagated to child acceptors.
*/
protected int panicLevel = 0;
/**
* Initial panic level when an error is found.
* If this value is bigger, MSV will take more time to recover from errors,
* Setting this value to 0 means turning the panic mode off entirely.
*/
private int initialPanicLevel = DEFAULT_PANIC_LEVEL;
private static final int DEFAULT_PANIC_LEVEL = 3;
public final void setPanicMode( boolean usePanicMode ) {
initialPanicLevel = usePanicMode?DEFAULT_PANIC_LEVEL:0;
}
public Verifier( DocumentDeclaration documentDecl, ErrorHandler errorHandler ) {
this.docDecl = documentDecl;
this.errorHandler = errorHandler;
}
/** this field is used to receive type information of character literals. */
private final DatatypeRef characterType = new DatatypeRef();
public Datatype[] getLastCharacterType() { return characterType.types; }
protected void verifyText() throws SAXException {
characterType.types=null;
switch( stringCareLevel ) {
case Acceptor.STRING_PROHIBITED:
// only whitespace is allowed.
final int len = text.length();
for( int i=0; i<len; i++ ) {
final char ch = text.charAt(i);
if( ch!=' ' && ch!='\t' && ch!='\r' && ch!='\n' ) {
// error
onError( null, localizeMessage( ERR_UNEXPECTED_TEXT, null ),
new ErrorInfo.BadText(text) );
break;// recover by ignoring this token
}
}
break;
case Acceptor.STRING_STRICT:
final String txt = new String(text);
if(!current.onText( txt, this, null, characterType )) {
// error
// diagnose error, if possible
StringRef err = new StringRef();
characterType.types=null;
current.onText( txt, this, err, characterType );
// report an error
onError( err, localizeMessage( ERR_UNEXPECTED_TEXT, null ),
new ErrorInfo.BadText(text) );
}
break;
case Acceptor.STRING_IGNORE:
// if STRING_IGNORE, no text should be appended.
if(text.length()!=0) throw new Error();
return;
default:
throw new Error(); //assertion failed
}
if(text.length()!=0)
text = new StringBuffer();
}
public void startElement( String namespaceUri, String localName, String qName, Attributes atts )
throws SAXException {
super.startElement(namespaceUri,localName,qName,atts);
if( com.sun.msv.driver.textui.Debug.debug )
System.out.println("\n-- startElement("+qName+")" + locator.getLineNumber()+":"+locator.getColumnNumber() );
verifyText(); // verify PCDATA first.
// push context
stack = new Context( stack, current, stringCareLevel, panicLevel );
sti.reinit(namespaceUri, localName, qName, atts, this );
// get Acceptor that will be used to validate the contents of this element.
Acceptor next = current.createChildAcceptor(sti,null);
panicLevel = Math.max( panicLevel-1, 0 );
if( next==null ) {
// no child element matchs this one
if( com.sun.msv.driver.textui.Debug.debug )
System.out.println("-- no children accepted: error recovery");
// let acceptor recover from this error.
StringRef ref = new StringRef();
next = current.createChildAcceptor(sti,ref);
ValidityViolation vv = onError(
ref, localizeMessage( ERR_UNEXPECTED_STARTTAG, new Object[]{qName} ),
new ErrorInfo.BadTagName(sti) );
if( next==null ) {
if( com.sun.msv.driver.textui.Debug.debug )
System.out.println("-- unable to recover");
throw new ValidationUnrecoverableException(vv);
}
}
onNextAcceptorReady(sti,next);
// feed attributes
final int len = atts.getLength();
for( int i=0; i<len; i++ )
feedAttribute( next,
atts.getURI(i), atts.getLocalName(i), atts.getQName(i), atts.getValue(i) );
// call the endAttributes
if(!next.onEndAttributes(sti,null)) {
// error.
if( com.sun.msv.driver.textui.Debug.debug )
System.out.println("-- required attributes missing: error recovery");
// let the acceptor recover from the error.
StringRef ref = new StringRef();
next.onEndAttributes(sti,ref);
onError( ref, localizeMessage( ERR_MISSING_ATTRIBUTE, new Object[]{qName} ),
new ErrorInfo.MissingAttribute(sti) );
}
stack.panicLevel = panicLevel; // back-patching.
stringCareLevel = next.getStringCareLevel();
if( stringCareLevel==Acceptor.STRING_IGNORE )
characterType.types = new Datatype[]{StringType.theInstance};
current = next;
}
/**
* this method is called from the startElement method
* after the tag name is processed and the child acceptor is created.
*
* <p>
* This method is called before the attributes are consumed.
*
* <p>
* derived class can use this method to do something useful.
*/
protected void onNextAcceptorReady( StartTagInfo sti, Acceptor nextAcceptor ) throws SAXException {}
/**
* the same instance is reused by the feedAttribute method to reduce
* the number of the object creation.
*/
private final DatatypeRef attributeType = new DatatypeRef();
protected Datatype[] feedAttribute( Acceptor child, String uri, String localName, String qName, String value ) throws SAXException {
if( com.sun.msv.driver.textui.Debug.debug )
System.out.println("-- processing attribute: @"+qName);
attributeType.types = null;
if( !child.onAttribute( uri,localName,qName,value,this,null,attributeType ) ) {
// error
if( com.sun.msv.driver.textui.Debug.debug )
System.out.println("-- bad attribute: error recovery");
// let the acceptor recover from the error.
StringRef ref = new StringRef();
child.onAttribute( uri,localName,qName,value,this,ref,null );
onError( ref, localizeMessage( ERR_UNEXPECTED_ATTRIBUTE, new Object[]{qName}),
new ErrorInfo.BadAttribute(sti,qName,uri,localName,value) );
}
return attributeType.types;
}
public void endElement( String namespaceUri, String localName, String qName )
throws SAXException {
if( com.sun.msv.driver.textui.Debug.debug )
System.out.println("\n-- endElement("+qName+")" + locator.getLineNumber()+":"+locator.getColumnNumber() );
verifyText();
if( !current.isAcceptState(null) && panicLevel==0 ) {
// error diagnosis
StringRef errRef = new StringRef();
current.isAcceptState(errRef);
onError( errRef, localizeMessage(
ERR_UNCOMPLETED_CONTENT,
new Object[]{qName} ),
new ErrorInfo.IncompleteContentModel(qName,namespaceUri,localName) );
// error recovery: pretend as if this state is satisfied
// fall through is enough
}
Acceptor child = current;
// pop context
current = stack.acceptor;
stringCareLevel = stack.stringCareLevel;
panicLevel = Math.max( panicLevel, stack.panicLevel );
stack = stack.previous;
if(!current.stepForward( child, null )) {
// error
StringRef ref = new StringRef();
current.stepForward( child, ref ); // force recovery
onError( ref,
localizeMessage( ERR_UNEXPECTED_ELEMENT, new Object[]{qName} ),
null );
} else
panicLevel = Math.max( panicLevel-1, 0 );
super.endElement( namespaceUri, localName, qName );
}
/**
* signals an error.
*
* This method can be overrided by the derived class to provide different behavior.
*/
protected ValidityViolation onError( StringRef ref, String defaultMsg, ErrorInfo ei ) throws SAXException {
if(ref==null) return onError( defaultMsg, ei );
if(ref.str==null) return onError( defaultMsg, ei );
else return onError( ref.str, ei );
}
protected ValidityViolation onError( String msg, ErrorInfo ei ) throws SAXException {
ValidityViolation vv = new ValidityViolation(locator,msg,ei);
hadError = true;
if( errorHandler!=null && panicLevel==0 )
errorHandler.error(vv);
panicLevel = initialPanicLevel;
return vv;
}
public Object getCurrentElementType() {
return current.getOwnerType();
}
public void characters( char[] buf, int start, int len ) throws SAXException {
if( stringCareLevel!=Acceptor.STRING_IGNORE )
text.append(buf,start,len);
}
public void ignorableWhitespace( char[] buf, int start, int len ) throws SAXException {
if( stringCareLevel!=Acceptor.STRING_IGNORE
&& stringCareLevel!=Acceptor.STRING_PROHIBITED )
// white space is allowed even if the current mode is STRING_PROHIBITED.
text.append(buf,start,len);
}
protected void init() {
super.init();
hadError=false;
isFinished=false;
text = new StringBuffer();
stack = null;
if(duplicateIds!=null) duplicateIds.clear();
}
public void startDocument() throws SAXException {
// reset everything.
// since Verifier maybe reused, initialization is better done here
// rather than constructor.
init();
// if Verifier is used without "divide&validate",
// this method is called and the initial acceptor
// is set by this method.
// When Verifier is used in IslandVerifierImpl,
// then initial acceptor is set at the constructor
// and this method is not called.
current = docDecl.createAcceptor();
}
public void endDocument() throws SAXException {
// ID/IDREF check
if(performIDcheck) {
if(!ids.containsAll(idrefs)) {
hadError = true;
Iterator itr = idrefs.iterator();
while( itr.hasNext() ) {
Object idref = itr.next();
if(!ids.contains(idref))
onError( localizeMessage( ERR_UNSOLD_IDREF, new Object[]{idref} ), null );
}
}
if(duplicateIds!=null) {
Iterator itr = duplicateIds.iterator();
while( itr.hasNext() ) {
Object id = itr.next();
onError( localizeMessage( ERR_DUPLICATE_ID, new Object[]{id} ), null );
}
}
}
isFinished=true;
}
/**
* Stores all duplicate id values.
* Errors are reported at the endDocument method because
* the onDuplicateId method cannot throw an exception.
*/
private Set duplicateIds;
public void onDuplicateId( String id ) {
if(duplicateIds==null)
duplicateIds = new java.util.HashSet();
duplicateIds.add(id);
}
public static String localizeMessage( String propertyName, Object[] args ) {
String format = java.util.ResourceBundle.getBundle(
"com.sun.msv.verifier.Messages").getString(propertyName);
return java.text.MessageFormat.format(format, args );
}
public static final String ERR_UNEXPECTED_TEXT = // arg:0
"Verifier.Error.UnexpectedText";
public static final String ERR_UNEXPECTED_ATTRIBUTE = // arg:1
"Verifier.Error.UnexpectedAttribute";
public static final String ERR_MISSING_ATTRIBUTE = // arg:1
"Verifier.Error.MissingAttribute";
public static final String ERR_UNEXPECTED_STARTTAG = // arg:1
"Verifier.Error.UnexpectedStartTag";
public static final String ERR_UNCOMPLETED_CONTENT = // arg:1
"Verifier.Error.UncompletedContent";
public static final String ERR_UNEXPECTED_ELEMENT = // arg:1
"Verifier.Error.UnexpectedElement";
public static final String ERR_UNSOLD_IDREF = // arg:1
"Verifier.Error.UnsoldIDREF";
public static final String ERR_DUPLICATE_ID = // arg:1
"Verifier.Error.DuplicateId";
}