/** * 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.requery; import static java.util.Objects.requireNonNull; import java.util.LinkedList; import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import javax.inject.Provider; import javax.sql.DataSource; import org.jooby.Env; import org.jooby.Env.ServiceKey; import org.jooby.Jooby.Module; import org.jooby.jdbc.Jdbc; import org.slf4j.bridge.SLF4JBridgeHandler; import com.google.inject.Binder; import com.google.inject.Key; import com.google.inject.util.Types; import com.typesafe.config.Config; import io.requery.EntityStore; import io.requery.TransactionListener; import io.requery.async.CompletableEntityStore; import io.requery.async.CompletionStageEntityStore; import io.requery.meta.EntityModel; import io.requery.reactivex.ReactiveEntityStore; import io.requery.reactivex.ReactiveSupport; import io.requery.reactor.ReactorEntityStore; import io.requery.sql.Configuration; import io.requery.sql.ConfigurationBuilder; import io.requery.sql.EntityDataStore; import io.requery.sql.EntityStateListener; import io.requery.sql.SchemaModifier; import io.requery.sql.StatementListener; import io.requery.sql.TableCreationMode; /** * <h1>requery</h1> * <p> * Safe, clean and efficient database access via * <a href="https://github.com/requery/requery">Requery.</a> * </p> * * <h2>usage</h2> * * <pre>{@code * { * use(new Jdbc()); * * use(new Requery(Models.DEFAULT)); * * get("/people", () -> { * EntityStore store = require(EntityStore.class); * return store.select(Person.class) * .where(Person.ID.eq(req.param("id").intValue())) * .get() * .first(); * }); * } * }</pre> * * <p> * This module requires a {@link DataSource} connection. That's why you also need the * {@link Jdbc} module. * </p> * * <h2>code generation</h2> * * <h3>maven</h3> * <p> * We do provide code generation via Maven profile. All you have to do is to write a * <code>requery.activator</code> file inside the <code>src/etc</code> folder. File presence * triggers Requery annotation processor and generated contents. * </p> * * <p> * Generated content can be found at: <code>target/generated-sources</code>. You can change the * default output location by setting the build property <code>requery.output</code> in your * <code>pom.xml</code>. * </p> * * <h3>gradle</h3> * <p> * Please refer to <a href= * "https://github.com/requery/requery/wiki/Gradle-&-Annotation-processing#annotation-processing">requery * documentation</a> for Gradle. * </p> * * <h2>schema generation</h2> * * <pre>{@code * { * use(new Requery(Models.DEFAULT) * .schema(TableCreationMode.DROP_CREATE) * ); * } * }</pre> * * <p> * Optionally, schema generation could be set from .conf file via <code>requery.schema</code> * property. * </p> * * <h3>listeners</h3> * * <pre>{@code * public class MyListener implements EntityStateListener<Person> { * @Inject * public MyListener(Dependency dep) { * this.dep = dep; * } * * ... * } * * { * use(new Requery(Models.DEFAULT) * .entityStateListener(MyListener.class) * ); * } * }</pre> * * <p> * Support for {@link TransactionListener} and {@link StatementListener} is also provided: * </p> * * <pre>{@code * { * use(new Requery(Models.DEFAULT) * .statementListener(MyStatementListener.class) * .transactionListener(TransactionListener.class) * ); * } * }</pre> * * <p> * You can add as many listener as you need. Each listener will be created by <code>Guice</code> * </p> * * <h2>Type-Safe injection</h2> * <p> * If you love <code>DAO</code> like classes, we are happy to tell you that it you easily inject * type-safe {@link EntityStore}: * </p> * * <pre>{@code * * public class PersonDAO { * private EntityStore<Persistable, Person> store; * * @Inject * public PersonDAO(EntityStore<<Persistable, Person> store) { * this.store = store; * } * } * }</pre> * * <p> * Please note we don't inject a <code>raw</code> {@link EntityStore}. Instead we ask for a * <code>Person</code> {@link EntityStore}. You can safely inject a {@link EntityStore} per each of * your domain objects. * </p> * * <h2>async and reactive idioms</h2> * * <p> * Rxjava: * </p> * * <pre>{@code * { * use(Requery.reactive(Models.DEFAULT)); * * get("/", () -> { * ReactiveEntityStore store = require(ReactiveEntityStore.class); * // work with reactive store * }); * } * }</pre> * * <p> * Reactor: * </p> * * <pre>{@code * { * use(Requery.reactor(Models.DEFAULT)); * * get("/", () -> { * ReactorEntityStore store = require(ReactorEntityStore.class); * // work with reactor store * }); * } * }</pre> * * <p> * Java 8: * </p> * * <pre>{@code * { * use(Requery.completionStage(Models.DEFAULT)); * * get("/", () -> { * CompletionStageEntityStore store = require(CompletionStageEntityStore.class); * // work with reactor store * }); * } * }</pre> * * <h2>advanced configuration</h2> * <p> * Advanced configuration is available via callback function: * </p> * * <pre>{@code * { * use(new Requery(Models.DEFAULT) * .doWith(builder -> { * builder.useDefaultLogging(); * .... * }) * ); * } * }</pre> * * @author edgar * */ @SuppressWarnings({"rawtypes", "unchecked" }) public class Requery implements Module { static { if (!SLF4JBridgeHandler.isInstalled()) { SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install(); } } private final EntityModel model; private TableCreationMode schema; private final Function<Configuration, EntityStore> store; private final Class<? extends EntityStore> storeType; private List<Class> states = new LinkedList<>(); private List<Class> statements = new LinkedList<>(); private List<Class> transactions = new LinkedList<>(); private BiConsumer<Config, ConfigurationBuilder> callback; private Provider<DataSource> dataSource; private Requery(final Class<? extends EntityStore> storeType, final EntityModel model, final Function<Configuration, EntityStore> store) { this.storeType = storeType; this.model = model; this.store = store; } /** * Creates a new {@link Requery} module. * * @param model Entity model. */ public Requery(final EntityModel model) { this(EntityStore.class, model, c -> new EntityDataStore<>(c)); } /** * Advanced configuration callback: * * <pre>{@code * * use(new Requery(Models.DEFAULT) * .doWith((conf, builder) -> { * builder.useDefaultLogging(); * }); * * }</pre> * * @param configurer Configurer callback. * @return This module. */ public Requery doWith(final BiConsumer<Config, ConfigurationBuilder> configurer) { this.callback = requireNonNull(configurer, "Configurer callback required."); return this; } /** * Advanced configuration callback: * * <pre>{@code * * use(new Requery(Models.DEFAULT) * .doWith(builder -> { * builder.useDefaultLogging(); * }) * ); * * }</pre> * * @param configurer Configurer callback. * @return This module. */ public Requery doWith(final Consumer<ConfigurationBuilder> configurer) { requireNonNull(configurer, "Configurer callback required."); return doWith((c, b) -> configurer.accept(b)); } /** * Set a custom {@link DataSource}. * * <pre>{@code * * use(new Requery(Models.DEFAULT) * .dataSource(() -> new MySuperDataSource()) * ); * * }</pre> * * @param dataSource DataSource to use. * @return This module. */ public Requery dataSource(final Provider<DataSource> dataSource) { this.dataSource = requireNonNull(dataSource, "DataSource required."); return this; } /** * Run the give schema command at application startup time. * * @param schema Command to run. * @return This module. */ public Requery schema(final TableCreationMode schema) { this.schema = schema; return this; } /** * Add an {@link EntityStateListener}. The listener will be created by Guice. * * @param listener Guice entity listener. * @return This module. */ public Requery entityStateListener(final Class<? extends EntityStateListener<?>> listener) { this.states.add(listener); return this; } /** * Add an {@link StatementListener}. The listener will be created by Guice. * * @param listener Guice statement listener. * @return This module. */ public Requery statementListener(final Class<? extends StatementListener> listener) { this.statements.add(listener); return this; } /** * Add an {@link TransactionListener}. The listener will be created by Guice. * * @param listener Guice transaction listener. * @return This module. */ public Requery transactionListener(final Class<? extends TransactionListener> listener) { this.transactions.add(listener); return this; } @Override public void configure(final Env env, final Config conf, final Binder binder) { AtomicReference<Object> holder = new AtomicReference<>(); Provider provider = holder::get; Consumer bind = k -> binder.bind((Key) k).toProvider(provider); ServiceKey keys = env.serviceKey(); /** * For each model class we publish a type-safe EntityStore, so users can easily inject stores * for specific types. */ model.getTypes().forEach(it -> { // Person(result) extends AbstractPerson implements Persistable (base) Class target = it.getClassType(); Class base = target.getInterfaces()[0]; bind.accept(Key.get(Types.newParameterizedType(storeType, base, target))); }); keys.generate(storeType, model.getName(), bind); env.onStart(registry -> { DataSource ds = Optional.ofNullable(this.dataSource) .map(Provider::get) .orElseGet(() -> registry.require(DataSource.class)); schema(conf, schema, schema -> new SchemaModifier(ds, model).createTables(schema)); ConfigurationBuilder builder = new ConfigurationBuilder(ds, model); if (callback != null) { callback.accept(conf, builder); } states .forEach(t -> builder.addEntityStateListener((EntityStateListener) registry.require(t))); statements .forEach(t -> builder.addStatementListener((StatementListener) registry.require(t))); transactions.forEach(t -> builder .addTransactionListenerFactory(() -> (TransactionListener) registry.require(t))); Configuration configuration = builder.build(); holder.set(store.apply(configuration)); }); } /** * Creates a Requery module with RxJava data store. * * <pre>{@code * { * use(Requery.reactive(Models.DEFAULT)); * * get("/", () -> { * ReactiveEntityStore store = require(ReactiveEntityStore.class); * // work with reactive store * }); * } * }</pre> * * @param model Entity model. * @return A new {@link Requery} module. */ public static Requery reactive(final EntityModel model) { return new Requery(ReactiveEntityStore.class, model, conf -> ReactiveSupport.toReactiveStore(new EntityDataStore<>(conf))); } /** * Creates a Requery module with Reactor data store. * * <pre>{@code * { * use(Requery.reactive(Models.DEFAULT)); * * get("/", () -> { * ReactorEntityStore store = require(ReactorEntityStore.class); * // work with reactive store * }); * } * }</pre> * * @param model Entity model. * @return A new {@link Requery} module. */ public static Requery reactor(final EntityModel model) { return new Requery(ReactorEntityStore.class, model, conf -> new ReactorEntityStore<>(new EntityDataStore<>(conf))); } /** * Creates a Requery module with Java 8 data store. * * <pre>{@code * { * use(Requery.reactive(Models.DEFAULT)); * * get("/", () -> { * CompletionStageEntityStore store = require(CompletionStageEntityStore.class); * // work with reactive store * }); * } * }</pre> * * @param model Entity model. * @return A new {@link Requery} module. */ public static Requery completionStage(final EntityModel model) { return new Requery(CompletionStageEntityStore.class, model, conf -> new CompletableEntityStore(new EntityDataStore<>(conf))); } private void schema(final Config conf, final TableCreationMode schema, final Consumer<TableCreationMode> callback) { if (schema != null) { callback.accept(schema); } if (conf.hasPath("requery.schema")) { callback.accept(TableCreationMode.valueOf(conf.getString("requery.schema").toUpperCase())); } } }