Swap Server from Builder to HandlerRegistry.

The idea is that Server would be provided a HandlerRegistry at construction time.

This has a simplistic implementation of HandlerRegistry. If we like the API, then I can implement a lock-less version as we find need.

Most classes are still under Server so that it is obvious what changes were made. Moving things out of Server would be a separate CL.
-------------
Created by MOE: http://code.google.com/p/moe-java
MOE_MIGRATED_REVID=73334671
diff --git a/core/src/main/java/com/google/net/stubby/HandlerRegistry.java b/core/src/main/java/com/google/net/stubby/HandlerRegistry.java
new file mode 100644
index 0000000..33ad884
--- /dev/null
+++ b/core/src/main/java/com/google/net/stubby/HandlerRegistry.java
@@ -0,0 +1,34 @@
+package com.google.net.stubby;
+
+import com.google.net.stubby.Server.MethodDefinition;
+import com.google.net.stubby.Server.ServiceDefinition;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** Registry of services and their methods for dispatching incoming calls. */
+@ThreadSafe
+public abstract class HandlerRegistry {
+  /** Lookup full method name, starting with '/'. Returns {@code null} if method not found. */
+  @Nullable
+  public abstract Method lookupMethod(String methodName);
+
+  /** A method definition and its parent's service definition. */
+  public static final class Method {
+    private final ServiceDefinition serviceDef;
+    private final MethodDefinition methodDef;
+
+    public Method(ServiceDefinition serviceDef, MethodDefinition methodDef) {
+      this.serviceDef = serviceDef;
+      this.methodDef = methodDef;
+    }
+
+    public ServiceDefinition getServiceDefinition() {
+      return serviceDef;
+    }
+
+    public MethodDefinition getMethodDefinition() {
+      return methodDef;
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/net/stubby/MutableHandlerRegistry.java b/core/src/main/java/com/google/net/stubby/MutableHandlerRegistry.java
new file mode 100644
index 0000000..855777a
--- /dev/null
+++ b/core/src/main/java/com/google/net/stubby/MutableHandlerRegistry.java
@@ -0,0 +1,20 @@
+package com.google.net.stubby;
+
+import com.google.net.stubby.Server.ServiceDefinition;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** Mutable registry of services and their methods for dispatching incoming calls. */
+@ThreadSafe
+public abstract class MutableHandlerRegistry extends HandlerRegistry {
+  /**
+   * Returns {@code null}, or previous service if {@code service} replaced an existing service.
+   */
+  @Nullable
+  public abstract ServiceDefinition addService(ServiceDefinition service);
+
+  /** Returns {@code false} if {@code service} was not registered. */
+  @Nullable
+  public abstract boolean removeService(ServiceDefinition service);
+}
diff --git a/core/src/main/java/com/google/net/stubby/MutableHandlerRegistryImpl.java b/core/src/main/java/com/google/net/stubby/MutableHandlerRegistryImpl.java
new file mode 100644
index 0000000..f6d686e
--- /dev/null
+++ b/core/src/main/java/com/google/net/stubby/MutableHandlerRegistryImpl.java
@@ -0,0 +1,52 @@
+package com.google.net.stubby;
+
+import com.google.net.stubby.Server.MethodDefinition;
+import com.google.net.stubby.Server.ServiceDefinition;
+
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** Mutable registry implementation of services and their methods for dispatching incoming calls. */
+@ThreadSafe
+public final class MutableHandlerRegistryImpl extends MutableHandlerRegistry {
+  private final ConcurrentMap<String, ServiceDefinition> services
+      = new ConcurrentHashMap<String, ServiceDefinition>();
+
+  @Override
+  @Nullable
+  public ServiceDefinition addService(ServiceDefinition service) {
+    return services.put(service.getName(), service);
+  }
+
+  @Override
+  public boolean removeService(ServiceDefinition service) {
+    return services.remove(service.getName(), service);
+  }
+
+  @Override
+  @Nullable
+  public Method lookupMethod(String methodName) {
+    methodName = methodName.replace('.', '/');
+    if (!methodName.startsWith("/")) {
+      return null;
+    }
+    methodName = methodName.substring(1);
+    int index = methodName.lastIndexOf("/");
+    if (index == -1) {
+      return null;
+    }
+    ServiceDefinition service = services.get(methodName.substring(0, index));
+    if (service == null) {
+      return null;
+    }
+    MethodDefinition method = service.getMethod(methodName.substring(index + 1));
+    if (method == null) {
+      return null;
+    }
+    return new Method(service, method);
+  }
+}
diff --git a/core/src/main/java/com/google/net/stubby/Server.java b/core/src/main/java/com/google/net/stubby/Server.java
index 5b6a85c..1ace9fb 100644
--- a/core/src/main/java/com/google/net/stubby/Server.java
+++ b/core/src/main/java/com/google/net/stubby/Server.java
@@ -1,10 +1,14 @@
 package com.google.net.stubby;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.Service;
 
 import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
@@ -15,24 +19,21 @@
  */
 @ThreadSafe
 public interface Server extends Service {
-  /** Builder that Servers are expected to provide for constructing new instances. */
-  abstract class Builder {
-    public abstract Builder addService(ServiceDef service);
-    public abstract Server build();
-  }
-
   /** Definition of a service to be exposed via a Server. */
-  final class ServiceDef {
-    public static ServiceDef.Builder builder(String serviceName) {
-      return new ServiceDef.Builder(serviceName);
+  final class ServiceDefinition {
+    public static ServiceDefinition.Builder builder(String serviceName) {
+      return new ServiceDefinition.Builder(serviceName);
     }
 
     private final String name;
-    private final ImmutableList<MethodDef> methods;
+    private final ImmutableList<MethodDefinition> methods;
+    private final ImmutableMap<String, MethodDefinition> methodLookup;
 
-    private ServiceDef(String name, ImmutableList<MethodDef> methods) {
+    private ServiceDefinition(String name, ImmutableList<MethodDefinition> methods,
+        Map<String, MethodDefinition> methodLookup) {
       this.name = name;
       this.methods = methods;
+      this.methodLookup = ImmutableMap.copyOf(methodLookup);
     }
 
     /** Simple name of the service. It is not an absolute path. */
@@ -40,14 +41,20 @@
       return name;
     }
 
-    public ImmutableList<MethodDef> getMethods() {
+    public ImmutableList<MethodDefinition> getMethods() {
       return methods;
     }
 
+    public MethodDefinition getMethod(String name) {
+      return methodLookup.get(name);
+    }
+
     /** Builder for constructing Service instances. */
     public static final class Builder {
       private final String serviceName;
-      private final ImmutableList.Builder<MethodDef> methods = ImmutableList.builder();
+      private final ImmutableList.Builder<MethodDefinition> methods = ImmutableList.builder();
+      private final Map<String, MethodDefinition> methodLookup
+          = new HashMap<String, MethodDefinition>();
 
       private Builder(String serviceName) {
         this.serviceName = serviceName;
@@ -63,29 +70,36 @@
        */
       public <ReqT, RespT> Builder addMethod(String name, Marshaller<ReqT> requestMarshaller,
           Marshaller<RespT> responseMarshaller, CallHandler<ReqT, RespT> handler) {
-        methods.add(
-            new MethodDef<ReqT, RespT>(name, requestMarshaller, responseMarshaller, handler));
+        Preconditions.checkNotNull(name, "name must not be null");
+        if (methodLookup.containsKey(name)) {
+          throw new IllegalStateException("Method by same name already registered");
+        }
+        MethodDefinition def = new MethodDefinition<ReqT, RespT>(name,
+            Preconditions.checkNotNull(requestMarshaller, "requestMarshaller must not be null"),
+            Preconditions.checkNotNull(responseMarshaller, "responseMarshaller must not be null"),
+            Preconditions.checkNotNull(handler, "handler must not be null"));
+        methodLookup.put(name, def);
+        methods.add(def);
         return this;
       }
 
-      /** Construct new ServiceDef. */
-      public ServiceDef build() {
-        return new ServiceDef(serviceName, methods.build());
+      /** Construct new ServiceDefinition. */
+      public ServiceDefinition build() {
+        return new ServiceDefinition(serviceName, methods.build(), methodLookup);
       }
     }
   }
 
   /** Definition of a method supported by a service. */
-  final class MethodDef<RequestT, ResponseT> {
+  final class MethodDefinition<RequestT, ResponseT> {
     private final String name;
     private final Marshaller<RequestT> requestMarshaller;
     private final Marshaller<ResponseT> responseMarshaller;
     private final CallHandler<RequestT, ResponseT> handler;
 
-    // MethodDef has no way of public creation, because all parameters are required. A builder
-    // wouldn't have any methods other than build(). addMethod() can be overriden if we ever need to
-    // extend what MethodDef contains or if we add a Builder or similar.
-    private MethodDef(String name, Marshaller<RequestT> requestMarshaller,
+    // MethodDefinition has no form of public construction. It is only created within the context of
+    // a ServiceDefinition.Builder.
+    private MethodDefinition(String name, Marshaller<RequestT> requestMarshaller,
         Marshaller<ResponseT> responseMarshaller, CallHandler<RequestT, ResponseT> handler) {
       this.name = name;
       this.requestMarshaller = requestMarshaller;
@@ -115,24 +129,47 @@
   }
 
   /**
-   * Class to begin processing incoming RPCs. Advanced applications and generated code implement
+   * Interface for intercepting incoming RPCs before the handler receives them.
+   */
+  @ThreadSafe
+  interface Interceptor {
+    /**
+     * Intercept a new call. General semantics of {@link Server.CallHandler#startCall} apply. {@code
+     * next} may only be called once. Returned listener must not be {@code null}.
+     *
+     * <p>If the implementation throws an exception, {@code call} will be closed with an error.
+     * Implementations must not throw an exception if they started processing that may use {@code
+     * call} on another thread.
+     *
+     * @param method metadata concerning the call
+     * @param call object for responding
+     * @param next next processor in the interceptor chain
+     * @return listener for processing incoming messages for {@code call}
+     */
+    <ReqT, RespT> Server.Call.Listener<ReqT> interceptCall(MethodDescriptor<ReqT, RespT> method,
+        Server.Call<ReqT, RespT> call, CallHandler<ReqT, RespT> next);
+  }
+
+  /**
+   * Interface to begin processing incoming RPCs. Advanced applications and generated code implement
    * this interface to implement service methods.
    */
   @ThreadSafe
   interface CallHandler<ReqT, RespT> {
     /**
-     * Produce a listener for the incoming call. Implementations are free to call methods on {@code
-     * call} before this method has returned.
+     * Produce a non-{@code null} listener for the incoming call. Implementations are free to call
+     * methods on {@code call} before this method has returned.
      *
-     * <p>If the implementation throws an exception or returns {@code null}, {@code call} will be
-     * closed with an error.
+     * <p>If the implementation throws an exception, {@code call} will be closed with an error.
+     * Implementations must not throw an exception if they started processing that may use {@code
+     * call} on another thread.
      *
-     * @param call object for responding
      * @param method metadata concerning the call
+     * @param call object for responding
      * @return listener for processing incoming messages for {@code call}
      */
-    Server.Call.Listener<ReqT> startCall(Server.Call<ReqT, RespT> call,
-        MethodDescriptor<ReqT, RespT> method);
+    Server.Call.Listener<ReqT> startCall(MethodDescriptor<ReqT, RespT> method,
+        Server.Call<ReqT, RespT> call);
   }
 
   /**
diff --git a/core/src/main/java/com/google/net/stubby/ServerInterceptors.java b/core/src/main/java/com/google/net/stubby/ServerInterceptors.java
new file mode 100644
index 0000000..1b82328
--- /dev/null
+++ b/core/src/main/java/com/google/net/stubby/ServerInterceptors.java
@@ -0,0 +1,94 @@
+package com.google.net.stubby;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.net.stubby.Server.CallHandler;
+import com.google.net.stubby.Server.Interceptor;
+import com.google.net.stubby.Server.MethodDefinition;
+import com.google.net.stubby.Server.ServiceDefinition;
+
+import java.util.List;
+import java.util.Iterator;
+
+/** Utility class for {@link Server.Interceptor}s. */
+public class ServerInterceptors {
+  // Prevent instantiation
+  private ServerInterceptors() {}
+
+  /**
+   * Create a new {@code ServiceDefinition} whose {@link Server.CallHandler}s will call {@code
+   * interceptors} before calling the pre-existing {@code CallHandler}.
+   */
+  public static ServiceDefinition intercept(ServiceDefinition serviceDef,
+      List<Interceptor> interceptors) {
+    Preconditions.checkNotNull(serviceDef);
+    List<Interceptor> immutableInterceptors = ImmutableList.copyOf(interceptors);
+    if (immutableInterceptors.isEmpty()) {
+      return serviceDef;
+    }
+    ServiceDefinition.Builder serviceDefBuilder = ServiceDefinition.builder(serviceDef.getName());
+    for (MethodDefinition<?, ?> method : serviceDef.getMethods()) {
+      wrapAndAddMethod(serviceDefBuilder, method, immutableInterceptors);
+    }
+    return serviceDefBuilder.build();
+  }
+
+  private static <ReqT, RespT> void wrapAndAddMethod(ServiceDefinition.Builder serviceDefBuilder,
+      MethodDefinition<ReqT, RespT> method, List<Interceptor> interceptors) {
+    CallHandler<ReqT, RespT> callHandler
+        = InterceptCallHandler.create(interceptors, method.getCallHandler());
+    serviceDefBuilder.addMethod(method.getName(), method.getRequestMarshaller(),
+        method.getResponseMarshaller(), callHandler);
+  }
+
+  private static class InterceptCallHandler<ReqT, RespT> implements CallHandler<ReqT, RespT> {
+    public static <ReqT, RespT> InterceptCallHandler<ReqT, RespT> create(
+        List<Interceptor> interceptors, CallHandler<ReqT, RespT> callHandler) {
+      return new InterceptCallHandler<ReqT, RespT>(interceptors, callHandler);
+    }
+
+    private final List<Interceptor> interceptors;
+    private final CallHandler<ReqT, RespT> callHandler;
+
+    private InterceptCallHandler(List<Interceptor> interceptors,
+        CallHandler<ReqT, RespT> callHandler) {
+      this.interceptors = interceptors;
+      this.callHandler = callHandler;
+    }
+
+    @Override
+    public Server.Call.Listener<ReqT> startCall(MethodDescriptor<ReqT, RespT> method,
+        Server.Call<ReqT, RespT> call) {
+      return ProcessInterceptorsCallHandler.create(interceptors.iterator(), callHandler)
+          .startCall(method, call);
+    }
+  }
+
+  private static class ProcessInterceptorsCallHandler<ReqT, RespT>
+      implements CallHandler<ReqT, RespT> {
+    public static <ReqT, RespT> ProcessInterceptorsCallHandler<ReqT, RespT> create(
+        Iterator<Interceptor> interceptors, CallHandler<ReqT, RespT> callHandler) {
+      return new ProcessInterceptorsCallHandler<ReqT, RespT>(interceptors, callHandler);
+    }
+
+    private Iterator<Interceptor> interceptors;
+    private final CallHandler<ReqT, RespT> callHandler;
+
+    private ProcessInterceptorsCallHandler(Iterator<Interceptor> interceptors,
+        CallHandler<ReqT, RespT> callHandler) {
+      this.interceptors = interceptors;
+      this.callHandler = callHandler;
+    }
+
+    @Override
+    public Server.Call.Listener<ReqT> startCall(MethodDescriptor<ReqT, RespT> method,
+        Server.Call<ReqT, RespT> call) {
+      if (interceptors != null && interceptors.hasNext()) {
+        return interceptors.next().interceptCall(method, call, this);
+      } else {
+        interceptors = null;
+        return callHandler.startCall(method, call);
+      }
+    }
+  }
+}
diff --git a/core/src/test/java/com/google/net/stubby/MutableHandlerRegistryImplTest.java b/core/src/test/java/com/google/net/stubby/MutableHandlerRegistryImplTest.java
new file mode 100644
index 0000000..ff42f7c
--- /dev/null
+++ b/core/src/test/java/com/google/net/stubby/MutableHandlerRegistryImplTest.java
@@ -0,0 +1,147 @@
+package com.google.net.stubby;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import com.google.net.stubby.Server.CallHandler;
+import com.google.net.stubby.Server.MethodDefinition;
+import com.google.net.stubby.Server.ServiceDefinition;
+import com.google.net.stubby.HandlerRegistry.Method;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+/** Unit tests for {@link MutableHandlerRegistryImpl}. */
+@RunWith(JUnit4.class)
+public class MutableHandlerRegistryImplTest {
+  private MutableHandlerRegistry registry = new MutableHandlerRegistryImpl();
+  private Marshaller<String> requestMarshaller = mock(Marshaller.class);
+  private Marshaller<Integer> responseMarshaller = mock(Marshaller.class);
+  private CallHandler<String, Integer> handler = mock(CallHandler.class);
+  private ServiceDefinition basicServiceDefinition = ServiceDefinition.builder("basic")
+        .addMethod("flow", requestMarshaller, responseMarshaller, handler).build();
+  private MethodDefinition flowMethodDefinition = basicServiceDefinition.getMethods().get(0);
+  private ServiceDefinition multiServiceDefinition = ServiceDefinition.builder("multi")
+        .addMethod("couple", requestMarshaller, responseMarshaller, handler)
+        .addMethod("few", requestMarshaller, responseMarshaller, handler).build();
+  private MethodDefinition coupleMethodDefinition = multiServiceDefinition.getMethod("couple");
+  private MethodDefinition fewMethodDefinition = multiServiceDefinition.getMethod("few");
+
+  @After
+  public void makeSureMocksUnused() {
+    Mockito.verifyZeroInteractions(requestMarshaller);
+    Mockito.verifyZeroInteractions(responseMarshaller);
+    Mockito.verifyZeroInteractions(handler);
+  }
+
+  @Test
+  public void simpleLookup() {
+    assertNull(registry.addService(basicServiceDefinition));
+    Method method = registry.lookupMethod("/basic.flow");
+    assertSame(flowMethodDefinition, method.getMethodDefinition());
+    assertSame(basicServiceDefinition, method.getServiceDefinition());
+    method = registry.lookupMethod("/basic.flow");
+    assertSame(flowMethodDefinition, method.getMethodDefinition());
+    assertSame(basicServiceDefinition, method.getServiceDefinition());
+    method = registry.lookupMethod("/basic/flow");
+    assertSame(flowMethodDefinition, method.getMethodDefinition());
+    assertSame(basicServiceDefinition, method.getServiceDefinition());
+
+    assertNull(registry.lookupMethod("basic.flow"));
+    assertNull(registry.lookupMethod("/basic.basic"));
+    assertNull(registry.lookupMethod("/flow.flow"));
+    assertNull(registry.lookupMethod("/completely.random"));
+  }
+
+  @Test
+  public void multiServiceLookup() {
+    assertNull(registry.addService(basicServiceDefinition));
+    assertNull(registry.addService(multiServiceDefinition));
+
+    Method method = registry.lookupMethod("/basic.flow");
+    assertSame(flowMethodDefinition, method.getMethodDefinition());
+    assertSame(basicServiceDefinition, method.getServiceDefinition());
+    method = registry.lookupMethod("/multi.couple");
+    assertSame(coupleMethodDefinition, method.getMethodDefinition());
+    assertSame(multiServiceDefinition, method.getServiceDefinition());
+    method = registry.lookupMethod("/multi.few");
+    assertSame(fewMethodDefinition, method.getMethodDefinition());
+    assertSame(multiServiceDefinition, method.getServiceDefinition());
+  }
+
+  @Test
+  public void removeAndLookup() {
+    assertNull(registry.addService(multiServiceDefinition));
+    assertNotNull(registry.lookupMethod("/multi.couple"));
+    assertNotNull(registry.lookupMethod("/multi.few"));
+    assertTrue(registry.removeService(multiServiceDefinition));
+    assertNull(registry.lookupMethod("/multi.couple"));
+    assertNull(registry.lookupMethod("/multi.few"));
+  }
+
+  @Test
+  public void replaceAndLookup() {
+    assertNull(registry.addService(basicServiceDefinition));
+    assertNotNull(registry.lookupMethod("/basic.flow"));
+    ServiceDefinition replaceServiceDefinition = ServiceDefinition.builder("basic")
+        .addMethod("another", requestMarshaller, responseMarshaller, handler).build();
+    MethodDefinition anotherMethodDefinition = replaceServiceDefinition.getMethods().get(0);
+    assertSame(basicServiceDefinition, registry.addService(replaceServiceDefinition));
+
+    assertNull(registry.lookupMethod("/basic.flow"));
+    Method method = registry.lookupMethod("/basic.another");
+    assertSame(anotherMethodDefinition, method.getMethodDefinition());
+    assertSame(replaceServiceDefinition, method.getServiceDefinition());
+  }
+
+  @Test
+  public void removeSameSucceeds() {
+    assertNull(registry.addService(basicServiceDefinition));
+    assertTrue(registry.removeService(basicServiceDefinition));
+  }
+
+  @Test
+  public void doubleRemoveFails() {
+    assertNull(registry.addService(basicServiceDefinition));
+    assertTrue(registry.removeService(basicServiceDefinition));
+    assertFalse(registry.removeService(basicServiceDefinition));
+  }
+
+  @Test
+  public void removeMissingFails() {
+    assertFalse(registry.removeService(basicServiceDefinition));
+  }
+
+  @Test
+  public void removeMissingNameConflictFails() {
+    assertNull(registry.addService(basicServiceDefinition));
+    assertFalse(registry.removeService(ServiceDefinition.builder("basic").build()));
+  }
+
+  @Test
+  public void initialAddReturnsNull() {
+    assertNull(registry.addService(basicServiceDefinition));
+    assertNull(registry.addService(multiServiceDefinition));
+  }
+
+  @Test
+  public void addAfterRemoveReturnsNull() {
+    assertNull(registry.addService(basicServiceDefinition));
+    assertTrue(registry.removeService(basicServiceDefinition));
+    assertNull(registry.addService(basicServiceDefinition));
+  }
+
+  @Test
+  public void addReturnsPrevious() {
+    assertNull(registry.addService(basicServiceDefinition));
+    assertSame(basicServiceDefinition,
+        registry.addService(ServiceDefinition.builder("basic").build()));
+  }
+}
diff --git a/core/src/test/java/com/google/net/stubby/ServerInterceptorsTest.java b/core/src/test/java/com/google/net/stubby/ServerInterceptorsTest.java
new file mode 100644
index 0000000..bc356d3
--- /dev/null
+++ b/core/src/test/java/com/google/net/stubby/ServerInterceptorsTest.java
@@ -0,0 +1,187 @@
+package com.google.net.stubby;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.same;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import com.google.net.stubby.Server.Interceptor;
+import com.google.net.stubby.Server.Call;
+import com.google.net.stubby.Server.CallHandler;
+import com.google.net.stubby.Server.MethodDefinition;
+import com.google.net.stubby.Server.ServiceDefinition;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Unit tests for {@link ServerInterceptors}. */
+@RunWith(JUnit4.class)
+public class ServerInterceptorsTest {
+  private Marshaller<String> requestMarshaller = mock(Marshaller.class);
+  private Marshaller<Integer> responseMarshaller = mock(Marshaller.class);
+  private CallHandler<String, Integer> handler = mock(CallHandler.class);
+  private Call.Listener<String> listener = mock(Call.Listener.class);
+  private MethodDescriptor<String, Integer> methodDescriptor = mock(MethodDescriptor.class);
+  private Call<String, Integer> call = mock(Call.class);
+  private ServiceDefinition serviceDefinition = ServiceDefinition.builder("basic")
+      .addMethod("flow", requestMarshaller, responseMarshaller, handler).build();
+
+  @Before
+  public void setUp() {
+    Mockito.when(handler.startCall(
+          Mockito.<MethodDescriptor<String, Integer>>any(), Mockito.<Call<String, Integer>>any()))
+        .thenReturn(listener);
+  }
+
+  @After
+  public void makeSureExpectedMocksUnused() {
+    verifyZeroInteractions(requestMarshaller);
+    verifyZeroInteractions(responseMarshaller);
+    verifyZeroInteractions(listener);
+    verifyZeroInteractions(methodDescriptor);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void npeForNullServiceDefinition() {
+    ServerInterceptors.intercept(null, Arrays.<Interceptor>asList());
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void npeForNullInterceptorList() {
+    ServerInterceptors.intercept(serviceDefinition, null);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void npeForNullInterceptor() {
+    ServerInterceptors.intercept(serviceDefinition, Arrays.asList((Interceptor) null));
+  }
+
+  @Test
+  public void noop() {
+    assertSame(serviceDefinition,
+        ServerInterceptors.intercept(serviceDefinition, Arrays.<Interceptor>asList()));
+  }
+
+  @Test
+  public void multipleInvocationsOfHandler() {
+    Interceptor interceptor = Mockito.spy(new NoopInterceptor());
+    ServiceDefinition intercepted
+        = ServerInterceptors.intercept(serviceDefinition, Arrays.asList(interceptor));
+    assertSame(listener,
+        intercepted.getMethods().get(0).getCallHandler().startCall(methodDescriptor, call));
+    verify(interceptor).interceptCall(same(methodDescriptor), same(call), anyCallHandler());
+    verify(handler).startCall(methodDescriptor, call);
+    verifyNoMoreInteractions(interceptor, handler);
+
+    assertSame(listener,
+        intercepted.getMethods().get(0).getCallHandler().startCall(methodDescriptor, call));
+    verify(interceptor, times(2))
+        .interceptCall(same(methodDescriptor), same(call), anyCallHandler());
+    verify(handler, times(2)).startCall(methodDescriptor, call);
+    verifyNoMoreInteractions(interceptor, handler);
+  }
+
+  @Test
+  public void correctHandlerCalled() {
+    CallHandler<String, Integer> handler2 = Mockito.mock(CallHandler.class);
+    serviceDefinition = ServiceDefinition.builder("basic")
+        .addMethod("flow", requestMarshaller, responseMarshaller, handler)
+        .addMethod("flow2", requestMarshaller, responseMarshaller, handler2).build();
+    ServiceDefinition intercepted = ServerInterceptors.intercept(
+        serviceDefinition, Arrays.<Interceptor>asList(new NoopInterceptor()));
+    intercepted.getMethod("flow").getCallHandler().startCall(methodDescriptor, call);
+    verify(handler).startCall(methodDescriptor, call);
+    verifyNoMoreInteractions(handler);
+    verifyZeroInteractions(handler2);
+
+    intercepted.getMethod("flow2").getCallHandler().startCall(methodDescriptor, call);
+    verify(handler2).startCall(methodDescriptor, call);
+    verifyNoMoreInteractions(handler);
+    verifyNoMoreInteractions(handler2);
+  }
+
+  @Test
+  public void ordered() {
+    final List<String> order = new ArrayList<String>();
+    handler = new CallHandler<String, Integer>() {
+          @Override
+          public Call.Listener<String> startCall(MethodDescriptor<String, Integer> method,
+              Call<String, Integer> call) {
+            order.add("handler");
+            return listener;
+          }
+        };
+    Interceptor interceptor1 = new Interceptor() {
+          @Override
+          public <ReqT, RespT> Call.Listener<ReqT> interceptCall(
+              MethodDescriptor<ReqT, RespT> method, Call<ReqT, RespT> call,
+              CallHandler<ReqT, RespT> next) {
+            order.add("i1");
+            return next.startCall(method, call);
+          }
+        };
+    Interceptor interceptor2 = new Interceptor() {
+          @Override
+          public <ReqT, RespT> Call.Listener<ReqT> interceptCall(
+              MethodDescriptor<ReqT, RespT> method, Call<ReqT, RespT> call,
+              CallHandler<ReqT, RespT> next) {
+            order.add("i2");
+            return next.startCall(method, call);
+          }
+        };
+    ServiceDefinition serviceDefinition = ServiceDefinition.builder("basic")
+        .addMethod("flow", requestMarshaller, responseMarshaller, handler).build();
+    ServiceDefinition intercepted = ServerInterceptors.intercept(
+        serviceDefinition, Arrays.asList(interceptor1, interceptor2));
+    assertSame(listener,
+        intercepted.getMethods().get(0).getCallHandler().startCall(methodDescriptor, call));
+    assertEquals(Arrays.asList("i1", "i2", "handler"), order);
+  }
+
+  @Test
+  public void argumentsPassed() {
+    final MethodDescriptor<String, Integer> method2 = mock(MethodDescriptor.class);
+    final Call<String, Integer> call2 = mock(Call.class);
+    final Call.Listener<String> listener2 = mock(Call.Listener.class);
+    Interceptor interceptor = new Interceptor() {
+          @Override
+          public <ReqT, RespT> Call.Listener<ReqT> interceptCall(
+              MethodDescriptor<ReqT, RespT> method, Call<ReqT, RespT> call,
+              CallHandler<ReqT, RespT> next) {
+            assertSame(method, methodDescriptor);
+            assertSame(call, ServerInterceptorsTest.this.call);
+            assertSame(listener, next.startCall((MethodDescriptor) method2, (Call) call2));
+            return (Call.Listener) listener2;
+          }
+        };
+    ServiceDefinition intercepted = ServerInterceptors.intercept(
+        serviceDefinition, Arrays.asList(interceptor));
+    assertSame(listener2,
+        intercepted.getMethods().get(0).getCallHandler().startCall(methodDescriptor, call));
+    verify(handler).startCall(method2, call2);
+  }
+
+  private CallHandler<String, Integer> anyCallHandler() {
+    return Mockito.<CallHandler<String, Integer>>any();
+  }
+
+  private static class NoopInterceptor implements Interceptor {
+    @Override
+    public <ReqT, RespT> Call.Listener<ReqT> interceptCall(MethodDescriptor<ReqT, RespT> method,
+        Call<ReqT, RespT> call, CallHandler<ReqT, RespT> next) {
+      return next.startCall(method, call);
+    }
+  }
+}