You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@curator.apache.org by ra...@apache.org on 2017/07/08 03:32:34 UTC

[2/6] curator git commit: Squashed commit of the following:

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/typed/TypedZPath7.java
----------------------------------------------------------------------
diff --git a/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/typed/TypedZPath7.java b/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/typed/TypedZPath7.java
new file mode 100644
index 0000000..a6eb4d3
--- /dev/null
+++ b/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/typed/TypedZPath7.java
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.curator.x.async.modeled.typed;
+
+import org.apache.curator.x.async.modeled.ZPath;
+
+/**
+ * Same as {@link org.apache.curator.x.async.modeled.typed.TypedZPath}, but with 7 parameters
+ */
+@FunctionalInterface
+public interface TypedZPath7<T1, T2, T3, T4, T5, T6, T7>
+{
+    ZPath resolved(T1 p1, T2 p2, T3 p3, T4 p4, T5 p5, T6 p6, T7 p7);
+
+    /**
+     * Return a TypedZPath using {@link org.apache.curator.x.async.modeled.ZPath#parseWithIds}
+     *
+     * @param pathWithIds path to pass to {@link org.apache.curator.x.async.modeled.ZPath#parseWithIds}
+     * @return TypedZPath
+     */
+    static <T1, T2, T3, T4, T5, T6, T7> TypedZPath7<T1, T2, T3, T4, T5, T6, T7> from(String pathWithIds)
+    {
+        return from(ZPath.parseWithIds(pathWithIds));
+    }
+
+    /**
+     * Return a TypedZPath
+     *
+     * @param path path to use
+     * @return TypedZPath
+     */
+    static <T1, T2, T3, T4, T5, T6, T7> TypedZPath7<T1, T2, T3, T4, T5, T6, T7> from(ZPath path)
+    {
+        return (p1, p2, p3, p4, p5, p6, p7) -> path.resolved(p1, p2, p3, p4, p5, p6, p7);
+    }
+}

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/typed/TypedZPath8.java
----------------------------------------------------------------------
diff --git a/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/typed/TypedZPath8.java b/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/typed/TypedZPath8.java
new file mode 100644
index 0000000..68086b5
--- /dev/null
+++ b/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/typed/TypedZPath8.java
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.curator.x.async.modeled.typed;
+
+import org.apache.curator.x.async.modeled.ZPath;
+
+/**
+ * Same as {@link org.apache.curator.x.async.modeled.typed.TypedZPath}, but with 8 parameters
+ */
+@FunctionalInterface
+public interface TypedZPath8<T1, T2, T3, T4, T5, T6, T7, T8>
+{
+    ZPath resolved(T1 p1, T2 p2, T3 p3, T4 p4, T5 p5, T6 p6, T7 p7, T8 p8);
+
+    /**
+     * Return a TypedZPath using {@link org.apache.curator.x.async.modeled.ZPath#parseWithIds}
+     *
+     * @param pathWithIds path to pass to {@link org.apache.curator.x.async.modeled.ZPath#parseWithIds}
+     * @return TypedZPath
+     */
+    static <T1, T2, T3, T4, T5, T6, T7, T8> TypedZPath8<T1, T2, T3, T4, T5, T6, T7, T8> from(String pathWithIds)
+    {
+        return from(ZPath.parseWithIds(pathWithIds));
+    }
+
+    /**
+     * Return a TypedZPath
+     *
+     * @param path path to use
+     * @return TypedZPath
+     */
+    static <T1, T2, T3, T4, T5, T6, T7, T8> TypedZPath8<T1, T2, T3, T4, T5, T6, T7, T8> from(ZPath path)
+    {
+        return (p1, p2, p3, p4, p5, p6, p7, p8) -> path.resolved(p1, p2, p3, p4, p5, p6, p7, p8);
+    }
+}

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/typed/TypedZPath9.java
----------------------------------------------------------------------
diff --git a/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/typed/TypedZPath9.java b/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/typed/TypedZPath9.java
new file mode 100644
index 0000000..e03c1f1
--- /dev/null
+++ b/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/typed/TypedZPath9.java
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.curator.x.async.modeled.typed;
+
+import org.apache.curator.x.async.modeled.ZPath;
+
+/**
+ * Same as {@link org.apache.curator.x.async.modeled.typed.TypedZPath}, but with 9 parameters
+ */
+@FunctionalInterface
+public interface TypedZPath9<T1, T2, T3, T4, T5, T6, T7, T8, T9>
+{
+    ZPath resolved(T1 p1, T2 p2, T3 p3, T4 p4, T5 p5, T6 p6, T7 p7, T8 p8, T9 p9);
+
+    /**
+     * Return a TypedZPath using {@link org.apache.curator.x.async.modeled.ZPath#parseWithIds}
+     *
+     * @param pathWithIds path to pass to {@link org.apache.curator.x.async.modeled.ZPath#parseWithIds}
+     * @return TypedZPath
+     */
+    static <T1, T2, T3, T4, T5, T6, T7, T8, T9> TypedZPath9<T1, T2, T3, T4, T5, T6, T7, T8, T9> from(String pathWithIds)
+    {
+        return from(ZPath.parseWithIds(pathWithIds));
+    }
+
+    /**
+     * Return a TypedZPath
+     *
+     * @param path path to use
+     * @return TypedZPath
+     */
+    static <T1, T2, T3, T4, T5, T6, T7, T8, T9> TypedZPath9<T1, T2, T3, T4, T5, T6, T7, T8, T9> from(ZPath path)
+    {
+        return (p1, p2, p3, p4, p5, p6, p7, p8, p9) -> path.resolved(p1, p2, p3, p4, p5, p6, p7, p8, p9);
+    }
+}

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/versioned/Versioned.java
----------------------------------------------------------------------
diff --git a/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/versioned/Versioned.java b/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/versioned/Versioned.java
new file mode 100644
index 0000000..0bd723b
--- /dev/null
+++ b/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/versioned/Versioned.java
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.curator.x.async.modeled.versioned;
+
+/**
+ * A container for a model instance and a version. Can be used with the
+ * {@link org.apache.curator.x.async.modeled.ModeledFramework#versioned()} APIs
+ */
+@FunctionalInterface
+public interface Versioned<T>
+{
+    /**
+     * Returns the contained model
+     *
+     * @return model
+     */
+    T model();
+
+    /**
+     * Returns the version of the model when it was read
+     *
+     * @return version
+     */
+    default int version()
+    {
+        return -1;
+    }
+
+    /**
+     * Return a new Versioned wrapper for the given model and version
+     *
+     * @param model model
+     * @param version version
+     * @return new Versioned wrapper
+     */
+    static <T> Versioned<T> from(T model, int version)
+    {
+        return new Versioned<T>()
+        {
+            @Override
+            public int version()
+            {
+                return version;
+            }
+
+            @Override
+            public T model()
+            {
+                return model;
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/versioned/VersionedModeledFramework.java
----------------------------------------------------------------------
diff --git a/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/versioned/VersionedModeledFramework.java b/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/versioned/VersionedModeledFramework.java
new file mode 100644
index 0000000..c725fd3
--- /dev/null
+++ b/curator-x-async/src/main/java/org/apache/curator/x/async/modeled/versioned/VersionedModeledFramework.java
@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.curator.x.async.modeled.versioned;
+
+import org.apache.curator.framework.api.transaction.CuratorOp;
+import org.apache.curator.x.async.AsyncStage;
+import org.apache.zookeeper.data.Stat;
+
+public interface VersionedModeledFramework<T>
+{
+    /**
+     * @see org.apache.curator.x.async.modeled.ModeledFramework#set(Object)
+     */
+    AsyncStage<String> set(Versioned<T> model);
+
+    /**
+     * @see org.apache.curator.x.async.modeled.ModeledFramework#set(Object, org.apache.zookeeper.data.Stat)
+     */
+    AsyncStage<String> set(Versioned<T> model, Stat storingStatIn);
+
+    /**
+     * @see org.apache.curator.x.async.modeled.ModeledFramework#read()
+     */
+    AsyncStage<Versioned<T>> read();
+
+    /**
+     * @see org.apache.curator.x.async.modeled.ModeledFramework#read(org.apache.zookeeper.data.Stat)
+     */
+    AsyncStage<Versioned<T>> read(Stat storingStatIn);
+
+    /**
+     * @see org.apache.curator.x.async.modeled.ModeledFramework#updateOp(Object)
+     */
+    AsyncStage<Stat> update(Versioned<T> model);
+
+    /**
+     * @see org.apache.curator.x.async.modeled.ModeledFramework#updateOp(Object)
+     */
+    CuratorOp updateOp(Versioned<T> model);
+}

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/site/confluence/async.confluence
----------------------------------------------------------------------
diff --git a/curator-x-async/src/site/confluence/async.confluence b/curator-x-async/src/site/confluence/async.confluence
new file mode 100644
index 0000000..619f5c8
--- /dev/null
+++ b/curator-x-async/src/site/confluence/async.confluence
@@ -0,0 +1,212 @@
+h1. Curator Async
+
+With this DSL you can do asynchronous tasks in a more natural, functional way using
+[Java 8 lambdas|https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html]. For example:
+
+{code}
+// let "client" be a CuratorFramework instance
+AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
+async.checkExists().forPath(somePath).thenAccept(stat -> mySuccessOperation(stat));
+{code}
+
+h2. Usage
+
+Note: To use Curator Async, you should be familiar with Java 8's lambdas, CompletedFuture and CompletionStage.
+
+Create a [[CuratorFramework|../curator\-framework/index.html]] instance in the normal way. You then wrap this instance using
+AsyncCuratorFramework. i.e.
+
+{code}
+// let "client" be a CuratorFramework instance
+AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
+{code}
+
+AsyncCuratorFramework has most of the same builder methods that CuratorFramework does with some important
+differences:
+
+* AsyncCuratorFramework builders return {{AsyncStage}} instances
+* AsyncCuratorFramework builders have no checked exceptions
+* Many of the builder methods have been simplified/clarified
+* All builders invoke the asynchronous versions of ZooKeeper APIs
+* Watchers also use CompletionStages \- see below for details
+
+h4. AsyncStage
+
+AsyncStage instances extend Java 8's CompletionStage. CompletionStage objects can be "completed" with a success
+value or an exception. The parameterized type of the AsyncStage will
+be whatever the builder used would naturally return as a success value. E.g. the async getData() builder's AsyncStage is
+parameterized with "byte\[\]".
+
+h4. Watchers
+
+ZooKeeper watchers also get the CompletionStage treatment in Curator Async. To add a watcher, call
+watched() prior to starting the appropriate builders. E.g.
+
+{code}
+async.watched().getData().forPath(path) ...
+{code}
+
+Thus, a data watcher will be set on the specified path. You access the CompletionStage for the watcher
+by using the event() method of AsyncStage. Here is a complete example:
+
+{code}
+async.watched().getData().forPath(path).event().thenAccept(watchedEvent -> watchWasTriggered(watchedEvent));
+{code}
+
+ZooKeeper calls watchers when there is a connection loss. This can make using the CompletionStage
+somewhat complicated (see AsyncEventException below). If you are not interested in watcher connection
+problems, you can tell Curator Async to not send them by calling:
+
+{code}
+// only complete the CompletionStage when the watcher is successfully triggered
+// i.e. don't complete on connection issues
+async.with(WatchMode.successOnly).watched()...
+{code}
+
+h4. AsyncEventException
+
+When an async watcher fails the exception set in the CompletionStage will be of type {{AsyncEventException}}.
+This exception allows you to see the KeeperState that caused the trigger and allows you to reset the
+completion stage. Reset is needed because ZooKeeper temporarily triggers watchers when there is a connection
+event (unless {{WatchMode.successOnly}} is used). However, the watcher stays set for the original operation. Use {{AsyncEventException#reset}}
+to start a new completion stage that will wait on the next trigger of the watcher.
+
+E.g.
+
+{code}
+AsyncStage stage = ...
+stage.event().exceptionally(e -> {
+    AsyncEventException asyncEx = (AsyncEventException)e;
+
+    ... note a connection problem ...
+
+    asyncEx.reset().thenAccept(watchedEvent -> watchWasTriggered(watchedEvent));
+});
+{code}
+
+h4. AsyncResult
+
+As a convenience, you can use {{AsyncResult}} to combine ZooKeeper method value, the ZooKeeper result
+code and any exception in one object allowing you to not worry about exceptional completions. i.e. the {{CompletionStage}}
+returned by {{AsyncResult.of()}} always completes successfully with an AsyncResult object.
+
+AsyncResult has methods to get either the method result (a path, Stat, etc.), a KeeperException code
+or a general exception:
+
+{code}
+Optional<T> getValue();
+
+KeeperException.Code getCode();
+
+Optional<Throwable> getException();
+{code}
+
+Use AsyncResult by wrapping an {{AsyncStage}} value. i.e.
+
+{code}
+CompletionStage<AsyncResult<Stat>> resultStage = AsyncResult.of(async.checkExists().forPath(path));
+resultStage.thenAccept(result -> {
+    if ( result.getValue().isPresent() ) {
+        // ...
+    } else if ( result.getCode() == KeeperException.Code.NOAUTH ) {
+        // ...
+    }
+    // etc.
+});
+{code}
+
+h2. Examples
+
+h4. Create a sequential ZNode
+
+Create a sequential ZNode and, once successfully completed, set a watcher
+on the ZNode. Note: this code does not deal with errors. Should a connection
+problem occur or another exception occur, the completion lambda will never be called.
+
+{code}
+async.create().withMode(PERSISTENT_SEQUENTIAL).forPath(path).thenAccept(actualPath ->
+    async.watched().getData().forPath(actualPath).thenApply(() -> watchTriggered()));
+{code}
+
+----
+
+h4. AsyncStage canonical usage
+
+This is the canonical way to deal with AsyncStage. Use the handle() method which provides
+both the success value and the exception. The exception will be non\-null on error.
+
+{code}
+async.create().withOptions(EnumSet.of(doProtected)).forPath(path).handle((actualPath, exception) -> {
+    if ( exception != null )
+    {
+        // handle problem
+    }
+    else
+    {
+        // actualPath is the path created
+    }
+    return null;
+});
+{code}
+
+----
+
+h4. Simplified usage via AsyncResult
+
+{code}
+AsyncResult.of(async.create().withOptions(EnumSet.of(doProtected)).forPath(path)).thenAccept(result -> {
+    if ( result.getRawValue() != null )
+    {
+        // result.getRawValue() is the path created
+    }
+    else
+    {
+        // ...
+    }
+});
+{code}
+
+----
+
+h4. Using executors
+
+Your completion routines can operate in a separate thread if you provide an executor.
+
+{code}
+async.create().withOptions(EnumSet.of(createParentsIfNeeded)).forPath("/a/b/c")
+    .thenAcceptAsync(path -> handleCreate(path), executor);
+{code}
+
+----
+
+h4. Separate handlers
+
+This example shows specifying separate completion handlers for success and exception.
+
+{code}
+AsyncStage<byte[]> stage = async.getData().forPath("/my/path");
+stage.exceptionally(e -> {
+    if ( e instanceof KeeperException.NoNodeException )
+    {
+        // handle no node
+    }
+    else
+    {
+        // handle other
+    }
+    return null;
+});
+stage.thenAccept(data -> processData(data));
+{code}
+
+----
+
+h4. Synchronous usage
+
+CompletionStage provides a blocking method as well so that you can block to get the result
+of an operation. i.e. this makes it possible to use the async APIs in a synchronous way.
+
+{code}
+// NOTE: get() specifies a checked exception
+async.create().forPath("/foo").toCompletableFuture().get();
+{code}

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/site/confluence/index.confluence
----------------------------------------------------------------------
diff --git a/curator-x-async/src/site/confluence/index.confluence b/curator-x-async/src/site/confluence/index.confluence
index 34b6c36..4d81d44 100644
--- a/curator-x-async/src/site/confluence/index.confluence
+++ b/curator-x-async/src/site/confluence/index.confluence
@@ -4,7 +4,7 @@ h2. Packaging
 
 Curator Async is in its own package in Maven Central: curator\-x\-async
 
-h2. What Is a Curator Async?
+h2. What Is Curator Async?
 
 Curator Async is a [DSL|https://en.wikipedia.org/wiki/Domain-specific_language] that wraps existing
 {{CuratorFramework}} instances. This DSL is entirely asynchronous and uses
@@ -13,212 +13,39 @@ mechanism for chaining, composing, etc. Additionally, Curator's original DSL has
 and simplified, in particular for operations such as {{create()}}.
 
 With this DSL you can do asynchronous tasks in a more natural, functional way using
-[Java 8 lambdas|https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html]. For example:
-
-{code}
-// let "client" be a CuratorFramework instance
-AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
-async.checkExists().forPath(somePath).thenAccept(stat -> mySuccessOperation(stat));
-{code}
+[Java 8 lambdas|https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html].
 
-h2. Usage
+The Curator Async package also contains a strongly typed DSL and strongly typed Cache Recipe wrappers that
+allows you to map a ZooKeeper path to a serializable class as opposed to raw byte arrays.
 
-Note: To use Curator Async, you should be familiar with Java 8's lambdas, CompletedFuture and CompletionStage.
+h2. [[Curator Async|async.html]]
 
-Create a [[CuratorFramework|../curator\-framework/index.html]] instance in the normal way. You then wrap this instance using
-AsyncCuratorFramework. i.e.
+With this DSL you can do asynchronous tasks in a more natural, functional way using
+[Java 8 lambdas|https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html]. For example:
 
 {code}
 // let "client" be a CuratorFramework instance
 AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
+async.checkExists().forPath(somePath).thenAccept(stat -> mySuccessOperation(stat));
 {code}
 
-AsyncCuratorFramework has most of the same builder methods that CuratorFramework does with some important
-differences:
-
-* AsyncCuratorFramework builders return {{AsyncStage}} instances
-* AsyncCuratorFramework builders have no checked exceptions
-* Many of the builder methods have been simplified/clarified
-* All builders invoke the asynchronous versions of ZooKeeper APIs
-* Watchers also use CompletionStages \- see below for details
-
-h4. AsyncStage
-
-AsyncStage instances extend Java 8's CompletionStage. CompletionStage objects can be "completed" with a success
-value or an exception. The parameterized type of the AsyncStage will
-be whatever the builder used would naturally return as a success value. E.g. the async getData() builder's AsyncStage is
-parameterized with "byte\[\]".
-
-h4. Watchers
-
-ZooKeeper watchers also get the CompletionStage treatment in Curator Async. To add a watcher, call
-watched() prior to starting the appropriate builders. E.g.
-
-{code}
-async.watched().getData().forPath(path) ...
-{code}
-
-Thus, a data watcher will be set on the specified path. You access the CompletionStage for the watcher
-by using the event() method of AsyncStage. Here is a complete example:
-
-{code}
-async.watched().getData().forPath(path).event().thenAccept(watchedEvent -> watchWasTriggered(watchedEvent));
-{code}
-
-ZooKeeper calls watchers when there is a connection loss. This can make using the CompletionStage
-somewhat complicated (see AsyncEventException below). If you are not interested in watcher connection
-problems, you can tell Curator Async to not send them by calling:
-
-{code}
-// only complete the CompletionStage when the watcher is successfully triggered
-// i.e. don't complete on connection issues
-async.with(WatchMode.successOnly).watched()...
-{code}
-
-h4. AsyncEventException
-
-When an async watcher fails the exception set in the CompletionStage will be of type {{AsyncEventException}}.
-This exception allows you to see the KeeperState that caused the trigger and allows you to reset the
-completion stage. Reset is needed because ZooKeeper temporarily triggers watchers when there is a connection
-event (unless {{WatchMode.successOnly}} is used). However, the watcher stays set for the original operation. Use {{AsyncEventException#reset}}
-to start a new completion stage that will wait on the next trigger of the watcher.
-
-E.g.
-
-{code}
-AsyncStage stage = ...
-stage.event().exceptionally(e -> {
-    AsyncEventException asyncEx = (AsyncEventException)e;
-
-    ... note a connection problem ...
-
-    asyncEx.reset().thenAccept(watchedEvent -> watchWasTriggered(watchedEvent));
-});
-{code}
-
-h4. AsyncResult
-
-As a convenience, you can use {{AsyncResult}} to combine ZooKeeper method value, the ZooKeeper result
-code and any exception in one object allowing you to not worry about exceptional completions. i.e. the {{CompletionStage}}
-returned by {{AsyncResult.of()}} always completes successfully with an AsyncResult object.
-
-AsyncResult has methods to get either the method result (a path, Stat, etc.), a KeeperException code
-or a general exception:
-
-{code}
-Optional<T> getValue();
-
-KeeperException.Code getCode();
-
-Optional<Throwable> getException();
-{code}
-
-Use AsyncResult by wrapping an {{AsyncStage}} value. i.e.
-
-{code}
-CompletionStage<AsyncResult<Stat>> resultStage = AsyncResult.of(async.checkExists().forPath(path));
-resultStage.thenAccept(result -> {
-    if ( result.getValue().isPresent() ) {
-        // ...
-    } else if ( result.getCode() == KeeperException.Code.NOAUTH ) {
-        // ...
-    }
-    // etc.
-});
-{code}
-
-h2. Examples
-
-h4. Create a sequential ZNode
-
-Create a sequential ZNode and, once successfully completed, set a watcher
-on the ZNode. Note: this code does not deal with errors. Should a connection
-problem occur or another exception occur, the completion lambda will never be called.
-
-{code}
-async.create().withMode(PERSISTENT_SEQUENTIAL).forPath(path).thenAccept(actualPath ->
-    async.watched().getData().forPath(actualPath).thenApply(() -> watchTriggered()));
-{code}
-
-----
-
-h4. AsyncStage canonical usage
-
-This is the canonical way to deal with AsyncStage. Use the handle() method which provides
-both the success value and the exception. The exception will be non\-null on error.
-
-{code}
-async.create().withOptions(EnumSet.of(doProtected)).forPath(path).handle((actualPath, exception) -> {
-    if ( exception != null )
-    {
-        // handle problem
-    }
-    else
-    {
-        // actualPath is the path created
-    }
-    return null;
-});
-{code}
-
-----
-
-h4. Simplified usage via AsyncResult
-
-{code}
-AsyncResult.of(async.create().withOptions(EnumSet.of(doProtected)).forPath(path)).thenAccept(result -> {
-    if ( result.getRawValue() != null )
-    {
-        // result.getRawValue() is the path created
-    }
-    else
-    {
-        // ...
-    }
-});
-{code}
-
-----
+See [[Curator Async|async.html]] for details.
 
-h4. Using executors
+h2. [[Modeled Curator|modeled.html]]
 
-Your completion routines can operate in a separate thread if you provide an executor.
+This is a strongly typed DSL that allows you to map a Curator\-style client to:
 
-{code}
-async.create().withOptions(EnumSet.of(createParentsIfNeeded)).forPath("/a/b/c")
-    .thenAcceptAsync(path -> handleCreate(path), executor);
-{code}
-
-----
+* A ZooKeeper path (supporting parameterized substitutions)
+* A serializer for the data stored at the path
+* Options for how nodes should be created (sequential, compressed data, ttl, etc.)
+* ACLs for the nodes at the path
+* Options for how to delete nodes (guaranteed, deleting children, etc.)
 
-h4. Separate handlers
-
-This example shows specifying separate completion handlers for success and exception.
+For example:
 
 {code}
-AsyncStage<byte[]> stage = async.getData().forPath("/my/path");
-stage.exceptionally(e -> {
-    if ( e instanceof KeeperException.NoNodeException )
-    {
-        // handle no node
-    }
-    else
-    {
-        // handle other
-    }
-    return null;
-});
-stage.thenAccept(data -> processData(data));
+ModeledFramework<Foo> modeled = ModeledFramework.wrap(client, fooModelSpec);
+modeled.set(new Foo());
 {code}
 
-----
-
-h4. Synchronous usage
-
-CompletionStage provides a blocking method as well so that you can block to get the result
-of an operation. i.e. this makes it possible to use the async APIs in a synchronous way.
-
-{code}
-// NOTE: get() specifies a checked exception
-async.create().forPath("/foo").toCompletableFuture().get();
-{code}
+See [[Modeled Curator|modeled.html]] for details.

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/site/confluence/modeled-components.confluence
----------------------------------------------------------------------
diff --git a/curator-x-async/src/site/confluence/modeled-components.confluence b/curator-x-async/src/site/confluence/modeled-components.confluence
new file mode 100644
index 0000000..62bcdeb
--- /dev/null
+++ b/curator-x-async/src/site/confluence/modeled-components.confluence
@@ -0,0 +1,186 @@
+h1. Modeled Curator \- Components
+
+Modeled Curator components are intended to allow you to model your ZooKeeper usage early in your application
+so that the majority of the code that interacts with ZooKeeper doesn't need to be concerned with
+paths, byte arrays, ACLs, options, etc. The [[Pub\-Sub Example|https://github.com/apache/curator/tree/master/curator-examples/src/main/java/pubsub]]
+can give you some ideas on how to accomplish this.
+
+h2. ZPath
+
+Instead of using raw string paths, Modeled Curator defines the {{ZPath}} interface that abstracts
+ZooKeeper paths. ZPaths can be simple static paths or can contain parameters that can be replaced
+as needed.
+
+To build a simple static path, use:
+
+{code}
+ZPath path = ZPath.parse("/my/static/path");
+{code}
+
+To build a path with parameters, use. {{ZPath.parseWithIds()}} using the value "\{XXXX\}" to
+denote a parameter. You can then use the {{resolve()}} method to replace the parameters. The value
+between "\{\}" can be any value. E.g.
+
+{code}
+ZPath path = ZPath.parseWithIds("/foo/{first param}/bar/{second param}");
+
+...
+
+ZPath resolvedPath = path.resolve(param1, param2);
+{code}
+
+h3. NodeName
+
+Parameters are resolved by calling {{toString()}} on the parameter. You can use {{NodeName}}
+to change this behavior. If a parameter implements {{NodeName}} the {{nodeName()}} method
+is used as the parameter value.
+
+h3. Partial Resolution
+
+Note: ZPaths can be partially resolved. E.g.
+
+{code}
+ZPath path = ZPath.parseWithIds("/foo/{type}/bar/{id}");
+
+...
+
+ZPath partial = path.resolve("standard");
+// partial is now "/foo/standard/bar/{id}"
+{code}
+
+ModeledFramework takes advantage of this. [[See below|#ModeledFramework]] for details.
+
+h2. ModelSpec
+
+A {{ModelSpec}} contains all the metadata needed to operate on a ZooKeeper path:
+
+* A ZPath
+* A serializer for the data stored at the path
+* Options for how nodes should be created (sequential, compressed data, ttl, etc.)
+* ACLs for the nodes at the path
+* Options for how to delete nodes (guaranteed, deleting children, etc.)
+
+ModelSpec instances are created via a builder. The builder sets defaults that should be
+useful for most applications but you can alter any of these as needed.
+
+{code}
+// a standard model spec for the given path and serializer
+// the model spec will have no ACLs and the options:
+// * createParentsAsContainers
+// * setDataIfExists
+// * DeleteOption.guaranteed
+ModelSpec<MyModel> spec = ModelSpec.builder(path, JacksonModelSerializer.build(MyModel.class)).build();
+{code}
+
+As a convenience, ModelSpec provides {{resolve()}} methods in case the ZPath used has parameters.
+E.g.
+
+{code}
+ZPath path = ZPath.parseWithIds("/foo/{id}/bar/{id}");
+ModelSpec<MyModel> spec = ModelSpec.builder(path, JacksonModelSerializer.build(MyModel.class)).build();
+
+...
+
+ModelSpec<MyModel> resolvedSpec = spec.resolve(param1, param2);
+{code}
+
+h3. JacksonModelSerializer
+
+A Jackson serializer, {{JacksonModelSerializer}}, is included. However, the Jackson dependency for it is
+specified as "provided" in the curator\-x\-async Maven POM file to avoid adding a new dependency to Curator.
+Therefore, if you wish to use the JacksonModelSerializer you must manually add the dependency to your build system.
+
+E.g. for Maven:
+
+{code}
+<dependency>
+    <groupId>com.fasterxml.jackson.core</groupId>
+    <artifactId>jackson-databind</artifactId>
+    <version>XXXX</version>
+</dependency>
+{code}
+
+h2. ModeledFramework
+
+{{ModeledFramework}} ties together all the metadata into a Curator\-style instance that is
+used to perform ZooKeeper operations. E.g.
+
+{code}
+ModeledFramework<MyModel> modeledClient = ModeledFramework.wrap(client, myModelSpec);
+
+...
+
+MyModel instance = ...
+modeledClient.set(instance);
+{code}
+
+The "set" call in the above example is the equivalent of:
+
+{code}
+MyModel instance = ...
+String path = "/foo/bar/" + instance.getId();
+byte[] data = serializer.serialize(instance);
+client.create()
+    .withOptions(Sets.newHashSet(CreateOption.createParentsAsContainers, CreateOption.setDataIfExists))
+    .forPath(path, data);
+{code}
+
+To get a value:
+
+{code}
+ModeledFramework<MyModel> modeledClient = ModeledFramework.wrap(client, myModelSpec);
+
+...
+
+modeledClient.read().whenComplete((value, e) -> {
+    if ( e != null ) {
+        // handle the error
+    } else {
+        // "value" is the MyModel instance
+    }
+});
+{code}
+
+The "read" call in the above example is the equivalent of:
+
+{code}
+String path = "/foo/bar/" + instanceId;
+client.getData().forPath(path).whenComplete((data, e) -> {
+    if ( e != null ) {
+        // handle the error
+    } else {
+        // NOTE: you must deal with possible deserialization problems
+        // caused by clients that write bad data
+        // If all of your code uses ModeledFramework you can guarantee that
+        // the data is always correctly written
+        MyModel model = serializer.deserialize(data);
+        // ...
+    }
+});
+{code}
+
+h3. Partially Resolved ZPaths and Set/Update
+
+ModeledFramework's various {{set}} and {{update}} methods check for unresolved ZPaths. If the current
+modelSpec has an unresolved ZPath when set/update is called, it is automatically resolved using the model
+instance being set/updated. E.g.
+
+{code}
+ZPath path = ZPath.parseWithIds("/root/{type}/instance/{id}");
+ModelSpec<MyModel> modelSpec = ModelSpec.builder(path, serializer);
+ModeledFramework<MyModel> modeledClient = ModeledFramework.wrap(modelSpec, client, modelSpec);
+
+...
+
+String currentType = ...
+MyModel model = ...
+modeledClient.resolved(currentType).set(model); // internally, ModeledFramework calls ZPath.resolved()
+                                                // using "model" as the argument to get the actual ZPath
+{code}
+
+h2. Caching and Typed Parameters
+
+In addition to the above features, Modeled Curator supports [[Integrated Caching|modeled-typed.html]],
+[[Typed Parameters|modeled-typed.html]] and [[Versioning|modeled-typed.html]]. See
+[[Caching and Typed Parameters|modeled-typed.html]] for details.
+

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/site/confluence/modeled-typed.confluence
----------------------------------------------------------------------
diff --git a/curator-x-async/src/site/confluence/modeled-typed.confluence b/curator-x-async/src/site/confluence/modeled-typed.confluence
new file mode 100644
index 0000000..c02ea80
--- /dev/null
+++ b/curator-x-async/src/site/confluence/modeled-typed.confluence
@@ -0,0 +1,89 @@
+h1. Modeled Curator \- Caching, Typed Parameters and Versioning
+
+In addition to its [[main features|modeled-components.html]] Modeled Curator also supports
+integrated caching, typed parameters and versioning.
+
+h2. Caching
+
+{{ModeledFramework}} instances can be wrapped with a facade that uses a Curator cache internally.
+All read operations use this cache instead of making direct ZooKeeper calls. You can also
+listen for node changes. E.g.
+
+{code}
+ModeledFramework<MyModel> modeledClient = ModeledFramework.wrap(client, myModelSpec);
+CachedModeledFramework<MyModel> cached = modeledClient.cached();
+cached.start();
+
+// reads come from the cache
+cached.read().whenComplete(...) ...
+
+cached.listenable.addListener((type, path, stat, model) -> {
+    // type is NODE_ADDED, NODE_UPDATED, etc.
+});
+{code}
+
+h3. Unresolved Paths and Caching
+
+If the last node in the ModelSpec's path is a parameter, CachedModeledFramework will automatically
+listen to the parent path. E.g.
+
+{code}
+ZPath path = ZPath.parseWithIds("/root/instance/{id}");
+ModelSpec<MyModel> modelSpec = ModelSpec.builder(path, serializer);
+ModeledFramework<MyModel> modeledClient = ModeledFramework.wrap(modelSpec, client, modelSpec);
+
+CachedModeledFramework<MyModel> cached = modeledClient.cached();
+cached.start(); // automatically listens to "/root/instance" and below
+{code}
+
+h2. Typed Parameters
+
+The "resolve" methods in ZPath et al consume untyped Objects. Ideally, we should be able to
+specify parameters in a strongly typed manner. Modeled Curator's "type" templates provide this. You
+can specify typed parameters for ZPaths, ModelSpecs and ModeledFramework.
+The [[Pub\-Sub Example|https://github.com/apache/curator/tree/master/curator-examples/src/main/java/pubsub]]
+shows how to use typed parameters with ModeledFramework.
+
+Typed interfaces are provided for up to 10 parameters and are named
+{{TypedZPath}}, {{TypedZPath2}}, {{TypedModelSpec}}, {{TypedModelSpec2}}, {{TypedModeledFramework}},
+{{TypedModeledFramework2}}, etc.
+
+Here's an example of a TypedModeledFramework that models a Person and uses two parameters
+to generate the path, a Group and an Organization:
+
+{code}
+TypedModeledFramework2<Person, Group, Organization> clientTemplate = TypedModeledFramework2.from(
+    ModeledFrameworkBuilder.build(),
+    personModelSpec
+);
+
+...
+
+Group group = ...
+Organization organization = ...
+ModeledFramework<Person> modeledClient = clientTemplate.resolve(asyncClient, group, organization);
+client.set(person);
+{code}
+
+TypedZPath and TypedModelSpec work similarly.
+
+h2. Versioning
+
+Modeled Curator supports associating a ZNode version with a model object via
+the {{Versioned}} interface and the {{VersionedModeledFramework}} APIs. To
+read a model along with its ZNode version use:
+
+{code}
+ModeledFramework<Person> client = ...
+
+client.versioned().read().whenComplete((value, e) -> {
+    if ( value != null ) {
+        // value's type is Versioned<Person>
+        Person personModel = value.model();
+        int znodeVersion = value.version();
+    }
+});
+{code}
+
+{{VersionedModeledFramework}} has set/update APIs which automatically use the version
+from the {{Versioned}} instance.

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/site/confluence/modeled.confluence
----------------------------------------------------------------------
diff --git a/curator-x-async/src/site/confluence/modeled.confluence b/curator-x-async/src/site/confluence/modeled.confluence
new file mode 100644
index 0000000..32e8727
--- /dev/null
+++ b/curator-x-async/src/site/confluence/modeled.confluence
@@ -0,0 +1,48 @@
+h1. Modeled Curator
+
+This is a strongly typed DSL that allows you to map a Curator\-style client to:
+
+* A ZooKeeper path (supporting parameterized substitutions)
+* A serializer for the data stored at the path
+* Options for how nodes should be created (sequential, compressed data, ttl, etc.)
+* ACLs for the nodes at the path
+* Options for how to delete nodes (guaranteed, deleting children, etc.)
+
+For example:
+
+{code}
+ModeledFramework<Foo> modeled = ModeledFramework.wrap(client, fooModelSpec);
+modeled.set(new Foo());
+{code}
+
+This ModeledFramework instance knows the path to use, how to serialize the "Foo" instance,
+which create options and ACLs to use, etc.
+
+h2. Background and Usage
+
+Note: To use Modeled Curator, you should be familiar with Java 8's lambdas, CompletedFuture and CompletionStage.
+You should also be familiar with [[Curator Async|async.html]] as Modeled Curator is based on it.
+
+Modeled Curator consists of the components:
+
+* [[ZPath|modeled-components.html]]
+* [[ModelSpec|modeled-components.html]]
+* [[ModeledFramework|modeled-components.html]]
+
+Additional functionality is provided by:
+
+* [[CachedModeledFramework|modeled-typed.html]]
+* [[Typed Parameter Templates|modeled-typed.html]]
+* [[Versioning|modeled-typed.html]]
+
+h2. Example
+
+A complete example usage of Modeled Curator along with CachedModeledFramework and Typed Parameter Templates
+can be found here: [[https://github.com/apache/curator/tree/master/curator-examples/src/main/java/pubsub]].
+
+h2. Details
+
+For more details see:
+
+* [[Components|modeled-components.html]]
+* [[Caching, Typed Parameters and Versioning|modeled-typed.html]]

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/site/site.xml
----------------------------------------------------------------------
diff --git a/curator-x-async/src/site/site.xml b/curator-x-async/src/site/site.xml
index 63fccaa..f78abc7 100644
--- a/curator-x-async/src/site/site.xml
+++ b/curator-x-async/src/site/site.xml
@@ -25,7 +25,11 @@
             <link rel="stylesheet" href="../css/site.css" />
             <script type="text/javascript">
                 $(function(){
-                $('a[title="Curator RPC Proxy"]').parent().addClass("active");
+                    if ( location &amp;&amp; location.pathname &amp;&amp; location.pathname.endsWith('/index.html') ) {
+                        $('a[title="Java 8/Async"]').parent().addClass("active");
+                    } else {
+                        $('a[title="Strongly Typed Models"]').parent().addClass("active");
+                    }
                 });
             </script>
         </head>

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/test/java/org/apache/curator/x/async/CompletableBaseClassForTests.java
----------------------------------------------------------------------
diff --git a/curator-x-async/src/test/java/org/apache/curator/x/async/CompletableBaseClassForTests.java b/curator-x-async/src/test/java/org/apache/curator/x/async/CompletableBaseClassForTests.java
new file mode 100644
index 0000000..232d301
--- /dev/null
+++ b/curator-x-async/src/test/java/org/apache/curator/x/async/CompletableBaseClassForTests.java
@@ -0,0 +1,65 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.curator.x.async;
+
+import org.apache.curator.test.BaseClassForTests;
+import org.apache.curator.test.Timing;
+import org.testng.Assert;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.BiConsumer;
+
+public abstract class CompletableBaseClassForTests extends BaseClassForTests
+{
+    protected static final Timing timing = new Timing();
+
+    protected <T, U> void complete(CompletionStage<T> stage)
+    {
+        complete(stage, (v, e) -> {});
+    }
+
+    protected <T, U> void complete(CompletionStage<T> stage, BiConsumer<? super T, Throwable> handler)
+    {
+        try
+        {
+            stage.handle((v, e) -> {
+                handler.accept(v, e);
+                return null;
+            }).toCompletableFuture().get(timing.forWaiting().milliseconds(), TimeUnit.MILLISECONDS);
+        }
+        catch ( InterruptedException e )
+        {
+            Thread.interrupted();
+        }
+        catch ( ExecutionException e )
+        {
+            if ( e.getCause() instanceof AssertionError )
+            {
+                throw (AssertionError)e.getCause();
+            }
+            Assert.fail("get() failed", e);
+        }
+        catch ( TimeoutException e )
+        {
+            Assert.fail("get() timed out");
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/test/java/org/apache/curator/x/async/TestAsyncWrappers.java
----------------------------------------------------------------------
diff --git a/curator-x-async/src/test/java/org/apache/curator/x/async/TestAsyncWrappers.java b/curator-x-async/src/test/java/org/apache/curator/x/async/TestAsyncWrappers.java
new file mode 100644
index 0000000..7ce7904
--- /dev/null
+++ b/curator-x-async/src/test/java/org/apache/curator/x/async/TestAsyncWrappers.java
@@ -0,0 +1,73 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.curator.x.async;
+
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.locks.InterProcessMutex;
+import org.apache.curator.retry.RetryOneTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class TestAsyncWrappers extends CompletableBaseClassForTests
+{
+    @Test
+    public void testBasic()
+    {
+        try ( CuratorFramework client = CuratorFrameworkFactory.newClient(server.getConnectString(), new RetryOneTime(1)) )
+        {
+            client.start();
+
+            InterProcessMutex lock = new InterProcessMutex(client, "/one/two");
+            complete(AsyncWrappers.lockAsync(lock), (__, e) -> {
+                Assert.assertNull(e);
+                AsyncWrappers.release(lock);
+            });
+        }
+    }
+
+    @Test
+    public void testContention() throws Exception
+    {
+        try ( CuratorFramework client = CuratorFrameworkFactory.newClient(server.getConnectString(), new RetryOneTime(1)) )
+        {
+            client.start();
+
+            InterProcessMutex lock1 = new InterProcessMutex(client, "/one/two");
+            InterProcessMutex lock2 = new InterProcessMutex(client, "/one/two");
+            CountDownLatch latch = new CountDownLatch(1);
+            AsyncWrappers.lockAsync(lock1).thenAccept(__ -> {
+                latch.countDown();  // don't release the lock
+            });
+            Assert.assertTrue(timing.awaitLatch(latch));
+
+            CountDownLatch latch2 = new CountDownLatch(1);
+            AsyncWrappers.lockAsync(lock2, timing.forSleepingABit().milliseconds(), TimeUnit.MILLISECONDS).exceptionally(e -> {
+                if ( e instanceof AsyncWrappers.TimeoutException )
+                {
+                    latch2.countDown();  // lock should still be held
+                }
+                return null;
+            });
+            Assert.assertTrue(timing.awaitLatch(latch2));
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/test/java/org/apache/curator/x/async/TestBasicOperations.java
----------------------------------------------------------------------
diff --git a/curator-x-async/src/test/java/org/apache/curator/x/async/TestBasicOperations.java b/curator-x-async/src/test/java/org/apache/curator/x/async/TestBasicOperations.java
index 1c4f241..0274413 100644
--- a/curator-x-async/src/test/java/org/apache/curator/x/async/TestBasicOperations.java
+++ b/curator-x-async/src/test/java/org/apache/curator/x/async/TestBasicOperations.java
@@ -44,9 +44,8 @@ import static org.apache.curator.x.async.api.CreateOption.compress;
 import static org.apache.curator.x.async.api.CreateOption.setDataIfExists;
 import static org.apache.zookeeper.CreateMode.EPHEMERAL_SEQUENTIAL;
 
-public class TestBasicOperations extends BaseClassForTests
+public class TestBasicOperations extends CompletableBaseClassForTests
 {
-    private static final Timing timing = new Timing();
     private AsyncCuratorFramework client;
 
     @BeforeMethod
@@ -181,36 +180,4 @@ public class TestBasicOperations extends BaseClassForTests
             Assert.assertEquals(v.getCode(), KeeperException.Code.CONNECTIONLOSS);
         });
     }
-
-    private <T, U> void complete(CompletionStage<T> stage)
-    {
-        complete(stage, (v, e) -> {});
-    }
-
-    private <T, U> void complete(CompletionStage<T> stage, BiConsumer<? super T, Throwable> handler)
-    {
-        try
-        {
-            stage.handle((v, e) -> {
-                handler.accept(v, e);
-                return null;
-            }).toCompletableFuture().get(timing.forWaiting().milliseconds(), TimeUnit.MILLISECONDS);
-        }
-        catch ( InterruptedException e )
-        {
-            Thread.interrupted();
-        }
-        catch ( ExecutionException e )
-        {
-            if ( e.getCause() instanceof AssertionError )
-            {
-                throw (AssertionError)e.getCause();
-            }
-            Assert.fail("get() failed", e);
-        }
-        catch ( TimeoutException e )
-        {
-            Assert.fail("get() timed out");
-        }
-    }
 }

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestCachedModeledFramework.java
----------------------------------------------------------------------
diff --git a/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestCachedModeledFramework.java b/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestCachedModeledFramework.java
new file mode 100644
index 0000000..49821e2
--- /dev/null
+++ b/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestCachedModeledFramework.java
@@ -0,0 +1,167 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.curator.x.async.modeled;
+
+import org.apache.curator.framework.state.ConnectionState;
+import org.apache.curator.test.Timing;
+import org.apache.curator.x.async.modeled.cached.CachedModeledFramework;
+import org.apache.curator.x.async.modeled.cached.ModeledCacheListener;
+import org.apache.curator.x.async.modeled.models.TestModel;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class TestCachedModeledFramework extends TestModeledFrameworkBase
+{
+    @Test
+    public void testThreading()
+    {
+        TestModel model = new TestModel("a", "b", "c", 1, BigInteger.ONE);
+        CachedModeledFramework<TestModel> client = ModeledFramework.wrap(async, modelSpec).cached().asyncDefault();
+
+        CountDownLatch latch = new CountDownLatch(1);
+        client.listenable().addListener((type, path1, stat, model1) -> latch.countDown());
+
+        complete(client.set(model));
+        client.start();
+        try
+        {
+            Assert.assertTrue(new Timing().awaitLatch(latch));
+
+            AtomicReference<Thread> completionThread = new AtomicReference<>();
+            complete(client.read().whenCompleteAsync((s, e) -> completionThread.set((e == null) ? Thread.currentThread() : null)));
+            Assert.assertNotNull(completionThread.get());
+            Assert.assertNotEquals(Thread.currentThread(), completionThread.get(), "Should be different threads");
+            completionThread.set(null);
+
+            complete(client.child("foo").read().whenCompleteAsync((v, e) -> completionThread.set((e != null) ? Thread.currentThread() : null)));
+            Assert.assertNotNull(completionThread.get());
+            Assert.assertNotEquals(Thread.currentThread(), completionThread.get(), "Should be different threads");
+            completionThread.set(null);
+        }
+        finally
+        {
+            client.close();
+        }
+    }
+
+    @Test
+    public void testCustomThreading()
+    {
+        AtomicReference<Thread> ourThread = new AtomicReference<>();
+        ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
+            Thread thread = new Thread(r, "testCustomThreading");
+            ourThread.set(thread);
+            return thread;
+        });
+        TestModel model = new TestModel("a", "b", "c", 1, BigInteger.ONE);
+        CachedModeledFramework<TestModel> client = ModeledFramework.wrap(async, modelSpec).cached(executor).asyncDefault();
+
+        CountDownLatch latch = new CountDownLatch(1);
+        client.listenable().addListener((type, path1, stat, model1) -> latch.countDown());
+
+        complete(client.set(model));
+        client.start();
+        try
+        {
+            Assert.assertTrue(new Timing().awaitLatch(latch));
+
+            AtomicReference<Thread> completionThread = new AtomicReference<>();
+            complete(client.read().thenAcceptAsync(s -> completionThread.set(Thread.currentThread())));
+            Assert.assertEquals(ourThread.get(), completionThread.get(), "Should be our thread");
+            completionThread.set(null);
+
+            complete(client.child("foo").read().whenCompleteAsync((v, e) -> completionThread.set((e != null) ? Thread.currentThread() : null)));
+            Assert.assertEquals(ourThread.get(), completionThread.get(), "Should be our thread");
+            completionThread.set(null);
+        }
+        finally
+        {
+            client.close();
+        }
+    }
+
+    @Test
+    public void testDownServer() throws IOException
+    {
+        Timing timing = new Timing();
+
+        TestModel model = new TestModel("a", "b", "c", 1, BigInteger.ONE);
+        CachedModeledFramework<TestModel> client = ModeledFramework.wrap(async, modelSpec).cached();
+        Semaphore semaphore = new Semaphore(0);
+        client.listenable().addListener((t, p, s, m) -> semaphore.release());
+
+        client.start();
+        try
+        {
+            client.set(model);
+            Assert.assertTrue(timing.acquireSemaphore(semaphore));
+
+            CountDownLatch latch = new CountDownLatch(1);
+            rawClient.getConnectionStateListenable().addListener((__, state) -> {
+                if ( state == ConnectionState.LOST )
+                {
+                    latch.countDown();
+                }
+            });
+            server.stop();
+            Assert.assertTrue(timing.awaitLatch(latch));
+
+            complete(client.read().whenComplete((value, e) -> {
+                Assert.assertNotNull(value);
+                Assert.assertNull(e);
+            }));
+        }
+        finally
+        {
+            client.close();
+        }
+    }
+
+    @Test
+    public void testPostInitializedFilter()
+    {
+        TestModel model1 = new TestModel("a", "b", "c", 1, BigInteger.ONE);
+        TestModel model2 = new TestModel("d", "e", "e", 1, BigInteger.ONE);
+        CachedModeledFramework<TestModel> client = ModeledFramework.wrap(async, modelSpec).cached();
+        Semaphore semaphore = new Semaphore(0);
+        ModeledCacheListener<TestModel> listener = (t, p, s, m) -> semaphore.release();
+        client.listenable().addListener(listener.postInitializedOnly());
+
+        complete(client.child("1").set(model1));  // set before cache is started
+        client.start();
+        try
+        {
+            Assert.assertFalse(timing.forSleepingABit().acquireSemaphore(semaphore));
+
+            client.child("2").set(model2);  // set before cache is started
+            Assert.assertTrue(timing.acquireSemaphore(semaphore));
+        }
+        finally
+        {
+            client.close();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestModeledFramework.java
----------------------------------------------------------------------
diff --git a/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestModeledFramework.java b/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestModeledFramework.java
new file mode 100644
index 0000000..42a9e63
--- /dev/null
+++ b/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestModeledFramework.java
@@ -0,0 +1,178 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.curator.x.async.modeled;
+
+import com.google.common.collect.Sets;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.schema.Schema;
+import org.apache.curator.framework.schema.SchemaSet;
+import org.apache.curator.framework.schema.SchemaViolation;
+import org.apache.curator.retry.RetryOneTime;
+import org.apache.curator.x.async.AsyncCuratorFramework;
+import org.apache.curator.x.async.AsyncStage;
+import org.apache.curator.x.async.modeled.models.TestModel;
+import org.apache.curator.x.async.modeled.models.TestNewerModel;
+import org.apache.curator.x.async.modeled.versioned.Versioned;
+import org.apache.curator.x.async.modeled.versioned.VersionedModeledFramework;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.ZooDefs;
+import org.apache.zookeeper.data.ACL;
+import org.apache.zookeeper.data.Id;
+import org.apache.zookeeper.server.auth.DigestAuthenticationProvider;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import java.math.BigInteger;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+
+public class TestModeledFramework extends TestModeledFrameworkBase
+{
+    @Test
+    public void testCrud()
+    {
+        TestModel rawModel = new TestModel("John", "Galt", "1 Galt's Gulch", 42, BigInteger.valueOf(1));
+        TestModel rawModel2 = new TestModel("Wayne", "Rooney", "Old Trafford", 10, BigInteger.valueOf(1));
+        ModeledFramework<TestModel> client = ModeledFramework.wrap(async, modelSpec);
+        AsyncStage<String> stage = client.set(rawModel);
+        Assert.assertNull(stage.event());
+        complete(stage, (s, e) -> Assert.assertNotNull(s));
+        complete(client.read(), (model, e) -> Assert.assertEquals(model, rawModel));
+        complete(client.update(rawModel2));
+        complete(client.read(), (model, e) -> Assert.assertEquals(model, rawModel2));
+        complete(client.delete());
+        complete(client.checkExists(), (stat, e) -> Assert.assertNull(stat));
+    }
+
+    @Test
+    public void testBackwardCompatibility()
+    {
+        TestNewerModel rawNewModel = new TestNewerModel("John", "Galt", "1 Galt's Gulch", 42, BigInteger.valueOf(1), 100);
+        ModeledFramework<TestNewerModel> clientForNew = ModeledFramework.wrap(async, newModelSpec);
+        complete(clientForNew.set(rawNewModel), (s, e) -> Assert.assertNotNull(s));
+
+        ModeledFramework<TestModel> clientForOld = ModeledFramework.wrap(async, modelSpec);
+        complete(clientForOld.read(), (model, e) -> Assert.assertTrue(rawNewModel.equalsOld(model)));
+    }
+
+    @Test
+    public void testWatched() throws InterruptedException
+    {
+        CountDownLatch latch = new CountDownLatch(1);
+        ModeledFramework<TestModel> client = ModeledFramework.builder(async, modelSpec).watched().build();
+        client.checkExists().event().whenComplete((event, ex) -> latch.countDown());
+        timing.sleepABit();
+        Assert.assertEquals(latch.getCount(), 1);
+        client.set(new TestModel());
+        Assert.assertTrue(timing.awaitLatch(latch));
+    }
+
+    @Test
+    public void testGetChildren()
+    {
+        TestModel model = new TestModel("John", "Galt", "1 Galt's Gulch", 42, BigInteger.valueOf(1));
+        ModeledFramework<TestModel> client = ModeledFramework.builder(async, modelSpec).build();
+        complete(client.child("one").set(model));
+        complete(client.child("two").set(model));
+        complete(client.child("three").set(model));
+
+        Set<ZPath> expected = Sets.newHashSet(path.child("one"), path.child("two"), path.child("three"));
+        complete(client.children(), (children, e) -> Assert.assertEquals(Sets.newHashSet(children), expected));
+    }
+
+    @Test
+    public void testBadNode()
+    {
+        complete(async.create().forPath(modelSpec.path().fullPath(), "fubar".getBytes()));
+
+        ModeledFramework<TestModel> client = ModeledFramework.builder(async, modelSpec).watched().build();
+        complete(client.read().whenComplete((model, e) -> Assert.assertTrue(e instanceof RuntimeException)));
+    }
+
+    @Test
+    public void testSchema() throws Exception
+    {
+        Schema schema = modelSpec.schema();
+        try ( CuratorFramework schemaClient = CuratorFrameworkFactory.builder()
+            .connectString(server.getConnectString())
+            .retryPolicy(new RetryOneTime(1))
+            .schemaSet(new SchemaSet(Collections.singletonList(schema), false))
+            .build() ) {
+            schemaClient.start();
+
+            try
+            {
+                schemaClient.create().forPath(modelSpec.path().fullPath(), "asflasfas".getBytes());
+                Assert.fail("Should've thrown SchemaViolation");
+            }
+            catch ( SchemaViolation dummy )
+            {
+                // expected
+            }
+
+            ModeledFramework<TestModel> modeledSchemaClient = ModeledFramework.wrap(AsyncCuratorFramework.wrap(schemaClient), modelSpec);
+            complete(modeledSchemaClient.set(new TestModel("one", "two", "three", 4, BigInteger.ONE)), (dummy, e) -> Assert.assertNull(e));
+        }
+    }
+
+    @Test
+    public void testVersioned()
+    {
+        ModeledFramework<TestModel> client = ModeledFramework.wrap(async, modelSpec);
+        client.set(new TestModel("John", "Galt", "Galt's Gulch", 21, BigInteger.valueOf(1010101)));
+
+        VersionedModeledFramework<TestModel> versioned = client.versioned();
+        complete(versioned.read().whenComplete((v, e) -> {
+            Assert.assertNull(e);
+            Assert.assertTrue(v.version() > 0);
+        }).thenCompose(versioned::set).whenComplete((s, e) -> Assert.assertNull(e))); // version is correct should succeed
+
+        complete(versioned.read().whenComplete((v, e) -> {
+            Assert.assertNull(e);
+            Assert.assertTrue(v.version() > 0);
+        }).thenCompose(value -> {
+            Versioned<TestModel> badVersion = Versioned.from(value.model(), Integer.MAX_VALUE);
+            return versioned.set(badVersion);
+        }).whenComplete((s, e) -> Assert.assertTrue(e instanceof KeeperException.BadVersionException)));
+    }
+
+    @Test
+    public void testAcl() throws NoSuchAlgorithmException
+    {
+        List<ACL> aclList = Collections.singletonList(new ACL(ZooDefs.Perms.WRITE, new Id("digest", DigestAuthenticationProvider.generateDigest("test:test"))));
+        ModelSpec<TestModel> aclModelSpec = ModelSpec.builder(modelSpec.path(), modelSpec.serializer()).withAclList(aclList).build();
+        ModeledFramework<TestModel> client = ModeledFramework.wrap(async, aclModelSpec);
+        complete(client.set(new TestModel("John", "Galt", "Galt's Gulch", 21, BigInteger.valueOf(1010101))));
+        complete(client.update(new TestModel("John", "Galt", "Galt's Gulch", 54, BigInteger.valueOf(88))), (__, e) -> Assert.assertNotNull(e, "Should've gotten an auth failure"));
+
+        try ( CuratorFramework authCurator = CuratorFrameworkFactory.builder()
+            .connectString(server.getConnectString())
+            .retryPolicy(new RetryOneTime(1))
+            .authorization("digest", "test:test".getBytes())
+            .build() )
+        {
+            authCurator.start();
+            ModeledFramework<TestModel> authClient = ModeledFramework.wrap(AsyncCuratorFramework.wrap(authCurator), aclModelSpec);
+            complete(authClient.update(new TestModel("John", "Galt", "Galt's Gulch", 42, BigInteger.valueOf(66))), (__, e) -> Assert.assertNull(e, "Should've succeeded"));
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestModeledFrameworkBase.java
----------------------------------------------------------------------
diff --git a/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestModeledFrameworkBase.java b/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestModeledFrameworkBase.java
new file mode 100644
index 0000000..61a4570
--- /dev/null
+++ b/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestModeledFrameworkBase.java
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.curator.x.async.modeled;
+
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.RetryOneTime;
+import org.apache.curator.utils.CloseableUtils;
+import org.apache.curator.x.async.AsyncCuratorFramework;
+import org.apache.curator.x.async.CompletableBaseClassForTests;
+import org.apache.curator.x.async.modeled.models.TestModel;
+import org.apache.curator.x.async.modeled.models.TestNewerModel;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+
+public class TestModeledFrameworkBase extends CompletableBaseClassForTests
+{
+    protected static final ZPath path = ZPath.parse("/test/path");
+    protected CuratorFramework rawClient;
+    protected ModelSpec<TestModel> modelSpec;
+    protected ModelSpec<TestNewerModel> newModelSpec;
+    protected AsyncCuratorFramework async;
+
+    @BeforeMethod
+    @Override
+    public void setup() throws Exception
+    {
+        super.setup();
+
+        rawClient = CuratorFrameworkFactory.newClient(server.getConnectString(), timing.session(), timing.connection(), new RetryOneTime(1));
+        rawClient.start();
+        async = AsyncCuratorFramework.wrap(rawClient);
+
+        JacksonModelSerializer<TestModel> serializer = JacksonModelSerializer.build(TestModel.class);
+        JacksonModelSerializer<TestNewerModel> newSerializer = JacksonModelSerializer.build(TestNewerModel.class);
+
+        modelSpec = ModelSpec.builder(path, serializer).build();
+        newModelSpec = ModelSpec.builder(path, newSerializer).build();
+    }
+
+    @AfterMethod
+    @Override
+    public void teardown() throws Exception
+    {
+        CloseableUtils.closeQuietly(rawClient);
+        super.teardown();
+    }
+}

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestZPath.java
----------------------------------------------------------------------
diff --git a/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestZPath.java b/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestZPath.java
new file mode 100644
index 0000000..d2c24da
--- /dev/null
+++ b/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/TestZPath.java
@@ -0,0 +1,126 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.curator.x.async.modeled;
+
+import org.apache.curator.utils.ZKPaths;
+import org.apache.curator.x.async.modeled.details.ZPathImpl;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import static org.apache.curator.x.async.modeled.ZPath.parameter;
+
+public class TestZPath
+{
+    @Test
+    public void testRoot()
+    {
+        Assert.assertEquals(ZPath.root.nodeName(), ZKPaths.PATH_SEPARATOR);
+        Assert.assertEquals(ZPath.root, ZPathImpl.root);
+        Assert.assertTrue(ZPath.root.isRoot());
+        Assert.assertEquals(ZPath.root.child("foo").parent(), ZPath.root);
+        Assert.assertTrue(ZPath.root.child("foo").parent().isRoot());
+    }
+
+    @Test
+    public void testBasic()
+    {
+        ZPath path = ZPath.root.child("one").child("two");
+        Assert.assertFalse(path.isRoot());
+        Assert.assertEquals(path, ZPath.root.child("one").child("two"));
+        Assert.assertNotEquals(path, ZPath.root.child("onex").child("two"));
+        Assert.assertEquals(path.nodeName(), "two");
+        Assert.assertEquals(path.fullPath(), "/one/two");
+        Assert.assertEquals(path.parent().fullPath(), "/one");
+        Assert.assertEquals(path.fullPath(), "/one/two");       // call twice to test the internal cache
+        Assert.assertEquals(path.parent().fullPath(), "/one");  // call twice to test the internal cache
+
+        Assert.assertTrue(path.startsWith(ZPath.root.child("one")));
+        Assert.assertFalse(path.startsWith(ZPath.root.child("two")));
+
+        ZPath checkIdLike = ZPath.parse("/one/{two}/three");
+        Assert.assertTrue(checkIdLike.isResolved());
+        checkIdLike = ZPath.parse("/one/" + ZPath.parameter() + "/three");
+        Assert.assertTrue(checkIdLike.isResolved());
+        checkIdLike = ZPath.parse("/one/" + ZPath.parameter("others") + "/three");
+        Assert.assertTrue(checkIdLike.isResolved());
+    }
+
+    @Test
+    public void testParsing()
+    {
+        Assert.assertEquals(ZPath.parse("/"), ZPath.root);
+        Assert.assertEquals(ZPath.parse("/one/two/three"), ZPath.root.child("one").child("two").child("three"));
+        Assert.assertEquals(ZPath.parse("/one/two/three"), ZPath.from("one", "two", "three"));
+        Assert.assertEquals(ZPath.parseWithIds("/one/{id}/two/{id}"), ZPath.from("one", parameter(), "two", parameter()));
+    }
+
+    @Test(expectedExceptions = IllegalStateException.class)
+    public void testUnresolvedPath()
+    {
+        ZPath path = ZPath.from("one", parameter(), "two");
+        path.fullPath();
+    }
+
+    @Test
+    public void testResolvedPath()
+    {
+        ZPath path = ZPath.from("one", parameter(), "two", parameter());
+        Assert.assertEquals(path.resolved("a", "b"), ZPath.from("one", "a", "two", "b"));
+    }
+
+    @Test
+    public void testSchema()
+    {
+        ZPath path = ZPath.from("one", parameter(), "two", parameter());
+        Assert.assertEquals(path.toSchemaPathPattern().toString(), "/one/.*/two/.*");
+        path = ZPath.parse("/one/two/three");
+        Assert.assertEquals(path.toSchemaPathPattern().toString(), "/one/two/three");
+        path = ZPath.parseWithIds("/one/{id}/three");
+        Assert.assertEquals(path.toSchemaPathPattern().toString(), "/one/.*/three");
+        path = ZPath.parseWithIds("/{id}/{id}/three");
+        Assert.assertEquals(path.toSchemaPathPattern().toString(), "/.*/.*/three");
+    }
+
+    @Test
+    public void testCustomIds()
+    {
+        Assert.assertEquals(ZPath.parseWithIds("/a/{a}/bee/{bee}/c/{c}").toString(), "/a/{a}/bee/{bee}/c/{c}");
+        Assert.assertEquals(ZPath.from("a", parameter(), "b", parameter()).toString(), "/a/{id}/b/{id}");
+        Assert.assertEquals(ZPath.from("a", parameter("foo"), "b", parameter("bar")).toString(), "/a/{foo}/b/{bar}");
+    }
+
+    @Test
+    public void testPartialResolution()
+    {
+        ZPath path = ZPath.parseWithIds("/one/{1}/two/{2}");
+        Assert.assertFalse(path.parent().isResolved());
+        Assert.assertFalse(path.parent().parent().isResolved());
+        Assert.assertTrue(path.parent().parent().parent().isResolved());
+        Assert.assertFalse(path.isResolved());
+
+        path = path.resolved("p1");
+        Assert.assertFalse(path.isResolved());
+        Assert.assertTrue(path.parent().isResolved());
+        Assert.assertEquals(path.toString(), "/one/p1/two/{2}");
+
+        path = path.resolved("p2");
+        Assert.assertTrue(path.isResolved());
+        Assert.assertEquals(path.toString(), "/one/p1/two/p2");
+    }
+}

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/models/TestModel.java
----------------------------------------------------------------------
diff --git a/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/models/TestModel.java b/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/models/TestModel.java
new file mode 100644
index 0000000..8a92d33
--- /dev/null
+++ b/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/models/TestModel.java
@@ -0,0 +1,115 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.curator.x.async.modeled.models;
+
+import java.math.BigInteger;
+import java.util.Objects;
+
+public class TestModel
+{
+    private final String firstName;
+    private final String lastName;
+    private final String address;
+    private final int age;
+    private final BigInteger salary;
+
+    public TestModel()
+    {
+        this("", "", "", 0, BigInteger.ZERO);
+    }
+
+    public TestModel(String firstName, String lastName, String address, int age, BigInteger salary)
+    {
+        this.firstName = Objects.requireNonNull(firstName, "firstName cannot be null");
+        this.lastName = Objects.requireNonNull(lastName, "lastName cannot be null");
+        this.address = Objects.requireNonNull(address, "address cannot be null");
+        this.age = Objects.requireNonNull(age, "age cannot be null");
+        this.salary = salary;
+    }
+
+    public String getFirstName()
+    {
+        return firstName;
+    }
+
+    public String getLastName()
+    {
+        return lastName;
+    }
+
+    public String getAddress()
+    {
+        return address;
+    }
+
+    public int getAge()
+    {
+        return age;
+    }
+
+    public BigInteger getSalary()
+    {
+        return salary;
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if ( this == o )
+        {
+            return true;
+        }
+        if ( o == null || getClass() != o.getClass() )
+        {
+            return false;
+        }
+
+        TestModel testModel = (TestModel)o;
+
+        if ( age != testModel.age )
+        {
+            return false;
+        }
+        if ( !firstName.equals(testModel.firstName) )
+        {
+            return false;
+        }
+        if ( !lastName.equals(testModel.lastName) )
+        {
+            return false;
+        }
+        //noinspection SimplifiableIfStatement
+        if ( !address.equals(testModel.address) )
+        {
+            return false;
+        }
+        return salary.equals(testModel.salary);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int result = firstName.hashCode();
+        result = 31 * result + lastName.hashCode();
+        result = 31 * result + address.hashCode();
+        result = 31 * result + age;
+        result = 31 * result + salary.hashCode();
+        return result;
+    }
+}

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/models/TestNewerModel.java
----------------------------------------------------------------------
diff --git a/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/models/TestNewerModel.java b/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/models/TestNewerModel.java
new file mode 100644
index 0000000..94e82fb
--- /dev/null
+++ b/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/models/TestNewerModel.java
@@ -0,0 +1,137 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.curator.x.async.modeled.models;
+
+import java.math.BigInteger;
+import java.util.Objects;
+
+public class TestNewerModel
+{
+    private final String firstName;
+    private final String lastName;
+    private final String address;
+    private final int age;
+    private final BigInteger salary;
+    private final long newField;
+
+    public TestNewerModel()
+    {
+        this("", "", "", 0, BigInteger.ZERO, 0);
+    }
+
+    public TestNewerModel(String firstName, String lastName, String address, int age, BigInteger salary, long newField)
+    {
+        this.firstName = Objects.requireNonNull(firstName, "firstName cannot be null");
+        this.lastName = Objects.requireNonNull(lastName, "lastName cannot be null");
+        this.address = Objects.requireNonNull(address, "address cannot be null");
+        this.age = Objects.requireNonNull(age, "age cannot be null");
+        this.salary = salary;
+        this.newField = newField;
+    }
+
+    public String getFirstName()
+    {
+        return firstName;
+    }
+
+    public String getLastName()
+    {
+        return lastName;
+    }
+
+    public String getAddress()
+    {
+        return address;
+    }
+
+    public int getAge()
+    {
+        return age;
+    }
+
+    public BigInteger getSalary()
+    {
+        return salary;
+    }
+
+    public long getNewField()
+    {
+        return newField;
+    }
+
+    public boolean equalsOld(TestModel model)
+    {
+        return firstName.equals(model.getFirstName())
+            && lastName.equals(model.getLastName())
+            && address.equals(model.getAddress())
+            && salary.equals(model.getSalary())
+            && age == model.getAge()
+            ;
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if ( this == o )
+        {
+            return true;
+        }
+        if ( o == null || getClass() != o.getClass() )
+        {
+            return false;
+        }
+
+        TestNewerModel that = (TestNewerModel)o;
+
+        if ( age != that.age )
+        {
+            return false;
+        }
+        if ( newField != that.newField )
+        {
+            return false;
+        }
+        if ( !firstName.equals(that.firstName) )
+        {
+            return false;
+        }
+        if ( !lastName.equals(that.lastName) )
+        {
+            return false;
+        }
+        //noinspection SimplifiableIfStatement
+        if ( !address.equals(that.address) )
+        {
+            return false;
+        }
+        return salary.equals(that.salary);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int result = firstName.hashCode();
+        result = 31 * result + lastName.hashCode();
+        result = 31 * result + address.hashCode();
+        result = 31 * result + age;
+        result = 31 * result + salary.hashCode();
+        result = 31 * result + (int)(newField ^ (newField >>> 32));
+        return result;
+    }
+}

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/models/TestSimpleModel.java
----------------------------------------------------------------------
diff --git a/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/models/TestSimpleModel.java b/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/models/TestSimpleModel.java
new file mode 100644
index 0000000..f998da2
--- /dev/null
+++ b/curator-x-async/src/test/java/org/apache/curator/x/async/modeled/models/TestSimpleModel.java
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.curator.x.async.modeled.models;
+
+import java.util.Objects;
+
+public class TestSimpleModel
+{
+    private final String name;
+    private final int age;
+
+    public TestSimpleModel()
+    {
+        this("", 0);
+    }
+
+    public TestSimpleModel(String name, int age)
+    {
+        this.name = Objects.requireNonNull(name, "name cannot be null");
+        this.age = Objects.requireNonNull(age, "age cannot be null");
+    }
+
+    public String getName()
+    {
+        return name;
+    }
+
+    public int getAge()
+    {
+        return age;
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if ( this == o )
+        {
+            return true;
+        }
+        if ( o == null || getClass() != o.getClass() )
+        {
+            return false;
+        }
+
+        TestSimpleModel that = (TestSimpleModel)o;
+
+        //noinspection SimplifiableIfStatement
+        if ( age != that.age )
+        {
+            return false;
+        }
+        return name.equals(that.name);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int result = name.hashCode();
+        result = 31 * result + age;
+        return result;
+    }
+
+    @Override
+    public String toString()
+    {
+        return "TestSimpleModel{" + "name='" + name + '\'' + ", age=" + age + '}';
+    }
+}

http://git-wip-us.apache.org/repos/asf/curator/blob/0f5d10da/curator-x-async/src/test/resources/log4j.properties
----------------------------------------------------------------------
diff --git a/curator-x-async/src/test/resources/log4j.properties b/curator-x-async/src/test/resources/log4j.properties
new file mode 100644
index 0000000..2a85e0d
--- /dev/null
+++ b/curator-x-async/src/test/resources/log4j.properties
@@ -0,0 +1,27 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+log4j.rootLogger=ERROR, console
+
+log4j.logger.org.apache.curator=DEBUG, console
+log4j.additivity.org.apache.curator=false
+
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+log4j.appender.console.layout.ConversionPattern=%-5p %c %x %m [%t]%n