@Override
  @SuppressFBWarnings("AT_OPERATION_SEQUENCE_ON_CONCURRENT_ABSTRACTION")
  public Object invoke(Object proxy, Method method, Object[] args) {
    if (args == null && "toString".equals(method.getName())) {
      return toString();
    } else if (args != null && args.length == 1 && "equals".equals(method.getName())) {
      return equals(args[0]);
    } else if (args == null && "hashCode".equals(method.getName())) {
      return hashCode();
    } else if (args == null && "outputRuntimeOptions".equals(method.getName())) {
      return outputRuntimeOptions((PipelineOptions) proxy);
    } else if (args == null && "revision".equals(method.getName())) {
      return revision.get();
    } else if (args != null && "as".equals(method.getName()) && args[0] instanceof Class) {
      @SuppressWarnings("unchecked")
      Class<? extends PipelineOptions> clazz = (Class<? extends PipelineOptions>) args[0];
      return as(clazz);
    } else if (args != null
        && "populateDisplayData".equals(method.getName())
        && args[0] instanceof DisplayData.Builder) {
      @SuppressWarnings("unchecked")
      DisplayData.Builder builder = (DisplayData.Builder) args[0];
      builder.delegate(new PipelineOptionsDisplayData());
      return Void.TYPE;
    }
    String methodName = method.getName();

    ComputedProperties properties = computedProperties;
    if (properties.gettersToPropertyNames.containsKey(methodName)) {
      String propertyName = properties.gettersToPropertyNames.get(methodName);
      // we can't use computeIfAbsent here because evaluating the default may cause more properties
      // to be evaluated, and computeIfAbsent is not re-entrant.
      if (!options.containsKey(propertyName)) {
        // Lazy bind the default to the method.
        Object value =
            jsonOptions.containsKey(propertyName)
                ? getValueFromJson(propertyName, method)
                : getDefault((PipelineOptions) proxy, method);
        options.put(propertyName, BoundValue.fromDefault(value));
      }
      return options.get(propertyName).getValue();
    } else if (properties.settersToPropertyNames.containsKey(methodName)) {
      BoundValue prev =
          options.put(
              properties.settersToPropertyNames.get(methodName),
              BoundValue.fromExplicitOption(args[0]));
      if (prev == null ? args[0] != null : !Objects.equals(args[0], prev.getValue())) {
        revision.incrementAndGet();
      }
      return Void.TYPE;
    }
    throw new RuntimeException(
        "Unknown method [" + method + "] invoked with args [" + Arrays.toString(args) + "].");
  }
@Test
  public void testInvokeWithUnknownMethod() throws Exception {
    expectedException.expect(RuntimeException.class);
    expectedException.expectMessage(
        "Unknown method [public abstract void "
            + "org.apache.beam.sdk.options.ProxyInvocationHandlerTest$UnknownMethod.unknownMethod()] "
            + "invoked with args [null].");

    ProxyInvocationHandler handler = new ProxyInvocationHandler(Maps.newHashMap());
    handler.invoke(handler, UnknownMethod.class.getMethod("unknownMethod"), null);
  }