Hi Daniel, +1 to both those suggestions.
Thanks, Jeff IBM Software Group - WebSphere Web Services Development Phone: (512) 286-5256 or TieLine: 363-5256 Internet e-mail and Sametime ID: barre...@us.ibm.com From: Daniel Kulp <dk...@apache.org> To: commons-dev@ws.apache.org Cc: wood...@apache.org Date: 03/23/2009 12:25 PM Subject: Re: svn commit: r757453 - in /webservices/commons/trunk/modules/XmlSchema/src: main/java/org/apache/ws/commons/schema/SchemaBuilder.java test/java/tests/SchemaBuilderCacheTest.java Couple questions about this commit: 1) Why not just use a ThreadLocal<Map<....>> instead of mimicking one with a HashMap based on thread id? That way, if the Thread goes away before someone clears it, we know the the data will get garbage collected. 2) Collections.synchronizedMap(new Hashtable<String, SoftReference<XmlSchema>>()) Uh..... Hashtable is already synchronized. Should that be a HashMap? Dan On Mon March 23 2009 1:14:57 pm wood...@apache.org wrote: > Author: woodroy > Date: Mon Mar 23 17:14:56 2009 > New Revision: 757453 > > URL: http://svn.apache.org/viewvc?rev=757453&view=rev > Log: > Update initCache and clearCache to account for multi-threaded case. > Essentially, one thread could change the static 'resolvedSchema' variable > from null to non-null after another thread found the value to be null. > > Contributor: Jeff Barrett via Roy Wood > > > Added: > > webservices/commons/trunk/modules/XmlSchema/src/test/java/tests/SchemaBuild >erCacheTest.java Modified: > > webservices/commons/trunk/modules/XmlSchema/src/main/java/org/apache/ws/com >mons/schema/SchemaBuilder.java > > Modified: > webservices/commons/trunk/modules/XmlSchema/src/main/java/org/apache/ws/com >mons/schema/SchemaBuilder.java URL: > http://svn.apache.org/viewvc/webservices/commons/trunk/modules/XmlSchema/sr >c/main/java/org/apache/ws/commons/schema/SchemaBuilder.java?rev=757453&r1=75 >7452&r2=757453&view=diff > =========================================================================== >=== --- > webservices/commons/trunk/modules/XmlSchema/src/main/java/org/apache/ws/com >mons/schema/SchemaBuilder.java (original) +++ > webservices/commons/trunk/modules/XmlSchema/src/main/java/org/apache/ws/com >mons/schema/SchemaBuilder.java Mon Mar 23 17:14:56 2009 @@ -24,6 +24,7 @@ > import java.util.Collections; > import java.util.HashMap; > import java.util.HashSet; > +import java.util.Hashtable; > import java.util.List; > import java.util.Map; > import java.util.Set; > @@ -66,7 +67,8 @@ > * initialize resolvedSchemas to non-null Clearing of cache is done by > calling clearCache() which will * clear and nullify resolvedSchemas > */ > - private static Map<String, SoftReference<XmlSchema>> resolvedSchemas; > + > + private static Map<String, Map<String, SoftReference<XmlSchema>>> > resolvedSchemas; private static final String[] RESERVED_ATTRIBUTES_LIST = > {"name", "type", "default", @@ -109,16 +111,108 @@ > currentSchema = new XmlSchema(); > } > > + > + /** > + * Setup the cache to be used by the current thread of execution. > Multiple + * threads can use the cache, and each one must call this > method at + * some point prior to attempting to resolve the first > schema, or the cache + * will not be used on that thread. > + * > + * IMPORTANT: The thread MUST call clearCache() when it is done with > the + * schemas or a large amount of memory may remain in-use. > + */ > + public static synchronized void initCache() { > + > + if (resolvedSchemas == null) { > + resolvedSchemas = > + Collections.synchronizedMap(new HashMap<String, Map<String, > SoftReference<XmlSchema>>>()); + } > + > + String threadID = String.valueOf(Thread.currentThread().getId()); > + > + Map<String, SoftReference<XmlSchema>> threadResolvedSchemas = > + resolvedSchemas.get(threadID); > + > + // If there is no entry yet for this thread ID, then create one > + if (threadResolvedSchemas == null) { > + threadResolvedSchemas = Collections.synchronizedMap(new > Hashtable<String, SoftReference<XmlSchema>>()); + > resolvedSchemas.put(threadID, threadResolvedSchemas); + > + } > + } > + > + /** > + * Remove any entries from the cache for the current thread. Entries > for + * other threads are not altered. > + */ > public static synchronized void clearCache() { > if (resolvedSchemas != null) { > - resolvedSchemas.clear(); // necessary? > - resolvedSchemas = null; > + String threadID = > String.valueOf(Thread.currentThread().getId()); + // If there > are entries for this thread ID, then clear them and + // remove > the entry > + Map<String, SoftReference<XmlSchema>> threadResolvedSchemas = > + resolvedSchemas.get(threadID); > + > + if (threadResolvedSchemas != null) { > + threadResolvedSchemas.clear(); > + resolvedSchemas.remove(threadID); > + } > + } > + } > + > + /** > + * Return a cached schema if one exists for this thread. In order for > schemas to be cached + * the thread must have done an initCache() > previously. > + * The parameters are used to construct a key used to lookup the > schema + * @param targetNamespace > + * @param schemaLocation > + * @param baseUri > + * @return The cached schema if one exists for this thread or null. > + */ > + private XmlSchema getCachedSchema(String targetNamespace, > + String schemaLocation, String baseUri) { > + > + XmlSchema resolvedSchema = null; > + > + if (resolvedSchemas != null) { // cache is initialized, use it > + String threadID = > String.valueOf(Thread.currentThread().getId()); + Map<String, > SoftReference<XmlSchema>> threadResolvedSchemas = + > resolvedSchemas.get(threadID); > + if (threadResolvedSchemas != null) { > + // Not being very smart about this at the moment. One > could, for example, + // see that the schemaLocation or > baseUri is the same as another, but differs + // only by a > trailing slash. As it is now, we assume a single character difference + > // means it's a schema that has yet to be resolved. + > String schemaKey = targetNamespace + schemaLocation + baseUri; + > SoftReference<XmlSchema> softref = > threadResolvedSchemas.get(schemaKey); + if (softref != null) > { > + resolvedSchema = softref.get(); > + } > + } > } > + return resolvedSchema; > } > > - public static synchronized void initCache() { > - if (resolvedSchemas == null) { > - resolvedSchemas = Collections.synchronizedMap(new > HashMap<String, SoftReference<XmlSchema>>()); + /** > + * Add an XmlSchema to the cache if the current thread has the cache > enabled. + * The first three parameters are used to construct a key > + * @param targetNamespace > + * @param schemaLocation > + * @param baseUri > + * This parameter is the value put under the key (if the cache is > enabled) + * @param readSchema > + */ > + private void putCachedSchema(String targetNamespace, String > schemaLocation, + String baseUri, XmlSchema readSchema) { > + > + if (resolvedSchemas != null) { > + String threadID = > String.valueOf(Thread.currentThread().getId()); + Map<String, > SoftReference<XmlSchema>> threadResolvedSchemas = + > resolvedSchemas.get(threadID); > + if (threadResolvedSchemas != null) { > + String schemaKey = targetNamespace + schemaLocation + > baseUri; + threadResolvedSchemas.put(schemaKey, new > SoftReference<XmlSchema>(readSchema)); + } > } > } > > @@ -917,22 +1011,10 @@ > XmlSchema resolveXmlSchema(String targetNamespace, String > schemaLocation, String baseUri, TargetNamespaceValidator validator) { > > - String schemaKey = null; > - if (resolvedSchemas != null) { // cache is initialized, use it > - // Not being very smart about this at the moment. One could, > for example, - // see that the schemaLocation or baseUri is the > same as another, but differs - // only by a trailing slash. As > it is now, we assume a single character difference - // means > it's a schema that has yet to be resolved. > - schemaKey = Thread.currentThread().getId() + targetNamespace + > schemaLocation + baseUri; - SoftReference<XmlSchema> softref = > resolvedSchemas.get(schemaKey); - if (softref != null) { > - XmlSchema resolvedSchema = softref.get(); > - if (resolvedSchema != null) { > - return resolvedSchema; > - } > - } > + if (getCachedSchema(targetNamespace, schemaLocation, baseUri) != > null) { + return getCachedSchema(targetNamespace, > schemaLocation, baseUri); } > - > + > // use the entity resolver provided if the schema location is > present null if (schemaLocation != null && !"".equals(schemaLocation)) { > InputSource source = > collection.getSchemaResolver().resolveEntity(targetNamespace, @@ -958,9 > +1040,7 @@ > collection.push(key); > try { > XmlSchema readSchema = collection.read(source, null, > validator); - if (resolvedSchemas != null) { > - resolvedSchemas.put(schemaKey, new > SoftReference<XmlSchema>(readSchema)); - } > + putCachedSchema(targetNamespace, schemaLocation, > baseUri, readSchema); return readSchema; > } catch (Exception e) { > throw new RuntimeException(e); > > Added: > webservices/commons/trunk/modules/XmlSchema/src/test/java/tests/SchemaBuild >erCacheTest.java URL: > http://svn.apache.org/viewvc/webservices/commons/trunk/modules/XmlSchema/sr >c/test/java/tests/SchemaBuilderCacheTest.java?rev=757453&view=auto > =========================================================================== >=== --- > webservices/commons/trunk/modules/XmlSchema/src/test/java/tests/SchemaBuild >erCacheTest.java (added) +++ > webservices/commons/trunk/modules/XmlSchema/src/test/java/tests/SchemaBuild >erCacheTest.java Mon Mar 23 17:14:56 2009 @@ -0,0 +1,424 @@ > +/* > + * Licensed to the Apache Software Foundation (ASF) under one > + * or more contributor license agreements. See the NOTICE file > + * distributed with this work for additional information > + * regarding copyright ownership. The ASF licenses 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 tests; > + > +import java.lang.ref.SoftReference; > +import java.lang.reflect.Field; > +import java.util.Hashtable; > +import java.util.Map; > + > +import javax.xml.parsers.DocumentBuilderFactory; > + > +import org.apache.ws.commons.schema.SchemaBuilder; > +import org.apache.ws.commons.schema.XmlSchema; > +import org.apache.ws.commons.schema.XmlSchemaCollection; > +import org.w3c.dom.Document; > + > +import org.junit.Assert; > +import org.junit.Test; > +import org.junit.Ignore; > + > +/** > + * Test the resolved Schema cache. > + */ > +public class SchemaBuilderCacheTest extends Assert { > + > + /** > + * Test that if the cache is not initialized, then it should not be > used when a schema is read. + * @throws Exception > + */ > + @Test > + public void testResolveCacheUninitialized() throws Exception { > + Document doc = setupDocument(); > + XmlSchemaCollection schemaCol = setupXmlSchemaCollection(); > + XmlSchema schema = schemaCol.read(doc,null); > + assertNotNull(schema); > + > + // If the cache is not in use, then it should be null > + assertNull(getResolvedSchemasHashtable()); > + } > + > + /** > + * Test if the cache is initialized it will be populated when a schema > is read and it will + * be cleared when the clearCache method is > called. > + * @throws Exception > + */ > + @Test > + public void testResolveCacheInitialized() throws Exception { > + try { > + SchemaBuilder.initCache(); > + Document doc = setupDocument(); > + XmlSchemaCollection schemaCol = setupXmlSchemaCollection(); > + XmlSchema schema = schemaCol.read(doc, null); > + assertNotNull(schema); > + > + // If the cache is in use, it should not be null and there > should + // be an entry for this thread ID > + assertNotNull(getResolvedSchemasHashtable()); > + Map<String, SoftReference<XmlSchema>> threadHT = > getThreadResolvedSchemaHashtable(); + assertNotNull(threadHT); > + assertFalse(threadHT.isEmpty()); > + assertEquals(1, threadHT.size()); > + > + // After clearing the cache, there should be no entry for this > thread ID, and + // the hashtable should not be null > + SchemaBuilder.clearCache(); > + assertNotNull(getResolvedSchemasHashtable()); > + assertNull(getThreadResolvedSchemaHashtable()); > + System.out.println("Line 13"); > + } finally { > + resetResolvedSchemasHashtable(); > + } > + > + // If the cache is enabled, then it should be non-null > + } > + > + /** > + * Test that threads can not affect the cache for other threads. > + */ > + @Test > + public void testMultithreadCache() { > + try { > + MultithreadUpdateLockMonitor testMonitor = new > MultithreadUpdateLockMonitor(); + > startupTestThreads(testMonitor); > + > + if (testMonitor.t1Exception != null) { > + fail("Thread T1 encountred an error: " > + + testMonitor.t1Exception.toString()); > + } > + if (testMonitor.t2Exception != null) { > + fail("Thread T2 encountred an error: " > + + testMonitor.t2Exception.toString()); > + } > + if (testMonitor.t3Exception != null) { > + fail("Thread T3 encountred an error: " > + + testMonitor.t3Exception.toString()); > + } > + } finally { > + resetResolvedSchemasHashtable(); > + } > + } > + > + > //========================================================================= >===================== + // Utility Methods > + > //========================================================================= >===================== + > + static Document setupDocument() { > + DocumentBuilderFactory documentBuilderFactory = > DocumentBuilderFactory.newInstance(); + > documentBuilderFactory.setNamespaceAware(true); > + Document doc; > + try { > + doc = documentBuilderFactory.newDocumentBuilder(). > + parse(Resources.asURI("importBase.xsd")); > + } catch (Exception e) { > + throw new RuntimeException(e); > + } > + return doc; > + } > + > + static XmlSchemaCollection setupXmlSchemaCollection() { > + XmlSchemaCollection schemaCol = new XmlSchemaCollection(); > + schemaCol.setBaseUri(Resources.TEST_RESOURCES); > + return schemaCol; > + } > + > + /** > + * Override the protection on the resolvedSchemas attribute in the > SchemaBuilder class + * and return its current value. > + * @return value of Hashtable resolvedSchemas, which may be null > + * @throws Exception If there are problems with Java reflection > (should not happen) + */ > + static Map<String, Map<String, SoftReference<XmlSchema>>> > getResolvedSchemasHashtable(){ + Map<String, Map<String, > SoftReference<XmlSchema>>> ht = null; + try { > + Field field = > SchemaBuilder.class.getDeclaredField("resolvedSchemas"); + > field.setAccessible(true); > + ht = (Map<String, Map<String, > SoftReference<XmlSchema>>>)field.get(null); + } catch (Exception e) > { > + throw new RuntimeException(e); > + } > + return ht; > + } > + > + /** > + * Return the Hashtable for the current thread or null if there is not > one. + * @return > + */ > + static Map<String, SoftReference<XmlSchema>> > getThreadResolvedSchemaHashtable() { + Map<String, > SoftReference<XmlSchema>> threadHashtable = null; + Map<String, > Map<String, SoftReference<XmlSchema>>> ht = getResolvedSchemasHashtable(); > + if (ht != null) { > + String threadID = > String.valueOf(Thread.currentThread().getId()); + > threadHashtable = (Map<String, SoftReference<XmlSchema>>) ht.get(threadID); > + } > + return threadHashtable; > + } > + > + /** > + * Set the resolvedSchemas collection to null. This should be done in > a finally block + * of any tests that cause an SchemaBuilder.initCache > to be done to cleanup for the next + * test. > + */ > + static void resetResolvedSchemasHashtable() { > + try { > + Field field = > SchemaBuilder.class.getDeclaredField("resolvedSchemas"); + > field.setAccessible(true); > + Map<String, Map<String, SoftReference<XmlSchema>>> ht = > (Map<String, Map<String, SoftReference<XmlSchema>>>) field.get(null); + > if (ht != null) { > + ht.clear(); > + field.set(null, null); > + } > + }catch (Exception e) { > + throw new RuntimeException(e); > + } > + } > + > + // Amount of time the testcase should wait on the test threads before > timing out + private static int THREAD_TIMEOUT = 90000; > + > + /** > + * Configure and start the test threads for the multi-threaded > testing. The threads will + * perform various tests between themselves > such as clearing cache in one thread and making + * sure the cache used > by a different thread is not affected. A monitor is used to control + > * the synchonization between the threads and for communicating faliures > back to the test + * method. > + * > + * See thed Runnable classes for details on the tests performed. > + * > + * @param testMonitor Used to synchronize the tests between the > threads + */ > + private void startupTestThreads(MultithreadUpdateLockMonitor > testMonitor) { + TestingRunnable1 testRunnable1 = new > TestingRunnable1(); > + testRunnable1.testMonitor = testMonitor; > + > + TestingRunnable2 testRunnable2 = new TestingRunnable2(); > + testRunnable2.testMonitor = testMonitor; > + > + TestingRunnable3 testRunnable3 = new TestingRunnable3(); > + testRunnable3.testMonitor = testMonitor; > + > + Thread thread1 = new Thread(testRunnable1); > + Thread thread2 = new Thread(testRunnable2); > + Thread thread3 = new Thread(testRunnable3); > + > + thread1.start(); > + thread2.start(); > + thread3.start(); > + > + // Join the threads to wait for their completion, specifying a > timeout to prevent + // a testcase hang if something goes wrong with > the threads. + try { > + thread1.join(THREAD_TIMEOUT); > + thread2.join(THREAD_TIMEOUT); > + thread3.join(THREAD_TIMEOUT); > + } catch (InterruptedException e) { > + e.printStackTrace(); > + fail("Unable to join to testing threads"); > + } > + } > +} > + > +/** > + * Monitor used to control synchronization between the testing threads and > communicate failures + * back to the test method. > + */ > +class MultithreadUpdateLockMonitor { > + boolean t1SetupComplete = false; > + boolean t2SetupComplete = false; > + Exception t1Exception = null; > + Exception t2Exception = null; > + Exception t3Exception = null; > +} > +//======================================================================== >========================= +// Test execution threads > +//======================================================================== >========================= +/** > + * Thread 1 will do the following > + * - Initialize the cache and verify it was used during a read > + * - Unblock Thread 2 > + * - Wait until Thread 2 unblocks it > + * - Verify that the clearCache done by Thread 2 did not affect this > Thread's cache. + */ > +...@ignore > +class TestingRunnable1 implements Runnable { > + > + MultithreadUpdateLockMonitor testMonitor = null; > + > + public void run() { > + SchemaBuilder.initCache(); > + Document doc = SchemaBuilderCacheTest.setupDocument(); > + XmlSchemaCollection schemaCol = > SchemaBuilderCacheTest.setupXmlSchemaCollection(); + XmlSchema > schema = schemaCol.read(doc, null); > + if (schema == null) { > + testMonitor.t1Exception = new Exception("Schema was null"); > + } > + > + // If the cache is in use, it should not be null and there should > be an entry for this + // Thread > + if (SchemaBuilderCacheTest.getResolvedSchemasHashtable() == null) > { + testMonitor.t1Exception = new Exception("resolvedSchemas was > null"); + } > + Map<String, SoftReference<XmlSchema>> threadHT = > SchemaBuilderCacheTest.getThreadResolvedSchemaHashtable(); + if > (threadHT == null ) { > + testMonitor.t1Exception = new Exception("Thread > resolvedSchemas was null"); + } > + > + if (threadHT.isEmpty()) { > + testMonitor.t1Exception = new Exception("Thread > resolvedSchemas was empty"); + } > + > + synchronized(testMonitor) { > + testMonitor.t1SetupComplete = true; > + testMonitor.notifyAll(); > + while (!testMonitor.t2SetupComplete) { > + try { > + testMonitor.wait(); > + } catch (InterruptedException e) { > + e.printStackTrace(); > + testMonitor.t1Exception = new RuntimeException(e); > + throw (RuntimeException) testMonitor.t1Exception; > + } > + } > + } > + > + // After the other thread does a reset, the cache for this thread > should NOT be null + if > (SchemaBuilderCacheTest.getResolvedSchemasHashtable() == null) { + > testMonitor.t1Exception = new Exception("resolvedSchemas was null after > reset"); + } > + threadHT = > SchemaBuilderCacheTest.getThreadResolvedSchemaHashtable(); + if > (threadHT == null ) { > + testMonitor.t1Exception = new Exception("Thread > resolvedSchemas was null after clear"); + } > + > + if (threadHT.isEmpty()) { > + testMonitor.t1Exception = new Exception("Thread > resolvedSchemas was empty after clear"); + } > + > + // Issue our a clear on this TID, and now there should be no > entires for it + SchemaBuilder.clearCache(); > + if (SchemaBuilderCacheTest.getResolvedSchemasHashtable() == null) > { + testMonitor.t1Exception = new Exception("resolvedSchemas was > null after clear on TID"); + } > + > + threadHT = > SchemaBuilderCacheTest.getThreadResolvedSchemaHashtable(); + if > (threadHT != null ) { > + testMonitor.t1Exception = new Exception("Thread > resolvedSchemas was not null after clear"); + } > + } > +} > + > +/** > + * Thread 2 will: > + * - Block until released by Thread 1 > + * - Initialize the cache then make sure it was used for a resolve on this > thread + * - clear the cache and make sure the entries for this Thread a > removed + * - Unblock Thread 1 to make sure the clear did not affect it > + */ > +...@ignore > +class TestingRunnable2 implements Runnable { > + > + MultithreadUpdateLockMonitor testMonitor = null; > + > + public void run() { > + synchronized (testMonitor) { > + while (!testMonitor.t1SetupComplete) { > + try { > + testMonitor.wait(); > + } catch (InterruptedException e) { > + e.printStackTrace(); > + testMonitor.t2Exception = new RuntimeException(e); > + throw (RuntimeException) testMonitor.t2Exception; > + } > + } > + } > + SchemaBuilder.initCache(); > + Document doc = SchemaBuilderCacheTest.setupDocument(); > + XmlSchemaCollection schemaCol = > SchemaBuilderCacheTest.setupXmlSchemaCollection(); + XmlSchema > schema = schemaCol.read(doc, null); > + if (schema == null) { > + testMonitor.t2Exception = new Exception("Schema was null"); > + } > + > + // If the cache is in use, it should not be null. > + if (SchemaBuilderCacheTest.getResolvedSchemasHashtable() == null) > { + testMonitor.t2Exception = new Exception("resolvedSchemas was > null"); + } > + Map<String, SoftReference<XmlSchema>> threadHT = > SchemaBuilderCacheTest.getThreadResolvedSchemaHashtable(); + if > (threadHT == null ) { > + testMonitor.t2Exception = new Exception("Thread > resolvedSchemas was null"); + } > + if (threadHT.isEmpty()) { > + testMonitor.t2Exception = new Exception("Thread > resolvedSchemas was empty"); + } > + > + // Issue our a clear on this TID, and now there should be no > entires for it + SchemaBuilder.clearCache(); > + if (SchemaBuilderCacheTest.getResolvedSchemasHashtable() == null) > { + testMonitor.t2Exception = new Exception("resolvedSchemas was > null after clear on TID"); + } > + > + threadHT = > SchemaBuilderCacheTest.getThreadResolvedSchemaHashtable(); + if > (threadHT != null ) { > + testMonitor.t2Exception = new Exception("Thread > resolvedSchemas was not null after clear"); + } > + > + synchronized (testMonitor) { > + testMonitor.t2SetupComplete = true; > + testMonitor.notifyAll(); > + } > + } > +} > + > +/** > + * Test that the init done by Thread 1 does NOT cause the cache to be used > by this thread + * which did not do an init. > + */ > +...@ignore > +class TestingRunnable3 implements Runnable { > + MultithreadUpdateLockMonitor testMonitor = null; > + > + public void run() { > + synchronized (testMonitor) { > + while (!testMonitor.t1SetupComplete) { > + try { > + testMonitor.wait(); > + } catch (InterruptedException e) { > + e.printStackTrace(); > + testMonitor.t3Exception = new RuntimeException(e); > + throw (RuntimeException) testMonitor.t2Exception; > + } > + } > + } > + Document doc = SchemaBuilderCacheTest.setupDocument(); > + XmlSchemaCollection schemaCol = > SchemaBuilderCacheTest.setupXmlSchemaCollection(); + XmlSchema > schema = schemaCol.read(doc, null); > + if (schema == null) { > + testMonitor.t3Exception = new Exception("Schema was null"); > + } > + Map<String, SoftReference<XmlSchema>> threadHT = > SchemaBuilderCacheTest.getThreadResolvedSchemaHashtable(); + if > (threadHT != null ) { > + testMonitor.t3Exception = new Exception("Thread > resolvedSchemas was not null"); + } > + > + > + } > + > +} -- Daniel Kulp dk...@apache.org http://www.dankulp.com/blog