package io.dropwizard.testing;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.dropwizard.Application;
import io.dropwizard.Configuration;
import io.dropwizard.cli.Command;
import io.dropwizard.cli.ServerCommand;
import io.dropwizard.configuration.ConfigurationSourceProvider;
import io.dropwizard.configuration.YamlConfigurationFactory;
import io.dropwizard.lifecycle.Managed;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import io.dropwizard.util.Sets;
import io.dropwizard.util.Strings;
import net.sourceforge.argparse4j.inf.Namespace;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import static java.util.Objects.requireNonNull;
public class DropwizardTestSupport<C extends Configuration> {
protected final Class<? extends Application<C>> applicationClass;
@Nullable
protected final String configPath;
@Nullable
protected final ConfigurationSourceProvider configSourceProvider;
protected final Set<ConfigOverride> configOverrides;
@Nullable
protected final String customPropertyPrefix;
protected final Function<Application<C>, Command> commandInstantiator;
protected final boolean explicitConfig;
@Nullable
protected C configuration;
@Nullable
protected Application<C> application;
@Nullable
protected Environment environment;
@Nullable
protected Server jettyServer;
protected List<ServiceListener<C>> listeners = new ArrayList<>();
public DropwizardTestSupport(Class<? extends Application<C>> applicationClass,
@Nullable String configPath,
ConfigOverride... configOverrides) {
this(applicationClass, configPath, (String) null, configOverrides);
}
public DropwizardTestSupport(Class<? extends Application<C>> applicationClass,
@Nullable String configPath,
@Nullable ConfigurationSourceProvider configSourceProvider,
ConfigOverride... configOverrides) {
this(applicationClass, configPath, configSourceProvider, null, configOverrides);
}
@Deprecated
public DropwizardTestSupport(Class<? extends Application<C>> applicationClass,
@Nullable String configPath,
Optional<String> customPropertyPrefix,
ConfigOverride... configOverrides) {
this(applicationClass, configPath, customPropertyPrefix.orElse(null), ServerCommand::new, configOverrides);
}
public DropwizardTestSupport(Class<? extends Application<C>> applicationClass,
@Nullable String configPath,
@Nullable ConfigurationSourceProvider configSourceProvider,
@Nullable String customPropertyPrefix,
ConfigOverride... configOverrides) {
this(applicationClass, configPath, configSourceProvider, customPropertyPrefix, ServerCommand::new, configOverrides);
}
public DropwizardTestSupport(Class<? extends Application<C>> applicationClass,
@Nullable String configPath,
@Nullable String customPropertyPrefix,
ConfigOverride... configOverrides) {
this(applicationClass, configPath, customPropertyPrefix, ServerCommand::new, configOverrides);
}
@Deprecated
public DropwizardTestSupport(Class<? extends Application<C>> applicationClass,
@Nullable String configPath,
Optional<String> customPropertyPrefix,
Function<Application<C>, Command> commandInstantiator,
ConfigOverride... configOverrides) {
this(applicationClass, configPath, customPropertyPrefix.orElse(null), commandInstantiator, configOverrides);
}
public DropwizardTestSupport(Class<? extends Application<C>> applicationClass,
@Nullable String configPath,
@Nullable String customPropertyPrefix,
Function<Application<C>, Command> commandInstantiator,
ConfigOverride... configOverrides) {
this(applicationClass, configPath, null, customPropertyPrefix, commandInstantiator, configOverrides);
}
public DropwizardTestSupport(Class<? extends Application<C>> applicationClass,
@Nullable String configPath,
@Nullable ConfigurationSourceProvider configSourceProvider,
@Nullable String customPropertyPrefix,
Function<Application<C>, Command> commandInstantiator,
ConfigOverride... configOverrides) {
this.applicationClass = applicationClass;
this.configPath = configPath;
this.configSourceProvider = configSourceProvider;
this.configOverrides = configOverrides == null ? Collections.emptySet() : Sets.of(configOverrides);
this.customPropertyPrefix = customPropertyPrefix;
this.explicitConfig = false;
this.commandInstantiator = commandInstantiator;
}
public DropwizardTestSupport(Class<? extends Application<C>> applicationClass,
C configuration) {
this(applicationClass, configuration, ServerCommand::new);
}
public DropwizardTestSupport(Class<? extends Application<C>> applicationClass,
@Nullable C configuration,
Function<Application<C>, Command> commandInstantiator) {
if (configuration == null) {
throw new IllegalArgumentException("Can not pass null configuration for explicitly configured instance");
}
this.applicationClass = applicationClass;
this.configPath = "";
this.configSourceProvider = null;
this.configOverrides = Collections.emptySet();
this.customPropertyPrefix = null;
this.configuration = configuration;
this.explicitConfig = true;
this.commandInstantiator = commandInstantiator;
}
public DropwizardTestSupport<C> addListener(ServiceListener<C> listener) {
this.listeners.add(listener);
return this;
}
public DropwizardTestSupport<C> manage(final Managed managed) {
return addListener(new ServiceListener<C>() {
@Override
public void onRun(C configuration, Environment environment, DropwizardTestSupport<C> rule) throws Exception {
environment.lifecycle().manage(managed);
}
});
}
public void before() throws Exception {
applyConfigOverrides();
try {
startIfRequired();
} catch (Exception e) {
after();
throw e;
}
}
public void after() {
try {
stopIfRequired();
} finally {
resetConfigOverrides();
}
}
private void stopIfRequired() {
if (jettyServer != null) {
for (ServiceListener<C> listener : listeners) {
try {
listener.onStop(this);
} catch (Exception ignored) {
}
}
try {
jettyServer.stop();
} catch (Exception e) {
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw new RuntimeException(e);
} finally {
jettyServer = null;
}
}
if (configuration != null) {
configuration.getLoggingFactory().reset();
}
}
private void applyConfigOverrides() {
configOverrides.forEach(ConfigOverride::addToSystemProperties);
}
private void resetConfigOverrides() {
configOverrides.forEach(ConfigOverride::removeFromSystemProperties);
}
private void startIfRequired() throws Exception {
if (jettyServer != null) {
return;
}
application = newApplication();
final Bootstrap<C> bootstrap = new Bootstrap<C>(getApplication()) {
@Override
public void run(C configuration, Environment environment) throws Exception {
environment.lifecycle().addServerLifecycleListener(server -> jettyServer = server);
DropwizardTestSupport.this.configuration = configuration;
DropwizardTestSupport.this.environment = environment;
super.run(configuration, environment);
for (ServiceListener<C> listener : listeners) {
try {
listener.onRun(configuration, environment, DropwizardTestSupport.this);
} catch (Exception ex) {
throw new RuntimeException("Error running app rule start listener", ex);
}
}
}
};
getApplication().initialize(bootstrap);
if (configSourceProvider != null) {
bootstrap.setConfigurationSourceProvider(configSourceProvider);
}
if (explicitConfig) {
bootstrap.setConfigurationFactoryFactory((klass, validator, objectMapper, propertyPrefix) ->
new POJOConfigurationFactory<>(getConfiguration()));
} else if (customPropertyPrefix != null) {
@NotNull
final String prefix = customPropertyPrefix;
bootstrap.setConfigurationFactoryFactory((klass, validator, objectMapper, propertyPrefix) ->
new YamlConfigurationFactory<>(klass, validator, objectMapper, prefix));
}
final Map<String, Object> namespaceAttributes;
if (!Strings.isNullOrEmpty(configPath)) {
namespaceAttributes = Collections.singletonMap("file", configPath);
} else {
namespaceAttributes = Collections.emptyMap();
}
final Namespace namespace = new Namespace(namespaceAttributes);
final Command command = commandInstantiator.apply(application);
command.run(bootstrap, namespace);
}
public C getConfiguration() {
return requireNonNull(configuration);
}
public int getLocalPort() {
return ((ServerConnector) requireNonNull(jettyServer).getConnectors()[0]).getLocalPort();
}
public int getAdminPort() {
final Connector[] connectors = requireNonNull(jettyServer).getConnectors();
return ((ServerConnector) connectors[connectors.length - 1]).getLocalPort();
}
public int getPort(int connectorIndex) {
return ((ServerConnector) requireNonNull(jettyServer).getConnectors()[connectorIndex]).getLocalPort();
}
public Application<C> newApplication() {
try {
return applicationClass.getConstructor().newInstance();
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
public <A extends Application<C>> A getApplication() {
return (A) requireNonNull(application);
}
public Environment getEnvironment() {
return requireNonNull(environment);
}
public ObjectMapper getObjectMapper() {
return getEnvironment().getObjectMapper();
}
public abstract static class ServiceListener<T extends Configuration> {
public void onRun(T configuration, Environment environment, DropwizardTestSupport<T> rule) throws Exception {
}
public void onStop(DropwizardTestSupport<T> rule) throws Exception {
}
}
}