[dna-commits] DNA SVN: r96 - in trunk: dna-common/src/main/java/org/jboss/dna/common/collection and 38 other directories.

dna-commits at lists.jboss.org dna-commits at lists.jboss.org
Mon Apr 21 22:47:54 EDT 2008


Author: rhauch
Date: 2008-04-21 22:47:53 -0400 (Mon, 21 Apr 2008)
New Revision: 96

Added:
   trunk/dna-common/src/main/java/org/jboss/dna/common/collection/Problem.java
   trunk/dna-common/src/main/java/org/jboss/dna/common/collection/Problems.java
   trunk/dna-repository/
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/RepositoryI18n.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/NodeChange.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/NodeChangeListener.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/NodeChanges.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/ObservationService.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/InvalidRuleSetException.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/RuleService.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/RuleSet.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/RuleSetRepositoryMonitor.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/InvalidSequencerPathExpression.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/Sequencer.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerConfig.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerException.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerOutputMap.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerPathExpression.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencingService.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/StreamSequencerAdapter.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/AbstractServiceAdministrator.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/AdministeredService.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/ServiceAdministrator.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/ExecutionContext.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/JcrTools.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/JndiSessionFactory.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/RepositoryNodePath.java
   trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/SessionFactory.java
   trunk/dna-repository/src/main/resources/org/jboss/dna/repository/
   trunk/dna-repository/src/main/resources/org/jboss/dna/repository/RepositoryI18n.properties
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/observation/
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/observation/NodeChangeTest.java
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleInput.java
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleResult.java
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleServiceTest.java
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleSetTest.java
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/MockSequencerA.java
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/MockSequencerB.java
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/SequencerConfigTest.java
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/SequencerPathExpressionTest.java
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/SequencingServiceTest.java
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/StreamSequencerAdapterTest.java
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/util/
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/util/JndiSessionFactoryTest.java
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/util/SimpleExecutionContext.java
   trunk/dna-spi/
   trunk/dna-spi/.classpath
   trunk/dna-spi/.project
   trunk/dna-spi/pom.xml
   trunk/dna-spi/src/
   trunk/dna-spi/src/main/
   trunk/dna-spi/src/main/java/
   trunk/dna-spi/src/main/java/org/
   trunk/dna-spi/src/main/java/org/jboss/
   trunk/dna-spi/src/main/java/org/jboss/dna/
   trunk/dna-spi/src/main/java/org/jboss/dna/spi/
   trunk/dna-spi/src/main/java/org/jboss/dna/spi/sequencers/
   trunk/dna-spi/src/main/java/org/jboss/dna/spi/sequencers/SequencerOutput.java
   trunk/dna-spi/src/main/java/org/jboss/dna/spi/sequencers/StreamSequencer.java
   trunk/dna-spi/src/main/resources/
   trunk/dna-spi/src/test/
   trunk/dna-spi/src/test/java/
   trunk/dna-spi/src/test/java/org/
   trunk/dna-spi/src/test/java/org/jboss/
   trunk/dna-spi/src/test/java/org/jboss/dna/
   trunk/dna-spi/src/test/java/org/jboss/dna/spi/
   trunk/dna-spi/src/test/java/org/jboss/dna/spi/sequencers/
   trunk/dna-spi/src/test/resources/
   trunk/dna-spi/src/test/resources/log4j.properties
Removed:
   trunk/dna-repository/src/main/java/org/jboss/dna/services/AbstractServiceAdministrator.java
   trunk/dna-repository/src/main/java/org/jboss/dna/services/AdministeredService.java
   trunk/dna-repository/src/main/java/org/jboss/dna/services/ExecutionContext.java
   trunk/dna-repository/src/main/java/org/jboss/dna/services/JndiSessionFactory.java
   trunk/dna-repository/src/main/java/org/jboss/dna/services/RepositoryNodePath.java
   trunk/dna-repository/src/main/java/org/jboss/dna/services/ServiceAdministrator.java
   trunk/dna-repository/src/main/java/org/jboss/dna/services/ServicesI18n.java
   trunk/dna-repository/src/main/java/org/jboss/dna/services/SessionFactory.java
   trunk/dna-repository/src/main/java/org/jboss/dna/services/observation/
   trunk/dna-repository/src/main/java/org/jboss/dna/services/rules/
   trunk/dna-repository/src/main/java/org/jboss/dna/services/sequencers/
   trunk/dna-repository/src/main/java/org/jboss/dna/services/util/
   trunk/dna-repository/src/main/resources/org/jboss/dna/services/
   trunk/dna-repository/src/test/java/org/jboss/dna/services/JndiSessionFactoryTest.java
   trunk/dna-repository/src/test/java/org/jboss/dna/services/SimpleExecutionContext.java
   trunk/dna-repository/src/test/java/org/jboss/dna/services/observation/
   trunk/dna-repository/src/test/java/org/jboss/dna/services/rules/
   trunk/dna-repository/src/test/java/org/jboss/dna/services/sequencers/
   trunk/dna-services/
Modified:
   trunk/
   trunk/dna-integration-tests/pom.xml
   trunk/dna-repository/.project
   trunk/dna-repository/pom.xml
   trunk/dna-repository/src/test/resources/log4j.properties
   trunk/dna-repository/src/test/resources/rule_test.dslr
   trunk/pom.xml
   trunk/sequencers/dna-sequencer-images/pom.xml
   trunk/sequencers/dna-sequencer-images/src/main/java/org/jboss/dna/sequencer/images/ImageMetadataSequencer.java
Log:
DNA-57: Refactor codebase to clearly and cleanly separate SPI and API


Property changes on: trunk
___________________________________________________________________
Name: svn:ignore
   + target


Added: trunk/dna-common/src/main/java/org/jboss/dna/common/collection/Problem.java
===================================================================
--- trunk/dna-common/src/main/java/org/jboss/dna/common/collection/Problem.java	                        (rev 0)
+++ trunk/dna-common/src/main/java/org/jboss/dna/common/collection/Problem.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,120 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.common.collection;
+
+import net.jcip.annotations.Immutable;
+import org.jboss.dna.common.i18n.I18n;
+import org.jboss.dna.common.util.ArgCheck;
+
+/**
+ * @author Randall Hauch
+ */
+ at Immutable
+public class Problem {
+
+    public static final int DEFAULT_CODE = 0;
+
+    public enum Status {
+        ERROR,
+        WARNING,
+        INFO;
+    }
+
+    private final Status status;
+    private final I18n message;
+    private final Object[] parameters;
+    private final Throwable throwable;
+    private final int code;
+    private final String resource;
+    private final String location;
+
+    public Problem( Status status, int code, I18n message, Object... params ) {
+        this(status, code, message, params, null, null, null);
+    }
+
+    public Problem( Status status, int code, I18n message, Object[] params, String resource, String location, Throwable throwable ) {
+        ArgCheck.isNotNull(status, "status");
+        ArgCheck.isNotNull(message, "message");
+        this.status = status;
+        this.code = code;
+        this.message = message;
+        this.parameters = params;
+        this.resource = resource != null ? resource.trim() : null;
+        this.location = location != null ? location.trim() : null;
+        this.throwable = throwable;
+    }
+
+    /**
+     * @return code
+     */
+    public int getCode() {
+        return this.code;
+    }
+
+    /**
+     * @return location
+     */
+    public String getLocation() {
+        return this.location;
+    }
+
+    /**
+     * Get the message written in the current locale.
+     * @return the message
+     */
+    public String getMessageString() {
+        return this.message.text(this.parameters);
+    }
+
+    /**
+     * @return message
+     */
+    public I18n getMessage() {
+        return this.message;
+    }
+
+    public Object[] getParameters() {
+        return this.parameters;
+    }
+
+    /**
+     * @return resource
+     */
+    public String getResource() {
+        return this.resource;
+    }
+
+    /**
+     * @return status
+     */
+    public Status getStatus() {
+        return this.status;
+    }
+
+    /**
+     * @return throwable
+     */
+    public Throwable getThrowable() {
+        return this.throwable;
+    }
+
+}

Added: trunk/dna-common/src/main/java/org/jboss/dna/common/collection/Problems.java
===================================================================
--- trunk/dna-common/src/main/java/org/jboss/dna/common/collection/Problems.java	                        (rev 0)
+++ trunk/dna-common/src/main/java/org/jboss/dna/common/collection/Problems.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,187 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.common.collection;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import net.jcip.annotations.NotThreadSafe;
+import org.jboss.dna.common.i18n.I18n;
+
+/**
+ * @author Randall Hauch
+ */
+ at NotThreadSafe
+public class Problems implements Iterable<Problem> {
+
+    private List<Problem> problems;
+
+    public Problems() {
+    }
+
+    public void addError( I18n message, Object... params ) {
+        addProblem(new Problem(Problem.Status.ERROR, Problem.DEFAULT_CODE, message, params));
+    }
+
+    public void addError( Throwable throwable, I18n message, Object... params ) {
+        addProblem(new Problem(Problem.Status.ERROR, Problem.DEFAULT_CODE, message, null, null, throwable));
+    }
+
+    public void addError( I18n message, String resource, String location, Object... params ) {
+        addProblem(new Problem(Problem.Status.ERROR, Problem.DEFAULT_CODE, message, resource, location));
+    }
+
+    public void addError( Throwable throwable, I18n message, String resource, String location, Object... params ) {
+        addProblem(new Problem(Problem.Status.ERROR, Problem.DEFAULT_CODE, message, resource, location, throwable));
+    }
+
+    public void addError( int code, I18n message, Object... params ) {
+        addProblem(new Problem(Problem.Status.ERROR, code, message));
+    }
+
+    public void addError( Throwable throwable, int code, I18n message, Object... params ) {
+        addProblem(new Problem(Problem.Status.ERROR, code, message, null, null, throwable));
+    }
+
+    public void addError( int code, I18n message, String resource, String location, Object... params ) {
+        addProblem(new Problem(Problem.Status.ERROR, code, message, resource, location));
+    }
+
+    public void addError( Throwable throwable, int code, I18n message, String resource, String location, Object... params ) {
+        addProblem(new Problem(Problem.Status.ERROR, code, message, resource, location, throwable));
+    }
+
+    public void addWarning( I18n message, Object... params ) {
+        addProblem(new Problem(Problem.Status.WARNING, Problem.DEFAULT_CODE, message));
+    }
+
+    public void addWarning( Throwable throwable, I18n message, Object... params ) {
+        addProblem(new Problem(Problem.Status.WARNING, Problem.DEFAULT_CODE, message, null, null, throwable));
+    }
+
+    public void addWarning( I18n message, String resource, String location, Object... params ) {
+        addProblem(new Problem(Problem.Status.WARNING, Problem.DEFAULT_CODE, message, resource, location));
+    }
+
+    public void addWarning( Throwable throwable, I18n message, String resource, String location, Object... params ) {
+        addProblem(new Problem(Problem.Status.WARNING, Problem.DEFAULT_CODE, message, resource, location, throwable));
+    }
+
+    public void addWarning( int code, I18n message, Object... params ) {
+        addProblem(new Problem(Problem.Status.WARNING, code, message));
+    }
+
+    public void addWarning( Throwable throwable, int code, I18n message, Object... params ) {
+        addProblem(new Problem(Problem.Status.WARNING, code, message, null, null, throwable));
+    }
+
+    public void addWarning( int code, I18n message, String resource, String location, Object... params ) {
+        addProblem(new Problem(Problem.Status.WARNING, code, message, resource, location));
+    }
+
+    public void addWarning( Throwable throwable, int code, I18n message, String resource, String location, Object... params ) {
+        addProblem(new Problem(Problem.Status.WARNING, code, message, resource, location, throwable));
+    }
+
+    public void addInfo( I18n message, Object... params ) {
+        addProblem(new Problem(Problem.Status.INFO, Problem.DEFAULT_CODE, message));
+    }
+
+    public void addInfo( Throwable throwable, I18n message, Object... params ) {
+        addProblem(new Problem(Problem.Status.INFO, Problem.DEFAULT_CODE, message, null, null, throwable));
+    }
+
+    public void addInfo( I18n message, String resource, String location, Object... params ) {
+        addProblem(new Problem(Problem.Status.INFO, Problem.DEFAULT_CODE, message, resource, location));
+    }
+
+    public void addInfo( Throwable throwable, I18n message, String resource, String location, Object... params ) {
+        addProblem(new Problem(Problem.Status.INFO, Problem.DEFAULT_CODE, message, resource, location, throwable));
+    }
+
+    public void addInfo( int code, I18n message, Object... params ) {
+        addProblem(new Problem(Problem.Status.INFO, code, message));
+    }
+
+    public void addInfo( Throwable throwable, int code, I18n message, Object... params ) {
+        addProblem(new Problem(Problem.Status.INFO, code, message, null, null, throwable));
+    }
+
+    public void addInfo( int code, I18n message, String resource, String location, Object... params ) {
+        addProblem(new Problem(Problem.Status.INFO, code, message, resource, location));
+    }
+
+    public void addInfo( Throwable throwable, int code, I18n message, String resource, String location, Object... params ) {
+        addProblem(new Problem(Problem.Status.INFO, code, message, resource, location, throwable));
+    }
+
+    public boolean hasProblems() {
+        return this.problems != null && this.problems.size() > 0;
+    }
+
+    public boolean hasErrors() {
+        if (this.problems == null) return false;
+        for (Problem problem : this.problems) {
+            if (problem.getStatus() == Problem.Status.ERROR) return true;
+        }
+        return false;
+    }
+
+    public boolean hasWarnings() {
+        if (this.problems == null) return false;
+        for (Problem problem : this.problems) {
+            if (problem.getStatus() == Problem.Status.WARNING) return true;
+        }
+        return false;
+    }
+
+    public boolean hasInfo() {
+        if (this.problems == null) return false;
+        for (Problem problem : this.problems) {
+            if (problem.getStatus() == Problem.Status.INFO) return true;
+        }
+        return false;
+    }
+
+    public boolean isEmpty() {
+        return this.problems == null || this.problems.isEmpty();
+    }
+
+    public int size() {
+        if (this.problems == null) return 0;
+        return this.problems.size();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Iterator<Problem> iterator() {
+        return problems.iterator();
+    }
+
+    protected void addProblem( Problem problem ) {
+        if (problem == null) return;
+        if (problems == null) problems = new LinkedList<Problem>();
+        problems.add(problem);
+    }
+
+}

Modified: trunk/dna-integration-tests/pom.xml
===================================================================
--- trunk/dna-integration-tests/pom.xml	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/dna-integration-tests/pom.xml	2008-04-22 02:47:53 UTC (rev 96)
@@ -31,7 +31,7 @@
     </dependency>
     <dependency>
       <groupId>org.jboss.dna</groupId>
-      <artifactId>dna-services</artifactId>
+      <artifactId>dna-repository</artifactId>
       <version>0.1-SNAPSHOT</version>
     </dependency>
     <dependency>

Copied: trunk/dna-repository (from rev 95, trunk/dna-services)

Modified: trunk/dna-repository/.project
===================================================================
--- trunk/dna-services/.project	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/dna-repository/.project	2008-04-22 02:47:53 UTC (rev 96)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <projectDescription>
-	<name>dna-services</name>
+	<name>dna-repository</name>
 	<comment></comment>
 	<projects>
 	</projects>

Modified: trunk/dna-repository/pom.xml
===================================================================
--- trunk/dna-services/pom.xml	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/dna-repository/pom.xml	2008-04-22 02:47:53 UTC (rev 96)
@@ -6,10 +6,10 @@
     <version>0.1-SNAPSHOT</version>
   </parent>
   <!-- The groupId and version values are inherited from parent -->
-  <artifactId>dna-services</artifactId>
+  <artifactId>dna-repository</artifactId>
   <packaging>jar</packaging>
-  <name>JBoss DNA Services</name>
-  <description>JBoss DNA Engine library with main service components</description>
+  <name>JBoss DNA Repository</name>
+  <description>JBoss DNA Repository library</description>
   <url>http://labs.jboss.org/dna</url>
   <!--
   Define the dependencies.  Note that all version and scopes default to those 
@@ -33,6 +33,11 @@
       <type>test-jar</type>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.jboss.dna</groupId>
+      <artifactId>dna-spi</artifactId>
+      <version>0.1-SNAPSHOT</version>
+    </dependency>
     <!-- 
     Rules
     -->
@@ -43,10 +48,12 @@
     <dependency>
       <groupId>org.drools</groupId>
       <artifactId>drools-jsr94</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.drools</groupId>
       <artifactId>drools-compiler</artifactId>
+      <scope>test</scope>
     </dependency>
     <!-- 
     Testing (note the scope)
@@ -54,14 +61,17 @@
     <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.jmock</groupId>
       <artifactId>jmock</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.jmock</groupId>
       <artifactId>jmock-junit4</artifactId>
+      <scope>test</scope>
     </dependency>
     <!-- 
     Logging (require SLF4J API for compiling, but use Log4J and its SLF4J binding for testing) 
@@ -73,10 +83,12 @@
     <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-log4j12</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>log4j</groupId>
       <artifactId>log4j</artifactId>
+      <scope>test</scope>
     </dependency>
     <!-- 
     Java Concurrency in Practice annotations
@@ -98,10 +110,12 @@
     <dependency>
       <groupId>org.apache.jackrabbit</groupId>
       <artifactId>jackrabbit-api</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.jackrabbit</groupId>
       <artifactId>jackrabbit-core</artifactId>
+      <scope>test</scope>
     </dependency>
   </dependencies>
 </project>

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/RepositoryI18n.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/RepositoryI18n.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/RepositoryI18n.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,101 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository;
+
+import org.jboss.dna.common.i18n.I18n;
+
+/**
+ * @author Randall Hauch
+ */
+public final class RepositoryI18n {
+
+    static {
+        try {
+            I18n.initialize(RepositoryI18n.class);
+        } catch (final Exception err) {
+            System.err.println(err);
+        }
+    }
+
+    public static I18n invalidStateString;
+    public static I18n serviceShutdowAndMayNotBeStarted;
+    public static I18n serviceShutdowAndMayNotBePaused;
+    public static I18n unableToFindRepositoryInJndi;
+    public static I18n errorProcessingEvents;
+    public static I18n errorFindingPropertyNameInPropertyAddedEvent;
+    public static I18n errorFindingPropertyNameInPropertyChangedEvent;
+    public static I18n errorFindingPropertyNameInPropertyRemovedEvent;
+
+    public static I18n unableToObtainJsr94RuleAdministrator;
+    public static I18n errorUsingJsr94RuleAdministrator;
+    public static I18n unableToObtainJsr94ServiceProvider;
+    public static I18n errorAddingOrUpdatingRuleSet;
+    public static I18n errorRollingBackRuleSetAfterUpdateFailed;
+    public static I18n errorReadingRulesAndProperties;
+    public static I18n errorDeregisteringRuleSetBeforeUpdatingIt;
+    public static I18n errorRecreatingRuleSet;
+    public static I18n errorRemovingRuleSet;
+    public static I18n errorRemovingRuleSetUponShutdown;
+    public static I18n unableToFindRuleSet;
+    public static I18n errorExecutingRuleSetWithGlobalsAndFacts;
+    public static I18n unableToBuildRuleSetRegularExpressionPattern;
+
+    public static I18n errorObtainingSessionToRepositoryWorkspace;
+    public static I18n errorWritingProblemsOnRuleSet;
+
+    public static I18n sequencingServiceName;
+    public static I18n unableToChangeExecutionContextWhileRunning;
+    public static I18n unableToStartSequencingServiceWithoutExecutionContext;
+    public static I18n errorWhileSequencingNode;
+    public static I18n errorInRepositoryWhileSequencingNode;
+    public static I18n errorFindingSequencersToRunAgainstNode;
+    public static I18n errorInRepositoryWhileFindingSequencersToRunAgainstNode;
+    public static I18n executionContextHasBeenClosed;
+    public static I18n sequencerTask;
+    public static I18n sequencerSubtask;
+    public static I18n unableToFindPropertyForSequencing;
+    public static I18n sequencingPropertyOnNode;
+    public static I18n writingOutputSequencedFromPropertyOnNodes;
+
+    public static I18n errorReadingPropertiesFromContainerNode;
+    public static I18n requiredPropertyOnNodeWasExpectedToBeStringValue;
+    public static I18n optionalPropertyOnNodeWasExpectedToBeStringValue;
+    public static I18n requiredPropertyOnNodeWasExpectedToBeStringArrayValue;
+    public static I18n optionalPropertyOnNodeWasExpectedToBeStringArrayValue;
+    public static I18n requiredPropertyOnNodeCouldNotBeRead;
+    public static I18n optionalPropertyOnNodeCouldNotBeRead;
+    public static I18n requiredPropertyIsMissingFromNode;
+    public static I18n errorGettingRequiredPropertyFromNode;
+    public static I18n errorGettingOptionalPropertyFromNode;
+    public static I18n errorClosingBinaryStreamForPropertyFromNode;
+    public static I18n requiredNodeDoesNotExistRelativeToNode;
+    public static I18n errorGettingNodeRelativeToNode;
+    public static I18n unknownPropertyValueType;
+
+    public static I18n pathExpressionIsInvalid;
+    public static I18n pathExpressionMayNotBeBlank;
+    public static I18n pathExpressionHasInvalidSelect;
+    public static I18n pathExpressionHasInvalidMatch;
+
+    public static I18n invalidRepositoryNodePath;
+
+}


Property changes on: trunk/dna-repository/src/main/java/org/jboss/dna/repository/RepositoryI18n.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/NodeChange.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/NodeChange.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/NodeChange.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,176 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.observation;
+
+import java.util.Collections;
+import java.util.Set;
+import net.jcip.annotations.Immutable;
+import org.jboss.dna.common.util.HashCode;
+
+/**
+ * A notification of changes to a node.
+ * @author Randall Hauch
+ */
+ at Immutable
+public class NodeChange {
+
+    private final String repositoryWorkspaceName;
+    private final String absolutePath;
+    private final int eventTypes;
+    private final Set<String> modifiedProperties;
+    private final Set<String> removedProperties;
+    private final int hc;
+
+    public NodeChange( String repositoryWorkspaceName, String absolutePath, int eventTypes, Set<String> modifiedProperties, Set<String> removedProperties ) {
+        assert repositoryWorkspaceName != null;
+        assert absolutePath != null;
+        this.repositoryWorkspaceName = repositoryWorkspaceName;
+        this.absolutePath = absolutePath.trim();
+        this.hc = HashCode.compute(this.repositoryWorkspaceName, this.absolutePath);
+        this.eventTypes = eventTypes;
+        if (modifiedProperties == null) modifiedProperties = Collections.emptySet();
+        if (removedProperties == null) removedProperties = Collections.emptySet();
+        this.modifiedProperties = Collections.unmodifiableSet(modifiedProperties);
+        this.removedProperties = Collections.unmodifiableSet(removedProperties);
+    }
+
+    /**
+     * @return absolutePath
+     */
+    public String getAbsolutePath() {
+        return this.absolutePath;
+    }
+
+    /**
+     * @return repositoryWorkspaceName
+     */
+    public String getRepositoryWorkspaceName() {
+        return this.repositoryWorkspaceName;
+    }
+
+    /**
+     * @return modifiedProperties
+     */
+    public Set<String> getModifiedProperties() {
+        return this.modifiedProperties;
+    }
+
+    /**
+     * @return removedProperties
+     */
+    public Set<String> getRemovedProperties() {
+        return this.removedProperties;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        return this.hc;
+    }
+
+    public boolean includesAllEventTypes( int... jcrEventTypes ) {
+        for (int jcrEventType : jcrEventTypes) {
+            if ((this.eventTypes & jcrEventType) == 0) return false;
+        }
+        return true;
+    }
+
+    public boolean includesEventTypes( int... jcrEventTypes ) {
+        for (int jcrEventType : jcrEventTypes) {
+            if ((this.eventTypes & jcrEventType) != 0) return true;
+        }
+        return false;
+    }
+
+    public boolean isSameNode( NodeChange that ) {
+        if (that == this) return true;
+        if (this.hc != that.hc) return false;
+        if (!this.repositoryWorkspaceName.equals(that.repositoryWorkspaceName)) return false;
+        if (!this.absolutePath.equals(that.absolutePath)) return false;
+        return true;
+    }
+
+    /**
+     * Return whether this node change occurs on a node on the supplied path.
+     * @param absolutePath the path
+     * @return true if the node is on the supplied absolute path, or false otherwise
+     * @see #isNotOnPath(String)
+     */
+    public boolean isOnPath( String absolutePath ) {
+        if (absolutePath == null) return false;
+        if (this.getAbsolutePath().startsWith(absolutePath)) return true;
+        return false;
+    }
+
+    /**
+     * Return whether this node change occurs on a node on a different path than that supplied.
+     * @param absolutePath the path
+     * @return true if the node is on a different path, or false if it is on the same path
+     * @see #isOnPath(String)
+     */
+    public boolean isNotOnPath( String absolutePath ) {
+        return !isOnPath(absolutePath);
+    }
+
+    /**
+     * Determine whether this node change includes the setting of new value(s) for the supplied property.
+     * @param property the name of the property
+     * @return true if the named property has a new value on this node, or false otherwise
+     */
+    public boolean isPropertyModified( String property ) {
+        return this.modifiedProperties.contains(property);
+    }
+
+    /**
+     * Determine whether this node change includes the removal of the supplied property.
+     * @param property the name of the property
+     * @return true if the named property was removed from this node, or false otherwise
+     */
+    public boolean isPropertyRemoved( String property ) {
+        return this.removedProperties.contains(property);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals( Object obj ) {
+        if (obj == this) return true;
+        if (obj instanceof NodeChange) {
+            NodeChange that = (NodeChange)obj;
+            if (!this.isSameNode(that)) return false;
+            if (this.eventTypes != that.eventTypes) return false;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        return this.repositoryWorkspaceName + "=>" + this.absolutePath;
+    }
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/NodeChangeListener.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/NodeChangeListener.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/NodeChangeListener.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,31 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.observation;
+
+/**
+ * @author Randall Hauch
+ */
+public interface NodeChangeListener {
+
+    void onNodeChanges( NodeChanges changes );
+
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/NodeChanges.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/NodeChanges.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/NodeChanges.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,195 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.observation;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.jcr.RepositoryException;
+import javax.jcr.observation.Event;
+import org.jboss.dna.common.i18n.I18n;
+import org.jboss.dna.common.util.Logger;
+import org.jboss.dna.repository.RepositoryI18n;
+
+/**
+ * A utility class that builds node changes from a sequence of events.
+ * @author Randall Hauch
+ */
+public class NodeChanges implements Iterable<NodeChange> {
+
+    public static NodeChanges create( final String repositoryWorkspaceName, Iterable<Event> events ) throws RepositoryException {
+        Map<String, NodeChangeDetails> detailsByNodePath = new HashMap<String, NodeChangeDetails>();
+        // Process each of the events, extracting the node path and property details for each ...
+        for (Event event : events) {
+            final int eventType = event.getType();
+            final String eventPath = event.getPath();
+            if (eventType == Event.PROPERTY_ADDED || eventType == Event.PROPERTY_CHANGED || eventType == Event.PROPERTY_REMOVED) {
+                // Extract the node's path and property name from the even path ...
+                int lastDelim = eventPath.lastIndexOf('/');
+                if (lastDelim < 1 || lastDelim == (eventPath.length() - 1)) {
+                    // The last delimiter doesn't exist, is the first character, or is the last character...
+                    I18n msg =
+                        eventType == Event.PROPERTY_ADDED ? RepositoryI18n.errorFindingPropertyNameInPropertyAddedEvent : eventType == Event.PROPERTY_CHANGED ? RepositoryI18n.errorFindingPropertyNameInPropertyChangedEvent : RepositoryI18n.errorFindingPropertyNameInPropertyRemovedEvent;
+                    Logger.getLogger(NodeChanges.class).error(msg, eventPath);
+                    continue;
+                }
+                String nodePath = eventPath.substring(0, lastDelim); // excludes the last delim
+                String propertyName = eventPath.substring(lastDelim + 1);
+                // Record the details ...
+                NodeChangeDetails details = detailsByNodePath.get(nodePath);
+                if (details == null) {
+                    details = new NodeChangeDetails(nodePath);
+                    detailsByNodePath.put(nodePath, details);
+                }
+                switch (eventType) {
+                    case Event.PROPERTY_ADDED: {
+                        details.addProperty(propertyName);
+                        break;
+                    }
+                    case Event.PROPERTY_CHANGED: {
+                        details.changeProperty(propertyName);
+                        break;
+                    }
+                    case Event.PROPERTY_REMOVED: {
+                        details.removeProperty(propertyName);
+                        break;
+                    }
+                }
+            } else if (eventType == Event.NODE_ADDED || eventType == Event.NODE_REMOVED) {
+                // Remove the last delimiter if it appears at the end of the path ...
+                String nodePath = eventPath;
+                if (nodePath.length() > 1 && nodePath.charAt(nodePath.length() - 1) == '/') {
+                    nodePath = nodePath.substring(0, nodePath.length() - 1);
+                }
+                // Record the details ...
+                NodeChangeDetails details = detailsByNodePath.get(nodePath);
+                if (details == null) {
+                    details = new NodeChangeDetails(nodePath);
+                    detailsByNodePath.put(nodePath, details);
+                }
+                details.addEventType(eventType);
+            }
+        }
+
+        // Create the node changes ...
+        List<NodeChange> result = new ArrayList<NodeChange>(detailsByNodePath.size());
+        for (NodeChangeDetails detail : detailsByNodePath.values()) {
+            NodeChange change = new NodeChange(repositoryWorkspaceName, detail.getNodePath(), detail.getEventTypes(), detail.getModifiedProperties(), detail.getRemovedProperties());
+            result.add(change);
+        }
+        return new NodeChanges(result);
+    }
+
+    protected static class NodeChangeDetails {
+
+        private final String nodePath;
+        private final Set<String> modifiedProperties = new HashSet<String>();
+        private final Set<String> removedProperties = new HashSet<String>();
+        private int eventTypes;
+
+        protected NodeChangeDetails( String nodePath ) {
+            this.nodePath = nodePath;
+        }
+
+        public void addEventType( int eventType ) {
+            this.eventTypes |= eventType;
+        }
+
+        public void addProperty( String propertyName ) {
+            this.modifiedProperties.add(propertyName);
+            this.eventTypes |= Event.PROPERTY_ADDED;
+        }
+
+        public void changeProperty( String propertyName ) {
+            this.modifiedProperties.add(propertyName);
+            this.eventTypes |= Event.PROPERTY_CHANGED;
+        }
+
+        public void removeProperty( String propertyName ) {
+            this.removedProperties.add(propertyName);
+            this.eventTypes |= Event.PROPERTY_REMOVED;
+        }
+
+        /**
+         * @return nodeAction
+         */
+        public int getEventTypes() {
+            return this.eventTypes;
+        }
+
+        /**
+         * @return nodePath
+         */
+        public String getNodePath() {
+            return this.nodePath;
+        }
+
+        /**
+         * @return addedProperties
+         */
+        public Set<String> getModifiedProperties() {
+            return this.modifiedProperties;
+        }
+
+        /**
+         * @return removedProperties
+         */
+        public Set<String> getRemovedProperties() {
+            return this.removedProperties;
+        }
+    }
+
+    protected static final Comparator<NodeChange> PRE_ORDER = new Comparator<NodeChange>() {
+
+        public int compare( NodeChange change1, NodeChange change2 ) {
+            return change1.getAbsolutePath().compareTo(change2.getAbsolutePath());
+        }
+    };
+
+    private final List<NodeChange> changesInPreOrder;
+
+    protected NodeChanges( List<NodeChange> changes ) {
+        this.changesInPreOrder = Collections.unmodifiableList(changes);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Iterator<NodeChange> iterator() {
+        return this.changesInPreOrder.iterator();
+    }
+
+    public Iterator<NodeChange> getPreOrder() {
+        return this.changesInPreOrder.iterator();
+    }
+
+    public int size() {
+        return this.changesInPreOrder.size();
+    }
+
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/ObservationService.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/ObservationService.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/observation/ObservationService.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,722 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.observation;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.UnsupportedRepositoryOperationException;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+import javax.jcr.observation.ObservationManager;
+import net.jcip.annotations.GuardedBy;
+import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.util.ArgCheck;
+import org.jboss.dna.common.util.Logger;
+import org.jboss.dna.repository.RepositoryI18n;
+import org.jboss.dna.repository.services.AbstractServiceAdministrator;
+import org.jboss.dna.repository.services.AdministeredService;
+import org.jboss.dna.repository.services.ServiceAdministrator;
+import org.jboss.dna.repository.util.SessionFactory;
+
+/**
+ * @author Randall Hauch
+ */
+public class ObservationService implements AdministeredService {
+
+    /**
+     * Interface to which problems with particular events are logged.
+     * @author Randall Hauch
+     */
+    public static interface ProblemLog {
+
+        void error( String repositoryWorkspaceName, Throwable t );
+    }
+
+    /**
+     * Problem log implementation that records problems in the log.
+     * @author Randall Hauch
+     */
+    public class DefaultProblemLog implements ProblemLog {
+
+        /**
+         * {@inheritDoc}
+         */
+        public void error( String repositoryWorkspaceName, Throwable t ) {
+            getLogger().error(t, RepositoryI18n.errorProcessingEvents, repositoryWorkspaceName);
+        }
+    }
+
+    protected static class NoOpProblemLog implements ProblemLog {
+
+        /**
+         * {@inheritDoc}
+         */
+        public void error( String repositoryWorkspaceName, Throwable t ) {
+        }
+    }
+
+    public static final ProblemLog NO_OP_PROBLEM_LOG = new NoOpProblemLog();
+
+    /**
+     * The administrative component for this service.
+     * @author Randall Hauch
+     */
+    protected class Administrator extends AbstractServiceAdministrator {
+
+        protected Administrator() {
+            super(State.STARTED);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        protected String serviceName() {
+            return "ObservationService";
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public boolean awaitTermination( long timeout, TimeUnit unit ) {
+            return true;
+        }
+
+    }
+
+    private Logger logger = Logger.getLogger(this.getClass());
+    private ProblemLog problemLog = new DefaultProblemLog();
+    private final Statistics statistics = new Statistics();
+    private final SessionFactory sessionFactory;
+    private final CopyOnWriteArrayList<EventListener> eventListeners = new CopyOnWriteArrayList<EventListener>();
+    private final CopyOnWriteArrayList<NodeChangeListener> nodeChangeListeners = new CopyOnWriteArrayList<NodeChangeListener>();
+    private final Administrator administrator = new Administrator();
+
+    public ObservationService( SessionFactory sessionFactory ) {
+        ArgCheck.isNotNull(sessionFactory, "session factory");
+        this.sessionFactory = sessionFactory;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public ServiceAdministrator getAdministrator() {
+        return this.administrator;
+    }
+
+    /**
+     * @return sessionFactory
+     */
+    public SessionFactory getSessionFactory() {
+        return this.sessionFactory;
+    }
+
+    /**
+     * Get the statistics for this system.
+     * @return the statistics, which are updated as the system is used
+     */
+    public Statistics getStatistics() {
+        return this.statistics;
+    }
+
+    /**
+     * Get the logger for this system
+     * @return the logger
+     */
+    public Logger getLogger() {
+        return this.logger;
+    }
+
+    /**
+     * Set the logger for this system.
+     * @param logger the logger, or null if the standard logging should be used
+     */
+    public void setLogger( Logger logger ) {
+        this.logger = logger != null ? logger : Logger.getLogger(this.getClass());
+    }
+
+    /**
+     * @return problemLog
+     */
+    public ProblemLog getProblemLog() {
+        return this.problemLog;
+    }
+
+    /**
+     * Set the problem log that will be notified of problems handling events. By default, such problems are sent to the log.
+     * @param problemLog the new problem log implementation; if null, then the default problem log is used
+     */
+    public void setProblemLog( ProblemLog problemLog ) {
+        this.problemLog = problemLog != null ? problemLog : new DefaultProblemLog();
+    }
+
+    public boolean addListener( EventListener listener ) {
+        if (listener == null) return false;
+        return this.eventListeners.addIfAbsent(listener);
+    }
+
+    public boolean removeListener( EventListener listener ) {
+        if (listener == null) return false;
+        return this.eventListeners.remove(listener);
+    }
+
+    public boolean addListener( NodeChangeListener listener ) {
+        return this.nodeChangeListeners.addIfAbsent(listener);
+    }
+
+    public boolean removeListener( NodeChangeListener listener ) {
+        if (listener == null) return false;
+        return this.nodeChangeListeners.remove(listener);
+    }
+
+    /**
+     * Monitor the supplied workspace for events of the given type on any node at or under the supplied path.
+     * <p>
+     * Monitoring is accomplished by registering a listener on the workspace, so this monitoring only has access to the
+     * information that visible to the session created by the {@link #getSessionFactory() session factory} for the given
+     * repository and workspace name.
+     * </p>
+     * <p>
+     * The listener returned from this method is not managed by this SequencingService instance. If the listener is no longer
+     * needed, it simply must be {@link ObservationManager#removeEventListener(EventListener) removed} as a listener of the
+     * workspace and garbage collected. If this service is {@link ServiceAdministrator#shutdown() shutdown} while there are still
+     * active listeners, those listeners will disconnect themselves from this service and the workspace with which they're
+     * registered when they attempt to forward the next events.
+     * </p>
+     * <p>
+     * The set of events that are monitored can be filtered by specifying restrictions based on characteristics of the node
+     * associated with the event. In the case of event types {@link Event#NODE_ADDED NODE_ADDED} and
+     * {@link Event#NODE_REMOVED NODE_REMOVED}, the node associated with an event is the node at (or formerly at) the path
+     * returned by {@link Event#getPath() Event.getPath()}. In the case of event types
+     * {@link Event#PROPERTY_ADDED PROPERTY_ADDED}, {@link Event#PROPERTY_REMOVED PROPERTY_REMOVED} and
+     * {@link Event#PROPERTY_CHANGED PROPERTY_CHANGED}, the node associated with an event is the parent node of the property at
+     * (or formerly at) the path returned by <code>Event.getPath</code>:
+     * <ul>
+     * <li> <code>absolutePath</code>, <code>isDeep</code>: Only events whose associated node is at
+     * <code>absolutePath</code> (or within its subtree, if <code>isDeep</code> is <code>true</code>) will be received. It
+     * is permissible to register a listener for a path where no node currently exists. </li>
+     * <li> <code>uuids</code>: Only events whose associated node has one of the UUIDs in this list will be received. If his
+     * parameter is <code>null</code> then no UUID-related restriction is placed on events received. </li>
+     * <li> <code>nodeTypeNames</code>: Only events whose associated node has one of the node types (or a subtype of one of the
+     * node types) in this list will be received. If this parameter is <code>null</code> then no node type-related restriction
+     * is placed on events received. </li>
+     * </ul>
+     * The restrictions are "ANDed" together. In other words, for a particular node to be "listened to" it must meet all the
+     * restrictions.
+     * </p>
+     * <p>
+     * Additionally, if <code>noLocal</code> is <code>true</code>, then events generated by the session through which the
+     * listener was registered are ignored. Otherwise, they are not ignored.
+     * </p>
+     * <p>
+     * The filters of an already-registered {@link WorkspaceListener} can be changed at runtime by changing the attributes and
+     * {@link WorkspaceListener#reregister() registering}.
+     * </p>
+     * @param repositoryWorkspaceName the name to be used with the session factory to obtain a session to the repository and
+     * workspace that is to be monitored
+     * @param absolutePath the absolute path of the node at or below which changes are to be monitored; may be null if all nodes
+     * in the workspace are to be monitored
+     * @param eventTypes the bitmask of the {@link Event} types that are to be monitored
+     * @param isDeep true if events below the node given by the <code>absolutePath</code> or by the <code>uuids</code> are to
+     * be processed, or false if only the events at the node
+     * @param uuids array of UUIDs of nodes that are to be monitored; may be null or empty if the UUIDs are not known
+     * @param nodeTypeNames array of node type names that are to be monitored; may be null or empty if the monitoring has no node
+     * type restrictions
+     * @param noLocal true if the events originating in the supplied workspace are to be ignored, or false if they are also to be
+     * processed.
+     * @return the listener that was created and registered to perform the monitoring
+     * @throws RepositoryException if there is a problem registering the listener
+     */
+    public WorkspaceListener monitor( String repositoryWorkspaceName, String absolutePath, int eventTypes, boolean isDeep, String[] uuids, String[] nodeTypeNames, boolean noLocal )
+        throws RepositoryException {
+        WorkspaceListener listener = new WorkspaceListener(repositoryWorkspaceName, eventTypes, absolutePath, isDeep, uuids, nodeTypeNames, noLocal);
+        listener.register();
+        return listener;
+    }
+
+    /**
+     * Monitor the supplied workspace for {@link WorkspaceListener#DEFAULT_EVENT_TYPES default event types} on any node at or
+     * under the supplied path.
+     * <p>
+     * Monitoring is accomplished by registering a listener on the workspace, so this monitoring only has access to the
+     * information that visible to the session created by the {@link #getSessionFactory() session factory} for the given
+     * repository and workspace name.
+     * </p>
+     * <p>
+     * The listener returned from this method is not managed by this SequencingService instance. If the listener is no longer
+     * needed, it simply must be {@link ObservationManager#removeEventListener(EventListener) removed} as a listener of the
+     * workspace and garbage collected.
+     * </p>
+     * @param repositoryWorkspaceName the name to be used with the session factory to obtain a session to the repository and
+     * workspace that is to be monitored
+     * @param absolutePath the absolute path of the node at or below which changes are to be monitored; may be null if all nodes
+     * in the workspace are to be monitored
+     * @param nodeTypeNames the names of the node types that are to be monitored; may be null or empty if the monitoring has no
+     * node type restrictions
+     * @return the listener that was created and registered to perform the monitoring
+     * @throws RepositoryException if there is a problem registering the listener
+     */
+    public WorkspaceListener monitor( String repositoryWorkspaceName, String absolutePath, String... nodeTypeNames ) throws RepositoryException {
+        return monitor(repositoryWorkspaceName, absolutePath, WorkspaceListener.DEFAULT_EVENT_TYPES, WorkspaceListener.DEFAULT_IS_DEEP, null, nodeTypeNames, WorkspaceListener.DEFAULT_NO_LOCAL);
+    }
+
+    /**
+     * Monitor the supplied workspace for the supplied event types on any node in the workspace.
+     * <p>
+     * Monitoring is accomplished by registering a listener on the workspace, so this monitoring only has access to the
+     * information that visible to the session created by the {@link #getSessionFactory() session factory} for the given
+     * repository and workspace name.
+     * </p>
+     * <p>
+     * The listener returned from this method is not managed by this SequencingService instance. If the listener is no longer
+     * needed, it simply must be {@link ObservationManager#removeEventListener(EventListener) removed} as a listener of the
+     * workspace and garbage collected.
+     * </p>
+     * @param repositoryWorkspaceName the name to be used with the session factory to obtain a session to the repository and
+     * workspace that is to be monitored
+     * @param eventTypes the bitmask of the {@link Event} types that are to be monitored
+     * @param nodeTypeNames the names of the node types that are to be monitored; may be null or empty if the monitoring has no
+     * node type restrictions
+     * @return the listener that was created and registered to perform the monitoring
+     * @throws RepositoryException if there is a problem registering the listener
+     */
+    public WorkspaceListener monitor( String repositoryWorkspaceName, int eventTypes, String... nodeTypeNames ) throws RepositoryException {
+        return monitor(repositoryWorkspaceName, WorkspaceListener.DEFAULT_ABSOLUTE_PATH, eventTypes, WorkspaceListener.DEFAULT_IS_DEEP, null, nodeTypeNames, WorkspaceListener.DEFAULT_NO_LOCAL);
+    }
+
+    /**
+     * From section 2.8.8 of the JSR-170 specification:
+     * <p>
+     * On each persistent change, those listeners that are entitled to receive one or more events will have their onEvent method
+     * called and be passed an EventIterator. The EventIterator will contain the event bundle reflecting the persistent changes
+     * made but excluding those to which that particular listener is not entitled, according to the listeners access permissions
+     * and filters.
+     * </p>
+     * @param events
+     * @param listener
+     */
+    protected void processEvents( EventIterator eventIterator, WorkspaceListener listener ) {
+        if (eventIterator == null) return;
+        List<Event> events = new ArrayList<Event>();
+        // Copy the events ...
+        while (eventIterator.hasNext()) {
+            events.add((Event)eventIterator.next());
+        }
+        if (!getAdministrator().isStarted()) {
+            this.statistics.recordIgnoredEventSet(events.size());
+            return;
+        }
+
+        // Notify the event listeners ...
+        boolean notifiedSomebody = false;
+        List<EventListener> eventListeners = this.eventListeners; // use one consistent snapshot
+        if (!eventListeners.isEmpty()) {
+            DelegatingEventIterator eventIter = new DelegatingEventIterator(events.iterator(), events.size());
+            for (EventListener eventListener : eventListeners) {
+                eventListener.onEvent(eventIter);
+            }
+            notifiedSomebody = true;
+        }
+
+        // Now create the node change events ...
+        List<NodeChangeListener> nodeChangeListeners = this.nodeChangeListeners; // use one consistent snapshot
+        if (!nodeChangeListeners.isEmpty()) {
+            final String repositoryWorkspaceName = listener.getRepositoryWorkspaceName();
+            try {
+                NodeChanges nodeChanges = NodeChanges.create(repositoryWorkspaceName, events);
+
+                // And notify the node change listeners ...
+                int nodeChangeCount = nodeChanges.size();
+                this.statistics.recordNodesChanged(nodeChangeCount);
+                for (NodeChangeListener nodeChangeListener : nodeChangeListeners) {
+                    nodeChangeListener.onNodeChanges(nodeChanges);
+                }
+            } catch (Throwable t) {
+                getProblemLog().error(repositoryWorkspaceName, t);
+            }
+            notifiedSomebody = true;
+        }
+
+        if (notifiedSomebody) {
+            this.statistics.recordEventSet(events.size());
+        } else {
+            this.statistics.recordIgnoredEventSet(events.size());
+        }
+    }
+
+    protected class DelegatingEventIterator implements EventIterator {
+
+        private final Iterator<Event> events;
+        private final int size;
+        private int position = 0;
+
+        protected DelegatingEventIterator( Iterator<Event> events, int size ) {
+            this.events = events;
+            this.size = size;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public Event nextEvent() {
+            ++position;
+            return events.next();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public long getPosition() {
+            return position;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public long getSize() {
+            return size;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public void skip( long skipNum ) {
+            for (int i = 0; i != skipNum; ++i) {
+                next();
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public boolean hasNext() {
+            return events.hasNext();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public Object next() {
+            return events.next();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public void remove() {
+            // does nothing
+        }
+
+    }
+
+    /**
+     * Implementation of the {@link EventListener JCR EventListener} interface, returned by the sequencing system.
+     * @author Randall Hauch
+     */
+    @ThreadSafe
+    public class WorkspaceListener implements EventListener {
+
+        public static final boolean DEFAULT_IS_DEEP = true;
+        public static final boolean DEFAULT_NO_LOCAL = false;
+        public static final int DEFAULT_EVENT_TYPES = Event.NODE_ADDED | /* Event.NODE_REMOVED| */Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED /* |Event.PROPERTY_REMOVED */;
+        public static final String DEFAULT_ABSOLUTE_PATH = "/";
+
+        private final String repositoryWorkspaceName;
+        private final Set<String> uuids;
+        private final Set<String> nodeTypeNames;
+        private final int eventTypes;
+        private final String absolutePath;
+        private final boolean deep;
+        private final boolean noLocal;
+        @GuardedBy( "this" )
+        private transient Session session;
+
+        protected WorkspaceListener( String repositoryWorkspaceName, int eventTypes, String absPath, boolean isDeep, String[] uuids, String[] nodeTypeNames, boolean noLocal ) {
+            this.repositoryWorkspaceName = repositoryWorkspaceName;
+            this.eventTypes = eventTypes;
+            this.deep = isDeep;
+            this.noLocal = noLocal;
+            this.absolutePath = absPath != null && absPath.trim().length() != 0 ? absPath.trim() : null;
+            // Set the UUIDs ...
+            Set<String> newUuids = new HashSet<String>();
+            if (uuids != null) {
+                for (String uuid : uuids) {
+                    if (uuid != null && uuid.trim().length() != 0) newUuids.add(uuid.trim());
+                }
+            }
+            this.uuids = Collections.unmodifiableSet(newUuids);
+            // Set the node type names
+            Set<String> newNodeTypeNames = new HashSet<String>();
+            if (nodeTypeNames != null) {
+                for (String nodeTypeName : nodeTypeNames) {
+                    if (nodeTypeName != null && nodeTypeName.trim().length() != 0) newNodeTypeNames.add(nodeTypeName.trim());
+                }
+            }
+            this.nodeTypeNames = Collections.unmodifiableSet(newNodeTypeNames);
+        }
+
+        /**
+         * @return repositoryWorkspaceName
+         */
+        public String getRepositoryWorkspaceName() {
+            return this.repositoryWorkspaceName;
+        }
+
+        /**
+         * @return eventTypes
+         */
+        public int getEventTypes() {
+            return this.eventTypes;
+        }
+
+        /**
+         * @return absolutePath
+         */
+        public String getAbsolutePath() {
+            return this.absolutePath;
+        }
+
+        /**
+         * @return deep
+         */
+        public boolean isDeep() {
+            return this.deep;
+        }
+
+        /**
+         * @return noLocal
+         */
+        public boolean isNoLocal() {
+            return this.noLocal;
+        }
+
+        /**
+         * @return uuids
+         */
+        public Set<String> getUuids() {
+            return this.uuids;
+        }
+
+        /**
+         * @return nodeTypeNames
+         */
+        public Set<String> getNodeTypeNames() {
+            return this.nodeTypeNames;
+        }
+
+        public synchronized boolean isRegistered() {
+            if (this.session != null && getAdministrator().isShutdown()) {
+                // This sequencing system has been shutdown, so unregister this listener
+                try {
+                    unregister();
+                } catch (RepositoryException re) {
+                    String msg = "Error unregistering workspace listener after sequencing system has been shutdow.";
+                    Logger.getLogger(this.getClass()).debug(re, msg);
+                }
+            }
+            return this.session != null;
+        }
+
+        public synchronized WorkspaceListener register() throws UnsupportedRepositoryOperationException, RepositoryException {
+            if (this.session != null) return this;
+            this.session = ObservationService.this.getSessionFactory().createSession(this.repositoryWorkspaceName);
+            String[] uuids = this.uuids.isEmpty() ? null : this.uuids.toArray(new String[this.uuids.size()]);
+            String[] nodeTypeNames = this.nodeTypeNames.isEmpty() ? null : this.nodeTypeNames.toArray(new String[this.nodeTypeNames.size()]);
+            this.session.getWorkspace().getObservationManager().addEventListener(this, eventTypes, absolutePath, deep, uuids, nodeTypeNames, noLocal);
+            return this;
+        }
+
+        public synchronized WorkspaceListener unregister() throws UnsupportedRepositoryOperationException, RepositoryException {
+            if (this.session == null) return this;
+            try {
+                this.session.getWorkspace().getObservationManager().removeEventListener(this);
+                this.session.logout();
+            } finally {
+                this.session = null;
+            }
+            return this;
+        }
+
+        public synchronized WorkspaceListener reregister() throws UnsupportedRepositoryOperationException, RepositoryException {
+            unregister();
+            register();
+            return this;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public void onEvent( EventIterator events ) {
+            if (events != null) {
+                if (getAdministrator().isShutdown()) {
+                    // This sequencing system has been shutdown, so unregister this listener
+                    try {
+                        unregister();
+                    } catch (RepositoryException re) {
+                        String msg = "Error unregistering workspace listener after sequencing system has been shutdow.";
+                        Logger.getLogger(this.getClass()).debug(re, msg);
+                    }
+                } else {
+                    ObservationService.this.processEvents(events, this);
+                }
+            }
+        }
+    }
+
+    /**
+     * The statistics for the system. Each sequencing system has an instance of this class that is updated.
+     * @author Randall Hauch
+     */
+    @ThreadSafe
+    public class Statistics {
+
+        @GuardedBy( "lock" )
+        private long numberOfEventsIgnored;
+        @GuardedBy( "lock" )
+        private long numberOfEventsEnqueued;
+        @GuardedBy( "lock" )
+        private long numberOfEventSetsIgnored;
+        @GuardedBy( "lock" )
+        private long numberOfEventSetsEnqueued;
+        private final AtomicLong numberOfNodeChangesEnqueued = new AtomicLong(0);
+        private final ReadWriteLock lock = new ReentrantReadWriteLock();
+        private final AtomicLong startTime;
+
+        protected Statistics() {
+            startTime = new AtomicLong(System.currentTimeMillis());
+        }
+
+        public Statistics reset() {
+            try {
+                lock.writeLock().lock();
+                this.startTime.set(System.currentTimeMillis());
+                this.numberOfEventsIgnored = 0;
+                this.numberOfEventsEnqueued = 0;
+                this.numberOfEventSetsIgnored = 0;
+                this.numberOfEventSetsEnqueued = 0;
+                this.numberOfNodeChangesEnqueued.set(0);
+            } finally {
+                lock.writeLock().unlock();
+            }
+            return this;
+        }
+
+        /**
+         * @return the system time when the statistics were started
+         */
+        public long getStartTime() {
+            return this.startTime.get();
+        }
+
+        /**
+         * @return the number of node changes that were processed
+         */
+        public long getNumberOfNodeChangesEnqueued() {
+            return this.numberOfNodeChangesEnqueued.get();
+        }
+
+        /**
+         * @return the number of events that were ignored because the system was not running
+         */
+        public long getNumberOfEventsIgnored() {
+            try {
+                lock.readLock().lock();
+                return this.numberOfEventsIgnored;
+            } finally {
+                lock.readLock().unlock();
+            }
+        }
+
+        /**
+         * @return the number of events that were enqueued for processing
+         */
+        public long getNumberOfEventsEnqueued() {
+            try {
+                lock.readLock().lock();
+                return this.numberOfEventsEnqueued;
+            } finally {
+                lock.readLock().unlock();
+            }
+        }
+
+        /**
+         * @return the number of event sets (transactions) that were enqueued for processing
+         */
+        public long getNumberOfEventSetsEnqueued() {
+            try {
+                lock.readLock().lock();
+                return this.numberOfEventSetsEnqueued;
+            } finally {
+                lock.readLock().unlock();
+            }
+        }
+
+        /**
+         * @return the number of event sets (transactions) that were ignored because the system was not running
+         */
+        public long getNumberOfEventSetsIgnored() {
+            try {
+                lock.readLock().lock();
+                return this.numberOfEventSetsIgnored;
+            } finally {
+                lock.readLock().unlock();
+            }
+        }
+
+        protected void recordNodesChanged( long changeCount ) {
+            this.numberOfNodeChangesEnqueued.addAndGet(changeCount);
+        }
+
+        protected void recordEventSet( long eventsInSet ) {
+            try {
+                lock.writeLock().lock();
+                this.numberOfEventsEnqueued += eventsInSet;
+                ++this.numberOfEventSetsEnqueued;
+            } finally {
+                lock.writeLock().unlock();
+            }
+        }
+
+        protected void recordIgnoredEventSet( long eventsInSet ) {
+            try {
+                lock.writeLock().lock();
+                this.numberOfEventsIgnored += eventsInSet;
+                this.numberOfEventSetsIgnored += 1;
+                ++this.numberOfEventSetsEnqueued;
+            } finally {
+                lock.writeLock().unlock();
+            }
+        }
+    }
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/InvalidRuleSetException.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/InvalidRuleSetException.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/InvalidRuleSetException.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,60 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.rules;
+
+/**
+ * @author Randall Hauch
+ */
+public class InvalidRuleSetException extends RuntimeException {
+
+    /**
+     * 
+     */
+    public InvalidRuleSetException() {
+    }
+
+    /**
+     * @param message
+     */
+    public InvalidRuleSetException( String message ) {
+        super(message);
+
+    }
+
+    /**
+     * @param cause
+     */
+    public InvalidRuleSetException( Throwable cause ) {
+        super(cause);
+
+    }
+
+    /**
+     * @param message
+     * @param cause
+     */
+    public InvalidRuleSetException( String message, Throwable cause ) {
+        super(message, cause);
+
+    }
+
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/RuleService.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/RuleService.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/RuleService.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,468 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.rules;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.rmi.RemoteException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import javax.rules.ConfigurationException;
+import javax.rules.RuleRuntime;
+import javax.rules.RuleServiceProvider;
+import javax.rules.RuleServiceProviderManager;
+import javax.rules.RuleSession;
+import javax.rules.StatelessRuleSession;
+import javax.rules.admin.LocalRuleExecutionSetProvider;
+import javax.rules.admin.RuleAdministrator;
+import javax.rules.admin.RuleExecutionSet;
+import javax.rules.admin.RuleExecutionSetCreateException;
+import javax.rules.admin.RuleExecutionSetDeregistrationException;
+import net.jcip.annotations.GuardedBy;
+import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.SystemFailureException;
+import org.jboss.dna.common.component.ClassLoaderFactory;
+import org.jboss.dna.common.component.StandardClassLoaderFactory;
+import org.jboss.dna.common.util.ArgCheck;
+import org.jboss.dna.common.util.Logger;
+import org.jboss.dna.common.util.StringUtil;
+import org.jboss.dna.repository.RepositoryI18n;
+import org.jboss.dna.repository.services.AbstractServiceAdministrator;
+import org.jboss.dna.repository.services.AdministeredService;
+import org.jboss.dna.repository.services.ServiceAdministrator;
+
+/**
+ * A rule service that is capable of executing rule sets using one or more JSR-94 rule engines. Sets of rules are
+ * {@link #addRuleSet(RuleSet) added}, {@link #updateRuleSet(RuleSet) updated}, and {@link #removeRuleSet(String) removed}
+ * (usually by some other component), and then these named rule sets can be {@link #executeRules(String, Map, Object...) run} with
+ * inputs and facts to obtain output.
+ * <p>
+ * This service is thread safe. While multiple rule sets can be safely {@link #executeRules(String, Map, Object...) executed} at
+ * the same time, all executions will be properly synchronized with methods to {@link #addRuleSet(RuleSet) add},
+ * {@link #updateRuleSet(RuleSet) update}, and {@link #removeRuleSet(String) remove} rule sets.
+ * </p>
+ * @author Randall Hauch
+ */
+ at ThreadSafe
+public class RuleService implements AdministeredService {
+
+    protected static final ClassLoaderFactory DEFAULT_CLASSLOADER_FACTORY = new StandardClassLoaderFactory(RuleService.class.getClassLoader());
+
+    /**
+     * The administrative component for this service.
+     * @author Randall Hauch
+     */
+    protected class Administrator extends AbstractServiceAdministrator {
+
+        protected Administrator() {
+            super(State.PAUSED);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        protected String serviceName() {
+            return "RuleService";
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        protected void doShutdown( State fromState ) {
+            super.doShutdown(fromState);
+            // Remove all rule sets ...
+            removeAllRuleSets();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public boolean awaitTermination( long timeout, TimeUnit unit ) throws InterruptedException {
+            return doAwaitTermination(timeout, unit);
+        }
+
+    }
+
+    private Logger logger;
+    private ClassLoaderFactory classLoaderFactory = DEFAULT_CLASSLOADER_FACTORY;
+    private final Administrator administrator = new Administrator();
+    private final ReadWriteLock lock = new ReentrantReadWriteLock();
+    @GuardedBy( "lock" )
+    private final Map<String, RuleSet> ruleSets = new HashMap<String, RuleSet>();
+    private final CountDownLatch shutdownLatch = new CountDownLatch(1);
+
+    /**
+     * Create a new rule service, configured with no rule sets. Upon construction, the system is
+     * {@link ServiceAdministrator#isPaused() paused} and must be configured and then {@link ServiceAdministrator#start() started}.
+     */
+    public RuleService() {
+        this.logger = Logger.getLogger(this.getClass());
+    }
+
+    /**
+     * Return the administrative component for this service.
+     * @return the administrative component; never null
+     */
+    public ServiceAdministrator getAdministrator() {
+        return this.administrator;
+    }
+
+    /**
+     * Get the class loader factory that should be used to load sequencers. By default, this service uses a factory that will
+     * return either the {@link Thread#getContextClassLoader() current thread's context class loader} (if not null) or the class
+     * loader that loaded this class.
+     * @return the class loader factory; never null
+     * @see #setClassLoaderFactory(ClassLoaderFactory)
+     */
+    public ClassLoaderFactory getClassLoaderFactory() {
+        return this.classLoaderFactory;
+    }
+
+    /**
+     * Set the Maven Repository that should be used to load the sequencer classes. By default, this service uses a class loader
+     * factory that will return either the {@link Thread#getContextClassLoader() current thread's context class loader} (if not
+     * null) or the class loader that loaded this class.
+     * @param classLoaderFactory the class loader factory reference, or null if the default class loader factory should be used.
+     * @see #getClassLoaderFactory()
+     */
+    public void setClassLoaderFactory( ClassLoaderFactory classLoaderFactory ) {
+        this.classLoaderFactory = classLoaderFactory != null ? classLoaderFactory : DEFAULT_CLASSLOADER_FACTORY;
+    }
+
+    /**
+     * Obtain the rule sets that are currently available in this service.
+     * @return an unmodifiable copy of the rule sets; never null, but possibly empty ...
+     */
+    public Collection<RuleSet> getRuleSets() {
+        List<RuleSet> results = new ArrayList<RuleSet>();
+        try {
+            this.lock.readLock().lock();
+            // Make a copy of the rule sets ...
+            if (ruleSets.size() != 0) results.addAll(this.ruleSets.values());
+        } finally {
+            this.lock.readLock().unlock();
+        }
+        return Collections.unmodifiableList(results);
+    }
+
+    /**
+     * Add a rule set, or update any existing one that represents the {@link RuleSet#equals(Object) same rule set}
+     * @param ruleSet the new rule set
+     * @return true if the rule set was added, or false if the rule set was not added (because it wasn't necessary)
+     * @throws IllegalArgumentException if <code>ruleSet</code> is null
+     * @throws InvalidRuleSetException if the supplied rule set is invalid, incomplete, incorrectly defined, or uses a JSR-94
+     * service provider that cannot be found
+     * @see #updateRuleSet(RuleSet)
+     * @see #removeRuleSet(String)
+     */
+    public boolean addRuleSet( RuleSet ruleSet ) {
+        ArgCheck.isNotNull(ruleSet, "rule set");
+        final String providerUri = ruleSet.getProviderUri();
+        final String ruleSetName = ruleSet.getName();
+        final String rules = ruleSet.getRules();
+        final Map<?, ?> properties = ruleSet.getExecutionSetProperties();
+        final Reader ruleReader = new StringReader(rules);
+        boolean updatedRuleSets = false;
+        try {
+            this.lock.writeLock().lock();
+
+            // Make sure the rule service provider is available ...
+            RuleServiceProvider ruleServiceProvider = findRuleServiceProvider(ruleSet);
+            assert ruleServiceProvider != null;
+
+            // Now register a new execution set ...
+            RuleAdministrator ruleAdmin = ruleServiceProvider.getRuleAdministrator();
+            if (ruleAdmin == null) {
+                throw new InvalidRuleSetException(RepositoryI18n.unableToObtainJsr94RuleAdministrator.text(providerUri, ruleSet.getComponentClassname(), ruleSetName));
+            }
+
+            // Is there is an existing rule set and, if so, whether it has changed ...
+            RuleSet existing = this.ruleSets.get(ruleSetName);
+
+            // Create the rule execution set (do this before deregistering, in case there is a problem)...
+            LocalRuleExecutionSetProvider ruleExecutionSetProvider = ruleAdmin.getLocalRuleExecutionSetProvider(null);
+            RuleExecutionSet executionSet = ruleExecutionSetProvider.createRuleExecutionSet(ruleReader, properties);
+
+            // We should add the execiting rule set if there wasn't one or if the rule set has changed ...
+            boolean shouldAdd = existing == null || ruleSet.hasChanged(existing);
+            if (existing != null && shouldAdd) {
+                // There is an existing execution set and it needs to be updated, so deregister it ...
+                ruleServiceProvider = deregister(ruleSet);
+            }
+            if (shouldAdd) {
+                boolean rollback = false;
+                try {
+                    // Now register the new execution set and update the rule set managed by this service ...
+                    ruleAdmin.registerRuleExecutionSet(ruleSetName, executionSet, null);
+                    this.ruleSets.remove(ruleSet.getName());
+                    this.ruleSets.put(ruleSet.getName(), ruleSet);
+                    updatedRuleSets = true;
+                } catch (Throwable t) {
+                    rollback = true;
+                    throw new InvalidRuleSetException(RepositoryI18n.errorAddingOrUpdatingRuleSet.text(ruleSet.getName()), t);
+                } finally {
+                    if (rollback) {
+                        try {
+                            // There was a problem, so re-register the original existing rule set ...
+                            if (existing != null) {
+                                final String oldRules = existing.getRules();
+                                final Map<?, ?> oldProperties = existing.getExecutionSetProperties();
+                                final Reader oldRuleReader = new StringReader(oldRules);
+                                ruleServiceProvider = findRuleServiceProvider(existing);
+                                assert ruleServiceProvider != null;
+                                executionSet = ruleExecutionSetProvider.createRuleExecutionSet(oldRuleReader, oldProperties);
+                                ruleAdmin.registerRuleExecutionSet(ruleSetName, executionSet, null);
+                                this.ruleSets.remove(ruleSetName);
+                                this.ruleSets.put(ruleSetName, existing);
+                            }
+                        } catch (Throwable rollbackError) {
+                            // There was a problem rolling back to the existing rule set, and we're going to throw the
+                            // exception associated with the updated/new rule set, so just log this problem
+                            this.logger.error(rollbackError, RepositoryI18n.errorRollingBackRuleSetAfterUpdateFailed, ruleSetName);
+                        }
+                    }
+                }
+            }
+        } catch (InvalidRuleSetException e) {
+            throw e;
+        } catch (ConfigurationException t) {
+            throw new InvalidRuleSetException(RepositoryI18n.unableToObtainJsr94RuleAdministrator.text(providerUri, ruleSet.getComponentClassname(), ruleSetName));
+        } catch (RemoteException t) {
+            throw new InvalidRuleSetException(RepositoryI18n.errorUsingJsr94RuleAdministrator.text(providerUri, ruleSet.getComponentClassname(), ruleSetName));
+        } catch (IOException t) {
+            throw new InvalidRuleSetException(RepositoryI18n.errorReadingRulesAndProperties.text(ruleSetName));
+        } catch (RuleExecutionSetDeregistrationException t) {
+            throw new InvalidRuleSetException(RepositoryI18n.errorDeregisteringRuleSetBeforeUpdatingIt.text(ruleSetName));
+        } catch (RuleExecutionSetCreateException t) {
+            throw new InvalidRuleSetException(RepositoryI18n.errorRecreatingRuleSet.text(ruleSetName));
+        } finally {
+            this.lock.writeLock().unlock();
+        }
+        return updatedRuleSets;
+    }
+
+    /**
+     * Update the configuration for a sequencer, or add it if there is no {@link RuleSet#equals(Object) matching configuration}.
+     * @param ruleSet the rule set to be updated
+     * @return true if the rule set was updated, or false if the rule set was not updated (because it wasn't necessary)
+     * @throws InvalidRuleSetException if the supplied rule set is invalid, incomplete, incorrectly defined, or uses a JSR-94
+     * service provider that cannot be found
+     * @see #addRuleSet(RuleSet)
+     * @see #removeRuleSet(String)
+     */
+    public boolean updateRuleSet( RuleSet ruleSet ) {
+        return addRuleSet(ruleSet);
+    }
+
+    /**
+     * Remove a rule set.
+     * @param ruleSetName the name of the rule set to be removed
+     * @return true if the rule set was removed, or if it was not an existing rule set
+     * @throws IllegalArgumentException if <code>ruleSetName</code> is null or empty
+     * @throws SystemFailureException if the rule set was found but there was a problem removing it
+     * @see #addRuleSet(RuleSet)
+     * @see #updateRuleSet(RuleSet)
+     */
+    public boolean removeRuleSet( String ruleSetName ) {
+        ArgCheck.isNotEmpty(ruleSetName, "rule set");
+        try {
+            this.lock.writeLock().lock();
+            RuleSet ruleSet = this.ruleSets.remove(ruleSetName);
+            if (ruleSet != null) {
+                try {
+                    deregister(ruleSet);
+                } catch (Throwable t) {
+                    // There was a problem deregistering the rule set, so put it back ...
+                    this.ruleSets.put(ruleSetName, ruleSet);
+                }
+                return true;
+            }
+        } catch (Throwable t) {
+            throw new SystemFailureException(RepositoryI18n.errorRemovingRuleSet.text(ruleSetName), t);
+        } finally {
+            this.lock.writeLock().unlock();
+        }
+        return false;
+    }
+
+    /**
+     * Get the logger for this system
+     * @return the logger
+     */
+    public Logger getLogger() {
+        return this.logger;
+    }
+
+    /**
+     * Set the logger for this system.
+     * @param logger the logger, or null if the standard logging should be used
+     */
+    public void setLogger( Logger logger ) {
+        this.logger = logger != null ? logger : Logger.getLogger(this.getClass());
+    }
+
+    /**
+     * Execute the set of rules defined by the supplied rule set name. This method is safe to be concurrently called by multiple
+     * threads, and is properly synchronized with the methods to {@link #addRuleSet(RuleSet) add},
+     * {@link #updateRuleSet(RuleSet) update}, and {@link #removeRuleSet(String) remove} rule sets.
+     * @param ruleSetName the {@link RuleSet#getName() name} of the {@link RuleSet} that should be used
+     * @param globals the global variables
+     * @param facts the facts
+     * @return the results of executing the rule set
+     * @throws IllegalArgumentException if the rule set name is null, empty or blank, or if there is no rule set with the given
+     * name
+     * @throws SystemFailureException if there is no JSR-94 rule service provider with the
+     * {@link RuleSet#getProviderUri() RuleSet's provider URI}.
+     */
+    public List<?> executeRules( String ruleSetName, Map<String, Object> globals, Object... facts ) {
+        ArgCheck.isNotEmpty(ruleSetName, "rule set name");
+        List<?> result = null;
+        List<?> factList = Arrays.asList(facts);
+        try {
+            this.lock.readLock().lock();
+
+            // Find the rule set ...
+            RuleSet ruleSet = this.ruleSets.get(ruleSetName);
+            if (ruleSet == null) {
+                throw new IllegalArgumentException(RepositoryI18n.unableToFindRuleSet.text(ruleSetName));
+            }
+
+            // Look up the provider ...
+            RuleServiceProvider ruleServiceProvider = findRuleServiceProvider(ruleSet);
+            assert ruleServiceProvider != null;
+
+            // Create the rule session ...
+            RuleRuntime ruleRuntime = ruleServiceProvider.getRuleRuntime();
+            String executionSetName = ruleSet.getRuleSetUri();
+            RuleSession session = ruleRuntime.createRuleSession(executionSetName, globals, RuleRuntime.STATELESS_SESSION_TYPE);
+            try {
+                StatelessRuleSession statelessSession = (StatelessRuleSession)session;
+                result = statelessSession.executeRules(factList);
+            } finally {
+                session.release();
+            }
+            if (this.logger.isTraceEnabled()) {
+                String msg = "Executed rule set '{1}' with globals {2} and facts {3} resulting in {4}";
+                this.logger.trace(msg, ruleSetName, StringUtil.readableString(globals), StringUtil.readableString(facts), StringUtil.readableString(result));
+            }
+        } catch (Throwable t) {
+            throw new SystemFailureException(RepositoryI18n.errorExecutingRuleSetWithGlobalsAndFacts.text(ruleSetName, StringUtil.readableString(globals), StringUtil.readableString(facts)), t);
+        } finally {
+            this.lock.readLock().unlock();
+        }
+        return result;
+    }
+
+    protected void removeAllRuleSets() {
+        try {
+            lock.writeLock().lock();
+            for (RuleSet ruleSet : ruleSets.values()) {
+                try {
+                    deregister(ruleSet);
+                } catch (Throwable t) {
+                    logger.error(t, RepositoryI18n.errorRemovingRuleSetUponShutdown, ruleSet.getName());
+                }
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+        this.shutdownLatch.countDown();
+    }
+
+    protected boolean doAwaitTermination( long timeout, TimeUnit unit ) throws InterruptedException {
+        return this.shutdownLatch.await(timeout, unit);
+    }
+
+    /**
+     * Finds the JSR-94 service provider instance and returns it. If it could not be found, this method attempts to load it.
+     * @param ruleSet the rule set for which the service provider is to be found; may not be null
+     * @return the rule service provider; never null
+     * @throws ConfigurationException if there is a problem loading the service provider
+     * @throws InvalidRuleSetException if the service provider could not be found
+     */
+    private RuleServiceProvider findRuleServiceProvider( RuleSet ruleSet ) throws ConfigurationException {
+        assert ruleSet != null;
+        String providerUri = ruleSet.getProviderUri();
+        RuleServiceProvider ruleServiceProvider = null;
+        try {
+            // If the provider could not be found, then a ConfigurationException will be thrown ...
+            ruleServiceProvider = RuleServiceProviderManager.getRuleServiceProvider(providerUri);
+        } catch (ConfigurationException e) {
+            try {
+                // Use JSR-94 to load the RuleServiceProvider instance ...
+                ClassLoader loader = this.classLoaderFactory.getClassLoader(ruleSet.getComponentClasspathArray());
+                // Don't call ClassLoader.loadClass(String), as this doesn't initialize the class!!
+                Class.forName(ruleSet.getComponentClassname(), true, loader);
+                ruleServiceProvider = RuleServiceProviderManager.getRuleServiceProvider(providerUri);
+                this.logger.debug("Loaded the rule service provider {0} ({1})", providerUri, ruleSet.getComponentClassname());
+            } catch (ConfigurationException ce) {
+                throw ce;
+            } catch (Throwable t) {
+                throw new InvalidRuleSetException(RepositoryI18n.unableToObtainJsr94ServiceProvider.text(providerUri, ruleSet.getComponentClassname()), t);
+            }
+        }
+        if (ruleServiceProvider == null) {
+            throw new InvalidRuleSetException(RepositoryI18n.unableToObtainJsr94ServiceProvider.text(providerUri, ruleSet.getComponentClassname()));
+        }
+        return ruleServiceProvider;
+    }
+
+    /**
+     * Deregister the supplied rule set, if it could be found. This method does nothing if any of the service provider components
+     * could not be found.
+     * @param ruleSet the rule set to be deregistered; may not be null
+     * @return the service provider reference, or null if the service provider could not be found ...
+     * @throws ConfigurationException
+     * @throws RuleExecutionSetDeregistrationException
+     * @throws RemoteException
+     */
+    private RuleServiceProvider deregister( RuleSet ruleSet ) throws ConfigurationException, RuleExecutionSetDeregistrationException, RemoteException {
+        assert ruleSet != null;
+        // Look up the provider ...
+        String providerUri = ruleSet.getProviderUri();
+        assert providerUri != null;
+
+        // Look for the provider ...
+        RuleServiceProvider ruleServiceProvider = RuleServiceProviderManager.getRuleServiceProvider(providerUri);
+        if (ruleServiceProvider != null) {
+            // Deregister the rule set ...
+            RuleAdministrator ruleAdmin = ruleServiceProvider.getRuleAdministrator();
+            if (ruleAdmin != null) {
+                ruleAdmin.deregisterRuleExecutionSet(ruleSet.getRuleSetUri(), null);
+            }
+        }
+        return ruleServiceProvider;
+    }
+
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/RuleSet.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/RuleSet.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/RuleSet.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,152 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.rules;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.rules.RuleServiceProvider;
+import javax.rules.admin.RuleExecutionSet;
+import javax.rules.admin.RuleExecutionSetProvider;
+import net.jcip.annotations.Immutable;
+import org.jboss.dna.common.component.ClassLoaderFactory;
+import org.jboss.dna.common.component.ComponentConfig;
+import org.jboss.dna.common.util.ArgCheck;
+
+/**
+ * A description of a set of rules compatible with a JSR-94 rule engine.
+ * @author Randall Hauch
+ */
+ at Immutable
+public class RuleSet extends ComponentConfig implements Cloneable {
+
+    private final String providerUri;
+    private final String ruleSetUri;
+    private final String rules;
+    private final Map<String, Object> properties;
+
+    /**
+     * Create a JSR-94 rule set definition.
+     * @param name the name of the rule set, which is considered the unique identifier
+     * @param description the description
+     * @param classname the name of the Java class used for the component
+     * @param classpath the optional classpath (defined in a way compatible with a {@link ClassLoaderFactory}
+     * @param providerUri the URI of the JSR-94 {@link RuleServiceProvider} implementation to use
+     * @param ruleSetUri the URI of the JSR-94 {@link RuleExecutionSet} represented by this object; if null, the name is used
+     * @param rules the string containing the rules in a provider-specific language
+     * @param properties the provider-specific properties, whose values should be strings or byte arrays (the latter if the
+     * provider expects an {@link Reader} with the value)
+     * @throws IllegalArgumentException if any of the name, classname, provider URI, or rules parameters are null, empty or blank,
+     * or if the classname is not a valid Java classname
+     */
+    public RuleSet( String name, String description, String classname, String[] classpath, String providerUri, String ruleSetUri, String rules, Map<String, Object> properties ) {
+        super(name, description, System.currentTimeMillis(), classname, classpath);
+        if (ruleSetUri == null) ruleSetUri = name.trim();
+        ArgCheck.isNotEmpty(ruleSetUri, "rule set URI");
+        ArgCheck.isNotEmpty(providerUri, "provider URI");
+        ArgCheck.isNotEmpty(rules, "rules");
+        this.providerUri = providerUri;
+        this.ruleSetUri = ruleSetUri;
+        this.rules = rules;
+        if (properties == null) properties = Collections.emptyMap();
+        this.properties = Collections.unmodifiableMap(properties);
+    }
+
+    /**
+     * Get the URI of the JSR-94 {@link RuleServiceProvider} implementation that should be used.
+     * @return the URI of the JSR-94 implementation; never null, empty or blank
+     */
+    public String getProviderUri() {
+        return this.providerUri;
+    }
+
+    /**
+     * Get the URI of this rule set. The value must be valid as defined by JSR-94 {@link RuleExecutionSet}.
+     * @return the rule set's URI; never null, empty or blank
+     */
+    public String getRuleSetUri() {
+        return this.ruleSetUri;
+    }
+
+    /**
+     * Get the rules defined in terms of the language reqired by the {@link #getProviderUri() provider}.
+     * @return the rules for this rule set
+     */
+    public String getRules() {
+        return this.rules;
+    }
+
+    /**
+     * Get this rule set's properties as an unmodifiable map. Note that the values of these properties are either strings if the
+     * value is to be {@link #getExecutionSetProperties() passed} literally, or a byte array if the value is to be
+     * {@link #getExecutionSetProperties() passed} as an InputStream.
+     * @return the unmodifiable properties; never null but possible empty
+     */
+    public Map<String, Object> getProperties() {
+        return this.properties;
+    }
+
+    /**
+     * Get the properties for this rule set that can be passed to an {@link RuleExecutionSetProvider}'s
+     * {@link RuleExecutionSetProvider#createRuleExecutionSet(String, Map) createRuleExecutionSet} method.
+     * <p>
+     * This method converts any byte array value in the {@link #getProperties() properties} into an {@link Reader}. Since
+     * {@link ByteArrayInputStream} is used, there is no need to close these stream.
+     * </p>
+     * @return the properties; never null but possible empty
+     */
+    public Map<Object, Object> getExecutionSetProperties() {
+        Map<Object, Object> props = new HashMap<Object, Object>();
+        for (Map.Entry<String, Object> entry : this.properties.entrySet()) {
+            String key = entry.getKey();
+            Object value = entry.getValue();
+            if (value instanceof byte[]) {
+                value = new InputStreamReader(new ByteArrayInputStream((byte[])value));
+            }
+            props.put(key, value);
+        }
+        return props;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean hasChanged( ComponentConfig obj ) {
+        if (super.hasChanged(obj)) return true;
+        RuleSet that = (RuleSet)obj;
+        if (!this.providerUri.equals(that.providerUri)) return true;
+        if (!this.ruleSetUri.equals(that.ruleSetUri)) return true;
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public RuleSet clone() {
+        return new RuleSet(this.getName(), this.getDescription(), this.getComponentClassname(), this.getComponentClasspathArray(), this.providerUri, this.ruleSetUri, this.rules, this.properties);
+    }
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/RuleSetRepositoryMonitor.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/RuleSetRepositoryMonitor.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/rules/RuleSetRepositoryMonitor.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,257 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.rules;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.collection.Problems;
+import org.jboss.dna.common.util.ArgCheck;
+import org.jboss.dna.common.util.Logger;
+import org.jboss.dna.repository.RepositoryI18n;
+import org.jboss.dna.repository.observation.NodeChange;
+import org.jboss.dna.repository.observation.NodeChangeListener;
+import org.jboss.dna.repository.observation.NodeChanges;
+import org.jboss.dna.repository.observation.ObservationService;
+import org.jboss.dna.repository.util.ExecutionContext;
+import org.jboss.dna.repository.util.JcrTools;
+
+/**
+ * A component that can listen to a JCR repository and keep the {@link RuleSet} instances of a {@link RuleService} synchronized
+ * with that repository.
+ * <p>
+ * This class is a {@link NodeChangeListener} that can {@link ObservationService#addListener(NodeChangeListener) subscribe} to
+ * changes in one or more JCR repositories being monitored by an {@link ObservationService}. As changes under the rule sets
+ * branch are discovered, they are processed asynchronously. This ensure that the processing of the repository contents does not
+ * block the other listeners of the {@link ObservationService}.
+ * </p>
+ * @author Randall Hauch
+ */
+ at ThreadSafe
+public class RuleSetRepositoryMonitor implements NodeChangeListener {
+
+    public static final String DEFAULT_JCR_ABSOLUTE_PATH = "/dna:system/dna:ruleSets/";
+
+    protected static final String JCR_PATH_DELIM = "/";
+
+    private final ExecutionContext executionContext;
+    private final RuleService ruleService;
+    private final String jcrAbsolutePath;
+    private final Pattern ruleSetNamePattern;
+    private final ExecutorService executorService;
+    private Logger logger;
+
+    /**
+     * Create an instance that can listen to the {@link RuleSet} definitions stored in a JCR repository and ensure that the
+     * {@link RuleSet} instances of a {@link RuleService} reflect the definitions in the repository.
+     * @param ruleService the rule service that should be kept in sync with the JCR repository.
+     * @param jcrAbsolutePath the absolute path to the branch where the rule sets are defined; if null or empty, the
+     * {@link #DEFAULT_JCR_ABSOLUTE_PATH default path} is used
+     * @param executionContext the context in which this monitor is to execute
+     * @throws IllegalArgumentException if the rule service or execution context is null, or if the supplied
+     * <code>jcrAbsolutePath</code> is invalid
+     */
+    public RuleSetRepositoryMonitor( RuleService ruleService, String jcrAbsolutePath, ExecutionContext executionContext ) {
+        ArgCheck.isNotNull(ruleService, "rule service");
+        ArgCheck.isNotNull(executionContext, "execution context");
+        this.ruleService = ruleService;
+        this.executionContext = executionContext;
+        this.executorService = Executors.newSingleThreadExecutor();
+        this.logger = Logger.getLogger(this.getClass());
+        if (jcrAbsolutePath != null) jcrAbsolutePath = jcrAbsolutePath.trim();
+        this.jcrAbsolutePath = jcrAbsolutePath != null && jcrAbsolutePath.length() != 0 ? jcrAbsolutePath : DEFAULT_JCR_ABSOLUTE_PATH;
+        try {
+            // Create the pattern to extract the rule set name from the absolute path ...
+            String leadingPath = this.jcrAbsolutePath;
+            if (!leadingPath.endsWith(JCR_PATH_DELIM)) leadingPath = leadingPath + JCR_PATH_DELIM;
+            this.ruleSetNamePattern = Pattern.compile(leadingPath + "([^/]+)/?.*");
+        } catch (PatternSyntaxException e) {
+            throw new IllegalArgumentException(RepositoryI18n.unableToBuildRuleSetRegularExpressionPattern.text(e.getPattern(), jcrAbsolutePath, e.getDescription()));
+        }
+    }
+
+    /**
+     * @return ruleService
+     */
+    public RuleService getRuleService() {
+        return this.ruleService;
+    }
+
+    /**
+     * @return jcrAbsolutePath
+     */
+    public String getAbsolutePathToRuleSets() {
+        return this.jcrAbsolutePath;
+    }
+
+    /**
+     * @return logger
+     */
+    public Logger getLogger() {
+        return this.logger;
+    }
+
+    /**
+     * @param logger Sets logger to the specified value.
+     */
+    public void setLogger( Logger logger ) {
+        this.logger = logger;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void onNodeChanges( NodeChanges changes ) {
+        final Map<String, Set<String>> ruleSetNamesByWorkspaceName = new HashMap<String, Set<String>>();
+        for (NodeChange nodeChange : changes) {
+            if (nodeChange.isNotOnPath(this.jcrAbsolutePath)) continue;
+            // Use a regular expression on the absolute path to get the name of the rule set that is affected ...
+            Matcher matcher = this.ruleSetNamePattern.matcher(nodeChange.getAbsolutePath());
+            if (!matcher.matches()) continue;
+            String ruleSetName = matcher.group(1);
+            // Record the repository name ...
+            String workspaceName = nodeChange.getRepositoryWorkspaceName();
+            Set<String> ruleSetNames = ruleSetNamesByWorkspaceName.get(workspaceName);
+            if (ruleSetNames == null) {
+                ruleSetNames = new HashSet<String>();
+                ruleSetNamesByWorkspaceName.put(workspaceName, ruleSetNames);
+            }
+            // Record the rule set name ...
+            ruleSetNames.add(ruleSetName);
+
+        }
+        if (ruleSetNamesByWorkspaceName.isEmpty()) return;
+        // Otherwise there are changes, so submit the names to the executor service ...
+        this.executorService.execute(new Runnable() {
+
+            public void run() {
+                processRuleSets(ruleSetNamesByWorkspaceName);
+            }
+        });
+    }
+
+    /**
+     * Process the rule sets given by the supplied names, keyed by the repository workspace name.
+     * @param ruleSetNamesByWorkspaceName the set of rule set names keyed by the repository workspace name
+     */
+    protected void processRuleSets( Map<String, Set<String>> ruleSetNamesByWorkspaceName ) {
+        final JcrTools tools = this.executionContext.getTools();
+        final String relPathToRuleSets = getAbsolutePathToRuleSets().substring(1);
+        for (Map.Entry<String, Set<String>> entry : ruleSetNamesByWorkspaceName.entrySet()) {
+            String workspaceName = entry.getKey();
+            Session session = null;
+            try {
+                session = this.executionContext.getSessionFactory().createSession(workspaceName);
+                // Look up the node that represents the parent of the rule set nodes ...
+                Node ruleSetsNode = session.getRootNode().getNode(relPathToRuleSets);
+
+                for (String ruleSetName : entry.getValue()) {
+                    // Look up the node that represents the rule set...
+                    if (ruleSetsNode.hasNode(ruleSetName)) {
+                        // We don't handle multiple siblings with the same name, so this should grab the first one ...
+                        Node ruleSetNode = ruleSetsNode.getNode(ruleSetName);
+                        RuleSet ruleSet = buildRuleSet(ruleSetName, ruleSetNode, tools);
+                        if (ruleSet != null) {
+                            // Only do something if the RuleSet was instantiated ...
+                            getRuleService().addRuleSet(ruleSet);
+                        }
+                    } else {
+                        // The node doesn't exist, so remove the rule set ...
+                        getRuleService().removeRuleSet(ruleSetName);
+                    }
+                }
+            } catch (RepositoryException e) {
+                getLogger().error(e, RepositoryI18n.errorObtainingSessionToRepositoryWorkspace, workspaceName);
+            } finally {
+                if (session != null) session.logout();
+            }
+        }
+    }
+
+    /**
+     * Create a rule set from the supplied node. This is called whenever a branch of the repository is changed.
+     * <p>
+     * This implementation expects a node of type 'dna:ruleSet' and the following properties (expressed as XPath statements
+     * relative to the supplied node):
+     * <ul>
+     * <li>The {@link RuleSet#getDescription() description} is obtained from the "<code>./@jcr:description</code>" string
+     * property. This property is optional.</li>
+     * <li>The {@link RuleSet#getComponentClassname() classname} is obtained from the "<code>./@dna:classname</code>" string
+     * property. This property is required.</li>
+     * <li>The {@link RuleSet#getComponentClasspath() classpath} is obtained from the "<code>./@dna:classpath</code>" string
+     * property. This property is optional, and if abscent then the classpath will be assumed from the current context.</li>
+     * <li>The {@link RuleSet#getProviderUri() provider URI} is obtained from the "<code>./@dna:serviceProviderUri</code>"
+     * string property, and corresponds to the URI of the JSR-94 rules engine service provider. This property is required.</li>
+     * <li>The {@link RuleSet#getRuleSetUri() rule set URI} is obtained from the "<code>./@dna:ruleSetUri</code>" string
+     * property. This property is optional and defaults to the node name (e.g., "<code>./@jcr:name</code>").</li>
+     * <li>The {@link RuleSet#getRules() definition of the rules} is obtained from the "<code>./@dna:rules</code>" string
+     * property. This property is required and must be in a form suitable for the JSR-94 rules engine.</li>
+     * <li>The {@link RuleSet#getProperties() properties} are obtained from the "<code>./dna:properties[contains(@jcr:mixinTypes,'dna:propertyContainer')]/*[@jcr:nodeType='dna:property']</code>"
+     * property nodes, where the name of the property is extracted from the property node's "<code>./@jcr:name</code>" string
+     * property and the value of the property is extracted from the property node's "<code>./@dna:propertyValue</code>" string
+     * property. Rule set properties are optional.</li>
+     * </ul>
+     * </p>
+     * @param name the name of the rule set; never null
+     * @param ruleSetNode the node representing the rule set; null if the rule set doesn't exist
+     * @return the rule set for the information stored in the repository, or null if the rule set does not exist or has errors
+     */
+    protected RuleSet buildRuleSet( String name, Node ruleSetNode, JcrTools tools ) {
+        if (ruleSetNode == null) return null;
+
+        Problems problems = new Problems();
+        String description = tools.getPropertyAsString(ruleSetNode, "jcr:description", false, problems);
+        String classname = tools.getPropertyAsString(ruleSetNode, "dna:classname", true, problems);
+        String[] classpath = tools.getPropertyAsStringArray(ruleSetNode, "dna:classpath", false, problems);
+        String providerUri = tools.getPropertyAsString(ruleSetNode, "dna:serviceProviderUri", true, problems);
+        String ruleSetUri = tools.getPropertyAsString(ruleSetNode, "dna:ruleSetUri", true, name, problems);
+        String rules = tools.getPropertyAsString(ruleSetNode, "dna:rules", true, problems);
+        Map<String, Object> properties = tools.loadProperties(ruleSetNode, problems);
+        if (problems.hasProblems()) {
+            // There are problems, so store and save them, and then return null ...
+            try {
+                if (tools.storeProblems(ruleSetNode, problems)) ruleSetNode.save();
+            } catch (RepositoryException e) {
+                this.logger.error(e, RepositoryI18n.errorWritingProblemsOnRuleSet, tools.getReadable(ruleSetNode));
+            }
+            return null;
+        }
+        // There are no problems with this rule set, so make sure that there are no persisted problems anymore ...
+        try {
+            if (tools.removeProblems(ruleSetNode)) ruleSetNode.save();
+        } catch (RepositoryException e) {
+            this.logger.error(e, RepositoryI18n.errorWritingProblemsOnRuleSet, tools.getReadable(ruleSetNode));
+        }
+        return new RuleSet(name, description, classname, classpath, providerUri, ruleSetUri, rules, properties);
+    }
+
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/InvalidSequencerPathExpression.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/InvalidSequencerPathExpression.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/InvalidSequencerPathExpression.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,62 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.sequencers;
+
+
+/**
+ * 
+ * @author Randall Hauch
+ */
+public class InvalidSequencerPathExpression extends RuntimeException {
+
+    /**
+     * 
+     */
+    public InvalidSequencerPathExpression() {
+    }
+
+    /**
+     * @param message
+     */
+    public InvalidSequencerPathExpression( String message ) {
+        super(message);
+
+    }
+
+    /**
+     * @param cause
+     */
+    public InvalidSequencerPathExpression( Throwable cause ) {
+        super(cause);
+
+    }
+
+    /**
+     * @param message
+     * @param cause
+     */
+    public InvalidSequencerPathExpression( String message, Throwable cause ) {
+        super(message, cause);
+
+    }
+
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/Sequencer.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/Sequencer.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/Sequencer.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,100 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.sequencers;
+
+import java.util.Set;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.component.Component;
+import org.jboss.dna.common.monitor.ProgressMonitor;
+import org.jboss.dna.repository.observation.NodeChange;
+import org.jboss.dna.repository.observation.NodeChangeListener;
+import org.jboss.dna.repository.observation.NodeChanges;
+import org.jboss.dna.repository.observation.ObservationService;
+import org.jboss.dna.repository.util.ExecutionContext;
+import org.jboss.dna.repository.util.RepositoryNodePath;
+
+/**
+ * The interface for a DNA sequencer, which sequences nodes and their content to extract additional information from the
+ * information.
+ * <p>
+ * Implementations must provide a no-argument constructor.
+ * </p>
+ * @author Randall Hauch
+ */
+ at ThreadSafe
+public interface Sequencer extends Component<SequencerConfig> {
+
+    /**
+     * Execute the sequencing operation on the supplied node, which has recently been created or changed. The implementation of
+     * this method is responsible for {@link ExecutionContext#getSessionFactory() getting sessions}, modifying the appropriate
+     * nodes, {@link Session#save() saving} any changes made by this sequencer, and {@link Session#logout() closing} all sessions
+     * (and any other acquired resources), even in the case of {@link ProgressMonitor#isCancelled() cancellation} or exceptions.
+     * <p>
+     * The {@link SequencingService} determines the sequencers that should be executed by monitoring the changes to one or more
+     * workspaces (it is a {@link NodeChangeListener} registered with the {@link ObservationService}). Changes in those
+     * workspaces are aggregated for each transaction, and organized into {@link NodeChanges changes for each node}. The
+     * SequencingService then determines for each {@link NodeChange set of changes to a node} the set of full paths to the
+     * properties that have changed and whether those paths {@link SequencerPathExpression#matcher(String) match} the sequencer's
+     * {@link SequencerConfig#getPathExpressions() path expressions}. Each path expression produces the path to the output node,
+     * and these output paths are accumulated and (with the original node that changed, the node change summary, and other
+     * information) supplied to the sequencer via this method.
+     * <p>
+     * It is possible that a sequencer is configured to apply to multiple properties on a node. So, in cases where multiple
+     * properties are changed on a single node (within a single repository transaction), the sequencer will only be executed once.
+     * Also, in such cases the sequencer's configuration may imply multiple output nodes, so it is left to the sequencer to define
+     * the behavior in such cases.
+     * </p>
+     * <p>
+     * This operation should report progress to the supplied {@link ProgressMonitor}. At the beginning of the operation, call
+     * {@link ProgressMonitor#beginTask(double, org.jboss.dna.common.i18n.I18n, Object...)} with a meaningful message describing
+     * the operation and a total for the amount of work that will be done by this sequencer. Then perform the sequencing work,
+     * periodically reporting work by specifying the {@link ProgressMonitor#worked(double) amount of work} that has was just
+     * completed or by {@link ProgressMonitor#createSubtask(double) creating a subtask} and reporting work against that subtask
+     * monitor.
+     * </p>
+     * <p>
+     * The implementation should also periodically check whether the operation has been
+     * {@link ProgressMonitor#isCancelled() cancelled}. If this method returns true, the implementation should abort all work as
+     * soon as possible and close any resources that were acquired or opened.
+     * </p>
+     * <p>
+     * Finally, the implementation should call {@link ProgressMonitor#done()} when the operation has finished.
+     * </p>
+     * @param input the node that has recently been created or changed; never null
+     * @param sequencedPropertyName the name of the property that caused this sequencer to be executed; never null and never empty
+     * @param changes the immutable summary of changes that occurred on the <code>input</code> node within the transaction;
+     * never null
+     * @param outputPaths the paths to the nodes where the sequencing content should be placed; never null and never empty, but
+     * the set may contain paths for non-existant nodes or may reference the <code>input</code> node
+     * @param context the context in which this sequencer is executing; never null
+     * @param progress the progress monitor that should be kept updated with the sequencer's progress and that should be
+     * frequently consulted as to whether this operation has been {@link ProgressMonitor#isCancelled() cancelled}.
+     * @throws RepositoryException if there is a problem while working with the repository
+     * @throws SequencerException if there is an error in this sequencer
+     */
+    void execute( Node input, String sequencedPropertyName, NodeChange changes, Set<RepositoryNodePath> outputPaths, ExecutionContext context, ProgressMonitor progress )
+        throws RepositoryException, SequencerException;
+
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerConfig.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerConfig.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerConfig.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,75 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.sequencers;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import net.jcip.annotations.Immutable;
+import org.jboss.dna.common.component.ComponentConfig;
+
+/**
+ * @author Randall Hauch
+ */
+ at Immutable
+public class SequencerConfig extends ComponentConfig {
+
+    private final Set<SequencerPathExpression> pathExpressions;
+
+    public SequencerConfig( String name, String description, String classname, String[] classpath, String... pathExpressions ) {
+        this(name, description, System.currentTimeMillis(), classname, classpath, pathExpressions);
+    }
+
+    public SequencerConfig( String name, String description, long timestamp, String classname, String[] classpath, String... pathExpressions ) {
+        super(name, description, timestamp, classname, classpath);
+        this.pathExpressions = buildPathExpressionSet(pathExpressions);
+    }
+
+    /* package */static Set<SequencerPathExpression> buildPathExpressionSet( String... pathExpressions ) {
+        Set<SequencerPathExpression> result = null;
+        if (pathExpressions != null) {
+            result = new LinkedHashSet<SequencerPathExpression>();
+            for (String pathExpression : pathExpressions) {
+                if (pathExpression == null) continue;
+                pathExpression = pathExpression.trim();
+                if (pathExpression.length() == 0) continue;
+                result.add(SequencerPathExpression.compile(pathExpression));
+            }
+            result = Collections.unmodifiableSet(result);
+        } else {
+            result = Collections.emptySet(); // already immutable
+        }
+        return result;
+    }
+
+    public Collection<SequencerPathExpression> getPathExpressions() {
+        return Collections.unmodifiableSet(this.pathExpressions);
+    }
+
+    public boolean hasChanged( SequencerConfig that ) {
+        if (super.hasChanged(that)) return true;
+        if (!this.getPathExpressions().equals(that.getPathExpressions())) return true;
+        return false;
+    }
+
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerException.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerException.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerException.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,56 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.sequencers;
+
+/**
+ * @author Randall Hauch
+ */
+public class SequencerException extends RuntimeException {
+
+    /**
+     */
+    public SequencerException() {
+    }
+
+    /**
+     * @param message
+     */
+    public SequencerException( String message ) {
+        super(message);
+    }
+
+    /**
+     * @param cause
+     */
+    public SequencerException( Throwable cause ) {
+        super(cause);
+    }
+
+    /**
+     * @param message
+     * @param cause
+     */
+    public SequencerException( String message, Throwable cause ) {
+        super(message, cause);
+    }
+
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerOutputMap.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerOutputMap.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerOutputMap.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,316 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.sequencers;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import net.jcip.annotations.Immutable;
+import net.jcip.annotations.NotThreadSafe;
+import org.jboss.dna.common.jcr.Path;
+import org.jboss.dna.common.util.StringUtil;
+import org.jboss.dna.spi.sequencers.SequencerOutput;
+
+/**
+ * A basic {@link SequencerOutput} that records all information in-memory and which organizes the properties by
+ * {@link Path node paths} and provides access to the nodes in a natural path-order.
+ * @author Randall Hauch
+ */
+ at NotThreadSafe
+public class SequencerOutputMap implements SequencerOutput, Iterable<SequencerOutputMap.Entry> {
+
+    private static final String JCR_PRIMARY_TYPE_PROPERTY_NAME = "jcr:primaryType";
+    private static final String JCR_NAME_PROPERTY_NAME = "jcr:name";
+
+    private final Map<Path, List<PropertyValue>> data;
+    private boolean valuesSorted = true;
+
+    public SequencerOutputMap() {
+        this.data = new HashMap<Path, List<PropertyValue>>();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void setProperty( String nodePath, String property, Object... values ) {
+        property = property.trim();
+        if (JCR_NAME_PROPERTY_NAME.equals(property)) return; // ignore the "jcr:name" property
+        nodePath = nodePath.trim();
+        if (nodePath.endsWith("/")) nodePath = nodePath.replaceFirst("/+$", "");
+
+        // Find or create the entry for this node ...
+        Path path = new Path(nodePath);
+        List<PropertyValue> properties = this.data.get(path);
+        if (properties == null) {
+            if (values == null || values.length == 0) return; // do nothing
+            properties = new ArrayList<PropertyValue>();
+            this.data.put(path, properties);
+        }
+        if (values == null || values.length == 0) {
+            properties.remove(new PropertyValue(property, null));
+        } else {
+            Object propValue = values.length == 1 ? values[0] : values;
+            PropertyValue value = new PropertyValue(property, propValue);
+            properties.add(value);
+            valuesSorted = false;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void setReference( String nodePath, String property, String... paths ) {
+        if (paths == null || paths.length == 0) {
+            setProperty(nodePath, property, (Object[])null);
+        } else if (paths.length == 1) {
+            setProperty(nodePath, property, new Path(paths[0]));
+        } else {
+            Path[] pathsArray = new Path[paths.length];
+            for (int i = 0; i != paths.length; ++i) {
+                pathsArray[i] = new Path(paths[i]);
+            }
+            setProperty(nodePath, property, (Object[])pathsArray);
+        }
+    }
+
+    /**
+     * Return the number of node entries in this map.
+     * @return the number of entries
+     */
+    public int size() {
+        return this.data.size();
+    }
+
+    /**
+     * Return whether there are no entries
+     * @return true if this container is empty, or false otherwise
+     */
+    public boolean isEmpty() {
+        return this.data.isEmpty();
+    }
+
+    protected List<PropertyValue> removeProperties( Path nodePath ) {
+        return this.data.remove(nodePath);
+    }
+
+    /**
+     * Get the properties for the node given by the supplied path.
+     * @param nodePath the path to the node
+     * @return the property values, or null if there are none
+     */
+    protected List<PropertyValue> getProperties( Path nodePath ) {
+        return data.get(nodePath);
+    }
+
+    /**
+     * Return the entries in this output in an order with shorter paths first.
+     * <p>
+     * {@inheritDoc}
+     */
+    public Iterator<Entry> iterator() {
+        LinkedList<Path> paths = new LinkedList<Path>(data.keySet());
+        Collections.sort(paths);
+        sortValues();
+        return new EntryIterator(paths.iterator());
+    }
+
+    protected void sortValues() {
+        if (!valuesSorted) {
+            for (List<PropertyValue> values : this.data.values()) {
+                if (values.size() > 1) Collections.sort(values);
+            }
+            valuesSorted = true;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        return StringUtil.readableString(this.data);
+    }
+
+    /**
+     * A property name and value pair. PropertyValue instances have a natural order where the <code>jcr:primaryType</code> is
+     * first, followed by all other properties in ascending lexicographical order according to the {@link #getName() name}.
+     * @author Randall Hauch
+     */
+    @Immutable
+    public class PropertyValue implements Comparable<PropertyValue> {
+
+        private final String name;
+        private final Object value;
+
+        protected PropertyValue( String propertyName, Object value ) {
+            this.name = propertyName;
+            this.value = value;
+        }
+
+        /**
+         * Get the property name.
+         * @return the property name; never null
+         */
+        public String getName() {
+            return this.name;
+        }
+
+        /**
+         * Get the property value, which is either a single value or an array of values.
+         * @return the property value
+         */
+        public Object getValue() {
+            return this.value;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public int compareTo( PropertyValue that ) {
+            if (this == that) return 0;
+            if (this.name.equals(JCR_PRIMARY_TYPE_PROPERTY_NAME)) return -1;
+            if (that.name.equals(JCR_PRIMARY_TYPE_PROPERTY_NAME)) return 1;
+            return this.name.compareTo(that.name);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public int hashCode() {
+            return this.name.hashCode();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public boolean equals( Object obj ) {
+            if (obj == this) return true;
+            if (obj instanceof PropertyValue) {
+                PropertyValue that = (PropertyValue)obj;
+                if (!this.getName().equals(that.getName())) return false;
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public String toString() {
+            return "[" + this.name + "=" + StringUtil.readableString(value) + "]";
+        }
+    }
+
+    /**
+     * An entry in a SequencerOutputMap, which contains the path of the node and the {@link #getPropertyValues() property values}
+     * on the node.
+     * @author Randall Hauch
+     */
+    @Immutable
+    public class Entry {
+
+        private final Path path;
+        private final String primaryType;
+        private final List<PropertyValue> properties;
+
+        protected Entry( Path path, List<PropertyValue> properties ) {
+            assert path != null;
+            assert properties != null;
+            this.path = path;
+            this.properties = properties;
+            if (this.properties.size() > 0 && this.properties.get(0).getName().equals("jcr:primaryType")) {
+                PropertyValue primaryTypeProperty = this.properties.remove(0);
+                this.primaryType = primaryTypeProperty.getValue().toString();
+            } else {
+                this.primaryType = null;
+            }
+        }
+
+        /**
+         * @return path
+         */
+        public Path getPath() {
+            return this.path;
+        }
+
+        /**
+         * Get the primary type specified for this node, or null if the type was not specified
+         * @return the primary type, or null
+         */
+        public String getPrimaryTypeValue() {
+            return this.primaryType;
+        }
+
+        /**
+         * Get the property values, which may be empty
+         * @return value
+         */
+        public List<PropertyValue> getPropertyValues() {
+            return getProperties(path);
+        }
+    }
+
+    protected class EntryIterator implements Iterator<Entry> {
+
+        private Path last;
+        private final Iterator<Path> iter;
+
+        protected EntryIterator( Iterator<Path> iter ) {
+            this.iter = iter;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public boolean hasNext() {
+            return iter.hasNext();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public Entry next() {
+            this.last = iter.next();
+            return new Entry(last, getProperties(last));
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public void remove() {
+            if (last == null) throw new IllegalStateException();
+            try {
+                removeProperties(last);
+            } finally {
+                last = null;
+            }
+        }
+    }
+
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerPathExpression.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerPathExpression.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencerPathExpression.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,476 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.sequencers;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import net.jcip.annotations.Immutable;
+import org.jboss.dna.common.util.ArgCheck;
+import org.jboss.dna.common.util.HashCode;
+import org.jboss.dna.repository.RepositoryI18n;
+
+/**
+ * An expression that defines a selection of some change in the repository that signals a sequencing operation should be run, and
+ * the location where the sequencing output should be placed. Sequencer path expressions are used within the
+ * {@link SequencerConfig sequencer configurations} and used to determine whether information in the repository needs to be
+ * sequenced.
+ * <p>
+ * A simple example is the following:
+ * 
+ * <pre>
+ *     /a/b/c at title =&gt; /d/e/f
+ * </pre>
+ * 
+ * which means that a sequencer (that uses this expression in its configuration) should be run any time there is a new or modified
+ * <code>title</code> property on the <code>/a/b/c</code> node, and that the output of the sequencing should be placed at
+ * <code>/d/e/f</code>.
+ * </p>
+ * @author Randall Hauch
+ */
+ at Immutable
+public class SequencerPathExpression implements Serializable {
+
+    /**
+     * The pattern used to break the initial input string into the two major parts, the selection and output expressions. Group 1
+     * contains the selection expression, and group 2 contains the output expression.
+     */
+    private static final Pattern TWO_PART_PATTERN = Pattern.compile("((?:[^=]|=(?!>))+)(?:=>(.+))?");
+
+    protected static final String DEFAULT_OUTPUT_EXPRESSION = ".";
+
+    private static final String REPLACEMENT_VARIABLE_PATTERN_STRING = "(?<!\\\\)\\$(\\d+)"; // (?<!\\)\$(\d+)
+    private static final Pattern REPLACEMENT_VARIABLE_PATTERN = Pattern.compile(REPLACEMENT_VARIABLE_PATTERN_STRING);
+
+    private static final String PARENT_PATTERN_STRING = "[^/]+/\\.\\./"; // [^/]+/\.\./
+    private static final Pattern PARENT_PATTERN = Pattern.compile(PARENT_PATTERN_STRING);
+
+    private static final String SEQUENCE_PATTERN_STRING = "\\[(\\d+(?:,\\d+)*)\\]"; // \[(\d+(,\d+)*)\]
+    private static final Pattern SEQUENCE_PATTERN = Pattern.compile(SEQUENCE_PATTERN_STRING);
+
+    /**
+     * Regular expression used to find unusable XPath predicates within an expression. This pattern results in unusable predicates
+     * in group 1. Note that some predicates may be valid at the end but not valid elsewhere.
+     * <p>
+     * Currently, only index-like predicates (including sequences) are allowed everywhere. Predicates with paths and properties
+     * are allowed only as the last predicate. Predicates with any operators are unused.
+     * </p>
+     * <p>
+     * Nested predicates are not currently allowed.
+     * </p>
+     */
+    // \[(?:(?:\d+(?:,\d+)*)|\*)\]|(?:\[[^\]\+\-\*=\!><'"\s]+\])$|(\[[^\]]+\])
+    private static final String UNUSABLE_PREDICATE_PATTERN_STRING = "\\[(?:(?:\\d+(?:,\\d+)*)|\\*)\\]|(?:\\[[^\\]\\+\\-\\*=\\!><'\"\\s]+\\])$|(\\[[^\\]]+\\])";
+    private static final Pattern UNUSABLE_PREDICATE_PATTERN = Pattern.compile(UNUSABLE_PREDICATE_PATTERN_STRING);
+
+    /**
+     * Regular expression used to find all XPath predicates except index and sequence patterns. This pattern results in the
+     * predicates to be removed in group 1.
+     */
+    // \[(?:(?:\d+(?:,\d+)*)|\*)\]|(\[[^\]]+\])
+    private static final String NON_INDEX_PREDICATE_PATTERN_STRING = "\\[(?:(?:\\d+(?:,\\d+)*)|\\*)\\]|(\\[[^\\]]+\\])";
+    private static final Pattern NON_INDEX_PREDICATE_PATTERN = Pattern.compile(NON_INDEX_PREDICATE_PATTERN_STRING);
+
+    /**
+     * Compile the supplied expression and return the resulting SequencerPathExpression2 instance.
+     * @param expression the expression
+     * @return the path expression; never null
+     * @throws IllegalArgumentException if the expression is null
+     * @throws InvalidSequencerPathExpression if the expression is blank or is not a valid expression
+     */
+    public static final SequencerPathExpression compile( String expression ) throws InvalidSequencerPathExpression {
+        ArgCheck.isNotNull(expression, "sequencer path expression");
+        expression = expression.trim();
+        if (expression.length() == 0) {
+            throw new InvalidSequencerPathExpression(RepositoryI18n.pathExpressionMayNotBeBlank.text());
+        }
+        java.util.regex.Matcher matcher = TWO_PART_PATTERN.matcher(expression);
+        if (!matcher.matches()) {
+            throw new InvalidSequencerPathExpression(RepositoryI18n.pathExpressionIsInvalid.text(expression));
+        }
+        String selectExpression = matcher.group(1);
+        String outputExpression = matcher.group(2);
+        return new SequencerPathExpression(selectExpression, outputExpression);
+    }
+
+    private final String selectExpression;
+    private final String outputExpression;
+    private final Pattern matchPattern;
+    private final Pattern selectPattern;
+    private final int hc;
+
+    protected SequencerPathExpression( String selectExpression, String outputExpression ) throws InvalidSequencerPathExpression {
+        ArgCheck.isNotNull(selectExpression, "select expression");
+        this.selectExpression = selectExpression.trim();
+        this.outputExpression = outputExpression != null ? outputExpression.trim() : DEFAULT_OUTPUT_EXPRESSION;
+        this.hc = HashCode.compute(this.selectExpression, this.outputExpression);
+
+        // Build the match pattern, which determines whether a path matches the condition ...
+        String matchString = this.selectExpression;
+        try {
+            matchString = removeUnusedPredicates(matchString);
+            matchString = replaceXPathPatterns(matchString);
+            this.matchPattern = Pattern.compile(matchString, Pattern.CASE_INSENSITIVE);
+        } catch (PatternSyntaxException e) {
+            String msg = RepositoryI18n.pathExpressionHasInvalidMatch.text(matchString, this.selectExpression, this.outputExpression);
+            throw new InvalidSequencerPathExpression(msg, e);
+        }
+        // Build the select pattern, which determines the path that will be selected ...
+        String selectString = this.selectExpression.trim();
+        try {
+            selectString = removeAllPredicatesExceptIndexes(selectString);
+            selectString = replaceXPathPatterns(selectString);
+            selectString = "(" + selectString + ").*"; // group 1 will have selected path ...
+            this.selectPattern = Pattern.compile(selectString, Pattern.CASE_INSENSITIVE);
+        } catch (PatternSyntaxException e) {
+            String msg = RepositoryI18n.pathExpressionHasInvalidSelect.text(selectString, this.selectExpression, this.outputExpression);
+            throw new InvalidSequencerPathExpression(msg, e);
+        }
+    }
+
+    /**
+     * Replace certain XPath patterns that are not used or understood.
+     * @param expression the input regular expressions string; may not be null
+     * @return the regular expression with all unused XPath patterns removed; never null
+     */
+    protected String removeUnusedPredicates( String expression ) {
+        assert expression != null;
+        java.util.regex.Matcher matcher = UNUSABLE_PREDICATE_PATTERN.matcher(expression);
+        StringBuffer sb = new StringBuffer();
+        if (matcher.find()) {
+            do {
+                // Remove those predicates that show up in group 1 ...
+                String predicateStr = matcher.group(0);
+                String unusablePredicateStr = matcher.group(1);
+                if (unusablePredicateStr != null) {
+                    predicateStr = "";
+                }
+                matcher.appendReplacement(sb, predicateStr);
+            } while (matcher.find());
+            matcher.appendTail(sb);
+            expression = sb.toString();
+        }
+        return expression;
+    }
+
+    /**
+     * Remove all XPath predicates from the supplied regular expression string.
+     * @param expression the input regular expressions string; may not be null
+     * @return the regular expression with all XPath predicates removed; never null
+     */
+    protected String removeAllPredicatesExceptIndexes( String expression ) {
+        assert expression != null;
+        java.util.regex.Matcher matcher = NON_INDEX_PREDICATE_PATTERN.matcher(expression);
+        StringBuffer sb = new StringBuffer();
+        if (matcher.find()) {
+            do {
+                // Remove those predicates that show up in group 1 ...
+                String predicateStr = matcher.group(0);
+                String unusablePredicateStr = matcher.group(1);
+                if (unusablePredicateStr != null) {
+                    predicateStr = "";
+                }
+                matcher.appendReplacement(sb, predicateStr);
+            } while (matcher.find());
+            matcher.appendTail(sb);
+            expression = sb.toString();
+        }
+        return expression;
+    }
+
+    /**
+     * Replace certain XPath patterns, including some predicates, with substrings that are compatible with regular expressions.
+     * @param expression the input regular expressions string; may not be null
+     * @return the regular expression with XPath patterns replaced with regular expression fragments; never null
+     */
+    protected String replaceXPathPatterns( String expression ) {
+        assert expression != null;
+        // replace 2 or more sequential '|' characters in an OR expression
+        expression = expression.replaceAll("[\\|]{2,}", "|");
+        // if there is an empty expression in an OR expression, make the whole segment optional ...
+        // (e.g., "/a/b/(c|)/d" => "a/b(/(c))?/d"
+        expression = expression.replaceAll("/(\\([^|]+)(\\|){2,}([^)]+\\))", "(/$1$2$3)?");
+        expression = expression.replaceAll("/\\(\\|+([^)]+)\\)", "(/($1))?");
+        expression = expression.replaceAll("/\\((([^|]+)(\\|[^|]+)*)\\|+\\)", "(/($1))?");
+
+        // // Allow any path (that doesn't contain an explicit counter) to contain a counter,
+        // // done by replacing any '/' or '|' that isn't preceded by ']' or '*' or '/' or '(' with '(\[\d+\])?/'...
+        // input = input.replaceAll("(?<=[^\\]\\*/(])([/|])", "(?:\\\\[\\\\d+\\\\])?$1");
+
+        // Does the path contain any '[]' or '[*]' or '[0]' or '[n]' (where n is any positive integers)...
+        // '[*]/' => '(\[\d+\])?/'
+        expression = expression.replaceAll("\\[\\]", "(?:\\\\[\\\\d+\\\\])?"); // index is optional
+        // '[]/' => '(\[\d+\])?/'
+        expression = expression.replaceAll("\\[[*]\\]", "(?:\\\\[\\\\d+\\\\])?"); // index is optional
+        // '[0]/' => '(\[0\])?/'
+        expression = expression.replaceAll("\\[0\\]", "(?:\\\\[0\\\\])?"); // index is optional
+        // '[n]/' => '\[n\]/'
+        expression = expression.replaceAll("\\[([1-9]\\d*)\\]", "\\\\[$1\\\\]"); // index is required
+
+        // Change any other end predicates to not be wrapped by braces but to begin with a slash ...
+        // ...'[x]' => ...'/x'
+        expression = expression.replaceAll("(?<!\\\\)\\[([^\\]]*)\\]$", "/$1");
+
+        // Replace all '[n,m,o,p]' type sequences with '[(n|m|o|p)]'
+        java.util.regex.Matcher matcher = SEQUENCE_PATTERN.matcher(expression);
+        StringBuffer sb = new StringBuffer();
+        boolean result = matcher.find();
+        if (result) {
+            do {
+                String sequenceStr = matcher.group(1);
+                boolean optional = false;
+                if (sequenceStr.startsWith("0,")) {
+                    sequenceStr = sequenceStr.replaceFirst("^0,", "");
+                    optional = true;
+                }
+                if (sequenceStr.endsWith(",0")) {
+                    sequenceStr = sequenceStr.replaceFirst(",0$", "");
+                    optional = true;
+                }
+                if (sequenceStr.contains(",0,")) {
+                    sequenceStr = sequenceStr.replaceAll(",0,", ",");
+                    optional = true;
+                }
+                sequenceStr = sequenceStr.replaceAll(",", "|");
+                String replacement = "\\\\[(?:" + sequenceStr + ")\\\\]";
+                if (optional) {
+                    replacement = "(?:" + replacement + ")?";
+                }
+                matcher.appendReplacement(sb, replacement);
+                result = matcher.find();
+            } while (result);
+            matcher.appendTail(sb);
+            expression = sb.toString();
+        }
+
+        // Order is important here
+        expression = expression.replaceAll("[*]", "[^/]*");
+        expression = expression.replaceAll("[/]{2,}", "(/[^/]*)*/");
+        return expression;
+    }
+
+    /**
+     * @return selectExpression
+     */
+    public String getSelectExpression() {
+        return this.selectExpression;
+    }
+
+    /**
+     * @return outputExpression
+     */
+    public String getOutputExpression() {
+        return this.outputExpression;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        return this.hc;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals( Object obj ) {
+        if (obj == this) return true;
+        if (obj instanceof SequencerPathExpression) {
+            SequencerPathExpression that = (SequencerPathExpression)obj;
+            if (!this.selectExpression.equalsIgnoreCase(that.selectExpression)) return false;
+            if (!this.outputExpression.equalsIgnoreCase(that.outputExpression)) return false;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        return this.selectExpression + "=>" + this.outputExpression;
+    }
+
+    /**
+     * @param absolutePath
+     * @return the matcher
+     */
+    public Matcher matcher( String absolutePath ) {
+        // Determine if the input path match the select expression ...
+        String originalAbsolutePath = absolutePath;
+        // if (!absolutePath.endsWith("/")) absolutePath = absolutePath + "/";
+        // Remove all trailing '/' ...
+        absolutePath = absolutePath.replaceAll("/+$", "");
+
+        // See if the supplied absolute path matches the pattern ...
+        final java.util.regex.Matcher matcher = this.matchPattern.matcher(absolutePath);
+        if (!matcher.matches()) {
+            // No match, so return immediately ...
+            return new Matcher(originalAbsolutePath, null, null);
+        }
+        Map<Integer, String> replacements = new HashMap<Integer, String>();
+        for (int i = 0, count = matcher.groupCount(); i <= count; ++i) {
+            replacements.put(i, matcher.group(i));
+        }
+
+        // The absolute path does match the pattern, so use the select pattern and try to grab the selected path ...
+        final java.util.regex.Matcher selectMatcher = this.selectPattern.matcher(absolutePath);
+        if (!selectMatcher.matches()) {
+            // Nothing can be selected, so return immediately ...
+            return new Matcher(originalAbsolutePath, null, null);
+        }
+        // Grab the selected path ...
+        String selectedPath = selectMatcher.group(1);
+
+        // Remove the trailing '/@property' ...
+        selectedPath = selectedPath.replaceAll("/@[^/\\[\\]]+$", "");
+
+        // Find the output path using the groups from the match pattern ...
+        String outputPath = this.outputExpression;
+        if (!DEFAULT_OUTPUT_EXPRESSION.equals(outputPath)) {
+            java.util.regex.Matcher replacementMatcher = REPLACEMENT_VARIABLE_PATTERN.matcher(outputPath);
+            StringBuffer sb = new StringBuffer();
+            if (replacementMatcher.find()) {
+                do {
+                    String variable = replacementMatcher.group(1);
+                    String replacement = replacements.get(Integer.valueOf(variable));
+                    if (replacement == null) replacement = replacementMatcher.group(0);
+                    replacementMatcher.appendReplacement(sb, replacement);
+                } while (replacementMatcher.find());
+                replacementMatcher.appendTail(sb);
+                outputPath = sb.toString();
+            }
+            // Make sure there is a trailing '/' ...
+            if (!outputPath.endsWith("/")) outputPath = outputPath + "/";
+
+            // Replace all references to "/./" with "/" ...
+            outputPath = outputPath.replaceAll("/\\./", "/");
+
+            // Remove any path segment followed by a parent reference ...
+            java.util.regex.Matcher parentMatcher = PARENT_PATTERN.matcher(outputPath);
+            while (parentMatcher.find()) {
+                outputPath = parentMatcher.replaceAll("");
+                // Make sure there is a trailing '/' ...
+                if (!outputPath.endsWith("/")) outputPath = outputPath + "/";
+                parentMatcher = PARENT_PATTERN.matcher(outputPath);
+            }
+
+            // Remove all multiple occurrences of '/' ...
+            outputPath = outputPath.replaceAll("/{2,}", "/");
+
+            // Remove the trailing '/@property' ...
+            outputPath = outputPath.replaceAll("/@[^/\\[\\]]+$", "");
+
+            // Remove a trailing '/' ...
+            outputPath = outputPath.replaceAll("/$", "");
+
+            // If the output path is blank, then use the default output expression ...
+            if (outputPath.length() == 0) outputPath = DEFAULT_OUTPUT_EXPRESSION;
+
+        }
+        if (DEFAULT_OUTPUT_EXPRESSION.equals(outputPath)) {
+            // The output path is the default expression, so use the selected path ...
+            outputPath = selectedPath;
+        }
+
+        return new Matcher(originalAbsolutePath, selectedPath, outputPath);
+    }
+
+    @Immutable
+    public static class Matcher {
+
+        private final String inputPath;
+        private final String selectedPath;
+        private final String outputPath;
+        private final int hc;
+
+        protected Matcher( String inputPath, String selectedPath, String outputPath ) {
+            this.inputPath = inputPath;
+            this.selectedPath = selectedPath;
+            this.outputPath = outputPath;
+            this.hc = HashCode.compute(this.inputPath, this.selectedPath, this.outputPath);
+        }
+
+        public boolean matches() {
+            return this.selectedPath != null && this.outputPath != null;
+        }
+
+        /**
+         * @return inputPath
+         */
+        public String getInputPath() {
+            return this.inputPath;
+        }
+
+        /**
+         * @return selectPattern
+         */
+        public String getSelectedPath() {
+            return this.selectedPath;
+        }
+
+        /**
+         * @return outputPath
+         */
+        public String getOutputPath() {
+            return this.outputPath;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public int hashCode() {
+            return this.hc;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public boolean equals( Object obj ) {
+            if (obj == this) return true;
+            if (obj instanceof SequencerPathExpression.Matcher) {
+                SequencerPathExpression.Matcher that = (SequencerPathExpression.Matcher)obj;
+                if (!this.inputPath.equalsIgnoreCase(that.inputPath)) return false;
+                if (!this.selectedPath.equalsIgnoreCase(that.selectedPath)) return false;
+                if (!this.outputPath.equalsIgnoreCase(that.outputPath)) return false;
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public String toString() {
+            return this.selectedPath + " => " + this.outputPath;
+        }
+    }
+
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencingService.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencingService.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/SequencingService.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,710 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.sequencers;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.observation.Event;
+import net.jcip.annotations.Immutable;
+import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.component.ClassLoaderFactory;
+import org.jboss.dna.common.component.ComponentLibrary;
+import org.jboss.dna.common.component.StandardClassLoaderFactory;
+import org.jboss.dna.common.monitor.LoggingProgressMonitor;
+import org.jboss.dna.common.monitor.ProgressMonitor;
+import org.jboss.dna.common.monitor.SimpleProgressMonitor;
+import org.jboss.dna.common.util.ArgCheck;
+import org.jboss.dna.common.util.HashCode;
+import org.jboss.dna.common.util.Logger;
+import org.jboss.dna.repository.RepositoryI18n;
+import org.jboss.dna.repository.observation.NodeChange;
+import org.jboss.dna.repository.observation.NodeChangeListener;
+import org.jboss.dna.repository.observation.NodeChanges;
+import org.jboss.dna.repository.services.AbstractServiceAdministrator;
+import org.jboss.dna.repository.services.AdministeredService;
+import org.jboss.dna.repository.services.ServiceAdministrator;
+import org.jboss.dna.repository.util.ExecutionContext;
+import org.jboss.dna.repository.util.JcrTools;
+import org.jboss.dna.repository.util.RepositoryNodePath;
+import org.jboss.dna.repository.util.SessionFactory;
+
+/**
+ * A sequencing system is used to monitor changes in the content of {@link Repository JCR repositories} and to sequence the
+ * content to extract or to generate structured information.
+ * @author Randall Hauch
+ */
+public class SequencingService implements AdministeredService, NodeChangeListener {
+
+    /**
+     * Interface used to select the set of {@link Sequencer} instances that should be run.
+     * @author Randall Hauch
+     */
+    public static interface Selector {
+
+        /**
+         * Select the sequencers that should be used to sequence the supplied node.
+         * @param sequencers the list of all sequencers available at the moment; never null
+         * @param node the node to be sequenced; never null
+         * @param nodeChange the set of node changes; never null
+         * @return the list of sequencers that should be used; may not be null
+         */
+        List<Sequencer> selectSequencers( List<Sequencer> sequencers, Node node, NodeChange nodeChange );
+    }
+
+    /**
+     * The default {@link Selector} implementation that selects every sequencer every time it's called, regardless of the node (or
+     * logger) supplied.
+     * @author Randall Hauch
+     */
+    protected static class DefaultSelector implements Selector {
+
+        public List<Sequencer> selectSequencers( List<Sequencer> sequencers, Node node, NodeChange nodeChange ) {
+            return sequencers;
+        }
+    }
+
+    /**
+     * Interface used to determine whether a {@link NodeChange} should be processed.
+     * @author Randall Hauch
+     */
+    public static interface NodeFilter {
+
+        /**
+         * Determine whether the node represented by the supplied change should be submitted for sequencing.
+         * @param nodeChange the node change event
+         * @return true if the node should be submitted for sequencing, or false if the change should be ignored
+         */
+        boolean accept( NodeChange nodeChange );
+    }
+
+    /**
+     * The default filter implementation, which accepts only new nodes or nodes that have new or changed properties.
+     * @author Randall Hauch
+     */
+    protected static class DefaultNodeFilter implements NodeFilter {
+
+        public boolean accept( NodeChange nodeChange ) {
+            // Only care about new nodes or nodes that have new/changed properies ...
+            return nodeChange.includesEventTypes(Event.NODE_ADDED, Event.PROPERTY_ADDED, Event.PROPERTY_CHANGED);
+        }
+    }
+
+    /**
+     * The default {@link Selector} that considers every {@link Sequencer} to be used for every node.
+     * @see SequencingService#setSequencerSelector(org.jboss.dna.repository.sequencers.SequencingService.Selector)
+     */
+    public static final Selector DEFAULT_SEQUENCER_SELECTOR = new DefaultSelector();
+    /**
+     * The default {@link NodeFilter} that accepts new nodes or nodes that have new/changed properties.
+     * @see SequencingService#setSequencerSelector(org.jboss.dna.repository.sequencers.SequencingService.Selector)
+     */
+    public static final NodeFilter DEFAULT_NODE_FILTER = new DefaultNodeFilter();
+
+    /**
+     * Class loader factory instance that always returns the
+     * {@link Thread#getContextClassLoader() current thread's context class loader} (if not null) or component library's class
+     * loader.
+     */
+    protected static final ClassLoaderFactory DEFAULT_CLASSLOADER_FACTORY = new StandardClassLoaderFactory(SequencingService.class.getClassLoader());
+
+    /**
+     * The administrative component for this service.
+     * @author Randall Hauch
+     */
+    protected class Administrator extends AbstractServiceAdministrator {
+
+        protected Administrator() {
+            super(State.PAUSED);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        protected String serviceName() {
+            return RepositoryI18n.sequencingServiceName.text();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        protected void doStart( State fromState ) {
+            super.doStart(fromState);
+            startService();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        protected void doShutdown( State fromState ) {
+            super.doShutdown(fromState);
+            shutdownService();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public boolean awaitTermination( long timeout, TimeUnit unit ) throws InterruptedException {
+            return doAwaitTermination(timeout, unit);
+        }
+
+    }
+
+    private ExecutionContext executionContext;
+    private ComponentLibrary<Sequencer, SequencerConfig> sequencerLibrary = new ComponentLibrary<Sequencer, SequencerConfig>();
+    private Selector sequencerSelector = DEFAULT_SEQUENCER_SELECTOR;
+    private NodeFilter nodeFilter = DEFAULT_NODE_FILTER;
+    private ExecutorService executorService;
+    private Logger logger = Logger.getLogger(this.getClass());
+    private final Statistics statistics = new Statistics();
+    private final Administrator administrator = new Administrator();
+
+    /**
+     * Create a new sequencing system, configured with no sequencers and not monitoring any workspaces. Upon construction, the
+     * system is {@link ServiceAdministrator#isPaused() paused} and must be configured and then
+     * {@link ServiceAdministrator#start() started}.
+     */
+    public SequencingService() {
+        this.sequencerLibrary.setClassLoaderFactory(DEFAULT_CLASSLOADER_FACTORY);
+    }
+
+    /**
+     * Return the administrative component for this service.
+     * @return the administrative component; never null
+     */
+    public ServiceAdministrator getAdministrator() {
+        return this.administrator;
+    }
+
+    /**
+     * Get the statistics for this system.
+     * @return statistics
+     */
+    public Statistics getStatistics() {
+        return this.statistics;
+    }
+
+    /**
+     * @return sequencerLibrary
+     */
+    protected ComponentLibrary<Sequencer, SequencerConfig> getSequencerLibrary() {
+        return this.sequencerLibrary;
+    }
+
+    /**
+     * Get the class loader factory that should be used to load sequencers. By default, this service uses a factory that will
+     * return either the {@link Thread#getContextClassLoader() current thread's context class loader} (if not null) or the class
+     * loader that loaded this class.
+     * @return the class loader factory; never null
+     * @see #setClassLoaderFactory(ClassLoaderFactory)
+     */
+    public ClassLoaderFactory getClassLoaderFactory() {
+        return this.sequencerLibrary.getClassLoaderFactory();
+    }
+
+    /**
+     * Set the Maven Repository that should be used to load the sequencer classes. By default, this service uses a class loader
+     * factory that will return either the {@link Thread#getContextClassLoader() current thread's context class loader} (if not
+     * null) or the class loader that loaded this class.
+     * @param classLoaderFactory the class loader factory reference, or null if the default class loader factory should be used.
+     * @see #getClassLoaderFactory()
+     */
+    public void setClassLoaderFactory( ClassLoaderFactory classLoaderFactory ) {
+        this.sequencerLibrary.setClassLoaderFactory(classLoaderFactory != null ? classLoaderFactory : DEFAULT_CLASSLOADER_FACTORY);
+    }
+
+    /**
+     * Add the configuration for a sequencer, or update any existing one that represents the
+     * {@link SequencerConfig#equals(Object) same configuration}
+     * @param config the new configuration
+     * @return true if the sequencer was added, or false if there already was an existing and
+     * {@link SequencerConfig#hasChanged(SequencerConfig) unchanged} sequencer configuration
+     * @throws IllegalArgumentException if <code>config</code> is null
+     * @see #updateSequencer(SequencerConfig)
+     * @see #removeSequencer(SequencerConfig)
+     */
+    public boolean addSequencer( SequencerConfig config ) {
+        return this.sequencerLibrary.add(config);
+    }
+
+    /**
+     * Update the configuration for a sequencer, or add it if there is no
+     * {@link SequencerConfig#equals(Object) matching configuration}.
+     * @param config the updated (or new) configuration
+     * @return true if the sequencer was updated, or false if there already was an existing and
+     * {@link SequencerConfig#hasChanged(SequencerConfig) unchanged} sequencer configuration
+     * @throws IllegalArgumentException if <code>config</code> is null
+     * @see #addSequencer(SequencerConfig)
+     * @see #removeSequencer(SequencerConfig)
+     */
+    public boolean updateSequencer( SequencerConfig config ) {
+        return this.sequencerLibrary.update(config);
+    }
+
+    /**
+     * Remove the configuration for a sequencer.
+     * @param config the configuration to be removed
+     * @return true if the sequencer was removed, or false if there was no existing sequencer
+     * @throws IllegalArgumentException if <code>config</code> is null
+     * @see #addSequencer(SequencerConfig)
+     * @see #updateSequencer(SequencerConfig)
+     */
+    public boolean removeSequencer( SequencerConfig config ) {
+        return this.sequencerLibrary.remove(config);
+    }
+
+    /**
+     * Get the logger for this system
+     * @return the logger
+     */
+    public Logger getLogger() {
+        return this.logger;
+    }
+
+    /**
+     * Set the logger for this system.
+     * @param logger the logger, or null if the standard logging should be used
+     */
+    public void setLogger( Logger logger ) {
+        this.logger = logger != null ? logger : Logger.getLogger(this.getClass());
+    }
+
+    /**
+     * @return executionContext
+     */
+    public ExecutionContext getExecutionContext() {
+        return this.executionContext;
+    }
+
+    /**
+     * @param executionContext Sets executionContext to the specified value.
+     */
+    public void setExecutionContext( ExecutionContext executionContext ) {
+        ArgCheck.isNotNull(executionContext, "execution context");
+        if (this.getAdministrator().isStarted()) {
+            throw new IllegalStateException(RepositoryI18n.unableToChangeExecutionContextWhileRunning.text());
+        }
+        this.executionContext = executionContext;
+    }
+
+    /**
+     * Get the executor service used to run the sequencers.
+     * @return the executor service
+     * @see #setExecutorService(ExecutorService)
+     */
+    public ExecutorService getExecutorService() {
+        return this.executorService;
+    }
+
+    /**
+     * Set the executor service that should be used by this system. By default, the system is set up with a
+     * {@link Executors#newSingleThreadExecutor() executor that uses a single thread}.
+     * @param executorService the executor service
+     * @see #getExecutorService()
+     * @see Executors#newCachedThreadPool()
+     * @see Executors#newCachedThreadPool(java.util.concurrent.ThreadFactory)
+     * @see Executors#newFixedThreadPool(int)
+     * @see Executors#newFixedThreadPool(int, java.util.concurrent.ThreadFactory)
+     * @see Executors#newScheduledThreadPool(int)
+     * @see Executors#newScheduledThreadPool(int, java.util.concurrent.ThreadFactory)
+     * @see Executors#newSingleThreadExecutor()
+     * @see Executors#newSingleThreadExecutor(java.util.concurrent.ThreadFactory)
+     * @see Executors#newSingleThreadScheduledExecutor()
+     * @see Executors#newSingleThreadScheduledExecutor(java.util.concurrent.ThreadFactory)
+     */
+    public void setExecutorService( ExecutorService executorService ) {
+        ArgCheck.isNotNull(executorService, "executor service");
+        if (this.getAdministrator().isStarted()) {
+            throw new IllegalStateException(RepositoryI18n.unableToChangeExecutionContextWhileRunning.text());
+        }
+        this.executorService = executorService;
+    }
+
+    /**
+     * Override this method to creates a different kind of default executor service. This method is called when the system is
+     * {@link #start() started} without an executor service being {@link #setExecutorService(ExecutorService) set}.
+     * <p>
+     * This method creates a {@link Executors#newSingleThreadExecutor() single-threaded executor}.
+     * </p>
+     * @return
+     */
+    protected ExecutorService createDefaultExecutorService() {
+        return Executors.newSingleThreadExecutor();
+    }
+
+    protected void startService() {
+        if (this.getExecutionContext() == null) {
+            throw new IllegalStateException(RepositoryI18n.unableToStartSequencingServiceWithoutExecutionContext.text());
+        }
+        if (this.executorService == null) {
+            this.executorService = createDefaultExecutorService();
+        }
+        assert this.executorService != null;
+        assert this.sequencerSelector != null;
+        assert this.nodeFilter != null;
+        assert this.sequencerLibrary != null;
+    }
+
+    protected void shutdownService() {
+        if (this.executorService != null) {
+            this.executorService.shutdown();
+        }
+    }
+
+    protected boolean doAwaitTermination( long timeout, TimeUnit unit ) throws InterruptedException {
+        if (this.executorService.isShutdown()) return true;
+        return this.executorService.awaitTermination(timeout, unit);
+    }
+
+    /**
+     * Get the sequencing selector used by this system.
+     * @return the sequencing selector
+     */
+    public Selector getSequencerSelector() {
+        return this.sequencerSelector;
+    }
+
+    /**
+     * Set the sequencer selector, or null if the {@link #DEFAULT_SEQUENCER_SELECTOR default sequencer selector} should be used.
+     * @param sequencerSelector the selector
+     */
+    public void setSequencerSelector( Selector sequencerSelector ) {
+        this.sequencerSelector = sequencerSelector != null ? sequencerSelector : DEFAULT_SEQUENCER_SELECTOR;
+    }
+
+    /**
+     * Get the node filter used by this system.
+     * @return the node filter
+     */
+    public NodeFilter getNodeFilter() {
+        return this.nodeFilter;
+    }
+
+    /**
+     * Set the filter that checks which nodes are to be sequenced, or null if the {@link #DEFAULT_NODE_FILTER default node filter}
+     * should be used.
+     * @param nodeFilter the new node filter
+     */
+    public void setNodeFilter( NodeFilter nodeFilter ) {
+        this.nodeFilter = nodeFilter != null ? nodeFilter : DEFAULT_NODE_FILTER;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void onNodeChanges( NodeChanges changes ) {
+        NodeFilter filter = this.getNodeFilter();
+        for (final NodeChange changedNode : changes) {
+            // Only care about new nodes or nodes that have new/changed properies ...
+            if (filter.accept(changedNode)) {
+                this.executorService.execute(new Runnable() {
+
+                    public void run() {
+                        processChangedNode(changedNode);
+                    }
+                });
+            }
+        }
+    }
+
+    /**
+     * Do the work of processing by sequencing the node. This method is called by the {@link #executorService executor service}
+     * when it performs it's work on the enqueued {@link ChangedNode ChangedNode runnable objects}.
+     * @param node the node to be processed.
+     */
+    protected void processChangedNode( NodeChange changedNode ) {
+        try {
+            final String repositoryWorkspaceName = changedNode.getRepositoryWorkspaceName();
+            Session session = null;
+            try {
+                // Figure out which sequencers accept this path,
+                // and track which output nodes should be passed to each sequencer...
+                final String nodePath = changedNode.getAbsolutePath();
+                Map<SequencerCall, Set<RepositoryNodePath>> sequencerCalls = new HashMap<SequencerCall, Set<RepositoryNodePath>>();
+                List<Sequencer> allSequencers = this.sequencerLibrary.getInstances();
+                List<Sequencer> sequencers = new ArrayList<Sequencer>(allSequencers.size());
+                for (Sequencer sequencer : allSequencers) {
+                    final SequencerConfig config = sequencer.getConfiguration();
+                    for (SequencerPathExpression pathExpression : config.getPathExpressions()) {
+                        for (String propertyName : changedNode.getModifiedProperties()) {
+                            String path = nodePath + "/@" + propertyName;
+                            SequencerPathExpression.Matcher matcher = pathExpression.matcher(path);
+                            if (matcher.matches()) {
+                                // String selectedPath = matcher.getSelectedPath();
+                                RepositoryNodePath outputPath = RepositoryNodePath.parse(matcher.getOutputPath(), repositoryWorkspaceName);
+                                SequencerCall call = new SequencerCall(sequencer, propertyName);
+                                // Record the output path ...
+                                Set<RepositoryNodePath> outputPaths = sequencerCalls.get(call);
+                                if (outputPaths == null) {
+                                    outputPaths = new HashSet<RepositoryNodePath>();
+                                    sequencerCalls.put(call, outputPaths);
+                                }
+                                outputPaths.add(outputPath);
+                                sequencers.add(sequencer);
+                                break;
+                            }
+                        }
+                    }
+                }
+
+                Node node = null;
+                if (!sequencers.isEmpty()) {
+                    // Create a session that we'll use for all sequencing ...
+                    session = this.getExecutionContext().getSessionFactory().createSession(repositoryWorkspaceName);
+
+                    // Find the changed node ...
+                    String relPath = changedNode.getAbsolutePath().replaceAll("^/+", "");
+                    node = session.getRootNode().getNode(relPath);
+
+                    // Figure out which sequencers should run ...
+                    sequencers = this.sequencerSelector.selectSequencers(sequencers, node, changedNode);
+                }
+                if (sequencers.isEmpty()) {
+                    this.statistics.recordNodeSkipped();
+                    if (this.logger.isDebugEnabled()) {
+                        this.logger.trace("Skipping '{}': no sequencers matched this condition", changedNode);
+                    }
+                } else {
+                    // Run each of those sequencers ...
+                    ProgressMonitor progressMonitor = new SimpleProgressMonitor(RepositoryI18n.sequencerTask.text(changedNode));
+                    if (this.logger.isTraceEnabled()) {
+                        progressMonitor = new LoggingProgressMonitor(progressMonitor, this.logger, Logger.Level.TRACE);
+                    }
+                    try {
+                        progressMonitor.beginTask(sequencerCalls.size(), RepositoryI18n.sequencerTask, changedNode);
+                        for (Map.Entry<SequencerCall, Set<RepositoryNodePath>> entry : sequencerCalls.entrySet()) {
+                            final SequencerCall sequencerCall = entry.getKey();
+                            final Set<RepositoryNodePath> outputPaths = entry.getValue();
+                            final Sequencer sequencer = sequencerCall.getSequencer();
+                            final String sequencerName = sequencer.getConfiguration().getName();
+                            final String propertyName = sequencerCall.getSequencedPropertyName();
+
+                            // Get the paths to the nodes where the sequencer should write it's output ...
+                            assert outputPaths != null && outputPaths.size() != 0;
+
+                            // Create a new execution context for each sequencer
+                            final Context executionContext = new Context();
+                            final ProgressMonitor sequenceMonitor = progressMonitor.createSubtask(1);
+                            try {
+                                sequenceMonitor.beginTask(100, RepositoryI18n.sequencerSubtask, sequencerName);
+                                sequencer.execute(node, propertyName, changedNode, outputPaths, executionContext, sequenceMonitor.createSubtask(80)); // 80%
+                            } catch (RepositoryException e) {
+                                this.logger.error(e, RepositoryI18n.errorInRepositoryWhileSequencingNode, sequencerName, changedNode);
+                            } catch (SequencerException e) {
+                                this.logger.error(e, RepositoryI18n.errorWhileSequencingNode, sequencerName, changedNode);
+                            } finally {
+                                try {
+                                    // Save the changes made by each sequencer ...
+                                    if (session != null) session.save();
+                                    sequenceMonitor.worked(10); // 90% of sequenceMonitor
+                                } finally {
+                                    try {
+                                        // And always close the context.
+                                        // This closes all sessions that may have been created by the sequencer.
+                                        executionContext.close();
+                                    } finally {
+                                        sequenceMonitor.done(); // 100% of sequenceMonitor
+                                    }
+                                }
+                            }
+                        }
+                        this.statistics.recordNodeSequenced();
+                    } finally {
+                        progressMonitor.done();
+                    }
+                }
+            } finally {
+                if (session != null) session.logout();
+            }
+        } catch (RepositoryException e) {
+            this.logger.error(e, RepositoryI18n.errorInRepositoryWhileFindingSequencersToRunAgainstNode, changedNode);
+        } catch (Exception e) {
+            this.logger.error(e, RepositoryI18n.errorFindingSequencersToRunAgainstNode, changedNode);
+        }
+    }
+
+    protected class Context implements ExecutionContext {
+
+        protected final SessionFactory factory;
+        private final Set<Session> sessions = new HashSet<Session>();
+        protected final AtomicBoolean closed = new AtomicBoolean(false);
+
+        protected Context() {
+            final SessionFactory delegate = SequencingService.this.getExecutionContext().getSessionFactory();
+            this.factory = new SessionFactory() {
+
+                public Session createSession( String name ) throws RepositoryException {
+                    if (closed.get()) throw new IllegalStateException(RepositoryI18n.executionContextHasBeenClosed.text());
+                    Session session = delegate.createSession(name);
+                    recordSession(session);
+                    return session;
+                }
+            };
+        }
+
+        protected synchronized void recordSession( Session session ) {
+            if (session != null) sessions.add(session);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public SessionFactory getSessionFactory() {
+            return this.factory;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public JcrTools getTools() {
+            return SequencingService.this.getExecutionContext().getTools();
+        }
+
+        public synchronized void close() {
+            if (this.closed.get()) return;
+            this.closed.set(true);
+            for (Session session : sessions) {
+                if (session != null) session.logout();
+            }
+        }
+    }
+
+    /**
+     * The statistics for the system. Each sequencing system has an instance of this class that is updated.
+     * @author Randall Hauch
+     */
+    @ThreadSafe
+    public class Statistics {
+
+        private final AtomicLong numberOfNodesSequenced = new AtomicLong(0);
+        private final AtomicLong numberOfNodesSkipped = new AtomicLong(0);
+        private final AtomicLong startTime;
+
+        protected Statistics() {
+            startTime = new AtomicLong(System.currentTimeMillis());
+        }
+
+        public Statistics reset() {
+            this.startTime.set(System.currentTimeMillis());
+            this.numberOfNodesSequenced.set(0);
+            this.numberOfNodesSkipped.set(0);
+            return this;
+        }
+
+        /**
+         * @return the system time when the statistics were started
+         */
+        public long getStartTime() {
+            return this.startTime.get();
+        }
+
+        /**
+         * @return the number of nodes that were sequenced
+         */
+        public long getNumberOfNodesSequenced() {
+            return this.numberOfNodesSequenced.get();
+        }
+
+        /**
+         * @return the number of nodes that were skipped because no sequencers applied
+         */
+        public long getNumberOfNodesSkipped() {
+            return this.numberOfNodesSkipped.get();
+        }
+
+        protected void recordNodeSequenced() {
+            this.numberOfNodesSequenced.incrementAndGet();
+        }
+
+        protected void recordNodeSkipped() {
+            this.numberOfNodesSkipped.incrementAndGet();
+        }
+    }
+
+    @Immutable
+    protected class SequencerCall {
+
+        private final Sequencer sequencer;
+        private final String sequencerName;
+        private final String sequencedPropertyName;
+        private final int hc;
+
+        protected SequencerCall( Sequencer sequencer, String sequencedPropertyName ) {
+            this.sequencer = sequencer;
+            this.sequencerName = sequencer.getConfiguration().getName();
+            this.sequencedPropertyName = sequencedPropertyName;
+            this.hc = HashCode.compute(this.sequencerName, this.sequencedPropertyName);
+        }
+
+        /**
+         * @return sequencer
+         */
+        public Sequencer getSequencer() {
+            return this.sequencer;
+        }
+
+        /**
+         * @return sequencedPropertyName
+         */
+        public String getSequencedPropertyName() {
+            return this.sequencedPropertyName;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public int hashCode() {
+            return this.hc;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public boolean equals( Object obj ) {
+            if (obj == this) return true;
+            if (obj instanceof SequencerCall) {
+                SequencerCall that = (SequencerCall)obj;
+                if (!this.sequencerName.equals(that.sequencerName)) return false;
+                if (!this.sequencedPropertyName.equals(that.sequencedPropertyName)) return false;
+                return true;
+            }
+            return false;
+        }
+    }
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/StreamSequencerAdapter.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/StreamSequencerAdapter.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/sequencers/StreamSequencerAdapter.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,253 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.sequencers;
+
+import java.io.InputStream;
+import java.util.Calendar;
+import java.util.Set;
+import javax.jcr.Node;
+import javax.jcr.PathNotFoundException;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import org.jboss.dna.common.jcr.Path;
+import org.jboss.dna.common.monitor.ProgressMonitor;
+import org.jboss.dna.common.util.Logger;
+import org.jboss.dna.repository.RepositoryI18n;
+import org.jboss.dna.repository.observation.NodeChange;
+import org.jboss.dna.repository.util.ExecutionContext;
+import org.jboss.dna.repository.util.RepositoryNodePath;
+import org.jboss.dna.spi.sequencers.StreamSequencer;
+
+/**
+ * An adapter class that wraps a {@link StreamSequencer} instance to be a {@link Sequencer}.
+ * @author Randall Hauch
+ */
+public class StreamSequencerAdapter implements Sequencer {
+
+    private SequencerConfig configuration;
+    private final StreamSequencer streamSequencer;
+
+    public StreamSequencerAdapter( StreamSequencer streamSequencer ) {
+        this.streamSequencer = streamSequencer;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public SequencerConfig getConfiguration() {
+        return this.configuration;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void setConfiguration( SequencerConfig configuration ) {
+        this.configuration = configuration;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void execute( Node input, String sequencedPropertyName, NodeChange changes, Set<RepositoryNodePath> outputPaths, ExecutionContext context, ProgressMonitor progressMonitor )
+        throws RepositoryException, SequencerException {
+        // 'sequencedPropertyName' contains the name of the modified property on 'input' that resuled the call to this sequencer
+        // 'changes' contains all of the changes to this node that occurred in the transaction.
+        // 'outputPaths' contains the paths of the node(s) where this sequencer is to save it's data
+
+        try {
+            progressMonitor.beginTask(100, RepositoryI18n.sequencingPropertyOnNode, sequencedPropertyName, input.getPath());
+
+            // Get the property that contains the image data, given by 'propertyName' ...
+            Property imageDataProperty = null;
+            try {
+                imageDataProperty = input.getProperty(sequencedPropertyName);
+            } catch (PathNotFoundException e) {
+                String msg = RepositoryI18n.unableToFindPropertyForSequencing.text(sequencedPropertyName, input.getPath());
+                throw new SequencerException(msg, e);
+            }
+            progressMonitor.worked(10);
+
+            // Get the binary property with the image content, and build the image metadata from the image ...
+            SequencerOutputMap output = new SequencerOutputMap();
+            InputStream stream = null;
+            Throwable firstError = null;
+            ProgressMonitor sequencingMonitor = progressMonitor.createSubtask(50);
+            try {
+                stream = imageDataProperty.getStream();
+                this.streamSequencer.sequence(stream, output, sequencingMonitor);
+            } catch (Throwable t) {
+                // Record the error ...
+                firstError = t;
+            } finally {
+                sequencingMonitor.done();
+                if (stream != null) {
+                    // Always close the stream, recording the error if we've not yet seen an error
+                    try {
+                        stream.close();
+                    } catch (Throwable t) {
+                        if (firstError == null) firstError = t;
+                    } finally {
+                        stream = null;
+                    }
+                }
+                if (firstError != null) {
+                    // Wrap and throw the first error that we saw ...
+                    throw new SequencerException(firstError);
+                }
+            }
+
+            // Find each output node and save the image metadata there ...
+            ProgressMonitor writingProgress = progressMonitor.createSubtask(40);
+            writingProgress.beginTask(outputPaths.size(), RepositoryI18n.writingOutputSequencedFromPropertyOnNodes, sequencedPropertyName, input.getPath(), outputPaths.size());
+            for (RepositoryNodePath outputPath : outputPaths) {
+                Session session = null;
+                try {
+                    // Get the name of the repository workspace and the path to the output node
+                    final String repositoryWorkspaceName = outputPath.getRepositoryWorkspaceName();
+                    final String nodePath = outputPath.getNodePath();
+
+                    // Create a session to the repository where the data should be written ...
+                    session = context.getSessionFactory().createSession(repositoryWorkspaceName);
+
+                    // Find or create the output node in this session ...
+                    Node outputNode = context.getTools().findOrCreateNode(session, nodePath);
+
+                    // Now save the image metadata to the output node ...
+                    if (saveOutput(outputNode, output, context)) {
+                        session.save();
+                    }
+                } finally {
+                    writingProgress.worked(1);
+                    // Always close the session ...
+                    if (session != null) session.logout();
+                }
+            }
+            writingProgress.done();
+        } finally {
+            progressMonitor.done();
+        }
+    }
+
+    /**
+     * Save the sequencing output to the supplied node. This method does not need to save the output, as that is done by the
+     * caller of this method.
+     * @param outputNode the existing node onto (or below) which the output is to be written; never null
+     * @param outputProperties the (immutable) sequencing output; never null
+     * @param context the execution context for this sequencing operation; never null
+     * @return true if the output was written to the node, or false if no information was written
+     * @throws RepositoryException
+     */
+    protected boolean saveOutput( Node outputNode, SequencerOutputMap output, ExecutionContext context ) throws RepositoryException {
+        if (output.isEmpty()) return false;
+        final Path outputNodePath = new Path(outputNode.getPath());
+
+        // Iterate over the entries in the output, in Path's natural order (shorter paths first and in lexicographical order by
+        // prefix and name)
+        for (SequencerOutputMap.Entry entry : output) {
+            Path targetNodePath = entry.getPath();
+            String primaryType = entry.getPrimaryTypeValue();
+
+            // Resolve this path relative to the output node path, handling any parent or self references ...
+            Path absolutePath = targetNodePath.isAbsolute() ? targetNodePath : outputNodePath.resolve(targetNodePath);
+            Path relativePath = absolutePath.relativeTo(outputNodePath);
+
+            // Find or add the node (which may involve adding intermediate nodes) ...
+            Node targetNode = outputNode;
+            for (int i = 0, max = relativePath.size(); i != max; ++i) {
+                Path.Segment segment = relativePath.getSegment(i);
+                String qualifiedName = segment.getQualifiedName(true);
+                if (targetNode.hasNode(qualifiedName)) {
+                    targetNode = targetNode.getNode(qualifiedName);
+                } else {
+                    // It doesn't exist, so create it ...
+                    if (segment.hasIndex()) {
+                        // Use a name without an index ...
+                        qualifiedName = segment.getQualifiedName(false);
+                    }
+                    // We only have the primary type for the final one ...
+                    if (i == (max - 1) && primaryType != null) {
+                        targetNode = targetNode.addNode(qualifiedName, primaryType);
+                    } else {
+                        targetNode = targetNode.addNode(qualifiedName);
+                    }
+                }
+                assert targetNode != null;
+            }
+            assert targetNode != null;
+
+            // Set all of the properties on this
+            for (SequencerOutputMap.PropertyValue property : entry.getPropertyValues()) {
+                String propertyName = property.getName();
+                Object value = property.getValue();
+                Logger.getLogger(this.getClass()).trace("Writing property {0}/{1}={2}", targetNode.getPath(), propertyName, value);
+                if (value instanceof Boolean) {
+                    targetNode.setProperty(propertyName, ((Boolean)value).booleanValue());
+                } else if (value instanceof String) {
+                    targetNode.setProperty(propertyName, (String)value);
+                } else if (value instanceof Integer) {
+                    targetNode.setProperty(propertyName, ((Integer)value).intValue());
+                } else if (value instanceof Short) {
+                    targetNode.setProperty(propertyName, ((Short)value).shortValue());
+                } else if (value instanceof Long) {
+                    targetNode.setProperty(propertyName, ((Long)value).longValue());
+                } else if (value instanceof Float) {
+                    targetNode.setProperty(propertyName, ((Float)value).floatValue());
+                } else if (value instanceof Double) {
+                    targetNode.setProperty(propertyName, ((Double)value).doubleValue());
+                } else if (value instanceof Calendar) {
+                    targetNode.setProperty(propertyName, (Calendar)value);
+                } else if (value instanceof Path) {
+                    // Find the path to reference node ...
+                    Path pathToReferencedNode = (Path)value;
+                    if (!pathToReferencedNode.isAbsolute()) {
+                        // Resolve the path relative to the output node ...
+                        pathToReferencedNode = outputNodePath.resolve(pathToReferencedNode);
+                    }
+                    // Find the referenced node ...
+                    try {
+                        Node referencedNode = outputNode.getNode(pathToReferencedNode.getString());
+                        targetNode.setProperty(propertyName, referencedNode);
+                    } catch (PathNotFoundException e) {
+                        String msg = RepositoryI18n.errorGettingNodeRelativeToNode.text(value, outputNode.getPath());
+                        throw new SequencerException(msg, e);
+                    }
+                } else if (value == null) {
+                    // Remove the property ...
+                    targetNode.setProperty(propertyName, (String)null);
+                } else {
+                    String msg = RepositoryI18n.unknownPropertyValueType.text(value, value.getClass().getName());
+                    throw new SequencerException(msg);
+                }
+            }
+        }
+
+        return true;
+    }
+
+    protected String[] extractMixinTypes( Object value ) {
+        if (value instanceof String[]) return (String[])value;
+        if (value instanceof String) return new String[] {(String)value};
+        return null;
+    }
+
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/AbstractServiceAdministrator.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/AbstractServiceAdministrator.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/AbstractServiceAdministrator.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,216 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.services;
+
+import org.jboss.dna.repository.RepositoryI18n;
+import net.jcip.annotations.GuardedBy;
+import net.jcip.annotations.ThreadSafe;
+
+/**
+ * Simple abstract implementation of the service administrator interface that can be easily subclassed by services that require an
+ * administrative interface.
+ * @author Randall Hauch
+ */
+ at ThreadSafe
+public abstract class AbstractServiceAdministrator implements ServiceAdministrator {
+
+    private volatile State state;
+
+    protected AbstractServiceAdministrator( State initialState ) {
+        assert initialState != null;
+        this.state = initialState;
+    }
+
+    /**
+     * Return the current state of this service.
+     * @return the current state
+     */
+    public State getState() {
+        return this.state;
+    }
+
+    /**
+     * Set the state of the service. This method does nothing if the desired state matches the current state.
+     * @param state the desired state
+     * @return this object for method chaining purposes
+     * @see #setState(String)
+     * @see #start()
+     * @see #pause()
+     * @see #shutdown()
+     */
+    @GuardedBy( "this" )
+    public synchronized ServiceAdministrator setState( State state ) {
+        switch (state) {
+            case STARTED:
+                start();
+                break;
+            case PAUSED:
+                pause();
+                break;
+            case SHUTDOWN:
+                shutdown();
+                break;
+        }
+        return this;
+    }
+
+    /**
+     * Set the state of the service. This method does nothing if the desired state matches the current state.
+     * @param state the desired state in string form
+     * @return this object for method chaining purposes
+     * @throws IllegalArgumentException if the specified state string is null or does not match one of the predefined
+     * {@link ServiceAdministrator.State predefined enumerated values}
+     * @see #setState(State)
+     * @see #start()
+     * @see #pause()
+     * @see #shutdown()
+     */
+    public ServiceAdministrator setState( String state ) {
+        State newState = state == null ? null : State.valueOf(state.toUpperCase());
+        if (newState == null) {
+            throw new IllegalArgumentException(RepositoryI18n.invalidStateString.text(state));
+        }
+        return setState(newState);
+    }
+
+    /**
+     * Start monitoring and sequence the events. This method can be called multiple times, including after the service is
+     * {@link #pause() paused}. However, once the service is {@link #shutdown() shutdown}, it cannot be started or paused.
+     * @return this object for method chaining purposes
+     * @throws IllegalStateException if called when the service has been {@link #shutdown() shutdown}.
+     * @see #pause()
+     * @see #shutdown()
+     * @see #isStarted()
+     */
+    public synchronized ServiceAdministrator start() {
+        if (isShutdown()) throw new IllegalStateException(RepositoryI18n.serviceShutdowAndMayNotBeStarted.text(serviceName()));
+        if (this.state != State.STARTED) {
+            doStart(this.state);
+            this.state = State.STARTED;
+        }
+        return this;
+    }
+
+    /**
+     * Implementation of the functionality to switch to the started state. This method is only called if the state from which the
+     * service is transitioning is appropriate ({@link State#PAUSED}). This method does nothing by default, and should be
+     * overridden if needed.
+     * @param fromState the state from which this service is transitioning; never null
+     * @throws IllegalStateException if the service is such that it cannot be transitioned from the supplied state
+     */
+    @GuardedBy( "this" )
+    protected void doStart( State fromState ) {
+    }
+
+    /**
+     * Temporarily stop monitoring and sequencing events. This method can be called multiple times, including after the service is
+     * {@link #start() started}. However, once the service is {@link #shutdown() shutdown}, it cannot be started or paused.
+     * @return this object for method chaining purposes
+     * @throws IllegalStateException if called when the service has been {@link #shutdown() shutdown}.
+     * @see #start()
+     * @see #shutdown()
+     * @see #isPaused()
+     */
+    public synchronized ServiceAdministrator pause() {
+        if (isShutdown()) throw new IllegalStateException(RepositoryI18n.serviceShutdowAndMayNotBePaused.text(serviceName()));
+        if (this.state != State.PAUSED) {
+            doPause(this.state);
+            this.state = State.PAUSED;
+        }
+        return this;
+    }
+
+    /**
+     * Implementation of the functionality to switch to the paused state. This method is only called if the state from which the
+     * service is transitioning is appropriate ({@link State#STARTED}). This method does nothing by default, and should be
+     * overridden if needed.
+     * @param fromState the state from which this service is transitioning; never null
+     * @throws IllegalStateException if the service is such that it cannot be transitioned from the supplied state
+     */
+    @GuardedBy( "this" )
+    protected void doPause( State fromState ) {
+    }
+
+    /**
+     * Permanently stop monitoring and sequencing events. This method can be called multiple times, but only the first call has an
+     * effect. Once the service has been shutdown, it may not be {@link #start() restarted} or {@link #pause() paused}.
+     * @return this object for method chaining purposes
+     * @see #start()
+     * @see #pause()
+     * @see #isShutdown()
+     */
+    public synchronized ServiceAdministrator shutdown() {
+        if (this.state != State.SHUTDOWN) {
+            doShutdown(this.state);
+            this.state = State.SHUTDOWN;
+        }
+        return this;
+    }
+
+    /**
+     * Implementation of the functionality to switch to the shutdown state. This method is only called if the state from which the
+     * service is transitioning is appropriate ({@link State#STARTED} or {@link State#PAUSED}). This method does nothing by
+     * default, and should be overridden if needed.
+     * @param fromState the state from which this service is transitioning; never null
+     * @throws IllegalStateException if the service is such that it cannot be transitioned from the supplied state
+     */
+    @GuardedBy( "this" )
+    protected void doShutdown( State fromState ) {
+    }
+
+    /**
+     * Return whether this service has been started and is currently running.
+     * @return true if started and currently running, or false otherwise
+     * @see #start()
+     * @see #pause()
+     * @see #isPaused()
+     * @see #isShutdown()
+     */
+    public boolean isStarted() {
+        return this.state == State.STARTED;
+    }
+
+    /**
+     * Return whether this service is currently paused.
+     * @return true if currently paused, or false otherwise
+     * @see #pause()
+     * @see #start()
+     * @see #isStarted()
+     * @see #isShutdown()
+     */
+    public boolean isPaused() {
+        return this.state == State.PAUSED;
+    }
+
+    /**
+     * Return whether this service is stopped and unable to be restarted.
+     * @return true if currently shutdown, or false otherwise
+     * @see #shutdown()
+     * @see #isPaused()
+     * @see #isStarted()
+     */
+    public boolean isShutdown() {
+        return this.state == State.SHUTDOWN;
+    }
+
+    protected abstract String serviceName();
+}


Property changes on: trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/AbstractServiceAdministrator.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/AdministeredService.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/AdministeredService.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/AdministeredService.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,30 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.services;
+
+/**
+ * @author Randall Hauch
+ */
+public interface AdministeredService {
+
+    ServiceAdministrator getAdministrator();
+}


Property changes on: trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/AdministeredService.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/ServiceAdministrator.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/ServiceAdministrator.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/ServiceAdministrator.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,144 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.services;
+
+import java.util.concurrent.TimeUnit;
+import net.jcip.annotations.ThreadSafe;
+
+/**
+ * Contract defining an administrative interface for controlling the running state of a service.
+ * @author Randall Hauch
+ */
+ at ThreadSafe
+public interface ServiceAdministrator {
+
+    /**
+     * The available states.
+     * @author Randall Hauch
+     */
+    public static enum State {
+        STARTED,
+        PAUSED,
+        SHUTDOWN;
+    }
+
+    /**
+     * Return the current state of this system.
+     * @return the current state
+     */
+    public State getState();
+
+    /**
+     * Set the state of the system. This method does nothing if the desired state matches the current state.
+     * @param state the desired state
+     * @return this object for method chaining purposes
+     * @see #setState(String)
+     * @see #start()
+     * @see #pause()
+     * @see #shutdown()
+     */
+    public ServiceAdministrator setState( State state );
+
+    /**
+     * Set the state of the system. This method does nothing if the desired state matches the current state.
+     * @param state the desired state in string form
+     * @return this object for method chaining purposes
+     * @throws IllegalArgumentException if the specified state string is null or does not match one of the predefined
+     * {@link State predefined enumerated values}
+     * @see #setState(State)
+     * @see #start()
+     * @see #pause()
+     * @see #shutdown()
+     */
+    public ServiceAdministrator setState( String state );
+
+    /**
+     * Start monitoring and sequence the events. This method can be called multiple times, including after the system is
+     * {@link #pause() paused}. However, once the system is {@link #shutdown() shutdown}, it cannot be started or paused.
+     * @return this object for method chaining purposes
+     * @throws IllegalStateException if called when the system has been {@link #shutdown() shutdown}.
+     * @see #pause()
+     * @see #shutdown()
+     * @see #isStarted()
+     */
+    public ServiceAdministrator start();
+
+    /**
+     * Temporarily stop monitoring and sequencing events. This method can be called multiple times, including after the system is
+     * {@link #start() started}. However, once the system is {@link #shutdown() shutdown}, it cannot be started or paused.
+     * @return this object for method chaining purposes
+     * @throws IllegalStateException if called when the system has been {@link #shutdown() shutdown}.
+     * @see #start()
+     * @see #shutdown()
+     * @see #isPaused()
+     */
+    public ServiceAdministrator pause();
+
+    /**
+     * Permanently stop monitoring and sequencing events. This method can be called multiple times, but only the first call has an
+     * effect. Once the system has been shutdown, it may not be {@link #start() restarted} or {@link #pause() paused}.
+     * @return this object for method chaining purposes
+     * @see #start()
+     * @see #pause()
+     * @see #isShutdown()
+     */
+    public ServiceAdministrator shutdown();
+
+    /**
+     * Blocks until all work has completed execution after a shutdown request, or the timeout occurs, or the current thread is
+     * interrupted, whichever happens first.
+     * @param timeout the maximum time to wait
+     * @param unit the time unit of the timeout argument
+     * @return <tt>true</tt> if this service terminated and <tt>false</tt> if the timeout elapsed before termination
+     * @throws InterruptedException if interrupted while waiting
+     */
+    boolean awaitTermination( long timeout, TimeUnit unit ) throws InterruptedException;
+
+    /**
+     * Return whether this system has been started and is currently running.
+     * @return true if started and currently running, or false otherwise
+     * @see #start()
+     * @see #pause()
+     * @see #isPaused()
+     * @see #isShutdown()
+     */
+    public boolean isStarted();
+
+    /**
+     * Return whether this system is currently paused.
+     * @return true if currently paused, or false otherwise
+     * @see #pause()
+     * @see #start()
+     * @see #isStarted()
+     * @see #isShutdown()
+     */
+    public boolean isPaused();
+
+    /**
+     * Return whether this system is stopped and unable to be restarted.
+     * @return true if currently shutdown, or false otherwise
+     * @see #shutdown()
+     * @see #isPaused()
+     * @see #isStarted()
+     */
+    public boolean isShutdown();
+}


Property changes on: trunk/dna-repository/src/main/java/org/jboss/dna/repository/services/ServiceAdministrator.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/ExecutionContext.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/ExecutionContext.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/ExecutionContext.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,45 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.util;
+
+import javax.jcr.Session;
+
+/**
+ * The context of an execution.
+ * @author Randall Hauch
+ */
+public interface ExecutionContext {
+
+    /**
+     * Get the session factory, which can be used to obtain sessions temporarily for this context. Any session obtained from this
+     * factory should be {@link Session#logout() closed} before the execution finishes.
+     * @return the session factory
+     */
+    SessionFactory getSessionFactory();
+
+    /**
+     * Get a set of utilities for working with JCR.
+     * @return the tools
+     */
+    JcrTools getTools();
+
+}


Property changes on: trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/ExecutionContext.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/JcrTools.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/JcrTools.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/JcrTools.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,300 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.PathNotFoundException;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+import org.jboss.dna.common.collection.Problem;
+import org.jboss.dna.common.collection.Problems;
+import org.jboss.dna.common.util.IoUtil;
+import org.jboss.dna.common.util.Logger;
+import org.jboss.dna.common.util.StringUtil;
+import org.jboss.dna.repository.RepositoryI18n;
+
+/**
+ * @author Randall Hauch
+ */
+public class JcrTools {
+
+    public Map<String, Object> loadProperties( Node propertyContainer, Problems problems ) {
+        return loadProperties(propertyContainer, null, problems);
+    }
+
+    public Map<String, Object> loadProperties( Node propertyContainer, Map<String, Object> properties, Problems problems ) {
+        if (properties == null) properties = new HashMap<String, Object>();
+        if (propertyContainer != null) {
+            try {
+                NodeIterator iter = propertyContainer.getNodes();
+                while (iter.hasNext()) {
+                    Node propertyNode = iter.nextNode();
+                    if (propertyNode != null && propertyNode.getPrimaryNodeType().isNodeType("dna:property")) {
+                        String propertyName = propertyNode.getName();
+                        Object propertyValue = getPropertyValue(propertyNode, "dna:propertyValue", true, problems);
+                        properties.put(propertyName, propertyValue);
+                    }
+                }
+            } catch (RepositoryException e) {
+                problems.addError(e, RepositoryI18n.errorReadingPropertiesFromContainerNode, getReadable(propertyContainer));
+            }
+        }
+
+        return properties;
+    }
+
+    public boolean removeProblems( Node parent ) throws RepositoryException {
+        Node problemsNode = null;
+        if (parent.hasNode("dna:problems")) {
+            problemsNode = parent.getNode("dna:problems");
+            problemsNode.remove();
+            return true;
+        }
+        return false;
+    }
+
+    public boolean storeProblems( Node parent, Problems problems ) throws RepositoryException {
+        Node problemsNode = null;
+        if (parent.hasNode("dna:problems")) {
+            problemsNode = parent.getNode("dna:problems");
+            // Delete all problems ...
+            removeAllChildren(problemsNode);
+        }
+        if (problems.isEmpty()) {
+            return false;
+        }
+        if (problemsNode == null) {
+            problemsNode = parent.addNode("dna:problems"); // primary type dictated by child definition
+        }
+
+        // Add a child for each problem ...
+        for (Problem problem : problems) {
+            Node problemNode = problemsNode.addNode("problem", "dna:problem");
+            // - dna:status (string) mandatory copy
+            // < 'ERROR', 'WARNING', 'INFO'
+            // - dna:message (string) mandatory copy
+            // - dna:code (string) copy
+            // - dna:type (string) copy
+            // - dna:resource (string) copy
+            // - dna:location (string) copy
+            // - dna:trace (string) copy
+            problemNode.setProperty("dna:status", problem.getStatus().name());
+            problemNode.setProperty("dna:message", problem.getMessageString());
+            if (problem.getCode() != Problem.DEFAULT_CODE) {
+                problemNode.setProperty("dna:code", Integer.toString(problem.getCode()));
+            }
+            String resource = problem.getResource();
+            if (resource != null) {
+                problemNode.setProperty("dna:resource", resource);
+            }
+            String location = problem.getLocation();
+            if (location != null) {
+                problemNode.setProperty("dna:location", location);
+            }
+            Throwable t = problem.getThrowable();
+            if (t != null) {
+                String trace = StringUtil.getStackTrace(t);
+                problemNode.setProperty("dna:trace", trace);
+            }
+        }
+        return true;
+    }
+
+    public int removeAllChildren( Node node ) throws RepositoryException {
+        int childrenRemoved = 0;
+        NodeIterator iter = node.getNodes();
+        while (iter.hasNext()) {
+            Node child = iter.nextNode();
+            child.remove();
+            ++childrenRemoved;
+        }
+        return childrenRemoved;
+    }
+
+    public String getPropertyAsString( Node node, String propertyName, boolean required, Problems problems ) {
+        return getPropertyAsString(node, propertyName, required, null);
+    }
+
+    public String getPropertyAsString( Node node, String propertyName, boolean required, String defaultValue, Problems problems ) {
+        try {
+            Property property = node.getProperty(propertyName);
+            return property.getString();
+        } catch (ValueFormatException e) {
+            if (required) {
+                problems.addError(e, RepositoryI18n.requiredPropertyOnNodeWasExpectedToBeStringValue, propertyName, getReadable(node));
+            } else {
+                problems.addError(e, RepositoryI18n.optionalPropertyOnNodeWasExpectedToBeStringValue, propertyName, getReadable(node));
+            }
+        } catch (PathNotFoundException e) {
+            if (required) {
+                problems.addError(e, RepositoryI18n.requiredPropertyIsMissingFromNode, propertyName, getReadable(node));
+            }
+            if (!required) return defaultValue;
+        } catch (RepositoryException err) {
+            if (required) {
+                problems.addError(err, RepositoryI18n.errorGettingRequiredPropertyFromNode, propertyName, getReadable(node));
+            } else {
+                problems.addError(err, RepositoryI18n.errorGettingOptionalPropertyFromNode, propertyName, getReadable(node));
+            }
+        }
+        return null;
+    }
+
+    public Object getPropertyValue( Node node, String propertyName, boolean required, Problems problems ) {
+        try {
+            Property property = node.getProperty(propertyName);
+            switch (property.getType()) {
+                case PropertyType.BINARY: {
+                    InputStream stream = property.getStream();
+                    try {
+                        stream = property.getStream();
+                        return IoUtil.readBytes(stream);
+                    } finally {
+                        if (stream != null) {
+                            try {
+                                stream.close();
+                            } catch (IOException e) {
+                                // Log ...
+                                Logger.getLogger(this.getClass()).error(e, RepositoryI18n.errorClosingBinaryStreamForPropertyFromNode, propertyName, node.getPath());
+                            }
+                        }
+                    }
+                }
+                default: {
+                    return property.getString();
+                }
+            }
+        } catch (IOException e) {
+            if (required) {
+                problems.addError(e, RepositoryI18n.requiredPropertyOnNodeCouldNotBeRead, propertyName, getReadable(node));
+            } else {
+                problems.addError(e, RepositoryI18n.optionalPropertyOnNodeCouldNotBeRead, propertyName, getReadable(node));
+            }
+        } catch (PathNotFoundException e) {
+            if (required) {
+                problems.addError(e, RepositoryI18n.requiredPropertyIsMissingFromNode, propertyName, getReadable(node));
+            }
+        } catch (RepositoryException err) {
+            if (required) {
+                problems.addError(err, RepositoryI18n.errorGettingRequiredPropertyFromNode, propertyName, getReadable(node));
+            } else {
+                problems.addError(err, RepositoryI18n.errorGettingOptionalPropertyFromNode, propertyName, getReadable(node));
+            }
+        }
+        return null;
+    }
+
+    public String[] getPropertyAsStringArray( Node node, String propertyName, boolean required, Problems problems, String... defaultValues ) {
+        String[] result = defaultValues;
+        try {
+            Property property = node.getProperty(propertyName);
+            if (property.getDefinition().isMultiple()) {
+                Value[] values = property.getValues();
+                result = new String[values.length];
+                int i = 0;
+                for (Value value : values) {
+                    result[i++] = value.getString();
+                }
+            } else {
+                result = new String[] {property.getString()};
+            }
+        } catch (ValueFormatException e) {
+            if (required) {
+                problems.addError(e, RepositoryI18n.requiredPropertyOnNodeWasExpectedToBeStringArrayValue, propertyName, getReadable(node));
+            } else {
+                problems.addError(e, RepositoryI18n.optionalPropertyOnNodeWasExpectedToBeStringArrayValue, propertyName, getReadable(node));
+            }
+        } catch (PathNotFoundException e) {
+            if (required) {
+                problems.addError(e, RepositoryI18n.requiredPropertyIsMissingFromNode, propertyName, getReadable(node));
+            }
+        } catch (RepositoryException err) {
+            if (required) {
+                problems.addError(err, RepositoryI18n.errorGettingRequiredPropertyFromNode, propertyName, getReadable(node));
+            } else {
+                problems.addError(err, RepositoryI18n.errorGettingOptionalPropertyFromNode, propertyName, getReadable(node));
+            }
+        }
+        return result;
+    }
+
+    public Node getNode( Node node, String relativePath, boolean required, Problems problems ) {
+        Node result = null;
+        try {
+            result = node.getNode(relativePath);
+        } catch (PathNotFoundException e) {
+            if (required) problems.addError(e, RepositoryI18n.requiredNodeDoesNotExistRelativeToNode, relativePath, getReadable(node));
+        } catch (RepositoryException err) {
+            problems.addError(err, RepositoryI18n.errorGettingNodeRelativeToNode, relativePath, getReadable(node));
+        }
+        return result;
+    }
+
+    public String getReadable( Node node ) {
+        if (node == null) return "";
+        try {
+            return node.getPath();
+        } catch (RepositoryException err) {
+            return node.toString();
+        }
+    }
+
+    public Node findOrCreateNode( Session session, String path ) throws RepositoryException {
+        Node root = session.getRootNode();
+        // Remove leading and trailing slashes ...
+        String relPath = path.replaceAll("^/+", "").replaceAll("/+$", "");
+
+        // Look for the node first ...
+        try {
+            return root.getNode(relPath);
+        } catch (PathNotFoundException e) {
+            // continue
+        }
+        // Create the node, which has to be done segment by segment ...
+        String[] pathSegments = relPath.split("/");
+        Node node = root;
+        for (String pathSegment : pathSegments) {
+            pathSegment = pathSegment.trim();
+            if (pathSegment.length() == 0) continue;
+            if (node.hasNode(pathSegment)) {
+                // Find the existing node ...
+                node = node.getNode(pathSegment);
+            } else {
+                // Make sure there is no index on the final segment ...
+                String pathSegmentWithNoIndex = pathSegment.replaceAll("(\\[\\d+\\])+$", "");
+                // Create the node ...
+                node = node.addNode(pathSegmentWithNoIndex);
+            }
+        }
+        return node;
+    }
+
+}

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/JndiSessionFactory.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/JndiSessionFactory.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/JndiSessionFactory.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,249 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+
+package org.jboss.dna.repository.util;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import javax.jcr.Credentials;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import org.jboss.dna.common.SystemFailureException;
+import org.jboss.dna.common.util.ArgCheck;
+import org.jboss.dna.repository.RepositoryI18n;
+
+/**
+ * A SessionFactory implementation that creates {@link Session} instances using {@link Repository} instances registered in JNDI.
+ * <p>
+ * This factory using a naming convention where the name supplied to the {@link #createSession(String)} contains both the name of
+ * the repository and the name of the workspace. Typically, this is <i><code>repositoryName/workspaceName</code></i>, where
+ * <code>repositoryName</code> is the JNDI name under which the Repository instance was bound, and <code>workspaceName</code>
+ * is the name of the workspace. Note that this method looks for the last delimiter in the whole name to distinguish between the
+ * repository and workspace names.
+ * </p>
+ * <p>
+ * For example, if "<code>java:comp/env/repository/dataRepository/myWorkspace</code>" is passed to the
+ * {@link #createSession(String)} method, this factory will look for a {@link Repository} instance registered in JDNI with the
+ * name "<code>java:comp/env/repository/dataRepository</code>" and use it to {@link Repository#login(String) create a session}
+ * to the workspace named "<code>myWorkspace</code>".
+ * </p>
+ * <p>
+ * By default, this factory creates an anonymous JCR session. To use sessions with specific {@link Credentials}, simply
+ * {@link #registerCredentials(String, Credentials) register} credentials for the appropriate repository/workspace name. For
+ * security reasons, it is not possible to retrieve the Credentials once registered with this factory.
+ * </p>
+ * @author Randall Hauch
+ */
+public class JndiSessionFactory implements SessionFactory {
+
+    protected static char[] DEFAULT_DELIMITERS = new char[] {'/'};
+
+    private final char[] workspaceDelims;
+    private final String workspaceDelimsRegexCharacterSet;
+    private final Context context;
+    private final Map<String, Credentials> credentials = new ConcurrentHashMap<String, Credentials>();
+
+    /**
+     * Create an instance of the factory by creating a new {@link InitialContext}. This is equivalent to calling
+     * <code>new JndiSessionFactory(new InitialContext())</code>.
+     * @throws NamingException if there is a problem creating the InitialContext.
+     */
+    public JndiSessionFactory() throws NamingException {
+        this(new InitialContext());
+    }
+
+    /**
+     * Create an instance of the factory by supplying the characters that may be used to delimit the workspace name from the
+     * repository name. This constructor initializes the factory with a new {@link InitialContext}, and is equivalent to calling
+     * <code>new JndiSessionFactory(new InitialContext(),workspaceDelimiters)</code>.
+     * @param workspaceDelimiters the delimiters, or null/empty if the default delimiter of '/' should be used.
+     * @throws NamingException if there is a problem creating the InitialContext.
+     */
+    public JndiSessionFactory( char... workspaceDelimiters ) throws NamingException {
+        this(new InitialContext(), workspaceDelimiters);
+    }
+
+    /**
+     * Create an instance of the factory using the supplied JNDI context.
+     * @param context the naming context
+     * @throws IllegalArgumentException if the context parameter is null
+     */
+    public JndiSessionFactory( Context context ) {
+        this(context, DEFAULT_DELIMITERS);
+    }
+
+    /**
+     * Create an instance of the factory by supplying naming context and the characters that may be used to delimit the workspace
+     * name from the repository name.
+     * @param context the naming context
+     * @param workspaceDelimiters the delimiters, or null/empty if the default delimiter of '/' should be used.
+     * @throws IllegalArgumentException if the context parameter is null
+     */
+    public JndiSessionFactory( Context context, char... workspaceDelimiters ) {
+        ArgCheck.isNotNull(context, "initial context");
+        this.context = context;
+        this.workspaceDelims = (workspaceDelimiters == null || workspaceDelimiters.length == 0) ? DEFAULT_DELIMITERS : workspaceDelimiters;
+        StringBuilder sb = new StringBuilder();
+        for (char delim : this.workspaceDelims) {
+            switch (delim) {
+                case '\\':
+                    sb.append("\\");
+                    break;
+                // case '[' : sb.append("\\["); break;
+                case ']':
+                    sb.append("\\]");
+                    break;
+                case '-':
+                    sb.append("\\-");
+                    break;
+                case '^':
+                    sb.append("\\^");
+                    break;
+                default:
+                    sb.append(delim);
+            }
+        }
+        this.workspaceDelimsRegexCharacterSet = sb.toString();
+    }
+
+    /**
+     * Convenience method to bind a repository in JNDI. Repository instances can be bound into JNDI using any technique, so this
+     * method need not be used. <i>Note that the name should not contain the workspace part.</i>
+     * @param name the name of the repository, without the workspace name component.
+     * @param repository the repository to be bound, or null if an existing repository should be unbound.
+     * @throws NamingException if there is a problem registering the supplied repository
+     */
+    public void registerRepository( String name, Repository repository ) throws NamingException {
+        assert name != null;
+        // Remove all trailing delimiters ...
+        name = name.replaceAll("[" + this.workspaceDelimsRegexCharacterSet + "]+$", "");
+        if (repository != null) {
+            this.context.bind(name, repository);
+        } else {
+            this.context.unbind(name);
+        }
+    }
+
+    /**
+     * Register the credentials for the repository and workspace given by the supplied name, username and password. This is
+     * equivalent to calling <code>registerCredentials(name, new SimpleCredentials(username,password))</code>, although if
+     * <code>username</code> is null then this is equivalent to <code>registerCredentials(name,null)</code>.
+     * @param name the name of the repository and workspace
+     * @param username the username to use, or null if the existing credentials for the named workspace should be removed
+     * @param password the password, may be null or empty
+     * @return true if this overwrote existing credentials
+     * @see #registerCredentials(String, Credentials)
+     * @see #removeCredentials(String)
+     */
+    public boolean registerCredentials( String name, String username, char[] password ) {
+        if (password == null && username != null) password = new char[] {};
+        Credentials creds = username == null ? null : new SimpleCredentials(username, password);
+        return registerCredentials(name, creds);
+    }
+
+    /**
+     * Register the credentials to be used for the named repository and workspace. Use the same name as used to
+     * {@link #createSession(String) create sessions}.
+     * @param name the name of the repository and workspace
+     * @param credentials the credentials to use, or null if the existing credentials for the named workspace should be removed
+     * @return true if this overwrote existing credentials
+     * @see #registerCredentials(String, String, char[])
+     * @see #removeCredentials(String)
+     */
+    public boolean registerCredentials( String name, Credentials credentials ) {
+        boolean foundExisting = false;
+        name = name != null ? name.trim() : null;
+        if (credentials == null) {
+            foundExisting = this.credentials.remove(name) != null;
+        } else {
+            foundExisting = this.credentials.put(name, credentials) != null;
+        }
+        return foundExisting;
+    }
+
+    /**
+     * Remove any credentials associated with the named repository and workspace. This is equivalent to calling
+     * <code>registerCredentials(name,null)</code>.
+     * @param name the name of the repository and workspace
+     * @return true if existing credentials were found and removed, or false if no such credentials existed
+     * @see #registerCredentials(String, Credentials)
+     */
+    public boolean removeCredentials( String name ) {
+        return registerCredentials(name, null);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Session createSession( String name ) throws RepositoryException {
+        ArgCheck.isNotNull(name, "session name");
+        name = name.trim();
+        // Look up the Repository object in JNDI ...
+        String repositoryName = getRepositoryName(name);
+        Repository repository = null;
+        try {
+            repository = (Repository)this.context.lookup(repositoryName);
+        } catch (NamingException err) {
+            throw new SystemFailureException(RepositoryI18n.unableToFindRepositoryInJndi.text(repositoryName));
+        }
+
+        // Determine the name of the workspace ...
+        String workspaceName = getWorkspaceName(name);
+
+        // Look up any credentials, which may be null ...
+        Credentials creds = this.credentials.get(name);
+
+        // Create a session to the specified workspace and using the credentials (either or both may be null) ...
+        return repository.login(creds, workspaceName);
+    }
+
+    protected String getWorkspaceName( String name ) {
+        assert name != null;
+        int index = getIndexOfLastWorkspaceDelimiter(name);
+        if (index == -1) return null;
+        if ((index + 1) == name.length()) return null; // delim is the last character
+        return name.substring(index + 1);
+    }
+
+    protected String getRepositoryName( String name ) {
+        assert name != null;
+        int index = getIndexOfLastWorkspaceDelimiter(name);
+        if (index == -1) return name; // no delim
+        if ((index + 1) == name.length()) return name.substring(0, index); // delim as last character
+        return name.substring(0, index);
+    }
+
+    protected int getIndexOfLastWorkspaceDelimiter( String name ) {
+        int index = -1;
+        for (char delim : this.workspaceDelims) {
+            int i = name.lastIndexOf(delim);
+            index = Math.max(index, i);
+        }
+        return index;
+    }
+
+}


Property changes on: trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/JndiSessionFactory.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/RepositoryNodePath.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/RepositoryNodePath.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/RepositoryNodePath.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,105 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.util;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import net.jcip.annotations.Immutable;
+import org.jboss.dna.common.util.HashCode;
+import org.jboss.dna.repository.RepositoryI18n;
+
+/**
+ * @author Randall Hauch
+ */
+ at Immutable
+public class RepositoryNodePath {
+
+    protected static final Pattern PATTERN = Pattern.compile("([^:/]):(/.*)");
+
+    public static RepositoryNodePath parse( String path, String defaultRepositoryWorkspaceName ) {
+        Matcher matcher = PATTERN.matcher(path);
+        if (matcher.matches()) {
+            try {
+                return new RepositoryNodePath(matcher.group(1), matcher.group(2));
+            } catch (Throwable t) {
+                throw new IllegalArgumentException(RepositoryI18n.invalidRepositoryNodePath.text(path, t.getMessage()));
+            }
+        }
+        return new RepositoryNodePath(defaultRepositoryWorkspaceName, path);
+
+    }
+
+    private final String repositoryName;
+    private final String nodePath;
+    private final int hc;
+
+    public RepositoryNodePath( String repositoryName, String nodePath ) {
+        this.repositoryName = repositoryName;
+        this.nodePath = nodePath;
+        this.hc = HashCode.compute(this.repositoryName, this.nodePath);
+    }
+
+    /**
+     * @return nodePath
+     */
+    public String getNodePath() {
+        return this.nodePath;
+    }
+
+    /**
+     * @return repositoryName
+     */
+    public String getRepositoryWorkspaceName() {
+        return this.repositoryName;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        return this.hc;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals( Object obj ) {
+        if (obj == this) return true;
+        if (obj instanceof RepositoryNodePath) {
+            RepositoryNodePath that = (RepositoryNodePath)obj;
+            if (!this.repositoryName.equals(that.repositoryName)) return false;
+            if (!this.nodePath.equals(that.nodePath)) return false;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        return this.repositoryName + ":" + this.nodePath;
+    }
+}


Property changes on: trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/RepositoryNodePath.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Added: trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/SessionFactory.java
===================================================================
--- trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/SessionFactory.java	                        (rev 0)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/repository/util/SessionFactory.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,35 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+
+package org.jboss.dna.repository.util;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+/**
+ * @author Randall Hauch
+ */
+public interface SessionFactory {
+
+    Session createSession( String name ) throws RepositoryException;
+
+}

Deleted: trunk/dna-repository/src/main/java/org/jboss/dna/services/AbstractServiceAdministrator.java
===================================================================
--- trunk/dna-services/src/main/java/org/jboss/dna/services/AbstractServiceAdministrator.java	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/services/AbstractServiceAdministrator.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -1,215 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source.
- * Copyright 2008, Red Hat Middleware LLC, and individual contributors
- * as indicated by the @author tags. See the copyright.txt file in the
- * distribution for a full listing of individual contributors. 
- *
- * This is free software; you can redistribute it and/or modify it
- * under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.jboss.dna.services;
-
-import net.jcip.annotations.GuardedBy;
-import net.jcip.annotations.ThreadSafe;
-
-/**
- * Simple abstract implementation of the service administrator interface that can be easily subclassed by services that require an
- * administrative interface.
- * @author Randall Hauch
- */
- at ThreadSafe
-public abstract class AbstractServiceAdministrator implements ServiceAdministrator {
-
-    private volatile State state;
-
-    protected AbstractServiceAdministrator( State initialState ) {
-        assert initialState != null;
-        this.state = initialState;
-    }
-
-    /**
-     * Return the current state of this service.
-     * @return the current state
-     */
-    public State getState() {
-        return this.state;
-    }
-
-    /**
-     * Set the state of the service. This method does nothing if the desired state matches the current state.
-     * @param state the desired state
-     * @return this object for method chaining purposes
-     * @see #setState(String)
-     * @see #start()
-     * @see #pause()
-     * @see #shutdown()
-     */
-    @GuardedBy( "this" )
-    public synchronized ServiceAdministrator setState( State state ) {
-        switch (state) {
-            case STARTED:
-                start();
-                break;
-            case PAUSED:
-                pause();
-                break;
-            case SHUTDOWN:
-                shutdown();
-                break;
-        }
-        return this;
-    }
-
-    /**
-     * Set the state of the service. This method does nothing if the desired state matches the current state.
-     * @param state the desired state in string form
-     * @return this object for method chaining purposes
-     * @throws IllegalArgumentException if the specified state string is null or does not match one of the predefined
-     * {@link ServiceAdministrator.State predefined enumerated values}
-     * @see #setState(State)
-     * @see #start()
-     * @see #pause()
-     * @see #shutdown()
-     */
-    public ServiceAdministrator setState( String state ) {
-        State newState = state == null ? null : State.valueOf(state.toUpperCase());
-        if (newState == null) {
-            throw new IllegalArgumentException(ServicesI18n.invalidStateString.text(state));
-        }
-        return setState(newState);
-    }
-
-    /**
-     * Start monitoring and sequence the events. This method can be called multiple times, including after the service is
-     * {@link #pause() paused}. However, once the service is {@link #shutdown() shutdown}, it cannot be started or paused.
-     * @return this object for method chaining purposes
-     * @throws IllegalStateException if called when the service has been {@link #shutdown() shutdown}.
-     * @see #pause()
-     * @see #shutdown()
-     * @see #isStarted()
-     */
-    public synchronized ServiceAdministrator start() {
-        if (isShutdown()) throw new IllegalStateException(ServicesI18n.serviceShutdowAndMayNotBeStarted.text(serviceName()));
-        if (this.state != State.STARTED) {
-            doStart(this.state);
-            this.state = State.STARTED;
-        }
-        return this;
-    }
-
-    /**
-     * Implementation of the functionality to switch to the started state. This method is only called if the state from which the
-     * service is transitioning is appropriate ({@link State#PAUSED}). This method does nothing by default, and should be
-     * overridden if needed.
-     * @param fromState the state from which this service is transitioning; never null
-     * @throws IllegalStateException if the service is such that it cannot be transitioned from the supplied state
-     */
-    @GuardedBy( "this" )
-    protected void doStart( State fromState ) {
-    }
-
-    /**
-     * Temporarily stop monitoring and sequencing events. This method can be called multiple times, including after the service is
-     * {@link #start() started}. However, once the service is {@link #shutdown() shutdown}, it cannot be started or paused.
-     * @return this object for method chaining purposes
-     * @throws IllegalStateException if called when the service has been {@link #shutdown() shutdown}.
-     * @see #start()
-     * @see #shutdown()
-     * @see #isPaused()
-     */
-    public synchronized ServiceAdministrator pause() {
-        if (isShutdown()) throw new IllegalStateException(ServicesI18n.serviceShutdowAndMayNotBePaused.text(serviceName()));
-        if (this.state != State.PAUSED) {
-            doPause(this.state);
-            this.state = State.PAUSED;
-        }
-        return this;
-    }
-
-    /**
-     * Implementation of the functionality to switch to the paused state. This method is only called if the state from which the
-     * service is transitioning is appropriate ({@link State#STARTED}). This method does nothing by default, and should be
-     * overridden if needed.
-     * @param fromState the state from which this service is transitioning; never null
-     * @throws IllegalStateException if the service is such that it cannot be transitioned from the supplied state
-     */
-    @GuardedBy( "this" )
-    protected void doPause( State fromState ) {
-    }
-
-    /**
-     * Permanently stop monitoring and sequencing events. This method can be called multiple times, but only the first call has an
-     * effect. Once the service has been shutdown, it may not be {@link #start() restarted} or {@link #pause() paused}.
-     * @return this object for method chaining purposes
-     * @see #start()
-     * @see #pause()
-     * @see #isShutdown()
-     */
-    public synchronized ServiceAdministrator shutdown() {
-        if (this.state != State.SHUTDOWN) {
-            doShutdown(this.state);
-            this.state = State.SHUTDOWN;
-        }
-        return this;
-    }
-
-    /**
-     * Implementation of the functionality to switch to the shutdown state. This method is only called if the state from which the
-     * service is transitioning is appropriate ({@link State#STARTED} or {@link State#PAUSED}). This method does nothing by
-     * default, and should be overridden if needed.
-     * @param fromState the state from which this service is transitioning; never null
-     * @throws IllegalStateException if the service is such that it cannot be transitioned from the supplied state
-     */
-    @GuardedBy( "this" )
-    protected void doShutdown( State fromState ) {
-    }
-
-    /**
-     * Return whether this service has been started and is currently running.
-     * @return true if started and currently running, or false otherwise
-     * @see #start()
-     * @see #pause()
-     * @see #isPaused()
-     * @see #isShutdown()
-     */
-    public boolean isStarted() {
-        return this.state == State.STARTED;
-    }
-
-    /**
-     * Return whether this service is currently paused.
-     * @return true if currently paused, or false otherwise
-     * @see #pause()
-     * @see #start()
-     * @see #isStarted()
-     * @see #isShutdown()
-     */
-    public boolean isPaused() {
-        return this.state == State.PAUSED;
-    }
-
-    /**
-     * Return whether this service is stopped and unable to be restarted.
-     * @return true if currently shutdown, or false otherwise
-     * @see #shutdown()
-     * @see #isPaused()
-     * @see #isStarted()
-     */
-    public boolean isShutdown() {
-        return this.state == State.SHUTDOWN;
-    }
-
-    protected abstract String serviceName();
-}

Deleted: trunk/dna-repository/src/main/java/org/jboss/dna/services/AdministeredService.java
===================================================================
--- trunk/dna-services/src/main/java/org/jboss/dna/services/AdministeredService.java	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/services/AdministeredService.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -1,30 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source.
- * Copyright 2008, Red Hat Middleware LLC, and individual contributors
- * as indicated by the @author tags. See the copyright.txt file in the
- * distribution for a full listing of individual contributors. 
- *
- * This is free software; you can redistribute it and/or modify it
- * under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.jboss.dna.services;
-
-/**
- * @author Randall Hauch
- */
-public interface AdministeredService {
-
-    ServiceAdministrator getAdministrator();
-}

Deleted: trunk/dna-repository/src/main/java/org/jboss/dna/services/ExecutionContext.java
===================================================================
--- trunk/dna-services/src/main/java/org/jboss/dna/services/ExecutionContext.java	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/services/ExecutionContext.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -1,46 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source.
- * Copyright 2008, Red Hat Middleware LLC, and individual contributors
- * as indicated by the @author tags. See the copyright.txt file in the
- * distribution for a full listing of individual contributors. 
- *
- * This is free software; you can redistribute it and/or modify it
- * under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.jboss.dna.services;
-
-import javax.jcr.Session;
-import org.jboss.dna.services.util.JcrTools;
-
-/**
- * The context of an execution.
- * @author Randall Hauch
- */
-public interface ExecutionContext {
-
-    /**
-     * Get the session factory, which can be used to obtain sessions temporarily for this context. Any session obtained from this
-     * factory should be {@link Session#logout() closed} before the execution finishes.
-     * @return the session factory
-     */
-    SessionFactory getSessionFactory();
-
-    /**
-     * Get a set of utilities for working with JCR.
-     * @return the tools
-     */
-    JcrTools getTools();
-
-}

Deleted: trunk/dna-repository/src/main/java/org/jboss/dna/services/JndiSessionFactory.java
===================================================================
--- trunk/dna-services/src/main/java/org/jboss/dna/services/JndiSessionFactory.java	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/services/JndiSessionFactory.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -1,248 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source.
- * Copyright 2008, Red Hat Middleware LLC, and individual contributors
- * as indicated by the @author tags. See the copyright.txt file in the
- * distribution for a full listing of individual contributors. 
- *
- * This is free software; you can redistribute it and/or modify it
- * under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-
-package org.jboss.dna.services;
-
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import javax.jcr.Credentials;
-import javax.jcr.Repository;
-import javax.jcr.RepositoryException;
-import javax.jcr.Session;
-import javax.jcr.SimpleCredentials;
-import javax.naming.Context;
-import javax.naming.InitialContext;
-import javax.naming.NamingException;
-import org.jboss.dna.common.SystemFailureException;
-import org.jboss.dna.common.util.ArgCheck;
-
-/**
- * A SessionFactory implementation that creates {@link Session} instances using {@link Repository} instances registered in JNDI.
- * <p>
- * This factory using a naming convention where the name supplied to the {@link #createSession(String)} contains both the name of
- * the repository and the name of the workspace. Typically, this is <i><code>repositoryName/workspaceName</code></i>, where
- * <code>repositoryName</code> is the JNDI name under which the Repository instance was bound, and <code>workspaceName</code>
- * is the name of the workspace. Note that this method looks for the last delimiter in the whole name to distinguish between the
- * repository and workspace names.
- * </p>
- * <p>
- * For example, if "<code>java:comp/env/repository/dataRepository/myWorkspace</code>" is passed to the
- * {@link #createSession(String)} method, this factory will look for a {@link Repository} instance registered in JDNI with the
- * name "<code>java:comp/env/repository/dataRepository</code>" and use it to {@link Repository#login(String) create a session}
- * to the workspace named "<code>myWorkspace</code>".
- * </p>
- * <p>
- * By default, this factory creates an anonymous JCR session. To use sessions with specific {@link Credentials}, simply
- * {@link #registerCredentials(String, Credentials) register} credentials for the appropriate repository/workspace name. For
- * security reasons, it is not possible to retrieve the Credentials once registered with this factory.
- * </p>
- * @author Randall Hauch
- */
-public class JndiSessionFactory implements SessionFactory {
-
-    protected static char[] DEFAULT_DELIMITERS = new char[] {'/'};
-
-    private final char[] workspaceDelims;
-    private final String workspaceDelimsRegexCharacterSet;
-    private final Context context;
-    private final Map<String, Credentials> credentials = new ConcurrentHashMap<String, Credentials>();
-
-    /**
-     * Create an instance of the factory by creating a new {@link InitialContext}. This is equivalent to calling
-     * <code>new JndiSessionFactory(new InitialContext())</code>.
-     * @throws NamingException if there is a problem creating the InitialContext.
-     */
-    public JndiSessionFactory() throws NamingException {
-        this(new InitialContext());
-    }
-
-    /**
-     * Create an instance of the factory by supplying the characters that may be used to delimit the workspace name from the
-     * repository name. This constructor initializes the factory with a new {@link InitialContext}, and is equivalent to calling
-     * <code>new JndiSessionFactory(new InitialContext(),workspaceDelimiters)</code>.
-     * @param workspaceDelimiters the delimiters, or null/empty if the default delimiter of '/' should be used.
-     * @throws NamingException if there is a problem creating the InitialContext.
-     */
-    public JndiSessionFactory( char... workspaceDelimiters ) throws NamingException {
-        this(new InitialContext(), workspaceDelimiters);
-    }
-
-    /**
-     * Create an instance of the factory using the supplied JNDI context.
-     * @param context the naming context
-     * @throws IllegalArgumentException if the context parameter is null
-     */
-    public JndiSessionFactory( Context context ) {
-        this(context, DEFAULT_DELIMITERS);
-    }
-
-    /**
-     * Create an instance of the factory by supplying naming context and the characters that may be used to delimit the workspace
-     * name from the repository name.
-     * @param context the naming context
-     * @param workspaceDelimiters the delimiters, or null/empty if the default delimiter of '/' should be used.
-     * @throws IllegalArgumentException if the context parameter is null
-     */
-    public JndiSessionFactory( Context context, char... workspaceDelimiters ) {
-        ArgCheck.isNotNull(context, "initial context");
-        this.context = context;
-        this.workspaceDelims = (workspaceDelimiters == null || workspaceDelimiters.length == 0) ? DEFAULT_DELIMITERS : workspaceDelimiters;
-        StringBuilder sb = new StringBuilder();
-        for (char delim : this.workspaceDelims) {
-            switch (delim) {
-                case '\\':
-                    sb.append("\\");
-                    break;
-                // case '[' : sb.append("\\["); break;
-                case ']':
-                    sb.append("\\]");
-                    break;
-                case '-':
-                    sb.append("\\-");
-                    break;
-                case '^':
-                    sb.append("\\^");
-                    break;
-                default:
-                    sb.append(delim);
-            }
-        }
-        this.workspaceDelimsRegexCharacterSet = sb.toString();
-    }
-
-    /**
-     * Convenience method to bind a repository in JNDI. Repository instances can be bound into JNDI using any technique, so this
-     * method need not be used. <i>Note that the name should not contain the workspace part.</i>
-     * @param name the name of the repository, without the workspace name component.
-     * @param repository the repository to be bound, or null if an existing repository should be unbound.
-     * @throws NamingException if there is a problem registering the supplied repository
-     */
-    public void registerRepository( String name, Repository repository ) throws NamingException {
-        assert name != null;
-        // Remove all trailing delimiters ...
-        name = name.replaceAll("[" + this.workspaceDelimsRegexCharacterSet + "]+$", "");
-        if (repository != null) {
-            this.context.bind(name, repository);
-        } else {
-            this.context.unbind(name);
-        }
-    }
-
-    /**
-     * Register the credentials for the repository and workspace given by the supplied name, username and password. This is
-     * equivalent to calling <code>registerCredentials(name, new SimpleCredentials(username,password))</code>, although if
-     * <code>username</code> is null then this is equivalent to <code>registerCredentials(name,null)</code>.
-     * @param name the name of the repository and workspace
-     * @param username the username to use, or null if the existing credentials for the named workspace should be removed
-     * @param password the password, may be null or empty
-     * @return true if this overwrote existing credentials
-     * @see #registerCredentials(String, Credentials)
-     * @see #removeCredentials(String)
-     */
-    public boolean registerCredentials( String name, String username, char[] password ) {
-        if (password == null && username != null) password = new char[] {};
-        Credentials creds = username == null ? null : new SimpleCredentials(username, password);
-        return registerCredentials(name, creds);
-    }
-
-    /**
-     * Register the credentials to be used for the named repository and workspace. Use the same name as used to
-     * {@link #createSession(String) create sessions}.
-     * @param name the name of the repository and workspace
-     * @param credentials the credentials to use, or null if the existing credentials for the named workspace should be removed
-     * @return true if this overwrote existing credentials
-     * @see #registerCredentials(String, String, char[])
-     * @see #removeCredentials(String)
-     */
-    public boolean registerCredentials( String name, Credentials credentials ) {
-        boolean foundExisting = false;
-        name = name != null ? name.trim() : null;
-        if (credentials == null) {
-            foundExisting = this.credentials.remove(name) != null;
-        } else {
-            foundExisting = this.credentials.put(name, credentials) != null;
-        }
-        return foundExisting;
-    }
-
-    /**
-     * Remove any credentials associated with the named repository and workspace. This is equivalent to calling
-     * <code>registerCredentials(name,null)</code>.
-     * @param name the name of the repository and workspace
-     * @return true if existing credentials were found and removed, or false if no such credentials existed
-     * @see #registerCredentials(String, Credentials)
-     */
-    public boolean removeCredentials( String name ) {
-        return registerCredentials(name, null);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    public Session createSession( String name ) throws RepositoryException {
-        ArgCheck.isNotNull(name, "session name");
-        name = name.trim();
-        // Look up the Repository object in JNDI ...
-        String repositoryName = getRepositoryName(name);
-        Repository repository = null;
-        try {
-            repository = (Repository)this.context.lookup(repositoryName);
-        } catch (NamingException err) {
-            throw new SystemFailureException(ServicesI18n.unableToFindRepositoryInJndi.text(repositoryName));
-        }
-
-        // Determine the name of the workspace ...
-        String workspaceName = getWorkspaceName(name);
-
-        // Look up any credentials, which may be null ...
-        Credentials creds = this.credentials.get(name);
-
-        // Create a session to the specified workspace and using the credentials (either or both may be null) ...
-        return repository.login(creds, workspaceName);
-    }
-
-    protected String getWorkspaceName( String name ) {
-        assert name != null;
-        int index = getIndexOfLastWorkspaceDelimiter(name);
-        if (index == -1) return null;
-        if ((index + 1) == name.length()) return null; // delim is the last character
-        return name.substring(index + 1);
-    }
-
-    protected String getRepositoryName( String name ) {
-        assert name != null;
-        int index = getIndexOfLastWorkspaceDelimiter(name);
-        if (index == -1) return name; // no delim
-        if ((index + 1) == name.length()) return name.substring(0, index); // delim as last character
-        return name.substring(0, index);
-    }
-
-    protected int getIndexOfLastWorkspaceDelimiter( String name ) {
-        int index = -1;
-        for (char delim : this.workspaceDelims) {
-            int i = name.lastIndexOf(delim);
-            index = Math.max(index, i);
-        }
-        return index;
-    }
-
-}

Deleted: trunk/dna-repository/src/main/java/org/jboss/dna/services/RepositoryNodePath.java
===================================================================
--- trunk/dna-services/src/main/java/org/jboss/dna/services/RepositoryNodePath.java	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/services/RepositoryNodePath.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -1,104 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source.
- * Copyright 2008, Red Hat Middleware LLC, and individual contributors
- * as indicated by the @author tags. See the copyright.txt file in the
- * distribution for a full listing of individual contributors. 
- *
- * This is free software; you can redistribute it and/or modify it
- * under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.jboss.dna.services;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import net.jcip.annotations.Immutable;
-import org.jboss.dna.common.util.HashCode;
-
-/**
- * @author Randall Hauch
- */
- at Immutable
-public class RepositoryNodePath {
-
-    protected static final Pattern PATTERN = Pattern.compile("([^:/]):(/.*)");
-
-    public static RepositoryNodePath parse( String path, String defaultRepositoryWorkspaceName ) {
-        Matcher matcher = PATTERN.matcher(path);
-        if (matcher.matches()) {
-            try {
-                return new RepositoryNodePath(matcher.group(1), matcher.group(2));
-            } catch (Throwable t) {
-                throw new IllegalArgumentException(ServicesI18n.invalidRepositoryNodePath.text(path, t.getMessage()));
-            }
-        }
-        return new RepositoryNodePath(defaultRepositoryWorkspaceName, path);
-
-    }
-
-    private final String repositoryName;
-    private final String nodePath;
-    private final int hc;
-
-    public RepositoryNodePath( String repositoryName, String nodePath ) {
-        this.repositoryName = repositoryName;
-        this.nodePath = nodePath;
-        this.hc = HashCode.compute(this.repositoryName, this.nodePath);
-    }
-
-    /**
-     * @return nodePath
-     */
-    public String getNodePath() {
-        return this.nodePath;
-    }
-
-    /**
-     * @return repositoryName
-     */
-    public String getRepositoryWorkspaceName() {
-        return this.repositoryName;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public int hashCode() {
-        return this.hc;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public boolean equals( Object obj ) {
-        if (obj == this) return true;
-        if (obj instanceof RepositoryNodePath) {
-            RepositoryNodePath that = (RepositoryNodePath)obj;
-            if (!this.repositoryName.equals(that.repositoryName)) return false;
-            if (!this.nodePath.equals(that.nodePath)) return false;
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public String toString() {
-        return this.repositoryName + ":" + this.nodePath;
-    }
-}

Deleted: trunk/dna-repository/src/main/java/org/jboss/dna/services/ServiceAdministrator.java
===================================================================
--- trunk/dna-services/src/main/java/org/jboss/dna/services/ServiceAdministrator.java	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/services/ServiceAdministrator.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -1,144 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source.
- * Copyright 2008, Red Hat Middleware LLC, and individual contributors
- * as indicated by the @author tags. See the copyright.txt file in the
- * distribution for a full listing of individual contributors. 
- *
- * This is free software; you can redistribute it and/or modify it
- * under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.jboss.dna.services;
-
-import java.util.concurrent.TimeUnit;
-import net.jcip.annotations.ThreadSafe;
-
-/**
- * Contract defining an administrative interface for controlling the running state of a service.
- * @author Randall Hauch
- */
- at ThreadSafe
-public interface ServiceAdministrator {
-
-    /**
-     * The available states.
-     * @author Randall Hauch
-     */
-    public static enum State {
-        STARTED,
-        PAUSED,
-        SHUTDOWN;
-    }
-
-    /**
-     * Return the current state of this system.
-     * @return the current state
-     */
-    public State getState();
-
-    /**
-     * Set the state of the system. This method does nothing if the desired state matches the current state.
-     * @param state the desired state
-     * @return this object for method chaining purposes
-     * @see #setState(String)
-     * @see #start()
-     * @see #pause()
-     * @see #shutdown()
-     */
-    public ServiceAdministrator setState( State state );
-
-    /**
-     * Set the state of the system. This method does nothing if the desired state matches the current state.
-     * @param state the desired state in string form
-     * @return this object for method chaining purposes
-     * @throws IllegalArgumentException if the specified state string is null or does not match one of the predefined
-     * {@link State predefined enumerated values}
-     * @see #setState(State)
-     * @see #start()
-     * @see #pause()
-     * @see #shutdown()
-     */
-    public ServiceAdministrator setState( String state );
-
-    /**
-     * Start monitoring and sequence the events. This method can be called multiple times, including after the system is
-     * {@link #pause() paused}. However, once the system is {@link #shutdown() shutdown}, it cannot be started or paused.
-     * @return this object for method chaining purposes
-     * @throws IllegalStateException if called when the system has been {@link #shutdown() shutdown}.
-     * @see #pause()
-     * @see #shutdown()
-     * @see #isStarted()
-     */
-    public ServiceAdministrator start();
-
-    /**
-     * Temporarily stop monitoring and sequencing events. This method can be called multiple times, including after the system is
-     * {@link #start() started}. However, once the system is {@link #shutdown() shutdown}, it cannot be started or paused.
-     * @return this object for method chaining purposes
-     * @throws IllegalStateException if called when the system has been {@link #shutdown() shutdown}.
-     * @see #start()
-     * @see #shutdown()
-     * @see #isPaused()
-     */
-    public ServiceAdministrator pause();
-
-    /**
-     * Permanently stop monitoring and sequencing events. This method can be called multiple times, but only the first call has an
-     * effect. Once the system has been shutdown, it may not be {@link #start() restarted} or {@link #pause() paused}.
-     * @return this object for method chaining purposes
-     * @see #start()
-     * @see #pause()
-     * @see #isShutdown()
-     */
-    public ServiceAdministrator shutdown();
-
-    /**
-     * Blocks until all work has completed execution after a shutdown request, or the timeout occurs, or the current thread is
-     * interrupted, whichever happens first.
-     * @param timeout the maximum time to wait
-     * @param unit the time unit of the timeout argument
-     * @return <tt>true</tt> if this service terminated and <tt>false</tt> if the timeout elapsed before termination
-     * @throws InterruptedException if interrupted while waiting
-     */
-    boolean awaitTermination( long timeout, TimeUnit unit ) throws InterruptedException;
-
-    /**
-     * Return whether this system has been started and is currently running.
-     * @return true if started and currently running, or false otherwise
-     * @see #start()
-     * @see #pause()
-     * @see #isPaused()
-     * @see #isShutdown()
-     */
-    public boolean isStarted();
-
-    /**
-     * Return whether this system is currently paused.
-     * @return true if currently paused, or false otherwise
-     * @see #pause()
-     * @see #start()
-     * @see #isStarted()
-     * @see #isShutdown()
-     */
-    public boolean isPaused();
-
-    /**
-     * Return whether this system is stopped and unable to be restarted.
-     * @return true if currently shutdown, or false otherwise
-     * @see #shutdown()
-     * @see #isPaused()
-     * @see #isStarted()
-     */
-    public boolean isShutdown();
-}

Deleted: trunk/dna-repository/src/main/java/org/jboss/dna/services/ServicesI18n.java
===================================================================
--- trunk/dna-services/src/main/java/org/jboss/dna/services/ServicesI18n.java	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/services/ServicesI18n.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -1,101 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source.
- * Copyright 2008, Red Hat Middleware LLC, and individual contributors
- * as indicated by the @author tags. See the copyright.txt file in the
- * distribution for a full listing of individual contributors. 
- *
- * This is free software; you can redistribute it and/or modify it
- * under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.jboss.dna.services;
-
-import org.jboss.dna.common.i18n.I18n;
-
-/**
- * @author Randall Hauch
- */
-public final class ServicesI18n {
-
-    static {
-        try {
-            I18n.initialize(ServicesI18n.class);
-        } catch (final Exception err) {
-            System.err.println(err);
-        }
-    }
-
-    public static I18n invalidStateString;
-    public static I18n serviceShutdowAndMayNotBeStarted;
-    public static I18n serviceShutdowAndMayNotBePaused;
-    public static I18n unableToFindRepositoryInJndi;
-    public static I18n errorProcessingEvents;
-    public static I18n errorFindingPropertyNameInPropertyAddedEvent;
-    public static I18n errorFindingPropertyNameInPropertyChangedEvent;
-    public static I18n errorFindingPropertyNameInPropertyRemovedEvent;
-
-    public static I18n unableToObtainJsr94RuleAdministrator;
-    public static I18n errorUsingJsr94RuleAdministrator;
-    public static I18n unableToObtainJsr94ServiceProvider;
-    public static I18n errorAddingOrUpdatingRuleSet;
-    public static I18n errorRollingBackRuleSetAfterUpdateFailed;
-    public static I18n errorReadingRulesAndProperties;
-    public static I18n errorDeregisteringRuleSetBeforeUpdatingIt;
-    public static I18n errorRecreatingRuleSet;
-    public static I18n errorRemovingRuleSet;
-    public static I18n errorRemovingRuleSetUponShutdown;
-    public static I18n unableToFindRuleSet;
-    public static I18n errorExecutingRuleSetWithGlobalsAndFacts;
-    public static I18n unableToBuildRuleSetRegularExpressionPattern;
-
-    public static I18n errorObtainingSessionToRepositoryWorkspace;
-    public static I18n errorWritingProblemsOnRuleSet;
-
-    public static I18n sequencingServiceName;
-    public static I18n unableToChangeExecutionContextWhileRunning;
-    public static I18n unableToStartSequencingServiceWithoutExecutionContext;
-    public static I18n errorWhileSequencingNode;
-    public static I18n errorInRepositoryWhileSequencingNode;
-    public static I18n errorFindingSequencersToRunAgainstNode;
-    public static I18n errorInRepositoryWhileFindingSequencersToRunAgainstNode;
-    public static I18n executionContextHasBeenClosed;
-    public static I18n sequencerTask;
-    public static I18n sequencerSubtask;
-    public static I18n unableToFindPropertyForSequencing;
-    public static I18n sequencingPropertyOnNode;
-    public static I18n writingOutputSequencedFromPropertyOnNodes;
-
-    public static I18n errorReadingPropertiesFromContainerNode;
-    public static I18n requiredPropertyOnNodeWasExpectedToBeStringValue;
-    public static I18n optionalPropertyOnNodeWasExpectedToBeStringValue;
-    public static I18n requiredPropertyOnNodeWasExpectedToBeStringArrayValue;
-    public static I18n optionalPropertyOnNodeWasExpectedToBeStringArrayValue;
-    public static I18n requiredPropertyOnNodeCouldNotBeRead;
-    public static I18n optionalPropertyOnNodeCouldNotBeRead;
-    public static I18n requiredPropertyIsMissingFromNode;
-    public static I18n errorGettingRequiredPropertyFromNode;
-    public static I18n errorGettingOptionalPropertyFromNode;
-    public static I18n errorClosingBinaryStreamForPropertyFromNode;
-    public static I18n requiredNodeDoesNotExistRelativeToNode;
-    public static I18n errorGettingNodeRelativeToNode;
-    public static I18n unknownPropertyValueType;
-
-    public static I18n pathExpressionIsInvalid;
-    public static I18n pathExpressionMayNotBeBlank;
-    public static I18n pathExpressionHasInvalidSelect;
-    public static I18n pathExpressionHasInvalidMatch;
-
-    public static I18n invalidRepositoryNodePath;
-
-}

Deleted: trunk/dna-repository/src/main/java/org/jboss/dna/services/SessionFactory.java
===================================================================
--- trunk/dna-services/src/main/java/org/jboss/dna/services/SessionFactory.java	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/dna-repository/src/main/java/org/jboss/dna/services/SessionFactory.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -1,35 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source.
- * Copyright 2008, Red Hat Middleware LLC, and individual contributors
- * as indicated by the @author tags. See the copyright.txt file in the
- * distribution for a full listing of individual contributors. 
- *
- * This is free software; you can redistribute it and/or modify it
- * under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-
-package org.jboss.dna.services;
-
-import javax.jcr.RepositoryException;
-import javax.jcr.Session;
-
-/**
- * @author Randall Hauch
- */
-public interface SessionFactory {
-
-    Session createSession( String name ) throws RepositoryException;
-
-}

Added: trunk/dna-repository/src/main/resources/org/jboss/dna/repository/RepositoryI18n.properties
===================================================================
--- trunk/dna-repository/src/main/resources/org/jboss/dna/repository/RepositoryI18n.properties	                        (rev 0)
+++ trunk/dna-repository/src/main/resources/org/jboss/dna/repository/RepositoryI18n.properties	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,61 @@
+invalidStateString = Invalid state parameter "{0}"
+serviceShutdowAndMayNotBeStarted = The {0} has been shutdown and may not be (re)started
+serviceShutdowAndMayNotBePaused = The {0} has been shutdown and my not be paused
+unableToFindRepositoryInJndi = Unable to find a JCR repository in JNDI at '{0}'
+errorProcessingEvents = Error processing events from {0}
+errorFindingPropertyNameInPropertyAddedEvent = Error finding the name of the added property in the event path {0}
+errorFindingPropertyNameInPropertyChangedEvent = Error finding the name of the changed property in the event path {0}
+errorFindingPropertyNameInPropertyRemovedEvent = Error finding the name of the removed property in the event path {0}
+
+unableToObtainJsr94RuleAdministrator = Unable to obtain the rule administrator for JSR-94 service provider {0} ({1}) while adding/updating rule set {2}
+errorUsingJsr94RuleAdministrator = Error using rule administrator for JSR-94 service provider {0} ({1}) while adding/updating rule set {2}
+unableToObtainJsr94ServiceProvider = Error using rule administrator for JSR-94 service provider {0} ({1})
+errorAddingOrUpdatingRuleSet = Error adding/updating rule set '{0}'
+errorRollingBackRuleSetAfterUpdateFailed = Error rolling back rule set '{0}' after new rule set failed
+errorReadingRulesAndProperties = Error reading the rules and properties while adding/updating rule set '{0}'
+errorDeregisteringRuleSetBeforeUpdatingIt = Error deregistering rule set '{0}' before updating it
+errorRecreatingRuleSet = Error (re)creating the rule set '{0}'
+errorRemovingRuleSet = Error removing rule set '{0}'
+errorRemovingRuleSetUponShutdown = Error removing rule set '{0}' upon shutdown
+unableToFindRuleSet = Unable to find rule set with name '{0}'
+errorExecutingRuleSetWithGlobalsAndFacts = Error executing rule set '{0}' and with globals {1} and facts {2}
+unableToBuildRuleSetRegularExpressionPattern = Unable to build the rule set name pattern "{0}" using the supplied absolute path ("{1}"): {2}
+
+errorObtainingSessionToRepositoryWorkspace = Error obtaining JCR session to repository workspace {0}
+errorWritingProblemsOnRuleSet = Error while writing problems on rule set node {0}
+
+sequencingServiceName = Sequencing Service
+unableToChangeExecutionContextWhileRunning = Unable to change the execution context while running
+unableToStartSequencingServiceWithoutExecutionContext = Unable to start the Sequencing Service without an execution context
+errorWhileSequencingNode = Error while sequencer {0} is sequencing node {1}
+errorInRepositoryWhileSequencingNode = Error in repository while sequencer {0} is sequencing node {1}
+errorInRepositoryWhileFindingSequencersToRunAgainstNode = Error in repository while finding sequencers to run against node {0}
+errorFindingSequencersToRunAgainstNode = Error finding sequencers to run against node {0}
+executionContextHasBeenClosed = This execution context has been closed and may not be used to create another session
+sequencerTask = Sequencing {0}
+sequencerSubtask = running {0}
+unableToFindPropertyForSequencing = Unable to find the {0} property while sequencing node {1}
+sequencingPropertyOnNode = Sequencing the {0} property on node {1}
+writingOutputSequencedFromPropertyOnNodes = Writing the output of the sequencing of the {0} property on node {1} to {2} nodes
+
+errorReadingPropertiesFromContainerNode = Error reading properties from property container node {0}
+requiredPropertyOnNodeWasExpectedToBeStringValue = The required {0} property on node {1} was expected to be a string value
+optionalPropertyOnNodeWasExpectedToBeStringValue = The optional {0} property on node {1} was expected to be a string value
+requiredPropertyOnNodeWasExpectedToBeStringArrayValue = The required {0} property on node {1} was expected to be a string array
+optionalPropertyOnNodeWasExpectedToBeStringArrayValue = The optional {0} property on node {1} was expected to be a string array
+requiredPropertyOnNodeCouldNotBeRead = The required {0} property on node {1} could not be read
+optionalPropertyOnNodeCouldNotBeRead = The optional {0} property on node {1} could not be read
+requiredPropertyIsMissingFromNode = The required {0} property is missing from node {1}
+errorGettingRequiredPropertyFromNode = Error while getting the required property {0} from node {1}
+errorGettingOptionalPropertyFromNode = Error while getting the optional property {0} from node {1}
+errorClosingBinaryStreamForPropertyFromNode = Error while closing the binary stream for property {0} on node {1}
+requiredNodeDoesNotExistRelativeToNode = The required node {0} does not exist relative to {1}
+errorGettingNodeRelativeToNode = Error while getting the node {0} (relative to {1})
+unknownPropertyValueType = Property value {0} is of a type ({1}) that is unknown
+
+pathExpressionMayNotBeBlank = The path expression may not be blank
+pathExpressionIsInvalid = The path expression {0} is not valid
+pathExpressionHasInvalidSelect = Invalid select expression '{0}' in the path expression '{1}=>{2}'
+pathExpressionHasInvalidMatch = Invalid match expression '{0}' in the path expression '{1}=>{2}'
+
+invalidRepositoryNodePath = The repository node path '{0}' is not valid: {1}

Added: trunk/dna-repository/src/test/java/org/jboss/dna/repository/observation/NodeChangeTest.java
===================================================================
--- trunk/dna-repository/src/test/java/org/jboss/dna/repository/observation/NodeChangeTest.java	                        (rev 0)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/repository/observation/NodeChangeTest.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,65 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.observation;
+
+import java.util.HashSet;
+import java.util.Set;
+import javax.jcr.observation.Event;
+import org.jboss.dna.repository.observation.NodeChange;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Randall Hauch
+ */
+public class NodeChangeTest {
+
+    private String validRepositoryWorkspaceName;
+    private String validAbsolutePath;
+    private int validEventTypes;
+    private Set<String> validModifiedProperties;
+    private Set<String> validRemovedProperties;
+    private NodeChange nodeChange;
+
+    @Before
+    public void beforeEach() throws Exception {
+        validRepositoryWorkspaceName = "repositoryX";
+        validAbsolutePath = "/a/b/c/d";
+        validEventTypes = Event.NODE_ADDED | Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED;
+        validModifiedProperties = new HashSet<String>();
+        validRemovedProperties = new HashSet<String>();
+        validModifiedProperties.add("jcr:name");
+        validModifiedProperties.add("jcr:title");
+        validRemovedProperties.add("jcr:mime");
+        nodeChange = new NodeChange(validRepositoryWorkspaceName, validAbsolutePath, validEventTypes, validModifiedProperties, validRemovedProperties);
+    }
+
+    @Test( expected = UnsupportedOperationException.class )
+    public void shouldHaveUnmodifiableSetOfModifiedPropertyNames() {
+        nodeChange.getModifiedProperties().clear();
+    }
+
+    @Test( expected = UnsupportedOperationException.class )
+    public void shouldHaveUnmodifiableSetOfRemovedPropertyNames() {
+        nodeChange.getRemovedProperties().clear();
+    }
+}

Added: trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleInput.java
===================================================================
--- trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleInput.java	                        (rev 0)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleInput.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,79 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.rules;
+
+public final class RuleInput {
+
+    String mimeType = "";
+    String header = "";
+    String fileName = "";
+
+    /**
+     * @return fileName
+     */
+    public String getFileName() {
+        return fileName;
+    }
+
+    /**
+     * @return header
+     */
+    public String getHeader() {
+        return header;
+    }
+
+    /**
+     * @return mimeType
+     */
+    public String getMimeType() {
+        return mimeType;
+    }
+
+    /**
+     * @param fileName Sets fileName to the specified value.
+     */
+    public void setFileName( String fileName ) {
+        this.fileName = fileName;
+    }
+
+    /**
+     * @param header Sets header to the specified value.
+     */
+    public void setHeader( String header ) {
+        this.header = header;
+    }
+
+    /**
+     * @param mimeType Sets mimeType to the specified value.
+     */
+    public void setMimeType( String mimeType ) {
+        this.mimeType = mimeType;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        return this.fileName + " (" + this.mimeType + ") => " + this.header;
+    }
+}

Added: trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleResult.java
===================================================================
--- trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleResult.java	                        (rev 0)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleResult.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,41 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.rules;
+
+/**
+ * @author John Verhaeg
+ */
+public final class RuleResult {
+
+    String name;
+
+    public RuleResult( String name ) {
+        this.name = name != null ? name : "";
+    }
+
+    /**
+     * @return name
+     */
+    public String getName() {
+        return name;
+    }
+}

Added: trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleServiceTest.java
===================================================================
--- trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleServiceTest.java	                        (rev 0)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleServiceTest.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,248 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.rules;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.hamcrest.core.IsNot.not;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.hamcrest.core.IsSame.sameInstance;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import java.io.IOException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.jboss.dna.common.component.ClassLoaderFactory;
+import org.jboss.dna.common.component.StandardClassLoaderFactory;
+import org.jboss.dna.common.util.IoUtil;
+import org.jboss.dna.common.util.Logger;
+import org.jboss.dna.repository.rules.InvalidRuleSetException;
+import org.jboss.dna.repository.rules.RuleService;
+import org.jboss.dna.repository.rules.RuleSet;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Randall Hauch
+ */
+public class RuleServiceTest {
+
+    protected static final String DROOLS_PROVIDER_CLASSNAME = "org.drools.jsr94.rules.RuleServiceProviderImpl";
+    protected static final String DROOLS_PROVIDER_URI = "http://drools.org/";
+
+    private RuleService ruleService;
+    private ClassLoaderFactory classLoaderFactory;
+
+    protected RuleSet createDroolsRuleSet( String desc ) throws IOException {
+        String name = "SampleRuleSet";
+        String description = desc != null ? desc : "This is a sample rule set that uses Drools";
+        String providerClassname = DROOLS_PROVIDER_CLASSNAME;
+        String providerUri = DROOLS_PROVIDER_URI;
+        String[] providerClasspath = new String[] {};
+        String ruleSetUri = null;
+        URL ruleFile = this.getClass().getResource("/rule_test.dslr");
+        URL dslFile = this.getClass().getResource("/rule_test.dsl");
+        String rules = IoUtil.read(ruleFile.openStream());
+        byte[] dslBytes = IoUtil.readBytes(dslFile.openStream());
+        Map<String, Object> properties = new HashMap<String, Object>();
+        properties.put("javax.rules.admin.RuleExecutionSet.source", "drl"); // as opposed to "xml"
+        properties.put("javax.rules.admin.RuleExecutionSet.dsl", dslBytes);
+        return new RuleSet(name, description, providerClassname, providerClasspath, providerUri, ruleSetUri, rules, properties);
+    }
+
+    protected RuleSet createRuleSetWithInvalidProvider() throws IOException {
+        String name = "SampleRuleSet"; // same name as Drools rule set
+        String description = "This is a sample rule set that uses Drools";
+        String providerClassname = DROOLS_PROVIDER_CLASSNAME;
+        String providerUri = DROOLS_PROVIDER_URI + "this/is/incorrect";
+        String[] providerClasspath = new String[] {};
+        String ruleSetUri = null;
+        URL ruleFile = this.getClass().getResource("/rule_test.dslr");
+        URL dslFile = this.getClass().getResource("/rule_test.dsl");
+        String rules = IoUtil.read(ruleFile.openStream());
+        byte[] dslBytes = IoUtil.readBytes(dslFile.openStream());
+        Map<String, Object> properties = new HashMap<String, Object>();
+        properties.put("javax.rules.admin.RuleExecutionSet.source", "drl"); // as opposed to "xml"
+        properties.put("javax.rules.admin.RuleExecutionSet.dsl", dslBytes);
+        return new RuleSet(name, description, providerClassname, providerClasspath, providerUri, ruleSetUri, rules, properties);
+    }
+
+    @Before
+    public void beforeEach() throws Exception {
+        this.ruleService = new RuleService();
+        this.classLoaderFactory = new StandardClassLoaderFactory();
+    }
+
+    @After
+    public void afterEach() throws Exception {
+        this.ruleService.getAdministrator().shutdown();
+    }
+
+    @Test
+    public void shouldHaveDefaultClasspathFactoryUponConstruction() {
+        assertThat(ruleService.getClassLoaderFactory(), is(notNullValue()));
+        assertThat(ruleService.getClassLoaderFactory(), is(sameInstance(RuleService.DEFAULT_CLASSLOADER_FACTORY)));
+    }
+
+    @Test
+    public void shouldHaveLoggerUponConstruction() {
+        assertThat(ruleService.getLogger(), is(notNullValue()));
+    }
+
+    @Test
+    public void shouldHaveEmptyRuleSetCollectionUponConstruction() {
+        assertThat(ruleService.getRuleSets(), is(notNullValue()));
+        assertThat(ruleService.getRuleSets().isEmpty(), is(true));
+    }
+
+    @Test( expected = UnsupportedOperationException.class )
+    public void shouldReturnUnmodifiableCollectionOfRuleSets() {
+        ruleService.getRuleSets().add(null);
+    }
+
+    @Test
+    public void shouldSetClassLoaderFactoryToDefaultWhenPassedNull() {
+        assertThat(ruleService.getClassLoaderFactory(), is(sameInstance(RuleService.DEFAULT_CLASSLOADER_FACTORY)));
+        ruleService.setClassLoaderFactory(classLoaderFactory);
+        assertThat(ruleService.getClassLoaderFactory(), is(sameInstance(classLoaderFactory)));
+        ruleService.setClassLoaderFactory(null);
+        assertThat(ruleService.getClassLoaderFactory(), is(sameInstance(RuleService.DEFAULT_CLASSLOADER_FACTORY)));
+    }
+
+    @Test
+    public void shouldSetLoggerToDefaultWhenPassedNull() {
+        Logger defaultLogger = ruleService.getLogger();
+        assertThat(ruleService.getLogger(), is(notNullValue()));
+        Logger orgLogger = Logger.getLogger("org");
+        ruleService.setLogger(orgLogger);
+        assertThat(ruleService.getLogger().getName(), is(orgLogger.getName()));
+        assertThat(ruleService.getLogger(), is(sameInstance(orgLogger)));
+        ruleService.setLogger(null);
+        assertThat(ruleService.getLogger().getName(), is(defaultLogger.getName()));
+        assertThat(ruleService.getLogger(), is(not(orgLogger)));
+    }
+
+    @Test( expected = InvalidRuleSetException.class )
+    public void shouldNotAddInvalidRuleSet() throws Exception {
+        ruleService.addRuleSet(createRuleSetWithInvalidProvider());
+
+    }
+
+    @Test
+    public void shouldAddValidRuleSet() throws Exception {
+        int numRuleSetsBefore = ruleService.getRuleSets().size();
+        RuleSet validRuleSet = createDroolsRuleSet(null);
+        assertThat(ruleService.addRuleSet(validRuleSet), is(true));
+        assertThat(ruleService.getRuleSets().size(), is(numRuleSetsBefore + 1));
+    }
+
+    @Test
+    public void shouldNotUpdateValidRuleSetWithInvalidRuleSet() throws Exception {
+        RuleSet validRuleSet = createDroolsRuleSet(null);
+        final String ruleSetName = validRuleSet.getName();
+
+        assertThat(ruleService.addRuleSet(validRuleSet), is(true));
+        assertThat(ruleService.getRuleSets().size(), is(1));
+        assertThat(ruleService.getRuleSets().iterator().next(), is(validRuleSet));
+        // Verify this rule set works ...
+        executeRuleSet(ruleSetName);
+
+        // Try to update the rule set with something that's invalid ...
+        RuleSet invalidRuleSet = createRuleSetWithInvalidProvider();
+        assertThat(invalidRuleSet.getName(), is(ruleSetName));
+        assertThat(invalidRuleSet.getName(), is(validRuleSet.getName()));
+        try {
+            ruleService.addRuleSet(invalidRuleSet);
+            fail("Should not get here");
+        } catch (InvalidRuleSetException e) {
+            // This is expected since it was a bad rule set ...
+        }
+
+        // Verify the original rule set is still there and still works ...
+        assertThat(ruleService.getRuleSets().size(), is(1));
+        assertThat(ruleService.getRuleSets().iterator().next(), is(validRuleSet));
+        executeRuleSet(ruleSetName);
+    }
+
+    @Test
+    public void shouldModifyExistingRuleSetWhenAddedOnlyWhenChanged() throws Exception {
+        int numRuleSetsBefore = ruleService.getRuleSets().size();
+        RuleSet validRuleSet = createDroolsRuleSet(null);
+        assertThat(ruleService.addRuleSet(validRuleSet), is(true));
+        assertThat(ruleService.getRuleSets().size(), is(numRuleSetsBefore + 1));
+
+        // Try to add an unchanged rule set ...
+        RuleSet validRuleSet2 = createDroolsRuleSet(null);
+        assertThat(validRuleSet2.hasChanged(validRuleSet), is(false));
+        assertThat(ruleService.addRuleSet(validRuleSet2), is(false));
+        assertThat(ruleService.getRuleSets().size(), is(numRuleSetsBefore + 1));
+
+        // Try to add a changed rule set ...
+        RuleSet validRuleSet3 = createDroolsRuleSet("This is an alternative description that causes it to be changed");
+        assertThat(validRuleSet3.hasChanged(validRuleSet), is(true));
+        assertThat(ruleService.addRuleSet(validRuleSet3), is(true));
+        assertThat(ruleService.getRuleSets().size(), is(numRuleSetsBefore + 1));
+    }
+
+    @Test
+    public void shouldExecuteRuleSet() throws Exception {
+        RuleSet validRuleSet = createDroolsRuleSet(null);
+        assertThat(ruleService.addRuleSet(validRuleSet), is(true));
+
+        executeRuleSet(validRuleSet.getName());
+    }
+
+    /**
+     * @param validRuleSet
+     */
+    protected void executeRuleSet( String ruleSetName ) {
+        // Create some simple fact objects ...
+        RuleInput info = new RuleInput();
+        info.setFileName("someOtherInput.dsl");
+        info.setHeader("This is the sequencer header");
+        info.setMimeType("text");
+
+        // Run the rules ...
+        Set<String> output = new LinkedHashSet<String>();
+        Map<String, Object> globals = new HashMap<String, Object>();
+        globals.put("output", output);
+
+        List<?> results = ruleService.executeRules(ruleSetName, globals, info);
+        assertThat(results, is(notNullValue()));
+        assertThat(results.isEmpty(), is(false));
+        assertThat(results.size(), is(2));
+        assertTrue(results.get(0) instanceof RuleResult || results.get(1) instanceof RuleResult);
+        // Object result = results.get(0);
+        // assertThat(result, is(instanceOf(RuleResult.class)));
+        assertThat(output.isEmpty(), is(false));
+        Object sequencer = output.iterator().next();
+        assertThat(sequencer, is(instanceOf(String.class)));
+        assertThat(((String)sequencer), is("A"));
+    }
+
+}

Added: trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleSetTest.java
===================================================================
--- trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleSetTest.java	                        (rev 0)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/repository/rules/RuleSetTest.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,299 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.rules;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.junit.Assert.assertThat;
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import org.jboss.dna.common.util.IoUtil;
+import org.jboss.dna.repository.rules.RuleSet;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Randall Hauch
+ */
+public class RuleSetTest {
+
+    private RuleSet ruleSet;
+    private String validName;
+    private String validDescription;
+    private String validClassname;
+    private String[] validClasspath;
+    private String validProviderUri;
+    private String validRuleSetUri;
+    private String validRules;
+    private Map<String, Object> validProperties;
+
+    @Before
+    public void beforeEach() throws Exception {
+        this.validName = "This is a valid name";
+        this.validDescription = "This is a valid description";
+        this.validClassname = "com.acme.SuperDuper";
+        this.validClasspath = new String[] {"classpath1", "classpath2"};
+        this.validProviderUri = "http://www.acme.com/super/duper/rules_engine";
+        this.validRuleSetUri = "com.something.whatever doesn't really need to be a URI";
+        this.validRules = "when something is true then cheer";
+        this.validProperties = new HashMap<String, Object>();
+        this.validProperties.put("key1", "value1");
+        this.validProperties.put("key2", null);
+        this.validProperties.put("key3", "value3".getBytes());
+        this.ruleSet = new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowNullName() {
+        new RuleSet(null, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowEmptyName() {
+        new RuleSet("", validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowBlankName() {
+        new RuleSet("  \t ", validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+    }
+
+    @Test
+    public void shouldTrimName() {
+        validName = "  this is a valid name with leading and trailing whitespace  ";
+        ruleSet = new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(ruleSet.getName(), is(validName.trim()));
+    }
+
+    @Test
+    public void shouldAllowNullOrEmptyOrBlankDescriptionAndShouldReplaceWithEmptyString() {
+        ruleSet = new RuleSet(validName, null, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(ruleSet.getDescription(), is(""));
+        ruleSet = new RuleSet(validName, "", validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(ruleSet.getDescription(), is(""));
+        ruleSet = new RuleSet(validName, "  \t ", validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(ruleSet.getDescription(), is(""));
+    }
+
+    @Test
+    public void shouldTrimDescription() {
+        ruleSet = new RuleSet(validName, "  valid ", validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(ruleSet.getDescription(), is("valid"));
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowNullClassname() {
+        new RuleSet(validName, validDescription, null, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowEmptyClassname() {
+        new RuleSet(validName, validDescription, "", validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowBlankClassname() {
+        new RuleSet(validName, validDescription, "   ", validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowClassnameThatDoesNotFollowJavaNamingRules() {
+        new RuleSet(validName, validDescription, "not a valid classname", validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+    }
+
+    @Test
+    public void shouldAllowNullOrEmptyClasspath() {
+        new RuleSet(validName, validDescription, validClassname, null, validProviderUri, validRuleSetUri, validRules, validProperties);
+    }
+
+    @Test
+    public void shouldRemoveNullOrBlankClasspathItems() {
+        new RuleSet(validName, validDescription, validClassname, new String[] {}, validProviderUri, validRuleSetUri, validRules, validProperties);
+    }
+
+    @Test
+    public void shouldRemoveDuplicateClasspathItemsInCaseSensitiveManner() {
+        validClasspath = new String[] {"path1", "path2", "path1", "path3", "path2"};
+        ruleSet = new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(ruleSet.getComponentClasspathArray(), is(new String[] {"path1", "path2", "path3"}));
+
+        validClasspath = new String[] {"path1", "path2", "path1", "path3", "Path2"};
+        ruleSet = new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(ruleSet.getComponentClasspathArray(), is(new String[] {"path1", "path2", "path3", "Path2"}));
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowNullProviderUri() {
+        new RuleSet(validName, validDescription, validClassname, validClasspath, null, validRuleSetUri, validRules, validProperties);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowEmptyProviderUri() {
+        new RuleSet(validName, validDescription, validClassname, validClasspath, "", validRuleSetUri, validRules, validProperties);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowBlankProviderUri() {
+        new RuleSet(validName, validDescription, validClassname, validClasspath, "  \t  ", validRuleSetUri, validRules, validProperties);
+    }
+
+    @Test
+    public void shouldUseNameInPlaceOfNullRuleSetUri() {
+        ruleSet = new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, null, validRules, validProperties);
+        assertThat(ruleSet.getRuleSetUri(), is(ruleSet.getName()));
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowEmptyRuleSetUri() {
+        new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, "", validRules, validProperties);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowBlankRuleSetUri() {
+        new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, "  \t  ", validRules, validProperties);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowNullRules() {
+        new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, null, validProperties);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowEmptyRules() {
+        new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, "", validProperties);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowBlankRules() {
+        new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, "   \t  ", validProperties);
+    }
+
+    @Test
+    public void shouldAllowNullOrEmptyProperties() {
+        validProperties = null;
+        ruleSet = new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(ruleSet.getProperties(), is(notNullValue()));
+    }
+
+    @Test
+    public void shouldWrapByteArrayPropertyValuesWithInputStreamInExecutionSetProperties() throws Exception {
+        Map<Object, Object> executionSetProps = ruleSet.getExecutionSetProperties();
+        assertThat(executionSetProps.size(), is(ruleSet.getProperties().size()));
+        assertThat(executionSetProps.keySet(), is((Set<Object>)new HashSet<Object>(ruleSet.getProperties().keySet())));
+        Iterator<Map.Entry<Object, Object>> executionSetPropIter = executionSetProps.entrySet().iterator();
+        Iterator<Map.Entry<String, Object>> propIter = ruleSet.getProperties().entrySet().iterator();
+        while (executionSetPropIter.hasNext() && propIter.hasNext()) {
+            Map.Entry<Object, Object> execSetEntry = executionSetPropIter.next();
+            Object execSetKey = execSetEntry.getKey();
+            Object execSetValue = execSetEntry.getValue();
+            Map.Entry<String, Object> propertyEntry = propIter.next();
+            String propertyKey = propertyEntry.getKey();
+            Object propertyValue = propertyEntry.getValue();
+            // Check the entries ...
+            assertThat(execSetKey, is((Object)propertyKey));
+            if (propertyValue instanceof byte[]) {
+                assertThat(execSetValue, is(instanceOf(Reader.class)));
+                String streamValueAsString = IoUtil.read((Reader)execSetValue);
+                String propertyValueAsString = new String((byte[])propertyValue);
+                assertThat(streamValueAsString, is(propertyValueAsString));
+            } else {
+                assertThat(execSetValue, is(propertyValue));
+            }
+        }
+        assertThat(executionSetPropIter.hasNext(), is(false));
+        assertThat(propIter.hasNext(), is(false));
+
+        new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+    }
+
+    @Test
+    public void shouldConsiderAnyAttributeDifferencesAsChange() {
+        RuleSet copy = ruleSet.clone();
+        assertThat(copy.hasChanged(ruleSet), is(false));
+        assertThat(copy.hasChanged(copy), is(false));
+
+        copy = new RuleSet(validName + "x", validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(copy.hasChanged(ruleSet), is(true));
+        assertThat(copy.hasChanged(copy), is(false));
+
+        copy = new RuleSet(validName, validDescription + "x", validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(copy.hasChanged(ruleSet), is(true));
+        assertThat(copy.hasChanged(copy), is(false));
+
+        copy = new RuleSet(validName, validDescription, validClassname + "x", validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(copy.hasChanged(ruleSet), is(true));
+        assertThat(copy.hasChanged(copy), is(false));
+
+        copy = new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri + "x", validRuleSetUri, validRules, validProperties);
+        assertThat(copy.hasChanged(ruleSet), is(true));
+        assertThat(copy.hasChanged(copy), is(false));
+
+        validClasspath = new String[] {"classpath1", "classpath2x"};
+        copy = new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri + "x", validRules, validProperties);
+        assertThat(copy.hasChanged(ruleSet), is(true));
+        assertThat(copy.hasChanged(copy), is(false));
+
+        copy = new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules + "x", validProperties);
+        assertThat(copy.hasChanged(ruleSet), is(true));
+        assertThat(copy.hasChanged(copy), is(false));
+
+        validProperties.remove("key1");
+        copy = new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(copy.hasChanged(ruleSet), is(true));
+        assertThat(copy.hasChanged(copy), is(false));
+    }
+
+    @Test
+    public void shouldConsiderOnlyNameWhenDeterminingIfSame() {
+        RuleSet copy = ruleSet.clone();
+        assertThat(copy.equals(ruleSet), is(true));
+
+        copy = new RuleSet(validName + "x", validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(copy.equals(ruleSet), is(false));
+
+        copy = new RuleSet(validName, validDescription + "x", validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(copy.equals(ruleSet), is(true));
+
+        copy = new RuleSet(validName, validDescription, validClassname + "x", validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(copy.equals(ruleSet), is(true));
+
+        copy = new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri + "x", validRuleSetUri, validRules, validProperties);
+        assertThat(copy.equals(ruleSet), is(true));
+
+        validClasspath = new String[] {"classpath1", "classpath2x"};
+        copy = new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri + "x", validRules, validProperties);
+        assertThat(copy.equals(ruleSet), is(true));
+
+        copy = new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules + "x", validProperties);
+        assertThat(copy.equals(ruleSet), is(true));
+
+        validProperties.remove("key1");
+        copy = new RuleSet(validName, validDescription, validClassname, validClasspath, validProviderUri, validRuleSetUri, validRules, validProperties);
+        assertThat(copy.equals(ruleSet), is(true));
+    }
+
+}

Added: trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/MockSequencerA.java
===================================================================
--- trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/MockSequencerA.java	                        (rev 0)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/MockSequencerA.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,102 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+
+package org.jboss.dna.repository.sequencers;
+
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.jcr.Node;
+import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.CoreI18n;
+import org.jboss.dna.common.monitor.ProgressMonitor;
+import org.jboss.dna.repository.observation.NodeChange;
+import org.jboss.dna.repository.sequencers.Sequencer;
+import org.jboss.dna.repository.sequencers.SequencerConfig;
+import org.jboss.dna.repository.util.ExecutionContext;
+import org.jboss.dna.repository.util.RepositoryNodePath;
+
+/**
+ * A sequencer that can be used for basic unit testing.
+ * @author Randall Hauch
+ */
+ at ThreadSafe
+public class MockSequencerA implements Sequencer {
+
+    private SequencerConfig config;
+    private AtomicInteger counter = new AtomicInteger();
+    private CountDownLatch latch = new CountDownLatch(0);
+
+    public void setExpectedCount( int numExpected ) {
+        this.latch = new CountDownLatch(numExpected);
+    }
+
+    public boolean awaitExecution( long timeout, TimeUnit unit ) throws InterruptedException {
+        return this.latch.await(timeout, unit);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void setConfiguration( SequencerConfig sequencerConfiguration ) {
+        this.config = sequencerConfiguration;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void execute( Node input, String sequencedPropertyName, NodeChange changes, Set<RepositoryNodePath> outputPaths, ExecutionContext context, ProgressMonitor progress ) {
+        try {
+            progress.beginTask(1, CoreI18n.passthrough, "Incrementing counter");
+            // increment the counter and record the progress ...
+            this.counter.incrementAndGet();
+            this.latch.countDown();
+            progress.worked(1);
+        } finally {
+            progress.done();
+        }
+    }
+
+    public int getCounter() {
+        return this.counter.get();
+    }
+
+    public boolean isConfigured() {
+        return this.config != null;
+    }
+
+    /**
+     * @return config
+     */
+    public SequencerConfig getConfiguration() {
+        return this.config;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        return (this.config != null ? this.config.getName() : "SampleSequencer") + " [" + this.getCounter() + "]";
+    }
+}

Added: trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/MockSequencerB.java
===================================================================
--- trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/MockSequencerB.java	                        (rev 0)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/MockSequencerB.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,102 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+
+package org.jboss.dna.repository.sequencers;
+
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.jcr.Node;
+import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.CoreI18n;
+import org.jboss.dna.common.monitor.ProgressMonitor;
+import org.jboss.dna.repository.observation.NodeChange;
+import org.jboss.dna.repository.sequencers.Sequencer;
+import org.jboss.dna.repository.sequencers.SequencerConfig;
+import org.jboss.dna.repository.util.ExecutionContext;
+import org.jboss.dna.repository.util.RepositoryNodePath;
+
+/**
+ * A sequencer that can be used for basic unit testing.
+ * @author Randall Hauch
+ */
+ at ThreadSafe
+public class MockSequencerB implements Sequencer {
+
+    private SequencerConfig config;
+    private AtomicInteger counter = new AtomicInteger();
+    private CountDownLatch latch = new CountDownLatch(0);
+
+    public void setExpectedCount( int numExpected ) {
+        this.latch = new CountDownLatch(numExpected);
+    }
+
+    public boolean awaitExecution( long timeout, TimeUnit unit ) throws InterruptedException {
+        return this.latch.await(timeout, unit);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void setConfiguration( SequencerConfig sequencerConfiguration ) {
+        this.config = sequencerConfiguration;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void execute( Node input, String sequencedPropertyName, NodeChange changes, Set<RepositoryNodePath> outputPaths, ExecutionContext context, ProgressMonitor progress ) {
+        try {
+            progress.beginTask(1, CoreI18n.passthrough, "Incrementing counter");
+            // increment the counter and record the progress ...
+            this.counter.incrementAndGet();
+            this.latch.countDown();
+            progress.worked(1);
+        } finally {
+            progress.done();
+        }
+    }
+
+    public int getCounter() {
+        return this.counter.get();
+    }
+
+    public boolean isConfigured() {
+        return this.config != null;
+    }
+
+    /**
+     * @return config
+     */
+    public SequencerConfig getConfiguration() {
+        return this.config;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        return (this.config != null ? this.config.getName() : "SampleSequencer") + " [" + this.getCounter() + "]";
+    }
+}

Added: trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/SequencerConfigTest.java
===================================================================
--- trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/SequencerConfigTest.java	                        (rev 0)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/SequencerConfigTest.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,131 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+
+package org.jboss.dna.repository.sequencers;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import org.jboss.dna.repository.sequencers.SequencerConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Randall Hauch
+ */
+public class SequencerConfigTest {
+
+    private SequencerConfig configA;
+    private SequencerConfig configB;
+    private SequencerConfig configA2;
+    private String validName;
+    private String validDescription;
+    private String validClassname;
+    private String[] validPathExpressions;
+    private String[] validMavenIds;
+
+    @Before
+    public void beforeEach() throws Exception {
+        this.validName = "valid configuration name";
+        this.validDescription = "a sequencer";
+        this.validClassname = MockSequencerA.class.getName();
+        this.validPathExpressions = new String[] {"/a/b/c/d[e/@attribute] => ."};
+        this.validMavenIds = new String[] {"com.acme:configA:1.0,com.acme:configB:1.0"};
+        this.configA = new SequencerConfig("configA", validDescription, MockSequencerA.class.getName(), validMavenIds, validPathExpressions);
+        this.configB = new SequencerConfig("configB", validDescription, MockSequencerB.class.getName(), validMavenIds, validPathExpressions);
+        this.configA2 = new SequencerConfig("conFigA", validDescription, MockSequencerA.class.getName(), validMavenIds, validPathExpressions);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowNullNameInConstructor() {
+        new SequencerConfig(null, validDescription, validClassname, validMavenIds, validPathExpressions);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowEmptyNameInConstructor() {
+        new SequencerConfig("", validDescription, validClassname, validMavenIds, validPathExpressions);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowBlankNameInConstructor() {
+        new SequencerConfig("   \t", validDescription, validClassname, validMavenIds, validPathExpressions);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowNullClassNameInConstructor() {
+        new SequencerConfig(validName, validDescription, null, validMavenIds, validPathExpressions);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowEmptyClassNameInConstructor() {
+        new SequencerConfig(validName, validDescription, "", validMavenIds, validPathExpressions);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowBlankClassNameInConstructor() {
+        new SequencerConfig(validName, validDescription, "   \t", validMavenIds, validPathExpressions);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowInvalidClassNameInConstructor() {
+        new SequencerConfig(validName, validDescription, "12.this is not a valid classname", validMavenIds, validPathExpressions);
+    }
+
+    @Test
+    public void shouldConsiderSameIfNamesAreEqualIgnoringCase() {
+        assertThat(configA.equals(configA2), is(true));
+    }
+
+    @Test
+    public void shouldConsiderNotSameIfNamesAreNotEqualIgnoringCase() {
+        assertThat(configA.equals(configB), is(false));
+    }
+
+    @Test
+    public void shouldNotAddNullOrBlankPathExpressions() {
+        assertThat(SequencerConfig.buildPathExpressionSet(null, "", "   ", validPathExpressions[0]).size(), is(1));
+    }
+
+    @Test
+    public void shouldNotAddSamePathExpressionMoreThanOnce() {
+        assertThat(SequencerConfig.buildPathExpressionSet(validPathExpressions[0], validPathExpressions[0], validPathExpressions[0]).size(), is(1));
+    }
+
+    @Test
+    public void shouldHaveNonNullPathExpressionCollectionWhenThereAreNoPathExpressions() {
+        configA = new SequencerConfig("configA", validDescription, validClassname, validMavenIds);
+        assertThat(configA.getPathExpressions().size(), is(0));
+    }
+
+    @Test
+    public void shouldSetClasspathWithValidMavenIds() {
+        assertThat(configA.getComponentClasspath().size(), is(validMavenIds.length));
+        assertThat(configA.getComponentClasspathArray(), is(validMavenIds));
+    }
+
+    @Test
+    public void shouldGetNonNullSequencerClasspathWhenEmpty() {
+        configA = new SequencerConfig("configA", validDescription, validClassname, null, validPathExpressions);
+        assertThat(configA.getComponentClasspath().size(), is(0));
+        assertThat(configA.getComponentClasspathArray().length, is(0));
+    }
+
+}

Added: trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/SequencerPathExpressionTest.java
===================================================================
--- trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/SequencerPathExpressionTest.java	                        (rev 0)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/SequencerPathExpressionTest.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,356 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.sequencers;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.junit.Assert.assertThat;
+import org.jboss.dna.repository.sequencers.InvalidSequencerPathExpression;
+import org.jboss.dna.repository.sequencers.SequencerPathExpression;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Randall Hauch
+ */
+public class SequencerPathExpressionTest {
+
+    private SequencerPathExpression expr;
+
+    @Before
+    public void beforeEach() throws Exception {
+        expr = new SequencerPathExpression(".*", "/output");
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotCompileNullExpression() {
+        SequencerPathExpression.compile(null);
+    }
+
+    @Test( expected = InvalidSequencerPathExpression.class )
+    public void shouldNotCompileZeroLengthExpression() {
+        SequencerPathExpression.compile("");
+    }
+
+    @Test( expected = InvalidSequencerPathExpression.class )
+    public void shouldNotCompileBlankExpression() {
+        SequencerPathExpression.compile("    ");
+    }
+
+    @Test
+    public void shouldCompileExpressionWithOnlySelectionExpression() {
+        expr = SequencerPathExpression.compile("/a/b/c");
+        assertThat(expr, is(notNullValue()));
+        assertThat(expr.getSelectExpression(), is("/a/b/c"));
+        assertThat(expr.getOutputExpression(), is(SequencerPathExpression.DEFAULT_OUTPUT_EXPRESSION));
+    }
+
+    @Test( expected = InvalidSequencerPathExpression.class )
+    public void shouldNotCompileExpressionWithSelectionExpressionAndDelimiterAndNoOutputExpression() {
+        SequencerPathExpression.compile("/a/b/c=>");
+    }
+
+    @Test
+    public void shouldCompileExpressionWithSelectionExpressionAndDelimiterAndOutputExpression() {
+        expr = SequencerPathExpression.compile("/a/b/c=>.");
+        assertThat(expr, is(notNullValue()));
+        assertThat(expr.getSelectExpression(), is("/a/b/c"));
+        assertThat(expr.getOutputExpression(), is("."));
+
+        expr = SequencerPathExpression.compile("/a/b/c=>/x/y");
+        assertThat(expr, is(notNullValue()));
+        assertThat(expr.getSelectExpression(), is("/a/b/c"));
+        assertThat(expr.getOutputExpression(), is("/x/y"));
+    }
+
+    @Test
+    public void shouldCompileExpressionWithExtraWhitespace() {
+        expr = SequencerPathExpression.compile(" /a/b/c => . ");
+        assertThat(expr, is(notNullValue()));
+        assertThat(expr.getSelectExpression(), is("/a/b/c"));
+        assertThat(expr.getOutputExpression(), is("."));
+
+        expr = SequencerPathExpression.compile("  /a/b/c => /x/y ");
+        assertThat(expr, is(notNullValue()));
+        assertThat(expr.getSelectExpression(), is("/a/b/c"));
+        assertThat(expr.getOutputExpression(), is("/x/y"));
+    }
+
+    @Test
+    public void shouldCompileExpressionWithIndexes() {
+        assertThat(SequencerPathExpression.compile("/a/b[0]/c[1]/d/e"), is(notNullValue()));
+        assertThat(SequencerPathExpression.compile("/a/b[0]/c[1]/d/e[2]"), is(notNullValue()));
+    }
+
+    @Test
+    public void shouldNotRemoveUsedPredicates() {
+        assertThat(expr.removeUnusedPredicates("/a/b/c"), is("/a/b/c"));
+        assertThat(expr.removeUnusedPredicates("/a/b[0]/c"), is("/a/b[0]/c"));
+        assertThat(expr.removeUnusedPredicates("/a/b[1]/c"), is("/a/b[1]/c"));
+        assertThat(expr.removeUnusedPredicates("/a/b[10]/c"), is("/a/b[10]/c"));
+        assertThat(expr.removeUnusedPredicates("/a/b[100]/c"), is("/a/b[100]/c"));
+        assertThat(expr.removeUnusedPredicates("/a/b[1000]/c"), is("/a/b[1000]/c"));
+        assertThat(expr.removeUnusedPredicates("/a/b[]/c"), is("/a/b[]/c"));
+        assertThat(expr.removeUnusedPredicates("/a/b[*]/c"), is("/a/b[*]/c"));
+        assertThat(expr.removeUnusedPredicates("/a/b[1,2]/c"), is("/a/b[1,2]/c"));
+        assertThat(expr.removeUnusedPredicates("/a/b[1,2,3,4,5]/c"), is("/a/b[1,2,3,4,5]/c"));
+        assertThat(expr.removeUnusedPredicates("/a/b/c[@title]"), is("/a/b/c[@title]"));
+        assertThat(expr.removeUnusedPredicates("/a/b/c[d/e/@title]"), is("/a/b/c[d/e/@title]"));
+        assertThat(expr.removeUnusedPredicates("/a/(b/c)[(d|e)/(f|g)/@something]"), is("/a/(b/c)[(d|e)/(f|g)/@something]"));
+        // These are legal, but aren't really useful ...
+        assertThat(expr.removeUnusedPredicates("/a/b[1][2][3]/c"), is("/a/b[1][2][3]/c"));
+    }
+
+    @Test
+    public void shouldRemoveUnusedPredicates() {
+        assertThat(expr.removeUnusedPredicates("/a/b[-1]/c"), is("/a/b/c"));
+        assertThat(expr.removeUnusedPredicates("/a/b[@name='wacky']/c"), is("/a/b/c"));
+        assertThat(expr.removeUnusedPredicates("/a/b[3][@name='wacky']/c"), is("/a/b[3]/c"));
+        assertThat(expr.removeUnusedPredicates("/a/b[3][@name]/c"), is("/a/b[3]/c"));
+        assertThat(expr.removeUnusedPredicates("/a/b[length(@name)=3]/c"), is("/a/b/c"));
+    }
+
+    @Test
+    public void shouldRemoveAllPredicates() {
+        assertThat(expr.removeAllPredicatesExceptIndexes("/a/b/c"), is("/a/b/c"));
+        assertThat(expr.removeAllPredicatesExceptIndexes("/a/b[0]/c"), is("/a/b[0]/c"));
+        assertThat(expr.removeAllPredicatesExceptIndexes("/a/b[1]/c"), is("/a/b[1]/c"));
+        assertThat(expr.removeAllPredicatesExceptIndexes("/a/b[10]/c"), is("/a/b[10]/c"));
+        assertThat(expr.removeAllPredicatesExceptIndexes("/a/b[100]/c"), is("/a/b[100]/c"));
+        assertThat(expr.removeAllPredicatesExceptIndexes("/a/b[1000]/c"), is("/a/b[1000]/c"));
+        assertThat(expr.removeAllPredicatesExceptIndexes("/a/b[]/c"), is("/a/b[]/c"));
+        assertThat(expr.removeAllPredicatesExceptIndexes("/a/b[*]/c"), is("/a/b[*]/c"));
+        assertThat(expr.removeAllPredicatesExceptIndexes("/a/b/c[@title]"), is("/a/b/c"));
+        // These are legal, but aren't really useful ...
+        assertThat(expr.removeAllPredicatesExceptIndexes("/a/b[1][2][3]/c"), is("/a/b[1][2][3]/c"));
+
+        assertThat(expr.removeAllPredicatesExceptIndexes("/a/b[-1]/c"), is("/a/b/c"));
+        assertThat(expr.removeAllPredicatesExceptIndexes("/a/b[@name='wacky']/c"), is("/a/b/c"));
+        assertThat(expr.removeAllPredicatesExceptIndexes("/a/b[3][@name='wacky']/c"), is("/a/b[3]/c"));
+        assertThat(expr.removeAllPredicatesExceptIndexes("/a/b[3][@name]/c"), is("/a/b[3]/c"));
+        assertThat(expr.removeAllPredicatesExceptIndexes("/a/b[length(@name)=3]/c"), is("/a/b/c"));
+    }
+
+    @Test
+    public void shouldReplaceAllXPathPatterns() {
+        assertThat(expr.replaceXPathPatterns("/a/b[3]/c"), is("/a/b\\[3\\]/c"));
+        assertThat(expr.replaceXPathPatterns("/a/b[*]/c"), is("/a/b(?:\\[\\d+\\])?/c"));
+        assertThat(expr.replaceXPathPatterns("/a/b[]/c"), is("/a/b(?:\\[\\d+\\])?/c"));
+        assertThat(expr.replaceXPathPatterns("/a/b[0]/c"), is("/a/b(?:\\[0\\])?/c"));
+        assertThat(expr.replaceXPathPatterns("/a/b[0,1,2,4]/c"), is("/a/b(?:\\[(?:1|2|4)\\])?/c"));
+        assertThat(expr.replaceXPathPatterns("/a/b[1,2,4,0]/c"), is("/a/b(?:\\[(?:1|2|4)\\])?/c"));
+        assertThat(expr.replaceXPathPatterns("/a/b[1,2,0,4]/c"), is("/a/b(?:\\[(?:1|2|4)\\])?/c"));
+        assertThat(expr.replaceXPathPatterns("/a/b[0,1,2,0,4,0]/c"), is("/a/b(?:\\[(?:1|2|4)\\])?/c"));
+        assertThat(expr.replaceXPathPatterns("/a/b[1,2,4]/c"), is("/a/b\\[(?:1|2|4)\\]/c"));
+        assertThat(expr.replaceXPathPatterns("/a/b[@param]"), is("/a/b/@param"));
+        assertThat(expr.replaceXPathPatterns("/a/b[3][@param]"), is("/a/b\\[3\\]/@param"));
+        assertThat(expr.replaceXPathPatterns("/a/b[c/d/@param]"), is("/a/b/c/d/@param"));
+
+        assertThat(expr.replaceXPathPatterns("/a/(b|c|d)/e"), is("/a/(b|c|d)/e"));
+        assertThat(expr.replaceXPathPatterns("/a/(b||c|d)/e"), is("/a/(b|c|d)/e"));
+        assertThat(expr.replaceXPathPatterns("/a/(b|||c|d)/e"), is("/a/(b|c|d)/e"));
+        assertThat(expr.replaceXPathPatterns("/a/(|b|c|d)/e"), is("/a(/(b|c|d))?/e"));
+        assertThat(expr.replaceXPathPatterns("/a/(b|c|d|)/e"), is("/a(/(b|c|d))?/e"));
+        assertThat(expr.replaceXPathPatterns("/a/(b|c|d)[]/e"), is("/a/(b|c|d)(?:\\[\\d+\\])?/e"));
+        assertThat(expr.replaceXPathPatterns("/a/(b|c[2]|d[])/e"), is("/a/(b|c\\[2\\]|d(?:\\[\\d+\\])?)/e"));
+        assertThat(expr.replaceXPathPatterns("/a/(b|c/d|e)/f"), is("/a/(b|c/d|e)/f"));
+        assertThat(expr.replaceXPathPatterns("/a/(b/c)[(d|e)/(f|g)/@something]"), is("/a/(b/c)/(d|e)/(f|g)/@something"));
+
+        assertThat(expr.replaceXPathPatterns("/a/*/f"), is("/a/[^/]*/f"));
+        assertThat(expr.replaceXPathPatterns("/a//f"), is("/a(/[^/]*)*/f"));
+        assertThat(expr.replaceXPathPatterns("/a///f"), is("/a(/[^/]*)*/f"));
+        assertThat(expr.replaceXPathPatterns("/a/////f"), is("/a(/[^/]*)*/f"));
+    }
+
+    protected void assertNotMatches( SequencerPathExpression.Matcher matcher ) {
+        assertThat(matcher, is(notNullValue()));
+        assertThat(matcher.getSelectedPath(), is(nullValue()));
+        assertThat(matcher.getOutputPath(), is(nullValue()));
+        assertThat(matcher.matches(), is(false));
+    }
+
+    protected void assertMatches( SequencerPathExpression.Matcher matcher, String selectedPath, String outputPath ) {
+        assertThat(matcher, is(notNullValue()));
+        assertThat(matcher.getSelectedPath(), is(selectedPath));
+        assertThat(matcher.getOutputPath(), is(outputPath));
+        if (selectedPath == null) {
+            assertThat(matcher.matches(), is(false));
+        } else {
+            assertThat(matcher.matches(), is(true));
+        }
+    }
+
+    @Test
+    public void shouldMatchExpressionsWithoutRegardToCase() {
+        expr = SequencerPathExpression.compile("/a/b/c/d/e[@something] => .");
+        assertMatches(expr.matcher("/a/b/c/d/e/@something"), "/a/b/c/d/e", "/a/b/c/d/e");
+        assertMatches(expr.matcher("/a/b/c/d/E/@something"), "/a/b/c/d/E", "/a/b/c/d/E");
+    }
+
+    @Test
+    public void shouldMatchExpressionsWithExactFullPath() {
+        expr = SequencerPathExpression.compile("/a/b/c/d/e[@something] => .");
+        assertMatches(expr.matcher("/a/b/c/d/e/@something"), "/a/b/c/d/e", "/a/b/c/d/e");
+        assertNotMatches(expr.matcher("/a/b/c/d/E/@something2"));
+        assertNotMatches(expr.matcher("/a/b/c/d/ex/@something"));
+        assertNotMatches(expr.matcher("/a/b[1]/c/d/e/@something"));
+    }
+
+    @Test
+    public void shouldMatchExpressionsWithExactFullPathAndExtraPathInsideMatch() {
+        expr = SequencerPathExpression.compile("/a/b/c[d/e/@something] => .");
+        assertMatches(expr.matcher("/a/b/c/d/e/@something"), "/a/b/c", "/a/b/c");
+        assertNotMatches(expr.matcher("/a/b/c/d/E/@something2"));
+        assertNotMatches(expr.matcher("/a/b/c/d/ex/@something"));
+        assertNotMatches(expr.matcher("/a/b[1]/c/d/e/@something"));
+    }
+
+    @Test
+    public void shouldMatchExpressionsWithWildcardSelection() {
+        expr = SequencerPathExpression.compile("/a/*/c[d/e/@something] => .");
+        assertMatches(expr.matcher("/a/b/c/d/e/@something"), "/a/b/c", "/a/b/c");
+        assertMatches(expr.matcher("/a/b[2]/c/d/e/@something"), "/a/b[2]/c", "/a/b[2]/c");
+        assertMatches(expr.matcher("/a/rt/c/d/e/@something"), "/a/rt/c", "/a/rt/c");
+        assertNotMatches(expr.matcher("/ac/d/e/@something"));
+    }
+
+    @Test
+    public void shouldMatchExpressionsWithSegmentWildcardSelection() {
+        expr = SequencerPathExpression.compile("/a//c[d/e/@something] => .");
+        assertMatches(expr.matcher("/a/c/d/e/@something"), "/a/c", "/a/c");
+        assertMatches(expr.matcher("/a/b/c/d/e/@something"), "/a/b/c", "/a/b/c");
+        assertMatches(expr.matcher("/a/b[2]/c/d/e/@something"), "/a/b[2]/c", "/a/b[2]/c");
+        assertMatches(expr.matcher("/a/rt/c/d/e/@something"), "/a/rt/c", "/a/rt/c");
+        assertMatches(expr.matcher("/a/r/s/t/c/d/e/@something"), "/a/r/s/t/c", "/a/r/s/t/c");
+        assertMatches(expr.matcher("/a/r[1]/s[2]/t[33]/c/d/e/@something"), "/a/r[1]/s[2]/t[33]/c", "/a/r[1]/s[2]/t[33]/c");
+        assertNotMatches(expr.matcher("/a[3]/c/d/e/@something"));
+    }
+
+    @Test
+    public void shouldMatchExpressionsWithIndexesInSelectionPaths() {
+        expr = SequencerPathExpression.compile("/a/b[2,3,4,5]/c/d/e[@something] => /x/y");
+        assertMatches(expr.matcher("/a/b[2]/c/d/e/@something"), "/a/b[2]/c/d/e", "/x/y");
+        assertMatches(expr.matcher("/a/b[3]/c/d/e/@something"), "/a/b[3]/c/d/e", "/x/y");
+        assertMatches(expr.matcher("/a/b[4]/c/d/e/@something"), "/a/b[4]/c/d/e", "/x/y");
+        assertMatches(expr.matcher("/a/b[5]/c/d/e/@something"), "/a/b[5]/c/d/e", "/x/y");
+        assertNotMatches(expr.matcher("/a/b[1]/c/d/e/@something"));
+        assertNotMatches(expr.matcher("/a/b/c/d/e/@something"));
+        assertNotMatches(expr.matcher("/a[1]/b/c/d/e/@something"));
+
+        expr = SequencerPathExpression.compile("/a/b[0,2,3,4,5]/c/d/e[@something] => /x/y");
+        assertMatches(expr.matcher("/a/b/c/d/e/@something"), "/a/b/c/d/e", "/x/y");
+        assertMatches(expr.matcher("/a/b[2]/c/d/e/@something"), "/a/b[2]/c/d/e", "/x/y");
+        assertMatches(expr.matcher("/a/b[3]/c/d/e/@something"), "/a/b[3]/c/d/e", "/x/y");
+        assertMatches(expr.matcher("/a/b[4]/c/d/e/@something"), "/a/b[4]/c/d/e", "/x/y");
+        assertMatches(expr.matcher("/a/b[5]/c/d/e/@something"), "/a/b[5]/c/d/e", "/x/y");
+        assertNotMatches(expr.matcher("/a/b[1]/c/d/e/@something"));
+        assertNotMatches(expr.matcher("/a[1]/b/c/d/e/@something"));
+    }
+
+    @Test
+    public void shouldMatchExpressionsWithAnyIndexesInSelectionPaths() {
+        expr = SequencerPathExpression.compile("/a/b[*]/c[]/d/e[@something] => /x/y");
+        assertMatches(expr.matcher("/a/b[2]/c/d/e/@something"), "/a/b[2]/c/d/e", "/x/y");
+        assertMatches(expr.matcher("/a/b[3]/c/d/e/@something"), "/a/b[3]/c/d/e", "/x/y");
+        assertMatches(expr.matcher("/a/b[4]/c/d/e/@something"), "/a/b[4]/c/d/e", "/x/y");
+        assertMatches(expr.matcher("/a/b[5]/c/d/e/@something"), "/a/b[5]/c/d/e", "/x/y");
+        assertMatches(expr.matcher("/a/b[1]/c/d/e/@something"), "/a/b[1]/c/d/e", "/x/y");
+        assertMatches(expr.matcher("/a/b[6]/c/d/e/@something"), "/a/b[6]/c/d/e", "/x/y");
+        assertMatches(expr.matcher("/a/b[6]/c[1]/d/e/@something"), "/a/b[6]/c[1]/d/e", "/x/y");
+        assertMatches(expr.matcher("/a/b/c/d/e/@something"), "/a/b/c/d/e", "/x/y");
+    }
+
+    @Test
+    public void shouldMatchExpressionsWithFullOutputPath() {
+        expr = SequencerPathExpression.compile("/a/b/c[d/e/@something] => /x/y");
+        assertMatches(expr.matcher("/a/b/c/d/e/@something"), "/a/b/c", "/x/y");
+    }
+
+    @Test
+    public void shouldMatchExpressionsWithRepositoryInSelectionPath() {
+        expr = SequencerPathExpression.compile("reposA:/a/b/c[d/e/@something] => /x/y");
+        assertMatches(expr.matcher("reposA:/a/b/c/d/e/@something"), "reposA:/a/b/c", "/x/y");
+    }
+
+    @Test
+    public void shouldMatchExpressionsWithRepositoryInFullOutputPath() {
+        expr = SequencerPathExpression.compile("/a/b/c[d/e/@something] => reposA:/x/y");
+        assertMatches(expr.matcher("/a/b/c/d/e/@something"), "/a/b/c", "reposA:/x/y");
+    }
+
+    @Test
+    public void shouldMatchExpressionsWithNamedGroupsInOutputPath() {
+        expr = SequencerPathExpression.compile("/a(//c)[d/e/@something] => $1/y/z");
+        assertMatches(expr.matcher("/a/b/c/d/e/@something"), "/a/b/c", "/b/c/y/z");
+
+        expr = SequencerPathExpression.compile("/a(/(b|c|d|)/e)[f/g/@something] => $1/y/z");
+        assertMatches(expr.matcher("/a/b/e/f/g/@something"), "/a/b/e", "/b/e/y/z");
+        assertMatches(expr.matcher("/a/c/e/f/g/@something"), "/a/c/e", "/c/e/y/z");
+        assertMatches(expr.matcher("/a/d/e/f/g/@something"), "/a/d/e", "/d/e/y/z");
+        assertMatches(expr.matcher("/a/e/f/g/@something"), "/a/e", "/e/y/z");
+        assertNotMatches(expr.matcher("/a/t/e/f/g/@something"));
+
+        expr = SequencerPathExpression.compile("/a/(b/c)[(d|e)/(f|g)/@something] => /u/$1/y/z/$2/$3");
+        assertMatches(expr.matcher("/a/b/c/d/f/@something"), "/a/b/c", "/u/b/c/y/z/d/f");
+        assertMatches(expr.matcher("/a/b/c/e/f/@something"), "/a/b/c", "/u/b/c/y/z/e/f");
+        assertMatches(expr.matcher("/a/b/c/d/g/@something"), "/a/b/c", "/u/b/c/y/z/d/g");
+        assertMatches(expr.matcher("/a/b/c/e/g/@something"), "/a/b/c", "/u/b/c/y/z/e/g");
+
+        expr = SequencerPathExpression.compile("/a/(b/c)/(d|e)/(f|g)/@something => /u/$1/y/z/$2/$3");
+        assertMatches(expr.matcher("/a/b/c/d/f/@something"), "/a/b/c/d/f", "/u/b/c/y/z/d/f");
+        assertMatches(expr.matcher("/a/b/c/e/f/@something"), "/a/b/c/e/f", "/u/b/c/y/z/e/f");
+        assertMatches(expr.matcher("/a/b/c/d/g/@something"), "/a/b/c/d/g", "/u/b/c/y/z/d/g");
+        assertMatches(expr.matcher("/a/b/c/e/g/@something"), "/a/b/c/e/g", "/u/b/c/y/z/e/g");
+    }
+
+    @Test
+    public void shouldMatchExpressionWithReoccurringNamedGroupsDollarsInOutputPath() {
+        expr = SequencerPathExpression.compile("/a/(b/c)[(d|e)/(f|g)/@something] => /u/$1/y/z/$2/$3/$1/$1");
+        assertMatches(expr.matcher("/a/b/c/d/f/@something"), "/a/b/c", "/u/b/c/y/z/d/f/b/c/b/c");
+    }
+
+    @Test
+    public void shouldMatchExpressionWithNamedGroupsAndEscapedDollarsInOutputPath() {
+        expr = SequencerPathExpression.compile("/a/(b/c)[(d|e)/(f|g)/@something] => /\\$2u/$1/y/z/$2/$3");
+        assertMatches(expr.matcher("/a/b/c/d/f/@something"), "/a/b/c", "/\\$2u/b/c/y/z/d/f");
+    }
+
+    @Test
+    public void shouldMatchExpressionWithParentReferencesInOutputPath() {
+        expr = SequencerPathExpression.compile("/a/b/c[d/e/@something] => /x/y/z/../..");
+        assertMatches(expr.matcher("/a/b/c/d/e/@something"), "/a/b/c", "/x");
+
+        expr = SequencerPathExpression.compile("/a/(b/c)[d/e/@something] => /x/$1/z/../../v");
+        assertMatches(expr.matcher("/a/b/c/d/e/@something"), "/a/b/c", "/x/b/v");
+    }
+
+    @Test
+    public void shouldMatchExpressionWithSelfReferencesInOutputPath() {
+        expr = SequencerPathExpression.compile("/a/b/c[d/e/@something] => /x/y/./z/.");
+        assertMatches(expr.matcher("/a/b/c/d/e/@something"), "/a/b/c", "/x/y/z");
+
+        expr = SequencerPathExpression.compile("/a/(b/c)[d/e/@something] => /x/$1/./z");
+        assertMatches(expr.matcher("/a/b/c/d/e/@something"), "/a/b/c", "/x/b/c/z");
+    }
+
+}

Added: trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/SequencingServiceTest.java
===================================================================
--- trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/SequencingServiceTest.java	                        (rev 0)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/SequencingServiceTest.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,331 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+
+package org.jboss.dna.repository.sequencers;
+
+import java.util.concurrent.TimeUnit;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.hamcrest.core.IsSame.sameInstance;
+import static org.junit.Assert.assertThat;
+import static org.junit.matchers.JUnitMatchers.hasItem;
+import javax.jcr.Node;
+import javax.jcr.Session;
+import javax.jcr.observation.Event;
+import org.jboss.dna.common.jcr.AbstractJcrRepositoryTest;
+import org.jboss.dna.repository.observation.ObservationService;
+import org.jboss.dna.repository.sequencers.Sequencer;
+import org.jboss.dna.repository.sequencers.SequencerConfig;
+import org.jboss.dna.repository.sequencers.SequencingService;
+import org.jboss.dna.repository.services.ServiceAdministrator;
+import org.jboss.dna.repository.util.ExecutionContext;
+import org.jboss.dna.repository.util.SimpleExecutionContext;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Randall Hauch
+ */
+public class SequencingServiceTest extends AbstractJcrRepositoryTest {
+
+    public static final int ALL_EVENT_TYPES = Event.NODE_ADDED | Event.NODE_REMOVED | Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED;
+    public static final String REPOSITORY_WORKSPACE_NAME = "testRepository-Workspace";
+
+    private ObservationService observationService;
+    private SequencingService sequencingService;
+    private ExecutionContext executionContext;
+
+    @Before
+    public void beforeEach() throws Exception {
+        this.executionContext = new SimpleExecutionContext(this, REPOSITORY_WORKSPACE_NAME);
+        this.sequencingService = new SequencingService();
+        this.sequencingService.setExecutionContext(this.executionContext);
+        this.observationService = new ObservationService(this.executionContext.getSessionFactory());
+        this.observationService.addListener(this.sequencingService);
+    }
+
+    @After
+    public void afterEach() throws Exception {
+        super.shutdownRepository();
+        this.sequencingService.getAdministrator().shutdown();
+    }
+
+    @Test
+    public void shouldHaveTheDefaultSelectorUponConstruction() {
+        assertThat(sequencingService.getSequencerSelector(), is(sameInstance(SequencingService.DEFAULT_SEQUENCER_SELECTOR)));
+    }
+
+    @Test
+    public void shouldHaveNoExecutorServiceUponConstruction() {
+        assertThat(sequencingService.getExecutorService(), is(nullValue()));
+    }
+
+    @Test
+    public void shouldCreateDefaultExecutorServiceWhenStartedIfNoExecutorServiceHasBeenSet() {
+        assertThat(sequencingService.getExecutorService(), is(nullValue()));
+        sequencingService.getAdministrator().start();
+        assertThat(sequencingService.getExecutorService(), is(notNullValue()));
+    }
+
+    @Test
+    public void shouldCreateExecutorServiceWhenStarted() {
+        assertThat(sequencingService.getExecutorService(), is(nullValue()));
+        sequencingService.getAdministrator().start();
+        assertThat(sequencingService.getExecutorService(), is(notNullValue()));
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldFailToSetStateToUnknownString() {
+        sequencingService.getAdministrator().setState("asdf");
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldFailToSetStateToNullString() {
+        sequencingService.getAdministrator().setState((String)null);
+    }
+
+    @Test
+    public void shouldSetStateUsingLowercaseString() {
+        assertThat(sequencingService.getAdministrator().setState("started").isStarted(), is(true));
+        assertThat(sequencingService.getAdministrator().setState("paused").isPaused(), is(true));
+        assertThat(sequencingService.getAdministrator().setState("shutdown").isShutdown(), is(true));
+    }
+
+    @Test
+    public void shouldSetStateUsingMixedCaseString() {
+        assertThat(sequencingService.getAdministrator().setState("StarTeD").isStarted(), is(true));
+        assertThat(sequencingService.getAdministrator().setState("PauSed").isPaused(), is(true));
+        assertThat(sequencingService.getAdministrator().setState("ShuTDowN").isShutdown(), is(true));
+    }
+
+    @Test
+    public void shouldSetStateUsingUppercasString() {
+        assertThat(sequencingService.getAdministrator().setState("STARTED").isStarted(), is(true));
+        assertThat(sequencingService.getAdministrator().setState("PAUSED").isPaused(), is(true));
+        assertThat(sequencingService.getAdministrator().setState("SHUTDOWN").isShutdown(), is(true));
+    }
+
+    @Test
+    public void shouldBePausedUponConstruction() {
+        assertThat(sequencingService.getAdministrator().isPaused(), is(true));
+        assertThat(sequencingService.getAdministrator().getState(), is(ServiceAdministrator.State.PAUSED));
+        assertThat(sequencingService.getAdministrator().isShutdown(), is(false));
+        assertThat(sequencingService.getAdministrator().isStarted(), is(false));
+    }
+
+    @Test
+    public void shouldBeAbleToShutdownWhenNotStarted() {
+        assertThat(sequencingService.getAdministrator().isShutdown(), is(false));
+        for (int i = 0; i != 3; ++i) {
+            assertThat(sequencingService.getAdministrator().shutdown().isShutdown(), is(true));
+            assertThat(sequencingService.getAdministrator().isPaused(), is(false));
+            assertThat(sequencingService.getAdministrator().isStarted(), is(false));
+            assertThat(sequencingService.getAdministrator().getState(), is(ServiceAdministrator.State.SHUTDOWN));
+        }
+    }
+
+    @Test
+    public void shouldBeAbleToBePauseAndRestarted() {
+        assertThat(sequencingService.getAdministrator().isShutdown(), is(false));
+        for (int i = 0; i != 3; ++i) {
+            // Now pause it ...
+            assertThat(sequencingService.getAdministrator().pause().isPaused(), is(true));
+            assertThat(sequencingService.getAdministrator().isStarted(), is(false));
+            assertThat(sequencingService.getAdministrator().isShutdown(), is(false));
+            assertThat(sequencingService.getAdministrator().getState(), is(ServiceAdministrator.State.PAUSED));
+
+            // Now start it back up ...
+            assertThat(sequencingService.getAdministrator().start().isStarted(), is(true));
+            assertThat(sequencingService.getAdministrator().isPaused(), is(false));
+            assertThat(sequencingService.getAdministrator().isShutdown(), is(false));
+            assertThat(sequencingService.getAdministrator().getState(), is(ServiceAdministrator.State.STARTED));
+        }
+    }
+
+    @Test( expected = IllegalStateException.class )
+    public void shouldNotBeAbleToBeRestartedAfterBeingShutdown() {
+        assertThat(sequencingService.getAdministrator().isShutdown(), is(false));
+        // Shut it down ...
+        assertThat(sequencingService.getAdministrator().shutdown().isShutdown(), is(true));
+        assertThat(sequencingService.getAdministrator().isPaused(), is(false));
+        assertThat(sequencingService.getAdministrator().isStarted(), is(false));
+        assertThat(sequencingService.getAdministrator().getState(), is(ServiceAdministrator.State.SHUTDOWN));
+
+        // Now start it back up ... this will fail
+        sequencingService.getAdministrator().start();
+    }
+
+    @Test
+    public void shouldBeAbleToMonitorWorkspaceWhenPausedOrStarted() throws Exception {
+        startRepository();
+        Session session = getRepository().login(getTestCredentials());
+
+        // Try when paused ...
+        assertThat(sequencingService.getAdministrator().isPaused(), is(true));
+        assertThat(observationService.getAdministrator().pause().isPaused(), is(true));
+        ObservationService.WorkspaceListener listener = observationService.monitor(REPOSITORY_WORKSPACE_NAME, Event.NODE_ADDED);
+        assertThat(listener, is(notNullValue()));
+        assertThat(listener.getAbsolutePath(), is("/"));
+        assertThat(listener.getEventTypes(), is(Event.NODE_ADDED));
+
+        // Cause an event ...
+        session.getRootNode().addNode("testnodeA", "nt:unstructured");
+        session.save();
+        Thread.sleep(100); // let the events be handled ...
+        assertThat(observationService.getStatistics().getNumberOfEventsIgnored(), is((long)1));
+
+        // Reset the statistics and remove the listener ...
+        sequencingService.getStatistics().reset();
+        observationService.getStatistics().reset();
+        assertThat(listener.isRegistered(), is(true));
+        listener.unregister();
+        assertThat(listener.isRegistered(), is(false));
+
+        // Start the sequencing sequencingService and try monitoring the workspace ...
+        assertThat(sequencingService.getAdministrator().start().isStarted(), is(true));
+        assertThat(observationService.getAdministrator().start().isStarted(), is(true));
+        ObservationService.WorkspaceListener listener2 = observationService.monitor(REPOSITORY_WORKSPACE_NAME, Event.NODE_ADDED);
+        assertThat(listener2.isRegistered(), is(true));
+        assertThat(listener2, is(notNullValue()));
+        assertThat(listener2.getAbsolutePath(), is("/"));
+        assertThat(listener2.getEventTypes(), is(Event.NODE_ADDED));
+
+        // Cause an event ...
+        session.getRootNode().addNode("testnodeB", "nt:unstructured");
+        session.save();
+        Thread.sleep(100); // let the events be handled ...
+
+        // Check the results: nothing ignored, and 1 node skipped (since no sequencers apply)
+        assertThat(observationService.getStatistics().getNumberOfEventsIgnored(), is((long)0));
+        assertThat(sequencingService.getStatistics().getNumberOfNodesSkipped(), is((long)1));
+
+        sequencingService.getAdministrator().shutdown();
+        sequencingService.getStatistics().reset();
+        observationService.getAdministrator().shutdown();
+        observationService.getStatistics().reset();
+
+        // Cause another event ...
+        session.getRootNode().addNode("testnodeC", "nt:unstructured");
+        session.save();
+        Thread.sleep(100); // let the events be handled ...
+        // The listener should no longer be registered ...
+        assertThat(listener2.isRegistered(), is(false));
+
+        // Check the results: nothing ignored, and nothing skipped
+        assertThat(observationService.getStatistics().getNumberOfEventsIgnored(), is((long)0));
+        assertThat(sequencingService.getStatistics().getNumberOfNodesSkipped(), is((long)0));
+    }
+
+    @Test
+    public void shouldUnregisterAllWorkspaceListenersWhenSystemIsShutdownAndNotWhenPaused() throws Exception {
+        startRepository();
+        Session session = getRepository().login(getTestCredentials());
+
+        // Start the sequencing sequencingService and try monitoring the workspace ...
+        assertThat(sequencingService.getAdministrator().start().isStarted(), is(true));
+        ObservationService.WorkspaceListener listener = observationService.monitor(REPOSITORY_WORKSPACE_NAME, Event.NODE_ADDED);
+        assertThat(listener.isRegistered(), is(true));
+        assertThat(listener, is(notNullValue()));
+        assertThat(listener.getAbsolutePath(), is("/"));
+        assertThat(listener.getEventTypes(), is(Event.NODE_ADDED));
+
+        // Cause an event ...
+        session.getRootNode().addNode("testnodeB", "nt:unstructured");
+        session.save();
+        assertThat(listener.isRegistered(), is(true));
+
+        // Pause the sequencingService, can cause an event ...
+        sequencingService.getAdministrator().pause();
+        session.getRootNode().addNode("testnodeB", "nt:unstructured");
+        session.save();
+        assertThat(listener.isRegistered(), is(true));
+
+        // Shut down the services and await termination ...
+        sequencingService.getAdministrator().shutdown();
+        observationService.getAdministrator().shutdown();
+        sequencingService.getAdministrator().awaitTermination(2, TimeUnit.SECONDS);
+        observationService.getAdministrator().awaitTermination(2, TimeUnit.SECONDS);
+
+        // Cause another event ...
+        session.getRootNode().addNode("testnodeC", "nt:unstructured");
+        session.save();
+
+        // The listener should no longer be registered ...
+        assertThat(listener.isRegistered(), is(false));
+    }
+
+    @Test
+    public void shouldExecuteSequencersUponChangesToRepositoryThatMatchSequencerPathExpressions() throws Exception {
+        // Add configurations for a sequencer ...
+        String name = "MockSequencerA";
+        String desc = "A mock sequencer that accumulates the number of times it's called";
+        String classname = MockSequencerA.class.getName();
+        String[] classpath = null;
+        String[] pathExpressions = {"/testnodeC/testnodeD/@description => ."};
+        SequencerConfig configA = new SequencerConfig(name, desc, classname, classpath, pathExpressions);
+        sequencingService.addSequencer(configA);
+
+        // Start the repository and get a session ...
+        startRepository();
+        Session session = getRepository().login(getTestCredentials());
+
+        // Start the sequencing sequencingService and try monitoring the workspace ...
+        assertThat(sequencingService.getAdministrator().start().isStarted(), is(true));
+        ObservationService.WorkspaceListener listener = observationService.monitor(REPOSITORY_WORKSPACE_NAME, ALL_EVENT_TYPES);
+        assertThat(listener.isRegistered(), is(true));
+        assertThat(listener, is(notNullValue()));
+        assertThat(listener.getAbsolutePath(), is("/"));
+        assertThat(listener.getEventTypes(), is(ALL_EVENT_TYPES));
+
+        // The sequencer should not yet have run ...
+        MockSequencerA sequencerA = (MockSequencerA)sequencingService.getSequencerLibrary().getInstances().get(0);
+        assertThat(sequencerA, is(notNullValue()));
+        assertThat(sequencerA.getCounter(), is(0));
+        assertThat(sequencingService.getSequencerLibrary().getInstances(), hasItem((Sequencer)sequencerA));
+
+        // Cause an event, but not one that the sequencer cares about ...
+        Node nodeC = session.getRootNode().addNode("testnodeC", "nt:unstructured");
+        assertThat(nodeC, is(notNullValue()));
+        session.save();
+        assertThat(sequencerA.getCounter(), is(0));
+        assertThat(sequencingService.getSequencerLibrary().getInstances(), hasItem((Sequencer)sequencerA));
+
+        // Cause another event, but again one that the sequencer does not care about ...
+        Node nodeD = nodeC.addNode("testnodeD", "nt:unstructured");
+        assertThat(nodeD, is(notNullValue()));
+        session.save();
+        assertThat(sequencerA.getCounter(), is(0));
+        assertThat(sequencingService.getSequencerLibrary().getInstances(), hasItem((Sequencer)sequencerA));
+
+        // Now set the property that the sequencer DOES care about ...
+        sequencerA.setExpectedCount(1);
+        nodeD.setProperty("description", "This is the value");
+        session.save();
+
+        // Wait for the event to be processed and the sequencer to be called ...
+        sequencerA.awaitExecution(4, TimeUnit.SECONDS); // wait for the sequencer to be called
+        assertThat(sequencerA.getCounter(), is(1));
+        assertThat(sequencingService.getSequencerLibrary().getInstances(), hasItem((Sequencer)sequencerA));
+    }
+}

Added: trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/StreamSequencerAdapterTest.java
===================================================================
--- trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/StreamSequencerAdapterTest.java	                        (rev 0)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/repository/sequencers/StreamSequencerAdapterTest.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,336 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.sequencers;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.hamcrest.core.IsSame.sameInstance;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import javax.jcr.Node;
+import javax.jcr.Session;
+import javax.jcr.observation.Event;
+import org.jboss.dna.common.jcr.AbstractJcrRepositoryTest;
+import org.jboss.dna.common.monitor.ProgressMonitor;
+import org.jboss.dna.common.monitor.RecordingProgressMonitor;
+import org.jboss.dna.repository.observation.NodeChange;
+import org.jboss.dna.repository.sequencers.SequencerConfig;
+import org.jboss.dna.repository.sequencers.SequencerException;
+import org.jboss.dna.repository.sequencers.SequencerOutputMap;
+import org.jboss.dna.repository.sequencers.StreamSequencerAdapter;
+import org.jboss.dna.repository.util.ExecutionContext;
+import org.jboss.dna.repository.util.JcrTools;
+import org.jboss.dna.repository.util.RepositoryNodePath;
+import org.jboss.dna.repository.util.SessionFactory;
+import org.jboss.dna.spi.sequencers.SequencerOutput;
+import org.jboss.dna.spi.sequencers.StreamSequencer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Randall Hauch
+ */
+public class StreamSequencerAdapterTest extends AbstractJcrRepositoryTest {
+
+    private StreamSequencer streamSequencer;
+    private StreamSequencerAdapter sequencer;
+    private String[] validExpressions = {"/a/* => /output"};
+    private SequencerConfig validConfig = new SequencerConfig("name", "desc", "something.class", null, validExpressions);
+    private JcrTools tools;
+    private Session session;
+    private SequencerOutputMap sequencerOutput;
+    private String sampleData = "The little brown fox didn't something bad.";
+    private ExecutionContext context;
+    private RecordingProgressMonitor progressMonitor;
+    private String repositoryWorkspaceName = "something";
+
+    @Before
+    public void beforeEach() throws Exception {
+        final JcrTools tools = new JcrTools();
+        this.tools = tools;
+        sequencerOutput = new SequencerOutputMap();
+        progressMonitor = new RecordingProgressMonitor(StreamSequencerAdapterTest.class.getName());
+        final SessionFactory sessionFactory = new SessionFactory() {
+
+            public Session createSession( String name ) {
+                return createTestSession();
+            }
+        };
+        context = new ExecutionContext() {
+
+            public SessionFactory getSessionFactory() {
+                return sessionFactory;
+            }
+
+            public JcrTools getTools() {
+                return tools;
+            }
+        };
+        final SequencerOutputMap finalOutput = sequencerOutput;
+        streamSequencer = new StreamSequencer() {
+
+            /**
+             * This method always copies the {@link StreamSequencerAdapterTest#sequencerOutput} data into the output {@inheritDoc},
+             * and does nothing else with any of the other parameters.
+             */
+            public void sequence( InputStream stream, SequencerOutput output, ProgressMonitor progressMonitor ) {
+                for (SequencerOutputMap.Entry entry : finalOutput) {
+                    String nodePath = entry.getPath().getString();
+                    for (SequencerOutputMap.PropertyValue property : entry.getPropertyValues()) {
+                        output.setProperty(nodePath, property.getName(), property.getValue());
+                    }
+                }
+            }
+        };
+        sequencer = new StreamSequencerAdapter(streamSequencer);
+    }
+
+    @After
+    public void afterEach() throws Exception {
+        if (session != null) {
+            try {
+                session.logout();
+            } finally {
+                session = null;
+            }
+        }
+    }
+
+    protected Session createTestSession() {
+        try {
+            return getRepository().login(getTestCredentials());
+        } catch (Exception e) {
+            fail("Unable to create repository session: " + e.getMessage());
+            return null; // won't get here
+        }
+    }
+
+    @Test
+    public void shouldNotHaveSequencerUponInstantiation() {
+        assertThat(sequencer.getConfiguration(), is(nullValue()));
+        sequencer.setConfiguration(validConfig);
+        assertThat(sequencer.getConfiguration(), is(sameInstance(validConfig)));
+    }
+
+    @Test
+    public void shouldExtractNullMixinTypesFromNullValue() {
+        assertThat(sequencer.extractMixinTypes(null), is(nullValue()));
+    }
+
+    @Test
+    public void shouldExtractMixinTypesFromStringValue() {
+        assertThat(sequencer.extractMixinTypes("value"), is(new String[] {"value"}));
+        assertThat(sequencer.extractMixinTypes(""), is(new String[] {""}));
+    }
+
+    @Test
+    public void shouldExtractMixinTypesFromStringArrayValue() {
+        assertThat(sequencer.extractMixinTypes(new String[] {"value1"}), is(new String[] {"value1"}));
+        assertThat(sequencer.extractMixinTypes(new String[] {"value1", "value2"}), is(new String[] {"value1", "value2"}));
+    }
+
+    @Test
+    public void shouldExtractMixinTypesFromStringArrayWithNullValue() {
+        assertThat(sequencer.extractMixinTypes(new String[] {"value1", null, "value2"}), is(new String[] {"value1", null, "value2"}));
+    }
+
+    @Test
+    public void shouldExecuteSequencerOnExistingNodeAndOutputToExistingNode() throws Exception {
+        startRepository();
+        session = getRepository().login(getTestCredentials());
+
+        // Set up the repository for the test ...
+        Node nodeC = tools.findOrCreateNode(session, "/a/b/c");
+        Node nodeE = tools.findOrCreateNode(session, "/d/e");
+        assertThat(nodeC, is(notNullValue()));
+        assertThat(nodeE, is(notNullValue()));
+        assertThat(nodeE.getNodes().getSize(), is(0l));
+        assertThat(nodeE.getProperties().getSize(), is(1l)); // jcr:primaryType
+        assertThat(nodeE.getProperty("jcr:primaryType").getString(), is("nt:unstructured"));
+
+        // Set the property that will be sequenced ...
+        nodeC.setProperty("sequencedProperty", new ByteArrayInputStream(sampleData.getBytes()));
+
+        // Set up the node changes ...
+        NodeChange nodeChange = new NodeChange(repositoryWorkspaceName, nodeC.getPath(), Event.PROPERTY_CHANGED, Collections.singleton("sequencedProperty"), null);
+
+        // Set up the output directory ...
+        Set<RepositoryNodePath> outputPaths = new HashSet<RepositoryNodePath>();
+        outputPaths.add(new RepositoryNodePath(repositoryWorkspaceName, nodeE.getPath()));
+
+        // Generate the output data that the sequencer subclass will produce and that should be saved to the repository ...
+        sequencerOutput.setProperty("alpha/beta", "isSomething", true);
+
+        // Call the sequencer ...
+        sequencer.execute(nodeC, "sequencedProperty", nodeChange, outputPaths, context, progressMonitor);
+    }
+
+    @Test( expected = SequencerException.class )
+    public void shouldExecuteSequencerOnExistingNodeWithMissingSequencedPropertyAndOutputToExistingNode() throws Exception {
+        startRepository();
+        session = getRepository().login(getTestCredentials());
+
+        // Set up the repository for the test ...
+        Node nodeC = tools.findOrCreateNode(session, "/a/b/c");
+        Node nodeE = tools.findOrCreateNode(session, "/d/e");
+        assertThat(nodeC, is(notNullValue()));
+        assertThat(nodeE, is(notNullValue()));
+        assertThat(nodeE.getNodes().getSize(), is(0l));
+        assertThat(nodeE.getProperties().getSize(), is(1l)); // jcr:primaryType
+        assertThat(nodeE.getProperty("jcr:primaryType").getString(), is("nt:unstructured"));
+
+        // Set the property that will be sequenced ...
+        // THIS TEST REQUIRES THIS PROPERTY TO BE NULL OR NON-EXISTANT
+        nodeC.setProperty("sequencedProperty", (InputStream)null);
+
+        // Set up the node changes ...
+        NodeChange nodeChange = new NodeChange(repositoryWorkspaceName, nodeC.getPath(), Event.PROPERTY_CHANGED, Collections.singleton("sequencedProperty"), null);
+
+        // Set up the output directory ...
+        Set<RepositoryNodePath> outputPaths = new HashSet<RepositoryNodePath>();
+        outputPaths.add(new RepositoryNodePath(repositoryWorkspaceName, nodeE.getPath()));
+
+        // Generate the output data that the sequencer subclass will produce and that should be saved to the repository ...
+        sequencerOutput.setProperty("alpha/beta", "isSomething", true);
+
+        // Call the sequencer, which should cause the exception ...
+        sequencer.execute(nodeC, "sequencedProperty", nodeChange, outputPaths, context, progressMonitor);
+    }
+
+    @Test
+    public void shouldExecuteSequencerOnExistingNodeAndOutputToMultipleExistingNodes() throws Exception {
+        startRepository();
+        session = getRepository().login(getTestCredentials());
+
+        // Set up the repository for the test ...
+        Node nodeC = tools.findOrCreateNode(session, "/a/b/c");
+        Node nodeE = tools.findOrCreateNode(session, "/d/e");
+        assertThat(nodeC, is(notNullValue()));
+        assertThat(nodeE, is(notNullValue()));
+        assertThat(nodeE.getNodes().getSize(), is(0l));
+        assertThat(nodeE.getProperties().getSize(), is(1l)); // jcr:primaryType
+        assertThat(nodeE.getProperty("jcr:primaryType").getString(), is("nt:unstructured"));
+
+        // Set the property that will be sequenced ...
+        nodeC.setProperty("sequencedProperty", new ByteArrayInputStream(sampleData.getBytes()));
+
+        // Set up the node changes ...
+        NodeChange nodeChange = new NodeChange(repositoryWorkspaceName, nodeC.getPath(), Event.PROPERTY_CHANGED, Collections.singleton("sequencedProperty"), null);
+
+        // Set up the output directory ...
+        Set<RepositoryNodePath> outputPaths = new HashSet<RepositoryNodePath>();
+        outputPaths.add(new RepositoryNodePath(repositoryWorkspaceName, "/d/e"));
+        outputPaths.add(new RepositoryNodePath(repositoryWorkspaceName, "/x/y/z"));
+
+        // Generate the output data that the sequencer subclass will produce and that should be saved to the repository ...
+        sequencerOutput.setProperty("alpha/beta", "isSomething", true);
+
+        // Call the sequencer ...
+        sequencer.execute(nodeC, "sequencedProperty", nodeChange, outputPaths, context, progressMonitor);
+
+        // Check to see that the output nodes have been created ...
+        assertThat(session.getRootNode().hasNode("d/e"), is(true));
+        assertThat(session.getRootNode().hasNode("x/y/z"), is(true));
+    }
+
+    @Test
+    public void shouldExecuteSequencerOnExistingNodeAndOutputToNonExistingNode() throws Exception {
+        startRepository();
+        session = getRepository().login(getTestCredentials());
+
+        // Set up the repository for the test ...
+        Node nodeC = tools.findOrCreateNode(session, "/a/b/c");
+        assertThat(session.getRootNode().hasNode("d"), is(false));
+        assertThat(nodeC, is(notNullValue()));
+
+        // Set the property that will be sequenced ...
+        nodeC.setProperty("sequencedProperty", new ByteArrayInputStream(sampleData.getBytes()));
+
+        // Set up the node changes ...
+        NodeChange nodeChange = new NodeChange(repositoryWorkspaceName, nodeC.getPath(), Event.PROPERTY_CHANGED, Collections.singleton("sequencedProperty"), null);
+
+        // Set up the output directory ...
+        Set<RepositoryNodePath> outputPaths = new HashSet<RepositoryNodePath>();
+        outputPaths.add(new RepositoryNodePath(repositoryWorkspaceName, "/d/e"));
+
+        // Generate the output data that the sequencer subclass will produce and that should be saved to the repository ...
+        sequencerOutput.setProperty("alpha/beta", "isSomething", true);
+
+        // Call the sequencer ...
+        sequencer.execute(nodeC, "sequencedProperty", nodeChange, outputPaths, context, progressMonitor);
+
+        // Check to see that the "/d/e" node has been created ...
+        assertThat(session.getRootNode().hasNode("d/e"), is(true));
+    }
+
+    @Test
+    public void shouldExecuteSequencerOnExistingNodeAndOutputToMultipleNonExistingNodes() throws Exception {
+        startRepository();
+        session = getRepository().login(getTestCredentials());
+
+        // Set up the repository for the test ...
+        Node nodeC = tools.findOrCreateNode(session, "/a/b/c");
+        assertThat(session.getRootNode().hasNode("d"), is(false));
+        assertThat(session.getRootNode().hasNode("x"), is(false));
+        assertThat(nodeC, is(notNullValue()));
+
+        // Set the property that will be sequenced ...
+        nodeC.setProperty("sequencedProperty", new ByteArrayInputStream(sampleData.getBytes()));
+
+        // Set up the node changes ...
+        NodeChange nodeChange = new NodeChange(repositoryWorkspaceName, nodeC.getPath(), Event.PROPERTY_CHANGED, Collections.singleton("sequencedProperty"), null);
+
+        // Set up the output directory ...
+        Set<RepositoryNodePath> outputPaths = new HashSet<RepositoryNodePath>();
+        outputPaths.add(new RepositoryNodePath(repositoryWorkspaceName, "/d/e"));
+        outputPaths.add(new RepositoryNodePath(repositoryWorkspaceName, "/x/y/z"));
+        outputPaths.add(new RepositoryNodePath(repositoryWorkspaceName, "/x/z"));
+
+        // Generate the output data that the sequencer subclass will produce and that should be saved to the repository ...
+        sequencerOutput.setProperty("alpha/beta", "isSomething", true);
+
+        // Call the sequencer ...
+        sequencer.execute(nodeC, "sequencedProperty", nodeChange, outputPaths, context, progressMonitor);
+
+        // Check to see that the output nodes have been created ...
+        assertThat(session.getRootNode().hasNode("d/e"), is(true));
+        assertThat(session.getRootNode().hasNode("x/y/z"), is(true));
+        assertThat(session.getRootNode().hasNode("x/z"), is(true));
+
+        // Check to see that the sequencer-generated nodes have been created ...
+        // Node beta = session.getRootNode().getNode("d/e/alpha/beta");
+        // for (PropertyIterator iter = beta.getProperties(); iter.hasNext();) {
+        // Property property = iter.nextProperty();
+        // System.out.println("Property on " + beta.getPath() + " ===> " + property.getName() + " = " + property.getValue());
+        // }
+        assertThat(session.getRootNode().getNode("d/e/alpha/beta").getProperty("isSomething").getBoolean(), is(true));
+        assertThat(session.getRootNode().getNode("x/y/z/alpha/beta").getProperty("isSomething").getBoolean(), is(true));
+        assertThat(session.getRootNode().getNode("x/z/alpha/beta").getProperty("isSomething").getBoolean(), is(true));
+    }
+}

Added: trunk/dna-repository/src/test/java/org/jboss/dna/repository/util/JndiSessionFactoryTest.java
===================================================================
--- trunk/dna-repository/src/test/java/org/jboss/dna/repository/util/JndiSessionFactoryTest.java	                        (rev 0)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/repository/util/JndiSessionFactoryTest.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,168 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.util;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.hamcrest.core.IsSame.sameInstance;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import javax.jcr.Repository;
+import javax.jcr.Session;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import org.jboss.dna.common.SystemFailureException;
+import org.jboss.dna.common.jcr.AbstractJcrRepositoryTest;
+import org.jboss.dna.common.naming.MockInitialContext;
+import org.jboss.dna.repository.util.JndiSessionFactory;
+import org.jmock.Mockery;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Randall Hauch
+ */
+public class JndiSessionFactoryTest extends AbstractJcrRepositoryTest {
+
+    public static final String MOCK_REPOSITORY_NAME = "java:jcr/unit/test/repository/1";
+    public static final String MOCK_REPOSITORY_NAME_ALT = "java:jcr/unit/test/repository/2";
+
+    private JndiSessionFactory factory;
+    private Mockery context;
+    private Repository mockRepository;
+    private Session session;
+
+    @Before
+    public void beforeEach() throws Exception {
+        this.context = new Mockery();
+        this.mockRepository = this.context.mock(Repository.class);
+        MockInitialContext.register(MOCK_REPOSITORY_NAME, this.mockRepository);
+        this.factory = new JndiSessionFactory();
+    }
+
+    @After
+    public void afterEach() throws Exception {
+        if (session != null) {
+            session.logout();
+        }
+        MockInitialContext.tearDown();
+    }
+
+    protected void assertNotRegistered( String name ) {
+        try {
+            new InitialContext().lookup(name);
+            fail("Unexpectedly found registered object");
+        } catch (NamingException e) {
+            // expected ...
+        }
+    }
+
+    protected void assertRegistered( String name, Object obj ) {
+        try {
+            assertThat(new InitialContext().lookup(name), is(sameInstance(obj)));
+        } catch (NamingException e) {
+            fail("Failed to find registered object \"" + name + "\"");
+        }
+    }
+
+    @Test
+    public void shouldCreateSessionForRegisteredRepository() {
+
+    }
+
+    @Test( expected = SystemFailureException.class )
+    public void shouldThrowSystemFailureWhenUnableToFindRegisteredRepository() throws Exception {
+        factory.createSession(MOCK_REPOSITORY_NAME + "something_extra_that_can't_be_found");
+    }
+
+    @Test
+    public void shouldFindWorkspaceInRegisteredName() {
+        assertThat(factory.getWorkspaceName("java:jcr/path/to/repository/workspaceName"), is("workspaceName"));
+    }
+
+    @Test
+    public void shouldReturnNullWorkspaceIfRegisteredNameEndsInDelimiter() {
+        assertThat(factory.getWorkspaceName("java:jcr/path/to/repository/"), is(nullValue()));
+    }
+
+    @Test
+    public void shouldReturnNullWorkspaceIfRegisteredNameHasNoDelimiter() {
+        assertThat(factory.getWorkspaceName("java:jcr"), is(nullValue()));
+    }
+
+    @Test
+    public void shouldFindRepositoryInRegisteredName() {
+        assertThat(factory.getRepositoryName("java:jcr/path/to/repository/workspaceName"), is("java:jcr/path/to/repository"));
+    }
+
+    @Test
+    public void shouldConsiderWholeRegisteredNameToBeRepositoryNameIfRegisteredNameEndsInDelimiter() {
+        assertThat(factory.getRepositoryName("java:jcr/path/to/repository/"), is("java:jcr/path/to/repository"));
+    }
+
+    @Test
+    public void shouldConsiderWholeRegisteredNameToBeRepositoryNameIfRegisteredNameHasNoDelimiter() {
+        assertThat(factory.getRepositoryName("java:jcr"), is("java:jcr"));
+    }
+
+    @Test
+    public void shouldRegisterSuppliedRepositoryInJndi() throws Exception {
+        assertNotRegistered(MOCK_REPOSITORY_NAME_ALT);
+        factory.registerRepository(MOCK_REPOSITORY_NAME_ALT, mockRepository);
+        assertRegistered(MOCK_REPOSITORY_NAME_ALT, mockRepository);
+    }
+
+    @Test
+    public void shouldUnregisterRepositoryInJndiIfNullRepositoryReference() throws Exception {
+        assertRegistered(MOCK_REPOSITORY_NAME, mockRepository);
+        factory.registerRepository(MOCK_REPOSITORY_NAME, null);
+        assertNotRegistered(MOCK_REPOSITORY_NAME);
+    }
+
+    @Test
+    public void shouldRemoveAllTrailingDelimitersWhenRegisteringRepository() throws Exception {
+        assertNotRegistered("java:jcr/unit/test/repository");
+        factory.registerRepository("java:jcr/unit/test/repository///", mockRepository);
+        assertRegistered("java:jcr/unit/test/repository", mockRepository);
+    }
+
+    @Test
+    public void shouldCreateAnonymousSessionInRepositoryIfNoCredentialsAreRegisterd() throws Exception {
+        Repository repository = getRepository();
+        factory.registerRepository("java:jcr/unit/test/repository/", repository);
+        session = factory.createSession("java:jcr/unit/test/repository/default");
+        assertThat(session, is(notNullValue()));
+        assertThat(session.getUserID(), is("anonymous")); // as defined in the Jackrabbit configuration file
+    }
+
+    @Test
+    public void shouldCreateNonAnonymousSessionInRepositoryIfCredentialsAreRegistered() throws Exception {
+        Repository repository = getRepository();
+        factory.registerRepository("java:jcr/unit/test/repository/", repository);
+        factory.registerCredentials("java:jcr/unit/test/repository/default", "jsmith", "secret".toCharArray());
+        session = factory.createSession("java:jcr/unit/test/repository/default");
+        assertThat(session, is(notNullValue()));
+        assertThat(session.getUserID(), is("jsmith")); // as defined in the Jackrabbit configuration file
+    }
+}


Property changes on: trunk/dna-repository/src/test/java/org/jboss/dna/repository/util/JndiSessionFactoryTest.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Added: trunk/dna-repository/src/test/java/org/jboss/dna/repository/util/SimpleExecutionContext.java
===================================================================
--- trunk/dna-repository/src/test/java/org/jboss/dna/repository/util/SimpleExecutionContext.java	                        (rev 0)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/repository/util/SimpleExecutionContext.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,67 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.repository.util;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import java.io.IOException;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import org.jboss.dna.common.SystemFailureException;
+import org.jboss.dna.common.jcr.AbstractJcrRepositoryTest;
+import org.jboss.dna.repository.util.ExecutionContext;
+import org.jboss.dna.repository.util.JcrTools;
+import org.jboss.dna.repository.util.SessionFactory;
+
+/**
+ * @author Randall Hauch
+ */
+public class SimpleExecutionContext implements ExecutionContext {
+
+    private JcrTools tools = new JcrTools();
+    private SessionFactory sessionFactory;
+
+    public SimpleExecutionContext( final AbstractJcrRepositoryTest test, final String repositoryName ) {
+        this.sessionFactory = new SessionFactory() {
+
+            public Session createSession( String name ) throws RepositoryException {
+                assertThat(name, is(repositoryName));
+                try {
+                    return test.getRepository().login(test.getTestCredentials());
+                } catch (IOException e) {
+                    throw new SystemFailureException(e);
+                }
+            }
+        };
+    }
+
+    public SessionFactory getSessionFactory() {
+        return sessionFactory;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public JcrTools getTools() {
+        return tools;
+    }
+}


Property changes on: trunk/dna-repository/src/test/java/org/jboss/dna/repository/util/SimpleExecutionContext.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Deleted: trunk/dna-repository/src/test/java/org/jboss/dna/services/JndiSessionFactoryTest.java
===================================================================
--- trunk/dna-services/src/test/java/org/jboss/dna/services/JndiSessionFactoryTest.java	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/services/JndiSessionFactoryTest.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -1,167 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source.
- * Copyright 2008, Red Hat Middleware LLC, and individual contributors
- * as indicated by the @author tags. See the copyright.txt file in the
- * distribution for a full listing of individual contributors. 
- *
- * This is free software; you can redistribute it and/or modify it
- * under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.jboss.dna.services;
-
-import static org.hamcrest.core.Is.is;
-import static org.hamcrest.core.IsNull.notNullValue;
-import static org.hamcrest.core.IsNull.nullValue;
-import static org.hamcrest.core.IsSame.sameInstance;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assert.fail;
-import javax.jcr.Repository;
-import javax.jcr.Session;
-import javax.naming.InitialContext;
-import javax.naming.NamingException;
-import org.jboss.dna.common.SystemFailureException;
-import org.jboss.dna.common.jcr.AbstractJcrRepositoryTest;
-import org.jboss.dna.common.naming.MockInitialContext;
-import org.jmock.Mockery;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * @author Randall Hauch
- */
-public class JndiSessionFactoryTest extends AbstractJcrRepositoryTest {
-
-    public static final String MOCK_REPOSITORY_NAME = "java:jcr/unit/test/repository/1";
-    public static final String MOCK_REPOSITORY_NAME_ALT = "java:jcr/unit/test/repository/2";
-
-    private JndiSessionFactory factory;
-    private Mockery context;
-    private Repository mockRepository;
-    private Session session;
-
-    @Before
-    public void beforeEach() throws Exception {
-        this.context = new Mockery();
-        this.mockRepository = this.context.mock(Repository.class);
-        MockInitialContext.register(MOCK_REPOSITORY_NAME, this.mockRepository);
-        this.factory = new JndiSessionFactory();
-    }
-
-    @After
-    public void afterEach() throws Exception {
-        if (session != null) {
-            session.logout();
-        }
-        MockInitialContext.tearDown();
-    }
-
-    protected void assertNotRegistered( String name ) {
-        try {
-            new InitialContext().lookup(name);
-            fail("Unexpectedly found registered object");
-        } catch (NamingException e) {
-            // expected ...
-        }
-    }
-
-    protected void assertRegistered( String name, Object obj ) {
-        try {
-            assertThat(new InitialContext().lookup(name), is(sameInstance(obj)));
-        } catch (NamingException e) {
-            fail("Failed to find registered object \"" + name + "\"");
-        }
-    }
-
-    @Test
-    public void shouldCreateSessionForRegisteredRepository() {
-
-    }
-
-    @Test( expected = SystemFailureException.class )
-    public void shouldThrowSystemFailureWhenUnableToFindRegisteredRepository() throws Exception {
-        factory.createSession(MOCK_REPOSITORY_NAME + "something_extra_that_can't_be_found");
-    }
-
-    @Test
-    public void shouldFindWorkspaceInRegisteredName() {
-        assertThat(factory.getWorkspaceName("java:jcr/path/to/repository/workspaceName"), is("workspaceName"));
-    }
-
-    @Test
-    public void shouldReturnNullWorkspaceIfRegisteredNameEndsInDelimiter() {
-        assertThat(factory.getWorkspaceName("java:jcr/path/to/repository/"), is(nullValue()));
-    }
-
-    @Test
-    public void shouldReturnNullWorkspaceIfRegisteredNameHasNoDelimiter() {
-        assertThat(factory.getWorkspaceName("java:jcr"), is(nullValue()));
-    }
-
-    @Test
-    public void shouldFindRepositoryInRegisteredName() {
-        assertThat(factory.getRepositoryName("java:jcr/path/to/repository/workspaceName"), is("java:jcr/path/to/repository"));
-    }
-
-    @Test
-    public void shouldConsiderWholeRegisteredNameToBeRepositoryNameIfRegisteredNameEndsInDelimiter() {
-        assertThat(factory.getRepositoryName("java:jcr/path/to/repository/"), is("java:jcr/path/to/repository"));
-    }
-
-    @Test
-    public void shouldConsiderWholeRegisteredNameToBeRepositoryNameIfRegisteredNameHasNoDelimiter() {
-        assertThat(factory.getRepositoryName("java:jcr"), is("java:jcr"));
-    }
-
-    @Test
-    public void shouldRegisterSuppliedRepositoryInJndi() throws Exception {
-        assertNotRegistered(MOCK_REPOSITORY_NAME_ALT);
-        factory.registerRepository(MOCK_REPOSITORY_NAME_ALT, mockRepository);
-        assertRegistered(MOCK_REPOSITORY_NAME_ALT, mockRepository);
-    }
-
-    @Test
-    public void shouldUnregisterRepositoryInJndiIfNullRepositoryReference() throws Exception {
-        assertRegistered(MOCK_REPOSITORY_NAME, mockRepository);
-        factory.registerRepository(MOCK_REPOSITORY_NAME, null);
-        assertNotRegistered(MOCK_REPOSITORY_NAME);
-    }
-
-    @Test
-    public void shouldRemoveAllTrailingDelimitersWhenRegisteringRepository() throws Exception {
-        assertNotRegistered("java:jcr/unit/test/repository");
-        factory.registerRepository("java:jcr/unit/test/repository///", mockRepository);
-        assertRegistered("java:jcr/unit/test/repository", mockRepository);
-    }
-
-    @Test
-    public void shouldCreateAnonymousSessionInRepositoryIfNoCredentialsAreRegisterd() throws Exception {
-        Repository repository = getRepository();
-        factory.registerRepository("java:jcr/unit/test/repository/", repository);
-        session = factory.createSession("java:jcr/unit/test/repository/default");
-        assertThat(session, is(notNullValue()));
-        assertThat(session.getUserID(), is("anonymous")); // as defined in the Jackrabbit configuration file
-    }
-
-    @Test
-    public void shouldCreateNonAnonymousSessionInRepositoryIfCredentialsAreRegistered() throws Exception {
-        Repository repository = getRepository();
-        factory.registerRepository("java:jcr/unit/test/repository/", repository);
-        factory.registerCredentials("java:jcr/unit/test/repository/default", "jsmith", "secret".toCharArray());
-        session = factory.createSession("java:jcr/unit/test/repository/default");
-        assertThat(session, is(notNullValue()));
-        assertThat(session.getUserID(), is("jsmith")); // as defined in the Jackrabbit configuration file
-    }
-}

Deleted: trunk/dna-repository/src/test/java/org/jboss/dna/services/SimpleExecutionContext.java
===================================================================
--- trunk/dna-services/src/test/java/org/jboss/dna/services/SimpleExecutionContext.java	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/services/SimpleExecutionContext.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -1,65 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source.
- * Copyright 2008, Red Hat Middleware LLC, and individual contributors
- * as indicated by the @author tags. See the copyright.txt file in the
- * distribution for a full listing of individual contributors. 
- *
- * This is free software; you can redistribute it and/or modify it
- * under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.jboss.dna.services;
-
-import static org.hamcrest.core.Is.is;
-import static org.junit.Assert.assertThat;
-import java.io.IOException;
-import javax.jcr.RepositoryException;
-import javax.jcr.Session;
-import org.jboss.dna.common.SystemFailureException;
-import org.jboss.dna.common.jcr.AbstractJcrRepositoryTest;
-import org.jboss.dna.services.util.JcrTools;
-
-/**
- * @author Randall Hauch
- */
-public class SimpleExecutionContext implements ExecutionContext {
-
-    private JcrTools tools = new JcrTools();
-    private SessionFactory sessionFactory;
-
-    public SimpleExecutionContext( final AbstractJcrRepositoryTest test, final String repositoryName ) {
-        this.sessionFactory = new SessionFactory() {
-
-            public Session createSession( String name ) throws RepositoryException {
-                assertThat(name, is(repositoryName));
-                try {
-                    return test.getRepository().login(test.getTestCredentials());
-                } catch (IOException e) {
-                    throw new SystemFailureException(e);
-                }
-            }
-        };
-    }
-
-    public SessionFactory getSessionFactory() {
-        return sessionFactory;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    public JcrTools getTools() {
-        return tools;
-    }
-}

Modified: trunk/dna-repository/src/test/resources/log4j.properties
===================================================================
--- trunk/dna-services/src/test/resources/log4j.properties	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/dna-repository/src/test/resources/log4j.properties	2008-04-22 02:47:53 UTC (rev 96)
@@ -9,7 +9,7 @@
 
 # Set up the default logging to be INFO level, then override specific units
 log4j.logger.org.jboss.dna=INFO
-log4j.logger.org.jboss.dna.services.sequencers=TRACE
+log4j.logger.org.jboss.dna.repository.sequencers=TRACE
 
 # Jackrabbit logging
 log4j.logger.org.apache.jackrabbit=WARN, stdout

Modified: trunk/dna-repository/src/test/resources/rule_test.dslr
===================================================================
--- trunk/dna-services/src/test/resources/rule_test.dslr	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/dna-repository/src/test/resources/rule_test.dslr	2008-04-22 02:47:53 UTC (rev 96)
@@ -1,5 +1,5 @@
 #created on: Jan 4, 2008
-package org.jboss.dna.services.rules
+package org.jboss.dna.repository.rules
 
 #list any import classes here.
 


Property changes on: trunk/dna-spi
___________________________________________________________________
Name: svn:ignore
   + target


Added: trunk/dna-spi/.classpath
===================================================================
--- trunk/dna-spi/.classpath	                        (rev 0)
+++ trunk/dna-spi/.classpath	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src/main/java"/>
+	<classpathentry kind="src" path="src/main/resources"/>
+	<classpathentry kind="src" output="target/test-classes" path="src/test/java"/>
+	<classpathentry kind="src" output="target/test-classes" path="src/test/resources"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry kind="con" path="org.maven.ide.eclipse.MAVEN2_CLASSPATH_CONTAINER"/>
+	<classpathentry kind="output" path="target/classes"/>
+</classpath>

Added: trunk/dna-spi/.project
===================================================================
--- trunk/dna-spi/.project	                        (rev 0)
+++ trunk/dna-spi/.project	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>dna-spi</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.maven.ide.eclipse.maven2Builder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+		<nature>org.maven.ide.eclipse.maven2Nature</nature>
+	</natures>
+</projectDescription>

Added: trunk/dna-spi/pom.xml
===================================================================
--- trunk/dna-spi/pom.xml	                        (rev 0)
+++ trunk/dna-spi/pom.xml	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?><project>
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.jboss.dna</groupId>
+    <artifactId>dna</artifactId>
+    <version>0.1-SNAPSHOT</version>
+  </parent>
+  <!-- The groupId and version values are inherited from parent -->
+  <artifactId>dna-spi</artifactId>
+  <description>The JBoss DNA Service Provider Interface (SPI) used to create extensions</description>
+  <packaging>jar</packaging>
+  <name>JBoss DNA Service Provider Interface (SPI)</name>
+  <url>http://labs.jboss.org/dna</url>
+  <!--
+  Define the dependencies.  Note that all version and scopes default to those 
+  defined in the dependencyManagement section of the parent pom.
+  -->
+  <dependencies>
+    <dependency>
+      <groupId>org.jboss.dna</groupId>
+      <artifactId>dna-common</artifactId>
+      <version>0.1-SNAPSHOT</version>
+    </dependency>
+    <dependency>
+      <groupId>org.jboss.dna</groupId>
+      <artifactId>dna-common</artifactId>
+      <version>0.1-SNAPSHOT</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <!-- 
+    Testing (note the scope)
+    -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.jmock</groupId>
+      <artifactId>jmock</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.jmock</groupId>
+      <artifactId>jmock-junit4</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <!-- 
+    Logging (require SLF4J API for compiling, but use Log4J and its SLF4J binding for testing) 
+    -->
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-log4j12</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>log4j</groupId>
+      <artifactId>log4j</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <!-- 
+    Java Concurrency in Practice annotations
+    -->
+    <dependency>
+      <groupId>net.jcip</groupId>
+      <artifactId>jcip-annotations</artifactId>
+    </dependency>
+  </dependencies>
+</project>

Added: trunk/dna-spi/src/main/java/org/jboss/dna/spi/sequencers/SequencerOutput.java
===================================================================
--- trunk/dna-spi/src/main/java/org/jboss/dna/spi/sequencers/SequencerOutput.java	                        (rev 0)
+++ trunk/dna-spi/src/main/java/org/jboss/dna/spi/sequencers/SequencerOutput.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,68 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.spi.sequencers;
+
+import java.io.InputStream;
+import java.util.Calendar;
+
+/**
+ * Interface for sequencers to use to generate their output.
+ *
+ * @author Randall Hauch
+ */
+public interface SequencerOutput {
+
+	/**
+	 * Set the supplied property on the supplied node.
+	 * <p>
+	 * The allowable values are any of the following:
+	 * <ul>
+	 * <li>primitives (which will be autoboxed)</li>
+	 * <li>{@link String} instances</li>
+	 * <li>{@link String} arrays</li>
+	 * <li>byte arrays</li>
+	 * <li>{@link InputStream} instances</li>
+	 * <li>{@link Calendar} instances</li>
+	 * <li></li>
+	 * </ul>
+	 * </p>
+	 *
+	 * @param nodePath the path to the node containing the property; may not be null
+	 * @param property the name of the property to be set
+	 * @param values the value(s) for the property; may be empty if any existing property is to be removed
+	 */
+	void setProperty( String nodePath,
+	                  String property,
+	                  Object... values );
+
+	/**
+	 * Set the supplied reference on the supplied node.
+	 *
+	 * @param nodePath the path to the node containing the property; may not be null
+	 * @param property the name of the property to be set
+	 * @param paths the paths to the referenced property, which may be absolute paths or relative to the sequencer output node;
+	 *        may be empty if any existing property is to be removed
+	 */
+	void setReference( String nodePath,
+	                   String property,
+	                   String... paths );
+}

Added: trunk/dna-spi/src/main/java/org/jboss/dna/spi/sequencers/StreamSequencer.java
===================================================================
--- trunk/dna-spi/src/main/java/org/jboss/dna/spi/sequencers/StreamSequencer.java	                        (rev 0)
+++ trunk/dna-spi/src/main/java/org/jboss/dna/spi/sequencers/StreamSequencer.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,68 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.spi.sequencers;
+
+import java.io.InputStream;
+import org.jboss.dna.common.monitor.ProgressMonitor;
+
+/**
+ * The interface for a DNA sequencer that processes a property as a stream to extract information from the content and store in
+ * the repository.
+ * <p>
+ * Implementations must provide a no-argument constructor.
+ * </p>
+ * @author Randall Hauch
+ */
+public interface StreamSequencer {
+
+    /**
+     * Sequence the data found in the supplied stream, placing the output information into the supplied map.
+     * <p>
+     * JBoss DNA's SequencingService determines the sequencers that should be executed by monitoring the changes to one or more
+     * workspaces that it is monitoring. Changes in those workspaces are aggregated and used to determine which sequencers should
+     * be called. If the sequencer implements this interface, then this method is called with the property that is to be sequenced
+     * along with the interface used to register the output. The framework takes care of all the rest.
+     * </p>
+     * <p>
+     * This operation should report progress to the supplied {@link ProgressMonitor}. At the beginning of the operation, call
+     * {@link ProgressMonitor#beginTask(double, org.jboss.dna.common.i18n.I18n, Object...)} with a meaningful message describing
+     * the operation and a total for the amount of work that will be done by this sequencer. Then perform the sequencing work,
+     * periodically reporting work by specifying the {@link ProgressMonitor#worked(double) amount of work} that has was just
+     * completed or by {@link ProgressMonitor#createSubtask(double) creating a subtask} and reporting work against that subtask
+     * monitor.
+     * </p>
+     * <p>
+     * The implementation should also periodically check whether the operation has been
+     * {@link ProgressMonitor#isCancelled() cancelled}. If this method returns true, the implementation should abort all work as
+     * soon as possible and close any resources that were acquired or opened.
+     * </p>
+     * <p>
+     * Finally, the implementation should call {@link ProgressMonitor#done()} when the operation has finished.
+     * </p>
+     * @param stream the stream with the data to be sequenced; never null
+     * @param output the output from the sequencing operation; never null
+     * @param progressMonitor the progress monitor that should be kept updated with the sequencer's progress and that should be
+     * frequently consulted as to whether this operation has been {@link ProgressMonitor#isCancelled() cancelled}.
+     */
+    void sequence( InputStream stream, SequencerOutput output, ProgressMonitor progressMonitor );
+
+}

Added: trunk/dna-spi/src/test/resources/log4j.properties
===================================================================
--- trunk/dna-spi/src/test/resources/log4j.properties	                        (rev 0)
+++ trunk/dna-spi/src/test/resources/log4j.properties	2008-04-22 02:47:53 UTC (rev 96)
@@ -0,0 +1,16 @@
+# Direct log messages to stdout
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.Target=System.out
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %m%n
+
+# Root logger option
+log4j.rootLogger=INFO, stdout
+
+# Set up the default logging to be INFO level, then override specific units
+log4j.logger.org.jboss.dna=INFO
+
+# Jackrabbit logging
+log4j.logger.org.apache.jackrabbit=WARN, stdout
+log4j.logger.org.apache.derby=INFO, stdout
+

Modified: trunk/pom.xml
===================================================================
--- trunk/pom.xml	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/pom.xml	2008-04-22 02:47:53 UTC (rev 96)
@@ -11,7 +11,7 @@
 	<modules>
 		<module>dna-common</module>
 		<module>dna-maven-classloader</module>
-		<module>dna-services</module>
+		<module>dna-repository</module>
 		<module>sequencers/dna-sequencer-images</module>
 		<module>dna-integration-tests</module>
 	</modules>
@@ -98,9 +98,14 @@
 			</dependency>
 			<dependency>
 				<groupId>org.jboss.dna</groupId>
-				<artifactId>dna-services</artifactId>
+				<artifactId>dna-spi</artifactId>
 				<version>0.1-SNAPSHOT</version>
 			</dependency>
+			<dependency>
+				<groupId>org.jboss.dna</groupId>
+				<artifactId>dna-repository</artifactId>
+				<version>0.1-SNAPSHOT</version>
+			</dependency>
 			<!-- 
 		    Rules
 		    -->

Modified: trunk/sequencers/dna-sequencer-images/pom.xml
===================================================================
--- trunk/sequencers/dna-sequencer-images/pom.xml	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/sequencers/dna-sequencer-images/pom.xml	2008-04-22 02:47:53 UTC (rev 96)
@@ -33,7 +33,7 @@
     </dependency>
     <dependency>
       <groupId>org.jboss.dna</groupId>
-      <artifactId>dna-services</artifactId>
+      <artifactId>dna-spi</artifactId>
     </dependency>
     <!-- 
     Testing (note the scope)

Modified: trunk/sequencers/dna-sequencer-images/src/main/java/org/jboss/dna/sequencer/images/ImageMetadataSequencer.java
===================================================================
--- trunk/sequencers/dna-sequencer-images/src/main/java/org/jboss/dna/sequencer/images/ImageMetadataSequencer.java	2008-04-22 01:42:33 UTC (rev 95)
+++ trunk/sequencers/dna-sequencer-images/src/main/java/org/jboss/dna/sequencer/images/ImageMetadataSequencer.java	2008-04-22 02:47:53 UTC (rev 96)
@@ -23,9 +23,8 @@
 
 import java.io.InputStream;
 import org.jboss.dna.common.monitor.ProgressMonitor;
-import org.jboss.dna.services.ExecutionContext;
-import org.jboss.dna.services.sequencers.SequencerOutput;
-import org.jboss.dna.services.sequencers.StreamSequencer;
+import org.jboss.dna.spi.sequencers.SequencerOutput;
+import org.jboss.dna.spi.sequencers.StreamSequencer;
 
 /**
  * A sequencer that processes the binary content of an image file, extracts the metadata for the image, and then writes that image
@@ -62,7 +61,7 @@
  * </p>
  * @author Randall Hauch
  */
-public class ImageMetadataSequencer extends StreamSequencer {
+public class ImageMetadataSequencer implements StreamSequencer {
 
     public static final String METADATA_NODE = "image:metadata";
     public static final String IMAGE_PRIMARY_TYPE = "jcr:primaryType";
@@ -83,8 +82,7 @@
     /**
      * {@inheritDoc}
      */
-    @Override
-    protected void sequence( InputStream stream, SequencerOutput output, ExecutionContext context, ProgressMonitor progressMonitor ) {
+    public void sequence( InputStream stream, SequencerOutput output, ProgressMonitor progressMonitor ) {
         ImageMetadata metadata = new ImageMetadata();
         metadata.setInput(stream);
         metadata.setDetermineImageNumber(true);




More information about the dna-commits mailing list