1bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor/*
21d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert * Copyright (C) 2006 The Guava Authors
3bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor *
4bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor * Licensed under the Apache License, Version 2.0 (the "License");
5bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor * you may not use this file except in compliance with the License.
6bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor * You may obtain a copy of the License at
7bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor *
8bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor * http://www.apache.org/licenses/LICENSE-2.0
9bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor *
10bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor * Unless required by applicable law or agreed to in writing, software
11bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor * distributed under the License is distributed on an "AS IS" BASIS,
12bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor * See the License for the specific language governing permissions and
14bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor * limitations under the License.
15bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor */
16bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor
17bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorpackage com.google.common.util.concurrent;
18bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor
19bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorimport static com.google.common.base.Preconditions.checkArgument;
20bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorimport static com.google.common.base.Preconditions.checkNotNull;
211d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert
221d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringertimport com.google.common.annotations.Beta;
231d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringertimport com.google.common.collect.ObjectArrays;
24bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorimport com.google.common.collect.Sets;
25bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor
26bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorimport java.lang.reflect.InvocationHandler;
27bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorimport java.lang.reflect.InvocationTargetException;
28bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorimport java.lang.reflect.Method;
29bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorimport java.lang.reflect.Proxy;
30bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorimport java.util.Set;
31bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorimport java.util.concurrent.Callable;
32bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorimport java.util.concurrent.ExecutionException;
33bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorimport java.util.concurrent.ExecutorService;
34bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorimport java.util.concurrent.Executors;
35bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorimport java.util.concurrent.Future;
36bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorimport java.util.concurrent.TimeUnit;
37bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnorimport java.util.concurrent.TimeoutException;
38bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor
39bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor/**
40bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor * A TimeLimiter that runs method calls in the background using an
41bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor * {@link ExecutorService}.  If the time limit expires for a given method call,
42bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor * the thread running the call will be interrupted.
43bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor *
44bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor * @author Kevin Bourrillion
451d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert * @since 1.0
46bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor */
471d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert@Beta
481d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringertpublic final class SimpleTimeLimiter implements TimeLimiter {
49bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor
50bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  private final ExecutorService executor;
51bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor
52bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  /**
53bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   * Constructs a TimeLimiter instance using the given executor service to
54bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   * execute proxied method calls.
55bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   * <p>
56bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   * <b>Warning:</b> using a bounded executor
57bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   * may be counterproductive!  If the thread pool fills up, any time callers
58bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   * spend waiting for a thread may count toward their time limit, and in
59bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   * this case the call may even time out before the target method is ever
60bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   * invoked.
61bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   *
62bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   * @param executor the ExecutorService that will execute the method calls on
63bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   *     the target objects; for example, a {@link
641d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert   *     Executors#newCachedThreadPool()}.
65bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   */
66bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  public SimpleTimeLimiter(ExecutorService executor) {
671d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert    this.executor = checkNotNull(executor);
68bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  }
69bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor
70bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  /**
71bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   * Constructs a TimeLimiter instance using a {@link
721d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert   * Executors#newCachedThreadPool()} to execute proxied method calls.
73bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   *
74bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   * <p><b>Warning:</b> using a bounded executor may be counterproductive! If
75bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   * the thread pool fills up, any time callers spend waiting for a thread may
76bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   * count toward their time limit, and in this case the call may even time out
77bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   * before the target method is ever invoked.
78bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor   */
79bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  public SimpleTimeLimiter() {
80bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    this(Executors.newCachedThreadPool());
81bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  }
82bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor
831d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert  @Override
84bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  public <T> T newProxy(final T target, Class<T> interfaceType,
85bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor      final long timeoutDuration, final TimeUnit timeoutUnit) {
86bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    checkNotNull(target);
87bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    checkNotNull(interfaceType);
88bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    checkNotNull(timeoutUnit);
89bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    checkArgument(timeoutDuration > 0, "bad timeout: " + timeoutDuration);
90bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    checkArgument(interfaceType.isInterface(),
91bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor        "interfaceType must be an interface type");
92bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor
93bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    final Set<Method> interruptibleMethods
94bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor        = findInterruptibleMethods(interfaceType);
95bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor
96bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    InvocationHandler handler = new InvocationHandler() {
971d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert      @Override
98bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor      public Object invoke(Object obj, final Method method, final Object[] args)
99bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor          throws Throwable {
100bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor        Callable<Object> callable = new Callable<Object>() {
1011d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert          @Override
102bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor          public Object call() throws Exception {
103bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor            try {
104bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor              return method.invoke(target, args);
105bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor            } catch (InvocationTargetException e) {
1061d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert              throwCause(e, false);
107bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor              throw new AssertionError("can't get here");
108bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor            }
109bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor          }
110bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor        };
111bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor        return callWithTimeout(callable, timeoutDuration, timeoutUnit,
112bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor            interruptibleMethods.contains(method));
113bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor      }
114bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    };
115bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    return newProxy(interfaceType, handler);
116bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  }
117bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor
118bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  // TODO: should this actually throw only ExecutionException?
1191d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert  @Override
120bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  public <T> T callWithTimeout(Callable<T> callable, long timeoutDuration,
121bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor      TimeUnit timeoutUnit, boolean amInterruptible) throws Exception {
122bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    checkNotNull(callable);
123bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    checkNotNull(timeoutUnit);
1241d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert    checkArgument(timeoutDuration > 0, "timeout must be positive: %s",
1251d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert        timeoutDuration);
126bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    Future<T> future = executor.submit(callable);
127bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    try {
128bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor      if (amInterruptible) {
129bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor        try {
130bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor          return future.get(timeoutDuration, timeoutUnit);
131bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor        } catch (InterruptedException e) {
132bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor          future.cancel(true);
133bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor          throw e;
134bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor        }
135bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor      } else {
1361d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert        return Uninterruptibles.getUninterruptibly(future,
1371d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert            timeoutDuration, timeoutUnit);
138bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor      }
139bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    } catch (ExecutionException e) {
1401d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert      throw throwCause(e, true);
141bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    } catch (TimeoutException e) {
142bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor      future.cancel(true);
143bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor      throw new UncheckedTimeoutException(e);
144bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    }
145bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  }
146bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor
1471d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert  private static Exception throwCause(Exception e, boolean combineStackTraces)
1481d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert      throws Exception {
1491d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert    Throwable cause = e.getCause();
1501d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert    if (cause == null) {
1511d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert      throw e;
1521d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert    }
1531d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert    if (combineStackTraces) {
1541d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert      StackTraceElement[] combined = ObjectArrays.concat(cause.getStackTrace(),
1551d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert          e.getStackTrace(), StackTraceElement.class);
1561d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert      cause.setStackTrace(combined);
1571d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert    }
1581d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert    if (cause instanceof Exception) {
1591d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert      throw (Exception) cause;
1601d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert    }
1611d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert    if (cause instanceof Error) {
1621d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert      throw (Error) cause;
1631d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert    }
1641d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert    // The cause is a weird kind of Throwable, so throw the outer exception.
1651d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert    throw e;
1661d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert  }
1671d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert
168bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  private static Set<Method> findInterruptibleMethods(Class<?> interfaceType) {
169bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    Set<Method> set = Sets.newHashSet();
170bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    for (Method m : interfaceType.getMethods()) {
171bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor      if (declaresInterruptedEx(m)) {
172bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor        set.add(m);
173bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor      }
174bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    }
175bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    return set;
176bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  }
177bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor
178bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  private static boolean declaresInterruptedEx(Method method) {
179bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    for (Class<?> exType : method.getExceptionTypes()) {
180bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor      // debate: == or isAssignableFrom?
181bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor      if (exType == InterruptedException.class) {
182bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor        return true;
183bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor      }
184bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    }
185bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    return false;
186bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  }
187bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor
1881d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert  // TODO: replace with version in common.reflect if and when it's open-sourced
189bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  private static <T> T newProxy(
190bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor      Class<T> interfaceType, InvocationHandler handler) {
1911d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert    Object object = Proxy.newProxyInstance(interfaceType.getClassLoader(),
1921d580d0f6ee4f21eb309ba7b509d2c6d671c4044Bjorn Bringert        new Class<?>[] { interfaceType }, handler);
193bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor    return interfaceType.cast(object);
194bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor  }
195bfe2dd089341dcb4c1fb65a5b6b077ad0ebbf6dcDan Egnor}
196