Beispiel #1
0
/**
 * Parent class for testing JAX-RS and Jersey-based applications using Jersey test framework.
 *
 * <p>At construction this class will obtain a {@link
 * org.glassfish.jersey.test.spi.TestContainerFactory test container factory} implementation.
 *
 * <p>Before each test method in an extending class is run the test container factory is used to
 * obtain a configured {@link org.glassfish.jersey.test.spi.TestContainer test container}. Then the
 * {@link TestContainer#start()} method is invoked on the configured test container. After each test
 * method has run, the {@link TestContainer#stop()} method is invoked on the test container. Stopped
 * test container generally shouldn't be again started for another test, rather a new test container
 * should be created. Every test method in the {@code JerseyTest} subclass can invoke the {@link
 * #client()} to obtain a JAX-RS {@link javax.ws.rs.client.Client}, from which {@link
 * javax.ws.rs.client.WebTarget} instances can be created to send arbitrary requests. Also, one of
 * the {@code target} methods ({@link #target()} or {@link #target(String)}) may be invoked to
 * obtain a JAX-RS {@link javax.ws.rs.client.WebTarget} instances from which requests can be sent to
 * and responses received from the Web application under test.
 *
 * <p>If a test container factory is not explicitly declared using the appropriate constructor (see
 * {@link #JerseyTest(TestContainerFactory)}) or by overriding the {@link
 * #getTestContainerFactory()} method, then a default test container factory will be obtained as
 * follows:
 *
 * <ol>
 *   <li>If a system property <tt>{@value
 *       org.glassfish.jersey.test.TestProperties#CONTAINER_FACTORY}</tt> is set and the value is a
 *       fully qualified class name of a class that extends from {@code TestContainerFactory} then
 *       the test container factory used by default will be an instance of that class. A {@link
 *       TestContainerException} will be thrown if the class cannot be loaded or instantiated.
 *   <li>Otherwise, {@code META-INF/services} locations on the class path will be scanned for
 *       implementation providers of {@code TestContainerFactory} SPI. If a single implementation is
 *       found, it will be used. If multiple implementations are found, the default <tt>{@value
 *       org.glassfish.jersey.test.TestProperties#CONTAINER_FACTORY}</tt> implementation will be
 *       selected if present, otherwise the first found implementation will be selected and a
 *       warning message will be logged.
 *   <li>If no {@code TestContainerFactory} has been selected in the steps above, Jersey test
 *       framework will try to instantiate the default test container factory implementation (
 *       <tt>{@value org.glassfish.jersey.test.TestProperties#DEFAULT_CONTAINER_FACTORY}</tt>)
 *       directly. A {@link TestContainerException} will be thrown if this class cannot be loaded or
 *       instantiated.
 * </ol>
 *
 * <p>The test container is configured by a {@link DeploymentContext} that is either provided by
 * subclass or automatically created by {@code JerseyTest} based on the provided JAX-RS / Jersey
 * {@code Application} class or instance to be tested. A {@link TestContainerException} will be
 * thrown if the configured test container factory cannot support the deployment context type. Two
 * deployment context are provided:
 *
 * <ol>
 *   <li>A basic deployment context, of type {@link DeploymentContext}, compatible with all test
 *       containers that are not based on Servlet deployment model.
 *   <li>A Servlet-based deployment context, of type {@link ServletDeploymentContext}, compatible
 *       with Servlet-based test containers.
 * </ol>
 *
 * @author Paul Sandoz
 * @author Srinivas Bhimisetty
 * @author Pavel Bucek (pavel.bucek at oracle.com)
 * @author Michal Gajdos (michal.gajdos at oracle.com)
 * @author Marek Potociar (marek.potociar at oracle.com)
 */
@SuppressWarnings("UnusedDeclaration")
public abstract class JerseyTest {

  private static final Logger LOGGER = Logger.getLogger(JerseyTest.class.getName());

  /**
   * Holds the test container factory class to be used for running the tests by default (if
   * testContainerFactory has not been set). This static field is initialized in {@link
   * #getDefaultTestContainerFactory()} method and is reused by any instances of {@code JerseyTest}
   * that are subsequently run. This is done to optimize the number of TestContainerFactory service
   * provider look-ups and class loading.
   */
  private static Class<? extends TestContainerFactory> defaultTestContainerFactoryClass;

  /** Configured deployment context for the tested application. */
  private final DeploymentContext context;

  /**
   * The test container factory which creates an instance of the test container on which the tests
   * would be run.
   */
  private TestContainerFactory testContainerFactory;

  /** The test container on which the tests would be run. */
  private TestContainer testContainer;

  private final AtomicReference<Client> client = new AtomicReference<>(null);

  /**
   * JerseyTest property bag that can be used to configure the test behavior. These properties can
   * be overridden with a system property.
   */
  private final Map<String, String> propertyMap = Maps.newHashMap();

  /**
   * JerseyTest forced property bag that can be used to configure the test behavior. These property
   * cannot be overridden with a system property.
   */
  private final Map<String, String> forcedPropertyMap = Maps.newHashMap();

  private JerseyTestLogHandler logHandler;
  private final Map<Logger, Level> logLevelMap = Maps.newIdentityHashMap();

  /**
   * Initialize JerseyTest instance.
   *
   * <p>This constructor can be used from an extending subclass.
   *
   * <p>When this constructor is used, the extending concrete subclass must implement one of the
   * {@link #configure()} or {@link #configureDeployment()} methods to provide the tested
   * application configuration and deployment context.
   */
  public JerseyTest() {
    // Note: this must be the first call in the constructor to allow setting config
    // properties (especially around logging) in the configure() or configureDeployment()
    // method overridden in subclass, otherwise the properties set in the subclass would
    // not be set soon enough
    this.context = configureDeployment();
    this.testContainerFactory = getTestContainerFactory();
  }

  /**
   * Initialize JerseyTest instance and specify the test container factory to be used by this test.
   *
   * <p>This constructor can be used from an extending subclass.
   *
   * <p>When this constructor is used, the extending concrete subclass must implement one of the
   * {@link #configure()} or {@link #configureDeployment()} methods to provide the tested
   * application configuration and deployment context.
   *
   * @param testContainerFactory the test container factory to use for testing.
   */
  public JerseyTest(final TestContainerFactory testContainerFactory) {
    // Note: this must be the first call in the constructor to allow setting config
    // properties (especially around logging) in the configure() or configureDeployment()
    // method overridden in subclass, otherwise the properties set in the subclass would
    // not be set soon enough
    this.context = configureDeployment();
    this.testContainerFactory = testContainerFactory;
  }

  /**
   * Initialize JerseyTest instance.
   *
   * <p>This constructor can be used from an extending subclass.
   *
   * <p>When this constructor is used, the extending concrete subclass must implement one of the
   * {@link #configure()} or {@link #configureDeployment()} methods are ignored.
   *
   * <p>Please note that when this constructor is used, recording of startup logs as well as
   * configuring other {@code JerseyTest} properties and features may not work properly. While using
   * this constructor should generally be avoided, in certain scenarios it may be necessary to use
   * this constructor. (E.g. when running parameterized tests in which application is created based
   * on test parameters passed in by JUnit framework via test constructor - in such case it is not
   * possible to propagate the necessary information to one of the overridden {@code
   * JerseyTest.configure...} methods).
   *
   * @param jaxrsApplication tested application.
   */
  public JerseyTest(final Application jaxrsApplication) {
    this.context = DeploymentContext.newInstance(jaxrsApplication);
    this.testContainerFactory = getTestContainerFactory();
  }

  /**
   * Return currently used test container to run the tests in. This method can be overridden.
   *
   * @return a test container instance or {@code null} if the container is not set.
   */
  /* package */ TestContainer getTestContainer() {
    return testContainer;
  }

  /**
   * Returns old test container used to run the tests in and set a new one. This method can be
   * overridden.
   *
   * @param testContainer a test container instance or {@code null} it the current test container
   *     should be released.
   * @return old test container instance.
   */
  /* package */ TestContainer setTestContainer(final TestContainer testContainer) {
    final TestContainer old = this.testContainer;
    this.testContainer = testContainer;
    return old;
  }

  private TestContainer createTestContainer(final DeploymentContext context) {
    return getTestContainerFactory().create(getBaseUri(), context);
  }

  /**
   * Programmatically enable a feature with a given name. Enabling of the feature may be overridden
   * via a system property.
   *
   * @param featureName name of the enabled feature.
   */
  protected final void enable(final String featureName) {
    // TODO: perhaps we could reuse the resource config for the test properties?
    propertyMap.put(featureName, Boolean.TRUE.toString());
  }

  /**
   * Programmatically disable a feature with a given name. Disabling of the feature may be
   * overridden via a system property.
   *
   * @param featureName name of the disabled feature.
   */
  protected final void disable(final String featureName) {
    propertyMap.put(featureName, Boolean.FALSE.toString());
  }

  /**
   * Programmatically force-enable a feature with a given name. Force-enabling of the feature cannot
   * be overridden via a system property. Use with care!
   *
   * @param featureName name of the force-enabled feature.
   */
  protected final void forceEnable(final String featureName) {
    forcedPropertyMap.put(featureName, Boolean.TRUE.toString());
  }

  /**
   * Programmatically force-disable a feature with a given name. Force-disabling of the feature
   * cannot be overridden via a system property. Use with care!
   *
   * @param featureName name of the force-disabled feature.
   */
  protected final void forceDisable(final String featureName) {
    forcedPropertyMap.put(featureName, Boolean.FALSE.toString());
  }

  /**
   * Programmatically set a value of a property with a given name. The property value may be
   * overridden via a system property.
   *
   * @param propertyName name of the property.
   * @param value property value.
   */
  protected final void set(final String propertyName, final Object value) {
    set(propertyName, value.toString());
  }

  /**
   * Programmatically set a value of a property with a given name. The property value may be
   * overridden via a system property.
   *
   * @param propertyName name of the property.
   * @param value property value.
   */
  protected final void set(final String propertyName, final String value) {
    propertyMap.put(propertyName, value);
  }

  /**
   * Programmatically force-set a value of a property with a given name. The force-set property
   * value cannot be overridden via a system property.
   *
   * @param propertyName name of the property.
   * @param value property value.
   */
  protected final void forceSet(final String propertyName, final String value) {
    forcedPropertyMap.put(propertyName, value);
  }

  /**
   * Check if the Jersey test boolean property (flag) has been set to {@code true}.
   *
   * @param propertyName name of the Jersey test boolean property.
   * @return {@code true} if the test property has been enabled, {@code false} otherwise.
   */
  protected final boolean isEnabled(final String propertyName) {
    return Boolean.valueOf(getProperty(propertyName));
  }

  private String getProperty(final String propertyName) {
    if (forcedPropertyMap.containsKey(propertyName)) {
      return forcedPropertyMap.get(propertyName);
    }

    final Properties systemProperties =
        AccessController.doPrivileged(PropertiesHelper.getSystemProperties());
    if (systemProperties.containsKey(propertyName)) {
      return systemProperties.getProperty(propertyName);
    }

    if (propertyMap.containsKey(propertyName)) {
      return propertyMap.get(propertyName);
    }

    return null;
  }

  private static String getSystemProperty(final String propertyName) {
    final Properties systemProperties =
        AccessController.doPrivileged(PropertiesHelper.getSystemProperties());
    return systemProperties.getProperty(propertyName);
  }

  /**
   * Create the tested JAX-RS /Jersey application.
   *
   * <p>This method may be overridden by subclasses to provide the configured JAX-RS /Jersey
   * application to be tested. The method may be also used to configure {@code JerseyTest} instance
   * properties.
   *
   * <p>Unless {@link #configureDeployment()} method is overridden in the subclass, the {@code
   * configure()} method is invoked by {@code configureDeployment()} to create default deployment
   * context for the tested application. As such, the method is invoked in the scope of one of the
   * {@code JerseyTest} constructors. Default implementation of this method throws {@link
   * UnsupportedOperationException}, so that construction of {@code JerseyTest} instance fails
   * unless one of the {@code configure()} or {@code configureDeployment()} methods is overridden in
   * the subclass.
   *
   * <p>Note that since the method is invoked from {@code JerseyTest} constructor, the overriding
   * implementation of the method must not depend on any subclass fields as those will not be
   * initialized yet when the method is invoked.
   *
   * <p>Also note that in case the {@link #JerseyTest(javax.ws.rs.core.Application)} constructor is
   * used, the method is never invoked.
   *
   * @return tested JAX-RS /Jersey application.
   */
  protected Application configure() {
    throw new UnsupportedOperationException(
        "The configure method must be implemented by the extending class");
  }

  /**
   * Create and configure deployment context for the tested application.
   *
   * <p>This method may be overridden by subclasses to provide custom test container deployment
   * context for the tested application. The method may be also used to configure {@code JerseyTest}
   * instance properties.
   *
   * <p>The method is invoked from {@code JerseyTest} constructors to provide deployment context for
   * the tested application. Default implementation of this method creates {@link
   * DeploymentContext#newInstance(javax.ws.rs.core.Application) new deployment context} using
   * JAX-RS application instance obtained by calling the {@link #configure()} method.
   *
   * <p>Note that since the method is invoked from {@code JerseyTest} constructor, the overriding
   * implementation of the method must not depend on any subclass fields as those will not be
   * initialized yet when the method is invoked.
   *
   * <p>Also note that in case the {@link #JerseyTest(javax.ws.rs.core.Application)} constructor is
   * used, the method is never invoked.
   *
   * @return configured deployment context for the tested application.
   * @since 2.8
   */
  protected DeploymentContext configureDeployment() {
    return DeploymentContext.builder(configure()).build();
  }

  /**
   * Return an instance of {@link TestContainerFactory} class.
   *
   * <p>This method is used only once during {@code JerseyTest} instance construction to retrieve
   * the factory responsible for providing {@link org.glassfish.jersey.test.spi.TestContainer} that
   * will be used to deploy the tested application.
   *
   * <p>A default implementation first searches for the {@code TestContainerFactory} set via {@link
   * #JerseyTest(org.glassfish.jersey.test.spi.TestContainerFactory) constructor}, then it looks for
   * a {@code TestContainerFactory} implementation class name set via <tt>{@value
   * org.glassfish.jersey.test.TestProperties#CONTAINER_FACTORY}</tt> system property with a
   * fallback to searching for {@code TestContainerFactory} service providers on the class path. At
   * last, if no {@code TestContainerFactory} has been found, the method attempts to create new
   * default {@code TestContainerFactory} implementation instance (<tt>{@value
   * org.glassfish.jersey.test.TestProperties#DEFAULT_CONTAINER_FACTORY}</tt>).
   *
   * <p>Alternatively, this method may be overridden to directly provide a custom {@code
   * TestContainerFactory} instance. Note that since the method is invoked from {@code JerseyTest}
   * constructor, the overriding implementation of the method must not depend on any subclass fields
   * as those will not be initialized yet when the method is invoked.
   *
   * @return an instance of {@link TestContainerFactory} class.
   * @throws TestContainerException if the initialization of {@link TestContainerFactory} instance
   *     is not successful.
   */
  protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
    if (testContainerFactory == null) {
      testContainerFactory = getDefaultTestContainerFactory();
    }
    return testContainerFactory;
  }

  private static synchronized TestContainerFactory getDefaultTestContainerFactory() {

    if (defaultTestContainerFactoryClass == null) {
      final String factoryClassName = getSystemProperty(TestProperties.CONTAINER_FACTORY);
      if (factoryClassName != null) {
        LOGGER.log(
            Level.CONFIG,
            "Loading test container factory '{0}' specified in the '{1}' system property.",
            new Object[] {factoryClassName, TestProperties.CONTAINER_FACTORY});

        defaultTestContainerFactoryClass = loadFactoryClass(factoryClassName);
      } else {
        final TestContainerFactory[] factories =
            ServiceFinder.find(TestContainerFactory.class).toArray();
        if (factories.length > 0) {
          // if there is only one factory instance, just return it
          if (factories.length == 1) {
            // cache the class for future reuse
            defaultTestContainerFactoryClass = factories[0].getClass();
            LOGGER.log(
                Level.CONFIG,
                "Using the single found TestContainerFactory service provider '{0}'",
                defaultTestContainerFactoryClass.getName());
            return factories[0];
          }

          // if default factory is present, use it.
          for (final TestContainerFactory tcf : factories) {
            if (TestProperties.DEFAULT_CONTAINER_FACTORY.equals(tcf.getClass().getName())) {
              // cache the class for future reuse
              defaultTestContainerFactoryClass = tcf.getClass();
              LOGGER.log(
                  Level.CONFIG,
                  "Found multiple TestContainerFactory service providers, using the default found '{0}'",
                  TestProperties.DEFAULT_CONTAINER_FACTORY);
              return tcf;
            }
          }

          // default factory is not in the list - log warning and return the first found factory
          // instance
          // cache the class for future reuse
          defaultTestContainerFactoryClass = factories[0].getClass();
          LOGGER.log(
              Level.WARNING,
              "Found multiple TestContainerFactory service providers, using the first found '{0}'",
              defaultTestContainerFactoryClass.getName());
          return factories[0];
        }

        LOGGER.log(
            Level.CONFIG,
            "No TestContainerFactory configured, trying to load and instantiate the default implementation '{0}'",
            TestProperties.DEFAULT_CONTAINER_FACTORY);
        defaultTestContainerFactoryClass =
            loadFactoryClass(TestProperties.DEFAULT_CONTAINER_FACTORY);
      }
    }

    try {
      return defaultTestContainerFactoryClass.newInstance();
    } catch (final Exception ex) {
      throw new TestContainerException(
          String.format(
              "Could not instantiate test container factory '%s'",
              defaultTestContainerFactoryClass.getName()),
          ex);
    }
  }

  private static Class<? extends TestContainerFactory> loadFactoryClass(
      final String factoryClassName) {
    Class<? extends TestContainerFactory> factoryClass;
    final Class<Object> loadedClass =
        AccessController.doPrivileged(ReflectionHelper.classForNamePA(factoryClassName, null));
    if (loadedClass == null) {
      throw new TestContainerException(
          String.format("Test container factory class '%s' cannot be loaded", factoryClassName));
    }
    try {
      return loadedClass.asSubclass(TestContainerFactory.class);
    } catch (final ClassCastException ex) {
      throw new TestContainerException(
          String.format(
              "Class '%s' does not implement TestContainerFactory SPI.", factoryClassName),
          ex);
    }
  }

  /**
   * Create a JAX-RS web target whose URI refers to the {@link #getBaseUri() base URI} the tested
   * JAX-RS / Jersey application is deployed at, plus the path specified in the {@code path}
   * argument.
   *
   * <p>This method is an equivalent of calling <tt>client().target(getBaseUri())</tt>.
   *
   * @return the created JAX-RS web target.
   */
  public final WebTarget target() {
    return client().target(getTestContainer().getBaseUri());
  }

  /**
   * Create a JAX-RS web target whose URI refers to the {@link #getBaseUri() base URI} the tested
   * JAX-RS / Jersey application is deployed at, plus the path specified in the {@code path}
   * argument.
   *
   * <p>This method is an equivalent of calling {@code target().path(path)}.
   *
   * @param path relative path (from tested application base URI) this web target should point to.
   * @return the created JAX-RS web target.
   */
  public final WebTarget target(final String path) {
    return target().path(path);
  }

  /**
   * Get the JAX-RS test client that is {@link
   * #configureClient(org.glassfish.jersey.client.ClientConfig) pre-configured} for this test.
   *
   * @return the configured test client.
   */
  public final Client client() {
    return getClient();
  }

  /**
   * Set up the test by creating a test container instance, {@link TestContainer#start() starting}
   * it and by creating a new {@link #configureClient(org.glassfish.jersey.client.ClientConfig)
   * pre-configured} test client. The test container is obtained from the {@link
   * #getTestContainerFactory() test container factory}.
   *
   * @throws TestContainerException if the default test container factory cannot be obtained, or the
   *     test application deployment context is not supported by the test container factory.
   * @throws Exception if an exception is thrown during setting up the test environment.
   */
  @Before
  public void setUp() throws Exception {
    if (isLogRecordingEnabled()) {
      registerLogHandler();
    }

    final TestContainer testContainer = createTestContainer(context);

    // Set current instance of test container and start it.
    setTestContainer(testContainer);
    testContainer.start();

    // Create an set new client.
    setClient(getClient(testContainer.getClientConfig()));
  }

  /**
   * Tear down the test by {@link TestContainer#stop() stopping} the test container obtained from
   * the {@link #getTestContainerFactory() test container factory} and by {@link
   * javax.ws.rs.client.Client#close() closing} and discarding the {@link
   * #configureClient(org.glassfish.jersey.client.ClientConfig) pre-configured} test client that was
   * {@link #setUp() set up} for the test.
   *
   * @throws Exception if an exception is thrown during tearing down the test environment.
   */
  @After
  public void tearDown() throws Exception {
    if (isLogRecordingEnabled()) {
      unregisterLogHandler();
    }

    try {
      TestContainer oldContainer = setTestContainer(null);
      if (oldContainer != null) {
        oldContainer.stop();
      }
    } finally {
      closeIfNotNull(setClient(null));
    }
  }

  /**
   * Get the JAX-RS test client that is {@link
   * #configureClient(org.glassfish.jersey.client.ClientConfig) pre-configured} for this test. This
   * method can be overridden.
   *
   * @return the configured test client.
   */
  protected Client getClient() {
    return client.get();
  }

  /**
   * Get the old JAX-RS test client and set a new one. This method can be overridden.
   *
   * @param client the configured test client.
   * @return old configured test client.
   */
  protected Client setClient(final Client client) {
    return this.client.getAndSet(client);
  }

  /**
   * Create an instance of test {@link Client} using the client configuration provided by the
   * configured {@link org.glassfish.jersey.test.spi.TestContainer}.
   *
   * <p>If the {@code TestContainer} does not provide any client configuration (passed {@code
   * clientConfig} is {@code null}), the default implementation of this method first creates an
   * empty new {@link org.glassfish.jersey.client.ClientConfig} instance. The client configuration
   * (provided by test container or created) is then passed to {@link
   * #configureClient(org.glassfish.jersey.client.ClientConfig)} which can be overridden in the
   * {@code JerseyTest} subclass to provide custom client configuration. At last, new JAX-RS {@link
   * Client} instance is created based on the resulting client configuration.
   *
   * @param clientConfig test client default configuration. May be {@code null}.
   * @return A Client instance.
   */
  private Client getClient(ClientConfig clientConfig) {
    if (clientConfig == null) {
      clientConfig = new ClientConfig();
    }

    // check if logging is required
    if (isEnabled(TestProperties.LOG_TRAFFIC)) {
      clientConfig.register(new LoggingFilter(LOGGER, isEnabled(TestProperties.DUMP_ENTITY)));
    }

    configureClient(clientConfig);

    return ClientBuilder.newClient(clientConfig);
  }

  /**
   * Configure the test client.
   *
   * <p>The method can be overridden by {@code JerseyTest} subclasses to conveniently configure the
   * test client instance used by Jersey test framework (either returned from {@link #client()}
   * method or used to create {@link javax.ws.rs.client.WebTarget} instances returned from one of
   * the {@code target} methods ({@link #target()} or {@link #target(String)}).
   *
   * <p>Prior to every test method run, a new client instance is configured and created using the
   * client configuration provided by the {@link org.glassfish.jersey.test.spi.TestContainer} as
   * well as any internal {@code JerseyTest} client configuration settings.
   *
   * <p>Before the actual client instance creation, Jersey test framework invokes this method in
   * order to allow the subclasses to further customize created client instance.
   *
   * <p>After each test method is run, the existing client instance is {@link
   * javax.ws.rs.client.Client#close() closed} and discarded.
   *
   * <p>Default implementation of the method is "no-op".
   *
   * @param config Jersey test client configuration that can be modified before the client is
   *     created.
   */
  protected void configureClient(final ClientConfig config) {
    // do nothing
  }

  /**
   * Returns the base URI of the tested application.
   *
   * @return the base URI of the tested application.
   */
  // TODO make final
  protected URI getBaseUri() {
    final TestContainer container = getTestContainer();

    if (container != null) {
      // called from outside of JerseyTest constructor
      return container.getBaseUri();
    }

    // called from within JerseyTest constructor
    return UriBuilder.fromUri("http://localhost/").port(getPort()).build();
  }

  /**
   * Get the port to be used for test application deployments.
   *
   * @return The HTTP port of the URI
   */
  protected final int getPort() {
    final TestContainer container = getTestContainer();

    if (container != null) {
      // called from outside of JerseyTest constructor
      return container.getBaseUri().getPort();
    }

    // called from within JerseyTest constructor
    final String value = getProperty(TestProperties.CONTAINER_PORT);
    if (value != null) {

      try {
        final int i = Integer.parseInt(value);
        if (i < 0) {
          throw new NumberFormatException("Value not positive.");
        }
        return i;
      } catch (final NumberFormatException e) {
        LOGGER.log(
            Level.CONFIG,
            "Value of "
                + TestProperties.CONTAINER_PORT
                + " property is not a valid positive integer ["
                + value
                + "]."
                + " Reverting to default ["
                + TestProperties.DEFAULT_CONTAINER_PORT
                + "].",
            e);
      }
    }
    return TestProperties.DEFAULT_CONTAINER_PORT;
  }

  /**
   * Get stored {@link LogRecord log records} if enabled by setting {@link
   * TestProperties#RECORD_LOG_LEVEL} or an empty list.
   *
   * @return list of log records or an empty list.
   */
  protected final List<LogRecord> getLoggedRecords() {
    return getLogHandler().getRecords();
  }

  /**
   * Get last stored {@link LogRecord log record} if enabled by setting {@link
   * TestProperties#RECORD_LOG_LEVEL} or {@code null}.
   *
   * @return last stored {@link LogRecord log record} or {@code null}.
   */
  protected final LogRecord getLastLoggedRecord() {
    final List<LogRecord> loggedRecords = getLoggedRecords();
    return loggedRecords.isEmpty() ? null : loggedRecords.get(loggedRecords.size() - 1);
  }

  /**
   * Retrieves a list of root loggers.
   *
   * @return list of root loggers.
   */
  private Set<Logger> getRootLoggers() {
    final LogManager logManager = LogManager.getLogManager();
    final Enumeration<String> loggerNames = logManager.getLoggerNames();

    final Set<Logger> rootLoggers = Sets.newHashSet();

    while (loggerNames.hasMoreElements()) {
      Logger logger = logManager.getLogger(loggerNames.nextElement());
      if (logger != null) {
        while (logger.getParent() != null) {
          logger = logger.getParent();
        }
        rootLoggers.add(logger);
      }
    }

    return rootLoggers;
  }

  /** Register {@link Handler log handler} to the list of root loggers. */
  private void registerLogHandler() {
    final String recordLogLevel = getProperty(TestProperties.RECORD_LOG_LEVEL);
    final int recordLogLevelInt = Integer.valueOf(recordLogLevel);
    final Level level = Level.parse(recordLogLevel);

    logLevelMap.clear();

    for (final Logger root : getRootLoggers()) {
      logLevelMap.put(root, root.getLevel());

      if (root.getLevel().intValue() > recordLogLevelInt) {
        root.setLevel(level);
      }

      root.addHandler(getLogHandler());
    }
  }

  /** Un-register {@link Handler log handler} from the list of root loggers. */
  private void unregisterLogHandler() {
    for (final Logger root : getRootLoggers()) {
      root.setLevel(logLevelMap.get(root));
      root.removeHandler(getLogHandler());
    }
    logHandler = null;
  }

  /**
   * Return {@code true} if log recoding is enabled.
   *
   * @return {@code true} if log recoding is enabled, {@code false} otherwise.
   */
  private boolean isLogRecordingEnabled() {
    return getProperty(TestProperties.RECORD_LOG_LEVEL) != null;
  }

  /**
   * Retrieves {@link Handler log handler} capable of storing {@link LogRecord logged records}.
   *
   * @return log handler.
   */
  private JerseyTestLogHandler getLogHandler() {
    if (logHandler == null) {
      logHandler = new JerseyTestLogHandler();
    }
    return logHandler;
  }

  /**
   * Returns {@link TestProperties#ASYNC_TIMEOUT_MULTIPLIER} or {@code 1} if the property is not
   * defined.
   *
   * @return Multiplier of the async timeout for async test.
   */
  protected int getAsyncTimeoutMultiplier() {
    final String property = getProperty(TestProperties.ASYNC_TIMEOUT_MULTIPLIER);
    Integer multi = 1;
    if (property != null) {
      multi = Integer.valueOf(property);
      if (multi <= 0) {
        throw new NumberFormatException(
            "Property "
                + TestProperties.ASYNC_TIMEOUT_MULTIPLIER
                + " must be a number greater than 0.");
      }
    }
    return multi;
  }

  /**
   * Utility method that safely closes a response without throwing an exception.
   *
   * @param responses responses to close. Each response may be {@code null}.
   * @since 2.5
   */
  public final void close(final Response... responses) {
    if (responses == null || responses.length == 0) {
      return;
    }

    for (final Response response : responses) {
      if (response == null) {
        continue;
      }
      try {
        response.close();
      } catch (final Throwable t) {
        LOGGER.log(Level.WARNING, "Error closing a response.", t);
      }
    }
  }

  /**
   * Utility method that safely closes a client instance without throwing an exception.
   *
   * @param clients client instances to close. Each instance may be {@code null}.
   * @since 2.5
   */
  public static void closeIfNotNull(final Client... clients) {
    if (clients == null || clients.length == 0) {
      return;
    }

    for (final Client c : clients) {
      if (c == null) {
        continue;
      }
      try {
        c.close();
      } catch (final Throwable t) {
        LOGGER.log(Level.WARNING, "Error closing a client instance.", t);
      }
    }
  }

  /** Custom logging handler used to store log records produces during an invocation of a test. */
  private class JerseyTestLogHandler extends Handler {

    private final int logLevel;
    private final List<LogRecord> records;

    private JerseyTestLogHandler() {
      this.logLevel = Integer.parseInt(getProperty(TestProperties.RECORD_LOG_LEVEL));
      this.records = new ArrayList<>();
    }

    @Override
    public void publish(final LogRecord record) {
      final String loggerName = record.getLoggerName();

      if (record.getLevel().intValue() >= logLevel
          && loggerName.startsWith("org.glassfish.jersey")
          && !loggerName.startsWith("org.glassfish.jersey.test")) {
        records.add(record);
      }
    }

    @Override
    public void flush() {}

    @Override
    public void close() throws SecurityException {}

    public List<LogRecord> getRecords() {
      return records;
    }
  }
}
Beispiel #2
0
/**
 * Parent class for all tests written using Jersey test framework.
 *
 * @author Paul Sandoz (paul.sandoz at oracle.com)
 * @author Srinivas Bhimisetty
 * @author Pavel Bucek (pavel.bucek at oracle.com)
 * @author Michal Gajdos (michal.gajdos at oracle.com)
 */
@SuppressWarnings("UnusedDeclaration")
public abstract class JerseyTest {

  private static final Logger LOGGER = Logger.getLogger(JerseyTest.class.getName());
  /** Holds the default test container factory class to be used for running the tests. */
  private static Class<? extends TestContainerFactory> testContainerFactoryClass;
  /**
   * The test container factory which creates an instance of the test container on which the tests
   * would be run.
   */
  private TestContainerFactory testContainerFactory;
  /** The test container on which the tests would be run. */
  private final TestContainer tc;

  private final ApplicationHandler application;
  private final AtomicReference<Client> client = new AtomicReference<Client>(null);
  /**
   * JerseyTest property bag that can be used to configure the test behavior. These properties can
   * be overridden with a system property.
   */
  private final Map<String, String> propertyMap = Maps.newHashMap();
  /**
   * JerseyTest forced property bag that can be used to configure the test behavior. These property
   * cannot be overridden with a system property.
   */
  private final Map<String, String> forcedPropertyMap = Maps.newHashMap();

  private Handler logHandler;
  private final List<LogRecord> loggedStartupRecords = Lists.newArrayList();
  private final List<LogRecord> loggedRuntimeRecords = Lists.newArrayList();
  private final Map<Logger, Level> logLevelMap = Maps.newIdentityHashMap();

  /**
   * An extending class must implement the {@link #configure()} method to provide an application
   * descriptor.
   *
   * @throws TestContainerException if the default test container factory cannot be obtained, or the
   *     application descriptor is not supported by the test container factory.
   */
  public JerseyTest() throws TestContainerException {
    ResourceConfig config = getResourceConfig(configure());
    config.register(
        new ServiceFinderBinder<TestContainerFactory>(
            TestContainerFactory.class, null, RuntimeType.SERVER));

    if (isLogRecordingEnabled()) {
      registerLogHandler();
    }
    this.application = new ApplicationHandler(config);
    this.tc = getContainer(application, getTestContainerFactory());
    if (isLogRecordingEnabled()) {
      loggedStartupRecords.addAll(loggedRuntimeRecords);
      loggedRuntimeRecords.clear();
      unregisterLogHandler();
    }
  }

  /**
   * Construct a new instance with a test container factory.
   *
   * <p>An extending class must implement the {@link #configure()} method to provide an application
   * descriptor.
   *
   * @param testContainerFactory the test container factory to use for testing.
   * @throws TestContainerException if the application descriptor is not supported by the test
   *     container factory.
   */
  public JerseyTest(TestContainerFactory testContainerFactory) {
    setTestContainerFactory(testContainerFactory);

    ResourceConfig config = getResourceConfig(configure());
    config.register(
        new ServiceFinderBinder<TestContainerFactory>(
            TestContainerFactory.class, null, RuntimeType.SERVER));
    if (isLogRecordingEnabled()) {
      registerLogHandler();
    }
    this.application = new ApplicationHandler(config);
    this.tc = getContainer(application, testContainerFactory);
    if (isLogRecordingEnabled()) {
      loggedStartupRecords.addAll(loggedRuntimeRecords);
      loggedRuntimeRecords.clear();
      unregisterLogHandler();
    }
  }

  /**
   * Construct a new instance with an application descriptor that defines how the test container is
   * configured.
   *
   * @param jaxrsApplication an application describing how to configure the test container.
   * @throws TestContainerException if the default test container factory cannot be obtained, or the
   *     application descriptor is not supported by the test container factory.
   */
  public JerseyTest(Application jaxrsApplication) throws TestContainerException {
    ResourceConfig config = getResourceConfig(jaxrsApplication);
    config.register(
        new ServiceFinderBinder<TestContainerFactory>(
            TestContainerFactory.class, null, RuntimeType.SERVER));
    if (isLogRecordingEnabled()) {
      registerLogHandler();
    }
    this.application = new ApplicationHandler(config);
    this.tc = getContainer(application, getTestContainerFactory());
    if (isLogRecordingEnabled()) {
      loggedStartupRecords.addAll(loggedRuntimeRecords);
      loggedRuntimeRecords.clear();
      unregisterLogHandler();
    }
  }

  /**
   * Construct a new instance with an {@link Application} class.
   *
   * @param jaxrsApplicationClass an application describing how to configure the test container.
   * @throws TestContainerException if the default test container factory cannot be obtained, or the
   *     application descriptor is not supported by the test container factory.
   */
  public JerseyTest(Class<? extends Application> jaxrsApplicationClass)
      throws TestContainerException {
    ResourceConfig config = ResourceConfig.forApplicationClass(jaxrsApplicationClass);
    config.register(
        new ServiceFinderBinder<TestContainerFactory>(
            TestContainerFactory.class, null, RuntimeType.SERVER));
    if (isLogRecordingEnabled()) {
      registerLogHandler();
    }
    this.application = new ApplicationHandler(config);
    this.tc = getContainer(application, getTestContainerFactory());
    if (isLogRecordingEnabled()) {
      loggedStartupRecords.addAll(loggedRuntimeRecords);
      loggedRuntimeRecords.clear();
      unregisterLogHandler();
    }
  }

  private ResourceConfig getResourceConfig(Application app) {
    return ResourceConfig.forApplication(app);
  }

  /**
   * Programmatically enable a feature with a given name. Enabling of the feature may be overridden
   * via a system property.
   *
   * @param featureName name of the enabled feature.
   */
  protected final void enable(String featureName) {
    // TODO: perhaps we could reuse the resource config for the test properties?
    propertyMap.put(featureName, Boolean.TRUE.toString());
  }

  /**
   * Programmatically disable a feature with a given name. Disabling of the feature may be
   * overridden via a system property.
   *
   * @param featureName name of the disabled feature.
   */
  protected final void disable(String featureName) {
    propertyMap.put(featureName, Boolean.FALSE.toString());
  }

  /**
   * Programmatically force-enable a feature with a given name. Force-enabling of the feature cannot
   * be overridden via a system property. Use with care!
   *
   * @param featureName name of the force-enabled feature.
   */
  protected final void forceEnable(String featureName) {
    forcedPropertyMap.put(featureName, Boolean.TRUE.toString());
  }

  /**
   * Programmatically force-disable a feature with a given name. Force-disabling of the feature
   * cannot be overridden via a system property. Use with care!
   *
   * @param featureName name of the force-disabled feature.
   */
  protected final void forceDisable(String featureName) {
    forcedPropertyMap.put(featureName, Boolean.FALSE.toString());
  }

  /**
   * Programmatically set a value of a property with a given name. The property value may be
   * overridden via a system property.
   *
   * @param propertyName name of the property.
   * @param value property value.
   */
  protected final void set(String propertyName, Object value) {
    set(propertyName, value.toString());
  }

  /**
   * Programmatically set a value of a property with a given name. The property value may be
   * overridden via a system property.
   *
   * @param propertyName name of the property.
   * @param value property value.
   */
  protected final void set(String propertyName, String value) {
    propertyMap.put(propertyName, value);
  }

  /**
   * Programmatically force-set a value of a property with a given name. The force-set property
   * value cannot be overridden via a system property.
   *
   * @param propertyName name of the property.
   * @param value property value.
   */
  protected final void forceSet(String propertyName, String value) {
    forcedPropertyMap.put(propertyName, value);
  }

  /**
   * Check if the Jersey test boolean property (flag) has been set to {@code true}.
   *
   * @param propertyName name of the Jersey test boolean property.
   * @return {@code true} if the test property has been enabled, {@code false} otherwise.
   */
  protected final boolean isEnabled(String propertyName) {
    return Boolean.valueOf(getProperty(propertyName));
  }

  private String getProperty(String propertyName) {
    if (forcedPropertyMap.containsKey(propertyName)) {
      return forcedPropertyMap.get(propertyName);
    }

    final Properties systemProperties =
        AccessController.doPrivileged(PropertiesHelper.getSystemProperties());
    if (systemProperties.containsKey(propertyName)) {
      return systemProperties.getProperty(propertyName);
    }

    if (propertyMap.containsKey(propertyName)) {
      return propertyMap.get(propertyName);
    }

    return null;
  }

  /**
   * Return an JAX-RS application that defines how the application in the test container is
   * configured.
   *
   * <p>If a constructor is utilized that does not supply an application descriptor then this method
   * must be overridden to return an application descriptor, otherwise an {@link
   * UnsupportedOperationException} exception will be thrown.
   *
   * <p>If a constructor is utilized that does supply an application descriptor then this method
   * does not require to be overridden and will not be invoked.
   *
   * @return the application descriptor.
   */
  protected Application configure() {
    throw new UnsupportedOperationException(
        "The configure method must be implemented by the extending class");
  }

  /**
   * Sets the test container factory to to be used for testing.
   *
   * @param testContainerFactory the test container factory to to be used for testing.
   */
  protected final void setTestContainerFactory(TestContainerFactory testContainerFactory) {
    this.testContainerFactory = testContainerFactory;
  }

  /**
   * Returns an instance of {@link TestContainerFactory} class. This instance can be set by a
   * constructor ({@link #JerseyTest(org.glassfish.jersey.test.spi.TestContainerFactory)}, as an
   * application {@link Providers Provider} or the {@link TestContainerFactory} class can be set as
   * a {@value org.glassfish.jersey.test.TestProperties#CONTAINER_FACTORY} property.
   *
   * @return an instance of {@link TestContainerFactory} class.
   * @throws TestContainerException if the initialization of {@link TestContainerFactory} instance
   *     is not successful.
   */
  protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
    if (testContainerFactory == null) {
      if (testContainerFactoryClass == null) {

        final String tcfClassName = getProperty(TestProperties.CONTAINER_FACTORY);
        if ((tcfClassName == null)) {
          Set<TestContainerFactory> testContainerFactories =
              Providers.getProviders(application.getServiceLocator(), TestContainerFactory.class);

          if (testContainerFactories.size() >= 1) {
            // if default factory is present, use it.
            for (TestContainerFactory tcFactory : testContainerFactories) {

              if (tcFactory.getClass().getName().equals(TestProperties.DEFAULT_CONTAINER_FACTORY)) {
                LOGGER.log(
                    Level.CONFIG,
                    "Found multiple TestContainerFactory implementations, using default {0}",
                    tcFactory.getClass().getName());

                testContainerFactoryClass = tcFactory.getClass(); // is this necessary?
                return tcFactory;
              }
            }

            if (testContainerFactories.size() != 1) {
              LOGGER.log(
                  Level.WARNING,
                  "Found multiple TestContainerFactory implementations, using {0}",
                  testContainerFactories.iterator().next().getClass().getName());
            }

            testContainerFactoryClass = testContainerFactories.iterator().next().getClass();
            return testContainerFactories.iterator().next();
          }
        } else {
          final Class<Object> tfClass =
              AccessController.doPrivileged(ReflectionHelper.classForNamePA(tcfClassName, null));
          if (tfClass == null) {
            throw new TestContainerException(
                "The default test container factory class name, "
                    + tcfClassName
                    + ", cannot be loaded");
          }
          try {
            testContainerFactoryClass = tfClass.asSubclass(TestContainerFactory.class);
          } catch (ClassCastException ex) {
            throw new TestContainerException(
                "The default test container factory class, "
                    + tcfClassName
                    + ", is not an instance of TestContainerFactory",
                ex);
          }
        }
      }

      try {
        return testContainerFactoryClass.newInstance();
      } catch (Exception ex) {
        throw new TestContainerException(
            "The default test container factory, "
                + testContainerFactoryClass
                + ", could not be instantiated",
            ex);
      }
    }

    return testContainerFactory;
  }

  /**
   * Create a web resource whose URI refers to the base URI the Web application is deployed at.
   *
   * @return the created web resource
   */
  public WebTarget target() {
    return client().target(tc.getBaseUri());
  }

  /**
   * Create a web resource whose URI refers to the base URI the Web application is deployed at plus
   * the path specified in the argument.
   *
   * <p>This method is an equivalent of calling {@code target().path(path)}.
   *
   * @param path Relative path (from base URI) this target should point to.
   * @return the created web resource
   */
  public WebTarget target(String path) {
    return target().path(path);
  }

  /**
   * Get the client that is configured for this test.
   *
   * @return the configured client.
   */
  public Client client() {
    return client.get();
  }

  /**
   * Set up the test by invoking {@link TestContainer#start() } on the test container obtained from
   * the test container factory.
   *
   * @throws Exception if an exception is thrown during setting up the test environment.
   */
  @Before
  public void setUp() throws Exception {
    if (isLogRecordingEnabled()) {
      loggedRuntimeRecords.clear();
      registerLogHandler();
    }

    tc.start();
    Client old = client.getAndSet(getClient(tc, application));
    close(old);
  }

  /**
   * Tear down the test by invoking {@link TestContainer#stop() } on the test container obtained
   * from the test container factory.
   *
   * @throws Exception if an exception is thrown during tearing down the test environment.
   */
  @After
  public void tearDown() throws Exception {
    if (isLogRecordingEnabled()) {
      loggedRuntimeRecords.clear();
      unregisterLogHandler();
    }

    try {
      tc.stop();
    } finally {
      Client old = client.getAndSet(null);
      close(old);
    }
  }

  private TestContainer getContainer(ApplicationHandler application, TestContainerFactory tcf) {
    if (application == null) {
      throw new IllegalArgumentException("The application cannot be null");
    }

    return tcf.create(getBaseUri(), application);
  }

  /**
   * Creates an instance of {@link Client}.
   *
   * <p>Checks whether TestContainer provides ClientConfig instance and if not, empty new {@link
   * org.glassfish.jersey.client.ClientConfig} instance will be used to create new client instance.
   *
   * <p>This method is called exactly once when JerseyTest is created.
   *
   * @param tc instance of {@link TestContainer}
   * @param applicationHandler instance of {@link ApplicationHandler}
   * @return A Client instance.
   */
  protected Client getClient(TestContainer tc, ApplicationHandler applicationHandler) {
    ClientConfig cc = tc.getClientConfig();

    if (cc == null) {
      cc = new ClientConfig();
    }

    // check if logging is required
    if (isEnabled(TestProperties.LOG_TRAFFIC)) {
      cc.register(new LoggingFilter(LOGGER, isEnabled(TestProperties.DUMP_ENTITY)));
    }

    configureClient(cc);

    return ClientBuilder.newClient(cc);
  }

  /**
   * Can be overridden by subclasses to conveniently configure the client instance used by the test.
   *
   * <p>Default implementation of the method is "no-op".
   *
   * @param config Jersey test client configuration that can be modified before the client is
   *     created.
   */
  protected void configureClient(ClientConfig config) {
    // nothing
  }

  /**
   * Returns the base URI of the application.
   *
   * @return The base URI of the application
   */
  protected URI getBaseUri() {
    return UriBuilder.fromUri("http://localhost/").port(getPort()).build();
  }

  /**
   * Get the port to be used for test application deployments.
   *
   * @return The HTTP port of the URI
   */
  protected final int getPort() {
    final String value = getProperty(TestProperties.CONTAINER_PORT);
    if (value != null) {

      try {
        final int i = Integer.parseInt(value);
        if (i <= 0) {
          throw new NumberFormatException("Value not positive.");
        }
        return i;
      } catch (NumberFormatException e) {
        LOGGER.log(
            Level.CONFIG,
            "Value of "
                + TestProperties.CONTAINER_PORT
                + " property is not a valid positive integer ["
                + value
                + "]."
                + " Reverting to default ["
                + TestProperties.DEFAULT_CONTAINER_PORT
                + "].",
            e);
      }
    }
    return TestProperties.DEFAULT_CONTAINER_PORT;
  }

  /**
   * Get stored {@link LogRecord log records} if enabled by setting {@link
   * TestProperties#RECORD_LOG_LEVEL} or an empty list.
   *
   * @return list of log records or an empty list.
   */
  protected List<LogRecord> getLoggedRecords() {
    final List<LogRecord> logRecords = Lists.newArrayList();
    logRecords.addAll(loggedStartupRecords);
    logRecords.addAll(loggedRuntimeRecords);
    return logRecords;
  }

  /**
   * Get last stored {@link LogRecord log record} if enabled by setting {@link
   * TestProperties#RECORD_LOG_LEVEL} or {@code null}.
   *
   * @return last stored {@link LogRecord log record} or {@code null}.
   */
  protected LogRecord getLastLoggedRecord() {
    final List<LogRecord> loggedRecords = getLoggedRecords();
    return loggedRecords.isEmpty() ? null : loggedRecords.get(loggedRecords.size() - 1);
  }

  /**
   * Retrieves a list of root loggers.
   *
   * @return list of root loggers.
   */
  private Set<Logger> getRootLoggers() {
    final LogManager logManager = LogManager.getLogManager();
    final Enumeration<String> loggerNames = logManager.getLoggerNames();

    final Set<Logger> rootLoggers = Sets.newHashSet();

    while (loggerNames.hasMoreElements()) {
      Logger logger = logManager.getLogger(loggerNames.nextElement());
      if (logger != null) {
        while (logger.getParent() != null) {
          logger = logger.getParent();
        }
        rootLoggers.add(logger);
      }
    }

    return rootLoggers;
  }

  /** Register {@link Handler log handler} to the list of root loggers. */
  private void registerLogHandler() {
    final String recordLogLevel = getProperty(TestProperties.RECORD_LOG_LEVEL);
    final int recordLogLevelInt = Integer.valueOf(recordLogLevel);
    final Level level = Level.parse(recordLogLevel);

    logLevelMap.clear();

    for (final Logger root : getRootLoggers()) {
      logLevelMap.put(root, root.getLevel());

      if (root.getLevel().intValue() > recordLogLevelInt) {
        root.setLevel(level);
      }

      root.addHandler(getLogHandler());
    }
  }

  /** Un-register {@link Handler log handler} from the list of root loggers. */
  private void unregisterLogHandler() {
    for (final Logger root : getRootLoggers()) {
      root.setLevel(logLevelMap.get(root));
      root.removeHandler(getLogHandler());
    }
    logHandler = null;
  }

  /**
   * Return {@code true} if log recoding is enabled.
   *
   * @return {@code true} if log recoding is enabled, {@code false} otherwise.
   */
  private boolean isLogRecordingEnabled() {
    return getProperty(TestProperties.RECORD_LOG_LEVEL) != null;
  }

  /**
   * Retrieves {@link Handler log handler} capable of storing {@link LogRecord logged records}.
   *
   * @return log handler.
   */
  private Handler getLogHandler() {
    if (logHandler == null) {
      final Integer logLevel = Integer.valueOf(getProperty(TestProperties.RECORD_LOG_LEVEL));
      logHandler =
          new Handler() {

            @Override
            public void publish(LogRecord record) {
              final String loggerName = record.getLoggerName();

              if (record.getLevel().intValue() >= logLevel
                  && loggerName.startsWith("org.glassfish.jersey")
                  && !loggerName.startsWith("org.glassfish.jersey.test")) {
                loggedRuntimeRecords.add(record);
              }
            }

            @Override
            public void flush() {}

            @Override
            public void close() throws SecurityException {}
          };
    }
    return logHandler;
  }

  /**
   * Utility method that safely closes a response without throwing an exception.
   *
   * @param responses responses to close. Each response may be {@code null}.
   * @since 2.5
   */
  public final void close(final Response... responses) {
    if (responses == null || responses.length == 0) {
      return;
    }

    for (Response response : responses) {
      if (response == null) {
        continue;
      }
      try {
        response.close();
      } catch (Throwable t) {
        LOGGER.log(Level.WARNING, "Error closing a response.", t);
      }
    }
  }

  /**
   * Utility method that safely closes a client instance without throwing an exception.
   *
   * @param clients client instances to close. Each instance may be {@code null}.
   * @since 2.5
   */
  public final void close(final Client... clients) {
    if (clients == null || clients.length == 0) {
      return;
    }

    for (Client c : clients) {
      if (c == null) {
        continue;
      }
      try {
        c.close();
      } catch (Throwable t) {
        LOGGER.log(Level.WARNING, "Error closing a client instance.", t);
      }
    }
  }
}