ratpack.impose.Impositions Maven / Gradle / Ivy
/*
* Copyright 2015 the original author or authors.
*
* 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 ratpack.impose;
import com.google.common.collect.Maps;
import com.google.common.collect.Queues;
import io.netty.util.concurrent.FastThreadLocal;
import ratpack.func.Action;
import ratpack.func.Factory;
import ratpack.registry.Registry;
import ratpack.util.Exceptions;
import java.util.Deque;
import java.util.Map;
import java.util.Optional;
/**
* A mechanism for imposing things on an application from the outside and typically during testing.
*
* The impositions mechanism exists primarily to facilitate convenient overriding of configuration and behaviour at test time.
* {@link ratpack.test.MainClassApplicationUnderTest} builds upon this mechanism.
* It uses the {@link #impose(Impositions, Factory)} to register impositions, while starting up the application under test within the given function.
*
* Ratpack “components” are explicitly designed to be aware of impositions.
* Such components obtain the impositions either via the {@link #current()} method, or via the server registry (e.g. Guice injection).
* User code does not typically need to be aware of impositions, as impositions are general used to influence upstream configuration.
* If you do need access to the impositions, prefer obtaining it from the server registry over {@link #current()} as this method is thread sensitive.
*
* Actual impositions are implemented as specific classes, known to the consumer.
* {@link ForceServerListenPortImposition} and {@link UserRegistryImposition} are both examples of such.
* The consumers of these impositions simply obtain them from the impositions registry.
*
*
*
{@code
* import ratpack.server.ServerConfig;
* import ratpack.impose.Impositions;
* import ratpack.impose.ServerConfigImposition;
* import ratpack.test.embed.EmbeddedApp;
*
* import static groovy.util.GroovyTestCase.assertEquals;
* import static java.util.Collections.singletonMap;
*
* public class Example {
* public static void main(String[] args) throws Exception {
* Impositions.of(i ->
* i.add(ServerConfigImposition.of(s -> s .props(singletonMap("foo", "imposed!"))))
* ).impose(() ->
* EmbeddedApp.of(s -> s
* .serverConfig(c -> c
* .props(singletonMap("foo", "original"))
* )
* .handlers(c -> c
* .get(ctx -> ctx.render(ctx.get(ServerConfig.class).get("/foo", String.class)))
* )
* )
* ).test(testHttpClient ->
* assertEquals("imposed!", testHttpClient.getText())
* );
* }
* }
* }
*
* @see ServerConfigImposition
* @see ForceServerListenPortImposition
* @see ForceDevelopmentImposition
* @see UserRegistryImposition
* @since 1.2
*/
@SuppressWarnings("JavadocReference")
public final class Impositions {
private static final FastThreadLocal> IMPOSITIONS = new FastThreadLocal<>();
private final Registry impositions;
private Impositions(Registry impositions) {
this.impositions = impositions;
}
/**
* Sets impositions that will be available during execution of the given function, from this thread.
*
* The given impositions will effectively be the value returned by {@link #current()} during the given function,
* which is executed immediately.
*
* The given impositions will only be returned from {@link #current()} if called from the same thread that is calling this method.
*
* @param impositions the impositions to impose during the given function
* @param during the function to execute while the impositions are imposed
* @param the type of result of the given function
* @return the result of the given function
* @throws Exception any thrown by {@code during}
*/
public static T impose(Impositions impositions, Factory extends T> during) throws Exception {
Deque queue = IMPOSITIONS.get();
if (queue == null) {
queue = Queues.newArrayDeque();
IMPOSITIONS.set(queue);
}
queue.push(impositions);
try {
return during.create();
} finally {
queue.poll();
if (queue.isEmpty()) {
IMPOSITIONS.remove();
}
}
}
/**
* Delegates to {@link #impose(Impositions, Factory)}, with {@code this} as the impositions.
*
* @param during the function to execute while the impositions are imposed
* @param the type of result of the given function
* @return the result of the given function
* @throws Exception any thrown by {@code during}
*/
public T impose(Factory extends T> during) throws Exception {
return impose(this, during);
}
/**
* The currently imposed impositions.
*
* When called during a call to {@link #impose(Impositions, Factory)} from the same thread,
* returns an the impositions given to that method.
*
* If no impositions have been imposed at call time, the returned impositions object is effectively empty.
*
* @return the currently imposed impositions
*/
public static Impositions current() {
return Optional.ofNullable(IMPOSITIONS.get())
.map(Deque::peek)
.orElseGet(Impositions::none);
}
/**
* An empty set of impositions.
*
* Possibly useful during testing.
*
* @return an empty set of impositions
*/
public static Impositions none() {
return Exceptions.uncheck(() -> of(Action.noop()));
}
/**
* Creates an impositions instance of the given imposition objects.
*
* Possibly useful during testing.
*
* @return an impositions instance of the given imposition objects
*/
public static Impositions of(Action super ImpositionsSpec> consumer) throws Exception {
Map, Imposition> map = Maps.newHashMap();
consumer.execute(new ImpositionsSpec() {
@Override
public ImpositionsSpec add(Imposition imposition) {
map.put(imposition.getClass(), imposition);
return this;
}
});
return new Impositions(Registry.of(r -> map.values().forEach(r::add)));
}
/**
* Return an imposition of the given type, if one is currently imposed.
*
* @param type the type of imposition
* @param the type of imposition
* @return the impositions of the given type
*/
public Optional get(Class type) {
return impositions.maybeGet(type);
}
}