/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.jooby; import static java.util.Objects.requireNonNull; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import java.util.function.BiFunction; import java.util.function.BinaryOperator; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.inject.Key; import com.google.inject.name.Names; import com.typesafe.config.Config; import javaslang.API; import javaslang.control.Option; import javaslang.control.Try.CheckedConsumer; /** * Allows to optimize, customize or apply defaults values for application services. * * <p> * A env is represented by it's name. For example: <code>dev</code>, <code>prod</code>, etc... A * <strong>dev</strong> env is special and a module provider could do some special configuration for * development, like turning off a cache, reloading of resources, etc. * </p> * <p> * Same is true for not <strong>dev</strong> environments. For example, a module provider might * create a high performance connection pool, caches, etc. * </p> * <p> * By default env is set to <code>dev</code>, but you can change it by setting the * <code>application.env</code> property to anything else. * </p> * * @author edgar * @since 0.1.0 */ public interface Env extends LifeCycle { /** * Property source for {@link Resolver} * * @author edgar * @since 1.1.0 */ interface PropertySource { /** * Get a property value or throw {@link NoSuchElementException}. * * @param key Property key/name. * @return Value or throw {@link NoSuchElementException}. * @throws NoSuchElementException If property is missing. */ String get(String key) throws NoSuchElementException; } /** * {@link PropertySource} for {@link Config}. * * @author edgar * @since 1.1.0 */ class ConfigSource implements PropertySource { private Config source; public ConfigSource(final Config source) { this.source = source; } @Override public String get(final String key) throws NoSuchElementException { if (source.hasPath(key)) { return source.getString(key); } throw new NoSuchElementException(key); } } /** * {@link PropertySource} for {@link Map}. * * @author edgar * @since 1.1.0 */ class MapSource implements PropertySource { private Map<String, Object> source; public MapSource(final Map<String, Object> source) { this.source = source; } @Override public String get(final String key) throws NoSuchElementException { Object value = source.get(key); if (value != null) { return value.toString(); } throw new NoSuchElementException(key); } } /** * Template literal implementation, replaces <code>${expression}</code> from a String using a * {@link Config} object. * * @author edgar */ class Resolver { private String startDelim = "${"; private String endDelim = "}"; private PropertySource source; private boolean ignoreMissing; /** * Set property source. * * @param source Source. * @return This resolver. */ public Resolver source(final Map<String, Object> source) { return source(new MapSource(source)); } /** * Set property source. * * @param source Source. * @return This resolver. */ public Resolver source(final PropertySource source) { this.source = source; return this; } /** * Set property source. * * @param source Source. * @return This resolver. */ public Resolver source(final Config source) { return source(new ConfigSource(source)); } /** * Set start and end delimiters. * * @param start Start delimiter. * @param end End delimiter. * @return This resolver. */ public Resolver delimiters(final String start, final String end) { this.startDelim = requireNonNull(start, "Start delimiter required."); this.endDelim = requireNonNull(end, "End delmiter required."); return this; } /** * Ignore missing property replacement and leave the expression untouch. * * @return This resolver. */ public Resolver ignoreMissing() { this.ignoreMissing = true; return this; } /** * Returns a string with all substitutions (the <code>${foo.bar}</code> syntax, * see <a href="https://github.com/typesafehub/config/blob/master/HOCON.md">the * spec</a>) resolved. Substitutions are looked up using the <code>source</code> param as the * root object, that is, a substitution <code>${foo.bar}</code> will be replaced with * the result of <code>getValue("foo.bar")</code>. * * @param text Text to process. * @return A processed string. */ public String resolve(final String text) { requireNonNull(text, "Text is required."); if (text.length() == 0) { return ""; } BiFunction<Integer, BiFunction<Integer, Integer, RuntimeException>, RuntimeException> err = ( start, ex) -> { String snapshot = text.substring(0, start); int line = Splitter.on('\n').splitToList(snapshot).size(); int column = start - snapshot.lastIndexOf('\n'); return ex.apply(line, column); }; StringBuilder buffer = new StringBuilder(); int offset = 0; int start = text.indexOf(startDelim); while (start >= 0) { int end = text.indexOf(endDelim, start + startDelim.length()); if (end == -1) { throw err.apply(start, (line, column) -> new IllegalArgumentException( "found '" + startDelim + "' expecting '" + endDelim + "' at " + line + ":" + column)); } buffer.append(text.substring(offset, start)); String key = text.substring(start + startDelim.length(), end); Object value; try { value = source.get(key); } catch (NoSuchElementException x) { if (ignoreMissing) { value = text.substring(start, end + endDelim.length()); } else { throw err.apply(start, (line, column) -> new NoSuchElementException( "Missing " + startDelim + key + endDelim + " at " + line + ":" + column)); } } buffer.append(value); offset = end + endDelim.length(); start = text.indexOf(startDelim, offset); } if (buffer.length() == 0) { return text; } if (offset < text.length()) { buffer.append(text.substring(offset)); } return buffer.toString(); } } /** * Utility class for generating {@link Key} for named services. * * @author edgar */ class ServiceKey { private Map<Object, Integer> instances = new HashMap<>(); /** * Generate at least one named key for the provided type. If this is the first call for the * provided type then it generates an unnamed key. * * @param type Service type. * @param name Service name. * @param keys Key callback. Invoked once with a named key, and optionally again with an unamed * key. * @param <T> Service type. */ public <T> void generate(final Class<T> type, final String name, final Consumer<Key<T>> keys) { Integer c = instances.put(type, instances.getOrDefault(type, 0) + 1); if (c == null) { // def key keys.accept(Key.get(type)); } keys.accept(Key.get(type, Names.named(name))); } } /** * Build an jooby environment. * * @author edgar */ interface Builder { /** * Build a new environment from a {@link Config} object. The environment is created from the * <code>application.env</code> property. If such property is missing, env's name must be: * <code>dev</code>. * * Please note an environment created with this method won't have a {@link Env#router()}. * * @param config A config instance. * @return A new environment. */ default Env build(final Config config) { return build(config, null, Locale.getDefault()); } /** * Build a new environment from a {@link Config} object. The environment is created from the * <code>application.env</code> property. If such property is missing, env's name must be: * <code>dev</code>. * * @param config A config instance. * @param router Application router. * @param locale App locale. * @return A new environment. */ Env build(Config config, Router router, Locale locale); } /** * Default builder. */ Env.Builder DEFAULT = (config, router, locale) -> { requireNonNull(config, "Config required."); String name = config.hasPath("application.env") ? config.getString("application.env") : "dev"; return new Env() { private ImmutableList.Builder<CheckedConsumer<Registry>> start = ImmutableList.builder(); private ImmutableList.Builder<CheckedConsumer<Registry>> started = ImmutableList.builder(); private ImmutableList.Builder<CheckedConsumer<Registry>> shutdown = ImmutableList.builder(); private Map<String, Function<String, String>> xss = new HashMap<>(); private ServiceKey key = new ServiceKey(); @Override public String name() { return name; } @Override public ServiceKey serviceKey() { return key; } @Override public Router router() { if (router == null) { throw new UnsupportedOperationException(); } return router; } @Override public Config config() { return config; } @Override public Locale locale() { return locale; } @Override public String toString() { return name(); } @Override public List<CheckedConsumer<Registry>> stopTasks() { return shutdown.build(); } @Override public Env onStop(final CheckedConsumer<Registry> task) { this.shutdown.add(task); return this; } @Override public Env onStart(final CheckedConsumer<Registry> task) { this.start.add(task); return this; } @Override public LifeCycle onStarted(final CheckedConsumer<Registry> task) { this.started.add(task); return this; } @Override public List<CheckedConsumer<Registry>> startTasks() { return this.start.build(); } @Override public List<CheckedConsumer<Registry>> startedTasks() { return this.started.build(); } @Override public Map<String, Function<String, String>> xss() { return Collections.unmodifiableMap(xss); } @Override public Env xss(final String name, final Function<String, String> escaper) { xss.put(requireNonNull(name, "Name required."), requireNonNull(escaper, "Function required.")); return this; } }; }; /** * @return Env's name. */ String name(); /** * Application router. * * @return Available {@link Router}. * @throws UnsupportedOperationException if router isn't available. */ Router router() throws UnsupportedOperationException; /** * @return environment properties. */ Config config(); /** * @return Default locale from <code>application.lang</code>. */ Locale locale(); /** * @return Utility method for generating keys for named services. */ default ServiceKey serviceKey() { return new ServiceKey(); } /** * Returns a string with all substitutions (the <code>${foo.bar}</code> syntax, * see <a href="https://github.com/typesafehub/config/blob/master/HOCON.md">the * spec</a>) resolved. Substitutions are looked up using the {@link #config()} as the root object, * that is, a substitution <code>${foo.bar}</code> will be replaced with * the result of <code>getValue("foo.bar")</code>. * * @param text Text to process. * @return A processed string. */ default String resolve(final String text) { return resolver().resolve(text); } /** * Creates a new environment {@link Resolver}. * * @return */ default Resolver resolver() { return new Resolver() .source(config()); } /** * Runs the callback function if the current env matches the given name. * * @param name A name to test for. * @param fn A callback function. * @param <T> A resulting type. * @return A resulting object. */ default <T> Optional<T> ifMode(final String name, final Supplier<T> fn) { if (name().equals(name)) { return Optional.of(fn.get()); } return Optional.empty(); } /** * Produces a {@link API.Match} of the current {@link Env}. * * <pre> * String accessKey = env.match() * .when("dev", () {@literal ->} "1234") * .when("stage", () {@literal ->} "4321") * .when("prod", () {@literal ->} "abc") * .get(); * </pre> * * @return A new matcher. */ default API.Match<String> match() { return API.Match(name()); } /** * Produces a {@link API.Match} of the current {@link Env}. * * <pre> * String accessKey = env.when("dev", () {@literal ->} "1234") * .when("stage", () {@literal ->} "4321") * .when("prod", () {@literal ->} "abc") * .get(); * </pre> * * @param name A name to test for. * @param fn A callback function. * @param <T> A resulting type. * @return A new matcher. */ default <T> Option<T> when(final String name, final Supplier<T> fn) { return match().option(API.Case(API.$(name), fn)); } /** * Produces a {@link API.Match} of the current {@link Env}. * * <pre> * String accessKey = env.when("dev", "1234") * .when("stage", "4321") * .when("prod", "abc") * .get(); * </pre> * * @param name A name to test for. * @param result A constant value to return. * @param <T> A resulting type. * @return A new matcher. */ default <T> Option<T> when(final String name, final T result) { return match().option(API.Case(API.$(name), result)); } /** * Produces a {@link API.Match} of the current {@link Env}. * * <pre> * String accessKey = env.when("dev", () {@literal ->} "1234") * .when("stage", () {@literal ->} "4321") * .when("prod", () {@literal ->} "abc") * .get(); * </pre> * * @param predicate A predicate to use. * @param result A constant value to return. * @param <T> A resulting type. * @return A new matcher. */ default <T> Option<T> when(final Predicate<String> predicate, final T result) { return match().option(API.Case(predicate, result)); } /** * @return XSS escape functions. */ Map<String, Function<String, String>> xss(); /** * Get or chain the required xss functions. * * @param xss XSS to combine. * @return Chain of required xss functions. */ default Function<String, String> xss(final String... xss) { Map<String, Function<String, String>> fn = xss(); BinaryOperator<Function<String, String>> reduce = Function::andThen; return Arrays.asList(xss) .stream() .map(fn::get) .filter(Objects::nonNull) .reduce(Function.identity(), reduce); } /** * Set/override a XSS escape function. * * @param name Escape's name. * @param escaper Escape function. * @return This environment. */ Env xss(String name, Function<String, String> escaper); /** * @return List of start tasks. */ List<CheckedConsumer<Registry>> startTasks(); /** * @return List of start tasks. */ List<CheckedConsumer<Registry>> startedTasks(); /** * @return List of stop tasks. */ List<CheckedConsumer<Registry>> stopTasks(); }