I added information on https://cwiki.apache.org/confluence/display/JENA/SPI+Contract+Details to cover this discussion. Please review and comment.
On Sun, Aug 24, 2014 at 2:19 PM, Andy Seaborne <a...@apache.org> wrote: > On 23/08/14 18:07, Claude Warren wrote: > >> Andy, >> >> I think we agree on transactions. >> >> I think the difference is in the understanding of when listeners are >> triggered. >> >> I realize that all current implementations of listeners appear to be on a >> single thread. But I did not realize that was a requirement of the >> listener interface. (yes that would be part of the listener contract >> test). I would think that a listener could place messages on a queue and >> that would be sufficient to meet the listener interface -- but that would >> mean the state within a transaction would be visible outside of the >> transaction. >> > > Fine - that is building a higher-level mechanism on top of the basic > callback system. > > The fact you can leak information from inside a transaction is nothing > special here. You can do it normally just by missing bytes across > threads. Don't! or at least, do with care. > > You can in JDBC as well. Use multiple connections. Or store in a > variable, abort the transaction and start again. > > > I have wanted the ability for one thread to be notified when another >> thread >> completed a transaction -- basically when the changes became visible. But >> for now that appears to be outside the scope of a listener. >> > > It's certainly outside the scope of an add() - transactions are groups of > add/delete. > > You want transaction monitoring - callbacks on transaction operations. > > > As listeners are same thread callbacks, does this mean that when a >> transaction is rolled back the listeners must be notified to undo the >> previous notifications -- for example >> >> begin Tx >> add T1 >> listener notified of add T1 >> rollback Tx >> listener notified of delete T1 >> > > I'd say "no". T1 is not deleted (there is no delete() call). > > You have passed information out of a transaction. > > > If not then we need to document this case. >> >> To be honest it is this issue that made me think that listeners should be >> notified after the transaction committed. If listeners are notified after >> commit then they can also be on different threads. >> > > That is a higher level abstraction - we should be able to support writing > that but not provide it as the one design. There are many different designs > which is why listeners were made the basic building block on which > applications can do what they want. > > Andy > > >> Claude >> >> >> >> >> >> On Sat, Aug 23, 2014 at 4:22 PM, Andy Seaborne <a...@apache.org> wrote: >> >> Claude, >>> >>> We seem to have different understandings about transactions. >>> >>> I see a transaction (as in ACID) as defining a scope or view of the >>> system >>> (or valid state of the system). Within a transaction changes happen only >>> from actions of the transaction, not outside. >>> >>> A transaction sees a consistent state of the world - transactions are >>> serialized onto the time line and appear to happen instantaneously as a >>> single unit. From outside, nothing changes until all of a sudden all >>> changes are made at once. These are "serializable" isolation which is >>> the >>> ideal. >>> >>> Weaker forms of isolation exist but they have unpredictable effects. For >>> us, a find() is a range query so even isolation level "repeatable reads" >>> can cause a find to see a state of the storage that never existed in any >>> application view point. >>> >>> http://en.wikipedia.org/wiki/Isolation_%28database_systems%29 >>> >>> We aren't considering nested transactions. >>> >>> add() just needs to define what add() does inside a transaction. At some >>> later time, all the change become visible. >>> >>> == add() >>> >>> Pre condition: >>> graph exists >>> >>> action: >>> add(t) >>> >>> Post condition: >>> in the view of the transaction for the current thread: >>> if no exception >>> graph contains t >>> else if AddDeniedException >>> graph contains t if and only if the graph contained t before. >>> >>> Listeners are same-thread callbacks so they are in the same transaction >>> as >>> the update. Complex systems on top of this are out of scope. Jena >>> provides >>> the building block. >>> >>> >>> On 17/08/14 11:43, Claude Warren wrote: >>> >>> I think the contract has to cover multi-threaded possibilities. >>>> However, >>>> for the most part the document I originally proposed is the view from >>>> within a single thread. >>>> >>>> >>> >>> For non-transactional, multi-threaded systems, I don't think anything >>> needs to said except "don't!!" - or rather "single view" or else all bets >>> are off. Failure modes are way too implementation specific - even across >>> JVMs (see IBM vs oracle JVMs for HashMap as we have know here). >>> >>> Jena in-memory is read-concurrent safe. >>> http://jena.apache.org/documentation/notes/concurrency-howto.html >>> >>> Even that is non-trivial to provide in the inference engine. >>> >>> >>> I agree that graphAdd serves no purpose and go as far as saying it >>> should >>> >>>> be removed in Jena 3. >>>> >>>> >>> Yes. >>> >>> Think that defining the add with the listener will clarify the >>> contract, >>> >>>> but we need clarification of the Listener contract later. >>>> >>>> I think that the current process is: >>>> >>>> 1. triple added to or deleted from graph >>>> 2. listeners notified >>>> >>>> >>>> I think that this is correct but that we need to add that exceptions in >>>> the >>>> listeners may not raise and add denied exception. >>>> >>>> >>> s/and/an/ ? If so - yes. >>> >>> How about: >>> >>> 1. listeners should not raise exceptions. >>> 2. If they do (outside the contract), the exception should be (logged >>> and) >>> dropped. >>> >>> It seems odd to me to have an exception and the triple be added. >>> >>> I believe that the >>> >>>> contract with listeners is: >>>> >>>> 1. they are notified after the event they are listening for has >>>> been >>>> >>>> completed. That they are not notified if an Exception is thrown in >>>> the add. >>>> 2. if a listener throws an exception it will not undo the add or >>>> delete. >>>> >>>> >>> Yes. >>> >>> 3. I believe that: #1 means that the listeners would be notified at >>> >>>> the >>>> >>>> commit of a transaction, so listeners are guaranteed to have >>>> messages >>>> queued by the end of the commit (if present) or at the end of add >>>> (if >>>> no >>>> transaction is present). >>>> >>>> >>> A basic listener is inside the transaction where the add() is happening.. >>> They are on the same thread anyway. I don't know how to implement >>> same-thread, different visibility. >>> >>> I wonder if listeners can be described with a separate contact - makes >>> the >>> contract tests modular. >>> >>> e.g. >>> C1/ Contract for add/delete/find/and others as actions of set of triples. >>> >>> C2/ Contract for listeners where >>> >>> add-with-listener => core add contract + listener called. >>> >>> This does lead to the possibility that a graph implementation may need >>> to >>> >>>> notify other components within the transaction that the add or delete >>>> was >>>> completed -- I am not certain that this is needed but raise the point >>>> here >>>> for further discussion if necessary. >>>> >>>> So the full process for an add is >>>> >>>> 1. begin add( triple ) >>>> 2. if adding is not allowed (Capabilities.addAllowed() returns >>>> false) >>>> throw AddDeniedException. >>>> 3. add to the underlying storage system, may throw an exception. >>>> 1. If a checked exception is thrown wrap it in an >>>> AddDeniedException. >>>> >>>> >>> Any other kind of exception is presumably a system error and leaves the >>> system in unknown state. >>> >>> 4. if not in a transaction notify listeners of add >>> >>>> 5. end add(triple) >>>> >>>> >>> "end add" means return to caller? >>> >>> So far, so good. >>> >>> 6. begin commit if in transaction >>> >>>> 7. commit the change so that it is visible to outside of the >>>> transaction. >>>> 8. notify listeners of add. >>>> 9. end commit. >>>> >>>> >>> I don't understand this. Are you trying for JDBC autocommit effects? >>> >>> See overall comments on transactions. >>> >>> Illustration: >>> W1, R1 R2 R3 -- transactions. >>> >>> Thread 1 Thread 2 >>> begin W1 >>> add t1 >>> add t2 >>> begin R1 -find-end R1 (sees no triples) >>> add t3 >>> find (sees 3 triples) >>> add t4 >>> begin R2-find-end R2 (sees no triples) >>> delete t2 >>> commit W1 >>> begin R3-find-end R3 sees t1 t3 t4 >>> >>> At no point is t2 visible outside thread 1. >>> >>> At no point are exactly triples t1 and t3 but not t4 visible outside >>> thread 1. >>> >>> Strictly, R3 is either see t1, t3, t4 or see no triples. There is no >>> guarantee on the exact time point. A detail of transactions. >>> >>> Autocommit where an implicit begin-commit goes round any add call that is >>> not made from a thread in a transaction is a possibility. >>> >>> i.e. >>> operation X >>> >>> if not in a transaction >>> => >>> begin >>> operation X >>> commit >>> >>> BUT this is very, very expensive when it's apserisstent storage to get D >>> durability. >>> >>> To get D you need a disk write so ~5-10ms of rotational disk (disk seek >>> time), 0.1ms if an SSD but it is also a system call (virtual memory >>> costs) >>> and still has to contend for the SSD controller. Adding a commit on >>> every >>> triple add reduces the maximum update rate to 10K triples per second in >>> ideal circumstances without any OS costs. Taht's dire. Batching wins! >>> ] >>> >>> c.f. JDBC where it is usually default "on" (safety) and leads to other >>> issues of dire performance at this granularity. >>> >>> If that it the case then the full process for a delete is >>> >>>> >>>> 1. begin delete( triple ) >>>> 2. if deleting is not allowed (Capabilities.deleteAllowed() returns >>>> false) throw DeleteDeniedException. >>>> 3. delete from the underlying storage system, may throw an >>>> exception. >>>> 1. If a checked exception is thrown wrap it in a >>>> DeleteDeniedException. >>>> 4. if not in a transaction notify listeners of delete >>>> 5. end delete(triple) >>>> 6. begin commit if in transaction >>>> 7. commit the change so that it is visible to outside of the >>>> transaction. >>>> 8. notify listeners of delete. >>>> 9. end commit. >>>> >>>> >>>> As for the find process >>>> >>>> 1. returns an ExtendedIterator of triples that match the specified >>>> triple. >>>> 2. If inside a transaction all uncommited triples are candidates >>>> for >>>> >>>> matching. >>>> >>>> The iterator may throw a ConcurrentModificationException in conditions >>>> outlined by >>>> http://docs.oracle.com/javase/7/docs/api/java/util/ >>>> ConcurrentModificationException.html >>>> with the following caveat: >>>> >>>> - If the find is taking place within a transaction and the current >>>> >>>> thread has not modified the underlying data the >>>> ConcurrentModificationException may not be thrown. >>>> >>>> >>> We can treat ConcurrentModificationException as an independent concept >>> from transactions. >>> >>> Andy >>> >>> >>> >>> >>>> Thoughs? >>>> Claude >>>> >>>> >>>> >>>> >>>> >>>> On Mon, Aug 11, 2014 at 6:19 PM, Andy Seaborne <a...@apache.org> wrote: >>>> >>>> On 08/08/14 22:13, Claude Warren wrote: >>>> >>>>> >>>>> This is a message stack for Graph SPI Contract testing. It covers >>>>> only >>>>> >>>>>> the >>>>>> Jena 2 Graph Contract. This an attempt to document the current Graph >>>>>> contract. Any correction should specify the bullet point number. >>>>>> >>>>>> >>>>>> Overall: >>>>> >>>>> Getting the exact contract is hard and I'm assuming this is only for >>>>> single-threaded code. >>>>> >>>>> Maybe start with a subset of Graph >>>>> >>>>> .add >>>>> .delete >>>>> .find >>>>> >>>>> then add listeners into the picture >>>>> then define other operations in terms of the primitives: >>>>> >>>>> .contains >>>>> .remove >>>>> .clear >>>>> >>>>> Transactions: >>>>> >>>>> The text around transactions does not distinguish being inside or >>>>> outside >>>>> a transaction. >>>>> >>>>> There are 2 base kinds of graphs - ones in datasets (views) and >>>>> standalone >>>>> ones, then things like InfGraph and other added functionality. >>>>> Transactions >>>>> on view graphs need to be defined in the context of the dataset because >>>>> transactions are connected. >>>>> >>>>> >>>>> 1. add() -- technically from GraphAdd >>>>> >>>>> >>>>>> >>>>>> IMO The "GraphAdd" interface serves no purpose. >>>>> >>>>> 1. when a triple is added to a graph all registered listeners >>>>> must >>>>> >>>>> >>>>>> receive an (add graph triple) message >>>>>> >>>>>> >>>>>> It's hard to define listeners: >>>>> >>>>> Does a listener see the graph before or after the triple is added? >>>>> Is a listener called if AddDeniedException is raised? >>>>> Can a listener cause AddDeniedException to be raised? >>>>> Is the listener guaranted to have been called by the >>>>> time add() returns? >>>>> >>>>> hence the suggestion of starting with just the basic operations. >>>>> >>>>> 2. subsequent graph.contains( triple ) must return true. >>>>> >>>>> 3. If add is performed within a transaction the listeners are >>>>>> not >>>>>> >>>>>> notified until after the commit. >>>>>> 4. If graph is read only (Capabilities.addAllowed() returns >>>>>> false) >>>>>> must throw AddDeniedException >>>>>> >>>>>> >>>>>> 1.1 and 1.2 have "must" text >>>>> >>>>> Surely it's: >>>>> >>>>> Either >>>>> the triple is added >>>>> or >>>>> an AddDeniedException exception is thrown. >>>>> >>>>> 2. clear() >>>>> >>>>> >>>>>> >>>>>> This is like remove(Node.ANY, Node.ANY, Node.ANY) except for the >>>>> listener >>>>> contract? >>>>> >>>>> 1. If the graph can be empty (Capabilities.canBeEmpty()) >>>>> there >>>>> >>>>> should >>>>>> >>>>>> be no triples returned from find( Triple.ANY ) >>>>>> >>>>>> >>>>>> Nothing except tests uses Capabilities.canBeEmpty. >>>>> >>>>> 2. If the graph can not be empty there should only be the >>>>> elements >>>>> >>>>> >>>>>> that were present when the graph was created. >>>>>> >>>>>> >>>>>> This implies part of the contract for create in that create does not >>>>> take >>>>> initial contents. >>>>> >>>>> Graph g2 = view of g1 >>>>> g1 can not be empty >>>>> >>>>> 3. if delete is not allowed (Capabilities.canDelete() is >>>>> >>>>> >>>>>> false) clear() must throw DeleteDeniedException >>>>>> >>>>>> >>>>>> An alternative is that if clear() causes a change, >>>>> DeleteDeniedException >>>>> is raised. >>>>> >>>>> Example - if the empty, read-only graph is cleared, why should >>>>> DeleteDeniedException be raised? >>>>> >>>>> There is a relationship to remove(ANY,ANY,ANY) >>>>> >>>>> 3. close() >>>>> >>>>> 1. after close isClosed() should return true >>>>>> 2. calling close on closed graph should not throw an >>>>>> exception. >>>>>> 3. calling any Graph method other than close() on a closed >>>>>> graph >>>>>> should throw a ClosedException >>>>>> >>>>>> >>>>>> Is there a need for close() long term, if not, then the deatiled >>>>> contract >>>>> is moot. >>>>> >>>>> This form of Graph.close() might work for a basic, storage graph but >>>>> there >>>>> are other cases. >>>>> >>>>> A graph may be a view of another - close is meaningless and is more >>>>> usefully a no-op. >>>>> >>>>> If the graph is from a system wide cache, close() might be a no-op so >>>>> as >>>>> to protect the cache. >>>>> >>>>> 4. contains() >>>>> >>>>> >>>>>> >>>>>> Defined as "find(S,P,O).hasNext()" >>>>> >>>>> 1. returns true if the graph contains the specified triple. >>>>> >>>>> 1. Node.ANY will match any node in the position. >>>>>> 2. if the graph supports transactions and a transaction is in >>>>>> >>>>>> progress the graph will only not show any triples that only >>>>>> exist >>>>>> within >>>>>> the transaction. >>>>>> >>>>>> >>>>>> If an app goes: >>>>> >>>>> begin >>>>> add(triple) >>>>> contains(triple) -> false >>>>> >>>>> it's going to be a bit confusing! >>>>> >>>>> 5. delete() >>>>> >>>>> 1. if delete is not allowed (Capabilities.canDelete() is >>>>>> false) >>>>>> delete() must throw DeleteDeniedException >>>>>> 2. when a triple is deleted from a graph all registered >>>>>> listeners >>>>>> >>>>>> must receive an (delete graph triple) message >>>>>> 3. subsequent graph.contains( triple ) must return false. >>>>>> 4. If add is performed within a transaction the listeners are >>>>>> not >>>>>> >>>>>> notified until after the commit. >>>>>> >>>>>> >>>>>> Same listener issues as add() >>>>> >>>>> 6. dependsOn() >>>>> >>>>> >>>>>> >>>>>> What is this used for nowadays? >>>>> >>>>> 1. true if this graph's content depends on the other graph. >>>>> May >>>>> be >>>>> >>>>> >>>>>> pessimistic (ie return true if it's not sure). Typically true >>>>>> when a graph >>>>>> is a composition of other graphs, eg union. >>>>>> 7. find() >>>>>> 1. returns an iterator of triples that match the specified >>>>>> triple. >>>>>> >>>>>> >>>>>> And the iterator? >>>>> >>>>> Specifically, there are ConcurrentModificationException issues even in >>>>> single threaded code. >>>>> >>>>> 8. getBulkUpdateHandler() -- deprecated / removed -- no tests >>>>> >>>>> 9. getCapabilities() >>>>>> >>>>>> >>>>>> Aside: Capabilities need clearing up. It's too black-and-white. it >>>>> can't >>>>> express the totality of possibilities. >>>>> >>>>> Big question: what use does application code make of capabilities? I >>>>> suspect none, or noe except to flag errors. I can't envisage getting a >>>>> graph that says"addAllowed=false" and doign anything but signalling the >>>>> user that they can't do what ever the task is. Yet it's going to have >>>>> ("should have") error handling code anyway. >>>>> >>>>> Maybe it reduces to >>>>> >>>>> Graph.isReadOnly >>>>> >>>>> I'm unconvinced the add/delete distinction matters. I can think of >>>>> graph >>>>> where there is a difference (append-only) but not of an application >>>>> that >>>>> adapts based on this other than to say "no, can't". >>>>> >>>>> e.g. >>>>> addAllowed( boolean everyTriple ); >>>>> >>>>> Capabilities.handlesLiteralTyping -- can't say "some, not others" >>>>> >>>>> 1. must not return null. >>>>> >>>>> >>>>>> >>>>>> If we retain the current Capabilities, then we need a way to say >>>>> "don't >>>>> know". Some of the capabilities are definite yes/no. >>>>> >>>>> e.g addAllowed -- presumably "yes" on most graphs but what if there is >>>>> a >>>>> security wrapper? Or system resources are >>>>> >>>>> 2. capabilities must match other results. >>>>> >>>>> 1. if not addAllowed() , add must throw exception >>>>>> 2. if not deleteAllowed(), >>>>>> 1. delete must throw exception >>>>>> 2. clear must throw exception >>>>>> >>>>>> >>>>>> clear() of an already empty graph? >>>>> >>>>> 3. if iteratorRemoveAllowed(), iterator from find must >>>>> allow >>>>> >>>>> remove() >>>>>> 4. if canBeEmpty() >>>>>> 1. initial construction must be empty() >>>>>> 2. clear() must be empty. >>>>>> 3. must pass Capabilities contract tests. >>>>>> 10. getEventManager() >>>>>> 1. May not return null >>>>>> 2. Listeners registered with event manager must be notified >>>>>> of >>>>>> changes. >>>>>> 3. EventManager must pass GraphEventManager contract test. >>>>>> 11. getPrefixMapping() >>>>>> 1. May not be null >>>>>> 2. changes to the prefixes managed by the PrefixMapping >>>>>> returned >>>>>> >>>>>> getPrefixMapping() must be reflected in all other >>>>>> PrefixMapping >>>>>> classes >>>>>> from the same graph. >>>>>> >>>>>> >>>>>> I disagree with the defined contract in javadoc! The "same object" is >>>>> horrible!! >>>>> >>>>> 3. Changes made to a prefix mapping within a transaction are >>>>> >>>>> visible >>>>>> >>>>>> outside of the transaction and are not rolled back by the >>>>>> transaction. >>>>>> >>>>>> >>>>>> !! >>>>> >>>>> 4. PrefixMapping must pass the PrefixMapping contract test >>>>> >>>>> 12. getStatisticsHandler() >>>>>> >>>>>> >>>>>> No longer used. >>>>> >>>>> 1. may be null >>>>> >>>>> 2. if not null must pass the GraphStatisticsHandler contract >>>>>> test. >>>>>> 3. all GraphStatisticsHandlers returned must pass >>>>>> handler.equals( >>>>>> handler2 ) >>>>>> 13. getTransactionHandler() >>>>>> 1. may not be null >>>>>> 2. must pass the TransactionHandler contract test. >>>>>> 14. isClosed() >>>>>> 1. must return false when the graph is created. >>>>>> 2. must return true after the close() has been called. >>>>>> 15. isEmpty() >>>>>> 1. must return true when graph is created if >>>>>> Capabilities.canBeEmpty() is true >>>>>> >>>>>> >>>>>> I don't understand this - a graph may be a view of another soit's not >>>>> empty at the start. >>>>> >>>>> 2. must not return true after triples are added >>>>> >>>>> 3. must return true after all triples are deleted if >>>>>> Capabilities.canBeEmpty() is true. >>>>>> 4. must return true after clear() if >>>>>> Capabilities.canBeEmpty() >>>>>> is >>>>>> true. >>>>>> 16. isIsomorphicWith() -- from ( >>>>>> >>>>>> http://www.w3.org/TR/2014/REC-rdf11-concepts-20140225/# >>>>>> section-graph-equality): >>>>>> Two RDF graphs G and G' are isomorphic (that is, they have an >>>>>> identical >>>>>> form) if there is a bijection M between the sets of nodes of the >>>>>> two >>>>>> graphs, such that: >>>>>> 1. M maps blank nodes to blank nodes. >>>>>> 2. M(lit)=lit for all RDF literals lit which are nodes of G. >>>>>> 3. M(iri)=iri for all IRIs iri which are nodes of G. >>>>>> 4. The triple ( s, p, o ) is in G if and only if the triple ( >>>>>> M(s), >>>>>> >>>>>> p, M(o) ) is in G' >>>>>> 17. remove() >>>>>> 1. when a triple is removed from a graph all registered >>>>>> listeners >>>>>> >>>>>> must receive an (remove graph triple) message >>>>>> >>>>>> >>>>>> remove() removes by pattern >>>>> >>>>> After remove(S,P,O), contains(S,P,O) is false (S/P/O can be Node.ANY) >>>>> >>>>> 2. subsequent graph.contains( triple ) must return false, >>>>> unless >>>>> >>>>> the >>>>>> >>>>>> triple was is in the newly constructed graph and >>>>>> Capabilities.canBeEmpty() >>>>>> is false. >>>>>> 3. If removed is performed within a transaction the listeners >>>>>> are >>>>>> not >>>>>> >>>>>> notified until after the commit. >>>>>> 4. If delete is denied (Capabilities.deleteAllowed() returns >>>>>> false) >>>>>> must throw DeleteDeniedException >>>>>> 18. size() >>>>>> 1. if Capabilities.sizeAccurate() is true >>>>>> 1. if transactions are supported >>>>>> (TransactionHandler.transactionsSupported() is true) >>>>>> 1. the size from within the transaction must function >>>>>> 1. adding a triple must increment the size of the >>>>>> graph. >>>>>> 2. removing a triple must decrement the size of the >>>>>> graph. >>>>>> 2. the size from outside the transaction must not >>>>>> change >>>>>> 2. if transactions are not in >>>>>> supported (TransactionHandler.transactionsSupported() is >>>>>> false) >>>>>> 1. adding a triple must increment the size of the >>>>>> graph. >>>>>> 2. removing a triple must decrement the size of the >>>>>> graph. >>>>>> 2. if Capabilities.sizeAccurate() is false >>>>>> 1. if transactions are supported >>>>>> (TransactionHandler.transactionsSupported() is true) >>>>>> 1. the size from within the transaction must function >>>>>> 1. adding a triple may increment the size of the >>>>>> graph. >>>>>> 2. adding a triple may not decrement the size of the >>>>>> graph. >>>>>> 3. removing a triple may decrement the size of the >>>>>> graph. >>>>>> 4. removing a triple may not increment the size of >>>>>> the >>>>>> graph. >>>>>> 2. the size from outside the transaction must not >>>>>> change >>>>>> 1. adding a triple may not decrement the size of the >>>>>> graph. >>>>>> 2. removing a triple may not increment the size of >>>>>> the >>>>>> graph. >>>>>> 2. if transactions are not in >>>>>> supported (TransactionHandler.transactionsSupported() is >>>>>> false) >>>>>> 1. adding a triple may increment the size of the graph. >>>>>> 2. adding a triple may not decrement the size of the >>>>>> graph. >>>>>> 3. removing a triple may decrement the size of the >>>>>> graph. >>>>>> 4. removing a triple may not increment the size of the >>>>>> graph. >>>>>> >>>>>> >>>>>> >>>>>> Please comment as appropriate. >>>>>> Claude >>>>>> >>>>>> >>>>>> >>>>>> >>>>> >>>> >>>> >>> >> >> > -- I like: Like Like - The likeliest place on the web <http://like-like.xenei.com> LinkedIn: http://www.linkedin.com/in/claudewarren