Conduit.java

/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed 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 androidx.test.espresso.web.bridge;

import static androidx.test.internal.util.Checks.checkNotNull;

import androidx.concurrent.futures.ResolvableFuture;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CanIgnoreReturnValue;

/**
 * A mechanism to get results out of a Javascript context and into a Java context.
 *
 * <p>Users can get instances of this class via JavaScriptBridge.makeConduit(). Each conduit can be
 * used once (and only once) to transmit results. Before evaluating javascript via a loadUrl call
 * the caller should call wrapScriptInConduit with the script to be evaluated. The script is wrapped
 * up into an additional handler statement which forwards the result of the script to the
 * JavaScriptBridge object. After calling loadUrl the caller can use the getResult method to get a
 * Future which will contain the result of the javascript execution.
 */
public final class Conduit {
  private final String bridgeName;
  private final String errorMethod;
  private final String successMethod;
  private final String token;
  private final ResolvableFuture<String> jsResult;

  private Conduit(Builder builder) {
    this.bridgeName = checkNotNull(builder.bridgeName);
    this.errorMethod = checkNotNull(builder.errorMethod);
    this.successMethod = checkNotNull(builder.successMethod);
    this.token = checkNotNull(builder.token);
    this.jsResult = checkNotNull(builder.jsResult);
  }

  /**
   * Takes Javascript code and wraps it within a statement that will pipe the results of
   * evaluation to the ListenableFuture this conduit holds.
   */
  public String wrapScriptInConduit(String script) {
    checkNotNull(script);
    return wrapScriptInConduit(new StringBuilder(script)).toString();
  }

  /**
   * Wraps a script within additional javascript code that will allow the function to
   * return its results back thru this conduit.
   *
   * @param script the buffer holding the script, it will be modified in place.
   * @return the StringBuilder passed in.
   */
  public StringBuilder wrapScriptInConduit(StringBuilder script) {
    String preamble = "try{" +
        "window." + bridgeName + "." + successMethod + "('" + token + "', ";
    script.insert(0, preamble)
        .append(");")
        .append("}catch(e){")
        .append("window.").append(bridgeName).append(".").append(errorMethod)
        .append("('").append(token).append("', 'error!');}");
    return script;
  }

  /**
   * The future that will be resolved when the Javascript evaluation completes.
   */
  public ListenableFuture<String> getResult() {
    return jsResult;
  }

  /** Allows JavascriptBoundBridge to set the result of javascript execution. */
  ResolvableFuture<String> internalGetResult() {
    return jsResult;
  }

  String getToken() {
    return token;
  }

  static class Builder {
    private String bridgeName;
    private String errorMethod;
    private String successMethod;
    private String token;
    private ResolvableFuture<String> jsResult;

    @CanIgnoreReturnValue
    public Builder withBridgeName(String bridgeName) {
      this.bridgeName = checkNotNull(bridgeName);
      return this;
    }

    @CanIgnoreReturnValue
    public Builder withErrorMethod(String errorMethod) {
      this.errorMethod = checkNotNull(errorMethod);
      return this;
    }

    @CanIgnoreReturnValue
    public Builder withSuccessMethod(String successMethod) {
      this.successMethod = checkNotNull(successMethod);
      return this;
    }

    @CanIgnoreReturnValue
    public Builder withToken(String token) {
      this.token = checkNotNull(token);
      return this;
    }

    @CanIgnoreReturnValue
    public Builder withJsResult(ResolvableFuture<String> jsResult) {
      this.jsResult = checkNotNull(jsResult);
      return this;
    }

    public Conduit build() {
      return new Conduit(this);
    }
  }
}