diff --git a/bom/pom.xml b/bom/pom.xml index c22ded21a..b43f9fb4e 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -503,6 +503,75 @@ helidon-common-metrics ${helidon.version} + + io.helidon.common + helidon-common-mapper + ${project.version} + + + + + io.helidon.dbclient + helidon-dbclient + ${project.version} + + + io.helidon.dbclient + helidon-dbclient-common + ${project.version} + + + io.helidon.dbclient + helidon-dbclient-jdbc + ${project.version} + + + io.helidon.dbclient + helidon-dbclient-mongodb + ${project.version} + + + io.helidon.dbclient + helidon-dbclient-mongodb + ${project.version} + + + io.helidon.dbclient + helidon-dbclient-health + ${project.version} + + + io.helidon.dbclient + helidon-dbclient-jsonp + ${project.version} + + + io.helidon.dbclient + helidon-dbclient-metrics + ${project.version} + + + io.helidon.dbclient + helidon-dbclient-metrics-jdbc + ${project.version} + + + io.helidon.dbclient + helidon-dbclient-tracing + ${project.version} + + + io.helidon.dbclient + helidon-dbclient-webserver-jsonp + ${project.version} + + + + + io.helidon.examples.dbclient + helidon-examples-dbclient-common + ${project.version} + diff --git a/common/configurable/src/main/java/io/helidon/common/configurable/LruCache.java b/common/configurable/src/main/java/io/helidon/common/configurable/LruCache.java new file mode 100644 index 000000000..6e1503d61 --- /dev/null +++ b/common/configurable/src/main/java/io/helidon/common/configurable/LruCache.java @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.common.configurable; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Optional; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Supplier; + +import io.helidon.config.Config; + +/** + * Least recently used cache. + * This cache has a capacity. When the capacity is reached, the oldest record is removed from the cache when a new one + * is added. + * + * @param type of the keys of the map + * @param type of the values of the map + */ +public final class LruCache { + /** + * Default capacity of the cache: {@value}. + */ + public static final int DEFAULT_CAPACITY = 10000; + + private final LinkedHashMap backingMap = new LinkedHashMap<>(); + private final ReadWriteLock rwLock = new ReentrantReadWriteLock(true); + private final Lock readLock = rwLock.readLock(); + private final Lock writeLock = rwLock.writeLock(); + + private final int capacity; + + private LruCache(Builder builder) { + this.capacity = builder.capacity; + } + + /** + * Create a new builder. + * + * @param key type + * @param value type + * @return a new fluent API builder instance + */ + public static Builder builder() { + return new Builder<>(); + } + + /** + * Create an instance with default configuration. + * + * @param key type + * @param value type + * @return a new cache instance + * @see #DEFAULT_CAPACITY + */ + public static LruCache create() { + Builder builder = builder(); + return builder.build(); + } + + /** + * Get a value from the cache. + * + * @param key key to retrieve + * @return value if present or empty + */ + public Optional get(K key) { + readLock.lock(); + + V value; + try { + value = backingMap.get(key); + } finally { + readLock.unlock(); + } + + if (null == value) { + return Optional.empty(); + } + + writeLock.lock(); + try { + // make sure the value is the last in the map (I do ignore a race here, as it is not significant) + // if some other thread moved another record to the front, we just move ours before it + + // TODO this hurts - we just need to move the key to the last position + // maybe this should be replaced with a list and a map? + value = backingMap.get(key); + if (null == value) { + return Optional.empty(); + } + backingMap.remove(key); + backingMap.put(key, value); + + return Optional.of(value); + } finally { + writeLock.unlock(); + } + } + + /** + * Remove a value from the cache. + * + * @param key key of the record to remove + * @return the value that was mapped to the key, or empty if none was + */ + public Optional remove(K key) { + + writeLock.lock(); + try { + return Optional.ofNullable(backingMap.remove(key)); + } finally { + writeLock.unlock(); + } + } + + /** + * Put a value to the cache. + * + * @param key key to add + * @param value value to add + * @return value that was already mapped or empty if the value was not mapped + */ + public Optional put(K key, V value) { + writeLock.lock(); + try { + V currentValue = backingMap.remove(key); + if (null == currentValue) { + // need to free space - we did not make the map smaller + if (backingMap.size() >= capacity) { + Iterator iterator = backingMap.values().iterator(); + iterator.next(); + iterator.remove(); + } + } + + backingMap.put(key, value); + return Optional.ofNullable(currentValue); + } finally { + writeLock.unlock(); + } + } + + /** + * Either return a cached value or compute it and cache it. + * In case this method is called in parallel for the same key, the value actually present in the map may be from + * any of the calls. + * This method always returns either the existing value from the map, or the value provided by the supplier. It + * never returns a result from another thread's supplier. + * + * @param key key to check/insert value for + * @param valueSupplier supplier called if the value is not yet cached, or is invalid + * @return current value from the cache, or computed value from the supplier + */ + public Optional computeValue(K key, Supplier> valueSupplier) { + // get is properly synchronized + Optional currentValue = get(key); + if (currentValue.isPresent()) { + return currentValue; + } + Optional newValue = valueSupplier.get(); + // put is also properly synchronized - nevertheless we may replace the value more then once + // if called from parallel threads + newValue.ifPresent(theValue -> put(key, theValue)); + + return newValue; + } + + /** + * Current size of the map. + * + * @return number of records currently cached + */ + public int size() { + readLock.lock(); + try { + return backingMap.size(); + } finally { + readLock.unlock(); + } + } + + /** + * Capacity of this cache. + * + * @return configured capacity of this cache + */ + public int capacity() { + return capacity; + } + + // for unit testing + V directGet(K key) { + return backingMap.get(key); + } + + /** + * Fluent API builder for {@link io.helidon.common.configurable.LruCache}. + * + * @param type of keys + * @param type of values + */ + public static class Builder implements io.helidon.common.Builder> { + private int capacity = DEFAULT_CAPACITY; + + @Override + public LruCache build() { + return new LruCache<>(this); + } + + /** + * Load configuration of this cache from configuration. + * + * @param config configuration + * @return updated builder instance + */ + public Builder config(Config config) { + config.get("capacity").asInt().ifPresent(this::capacity); + return this; + } + + /** + * Configure capacity of the cache. + * + * @param capacity maximal number of records in the cache before the oldest one is removed + * @return updated builder instance + */ + public Builder capacity(int capacity) { + this.capacity = capacity; + return this; + } + } +} diff --git a/common/configurable/src/test/java/io/helidon/common/configurable/LruCacheTest.java b/common/configurable/src/test/java/io/helidon/common/configurable/LruCacheTest.java new file mode 100644 index 000000000..7c8167fc8 --- /dev/null +++ b/common/configurable/src/test/java/io/helidon/common/configurable/LruCacheTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.common.configurable; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Unit test for {@link LruCache}. + */ +class LruCacheTest { + @Test + void testCache() { + LruCache theCache = LruCache.create(); + String value = "cached"; + String key = "theKey"; + String newValue = "not-cached"; + + Optional res = theCache.put(key, value); + assertThat(res, is(Optional.empty())); + res = theCache.get(key); + assertThat(res, is(Optional.of(value))); + res = theCache.computeValue(key, () -> Optional.of(newValue)); + assertThat(res, is(Optional.of(value))); + res = theCache.remove(key); + assertThat(res, is(Optional.of(value))); + res = theCache.get(key); + assertThat(res, is(Optional.empty())); + } + + @Test + void testCacheComputeValue() { + LruCache theCache = LruCache.create(); + String value = "cached"; + String key = "theKey"; + String newValue = "not-cached"; + + Optional res = theCache.computeValue(key, () -> Optional.of(value)); + assertThat(res, is(Optional.of(value))); + res = theCache.get(key); + assertThat(res, is(Optional.of(value))); + res = theCache.computeValue(key, () -> Optional.of(newValue)); + assertThat(res, is(Optional.of(value))); + res = theCache.remove(key); + assertThat(res, is(Optional.of(value))); + res = theCache.get(key); + assertThat(res, is(Optional.empty())); + } + + @Test + void testMaxCapacity() { + LruCache theCache = LruCache.builder().capacity(10).build(); + for (int i = 0; i < 10; i++) { + theCache.put(i, i); + } + for (int i = 0; i < 10; i++) { + Optional integer = theCache.get(i); + assertThat(integer, is(Optional.of(i))); + } + theCache.put(10, 10); + Optional res = theCache.get(0); + assertThat(res, is(Optional.empty())); + res = theCache.get(10); + assertThat(res, is(Optional.of(10))); + } + + @Test + void testLruBehavior() { + LruCache theCache = LruCache.builder().capacity(10).build(); + for (int i = 0; i < 10; i++) { + // insert all + theCache.put(i, i); + } + for (int i = 0; i < 10; i++) { + // use them in ascending order + Optional integer = theCache.get(i); + assertThat(integer, is(Optional.of(i))); + } + // now use 0 + Optional value = theCache.get(0); + assertThat(value, is(Optional.of(0))); + + theCache.put(10, 10); + + // 0 should be in + value = theCache.get(0); + assertThat(value, is(Optional.of(0))); + + // 1 should not + value = theCache.get(1); + assertThat(value, is(Optional.empty())); + + } +} diff --git a/common/reactive/src/main/java/io/helidon/common/reactive/Flows.java b/common/reactive/src/main/java/io/helidon/common/reactive/Flows.java new file mode 100644 index 000000000..d4e9e87af --- /dev/null +++ b/common/reactive/src/main/java/io/helidon/common/reactive/Flows.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.common.reactive; + +import java.util.concurrent.Flow; + +/** + * Utilities for Flow API. + */ +public final class Flows { + private Flows() { + } + + /** + * Empty publisher. + * + * @param type of the publisher + * @return a new empty publisher that just completes the subscriber + */ + public static Flow.Publisher emptyPublisher() { + return subscriber -> subscriber.onSubscribe(new Flow.Subscription() { + @Override + public void request(long n) { + subscriber.onComplete(); + } + + @Override + public void cancel() { + } + }); + } + + /** + * A publisher of a single value. + * + * @param value value to publish + * @param type of the publisher + * @return a new publisher that publishes the single value and completes the subscriber + */ + public static Flow.Publisher singletonPublisher(T value) { + return subscriber -> subscriber.onSubscribe(new Flow.Subscription() { + @Override + public void request(long n) { + subscriber.onNext(value); + subscriber.onComplete(); + } + + @Override + public void cancel() { + } + }); + } +} diff --git a/common/reactive/src/main/java/io/helidon/common/reactive/MappingProcessor.java b/common/reactive/src/main/java/io/helidon/common/reactive/MappingProcessor.java new file mode 100644 index 000000000..435083f1c --- /dev/null +++ b/common/reactive/src/main/java/io/helidon/common/reactive/MappingProcessor.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.common.reactive; + +import java.util.concurrent.Flow; +import java.util.function.Function; + +/** + * A {@link Flow.Processor} that only maps the source type to target type using a mapping function. + * + * @param type of the publisher we subscribe to + * @param type of the publisher we expose + */ +public final class MappingProcessor implements Flow.Processor { + private final Function resultMapper; + private Flow.Subscriber mySubscriber; + private Flow.Subscription subscription; + + private MappingProcessor(Function resultMapper) { + this.resultMapper = resultMapper; + } + + /** + * Create a mapping processor for a mapping function. + * @param mappingFunction function that maps source to target (applied for each record) + * @param Source type + * @param Target type + * @return a new mapping processor + */ + public static MappingProcessor create(Function mappingFunction) { + return new MappingProcessor<>(mappingFunction); + } + + @Override + public void subscribe(Flow.Subscriber subscriber) { + this.mySubscriber = subscriber; + subscriber.onSubscribe(new Flow.Subscription() { + @Override + public void request(long n) { + if (null != subscription) { + subscription.request(n); + } + } + + @Override + public void cancel() { + if (null != subscription) { + subscription.cancel(); + } + } + }); + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + } + + @Override + public void onNext(SOURCE item) { + mySubscriber.onNext(resultMapper.apply(item)); + } + + @Override + public void onError(Throwable throwable) { + mySubscriber.onError(throwable); + } + + @Override + public void onComplete() { + mySubscriber.onComplete(); + } +} diff --git a/common/reactive/src/main/java/io/helidon/common/reactive/OptionalCompletionStage.java b/common/reactive/src/main/java/io/helidon/common/reactive/OptionalCompletionStage.java new file mode 100644 index 000000000..b8785ce39 --- /dev/null +++ b/common/reactive/src/main/java/io/helidon/common/reactive/OptionalCompletionStage.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.common.reactive; + +import java.util.Optional; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; + +/** + * A completion stage that allows processing of cases when the element + * is present and when not. + * + * @param return type of the asynchronous operation + */ +public interface OptionalCompletionStage extends CompletionStage> { + + /** + * Returns a new {@code OptionalCompletionStage} that, when this stage completes + * normally and returns {@code null}, executes the given action. + * + * @param action the action to perform before completing the + * returned {@code OptionalCompletionStage} + * @return the new {@code OptionalCompletionStage} + */ + OptionalCompletionStage onEmpty(Runnable action); + + /** + * Returns a new {@code OptionalCompletionStage} that, when this stage completes + * normally and returns non-{@code null}, is executed with this stage's + * result as the argument to the supplied action. + * + * @param action the action to perform before completing the + * returned {@code OptionalCompletionStage} + * @return the new {@code OptionalCompletionStage} + */ + OptionalCompletionStage onValue(Consumer action); + + /** + * Creates a new instance of the completion stage that allows processing of cases when the element + * is present and when not. + * + * @param return type of the asynchronous operation + * @param originalStage source completion stage instance + * @return the new {@code OptionalCompletionStage} + */ + static OptionalCompletionStage create(CompletionStage> originalStage) { + return new OptionalCompletionStageImpl<>(originalStage); + } + +} diff --git a/common/reactive/src/main/java/io/helidon/common/reactive/OptionalCompletionStageImpl.java b/common/reactive/src/main/java/io/helidon/common/reactive/OptionalCompletionStageImpl.java new file mode 100644 index 000000000..9832fc1ca --- /dev/null +++ b/common/reactive/src/main/java/io/helidon/common/reactive/OptionalCompletionStageImpl.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.common.reactive; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Internal implementation of the completion stage that allows processing of cases when the element is present and when + * not. + * + * @param return type of the asynchronous operation + */ +class OptionalCompletionStageImpl implements OptionalCompletionStage { + + private final CompletionStage> originalStage; + + OptionalCompletionStageImpl(CompletionStage> originalStag) { + this.originalStage = originalStag; + } + + @Override + public OptionalCompletionStage onEmpty(Runnable action) { + originalStage.thenAccept(original -> { + if (!original.isPresent()) { + action.run(); + } + }); + return this; + } + + @Override + public OptionalCompletionStage onValue(Consumer action) { + originalStage.thenAccept(original -> { + original.ifPresent(action); + }); + return this; + } + + @Override + public CompletionStage thenApply(Function, ? extends U> fn) { + return originalStage.thenApply(fn); + } + + @Override + public CompletionStage thenApplyAsync(Function, ? extends U> fn) { + return originalStage.thenApplyAsync(fn); + } + + @Override + public CompletionStage thenApplyAsync(Function, ? extends U> fn, + Executor executor) { + return originalStage.thenApplyAsync(fn, executor); + } + + @Override + public CompletionStage thenAccept(Consumer> action) { + return originalStage.thenAccept(action); + } + + @Override + public CompletionStage thenAcceptAsync(Consumer> action) { + return originalStage.thenAcceptAsync(action); + } + + @Override + public CompletionStage thenAcceptAsync(Consumer> action, + Executor executor) { + return originalStage.thenAcceptAsync(action, executor); + } + + @Override + public CompletionStage thenRun(Runnable action) { + return originalStage.thenRun(action); + } + + @Override + public CompletionStage thenRunAsync(Runnable action) { + return originalStage.thenRunAsync(action); + } + + @Override + public CompletionStage thenRunAsync(Runnable action, Executor executor) { + return originalStage.thenRunAsync(action, executor); + } + + @Override + public CompletionStage thenCombine(CompletionStage other, + BiFunction, ? super U, ? extends V> fn) { + return originalStage.thenCombine(other, fn); + } + + @Override + public CompletionStage thenCombineAsync(CompletionStage other, + BiFunction, ? super U, ? extends V> fn) { + return originalStage.thenCombineAsync(other, fn); + } + + @Override + public CompletionStage thenCombineAsync(CompletionStage other, + BiFunction, ? super U, ? extends V> fn, + Executor executor) { + return originalStage.thenCombineAsync(other, fn, executor); + } + + @Override + public CompletionStage thenAcceptBoth(CompletionStage other, + BiConsumer, ? super U> action) { + return originalStage.thenAcceptBoth(other, action); + } + + @Override + public CompletionStage thenAcceptBothAsync(CompletionStage other, + BiConsumer, ? super U> action) { + return originalStage.thenAcceptBothAsync(other, action); + } + + @Override + public CompletionStage thenAcceptBothAsync(CompletionStage other, + BiConsumer, ? super U> action, + Executor executor) { + return originalStage.thenAcceptBothAsync(other, action, executor); + } + + @Override + public CompletionStage runAfterBoth(CompletionStage other, Runnable action) { + return originalStage.runAfterBoth(other, action); + } + + @Override + public CompletionStage runAfterBothAsync(CompletionStage other, Runnable action) { + return originalStage.runAfterBothAsync(other, action); + } + + @Override + public CompletionStage runAfterBothAsync(CompletionStage other, Runnable action, Executor executor) { + return originalStage.runAfterBothAsync(other, action, executor); + } + + @Override + public CompletionStage applyToEither(CompletionStage> other, + Function, U> fn) { + return originalStage.applyToEither(other, fn); + } + + @Override + public CompletionStage applyToEitherAsync(CompletionStage> other, + Function, U> fn) { + return originalStage.applyToEitherAsync(other, fn); + } + + @Override + public CompletionStage applyToEitherAsync(CompletionStage> other, + Function, U> fn, + Executor executor) { + return originalStage.applyToEitherAsync(other, fn, executor); + } + + @Override + public CompletionStage acceptEither(CompletionStage> other, + Consumer> action) { + return originalStage.acceptEither(other, action); + } + + @Override + public CompletionStage acceptEitherAsync(CompletionStage> other, + Consumer> action) { + return originalStage.acceptEitherAsync(other, action); + } + + @Override + public CompletionStage acceptEitherAsync(CompletionStage> other, + Consumer> action, + Executor executor) { + return originalStage.acceptEitherAsync(other, action, executor); + } + + @Override + public CompletionStage runAfterEither(CompletionStage other, Runnable action) { + return originalStage.runAfterEither(other, action); + } + + @Override + public CompletionStage runAfterEitherAsync(CompletionStage other, Runnable action) { + return originalStage.runAfterEitherAsync(other, action); + } + + @Override + public CompletionStage runAfterEitherAsync(CompletionStage other, + Runnable action, + Executor executor) { + return originalStage.runAfterEitherAsync(other, action, executor); + } + + @Override + public CompletionStage thenCompose(Function, ? extends CompletionStage> fn) { + return originalStage.thenCompose(fn); + } + + @Override + public CompletionStage thenComposeAsync(Function, ? extends CompletionStage> fn) { + return originalStage.thenComposeAsync(fn); + } + + @Override + public CompletionStage thenComposeAsync(Function, ? extends CompletionStage> fn, + Executor executor) { + return originalStage.thenComposeAsync(fn, executor); + } + + @Override + public CompletionStage> exceptionally(Function> fn) { + return originalStage.exceptionally(fn); + } + + @Override + public CompletionStage> whenComplete(BiConsumer, ? super Throwable> action) { + return originalStage.whenComplete(action); + } + + @Override + public CompletionStage> whenCompleteAsync(BiConsumer, ? super Throwable> action) { + return originalStage.whenCompleteAsync(action); + } + + @Override + public CompletionStage> whenCompleteAsync(BiConsumer, ? super Throwable> action, + Executor executor) { + return originalStage.whenCompleteAsync(action, executor); + } + + @Override + public CompletionStage handle(BiFunction, Throwable, ? extends U> fn) { + return originalStage.handle(fn); + } + + @Override + public CompletionStage handleAsync(BiFunction, Throwable, ? extends U> fn) { + return originalStage.handleAsync(fn); + } + + @Override + public CompletionStage handleAsync(BiFunction, Throwable, ? extends U> fn, + Executor executor) { + return originalStage.handleAsync(fn, executor); + } + + @Override + public CompletableFuture> toCompletableFuture() { + return originalStage.toCompletableFuture(); + } + +} diff --git a/common/reactive/src/main/java/io/helidon/common/reactive/Single.java b/common/reactive/src/main/java/io/helidon/common/reactive/Single.java index 86070daed..8d7e8f9b0 100644 --- a/common/reactive/src/main/java/io/helidon/common/reactive/Single.java +++ b/common/reactive/src/main/java/io/helidon/common/reactive/Single.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package io.helidon.common.reactive; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; @@ -62,8 +63,9 @@ public interface Single extends Subscribable { } /** - * Exposes this {@link Single} instance as a {@link CompletionStage}. Note that if this {@link Single} completes without a - * value, the resulting {@link CompletionStage} will be completed exceptionally with an {@link IllegalStateException} + * Exposes this {@link Single} instance as a {@link CompletionStage}. + * Note that if this {@link Single} completes without a value, the resulting {@link CompletionStage} will be completed + * exceptionally with an {@link IllegalStateException} * * @return CompletionStage */ @@ -79,6 +81,26 @@ public interface Single extends Subscribable { } } + /** + * Exposes this {@link Single} instance as a {@link CompletionStage} with {@code Optional} return type + * of the asynchronous operation. + * Note that if this {@link Single} completes without a value, the resulting {@link CompletionStage} will be completed + * exceptionally with an {@link IllegalStateException} + * + * @return CompletionStage + */ + default CompletionStage> toOptionalStage() { + try { + SingleToOptionalFuture subscriber = new SingleToOptionalFuture<>(); + this.subscribe(subscriber); + return subscriber; + } catch (Throwable ex) { + CompletableFuture> future = new CompletableFuture<>(); + future.completeExceptionally(ex); + return future; + } + } + /** * Short-hand for {@code toFuture().toCompletableFuture().get()}. * @return T diff --git a/common/reactive/src/main/java/io/helidon/common/reactive/SingleToOptionalFuture.java b/common/reactive/src/main/java/io/helidon/common/reactive/SingleToOptionalFuture.java new file mode 100644 index 000000000..310add45c --- /dev/null +++ b/common/reactive/src/main/java/io/helidon/common/reactive/SingleToOptionalFuture.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.common.reactive; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Flow.Subscriber; +import java.util.concurrent.Flow.Subscription; +import java.util.concurrent.atomic.AtomicReference; + +/** + * {@link io.helidon.common.reactive.Single} exposed as a {@link java.util.concurrent.CompletableFuture}. + */ +class SingleToOptionalFuture extends CompletableFuture> implements Subscriber { + + private final AtomicReference ref = new AtomicReference<>(); + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + boolean cancelled = super.cancel(mayInterruptIfRunning); + if (cancelled) { + Subscription s = ref.getAndSet(null); + if (s != null) { + s.cancel(); + } + } + return cancelled; + } + + @Override + public void onSubscribe(Subscription next) { + Subscription current = ref.getAndSet(next); + Objects.requireNonNull(next, "Subscription cannot be null"); + if (current != null) { + next.cancel(); + current.cancel(); + } else { + next.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T item) { + Subscription s = ref.getAndSet(null); + if (s != null) { + super.complete(Optional.ofNullable(item)); + s.cancel(); + } + } + + @Override + public void onError(Throwable ex) { + if (ref.getAndSet(null) != null) { + super.completeExceptionally(ex); + } + } + + @Override + public void onComplete() { + if (ref.getAndSet(null) != null) { + super.complete(Optional.empty()); + } + } + + @Override + public boolean complete(Optional value) { + throw new UnsupportedOperationException("This future cannot be completed manually"); + } + + @Override + public boolean completeExceptionally(Throwable ex) { + throw new UnsupportedOperationException("This future cannot be completed manually"); + } +} diff --git a/config/config/src/test/java/io/helidon/config/ConfigSupplierTest.java b/config/config/src/test/java/io/helidon/config/ConfigSupplierTest.java index e78f50d76..0ffb61baf 100644 --- a/config/config/src/test/java/io/helidon/config/ConfigSupplierTest.java +++ b/config/config/src/test/java/io/helidon/config/ConfigSupplierTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2020 Oracle and/or its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import io.helidon.config.spi.ConfigNode; import io.helidon.config.spi.ConfigNode.ObjectNode; import io.helidon.config.spi.TestingConfigSource; +import org.junit.Ignore; import org.junit.jupiter.api.Test; import static io.helidon.config.ConfigTest.waitForAssert; @@ -175,7 +176,13 @@ public class ConfigSupplierTest { is(ConfigValues.simpleValue("NEW item 1"))); } + @Ignore @Test + // TODO cause of intermittent test failures: + /* + Tests in error: + ConfigSupplierTest.testSupplierFromMissingToListNode:209->lambda$testSupplierFromMissingToListNode$16:216 ยป IllegalState + */ public void testSupplierFromMissingToListNode() throws InterruptedException { // config source TestingConfigSource configSource = TestingConfigSource.builder().build(); diff --git a/dbclient/common/pom.xml b/dbclient/common/pom.xml new file mode 100644 index 000000000..5caf7abd7 --- /dev/null +++ b/dbclient/common/pom.xml @@ -0,0 +1,51 @@ + + + + + + helidon-dbclient-project + io.helidon.dbclient + 2.0-SNAPSHOT + + 4.0.0 + + helidon-dbclient-common + Helidon DB Client Common + + + + io.helidon.dbclient + helidon-dbclient + + + io.helidon.common + helidon-common-configurable + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + diff --git a/dbclient/common/src/main/java/io/helidon/dbclient/common/AbstractDbExecute.java b/dbclient/common/src/main/java/io/helidon/dbclient/common/AbstractDbExecute.java new file mode 100644 index 000000000..a78dfeab1 --- /dev/null +++ b/dbclient/common/src/main/java/io/helidon/dbclient/common/AbstractDbExecute.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.common; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +import io.helidon.dbclient.DbExecute; +import io.helidon.dbclient.DbStatementDml; +import io.helidon.dbclient.DbStatementGeneric; +import io.helidon.dbclient.DbStatementGet; +import io.helidon.dbclient.DbStatementQuery; +import io.helidon.dbclient.DbStatementType; +import io.helidon.dbclient.DbStatements; + +/** + * Implements methods that do not require implementation for each provider. + */ +public abstract class AbstractDbExecute implements DbExecute { + private final DbStatements statements; + + /** + * Create an instance with configured statements. + * + * @param statements statements to obtains named statements, esp. for {@link #statementText(String)}. + */ + protected AbstractDbExecute(DbStatements statements) { + this.statements = statements; + } + + /** + * Return a statement text based on the statement name. + * This is a utility method that probably would use {@link io.helidon.dbclient.DbStatements} to retrieve the named statements. + * + * @param name name of the statement + * @return statement text + */ + protected String statementText(String name) { + return statements.statement(name); + } + + @Override + public DbStatementQuery createNamedQuery(String statementName) { + return createNamedQuery(statementName, statementText(statementName)); + } + + @Override + public DbStatementQuery createQuery(String statement) { + return createNamedQuery(generateName(DbStatementType.QUERY, statement), statement); + } + + @Override + public DbStatementGet createNamedGet(String statementName) { + return createNamedGet(statementName, statementText(statementName)); + } + + @Override + public DbStatementGet createGet(String statement) { + return createNamedGet(generateName(DbStatementType.GET, statement), statement); + } + + @Override + public DbStatementDml createNamedInsert(String statementName) { + return createNamedInsert(statementName, statementText(statementName)); + } + + @Override + public DbStatementDml createInsert(String statement) { + return createNamedInsert(generateName(DbStatementType.INSERT, statement), statement); + } + + @Override + public DbStatementDml createNamedUpdate(String statementName) { + return createNamedUpdate(statementName, statementText(statementName)); + } + + @Override + public DbStatementDml createUpdate(String statement) { + return createNamedUpdate(generateName(DbStatementType.UPDATE, statement), statement); + } + + @Override + public DbStatementDml createNamedDelete(String statementName) { + return createNamedDelete(statementName, statementText(statementName)); + } + + @Override + public DbStatementDml createDelete(String statement) { + return createNamedDelete(generateName(DbStatementType.DELETE, statement), statement); + } + + @Override + public DbStatementDml createNamedDmlStatement(String statementName) { + return createNamedDmlStatement(statementName, statementText(statementName)); + } + + @Override + public DbStatementDml createDmlStatement(String statement) { + return createNamedDmlStatement(generateName(DbStatementType.DML, statement), statement); + } + + @Override + public DbStatementGeneric createNamedStatement(String statementName) { + return createNamedStatement(statementName, statementText(statementName)); + } + + @Override + public DbStatementGeneric createStatement(String statement) { + return createNamedStatement(generateName(DbStatementType.UNKNOWN, statement), statement); + } + + /** + * Generate a name for a statement. + * The default implementation uses {@code SHA-256} so the same name is always + * returned for the same statement. + *

+ * As there is always a small risk of duplicity, named statements are recommended! + * + * @param type type of the statement + * @param statement statement that it going to be executed + * @return name of the statement + */ + protected String generateName(DbStatementType type, String statement) { + String sha256; + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update(statement.getBytes(StandardCharsets.UTF_8)); + sha256 = Base64.getEncoder().encodeToString(digest.digest()); + } catch (NoSuchAlgorithmException ignored) { + return "sha256failed"; + } + return type.prefix() + '_' + sha256; + } +} diff --git a/dbclient/common/src/main/java/io/helidon/dbclient/common/AbstractStatement.java b/dbclient/common/src/main/java/io/helidon/dbclient/common/AbstractStatement.java new file mode 100644 index 000000000..2d67608a6 --- /dev/null +++ b/dbclient/common/src/main/java/io/helidon/dbclient/common/AbstractStatement.java @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.common; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.logging.Logger; + +import io.helidon.common.context.Context; +import io.helidon.common.context.Contexts; +import io.helidon.common.mapper.MapperManager; +import io.helidon.dbclient.DbInterceptor; +import io.helidon.dbclient.DbInterceptorContext; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.DbStatement; +import io.helidon.dbclient.DbStatementType; + +/** + * Common statement methods and fields. + * + * @param type of a subclass + * @param the result type of the statement as returned by {@link #execute()} + */ +public abstract class AbstractStatement, R> implements DbStatement { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(AbstractStatement.class.getName()); + + private ParamType paramType = ParamType.UNKNOWN; + private StatementParameters parameters; + private final DbStatementType dbStatementType; + private final String statementName; + private final String statement; + private final DbMapperManager dbMapperManager; + private final MapperManager mapperManager; + private final InterceptorSupport interceptors; + + /** + * Statement that handles parameters. + * + * @param dbStatementType type of this statement + * @param statementName name of this statement + * @param statement text of this statement + * @param dbMapperManager db mapper manager to use when mapping types to parameters + * @param mapperManager mapper manager to use when mapping results + * @param interceptors interceptors to be executed + */ + protected AbstractStatement(DbStatementType dbStatementType, + String statementName, + String statement, + DbMapperManager dbMapperManager, + MapperManager mapperManager, + InterceptorSupport interceptors) { + this.dbStatementType = dbStatementType; + this.statementName = statementName; + this.statement = statement; + this.dbMapperManager = dbMapperManager; + this.mapperManager = mapperManager; + this.interceptors = interceptors; + } + + @Override + public CompletionStage execute() { + CompletableFuture queryFuture = new CompletableFuture<>(); + CompletableFuture statementFuture = new CompletableFuture<>(); + DbInterceptorContext dbContext = DbInterceptorContext.create(dbType()) + .resultFuture(queryFuture) + .statementFuture(statementFuture); + + update(dbContext); + CompletionStage dbContextFuture = invokeInterceptors(dbContext); + return doExecute(dbContextFuture, statementFuture, queryFuture); + } + + /** + * Invoke all interceptors. + * + * @param dbContext initial interceptor context + * @return future with the result of interceptors processing + */ + CompletionStage invokeInterceptors(DbInterceptorContext dbContext) { + CompletableFuture result = CompletableFuture.completedFuture(dbContext); + + dbContext.context(Contexts.context().orElseGet(Context::create)); + + for (DbInterceptor interceptor : interceptors.interceptors(statementType(), statementName())) { + result = result.thenCompose(interceptor::statement); + } + + return result; + } + + /** + * Type of this statement. + * + * @return statement type + */ + protected DbStatementType statementType() { + return dbStatementType; + } + + /** + * Execute the statement against the database. + * + * @param dbContext future that completes after all interceptors are invoked + * @param statementFuture future that should complete when the statement finishes execution + * @param queryFuture future that should complete when the result set is fully read (if one exists), + * otherwise complete same as statementFuture + * @return result of this db statement. + */ + protected abstract CompletionStage doExecute(CompletionStage dbContext, + CompletableFuture statementFuture, + CompletableFuture queryFuture); + + /** + * Type of this database to use in interceptor context. + * + * @return type of this db + */ + protected abstract String dbType(); + + @Override + public S params(List parameters) { + Objects.requireNonNull(parameters, "Parameters cannot be null (may be an empty list)"); + + initParameters(ParamType.INDEXED); + this.parameters.params(parameters); + + return me(); + } + + @Override + public S params(Map parameters) { + initParameters(ParamType.NAMED); + this.parameters.params(parameters); + return me(); + } + + @Override + public S namedParam(Object parameters) { + initParameters(ParamType.NAMED); + this.parameters.namedParam(parameters); + return me(); + } + + @Override + public S indexedParam(Object parameters) { + initParameters(ParamType.INDEXED); + this.parameters.indexedParam(parameters); + return me(); + } + + @Override + public S addParam(Object parameter) { + initParameters(ParamType.INDEXED); + this.parameters.addParam(parameter); + return me(); + } + + @Override + public S addParam(String name, Object parameter) { + initParameters(ParamType.NAMED); + this.parameters.addParam(name, parameter); + return me(); + } + + /** + * Type of parameters of this statement. + * + * @return indexed or named, or unknown in case it could not be yet defined + */ + protected ParamType paramType() { + return paramType; + } + + /** + * Db mapper manager. + * + * @return mapper manager for DB types + */ + protected DbMapperManager dbMapperManager() { + return dbMapperManager; + } + + /** + * Mapper manager. + * + * @return generic mapper manager + */ + protected MapperManager mapperManager() { + return mapperManager; + } + + /** + * Get the named parameters of this statement. + * + * @return name parameter map + * @throws java.lang.IllegalStateException in case this statement is using indexed parameters + */ + protected Map namedParams() { + initParameters(ParamType.NAMED); + return parameters.namedParams(); + } + + /** + * Get the indexed parameters of this statement. + * + * @return parameter list + * @throws java.lang.IllegalStateException in case this statement is using named parameters + */ + protected List indexedParams() { + initParameters(ParamType.INDEXED); + return parameters.indexedParams(); + } + + /** + * Statement name. + * + * @return name of this statement (never null, may be generated) + */ + protected String statementName() { + return statementName; + } + + /** + * Statement text. + * + * @return text of this statement + */ + protected String statement() { + return statement; + } + + /** + * Update the interceptor context with the statement name, statement and + * statement parameters. + * + * @param dbContext interceptor context + */ + protected void update(DbInterceptorContext dbContext) { + dbContext.statementName(statementName); + initParameters(ParamType.INDEXED); + + if (paramType == ParamType.NAMED) { + dbContext.statement(statement, parameters.namedParams()); + } else { + dbContext.statement(statement, parameters.indexedParams()); + } + dbContext.statementType(statementType()); + } + + /** + * Returns this builder cast to the correct type. + * + * @return this as type extending this class + */ + @SuppressWarnings("unchecked") + protected S me() { + return (S) this; + } + + private void initParameters(ParamType type) { + if (this.paramType != ParamType.UNKNOWN) { + // already initialized + return; + } + switch (type) { + case NAMED: + this.paramType = ParamType.NAMED; + this.parameters = new NamedStatementParameters(dbMapperManager); + break; + case INDEXED: + case UNKNOWN: + default: + this.paramType = ParamType.INDEXED; + this.parameters = new IndexedStatementParameters(dbMapperManager); + break; + } + } +} diff --git a/dbclient/common/src/main/java/io/helidon/dbclient/common/IndexedStatementParameters.java b/dbclient/common/src/main/java/io/helidon/dbclient/common/IndexedStatementParameters.java new file mode 100644 index 000000000..edddcfb8a --- /dev/null +++ b/dbclient/common/src/main/java/io/helidon/dbclient/common/IndexedStatementParameters.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.common; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import io.helidon.dbclient.DbClientException; +import io.helidon.dbclient.DbMapperManager; + +/** + * Statement with indexed parameters. + */ +class IndexedStatementParameters implements StatementParameters { + + private final List parameters = new LinkedList<>(); + private final DbMapperManager dbMapperManager; + + IndexedStatementParameters(DbMapperManager dbMapperManager) { + this.dbMapperManager = dbMapperManager; + } + + @Override + public StatementParameters params(List parameters) { + this.parameters.clear(); + this.parameters.addAll(parameters); + return this; + } + + @SuppressWarnings("unchecked") + @Override + public StatementParameters indexedParam(T parameters) { + Class theClass = (Class) parameters.getClass(); + + params(dbMapperManager.toIndexedParameters(parameters, theClass)); + return this; + } + + @Override + public List indexedParams() { + return this.parameters; + } + + @Override + public StatementParameters addParam(Object parameter) { + parameters.add(parameter); + return this; + } + + private static final String CANT_USE_INDEXED_PARAMS + = "This is a statement with indexed parameters, cannot use named parameters."; + + @Override + public StatementParameters params(Map parameters) { + throw new DbClientException(CANT_USE_INDEXED_PARAMS); + } + + @Override + public StatementParameters namedParam(T parameters) { + throw new DbClientException(CANT_USE_INDEXED_PARAMS); + } + + @Override + public StatementParameters addParam(String name, Object parameter) { + throw new DbClientException(CANT_USE_INDEXED_PARAMS); + } + + @Override + public Map namedParams() { + throw new DbClientException(CANT_USE_INDEXED_PARAMS); + } + +} diff --git a/dbclient/common/src/main/java/io/helidon/dbclient/common/InterceptorSupport.java b/dbclient/common/src/main/java/io/helidon/dbclient/common/InterceptorSupport.java new file mode 100644 index 000000000..2390ce7ec --- /dev/null +++ b/dbclient/common/src/main/java/io/helidon/dbclient/common/InterceptorSupport.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.common; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import io.helidon.common.configurable.LruCache; +import io.helidon.dbclient.DbInterceptor; +import io.helidon.dbclient.DbStatementType; + +/** + * Support for interceptors. + */ +public interface InterceptorSupport { + + /** + * Get a list of interceptors to be executed for the specified statement. + * + * @param dbStatementType Type of the statement + * @param statementName Name of the statement (unnamed statements should have a name generated, see + * {@link AbstractDbExecute#generateName(io.helidon.dbclient.DbStatementType, String)} + * @return list of interceptors to executed for the defined type and name (may be empty) + * @see io.helidon.dbclient.DbInterceptor + */ + List interceptors(DbStatementType dbStatementType, String statementName); + + /** + * Create a new fluent API builder. + * + * @return a builder instance + */ + static Builder builder() { + return new Builder(); + } + + /** + * Fluent API builder for {@link io.helidon.dbclient.common.InterceptorSupport}. + */ + final class Builder implements io.helidon.common.Builder { + private final List interceptors = new LinkedList<>(); + private final Map> typeInterceptors = new EnumMap<>(DbStatementType.class); + private final Map> namedStatementInterceptors = new HashMap<>(); + + private Builder() { + } + + @Override + public InterceptorSupport build() { + // the result must be immutable (if somebody modifies the builder, the behavior must not change) + List interceptors = new LinkedList<>(this.interceptors); + final Map> typeInterceptors = new EnumMap<>(this.typeInterceptors); + final Map> namedStatementInterceptors = new HashMap<>(this.namedStatementInterceptors); + + final LruCache> cachedInterceptors = LruCache.create(); + return new InterceptorSupport() { + @Override + public List interceptors(DbStatementType dbStatementType, String statementName) { + // order is defined in DbInterceptor interface + return cachedInterceptors.computeValue(new CacheKey(dbStatementType, statementName), () -> { + List result = new LinkedList<>(); + addAll(result, namedStatementInterceptors.get(statementName)); + addAll(result, typeInterceptors.get(dbStatementType)); + result.addAll(interceptors); + return Optional.of(Collections.unmodifiableList(result)); + }).orElseGet(List::of); + } + + private void addAll(List result, List dbInterceptors) { + if (null == dbInterceptors) { + return; + } + result.addAll(dbInterceptors); + } + }; + } + + public Builder add(DbInterceptor interceptor) { + this.interceptors.add(interceptor); + return this; + } + + public Builder add(DbInterceptor interceptor, String... statementNames) { + for (String statementName : statementNames) { + this.namedStatementInterceptors.computeIfAbsent(statementName, theName -> new LinkedList<>()) + .add(interceptor); + } + return this; + } + + public Builder add(DbInterceptor interceptor, DbStatementType... dbStatementTypes) { + for (DbStatementType dbStatementType : dbStatementTypes) { + this.typeInterceptors.computeIfAbsent(dbStatementType, theType -> new LinkedList<>()) + .add(interceptor); + } + return this; + } + + private static final class CacheKey { + private final DbStatementType type; + private final String statementName; + + private CacheKey(DbStatementType type, String statementName) { + this.type = type; + this.statementName = statementName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CacheKey)) { + return false; + } + CacheKey cacheKey = (CacheKey) o; + return (type == cacheKey.type) + && statementName.equals(cacheKey.statementName); + } + + @Override + public int hashCode() { + return Objects.hash(type, statementName); + } + } + } + +} diff --git a/dbclient/common/src/main/java/io/helidon/dbclient/common/NamedStatementParameters.java b/dbclient/common/src/main/java/io/helidon/dbclient/common/NamedStatementParameters.java new file mode 100644 index 000000000..cea2f822b --- /dev/null +++ b/dbclient/common/src/main/java/io/helidon/dbclient/common/NamedStatementParameters.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.common; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.helidon.dbclient.DbClientException; +import io.helidon.dbclient.DbMapperManager; + +/** + * Statement with indexed parameters. + */ +class NamedStatementParameters implements StatementParameters { + + private final Map parameters = new HashMap<>(); + private final DbMapperManager dbMapperManager; + + NamedStatementParameters(DbMapperManager dbMapperManager) { + this.dbMapperManager = dbMapperManager; + } + + @Override + public StatementParameters params(Map parameters) { + this.parameters.clear(); + this.parameters.putAll(parameters); + return this; + } + + @SuppressWarnings("unchecked") + @Override + public StatementParameters namedParam(T parameters) { + Class theClass = (Class) parameters.getClass(); + + return params(dbMapperManager.toNamedParameters(parameters, theClass)); + } + + @Override + public StatementParameters addParam(String name, Object parameter) { + this.parameters.put(name, parameter); + return this; + } + + @Override + public Map namedParams() { + return this.parameters; + } + + private static final String CANT_USE_NAMED_PARAMS + = "This is a statement with named parameters, cannot use indexed parameters."; + + @Override + public StatementParameters params(List parameters) { + throw new DbClientException(CANT_USE_NAMED_PARAMS); + } + + @Override + public StatementParameters indexedParam(T parameters) { + throw new DbClientException(CANT_USE_NAMED_PARAMS); + } + + @Override + public StatementParameters addParam(Object parameter) { + throw new DbClientException(CANT_USE_NAMED_PARAMS); + } + + @Override + public List indexedParams() { + throw new DbClientException(CANT_USE_NAMED_PARAMS); + } + +} diff --git a/dbclient/common/src/main/java/io/helidon/dbclient/common/ParamType.java b/dbclient/common/src/main/java/io/helidon/dbclient/common/ParamType.java new file mode 100644 index 000000000..0b34570fc --- /dev/null +++ b/dbclient/common/src/main/java/io/helidon/dbclient/common/ParamType.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.common; + +/** + * Type of statement parameters. + */ +public enum ParamType { + + /** + * Indexed values to be passed to the statement in order. + * In JDBC, this is used for statements that use {@code ?} as a placeholder + * for parameters. + */ + INDEXED, + + /** + * Named values to be passed to the statement by name. + * Unless the underlying database directly supports named parameters, + * we use {@code :name} in the statement text to represent + * a named parameter. + */ + NAMED, + + /** + * Statement type is not known. + */ + UNKNOWN + +} diff --git a/dbclient/common/src/main/java/io/helidon/dbclient/common/StatementParameters.java b/dbclient/common/src/main/java/io/helidon/dbclient/common/StatementParameters.java new file mode 100644 index 000000000..01bba2b1e --- /dev/null +++ b/dbclient/common/src/main/java/io/helidon/dbclient/common/StatementParameters.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.common; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * Db statement that does not support execution, only parameters. + */ +interface StatementParameters { + + /** + * Configure parameters from a {@link java.util.List} by order. + * The statement must use indexed parameters and configure them by order in the provided array. + * + * @param parameters ordered parameters to set on this statement, never null + * @return updated db statement + */ + StatementParameters params(List parameters); + + /** + * Configure parameters from an array by order. + * The statement must use indexed parameters and configure them by order in the provided array. + * + * @param parameters ordered parameters to set on this statement + * @param type of the array + * @return updated db statement + */ + default StatementParameters params(T... parameters) { + return params(Arrays.asList(parameters)); + } + + /** + * Configure named parameters. + * The statement must use named parameters and configure them from the provided map. + * + * @param parameters named parameters to set on this statement + * @return updated db statement + */ + StatementParameters params(Map parameters); + + /** + * Configure parameters using {@link Object} instance with registered mapper. + * The statement must use named parameters and configure them from the map provided by mapper. + * + * @param parameters {@link Object} instance containing parameters + * @param type of the parameters + * @return updated db statement + */ + StatementParameters namedParam(T parameters); + + /** + * Configure parameters using {@link Object} instance with registered mapper. + * The statement must use indexed parameters and configure them by order in the array provided by mapper. + * + * @param parameters {@link Object} instance containing parameters + * @param type of the parameters + * @return updated db statement + */ + StatementParameters indexedParam(T parameters); + + /** + * Configure parameters using {@link Object} instance with registered mapper. + * + * @param parameters {@link Object} instance containing parameters + * @param mapper method to create map of statement named parameters mapped to values to be set + * @param type of the parameters + * @return updated db statement + */ + default StatementParameters namedParam(T parameters, Function> mapper) { + return params(mapper.apply(parameters)); + } + + /** + * Configure parameters using {@link Object} instance with registered mapper. + * + * @param parameters {@link Object} instance containing parameters + * @param mapper method to create map of statement named parameters mapped to values to be set + * @param type of the parameters + * @return updated db statement + */ + default StatementParameters indexedParam(T parameters, Function> mapper) { + return params(mapper.apply(parameters)); + } + + /** + * Add next parameter to the list of ordered parameters (e.g. the ones that use {@code ?} in SQL). + * + * @param parameter next parameter to set on this statement + * @return updated db statement + */ + StatementParameters addParam(Object parameter); + + /** + * Add next parameter to the map of named parameters (e.g. the ones that use {@code :name} in Helidon + * JDBC SQL integration). + * + * @param name name of parameter + * @param parameter value of parameter + * @return updated db statement + */ + StatementParameters addParam(String name, Object parameter); + + /** + * Return {@code Map} containing all named parameters. + * + * @return {@code Map} containing all named parameters + */ + Map namedParams(); + + /** + * Return {@code List} containing all ordered parameters. + * + * @return {@code List} containing all ordered parameters + */ + List indexedParams(); + +} diff --git a/dbclient/common/src/main/java/io/helidon/dbclient/common/package-info.java b/dbclient/common/src/main/java/io/helidon/dbclient/common/package-info.java new file mode 100644 index 000000000..77ea28d0e --- /dev/null +++ b/dbclient/common/src/main/java/io/helidon/dbclient/common/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ +/** + * Helper classes to use in various implementations. + */ +package io.helidon.dbclient.common; diff --git a/dbclient/common/src/main/java/module-info.java b/dbclient/common/src/main/java/module-info.java new file mode 100644 index 000000000..becd92c82 --- /dev/null +++ b/dbclient/common/src/main/java/module-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Helidon DB Client Common. + */ +module io.helidon.dbclient.common { + requires java.logging; + requires java.sql; + + requires transitive io.helidon.common; + requires transitive io.helidon.common.configurable; + requires transitive io.helidon.dbclient; + + exports io.helidon.dbclient.common; +} diff --git a/dbclient/dbclient/pom.xml b/dbclient/dbclient/pom.xml new file mode 100644 index 000000000..ac3a8c4ba --- /dev/null +++ b/dbclient/dbclient/pom.xml @@ -0,0 +1,60 @@ + + + + + 4.0.0 + + helidon-dbclient-project + io.helidon.dbclient + 2.0-SNAPSHOT + + + helidon-dbclient + Helidon DB Client + Helidon Reactive Database Client API + + + + io.helidon.config + helidon-config + + + io.helidon.common + helidon-common-service-loader + + + io.helidon.common + helidon-common-context + + + io.helidon.common + helidon-common-mapper + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbClient.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbClient.java new file mode 100644 index 000000000..279a79f5a --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbClient.java @@ -0,0 +1,340 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.Arrays; +import java.util.List; +import java.util.ServiceLoader; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; + +import io.helidon.common.HelidonFeatures; +import io.helidon.common.HelidonFlavor; +import io.helidon.common.mapper.MapperManager; +import io.helidon.common.serviceloader.HelidonServiceLoader; +import io.helidon.config.Config; +import io.helidon.config.ConfigValue; +import io.helidon.dbclient.spi.DbClientProvider; +import io.helidon.dbclient.spi.DbClientProviderBuilder; +import io.helidon.dbclient.spi.DbInterceptorProvider; +import io.helidon.dbclient.spi.DbMapperProvider; + +/** + * Helidon database client. + */ +public interface DbClient { + /** + * Execute database statements in transaction. + * + * @param statement execution result type + * @param executor database statement executor, see {@link DbExecute} + * @return statement execution result + */ + CompletionStage inTransaction(Function> executor); + + /** + * Execute database statement. + * + * @param statement execution result type + * @param executor database statement executor, see {@link DbExecute} + * @return statement execution result + */ + > T execute(Function executor); + + /** + * Pings the database, completes when DB is up and ready, completes exceptionally if not. + * + * @return stage that completes when the ping finished + */ + CompletionStage ping(); + + /** + * Type of this database provider (such as jdbc:mysql, mongoDB etc.). + * + * @return name of the database provider + */ + String dbType(); + + /** + * Create Helidon database handler builder. + * + * @param config name of the configuration node with driver configuration + * @return database handler builder + */ + static DbClient create(Config config) { + return builder(config).build(); + } + + /** + * Create Helidon database handler builder. + *

Database driver is loaded as SPI provider which implements {@link io.helidon.dbclient.spi.DbClientProvider} interface. + * First provider on the class path is selected.

+ * + * @return database handler builder + */ + static Builder builder() { + DbClientProvider theSource = DbClientProviderLoader.first(); + if (null == theSource) { + throw new DbClientException( + "No DbSource defined on classpath/module path. An implementation of io.helidon.dbclient.spi.DbSource is required " + + "to access a DB"); + } + + return builder(theSource); + } + + /** + * Create Helidon database handler builder. + * + * @param source database driver + * @return database handler builder + */ + static Builder builder(DbClientProvider source) { + return new Builder(source); + } + + /** + * Create Helidon database handler builder. + *

Database driver is loaded as SPI provider which implements {@link io.helidon.dbclient.spi.DbClientProvider} interface. + * Provider on the class path with matching name is selected.

+ * + * @param dbSource SPI provider name + * @return database handler builder + */ + static Builder builder(String dbSource) { + return DbClientProviderLoader.get(dbSource) + .map(DbClient::builder) + .orElseThrow(() -> new DbClientException( + "No DbSource defined on classpath/module path for name: " + + dbSource + + ", available names: " + Arrays.toString(DbClientProviderLoader.names()))); + } + + /** + * Create a Helidon database handler builder from configuration. + * + * @param dbConfig configuration that should contain the key {@code source} that defines the type of this database + * and is used to load appropriate {@link io.helidon.dbclient.spi.DbClientProvider} from Java Service loader + * @return a builder pre-configured from the provided config + */ + static Builder builder(Config dbConfig) { + return dbConfig.get("source") + .asString() + // use builder for correct DbSource + .map(DbClient::builder) + // or use the default one + .orElseGet(DbClient::builder) + .config(dbConfig); + } + + /** + * Helidon database handler builder. + */ + final class Builder implements io.helidon.common.Builder { + static { + HelidonFeatures.register(HelidonFlavor.SE, "DbClient"); + } + + private final HelidonServiceLoader.Builder interceptorServices = HelidonServiceLoader.builder( + ServiceLoader.load(DbInterceptorProvider.class)); + + /** + * Provider specific database handler builder instance. + */ + private final DbClientProviderBuilder theBuilder; + private Config config; + + /** + * Create an instance of Helidon database handler builder. + * + * @param dbClientProvider provider specific {@link io.helidon.dbclient.spi.DbClientProvider} instance + */ + private Builder(DbClientProvider dbClientProvider) { + this.theBuilder = dbClientProvider.builder(); + } + + /** + * Build provider specific database handler. + * + * @return new database handler instance + */ + @Override + public DbClient build() { + // add interceptors from service loader + if (null != config) { + Config interceptors = config.get("interceptors"); + List providers = interceptorServices.build().asList(); + for (DbInterceptorProvider provider : providers) { + Config providerConfig = interceptors.get(provider.configKey()); + if (!providerConfig.exists()) { + continue; + } + // if configured, we want to at least add a global one + AtomicBoolean added = new AtomicBoolean(false); + Config global = providerConfig.get("global"); + if (global.exists() && !global.isLeaf()) { + // we must iterate through nodes + global.asNodeList().ifPresent(configs -> { + configs.forEach(globalConfig -> { + added.set(true); + addInterceptor(provider.create(globalConfig)); + }); + }); + } + + Config named = providerConfig.get("named"); + if (named.exists()) { + // we must iterate through nodes + named.asNodeList().ifPresent(configs -> { + configs.forEach(namedConfig -> { + ConfigValue> names = namedConfig.get("names").asList(String.class); + names.ifPresent(nameList -> { + added.set(true); + addInterceptor(provider.create(namedConfig), nameList.toArray(new String[0])); + }); + }); + }); + } + Config typed = providerConfig.get("typed"); + if (typed.exists()) { + typed.asNodeList().ifPresent(configs -> { + configs.forEach(typedConfig -> { + ConfigValue> types = typedConfig.get("types").asList(String.class); + types.ifPresent(typeList -> { + DbStatementType[] typeArray = typeList.stream() + .map(DbStatementType::valueOf) + .toArray(DbStatementType[]::new); + + added.set(true); + addInterceptor(provider.create(typedConfig), typeArray); + }); + }); + }); + } + if (!added.get()) { + if (global.exists()) { + addInterceptor(provider.create(global)); + } else { + addInterceptor(provider.create(providerConfig)); + } + } + } + } + + return theBuilder.build(); + } + + /** + * Add an interceptor provider. + * The provider is only used when configuration is used ({@link #config(io.helidon.config.Config)}. + * + * @param provider provider to add to the list of loaded providers + * @return updated builder instance + */ + public Builder addInterceptorProvider(DbInterceptorProvider provider) { + this.interceptorServices.addService(provider); + return this; + } + + /** + * Add a global interceptor. + * + * A global interceptor is applied to each statement. + * @param interceptor interceptor to apply + * @return updated builder instance + */ + public Builder addInterceptor(DbInterceptor interceptor) { + theBuilder.addInterceptor(interceptor); + return this; + } + + /** + * Add an interceptor to specific named statements. + * + * @param interceptor interceptor to apply + * @param statementNames names of statements to apply it on + * @return updated builder instance + */ + public Builder addInterceptor(DbInterceptor interceptor, String... statementNames) { + theBuilder.addInterceptor(interceptor, statementNames); + return this; + } + + /** + * Add an interceptor to specific statement types. + * + * @param interceptor interceptor to apply + * @param dbStatementTypes types of statements to apply it on + * @return updated builder instance + */ + public Builder addInterceptor(DbInterceptor interceptor, DbStatementType... dbStatementTypes) { + theBuilder.addInterceptor(interceptor, dbStatementTypes); + return this; + } + + /** + * Use database connection configuration from configuration file. + * + * @param config {@link io.helidon.config.Config} instance with database connection attributes + * @return database provider builder + */ + public Builder config(Config config) { + theBuilder.config(config); + + this.config = config; + + return this; + } + + /** + * Statements to use either from configuration + * or manually configured. + * + * @param statements Statements to use + * @return updated builder instance + */ + public Builder statements(DbStatements statements) { + theBuilder.statements(statements); + return this; + } + + /** + * Database schema mappers provider. + * Mappers associated with types in this provider will override existing types associations loaded + * as {@link io.helidon.dbclient.spi.DbMapperProvider} Java services. + * + * @param provider database schema mappers provider to use + * @return updated builder instance + */ + public Builder mapperProvider(DbMapperProvider provider) { + theBuilder.addMapperProvider(provider); + return this; + } + + /** + * Mapper manager for generic mapping, such as mapping of parameters to expected types. + * + * @param manager mapper manager + * @return updated builder instance + */ + public Builder mapperManager(MapperManager manager) { + theBuilder.mapperManager(manager); + return this; + } + } + +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbClientException.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbClientException.java new file mode 100644 index 000000000..c6c3b620c --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbClientException.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +/** + * A {@link RuntimeException} used by Helidon DB component. + */ +public class DbClientException extends RuntimeException { + + /** + * Create a new exception for a message. + * @param message descriptive message + */ + public DbClientException(String message) { + super(message); + } + + /** + * Create a new exception for a message and a cause. + * + * @param message descriptive message + * @param cause original throwable causing this exception + */ + public DbClientException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbClientProviderLoader.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbClientProviderLoader.java new file mode 100644 index 000000000..ede489cf9 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbClientProviderLoader.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; + +import io.helidon.common.serviceloader.HelidonServiceLoader; +import io.helidon.dbclient.spi.DbClientProvider; + +/** + * Loads database client providers from Java Service loader. + */ +final class DbClientProviderLoader { + + private static final Map DB_SOURCES = new HashMap<>(); + private static final String[] NAMES; + private static final DbClientProvider FIRST; + + static { + HelidonServiceLoader serviceLoader = HelidonServiceLoader + .builder(ServiceLoader.load(DbClientProvider.class)) + .build(); + + List sources = serviceLoader.asList(); + + DbClientProvider first = null; + + if (!sources.isEmpty()) { + first = sources.get(0); + } + + FIRST = first; + sources.forEach(dbProvider -> DB_SOURCES.put(dbProvider.name(), dbProvider)); + NAMES = sources.stream() + .map(DbClientProvider::name) + .toArray(String[]::new); + } + + private DbClientProviderLoader() { + } + + static DbClientProvider first() { + return FIRST; + } + + static Optional get(String name) { + return Optional.ofNullable(DB_SOURCES.get(name)); + } + + static String[] names() { + return NAMES; + } +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbColumn.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbColumn.java new file mode 100644 index 000000000..7a8465213 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbColumn.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.Optional; + +import io.helidon.common.GenericType; +import io.helidon.common.mapper.MapperException; + +/** + * Column data and metadata. + */ +public interface DbColumn { + /** + * Typed value of this column. + * This method can return a correct result only if the type is the same as {@link #javaType()} or there is a + * {@link io.helidon.common.mapper.Mapper} registered that can map it. + * + * @param type class of the type that should be returned (must be supported by the underlying data type) + * @param type of the returned value + * @return value of this column correctly typed + * @throws MapperException in case the type is not the underlying {@link #javaType()} and + * there is no mapper registered for it + */ + T as(Class type) throws MapperException; + + /** + * Value of this column as a generic type. + * This method can return a correct result only if the type represents a class, or if there is a + * {@link io.helidon.common.mapper.Mapper} registered that can map underlying {@link #javaType()} to the type requested. + * + * @param type requested type + * @param type of the returned value + * @return value mapped to the expected type if possible + * @throws MapperException in case the mapping cannot be done + */ + T as(GenericType type) throws MapperException; + + /** + * Untyped value of this column, returns java type as provided by the underlying database driver. + * + * @return value of this column + */ + default Object value() { + return as(javaType()); + } + + /** + * Type of the column as would be returned by the underlying database driver. + * + * @return class of the type + * @see #dbType() + */ + Class javaType(); + + /** + * Type of the column in the language of the database. + *

+ * Example for SQL - if a column is declared as {@code VARCHAR(256)} in the database, + * this method would return {@code VARCHAR} and method {@link #javaType()} would return {@link String}. + * + * @return column type as the database understands it + */ + String dbType(); + + /** + * Column name. + * + * @return name of this column + */ + String name(); + + /** + * Precision of this column. + *

+ * Precision depends on data type: + *

    + *
  • Numeric: The maximal number of digits of the number
  • + *
  • String/Character: The maximal length
  • + *
  • Binary: The maximal number of bytes
  • + *
  • Other: Implementation specific
  • + *
+ * + * @return precision of this column or {@code empty} if precision is not available + */ + default Optional precision() { + return Optional.empty(); + } + + /** + * Scale of this column. + *

+ * Scale is the number of digits in a decimal number to the right of the decimal separator. + * + * @return scale of this column or {@code empty} if scale is not available + */ + default Optional scale() { + return Optional.empty(); + } +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbExecute.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbExecute.java new file mode 100644 index 000000000..726f73c8b --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbExecute.java @@ -0,0 +1,409 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +import io.helidon.common.reactive.OptionalCompletionStage; + +/** + * Database executor. + *

The database executor provides methods to create {@link DbStatement} instances for different types + * of database statements.

+ *

The recommended approach is to use named statements, as that allows better metrics, tracing, logging etc. + * In case an unnamed statement is used, a name must be generated + *

There are five methods for each {@link DbStatementType}, example for query (the implementation + * detail is for the default implementation, providers may differ): + *

    + *
  1. {@code DbStatement} {@link #createNamedQuery(String, String)} - full control over the name and content of the + * statement
  2. + *
  3. {@code DbStatement} {@link #createNamedQuery(String)} - use statement text from configuration
  4. + *
  5. {@code DbStatement} {@link #createQuery(String)} - use the provided statement, name is generated
  6. + *
  7. {@code DbRowResult} {@link #namedQuery(String, Object...)} - shortcut method to a named query with a list of + * parameters (or with no parameters at all)
  8. + *
  9. {@code DbRowResult} {@link #query(String, Object...)} - shortcut method to unnamed query with a list of parameters + * (or with no parameters at all)
  10. + *
+ * The first three methods return a statement that can have parameters configured (and other details modified). + * The last two methods directly execute the statement and provide appropriate response for future processing. + * All the methods are non-blocking. + */ +public interface DbExecute { + /* + * QUERY + */ + + /** + * Create a database query using a named statement passed as argument. + * + * @param statementName the name of the statement + * @param statement the query statement + * @return database statement that can process query returning multiple rows + */ + DbStatementQuery createNamedQuery(String statementName, String statement); + + /** + * Create a database query using a statement defined in the configuration file. + * + * @param statementName the name of the configuration node with statement + * @return database statement that can process query returning multiple rows + */ + DbStatementQuery createNamedQuery(String statementName); + + /** + * Create a database query using a statement passed as an argument. + * + * @param statement the query statement to be executed + * @return database statement that can process the query returning multiple rows + */ + DbStatementQuery createQuery(String statement); + + /** + * Create and execute a database query using a statement defined in the configuration file. + * + * @param statementName the name of the configuration node with statement + * @param parameters query parameters to set + * @return database query execution result which can contain multiple rows + */ + default CompletionStage> namedQuery(String statementName, Object... parameters) { + return createNamedQuery(statementName).params(parameters).execute(); + } + + /** + * Create and execute a database query using a statement passed as an argument. + * + * @param statement the query statement to be executed + * @param parameters query parameters to set + * @return database query execution result which can contain multiple rows + */ + default CompletionStage> query(String statement, Object... parameters) { + return createQuery(statement).params(parameters).execute(); + } + + /* + * GET + */ + + /** + * Create a database query returning a single row using a named statement passed as an argument. + * + * @param statementName the name of the statement + * @param statement the statement text + * @return database statement that can process query returning a single row + */ + DbStatementGet createNamedGet(String statementName, String statement); + + /** + * Create a database query returning a single row using a statement defined in the configuration file. + * + * @param statementName the name of the configuration node with statement + * @return database statement that can process query returning a single row + */ + DbStatementGet createNamedGet(String statementName); + + /** + * Create a database query returning a single row using a statement passed as an argument. + * + * @param statement the query statement to be executed + * @return database statement that can process query returning a single row + */ + DbStatementGet createGet(String statement); + + /** + * Create and execute a database query using a statement defined in the configuration file. + * + * @param statementName the name of the configuration node with statement + * @param parameters query parameters to set + * @return database query execution result which can contain single row + */ + default OptionalCompletionStage namedGet(String statementName, Object... parameters) { + return OptionalCompletionStage.create(createNamedGet(statementName).params(parameters).execute()); + } + + /** + * Create and execute a database query using a statement passed as an argument. + * + * @param statement the query statement to be executed + * @param parameters query parameters to set + * @return database query execution result which can contain single row + */ + default CompletionStage> get(String statement, Object... parameters) { + return createGet(statement).params(parameters).execute(); + } + + /* + * INSERT + */ + + /** + * Create an insert statement using a named statement passed as an argument. + * + * @param statementName the name of the statement + * @param statement the statement text + * @return database statement that can insert data + */ + default DbStatementDml createNamedInsert(String statementName, String statement) { + return createNamedDmlStatement(statementName, statement); + } + + /** + * Create an insert statement using a named statement. + * + * @param statementName the name of the statement + * @return database statement that can insert data + */ + DbStatementDml createNamedInsert(String statementName); + + /** + * Create an insert statement using a statement text. + * + * @param statement the statement text + * @return database statement that can insert data + */ + DbStatementDml createInsert(String statement); + + /** + * Create and execute insert statement using a statement defined in the configuration file. + * + * @param statementName the name of the configuration node with statement + * @param parameters query parameters to set + * @return number of rows inserted into the database + */ + default CompletionStage namedInsert(String statementName, Object... parameters) { + return createNamedInsert(statementName).params(parameters).execute(); + } + + /** + * Create and execute insert statement using a statement passed as an argument. + * + * @param statement the insert statement to be executed + * @param parameters query parameters to set + * @return number of rows inserted into the database + */ + default CompletionStage insert(String statement, Object... parameters) { + return createInsert(statement).params(parameters).execute(); + } + + /* + * UPDATE + */ + + /** + * Create an update statement using a named statement passed as an argument. + * + * @param statementName the name of the statement + * @param statement the statement text + * @return database statement that can update data + */ + default DbStatementDml createNamedUpdate(String statementName, String statement) { + return createNamedDmlStatement(statementName, statement); + } + + /** + * Create an update statement using a named statement. + * + * @param statementName the name of the statement + * @return database statement that can update data + */ + DbStatementDml createNamedUpdate(String statementName); + + /** + * Create an update statement using a statement text. + * + * @param statement the statement text + * @return database statement that can update data + */ + DbStatementDml createUpdate(String statement); + + /** + * Create and execute update statement using a statement defined in the configuration file. + * + * @param statementName the name of the configuration node with statement + * @param parameters query parameters to set + * @return number of rows updateed into the database + */ + default CompletionStage namedUpdate(String statementName, Object... parameters) { + return createNamedUpdate(statementName).params(parameters).execute(); + } + + /** + * Create and execute update statement using a statement passed as an argument. + * + * @param statement the update statement to be executed + * @param parameters query parameters to set + * @return number of rows updateed into the database + */ + default CompletionStage update(String statement, Object... parameters) { + return createUpdate(statement).params(parameters).execute(); + } + + /* + * DELETE + */ + + /** + * Create a delete statement using a named statement passed as an argument. + * + * @param statementName the name of the statement + * @param statement the statement text + * @return database statement that can delete data + */ + default DbStatementDml createNamedDelete(String statementName, String statement) { + return createNamedDmlStatement(statementName, statement); + } + + /** + * Create andelete statement using a named statement. + * + * @param statementName the name of the statement + * @return database statement that can delete data + */ + DbStatementDml createNamedDelete(String statementName); + + /** + * Create a delete statement using a statement text. + * + * @param statement the statement text + * @return database statement that can delete data + */ + DbStatementDml createDelete(String statement); + + /** + * Create and execute delete statement using a statement defined in the configuration file. + * + * @param statementName the name of the configuration node with statement + * @param parameters query parameters to set + * @return number of rows deleted from the database + */ + default CompletionStage namedDelete(String statementName, Object... parameters) { + return createNamedDelete(statementName).params(parameters).execute(); + } + + /** + * Create and execute delete statement using a statement passed as an argument. + * + * @param statement the delete statement to be executed + * @param parameters query parameters to set + * @return number of rows deleted from the database + */ + default CompletionStage delete(String statement, Object... parameters) { + return createDelete(statement).params(parameters).execute(); + } + + /* + * DML + */ + + /** + * Create a data modification statement using a named statement passed as an argument. + * + * @param statementName the name of the statement + * @param statement the statement text + * @return data modification statement + */ + DbStatementDml createNamedDmlStatement(String statementName, String statement); + + /** + * Create a data modification statement using a statement defined in the configuration file. + * + * @param statementName the name of the configuration node with statement + * @return data modification statement + */ + DbStatementDml createNamedDmlStatement(String statementName); + + /** + * Create a data modification statement using a statement passed as an argument. + * + * @param statement the data modification statement to be executed + * @return data modification statement + */ + DbStatementDml createDmlStatement(String statement); + + /** + * Create and execute a data modification statement using a statement defined in the configuration file. + * + * @param statementName the name of the configuration node with statement + * @param parameters query parameters to set + * @return number of rows modified + */ + default CompletionStage namedDml(String statementName, Object... parameters) { + return createNamedDmlStatement(statementName).params(parameters).execute(); + } + + /** + * Create and execute data modification statement using a statement passed as an argument. + * + * @param statement the delete statement to be executed + * @param parameters query parameters to set + * @return number of rows modified + */ + default CompletionStage dml(String statement, Object... parameters) { + return createDmlStatement(statement).params(parameters).execute(); + } + + /* + * UNKNOWN + */ + + /** + * Create a generic database statement using a named statement passed as an argument. + * + * @param statementName the name of the statement + * @param statement the statement text + * @return generic database statement that can return any result + */ + DbStatementGeneric createNamedStatement(String statementName, String statement); + + /** + * Create a generic database statement using a statement defined in the configuration file. + * + * @param statementName the name of the configuration node with statement + * @return generic database statement that can return any result + */ + DbStatementGeneric createNamedStatement(String statementName); + + /** + * Create a generic database statement using a statement passed as an argument. + * + * @param statement the statement to be executed + * @return generic database statement that can return any result + */ + DbStatementGeneric createStatement(String statement); + + /** + * Create and execute common statement using a statement defined in the configuration file. + * + * @param statementName the name of the configuration node with statement + * @param parameters query parameters to set + * @return generic statement execution result + */ + default CompletionStage namedStatement(String statementName, Object... parameters) { + return createNamedStatement(statementName).params(parameters).execute(); + } + + /** + * Create and execute common statement using a statement passed as an argument. + * + * @param statement the statement to be executed + * @param parameters query parameters to set + * @return generic statement execution result + */ + default CompletionStage statement(String statement, Object... parameters) { + return createStatement(statement).params(parameters).execute(); + } + +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbInterceptor.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbInterceptor.java new file mode 100644 index 000000000..94eddf13d --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbInterceptor.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.concurrent.CompletionStage; + +/** + * Interceptor to handle work around a database statement. + * Example of such interceptors: tracing, metrics. + *

+ * Interceptors can be defined as global interceptors, interceptors for a type of a statement and interceptors for a named + * statement. + * These are executed in the following order: + *

    + *
  1. Named interceptors - if there are any interceptors configured for a specific statement, they are executed first
  2. + *
  3. Type interceptors - if there are any interceptors configured for a type of statement, they are executed next
  4. + *
  5. Global interceptors - if there are any interceptors configured globally, they are executed last
  6. + *
+ * Order of interceptors within a group is based on the order they are registered in a builder, or by their priority when + * loaded from a Java Service loader + */ +@FunctionalInterface +public interface DbInterceptor { + /** + * Statement execution to be intercepted. + * This method is called before the statement execution starts. + * + * @param context Context to access data needed to process an interceptor + * @return completion stage that completes when this interceptor is finished + */ + CompletionStage statement(DbInterceptorContext context); +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbInterceptorContext.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbInterceptorContext.java new file mode 100644 index 000000000..1b4491f87 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbInterceptorContext.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +import io.helidon.common.context.Context; + +/** + * Interceptor context to get (and possibly manipulate) database operations. + *

+ * This is a mutable object - acts as a builder during the invocation of {@link io.helidon.dbclient.DbInterceptor}. + * The interceptors are executed sequentially, so there is no need for synchronization. + */ +public interface DbInterceptorContext { + /** + * Create a new interceptor context for a database provider. + * + * @param dbType a short name of the db type (such as jdbc:mysql) + * @return a new interceptor context ready to be configured + */ + static DbInterceptorContext create(String dbType) { + return new DbInterceptorContextImpl(dbType); + } + + /** + * Type of this database (usually the same string used by the {@link io.helidon.dbclient.spi.DbClientProvider#name()}). + * + * @return type of database + */ + String dbType(); + + /** + * Context with parameters passed from the caller, such as {@code SpanContext} for tracing. + * + * @return context associated with this request + */ + Context context(); + + /** + * Name of a statement to be executed. + * Ad hoc statements have names generated. + * + * @return name of the statement + */ + String statementName(); + + /** + * Text of the statement to be executed. + * + * @return statement text + */ + String statement(); + + /** + * A stage that is completed once the statement finishes execution. + * + * @return statement future + */ + CompletionStage statementFuture(); + + /** + * A stage that is completed once the results were fully read. The number returns either the number of modified + * records or the number of records actually read. + * + * @return stage that completes once all query results were processed. + */ + CompletionStage resultFuture(); + + /** + * Indexed parameters (if used). + * + * @return indexed parameters (empty if this statement parameters are not indexed) + */ + Optional> indexedParameters(); + + /** + * Named parameters (if used). + * + * @return named parameters (empty if this statement parameters are not named) + */ + Optional> namedParameters(); + + /** + * Whether this is a statement with indexed parameters. + * + * @return Whether this statement has indexed parameters ({@code true}) or named parameters {@code false}. + */ + boolean isIndexed(); + + /** + * Whether this is a statement with named parameters. + * + * @return Whether this statement has named parameters ({@code true}) or indexed parameters {@code false}. + */ + boolean isNamed(); + + /** + * Type of the statement being executed. + * @return statement type + */ + DbStatementType statementType(); + + /** + * Set a new context to be used by other interceptors and when executing the statement. + * + * @param context context to use + * @return updated interceptor context + */ + DbInterceptorContext context(Context context); + + /** + * Set a new statement name to be used. + * + * @param newName statement name to use + * @return updated interceptor context + */ + DbInterceptorContext statementName(String newName); + + /** + * Set a new future to mark completion of the statement. + * + * @param statementFuture future + * @return updated interceptor context + */ + DbInterceptorContext statementFuture(CompletionStage statementFuture); + + /** + * Set a new future to mark completion of the result (e.g. query or number of modified records). + * + * @param queryFuture future + * @return updated interceptor context + */ + DbInterceptorContext resultFuture(CompletionStage queryFuture); + + /** + * Set a new statement with indexed parameters to be used. + * + * @param statement statement text + * @param indexedParams indexed parameters + * @return updated interceptor context + */ + DbInterceptorContext statement(String statement, List indexedParams); + + /** + * Set a new statement with named parameters to be used. + * + * @param statement statement text + * @param namedParams named parameters + * @return updated interceptor context + */ + DbInterceptorContext statement(String statement, Map namedParams); + + /** + * Set new indexed parameters to be used. + * + * @param indexedParameters parameters + * @return updated interceptor context + * @throws IllegalArgumentException in case the statement is using named parameters + */ + DbInterceptorContext parameters(List indexedParameters); + + /** + * Set new named parameters to be used. + * + * @param namedParameters parameters + * @return updated interceptor context + * @throws IllegalArgumentException in case the statement is using indexed parameters + */ + DbInterceptorContext parameters(Map namedParameters); + + /** + * Set the type of the statement. + * + * @param type statement type + * @return updated interceptor context + */ + DbInterceptorContext statementType(DbStatementType type); +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbInterceptorContextImpl.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbInterceptorContextImpl.java new file mode 100644 index 000000000..568c10fe1 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbInterceptorContextImpl.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +import io.helidon.common.context.Context; + +/** + * Interceptor is a mutable object that is sent to {@link io.helidon.dbclient.DbInterceptor}. + */ +class DbInterceptorContextImpl implements DbInterceptorContext { + private final String dbType; + private DbStatementType dbStatementType = DbStatementType.UNKNOWN; + private Context context; + private String statementName; + private String statement; + private CompletionStage statementFuture; + private CompletionStage queryFuture; + private List indexedParams; + private Map namedParams; + private boolean indexed; + + DbInterceptorContextImpl(String dbType) { + this.dbType = dbType; + } + + @Override + public String dbType() { + return dbType; + } + + @Override + public Context context() { + return context; + } + + @Override + public String statementName() { + return statementName; + } + + @Override + public String statement() { + return statement; + } + + @Override + public CompletionStage statementFuture() { + return statementFuture; + } + + @Override + public Optional> indexedParameters() { + if (indexed) { + return Optional.of(indexedParams); + } + throw new IllegalStateException("Indexed parameters are not available for statement with named parameters"); + } + + @Override + public Optional> namedParameters() { + if (indexed) { + throw new IllegalStateException("Named parameters are not available for statement with indexed parameters"); + } + return Optional.of(namedParams); + } + + @Override + public boolean isIndexed() { + return indexed; + } + + @Override + public boolean isNamed() { + return !indexed; + } + + @Override + public DbInterceptorContext context(Context context) { + this.context = context; + return this; + } + + @Override + public DbInterceptorContext statementName(String newName) { + this.statementName = newName; + return this; + } + + @Override + public DbInterceptorContext statementFuture(CompletionStage statementFuture) { + this.statementFuture = statementFuture; + return this; + } + + @Override + public CompletionStage resultFuture() { + return queryFuture; + } + + @Override + public DbInterceptorContext resultFuture(CompletionStage resultFuture) { + this.queryFuture = resultFuture; + return this; + } + + @Override + public DbInterceptorContext statement(String statement, List indexedParams) { + this.statement = statement; + this.indexedParams = indexedParams; + this.indexed = true; + return this; + } + + @Override + public DbInterceptorContext statement(String statement, Map namedParams) { + this.statement = statement; + this.namedParams = namedParams; + this.indexed = false; + return this; + } + + @Override + public DbInterceptorContext parameters(List indexedParameters) { + if (indexed) { + this.indexedParams = indexedParameters; + } else { + throw new IllegalStateException("Cannot configure indexed parameters for a statement that expects named " + + "parameters"); + } + return this; + } + + @Override + public DbInterceptorContext parameters(Map namedParameters) { + if (indexed) { + throw new IllegalStateException("Cannot configure named parameters for a statement that expects indexed " + + "parameters"); + } + + this.namedParams = namedParameters; + return this; + } + + @Override + public DbStatementType statementType() { + return dbStatementType; + } + + @Override + public DbInterceptorContext statementType(DbStatementType type) { + this.dbStatementType = type; + return this; + } +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbMapper.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbMapper.java new file mode 100644 index 000000000..ba7d18e18 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbMapper.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.List; +import java.util.Map; + +/** + * A mapper to map database objects to/from a specific type. + *

+ * Mappers can be either provided through {@link io.helidon.dbclient.spi.DbClientProvider} or registered directly + * with the {@link io.helidon.dbclient.spi.DbClientProviderBuilder#addMapper(DbMapper, Class)}. + * + * @param target mapping type + */ +public interface DbMapper { + /** + * Read database row and convert it to target type instance. + * + * @param row source database row + * @return target type instance containing database row + */ + T read(DbRow row); + + /** + * Convert target type instance to a statement named parameters map. + * + * @param value mapping type instance containing values to be set into statement + * @return map of statement named parameters mapped to values to be set + * @see io.helidon.dbclient.DbStatement#namedParam(Object) + */ + Map toNamedParameters(T value); + + /** + * Convert target type instance to a statement indexed parameters list. + *

+ * Using indexed parameters with typed values is probably not going to work nicely, unless + * the order is specified and the number of parameters is always related the provided value. + * There are cases where this is useful though - e.g. for types that represent an iterable collection. + * + * @param value mapping type instance containing values to be set into statement + * @return map of statement named parameters mapped to values to be set + * @see io.helidon.dbclient.DbStatement#indexedParam(Object) + */ + List toIndexedParameters(T value); + +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbMapperManager.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbMapperManager.java new file mode 100644 index 000000000..85dc76c1f --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbMapperManager.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; + +import io.helidon.common.GenericType; +import io.helidon.common.mapper.MapperException; +import io.helidon.common.serviceloader.HelidonServiceLoader; +import io.helidon.dbclient.spi.DbMapperProvider; + +/** + * Mapper manager of all configured {@link io.helidon.dbclient.DbMapper mappers}. + */ +public interface DbMapperManager { + /** + * Generic type for the {@link io.helidon.dbclient.DbRow} class. + */ + GenericType TYPE_DB_ROW = GenericType.create(DbRow.class); + /** + * Generic type for the {@link Map} of String to value pairs for named parameters. + */ + GenericType> TYPE_NAMED_PARAMS = new GenericType>() { }; + /** + * Generic type for the {@link List} of indexed parameters. + */ + GenericType> TYPE_INDEXED_PARAMS = new GenericType>() { }; + + /** + * Create a fluent API builder to configure the mapper manager. + * + * @return a new builder instance + */ + static Builder builder() { + return new Builder(); + } + + /** + * Create a new mapper manager from Java Service loader only. + * + * @return mapper manager + */ + static DbMapperManager create() { + return DbMapperManager.builder().build(); + } + + /** + * Create a new mapper manager from customized {@link io.helidon.common.serviceloader.HelidonServiceLoader}. + * + * @param serviceLoader service loader to use to read all {@link io.helidon.dbclient.spi.DbMapperProvider} + * @return mapper manager + */ + static DbMapperManager create(HelidonServiceLoader serviceLoader) { + return DbMapperManager.builder() + .serviceLoader(serviceLoader) + .build(); + } + + /** + * Read database row into a typed value. + * + * @param row row from a database + * @param expectedType class of the response + * @param type of the response + * @return instance with data from the row + * @throws MapperException in case the mapper was not found + * @see io.helidon.dbclient.DbRow#as(Class) + */ + T read(DbRow row, Class expectedType) throws MapperException; + + /** + * Read database row into a typed value. + * + * @param row row from a database + * @param expectedType generic type of the response + * @param type of the response + * @return instance with data from the row + * @throws MapperException in case the mapper was not found + * @see io.helidon.dbclient.DbRow#as(io.helidon.common.GenericType) + */ + T read(DbRow row, GenericType expectedType) throws MapperException; + + /** + * Read object into a map of named parameters. + * + * @param value the typed value + * @param valueClass type of the value object + * @param type of value + * @return map with the named parameters + * @see io.helidon.dbclient.DbStatement#namedParam(Object) + */ + Map toNamedParameters(T value, Class valueClass); + + /** + * Read object into a list of indexed parameters. + * + * @param value the typed value + * @param valueClass type of the value object + * @param type of value + * @return list with indexed parameters (in the order expected by statements using this object) + * @see io.helidon.dbclient.DbStatement#indexedParam(Object) + */ + List toIndexedParameters(T value, Class valueClass); + + /** + * Fluent API builder for {@link io.helidon.dbclient.DbMapperManager}. + */ + final class Builder implements io.helidon.common.Builder { + + private final HelidonServiceLoader.Builder providers = HelidonServiceLoader + .builder(ServiceLoader.load(DbMapperProvider.class)); + + private HelidonServiceLoader providerLoader; + + private Builder() { + } + + @Override + public DbMapperManager build() { + return new DbMapperManagerImpl(this); + } + + /** + * Add a mapper provider. + * + * @param provider prioritized provider + * @return updated builder instance + */ + public Builder addMapperProvider(DbMapperProvider provider) { + this.providers.addService(provider); + return this; + } + + /** + * Add a mapper provider with custom priority. + * + * @param provider provider + * @param priority priority to use + * @return updated builder instance + * @see io.helidon.common.Prioritized + * @see javax.annotation.Priority + */ + public Builder addMapperProvider(DbMapperProvider provider, int priority) { + this.providers.addService(provider, priority); + return this; + } + + // to be used by implementation + List mapperProviders() { + if (null == providerLoader) { + return providers.build().asList(); + } else { + return providerLoader.asList(); + } + } + + private Builder serviceLoader(HelidonServiceLoader serviceLoader) { + this.providerLoader = serviceLoader; + return this; + } + } +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbMapperManagerImpl.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbMapperManagerImpl.java new file mode 100644 index 000000000..a21f4f7d7 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbMapperManagerImpl.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +import io.helidon.common.GenericType; +import io.helidon.common.mapper.MapperException; +import io.helidon.dbclient.spi.DbMapperProvider; + +/** + * Default implementation of the DbMapperManager. + */ +class DbMapperManagerImpl implements DbMapperManager { + public static final String ERROR_NO_MAPPER_FOUND = "Failed to find DB mapper."; + private final List providers; + private final Map, DbMapper> byClass = new ConcurrentHashMap<>(); + private final Map, DbMapper> byType = new ConcurrentHashMap<>(); + + DbMapperManagerImpl(Builder builder) { + this.providers = builder.mapperProviders(); + } + + @Override + public T read(DbRow row, Class expectedType) { + return executeMapping(() -> findMapper(expectedType, false) + .read(row), + row, + TYPE_DB_ROW, + GenericType.create(expectedType)); + } + + @Override + public T read(DbRow row, GenericType expectedType) { + return executeMapping(() -> findMapper(expectedType, false) + .read(row), + row, + TYPE_DB_ROW, + expectedType); + } + + @Override + public Map toNamedParameters(T value, Class valueClass) { + return executeMapping(() -> findMapper(valueClass, false) + .toNamedParameters(value), + value, + GenericType.create(valueClass), + TYPE_NAMED_PARAMS); + } + + @Override + public List toIndexedParameters(T value, Class valueClass) { + return executeMapping(() -> findMapper(valueClass, false) + .toIndexedParameters(value), + value, + GenericType.create(valueClass), + TYPE_INDEXED_PARAMS); + } + + private T executeMapping(Supplier mapping, Object source, GenericType sourceType, GenericType targetType) { + try { + return mapping.get(); + } catch (MapperException e) { + throw e; + } catch (Exception e) { + throw createMapperException(source, sourceType, targetType, e); + } + } + + @SuppressWarnings("unchecked") + private DbMapper findMapper(Class type, boolean fromTypes) { + DbMapper mapper = byClass.computeIfAbsent(type, aClass -> { + return fromProviders(type) + .orElseGet(() -> { + GenericType targetType = GenericType.create(type); + if (fromTypes) { + return notFoundMapper(targetType); + } + return findMapper(targetType, true); + }); + }); + + return (DbMapper) mapper; + } + + @SuppressWarnings("unchecked") + private DbMapper findMapper(GenericType type, boolean fromClasses) { + DbMapper mapper = byType.computeIfAbsent(type, aType -> { + return fromProviders(type) + .orElseGet(() -> { + if (!fromClasses && type.isClass()) { + return findMapper((Class) type.rawType(), true); + } + return notFoundMapper(type); + }); + }); + + return (DbMapper) mapper; + } + + private Optional> fromProviders(Class type) { + return providers.stream() + .flatMap(provider -> provider.mapper(type).stream()).findFirst(); + } + + private Optional> fromProviders(GenericType type) { + return providers.stream() + .flatMap(provider -> provider.mapper(type).stream()).findFirst(); + } + + private RuntimeException createMapperException(Object source, + GenericType sourceType, + GenericType targetType, + Throwable throwable) { + + throw new MapperException(sourceType, + targetType, + "Failed to map source of class '" + source.getClass().getName() + "'", + throwable); + } + + private static DbMapper notFoundMapper(GenericType type) { + return new DbMapper() { + @Override + public T read(DbRow row) { + throw new MapperException(TYPE_DB_ROW, type, ERROR_NO_MAPPER_FOUND); + } + + @Override + public Map toNamedParameters(T value) { + throw new MapperException(type, TYPE_NAMED_PARAMS, ERROR_NO_MAPPER_FOUND); + } + + @Override + public List toIndexedParameters(T value) { + throw new MapperException(type, TYPE_INDEXED_PARAMS, ERROR_NO_MAPPER_FOUND); + } + }; + } +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbResult.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbResult.java new file mode 100644 index 000000000..a2de6a8b6 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbResult.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; + +/** + * {@link DbExecute#createNamedStatement(String)} (and other generic statements) execution result. + * This is used when we do not know in advance whether we execute a query or a DML statement (such as insert, update, delete). + *

+ * This class represents a future of two possible types - either a DML result returning the + * number of changed rows (objects - depending on database type), or a query result returning the + * {@link DbRows}. + *

+ * One of the consumers on this interface is called as soon as it is known what type of statement was + * executed - for SQL this would be when we finish execution of the prepared statement. + *

+ * Alternative (or in combination) is to use the methods that return {@link java.util.concurrent.CompletionStage} + * to process the results when (and if) they are done. + */ +public interface DbResult { + /** + * For DML statements, number of changed rows/objects is provided as soon as the statement completes. + * + * @param consumer consumer that eventually receives the count + * @return DbResult to continue with processing a possible query result + */ + DbResult whenDml(Consumer consumer); + + /** + * For query statements, {@link DbRows} is provided as soon as the statement completes. + * For example in SQL, this would be the time we get the ResultSet from the database. Nevertheless the + * rows may not be read ({@link DbRows} itself represents a future of rows) + * + * @param consumer consumer that eventually processes the query result + * @return DbResult to continue with processing a possible dml result + */ + DbResult whenRs(Consumer> consumer); + + /** + * In case any exception occurs during processing, the handler is invoked. + * + * @param exceptionHandler handler to handle exceptional cases when processing the asynchronous request + * @return DbResult ot continue with other methods + */ + DbResult exceptionally(Consumer exceptionHandler); + + /** + * This future completes if (and only if) the statement was a DML statement. + * In case of any exception before the identification of statement type, all of + * {@link #dmlFuture()}, {@link #rsFuture()} finish exceptionally, and {@link #exceptionFuture()} completes with the + * exception. + * In case the exception occurs after the identification of statement type, such as when + * processing a result set of a query, only the {@link #exceptionFuture()} + * completes. Exceptions that occur during processing of result set are handled by + * methods in the {@link DbRows}. + * + * @return future for the DML result + */ + CompletionStage dmlFuture(); + + /** + * This future completes if (and only if) the statement was a query statement. + * In case of any exception before the identification of statement type, all of + * {@link #dmlFuture()}, {@link #rsFuture()} finish exceptionally, and {@link #exceptionFuture()} completes with the + * exception. + * In case the exception occurs after the identification of statement type, such as when + * processing a result set of a query, only the {@link #exceptionFuture()} + * completes. Exceptions that occur during processing of result set are handled by + * methods in the {@link DbRows}. + * + * @return future for the query result + */ + CompletionStage> rsFuture(); + + /** + * This future completes if (and only if) the statement finished with an exception, either + * when executing the statement, or when processing the result set. + * + * @return future for an exceptional result + */ + CompletionStage exceptionFuture(); +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbRow.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbRow.java new file mode 100644 index 000000000..bc77448de --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbRow.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.function.Consumer; +import java.util.function.Function; + +import io.helidon.common.GenericType; +import io.helidon.common.mapper.MapperException; + +/** + * Representation of a single row in a database (in SQL this would be a row, in a Document DB, this would be a single document). + */ +public interface DbRow { + /** + * Get a column in this row. Column is identified by its name. + * + * @param name column name + * @return a column in this row + */ + DbColumn column(String name); + + /** + * Get a column in this row. Column is identified by its index. + * + * @param index column index starting from {@code 1} + * @return a column in this row + */ + DbColumn column(int index); + + /** + * Iterate through each column in this row. + * + * @param columnAction what to do with each column + */ + void forEach(Consumer columnAction); + + /** + * Get specific class instance representation of this row. + * Mapper for target class must be already registered. + * + * @param type of the returned value + * @param type class of the returned value type + * @return instance of requested class containing this database row + * @throws MapperException in case the mapping is not defined or fails + */ + T as(Class type) throws MapperException; + + /** + * Map this row to an object using a {@link io.helidon.dbclient.DbMapper}. + * + * @param type type that supports generic declarations + * @param type to be returned + * @return typed row + * @throws MapperException in case the mapping is not defined or fails + * @throws MapperException in case the mapping is not defined or fails + */ + T as(GenericType type) throws MapperException; + + /** + * Get specific class instance representation of this row. + * Mapper for target class is provided as an argument. + * + * @param type of the returned value + * @param mapper method to create an target class instance from {@link DbRow} + * @return instance of requested class containing this database row + */ + T as(Function mapper); + +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbRows.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbRows.java new file mode 100644 index 000000000..f50c4e3e2 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbRows.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Flow; +import java.util.function.Function; + +import io.helidon.common.GenericType; + +/** + * Execution result containing result set with multiple rows. + * + * @param type of the result, starts as {@link io.helidon.dbclient.DbRow} + */ +public interface DbRows { + /** + * Map this row result using a mapping function. + * + * @param mapper mapping function + * @param new type of the row result + * @return row result of the correct type + */ + DbRows map(Function mapper); + + /** + * Map this row result using a configured {@link io.helidon.common.mapper.Mapper} that can + * map current type to the desired type. + * + *

+ * The first mapping for row results of type {@link io.helidon.dbclient.DbRow} will also try to locate + * appropriate mapper using {@link io.helidon.dbclient.DbMapperManager}. + * + * @param type class to map values to + * @param new type of the row result + * @return row result of the correct type + */ + DbRows map(Class type); + + /** + * Map this row result using a configured {@link io.helidon.common.mapper.Mapper} that can + * map current type to the desired type. + *

+ * The first mapping for row results of type {@link io.helidon.dbclient.DbRow} will also try to locate + * appropriate mapper using {@link io.helidon.dbclient.DbMapperManager}. + * + * @param type generic type to map values to + * @param new type of the row result + * @return row result of the target type + */ + DbRows map(GenericType type); + + /** + * Get this result as a publisher of rows mapped to the correct type. + * + * @return publisher + */ + Flow.Publisher publisher(); + + /** + * Collect all the results into a list of rows mapped to the correct type. + *

This is a dangerous operation, as it collects all results in memory. Use with care. + * @return future with the list + */ + CompletionStage> collect(); +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatement.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatement.java new file mode 100644 index 000000000..c55c52f54 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatement.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; + +/** + * Database statement that can process parameters. + * Method {@link #execute()} processes the statement and returns appropriate response. + *

+ * All methods are non-blocking. The {@link #execute()} method returns either a {@link java.util.concurrent.CompletionStage} + * or another object that provides similar API for eventual processing of the response. + *

+ * Once parameters are set using one of the {@code params} methods, all other methods throw an + * {@link IllegalStateException}. + *

+ * Once a parameter is added using {@link #addParam(Object)} or {@link #addParam(String, Object)}, all other + * {@code params} methods throw an {@link IllegalStateException}. + *

+ * Once {@link #execute()} is called, all methods would throw an {@link IllegalStateException}. + * + * @param Type of the result of this statement (e.g. a {@link java.util.concurrent.CompletionStage}) + * @param Type of the descendant of this class + */ +public interface DbStatement, R> { + /** + * Configure parameters from a {@link java.util.List} by order. + * The statement must use indexed parameters and configure them by order in the provided array. + * + * @param parameters ordered parameters to set on this statement, never null + * @return updated db statement + */ + D params(List parameters); + + /** + * Configure parameters from an array by order. + * The statement must use indexed parameters and configure them by order in the provided array. + * + * @param parameters ordered parameters to set on this statement + * @return updated db statement + */ + default D params(Object... parameters) { + return params(Arrays.asList(parameters)); + } + + /** + * Configure named parameters. + * The statement must use named parameters and configure them from the provided map. + * + * @param parameters named parameters to set on this statement + * @return updated db statement + */ + D params(Map parameters); + + /** + * Configure parameters using {@link Object} instance with registered mapper. + * The statement must use named parameters and configure them from the map provided by mapper. + * + * @param parameters {@link Object} instance containing parameters + * @return updated db statement + */ + D namedParam(Object parameters); + + /** + * Configure parameters using {@link Object} instance with registered mapper. + * The statement must use indexed parameters and configure them by order in the array provided by mapper. + * + * @param parameters {@link Object} instance containing parameters + * @return updated db statement + */ + D indexedParam(Object parameters); + + /** + * Add next parameter to the list of ordered parameters (e.g. the ones that use {@code ?} in SQL). + * + * @param parameter next parameter to set on this statement + * @return updated db statement + */ + D addParam(Object parameter); + + /** + * Add next parameter to the map of named parameters (e.g. the ones that use {@code :name} in Helidon + * JDBC SQL integration). + * + * @param name name of parameter + * @param parameter value of parameter + * @return updated db statement + */ + D addParam(String name, Object parameter); + + /** + * Execute this statement using the parameters configured with {@code params} and {@code addParams} methods. + * + * @return The future with result of this statement, as soon as the statement is executed; note that for queries + * this is before the results are actually obtained from the database + */ + CompletionStage execute(); +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementDml.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementDml.java new file mode 100644 index 000000000..70bd44141 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementDml.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +/** + * DML Database statement. + * A DML statement modifies records in the database and returns the number of modified records. + */ +public interface DbStatementDml extends DbStatement { +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementGeneric.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementGeneric.java new file mode 100644 index 000000000..55382cdf5 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementGeneric.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +/** + * A database statement that has unknown type (query or DML). + */ +public interface DbStatementGeneric extends DbStatement { +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementGet.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementGet.java new file mode 100644 index 000000000..49973c191 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementGet.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.Optional; + +/** + * Database statement that queries the database and returns a single row if present, or an empty optional. + * In case the statement returns more than one rows, the future returned by {@link #execute()} will end in + * {@link java.util.concurrent.CompletionStage#exceptionally(java.util.function.Function)}. + */ +public interface DbStatementGet extends DbStatement> { + +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementQuery.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementQuery.java new file mode 100644 index 000000000..14e3eeade --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementQuery.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +/** + * Database query statement. + */ +public interface DbStatementQuery extends DbStatement> { +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementType.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementType.java new file mode 100644 index 000000000..184b90289 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementType.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +/** + * Usual supported statement types. + */ +public enum DbStatementType { + /** + * Query is statement that returns zero or more results. + */ + QUERY("q"), + /** + * Get is a statement that returns zero or one results. + */ + GET("g"), + /** + * Insert is a statements that creates new records. + */ + INSERT("i"), + /** + * Update is a statement that updates existing records. + */ + UPDATE("u"), + /** + * Delete is a statement that deletes existing records. + */ + DELETE("d"), + /** + * Generic DML statement. + */ + DML("dml"), + /** + * Database command not related to a specific collection. + */ + COMMAND("c"), + /** + * The statement type is not yet knows (e.g. when invoking + * {@link DbExecute#createNamedStatement(String)}) + */ + UNKNOWN("x"); + + private final String prefix; + + DbStatementType(String prefix) { + this.prefix = prefix; + } + + /** + * Short prefix of this type. + * This is used when generating a name for an unnamed statement. + * + * @return short prefix defining this type (should be very short) + */ + public String prefix() { + return prefix; + } +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatements.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatements.java new file mode 100644 index 000000000..1c1dc07c2 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatements.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import io.helidon.config.Config; + +/** + * Configuration of statements to be used by database provider. + */ +@FunctionalInterface +public interface DbStatements { + /** + * Get statement text for a named statement. + * + * @param name name of the statement + * @return text of the statement (such as SQL code for SQL-based database statements) + * @throws DbClientException in case the statement name does not exist + */ + String statement(String name) throws DbClientException; + + /** + * Builder of statements. + * + * @return a builder to customize statements + */ + static Builder builder() { + return new Builder(); + } + + /** + * Create statements from configuration. + * Statement configuration is expected to be a map of name to statement pairs. + * + * @param config configuration of the statements + * @return statements as read from the configuration + */ + static DbStatements create(Config config) { + return DbStatements.builder() + .config(config) + .build(); + } + + /** + * Fluent API builder for {@link io.helidon.dbclient.DbStatements}. + */ + class Builder implements io.helidon.common.Builder { + private final Map configuredStatements = new HashMap<>(); + + /** + * Add named database statement to database configuration.. + * + * @param name database statement name + * @param statement database statement {@link String} + * @return database provider builder + */ + public Builder addStatement(String name, String statement) { + Objects.requireNonNull(name, "Statement name must be provided"); + Objects.requireNonNull(statement, "Statement body must be provided"); + configuredStatements.put(name, statement); + return this; + } + + /** + * Set statements from configuration. Each key in the current node is treated as a name of the statement, + * each value as the statement content. + * + * @param config config node located on correct node + * @return updated builder instance + */ + public Builder config(Config config) { + config.detach().asMap() + .ifPresent(configuredStatements::putAll); + return this; + } + + @Override + public DbStatements build() { + return new DbStatements() { + private final Map statements = new HashMap<>(configuredStatements); + + @Override + public String statement(String name) { + String statement = statements.get(name); + + if (null == statement) { + throw new DbClientException("Statement named '" + name + "' is not defined"); + } + + return statement; + } + + }; + } + } +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbTransaction.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbTransaction.java new file mode 100644 index 000000000..383c78a45 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbTransaction.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient; + +/** + * Database transaction. + * Holds a single transaction to the database (if supported). + * The transaction completes once {@link io.helidon.dbclient.DbClient#inTransaction(java.util.function.Function)} returns + * the result provided by the body of the lambda within it. + */ +public interface DbTransaction extends DbExecute { + /** + * Configure this transaction to (eventually) rollback. + */ + void rollback(); +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/package-info.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/package-info.java new file mode 100644 index 000000000..1f9e07088 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Reactive Database API for Helidon. + */ +package io.helidon.dbclient; diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/spi/DbClientProvider.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/spi/DbClientProvider.java new file mode 100644 index 000000000..a0327df93 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/spi/DbClientProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.spi; + +/** + * Java Service loader interface that provides drivers for a database (or a set of databases). + */ +public interface DbClientProvider { + + /** + * Name of this provider. This is used to find correct provider when using configuration only approach. + * + * @return provider name (such as {@code jdbc} or {@code mongo} + */ + String name(); + + /** + * The implementation should provide its implementation of the {@link DbClientProviderBuilder}. + * + * @return a new builder instance + */ + DbClientProviderBuilder builder(); + +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/spi/DbClientProviderBuilder.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/spi/DbClientProviderBuilder.java new file mode 100644 index 000000000..70cf7a9c5 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/spi/DbClientProviderBuilder.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.spi; + +import io.helidon.common.Builder; +import io.helidon.common.GenericType; +import io.helidon.common.mapper.MapperManager; +import io.helidon.config.Config; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbInterceptor; +import io.helidon.dbclient.DbMapper; +import io.helidon.dbclient.DbStatementType; +import io.helidon.dbclient.DbStatements; + +/** + * Database provider builder. + * + * @param type of the builder extending implementing this interface. + */ +public interface DbClientProviderBuilder> extends Builder { + + /** + * Use database connection configuration from configuration file. + * + * @param config {@link io.helidon.config.Config} instance with database connection attributes + * @return database provider builder + */ + T config(Config config); + + /** + * Set database connection string (URL). + * + * @param url database connection string + * @return database provider builder + */ + T url(String url); + + /** + * Set database connection user name. + * + * @param username database connection user name + * @return database provider builder + */ + T username(String username); + + /** + * Set database connection pยจassword. + * + * @param password database connection password + * @return database provider builder + */ + T password(String password); + + /** + * Statements to use either from configuration + * or manually configured. + * + * @param statements Statements to use + * @return updated builder instance + */ + T statements(DbStatements statements); + + /** + * Database schema mappers provider. + * + * @param provider database schema mappers provider to use + * @return updated builder instance + */ + T addMapperProvider(DbMapperProvider provider); + + /** + * Add a custom mapper. + * + * @param dbMapper the mapper capable of mapping the mappedClass to various database objects + * @param mappedClass class that this mapper supports + * @param type of the supported class + * @return updated builder instance. + */ + T addMapper(DbMapper dbMapper, Class mappedClass); + + /** + * Add a custom mapper with generic types support. + * + * @param dbMapper the mapper capable of mapping the mappedClass to various database objects + * @param mappedType type that this mapper supports + * @param type of the supported class + * @return updated builder instance. + */ + T addMapper(DbMapper dbMapper, GenericType mappedType); + + /** + * Mapper manager for generic mapping, such as mapping of parameters to expected types. + * + * @param manager mapper manager + * @return updated builder instance + */ + T mapperManager(MapperManager manager); + + /** + * Add an interceptor. + * This allows to add implementation of tracing, metrics, logging etc. without the need to hard-code these into + * the base. + * + * @param interceptor interceptor instance + * @return updated builder instance + */ + T addInterceptor(DbInterceptor interceptor); + + /** + * Add an interceptor that is active only on the configured statement names. + * This interceptor is only executed on named statements. + * + * @param interceptor interceptor instance + * @param statementNames statement names to be active on + * @return updated builder instance + */ + T addInterceptor(DbInterceptor interceptor, String... statementNames); + + /** + * Add an interceptor thas is active only on configured statement types. + * This interceptor is executed on all statements of that type. + *

+ * Note the specific handling of the following types: + *

    + *
  • {@link io.helidon.dbclient.DbStatementType#DML} - used only when the statement is created as a DML statement + * such as when using {@link io.helidon.dbclient.DbExecute#createDmlStatement(String)} + * (this interceptor would not be enabled for inserts, updates, deletes)
  • + *
  • {@link io.helidon.dbclient.DbStatementType#UNKNOWN} - used only when the statement is created as a general statement + * such as when using {@link io.helidon.dbclient.DbExecute#createStatement(String)} + * (this interceptor would not be enabled for any other statements)
  • + *
+ * + * @param interceptor interceptor instance + * @param dbStatementTypes statement types to be active on + * @return updated builder instance + */ + T addInterceptor(DbInterceptor interceptor, DbStatementType... dbStatementTypes); + + /** + * Build database handler for specific provider. + * + * @return database handler instance + */ + @Override + DbClient build(); + +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/spi/DbInterceptorProvider.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/spi/DbInterceptorProvider.java new file mode 100644 index 000000000..1e7f7c6f9 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/spi/DbInterceptorProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.spi; + +import io.helidon.config.Config; +import io.helidon.dbclient.DbInterceptor; + +/** + * Java service loader service to configure interceptors. + */ +public interface DbInterceptorProvider { + + /** + * The configuration key expected in config. + * If the key exists, the builder looks into + * {@code global}, {@code named}, and {@code typed} subkeys + * to configure appropriate instances. + * Method {@link #create(io.helidon.config.Config)} is called for each + * configuration as follows: + *
    + *
  • {@code global}: the configuration key is used to get a new instance
  • + *
  • {code named}: for each configuration node with a list of nodes, a new instance is requested
  • + *
  • {code typed}: for each configuration node with a list of types, a new instance is requested
  • + *
+ * @return name of the configuration key (such as "tracing") + */ + String configKey(); + + /** + * Create a new interceptor instance with the configuration provided. + * + * @param config configuration node with additional properties that are (maybe) configured for this interceptor + * @return an interceptor to handle DB statements + */ + DbInterceptor create(Config config); + +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/spi/DbMapperProvider.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/spi/DbMapperProvider.java new file mode 100644 index 000000000..9b9b98ee6 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/spi/DbMapperProvider.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.spi; + +import java.util.Optional; + +import io.helidon.common.GenericType; +import io.helidon.dbclient.DbMapper; + +/** + * Java Service loader interface for database mappers. + * + * @see io.helidon.dbclient.DbMapper + */ +public interface DbMapperProvider { + /** + * Returns mapper for specific type. + * + * @param target mapping type + * @param type class of the returned mapper type + * @return a mapper for the specified type or empty + */ + Optional> mapper(Class type); + + /** + * Returns mapper for specific type supporting generic types as well. + * To get a list of strings: {@code mapper(new GenericType>(){})} + * + * @param type type to find mapper for + * @param type of the response + * @return a mapper for the specified type or empty + */ + default Optional> mapper(GenericType type) { + return Optional.empty(); + } +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/spi/package-info.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/spi/package-info.java new file mode 100644 index 000000000..8130a659a --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/spi/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ +/** + * Service provider interface for Helidon DB. + * The main entry point for driver implementor is {@link io.helidon.dbclient.spi.DbClientProvider}. + * + * @see io.helidon.dbclient.spi.DbInterceptorProvider + * @see io.helidon.dbclient.spi.DbMapperProvider + */ +package io.helidon.dbclient.spi; diff --git a/dbclient/dbclient/src/main/java/module-info.java b/dbclient/dbclient/src/main/java/module-info.java new file mode 100644 index 000000000..91e641161 --- /dev/null +++ b/dbclient/dbclient/src/main/java/module-info.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Helidon DB Client. + * + * @see io.helidon.dbclient.DbClient + */ +module io.helidon.dbclient { + requires java.logging; + requires transitive io.helidon.config; + requires transitive io.helidon.common; + requires transitive io.helidon.common.context; + requires transitive io.helidon.common.mapper; + requires transitive io.helidon.common.serviceloader; + + exports io.helidon.dbclient; + exports io.helidon.dbclient.spi; + + uses io.helidon.dbclient.spi.DbClientProvider; + uses io.helidon.dbclient.spi.DbMapperProvider; + uses io.helidon.dbclient.spi.DbInterceptorProvider; +} diff --git a/dbclient/health/pom.xml b/dbclient/health/pom.xml new file mode 100644 index 000000000..a0bf98f9a --- /dev/null +++ b/dbclient/health/pom.xml @@ -0,0 +1,43 @@ + + + + + 4.0.0 + + io.helidon.dbclient + helidon-dbclient-project + 2.0-SNAPSHOT + + + helidon-dbclient-health + Helidon DB Client Health + + Health check for Helidon DB Client + + + + io.helidon.dbclient + helidon-dbclient + + + io.helidon.health + helidon-health + + + diff --git a/dbclient/health/src/main/java/io/helidon/dbclient/health/DbClientHealthCheck.java b/dbclient/health/src/main/java/io/helidon/dbclient/health/DbClientHealthCheck.java new file mode 100644 index 000000000..130d6d014 --- /dev/null +++ b/dbclient/health/src/main/java/io/helidon/dbclient/health/DbClientHealthCheck.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.health; + +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.common.HelidonFeatures; +import io.helidon.common.HelidonFlavor; +import io.helidon.dbclient.DbClient; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheckResponseBuilder; + +/** + * Database health check. + */ +public final class DbClientHealthCheck implements HealthCheck { + static { + HelidonFeatures.register(HelidonFlavor.SE, "DbClient", "HealthCheck"); + } + + private final DbClient dbClient; + private final String name; + + private DbClientHealthCheck(Builder builder) { + this.dbClient = builder.database; + this.name = builder.name; + } + + /** + * Create a health check for the database. + * + * @param dbClient A database that implements {@link io.helidon.dbclient.DbClient#ping()} + * @return health check that can be used with + * {@link io.helidon.health.HealthSupport.Builder#add(org.eclipse.microprofile.health.HealthCheck...)} + */ + public static DbClientHealthCheck create(DbClient dbClient) { + return builder(dbClient).build(); + } + + /** + * A fluent API builder to create a fully customized database health check. + * + * @param dbClient database + * @return a new builder + */ + public static Builder builder(DbClient dbClient) { + return new Builder(dbClient); + } + + @Override + public HealthCheckResponse call() { + HealthCheckResponseBuilder builder = HealthCheckResponse.builder() + .name(name); + + AtomicReference throwable = new AtomicReference<>(); + try { + dbClient.ping().toCompletableFuture() + .exceptionally(theThrowable -> { + throwable.set(theThrowable); + return null; + }) + .get(); + } catch (Throwable e) { + builder.down(); + throwable.set(e); + } + + Throwable thrown = throwable.get(); + + if (null == thrown) { + builder.up(); + } else { + thrown = thrown.getCause(); + builder.down(); + builder.withData("ErrorMessage", thrown.getMessage()); + builder.withData("ErrorClass", thrown.getClass().getName()); + } + return builder.build(); + } + + /** + * Fluent API builder for {@link DbClientHealthCheck}. + */ + public static final class Builder implements io.helidon.common.Builder { + private final DbClient database; + private String name; + + private Builder(DbClient database) { + this.database = database; + this.name = database.dbType(); + } + + @Override + public DbClientHealthCheck build() { + return new DbClientHealthCheck(this); + } + + /** + * Customized name of the health check. + * Default uses {@link io.helidon.dbclient.DbClient#dbType()}. + * + * @param name name of the health check + * @return updated builder instance + */ + public Builder name(String name) { + this.name = name; + return this; + } + } + +} diff --git a/dbclient/health/src/main/java/io/helidon/dbclient/health/package-info.java b/dbclient/health/src/main/java/io/helidon/dbclient/health/package-info.java new file mode 100644 index 000000000..cb138f7d1 --- /dev/null +++ b/dbclient/health/src/main/java/io/helidon/dbclient/health/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ +/** + * Health check support for Helidon DB Client. + */ +package io.helidon.dbclient.health; diff --git a/dbclient/health/src/main/java/module-info.java b/dbclient/health/src/main/java/module-info.java new file mode 100644 index 000000000..e5690f1dd --- /dev/null +++ b/dbclient/health/src/main/java/module-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Helidon DB Client Health Check. + */ +module io.helidon.dbclient.health { + requires java.logging; + requires io.helidon.dbclient; + requires io.helidon.health; + + exports io.helidon.dbclient.health; +} diff --git a/dbclient/jdbc/etc/spotbugs/exclude.xml b/dbclient/jdbc/etc/spotbugs/exclude.xml new file mode 100644 index 000000000..0e0f5ff8e --- /dev/null +++ b/dbclient/jdbc/etc/spotbugs/exclude.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + diff --git a/dbclient/jdbc/pom.xml b/dbclient/jdbc/pom.xml new file mode 100644 index 000000000..9d4f78bba --- /dev/null +++ b/dbclient/jdbc/pom.xml @@ -0,0 +1,64 @@ + + + + + 4.0.0 + + helidon-dbclient-project + io.helidon.dbclient + 2.0-SNAPSHOT + + + helidon-dbclient-jdbc + Helidon DB Client JDBC + Helidon DB implementation for JDBC + + + etc/spotbugs/exclude.xml + + + + + io.helidon.dbclient + helidon-dbclient + + + io.helidon.dbclient + helidon-dbclient-common + + + io.helidon.common + helidon-common-configurable + + + com.zaxxer + HikariCP + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/ConnectionPool.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/ConnectionPool.java new file mode 100644 index 000000000..6a6bfb876 --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/ConnectionPool.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc; + +import java.sql.Connection; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.ServiceLoader; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import io.helidon.common.serviceloader.HelidonServiceLoader; +import io.helidon.config.Config; +import io.helidon.dbclient.jdbc.spi.HikariCpExtensionProvider; + +/** + * JDBC Configuration parameters. + */ +@FunctionalInterface +public interface ConnectionPool { + + /** + * Create a JDBC connection pool from provided configuration. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Optional configuration parameters
keydefault valuedescription
url JDBC URL of the database - this property is required when only configuration is used. + * Example: {@code jdbc:mysql://127.0.0.1:3306/pokemon?useSSL=false}
username Username used to connect to the database
password Password used to connect to the database
+ * + * @param config configuration of connection pool + * @return a new instance configured from the provided config + */ + static ConnectionPool create(Config config) { + return ConnectionPool.builder() + .config(config) + .build(); + } + + /** + * Create a fluent API builder for a JDBC Connection pool based on URL, username and password. + * @return a new builder + */ + static Builder builder() { + return new Builder(); + } + + /** + * Return a connection from the pool. + * The call to {@link java.sql.Connection#close()} should return that connection to the pool. + * The connection pool should handle capacity issues and timeouts using unchecked exceptions thrown by this method. + * + * @return a connection read to execute statements + */ + Connection connection(); + + /** + * The type of this database - if better details than {@value JdbcDbClientProvider#JDBC_DB_TYPE} is + * available, return it. This could be "jdbc:mysql" etc. + * + * @return type of this database + */ + default String dbType() { + return JdbcDbClientProvider.JDBC_DB_TYPE; + } + + /** + * Fluent API builder for {@link io.helidon.dbclient.jdbc.ConnectionPool}. + * The builder will produce a connection pool based on Hikari connection pool and will support + * {@link io.helidon.dbclient.jdbc.spi.HikariCpExtensionProvider} to enhance the Hikari pool. + */ + final class Builder implements io.helidon.common.Builder { + /** + * Database connection URL configuration key. + */ + static final String URL = "url"; + /** + * Database connection user name configuration key. + */ + static final String USERNAME = "username"; + /** + * Database connection user password configuration key. + */ + static final String PASSWORD = "password"; + /** + * Database connection configuration key for Helidon specific + * properties. + */ + static final String HELIDON_RESERVED_CONFIG_KEY = "helidon"; + + //jdbc:mysql://127.0.0.1:3306/pokemon?useSSL=false + private static final Pattern URL_PATTERN = Pattern.compile("(\\w+:\\w+):.*"); + + private Properties properties = new Properties(); + + private String url; + private String username; + private String password; + private Config extensionsConfig; + private final HelidonServiceLoader.Builder extensionLoader = HelidonServiceLoader + .builder(ServiceLoader.load(HikariCpExtensionProvider.class)); + + private Builder() { + } + + @Override + public ConnectionPool build() { + final Matcher matcher = URL_PATTERN.matcher(url); + String dbType = matcher.matches() + ? matcher.group(1) + : JdbcDbClientProvider.JDBC_DB_TYPE; + + return new HikariConnectionPool(this, dbType, extensions()); + } + + private List extensions() { + if (null == extensionsConfig) { + extensionsConfig = Config.empty(); + } + return extensionLoader.build() + .asList() + .stream() + .map(provider -> provider.extension(extensionsConfig.get(provider.configKey()))) + .collect(Collectors.toList()); + } + + public Builder config(Config config) { + Map poolConfig = config.detach().asMap().get(); + poolConfig.forEach((key, value) -> { + switch (key) { + case URL: + url(value); + break; + case USERNAME: + username(value); + break; + case PASSWORD: + password(value); + break; + default: + if (!key.startsWith(HELIDON_RESERVED_CONFIG_KEY + ".")) { + // all other properties are sent to the pool + properties.setProperty(key, value); + } + + } + }); + this.extensionsConfig = config.get(HELIDON_RESERVED_CONFIG_KEY); + return this; + } + + public Builder url(String url) { + this.url = url; + return this; + } + + public Builder username(String username) { + this.username = username; + return this; + } + + public Builder password(String password) { + this.password = password; + return this; + } + + public Builder properties(Properties properties) { + this.properties = properties; + return this; + } + + Properties properties() { + return properties; + } + + String url() { + return url; + } + + String username() { + return username; + } + + String password() { + return password; + } + } +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/HikariConnectionPool.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/HikariConnectionPool.java new file mode 100644 index 000000000..e9790df5f --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/HikariConnectionPool.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +import io.helidon.dbclient.DbClientException; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +/** + * Hikari Connection Pool integration. + */ +public class HikariConnectionPool implements ConnectionPool { + /** Hikari Connection Pool instance. */ + private final HikariDataSource dataSource; + + /** The type of this database. */ + private final String dbType; + + HikariConnectionPool(Builder builder, String dbType, List extensions) { + this.dbType = dbType; + HikariConfig config = new HikariConfig(builder.properties()); + config.setJdbcUrl(builder.url()); + config.setUsername(builder.username()); + config.setPassword(builder.password()); + // Apply configuration update from extensions + extensions.forEach(interceptor -> { + interceptor.configure(config); + }); + this.dataSource = new HikariDataSource(config); + } + + @Override + public Connection connection() { + try { + return dataSource.getConnection(); + } catch (SQLException ex) { + throw new DbClientException( + String.format("Failed to create a connection to %s", dataSource.getJdbcUrl()), ex); + } + } + + @Override + public String dbType() { + return dbType; + } + +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/HikariCpExtension.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/HikariCpExtension.java new file mode 100644 index 000000000..21d0bfae3 --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/HikariCpExtension.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc; + +import com.zaxxer.hikari.HikariConfig; + +/** + * Interceptor to handle connection pool configuration. + */ +public interface HikariCpExtension { + /** + * Set additional configuration option on DB client configuration. + * + * @param poolConfig client configuration instance + */ + void configure(HikariConfig poolConfig); +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcDbClient.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcDbClient.java new file mode 100644 index 000000000..7df021ed3 --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcDbClient.java @@ -0,0 +1,355 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutorService; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.common.HelidonFeatures; +import io.helidon.common.HelidonFlavor; +import io.helidon.common.mapper.MapperManager; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbClientException; +import io.helidon.dbclient.DbExecute; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.DbStatementDml; +import io.helidon.dbclient.DbStatementGeneric; +import io.helidon.dbclient.DbStatementGet; +import io.helidon.dbclient.DbStatementQuery; +import io.helidon.dbclient.DbStatementType; +import io.helidon.dbclient.DbStatements; +import io.helidon.dbclient.DbTransaction; +import io.helidon.dbclient.common.AbstractDbExecute; +import io.helidon.dbclient.common.InterceptorSupport; + +/** + * Helidon DB implementation for JDBC drivers. + */ +class JdbcDbClient implements DbClient { + static { + HelidonFeatures.register(HelidonFlavor.SE, "DbClient", "JDBC"); + } + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(DbClient.class.getName()); + + private final ExecutorService executorService; + private final ConnectionPool connectionPool; + private final DbStatements statements; + private final DbMapperManager dbMapperManager; + private final MapperManager mapperManager; + private final InterceptorSupport interceptors; + + JdbcDbClient(JdbcDbClientProviderBuilder builder) { + this.executorService = builder.executorService(); + this.connectionPool = builder.connectionPool(); + this.statements = builder.statements(); + this.dbMapperManager = builder.dbMapperManager(); + this.mapperManager = builder.mapperManager(); + this.interceptors = builder.interceptors(); + } + + @Override + @SuppressWarnings("unchecked") + public CompletionStage inTransaction(Function> executor) { + + JdbcTxExecute execute = new JdbcTxExecute( + statements, + executorService, + interceptors, + connectionPool, + dbMapperManager, + mapperManager); + CompletionStage stage = executor.apply(execute) + .thenApply(it -> { + execute.context().whenComplete() + .thenAccept(nothing -> { + LOGGER.finest(() -> "Transaction commit"); + execute.doCommit().exceptionally(RollbackHandler.create(execute, Level.WARNING)); + }).exceptionally(RollbackHandler.create(execute, Level.WARNING)); + return it; + }); + + stage.exceptionally(RollbackHandler.create(execute, Level.FINEST)); + + return stage; + } + + /** + * Functional interface called to rollback failed transaction. + * + * @param statement execution result type + */ + private static final class RollbackHandler implements Function { + + private final JdbcTxExecute execute; + private final Level level; + + private static RollbackHandler create(final JdbcTxExecute execute, final Level level) { + return new RollbackHandler(execute, level); + } + + private RollbackHandler(final JdbcTxExecute execute, final Level level) { + this.execute = execute; + this.level = level; + } + + @Override + public T apply(Throwable t) { + LOGGER.log(level, + String.format("Transaction rollback: %s", t.getMessage()), + t); + execute.doRollback().exceptionally(t2 -> { + LOGGER.log(level, + String.format("Transaction rollback failed: %s", t2.getMessage()), + t2); + return null; + }); + return null; + } + + } + + @Override + public > T execute(Function executor) { + JdbcExecute execute = new JdbcExecute(statements, + executorService, + interceptors, + connectionPool, + dbMapperManager, + mapperManager); + + T resultFuture = executor.apply(execute); + + resultFuture.thenApply(it -> { + execute.context().whenComplete() + .thenAccept(nothing -> { + LOGGER.finest(() -> "Execution finished, closing connection"); + execute.close(); + }).exceptionally(throwable -> { + LOGGER.log(Level.WARNING, + String.format("Execution failed: %s", throwable.getMessage()), + throwable); + execute.close(); + return null; + }); + return it; + }); + + resultFuture.exceptionally(throwable -> { + LOGGER.log(Level.FINEST, + String.format("Execution failed: %s", throwable.getMessage()), + throwable); + execute.close(); + return null; + }); + + return resultFuture; + } + + @Override + public CompletionStage ping() { + return execute(exec -> exec.namedUpdate("ping")) + // need to get from Long to Void + .thenRun(() -> { + }); + } + + @Override + public String dbType() { + return connectionPool.dbType(); + } + + private static final class JdbcTxExecute extends JdbcExecute implements DbTransaction { + + private volatile boolean setRollbackOnly = false; + + private JdbcTxExecute(DbStatements statements, + ExecutorService executorService, + InterceptorSupport interceptors, + ConnectionPool connectionPool, + DbMapperManager dbMapperManager, + MapperManager mapperManager) { + super(statements, createTxContext(executorService, interceptors, connectionPool, dbMapperManager, mapperManager)); + } + + private static JdbcExecuteContext createTxContext(ExecutorService executorService, + InterceptorSupport interceptors, + ConnectionPool connectionPool, + DbMapperManager dbMapperManager, + MapperManager mapperManager) { + CompletionStage connection = CompletableFuture.supplyAsync(connectionPool::connection, executorService) + .thenApply(conn -> { + try { + conn.setAutoCommit(false); + } catch (SQLException e) { + throw new DbClientException("Failed to set autocommit to false", e); + } + return conn; + }); + + return JdbcExecuteContext.create(executorService, + interceptors, + connectionPool.dbType(), + connection, + dbMapperManager, + mapperManager); + } + + @Override + public void rollback() { + setRollbackOnly = true; + } + + private CompletionStage doRollback() { + return context().connection() + .thenApply(conn -> { + try { + conn.rollback(); + conn.close(); + } catch (SQLException e) { + throw new DbClientException("Failed to rollback a transaction, or close a connection", e); + } + + return conn; + }); + } + + private CompletionStage doCommit() { + if (setRollbackOnly) { + return doRollback(); + } + return context().connection() + .thenApply(conn -> { + try { + conn.commit(); + conn.close(); + } catch (SQLException e) { + throw new DbClientException("Failed to commit a transaction, or close a connection", e); + } + return conn; + }); + } + } + + private static class JdbcExecute extends AbstractDbExecute { + + private final JdbcExecuteContext context; + + private JdbcExecute(DbStatements statements, JdbcExecuteContext context) { + super(statements); + + this.context = context; + } + + private JdbcExecute(DbStatements statements, + ExecutorService executorService, + InterceptorSupport interceptors, + ConnectionPool connectionPool, + DbMapperManager dbMapperManager, + MapperManager mapperManager) { + this(statements, createContext(executorService, interceptors, connectionPool, dbMapperManager, mapperManager)); + } + + private static JdbcExecuteContext createContext(ExecutorService executorService, + InterceptorSupport interceptors, + ConnectionPool connectionPool, + DbMapperManager dbMapperManager, + MapperManager mapperManager) { + CompletionStage connection = CompletableFuture.supplyAsync(connectionPool::connection, executorService) + .thenApply(conn -> { + try { + conn.setAutoCommit(true); + } catch (SQLException e) { + throw new DbClientException("Failed to set autocommit to true", e); + } + return conn; + }); + + return JdbcExecuteContext.create(executorService, + interceptors, + connectionPool.dbType(), + connection, + dbMapperManager, + mapperManager); + } + + @Override + public DbStatementQuery createNamedQuery(String statementName, String statement) { + return new JdbcStatementQuery(context, + JdbcStatementContext.create(DbStatementType.QUERY, statementName, statement)); + + } + + @Override + public DbStatementGet createNamedGet(String statementName, String statement) { + return new JdbcStatementGet(context, + JdbcStatementContext.create(DbStatementType.GET, statementName, statement)); + } + + @Override + public DbStatementDml createNamedDmlStatement(String statementName, String statement) { + return new JdbcStatementDml(context, + JdbcStatementContext.create(DbStatementType.DML, statementName, statement)); + } + + @Override + public DbStatementDml createNamedInsert(String statementName, String statement) { + return new JdbcStatementDml(context, + JdbcStatementContext.create(DbStatementType.INSERT, statementName, statement)); + } + + @Override + public DbStatementDml createNamedUpdate(String statementName, String statement) { + return new JdbcStatementDml(context, + JdbcStatementContext.create(DbStatementType.UPDATE, statementName, statement)); + } + + @Override + public DbStatementDml createNamedDelete(String statementName, String statement) { + return new JdbcStatementDml(context, + JdbcStatementContext.create(DbStatementType.DELETE, statementName, statement)); + } + + @Override + public DbStatementGeneric createNamedStatement(String statementName, String statement) { + return new JdbcStatementGeneric(context, + JdbcStatementContext.create(DbStatementType.UNKNOWN, statementName, statement)); + } + + JdbcExecuteContext context() { + return context; + } + + void close() { + context.connection() + .thenAccept(conn -> { + try { + conn.close(); + } catch (SQLException e) { + LOGGER.log(Level.WARNING, String.format("Could not close connection: %s", e.getMessage()), e); + } + }); + } + } + +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcDbClientProvider.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcDbClientProvider.java new file mode 100644 index 000000000..b9b835008 --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcDbClientProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc; + +import io.helidon.dbclient.spi.DbClientProvider; + +/** + * Provider for JDBC database implementation. + */ +public class JdbcDbClientProvider implements DbClientProvider { + + static final String JDBC_DB_TYPE = "jdbc"; + + @Override + public String name() { + return JDBC_DB_TYPE; + } + + @Override + public JdbcDbClientProviderBuilder builder() { + return new JdbcDbClientProviderBuilder(); + } + +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcDbClientProviderBuilder.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcDbClientProviderBuilder.java new file mode 100644 index 000000000..9a15951b2 --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcDbClientProviderBuilder.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc; + +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +import io.helidon.common.GenericType; +import io.helidon.common.configurable.ThreadPoolSupplier; +import io.helidon.common.mapper.MapperManager; +import io.helidon.config.Config; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbClientException; +import io.helidon.dbclient.DbInterceptor; +import io.helidon.dbclient.DbMapper; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.DbStatementType; +import io.helidon.dbclient.DbStatements; +import io.helidon.dbclient.common.InterceptorSupport; +import io.helidon.dbclient.spi.DbClientProviderBuilder; +import io.helidon.dbclient.spi.DbMapperProvider; + +/** + * Fluent API builder for {@link JdbcDbClientProviderBuilder} that implements + * the {@link io.helidon.dbclient.spi.DbClientProviderBuilder} from Helidon DB API. + */ +public final class JdbcDbClientProviderBuilder implements DbClientProviderBuilder { + + private final InterceptorSupport.Builder interceptors = InterceptorSupport.builder(); + private final DbMapperManager.Builder dbMapperBuilder = DbMapperManager.builder(); + + private String url; + private String username; + private String password; + private DbStatements statements; + private MapperManager mapperManager; + private DbMapperManager dbMapperManager; + private Supplier executorService; + private ConnectionPool connectionPool; + + JdbcDbClientProviderBuilder() { + } + + @Override + public DbClient build() { + if (null == connectionPool) { + if (null == url) { + throw new DbClientException("No database connection configuration (%s) was found. Use \"connection\" " + + "configuration key, or configure on builder using \"connectionPool" + + "(ConnectionPool)\""); + } + connectionPool = ConnectionPool.builder() + .url(url) + .username(username) + .password(password) + .build(); + } + if (null == dbMapperManager) { + this.dbMapperManager = dbMapperBuilder.build(); + } + if (null == mapperManager) { + this.mapperManager = MapperManager.create(); + } + if (null == executorService) { + executorService = ThreadPoolSupplier.create(); + } + return new JdbcDbClient(this); + } + + @Override + public JdbcDbClientProviderBuilder config(Config config) { + config.get("connection") + .detach() + .ifExists(cfg -> connectionPool(ConnectionPool.create(cfg))); + + config.get("statements").as(DbStatements::create).ifPresent(this::statements); + config.get("executor-service").as(ThreadPoolSupplier::create).ifPresent(this::executorService); + return this; + } + + /** + * Configure a connection pool. + * + * @param connectionPool connection pool to get connections to a database + * @return updated builder instance + */ + public JdbcDbClientProviderBuilder connectionPool(ConnectionPool connectionPool) { + this.connectionPool = connectionPool; + return this; + } + + /** + * Configure an explicit executor service supplier. + * The executor service is used to execute blocking calls to a database. + * + * @param executorServiceSupplier supplier to obtain an executor service from + * @return updated builder instance + */ + public JdbcDbClientProviderBuilder executorService(Supplier executorServiceSupplier) { + this.executorService = executorServiceSupplier; + return this; + } + + @Override + public JdbcDbClientProviderBuilder url(String url) { + this.url = url; + return this; + } + + @Override + public JdbcDbClientProviderBuilder username(String username) { + this.username = username; + return this; + } + + @Override + public JdbcDbClientProviderBuilder password(String password) { + this.password = password; + return this; + } + + @Override + public JdbcDbClientProviderBuilder statements(DbStatements statements) { + this.statements = statements; + return this; + } + + @Override + public JdbcDbClientProviderBuilder addMapper(DbMapper dbMapper, Class mappedClass) { + this.dbMapperBuilder.addMapperProvider(new DbMapperProvider() { + @SuppressWarnings("unchecked") + @Override + public Optional> mapper(Class type) { + if (type.equals(mappedClass)) { + return Optional.of((DbMapper) dbMapper); + } + return Optional.empty(); + } + }); + return this; + } + + @Override + public JdbcDbClientProviderBuilder addMapper(DbMapper dbMapper, GenericType mappedType) { + this.dbMapperBuilder.addMapperProvider(new DbMapperProvider() { + @Override + public Optional> mapper(Class type) { + return Optional.empty(); + } + + @SuppressWarnings("unchecked") + @Override + public Optional> mapper(GenericType type) { + if (type.equals(mappedType)) { + return Optional.of((DbMapper) dbMapper); + } + return Optional.empty(); + } + }); + return this; + } + + @Override + public JdbcDbClientProviderBuilder mapperManager(MapperManager manager) { + this.mapperManager = manager; + return this; + } + + @Override + public JdbcDbClientProviderBuilder addMapperProvider(DbMapperProvider provider) { + this.dbMapperBuilder.addMapperProvider(provider); + return this; + } + + @Override + public JdbcDbClientProviderBuilder addInterceptor(DbInterceptor interceptor) { + this.interceptors.add(interceptor); + return this; + } + + @Override + public JdbcDbClientProviderBuilder addInterceptor(DbInterceptor interceptor, String... statementNames) { + this.interceptors.add(interceptor, statementNames); + return this; + } + + @Override + public JdbcDbClientProviderBuilder addInterceptor(DbInterceptor interceptor, DbStatementType... dbStatementTypes) { + this.interceptors.add(interceptor, dbStatementTypes); + return this; + } + + DbStatements statements() { + return statements; + } + + InterceptorSupport interceptors() { + return interceptors.build(); + } + + DbMapperManager dbMapperManager() { + return dbMapperManager; + } + + MapperManager mapperManager() { + return mapperManager; + } + + ExecutorService executorService() { + return executorService.get(); + } + + ConnectionPool connectionPool() { + return connectionPool; + } + +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcExecuteContext.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcExecuteContext.java new file mode 100644 index 000000000..75066fa58 --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcExecuteContext.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc; + +import java.sql.Connection; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; + +import io.helidon.common.mapper.MapperManager; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.common.InterceptorSupport; + +/** + * Stuff needed by each and every statement. + */ +final class JdbcExecuteContext { + + private final ExecutorService executorService; + private final InterceptorSupport interceptors; + private final DbMapperManager dbMapperManager; + private final MapperManager mapperManager; + private final String dbType; + private final CompletionStage connection; + private final ConcurrentHashMap.KeySetView, Boolean> futures = ConcurrentHashMap.newKeySet(); + + private JdbcExecuteContext(ExecutorService executorService, + InterceptorSupport interceptors, + DbMapperManager dbMapperManager, + MapperManager mapperManager, + String dbType, + CompletionStage connection) { + this.executorService = executorService; + this.interceptors = interceptors; + this.dbMapperManager = dbMapperManager; + this.mapperManager = mapperManager; + this.dbType = dbType; + this.connection = connection; + } + + static JdbcExecuteContext create(ExecutorService executorService, + InterceptorSupport interceptors, + String dbType, + CompletionStage connection, + DbMapperManager dbMapperManager, + MapperManager mapperManager) { + return new JdbcExecuteContext(executorService, + interceptors, + dbMapperManager, + mapperManager, + dbType, + connection); + } + + ExecutorService executorService() { + return executorService; + } + + InterceptorSupport interceptors() { + return interceptors; + } + + DbMapperManager dbMapperManager() { + return dbMapperManager; + } + + MapperManager mapperManager() { + return mapperManager; + } + + String dbType() { + return dbType; + } + + CompletionStage connection() { + return connection; + } + + void addFuture(CompletableFuture queryFuture) { + this.futures.add(queryFuture); + } + + public CompletionStage whenComplete() { + CompletionStage overallStage = CompletableFuture.completedFuture(null); + + for (CompletableFuture future : futures) { + overallStage = overallStage.thenCompose(o -> future); + } + + return overallStage.thenAccept(it -> { + }); + } + +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcQueryExecutor.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcQueryExecutor.java new file mode 100644 index 000000000..ff5071a7c --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcQueryExecutor.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * An executor that cycles through in-process queries and executes read operations. + */ +class JdbcQueryExecutor { + + private final Random random = new Random(); + private final List runnables = new ArrayList<>(); + + void submit(QueryProcessor processor) { + /* + The idea is to associate the processor with a thread that is next to have free cycles. + The same thread should process the same processors - e.g. the tryNext should always read only a single record (or a + small set of records) from the result set. + The number of threads to use must be configurable (and may be changing over time such as in an executor service + Once the query processor completes, we remove it from teh cycle of that thread + */ + for (StmtRunnable runnable : runnables) { + if (runnable.processors.isEmpty()) { + runnable.addProcessor(processor); + return; + } + } + for (StmtRunnable runnable : runnables) { + if (runnable.idle.get()) { + runnable.addProcessor(processor); + return; + } + } + // none is idle, add it to a random one + // TODO this must have size limits on the number of processors per runnable - what to do if all busy? add a thread + // what to do if all threads are used - throw a nice exception + // we rather refuse work than kill everything + runnables.get(random.nextInt(runnables.size())).addProcessor(processor); + } + + interface QueryProcessor { + boolean tryNext(); + + boolean isCompleted(); + } + + // FIXME: This may need some review and redesign. + private static class StmtRunnable implements Runnable { + + private final Set processors = Collections.newSetFromMap(new IdentityHashMap<>()); + private final AtomicBoolean idle = new AtomicBoolean(); + private final AtomicBoolean enabled = new AtomicBoolean(true); + + void addProcessor(QueryProcessor processor) { + // lock + processors.add(processor); + // unlock + // we have added a processor, maybe it wants to do stuff immediately + requestRun(); + } + + void requestRun() { + // let the next run know, that it should run, or release the waiting "run" method +// something.release(); + } + + @Override + public void run() { + while (enabled.get()) { + // this is the idle loop + idle.set(true); +// something.await(); + idle.set(false); + + // this is the one-cycle of processing loop + boolean working = true; + while (working) { + working = false; + + List toRemove = new LinkedList<>(); + // read lock + for (QueryProcessor processor : processors) { + if (processor.isCompleted()) { + toRemove.add(processor); + } else { + if (processor.tryNext()) { + working = true; + } + } + } + // write lock + toRemove.forEach(processors::remove); + } + } + } + + } + +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java new file mode 100644 index 000000000..1285e2431 --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java @@ -0,0 +1,748 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbClientException; +import io.helidon.dbclient.DbInterceptorContext; +import io.helidon.dbclient.DbStatement; +import io.helidon.dbclient.common.AbstractStatement; + +/** + * Common JDBC statement builder. + * + * @param subclass of this class + * @param Statement execution result type + */ +abstract class JdbcStatement, R> extends AbstractStatement { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(JdbcStatement.class.getName()); + + private final ExecutorService executorService; + private final String dbType; + private final CompletionStage connection; + private final JdbcExecuteContext executeContext; + + JdbcStatement(JdbcExecuteContext executeContext, JdbcStatementContext statementContext) { + super(statementContext.statementType(), + statementContext.statementName(), + statementContext.statement(), + executeContext.dbMapperManager(), + executeContext.mapperManager(), + executeContext.interceptors()); + + this.executeContext = executeContext; + this.dbType = executeContext.dbType(); + this.connection = executeContext.connection(); + this.executorService = executeContext.executorService(); + } + + PreparedStatement build(Connection conn, DbInterceptorContext dbContext) { + LOGGER.fine(() -> String.format("Building SQL statement: %s", dbContext.statement())); + String statement = dbContext.statement(); + String statementName = dbContext.statementName(); + + Supplier simpleStatementSupplier = () -> prepareStatement(conn, statementName, statement); + + if (dbContext.isIndexed()) { + return dbContext.indexedParameters() + .map(params -> prepareIndexedStatement(conn, statementName, statement, params)) + .orElseGet(simpleStatementSupplier); + } else { + return dbContext.namedParameters() + .map(params -> prepareNamedStatement(conn, statementName, statement, params)) + .orElseGet(simpleStatementSupplier); + } + } + + /** + * Switch to {@link #build(java.sql.Connection, io.helidon.dbclient.DbInterceptorContext)} and use interceptors. + * + * @param connection connection to use + * @return prepared statement + */ + @Deprecated + protected PreparedStatement build(Connection connection) { + LOGGER.fine(() -> String.format("Building SQL statement: %s", statement())); + switch (paramType()) { + // Statement may not contain any parameters, no conversion is needed. + case UNKNOWN: + return prepareStatement(connection, statementName(), statement()); + case INDEXED: + return prepareIndexedStatement(connection, statementName(), statement(), indexedParams()); + case NAMED: + return prepareNamedStatement(connection, statementName(), statement(), namedParams()); + default: + throw new IllegalStateException("Unknown SQL statement type"); + } + } + + @Override + protected String dbType() { + return dbType; + } + + CompletionStage connection() { + return connection; + } + + ExecutorService executorService() { + return executorService; + } + + JdbcExecuteContext executeContext() { + return executeContext; + } + + private PreparedStatement prepareStatement(Connection conn, String statementName, String statement) { + try { + return conn.prepareStatement(statement); + } catch (SQLException e) { + throw new DbClientException(String.format("Failed to prepare statement: %s", statementName), e); + } + } + + private PreparedStatement prepareNamedStatement(Connection connection, + String statementName, + String statement, + Map parameters) { + + PreparedStatement preparedStatement = null; + try { + // Parameters names must be replaced with ? and names occurence order must be stored. + Parser parser = new Parser(statement); + String jdbcStatement = parser.convert(); + LOGGER.finest(() -> String.format("Converted statement: %s", jdbcStatement)); + preparedStatement = connection.prepareStatement(jdbcStatement); + List namesOrder = parser.namesOrder(); + // SQL statement and provided parameters integrity check + if (namesOrder.size() > parameters.size()) { + throw new DbClientException(namedStatementErrorMessage(namesOrder, parameters)); + } + // Set parameters into prepared statement + int i = 1; + for (String name : namesOrder) { + if (parameters.containsKey(name)) { + Object value = parameters.get(name); + LOGGER.finest(String.format("Mapped parameter %d: %s -> %s", i, name, value)); + preparedStatement.setObject(i, value); + i++; + } else { + throw new DbClientException(namedStatementErrorMessage(namesOrder, parameters)); + } + } + return preparedStatement; + } catch (SQLException e) { + closePreparedStatement(preparedStatement); + throw new DbClientException("Failed to prepare statement with named parameters: " + statementName, e); + } + } + + private PreparedStatement prepareIndexedStatement(Connection connection, + String statementName, + String statement, + List parameters) { + + PreparedStatement preparedStatement = null; + try { + preparedStatement = connection.prepareStatement(statement); + int i = 1; // JDBC set position parameter starts from 1. + for (Object value : parameters) { + LOGGER.finest(String.format("Indexed parameter %d: %s", i, value)); + preparedStatement.setObject(i, value); + // increase value for next iteration + i++; + } + return preparedStatement; + } catch (SQLException e) { + closePreparedStatement(preparedStatement); + throw new DbClientException(String.format("Failed to prepare statement with indexed params: %s", statementName), e); + } + } + + private void closePreparedStatement(final PreparedStatement preparedStatement) { + if (preparedStatement != null) { + try { + preparedStatement.close(); + } catch (SQLException e) { + LOGGER.log(Level.WARNING, String.format("Could not close PreparedStatement: %s", e.getMessage()), e); + } + } + } + + private static String namedStatementErrorMessage(final List namesOrder, final Map parameters) { + // Parameters in query missing in parameters Map + List notInParams = new ArrayList<>(namesOrder.size()); + for (String name : namesOrder) { + if (!parameters.containsKey(name)) { + notInParams.add(name); + } + } + StringBuilder sb = new StringBuilder(); + sb.append("Query parameters missing in Map: "); + boolean first = true; + for (String name : notInParams) { + if (first) { + first = false; + } else { + sb.append(", "); + } + sb.append(name); + } + return sb.toString(); + } + + /** + * Mapping parser state machine. + * + * Before replacement: + * {@code SELECT * FROM table WHERE name = :name AND type = :type} + * After replacement: + * {@code SELECT * FROM table WHERE name = ? AND type = ?} + * Expected list of parameters: + * {@code "name", "type"} + */ + static final class Parser { + @FunctionalInterface + + private interface Action extends Consumer {} + + /** + * Character classes used in state machine. + */ + private enum CharClass { + LETTER, // Letter (any unicode letter) + NUMBER, // Number (any unicode digit) + LF, // Line feed / new line (\n), terminates line alone or in CR LF sequence + CR, // Carriage return (\r), terminates line in CR LF sequence + APOSTROPHE, // Single quote ('), begins string in SQL + STAR, // Star (*), part of multiline comment beginning "/*" and ending "*/" sequence + DASH, // Dash (-), part of single line comment beginning sequence "--" + SLASH, // Slash (/), part of multiline comment beginning "/*" and ending "*/" sequence + COLON, // Colon (:), begins named parameter + OTHER; // Other characters + + /** + * Returns character class corresponding to provided character. + * + * @param c character to determine its character class + * @return character class corresponding to provided character + */ + private static CharClass charClass(char c) { + switch (c) { + case '\r': return CR; + case '\n': return LF; + case '\'': return APOSTROPHE; + case '*': return STAR; + case '-': return DASH; + case '/': return SLASH; + case ':': return COLON; + default: + return Character.isLetter(c) + ? LETTER + : (Character.isDigit(c) ? NUMBER : OTHER); + } + } + + } + + /** + * States used in state machine. + */ + private enum State { + STATEMENT, // Common statement processing + STRING, // SQL string processing after 1st APOSTROPHE was recieved + COLON, // Symbolic name processing after opening COLON (colon) was recieved + PARAMETER, // Symbolic name processing after 1st LETTER or later LETTER + // or NUMBER of parameter name was recieved + MULTILN_COMMENT_BG, // Multiline comment processing after opening slash was recieved from the "/*" sequence + MULTILN_COMMENT_END, // Multiline comment processing after closing star was recieved from the "*/" sequence + MULTILN_COMMENT, // Multiline comment processing of the comment itself + SINGLELN_COMMENT_BG, // Single line comment processing after opening dash was recieved from the "--" sequence + SINGLELN_COMMENT_END, // Single line comment processing after closing CR was recieved from the CR LF sequence + SINGLELN_COMMENT; // Single line comment processing of the comment itself + + /** States transition table. */ + private static final State[][] TRANSITION = { + // Transitions from STATEMENT state + { + STATEMENT, // LETTER: regular part of the statement, keep processing it + STATEMENT, // NUMBER: regular part of the statement, keep processing it + STATEMENT, // LF: regular part of the statement, keep processing it + STATEMENT, // CR: regular part of the statement, keep processing it + STRING, // APOSTROPHE: beginning of SQL string processing, switch to STRING state + STATEMENT, // STAR: regular part of the statement, keep processing it + SINGLELN_COMMENT_BG, // DASH: possible starting sequence of single line comment, + // switch to SINGLELN_COMMENT_BG state + MULTILN_COMMENT_BG, // SLASH: possible starting sequence of multi line comment, + // switch to MULTILN_COMMENT_BG state + COLON, // COLON: possible beginning of named parameter, switch to COLON state + STATEMENT // OTHER: regular part of the statement, keep processing it + }, + // Transitions from STRING state + { + STRING, // LETTER: regular part of the SQL string, keep processing it + STRING, // NUMBER: regular part of the SQL string, keep processing it + STRING, // LF: regular part of the SQL string, keep processing it + STRING, // CR: regular part of the SQL string, keep processing it + STATEMENT, // APOSTROPHE: end of SQL string processing, go back to STATEMENT state + STRING, // STAR: regular part of the SQL string, keep processing it + STRING, // DASH: regular part of the SQL string, keep processing it + STRING, // SLASH: regular part of the SQL string, keep processing it + STRING, // COLON: regular part of the SQL string, keep processing it + STRING // OTHER: regular part of the SQL string, keep processing it + }, + // Transitions from COLON state + { + PARAMETER, // LETTER: first character of named parameter, switch to PARAMETER state + STATEMENT, // NUMBER: can't be first character of named parameter, go back to STATEMENT state + STATEMENT, // LF: can't be first character of named parameter, go back to STATEMENT state + STATEMENT, // CR: can't be first character of named parameter, go back to STATEMENT state + STRING, // APOSTROPHE: not a named parameter but beginning of SQL string processing, + // switch to STRING state + STATEMENT, // STAR: can't be first character of named parameter, go back to STATEMENT state + SINGLELN_COMMENT_BG, // DASH: not a named parameter but possible starting sequence of single line comment, + // switch to SINGLELN_COMMENT_BG state + MULTILN_COMMENT_BG, // SLASH: not a named parameter but possible starting sequence of multi line comment, + // switch to MULTILN_COMMENT_BG state + COLON, // COLON: not a named parameter but possible beginning of another named parameter, + // retry named parameter processing + STATEMENT // OTHER: can't be first character of named parameter, go back to STATEMENT state + }, + // Transitions from PARAMETER state + { + PARAMETER, // LETTER: next character of named parameter, keep processing it + PARAMETER, // NUMBER: next character of named parameter, keep processing it + STATEMENT, // LF: can't be next character of named parameter, go back to STATEMENT state + STATEMENT, // CR: can't be next character of named parameter, go back to STATEMENT state + STRING, // APOSTROPHE: end of named parameter and beginning of SQL string processing, + // switch to STRING state + STATEMENT, // STAR: can't be next character of named parameter, go back to STATEMENT state + SINGLELN_COMMENT_BG, // DASH: end of named parameter and possible starting sequence of single line comment, + // switch to SINGLELN_COMMENT_BG state + MULTILN_COMMENT_BG, // SLASH: end of named parameter and possible starting sequence of multi line comment, + // switch to MULTILN_COMMENT_BG state + COLON, // COLON: end of named parameter and possible beginning of another named parameter, + // switch to COLON state to restart named parameter processing + STATEMENT // OTHER: can't be next character of named parameter, go back to STATEMENT state + }, + // Transitions from MULTILN_COMMENT_BG state + { + STATEMENT, // LETTER: not starting sequence of multi line comment, go back to STATEMENT state + STATEMENT, // NUMBER: not starting sequence of multi line comment, go back to STATEMENT state + STATEMENT, // LF: not starting sequence of multi line comment, go back to STATEMENT state + STATEMENT, // CR: not starting sequence of multi line comment, go back to STATEMENT state + STRING, // APOSTROPHE: not starting sequence of multi line comment but beginning of SQL + // string processing, switch to STRING state + MULTILN_COMMENT, // STAR: end of starting sequence of multi line comment, + // switch to MULTILN_COMMENT state + SINGLELN_COMMENT_BG, // DASH: not starting sequence of multi line comment but possible starting sequence + // of single line comment, switch to SINGLELN_COMMENT_BG state + MULTILN_COMMENT_BG, // SLASH: not starting sequence of multi line comment but possible starting sequence + // of next multi line comment, retry multi line comment processing + COLON, // COLON: not starting sequence of multi line comment but possible beginning + // of named parameter, switch to COLON state + STATEMENT // OTHER: not starting sequence of multi line comment, go back to STATEMENT state + }, + // Transitions from MULTILN_COMMENT_END state + { + MULTILN_COMMENT, // LETTER: not ending sequence of multi line comment, go back to MULTILN_COMMENT state + MULTILN_COMMENT, // NUMBER: not ending sequence of multi line comment, go back to MULTILN_COMMENT state + MULTILN_COMMENT, // LF: not ending sequence of multi line comment, go back to MULTILN_COMMENT state + MULTILN_COMMENT, // CR: not ending sequence of multi line comment, go back to MULTILN_COMMENT state + MULTILN_COMMENT, // APOSTROPHE: not ending sequence of multi line comment, + // go back to MULTILN_COMMENT state + MULTILN_COMMENT_END, // STAR: not ending sequence of multi line comment but possible ending sequence + // of next multi line comment, retry end of multi line comment processing + MULTILN_COMMENT, // DASH: not ending sequence of multi line comment, go back to MULTILN_COMMENT state + STATEMENT, // SLASH: end of ending sequence of multi line comment, + // switch to STATEMENT state + MULTILN_COMMENT, // COLON: not ending sequence of multi line comment, go back to MULTILN_COMMENT state + MULTILN_COMMENT // OTHER: not ending sequence of multi line comment, go back to MULTILN_COMMENT state + }, + // Transitions from MULTILN_COMMENT state + { + MULTILN_COMMENT, // LETTER: regular multi line comment, keep processing it + MULTILN_COMMENT, // NUMBER: regular multi line comment, keep processing it + MULTILN_COMMENT, // LF: regular multi line comment, keep processing it + MULTILN_COMMENT, // CR: regular multi line comment, keep processing it + MULTILN_COMMENT, // APOSTROPHE: regular multi line comment, keep processing it + MULTILN_COMMENT_END, // STAR: possible ending sequence of multi line comment, + // switch to MULTILN_COMMENT_END state + MULTILN_COMMENT, // DASH: regular multi line comment, keep processing it + MULTILN_COMMENT, // SLASH: regular multi line comment, keep processing it + MULTILN_COMMENT, // COLON: regular multi line comment, keep processing it + MULTILN_COMMENT // OTHER: regular multi line comment, keep processing it + }, + // Transitions from SINGLELN_COMMENT_BG state + { + STATEMENT, // LETTER: not starting sequence of single line comment, go back to STATEMENT state + STATEMENT, // NUMBER: not starting sequence of single line comment, go back to STATEMENT state + STATEMENT, // LF: not starting sequence of single line comment, go back to STATEMENT state + STATEMENT, // CR: not starting sequence of single line comment, go back to STATEMENT state + STRING, // APOSTROPHE: not starting sequence of single line comment but beginning of SQL + // string processing, switch to STRING state + STATEMENT, // STAR: not starting sequence of single line comment, go back to STATEMENT state + SINGLELN_COMMENT, // DASH: end of starting sequence of single line comment, + // switch to SINGLELN_COMMENT state + MULTILN_COMMENT_BG, // SLASH: not starting sequence of single line comment but possible starting sequence + // of next multi line comment, switch to MULTILN_COMMENT_BG state + COLON, // COLON: not starting sequence of single line comment but possible beginning + // of named parameter, switch to COLON state + STATEMENT // OTHER: not starting sequence of single line comment, go back to STATEMENT state + }, + // Transitions from SINGLELN_COMMENT_END state + { + SINGLELN_COMMENT, // LETTER: not ending sequence of single line comment, go back to SINGLELN_COMMENT state + SINGLELN_COMMENT, // NUMBER: not ending sequence of single line comment, go back to SINGLELN_COMMENT state + STATEMENT, // LF: end of single line comment, switch to STATEMENT state + SINGLELN_COMMENT_END, // CR: not ending sequence of single line comment but possible ending sequence + // of next single line comment, retry end of single line comment processing + SINGLELN_COMMENT, // APOSTROPHE: not ending sequence of single line comment, + // go back to SINGLELN_COMMENT state + SINGLELN_COMMENT, // STAR: not ending sequence of single line comment, go back to SINGLELN_COMMENT state + SINGLELN_COMMENT, // DASH: not ending sequence of single line comment, go back to SINGLELN_COMMENT state + SINGLELN_COMMENT, // SLASH: not ending sequence of single line comment, go back to SINGLELN_COMMENT state + SINGLELN_COMMENT, // COLON: not ending sequence of single line comment, go back to SINGLELN_COMMENT state + SINGLELN_COMMENT // OTHER: not ending sequence of single line comment, go back to SINGLELN_COMMENT state + }, + // Transitions from SINGLELN_COMMENT state + { + SINGLELN_COMMENT, // LETTER: regular single line comment, keep processing it + SINGLELN_COMMENT, // NUMBER: regular single line comment, keep processing it + STATEMENT, // LF: end of single line comment, switch to STATEMENT state + SINGLELN_COMMENT_END, // CR: possible beginning of ending sequence of multi line comment, + // switch to SINGLELN_COMMENT_END state + SINGLELN_COMMENT, // APOSTROPHE: regular single line comment, keep processing it + SINGLELN_COMMENT, // STAR: regular single line comment, keep processing it + SINGLELN_COMMENT, // DASH: regular single line comment, keep processing it + SINGLELN_COMMENT, // SLASH: regular single line comment, keep processing it + SINGLELN_COMMENT, // COLON: regular single line comment, keep processing it + SINGLELN_COMMENT // OTHER: regular single line comment, keep processing it + } + }; + } + + /** + * State automaton action table. + */ + private static final Action[][] ACTION = { + // Actions performed on transitions from STATEMENT state + { + Parser::copyChar, // LETTER: copy regular statement character to output + Parser::copyChar, // NUMBER: copy regular statement character to output + Parser::copyChar, // LF: copy regular statement character to output + Parser::copyChar, // CR: copy regular statement character to output + Parser::copyChar, // APOSTROPHE: copy SQL string character to output + Parser::copyChar, // STAR: copy regular statement character to output + Parser::copyChar, // DASH: copy character to output, no matter wheter it's comment or not + Parser::copyChar, // SLASH: copy character to output, no matter wheter it's comment or not + Parser::doNothing, // COLON: delay character copying until it's obvious whether this is parameter or not + Parser::copyChar // OTHER: copy regular statement character to output + }, + // Actions performed on transitions from STRING state + { + Parser::copyChar, // LETTER: copy SQL string character to output + Parser::copyChar, // NUMBER: copy SQL string character to output + Parser::copyChar, // LF: copy SQL string character to output + Parser::copyChar, // CR: copy SQL string character to output + Parser::copyChar, // APOSTROPHE: copy SQL string character to output + Parser::copyChar, // STAR: copy SQL string character to output + Parser::copyChar, // DASH: copy SQL string character to output + Parser::copyChar, // SLASH: copy SQL string character to output + Parser::copyChar, // COLON: copy SQL string character to output + Parser::copyChar // OTHER: copy SQL string character to output + }, + // Actions performed on transitions from COLON state + { + Parser::setFirstParamChar, // LETTER: set first parameter character + Parser::addColonAndCopyChar, // NUMBER: not a parameter, add delayed colon and copy current statement character + // to output + Parser::addColonAndCopyChar, // LF: not a parameter, add delayed colon and copy current statement character + // to output + Parser::addColonAndCopyChar, // CR: not a parameter, add delayed colon and copy current statement character + // to output + Parser::addColonAndCopyChar, // APOSTROPHE: not a parameter, add delayed colon and copy current SQL string + // character to output + Parser::addColonAndCopyChar, // STAR: not a parameter, add delayed colon and copy current statement character + // to output + Parser::addColonAndCopyChar, // DASH: not a parameter, add delayed colon and copy current statement character + // to output, no matter wheter it's comment or not + Parser::addColonAndCopyChar, // SLASH: not a parameter, add delayed colon and copy current statement character + // to output, no matter wheter it's comment or not + Parser::addColon, // COLON: not a parameter, add delayed colon and delay current colon copying + // until it's obvious whether this is parameter or not + Parser::addColonAndCopyChar // OTHER: not a parameter, add delayed colon and copy current statement character + // to output + }, + // Actions performed on transitions from PARAMETER state + { + Parser::setNextParamChar, // LETTER: set next parameter character + Parser::setNextParamChar, // NUMBER: set next parameter character + Parser::finishParamAndCopyChar, // LF: finish parameter processing and copy current character as part + // of regular statement + Parser::finishParamAndCopyChar, // CR: finish parameter processing and copy current character as part + // of regular statement + Parser::finishParamAndCopyChar, // APOSTROPHE: finish parameter processing and copy current character as part + // of regular statement + Parser::finishParamAndCopyChar, // STAR: finish parameter processing and copy current character as part + // of regular statement + Parser::finishParamAndCopyChar, // DASH: finish parameter processing and copy current character as part + // of regular statement + Parser::finishParamAndCopyChar, // SLASH: finish parameter processing and copy current character as part + // of regular statement + Parser::finishParam, // COLON: finish parameter processing and delay character copying until + // it's obvious whether this is next parameter or not + Parser::finishParamAndCopyChar // OTHER: finish parameter processing and copy current character as part + // of regular statement + }, + // Actions performed on transitions from MULTILN_COMMENT_BG state + { + Parser::copyChar, // LETTER: copy regular statement character to output + Parser::copyChar, // NUMBER: copy regular statement character to output + Parser::copyChar, // LF: copy regular statement character to output + Parser::copyChar, // CR: copy regular statement character to output + Parser::copyChar, // APOSTROPHE: copy SQL string character to output + Parser::copyChar, // STAR: copy multi line comment character to output + Parser::copyChar, // DASH: copy character to output, no matter wheter it's comment or not + Parser::copyChar, // SLASH: copy character to output, no matter wheter it's comment or not + Parser::doNothing, // COLON: delay character copying until it's obvious whether this is parameter or not + Parser::copyChar // OTHER: copy regular statement character to output + }, + // Actions performed on transitions from MULTILN_COMMENT_END state + { + Parser::copyChar, // LETTER: copy multi line comment character to output + Parser::copyChar, // NUMBER: copy multi line comment character to output + Parser::copyChar, // LF: copy multi line comment character to output + Parser::copyChar, // CR: copy multi line comment character to output + Parser::copyChar, // APOSTROPHE: copy multi line comment character to output + Parser::copyChar, // STAR: copy multi line comment character to output + Parser::copyChar, // DASH: copy multi line comment character to output + Parser::copyChar, // SLASH: copy multi line comment character to output + Parser::copyChar, // COLON: copy multi line comment character to output + Parser::copyChar // OTHER: copy multi line comment character to output + }, + // Actions performed on transitions from MULTILN_COMMENT state + { + Parser::copyChar, // LETTER: copy multi line comment character to output + Parser::copyChar, // NUMBER: copy multi line comment character to output + Parser::copyChar, // LF: copy multi line comment character to output + Parser::copyChar, // CR: copy multi line comment character to output + Parser::copyChar, // APOSTROPHE: copy multi line comment character to output + Parser::copyChar, // STAR: copy multi line comment character to output + Parser::copyChar, // DASH: copy multi line comment character to output + Parser::copyChar, // SLASH: copy multi line comment character to output + Parser::copyChar, // COLON: copy multi line comment character to output + Parser::copyChar // OTHER: copy multi line comment character to output + }, + // Actions performed on transitions from SINGLELN_COMMENT_BG state + { + Parser::copyChar, // LETTER: copy regular statement character to output + Parser::copyChar, // NUMBER: copy regular statement character to output + Parser::copyChar, // LF: copy regular statement character to output + Parser::copyChar, // CR: copy regular statement character to output + Parser::copyChar, // APOSTROPHE: copy SQL string character to output + Parser::copyChar, // STAR: copy regular statement character to output + Parser::copyChar, // DASH: copy single line comment character to output + Parser::copyChar, // SLASH: copy character to output, no matter wheter it's comment or not + Parser::doNothing, // COLON: delay character copying until it's obvious whether this is parameter or not + Parser::copyChar // OTHER: copy regular statement character to output + }, + // Actions performed on transitions from SINGLELN_COMMENT_END state + { + Parser::copyChar, // LETTER: copy single line comment character to output + Parser::copyChar, // NUMBER: copy single line comment character to output + Parser::copyChar, // LF: copy single line comment character to output + Parser::copyChar, // CR: copy single line comment character to output + Parser::copyChar, // APOSTROPHE: copy single line comment character to output + Parser::copyChar, // STAR: copy single line comment character to output + Parser::copyChar, // DASH: copy single line comment character to output + Parser::copyChar, // SLASH: copy single line comment character to output + Parser::copyChar, // COLON: copy single line comment character to output + Parser::copyChar // OTHER: copy single line comment character to output + }, + // Actions performed on transitions from SINGLELN_COMMENT state + { + Parser::copyChar, // LETTER: copy single line comment character to output + Parser::copyChar, // NUMBER: copy single line comment character to output + Parser::copyChar, // LF: copy single line comment character to output + Parser::copyChar, // CR: copy single line comment character to output + Parser::copyChar, // APOSTROPHE: copy single line comment character to output + Parser::copyChar, // STAR: copy single line comment character to output + Parser::copyChar, // DASH: copy single line comment character to output + Parser::copyChar, // SLASH: copy single line comment character to output + Parser::copyChar, // COLON: copy single line comment character to output + Parser::copyChar // OTHER: copy single line comment character to output + } + }; + + /** + * Do nothing. + * + * @param parser parser instance + */ + private static void doNothing(Parser parser) { + } + + /** + * Copy character from input string to output as is. + * + * @param parser parser instance + */ + private static void copyChar(Parser parser) { + parser.sb.append(parser.c); + } + + /** + * Add previous colon character to output. + * + * @param parser parser instance + */ + private static void addColon(Parser parser) { + parser.sb.append(':'); + } + + /** + * Copy previous colon and current input string character to output. + * + * @param parser parser instance + */ + private static void addColonAndCopyChar(Parser parser) { + parser.sb.append(':'); + parser.sb.append(parser.c); + } + + /** + * Store 1st named parameter letter. + * + * @param parser parser instance + */ + private static void setFirstParamChar(Parser parser) { + parser.nap.setLength(0); + parser.nap.append(parser.c); + } + + /** + * Store next named parameter letter or number. + * + * @param parser parser instance + */ + private static void setNextParamChar(Parser parser) { + parser.nap.append(parser.c); + } + + /** + * Finish stored named parameter and copy current character from input string to output as is. + * + * @param parser parser instance + */ + private static void finishParamAndCopyChar(Parser parser) { + String parName = parser.nap.toString(); + parser.names.add(parName); + parser.sb.append('?'); + parser.sb.append(parser.c); + } + + /** + * Finish stored named parameter without copying current character from input string to output as is. + * + * @param parser parser instance + */ + private static void finishParam(Parser parser) { + String parName = parser.nap.toString(); + parser.names.add(parName); + parser.sb.append('?'); + } + + /** + * SQL statement to be parsed. + */ + private final String statement; + + /** + * Target SQL statement builder. + */ + private final StringBuilder sb; + + /** + * Temporary string storage. + */ + private final StringBuilder nap; + + /** + * Ordered list of parameter names. + */ + private final List names; + + /** + * Character being currently processed. + */ + private char c; + + /** + * Character class of character being currently processed. + */ + private CharClass cl; + + Parser(String statement) { + this.sb = new StringBuilder(statement.length()); + this.nap = new StringBuilder(32); + this.names = new LinkedList<>(); + this.statement = statement; + this.c = '\0'; + this.cl = null; + + } + + String convert() { + State state = State.STATEMENT; // Initial state: common statement processing + int len = statement.length(); + for (int i = 0; i < len; i++) { + c = statement.charAt(i); + cl = CharClass.charClass(c); + ACTION[state.ordinal()][cl.ordinal()].accept(this); + state = State.TRANSITION[state.ordinal()][cl.ordinal()]; + } + // Process end of statement + if (state == State.PARAMETER) { + String parName = nap.toString(); + names.add(parName); + sb.append('?'); + } + return sb.toString(); + } + + List namesOrder() { + return names; + } + + } + +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementContext.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementContext.java new file mode 100644 index 000000000..30681ccfc --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementContext.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc; + +import io.helidon.dbclient.DbStatementType; + +/** + * Stuff needed by each and every statement. + */ +class JdbcStatementContext { + + private final DbStatementType statementType; + private final String statementName; + private final String statement; + + private JdbcStatementContext(DbStatementType statementType, String statementName, String statement) { + this.statementType = statementType; + this.statementName = statementName; + this.statement = statement; + } + + static JdbcStatementContext create(DbStatementType statementType, String statementName, String statement) { + return new JdbcStatementContext(statementType, statementName, statement); + } + + DbStatementType statementType() { + return statementType; + } + + String statementName() { + return statementName; + } + + String statement() { + return statement; + } + +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementDml.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementDml.java new file mode 100644 index 000000000..9f4c2ed3d --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementDml.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc; + +import java.sql.PreparedStatement; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import io.helidon.dbclient.DbInterceptorContext; +import io.helidon.dbclient.DbStatementDml; + +class JdbcStatementDml extends JdbcStatement implements DbStatementDml { + + JdbcStatementDml(JdbcExecuteContext executeContext, + JdbcStatementContext statementContext) { + super(executeContext, statementContext); + } + + @Override + protected CompletionStage doExecute(CompletionStage dbContextFuture, + CompletableFuture statementFuture, + CompletableFuture queryFuture) { + + executeContext().addFuture(queryFuture); + + // query and statement future must always complete either OK, or exceptionally + dbContextFuture.exceptionally(throwable -> { + statementFuture.completeExceptionally(throwable); + queryFuture.completeExceptionally(throwable); + return null; + }); + + return dbContextFuture.thenCompose(dbContext -> { + return connection().thenCompose(connection -> { + executorService().submit(() -> { + try { + PreparedStatement preparedStatement = build(connection, dbContext); + long count = preparedStatement.executeLargeUpdate(); + statementFuture.complete(null); + queryFuture.complete(count); + preparedStatement.close(); + } catch (Exception e) { + statementFuture.completeExceptionally(e); + queryFuture.completeExceptionally(e); + } + }); + // the query future is reused, as it completes with the number of updated records + return queryFuture; + }); + }); + } + +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementGeneric.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementGeneric.java new file mode 100644 index 000000000..617ee9ba9 --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementGeneric.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbInterceptorContext; +import io.helidon.dbclient.DbResult; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.dbclient.DbStatementGeneric; + +/** + * Generic statement. + */ +class JdbcStatementGeneric extends JdbcStatement implements DbStatementGeneric { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(JdbcStatementGeneric.class.getName()); + + private static final class GenericDbResult implements DbResult { + + private final CompletableFuture> queryResultFuture; + private final CompletableFuture dmlResultFuture; + private final CompletableFuture exceptionFuture; + + GenericDbResult( + final CompletableFuture> queryResultFuture, + final CompletableFuture dmlResultFuture, + final CompletableFuture exceptionFuture + ) { + this.queryResultFuture = queryResultFuture; + this.dmlResultFuture = dmlResultFuture; + this.exceptionFuture = exceptionFuture; + } + + @Override + public DbResult whenDml(Consumer consumer) { + dmlResultFuture.thenAccept(consumer); + return this; + } + + @Override + public DbResult whenRs(Consumer> consumer) { + queryResultFuture.thenAccept(consumer); + return this; + } + + @Override + public DbResult exceptionally(Consumer exceptionHandler) { + exceptionFuture.thenAccept(exceptionHandler); + return this; + } + + @Override + public CompletionStage dmlFuture() { + return dmlResultFuture; + } + + @Override + public CompletionStage> rsFuture() { + return queryResultFuture; + } + + @Override + public CompletionStage exceptionFuture() { + return exceptionFuture; + } + } + + JdbcStatementGeneric(JdbcExecuteContext executeContext, + JdbcStatementContext statementContext) { + super(executeContext, statementContext); + } + + @Override + protected CompletionStage doExecute(CompletionStage dbContextFuture, + CompletableFuture interceptorStatementFuture, + CompletableFuture interceptorQueryFuture) { + + executeContext().addFuture(interceptorQueryFuture); + + CompletableFuture> queryResultFuture = new CompletableFuture<>(); + CompletableFuture dmlResultFuture = new CompletableFuture<>(); + CompletableFuture exceptionFuture = new CompletableFuture<>(); + CompletableFuture resultSetFuture = new CompletableFuture<>(); + + dbContextFuture.exceptionally(throwable -> { + resultSetFuture.completeExceptionally(throwable); + + return null; + }); + // this is completed on execution of statement + resultSetFuture.exceptionally(throwable -> { + interceptorStatementFuture.completeExceptionally(throwable); + queryResultFuture.completeExceptionally(throwable); + dmlResultFuture.completeExceptionally(throwable); + exceptionFuture.completeExceptionally(throwable); + return null; + }); + + dbContextFuture.thenAccept(dbContext -> { + // now let's execute the statement + connection().thenAccept(conn -> { + executorService().submit(() -> { + try { + PreparedStatement statement = super.build(conn, dbContext); + + boolean isQuery = statement.execute(); + // statement is executed, we can finish the statement future + interceptorStatementFuture.complete(null); + + if (isQuery) { + ResultSet resultSet = statement.getResultSet(); + // at this moment we have a DbRowResult + resultSetFuture.complete(new JdbcStatementQuery.ResultWithConn(resultSet, conn)); + } else { + try { + long update = statement.getLargeUpdateCount(); + interceptorQueryFuture.complete(update); + dmlResultFuture.complete(update); + statement.close(); + } finally { + conn.close(); + } + } + } catch (Exception e) { + if (null != conn) { + try { + // we would not close the connection in the resultSetFuture, so we have to close it here + conn.close(); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, + String.format("Failed to close connection: %s", ex.getMessage()), + ex); + } + } + resultSetFuture.completeExceptionally(e); + } + }); + }); + }); + + + /* + TODO Too many futures + Futures: + interceptorStatementFuture - completed once we call the statement + interceptorQueryFuture - completed once we read all records from a query (or finish a DML) - requires count + queryResultFuture - completed once we know this is a result set and we have prepared the DbRowResult + dmlResultFuture - completed once we know this is a DML statement + exceptionFuture - completes in case of any exception + resultSetFuture - completes if this is a result set and has the connection & result set objects + */ + // for DML - everything is finished and done + /* + For Query + Ignored: + dmlResultFuture + Completed: + interceptorStatementFuture + resultSetFuture + Open: + interceptorQueryFuture + queryResultFuture + exceptionFuture + */ + + // and now, let's construct the DbRowResult + resultSetFuture.thenAccept(rsAndConn -> { + DbRows dbRows = JdbcStatementQuery.processResultSet( + executorService(), + dbMapperManager(), + mapperManager(), + interceptorQueryFuture, + rsAndConn.resultSet()); + + interceptorQueryFuture.exceptionally(throwable -> { + exceptionFuture.complete(throwable); + return null; + }); + queryResultFuture.complete(dbRows); + }).exceptionally(throwable -> { + interceptorQueryFuture.completeExceptionally(throwable); + queryResultFuture.completeExceptionally(throwable); + exceptionFuture.complete(throwable); + return null; + }); + + return interceptorStatementFuture.thenApply(nothing -> { + return new GenericDbResult(queryResultFuture, dmlResultFuture, exceptionFuture); + }); + } + +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementGet.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementGet.java new file mode 100644 index 000000000..a555c0f41 --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementGet.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +import io.helidon.common.reactive.Single; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbStatementGet; + +/** + * A JDBC get implementation. + * Delegates to {@link io.helidon.dbclient.jdbc.JdbcStatementQuery} and processes the result using a subscriber + * to read the first value. + */ +class JdbcStatementGet implements DbStatementGet { + + private final JdbcStatementQuery query; + + JdbcStatementGet(JdbcExecuteContext executeContext, + JdbcStatementContext statementContext) { + + this.query = new JdbcStatementQuery(executeContext, + statementContext); + } + + @Override + public JdbcStatementGet params(List parameters) { + query.params(parameters); + return this; + } + + @Override + public JdbcStatementGet params(Map parameters) { + query.params(parameters); + return this; + } + + @Override + public JdbcStatementGet namedParam(Object parameters) { + query.namedParam(parameters); + return this; + } + + @Override + public JdbcStatementGet indexedParam(Object parameters) { + query.indexedParam(parameters); + return this; + } + + @Override + public JdbcStatementGet addParam(Object parameter) { + query.addParam(parameter); + return this; + } + + @Override + public JdbcStatementGet addParam(String name, Object parameter) { + query.addParam(name, parameter); + return this; + } + + @Override + public CompletionStage> execute() { + return query.execute() + .thenApply(dbRows -> Single.from(dbRows.publisher())) + .thenCompose(Single::toOptionalStage); + } + +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementQuery.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementQuery.java new file mode 100644 index 000000000..c91769c65 --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementQuery.java @@ -0,0 +1,573 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Flow; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.common.GenericType; +import io.helidon.common.mapper.MapperException; +import io.helidon.common.mapper.MapperManager; +import io.helidon.common.reactive.Multi; +import io.helidon.dbclient.DbClientException; +import io.helidon.dbclient.DbColumn; +import io.helidon.dbclient.DbInterceptorContext; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.dbclient.DbStatementQuery; + +/** + * Implementation of query. + */ +class JdbcStatementQuery extends JdbcStatement> implements DbStatementQuery { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(JdbcStatementQuery.class.getName()); + + JdbcStatementQuery(JdbcExecuteContext executeContext, + JdbcStatementContext statementContext) { + super(executeContext, statementContext); + } + + @Override + protected CompletionStage> doExecute(CompletionStage dbContextFuture, + CompletableFuture statementFuture, + CompletableFuture queryFuture) { + + executeContext().addFuture(queryFuture); + + CompletionStage> result = dbContextFuture.thenCompose(interceptorContext -> { + return connection().thenApply(conn -> { + PreparedStatement statement = super.build(conn, interceptorContext); + try { + ResultSet rs = statement.executeQuery(); + // at this moment we have a DbRows + statementFuture.complete(null); + return processResultSet(executorService(), + dbMapperManager(), + mapperManager(), + queryFuture, + rs); + } catch (SQLException e) { + LOGGER.log(Level.FINEST, + String.format("Failed to execute query %s: %s", statement.toString(), e.getMessage()), + e); + throw new DbClientException("Failed to execute query", e); + } + }); + }); + + result.exceptionally(throwable -> { + statementFuture.completeExceptionally(throwable); + return null; + }); + + return result; + } + + static DbRows processResultSet( + ExecutorService executorService, + DbMapperManager dbMapperManager, + MapperManager mapperManager, + CompletableFuture queryFuture, + ResultSet resultSet) { + + return new JdbcDbRows<>( + resultSet, + executorService, + dbMapperManager, + mapperManager, + queryFuture, + DbRow.class); + } + + static Map createMetadata(ResultSet rs) throws SQLException { + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + + Map byNumbers = new HashMap<>(); + + for (int i = 1; i <= columnCount; i++) { + String name = metaData.getColumnName(i); + String sqlType = metaData.getColumnTypeName(i); + Class javaClass = classByName(metaData.getColumnClassName(i)); + DbColumn column = new DbColumn() { + @Override + public T as(Class type) { + return null; + } + + @Override + public T as(GenericType type) { + return null; + } + + @Override + public Class javaType() { + return javaClass; + } + + @Override + public String dbType() { + return sqlType; + } + + @Override + public String name() { + return name; + } + }; + byNumbers.put((long) i, column); + } + return byNumbers; + } + + private static Class classByName(String columnClassName) { + if (columnClassName == null) { + return null; + } + try { + return Class.forName(columnClassName); + } catch (ClassNotFoundException e) { + return null; + } + } + + String name() { + return statementName(); + } + + private static final class JdbcDbRows implements DbRows { + private final AtomicBoolean resultRequested = new AtomicBoolean(); + private final ExecutorService executorService; + private final DbMapperManager dbMapperManager; + private final MapperManager mapperManager; + private final CompletableFuture queryFuture; + private final ResultSet resultSet; + private final JdbcDbRows parent; + private final GenericType currentType; + private final Function resultMapper; + + private JdbcDbRows(ResultSet resultSet, + ExecutorService executorService, + DbMapperManager dbMapperManager, + MapperManager mapperManager, + CompletableFuture queryFuture, + Class initialType) { + + this(resultSet, + executorService, + dbMapperManager, + mapperManager, + queryFuture, + GenericType.create(initialType), + Function.identity(), + null); + } + + private JdbcDbRows(ResultSet resultSet, + ExecutorService executorService, + DbMapperManager dbMapperManager, + MapperManager mapperManager, + CompletableFuture queryFuture, + GenericType nextType, + Function resultMapper, + JdbcDbRows parent) { + + this.executorService = executorService; + this.dbMapperManager = dbMapperManager; + this.mapperManager = mapperManager; + this.queryFuture = queryFuture; + this.resultSet = resultSet; + this.currentType = nextType; + this.resultMapper = resultMapper; + this.parent = parent; + } + + @Override + public DbRows map(Function mapper) { + return new JdbcDbRows<>(resultSet, + executorService, + dbMapperManager, + mapperManager, + queryFuture, + null, + mapper, + this); + } + + @Override + public DbRows map(Class type) { + return map(GenericType.create(type)); + } + + @Override + public DbRows map(GenericType type) { + GenericType currentType = this.currentType; + + Function theMapper; + + if (null == currentType) { + theMapper = value -> mapperManager.map(value, + GenericType.create(value.getClass()), + type); + } else if (currentType.equals(DbMapperManager.TYPE_DB_ROW)) { + // maybe we want the same type + if (type.equals(DbMapperManager.TYPE_DB_ROW)) { + return (DbRows) this; + } + // try to find mapper in db mapper manager + theMapper = value -> { + //first try db mapper + try { + return dbMapperManager.read((DbRow) value, type); + } catch (MapperException originalException) { + // not found in db mappers, use generic mappers + try { + return mapperManager.map(value, + DbMapperManager.TYPE_DB_ROW, + type); + } catch (MapperException ignored) { + throw originalException; + } + } + }; + } else { + // one type to another + theMapper = value -> mapperManager.map(value, + currentType, + type); + } + return new JdbcDbRows<>(resultSet, + executorService, + dbMapperManager, + mapperManager, + queryFuture, + type, + theMapper, + this); + } + + @Override + public Flow.Publisher publisher() { + checkResult(); + return toPublisher(); + } + + @Override + public CompletionStage> collect() { + checkResult(); + return toFuture(); + } + + @SuppressWarnings("unchecked") + private Flow.Publisher toPublisher() { + if (null == parent) { + // this is DbRow type + return (Flow.Publisher) new RowPublisher(executorService, + resultSet, + queryFuture, + dbMapperManager, + mapperManager); + } + + Function mappingFunction = (Function) resultMapper; + Multi parentMulti = (Multi) parent.multi(); + + return parentMulti.map(mappingFunction::apply); + } + + private Multi multi() { + return Multi.from(publisher()); + } + + private CompletionStage> toFuture() { + + return Multi.from(toPublisher()) + .collectList() + .toStage(); + } + + private void checkResult() { + if (resultRequested.get()) { + throw new IllegalStateException("Result has already been requested"); + } + resultRequested.set(true); + } + } + + + private static final class RowPublisher implements Flow.Publisher { + private final ExecutorService executorService; + private final ResultSet rs; + private final CompletableFuture queryFuture; + private final DbMapperManager dbMapperManager; + private final MapperManager mapperManager; + + private RowPublisher(ExecutorService executorService, + ResultSet rs, + CompletableFuture queryFuture, + DbMapperManager dbMapperManager, + MapperManager mapperManager) { + + this.executorService = executorService; + this.rs = rs; + this.queryFuture = queryFuture; + this.dbMapperManager = dbMapperManager; + this.mapperManager = mapperManager; + } + + @Override + public void subscribe(Flow.Subscriber subscriber) { + LinkedBlockingQueue requestQueue = new LinkedBlockingQueue<>(); + AtomicBoolean cancelled = new AtomicBoolean(); + + // we have executed the statement, we can correctly subscribe + subscriber.onSubscribe(new Flow.Subscription() { + @Override + public void request(long n) { + // add the requested number to the queue + requestQueue.add(n); + } + + @Override + public void cancel() { + cancelled.set(true); + requestQueue.clear(); + } + }); + + // TODO + // we should only use a thread to read data that was actually requested + // I would prefer to use the same thread to process a single query (to make sure we honor thread locals + // that may be used by the database) + + // and now we can process the data from the database + executorService.submit(() -> { + //now we have a subscriber, we can handle the processing of result set + try (ResultSet rs = this.rs) { + Map metadata = createMetadata(rs); + long count = 0; + + // now we only want to process next record if it was requested + while (!cancelled.get()) { + Long nextElement; + try { + nextElement = requestQueue.poll(10, TimeUnit.MINUTES); + } catch (InterruptedException e) { + LOGGER.finest("Interrupted while polling for requests, terminating DB read"); + subscriber.onError(e); + break; + } + if (nextElement == null) { + LOGGER.finest("No data requested for 10 minutes, terminating DB read"); + subscriber.onError(new TimeoutException("No data requested in 10 minutes")); + break; + } + for (long i = 0; i < nextElement; i++) { + if (rs.next()) { + DbRow dbRow = createDbRow(rs, metadata, dbMapperManager, mapperManager); + subscriber.onNext(dbRow); + count++; + } else { + queryFuture.complete(count); + subscriber.onComplete(); + return; + } + } + } + + if (cancelled.get()) { + queryFuture + .completeExceptionally(new CancellationException("Processing cancelled by subscriber")); + } + } catch (SQLException e) { + queryFuture.completeExceptionally(e); + subscriber.onError(e); + } + }); + } + + private DbRow createDbRow(ResultSet rs, + Map metadata, + DbMapperManager dbMapperManager, + MapperManager mapperManager) throws SQLException { + // read whole row + // for each column + Map byStringsWithValues = new HashMap<>(); + Map byNumbersWithValues = new HashMap<>(); + + for (int i = 1; i <= metadata.size(); i++) { + DbColumn meta = metadata.get((long) i); + Object value = rs.getObject(i); + DbColumn withValue = new DbColumn() { + @Override + public T as(Class type) { + if (null == value) { + return null; + } + if (type.isAssignableFrom(value.getClass())) { + return type.cast(value); + } + return map(value, type); + } + + @SuppressWarnings("unchecked") + T map(SRC value, Class type) { + Class theClass = (Class) value.getClass(); + return mapperManager.map(value, theClass, type); + } + + @SuppressWarnings("unchecked") + T map(SRC value, GenericType type) { + Class theClass = (Class) value.getClass(); + return mapperManager.map(value, GenericType.create(theClass), type); + } + + @Override + public T as(GenericType type) { + if (null == value) { + return null; + } + if (type.isClass()) { + Class theClass = type.rawType(); + if (theClass.isAssignableFrom(value.getClass())) { + return type.cast(value); + } + } + return map(value, type); + } + + @Override + public Class javaType() { + if (null == meta.javaType()) { + if (null == value) { + return null; + } + return value.getClass(); + } else { + return meta.javaType(); + } + } + + @Override + public String dbType() { + return meta.dbType(); + } + + @Override + public String name() { + return meta.name(); + } + }; + byStringsWithValues.put(meta.name(), withValue); + byNumbersWithValues.put(i, withValue); + } + + return new DbRow() { + @Override + public DbColumn column(String name) { + return byStringsWithValues.get(name); + } + + @Override + public DbColumn column(int index) { + return byNumbersWithValues.get(index); + } + + @Override + public void forEach(Consumer columnAction) { + byStringsWithValues.values() + .forEach(columnAction); + } + + @Override + public T as(Class type) { + return dbMapperManager.read(this, type); + } + + @Override + public T as(GenericType type) { + return dbMapperManager.read(this, type); + } + + @Override + public T as(Function mapper) { + return mapper.apply(this); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + boolean first = true; + sb.append('{'); + for (DbColumn col : byStringsWithValues.values()) { + if (first) { + first = false; + } else { + sb.append(','); + } + sb.append(col.name()); + sb.append(':'); + sb.append(col.value().toString()); + } + sb.append('}'); + return sb.toString(); + } + }; + } + } + + static final class ResultWithConn { + private final ResultSet resultSet; + private final Connection connection; + + ResultWithConn(ResultSet resultSet, Connection connection) { + this.resultSet = resultSet; + this.connection = connection; + } + + public ResultSet resultSet() { + return resultSet; + } + + public Connection connection() { + return connection; + } + } + +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/package-info.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/package-info.java new file mode 100644 index 000000000..ca8f8c3d5 --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ +/** + * Helidon DB implementation for JDBC. + */ +package io.helidon.dbclient.jdbc; diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/spi/HikariCpExtensionProvider.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/spi/HikariCpExtensionProvider.java new file mode 100644 index 000000000..6f694b748 --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/spi/HikariCpExtensionProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc.spi; + +import io.helidon.config.Config; +import io.helidon.dbclient.jdbc.HikariCpExtension; + +/** + * Java Service loader interface that provides JDBC DB Client configuration extension. + */ +public interface HikariCpExtensionProvider { + /** + * Configuration key of the extension provider. + * @return configuration key expected under {@code connection.helidon} + */ + String configKey(); + + /** + * Get instance of JDBC DB Client configuration extension. + * + * @param config configuration of this provider to obtain an extension instance + * @return JDBC DB Client configuration extension + */ + HikariCpExtension extension(Config config); +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/spi/package-info.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/spi/package-info.java new file mode 100644 index 000000000..a675cf4d1 --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/spi/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ +/** + * Service provider interface for Helidon DB implementation for JDBC. + * + * The main entry point for JDBC DB Client configuration interceptors implementation + * is {@link io.helidon.dbclient.jdbc.spi.HikariCpExtensionProvider}. + */ +package io.helidon.dbclient.jdbc.spi; diff --git a/dbclient/jdbc/src/main/java/module-info.java b/dbclient/jdbc/src/main/java/module-info.java new file mode 100644 index 000000000..f1c0b73b8 --- /dev/null +++ b/dbclient/jdbc/src/main/java/module-info.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +import io.helidon.dbclient.jdbc.spi.HikariCpExtensionProvider; + +/** + * Helidon Common Mapper. + */ +module io.helidon.dbclient.jdbc { + uses HikariCpExtensionProvider; + requires java.logging; + requires java.sql; + requires com.zaxxer.hikari; + + requires transitive io.helidon.common; + requires transitive io.helidon.common.configurable; + requires transitive io.helidon.dbclient; + requires transitive io.helidon.dbclient.common; + + exports io.helidon.dbclient.jdbc; + exports io.helidon.dbclient.jdbc.spi; + + provides io.helidon.dbclient.spi.DbClientProvider with io.helidon.dbclient.jdbc.JdbcDbClientProvider; +} diff --git a/dbclient/jdbc/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbClientProvider b/dbclient/jdbc/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbClientProvider new file mode 100644 index 000000000..95dedef04 --- /dev/null +++ b/dbclient/jdbc/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbClientProvider @@ -0,0 +1,17 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +io.helidon.dbclient.jdbc.JdbcDbClientProvider diff --git a/dbclient/jdbc/src/test/java/io/helidon/dbclient/jdbc/JdbcStatementParserTest.java b/dbclient/jdbc/src/test/java/io/helidon/dbclient/jdbc/JdbcStatementParserTest.java new file mode 100644 index 000000000..9392d55de --- /dev/null +++ b/dbclient/jdbc/src/test/java/io/helidon/dbclient/jdbc/JdbcStatementParserTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jdbc; + +import java.util.ArrayList; +import java.util.List; + +import io.helidon.dbclient.jdbc.JdbcStatement.Parser; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit test for {@link JdbcStatement.Parser}. + * Tests must cover as large automaton states space as possible. + */ +public class JdbcStatementParserTest { + + /** + * Test simple SQL statement without parameters. + * String parsing shall go trough all local transitions without leaving STMT and STR states. + */ + @Test + void testStatementWithNoParameter() { + String stmtIn = + "SELECT *, 2 FROM table\r\n" + + " WHERE name LIKE 'a?e%'\n"; + Parser parser = new Parser(stmtIn); + String stmtOut = parser.convert(); + List names= parser.namesOrder(); + assertEquals(stmtIn, stmtOut); + assertTrue(names.isEmpty()); + } + + /** + * Test simple SQL statement with parameters. + * Parameters contain both letters and numbers in proper order. + */ + @Test + void testStatementWithParameters() { + String stmtIn = + "SELECT t.*, 'first' FROM table t\r\n" + + " WHERE name = :n4m3\n" + + " AND age > :ag3"; + String stmtExp = + "SELECT t.*, 'first' FROM table t\r\n" + + " WHERE name = ?\n" + + " AND age > ?"; + List namesExp = new ArrayList<>(2); + namesExp.add("n4m3"); + namesExp.add("ag3"); + Parser parser = new Parser(stmtIn); + String stmtOut = parser.convert(); + List names= parser.namesOrder(); + assertEquals(stmtExp, stmtOut); + assertEquals(namesExp, names); + } + + /** + * Test simple SQL statement with parameters inside multi-line comment. + * Only parameters outside comments shall be returned. + */ + @Test + void testStatementWithParametersInMultiLineCommnet() { + String stmtIn = + "SELECT t.*, 'first' FROM table t /* Parameter for name is :n4me\r\n" + + " and for age is :ag3 */\n" + + " WHERE address IS NULL\r\n" + + " AND name = :n4m3\n" + + " AND age > :ag3"; + String stmtExp = + "SELECT t.*, 'first' FROM table t /* Parameter for name is :n4me\r\n" + + " and for age is :ag3 */\n" + + " WHERE address IS NULL\r\n" + + " AND name = ?\n" + + " AND age > ?"; + List namesExp = new ArrayList<>(2); + namesExp.add("n4m3"); + namesExp.add("ag3"); + Parser parser = new Parser(stmtIn); + String stmtOut = parser.convert(); + List names= parser.namesOrder(); + assertEquals(stmtExp, stmtOut); + assertEquals(namesExp, names); + } + + /** + * Test simple SQL statement with parameters inside multi-line comment. + * Only parameters outside comments shall be returned. + */ + @Test + void testStatementWithParametersInSingleLineCommnet() { + String stmtIn = + "SELECT t.*, 'first' FROM table t -- Parameter for name is :n4me\r\r\n" + + " WHERE address IS NULL\r\n" + + " AND name = :myN4m3\n" + + " AND age > :ag3"; + String stmtExp = + "SELECT t.*, 'first' FROM table t -- Parameter for name is :n4me\r\r\n" + + " WHERE address IS NULL\r\n" + + " AND name = ?\n" + + " AND age > ?"; + List namesExp = new ArrayList<>(2); + namesExp.add("myN4m3"); + namesExp.add("ag3"); + Parser parser = new Parser(stmtIn); + String stmtOut = parser.convert(); + List names= parser.namesOrder(); + assertEquals(stmtExp, stmtOut); + assertEquals(namesExp, names); + } + + /** + * Test simple SQL statement with valid and invalid name parameters. + * Only parameters outside comments shall be returned. + */ + @Test + void testStatementWithValidandinvalidParameters() { + String stmtIn = + "SELECT p.firstName, p.secondName, a.street, a.towm" + + " FROM person p" + + " INNER JOIN address a ON a.id = p.aid" + + " WHERE p.age > :12age" + + " AND a.zip = :zip"; + String stmtExp = + "SELECT p.firstName, p.secondName, a.street, a.towm" + + " FROM person p" + + " INNER JOIN address a ON a.id = p.aid" + + " WHERE p.age > :12age" + + " AND a.zip = ?"; + List namesExp = new ArrayList<>(2); + namesExp.add("zip"); + Parser parser = new Parser(stmtIn); + String stmtOut = parser.convert(); + List names= parser.namesOrder(); + assertEquals(stmtExp, stmtOut); + assertEquals(namesExp, names); + } + +} diff --git a/dbclient/jsonp/pom.xml b/dbclient/jsonp/pom.xml new file mode 100644 index 000000000..543f93a71 --- /dev/null +++ b/dbclient/jsonp/pom.xml @@ -0,0 +1,45 @@ + + + + + + helidon-dbclient-project + io.helidon.dbclient + 2.0-SNAPSHOT + + 4.0.0 + + helidon-dbclient-jsonp + Helidon DB Client JSON-Processing + + + Support for JSON-Processing as parameters and responses in Helidon DB + + + + + io.helidon.dbclient + helidon-dbclient + + + org.glassfish + javax.json + + + diff --git a/dbclient/jsonp/src/main/java/io/helidon/dbclient/jsonp/JsonProcessingMapper.java b/dbclient/jsonp/src/main/java/io/helidon/dbclient/jsonp/JsonProcessingMapper.java new file mode 100644 index 000000000..982b7b53e --- /dev/null +++ b/dbclient/jsonp/src/main/java/io/helidon/dbclient/jsonp/JsonProcessingMapper.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jsonp; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.json.Json; +import javax.json.JsonBuilderFactory; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import javax.json.JsonValue; + +import io.helidon.dbclient.DbMapper; +import io.helidon.dbclient.DbRow; + +/** + * Json processing mapper. + */ +public final class JsonProcessingMapper implements DbMapper { + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + private static final Map, DbJsonWriter> JSON_WRITERS = new IdentityHashMap<>(); + private static final DbJsonWriter NUMBER_WRITER = (builder, name, value) -> builder.add(name, ((Number) value).longValue()); + private static final DbJsonWriter OBJECT_WRITER = (builder, name, value) -> builder.add(name, String.valueOf(value)); + + static { + JSON_WRITERS.put(Integer.class, (builder, name, value) -> builder.add(name, (Integer) value)); + JSON_WRITERS.put(Short.class, (builder, name, value) -> builder.add(name, (Short) value)); + JSON_WRITERS.put(Byte.class, (builder, name, value) -> builder.add(name, (Byte) value)); + JSON_WRITERS.put(AtomicInteger.class, (builder, name, value) -> builder.add(name, ((AtomicInteger) value).get())); + JSON_WRITERS.put(Float.class, (builder, name, value) -> builder.add(name, (Float) value)); + JSON_WRITERS.put(Double.class, (builder, name, value) -> builder.add(name, (Double) value)); + JSON_WRITERS.put(BigInteger.class, (builder, name, value) -> builder.add(name, (BigInteger) value)); + JSON_WRITERS.put(BigDecimal.class, (builder, name, value) -> builder.add(name, (BigDecimal) value)); + JSON_WRITERS.put(Long.class, (builder, name, value) -> builder.add(name, (Long) value)); + JSON_WRITERS.put(String.class, (builder, name, value) -> builder.add(name, (String) value)); + JSON_WRITERS.put(Boolean.class, (builder, name, value) -> builder.add(name, (Boolean) value)); + // primitives + JSON_WRITERS.put(int.class, JSON_WRITERS.get(Integer.class)); + JSON_WRITERS.put(short.class, JSON_WRITERS.get(Short.class)); + JSON_WRITERS.put(byte.class, JSON_WRITERS.get(Byte.class)); + JSON_WRITERS.put(float.class, JSON_WRITERS.get(Float.class)); + JSON_WRITERS.put(double.class, JSON_WRITERS.get(Double.class)); + JSON_WRITERS.put(long.class, JSON_WRITERS.get(Long.class)); + JSON_WRITERS.put(boolean.class, JSON_WRITERS.get(Boolean.class)); + } + + private JsonProcessingMapper() { + } + + /** + * Create a new mapper that can map {@link javax.json.JsonObject} to DB parameters and {@link io.helidon.dbclient.DbRow} + * to a {@link javax.json.JsonObject}. + * + * @return a new mapper + */ + public static JsonProcessingMapper create() { + return new JsonProcessingMapper(); + } + + /** + * Get a JSON-P representation of this row. + * + * @return json object containing column name to column value. + */ + @Override + public JsonObject read(DbRow row) { + JsonObjectBuilder objectBuilder = JSON.createObjectBuilder(); + row.forEach(dbCol -> toJson(objectBuilder, dbCol.name(), dbCol.javaType(), dbCol.value())); + return objectBuilder.build(); + } + + @Override + public Map toNamedParameters(JsonObject value) { + Map result = new HashMap<>(); + value.forEach((name, json) -> result.put(name, toObject(name, json, value))); + + return result; + } + + @Override + public List toIndexedParameters(JsonObject value) { + // in case the underlying map is linked, we can do this + // obviously the number of parameters must match the number in statement, so most likely this is + // going to fail + List result = new LinkedList<>(); + value.forEach((name, json) -> result.add(toObject(name, json, value))); + return result; + } + + private void toJson(JsonObjectBuilder objectBuilder, String name, Class valueClass, Object value) { + if (value == null) { + objectBuilder.addNull(name); + } + getJsonWriter(valueClass).write(objectBuilder, name, value); + } + + private DbJsonWriter getJsonWriter(Class valueClass) { + DbJsonWriter writer = JSON_WRITERS.get(valueClass); + if (null != writer) { + return writer; + } + if (Number.class.isAssignableFrom(valueClass)) { + return NUMBER_WRITER; + } + return OBJECT_WRITER; + } + + private Object toObject(String name, JsonValue json, JsonObject jsonObject) { + if (json == null) { + return null; + } + switch (json.getValueType()) { + case STRING: + return jsonObject.getString(name); + case NUMBER: + return jsonObject.getJsonNumber(name).numberValue(); + case TRUE: + return Boolean.TRUE; + case FALSE: + return Boolean.FALSE; + case NULL: + return null; + case OBJECT: + return jsonObject.getJsonObject(name); + case ARRAY: + return jsonObject.getJsonArray(name); + default: + throw new IllegalStateException(String.format("Unknown JSON value type: %s", json.getValueType())); + } + } + + @FunctionalInterface + private interface DbJsonWriter { + void write(JsonObjectBuilder objectBuilder, String name, Object value); + } +} diff --git a/dbclient/jsonp/src/main/java/io/helidon/dbclient/jsonp/JsonProcessingMapperProvider.java b/dbclient/jsonp/src/main/java/io/helidon/dbclient/jsonp/JsonProcessingMapperProvider.java new file mode 100644 index 000000000..cff727e4a --- /dev/null +++ b/dbclient/jsonp/src/main/java/io/helidon/dbclient/jsonp/JsonProcessingMapperProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.jsonp; + +import java.util.Optional; + +import javax.annotation.Priority; +import javax.json.JsonObject; + +import io.helidon.common.Prioritized; +import io.helidon.dbclient.DbMapper; +import io.helidon.dbclient.spi.DbMapperProvider; + +/** + * JSON-P mapper provider. + */ +@Priority(Prioritized.DEFAULT_PRIORITY) +public class JsonProcessingMapperProvider implements DbMapperProvider { + @SuppressWarnings("unchecked") + @Override + public Optional> mapper(Class type) { + if (type.equals(JsonObject.class)) { + return Optional.of((DbMapper) JsonProcessingMapper.create()); + } + return Optional.empty(); + } +} diff --git a/dbclient/jsonp/src/main/java/io/helidon/dbclient/jsonp/package-info.java b/dbclient/jsonp/src/main/java/io/helidon/dbclient/jsonp/package-info.java new file mode 100644 index 000000000..1f48acd29 --- /dev/null +++ b/dbclient/jsonp/src/main/java/io/helidon/dbclient/jsonp/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ +/** + * JSON Processing support for Helidon DB. + */ +package io.helidon.dbclient.jsonp; diff --git a/dbclient/jsonp/src/main/java/module-info.java b/dbclient/jsonp/src/main/java/module-info.java new file mode 100644 index 000000000..3306614c3 --- /dev/null +++ b/dbclient/jsonp/src/main/java/module-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Helidon DB JSON-P Mapper. + */ +module io.helidon.dbclient.jsonp { + requires java.logging; + requires io.helidon.dbclient; + requires java.json; + + exports io.helidon.dbclient.jsonp; + + provides io.helidon.dbclient.spi.DbMapperProvider with io.helidon.dbclient.jsonp.JsonProcessingMapperProvider; +} + diff --git a/dbclient/jsonp/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbMapperProvider b/dbclient/jsonp/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbMapperProvider new file mode 100644 index 000000000..34f813b56 --- /dev/null +++ b/dbclient/jsonp/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbMapperProvider @@ -0,0 +1,17 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +io.helidon.dbclient.jsonp.JsonProcessingMapperProvider diff --git a/dbclient/metrics-jdbc/pom.xml b/dbclient/metrics-jdbc/pom.xml new file mode 100644 index 000000000..6319ca968 --- /dev/null +++ b/dbclient/metrics-jdbc/pom.xml @@ -0,0 +1,55 @@ + + + + + 4.0.0 + + io.helidon.dbclient + helidon-dbclient-project + 2.0-SNAPSHOT + + + helidon-dbclient-metrics-jdbc + Helidon DB Client JDBC Metrics + + Metrics support for Helidon DB implementation for JDBC + + + + io.helidon.dbclient + helidon-dbclient-jdbc + + + io.helidon.dbclient + helidon-dbclient-metrics + + + io.helidon.metrics + helidon-metrics + + + com.zaxxer + HikariCP + + + io.dropwizard.metrics + metrics-core + + + diff --git a/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/DropwizardMetricsListener.java b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/DropwizardMetricsListener.java new file mode 100644 index 000000000..fef5e75c4 --- /dev/null +++ b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/DropwizardMetricsListener.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics.jdbc; + +import java.util.logging.Logger; + +import io.helidon.config.Config; +import io.helidon.metrics.RegistryFactory; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Histogram; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistryListener; +import com.codahale.metrics.Timer; +import org.eclipse.microprofile.metrics.MetricRegistry; + +/** + * Hikari CP to Helidon metrics mapper. + * + * Listeners for events from the metrics registry and (un)registers metrics instances in Helidon. + */ +public class DropwizardMetricsListener implements MetricRegistryListener { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(DropwizardMetricsListener.class.getName()); + + private final String prefix; + // Helidon metrics registry + private final MetricRegistry registry; + + private DropwizardMetricsListener(String prefix) { + this.prefix = prefix; + this.registry = RegistryFactory.getInstance().getRegistry(MetricRegistry.Type.VENDOR); + } + + static MetricRegistryListener create(Config config) { + return new DropwizardMetricsListener(config.get("name-prefix").asString().orElse("db.pool.")); + } + + @Override + public void onGaugeAdded(String name, Gauge gauge) { + LOGGER.finest(() -> String.format("Gauge added: %s", name)); + registry.register(prefix + name, new JdbcMetricsGauge<>(gauge)); + } + + @Override + public void onGaugeRemoved(String name) { + LOGGER.finest(() -> String.format("Gauge removed: %s", name)); + registry.remove(prefix + name); + } + + @Override + public void onCounterAdded(String name, Counter counter) { + LOGGER.finest(() -> String.format("Counter added: %s", name)); + registry.register(prefix + name, new JdbcMetricsCounter(counter)); + } + + @Override + public void onCounterRemoved(String name) { + LOGGER.finest(() -> String.format("Counter removed: %s", name)); + registry.remove(prefix + name); + } + + @Override + public void onHistogramAdded(String name, Histogram histogram) { + LOGGER.finest(() -> String.format("Histogram added: %s", name)); + registry.register(prefix + name, new JdbcMetricsHistogram(histogram)); + } + + @Override + public void onHistogramRemoved(String name) { + LOGGER.finest(() -> String.format("Histogram removed: %s", name)); + registry.remove(prefix + name); + } + + @Override + public void onMeterAdded(String name, Meter meter) { + LOGGER.finest(() -> String.format("Meter added: %s", name)); + registry.register(prefix + name, new JdbcMetricsMeter(meter)); + } + + @Override + public void onMeterRemoved(String name) { + LOGGER.finest(() -> String.format("Meter removed: %s", name)); + registry.remove(prefix + name); + } + + @Override + public void onTimerAdded(String name, Timer timer) { + LOGGER.finest(() -> String.format("Timer added: %s", name)); + registry.register(prefix + name, new JdbcMetricsTimer(timer)); + } + + @Override + public void onTimerRemoved(String name) { + LOGGER.finest(() -> String.format("Timer removed: %s", name)); + registry.remove(prefix + name); + } + +} diff --git a/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/HikariMetricsExtension.java b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/HikariMetricsExtension.java new file mode 100644 index 000000000..1c5b41f99 --- /dev/null +++ b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/HikariMetricsExtension.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics.jdbc; + +import io.helidon.config.Config; +import io.helidon.dbclient.jdbc.HikariCpExtension; + +import com.codahale.metrics.MetricRegistry; +import com.zaxxer.hikari.HikariConfig; + +/** + * JDBC Configuration Interceptor for Metrics. + * + * Registers JDBC connection pool metrics to {@code HikariConnectionPool}. + */ +final class HikariMetricsExtension implements HikariCpExtension { + private final Config config; + private final boolean enabled; + + private HikariMetricsExtension(Config config, boolean enabled) { + this.config = config; + this.enabled = enabled; + } + + static HikariMetricsExtension create(Config config) { + return new HikariMetricsExtension(config, config.get("enabled").asBoolean().orElse(true)); + } + + /** + * Register {@code MetricRegistry} instance with listener into Hikari CP configuration. + * + * @param poolConfig Hikari CP configuration + */ + @Override + public void configure(HikariConfig poolConfig) { + if (enabled) { + final MetricRegistry metricRegistry = new MetricRegistry(); + metricRegistry.addListener(DropwizardMetricsListener.create(config)); + poolConfig.setMetricRegistry(metricRegistry); + } + } +} diff --git a/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsCounter.java b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsCounter.java new file mode 100644 index 000000000..79d26ee2d --- /dev/null +++ b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsCounter.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics.jdbc; + +import org.eclipse.microprofile.metrics.Counter; + +/** + * {@link Counter} metric wrapper for Hikari CP metric. + */ +public class JdbcMetricsCounter implements Counter { + + private final com.codahale.metrics.Counter counter; + + JdbcMetricsCounter(final com.codahale.metrics.Counter counter) { + this.counter = counter; + } + + @Override + public void inc() { + counter.inc(); + } + + @Override + public void inc(long n) { + counter.inc(n); + } + + @Override + public long getCount() { + return counter.getCount(); + } + +} diff --git a/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsExtensionProvider.java b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsExtensionProvider.java new file mode 100644 index 000000000..73643a90a --- /dev/null +++ b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsExtensionProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics.jdbc; + +import io.helidon.config.Config; +import io.helidon.dbclient.jdbc.HikariCpExtension; +import io.helidon.dbclient.jdbc.spi.HikariCpExtensionProvider; + +/** + * JDBC Configuration Interceptor Provider for Metrics. + * + * Returns JDBC Configuration Interceptor instance on request. + */ +public class JdbcMetricsExtensionProvider implements HikariCpExtensionProvider { + @Override + public String configKey() { + return "pool-metrics"; + } + + @Override + public HikariCpExtension extension(Config config) { + return HikariMetricsExtension.create(config); + } +} diff --git a/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsGauge.java b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsGauge.java new file mode 100644 index 000000000..ae3ee846f --- /dev/null +++ b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsGauge.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics.jdbc; + +import org.eclipse.microprofile.metrics.Gauge; + +/** + * {@link Gauge} metric wrapper for Hikari CP metric. + * + * @param the type of the metric's value + */ +public class JdbcMetricsGauge implements Gauge { + + private final com.codahale.metrics.Gauge gauge; + + JdbcMetricsGauge(final com.codahale.metrics.Gauge counter) { + this.gauge = counter; + } + + @Override + public T getValue() { + return gauge.getValue(); + } + +} diff --git a/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsHistogram.java b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsHistogram.java new file mode 100644 index 000000000..dd428462e --- /dev/null +++ b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsHistogram.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics.jdbc; + +import org.eclipse.microprofile.metrics.Histogram; +import org.eclipse.microprofile.metrics.Snapshot; + +/** + * {@link Histogram} metric wrapper for Hikari CP metric. + */ +public class JdbcMetricsHistogram implements Histogram { + + private final com.codahale.metrics.Histogram histogram; + + JdbcMetricsHistogram(final com.codahale.metrics.Histogram histogram) { + this.histogram = histogram; + } + + @Override + public void update(int value) { + histogram.update(value); + } + + @Override + public void update(long value) { + histogram.update(value); + } + + @Override + public long getCount() { + return histogram.getCount(); + } + + @Override + public Snapshot getSnapshot() { + return new JdbcMetricsSnapshot(histogram.getSnapshot()); + } + +} diff --git a/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsMeter.java b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsMeter.java new file mode 100644 index 000000000..358cb1969 --- /dev/null +++ b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsMeter.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics.jdbc; + +import org.eclipse.microprofile.metrics.Meter; + +/** + * {@link Meter} metric wrapper for Hikari CP metric. + */ +public class JdbcMetricsMeter implements Meter { + + private final com.codahale.metrics.Meter meter; + + JdbcMetricsMeter(final com.codahale.metrics.Meter meter) { + this.meter = meter; + } + + @Override + public void mark() { + meter.mark(); + } + + @Override + public void mark(long n) { + meter.mark(n); + } + + @Override + public long getCount() { + return meter.getCount(); + } + + @Override + public double getFifteenMinuteRate() { + return meter.getFifteenMinuteRate(); + } + + @Override + public double getFiveMinuteRate() { + return meter.getFiveMinuteRate(); + } + + @Override + public double getMeanRate() { + return meter.getMeanRate(); + } + + @Override + public double getOneMinuteRate() { + return meter.getOneMinuteRate(); + } + +} diff --git a/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsSnapshot.java b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsSnapshot.java new file mode 100644 index 000000000..48c1476f5 --- /dev/null +++ b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsSnapshot.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics.jdbc; + +import java.io.OutputStream; + +import org.eclipse.microprofile.metrics.Snapshot; + +/** + * Metric {@link Snapshot} wrapper for Hikari CP metric. + */ +public class JdbcMetricsSnapshot extends Snapshot { + + private final com.codahale.metrics.Snapshot snapshot; + + JdbcMetricsSnapshot(final com.codahale.metrics.Snapshot snapshot) { + this.snapshot = snapshot; + } + + @Override + public double getValue(double quantile) { + return snapshot.getValue(quantile); + } + + @Override + public long[] getValues() { + return snapshot.getValues(); + } + + @Override + public int size() { + return snapshot.size(); + } + + @Override + public long getMax() { + return snapshot.getMax(); + } + + @Override + public double getMean() { + return snapshot.getMean(); + } + + @Override + public long getMin() { + return snapshot.getMin(); + } + + @Override + public double getStdDev() { + return snapshot.getStdDev(); + } + + @Override + public void dump(OutputStream output) { + snapshot.dump(output); + } + +} diff --git a/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsTimer.java b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsTimer.java new file mode 100644 index 000000000..1e6971959 --- /dev/null +++ b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsTimer.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics.jdbc; + +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import org.eclipse.microprofile.metrics.Snapshot; +import org.eclipse.microprofile.metrics.Timer; + +/** + * {@link Timer} metric wrapper for Hikari CP metric. + */ +public class JdbcMetricsTimer implements Timer { + + private final com.codahale.metrics.Timer meter; + + JdbcMetricsTimer(final com.codahale.metrics.Timer meter) { + this.meter = meter; + } + + @Override + public void update(long duration, TimeUnit unit) { + meter.update(duration, unit); + } + + @Override + public T time(Callable event) throws Exception { + return meter.time(event); + } + + @Override + public void time(Runnable event) { + meter.time(event); + } + + @Override + public Context time() { + return new JdbcMetricsTimerContext(meter.time()); + } + + @Override + public long getCount() { + return meter.getCount(); + } + + @Override + public double getFifteenMinuteRate() { + return meter.getFifteenMinuteRate(); + } + + @Override + public double getFiveMinuteRate() { + return meter.getFiveMinuteRate(); + } + + @Override + public double getMeanRate() { + return meter.getMeanRate(); + } + + @Override + public double getOneMinuteRate() { + return meter.getOneMinuteRate(); + } + + @Override + public Snapshot getSnapshot() { + return new JdbcMetricsSnapshot(meter.getSnapshot()); + } + +} diff --git a/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsTimerContext.java b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsTimerContext.java new file mode 100644 index 000000000..7b0931731 --- /dev/null +++ b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/JdbcMetricsTimerContext.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics.jdbc; + +import org.eclipse.microprofile.metrics.Timer; + +/** + * Metric {@link Timer.Context} wrapper for Hikari CP metric. + */ +public class JdbcMetricsTimerContext implements Timer.Context { + + private final com.codahale.metrics.Timer.Context context; + + JdbcMetricsTimerContext(final com.codahale.metrics.Timer.Context context) { + this.context = context; + } + + @Override + public long stop() { + return context.stop(); + } + + @Override + public void close() { + context.close(); + } + +} diff --git a/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/package-info.java b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/package-info.java new file mode 100644 index 000000000..35a4c3b92 --- /dev/null +++ b/dbclient/metrics-jdbc/src/main/java/io/helidon/dbclient/metrics/jdbc/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ +/** + * Metrics support for Helidon DB JDBC Client. + */ +package io.helidon.dbclient.metrics.jdbc; diff --git a/dbclient/metrics-jdbc/src/main/java/module-info.java b/dbclient/metrics-jdbc/src/main/java/module-info.java new file mode 100644 index 000000000..19dea58ce --- /dev/null +++ b/dbclient/metrics-jdbc/src/main/java/module-info.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +import io.helidon.dbclient.jdbc.spi.HikariCpExtensionProvider; + +/** + * Helidon JDBC DB Client Metrics. + */ +module io.helidon.dbclient.metrics.jdbc { + requires java.logging; + requires io.helidon.dbclient; + requires io.helidon.dbclient.jdbc; + requires io.helidon.metrics; + requires io.helidon.dbclient.metrics; + requires com.zaxxer.hikari; + requires com.codahale.metrics; + + exports io.helidon.dbclient.metrics.jdbc; + + provides HikariCpExtensionProvider with io.helidon.dbclient.metrics.jdbc.JdbcMetricsExtensionProvider; + +} diff --git a/dbclient/metrics-jdbc/src/main/resources/META-INF/services/io.helidon.dbclient.jdbc.spi.JdbcClientExtensionProvider b/dbclient/metrics-jdbc/src/main/resources/META-INF/services/io.helidon.dbclient.jdbc.spi.JdbcClientExtensionProvider new file mode 100644 index 000000000..4b8f83abe --- /dev/null +++ b/dbclient/metrics-jdbc/src/main/resources/META-INF/services/io.helidon.dbclient.jdbc.spi.JdbcClientExtensionProvider @@ -0,0 +1,17 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +io.helidon.dbclient.metrics.jdbc.JdbcMetricsExtensionProvider diff --git a/dbclient/metrics/pom.xml b/dbclient/metrics/pom.xml new file mode 100644 index 000000000..997396b49 --- /dev/null +++ b/dbclient/metrics/pom.xml @@ -0,0 +1,43 @@ + + + + + 4.0.0 + + io.helidon.dbclient + helidon-dbclient-project + 2.0-SNAPSHOT + + + helidon-dbclient-metrics + Helidon DB Client Metrics + + Metrics support for Helidon DB + + + + io.helidon.dbclient + helidon-dbclient + + + io.helidon.metrics + helidon-metrics + + + diff --git a/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbCounter.java b/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbCounter.java new file mode 100644 index 000000000..3d0edea22 --- /dev/null +++ b/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbCounter.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics; + +import java.util.concurrent.CompletionStage; + +import io.helidon.config.Config; + +import org.eclipse.microprofile.metrics.Counter; +import org.eclipse.microprofile.metrics.Metadata; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.MetricType; + +/** + * Counter metric for Helidon DB. This class implements the {@link io.helidon.dbclient.DbInterceptor} and + * can be configured either through a {@link io.helidon.dbclient.DbClient.Builder} or through configuration. + */ +public final class DbCounter extends DbMetric { + private DbCounter(Builder builder) { + super(builder); + } + + /** + * Create a counter from configuration. + * + * @param config configuration to read + * @return a new counter + * @see io.helidon.dbclient.metrics.DbMetricBuilder#config(io.helidon.config.Config) + */ + public static DbCounter create(Config config) { + return builder().config(config).build(); + } + + /** + * Create a new counter using default configuration. + *

By default the name format is {@code db.counter.statement-name}, where {@code statement-name} + * is provided at runtime. + * + * @return a new counter + */ + public static DbCounter create() { + return builder().build(); + } + + /** + * Create a new fluent API builder to create a new counter metric. + * @return a new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + protected void executeMetric(Counter metric, CompletionStage aFuture) { + aFuture + .thenAccept(nothing -> { + if (measureSuccess()) { + metric.inc(); + } + }) + .exceptionally(throwable -> { + if (measureErrors()) { + metric.inc(); + } + return null; + }); + } + + @Override + protected MetricType metricType() { + return MetricType.COUNTER; + } + + @Override + protected Counter metric(MetricRegistry registry, Metadata meta) { + return registry.counter(meta); + } + + @Override + protected String defaultNamePrefix() { + return "db.counter."; + } + + /** + * Fluent API builder for {@link io.helidon.dbclient.metrics.DbCounter}. + */ + public static class Builder extends DbMetricBuilder implements io.helidon.common.Builder { + @Override + public DbCounter build() { + return new DbCounter(this); + } + } +} diff --git a/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbMeter.java b/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbMeter.java new file mode 100644 index 000000000..f0cb8096f --- /dev/null +++ b/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbMeter.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics; + +import java.util.concurrent.CompletionStage; + +import io.helidon.config.Config; + +import org.eclipse.microprofile.metrics.Metadata; +import org.eclipse.microprofile.metrics.Meter; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.MetricType; + +/** + * Meter for Helidon DB. This class implements the {@link io.helidon.dbclient.DbInterceptor} and + * can be configured either through a {@link io.helidon.dbclient.DbClient.Builder} or through configuration. + */ +public final class DbMeter extends DbMetric { + private DbMeter(Builder builder) { + super(builder); + } + + /** + * Create a meter from configuration. + * + * @param config configuration to read + * @return a new meter + * @see io.helidon.dbclient.metrics.DbMetricBuilder#config(io.helidon.config.Config) + */ + public static DbMeter create(Config config) { + return builder().config(config).build(); + } + + /** + * Create a new meter using default configuration. + *

By default the name format is {@code db.meter.statement-name}, where {@code statement-name} + * is provided at runtime. + * + * @return a new meter + */ + public static DbMeter create() { + return builder().build(); + } + + /** + * Create a new fluent API builder to create a new meter metric. + * @return a new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + protected void executeMetric(Meter metric, CompletionStage aFuture) { + aFuture + .thenAccept(nothing -> { + if (measureSuccess()) { + metric.mark(); + } + }) + .exceptionally(throwable -> { + if (measureErrors()) { + metric.mark(); + } + return null; + }); + } + + @Override + protected MetricType metricType() { + return MetricType.COUNTER; + } + + @Override + protected Meter metric(MetricRegistry registry, Metadata meta) { + return registry.meter(meta); + } + + @Override + protected String defaultNamePrefix() { + return "db.meter."; + } + + /** + * Fluent API builder for {@link io.helidon.dbclient.metrics.DbMeter}. + */ + public static class Builder extends DbMetricBuilder implements io.helidon.common.Builder { + @Override + public DbMeter build() { + return new DbMeter(this); + } + } +} diff --git a/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbMetric.java b/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbMetric.java new file mode 100644 index 000000000..dc65bce30 --- /dev/null +++ b/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbMetric.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; + +import io.helidon.dbclient.DbInterceptor; +import io.helidon.dbclient.DbInterceptorContext; +import io.helidon.dbclient.DbStatementType; +import io.helidon.metrics.RegistryFactory; + +import org.eclipse.microprofile.metrics.Metadata; +import org.eclipse.microprofile.metrics.MetadataBuilder; +import org.eclipse.microprofile.metrics.Metric; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.MetricType; + +/** + * Common ancestor for Helidon DB metrics. + */ +abstract class DbMetric implements DbInterceptor { + private final Metadata meta; + private final String description; + private final BiFunction nameFunction; + private final MetricRegistry registry; + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + private final boolean measureErrors; + private final boolean measureSuccess; + + protected DbMetric(DbMetricBuilder builder) { + BiFunction namedFunction = builder.nameFormat(); + this.meta = builder.meta(); + + if (null == namedFunction) { + namedFunction = (name, statement) -> defaultNamePrefix() + name; + } + this.nameFunction = namedFunction; + this.registry = RegistryFactory.getInstance().getRegistry(MetricRegistry.Type.APPLICATION); + this.measureErrors = builder.measureErrors(); + this.measureSuccess = builder.measureSuccess(); + String tmpDescription; + if (builder.description() == null) { + tmpDescription = ((null == meta) ? null : meta.getDescription().orElse(null)); + } else { + tmpDescription = builder.description(); + } + this.description = tmpDescription; + } + + protected abstract String defaultNamePrefix(); + + @Override + public CompletableFuture statement(DbInterceptorContext interceptorContext) { + DbStatementType dbStatementType = interceptorContext.statementType(); + String statementName = interceptorContext.statementName(); + + T metric = cache.computeIfAbsent(statementName, s -> { + String name = nameFunction.apply(statementName, dbStatementType); + MetadataBuilder builder = (meta == null) + ? Metadata.builder().withName(name).withType(metricType()) + : Metadata.builder(meta); + if (description != null) { + builder = builder.withDescription(description); + } + return metric(registry, builder.build()); + }); + + executeMetric(metric, interceptorContext.statementFuture()); + + return CompletableFuture.completedFuture(interceptorContext); + } + + protected boolean measureErrors() { + return measureErrors; + } + + protected boolean measureSuccess() { + return measureSuccess; + } + + protected abstract void executeMetric(T metric, CompletionStage aFuture); + protected abstract MetricType metricType(); + protected abstract T metric(MetricRegistry registry, Metadata meta); +} diff --git a/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbMetricBuilder.java b/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbMetricBuilder.java new file mode 100644 index 000000000..7a8142fb1 --- /dev/null +++ b/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbMetricBuilder.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics; + +import java.util.function.BiFunction; + +import io.helidon.common.HelidonFeatures; +import io.helidon.common.HelidonFlavor; +import io.helidon.config.Config; +import io.helidon.dbclient.DbStatementType; + +import org.eclipse.microprofile.metrics.Metadata; + +/** + * A metric builder used as a base for Helidon DB metrics. + * @param type of the builder extending this class + */ +public abstract class DbMetricBuilder> { + static { + HelidonFeatures.register(HelidonFlavor.SE, "DbClient", "Metrics"); + } + + private Metadata meta; + private BiFunction nameFormat; + private boolean measureErrors = true; + private boolean measureSuccess = true; + private String description; + + /** + * Configure a metric name. + * + * @param metricName name of the metric + * @return updated builder instance + */ + public T name(String metricName) { + nameFormat = (s, s2) -> metricName; + return me(); + } + + /** + * Configure metric metadata. + * + * @param meta metric metadata + * @return updated builder instance + */ + public T metadata(Metadata meta) { + this.meta = meta; + return me(); + } + + /** + * Configure a name format. + *

The format can use up to two parameters - first is the statement name, second the {@link io.helidon.dbclient.DbStatementType} + * as a string. + * + * @param format format string expecting zero to two parameters that can be processed by + * {@link String#format(String, Object...)} + * @return updated builder instance + */ + public T nameFormat(String format) { + return nameFormat((name, queryType) -> String.format(format, name, queryType.toString())); + } + + /** + * Configure a name format function. + *

The first parameter is the statement name. + * + * @param function function to use to create metric name + * @return updated builder instance + */ + public T nameFormat(BiFunction function) { + this.nameFormat = function; + return me(); + } + + /** + * Whether to measure failed statements. + * + * @param shouldWe set to {@code false} if errors should be ignored + * @return updated builder instance + */ + public T measureErrors(boolean shouldWe) { + this.measureErrors = shouldWe; + return me(); + } + + /** + * Whether to measure successful statements. + * + * @param shouldWe set to {@code false} if successes should be ignored + * @return updated builder instance + */ + public T measureSuccess(boolean shouldWe) { + this.measureSuccess = shouldWe; + return me(); + } + + /** + * Description of the metric used in metric metadata. + * + * @param description description + * @return updated builder instance + */ + public T description(String description) { + this.description = description; + return me(); + } + + /** + * Configure a metric from configuration. + * The following configuration key are used: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
DB Metric configuration options
keydefaultdescription
errors{@code true}Whether this metric triggers for error states
success{@code true}Whether this metric triggers for successful executions
name-format{@code db.metric-type.statement-name}A string format used to construct a metric name. The format gets two parameters: the statement name and the + * statement type
description Description of this metric, used in metric {@link org.eclipse.microprofile.metrics.Metadata}
+ * + * @param config configuration to configure this metric + * @return updated builder instance + */ + public T config(Config config) { + config.get("errors").asBoolean().ifPresent(this::measureErrors); + config.get("success").asBoolean().ifPresent(this::measureSuccess); + config.get("name-format").asString().ifPresent(this::nameFormat); + config.get("description").asString().ifPresent(this::description); + return me(); + } + + String description() { + return description; + } + + @SuppressWarnings("unchecked") + T me() { + return (T) this; + } + + Metadata meta() { + return meta; + } + + BiFunction nameFormat() { + return nameFormat; + } + + boolean measureErrors() { + return measureErrors; + } + + boolean measureSuccess() { + return measureSuccess; + } +} diff --git a/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbMetricsProvider.java b/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbMetricsProvider.java new file mode 100644 index 000000000..141d61960 --- /dev/null +++ b/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbMetricsProvider.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics; + +import io.helidon.config.Config; +import io.helidon.dbclient.DbClientException; +import io.helidon.dbclient.DbInterceptor; +import io.helidon.dbclient.spi.DbInterceptorProvider; + +/** + * Service for DB metrics. + */ +public class DbMetricsProvider implements DbInterceptorProvider { + @Override + public String configKey() { + return "metrics"; + } + + @Override + public DbInterceptor create(Config config) { + String type = config.get("type").asString().orElse("COUNTER"); + switch (type) { + case "COUNTER": + return DbCounter.create(config); + case "METER": + return DbMeter.create(config); + case "TIMER": + return DbTimer.create(config); + default: + throw new DbClientException("Metrics type " + type + " is not supported through service loader"); + } + } +} diff --git a/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbTimer.java b/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbTimer.java new file mode 100644 index 000000000..fd4e4641a --- /dev/null +++ b/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/DbTimer.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.metrics; + +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; + +import io.helidon.config.Config; + +import org.eclipse.microprofile.metrics.Metadata; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.MetricType; +import org.eclipse.microprofile.metrics.Timer; + +/** + * Timer metric for Helidon DB. This class implements the {@link io.helidon.dbclient.DbInterceptor} and + * can be configured either through a {@link io.helidon.dbclient.DbClient.Builder} or through configuration. + */ +public final class DbTimer extends DbMetric { + + private DbTimer(Builder builder) { + super(builder); + } + + /** + * Create a timer from configuration. + * + * @param config configuration to read + * @return a new timer + * @see io.helidon.dbclient.metrics.DbMetricBuilder#config(io.helidon.config.Config) + */ + public static DbTimer create(Config config) { + return builder().config(config).build(); + } + + /** + * Create a new timer using default configuration. + *

By default the name format is {@code db.timer.statement-name}, where {@code statement-name} + * is provided at runtime. + * + * @return a new timer + */ + public static DbTimer create() { + return builder().build(); + } + + /** + * Create a new fluent API builder to create a new timer metric. + * @return a new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + protected void executeMetric(Timer metric, CompletionStage aFuture) { + long started = System.nanoTime(); + + aFuture + .thenAccept(nothing -> { + if (measureSuccess()) { + update(metric, started); + } + }) + .exceptionally(throwable -> { + if (measureErrors()) { + update(metric, started); + } + return null; + }); + } + + private void update(Timer metric, long started) { + long delta = System.nanoTime() - started; + metric.update(delta, TimeUnit.NANOSECONDS); + } + + @Override + protected String defaultNamePrefix() { + return "db.timer."; + } + + @Override + protected MetricType metricType() { + return MetricType.COUNTER; + } + + @Override + protected Timer metric(MetricRegistry registry, Metadata meta) { + return registry.timer(meta); + } + + /** + * Fluent API builder for {@link io.helidon.dbclient.metrics.DbTimer}. + */ + public static class Builder extends DbMetricBuilder implements io.helidon.common.Builder { + @Override + public DbTimer build() { + return new DbTimer(this); + } + } +} diff --git a/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/package-info.java b/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/package-info.java new file mode 100644 index 000000000..afd3b2b94 --- /dev/null +++ b/dbclient/metrics/src/main/java/io/helidon/dbclient/metrics/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ +/** + * Metrics support for Helidon DB Client. + */ +package io.helidon.dbclient.metrics; diff --git a/dbclient/metrics/src/main/java/module-info.java b/dbclient/metrics/src/main/java/module-info.java new file mode 100644 index 000000000..f77e5919a --- /dev/null +++ b/dbclient/metrics/src/main/java/module-info.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Helidon DB Client Metrics. + */ +module io.helidon.dbclient.metrics { + requires java.logging; + requires io.helidon.dbclient; + requires io.helidon.metrics; + + exports io.helidon.dbclient.metrics; + provides io.helidon.dbclient.spi.DbInterceptorProvider with io.helidon.dbclient.metrics.DbMetricsProvider; +} + diff --git a/dbclient/metrics/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbInterceptorProvider b/dbclient/metrics/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbInterceptorProvider new file mode 100644 index 000000000..9830fbc15 --- /dev/null +++ b/dbclient/metrics/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbInterceptorProvider @@ -0,0 +1,17 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +io.helidon.dbclient.metrics.DbMetricsProvider diff --git a/dbclient/mongodb/pom.xml b/dbclient/mongodb/pom.xml new file mode 100644 index 000000000..8f2936b71 --- /dev/null +++ b/dbclient/mongodb/pom.xml @@ -0,0 +1,67 @@ + + + + + + helidon-dbclient-project + io.helidon.dbclient + 2.0-SNAPSHOT + + 4.0.0 + + helidon-dbclient-mongodb + Helidon DB Client MongoDB + + + Integration with MongoDB + + + + + io.helidon.common + helidon-common-mapper + + + io.helidon.dbclient + helidon-dbclient + + + io.helidon.dbclient + helidon-dbclient-common + + + org.glassfish + javax.json + + + org.mongodb + mongodb-driver-reactivestreams + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbClient.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbClient.java new file mode 100644 index 000000000..bf42cac2a --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbClient.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; + +import io.helidon.common.HelidonFeatures; +import io.helidon.common.HelidonFlavor; +import io.helidon.common.mapper.MapperManager; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbExecute; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.DbStatements; +import io.helidon.dbclient.DbTransaction; +import io.helidon.dbclient.common.InterceptorSupport; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoCredential; +import com.mongodb.reactivestreams.client.ClientSession; +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoClients; +import com.mongodb.reactivestreams.client.MongoDatabase; +import org.reactivestreams.Subscription; + +/** + * MongoDB driver handler. + */ +public class MongoDbClient implements DbClient { + static { + HelidonFeatures.register(HelidonFlavor.SE, "DbClient", "MongoDB"); + } + + private final MongoDbClientConfig config; + private final DbStatements statements; + private final MongoClient client; + private final MongoDatabase db; + private final MapperManager mapperManager; + private final DbMapperManager dbMapperManager; + private final ConnectionString connectionString; + private final InterceptorSupport interceptors; + + /** + * Creates an instance of MongoDB driver handler. + * + * @param builder builder for mongoDB database + */ + MongoDbClient(MongoDbClientProviderBuilder builder) { + this.config = builder.dbConfig(); + this.connectionString = new ConnectionString(config.url()); + this.statements = builder.statements(); + this.mapperManager = builder.mapperManager(); + this.dbMapperManager = builder.dbMapperManager(); + this.client = initMongoClient(); + this.db = initMongoDatabase(); + this.interceptors = builder.interceptors(); + } + + private static final class MongoSessionSubscriber implements org.reactivestreams.Subscriber { + + private final CompletableFuture txFuture; + private ClientSession tx; + private Subscription subscription; + + MongoSessionSubscriber(CompletableFuture txFuture) { + this.txFuture = txFuture; + this.tx = null; + } + + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + this.subscription.request(1); + } + + @Override + public void onNext(ClientSession session) { + this.tx = session; + this.subscription.cancel(); + } + + @Override + public void onError(Throwable t) { + txFuture.completeExceptionally(t); + } + + @Override + public void onComplete() { + txFuture.complete(tx); + } + + } + + @Override + public CompletionStage inTransaction(Function> executor) { + // Disable MongoDB transactions until they are tested. + if (true) { + throw new UnsupportedOperationException("Transactions are not yet supported in MongoDB"); + } + CompletableFuture txFuture = new CompletableFuture<>(); + client.startSession().subscribe(new MongoSessionSubscriber(txFuture)); + return txFuture.thenCompose(tx -> { + MongoDbTransaction mongoTx = new MongoDbTransaction( + db, tx, statements, dbMapperManager, mapperManager, interceptors); + CompletionStage future = executor.apply(mongoTx); + // FIXME: Commit and rollback return Publisher so another future must be introduced here + // to cover commit or rollback. This future may be passed using allRegistered call + // and combined with transaction future + future.thenRun(mongoTx.txManager()::allRegistered); + return future; + }); + } + + @Override + public > T execute(Function executor) { + return executor.apply(new MongoDbExecute(db, statements, dbMapperManager, mapperManager, interceptors)); + } + + @Override + public CompletionStage ping() { + return execute(exec -> exec + .statement("{\"operation\":\"command\",\"query\":{ping:1}}")) + .thenRun(() -> {}); + } + + @Override + public String dbType() { + return MongoDbClientProvider.DB_TYPE; + } + + /** + * Constructor helper to build MongoDB client from provided configuration. + */ + private MongoClient initMongoClient() { + MongoClientSettings.Builder settingsBuilder = MongoClientSettings.builder() + .applyConnectionString(connectionString); + + if ((config.username() != null) || (config.password() != null)) { + String credDb = (config.credDb() == null) ? connectionString.getDatabase() : config.credDb(); + + MongoCredential credentials = MongoCredential.createCredential( + config.username(), + credDb, + config.password().toCharArray()); + + settingsBuilder.credential(credentials); + } + + return MongoClients.create(settingsBuilder.build()); + } + + /** + * Constructor helper to build MongoDB database from provided configuration and client. + */ + private MongoDatabase initMongoDatabase() { + return client.getDatabase(connectionString.getDatabase()); + } +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbClientConfig.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbClientConfig.java new file mode 100644 index 000000000..2a8c9e77e --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbClientConfig.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +/** + * MongoDB Configuration parameters. + * MongoDB connection string URI: + * {@code mongodb://[username:password@]host1[:port1][,...hostN[:portN]]][/[database][?options]]} + */ +public class MongoDbClientConfig { + + private final String url; + private final String username; + private final String password; + private final String credDb; + + MongoDbClientConfig(String url, String username, String password, String credDb) { + this.url = url; + this.username = username; + this.password = password; + this.credDb = credDb; + } + + String url() { + return url; + } + + String username() { + return username; + } + + String password() { + return password; + } + + String credDb() { + return credDb; + } + +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbClientProvider.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbClientProvider.java new file mode 100644 index 000000000..f214c797a --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbClientProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import io.helidon.dbclient.spi.DbClientProvider; + +/** + * Helidon DB Provider for mongoDB. + * + * @see MongoDbClientProviderBuilder + */ +public class MongoDbClientProvider implements DbClientProvider { + + static final String DB_TYPE = "mongoDb"; + + @Override + public String name() { + return DB_TYPE; + } + + @Override + public MongoDbClientProviderBuilder builder() { + return new MongoDbClientProviderBuilder(); + } + +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbClientProviderBuilder.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbClientProviderBuilder.java new file mode 100644 index 000000000..01722275e --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbClientProviderBuilder.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.util.Optional; + +import io.helidon.common.GenericType; +import io.helidon.common.mapper.MapperManager; +import io.helidon.config.Config; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbClientException; +import io.helidon.dbclient.DbInterceptor; +import io.helidon.dbclient.DbMapper; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.DbStatementType; +import io.helidon.dbclient.DbStatements; +import io.helidon.dbclient.common.InterceptorSupport; +import io.helidon.dbclient.spi.DbClientProviderBuilder; +import io.helidon.dbclient.spi.DbMapperProvider; + +/** + * Builder for mongoDB database. + */ +public final class MongoDbClientProviderBuilder implements DbClientProviderBuilder { + + private final InterceptorSupport.Builder interceptors = InterceptorSupport.builder(); + private final DbMapperManager.Builder dbMapperBuilder = DbMapperManager.builder(); + + private String url; + private String username; + private String password; + private String credDb; + private DbStatements statements; + private MapperManager mapperManager; + private DbMapperManager dbMapperManager; + private MongoDbClientConfig dbConfig; + + MongoDbClientProviderBuilder() { + } + + @Override + public DbClient build() { + if (null == dbMapperManager) { + this.dbMapperManager = dbMapperBuilder.build(); + } + if (null == mapperManager) { + this.mapperManager = MapperManager.create(); + } + if (null == dbConfig) { + dbConfig = new MongoDbClientConfig(url, username, password, credDb); + } + + return new MongoDbClient(this); + } + + @Override + public MongoDbClientProviderBuilder config(Config config) { + config.get("connection").asNode().ifPresentOrElse(conn -> { + conn.get("url").asString().ifPresent(this::url); + conn.get("username").asString().ifPresent(this::username); + conn.get("password").asString().ifPresent(this::password); + }, () -> { + throw new DbClientException(String.format( + "No database connection configuration (%s) was found", + config.get("connection").key())); + }); + config.get("credDb").asString().ifPresent(this::credDb); + statements = DbStatements.create(config.get("statements")); + return this; + } + + @Override + public MongoDbClientProviderBuilder url(String url) { + this.url = url; + return this; + } + + @Override + public MongoDbClientProviderBuilder username(String username) { + this.username = username; + return this; + } + + @Override + public MongoDbClientProviderBuilder password(String password) { + this.password = password; + return this; + } + + /** + * Credential database. + * + * @param db database name + * @return updated builder instance + */ + public MongoDbClientProviderBuilder credDb(String db) { + this.credDb = db; + return this; + } + + @Override + public MongoDbClientProviderBuilder statements(DbStatements statements) { + this.statements = statements; + return this; + } + + @Override + public MongoDbClientProviderBuilder addInterceptor(DbInterceptor interceptor) { + this.interceptors.add(interceptor); + return this; + } + + @Override + public MongoDbClientProviderBuilder addInterceptor(DbInterceptor interceptor, String... statementNames) { + this.interceptors.add(interceptor, statementNames); + return this; + } + + @Override + public MongoDbClientProviderBuilder addInterceptor(DbInterceptor interceptor, DbStatementType... dbStatementTypes) { + this.interceptors.add(interceptor, dbStatementTypes); + return this; + } + + @Override + public MongoDbClientProviderBuilder addMapper(DbMapper dbMapper, Class mappedClass) { + this.dbMapperBuilder.addMapperProvider(new DbMapperProvider() { + @SuppressWarnings("unchecked") + @Override + public Optional> mapper(Class type) { + if (type.equals(mappedClass)) { + return Optional.of((DbMapper) dbMapper); + } + return Optional.empty(); + } + }); + return this; + } + + @Override + public MongoDbClientProviderBuilder addMapper(DbMapper dbMapper, GenericType mappedType) { + this.dbMapperBuilder.addMapperProvider(new DbMapperProvider() { + @Override + public Optional> mapper(Class type) { + return Optional.empty(); + } + + @SuppressWarnings("unchecked") + @Override + public Optional> mapper(GenericType type) { + if (type.equals(mappedType)) { + return Optional.of((DbMapper) dbMapper); + } + return Optional.empty(); + } + }); + return this; + } + + @Override + public MongoDbClientProviderBuilder mapperManager(MapperManager manager) { + this.mapperManager = manager; + return this; + } + + @Override + public MongoDbClientProviderBuilder addMapperProvider(DbMapperProvider provider) { + this.dbMapperBuilder.addMapperProvider(provider); + return this; + } + + InterceptorSupport interceptors() { + return interceptors.build(); + } + + DbMapperManager.Builder dbMapperBuilder() { + return dbMapperBuilder; + } + + DbStatements statements() { + return statements; + } + + MapperManager mapperManager() { + return mapperManager; + } + + DbMapperManager dbMapperManager() { + return dbMapperManager; + } + + MongoDbClientConfig dbConfig() { + return dbConfig; + } + +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbColumn.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbColumn.java new file mode 100644 index 000000000..187f61ef7 --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbColumn.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import io.helidon.common.GenericType; +import io.helidon.common.mapper.MapperException; +import io.helidon.common.mapper.MapperManager; +import io.helidon.dbclient.DbColumn; +import io.helidon.dbclient.DbMapperManager; + +/** + * Mongo specific column data and metadata. + */ +public final class MongoDbColumn implements DbColumn { + + private final MapperManager mapperManager; + private final String name; + private final Object value; + + MongoDbColumn(DbMapperManager dbMapperManager, MapperManager mapperManager, String name, Object value) { + this.mapperManager = mapperManager; + this.name = name; + this.value = value; + } + + @SuppressWarnings("unchecked") + @Override + public T as(Class type) throws MapperException { + if (type.equals(javaType())) { + return (T) value; + } + + return map(value, type); + } + + @Override + public T as(GenericType type) throws MapperException { + return map(value, type); + } + + @SuppressWarnings("unchecked") + private T map(S value, Class targetType) { + Class sourceType = (Class) javaType(); + + return mapperManager.map(value, sourceType, targetType); + } + + @SuppressWarnings("unchecked") + private T map(S value, GenericType targetType) { + Class sourceClass = (Class) javaType(); + GenericType sourceType = GenericType.create(sourceClass); + + return mapperManager.map(value, sourceType, targetType); + } + + @Override + public Class javaType() { + return (null == value) ? String.class : value.getClass(); + } + + @Override + public String dbType() { + throw new UnsupportedOperationException("dbType() is not supported yet."); + } + + @Override + public String name() { + return name; + } + +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbCommandExecutor.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbCommandExecutor.java new file mode 100644 index 000000000..e6691cf1e --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbCommandExecutor.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.logging.Logger; + +import io.helidon.common.GenericType; +import io.helidon.common.reactive.Multi; +import io.helidon.dbclient.DbInterceptorContext; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.dbclient.DbStatementType; + +import com.mongodb.reactivestreams.client.MongoDatabase; +import org.bson.Document; +import org.reactivestreams.Publisher; + +import static io.helidon.dbclient.mongodb.MongoDbStatement.READER_FACTORY; + +/** + * Executes Mongo specific database command and returns result. + * Utility class with static methods only. + */ +final class MongoDbCommandExecutor { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(MongoDbCommandExecutor.class.getName()); + + static final class CommandRows implements DbRows { + + private final AtomicBoolean resultRequested = new AtomicBoolean(false); + private final Publisher publisher; + private final MongoDbStatement dbStatement; + private final CompletableFuture statementFuture; + private final CompletableFuture commandFuture; + + CommandRows( + Publisher publisher, + MongoDbStatement dbStatement, + CompletableFuture statementFuture, + CompletableFuture commandFuture + ) { + this.publisher = publisher; + this.dbStatement = dbStatement; + this.statementFuture = statementFuture; + this.commandFuture = commandFuture; + } + + @Override + public DbRows map(Function mapper) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public DbRows map(Class type) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public DbRows map(GenericType type) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Flow.Publisher publisher() { + checkResult(); + return toDbPublisher(); + } + + @Override + public CompletionStage> collect() { + checkResult(); + return Multi.from(toDbPublisher()) + .collectList() + .toStage(); + } + + private Flow.Publisher toDbPublisher() { + MongoDbQueryProcessor qp = new MongoDbQueryProcessor( + dbStatement, + statementFuture, + commandFuture); + publisher.subscribe(qp); + return qp; + } + + private void checkResult() { + if (resultRequested.get()) { + throw new IllegalStateException("Result has already been requested"); + } + resultRequested.set(true); + } + + } + + private MongoDbCommandExecutor() { + throw new UnsupportedOperationException("Utility class MongoDbCommandExecutor instances are not allowed!"); + } + + static CompletionStage> executeCommand( + MongoDbStatement dbStatement, + CompletionStage dbContextFuture, + CompletableFuture statementFuture, + CompletableFuture commandFuture + ) { + + dbContextFuture.exceptionally(throwable -> { + statementFuture.completeExceptionally(throwable); + commandFuture.completeExceptionally(throwable); + return null; + }); + + CompletionStage mongoStmtFuture = dbContextFuture.thenApply(dbContext -> { + MongoDbStatement.MongoStatement stmt + = new MongoDbStatement.MongoStatement(DbStatementType.COMMAND, READER_FACTORY, dbStatement.build()); + if (stmt.getOperation() == MongoDbStatement.MongoOperation.COMMAND) { + return stmt; + } else { + throw new UnsupportedOperationException( + String.format("Operation %s is not supported", stmt.getOperation().toString())); + } + }); + + return executeCommandInMongoDB(dbStatement, mongoStmtFuture, statementFuture, commandFuture); + } + + private static CompletionStage> executeCommandInMongoDB( + MongoDbStatement dbStatement, + CompletionStage stmtFuture, + CompletableFuture statementFuture, + CompletableFuture commandFuture + ) { + return stmtFuture.thenApply(mongoStmt -> { + MongoDatabase db = dbStatement.db(); + Document command = mongoStmt.getQuery(); + LOGGER.fine(() -> String.format("Command: %s", command.toString())); + Publisher publisher = dbStatement.noTx() + ? db.runCommand(command) + : db.runCommand(dbStatement.txManager().tx(), command); + return publisher; + }).thenApply(publisher -> { + return new CommandRows( + publisher, + dbStatement, + statementFuture, + commandFuture); + }); + } + + + +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbDMLExecutor.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbDMLExecutor.java new file mode 100644 index 000000000..beb8d4013 --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbDMLExecutor.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.LongAdder; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbInterceptorContext; +import io.helidon.dbclient.DbStatementType; + +import com.mongodb.client.result.DeleteResult; +import com.mongodb.client.result.UpdateResult; +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.Success; +import org.bson.Document; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; + +/** + * Executes Mongo specific DML statement and returns result. + * Utility class with static methods only. + */ +final class MongoDbDMLExecutor { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(MongoDbDMLExecutor.class.getName()); + + private MongoDbDMLExecutor() { + throw new UnsupportedOperationException("Utility class MongoDbDMLExecutor instances are not allowed!"); + } + + static CompletionStage executeDml( + MongoDbStatement dbStatement, + DbStatementType dbStatementType, + MongoDbStatement.MongoStatement mongoStatement, + CompletionStage dbContextFuture, + CompletableFuture statementFuture, + CompletableFuture queryFuture + ) { + + // if the iterceptors fail with exception, we must fail as well + dbContextFuture.exceptionally(throwable -> { + statementFuture.completeExceptionally(throwable); + queryFuture.completeExceptionally(throwable); + return null; + }); + + return dbContextFuture.thenCompose(dbContext -> { + switch (dbStatementType) { + case INSERT: + return executeInsert(dbStatement, dbStatementType, mongoStatement, statementFuture, queryFuture); + case UPDATE: + return executeUpdate(dbStatement, dbStatementType, mongoStatement, statementFuture, queryFuture); + case DELETE: + return executeDelete(dbStatement, dbStatementType, mongoStatement, statementFuture, queryFuture); + default: + CompletableFuture result = new CompletableFuture<>(); + Throwable failure = new UnsupportedOperationException( + String.format("Statement operation not yet supported: %s", dbStatementType.name())); + result.completeExceptionally(failure); + statementFuture.completeExceptionally(failure); + queryFuture.completeExceptionally(failure); + return result; + } + }); + } + + private abstract static class DmlResultSubscriber implements org.reactivestreams.Subscriber { + + private final MongoDbStatement dbStatement; + private final DbStatementType dbStatementType; + private final CompletableFuture statementFuture; + private final CompletableFuture queryFuture; + private final LongAdder count; + + private DmlResultSubscriber( + MongoDbStatement dbStatement, + DbStatementType dbStatementType, + CompletableFuture queryFuture, + CompletableFuture statementFuture + ) { + this.dbStatement = dbStatement; + this.dbStatementType = dbStatementType; + this.statementFuture = statementFuture; + this.queryFuture = queryFuture; + this.count = new LongAdder(); + } + + @Override + public void onSubscribe(Subscription s) { + // no need for flow control, we only add the result + s.request(Long.MAX_VALUE); + } + + @Override + public void onError(Throwable t) { + statementFuture.completeExceptionally(t); + queryFuture.completeExceptionally(t); + if (dbStatement.txManager() != null) { + dbStatement.txManager().stmtFailed(dbStatement); + } + LOGGER.fine(() -> String.format( + "%s DML %s execution failed", dbStatementType.name(), dbStatement.statementName())); + } + + @Override + public void onComplete() { + statementFuture.complete(null); + queryFuture.complete(count.sum()); + if (dbStatement.txManager() != null) { + dbStatement.txManager().stmtFinished(dbStatement); + } + LOGGER.fine(() -> String.format( + "%s DML %s execution succeeded", dbStatementType.name(), dbStatement.statementName())); + } + + LongAdder count() { + return count; + } + + } + + private static final class InsertResultSubscriber extends DmlResultSubscriber { + + private InsertResultSubscriber( + MongoDbStatement dbStatement, + DbStatementType dbStatementType, + CompletableFuture queryFuture, + CompletableFuture statementFuture + ) { + super(dbStatement, dbStatementType, queryFuture, statementFuture); + } + + @Override + public void onNext(Success r) { + count().increment(); + } + + } + + private static final class UpdateResultSubscriber extends DmlResultSubscriber { + + private UpdateResultSubscriber( + MongoDbStatement dbStatement, + DbStatementType dbStatementType, + CompletableFuture queryFuture, + CompletableFuture statementFuture + ) { + super(dbStatement, dbStatementType, queryFuture, statementFuture); + } + + @Override + public void onNext(UpdateResult r) { + count().add(r.getModifiedCount()); + } + + } + + private static final class DeleteResultSubscriber extends DmlResultSubscriber { + + private DeleteResultSubscriber( + MongoDbStatement dbStatement, + DbStatementType dbStatementType, + CompletableFuture queryFuture, + CompletableFuture statementFuture + ) { + super(dbStatement, dbStatementType, queryFuture, statementFuture); + } + + @Override + public void onNext(DeleteResult r) { + count().add(r.getDeletedCount()); + } + + } + + private static CompletionStage executeInsert( + MongoDbStatement dbStatement, + DbStatementType dbStatementType, + MongoDbStatement.MongoStatement mongoStatement, + CompletableFuture statementFuture, + CompletableFuture queryFuture + ) { + MongoCollection mc = dbStatement.db().getCollection(mongoStatement.getCollection()); + Publisher insertPublisher = dbStatement.noTx() + ? mc.insertOne(mongoStatement.getValue()) + : mc.insertOne(dbStatement.txManager().tx(), mongoStatement.getValue()); + insertPublisher.subscribe(new InsertResultSubscriber(dbStatement, dbStatementType, queryFuture, statementFuture)); + return queryFuture; + } + + @SuppressWarnings("SubscriberImplementation") + private static CompletionStage executeUpdate( + MongoDbStatement dbStatement, + DbStatementType dbStatementType, + MongoDbStatement.MongoStatement mongoStatement, + CompletableFuture statementFuture, + CompletableFuture queryFuture + ) { + MongoCollection mc = dbStatement.db().getCollection(mongoStatement.getCollection()); + Document query = mongoStatement.getQuery(); + // TODO should the next line be used? + //Document value = mongoStatement.getValue(); + Publisher updatePublisher = dbStatement.noTx() + ? mc.updateMany(query, mongoStatement.getValue()) + : mc.updateMany(dbStatement.txManager().tx(), query, mongoStatement.getValue()); + updatePublisher.subscribe(new UpdateResultSubscriber(dbStatement, dbStatementType, queryFuture, statementFuture)); + return queryFuture; + } + + @SuppressWarnings("SubscriberImplementation") + private static CompletionStage executeDelete( + MongoDbStatement dbStatement, + DbStatementType dbStatementType, + MongoDbStatement.MongoStatement mongoStatement, + CompletableFuture statementFuture, + CompletableFuture queryFuture + ) { + MongoCollection mc = dbStatement.db().getCollection(mongoStatement.getCollection()); + Document query = mongoStatement.getQuery(); + Publisher deletePublisher = dbStatement.noTx() + ? mc.deleteMany(query) + : mc.deleteMany(dbStatement.txManager().tx(), query); + deletePublisher.subscribe(new DeleteResultSubscriber(dbStatement, dbStatementType, queryFuture, statementFuture)); + return queryFuture; + } + +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbExecute.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbExecute.java new file mode 100644 index 000000000..82551dc69 --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbExecute.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import io.helidon.common.mapper.MapperManager; +import io.helidon.dbclient.DbExecute; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.DbStatementDml; +import io.helidon.dbclient.DbStatementGeneric; +import io.helidon.dbclient.DbStatementGet; +import io.helidon.dbclient.DbStatementQuery; +import io.helidon.dbclient.DbStatementType; +import io.helidon.dbclient.DbStatements; +import io.helidon.dbclient.common.AbstractDbExecute; +import io.helidon.dbclient.common.InterceptorSupport; + +import com.mongodb.reactivestreams.client.MongoDatabase; + +/** + * Execute implementation for MongoDB. + */ +public class MongoDbExecute extends AbstractDbExecute implements DbExecute { + + private final DbMapperManager dbMapperManager; + private final MapperManager mapperManager; + private final InterceptorSupport interceptors; + private final MongoDatabase db; + + MongoDbExecute(MongoDatabase db, + DbStatements statements, + DbMapperManager dbMapperManager, + MapperManager mapperManager, + InterceptorSupport interceptors) { + super(statements); + this.db = db; + this.dbMapperManager = dbMapperManager; + this.mapperManager = mapperManager; + this.interceptors = interceptors; + } + + @Override + public DbStatementQuery createNamedQuery(String statementName, String statement) { + return new MongoDbStatementQuery(DbStatementType.QUERY, + db, + statementName, + statement, + dbMapperManager, + mapperManager, + interceptors); + } + + @Override + public DbStatementGet createNamedGet(String statementName, String statement) { + return new MongoDbStatementGet(db, statementName, statement, dbMapperManager, mapperManager, interceptors); + } + + @Override + public DbStatementDml createNamedDmlStatement(String statementName, String statement) { + return new MongoDbStatementDml(DbStatementType.DML, db, statementName, statement, dbMapperManager, mapperManager, + interceptors); + } + + @Override + public DbStatementDml createNamedInsert(String statementName, String statement) { + return new MongoDbStatementDml(DbStatementType.INSERT, db, statementName, statement, dbMapperManager, mapperManager, + interceptors); + } + + @Override + public DbStatementDml createNamedUpdate(String statementName, String statement) { + return new MongoDbStatementDml(DbStatementType.UPDATE, db, statementName, statement, dbMapperManager, mapperManager, + interceptors); + } + + @Override + public DbStatementDml createNamedDelete(String statementName, String statement) { + return new MongoDbStatementDml(DbStatementType.DELETE, db, statementName, statement, dbMapperManager, mapperManager, + interceptors); + } + + @Override + public DbStatementGeneric createNamedStatement(String statementName, String statement) { + return new MongoDbStatementGeneric(DbStatementType.UNKNOWN, db, statementName, statement, dbMapperManager, + mapperManager, interceptors); + } + + } diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbQueryExecutor.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbQueryExecutor.java new file mode 100644 index 000000000..48c235c21 --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbQueryExecutor.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbInterceptorContext; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.dbclient.DbStatementType; + +import com.mongodb.reactivestreams.client.FindPublisher; +import com.mongodb.reactivestreams.client.MongoCollection; +import org.bson.Document; + +import static io.helidon.dbclient.mongodb.MongoDbStatement.READER_FACTORY; + +/** + * Executes Mongo specific query and returns result. + * Utility class with static methods only. + */ +final class MongoDbQueryExecutor { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(MongoDbStatementQuery.class.getName()); + + private MongoDbQueryExecutor() { + throw new UnsupportedOperationException("Utility class MongoDbQueryExecutor instances are not allowed!"); + } + + static CompletionStage> executeQuery( + MongoDbStatement dbStatement, + CompletionStage dbContextFuture, + CompletableFuture statementFuture, + CompletableFuture queryFuture + ) { + + dbContextFuture.exceptionally(throwable -> { + statementFuture.completeExceptionally(throwable); + queryFuture.completeExceptionally(throwable); + return null; + }); + + CompletionStage mongoStmtFuture = dbContextFuture.thenApply(dbContext -> { + MongoDbStatement.MongoStatement stmt + = new MongoDbStatement.MongoStatement(DbStatementType.QUERY, READER_FACTORY, dbStatement.build()); + if (stmt.getOperation() == MongoDbStatement.MongoOperation.QUERY) { + return stmt; + } else { + throw new UnsupportedOperationException( + String.format("Operation %s is not supported", stmt.getOperation().toString())); + } + }); + + return executeQueryInMongoDB(dbStatement, mongoStmtFuture, statementFuture, queryFuture); + } + + private static CompletionStage> executeQueryInMongoDB( + MongoDbStatement dbStatement, + CompletionStage stmtFuture, + CompletableFuture statementFuture, + CompletableFuture queryFuture + ) { + + return stmtFuture.thenApply(mongoStmt -> { + final MongoCollection mc = dbStatement.db() + .getCollection(mongoStmt.getCollection()); + final Document query = mongoStmt.getQuery(); + final Document projection = mongoStmt.getProjection(); + LOGGER.fine(() -> String.format( + "Query: %s, Projection: %s", query.toString(), (projection != null ? projection : "N/A"))); + FindPublisher publisher = dbStatement.noTx() + ? mc.find(query) + : mc.find(dbStatement.txManager().tx(), query); + if (projection != null) { + publisher = publisher.projection(projection); + } + return publisher; + }).thenApply(mongoPublisher -> { + return new MongoDbRows<>( + mongoPublisher, + dbStatement, + DbRow.class, + statementFuture, + queryFuture); + }); + } +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbQueryProcessor.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbQueryProcessor.java new file mode 100644 index 000000000..875f0c30b --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbQueryProcessor.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbRow; + +import org.bson.Document; +import org.reactivestreams.Subscription; + +/** + * Mongo specific query asynchronous processor. + */ +final class MongoDbQueryProcessor implements org.reactivestreams.Subscriber, Flow.Publisher, Flow.Subscription { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(MongoDbQueryProcessor.class.getName()); + + private final AtomicLong count = new AtomicLong(); + private final CompletableFuture queryFuture; + private final MongoDbStatement dbStatement; + private final CompletableFuture statementFuture; + private Flow.Subscriber subscriber; + private Subscription subscription; + + MongoDbQueryProcessor( + MongoDbStatement dbStatement, + CompletableFuture statementFuture, + CompletableFuture queryFuture + ) { + this.statementFuture = statementFuture; + this.queryFuture = queryFuture; + this.dbStatement = dbStatement; + } + + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + } + + @Override + public void onNext(Document doc) { + MongoDbRow dbRow = new MongoDbRow(dbStatement.dbMapperManager(), dbStatement.mapperManager(), doc.size()); + doc.forEach((name, value) -> { + LOGGER.finest(() -> String.format( + "Column name = %s, value = %s", name, (value != null ? value.toString() : "N/A"))); + dbRow.add(name, new MongoDbColumn(dbStatement.dbMapperManager(), dbStatement.mapperManager(), name, value)); + }); + count.incrementAndGet(); + subscriber.onNext(dbRow); + } + + @Override + public void onError(Throwable t) { + LOGGER.finest(() -> String.format("Query error: %s", t.getMessage())); + statementFuture.completeExceptionally(t); + queryFuture.completeExceptionally(t); + if (dbStatement.txManager() != null) { + dbStatement.txManager().stmtFailed(dbStatement); + } + subscriber.onError(t); + LOGGER.finest(() -> String.format("Query %s execution failed", dbStatement.statementName())); + } + + @Override + public void onComplete() { + LOGGER.finest(() -> "Query finished"); + statementFuture.complete(null); + queryFuture.complete(count.get()); + if (dbStatement.txManager() != null) { + dbStatement.txManager().stmtFinished(dbStatement); + } + subscriber.onComplete(); + LOGGER.finest(() -> String.format("Query %s execution succeeded", dbStatement.statementName())); + } + + @Override + public void subscribe(Flow.Subscriber subscriber) { + this.subscriber = subscriber; + LOGGER.finest(() -> "Calling onSubscribe on subscriber"); + subscriber.onSubscribe(this); + } + + @Override + public void request(long n) { + LOGGER.finest(() -> String.format("Requesting %d records from MongoDB", n)); + this.subscription.request(n); + } + + @Override + public void cancel() { + LOGGER.finest(() -> "Cancelling MongoDB result processing"); + this.subscription.cancel(); + } + +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbRow.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbRow.java new file mode 100644 index 000000000..ccd672742 --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbRow.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +import io.helidon.common.GenericType; +import io.helidon.common.mapper.MapperException; +import io.helidon.common.mapper.MapperManager; +import io.helidon.dbclient.DbColumn; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.DbRow; + +/** + * Mongo specific representation of a single row in a database. + */ +final class MongoDbRow implements DbRow { + + private final Map columnsByName; + private final List columnsList; + + private final DbMapperManager dbMapperManager; + private final MapperManager mapperManager; + + MongoDbRow(DbMapperManager dbMapperManager, MapperManager mapperManager, int size) { + this.dbMapperManager = dbMapperManager; + this.mapperManager = mapperManager; + this.columnsByName = new HashMap<>(size); + this.columnsList = new ArrayList<>(size); + } + + MongoDbRow(DbMapperManager dbMapperManager, MapperManager mapperManager) { + this.dbMapperManager = dbMapperManager; + this.mapperManager = mapperManager; + this.columnsByName = new HashMap<>(); + this.columnsList = new ArrayList<>(); + } + + void add(String name, DbColumn column) { + columnsByName.put(name, column); + columnsList.add(column); + } + + @Override + public DbColumn column(String name) { + return columnsByName.get(name); + } + + @Override + public DbColumn column(int index) { + return columnsList.get(index - 1); + } + + @Override + public void forEach(Consumer columnAction) { + columnsByName.values().forEach(columnAction); + } + + @Override + public T as(Class type) { + return dbMapperManager.read(this, type); + } + + @Override + public T as(GenericType type) throws MapperException { + return dbMapperManager.read(this, type); + } + + @Override + public T as(Function mapper) { + return mapper.apply(this); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + boolean first = true; + sb.append('{'); + for (DbColumn col : columnsList) { + if (first) { + first = false; + } else { + sb.append(','); + } + sb.append(col.name()); + sb.append(':'); + sb.append(col.value().toString()); + } + sb.append('}'); + return sb.toString(); + } + +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbRows.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbRows.java new file mode 100644 index 000000000..14fbd2e87 --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbRows.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; + +import io.helidon.common.GenericType; +import io.helidon.common.mapper.MapperException; +import io.helidon.common.reactive.MappingProcessor; +import io.helidon.common.reactive.Multi; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; + +import com.mongodb.reactivestreams.client.FindPublisher; +import org.bson.Document; + +/** + * Mongo specific execution result containing result set with multiple rows. + * + * @param type of the result, starts as {@link io.helidon.dbclient.DbRow} + */ +public final class MongoDbRows implements DbRows { + + private final AtomicBoolean resultRequested = new AtomicBoolean(); + private final FindPublisher documentFindPublisher; + private final MongoDbStatement dbStatement; + private final CompletableFuture queryFuture; + private final GenericType currentType; + private final Function resultMapper; + private final MongoDbRows parent; + private final CompletableFuture statementFuture; + + MongoDbRows(FindPublisher documentFindPublisher, + MongoDbStatement dbStatement, + Class initialType, + CompletableFuture statementFuture, + CompletableFuture queryFuture) { + this.documentFindPublisher = documentFindPublisher; + this.dbStatement = dbStatement; + this.statementFuture = statementFuture; + this.queryFuture = queryFuture; + this.currentType = GenericType.create(initialType); + this.resultMapper = Function.identity(); + this.parent = null; + } + + private MongoDbRows(FindPublisher documentFindPublisher, + MongoDbStatement dbStatement, + CompletableFuture statementFuture, + CompletableFuture queryFuture, + GenericType nextType, + Function resultMapper, + MongoDbRows parent) { + this.documentFindPublisher = documentFindPublisher; + this.dbStatement = dbStatement; + this.statementFuture = statementFuture; + this.queryFuture = queryFuture; + this.resultMapper = resultMapper; + this.currentType = nextType; + this.parent = parent; + } + + @Override + public DbRows map(Function mapper) { + return new MongoDbRows<>( + documentFindPublisher, + dbStatement, + statementFuture, + queryFuture, + null, + mapper, + this); + } + + @Override + public DbRows map(Class type) { + return map(GenericType.create(type)); + } + + @Override + @SuppressWarnings("unchecked") + public DbRows map(GenericType type) { + GenericType localCurrentType = this.currentType; + + Function theMapper; + + if (null == localCurrentType) { + theMapper = value -> dbStatement.mapperManager().map(value, + GenericType.create(value.getClass()), + type); + } else if (localCurrentType.equals(DbMapperManager.TYPE_DB_ROW)) { + // maybe we want the same type + if (type.equals(DbMapperManager.TYPE_DB_ROW)) { + return (DbRows) this; + } + // try to find mapper in db mapper manager + theMapper = value -> { + //first try db mapper + try { + return dbStatement.dbMapperManager().read((DbRow) value, type); + } catch (MapperException originalException) { + // not found in db mappers, use generic mappers + try { + return dbStatement.mapperManager().map(value, + DbMapperManager.TYPE_DB_ROW, + type); + } catch (MapperException ignored) { + throw originalException; + } + } + }; + } else { + // one type to another + theMapper = value -> dbStatement.mapperManager().map(value, + localCurrentType, + type); + } + return new MongoDbRows<>( + documentFindPublisher, + dbStatement, + statementFuture, + queryFuture, + type, + theMapper, + this); + } + + @Override + public Flow.Publisher publisher() { + checkResult(); + + return toPublisher(); + } + + @SuppressWarnings("unchecked") + private Flow.Publisher toPublisher() { + // if parent is null, this is the DbRow type + if (null == parent) { + return (Flow.Publisher) toDbPublisher(); + } + + Flow.Publisher parentPublisher = parent.publisher(); + Function mappingFunction = (Function) resultMapper; + // otherwise we must apply mapping + MappingProcessor processor = MappingProcessor.create(mappingFunction); + parentPublisher.subscribe(processor); + return processor; + } + + @Override + public CompletionStage> collect() { + checkResult(); + + return Multi.from(toPublisher()) + .collectList() + .toStage(); + } + + private Flow.Publisher toDbPublisher() { + MongoDbQueryProcessor qp = new MongoDbQueryProcessor( + dbStatement, + statementFuture, + queryFuture); + documentFindPublisher.subscribe(qp); + + return qp; + } + + private void checkResult() { + if (resultRequested.get()) { + throw new IllegalStateException("Result has already been requested"); + } + resultRequested.set(true); + } + +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatement.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatement.java new file mode 100644 index 000000000..b8af82ca6 --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatement.java @@ -0,0 +1,359 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import javax.json.Json; +import javax.json.JsonReaderFactory; + +import io.helidon.common.mapper.MapperManager; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.DbStatement; +import io.helidon.dbclient.DbStatementType; +import io.helidon.dbclient.common.AbstractStatement; +import io.helidon.dbclient.common.InterceptorSupport; +import io.helidon.dbclient.mongodb.MongoDbTransaction.TransactionManager; + +import com.mongodb.reactivestreams.client.MongoDatabase; +import org.bson.Document; + +/** + * Common MongoDB statement builder. + * + * @param MongoDB statement type + * @param Statement execution result type + */ +abstract class MongoDbStatement, R> extends AbstractStatement { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(MongoDbStatement.class.getName()); + + /** + * Empty JSON object. + */ + static final Document EMPTY = Document.parse(Json.createObjectBuilder().build().toString()); + + /** + * Operation JSON parameter name. + */ + protected static final String JSON_OPERATION = "operation"; + /** + * Collection JSON parameter name. + */ + protected static final String JSON_COLLECTION = "collection"; + /** + * Query JSON parameter name. + */ + protected static final String JSON_QUERY = "query"; + /** + * Value JSON parameter name. + */ + protected static final String JSON_VALUE = "value"; + /** + * Projection JSON parameter name: Defines projection to restrict returned fields. + */ + protected static final String JSON_PROJECTION = "projection"; + /** + * JSON reader factory. + */ + protected static final JsonReaderFactory READER_FACTORY = Json.createReaderFactory(Collections.emptyMap()); + + /** MongoDB database. */ + private final MongoDatabase db; + /** MongoDB transaction manager. Set to {@code null} when not running in transaction. */ + private TransactionManager txManager; + + /** + * Creates an instance of MongoDB statement builder. + * + * @param dbStatementType type of this statement + * @param db mongo database handler + * @param statementName name of this statement + * @param statement text of this statement + * @param dbMapperManager db mapper manager to use when mapping types to parameters + * @param mapperManager mapper manager to use when mapping results + * @param interceptors interceptors to be executed + */ + MongoDbStatement(DbStatementType dbStatementType, + MongoDatabase db, + String statementName, + String statement, + DbMapperManager dbMapperManager, + MapperManager mapperManager, + InterceptorSupport interceptors) { + + super(dbStatementType, + statementName, + statement, + dbMapperManager, + mapperManager, + interceptors); + + this.db = db; + this.txManager = null; + } + + /** + * Set target transaction for this statement. + * + * @param tx MongoDB transaction session + * @return MongoDB statement builder + */ + @SuppressWarnings("unchecked") + B inTransaction(TransactionManager tx) { + this.txManager = tx; + this.txManager.addStatement(this); + return (B) this; + } + + String build() { + switch (paramType()) { + // Statement shall not contain any parameters, no conversion is needed. + case UNKNOWN: + return statement(); + case INDEXED: + return StatementParsers.indexedParser(statement(), indexedParams()).convert(); + // Replace parameter identifiers with values from name to value map + case NAMED: + return StatementParsers.namedParser(statement(), namedParams()).convert(); + default: + throw new IllegalStateException("Unknown SQL statement type: " + paramType()); + } + } + + /** + * Statement name. + * + * @return name of this statement (never null, may be generated) + */ + @Override + public String statementName() { + return super.statementName(); + } + + MongoDatabase db() { + return db; + } + + boolean noTx() { + return txManager == null; + } + + TransactionManager txManager() { + return txManager; + } + + /** + * Db mapper manager. + * + * @return mapper manager for DB types + */ + @Override + protected DbMapperManager dbMapperManager() { + return super.dbMapperManager(); + } + + /** + * Mapper manager. + * + * @return generic mapper manager + */ + @Override + protected MapperManager mapperManager() { + return super.mapperManager(); + } + + @Override + protected String dbType() { + return MongoDbClientProvider.DB_TYPE; + } + + /** + * Mongo operation enumeration. + */ + enum MongoOperation { + QUERY("query", "find", "select"), + INSERT("insert"), + UPDATE("update"), + DELETE("delete"), + // Database command not related to a specific collection + // Only executable using generic statement + COMMAND("command"); + + private static final Map NAME_TO_OPERATION = new HashMap<>(); + + static { + for (MongoOperation value : MongoOperation.values()) { + for (String name : value.names) { + NAME_TO_OPERATION.put(name.toLowerCase(), value); + } + } + } + + static MongoOperation operationByName(String name) { + if (name == null) { + return null; + } + return NAME_TO_OPERATION.get(name.toLowerCase()); + } + + private final String[] names; + + MongoOperation(String... names) { + this.names = names; + } + } + + static class MongoStatement { + private final String preparedStmt; + + private static Document/*JsonObject*/ readStmt(JsonReaderFactory jrf, String preparedStmt) { + return Document.parse(preparedStmt); + } + + private final Document/*JsonObject*/ jsonStmt; + private final MongoOperation operation; + private final String collection; + private final Document query; + private final Document value; + private final Document projection; + + MongoStatement(DbStatementType dbStatementType, JsonReaderFactory jrf, String preparedStmt) { + this.preparedStmt = preparedStmt; + this.jsonStmt = readStmt(jrf, preparedStmt); + + MongoOperation operation; + if (jsonStmt.containsKey(JSON_OPERATION)) { + operation = MongoOperation.operationByName(jsonStmt.getString(JSON_OPERATION)); + // make sure we have alignment between statement type and operation + switch (dbStatementType) { + case QUERY: + case GET: + validateOperation(dbStatementType, operation, MongoOperation.QUERY); + break; + case INSERT: + validateOperation(dbStatementType, operation, MongoOperation.INSERT); + break; + case UPDATE: + validateOperation(dbStatementType, operation, MongoOperation.UPDATE); + break; + case DELETE: + validateOperation(dbStatementType, operation, MongoOperation.DELETE); + break; + case DML: + validateOperation(dbStatementType, operation, MongoOperation.INSERT, + MongoOperation.UPDATE, MongoOperation.DELETE); + break; + case UNKNOWN: + validateOperation(dbStatementType, operation, MongoOperation.QUERY, + MongoOperation.INSERT, MongoOperation.UPDATE, + MongoOperation.DELETE, MongoOperation.COMMAND); + break; + case COMMAND: + validateOperation(dbStatementType, operation, MongoOperation.COMMAND); + break; + default: + throw new IllegalStateException( + "Operation type is not defined in statement, and cannot be inferred from statement type: " + + dbStatementType); + } + } else { + switch (dbStatementType) { + case QUERY: + operation = MongoOperation.QUERY; + break; + case GET: + operation = MongoOperation.QUERY; + break; + case INSERT: + operation = MongoOperation.INSERT; + break; + case UPDATE: + operation = MongoOperation.UPDATE; + break; + case DELETE: + operation = MongoOperation.DELETE; + break; + case COMMAND: + operation = MongoOperation.COMMAND; + break; + case DML: + case UNKNOWN: + default: + throw new IllegalStateException( + "Operation type is not defined in statement, and cannot be inferred from statement type: " + + dbStatementType); + } + } + this.operation = operation; + this.collection = jsonStmt.getString(JSON_COLLECTION); + this.value = jsonStmt.get(JSON_VALUE, Document.class); + this.query = jsonStmt.get(JSON_QUERY, Document.class); + this.projection = jsonStmt.get(JSON_PROJECTION, Document.class); + } + + private static void validateOperation(DbStatementType dbStatementType, + MongoOperation actual, + MongoOperation... expected) { + + // PERF: time complexity of this check is terrible + for (MongoOperation operation : expected) { + if (actual == operation) { + return; + } + } + + throw new IllegalStateException("Statement type is " + + dbStatementType + + ", yet operation in statement is: " + + actual); + } + + Document/*JsonObject*/ getJsonStmt() { + return jsonStmt; + } + + MongoOperation getOperation() { + return operation; + } + + String getCollection() { + return collection; + } + + Document getQuery() { + return query != null ? query : EMPTY; + } + + Document getValue() { + return value; + } + + Document getProjection() { + return projection; + } + + @Override + public String toString() { + return preparedStmt; + } + } + +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementDml.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementDml.java new file mode 100644 index 000000000..7d4953c84 --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementDml.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.logging.Logger; + +import io.helidon.common.mapper.MapperManager; +import io.helidon.dbclient.DbInterceptorContext; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.DbStatementDml; +import io.helidon.dbclient.DbStatementType; +import io.helidon.dbclient.common.InterceptorSupport; + +import com.mongodb.reactivestreams.client.MongoDatabase; + +/** + * DML statement for MongoDB. + */ +public class MongoDbStatementDml extends MongoDbStatement implements DbStatementDml { + + private static final Logger LOGGER = Logger.getLogger(MongoDbStatementDml.class.getName()); + + private DbStatementType dbStatementType; + + private MongoStatement statement; + + MongoDbStatementDml( + DbStatementType dbStatementType, + MongoDatabase db, + String statementName, + String statement, + DbMapperManager dbMapperManager, + MapperManager mapperManager, + InterceptorSupport interceptors + ) { + super(dbStatementType, + db, + statementName, + statement, + dbMapperManager, + mapperManager, + interceptors); + this.dbStatementType = dbStatementType; + } + + @Override + public CompletionStage execute() { + statement = new MongoStatement(dbStatementType, READER_FACTORY, build()); + switch (statement.getOperation()) { + case INSERT: + dbStatementType = DbStatementType.INSERT; + break; + case UPDATE: + dbStatementType = DbStatementType.UPDATE; + break; + case DELETE: + dbStatementType = DbStatementType.DELETE; + break; + default: + throw new IllegalStateException( + String.format("Unexpected value for DML statement: %s", statement.getOperation())); + } + return super.execute(); + } + + @Override + protected CompletionStage doExecute( + CompletionStage dbContextFuture, + CompletableFuture statementFuture, + CompletableFuture queryFuture + ) { + return MongoDbDMLExecutor.executeDml( + this, + dbStatementType, + statement, + dbContextFuture, + statementFuture, + queryFuture); + } + + @Override + protected DbStatementType statementType() { + return dbStatementType; + } + +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementGeneric.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementGeneric.java new file mode 100644 index 000000000..cde7b1c57 --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementGeneric.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import java.util.logging.Logger; + +import io.helidon.common.mapper.MapperManager; +import io.helidon.dbclient.DbInterceptorContext; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.DbResult; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.dbclient.DbStatementGeneric; +import io.helidon.dbclient.DbStatementType; +import io.helidon.dbclient.common.InterceptorSupport; + +import com.mongodb.reactivestreams.client.MongoDatabase; + +/** + * Generic statement for mongoDB. + */ +public class MongoDbStatementGeneric extends MongoDbStatement implements DbStatementGeneric { + + private abstract static class MongoDbAbstractResult implements DbResult { + + private final CompletionStage resultFuture; + private final CompletableFuture throwableFuture; + + private MongoDbAbstractResult(CompletionStage resultFuture) { + this.resultFuture = resultFuture; + throwableFuture = new CompletableFuture<>(); + resultFuture.exceptionally(throwable -> { + throwableFuture.complete(throwable); + return null; + }); + } + + @Override + public DbResult exceptionally(Consumer exceptionHandler) { + resultFuture.exceptionally(throwable -> { + exceptionHandler.accept(throwable); + return null; + }); + return this; + } + + @Override + public CompletionStage exceptionFuture() { + return throwableFuture; + } + + CompletionStage resultFuture() { + return resultFuture; + } + + } + + private static final class MongoDbQueryResult extends MongoDbAbstractResult> { + + private MongoDbQueryResult(CompletionStage> dbRowsFuture) { + super(dbRowsFuture); + } + + @Override + public DbResult whenDml(Consumer consumer) { + throw new IllegalStateException("Statement is not DML."); + } + + @Override + public DbResult whenRs(Consumer> consumer) { + resultFuture().thenAccept(consumer); + return this; + } + + @Override + public CompletionStage dmlFuture() { + throw new IllegalStateException("Statement is not DML."); + } + + @Override + public CompletionStage> rsFuture() { + return resultFuture(); + } + + } + + private static final class MongoDbDmlResult extends MongoDbAbstractResult { + + private MongoDbDmlResult(CompletionStage dmlResultFuture) { + super(dmlResultFuture); + } + + @Override + public DbResult whenDml(Consumer consumer) { + resultFuture().thenAccept(consumer); + return this; + } + + @Override + public DbResult whenRs(Consumer> consumer) { + throw new IllegalStateException("Statement is not query."); + } + + @Override + public CompletionStage dmlFuture() { + return resultFuture(); + } + + @Override + public CompletionStage> rsFuture() { + throw new IllegalStateException("Statement is not query."); + } + + } + + private static final Logger LOGGER = Logger.getLogger(MongoDbStatementGeneric.class.getName()); + + private DbStatementType dbStatementType; + + private MongoStatement statement; + + MongoDbStatementGeneric( + DbStatementType dbStatementType, + MongoDatabase db, + String statementName, + String statement, + DbMapperManager dbMapperManager, + MapperManager mapperManager, + InterceptorSupport interceptors + ) { + super(dbStatementType, + db, + statementName, + statement, + dbMapperManager, + mapperManager, + interceptors); + this.dbStatementType = dbStatementType; + } + + @Override + public CompletionStage execute() { + statement = new MongoStatement(dbStatementType, READER_FACTORY, build()); + switch (statement.getOperation()) { + case QUERY: + dbStatementType = DbStatementType.QUERY; + break; + case INSERT: + dbStatementType = DbStatementType.INSERT; + break; + case UPDATE: + dbStatementType = DbStatementType.UPDATE; + break; + case DELETE: + dbStatementType = DbStatementType.DELETE; + break; + // Command not related to a specific collection can be executed only as generic statement. + case COMMAND: + dbStatementType = DbStatementType.COMMAND; + break; + default: + throw new IllegalStateException( + String.format("Unexpected value for generic statement: %s", statement.getOperation())); + } + return super.execute(); + } + + @Override + protected CompletionStage doExecute( + CompletionStage dbContextFuture, + CompletableFuture statementFuture, + CompletableFuture queryFuture + ) { + switch (dbStatementType) { + case QUERY: + return CompletableFuture.completedFuture(new MongoDbQueryResult( + MongoDbQueryExecutor.executeQuery( + this, + dbContextFuture, + statementFuture, + queryFuture) + )); + case INSERT: + case UPDATE: + case DELETE: + return CompletableFuture.completedFuture(new MongoDbDmlResult( + MongoDbDMLExecutor.executeDml( + this, + dbStatementType, + statement, + dbContextFuture, + statementFuture, + queryFuture) + )); + case COMMAND: + return CompletableFuture.completedFuture(new MongoDbQueryResult( + MongoDbCommandExecutor.executeCommand( + this, + dbContextFuture, + statementFuture, + queryFuture) + )); + default: + throw new UnsupportedOperationException( + String.format("Operation %s is not supported.", dbStatementType.name())); + } + } + +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementGet.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementGet.java new file mode 100644 index 000000000..48d4a20f9 --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementGet.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +import io.helidon.common.mapper.MapperManager; +import io.helidon.common.reactive.Single; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbStatementGet; +import io.helidon.dbclient.DbStatementType; +import io.helidon.dbclient.common.InterceptorSupport; +import io.helidon.dbclient.mongodb.MongoDbTransaction.TransactionManager; + +import com.mongodb.reactivestreams.client.MongoDatabase; + +/** + * Statement for GET operation in mongoDB. + */ +public class MongoDbStatementGet implements DbStatementGet { + + private final MongoDbStatementQuery theQuery; + + MongoDbStatementGet(MongoDatabase db, + String statementName, + String statement, + DbMapperManager dbMapperManager, + MapperManager mapperManager, + InterceptorSupport interceptors) { + this.theQuery = new MongoDbStatementQuery(DbStatementType.GET, + db, + statementName, + statement, + dbMapperManager, + mapperManager, + interceptors); + } + + @Override + public MongoDbStatementGet params(List parameters) { + theQuery.params(parameters); + return this; + } + + @Override + public MongoDbStatementGet params(Map parameters) { + theQuery.params(parameters); + return this; + } + + @Override + public MongoDbStatementGet namedParam(Object parameters) { + theQuery.namedParam(parameters); + return this; + } + + @Override + public MongoDbStatementGet indexedParam(Object parameters) { + theQuery.indexedParam(parameters); + return this; + } + + @Override + public MongoDbStatementGet addParam(Object parameter) { + theQuery.addParam(parameter); + return this; + } + + @Override + public MongoDbStatementGet addParam(String name, Object parameter) { + theQuery.addParam(name, parameter); + return this; + } + + @Override + public CompletionStage> execute() { + return theQuery.execute() + .thenApply(dbRows -> Single.from(dbRows.publisher())) + .thenCompose(Single::toOptionalStage); + } + + /** + * Set target transaction for this statement. + * + * @param tx MongoDB transaction session + * @return MongoDB statement builder + */ + MongoDbStatementGet inTransaction(TransactionManager tx) { + theQuery.inTransaction(tx); + return this; + } + +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementQuery.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementQuery.java new file mode 100644 index 000000000..997524a6e --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementQuery.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.logging.Logger; + +import io.helidon.common.mapper.MapperManager; +import io.helidon.dbclient.DbInterceptorContext; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.dbclient.DbStatementQuery; +import io.helidon.dbclient.DbStatementType; +import io.helidon.dbclient.common.InterceptorSupport; + +import com.mongodb.reactivestreams.client.MongoDatabase; + +/** + * Implementation of a query for MongoDB. + */ +class MongoDbStatementQuery extends MongoDbStatement> implements DbStatementQuery { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(MongoDbStatementQuery.class.getName()); + + MongoDbStatementQuery( + DbStatementType dbStatementType, + MongoDatabase db, + String statementName, + String statement, + DbMapperManager dbMapperManager, + MapperManager mapperManager, + InterceptorSupport interceptors + ) { + super( + dbStatementType, + db, + statementName, + statement, + dbMapperManager, + mapperManager, + interceptors); + } + + @Override + protected CompletionStage> doExecute( + CompletionStage dbContextFuture, + CompletableFuture statementFuture, + CompletableFuture queryFuture + ) { + return MongoDbQueryExecutor.executeQuery( + this, + dbContextFuture, + statementFuture, + queryFuture); + } + +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbTransaction.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbTransaction.java new file mode 100644 index 000000000..8e700fcc4 --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbTransaction.java @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Logger; + +import io.helidon.common.mapper.MapperManager; +import io.helidon.dbclient.DbMapperManager; +import io.helidon.dbclient.DbStatementDml; +import io.helidon.dbclient.DbStatementGeneric; +import io.helidon.dbclient.DbStatementGet; +import io.helidon.dbclient.DbStatementQuery; +import io.helidon.dbclient.DbStatements; +import io.helidon.dbclient.DbTransaction; +import io.helidon.dbclient.common.InterceptorSupport; + +import com.mongodb.reactivestreams.client.ClientSession; +import com.mongodb.reactivestreams.client.MongoDatabase; + +/** + * Transaction execute implementation for MongoDB. + */ +public class MongoDbTransaction extends MongoDbExecute implements DbTransaction { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(MongoDbTransaction.class.getName()); + + static final class TransactionManager { + + /** MongoDB client session (transaction handler). */ + private final ClientSession tx; + /** Whether transaction shall always finish with rollback. */ + private final AtomicBoolean rollbackOnly; + /** All transaction statements were processed. */ + private final AtomicBoolean finished; + /** Set of statements being processed (started, but not finished yet). */ + private final Set statements; + /** Shared resources lock. */ + private final Lock lock; + + /** + * Creates an instance of transaction manager. + * + * @param tx MongoDB client session (transaction handler) + */ + private TransactionManager(ClientSession tx) { + this.tx = tx; + this.tx.startTransaction(); + this.rollbackOnly = new AtomicBoolean(false); + this.finished = new AtomicBoolean(false); + this.statements = ConcurrentHashMap.newKeySet(); + this.lock = new ReentrantLock(); + } + + /** + * Set current transaction as rollback only. + * Transaction can't be completed successfully after this. + * Locks on current transaction manager instance lock. + */ + void rollbackOnly() { + lock.lock(); + try { + rollbackOnly.set(false); + } finally { + lock.unlock(); + } + LOGGER.finest(() -> String.format("Transaction marked as failed")); + } + + /** + * Mark provided statement as finished. + * Locks on current transaction manager instance lock. + * + * @param stmt statement to mark + */ + void stmtFinished(MongoDbStatement stmt) { + lock.lock(); + try { + statements.remove(stmt); + if (statements.isEmpty() && this.finished.get()) { + commitOrRollback(); + } + } finally { + lock.unlock(); + } + LOGGER.finest(() -> String.format("Statement %s marked as finished in transaction", stmt.statementName())); + } + + /** + * Mark provided statement as failed. + * Transaction can't be completed successfully after this. + * Locks on current transaction manager instance lock. + * + * @param stmt statement to mark + */ + void stmtFailed(MongoDbStatement stmt) { + lock.lock(); + try { + rollbackOnly.set(false); + statements.remove(stmt); + if (statements.isEmpty() && this.finished.get()) { + tx.abortTransaction(); + } + } finally { + lock.unlock(); + } + LOGGER.finest(() -> String.format("Statement %s marked as failed in transaction", stmt.statementName())); + } + + /** + * Notify transaction manager that all statements in the transaction were started. + * Locks on current transaction manager instance lock. + */ + void allRegistered() { + lock.lock(); + try { + this.finished.set(true); + if (statements.isEmpty()) { + commitOrRollback(); + } + } finally { + lock.unlock(); + } + LOGGER.finest(() -> String.format("All statements are registered in current transaction")); + } + + /** + * Complete transaction. + * Transaction is completed depending on rollback only flag. + * Must run while holding the {@code lock}! + */ + private void commitOrRollback() { + // FIXME: Handle + if (rollbackOnly.get()) { + tx.abortTransaction(); + } else { + tx.commitTransaction(); + } + } + + /** + * Get MongoDB client session (transaction handler). + * + * @return MongoDB client session + */ + ClientSession tx() { + return tx; + } + + /** + * Add statement to be monitored by transaction manager. + * All statements in transaction must be registered using this method. + * + * @param stmt statement to add + */ + void addStatement(MongoDbStatement stmt) { + statements.add(stmt); + } + + } + + /** Transaction manager instance. */ + private final TransactionManager txManager; + + /** + * Creates an instance of MongoDB transaction handler. + * + * @param db MongoDB database + * @param tx MongoDB client session (transaction handler) + * @param statements configured statements to be used by database provider + * @param dbMapperManager mapper manager of all configured DB mappers + * @param mapperManager mapper manager of all configured mappers + * @param interceptors interceptors to be executed + */ + MongoDbTransaction( + MongoDatabase db, + ClientSession tx, + DbStatements statements, + DbMapperManager dbMapperManager, + MapperManager mapperManager, + InterceptorSupport interceptors + ) { + super(db, statements, dbMapperManager, mapperManager, interceptors); + this.txManager = new TransactionManager(tx); + } + + @Override + public DbStatementQuery createNamedQuery(String statementName, String statement) { + return ((MongoDbStatementQuery) super.createNamedQuery(statementName, statement)).inTransaction(txManager); + } + + @Override + public DbStatementGet createNamedGet(String statementName, String statement) { + return ((MongoDbStatementGet) super.createNamedGet(statementName, statement)).inTransaction(txManager); + } + + @Override + public DbStatementDml createNamedDmlStatement(String statementName, String statement) { + return ((MongoDbStatementDml) super.createNamedDmlStatement(statementName, statement)).inTransaction(txManager); + } + + @Override + public DbStatementDml createNamedInsert(String statementName, String statement) { + return ((MongoDbStatementDml) super.createNamedInsert(statementName, statement)).inTransaction(txManager); + } + + @Override + public DbStatementDml createNamedUpdate(String statementName, String statement) { + return ((MongoDbStatementDml) super.createNamedUpdate(statementName, statement)).inTransaction(txManager); + } + + @Override + public DbStatementDml createNamedDelete(String statementName, String statement) { + return ((MongoDbStatementDml) super.createNamedDelete(statementName, statement)).inTransaction(txManager); + } + + @Override + public DbStatementGeneric createNamedStatement(String statementName, String statement) { + return ((MongoDbStatementGeneric) super.createNamedStatement(statementName, statement)).inTransaction(txManager); + } + + @Override + public void rollback() { + this.txManager.rollbackOnly(); + } + + TransactionManager txManager() { + return txManager; + } + +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/StatementParsers.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/StatementParsers.java new file mode 100644 index 000000000..71b285acd --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/StatementParsers.java @@ -0,0 +1,553 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.function.Consumer; +import java.util.logging.Logger; + +import javax.json.Json; + +/** + * Statement parameter parsers. + */ +final class StatementParsers { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(StatementParsers.class.getName()); + + static String toJson(Object value) { + if ((value instanceof Integer) || (value instanceof Short) || (value instanceof Byte)){ + return Json.createValue(((Number) value).intValue()).toString(); + } + if (value instanceof Long) { + return Json.createValue((Long) value).toString(); + } + if ((value instanceof Double) || (value instanceof Float)) { + return Json.createValue(((Number) value).doubleValue()).toString(); + } + if (value instanceof BigInteger) { + return Json.createValue((BigInteger) value).toString(); + } + if (value instanceof BigDecimal) { + return Json.createValue((BigDecimal) value).toString(); + } + if (value instanceof Boolean) { + return value.toString(); + } + // Check instanceof Number is more expensive than final types, it shall be at the end + if (value instanceof Number) { + return value.toString(); + } + // String.valueOf handles null value + return Json.createValue(String.valueOf(value)).toString(); + } + + private StatementParsers() { + } + + static StatementParser indexedParser(String statement, List indexedParams) { + return new IndexedParser(statement, indexedParams); + } + + static StatementParser namedParser(String statement, Map indexedParams) { + return new NamedParser(statement, indexedParams); + } + + @FunctionalInterface + interface StatementParser { + String convert(); + } + + // Slightly modified copy-paste from JDBC statement parser + // Replaces "$name" named parameters with value from mappings if exists + abstract static class Parser { + /** + * Parser ACTION to be performed. + */ + @FunctionalInterface + interface Action { + void process(Parser parser); + } + + /** + * States used in state machine. + */ + enum State { + STATEMENT, // Common statement + STRING, // SQL string + PAR_BEG, // After colon, expecting parameter name + PARAMETER // Processing parameter name + } + + /** + * Do nothing. + * + * @param parser parser instance + */ + static void doNothing(Parser parser) { + } + + /** + * Copy character from input string to output as is. + * + * @param parser parser instance + */ + static void copyChar(Parser parser) { + parser.sb.append(parser.c); + } + + /** + * SQL statement to be parsed. + */ + private final String statement; + /** + * Target SQL statement builder. + */ + private final StringBuilder sb; + /** + * Temporary string storage. + */ + private final StringBuilder nap; + + /** + * Character being currently processed. + */ + private char c; + + private Parser(String statement) { + this.sb = new StringBuilder(statement.length()); + this.nap = new StringBuilder(32); + this.statement = statement; + this.c = '\0'; + } + + String statement() { + return statement; + } + + StringBuilder sb() { + return sb; + } + + StringBuilder nap() { + return nap; + } + + char c() { + return c; + } + + void c(char newC) { + c = newC; + } + } + + /** + * Mapping parser state machine. + */ + static final class NamedParser implements StatementParser { + + /** + * First character of named parameter identifier. + */ + private static final char PAR_BEG = '$'; + + /** + * Parser ACTION interface. + */ + private interface Action extends Consumer {} + + /** + * States used in state machine. + */ + enum State { + STATEMENT, // Common statement + STRING, // SQL string + PAR_BEG, // After colon, expecting parameter name + PARAMETER // Processing parameter name + } + + /** + * Character classes used in state machine. + */ + private enum CharClass { + LETTER, // Letter + NUMBER, // Number + QUOTE, // Double quote, begins/ends string in JSON + COLON, // Colon, not a parameter when part of the sequence + DOLLAR, // Dollar sign, begins named parameter + OTHER; // Other character + + // This is far from optimal code, but direct translation table would be too large for Java char + private static CharClass charClass(char c) { + switch (c) { + case '"': return QUOTE; + case PAR_BEG: return DOLLAR; + case ':': return COLON; + default: + return Character.isLetter(c) + ? LETTER + : (Character.isDigit(c) ? NUMBER : OTHER); + } + } + + } + + /** + * States TRANSITION table. + */ + private static final State[][] TRANSITION = { + // Transitions from STATEMENT state + { + State.STATEMENT, // LETTER: regular part of the statement, keep processing it + State.STATEMENT, // NUMBER: regular part of the statement, keep processing it + State.STRING, // QUOTE: beginning of JSON string processing, switch to STRING state + State.STATEMENT, // COLON: regular part of the statement, keep processing it + State.PAR_BEG, // DOLLAR: possible beginning of named parameter, switch to PAR_BEG state + State.STATEMENT // OTHER: regular part of the statement, keep processing it + }, + // Transitions from STRING state + { + State.STRING, // LETTER: regular part of the JSON string, keep processing it + State.STRING, // NUMBER: regular part of the JSON string, keep processing it + State.STATEMENT, // QUOTE: end of JSON string processing, go back to STATEMENT state + State.STRING, // COLON: regular part of the JSON string, keep processing it + State.STRING, // DOLLAR: regular part of the JSON string, keep processing it + State.STRING // OTHER: regular part of the JSON string, keep processing it + }, + // Transitions from PAR_BEG state + { + State.PARAMETER, // LETTER: first character of named parameter, switch to PARAMETER state + State.STATEMENT, // NUMBER: can't be first character of named parameter, go back to STATEMENT state + State.STRING, // QUOTE: not a named parameter but beginning of JSON string processing, + // switch to STRING state + State.STATEMENT, // COLON: can't be first character of named parameter, go back to STATEMENT state + State.PAR_BEG, // DOLLAR: not a named parameter but possible beginning of another named parameter, + // retry named parameter processing + State.STATEMENT // OTHER: can't be first character of named parameter, go back to STATEMENT state + }, + // Transitions from PARAMETER state + { + State.PARAMETER, // LETTER: next character of named parameter, keep processing it + State.PARAMETER, // NUMBER: next character of named parameter, keep processing it + State.STATEMENT, // QUOTE: end of named parameter and beginning of JSON string processing, + // switch to STRING state + State.STATEMENT, // COLON: not a named parameter, colon is part of the name, go back to STATEMENT state + State.PAR_BEG, // DOLLAR: end of named parameter and possible beginning of another named parameter + // switch to PAR_BEG state + State.STATEMENT // OTHER: can't be next character of named parameter, go back to STATEMENT state + } + }; + + + /** + * States TRANSITION ACTION table. + */ + private static final Action[][] ACTION = { + // Actions performed on transitions from STATEMENT state + { + NamedParser::copyCurrChar, // LETTER: copy regular statement character to output + NamedParser::copyCurrChar, // NUMBER: copy regular statement character to output + NamedParser::copyCurrChar, // QUOTE: copy regular statement character to output + NamedParser::copyCurrChar, // COLON: copy regular statement character to output + NamedParser::storeCharPos, // DOLLAR: store current character position + NamedParser::copyCurrChar // OTHER: copy regular statement character to output + }, + // Actions performed on transitions from STRING state + { + NamedParser::copyCurrChar, // LETTER: copy regular statement character to output + NamedParser::copyCurrChar, // NUMBER: copy regular statement character to output + NamedParser::copyCurrChar, // QUOTE: copy regular statement character to output + NamedParser::copyCurrChar, // COLON: copy regular statement character to output + NamedParser::copyCurrChar, // DOLLAR: copy regular statement character to output + NamedParser::copyCurrChar // OTHER: copy regular statement character to output + }, + // Actions performed on transitions from PAR_BEG state + { + NamedParser::doNothing, // LETTER: do nothing, parameter name was not finished yet + NamedParser::copyStoredChars, // NUMBER: copy characters from stored position up to current character to output + NamedParser::copyStoredChars, // QUOTE: copy characters from stored position up to current character to output + NamedParser::copyStoredChars, // COLON: copy characters from stored position up to current character to output + NamedParser::copyStoredCharsStoreCharPos, // DOLLAR: copy characters from stored position + // up to current character to output + NamedParser::copyStoredChars // OTHER: copy characters from stored position up to current character to output + }, + // Actions performed on transitions from PARAMETER state + { + NamedParser::doNothing, // LETTER: do nothing, parameter name was not finished yet + NamedParser::doNothing, // NUMBER: do nothing, parameter name was not finished yet + NamedParser::finishParamCopyCurrChar, // QUOTE: finish parameter processing + NamedParser::copyStoredChars, // COLON: copy characters from stored position + // up to current character to output + NamedParser::finishParamStoreCharPos, // DOLLAR: finish parameter processing + NamedParser::finishParamCopyCurrChar // OTHER: finish parameter processing + } + }; + + /** + * Do nothing. + * + * @param parser parser instance + */ + static void doNothing(NamedParser parser) { + } + + /** + * Copy current character from input string to output as is. + * + * @param parser parser instance + */ + static void copyCurrChar(NamedParser parser) { + parser.sb.append(parser.statement.charAt(parser.curPos)); + } + + /** + * Copy characters from stored position up to current character from input string to output as is. + * + * @param parser parser instance + */ + static void copyStoredChars(NamedParser parser) { + parser.sb.append(parser.statement, parser.paramBegPos, parser.curPos + 1); + } + + /** + * Store current character position into parser instance. + * + * @param parser parser instance + */ + private static void storeCharPos(NamedParser parser) { + parser.paramBegPos = parser.curPos; + } + + /** + * Copy characters from stored position up to previous character from input string to output as is. + * Store current character position into parser instance. + * + * @param parser parser instance + */ + static void copyStoredCharsStoreCharPos(NamedParser parser) { + parser.sb.append(parser.statement, parser.paramBegPos, parser.curPos); + parser.paramBegPos = parser.curPos; + } + + /** + * Finish parameter processing and copy current character from input string to output. + * Parameter name is replaced by mapped value if mapping for given parameter exists. + * Otherwise parameter name is left in statement as is without replacing it. + * + * @param parser parser instance + */ + private static void finishParamCopyCurrChar(NamedParser parser) { + String parName = parser.statement.substring(parser.paramBegPos + 1, parser.curPos); + if (parser.mappings.containsKey(parName)) { + parser.sb.append(toJson(parser.mappings.get(parName))); + parser.sb.append(parser.statement.charAt(parser.curPos)); + } else { + parser.sb.append(parser.statement, parser.paramBegPos, parser.curPos + 1); + } + } + + /** + * Finish parameter processing and store current character position into parser instance. + * Parameter name is replaced by mapped value if mapping for given parameter exists. + * Otherwise parameter name is left in statement as is without replacing it. + * + * @param parser parser instance + */ + private static void finishParamStoreCharPos(NamedParser parser) { + String parName = parser.statement.substring(parser.paramBegPos + 1, parser.curPos); + if (parser.mappings.containsKey(parName)) { + parser.sb.append(toJson(parser.mappings.get(parName))); + } else { + parser.sb.append(parser.statement, parser.paramBegPos, parser.curPos); + } + parser.paramBegPos = parser.curPos; + } + + /** Parameter name to value mapping. */ + private final Map mappings; + /** SQL statement to be parsed. */ + private final String statement; + /** Target SQL statement builder. */ + private final StringBuilder sb; + /** Current position in the parsed String. */ + private int curPos; + /** Parameter beginning position (index of '$' character). */ + private int paramBegPos; + + /** + * Character class of character being currently processed. + */ + private CharClass cl; + + NamedParser(String statement, Map mappings) { + this.statement = statement; + this.sb = new StringBuilder(statement.length()); + this.mappings = mappings; + this.cl = null; + } + + @Override + public String convert() { + State state = State.STATEMENT; // Initial state: common statement processing + final int len = statement.length(); + for (curPos = 0; curPos < len; curPos++) { + cl = CharClass.charClass(statement.charAt(curPos)); + ACTION[state.ordinal()][cl.ordinal()].accept(this); + state = TRANSITION[state.ordinal()][cl.ordinal()]; + } + switch (state) { + case PAR_BEG: + sb.append(statement, paramBegPos, len); + break; + case PARAMETER: + String parName = statement.substring(paramBegPos + 1, len); + if (mappings.containsKey(parName)) { + sb.append(toJson(mappings.get(parName))); + } else { + sb.append(statement, paramBegPos, len); + } + break; + default: + } + LOGGER.fine(() -> String.format("Named Statement %s", sb.toString())); + return sb.toString(); + } + + } + + static final class IndexedParser extends Parser implements StatementParser { + + /** + * First character of named parameter identifier. + */ + private static final char PAR_LT = '?'; + + /** + * Character classes used in state machine. + */ + private enum CharClass { + LT, // Letter + NUM, // Number + DQ, // Double quote, begins/ends string in JSON + QST, // Question mark, parameter + OTH; // Other character + + // This is far from optimal code, but direct translation table would be too large for Java char + private static CharClass charClass(char c) { + // DO NOT replace with a single line combined ternary expression! + if (Character.isLetter(c)) { + return LT; + } + + if (Character.isDigit(c)) { + return NUM; + } + + if (c == '"') { + return DQ; + } + + if (c == PAR_LT) { + return QST; + } + + return OTH; + } + + } + + /** + * States TRANSITION table. + */ + private static final State[][] TRANSITION = { + // Transition from STATEMENT + { + State.STATEMENT, + State.STATEMENT, + State.STRING, + State.STATEMENT, + State.STATEMENT + }, + // Transition from STRING + { + State.STRING, + State.STRING, + State.STATEMENT, + State.STRING, + State.STRING + } + }; + + private static final Action COPY_ACTION = Parser::copyChar; + private static final Action VAAP_ACTION = IndexedParser::nextParam; + /** + * States TRANSITION ACTION table. + */ + private static final Action[][] ACTION = { + // LETTER NUMBER DQ QST OTHER + {COPY_ACTION, COPY_ACTION, COPY_ACTION, VAAP_ACTION, COPY_ACTION}, // STATEMENT + {COPY_ACTION, COPY_ACTION, COPY_ACTION, COPY_ACTION, COPY_ACTION} // STRING + }; + + /** + * Append next parameter from parameters list. + * + * @param parser parser instance + */ + private static void nextParam(Parser parser) { + if (((IndexedParser) parser).parIt.hasNext()) { + parser.sb.append(toJson(((IndexedParser) parser).parIt.next())); + } else { + parser.sb.append(parser.c); + } + } + + private final List parameters; + private final ListIterator parIt; + /** + * Character class of character being currently processed. + */ + private CharClass cl; + + private IndexedParser(String statement, List parameters) { + super(statement); + this.parameters = parameters; + this.parIt = parameters.listIterator(); + this.cl = null; + } + + @Override + public String convert() { + State state = State.STATEMENT; // Initial state: common statement processing + int len = statement().length(); + for (int i = 0; i < len; i++) { + c(statement().charAt(i)); + cl = CharClass.charClass(c()); + ACTION[state.ordinal()][cl.ordinal()].process(this); + state = TRANSITION[state.ordinal()][cl.ordinal()]; + } + LOGGER.fine(() -> String.format("Indexed Statement %s", sb().toString())); + return sb().toString(); + } + + } +} diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/package-info.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/package-info.java new file mode 100644 index 000000000..b1f989cbb --- /dev/null +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ +/** + * Helidon DB integration for reactive mongoDB. + */ +package io.helidon.dbclient.mongodb; diff --git a/dbclient/mongodb/src/main/java/module-info.java b/dbclient/mongodb/src/main/java/module-info.java new file mode 100644 index 000000000..b7ff5b136 --- /dev/null +++ b/dbclient/mongodb/src/main/java/module-info.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Helidon Common Mapper. + */ +module io.helidon.dbclient.mongodb { + requires java.logging; + requires java.sql; + + requires transitive java.json; + requires mongodb.driver.reactivestreams; + requires org.mongodb.driver.core; + requires org.mongodb.bson; + requires org.mongodb.driver.async.client; + requires transitive io.helidon.common.configurable; + requires transitive io.helidon.dbclient; + requires transitive io.helidon.dbclient.common; + requires org.reactivestreams; + + exports io.helidon.dbclient.mongodb; + provides io.helidon.dbclient.spi.DbClientProvider with io.helidon.dbclient.mongodb.MongoDbClientProvider; +} diff --git a/dbclient/mongodb/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbClientProvider b/dbclient/mongodb/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbClientProvider new file mode 100644 index 000000000..ddc754dee --- /dev/null +++ b/dbclient/mongodb/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbClientProvider @@ -0,0 +1,17 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +io.helidon.dbclient.mongodb.MongoDbClientProvider diff --git a/dbclient/mongodb/src/test/java/io/helidon/dbclient/mongodb/StatementParsersTest.java b/dbclient/mongodb/src/test/java/io/helidon/dbclient/mongodb/StatementParsersTest.java new file mode 100644 index 000000000..4e7e5a7da --- /dev/null +++ b/dbclient/mongodb/src/test/java/io/helidon/dbclient/mongodb/StatementParsersTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.mongodb; + +import java.util.HashMap; +import java.util.Map; + +import io.helidon.dbclient.mongodb.StatementParsers.NamedParser; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit test for {@link StatementParsers}. + */ +public class StatementParsersTest { + + /** + * Test simple MongoDb statement with parameters and mapping. + */ + @Test + void testStatementWithParameters() { + String stmtIn = "{ id: { $gt: $idmin }, id: { $lt: $idmax } }"; + Map mapping = new HashMap<>(2); + mapping.put("idmin", 1); + mapping.put("idmax", 7); + String stmtExp = stmtIn + .replace("$idmin", String.valueOf(mapping.get("idmin"))) + .replace("$idmax", String.valueOf(mapping.get("idmax"))); + NamedParser parser = new NamedParser(stmtIn, mapping); + String stmtOut = parser.convert(); + assertEquals(stmtExp, stmtOut); + } + +} diff --git a/dbclient/pom.xml b/dbclient/pom.xml new file mode 100644 index 000000000..aba42a990 --- /dev/null +++ b/dbclient/pom.xml @@ -0,0 +1,47 @@ + + + + + 4.0.0 + + helidon-project + io.helidon + 2.0-SNAPSHOT + + pom + + io.helidon.dbclient + helidon-dbclient-project + Helidon DB Client Project + + Support for reactive database client access. + + + dbclient + common + jdbc + mongodb + tracing + metrics + metrics-jdbc + health + jsonp + webserver-jsonp + + diff --git a/dbclient/tracing/pom.xml b/dbclient/tracing/pom.xml new file mode 100644 index 000000000..faf4369f7 --- /dev/null +++ b/dbclient/tracing/pom.xml @@ -0,0 +1,51 @@ + + + + + 4.0.0 + + io.helidon.dbclient + helidon-dbclient-project + 2.0-SNAPSHOT + + + helidon-dbclient-tracing + Helidon DB Client Tracing + + Tracing support for Helidon DB + + + + io.helidon.dbclient + helidon-dbclient + + + io.helidon.tracing + helidon-tracing-config + + + io.opentracing + opentracing-api + + + io.opentracing + opentracing-util + + + diff --git a/dbclient/tracing/src/main/java/io/helidon/dbclient/tracing/DbClientTracing.java b/dbclient/tracing/src/main/java/io/helidon/dbclient/tracing/DbClientTracing.java new file mode 100644 index 000000000..ed42516ae --- /dev/null +++ b/dbclient/tracing/src/main/java/io/helidon/dbclient/tracing/DbClientTracing.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.tracing; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import io.helidon.common.HelidonFeatures; +import io.helidon.common.HelidonFlavor; +import io.helidon.common.context.Context; +import io.helidon.config.Config; +import io.helidon.dbclient.DbInterceptor; +import io.helidon.dbclient.DbInterceptorContext; +import io.helidon.tracing.config.SpanTracingConfig; +import io.helidon.tracing.config.TracingConfigUtil; + +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; +import io.opentracing.util.GlobalTracer; + +/** + * Tracing interceptor. + * This interceptor is added through Java Service loader. + */ +public class DbClientTracing implements DbInterceptor { + static { + HelidonFeatures.register(HelidonFlavor.SE, "DbClient", "Tracing"); + } + + /** + * Create a new tracing interceptor based on the configuration. + * + * @param config configuration node for this interceptor (currently ignored) + * @return a new tracing interceptor + */ + public static DbClientTracing create(Config config) { + return create(); + } + + /** + * Create a new interceptor to trace requests. + * @return a new tracing interceptor + */ + public static DbClientTracing create() { + return new DbClientTracing(); + } + + @Override + public CompletableFuture statement(DbInterceptorContext interceptorContext) { + SpanTracingConfig spanConfig = TracingConfigUtil.spanConfig("dbclient", "statement"); + + if (!spanConfig.enabled()) { + return CompletableFuture.completedFuture(interceptorContext); + } + + Context context = interceptorContext.context(); + Tracer tracer = context.get(Tracer.class).orElseGet(GlobalTracer::get); + + // now if span context is missing, we build a span without a parent + Tracer.SpanBuilder spanBuilder = tracer.buildSpan(interceptorContext.statementName()); + + context.get(SpanContext.class) + .ifPresent(spanBuilder::asChildOf); + + Span span = spanBuilder.start(); + + span.setTag("db.operation", interceptorContext.statementType().toString()); + if (spanConfig.logEnabled("statement", true)) { + Tags.DB_STATEMENT.set(span, interceptorContext.statement()); + } + Tags.COMPONENT.set(span, "dbclient"); + Tags.DB_TYPE.set(span, interceptorContext.dbType()); + + interceptorContext.statementFuture().thenAccept(nothing -> { + if (spanConfig.logEnabled("statement-finish", true)) { + span.log(Map.of("type", "statement")); + } + }); + + interceptorContext.resultFuture().thenAccept(count -> { + if (spanConfig.logEnabled("result-finish", true)) { + span.log(Map.of("type", "result", + "count", count)); + } + span.finish(); + }).exceptionally(throwable -> { + Tags.ERROR.set(span, Boolean.TRUE); + span.log(Map.of("event", "error", + "error.kind", "Exception", + "error.object", throwable, + "message", throwable.getMessage())); + span.finish(); + return null; + }); + + return CompletableFuture.completedFuture(interceptorContext); + } +} diff --git a/dbclient/tracing/src/main/java/io/helidon/dbclient/tracing/DbClientTracingProvider.java b/dbclient/tracing/src/main/java/io/helidon/dbclient/tracing/DbClientTracingProvider.java new file mode 100644 index 000000000..59dfd60ca --- /dev/null +++ b/dbclient/tracing/src/main/java/io/helidon/dbclient/tracing/DbClientTracingProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.tracing; + +import io.helidon.config.Config; +import io.helidon.dbclient.DbInterceptor; +import io.helidon.dbclient.spi.DbInterceptorProvider; + +/** + * Provider of tracing interceptors. + */ +public class DbClientTracingProvider implements DbInterceptorProvider { + @Override + public String configKey() { + return "tracing"; + } + + @Override + public DbInterceptor create(Config config) { + return DbClientTracing.create(config); + } +} diff --git a/dbclient/tracing/src/main/java/io/helidon/dbclient/tracing/package-info.java b/dbclient/tracing/src/main/java/io/helidon/dbclient/tracing/package-info.java new file mode 100644 index 000000000..7bc8e2bcb --- /dev/null +++ b/dbclient/tracing/src/main/java/io/helidon/dbclient/tracing/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ +/** + * Tracing support for Helidon DB. + */ +package io.helidon.dbclient.tracing; diff --git a/dbclient/tracing/src/main/java/module-info.java b/dbclient/tracing/src/main/java/module-info.java new file mode 100644 index 000000000..4163e446e --- /dev/null +++ b/dbclient/tracing/src/main/java/module-info.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Helidon DB Client Tracing. + */ +module io.helidon.dbclient.tracing { + requires java.logging; + + requires io.helidon.dbclient; + requires io.helidon.tracing.config; + + requires io.opentracing.api; + requires io.opentracing.util; + + exports io.helidon.dbclient.tracing; + + provides io.helidon.dbclient.spi.DbInterceptorProvider with io.helidon.dbclient.tracing.DbClientTracingProvider; +} diff --git a/dbclient/tracing/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbInterceptorProvider b/dbclient/tracing/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbInterceptorProvider new file mode 100644 index 000000000..c97f854a3 --- /dev/null +++ b/dbclient/tracing/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbInterceptorProvider @@ -0,0 +1,17 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +io.helidon.dbclient.tracing.DbClientTracingProvider diff --git a/dbclient/webserver-jsonp/etc/spotbugs/exclude.xml b/dbclient/webserver-jsonp/etc/spotbugs/exclude.xml new file mode 100644 index 000000000..2e6e93054 --- /dev/null +++ b/dbclient/webserver-jsonp/etc/spotbugs/exclude.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/dbclient/webserver-jsonp/pom.xml b/dbclient/webserver-jsonp/pom.xml new file mode 100644 index 000000000..d015e542b --- /dev/null +++ b/dbclient/webserver-jsonp/pom.xml @@ -0,0 +1,50 @@ + + + + + 4.0.0 + + io.helidon.dbclient + helidon-dbclient-project + 2.0-SNAPSHOT + + + helidon-dbclient-webserver-jsonp + Helidon DB Client WebServer JSON-P support + + + etc/spotbugs/exclude.xml + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.dbclient + helidon-dbclient + + + org.glassfish + javax.json + + + + diff --git a/dbclient/webserver-jsonp/src/main/java/io/helidon/dbclient/webserver/jsonp/DbResultSupport.java b/dbclient/webserver-jsonp/src/main/java/io/helidon/dbclient/webserver/jsonp/DbResultSupport.java new file mode 100644 index 000000000..ba3a2847c --- /dev/null +++ b/dbclient/webserver-jsonp/src/main/java/io/helidon/dbclient/webserver/jsonp/DbResultSupport.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.dbclient.webserver.jsonp; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Flow; +import java.util.logging.Logger; + +import javax.json.Json; +import javax.json.JsonBuilderFactory; +import javax.json.JsonObject; +import javax.json.JsonWriter; +import javax.json.JsonWriterFactory; + +import io.helidon.common.http.DataChunk; +import io.helidon.dbclient.DbResult; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.media.common.ContentWriters; +import io.helidon.webserver.Handler; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.ServerResponse; +import io.helidon.webserver.Service; + +/** + * Support to write {@link io.helidon.dbclient.DbRows} directly to webserver. + * This result support creates an array of json objects and writes them to the response entity. + * + * @deprecated This class is a hack to work around insufficient support for stream of objects in + * WebServer - the update to WebServer is in progress. This module will be removed. + */ +@Deprecated +public final class DbResultSupport implements Service, Handler { + + private static final Logger LOGGER = Logger.getLogger(DbResultSupport.class.getName()); + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + private static final JsonWriterFactory WRITER_FACTORY = Json.createWriterFactory(Collections.emptyMap()); + private static final byte[] EMPTY_JSON_BYTES = "[]".getBytes(StandardCharsets.UTF_8); + private static final byte[] ARRAY_JSON_END_BYTES = "]".getBytes(StandardCharsets.UTF_8); + private static final byte[] ARRAY_JSON_BEGIN_BYTES = "[".getBytes(StandardCharsets.UTF_8); + private static final byte[] COMMA_BYTES = ",".getBytes(StandardCharsets.UTF_8); + + private DbResultSupport() { + } + + /** + * Create a new instance to register with a webserver. + * @return {@link io.helidon.webserver.WebServer} {@link io.helidon.webserver.Service} + */ + public static DbResultSupport create() { + return new DbResultSupport(); + } + + @Override + public void accept(ServerRequest serverRequest, ServerResponse serverResponse) { + serverResponse.registerWriter(DbRows.class, DbResultSupport::writer); + serverResponse.registerWriter(DbResult.class, DbResultWriter::new); + serverRequest.next(); + } + + @Override + public void update(Routing.Rules rules) { + rules.any(this); + } + + private static final class DbResultWriter implements Flow.Publisher { + private final CompletableFuture dml = new CompletableFuture<>(); + private final CompletableFuture> query = new CompletableFuture<>(); + + private DbResultWriter(DbResult dbResult) { + dbResult + .whenDml(count -> { + dml.complete(count); + query.complete(null); + }) + .whenRs(rs -> { + query.complete(rs); + dml.complete(null); + }); + } + + @Override + public void subscribe(Flow.Subscriber subscriber) { + query.thenAccept(rs -> { + if (null != rs) { + writer(rs).subscribe(subscriber); + } + }); + dml.thenAccept(count -> { + if (null != count) { + objectWriter(JSON.createObjectBuilder().add("count", count).build()); + } + }); + } + + private static Flow.Publisher objectWriter(JsonObject json) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (JsonWriter writer = WRITER_FACTORY.createWriter(baos)) { + writer.write(json); + } + return ContentWriters.byteArrayWriter(false) + .apply(baos.toByteArray()); + } + + } + + private static final class DbRowSubscription implements Flow.Subscription { + + private final Flow.Subscription subscription; + + private DbRowSubscription(final Flow.Subscription subscription) { + this.subscription = subscription; + } + + @Override + public void request(long l) { + subscription.request(l); + } + + @Override + public void cancel() { + subscription.cancel(); + } + + } + + private static final class DbRowSubscriber implements Flow.Subscriber { + + private volatile boolean first = true; + private final Flow.Subscriber subscriber; + + private DbRowSubscriber(final Flow.Subscriber subscriber) { + this.subscriber = subscriber; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + Flow.Subscription mySubscription = new DbRowSubscription(subscription); + subscriber.onSubscribe(mySubscription); + } + + @Override + public void onNext(DbRow dbRow) { + LOGGER.finest(String.format("onNext: %s", dbRow.toString())); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + if (first) { + try { + baos.write(ARRAY_JSON_BEGIN_BYTES); + } catch (IOException ignored) { + } + first = false; + } else { + try { + baos.write(COMMA_BYTES); + } catch (IOException ignored) { + } + } + JsonWriter writer = WRITER_FACTORY.createWriter(baos); + writer.write(dbRow.as(JsonObject.class)); + writer.close(); + subscriber.onNext(DataChunk.create(baos.toByteArray())); + } + + @Override + public void onError(Throwable throwable) { + subscriber.onError(throwable); + } + + @Override + public void onComplete() { + LOGGER.finest("onComplete"); + + if (first) { + subscriber.onNext(DataChunk.create(EMPTY_JSON_BYTES)); + } else { + subscriber.onNext(DataChunk.create(ARRAY_JSON_END_BYTES)); + } + subscriber.onComplete(); + } + + } + + private static final class DataChunkPublisher implements Flow.Publisher { + + private final DbRows dbRows; + + private DataChunkPublisher(final DbRows dbRows) { + this.dbRows = dbRows; + } + + @Override + public void subscribe(Flow.Subscriber subscriber) { + dbRows.publisher().subscribe(new DbRowSubscriber(subscriber)); + } + + } + + // server send streaming + // json streaming & data type + private static Flow.Publisher writer(DbRows dbRows) { + return new DataChunkPublisher(dbRows); + } + +} diff --git a/dbclient/webserver-jsonp/src/main/java/io/helidon/dbclient/webserver/jsonp/package-info.java b/dbclient/webserver-jsonp/src/main/java/io/helidon/dbclient/webserver/jsonp/package-info.java new file mode 100644 index 000000000..5bd3a0078 --- /dev/null +++ b/dbclient/webserver-jsonp/src/main/java/io/helidon/dbclient/webserver/jsonp/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ +/** + * Support for sending DB Client results as JSON. + */ +package io.helidon.dbclient.webserver.jsonp; diff --git a/dbclient/webserver-jsonp/src/main/java/module-info.java b/dbclient/webserver-jsonp/src/main/java/module-info.java new file mode 100644 index 000000000..f0bdd8b53 --- /dev/null +++ b/dbclient/webserver-jsonp/src/main/java/module-info.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ +/** + * JSON-P support to map DB Client responses to webserver responses. + */ +module io.helidon.dbclient.webserver.jsonp { + requires java.logging; + requires java.json; + requires io.helidon.common.http; + requires io.helidon.media.common; + requires io.helidon.webserver; + requires io.helidon.dbclient; + + exports io.helidon.dbclient.webserver.jsonp; +} diff --git a/dependencies/pom.xml b/dependencies/pom.xml index a592f21d7..db3052358 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -41,6 +41,7 @@ 4.5.10 0.35.0 2.0 + 4.1.2 2.7.4 3.0.0 3.0.0 @@ -66,7 +67,7 @@ 7.6.0.Final 3.1.0 2.29.1 - 1.0.1 + 1.0.2 1.1.2 1.1.2 5.1.0 @@ -79,6 +80,7 @@ 1.3.1 1.3.3 2.23.4 + 1.11.0 8.0.11 5.9.3.Final 4.1.42.Final @@ -100,7 +102,7 @@ 2.12.0 1.5.18 2.3.1 - 1.0.3 + 1.0.6 @@ -381,6 +383,13 @@ + + + org.mongodb + mongodb-driver-reactivestreams + ${version.lib.mongodb.reactivestreams} + + org.eclipse.microprofile.config @@ -555,6 +564,12 @@ HikariCP ${version.lib.hikaricp} + + + io.dropwizard.metrics + metrics-core + ${version.lib.dropwizard.metrics} + redis.clients jedis diff --git a/docs-internal/db.md b/docs-internal/db.md new file mode 100644 index 000000000..26a117d82 --- /dev/null +++ b/docs-internal/db.md @@ -0,0 +1,363 @@ +# Helidon Database Proposal + +Provide an API that enables reactive database support to be used with Helidon SE. + +## Proposal + +The Helidon DB is an abstraction layer over +- database configuration +- statement configuration +- statement processing +- handling of results + +The Helidon DB also supports +- queries using either indexed or named parameters +- mapping of POJOs to database parameters using provided mapper(s) + - `.createNamedDmlStatement("insert-mon").namedParam(pokemon)` would work if `Pokemon` db mapper is registered +- mapping of database query results to POJOs using provided mapper(s) + - `dbRow.as(Pokemon.class)` would work if `Pokemon` db mapper is registered +- mapping of database columns to arbitrary types using provided mappers(s) + - `column.as(Long.class)` would work even for String column, as long as `String -> Long` mapper is registered + + +The Helidon DB is *NOT* +- Statement abstraction + - users write statements in the language understood by the database (such as `SQL`) + - there is *NO* query language other than the database native query language + +As part of this proposal there is also added support for [generic mapping](generic-mapping.md) + +### Required features + +1. The API must be reactive +2. The API must support backpressure for cases where multiple results are returned (queries) +3. There must be support for configuring Tracing without dependency on Tracing API +4. There must be support for configuring Metrics without dependency on Metrics API +5. There must be support for configuring Healthchecks without dependency on Healthcheck API +6. The first implementation must work at least over JDBC + +### API + +The API main interfaces/classes: +- `HelidonDb` - the entry point to create an instance of a Helidon DB, uses the usual `Builder`/`Config` pattern + the provider to be used is either explicitly configured, or defined by name (see `DbProvider` below) or + the first available one is used (ordered by priority). The instance has two methods to execute statements - + `execute` and `inTransaction` +- `HelidonDbExecute` - the entry point to execution of statements +- `DbStatement` - the abstraction of any type of statement (with more specific `DbStatementQuery` etc.) +- `DbRowResult` - the interface for query results (supports access via `Subscriber`, can collect rows in memory etc.) +- `DbRow` - represents a single row in the database (or single object), provides mapping methods (using mappers from + SPI `DbMapperProvider`) +- `DbColumn` - represent a single column in the database (or an object property), provides mapping methods (using + generic `MapperProvider`) +- `DbResult` - used for statements of unknown type, invokes `Consumer` of either a DML statement (`Consumer`) + or a query statement (`Consumer`) +- `DbMapper` - defines possible mapping operations required to map arbitrary types to types needed by the database +- `DbMapperManager` - used by DB implementations to access `DbMapper`s configured by `DbMapperProvider`s +- `DbInterceptor` and `DbInterceptorContext` provide support for integration with the DB for Metrics, Tracing and similar +- `DbException` a runtime exception to use when something fails (for JDBC this would usually wrap a `java.sql.SqlException`) + + +### SPI + +SPI is used to add support for additional drivers (such as JDBC, Mongo DB etc.). +The SPI Classes: +- `DbProvider` - a Java Service loader interface used to locate available implementations +- `DbProviderBuilder` - builder used to configure the underlying database driver and behavior of the + driver implementation (such as `DbMapperProvider`, statements etc.) +- `DbMapperProvider` - a Java Service loader interface used to locate available implementations of mappers specific + to database handling + +## Possible Implementations + +Our plan is to provide Helidon Database API implementations as very thin layers over several existing database APIs which are not reactive or their reactive API is unnecessarily complex or does not match Helidon style of API: + +- JDBC +- MongoDB reactive driver +- Oracle NoSQL +- R2DBC - currently milestone releases +- ADBA - currently in Alpha version + +## Configuration + +There is a single configuration option expected when loading the `HelidonDb` from configuration - +`source`. If defined, it is used to locate the `DbProvider` with the same name. If undefined, the +first provider is used (ordered by priority). + +Each database driver implementation may choose what is required in configuration, though the following + is recommended: +- Connectivity details + - `url` - the URL to the database (this may be a `jdbc` URL, or any database specific way of locating an instance) + - `username` - the user to connect to the database. If a database supports username/password based authentication, use this + property + - `password` - password of the user to connect. If a database supports username/password based authentication, use this + property +- Statements configuration + - `statements` - a configuration node that contains name-statement pairs + +*Note on named statements* +Helidon DB is using named statements as a preferred way of configuration, as we can simply reference a statement + in logging, exception handling, tracing etc. If you use arbitrary statements, the name is generated as a SHA-256. +The statements can be configured either in configuration file, or using the `HelidonDb.Builder.statements(DbStatements)` method. + +### Example +The following Yaml file configures JDBC data source to MySQL with custom statements: +```yaml +helidon-db: + source: jdbc + url: jdbc:mysql://127.0.0.1:3306/pokemon?useSSL=false + username: user + password: password + statements: + # required ping statement (such as for Healthcheck support) + ping: "DO 0" + # Insert new pokemon + insert-mon: "INSERT INTO pokemons VALUES(:name, :type)" + select-mon-by-type: "SELECT * FROM pokemons WHERE type = ?" + select-mon: "SELECT * FROM pokemons WHERE name = ?" + select-mon-all: "SELECT * FROM pokemons" + update-mon-type: "UPDATE pokemons SET type = :type WHERE name = :name" + delete-mon: "DELETE FROM pokemons WHERE name = ?" + delete-mon-all: "DELETE FROM pokemons" +``` + +This configuration file defines both connectivity and statements. + +It also shows an example of the two options for parameters: +1. Indexed parameters, such as in statement `delete-mon` +2. Named parameters, such as in statement `insert-mon` + +## Statement Types + +Two basic types of statements are recognized: + +- DML (Data Manipulation Language) statements: INSERT, UPDATE, DELETE, etc. +- DQL (Data Query Language) statements: SELECT + +Each type of statement execution returns different result. + +### Statement Parameters + +Statement may contain parameters. Supported parameter types are: + +1. indexed parameters identical to JDBC: `?` +2. named parameters similar to JPQL: `:name`, `$name` + +Both types of parameters can't be mixed in a single statement. + +Statements can be defined in Helidon configuration file or passed directly as a String argument. + +#### Indexed Parameters + +Parameters values are supplied in order identical to order of the `?` symbols in the statement. +Setters do not contain index argument. Index is determined from the order of their calls or index of provided List or array of parameters. + +#### Named Parameters + +Parameters values are supplied with corresponding parameter names. This can be done using name and value pairs or a Map + +### Parameters substitution + +Parameter values can be read from various class instances if `DbMapper` interface is defined for them. + +*Parameter substitution shall always be done using prepared statement if target database supports it.* + +## Statement Execution Result + +Execution result depends on statement type: + +- DML statement: returns information about number of modified rows in the database +- DQL (query) statement: returns database rows matching the query + +Statement execution is blocking operation so `CompletionStage` or `Flow` API must be returned by statement execute methods. + +### DML Statement Execution Result + +DML statement execute method returns `CompletionStage` to allow asynchronous processing of the result or related exceptions. + +### DQL (Query) Statement Execution Result + +DQL statement execute method returns `DbRowResult` interface which gives user several options how to process returned database rows asynchronously: + +- `Flow.Publisher publisher()`: allows registration of Flow.Subscriber to process database rows when they are available +- `CompletionStage consume(Consumer> consumer)`: allows to register consumer of database rows +- `CompletionStage> collect()`: allows to retrieve and process result as List of database rows. This method is limited to small result sets. + +### Database Row + +Database rows returned by query statement execution are returned as `DbRow` interface. This interface allows direct column access. It's also possible to map this row to another class instance using `DbMapper` interface. + +## Transactions + +Transactions are supported using similar code pattern like simple statement execution. The only difference is usage of ` T inTransaction(Function executor)` method instead of `execute`. Transaction scope and lifecycle is bound to `executor` instance. +Transaction is committed at the end of `executor` function scope when no exception has been thrown. Any exception thrown from `executor` function will cause transaction rollback. + +## Example + +### Application Configuration File +A configuration file used when creating a `Config` instance: +```yaml +helidon-db: + source: jdbc + url: jdbc:mysql://127.0.0.1:3306/myDatabase + username: user + password: password + statements: + insert-indexed-params: "INSERT INTO my_table VALUES(?, ?)" + insert-named-params: "INSERT INTO my_table VALUES(:name, :type)" + select-all: "SELECT * FROM my_table" + select-indexed-params: "SELECT * FROM my_table WHERE name = ?" + select-named-params: "SELECT * FROM my_table WHERE name = :name" +``` + +### Database Initialization: +```java +HelidonDb db = config + .get("helidon-db") + .as(HelidonDb::create) + .orElseThrow(() -> new IllegalStateException("Configuration is missing")); +``` + +### Statements Execution: + +#### Insert with Indexed Parameters +```java +db.execute(exec -> exec + .createNamedDmlStatement("insert-indexed-params") + .addParam("Pikachu") + .addParam("electric") + .execute()) + .thenAccept(count -> ) + .exceptionally(throwable -> ); +``` + +#### Insert with Named Parameters +```java +db.execute(exec -> exec + .createNamedDmlStatement("insert-named-params") + .addParam("name", "Pikachu") + .addParam("type", "electric") + .execute() + ) + .thenAccept(count -> ) + .exceptionally(throwable -> ); +``` + +#### Query without parameters + +```java +db.execute(exec -> exec.namedQuery("select-all")) + .consume(response::send) + .exceptionally(throwable -> sendError(throwable, response)); +``` + +#### Get with indexed parameters +The `get` methods (`get`, `namedGet`, `createGet` and `createNamedGet`) are a shortcut to a query method that expects zero to 1 results. +For Java 8, you need to use `OptionalHelper.from(maybeRow)` to be able to use `ifPresentOrElse`. +When using newer versions of Java, `ifPresentOrElse` is available on `Optional`. +```java +db.execute(exec -> exec.namedGet("select-indexed-params"), "Pikachu") + .thenAccept(maybeRow -> maybeRow.ifPresentOrElse(row -> , + () -> )) + .exceptionally(throwable -> ); +``` + +#### Simple Transaction +```java +db.inTransaction(exec -> exec + .get("SELECT type FROM my_table WHERE name = ?", "Pikachu") + .thenAccept(maybeRow -> maybeRow + .ifPresent(row -> exec + .insert("INSERT INTO my_table VALUES(?, ?)", "Raichu", row.column("type")) + ) + ) +).exceptionally(throwable -> ) +``` + +### Interceptor implementation + +Example interceptor (for tracing): +```java +public class DbTracing implements DbInterceptor { + @Override + public void statement(DbInterceptorContext interceptorContext) { + Context context = interceptorContext.context(); + Tracer tracer = context.get(Tracer.class).orElseGet(GlobalTracer::get); + + // now if span context is missing, we build a span without a parent + Tracer.SpanBuilder spanBuilder = tracer.buildSpan(interceptorContext.dbType() + ":" + interceptorContext.statementName()); + + context.get(SpanContext.class) + .ifPresent(spanBuilder::asChildOf); + + Span span = spanBuilder.start(); + + interceptorContext.statementFuture().thenAccept(nothing -> span.log(CollectionsHelper.mapOf("type", "statement"))); + + interceptorContext.resultFuture().thenAccept(count -> span.log(CollectionsHelper.mapOf("type", "result", + "count", count)).finish()) + .exceptionally(throwable -> { + Tags.ERROR.set(span, Boolean.TRUE); + span.log(CollectionsHelper.mapOf("event", "error", + "error.kind", "Exception", + "error.object", throwable, + "message", throwable.getMessage())); + span.finish(); + return null; + }); + + } + + public static DbTracing create() { + return new DbTracing(); + } +} +``` + +### Mapper implementation + +Example mapper implementation for Pokemon: +```java +public class PokemonMapper implements DbMapper { + + @Override + public Pokemon read(DbRow row) { + DbColumn name = row.column("name"); + DbColumn type = row.column("type"); + return new Pokemon(name.as(String.class), type.as(String.class)); + } + + @Override + public Map toNamedParameters(Pokemon value) { + Map map = new HashMap<>(1); + map.put("name", value.getName()); + map.put("type", value.getType()); + return map; + } + + @Override + public List toIndexedParameters(Pokemon value) { + List list = new ArrayList<>(2); + list.add(value.getName()); + list.add(value.getType()); + return list; + } + +} +``` + +## Open questions +The support for writing rows to WebServer currently transforms the row to JsonObject and then writes it to + the `DataChunk` to send it. +Integration with WebServer should be (IMHO) more straight-forward. +Example that does not work today: +```java +DbRowResult result = ....; +response.send(result.map(JsonObject.class).publisher()); +``` + +Or even: +```java +DbRowResult result = ....; +response.send(result.map(Pokemon.class).publisher()); +``` \ No newline at end of file diff --git a/docs-internal/generic-mapping.md b/docs-internal/generic-mapping.md new file mode 100644 index 000000000..9c4c66bcc --- /dev/null +++ b/docs-internal/generic-mapping.md @@ -0,0 +1,252 @@ +# Generic Mapping Proposal + +This change was merged into master. +Module: `common/mapper` +Since: Helidon 1.2.2 +Please consult javadocs for current documentation. + +Provide an API to map an arbitrary Java type to another arbitrary Java type. + +## Proposal + +The API consist of the following classes (may be changed due to implementation details): +- `MapperManager` - the entry point to mapping of types +- `Mapper` - a class capable of converting one type to another +- `MapperProvider` - SPI class to support providers loaded through Java Service loader or configured through a builder +- `MapperException` - `RuntimeException` thrown when a mapping is missing or the mapping itself failed + +This API should be also added to `Config` as an additional source of mappings. + +### Mapper Manager + +Mapping provides tools to map a type to another type. + +The types may be + - a java class (`String.class`) + - a generic type (`io.helidon.common.GenericType` for any java type, such as `Supplier`) + +Mappings can be provided either through Java Service loader, or through + explicitly configured providers using a builder. + +The mapping function gets either a pair of `Class` objects, or a pair of `GenericType` objects + that defines the `SOURCE` and the `TARGET` of the mapping. + +For `Class` parameters, lookup is done as follows: +1. Ask each mapping provider if such a pair of classes is supported, if yes, use the first mapper +2. Convert each class to `GenericType` and ask each mapping provider if supported, if yes, use the first mapper + +For `GenericType` parameters, lookup is done as follows: +1. Ask each mapping provider if such a pair of types is supported, if yes, use the first mapper +2. If both generic types represent a `Class`, convert each to a `Class` and + ask each mapping provider if supported, if yes, use the first mapper + +The results are cached (so lookup for a defined pair is done only once). +In case no mapper is found, the result should be cached as well. + +The main API class is `MapperManager`. +```java +/** + * Map from source to target. + * + * @param source object to map + * @param sourceType type of the source object (to locate the mapper) + * @param targetType type of the target object (to locate the mapper) + * @return result of the mapping + * @throws io.helidon.common.mapping.MapperException in case the mapper was not found or failed + */ + TARGET map(SOURCE source, GenericType sourceType, GenericType targetType); + +/** + * Map from source to target. + * + * @param source object to map + * @param sourceType class of the source object (to locate the mapper) + * @param targetType class of teh target object (to locate the mapper) + * @return result of the mapping + * @throws io.helidon.common.mapping.MapperException in case the mapper was not found or failed + */ + TARGET map(SOURCE source, Class sourceType, Class targetType); +``` + +`MapperManager` can be created using the usual `Builder`: +```java +/** + * Replace the service loader with the one provided. + * @param serviceLoader fully configured service loader to be used to load mapper providers + * @return updated builder instance + */ +Builder mapperProviders(HelidonServiceLoader serviceLoader); + +/** + * Add a new {@link io.helidon.common.mapping.spi.MapperProvider} to the list of providers loaded from + * system service loader. + * + * @param provider prioritized mapper provider to use + * @return updated builder instance + */ +Builder addMapperProvider(MapperProvider provider); + +/** + * Add a mapper to the list of mapper. + * + * @param mapper the mapper to map source instances to target instances + * @param sourceType class of the source instance + * @param targetType class of the target instance + * @param type of source + * @param type of target + * @return updated builder instance + */ + Builder addMapper(Mapper mapper, Class sourceType, Class targetType); + +/** + * Add a mapper to the list of mapper. + * + * @param mapper the mapper to map source instances to target instances + * @param sourceType generic type of the source instance + * @param targetType generic type of the target instance + * @param type of source + * @param type of target + * @return updated builder instance + */ + Builder addMapper(Mapper mapper, GenericType sourceType, GenericType targetType); +``` + +### Mapper implementation +`Mapper` is the class doing the actual work of mapping one type to another. +Mappers are either provided by user when creating the `MapperManager` or obtained +from `MapperProvider` services. + +`Mapper`: +```java +/** + * Map an instance of source type to an instance of target type. + * + * @param source object to map + * @return result of the mapping + */ +TARGET map(SOURCE source); +``` + +The `Mapper` provides unidirectional mapping - there can be a mapper +from `String` to `Long` and another one from `Long` to `String`. + +The knowledge of the `SOURCE` and `TARGET` types comes from the developer +providing these mappers - there is no possibility to register a `Mapper` +without explicitly defining the types it is registered for. + +### Mapper provider implementations + +Service implementation can use `@Priority` or implement `io.helidon.common.Prioritized` + to define its priority (lower number will be used first) + +Service implementation requires implementation for `Class` types, + may also implement support for `GenericType`. + +The main interface for SPI is `MapperProvider`: +```java +/** + * Find a mapper that is capable of mapping from source to target classes. + * + * @param sourceClass class of the source + * @param targetClass class of the target + * @param type of the source + * @param type of the target + * @return a mapper that is capable of mapping (or converting) sources to targets + */ + Optional> mapper(Class sourceClass, Class targetClass); + +/** + * Find a mapper that is capable of mapping from source to target types. + * This method supports mapping to/from types that contain generics. + * + * @param sourceType generic type of the source + * @param targetType generic type of the target + * @param type of the source + * @param type of the target + * @return a mapper that is capable of mapping (or converting) sources to targets + */ +default Optional> mapper(GenericType sourceType, GenericType targetType) { + return Optional.empty(); +} +``` + +### Built-in mapper +We may provide (as a separate library?) a set of built-in generic mappers, especially for primitive types. +For each pair define here, we should have + - bi-directional mapping between the types + - mapping of `List` <-> `List` + - mapping of `List` <-> `Set` + - mapping of `Set` <-> `Set` + - mapping of `Array` <-> `List` + - mapping of `Stream` <-> `Stream` + +Suggested supported mapping pairs (primitive types should be supported as well): + - `String` to the same types as defined in `ConfigMappers.initBuiltInMappers` except for `Map` and `Properties` + - `BigInteger`, `Long` - should throw an exception if too big + - `BigInteger`, `Integer` - should throw an exception if too big + - `BigInteger`, `BigDecimal` + - `Long`, `Integer` - should throw an exception if too big + - `Integer`, `Short` - should throw an exception if too big + +Other reasonable mapping pairs can be added. + +## Examples + +### Using the MapperManager +The following example creates the `MapperManager` from Java Service loader +```java +// creates a mapper manager from system service loader +MapperManager mm = MapperManager.create(); + +// this will work if a String to Long mapper is configured +Long longValue = mm.map("1094444", String.class, Long.class); + +// this will work if a List to List mapper is configured +List stringList = CollectionsHelper.listOf("140", "145"); +List longList = mm.map(stringList, new GenericType>(){}, new GenericType>(){}); +``` + +### Creating a MapperProvider +The following example creates a service implementation that supports mapping to/from `String` and `Long` and a mapping +from `List` to `List`: + +```java +private static final Class LONG_CLASS = Long.class; +private static final GenericType> LONG_LIST = new GenericType>() { }; +private static final Class STRING_CLASS = String.class; +private static final GenericType> STRING_LIST = new GenericType>() { }; + +@Override +public Optional> mapper(Class sourceClass, Class targetClass) { + if (sourceClass.equals(LONG_CLASS) && targetClass.equals(STRING_CLASS)) { + return Optional.of((Mapper) longToString()); + } + if (sourceClass.equals(STRING_CLASS) && targetClass.equals(LONG_CLASS)) { + return Optional.of((Mapper) stringToLong()); + } + return Optional.empty(); +} + +@Override +public Optional> mapper(GenericType sourceType, + GenericType targetType) { + if (sourceType.equals(STRING_LIST) && targetType.equals(LONG_LIST)) { + return Optional.of((Mapper) stringListToLongList()); + } + return Optional.empty(); +} + +private Mapper, List> stringListToLongList() { + return strings -> strings.stream() + .map(Long::parseLong) + .collect(Collectors.toList()); +} + +private Mapper stringToLong() { + return Long::parseLong; +} + +private Mapper longToString() { + return String::valueOf; +} +``` \ No newline at end of file diff --git a/examples/dbclient/README.md b/examples/dbclient/README.md new file mode 100644 index 000000000..a37127d38 --- /dev/null +++ b/examples/dbclient/README.md @@ -0,0 +1,4 @@ +# Helidon DB Client Examples + +Each subdirectory contains example code that highlights specific aspects of +Helidon DB Client. \ No newline at end of file diff --git a/examples/dbclient/common/pom.xml b/examples/dbclient/common/pom.xml new file mode 100644 index 000000000..01488fb6c --- /dev/null +++ b/examples/dbclient/common/pom.xml @@ -0,0 +1,45 @@ + + + + + 4.0.0 + + io.helidon.examples.dbclient + helidon-examples-dbclient-project + 2.0-SNAPSHOT + + + helidon-examples-dbclient-common + Helidon Examples DB Client Common + + + + io.helidon.dbclient + helidon-dbclient + + + io.helidon.webserver + helidon-webserver + + + io.helidon.media.jsonp + helidon-media-jsonp-server + + + diff --git a/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/AbstractPokemonService.java b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/AbstractPokemonService.java new file mode 100644 index 000000000..9ddabeb22 --- /dev/null +++ b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/AbstractPokemonService.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.examples.dbclient.common; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.json.JsonObject; + +import io.helidon.common.http.Http; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbRow; +import io.helidon.webserver.Handler; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.ServerResponse; +import io.helidon.webserver.Service; + +/** + * Common methods that do not differ between JDBC and MongoDB. + */ +public abstract class AbstractPokemonService implements Service { + private static final Logger LOGGER = Logger.getLogger(AbstractPokemonService.class.getName()); + + private final DbClient dbClient; + + /** + * Create a new pokemon service with a DB client. + * + * @param dbClient DB client to use for database operations + */ + protected AbstractPokemonService(DbClient dbClient) { + this.dbClient = dbClient; + } + + @Override + public void update(Routing.Rules rules) { + rules + .get("/", this::listPokemons) + // create new + .put("/", Handler.create(Pokemon.class, this::insertPokemon)) + // update existing + .post("/{name}/type/{type}", this::insertPokemonSimple) + // delete all + .delete("/", this::deleteAllPokemons) + // get one + .get("/{name}", this::getPokemon) + // delete one + .delete("/{name}", this::deletePokemon) + // example of transactional API (local transaction only!) + .put("/transactional", Handler.create(Pokemon.class, this::transactional)) + // update one (TODO this is intentionally wrong - should use JSON request, just to make it simple we use path) + .put("/{name}/type/{type}", this::updatePokemonType); + } + + /** + * The DB client associated with this service. + * + * @return DB client instance + */ + protected DbClient dbClient() { + return dbClient; + } + + /** + * This method is left unimplemented to show differences between native statements that can be used. + * + * @param request Server request + * @param response Server response + */ + protected abstract void deleteAllPokemons(ServerRequest request, ServerResponse response); + + /** + * Insert new pokemon with specified name. + * + * @param request the server request + * @param response the server response + */ + private void insertPokemon(ServerRequest request, ServerResponse response, Pokemon pokemon) { + dbClient.execute(exec -> exec + .createNamedInsert("insert2") + .namedParam(pokemon) + .execute()) + .thenAccept(count -> response.send("Inserted: " + count + " values")) + .exceptionally(throwable -> sendError(throwable, response)); + } + + /** + * Insert new pokemon with specified name. + * + * @param request the server request + * @param response the server response + */ + private void insertPokemonSimple(ServerRequest request, ServerResponse response) { + // Test Pokemon POJO mapper + Pokemon pokemon = new Pokemon(request.path().param("name"), request.path().param("type")); + + dbClient.execute(exec -> exec + .createNamedInsert("insert2") + .namedParam(pokemon) + .execute()) + .thenAccept(count -> response.send("Inserted: " + count + " values")) + .exceptionally(throwable -> sendError(throwable, response)); + } + + /** + * Get a single pokemon by name. + * + * @param request server request + * @param response server response + */ + private void getPokemon(ServerRequest request, ServerResponse response) { + String pokemonName = request.path().param("name"); + + dbClient.execute(exec -> exec.namedGet("select-one", pokemonName)) + .onEmpty(() -> sendNotFound(response, "Pokemon " + pokemonName + " not found")) + .onValue(row -> sendRow(row, response)) + .exceptionally(throwable -> sendError(throwable, response)); + } + + /** + * Return JsonArray with all stored pokemons or pokemons with matching attributes. + * + * @param request the server request + * @param response the server response + */ + private void listPokemons(ServerRequest request, ServerResponse response) { + dbClient.execute(exec -> exec.namedQuery("select-all")) + .thenAccept(response::send) + .exceptionally(throwable -> sendError(throwable, response)); + } + + /** + * Update a pokemon. + * Uses a transaction. + * + * @param request the server request + * @param response the server response + */ + private void updatePokemonType(ServerRequest request, ServerResponse response) { + final String name = request.path().param("name"); + final String type = request.path().param("type"); + + dbClient.execute(exec -> exec + .createNamedUpdate("update") + .addParam("name", name) + .addParam("type", type) + .execute()) + .thenAccept(count -> response.send("Updated: " + count + " values")) + .exceptionally(throwable -> sendError(throwable, response)); + } + + private void transactional(ServerRequest request, ServerResponse response, Pokemon pokemon) { + + dbClient.inTransaction(tx -> tx + .createNamedGet("select-for-update") + .namedParam(pokemon) + .execute() + // TODO use onPresent/onEmpty + .thenCompose(maybeRow -> maybeRow.map(dbRow -> tx.createNamedUpdate("update") + .namedParam(pokemon).execute()) + .orElseGet(() -> CompletableFuture.completedFuture(0L))) + ).thenAccept(count -> response.send("Updated " + count + " records")); + + } + + /** + * Delete pokemon with specified name (key). + * + * @param request the server request + * @param response the server response + */ + private void deletePokemon(ServerRequest request, ServerResponse response) { + final String name = request.path().param("name"); + + dbClient.execute(exec -> exec.namedDelete("delete", name)) + .thenAccept(count -> response.send("Deleted: " + count + " values")) + .exceptionally(throwable -> sendError(throwable, response)); + } + + /** + * Send a 404 status code. + * + * @param response the server response + * @param message entity content + */ + protected void sendNotFound(ServerResponse response, String message) { + response.status(Http.Status.NOT_FOUND_404); + response.send(message); + } + + /** + * Send a single DB row as JSON object. + * + * @param row row as read from the database + * @param response server response + */ + protected void sendRow(DbRow row, ServerResponse response) { + response.send(row.as(JsonObject.class)); + } + + /** + * Send a 500 response code and a few details. + * + * @param throwable throwable that caused the issue + * @param response server response + * @param type of expected response, will be always {@code null} + * @return {@code Void} so this method can be registered as a lambda + * with {@link java.util.concurrent.CompletionStage#exceptionally(java.util.function.Function)} + */ + protected T sendError(Throwable throwable, ServerResponse response) { + Throwable realCause = throwable; + if (throwable instanceof CompletionException) { + realCause = throwable.getCause(); + } + response.status(Http.Status.INTERNAL_SERVER_ERROR_500); + response.send("Failed to process request: " + realCause.getClass().getName() + "(" + realCause.getMessage() + ")"); + LOGGER.log(Level.WARNING, "Failed to process request", throwable); + return null; + } + +} diff --git a/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/Pokemon.java b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/Pokemon.java new file mode 100644 index 000000000..43cad6311 --- /dev/null +++ b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/Pokemon.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.examples.dbclient.common; + +/** + * POJO representing a very simplified Pokemon. + */ +public class Pokemon { + private String name; + private String type; + + /** + * Default constructor. + */ + public Pokemon() { + // JSON-B + } + + /** + * Create pokemon with name and type. + * + * @param name name of the beast + * @param type type of the beast + */ + public Pokemon(String name, String type) { + this.name = name; + this.type = type; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/PokemonMapper.java b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/PokemonMapper.java new file mode 100644 index 000000000..ae726829e --- /dev/null +++ b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/PokemonMapper.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.examples.dbclient.common; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.helidon.dbclient.DbColumn; +import io.helidon.dbclient.DbMapper; +import io.helidon.dbclient.DbRow; + +/** + * Maps database statements to {@link io.helidon.examples.dbclient.common.Pokemon} class. + */ +public class PokemonMapper implements DbMapper { + + @Override + public Pokemon read(DbRow row) { + DbColumn name = row.column("name"); + // we know that in mongo this is not true + if (null == name) { + name = row.column("_id"); + } + + DbColumn type = row.column("type"); + return new Pokemon(name.as(String.class), type.as(String.class)); + } + + @Override + public Map toNamedParameters(Pokemon value) { + Map map = new HashMap<>(1); + map.put("name", value.getName()); + map.put("type", value.getType()); + return map; + } + + @Override + public List toIndexedParameters(Pokemon value) { + List list = new ArrayList<>(2); + list.add(value.getName()); + list.add(value.getType()); + return list; + } +} diff --git a/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/PokemonMapperProvider.java b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/PokemonMapperProvider.java new file mode 100644 index 000000000..54a15e082 --- /dev/null +++ b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/PokemonMapperProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.examples.dbclient.common; + +import java.util.Optional; + +import javax.annotation.Priority; + +import io.helidon.dbclient.DbMapper; +import io.helidon.dbclient.spi.DbMapperProvider; + +/** + * Provides pokemon mappers. + */ +@Priority(1000) +public class PokemonMapperProvider implements DbMapperProvider { + private static final PokemonMapper MAPPER = new PokemonMapper(); + + @SuppressWarnings("unchecked") + @Override + public Optional> mapper(Class type) { + if (type.equals(Pokemon.class)) { + return Optional.of((DbMapper) MAPPER); + } + return Optional.empty(); + } +} diff --git a/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/package-info.java b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/package-info.java new file mode 100644 index 000000000..8404fd469 --- /dev/null +++ b/examples/dbclient/common/src/main/java/io/helidon/examples/dbclient/common/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ +/** + * Common classes shared by JDBC and MongoDB examples. + */ +package io.helidon.examples.dbclient.common; diff --git a/examples/dbclient/common/src/main/java/module-info.java b/examples/dbclient/common/src/main/java/module-info.java new file mode 100644 index 000000000..d931289ec --- /dev/null +++ b/examples/dbclient/common/src/main/java/module-info.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ +/** + * Common classes for Pokemon examples for DB Client. + */ +module io.helidon.examples.dbclient.common { + requires java.logging; + + requires java.json; + requires transitive io.helidon.dbclient; + requires io.helidon.common.http; + requires transitive io.helidon.webserver; + + exports io.helidon.examples.dbclient.common; + + provides io.helidon.dbclient.spi.DbMapperProvider with io.helidon.examples.dbclient.common.PokemonMapperProvider; +} diff --git a/examples/dbclient/common/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbMapperProvider b/examples/dbclient/common/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbMapperProvider new file mode 100644 index 000000000..e78014ea0 --- /dev/null +++ b/examples/dbclient/common/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbMapperProvider @@ -0,0 +1,17 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +io.helidon.examples.dbclient.common.PokemonMapperProvider diff --git a/examples/dbclient/jdbc/README.md b/examples/dbclient/jdbc/README.md new file mode 100644 index 000000000..e1d925164 --- /dev/null +++ b/examples/dbclient/jdbc/README.md @@ -0,0 +1,36 @@ +# Helidon DB Client JDBC Example + +This example shows how to run Helidon DB Client over JDBC. + + +## Build + +``` +mvn package +``` + +## Run + +This example requires a MySQL database, start it using docker: +`docker run --rm --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=pokemon -e MYSQL_USER=user -e MYSQL_PASSWORD=password mysql:5.7` + +Then run the `io.helidon.examples.dbclient.jdbc.JdbcExampleMain` class + +##ย Exercise + +The application has the following endpoints: + +- http://localhost:8079/db - the main business endpoint (see `curl` commands below) +- http://localhost:8079/metrics - the metrics endpoint (query adds application metrics) +- http://localhost:8079/health - has a custom database health check + +Application also connects to zipkin on default address. +The query operation adds database trace. + +`curl` commands: + +- `curl http://localhost:8079/db` - list all Pokemon in the database +- `curl -i -X PUT -d '{"name":"Squirtle","type":"water"}' http://localhost:8079/db` - add a new pokemon +- `curl http://localhost:8079/db/Squirtle` - get a single pokemon + +The application also supports update and delete - see `PokemonService.java` for bound endpoints. diff --git a/examples/dbclient/jdbc/pom.xml b/examples/dbclient/jdbc/pom.xml new file mode 100644 index 000000000..7e8e717f6 --- /dev/null +++ b/examples/dbclient/jdbc/pom.xml @@ -0,0 +1,125 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 2.0-SNAPSHOT + ../../../applications/se/pom.xml + + + helidon-examples-dbclient-jdbc + Helidon Examples DB Client JDBC + + + io.helidon.examples.dbclient.jdbc.JdbcExampleMain + + + + + io.helidon.health + helidon-health + + + io.helidon.metrics + helidon-metrics + + + io.helidon.tracing + helidon-tracing + + + io.helidon.tracing + helidon-tracing-zipkin + + + io.helidon.dbclient + helidon-dbclient-jdbc + + + io.helidon.dbclient + helidon-dbclient-tracing + + + io.helidon.dbclient + helidon-dbclient-metrics + + + io.helidon.dbclient + helidon-dbclient-metrics-jdbc + + + io.helidon.dbclient + helidon-dbclient-health + + + io.helidon.dbclient + helidon-dbclient-jsonp + + + io.helidon.dbclient + helidon-dbclient-webserver-jsonp + + + mysql + mysql-connector-java + + + com.zaxxer + HikariCP + + + org.slf4j + slf4j-jdk14 + + + io.helidon.media.jsonb + helidon-media-jsonb-server + + + io.helidon.config + helidon-config-yaml + + + io.helidon.examples.dbclient + helidon-examples-dbclient-common + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + \ No newline at end of file diff --git a/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/JdbcExampleMain.java b/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/JdbcExampleMain.java new file mode 100644 index 000000000..5c28f86cb --- /dev/null +++ b/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/JdbcExampleMain.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.examples.dbclient.jdbc; + +import java.io.IOException; +import java.util.logging.LogManager; + +import io.helidon.config.Config; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.health.DbClientHealthCheck; +import io.helidon.dbclient.webserver.jsonp.DbResultSupport; +import io.helidon.health.HealthSupport; +import io.helidon.media.jsonb.server.JsonBindingSupport; +import io.helidon.media.jsonp.server.JsonSupport; +import io.helidon.metrics.MetricsSupport; +import io.helidon.tracing.TracerBuilder; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerConfiguration; +import io.helidon.webserver.WebServer; + +/** + * Simple Hello World rest application. + */ +public final class JdbcExampleMain { + + /** + * Cannot be instantiated. + */ + private JdbcExampleMain() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + * @throws java.io.IOException if there are problems reading logging properties + */ + public static void main(final String[] args) throws IOException { + startServer(); + } + + /** + * Start the server. + * + * @return the created {@link io.helidon.webserver.WebServer} instance + * @throws java.io.IOException if there are problems reading logging properties + */ + static WebServer startServer() throws IOException { + + // load logging configuration + LogManager.getLogManager().readConfiguration( + JdbcExampleMain.class.getResourceAsStream("/logging.properties")); + + // By default this will pick up application.yaml from the classpath + Config config = Config.create(); + + // Get webserver config from the "server" section of application.yaml + ServerConfiguration serverConfig = + ServerConfiguration.builder(config.get("server")) + .tracer(TracerBuilder.create(config.get("tracing")).build()) + .build(); + + // Prepare routing for the server + Routing routing = createRouting(config); + + WebServer server = WebServer.create(serverConfig, routing); + + // Start the server and print some info. + server.start().thenAccept(ws -> { + System.out.println( + "WEB server is up! http://localhost:" + ws.port() + "/"); + }); + + // Server threads are not daemon. NO need to block. Just react. + server.whenShutdown().thenRun(() + -> System.out.println("WEB server is DOWN. Good bye!")); + + return server; + } + + /** + * Creates new {@link io.helidon.webserver.Routing}. + * + * @param config configuration of this server + * @return routing configured with JSON support, a health check, and a service + */ + private static Routing createRouting(Config config) { + Config dbConfig = config.get("db"); + + // Interceptors are added through a service loader - see mongoDB example for explicit interceptors + DbClient dbClient = DbClient.builder(dbConfig) + .build(); + + HealthSupport health = HealthSupport.builder() + .addLiveness(DbClientHealthCheck.create(dbClient)) + .build(); + + return Routing.builder() + .register(JsonSupport.create()) + .register(JsonBindingSupport.create()) + .register(DbResultSupport.create()) + .register(health) // Health at "/health" + .register(MetricsSupport.create()) // Metrics at "/metrics" + .register("/db", new PokemonService(dbClient)) + .build(); + } +} diff --git a/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/PokemonService.java b/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/PokemonService.java new file mode 100644 index 000000000..983873cf4 --- /dev/null +++ b/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/PokemonService.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.examples.dbclient.jdbc; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbClient; +import io.helidon.examples.dbclient.common.AbstractPokemonService; +import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.ServerResponse; + +/** + * Example service using a database. + */ +public class PokemonService extends AbstractPokemonService { + private static final Logger LOGGER = Logger.getLogger(PokemonService.class.getName()); + + PokemonService(DbClient dbClient) { + super(dbClient); + + // dirty hack to prepare database for our POC + // MySQL init + dbClient.execute(handle -> handle.namedDml("create-table")) + .thenAccept(System.out::println) + .exceptionally(throwable -> { + LOGGER.log(Level.WARNING, "Failed to create table, maybe it already exists?", throwable); + return null; + }); + } + + /** + * Delete all pokemons. + * + * @param request the server request + * @param response the server response + */ + @Override + protected void deleteAllPokemons(ServerRequest request, ServerResponse response) { + dbClient().execute(exec -> exec + // this is to show how ad-hoc statements can be executed (and their naming in Tracing and Metrics) + .createDelete("DELETE FROM pokemons") + .execute()) + .thenAccept(count -> response.send("Deleted: " + count + " values")) + .exceptionally(throwable -> sendError(throwable, response)); + } + + + + + +} diff --git a/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/package-info.java b/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/package-info.java new file mode 100644 index 000000000..3cc3ddbc7 --- /dev/null +++ b/examples/dbclient/jdbc/src/main/java/io/helidon/examples/dbclient/jdbc/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Quick start demo application. + */ +package io.helidon.examples.dbclient.jdbc; diff --git a/examples/dbclient/jdbc/src/main/java/module-info.java b/examples/dbclient/jdbc/src/main/java/module-info.java new file mode 100644 index 000000000..a67580463 --- /dev/null +++ b/examples/dbclient/jdbc/src/main/java/module-info.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * JDBC example. + */ +module io.helidon.examples.dbclient.jdbc { + requires java.logging; + + requires io.helidon.config; + requires io.helidon.dbclient.health; + requires io.helidon.health; + requires io.helidon.dbclient.webserver.jsonp; + requires io.helidon.media.jsonb.server; + requires io.helidon.media.jsonp.server; + requires io.helidon.metrics; + requires io.helidon.tracing; + requires io.helidon.examples.dbclient.common; +} diff --git a/examples/dbclient/jdbc/src/main/resources/application.yaml b/examples/dbclient/jdbc/src/main/resources/application.yaml new file mode 100644 index 000000000..021ad46b1 --- /dev/null +++ b/examples/dbclient/jdbc/src/main/resources/application.yaml @@ -0,0 +1,84 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +server: + port: 8079 + host: 0.0.0.0 + features: + print-details: true + +tracing: + service: jdbc-db + +# docker run --rm --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=pokemon -e MYSQL_USER=user -e MYSQL_PASSWORD=password mysql:5.7 +db: + source: jdbc + connection: + url: jdbc:mysql://127.0.0.1:3306/pokemon?useSSL=false + username: user + password: password + poolName: mysql + initializationFailTimeout: -1 + connectionTimeout: 2000 + helidon: + pool-metrics: + enabled: true + # name prefix defaults to "db.pool." - if you have more than one client within a JVM, you may want to distinguish between them + name-prefix: "hikari." + interceptors: + tracing: + global: + metrics: + # possible also global: + global: + - type: METER + # changing the name format - by removing the parameter (statement name) we get an overall metric + name-format: "db.meter.overall" + - type: METER + # meter per statement name + - type: METER + # meter per statement type + name-format: "db.meter.%2$s" + named: + - names: ["select-all", "select-one"] + errors: false + type: TIMER + description: "Timer for successful selects" + typed: + - types: ["DELETE", "UPDATE", "INSERT", "DML"] + type: COUNTER + errors: false + name-format: "db.counter.%s.success" + description: "Counter of successful DML statements" + - types: ["DELETE", "UPDATE", "INSERT", "DML"] + type: COUNTER + success: false + name-format: "db.counter.%s.error" + description: "Counter of failed DML statements" + statements: + # required ping statement + ping: "DO 0" + # Insert new pokemon + create-table: "CREATE TABLE pokemons (name VARCHAR(64) NOT NULL PRIMARY KEY, type VARCHAR(32))" + insert1: "INSERT INTO pokemons VALUES(?, ?)" + insert2: "INSERT INTO pokemons VALUES(:name, :type)" + select-by-type: "SELECT * FROM pokemons WHERE type = ?" + select-one: "SELECT * FROM pokemons WHERE name = ?" + select-all: "SELECT * FROM pokemons" + select-for-update: "SELECT * FROM pokemons WHERE name = :name for UPDATE" + update: "UPDATE pokemons SET type = :type WHERE name = :name" + delete: "DELETE FROM pokemons WHERE name = ?" + # delete-all: "DELETE FROM pokemons" diff --git a/examples/dbclient/jdbc/src/main/resources/logging.properties b/examples/dbclient/jdbc/src/main/resources/logging.properties new file mode 100644 index 000000000..3120aaf00 --- /dev/null +++ b/examples/dbclient/jdbc/src/main/resources/logging.properties @@ -0,0 +1,37 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=java.util.logging.ConsoleHandler + +# Global default logging level. Can be overriden by specific handlers and loggers +.level=INFO + +# Helidon Web Server has a custom log formatter that extends SimpleFormatter. +# It replaces "!thread!" with the current thread name +java.util.logging.ConsoleHandler.level=INFO +java.util.logging.ConsoleHandler.formatter=io.helidon.webserver.WebServerLogFormatter +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +#Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO +#io.netty.level=INFO diff --git a/examples/dbclient/mongodb/README.md b/examples/dbclient/mongodb/README.md new file mode 100644 index 000000000..69672b87e --- /dev/null +++ b/examples/dbclient/mongodb/README.md @@ -0,0 +1,38 @@ +# Helidon DB Client mongoDB Example + +This example shows how to run Helidon DB over mongoDB. + + +## Build + +``` +mvn package +``` + +## Run + +This example requires a mongoDB database, start it using docker: +`docker run --rm --name mongo -p 27017:27017 mongo` + +Then run the `io.helidon.examples.dbclient.mongo.MongoDbExampleMain` class + +##ย Exercise + +The application has the following endpoints: + +- http://localhost:8079/db - the main business endpoint (see `curl` commands below) +- http://localhost:8079/metrics - the metrics endpoint (query adds application metrics) +- http://localhost:8079/health - has a custom database health check + +Application also connects to zipkin on default address. +The query operation adds database trace. + +`curl` commands: + +- `curl http://localhost:8079/db` - list all Pokemon in the database +- `curl -i -X POST -d '{"name":"Squirtle","type":"water"}' http://localhost:8079/db` - add a new pokemon +- `curl http://localhost:8079/db/Squirtle` - get a single pokemon +- `curl -i -X DELETE http://localhost:8079/db/Squirtle` - delete a single pokemon +- `curl -i -X DELETE http://localhost:8079/db` - delete all pokemon + +The application also supports update and delete - see `PokemonService.java` for bound endpoints. diff --git a/examples/dbclient/mongodb/pom.xml b/examples/dbclient/mongodb/pom.xml new file mode 100644 index 000000000..d0122d6ba --- /dev/null +++ b/examples/dbclient/mongodb/pom.xml @@ -0,0 +1,98 @@ + + + + + 4.0.0 + + io.helidon.examples.dbclient + helidon-examples-dbclient-project + 2.0-SNAPSHOT + + + helidon-examples-dbclient-mongodb + Helidon Examples DB Mongo + + + + io.helidon.common + helidon-common + + + io.helidon.common + helidon-common-mapper + + + io.helidon.dbclient + helidon-dbclient-mongodb + + + io.helidon.dbclient + helidon-dbclient-tracing + + + io.helidon.dbclient + helidon-dbclient-metrics + + + io.helidon.dbclient + helidon-dbclient-health + + + io.helidon.dbclient + helidon-dbclient-jsonp + + + io.helidon.dbclient + helidon-dbclient-webserver-jsonp + + + io.helidon.health + helidon-health + + + io.helidon.metrics + helidon-metrics + + + io.helidon.tracing + helidon-tracing + + + io.helidon.tracing + helidon-tracing-zipkin + + + io.helidon.media.jsonb + helidon-media-jsonb-server + + + io.helidon.config + helidon-config-yaml + + + io.helidon.examples.dbclient + helidon-examples-dbclient-common + + + org.junit.jupiter + junit-jupiter-engine + test + + + diff --git a/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/MongoDbExampleMain.java b/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/MongoDbExampleMain.java new file mode 100644 index 000000000..1fe7e1905 --- /dev/null +++ b/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/MongoDbExampleMain.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.examples.dbclient.mongo; + +import java.io.IOException; +import java.util.logging.LogManager; + +import io.helidon.config.Config; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbStatementType; +import io.helidon.dbclient.health.DbClientHealthCheck; +import io.helidon.dbclient.metrics.DbCounter; +import io.helidon.dbclient.metrics.DbTimer; +import io.helidon.dbclient.tracing.DbClientTracing; +import io.helidon.dbclient.webserver.jsonp.DbResultSupport; +import io.helidon.health.HealthSupport; +import io.helidon.media.jsonb.server.JsonBindingSupport; +import io.helidon.media.jsonp.server.JsonSupport; +import io.helidon.metrics.MetricsSupport; +import io.helidon.tracing.TracerBuilder; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerConfiguration; +import io.helidon.webserver.WebServer; + +/** + * Simple Hello World rest application. + */ +public final class MongoDbExampleMain { + + /** + * Cannot be instantiated. + */ + private MongoDbExampleMain() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + * @throws java.io.IOException if there are problems reading logging properties + */ + public static void main(final String[] args) throws IOException { + startServer(); + } + + /** + * Start the server. + * + * @return the created {@link io.helidon.webserver.WebServer} instance + * @throws java.io.IOException if there are problems reading logging properties + */ + static WebServer startServer() throws IOException { + + // load logging configuration + LogManager.getLogManager().readConfiguration( + MongoDbExampleMain.class.getResourceAsStream("/logging.properties")); + + // By default this will pick up application.yaml from the classpath + Config config = Config.create(); + + // Get webserver config from the "server" section of application.yaml + ServerConfiguration serverConfig = + ServerConfiguration.builder(config.get("server")) + .tracer(TracerBuilder.create("mongo-db").build()) + .build(); + + WebServer server = WebServer.create(serverConfig, createRouting(config)); + + // Start the server and print some info. + server.start().thenAccept(ws -> { + System.out.println( + "WEB server is up! http://localhost:" + ws.port() + "/"); + }); + + // Server threads are not daemon. NO need to block. Just react. + server.whenShutdown().thenRun(() -> System.out.println("WEB server is DOWN. Good bye!")); + + return server; + } + + /** + * Creates new {@link io.helidon.webserver.Routing}. + * + * @param config configuration of this server + * @return routing configured with JSON support, a health check, and a service + */ + private static Routing createRouting(Config config) { + Config dbConfig = config.get("db"); + + DbClient dbClient = DbClient.builder(dbConfig) + // add an interceptor to named statement(s) + .addInterceptor(DbCounter.create(), "select-all", "select-one") + // add an interceptor to statement type(s) + .addInterceptor(DbTimer.create(), DbStatementType.DELETE, DbStatementType.UPDATE, DbStatementType.INSERT) + // add an interceptor to all statements + .addInterceptor(DbClientTracing.create()) + .build(); + + HealthSupport health = HealthSupport.builder() + .addLiveness(DbClientHealthCheck.create(dbClient)) + .build(); + + return Routing.builder() + .register("/db", JsonSupport.create()) + .register("/db", JsonBindingSupport.create()) + .register("/db", DbResultSupport.create()) + .register(health) // Health at "/health" + .register(MetricsSupport.create()) // Metrics at "/metrics" + .register("/db", new PokemonService(dbClient)) + .build(); + } + + private static IllegalStateException noConfigError(String key) { + return new IllegalStateException("Attempting to create a Pokemon service with no configuration" + + ", config key: " + key); + } + +} diff --git a/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/PokemonService.java b/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/PokemonService.java new file mode 100644 index 000000000..b19238397 --- /dev/null +++ b/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/PokemonService.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.examples.dbclient.mongo; + +import io.helidon.dbclient.DbClient; +import io.helidon.examples.dbclient.common.AbstractPokemonService; +import io.helidon.webserver.ServerRequest; +import io.helidon.webserver.ServerResponse; + +/** + * A simple service to greet you. Examples: + * + * Get default greeting message: + * curl -X GET http://localhost:8080/greet + * + * Get greeting message for Joe: + * curl -X GET http://localhost:8080/greet/Joe + * + * Change greeting + * curl -X PUT http://localhost:8080/greet/greeting/Hola + * + * The message is returned as a JSON object + */ + +public class PokemonService extends AbstractPokemonService { + + PokemonService(DbClient dbClient) { + super(dbClient); + } + + /** + * Delete all pokemons. + * + * @param request the server request + * @param response the server response + */ + @Override + protected void deleteAllPokemons(ServerRequest request, ServerResponse response) { + dbClient().execute(exec -> exec + .createNamedDelete("delete-all") + .execute()) + .thenAccept(count -> response.send("Deleted: " + count + " values")) + .exceptionally(throwable -> sendError(throwable, response)); + } +} diff --git a/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/package-info.java b/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/package-info.java new file mode 100644 index 000000000..dfc64d083 --- /dev/null +++ b/examples/dbclient/mongodb/src/main/java/io/helidon/examples/dbclient/mongo/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Quick start demo application. + */ +package io.helidon.examples.dbclient.mongo; diff --git a/examples/dbclient/mongodb/src/main/java/module-info.java b/examples/dbclient/mongodb/src/main/java/module-info.java new file mode 100644 index 000000000..41974ce82 --- /dev/null +++ b/examples/dbclient/mongodb/src/main/java/module-info.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * MongoDB example. + */ +module io.helidon.examples.dbclient.mongodb { + requires java.logging; + + requires io.helidon.config; + requires io.helidon.dbclient.health; + requires io.helidon.dbclient.webserver.jsonp; + requires io.helidon.health; + requires io.helidon.media.jsonb.server; + requires io.helidon.media.jsonp.server; + requires io.helidon.metrics; + requires io.helidon.tracing; + + requires io.helidon.examples.dbclient.common; + requires io.helidon.dbclient.metrics; + requires io.helidon.dbclient.tracing; +} diff --git a/examples/dbclient/mongodb/src/main/resources/application.yaml b/examples/dbclient/mongodb/src/main/resources/application.yaml new file mode 100644 index 000000000..d2e0fdc01 --- /dev/null +++ b/examples/dbclient/mongodb/src/main/resources/application.yaml @@ -0,0 +1,67 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +server: + port: 8079 + host: 0.0.0.0 + features: + print-details: true + +# docker run --rm --name mongo -p 27017:27017 mongo +db: + source: "mongoDb" + connection: + url: "mongodb://127.0.0.1:27017/pokemon" + statements: + # Insert operation contains collection name, operation type and data to be inserted. + # Name variable is stored as MongoDB primary key attribute _id + insert2: '{ + "collection": "pokemons", + "value": { + "_id": $name, + "type": $type + } + }' + select-all: '{ + "collection": "pokemons", + "query": {} + }' + select-one: '{ + "collection": "pokemons", + "query": { + "_id": ? + } + }' + delete-all: '{ + "collection": "pokemons", + "operation": "delete" + }' + update: '{ + "collection": "pokemons", + "query": { + "_id": $name + }, + "value": { + $set: { "type": $type } + } + }' + delete: '{ + "collection": "pokemons", + "query": { + "_id": ? + } + }' + diff --git a/examples/dbclient/mongodb/src/main/resources/logging.properties b/examples/dbclient/mongodb/src/main/resources/logging.properties new file mode 100644 index 000000000..3120aaf00 --- /dev/null +++ b/examples/dbclient/mongodb/src/main/resources/logging.properties @@ -0,0 +1,37 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=java.util.logging.ConsoleHandler + +# Global default logging level. Can be overriden by specific handlers and loggers +.level=INFO + +# Helidon Web Server has a custom log formatter that extends SimpleFormatter. +# It replaces "!thread!" with the current thread name +java.util.logging.ConsoleHandler.level=INFO +java.util.logging.ConsoleHandler.formatter=io.helidon.webserver.WebServerLogFormatter +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +#Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO +#io.netty.level=INFO diff --git a/examples/dbclient/pom.xml b/examples/dbclient/pom.xml new file mode 100644 index 000000000..c1007efdb --- /dev/null +++ b/examples/dbclient/pom.xml @@ -0,0 +1,44 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 2.0-SNAPSHOT + + + pom + + io.helidon.examples.dbclient + helidon-examples-dbclient-project + Helidon Examples DB Client + + + Examples of Helidon DB Client + + + + + jdbc + mongodb + common + + diff --git a/examples/pom.xml b/examples/pom.xml index 74dc4b074..0d678febf 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -1,7 +1,7 @@ + + + 4.0.0 + + + io.helidon.tests.integration.dbclient + helidon-tests-integration-dbclient-project + 2.0-SNAPSHOT + ../pom.xml + + + io.helidon.tests.integration.dbclient + heldion-tests-integration-dbclient-common + Integration Tests: DB Client Common + + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-yaml + + + io.helidon.dbclient + helidon-dbclient-common + + + io.helidon.dbclient + helidon-dbclient-health + + + io.helidon.dbclient + helidon-dbclient-metrics + + + org.glassfish + javax.json + + + org.junit.jupiter + junit-jupiter-api + + + org.hamcrest + hamcrest-all + + + + diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/AbstractIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/AbstractIT.java new file mode 100644 index 000000000..3ad47dd35 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/AbstractIT.java @@ -0,0 +1,239 @@ +package io.helidon.tests.integration.dbclient.common; + +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbMapper; +import io.helidon.dbclient.DbRow; + +/** + * Common testing code. + */ +public abstract class AbstractIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(AbstractIT.class.getName()); + + public static final Config CONFIG = Config.create(ConfigSources.classpath("test.yaml")); + + public static final DbClient DB_CLIENT = initDbClient(); + + public static DbClient initDbClient() { + Config dbConfig = CONFIG.get("db"); + return DbClient.builder(dbConfig).build(); + } + + /** + * Pokemon type POJO. + */ + public static final class Type { + private final int id; + private final String name; + + public Type(int id, String name) { + this.id = id; + this.name = name; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "Type: {id="+id+", name="+name+"}"; + } + + } + + /** + * Pokemon POJO mapper. + */ + public static final class PokemonMapper implements DbMapper { + + public static final PokemonMapper INSTANCE = new PokemonMapper(); + + @Override + public Pokemon read(DbRow row) { + return new Pokemon(row.column("id").as(Integer.class), row.column("name").as(String.class)); + } + + @Override + public Map toNamedParameters(Pokemon value) { + Map params = new HashMap<>(2); + params.put("id", value.getId()); + params.put("name", value.getName()); + return params; + } + + @Override + public List toIndexedParameters(Pokemon value) { + List params = new ArrayList<>(2); + params.add(value.getName()); + params.add(value.getId()); + return params; + } + + } + + /** + * Pokemon POJO. + */ + public static final class Pokemon { + + private final int id; + private final String name; + private final List types; + + public Pokemon(int id, String name, Type... types) { + this.id = id; + this.name = name; + this.types = new ArrayList<>(types != null ? types.length : 0); + if (types != null) { + for (Type type : types) { + this.types.add(type); + } + } + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public List getTypes() { + return types; + } + + public Type[] getTypesArray() { + return types.toArray(new Type[types.size()]); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Pokemon: {id="); + sb.append(id); + sb.append(", name="); + sb.append(name); + sb.append(", types=["); + boolean first = true; + for (Type type : types) { + if (first) { + first = false; + } else { + sb.append(", "); + } + sb.append(type.toString()); + } + sb.append("]}"); + return sb.toString(); + } + + } + + /** Map of pokemon types. */ + public static final Map TYPES = new HashMap<>(); + + // Initialize pokemon types Map + static { + TYPES.put(1, new Type(1, "Normal")); + TYPES.put(2, new Type(2, "Fighting")); + TYPES.put(3, new Type(3, "Flying")); + TYPES.put(4, new Type(4, "Poison")); + TYPES.put(5, new Type(5, "Ground")); + TYPES.put(6, new Type(6, "Rock")); + TYPES.put(7, new Type(7, "Bug")); + TYPES.put(8, new Type(8, "Ghost")); + TYPES.put(9, new Type(9, "Steel")); + TYPES.put(10, new Type(10, "Fire")); + TYPES.put(11, new Type(11, "Water")); + TYPES.put(12, new Type(12, "Grass")); + TYPES.put(13, new Type(13, "Electric")); + TYPES.put(14, new Type(14, "Psychic")); + TYPES.put(15, new Type(15, "Ice")); + TYPES.put(16, new Type(16, "Dragon")); + TYPES.put(17, new Type(17, "Dark")); + TYPES.put(18, new Type(18, "Fairy")); + } + + /** Map of pokemons. */ + public static final Map POKEMONS = new HashMap<>(); + + // Initialize pokemons Map + static { + // Pokemons for query tests + POKEMONS.put(1, new Pokemon(1, "Pikachu", TYPES.get(13))); + POKEMONS.put(2, new Pokemon(2, "Raichu", TYPES.get(13))); + POKEMONS.put(3, new Pokemon(3, "Machop", TYPES.get(2))); + POKEMONS.put(4, new Pokemon(4, "Snorlax", TYPES.get(1))); + POKEMONS.put(5, new Pokemon(5, "Charizard", TYPES.get(10), TYPES.get(3))); + POKEMONS.put(6, new Pokemon(6, "Meowth", TYPES.get(1))); + POKEMONS.put(7, new Pokemon(7, "Gyarados", TYPES.get(3), TYPES.get(11))); + } + + /** Last used id in Pokemons table. */ + public static final int LAST_POKEMON_ID = 5; + + /** Select statement with named arguments for Pokemon class. */ + public static final String SELECT_POKEMON_NAMED_ARG + = CONFIG.get("db.statements.select-pokemon-named-arg").asString().get(); + + /** Select statement with ordered arguments for Pokemon class. */ + public static final String SELECT_POKEMON_ORDER_ARG + = CONFIG.get("db.statements.select-pokemon-order-arg").asString().get(); + + /** Insert statement with named arguments for Pokemon class. */ + public static final String INSERT_POKEMON_NAMED_ARG + = CONFIG.get("db.statements.insert-pokemon-named-arg").asString().get(); + + /** Insert statement with ordered arguments for Pokemon class. */ + public static final String INSERT_POKEMON_ORDER_ARG + = CONFIG.get("db.statements.insert-pokemon-order-arg").asString().get(); + + /** Update statement with named arguments for Pokemon class. */ + public static final String UPDATE_POKEMON_NAMED_ARG + = CONFIG.get("db.statements.update-pokemon-named-arg").asString().get(); + + /** Update statement with ordered arguments for Pokemon class. */ + public static final String UPDATE_POKEMON_ORDER_ARG + = CONFIG.get("db.statements.update-pokemon-order-arg").asString().get(); + + /** Delete statement with named arguments for Pokemon class. */ + public static final String DELETE_POKEMON_NAMED_ARG + = CONFIG.get("db.statements.delete-pokemon-named-arg").asString().get(); + + /** Delete statement with ordered arguments for Pokemon class. */ + public static final String DELETE_POKEMON_ORDER_ARG + = CONFIG.get("db.statements.delete-pokemon-order-arg").asString().get(); + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/spi/MapperProvider.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/spi/MapperProvider.java new file mode 100644 index 000000000..d4368038c --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/spi/MapperProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.spi; + +import java.util.Optional; + +import io.helidon.dbclient.DbMapper; +import io.helidon.dbclient.spi.DbMapperProvider; +import io.helidon.tests.integration.dbclient.common.AbstractIT; +import io.helidon.tests.integration.dbclient.common.utils.RangePoJo; + +/** + * Mapper provider used in integration tests. + */ +public class MapperProvider implements DbMapperProvider { + + @Override + @SuppressWarnings("unchecked") + public Optional> mapper(Class type) { + if (type.equals(RangePoJo.class)) { + return Optional.of((DbMapper) RangePoJo.Mapper.INSTANCE); + } else if (type.equals(AbstractIT.Pokemon.class)) { + return Optional.of((DbMapper) AbstractIT.PokemonMapper.INSTANCE); + } + return Optional.empty(); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/dbresult/FlowControlIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/dbresult/FlowControlIT.java new file mode 100644 index 000000000..2f3f36976 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/dbresult/FlowControlIT.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.dbresult; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Flow.Subscriber; +import java.util.concurrent.Flow.Subscription; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.tests.integration.dbclient.common.AbstractIT.Type; + +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.TYPES; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Verify proper flow control handling in query processing. + */ +public class FlowControlIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(FlowControlIT.class.getName()); + + /** + * Test subscriber. + * Verifies proper flow control handling of returned pokemon types. + */ + private static final class TestSubscriber implements Subscriber { + + /** Requests sequence. Total amount of pokemon types is 18. */ + private static final int[] REQUESTS = new int[] {3, 5, 4, 6, 1}; + + /** Subscription instance. */ + private Subscription subscription; + /** Current index of REQUESTS array. */ + private int reqIdx; + /** Currently requested amount. */ + private int requested; + /** Currently remaining from last request. */ + private int remaining; + /** Total amount of records processed. */ + private int total; + /** Whether processing was finished. */ + private boolean finished; + /** Error message to terminate test. */ + private String error; + + private TestSubscriber() { + } + + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + total = 0; + reqIdx = 0; + finished = false; + error = null; + // Initially request 3 DbRows. + requested = REQUESTS[reqIdx]; + remaining = REQUESTS[reqIdx++]; + LOGGER.info(() -> String.format("Requesting first rows: %d", requested)); + this.subscription.request(requested); + } + + @Override + public void onNext(final DbRow dbRow) { + final Type type = new Type(dbRow.column(1).as(Integer.class), dbRow.column(2).as(String.class)); + total++; + if (remaining > 0) { + LOGGER.info(() -> String.format( + "NEXT: tot: %d req: %d rem: %d type: %s", total, requested, remaining, type.toString())); + remaining -= 1; + if (remaining == 0 && reqIdx < REQUESTS.length) { + LOGGER.info(() -> String.format("Notifying main thread to request more rows")); + synchronized (this) { + this.notify(); + } + } + // Shall not recieve dbRow when not requested! + } else { + LOGGER.warning(() -> String.format( + "NEXT: tot: %d req: %d rem: %d type: %s", total, requested, remaining, type.toString())); + throw new IllegalStateException(String.format("Recieved unexpected row: %s", type.toString())); + } + } + + @Override + public void onError(Throwable throwable) { + error = throwable.getMessage(); + LOGGER.warning(() -> String.format("EXCEPTION: %s", throwable.getMessage())); + finished = true; + } + + @Override + public void onComplete() { + LOGGER.info(() -> String.format("COMPLETE: tot: %d req: %d rem: %d", total, requested, remaining)); + finished = true; + synchronized (this) { + this.notify(); + } + } + + public boolean canRequestNext() { + return remaining == 0 && reqIdx < REQUESTS.length; + } + + public void requestNext() { + if (reqIdx < REQUESTS.length) { + requested = remaining = REQUESTS[reqIdx++]; + LOGGER.info(() -> String.format("Requesting more rows: %d", requested)); + this.subscription.request(requested); + } else { + fail("Can't request more rows, processing shall be finished now."); + } + } + + } + + /** + * Source data verification. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testSourceData() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-types") + ).toCompletableFuture().get(); + assertThat(rows, notNullValue()); + List list = rows.collect().toCompletableFuture().get(); + assertThat(list, not(empty())); + assertThat(list.size(), equalTo(18)); + for (DbRow row : list) { + Integer id = row.column(1).as(Integer.class); + String name = row.column(2).as(String.class); + final Type type = new Type(id, name); + assertThat(name, TYPES.get(id).getName().equals(name)); + LOGGER.info(() -> String.format("Type: %s", type.toString())); + } + } + + /** + * Flow control test. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testFlowControl() throws ExecutionException, InterruptedException { + CompletableFuture rowsFuture = new CompletableFuture<>(); + TestSubscriber subscriber = new TestSubscriber(); + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-types") + ).toCompletableFuture().get(); + rows.publisher().subscribe(subscriber); + while (!subscriber.finished) { + synchronized (subscriber) { + try { + subscriber.wait(20000); + } catch (InterruptedException ex) { + fail(String.format("Test failed with exception: %s", ex.getMessage())); + } + } + if (subscriber.canRequestNext()) { + // Small delay before requesting next records to see whether some unexpected will come + Thread.sleep(500); + subscriber.requestNext(); + } else { + LOGGER.info(() -> String.format("All requests were already done.")); + } + } + if (subscriber.error != null) { + fail(subscriber.error); + } + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/health/HealthCheckIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/health/HealthCheckIT.java new file mode 100644 index 000000000..c57cd2510 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/health/HealthCheckIT.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.health; + +import java.util.logging.Logger; + +import io.helidon.dbclient.health.DbClientHealthCheck; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Verify that health check works. + */ +public class HealthCheckIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(HealthCheckIT.class.getName()); + + /** + * Verify health BASIC check implementation. + */ + @Test + public void testHealthCheck() { + HealthCheck check = DbClientHealthCheck.create(DB_CLIENT); + HealthCheckResponse response = check.call(); + HealthCheckResponse.State state = response.getState(); + assertThat(state, equalTo(HealthCheckResponse.State.UP)); + } + + /** + * Verify health check implementation with builder and custom name. + */ + @Test + public void testHealthCheckWithName() { + final String hcName = "TestHC"; + HealthCheck check = DbClientHealthCheck.builder(DB_CLIENT).name(hcName).build(); + HealthCheckResponse response = check.call(); + String name = response.getName(); + HealthCheckResponse.State state = response.getState(); + assertThat(name, equalTo(hcName)); + assertThat(state, equalTo(HealthCheckResponse.State.UP)); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/health/ServerHealthCheckIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/health/ServerHealthCheckIT.java new file mode 100644 index 000000000..1596718eb --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/health/ServerHealthCheckIT.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.health; + +import java.io.IOException; +import java.io.StringReader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonReader; +import javax.json.JsonStructure; +import javax.json.JsonValue; +import javax.json.stream.JsonParsingException; + +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.dbclient.health.DbClientHealthCheck; +import io.helidon.health.HealthSupport; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerConfiguration; +import io.helidon.webserver.WebServer; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.CONFIG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Verify health check in web server environment. + */ +public class ServerHealthCheckIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(ServerHealthCheckIT.class.getName()); + + private static WebServer SERVER; + private static String URL; + + private static Routing createRouting() { + final HealthSupport health = HealthSupport.builder() + .addLiveness(DbClientHealthCheck.create(DB_CLIENT)) + .build(); + return Routing.builder() + .register(health) // Health at "/health" + .build(); + } + + /** + * Start Helidon Web Server with DB Client health check support. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @BeforeAll + public static void startup() throws InterruptedException, ExecutionException { + final ServerConfiguration serverConfig = ServerConfiguration.builder(CONFIG.get("server")) + .build(); + final WebServer server = WebServer.create(serverConfig, createRouting()); + final CompletionStage serverFuture = server.start(); + serverFuture.thenAccept(srv -> { + LOGGER.info(() -> String.format("WEB server is running at http://%s:%d", srv.configuration().bindAddress(), srv.port())); + URL = String.format("http://localhost:%d", srv.port()); + }); + SERVER = serverFuture.toCompletableFuture().get(); + } + + /** + * Stop Helidon Web Server with DB Client health check support. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @AfterAll + public static void shutdown() throws InterruptedException, ExecutionException { + SERVER.shutdown().toCompletableFuture().get(); + } + + /** + * Retrieve server health status from Helidon Web Server. + * + * @param url server health status URL + * @return server health status response (JSON) + * @throws IOException if an I/O error occurs when sending or receiving HTTP request + * @throws InterruptedException if the current thread was interrupted + */ + private static String get(String url) throws IOException, InterruptedException { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Accept", "application/json") + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + return response.body(); + } + + /** + * Read and check DB Client health status from Helidon Web Server. + * + * @throws InterruptedException if the current thread was interrupted + * @throws IOException if an I/O error occurs when sending or receiving HTTP request + */ + @Test + public void testHttpHealth() throws IOException, InterruptedException, ExecutionException { + // Call select-pokemons to warm up server + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-pokemons") + ).toCompletableFuture().get(); + List pokemonList = rows.collect().toCompletableFuture().get(); + // Read and process health check response + String response = get(URL + "/health"); + LOGGER.info("RESPONSE: " + response); + JsonStructure jsonResponse = null; + try (JsonReader jr = Json.createReader(new StringReader(response))) { + jsonResponse = jr.read(); + } catch (JsonParsingException | IllegalStateException ex) { + fail(String.format("Error parsing response: %s", ex.getMessage())); + } + JsonArray checks = jsonResponse.asJsonObject().getJsonArray("checks"); + assertThat(checks.size(), greaterThan(0)); + for (JsonValue check : checks) { + String name = check.asJsonObject().getString("name"); + String state = check.asJsonObject().getString("state"); + String status = check.asJsonObject().getString("status"); + assertThat(state, equalTo("UP")); + assertThat(status, equalTo("UP")); + } + } + + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/interceptor/InterceptorIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/interceptor/InterceptorIT.java new file mode 100644 index 000000000..4bc12897d --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/interceptor/InterceptorIT.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.interceptor; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.config.Config; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbInterceptor; +import io.helidon.dbclient.DbInterceptorContext; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.POKEMONS; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Verify interceptors handling. + */ +public class InterceptorIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(InterceptorIT.class.getName()); + + private static final class TestInterceptor implements DbInterceptor { + + private boolean called; + private DbInterceptorContext context; + + private TestInterceptor() { + this.called = false; + this.context = null; + } + + @Override + public CompletionStage statement(DbInterceptorContext context) { + this.called = true; + this.context = context; + return CompletableFuture.completedFuture(context); + } + + private boolean called() { + return called; + } + + private DbInterceptorContext getContext() { + return context; + } + + } + + private static DbClient initDbClient(TestInterceptor interceptor) { + Config dbConfig = AbstractIT.CONFIG.get("db"); + return DbClient.builder(dbConfig).addInterceptor(interceptor).build(); + } + + /** + * Check that statement interceptor was called before statement execution. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testStatementInterceptor() throws ExecutionException, InterruptedException { + TestInterceptor interceptor = new TestInterceptor(); + DbClient dbClient = initDbClient(interceptor); + DbRows rows = dbClient.execute(exec -> exec + .createNamedQuery("select-pokemon-named-arg") + .addParam("name", POKEMONS.get(6).getName()).execute() + ).toCompletableFuture().get(); + assertThat(interceptor.called(), equalTo(true)); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/mapping/MapperIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/mapping/MapperIT.java new file mode 100644 index 000000000..5b486a086 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/mapping/MapperIT.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.mapping; + +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyDeletePokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyInsertPokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyPokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyUpdatePokemon; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Verify mapping interface. + * Pokemon POJO mapper is defined in parent class. + */ +public class MapperIT extends AbstractIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(MapperIT.class.getName()); + /** Maximum Pokemon ID. */ + private static final int BASE_ID = LAST_POKEMON_ID + 400; + + private static void addPokemon(Pokemon pokemon) throws ExecutionException, InterruptedException { + POKEMONS.put(pokemon.getId(), pokemon); + Long result = DB_CLIENT.execute(exec -> exec + .namedInsert("insert-pokemon", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Initialize tests of basic JDBC updates. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @BeforeAll + public static void setup() throws ExecutionException, InterruptedException { + try { + // BASE_ID+1, 2 is used for inserts + int curId = BASE_ID+2; + addPokemon(new Pokemon(++curId, "Moltres", TYPES.get(3), TYPES.get(10))); // BASE_ID+3 + addPokemon(new Pokemon(++curId, "Masquerain", TYPES.get(3), TYPES.get(7))); // BASE_ID+4 + addPokemon(new Pokemon(++curId, "Makuhita", TYPES.get(2))); // BASE_ID+5 + addPokemon(new Pokemon(++curId, "Hariyama", TYPES.get(2))); // BASE_ID+6 + } catch (Exception ex) { + LOGGER.warning(() -> String.format("Exception in setup: %s", ex.getMessage())); + throw ex; + } + } + + /** + * Verify insertion of PoJo instance using indexed mapping. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @Test + public void testInsertWithOrderMapping() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+1 , "Articuno", TYPES.get(3), TYPES.get(15)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedInsert("insert-pokemon-order-arg-rev") + .indexedParam(pokemon) + .execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify insertion of PoJo instance using named mapping. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @Test + public void testInsertWithNamedMapping() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+2 , "Zapdos", TYPES.get(3), TYPES.get(13)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedInsert("insert-pokemon-named-arg") + .namedParam(pokemon) + .execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify update of PoJo instance using indexed mapping. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @Test + public void testUpdateWithOrderMapping() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+3 , "Masquerain", TYPES.get(3), TYPES.get(15)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedUpdate("update-pokemon-order-arg") + .indexedParam(pokemon) + .execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + + /** + * Verify update of PoJo instance using named mapping. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @Test + public void testUpdateWithNamedMapping() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+4 , "ZapdMoltresos", TYPES.get(3), TYPES.get(13)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedUpdate("update-pokemon-named-arg") + .namedParam(pokemon) + .execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + + /** + * Verify delete of PoJo instance using indexed mapping. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @Test + public void testDeleteWithOrderMapping() throws ExecutionException, InterruptedException { + Pokemon pokemon = POKEMONS.get(BASE_ID+5); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDelete("delete-pokemon-full-order-arg") + .indexedParam(pokemon) + .execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, pokemon); + } + + /** + * Verify delete of PoJo instance using named mapping. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @Test + public void testDeleteWithNamedMapping() throws ExecutionException, InterruptedException { + Pokemon pokemon = POKEMONS.get(BASE_ID+6); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDelete("delete-pokemon-full-named-arg") + .namedParam(pokemon) + .execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, pokemon); + } + + /** + * Verify query of PoJo instance as a result using mapping. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @Test + public void testQueryWithMapping() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pokemon-named-arg") + .addParam("name", POKEMONS.get(2).getName()).execute() + ).toCompletableFuture().get(); + DbRows pokemonRows = rows.map(Pokemon.class); + Pokemon pokemon = pokemonRows.collect().toCompletableFuture().get().get(0); + verifyPokemon(pokemon, POKEMONS.get(2)); + } + + /** + * Verify get of PoJo instance as a result using mapping. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @Test + public void testGetWithMapping() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .createNamedGet("select-pokemon-named-arg") + .addParam("name", POKEMONS.get(3).getName()).execute() + ).toCompletableFuture().get(); + assertThat(maybeRow.isPresent(), equalTo(true)); + Pokemon pokemon = maybeRow.get().as(Pokemon.class); + verifyPokemon(pokemon, POKEMONS.get(3)); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/metrics/ServerMetricsCheckIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/metrics/ServerMetricsCheckIT.java new file mode 100644 index 000000000..3d36ae2f8 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/metrics/ServerMetricsCheckIT.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.metrics; + +import java.io.IOException; +import java.io.StringReader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonReader; +import javax.json.JsonStructure; +import javax.json.JsonValue; +import javax.json.stream.JsonParsingException; + +import io.helidon.config.Config; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.dbclient.DbStatementType; +import io.helidon.dbclient.metrics.DbCounter; +import io.helidon.dbclient.metrics.DbTimer; +import io.helidon.metrics.MetricsSupport; +import io.helidon.tests.integration.dbclient.common.AbstractIT; +import io.helidon.tests.integration.dbclient.common.AbstractIT.Pokemon; +import io.helidon.webserver.Routing; +import io.helidon.webserver.ServerConfiguration; +import io.helidon.webserver.WebServer; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.CONFIG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.LAST_POKEMON_ID; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.TYPES; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Verify metrics check in web server environment. + */ +public class ServerMetricsCheckIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(ServerMetricsCheckIT.class.getName()); + + /** Maximum Pokemon ID. */ + private static final int BASE_ID = LAST_POKEMON_ID + 300; + + private static DbClient DB_CLIENT; + private static WebServer SERVER; + private static String URL; + + private static Routing createRouting() { + return Routing.builder() + .register(MetricsSupport.create()) // Metrics at "/metrics" + .build(); + } + + /** + * Initialize DB Client from test configuration. + * + * @return DB Client instance + */ + private static DbClient initDbClient() { + Config dbConfig = AbstractIT.CONFIG.get("db"); + return DbClient.builder(dbConfig) + // add an interceptor to named statement(s) + .addInterceptor(DbCounter.create(), "select-pokemons", "insert-pokemon") + // add an interceptor to statement type(s) + .addInterceptor(DbTimer.create(), DbStatementType.INSERT) + .build(); + } + + /** + * Start Helidon Web Server with DB Client metrics support. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @BeforeAll + public static void startup() throws InterruptedException, ExecutionException { + DB_CLIENT = initDbClient(); + final ServerConfiguration serverConfig = ServerConfiguration.builder(CONFIG.get("server")) + .build(); + final WebServer server = WebServer.create(serverConfig, createRouting()); + final CompletionStage serverFuture = server.start(); + serverFuture.thenAccept(srv -> { + LOGGER.info(() -> String.format("WEB server is running at http://%s:%d", srv.configuration().bindAddress(), srv.port())); + URL = String.format("http://localhost:%d", srv.port()); + }); + SERVER = serverFuture.toCompletableFuture().get(); + } + + /** + * Stop Helidon Web Server with DB Client metrics support. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @AfterAll + public static void shutdown() throws InterruptedException, ExecutionException { + SERVER.shutdown().toCompletableFuture().get(); + } + + /** + * Retrieve server metrics status from Helidon Web Server. + * + * @param url server health status URL + * @return server health status response (JSON) + * @throws IOException if an I/O error occurs when sending or receiving HTTP request + * @throws InterruptedException if the current thread was interrupted + */ + private static String get(String url) throws IOException, InterruptedException { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Accept", "application/json") + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + return response.body(); + } + + /** + * Read and check DB Client metrics from Helidon Web Server. + * + * @throws InterruptedException if the current thread was interrupted + * @throws IOException if an I/O error occurs when sending or receiving HTTP request + * @throws ExecutionException when database query failed + */ + @Test + public void testHttpMetrics() throws IOException, InterruptedException, ExecutionException { + // Call select-pokemons to trigger it + DbRows rows; + try { + rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-pokemons") + ).toCompletableFuture().get(); + } catch (Throwable t) { + t.printStackTrace(); + throw t; + } + List pokemonList = rows.collect().toCompletableFuture().get(); + // Call insert-pokemon to trigger it + Pokemon pokemon = new Pokemon(BASE_ID+1, "Lickitung", TYPES.get(1)); + Long result = DB_CLIENT.execute(exec -> exec + .namedInsert("insert-pokemon", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + // Read and process metrics response + String response = get(URL + "/metrics"); + LOGGER.info("RESPONSE: " + response); + JsonStructure jsonResponse = null; + try (JsonReader jr = Json.createReader(new StringReader(response))) { + jsonResponse = jr.read(); + } catch (JsonParsingException | IllegalStateException ex) { + fail(String.format("Error parsing response: %s", ex.getMessage())); + } + assertThat(jsonResponse, notNullValue()); + assertThat(jsonResponse.getValueType(), equalTo(JsonValue.ValueType.OBJECT)); + assertThat(jsonResponse.asJsonObject().containsKey("application"), equalTo(true)); + JsonObject application = jsonResponse.asJsonObject().getJsonObject("application"); + assertThat(application.size(), greaterThan(0)); + assertThat(application.containsKey("db.counter.select-pokemons"), equalTo(true)); + assertThat(application.containsKey("db.counter.insert-pokemon"), equalTo(true)); + int selectPokemons = application.getInt("db.counter.select-pokemons"); + int insertPokemons = application.getInt("db.counter.insert-pokemon"); + assertThat(selectPokemons, equalTo(1)); + assertThat(insertPokemons, equalTo(1)); + assertThat(application.containsKey("db.timer.insert-pokemon"), equalTo(true)); + JsonObject insertTimer = application.getJsonObject("db.timer.insert-pokemon"); + assertThat(insertTimer.containsKey("count"), equalTo(true)); + assertThat(insertTimer.containsKey("min"), equalTo(true)); + assertThat(insertTimer.containsKey("max"), equalTo(true)); + int timerCount = insertTimer.getInt("count"); + assertThat(timerCount, equalTo(1)); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/ExceptionalStmtIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/ExceptionalStmtIT.java new file mode 100644 index 000000000..07d903161 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/ExceptionalStmtIT.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.simple; + +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbClientException; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.LAST_POKEMON_ID; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.POKEMONS; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test exceptional states. + */ +public class ExceptionalStmtIT extends AbstractIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(ExceptionalStmtIT.class.getName()); + + /** Maximum Pokemon ID. */ + private static final int BASE_ID = LAST_POKEMON_ID + 40; + + /** + * Verify that execution of query with non existing named statement throws an exception. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryNonExistentStmt() throws ExecutionException, InterruptedException { + LOGGER.info(() -> "Starting test"); + try { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pokemons-not-exists") + .execute() + ).toCompletableFuture().get(); + LOGGER.warning(() -> "Test failed"); + fail("Execution of non existing statement shall cause an exception to be thrown."); + } catch (DbClientException ex) { + LOGGER.info(() -> String.format("Expected exception: %s", ex.getMessage())); + } + } + + /** + * Verify that execution of query with both named and ordered arguments throws an exception. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryNamedAndOrderArgsWithoutArgs() throws ExecutionException, InterruptedException { + LOGGER.info(() -> "Starting test"); + try { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pokemons-error-arg") + .execute() + ).toCompletableFuture().get(); + LOGGER.warning(() -> "Test failed"); + fail("Execution of query with both named and ordered parameters without passing any shall fail."); + } catch (DbClientException | ExecutionException ex) { + LOGGER.info(() -> String.format("Expected exception: %s", ex.getMessage())); + } + } + + /** + * Verify that execution of query with both named and ordered arguments throws an exception. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryNamedAndOrderArgsWithArgs() throws ExecutionException, InterruptedException { + LOGGER.info(() -> "Starting test"); + try { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pokemons-error-arg") + .addParam("id", POKEMONS.get(5).getId()) + .addParam(POKEMONS.get(5).getName()) + .execute() + ).toCompletableFuture().get(); + LOGGER.warning(() -> "Test failed"); + fail("Execution of query with both named and ordered parameters without passing them shall fail."); + } catch (DbClientException | ExecutionException ex) { + LOGGER.info(() -> String.format("Expected exception: %s", ex.getMessage())); + } + } + + /** + * Verify that execution of query with named arguments throws an exception while trying to set ordered argument. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryNamedArgsSetOrderArg() throws ExecutionException, InterruptedException { + LOGGER.info(() -> "Starting test"); + try { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pokemon-named-arg") + .addParam(POKEMONS.get(5).getName()) + .execute() + ).toCompletableFuture().get(); + LOGGER.warning(() -> "Test failed"); + fail("Execution of query with named parameter with passing ordered parameter value shall fail."); + } catch (DbClientException | ExecutionException ex) { + LOGGER.info(() -> String.format("Expected exception: %s", ex.getMessage())); + } + } + + /** + * Verify that execution of query with ordered arguments throws an exception while trying to set named argument. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryOrderArgsSetNamedArg() throws ExecutionException, InterruptedException { + LOGGER.info(() -> "Starting test"); + try { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pokemon-order-arg") + .addParam("name", POKEMONS.get(6).getName()) + .execute() + ).toCompletableFuture().get(); + LOGGER.warning(() -> "Test failed"); + fail("Execution of query with ordered parameter with passing named parameter value shall fail."); + } catch (DbClientException | ExecutionException ex) { + LOGGER.info(() -> String.format("Expected exception: %s", ex.getMessage())); + } + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleDeleteIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleDeleteIT.java new file mode 100644 index 000000000..346018db3 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleDeleteIT.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.simple; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.TYPES; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyDeletePokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyInsertPokemon; + +/** + * Test set of basic JDBC delete calls. + */ +public class SimpleDeleteIT extends AbstractIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(SimpleDeleteIT.class.getName()); + + /** Maximum Pokemon ID. */ + private static final int BASE_ID = LAST_POKEMON_ID + 30; + + /** Map of pokemons for update tests. */ + private static final Map POKEMONS = new HashMap<>(); + + private static void addPokemon(Pokemon pokemon) throws ExecutionException, InterruptedException { + POKEMONS.put(pokemon.getId(), pokemon); + Long result = DB_CLIENT.execute(exec -> exec + .namedInsert("insert-pokemon", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Initialize tests of basic JDBC deletes. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @BeforeAll + public static void setup() throws ExecutionException, InterruptedException { + try { + int curId = BASE_ID; + addPokemon(new Pokemon(++curId, "Rayquaza", TYPES.get(3), TYPES.get(16))); // BASE_ID+1 + addPokemon(new Pokemon(++curId, "Lugia", TYPES.get(3), TYPES.get(14))); // BASE_ID+2 + addPokemon(new Pokemon(++curId, "Ho-Oh", TYPES.get(3), TYPES.get(10))); // BASE_ID+3 + addPokemon(new Pokemon(++curId, "Raikou", TYPES.get(13))); // BASE_ID+4 + addPokemon(new Pokemon(++curId, "Giratina", TYPES.get(8), TYPES.get(16))); // BASE_ID+5 + addPokemon(new Pokemon(++curId, "Regirock", TYPES.get(6))); // BASE_ID+6 + addPokemon(new Pokemon(++curId, "Kyogre", TYPES.get(11))); // BASE_ID+7 + } catch (Exception ex) { + LOGGER.warning(() -> String.format("Exception in setup: %s", ex)); + throw ex; + } + } + + + /** + * Verify {@code createNamedDelete(String, String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedDeleteStrStrOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDelete("delete-rayquaza", DELETE_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(BASE_ID+1).getId()).execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+1)); + } + + /** + * Verify {@code createNamedDelete(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedDeleteStrNamedArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDelete("delete-pokemon-named-arg") + .addParam("id", POKEMONS.get(BASE_ID+2).getId()).execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+2)); + } + + /** + * Verify {@code createNamedDelete(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedDeleteStrOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDelete("delete-pokemon-order-arg") + .addParam(POKEMONS.get(BASE_ID+3).getId()).execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+3)); + } + + /** + * Verify {@code createDelete(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateDeleteNamedArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .createDelete(DELETE_POKEMON_NAMED_ARG) + .addParam("id", POKEMONS.get(BASE_ID+4).getId()).execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+4)); + } + + /** + * Verify {@code createDelete(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateDeleteOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .createDelete(DELETE_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(BASE_ID+5).getId()).execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+5)); + } + + /** + * Verify {@code namedDelete(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedDeleteOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .namedDelete("delete-pokemon-order-arg", POKEMONS.get(BASE_ID+6).getId()) + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+6)); + } + + /** + * Verify {@code delete(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDeleteOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .delete(DELETE_POKEMON_ORDER_ARG, POKEMONS.get(BASE_ID+7).getId()) + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+7)); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleDmlIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleDmlIT.java new file mode 100644 index 000000000..d3fe42b68 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleDmlIT.java @@ -0,0 +1,423 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.simple; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.LAST_POKEMON_ID; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.TYPES; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyDeletePokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyInsertPokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyUpdatePokemon; + +/** + * Test set of basic JDBC DML statement calls. + */ +public class SimpleDmlIT extends AbstractIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(SimpleDmlIT.class.getName()); + + /** Maximum Pokemon ID. */ + private static final int BASE_ID = LAST_POKEMON_ID + 40; + + /** Map of pokemons for update tests. */ + private static final Map POKEMONS = new HashMap<>(); + + private static void addPokemon(Pokemon pokemon) throws ExecutionException, InterruptedException { + POKEMONS.put(pokemon.getId(), pokemon); + Long result = DB_CLIENT.execute(exec -> exec + .namedInsert("insert-pokemon", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Initialize tests of basic JDBC updates. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @BeforeAll + public static void setup() throws ExecutionException, InterruptedException { + try { + // BASE_ID + 1 .. BASE_ID + 9 is reserved for inserts + // BASE_ID + 10 .. BASE_ID + 19 are pokemons for updates + addPokemon(new Pokemon(BASE_ID + 10, "Piplup", TYPES.get(11))); // BASE_ID+10 + addPokemon(new Pokemon(BASE_ID + 11, "Prinplup", TYPES.get(11))); // BASE_ID+11 + addPokemon(new Pokemon(BASE_ID + 12, "Empoleon", TYPES.get(9), TYPES.get(11))); // BASE_ID+12 + addPokemon(new Pokemon(BASE_ID + 13, "Staryu", TYPES.get(11))); // BASE_ID+13 + addPokemon(new Pokemon(BASE_ID + 14, "Starmie", TYPES.get(11), TYPES.get(14))); // BASE_ID+14 + addPokemon(new Pokemon(BASE_ID + 15, "Horsea", TYPES.get(11))); // BASE_ID+15 + addPokemon(new Pokemon(BASE_ID + 16, "Seadra", TYPES.get(11))); // BASE_ID+16 + // BASE_ID + 20 .. BASE_ID + 29 are pokemons for deletes + addPokemon(new Pokemon(BASE_ID + 20, "Mudkip", TYPES.get(11))); // BASE_ID+20 + addPokemon(new Pokemon(BASE_ID + 21, "Marshtomp", TYPES.get(5), TYPES.get(11))); // BASE_ID+21 + addPokemon(new Pokemon(BASE_ID + 22, "Swampert", TYPES.get(5), TYPES.get(11))); // BASE_ID+22 + addPokemon(new Pokemon(BASE_ID + 23, "Muk", TYPES.get(4))); // BASE_ID+23 + addPokemon(new Pokemon(BASE_ID + 24, "Grimer", TYPES.get(4))); // BASE_ID+24 + addPokemon(new Pokemon(BASE_ID + 25, "Cubchoo", TYPES.get(15))); // BASE_ID+25 + addPokemon(new Pokemon(BASE_ID + 26, "Beartic", TYPES.get(15))); // BASE_ID+26 + } catch (Exception ex) { + LOGGER.warning(() -> String.format("Exception in setup: %s", ex)); + throw ex; + } + } + + /** + * Verify {@code createNamedDmlStatement(String, String)} API method with insert with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedDmlWithInsertStrStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+1, "Torchic", TYPES.get(10)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("insert-torchic", INSERT_POKEMON_NAMED_ARG) + .addParam("id", pokemon.getId()).addParam("name", pokemon.getName()).execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createNamedDmlStatement(String)} API method with insert with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedDmlWithInsertStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+2, "Combusken", TYPES.get(2), TYPES.get(10)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("insert-pokemon-named-arg") + .addParam("id", pokemon.getId()).addParam("name", pokemon.getName()).execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createNamedDmlStatement(String)} API method with insert with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedDmlWithInsertStrOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+3, "Treecko", TYPES.get(12)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("insert-pokemon-order-arg") + .addParam(pokemon.getId()).addParam(pokemon.getName()).execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createDmlStatement(String)} API method with insert with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateDmlWithInsertNamedArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+4, "Grovyle", TYPES.get(12)); + Long result = DB_CLIENT.execute(exec -> exec + .createDmlStatement(INSERT_POKEMON_NAMED_ARG) + .addParam("id", pokemon.getId()).addParam("name", pokemon.getName()).execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createDmlStatement(String)} API method with insert with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateDmlWithInsertOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+5, "Sceptile", TYPES.get(12)); + Long result = DB_CLIENT.execute(exec -> exec + .createDmlStatement(INSERT_POKEMON_ORDER_ARG) + .addParam(pokemon.getId()).addParam(pokemon.getName()).execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code namedDml(String)} API method with insert with ordered parameters passed directly + * to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedDmlWithInsertOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+6, "Snover", TYPES.get(12), TYPES.get(15)); + Long result = DB_CLIENT.execute(exec -> exec + .namedDml("insert-pokemon-order-arg", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code dml(String)} API method with insert with ordered parameters passed directly + * to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlWithInsertOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+7, "Abomasnow", TYPES.get(12), TYPES.get(15)); + Long result = DB_CLIENT.execute(exec -> exec + .dml(INSERT_POKEMON_ORDER_ARG, pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createNamedDmlStatement(String, String)} API method with update with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedDmlWithUpdateStrStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+10); + Pokemon updatedPokemon = new Pokemon(BASE_ID+10, "Prinplup", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("update-piplup", UPDATE_POKEMON_NAMED_ARG) + .addParam("name", updatedPokemon.getName()).addParam("id", updatedPokemon.getId()).execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createNamedDmlStatement(String)} API method with update with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedDmlWithUpdateStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+11); + Pokemon updatedPokemon = new Pokemon(BASE_ID+11, "Empoleon", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("update-pokemon-named-arg") + .addParam("name", updatedPokemon.getName()).addParam("id", updatedPokemon.getId()).execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createNamedDmlStatement(String)} API method with update with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedDmlWithUpdateStrOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+12); + Pokemon updatedPokemon = new Pokemon(BASE_ID+12, "Piplup", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("update-pokemon-order-arg") + .addParam(updatedPokemon.getName()).addParam(updatedPokemon.getId()).execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createDmlStatement(String)} API method with update with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateDmlWithUpdateNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+13); + Pokemon updatedPokemon = new Pokemon(BASE_ID+13, "Starmie", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .createDmlStatement(UPDATE_POKEMON_NAMED_ARG) + .addParam("name", updatedPokemon.getName()).addParam("id", updatedPokemon.getId()).execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createDmlStatement(String)} API method with update with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateDmlWithUpdateOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+14); + Pokemon updatedPokemon = new Pokemon(BASE_ID+14, "Staryu", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .createDmlStatement(UPDATE_POKEMON_ORDER_ARG) + .addParam(updatedPokemon.getName()).addParam(updatedPokemon.getId()).execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code namedDml(String)} API method with update with ordered parameters passed directly + * to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedDmlWithUpdateOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+15); + Pokemon updatedPokemon = new Pokemon(BASE_ID+15, "Seadra", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .namedDml("update-pokemon-order-arg", updatedPokemon.getName(), updatedPokemon.getId()) + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code dml(String)} API method with update with ordered parameters passed directly + * to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlWithUpdateOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+16); + Pokemon updatedPokemon = new Pokemon(BASE_ID+16, "Horsea", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .dml(UPDATE_POKEMON_ORDER_ARG, updatedPokemon.getName(), updatedPokemon.getId()) + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createNamedDmlStatement(String, String)} API method with delete with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedDmlWithDeleteStrStrOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("delete-mudkip", DELETE_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(BASE_ID+20).getId()).execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+20)); + } + + /** + * Verify {@code createNamedDmlStatement(String)} API method with delete with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedDmlWithDeleteStrNamedArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("delete-pokemon-named-arg") + .addParam("id", POKEMONS.get(BASE_ID+21).getId()).execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+21)); + } + + /** + * Verify {@code createNamedDmlStatement(String)} API method with delete with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedDmlWithDeleteStrOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("delete-pokemon-order-arg") + .addParam(POKEMONS.get(BASE_ID+22).getId()).execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+22)); + } + + /** + * Verify {@code createDmlStatement(String)} API method with delete with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateDmlWithDeleteNamedArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .createDmlStatement(DELETE_POKEMON_NAMED_ARG) + .addParam("id", POKEMONS.get(BASE_ID+23).getId()).execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+23)); + } + + /** + * Verify {@code createDmlStatement(String)} API method with delete with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateDmlWithDeleteOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .createDmlStatement(DELETE_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(BASE_ID+24).getId()).execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+24)); + } + + /** + * Verify {@code namedDml(String)} API method with delete with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedDmlWithDeleteOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .namedDml("delete-pokemon-order-arg", POKEMONS.get(BASE_ID+25).getId()) + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+25)); + } + + /** + * Verify {@code dml(String)} API method with delete with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlWithDeleteOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .dml(DELETE_POKEMON_ORDER_ARG, POKEMONS.get(BASE_ID+26).getId()) + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+26)); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleGetIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleGetIT.java new file mode 100644 index 000000000..b3f71b589 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleGetIT.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.simple; + +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +import io.helidon.dbclient.DbRow; +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.POKEMONS; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyPokemon; + +/** + * Test set of basic JDBC get calls. + */ +public class SimpleGetIT extends AbstractIT { + + /** + * Verify {@code createNamedGet(String, String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedGetStrStrNamedArgs() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .createNamedGet("select-pikachu", SELECT_POKEMON_NAMED_ARG) + .addParam("name", POKEMONS.get(1).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(maybeRow, POKEMONS.get(1)); + } + + /** + * Verify {@code createNamedGet(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedGetStrNamedArgs() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .createNamedGet("select-pokemon-named-arg") + .addParam("name", POKEMONS.get(2).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(maybeRow, POKEMONS.get(2)); + } + + /** + * Verify {@code createNamedGet(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedGetStrOrderArgs() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .createNamedGet("select-pokemon-order-arg") + .addParam(POKEMONS.get(3).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(maybeRow, POKEMONS.get(3)); + } + + /** + * Verify {@code createGet(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateGetNamedArgs() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .createGet(SELECT_POKEMON_NAMED_ARG) + .addParam("name", POKEMONS.get(4).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(maybeRow, POKEMONS.get(4)); + } + + /** + * Verify {@code createGet(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateGetOrderArgs() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .createGet(SELECT_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(5).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(maybeRow, POKEMONS.get(5)); + } + + /** + * Verify {@code namedGet(String)} API method with ordered parameters passed directly to the {@code query} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedGetStrOrderArgs() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .namedGet("select-pokemon-order-arg", POKEMONS.get(6).getName()) + ).toCompletableFuture().get(); + verifyPokemon(maybeRow, POKEMONS.get(6)); + } + + /** + * Verify {@code get(String)} API method with ordered parameters passed directly to the {@code query} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testGetStrOrderArgs() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .get(SELECT_POKEMON_ORDER_ARG, POKEMONS.get(7).getName()) + ).toCompletableFuture().get(); + verifyPokemon(maybeRow, POKEMONS.get(7)); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleInsertIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleInsertIT.java new file mode 100644 index 000000000..7ce51b592 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleInsertIT.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.simple; + +import java.util.concurrent.ExecutionException; + +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyInsertPokemon; + +/** + * Test set of basic JDBC inserts. + */ +public class SimpleInsertIT extends AbstractIT { + + /** Maximum Pokemon ID. */ + private static final int BASE_ID = LAST_POKEMON_ID + 10; + + /** + * Verify {@code createNamedInsert(String, String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedInsertStrStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+1, "Bulbasaur", TYPES.get(4), TYPES.get(12)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedInsert("insert-bulbasaur", INSERT_POKEMON_NAMED_ARG) + .addParam("id", pokemon.getId()).addParam("name", pokemon.getName()).execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createNamedInsert(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedInsertStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+2, "Ivysaur", TYPES.get(4), TYPES.get(12)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedInsert("insert-pokemon-named-arg") + .addParam("id", pokemon.getId()).addParam("name", pokemon.getName()).execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createNamedInsert(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedInsertStrOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+3, "Venusaur", TYPES.get(4), TYPES.get(12)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedInsert("insert-pokemon-order-arg") + .addParam(pokemon.getId()).addParam(pokemon.getName()).execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createInsert(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateInsertNamedArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+4, "Magby", TYPES.get(10)); + Long result = DB_CLIENT.execute(exec -> exec + .createInsert(INSERT_POKEMON_NAMED_ARG) + .addParam("id", pokemon.getId()).addParam("name", pokemon.getName()).execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createInsert(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateInsertOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+5, "Magmar", TYPES.get(10)); + Long result = DB_CLIENT.execute(exec -> exec + .createInsert(INSERT_POKEMON_ORDER_ARG) + .addParam(pokemon.getId()).addParam(pokemon.getName()).execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code namedInsert(String)} API method with ordered parameters passed directly to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedInsertOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+6, "Rattata", TYPES.get(1)); + Long result = DB_CLIENT.execute(exec -> exec + .namedInsert("insert-pokemon-order-arg", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code insert(String)} API method with ordered parameters passed directly to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testInsertOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+7, "Raticate", TYPES.get(1)); + Long result = DB_CLIENT.execute(exec -> exec + .insert(INSERT_POKEMON_ORDER_ARG, pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleQueriesIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleQueriesIT.java new file mode 100644 index 000000000..183bc7eee --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleQueriesIT.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.simple; + +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyPokemon; + +/** + * Test set of basic JDBC queries. + */ +public class SimpleQueriesIT extends AbstractIT { + + /** Local logger instance. */ + static final Logger LOGGER = Logger.getLogger(SimpleQueriesIT.class.getName()); + + /** + * Verify {@code createNamedQuery(String, String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryStrStrOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pikachu", SELECT_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(1).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(rows, POKEMONS.get(1)); + } + + /** + * Verify {@code createNamedQuery(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryStrNamedArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pokemon-named-arg") + .addParam("name", POKEMONS.get(2).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(rows, POKEMONS.get(2)); + } + + /** + * Verify {@code createNamedQuery(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryStrOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pokemon-order-arg") + .addParam(POKEMONS.get(3).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(rows, POKEMONS.get(3)); + } + + /** + * Verify {@code createQuery(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateQueryNamedArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createQuery(SELECT_POKEMON_NAMED_ARG) + .addParam("name", POKEMONS.get(4).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(rows, POKEMONS.get(4)); + } + + /** + * Verify {@code createQuery(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateQueryOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createQuery(SELECT_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(5).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(rows, POKEMONS.get(5)); + } + + /** + * Verify {@code namedQuery(String)} API method with ordered parameters passed directly to the {@code namedQuery} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedQueryOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-pokemon-order-arg", POKEMONS.get(6).getName()) + ).toCompletableFuture().get(); + verifyPokemon(rows, POKEMONS.get(6)); + } + + /** + * Verify {@code query(String)} API method with ordered parameters passed directly to the {@code query} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .query(SELECT_POKEMON_ORDER_ARG, POKEMONS.get(7).getName()) + ).toCompletableFuture().get(); + verifyPokemon(rows, POKEMONS.get(7)); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleStatementIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleStatementIT.java new file mode 100644 index 000000000..c3ffda145 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleStatementIT.java @@ -0,0 +1,530 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.simple; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.LAST_POKEMON_ID; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.TYPES; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyDeletePokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyInsertPokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyPokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyUpdatePokemon; + +/** + * Test set of basic JDBC common statement calls. + */ +public class SimpleStatementIT extends AbstractIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(SimpleStatementIT.class.getName()); + + /** Maximum Pokemon ID. */ + private static final int BASE_ID = LAST_POKEMON_ID + 70; + + /** Map of pokemons for update tests. */ + private static final Map POKEMONS = new HashMap<>(); + + private static void addPokemon(AbstractIT.Pokemon pokemon) throws ExecutionException, InterruptedException { + POKEMONS.put(pokemon.getId(), pokemon); + Long result = DB_CLIENT.execute(exec -> exec + .namedInsert("insert-pokemon", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Initialize tests of basic JDBC updates. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @BeforeAll + public static void setup() throws ExecutionException, InterruptedException { + try { + // BASE_ID + 1 .. BASE_ID + 9 is reserved for inserts + // BASE_ID + 10 .. BASE_ID + 19 are pokemons for updates + addPokemon(new AbstractIT.Pokemon(BASE_ID + 10, "Smoochum", TYPES.get(14), TYPES.get(15))); // BASE_ID+10 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 11, "Jynx", TYPES.get(14), TYPES.get(15))); // BASE_ID+11 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 12, "Krabby", TYPES.get(11))); // BASE_ID+12 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 13, "Kingler", TYPES.get(11))); // BASE_ID+13 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 14, "Dratini", TYPES.get(16))); // BASE_ID+14 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 15, "Dragonair", TYPES.get(16))); // BASE_ID+15 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 16, "Dragonite", TYPES.get(3), TYPES.get(16))); // BASE_ID+16 + // BASE_ID + 20 .. BASE_ID + 29 are pokemons for deletes + addPokemon(new AbstractIT.Pokemon(BASE_ID + 20, "Cleffa", TYPES.get(18))); // BASE_ID+20 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 21, "Clefairy", TYPES.get(18))); // BASE_ID+21 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 22, "Clefable", TYPES.get(18))); // BASE_ID+22 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 23, "Misdreavus", TYPES.get(8))); // BASE_ID+23 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 24, "Mismagius", TYPES.get(8))); // BASE_ID+24 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 25, "Growlithe", TYPES.get(10))); // BASE_ID+25 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 26, "Arcanine", TYPES.get(10))); // BASE_ID+26 + } catch (Exception ex) { + LOGGER.warning(() -> String.format("Exception in setup: %s", ex)); + throw ex; + } + } + + /** + * Verify {@code createNamedStatement(String, String)} API method with query with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithQueryStrStrOrderArgs() throws ExecutionException, InterruptedException { + // This call shall register named query + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedStatement("select-pikachu", SELECT_POKEMON_ORDER_ARG) + .addParam(AbstractIT.POKEMONS.get(1).getName()).execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemon(rows, AbstractIT.POKEMONS.get(1)); + } + + /** + * Verify {@code createNamedStatement(String)} API method with query with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithQueryStrNamedArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedStatement("select-pokemon-named-arg") + .addParam("name", AbstractIT.POKEMONS.get(2).getName()).execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemon(rows, AbstractIT.POKEMONS.get(2)); + } + + /** + * Verify {@code createNamedStatement(String)} API method with query with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithQueryStrOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedStatement("select-pokemon-order-arg") + .addParam(AbstractIT.POKEMONS.get(3).getName()).execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemon(rows, AbstractIT.POKEMONS.get(3)); + } + + /** + * Verify {@code createStatement(String)} API method with query with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatementWithQueryNamedArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createStatement(SELECT_POKEMON_NAMED_ARG) + .addParam("name", AbstractIT.POKEMONS.get(4).getName()).execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemon(rows, AbstractIT.POKEMONS.get(4)); + } + + /** + * Verify {@code createStatement(String)} API method with query with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatementWithQueryOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createStatement(SELECT_POKEMON_ORDER_ARG) + .addParam(AbstractIT.POKEMONS.get(5).getName()).execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemon(rows, AbstractIT.POKEMONS.get(5)); + } + + /** + * Verify {@code namedStatement(String)} API method with query with ordered parameters passed directly to the {@code namedQuery} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedStatementWithQueryOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedStatement("select-pokemon-order-arg", AbstractIT.POKEMONS.get(6).getName()) + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemon(rows, AbstractIT.POKEMONS.get(6)); + } + + /** + * Verify {@code statement(String)} API method with query with ordered parameters passed directly to the {@code query} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testStatementWithQueryOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .statement(SELECT_POKEMON_ORDER_ARG, AbstractIT.POKEMONS.get(7).getName()) + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemon(rows, AbstractIT.POKEMONS.get(7)); + } + + /** + * Verify {@code createNamedStatement(String, String)} API method with insert with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithInsertStrStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+1, "Zubat", TYPES.get(3), TYPES.get(4)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("insert-zubat", INSERT_POKEMON_NAMED_ARG) + .addParam("id", pokemon.getId()).addParam("name", pokemon.getName()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createNamedStatement(String)} API method with insert with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithInsertStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+2, "Golbat", TYPES.get(3), TYPES.get(4)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("insert-pokemon-named-arg") + .addParam("id", pokemon.getId()).addParam("name", pokemon.getName()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createNamedStatement(String)} API method with insert with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithInsertStrOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+3, "Crobat", TYPES.get(3), TYPES.get(4)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("insert-pokemon-order-arg") + .addParam(pokemon.getId()).addParam(pokemon.getName()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createStatement(String)} API method with insert with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatementWithInsertNamedArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+4, "Psyduck", TYPES.get(11)); + Long result = DB_CLIENT.execute(exec -> exec + .createStatement(INSERT_POKEMON_NAMED_ARG) + .addParam("id", pokemon.getId()).addParam("name", pokemon.getName()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createStatement(String)} API method with insert with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatementWithInsertOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+5, "Golduck", TYPES.get(11)); + Long result = DB_CLIENT.execute(exec -> exec + .createStatement(INSERT_POKEMON_ORDER_ARG) + .addParam(pokemon.getId()).addParam(pokemon.getName()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code namedStatement(String)} API method with insert with ordered parameters passed directly + * to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedStatementWithInsertOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+6, "Aipom", TYPES.get(1)); + Long result = DB_CLIENT.execute(exec -> exec + .namedStatement("insert-pokemon-order-arg", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code statement(String)} API method with insert with ordered parameters passed directly + * to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testStatementWithInsertOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+7, "Ambipom", TYPES.get(1)); + Long result = DB_CLIENT.execute(exec -> exec + .statement(INSERT_POKEMON_ORDER_ARG, pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createNamedStatement(String, String)} API method with update with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithUpdateStrStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+10); + Pokemon updatedPokemon = new Pokemon(BASE_ID+10, "Prinplup", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("update-piplup", UPDATE_POKEMON_NAMED_ARG) + .addParam("name", updatedPokemon.getName()).addParam("id", updatedPokemon.getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createNamedStatement(String)} API method with update with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithUpdateStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+11); + Pokemon updatedPokemon = new Pokemon(BASE_ID+11, "Empoleon", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("update-pokemon-named-arg") + .addParam("name", updatedPokemon.getName()).addParam("id", updatedPokemon.getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createNamedStatement(String)} API method with update with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithUpdateStrOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+12); + Pokemon updatedPokemon = new Pokemon(BASE_ID+12, "Piplup", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("update-pokemon-order-arg") + .addParam(updatedPokemon.getName()).addParam(updatedPokemon.getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createStatement(String)} API method with update with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatemenWithUpdateNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+13); + Pokemon updatedPokemon = new Pokemon(BASE_ID+13, "Starmie", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .createStatement(UPDATE_POKEMON_NAMED_ARG) + .addParam("name", updatedPokemon.getName()).addParam("id", updatedPokemon.getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createStatement(String)} API method with update with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatemenWithUpdateOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+14); + Pokemon updatedPokemon = new Pokemon(BASE_ID+14, "Staryu", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .createStatement(UPDATE_POKEMON_ORDER_ARG) + .addParam(updatedPokemon.getName()).addParam(updatedPokemon.getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code namedStatement(String)} API method with update with ordered parameters passed directly + * to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedStatemenWithUpdateOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+15); + Pokemon updatedPokemon = new Pokemon(BASE_ID+15, "Seadra", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .namedStatement("update-pokemon-order-arg", updatedPokemon.getName(), updatedPokemon.getId()) + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code statement(String)} API method with update with ordered parameters passed directly + * to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testStatemenWithUpdateOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+16); + Pokemon updatedPokemon = new Pokemon(BASE_ID+16, "Horsea", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .statement(UPDATE_POKEMON_ORDER_ARG, updatedPokemon.getName(), updatedPokemon.getId()) + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createNamedStatement(String, String)} API method with delete with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatemenWithDeleteStrStrOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("delete-mudkip", DELETE_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(BASE_ID+20).getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+20)); + } + + /** + * Verify {@code createNamedStatement(String)} API method with delete with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatemenWithDeleteStrNamedArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("delete-pokemon-named-arg") + .addParam("id", POKEMONS.get(BASE_ID+21).getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+21)); + } + + /** + * Verify {@code createNamedStatement(String)} API method with delete with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatemenWithDeleteStrOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("delete-pokemon-order-arg") + .addParam(POKEMONS.get(BASE_ID+22).getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+22)); + } + + /** + * Verify {@code createStatement(String)} API method with delete with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatemenWithDeleteNamedArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .createStatement(DELETE_POKEMON_NAMED_ARG) + .addParam("id", POKEMONS.get(BASE_ID+23).getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+23)); + } + + /** + * Verify {@code createStatement(String)} API method with delete with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatemenWithDeleteOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .createStatement(DELETE_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(BASE_ID+24).getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+24)); + } + + /** + * Verify {@code namedStatement(String)} API method with delete with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedStatemenWithDeleteOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .namedStatement("delete-pokemon-order-arg", POKEMONS.get(BASE_ID+25).getId()) + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+25)); + } + + /** + * Verify {@code statement(String)} API method with delete with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testStatemenWithDeleteOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(exec -> exec + .statement(DELETE_POKEMON_ORDER_ARG, POKEMONS.get(BASE_ID+26).getId()) + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+26)); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleUpdateIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleUpdateIT.java new file mode 100644 index 000000000..f567b0d2c --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/simple/SimpleUpdateIT.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.simple; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyInsertPokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyUpdatePokemon; + +/** + * Test set of basic JDBC updates. + */ +public class SimpleUpdateIT extends AbstractIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(SimpleUpdateIT.class.getName()); + + /** Maximum Pokemon ID. */ + private static final int BASE_ID = LAST_POKEMON_ID + 20; + + /** Map of pokemons for update tests. */ + private static final Map POKEMONS = new HashMap<>(); + + private static void addPokemon(Pokemon pokemon) throws ExecutionException, InterruptedException { + POKEMONS.put(pokemon.getId(), pokemon); + Long result = DB_CLIENT.execute(exec -> exec + .namedInsert("insert-pokemon", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Initialize tests of basic JDBC updates. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @BeforeAll + public static void setup() throws ExecutionException, InterruptedException { + try { + int curId = BASE_ID; + addPokemon(new Pokemon(++curId, "Spearow", TYPES.get(1), TYPES.get(3))); // BASE_ID+1 + addPokemon(new Pokemon(++curId, "Fearow", TYPES.get(1), TYPES.get(3))); // BASE_ID+2 + addPokemon(new Pokemon(++curId, "Ekans", TYPES.get(4))); // BASE_ID+3 + addPokemon(new Pokemon(++curId, "Arbok", TYPES.get(4))); // BASE_ID+4 + addPokemon(new Pokemon(++curId, "Sandshrew", TYPES.get(5))); // BASE_ID+5 + addPokemon(new Pokemon(++curId, "Sandslash", TYPES.get(5))); // BASE_ID+6 + addPokemon(new Pokemon(++curId, "Diglett", TYPES.get(5))); // BASE_ID+7 + } catch (Exception ex) { + LOGGER.warning(() -> String.format("Exception in setup: %s", ex)); + throw ex; + } + } + + /** + * Verify {@code createNamedUpdate(String, String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedUpdateStrStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+1); + Pokemon updatedPokemon = new Pokemon(BASE_ID+1, "Fearow", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedUpdate("update-spearow", UPDATE_POKEMON_NAMED_ARG) + .addParam("name", updatedPokemon.getName()).addParam("id", updatedPokemon.getId()).execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createNamedUpdate(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedUpdateStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+2); + Pokemon updatedPokemon = new Pokemon(BASE_ID+2, "Spearow", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedUpdate("update-pokemon-named-arg") + .addParam("name", updatedPokemon.getName()).addParam("id", updatedPokemon.getId()).execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createNamedUpdate(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedUpdateStrOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+3); + Pokemon updatedPokemon = new Pokemon(BASE_ID+3, "Arbok", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedUpdate("update-pokemon-order-arg") + .addParam(updatedPokemon.getName()).addParam(updatedPokemon.getId()).execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createUpdate(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateUpdateNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+4); + Pokemon updatedPokemon = new Pokemon(BASE_ID+4, "Ekans", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .createUpdate(UPDATE_POKEMON_NAMED_ARG) + .addParam("name", updatedPokemon.getName()).addParam("id", updatedPokemon.getId()).execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createUpdate(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateUpdateOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+5); + Pokemon updatedPokemon = new Pokemon(BASE_ID+5, "Diglett", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .createUpdate(UPDATE_POKEMON_ORDER_ARG) + .addParam(updatedPokemon.getName()).addParam(updatedPokemon.getId()).execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code namedUpdate(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedUpdateNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+6); + Pokemon updatedPokemon = new Pokemon(BASE_ID+6, "Sandshrew", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .namedUpdate("update-pokemon-order-arg", updatedPokemon.getName(), updatedPokemon.getId()) + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code update(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testUpdateOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+7); + Pokemon updatedPokemon = new Pokemon(BASE_ID+7, "Sandslash", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.execute(exec -> exec + .update(UPDATE_POKEMON_ORDER_ARG, updatedPokemon.getName(), updatedPokemon.getId()) + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/statement/DmlStatementIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/statement/DmlStatementIT.java new file mode 100644 index 000000000..27b29e4e1 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/statement/DmlStatementIT.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.statement; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.LAST_POKEMON_ID; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.TYPES; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyInsertPokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyUpdatePokemon; + +/** + * Test DbStatementDml methods. + */ +public class DmlStatementIT extends AbstractIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(DmlStatementIT.class.getName()); + + /** Maximum Pokemon ID. */ + private static final int BASE_ID = LAST_POKEMON_ID + 100; + + /** Map of pokemons for DbStatementDml methods tests. */ + private static final Map POKEMONS = new HashMap<>(); + + private static void addPokemon(Pokemon pokemon) throws ExecutionException, InterruptedException { + POKEMONS.put(pokemon.getId(), pokemon); + Long result = DB_CLIENT.execute(exec -> exec + .namedInsert("insert-pokemon", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Initialize DbStatementDml methods tests. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @BeforeAll + public static void setup() throws ExecutionException, InterruptedException { + try { + addPokemon(new Pokemon(BASE_ID + 0, "Shinx", TYPES.get(13))); // BASE_ID+0 + addPokemon(new Pokemon(BASE_ID + 1, "Luxio", TYPES.get(13))); // BASE_ID+1 + addPokemon(new Pokemon(BASE_ID + 2, "Luxray", TYPES.get(13))); // BASE_ID+2 + addPokemon(new Pokemon(BASE_ID + 3, "Kricketot", TYPES.get(7))); // BASE_ID+3 + addPokemon(new Pokemon(BASE_ID + 4, "Kricketune", TYPES.get(7))); // BASE_ID+4 + addPokemon(new Pokemon(BASE_ID + 5, "Phione", TYPES.get(11))); // BASE_ID+5 + addPokemon(new Pokemon(BASE_ID + 6, "Chatot", TYPES.get(1), TYPES.get(3))); // BASE_ID+6 + } catch (Exception ex) { + LOGGER.warning(() -> String.format("Exception in setup: %s", ex)); + throw ex; + } + } + + /** + * Verify {@code params(Object... parameters)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlArrayParams() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID + 0, "Luxio", TYPES.get(13)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("update-pokemon-order-arg") + .params(pokemon.getName(), pokemon.getId()) + .execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + + /** + * Verify {@code params(List)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlListParams() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID + 1, "Luxray", TYPES.get(13)); + List params = new ArrayList<>(2); + params.add(pokemon.getName()); + params.add(pokemon.getId()); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("update-pokemon-order-arg") + .params(params) + .execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + + /** + * Verify {@code params(Map)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlMapParams() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID + 2, "Shinx", TYPES.get(13)); + Map params = new HashMap<>(2); + params.put("name", pokemon.getName()); + params.put("id", pokemon.getId()); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("update-pokemon-named-arg") + .params(params) + .execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + + /** + * Verify {@code addParam(Object parameter)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlOrderParam() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID + 3, "Kricketune", TYPES.get(7)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("update-pokemon-order-arg") + .addParam(pokemon.getName()) + .addParam(pokemon.getId()) + .execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + + /** + * Verify {@code addParam(String name, Object parameter)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlNamedParam() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID + 4, "Kricketot", TYPES.get(7)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("update-pokemon-named-arg") + .addParam("name", pokemon.getName()) + .addParam("id", pokemon.getId()) + .execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + + /** + * Verify {@code namedParam(Object parameters)} mapped parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlMappedNamedParam() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID + 5, "Chatot", TYPES.get(1), TYPES.get(3)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("update-pokemon-named-arg") + .namedParam(pokemon) + .execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + + /** + * Verify {@code indexedParam(Object parameters)} mapped parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlMappedOrderParam() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID + 6, "Phione", TYPES.get(11)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedDmlStatement("update-pokemon-order-arg") + .indexedParam(pokemon) + .execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/statement/GenericStatementIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/statement/GenericStatementIT.java new file mode 100644 index 000000000..084dc11d7 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/statement/GenericStatementIT.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.statement; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.tests.integration.dbclient.common.AbstractIT; +import io.helidon.tests.integration.dbclient.common.utils.RangePoJo; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.LAST_POKEMON_ID; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.TYPES; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyInsertPokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyPokemonsIdRange; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyUpdatePokemon; + +/** + * Test DbStatementGeneric methods. + */ +public class GenericStatementIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(GenericStatementIT.class.getName()); + + /** Maximum Pokemon ID. */ + private static final int BASE_ID = LAST_POKEMON_ID + 110; + + /** Map of pokemons for DbStatementDml methods tests. */ + private static final Map POKEMONS = new HashMap<>(); + + private static void addPokemon(AbstractIT.Pokemon pokemon) throws ExecutionException, InterruptedException { + POKEMONS.put(pokemon.getId(), pokemon); + Long result = DB_CLIENT.execute(exec -> exec + .namedInsert("insert-pokemon", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Initialize DbStatementDml methods tests. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @BeforeAll + public static void setup() throws ExecutionException, InterruptedException { + try { + addPokemon(new AbstractIT.Pokemon(BASE_ID + 0, "Klink", TYPES.get(9))); // BASE_ID+0 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 1, "Klang", TYPES.get(9))); // BASE_ID+1 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 2, "Klinklang", TYPES.get(9))); // BASE_ID+2 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 3, "Drilbur", TYPES.get(5))); // BASE_ID+3 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 4, "Excadrill", TYPES.get(5), TYPES.get(9))); // BASE_ID+4 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 5, "Ducklett", TYPES.get(3), TYPES.get(11))); // BASE_ID+5 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 6, "Swanna", TYPES.get(3), TYPES.get(11))); // BASE_ID+6 + } catch (Exception ex) { + LOGGER.warning(() -> String.format("Exception in setup: %s", ex)); + throw ex; + } + } + + /** + * Verify {@code params(Object... parameters)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryArrayParams() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedStatement("select-pokemons-idrng-order-arg") + .params(1, 7) + .execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemonsIdRange(rows, 1, 7); + } + + /** + * Verify {@code params(List)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryListParams() throws ExecutionException, InterruptedException { + List params = new ArrayList<>(2); + params.add(1); + params.add(7); + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedStatement("select-pokemons-idrng-order-arg") + .params(params) + .execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemonsIdRange(rows, 1, 7); + } + + /** + * Verify {@code params(Map)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryMapParams() throws ExecutionException, InterruptedException { + Map params = new HashMap<>(2); + params.put("idmin", 1); + params.put("idmax", 7); + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedStatement("select-pokemons-idrng-named-arg") + .params(params) + .execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemonsIdRange(rows, 1, 7); + } + + /** + * Verify {@code addParam(Object parameter)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryOrderParam() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedStatement("select-pokemons-idrng-order-arg") + .addParam(1) + .addParam(7) + .execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemonsIdRange(rows, 1, 7); + } + + /** + * Verify {@code addParam(String name, Object parameter)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryNamedParam() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedStatement("select-pokemons-idrng-named-arg") + .addParam("idmin", 1) + .addParam("idmax", 7) + .execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemonsIdRange(rows, 1, 7); + } + + /** + * Verify {@code namedParam(Object parameters)} mapped parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryMappedNamedParam() throws ExecutionException, InterruptedException { + RangePoJo range = new RangePoJo(1, 7); + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedStatement("select-pokemons-idrng-named-arg") + .namedParam(range) + .execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemonsIdRange(rows, 1, 7); + } + + /** + * Verify {@code indexedParam(Object parameters)} mapped parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryMappedOrderParam() throws ExecutionException, InterruptedException { + RangePoJo range = new RangePoJo(1, 7); + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedStatement("select-pokemons-idrng-order-arg") + .indexedParam(range) + .execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemonsIdRange(rows, 1, 7); + } + + /** + * Verify {@code params(Object... parameters)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlArrayParams() throws ExecutionException, InterruptedException { + AbstractIT.Pokemon pokemon = new AbstractIT.Pokemon(BASE_ID + 0, "Klang", TYPES.get(9)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("update-pokemon-order-arg") + .params(pokemon.getName(), pokemon.getId()) + .execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + + /** + * Verify {@code params(List)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlListParams() throws ExecutionException, InterruptedException { + AbstractIT.Pokemon pokemon = new AbstractIT.Pokemon(BASE_ID + 1, "Klinklang", TYPES.get(9)); + List params = new ArrayList<>(2); + params.add(pokemon.getName()); + params.add(pokemon.getId()); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("update-pokemon-order-arg") + .params(params) + .execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + + /** + * Verify {@code params(Map)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlMapParams() throws ExecutionException, InterruptedException { + AbstractIT.Pokemon pokemon = new AbstractIT.Pokemon(BASE_ID + 2, "Klink", TYPES.get(9)); + Map params = new HashMap<>(2); + params.put("name", pokemon.getName()); + params.put("id", pokemon.getId()); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("update-pokemon-named-arg") + .params(params) + .execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + + /** + * Verify {@code addParam(Object parameter)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlOrderParam() throws ExecutionException, InterruptedException { + AbstractIT.Pokemon pokemon = new AbstractIT.Pokemon(BASE_ID + 3, "Excadrill", TYPES.get(5), TYPES.get(9)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("update-pokemon-order-arg") + .addParam(pokemon.getName()) + .addParam(pokemon.getId()) + .execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + + /** + * Verify {@code addParam(String name, Object parameter)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlNamedParam() throws ExecutionException, InterruptedException { + AbstractIT.Pokemon pokemon = new AbstractIT.Pokemon(BASE_ID + 4, "Drilbur", TYPES.get(5)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("update-pokemon-named-arg") + .addParam("name", pokemon.getName()) + .addParam("id", pokemon.getId()) + .execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + + /** + * Verify {@code namedParam(Object parameters)} mapped parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlMappedNamedParam() throws ExecutionException, InterruptedException { + AbstractIT.Pokemon pokemon = new AbstractIT.Pokemon(BASE_ID + 5, "Swanna", TYPES.get(3), TYPES.get(11)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("update-pokemon-named-arg") + .namedParam(pokemon) + .execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + + /** + * Verify {@code indexedParam(Object parameters)} mapped parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlMappedOrderParam() throws ExecutionException, InterruptedException { + AbstractIT.Pokemon pokemon = new AbstractIT.Pokemon(BASE_ID + 6, "Ducklett", TYPES.get(3), TYPES.get(11)); + Long result = DB_CLIENT.execute(exec -> exec + .createNamedStatement("update-pokemon-order-arg") + .indexedParam(pokemon) + .execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, pokemon); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/statement/GetStatementIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/statement/GetStatementIT.java new file mode 100644 index 000000000..b8bf90300 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/statement/GetStatementIT.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.statement; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +import io.helidon.dbclient.DbRow; +import io.helidon.tests.integration.dbclient.common.utils.RangePoJo; + +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyPokemonsIdRange; + +/** + * Test DbStatementGet methods. + */ +public class GetStatementIT { + + /** + * Verify {@code params(Object... parameters)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testGetArrayParams() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .createNamedGet("select-pokemons-idrng-order-arg") + .params(1, 3) + .execute() + ).toCompletableFuture().get(); + verifyPokemonsIdRange(maybeRow, 1, 3); + } + + /** + * Verify {@code params(List)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testGetListParams() throws ExecutionException, InterruptedException { + List params = new ArrayList<>(2); + params.add(2); + params.add(4); + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .createNamedGet("select-pokemons-idrng-order-arg") + .params(params) + .execute() + ).toCompletableFuture().get(); + verifyPokemonsIdRange(maybeRow, 2, 4); + } + + /** + * Verify {@code params(Map)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testGetMapParams() throws ExecutionException, InterruptedException { + Map params = new HashMap<>(2); + params.put("idmin", 3); + params.put("idmax", 5); + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .createNamedGet("select-pokemons-idrng-named-arg") + .params(params) + .execute() + ).toCompletableFuture().get(); + verifyPokemonsIdRange(maybeRow, 3, 5); + } + + /** + * Verify {@code addParam(Object parameter)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testGetOrderParam() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .createNamedGet("select-pokemons-idrng-order-arg") + .addParam(4) + .addParam(6) + .execute() + ).toCompletableFuture().get(); + verifyPokemonsIdRange(maybeRow, 4, 6); + } + + /** + * Verify {@code addParam(String name, Object parameter)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testGetNamedParam() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .createNamedGet("select-pokemons-idrng-named-arg") + .addParam("idmin", 5) + .addParam("idmax", 7) + .execute() + ).toCompletableFuture().get(); + verifyPokemonsIdRange(maybeRow, 5, 7); + } + + /** + * Verify {@code namedParam(Object parameters)} mapped parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testGetMappedNamedParam() throws ExecutionException, InterruptedException { + RangePoJo range = new RangePoJo(0, 2); + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .createNamedGet("select-pokemons-idrng-named-arg") + .namedParam(range) + .execute() + ).toCompletableFuture().get(); + verifyPokemonsIdRange(maybeRow, 0, 2); + } + + /** + * Verify {@code indexedParam(Object parameters)} mapped parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testGetMappedOrderParam() throws ExecutionException, InterruptedException { + RangePoJo range = new RangePoJo(6, 8); + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .createNamedGet("select-pokemons-idrng-order-arg") + .indexedParam(range) + .execute() + ).toCompletableFuture().get(); + verifyPokemonsIdRange(maybeRow, 6, 8); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/statement/QueryStatementIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/statement/QueryStatementIT.java new file mode 100644 index 000000000..97640a69f --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/statement/QueryStatementIT.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.statement; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.tests.integration.dbclient.common.AbstractIT; +import io.helidon.tests.integration.dbclient.common.utils.RangePoJo; + +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyPokemonsIdRange; + +/** + * Test DbStatementQuery methods. + */ +public class QueryStatementIT extends AbstractIT { + + /** + * Verify {@code params(Object... parameters)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryArrayParams() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pokemons-idrng-order-arg") + .params(1, 7) + .execute() + ).toCompletableFuture().get(); + verifyPokemonsIdRange(rows, 1, 7); + } + + /** + * Verify {@code params(List)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryListParams() throws ExecutionException, InterruptedException { + List params = new ArrayList<>(2); + params.add(1); + params.add(7); + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pokemons-idrng-order-arg") + .params(params) + .execute() + ).toCompletableFuture().get(); + verifyPokemonsIdRange(rows, 1, 7); + } + + /** + * Verify {@code params(Map)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryMapParams() throws ExecutionException, InterruptedException { + Map params = new HashMap<>(2); + params.put("idmin", 1); + params.put("idmax", 7); + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pokemons-idrng-named-arg") + .params(params) + .execute() + ).toCompletableFuture().get(); + verifyPokemonsIdRange(rows, 1, 7); + } + + /** + * Verify {@code addParam(Object parameter)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryOrderParam() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pokemons-idrng-order-arg") + .addParam(1) + .addParam(7) + .execute() + ).toCompletableFuture().get(); + verifyPokemonsIdRange(rows, 1, 7); + } + + /** + * Verify {@code addParam(String name, Object parameter)} parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryNamedParam() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pokemons-idrng-named-arg") + .addParam("idmin", 1) + .addParam("idmax", 7) + .execute() + ).toCompletableFuture().get(); + verifyPokemonsIdRange(rows, 1, 7); + } + + /** + * Verify {@code namedParam(Object parameters)} mapped parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryMappedNamedParam() throws ExecutionException, InterruptedException { + RangePoJo range = new RangePoJo(1, 7); + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pokemons-idrng-named-arg") + .namedParam(range) + .execute() + ).toCompletableFuture().get(); + verifyPokemonsIdRange(rows, 1, 7); + } + + /** + * Verify {@code indexedParam(Object parameters)} mapped parameters setting method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryMappedOrderParam() throws ExecutionException, InterruptedException { + RangePoJo range = new RangePoJo(1, 7); + DbRows rows = DB_CLIENT.execute(exec -> exec + .createNamedQuery("select-pokemons-idrng-order-arg") + .indexedParam(range) + .execute() + ).toCompletableFuture().get(); + verifyPokemonsIdRange(rows, 1, 7); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionDeleteIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionDeleteIT.java new file mode 100644 index 000000000..0c08bd9dc --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionDeleteIT.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.transaction; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DELETE_POKEMON_ORDER_ARG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.LAST_POKEMON_ID; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.TYPES; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyDeletePokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyInsertPokemon; + +/** + * Test set of basic JDBC delete calls in transaction. + */ +public class TransactionDeleteIT extends AbstractIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(TransactionDeleteIT.class.getName()); + + /** Maximum Pokemon ID. */ + private static final int BASE_ID = LAST_POKEMON_ID + 230; + + /** Map of pokemons for update tests. */ + private static final Map POKEMONS = new HashMap<>(); + + private static void addPokemon(Pokemon pokemon) throws ExecutionException, InterruptedException { + POKEMONS.put(pokemon.getId(), pokemon); + Long result = DB_CLIENT.execute(exec -> exec + .namedInsert("insert-pokemon", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Initialize tests of basic JDBC deletes. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @BeforeAll + public static void setup() throws ExecutionException, InterruptedException { + try { + int curId = BASE_ID; + addPokemon(new Pokemon(++curId, "Omanyte", TYPES.get(6), TYPES.get(11))); // BASE_ID+1 + addPokemon(new Pokemon(++curId, "Omastar", TYPES.get(6), TYPES.get(11))); // BASE_ID+2 + addPokemon(new Pokemon(++curId, "Kabuto", TYPES.get(6), TYPES.get(11))); // BASE_ID+3 + addPokemon(new Pokemon(++curId, "Kabutops", TYPES.get(6), TYPES.get(11))); // BASE_ID+4 + addPokemon(new Pokemon(++curId, "Chikorita", TYPES.get(12))); // BASE_ID+5 + addPokemon(new Pokemon(++curId, "Bayleef", TYPES.get(12))); // BASE_ID+6 + addPokemon(new Pokemon(++curId, "Meganium", TYPES.get(12))); // BASE_ID+7 + } catch (Exception ex) { + LOGGER.warning(() -> String.format("Exception in setup: %s", ex)); + throw ex; + } + } + + /** + * Verify {@code createNamedDelete(String, String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedDeleteStrStrOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedDelete("delete-rayquaza", DELETE_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(BASE_ID+1).getId()).execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+1)); + } + + /** + * Verify {@code createNamedDelete(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedDeleteStrNamedArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedDelete("delete-pokemon-named-arg") + .addParam("id", POKEMONS.get(BASE_ID+2).getId()).execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+2)); + } + + /** + * Verify {@code createNamedDelete(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedDeleteStrOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedDelete("delete-pokemon-order-arg") + .addParam(POKEMONS.get(BASE_ID+3).getId()).execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+3)); + } + + /** + * Verify {@code createDelete(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateDeleteNamedArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.inTransaction(exec -> exec + .createDelete(DELETE_POKEMON_NAMED_ARG) + .addParam("id", POKEMONS.get(BASE_ID+4).getId()).execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+4)); + } + + /** + * Verify {@code createDelete(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateDeleteOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.execute(tx -> tx + .createDelete(DELETE_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(BASE_ID+5).getId()).execute() + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+5)); + } + + /** + * Verify {@code namedDelete(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedDeleteOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.inTransaction(tx -> tx + .namedDelete("delete-pokemon-order-arg", POKEMONS.get(BASE_ID+6).getId()) + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+6)); + } + + /** + * Verify {@code delete(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDeleteOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.inTransaction(tx -> tx + .delete(DELETE_POKEMON_ORDER_ARG, POKEMONS.get(BASE_ID+7).getId()) + ).toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+7)); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionExceptionalStmtIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionExceptionalStmtIT.java new file mode 100644 index 000000000..dcc4ab532 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionExceptionalStmtIT.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.transaction; + +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbClientException; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; + +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.LAST_POKEMON_ID; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.POKEMONS; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test exceptional states. + */ +public class TransactionExceptionalStmtIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(TransactionExceptionalStmtIT.class.getName()); + + /** Maximum Pokemon ID. */ + private static final int BASE_ID = LAST_POKEMON_ID + 40; + + /** + * Verify that execution of query with non existing named statement throws an exception. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryNonExistentStmt() throws ExecutionException, InterruptedException { + LOGGER.info(() -> "Starting test"); + try { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .createNamedQuery("select-pokemons-not-exists") + .execute() + ).toCompletableFuture().get(); + LOGGER.warning(() -> "Test failed"); + fail("Execution of non existing statement shall cause an exception to be thrown."); + } catch (DbClientException ex) { + LOGGER.info(() -> String.format("Expected exception: %s", ex.getMessage())); + } + } + + /** + * Verify that execution of query with both named and ordered arguments throws an exception. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryNamedAndOrderArgsWithoutArgs() throws ExecutionException, InterruptedException { + LOGGER.info(() -> "Starting test"); + try { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .createNamedQuery("select-pokemons-error-arg") + .execute() + ).toCompletableFuture().get(); + LOGGER.warning(() -> "Test failed"); + fail("Execution of query with both named and ordered parameters without passing any shall fail."); + } catch (DbClientException | ExecutionException ex) { + LOGGER.info(() -> String.format("Expected exception: %s", ex.getMessage())); + } + } + + /** + * Verify that execution of query with both named and ordered arguments throws an exception. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryNamedAndOrderArgsWithArgs() throws ExecutionException, InterruptedException { + LOGGER.info(() -> "Starting test"); + try { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .createNamedQuery("select-pokemons-error-arg") + .addParam("id", POKEMONS.get(5).getId()) + .addParam(POKEMONS.get(5).getName()) + .execute() + ).toCompletableFuture().get(); + LOGGER.warning(() -> "Test failed"); + fail("Execution of query with both named and ordered parameters without passing them shall fail."); + } catch (DbClientException | ExecutionException ex) { + LOGGER.info(() -> String.format("Expected exception: %s", ex.getMessage())); + } + } + + /** + * Verify that execution of query with named arguments throws an exception while trying to set ordered argument. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryNamedArgsSetOrderArg() throws ExecutionException, InterruptedException { + LOGGER.info(() -> "Starting test"); + try { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .createNamedQuery("select-pokemon-named-arg") + .addParam(POKEMONS.get(5).getName()) + .execute() + ).toCompletableFuture().get(); + LOGGER.warning(() -> "Test failed"); + fail("Execution of query with named parameter with passing ordered parameter value shall fail."); + } catch (DbClientException | ExecutionException ex) { + LOGGER.info(() -> String.format("Expected exception: %s", ex.getMessage())); + } + } + + /** + * Verify that execution of query with ordered arguments throws an exception while trying to set named argument. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryOrderArgsSetNamedArg() throws ExecutionException, InterruptedException { + LOGGER.info(() -> "Starting test"); + try { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .createNamedQuery("select-pokemon-order-arg") + .addParam("name", POKEMONS.get(6).getName()) + .execute() + ).toCompletableFuture().get(); + LOGGER.warning(() -> "Test failed"); + fail("Execution of query with ordered parameter with passing named parameter value shall fail."); + } catch (DbClientException | ExecutionException ex) { + LOGGER.info(() -> String.format("Expected exception: %s", ex.getMessage())); + } + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionGetIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionGetIT.java new file mode 100644 index 000000000..613e8ecd5 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionGetIT.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.transaction; + +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +import io.helidon.dbclient.DbRow; +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.POKEMONS; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.SELECT_POKEMON_NAMED_ARG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.SELECT_POKEMON_ORDER_ARG; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyPokemon; + +/** + * Test set of basic JDBC get calls in transaction. + */ +public class TransactionGetIT extends AbstractIT { + + /** + * Verify {@code createNamedGet(String, String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedGetStrStrNamedArgs() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.inTransaction(tx -> tx + .createNamedGet("select-pikachu", SELECT_POKEMON_NAMED_ARG) + .addParam("name", POKEMONS.get(1).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(maybeRow, POKEMONS.get(1)); + } + + /** + * Verify {@code createNamedGet(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedGetStrNamedArgs() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.inTransaction(tx -> tx + .createNamedGet("select-pokemon-named-arg") + .addParam("name", POKEMONS.get(2).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(maybeRow, POKEMONS.get(2)); + } + + /** + * Verify {@code createNamedGet(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedGetStrOrderArgs() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.inTransaction(tx -> tx + .createNamedGet("select-pokemon-order-arg") + .addParam(POKEMONS.get(3).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(maybeRow, POKEMONS.get(3)); + } + + /** + * Verify {@code createGet(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateGetNamedArgs() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.inTransaction(tx -> tx + .createGet(SELECT_POKEMON_NAMED_ARG) + .addParam("name", POKEMONS.get(4).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(maybeRow, POKEMONS.get(4)); + } + + /** + * Verify {@code createGet(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateGetOrderArgs() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.inTransaction(tx -> tx + .createGet(SELECT_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(5).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(maybeRow, POKEMONS.get(5)); + } + + /** + * Verify {@code namedGet(String)} API method with ordered parameters passed directly to the {@code query} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedGetStrOrderArgs() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.inTransaction(tx -> tx + .namedGet("select-pokemon-order-arg", POKEMONS.get(6).getName()) + ).toCompletableFuture().get(); + verifyPokemon(maybeRow, POKEMONS.get(6)); + } + + /** + * Verify {@code get(String)} API method with ordered parameters passed directly to the {@code query} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testGetStrOrderArgs() throws ExecutionException, InterruptedException { + Optional maybeRow = DB_CLIENT.inTransaction(tx -> tx + .get(SELECT_POKEMON_ORDER_ARG, POKEMONS.get(7).getName()) + ).toCompletableFuture().get(); + verifyPokemon(maybeRow, POKEMONS.get(7)); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionInsertIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionInsertIT.java new file mode 100644 index 000000000..17a4ffa31 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionInsertIT.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.transaction; + +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.INSERT_POKEMON_NAMED_ARG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.INSERT_POKEMON_ORDER_ARG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.LAST_POKEMON_ID; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.TYPES; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyInsertPokemon; + +/** + * Test set of basic JDBC inserts in transaction. + */ +public class TransactionInsertIT extends AbstractIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(TransactionInsertIT.class.getName()); + + /** Maximum Pokemon ID. */ + private static final int BASE_ID = LAST_POKEMON_ID + 210; + + /** + * Verify {@code createNamedInsert(String, String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedInsertStrStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+1, "Sentret", TYPES.get(1)); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedInsert("insert-bulbasaur", INSERT_POKEMON_NAMED_ARG) + .addParam("id", pokemon.getId()).addParam("name", pokemon.getName()).execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createNamedInsert(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedInsertStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+2, "Furret", TYPES.get(1)); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedInsert("insert-pokemon-named-arg") + .addParam("id", pokemon.getId()).addParam("name", pokemon.getName()).execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createNamedInsert(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedInsertStrOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+3, "Chinchou", TYPES.get(11), TYPES.get(13)); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedInsert("insert-pokemon-order-arg") + .addParam(pokemon.getId()).addParam(pokemon.getName()).execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createInsert(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateInsertNamedArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+4, "Lanturn", TYPES.get(11), TYPES.get(13)); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createInsert(INSERT_POKEMON_NAMED_ARG) + .addParam("id", pokemon.getId()).addParam("name", pokemon.getName()).execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createInsert(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateInsertOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+5, "Swinub", TYPES.get(5), TYPES.get(15)); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createInsert(INSERT_POKEMON_ORDER_ARG) + .addParam(pokemon.getId()).addParam(pokemon.getName()).execute() + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code namedInsert(String)} API method with ordered parameters passed directly to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedInsertOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+6, "Piloswine", TYPES.get(5), TYPES.get(15)); + Long result = DB_CLIENT.inTransaction(tx -> tx + .namedInsert("insert-pokemon-order-arg", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code insert(String)} API method with ordered parameters passed directly to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testInsertOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+7, "Mamoswine", TYPES.get(5), TYPES.get(15)); + Long result = DB_CLIENT.inTransaction(tx -> tx + .insert(INSERT_POKEMON_ORDER_ARG, pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionQueriesIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionQueriesIT.java new file mode 100644 index 000000000..2175b0990 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionQueriesIT.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.transaction; + +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.POKEMONS; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.SELECT_POKEMON_NAMED_ARG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.SELECT_POKEMON_ORDER_ARG; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyPokemon; + +/** + * Test set of basic JDBC queries in transaction. + */ +public class TransactionQueriesIT extends AbstractIT { + + /** Local logger instance. */ + static final Logger LOGGER = Logger.getLogger(TransactionQueriesIT.class.getName()); + + /** + * Verify {@code createNamedQuery(String, String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryStrStrOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .createNamedQuery("select-pikachu", SELECT_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(1).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(rows, POKEMONS.get(1)); + } + + /** + * Verify {@code createNamedQuery(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryStrNamedArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .createNamedQuery("select-pokemon-named-arg") + .addParam("name", POKEMONS.get(2).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(rows, POKEMONS.get(2)); + } + + /** + * Verify {@code createNamedQuery(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedQueryStrOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .createNamedQuery("select-pokemon-order-arg") + .addParam(POKEMONS.get(3).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(rows, POKEMONS.get(3)); + } + + /** + * Verify {@code createQuery(String)} API method with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateQueryNamedArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .createQuery(SELECT_POKEMON_NAMED_ARG) + .addParam("name", POKEMONS.get(4).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(rows, POKEMONS.get(4)); + } + + /** + * Verify {@code createQuery(String)} API method with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateQueryOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .createQuery(SELECT_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(5).getName()).execute() + ).toCompletableFuture().get(); + verifyPokemon(rows, POKEMONS.get(5)); + } + + /** + * Verify {@code namedQuery(String)} API method with ordered parameters passed directly to the {@code namedQuery} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedQueryOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .namedQuery("select-pokemon-order-arg", POKEMONS.get(6).getName()) + ).toCompletableFuture().get(); + verifyPokemon(rows, POKEMONS.get(6)); + } + + /** + * Verify {@code query(String)} API method with ordered parameters passed directly to the {@code query} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testQueryOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .query(SELECT_POKEMON_ORDER_ARG, POKEMONS.get(7).getName()) + ).toCompletableFuture().get(); + verifyPokemon(rows, POKEMONS.get(7)); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionStatementIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionStatementIT.java new file mode 100644 index 000000000..38b31b5ec --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionStatementIT.java @@ -0,0 +1,538 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.transaction; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DELETE_POKEMON_NAMED_ARG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DELETE_POKEMON_ORDER_ARG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.INSERT_POKEMON_NAMED_ARG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.INSERT_POKEMON_ORDER_ARG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.LAST_POKEMON_ID; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.SELECT_POKEMON_NAMED_ARG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.SELECT_POKEMON_ORDER_ARG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.TYPES; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.UPDATE_POKEMON_NAMED_ARG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.UPDATE_POKEMON_ORDER_ARG; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyDeletePokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyInsertPokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyPokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyUpdatePokemon; + +/** + * Test set of basic JDBC common statement calls in transaction. + */ +public class TransactionStatementIT extends AbstractIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(TransactionStatementIT.class.getName()); + + /** Maximum Pokemon ID. */ + private static final int BASE_ID = LAST_POKEMON_ID + 270; + + /** Map of pokemons for update tests. */ + private static final Map POKEMONS = new HashMap<>(); + + private static void addPokemon(AbstractIT.Pokemon pokemon) throws ExecutionException, InterruptedException { + POKEMONS.put(pokemon.getId(), pokemon); + Long result = DB_CLIENT.execute(exec -> exec + .namedInsert("insert-pokemon", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Initialize tests of basic JDBC updates. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @BeforeAll + public static void setup() throws ExecutionException, InterruptedException { + try { + // BASE_ID + 1 .. BASE_ID + 9 is reserved for inserts + // BASE_ID + 10 .. BASE_ID + 19 are pokemons for updates + addPokemon(new AbstractIT.Pokemon(BASE_ID + 10, "Wailmer", TYPES.get(11))); // BASE_ID+10 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 11, "Wailord", TYPES.get(11))); // BASE_ID+11 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 12, "Plusle", TYPES.get(13))); // BASE_ID+12 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 13, "Minun", TYPES.get(13))); // BASE_ID+13 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 14, "Spheal", TYPES.get(11), TYPES.get(15))); // BASE_ID+14 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 15, "Sealeo", TYPES.get(11), TYPES.get(15))); // BASE_ID+15 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 16, "Walrein", TYPES.get(11), TYPES.get(15))); // BASE_ID+16 + // BASE_ID + 20 .. BASE_ID + 29 are pokemons for deletes + addPokemon(new AbstractIT.Pokemon(BASE_ID + 20, "Duskull", TYPES.get(8))); // BASE_ID+20 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 21, "Dusclops", TYPES.get(8))); // BASE_ID+21 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 22, "Shuppet", TYPES.get(8))); // BASE_ID+22 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 23, "Banette", TYPES.get(8))); // BASE_ID+23 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 24, "Bagon", TYPES.get(16))); // BASE_ID+24 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 25, "Shelgon", TYPES.get(16))); // BASE_ID+25 + addPokemon(new AbstractIT.Pokemon(BASE_ID + 26, "Salamence", TYPES.get(3), TYPES.get(16))); // BASE_ID+26 + } catch (Exception ex) { + LOGGER.warning(() -> String.format("Exception in setup: %s", ex)); + throw ex; + } + } + + /** + * Verify {@code createNamedStatement(String, String)} API method with query with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithQueryStrStrOrderArgs() throws ExecutionException, InterruptedException { + // This call shall register named query + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .createNamedStatement("select-pikachu", SELECT_POKEMON_ORDER_ARG) + .addParam(AbstractIT.POKEMONS.get(1).getName()).execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemon(rows, AbstractIT.POKEMONS.get(1)); + } + + /** + * Verify {@code createNamedStatement(String)} API method with query with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithQueryStrNamedArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .createNamedStatement("select-pokemon-named-arg") + .addParam("name", AbstractIT.POKEMONS.get(2).getName()).execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemon(rows, AbstractIT.POKEMONS.get(2)); + } + + /** + * Verify {@code createNamedStatement(String)} API method with query with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithQueryStrOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .createNamedStatement("select-pokemon-order-arg") + .addParam(AbstractIT.POKEMONS.get(3).getName()).execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemon(rows, AbstractIT.POKEMONS.get(3)); + } + + /** + * Verify {@code createStatement(String)} API method with query with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatementWithQueryNamedArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .createStatement(SELECT_POKEMON_NAMED_ARG) + .addParam("name", AbstractIT.POKEMONS.get(4).getName()).execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemon(rows, AbstractIT.POKEMONS.get(4)); + } + + /** + * Verify {@code createStatement(String)} API method with query with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatementWithQueryOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .createStatement(SELECT_POKEMON_ORDER_ARG) + .addParam(AbstractIT.POKEMONS.get(5).getName()).execute() + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemon(rows, AbstractIT.POKEMONS.get(5)); + } + + /** + * Verify {@code namedStatement(String)} API method with query with ordered parameters passed directly to the {@code namedQuery} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedStatementWithQueryOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .namedStatement("select-pokemon-order-arg", AbstractIT.POKEMONS.get(6).getName()) + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemon(rows, AbstractIT.POKEMONS.get(6)); + } + + /** + * Verify {@code statement(String)} API method with query with ordered parameters passed directly to the {@code query} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testStatementWithQueryOrderArgs() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.inTransaction(tx -> tx + .statement(SELECT_POKEMON_ORDER_ARG, AbstractIT.POKEMONS.get(7).getName()) + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + verifyPokemon(rows, AbstractIT.POKEMONS.get(7)); + } + + /** + * Verify {@code createNamedStatement(String, String)} API method with insert with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithInsertStrStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+1, "Swablu", TYPES.get(1), TYPES.get(3)); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedStatement("insert-zubat", INSERT_POKEMON_NAMED_ARG) + .addParam("id", pokemon.getId()).addParam("name", pokemon.getName()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createNamedStatement(String)} API method with insert with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithInsertStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+2, "Altaria", TYPES.get(3), TYPES.get(16)); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedStatement("insert-pokemon-named-arg") + .addParam("id", pokemon.getId()).addParam("name", pokemon.getName()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createNamedStatement(String)} API method with insert with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithInsertStrOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+3, "Corphish", TYPES.get(11)); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedStatement("insert-pokemon-order-arg") + .addParam(pokemon.getId()).addParam(pokemon.getName()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createStatement(String)} API method with insert with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatementWithInsertNamedArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+4, "Crawdaunt", TYPES.get(11), TYPES.get(17)); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createStatement(INSERT_POKEMON_NAMED_ARG) + .addParam("id", pokemon.getId()).addParam("name", pokemon.getName()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createStatement(String)} API method with insert with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatementWithInsertOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+5, "Whismur", TYPES.get(1)); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createStatement(INSERT_POKEMON_ORDER_ARG) + .addParam(pokemon.getId()).addParam(pokemon.getName()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code namedStatement(String)} API method with insert with ordered parameters passed directly + * to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedStatementWithInsertOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+6, "Loudred", TYPES.get(1)); + Long result = DB_CLIENT.inTransaction(tx -> tx + .namedStatement("insert-pokemon-order-arg", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code statement(String)} API method with insert with ordered parameters passed directly + * to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testStatementWithInsertOrderArgs() throws ExecutionException, InterruptedException { + Pokemon pokemon = new Pokemon(BASE_ID+7, "Exploud", TYPES.get(1)); + Long result = DB_CLIENT.inTransaction(tx -> tx + .statement(INSERT_POKEMON_ORDER_ARG, pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Verify {@code createNamedStatement(String, String)} API method with update with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithUpdateStrStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+10); + Pokemon updatedPokemon = new Pokemon(BASE_ID+10, "Wailord", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedStatement("update-piplup", UPDATE_POKEMON_NAMED_ARG) + .addParam("name", updatedPokemon.getName()).addParam("id", updatedPokemon.getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createNamedStatement(String)} API method with update with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithUpdateStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+11); + Pokemon updatedPokemon = new Pokemon(BASE_ID+11, "Wailmer", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedStatement("update-pokemon-named-arg") + .addParam("name", updatedPokemon.getName()).addParam("id", updatedPokemon.getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createNamedStatement(String)} API method with update with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatementWithUpdateStrOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+12); + Pokemon updatedPokemon = new Pokemon(BASE_ID+12, "Minun", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedStatement("update-pokemon-order-arg") + .addParam(updatedPokemon.getName()).addParam(updatedPokemon.getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createStatement(String)} API method with update with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatemenWithUpdateNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+13); + Pokemon updatedPokemon = new Pokemon(BASE_ID+13, "Plusle", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createStatement(UPDATE_POKEMON_NAMED_ARG) + .addParam("name", updatedPokemon.getName()).addParam("id", updatedPokemon.getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createStatement(String)} API method with update with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatemenWithUpdateOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+14); + Pokemon updatedPokemon = new Pokemon(BASE_ID+14, "Sealeo", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createStatement(UPDATE_POKEMON_ORDER_ARG) + .addParam(updatedPokemon.getName()).addParam(updatedPokemon.getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code namedStatement(String)} API method with update with ordered parameters passed directly + * to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedStatemenWithUpdateOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+15); + Pokemon updatedPokemon = new Pokemon(BASE_ID+15, "Walrein", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.inTransaction(tx -> tx + .namedStatement("update-pokemon-order-arg", updatedPokemon.getName(), updatedPokemon.getId()) + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code statement(String)} API method with update with ordered parameters passed directly + * to the {@code insert} method. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testStatemenWithUpdateOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+16); + Pokemon updatedPokemon = new Pokemon(BASE_ID+16, "Spheal", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.inTransaction(tx -> tx + .statement(UPDATE_POKEMON_ORDER_ARG, updatedPokemon.getName(), updatedPokemon.getId()) + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createNamedStatement(String, String)} API method with delete with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatemenWithDeleteStrStrOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedStatement("delete-mudkip", DELETE_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(BASE_ID+20).getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+20)); + } + + /** + * Verify {@code createNamedStatement(String)} API method with delete with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatemenWithDeleteStrNamedArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedStatement("delete-pokemon-named-arg") + .addParam("id", POKEMONS.get(BASE_ID+21).getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+21)); + } + + /** + * Verify {@code createNamedStatement(String)} API method with delete with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateNamedStatemenWithDeleteStrOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedStatement("delete-pokemon-order-arg") + .addParam(POKEMONS.get(BASE_ID+22).getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+22)); + } + + /** + * Verify {@code createStatement(String)} API method with delete with named parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatemenWithDeleteNamedArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.inTransaction(tx -> tx + .createStatement(DELETE_POKEMON_NAMED_ARG) + .addParam("id", POKEMONS.get(BASE_ID+23).getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+23)); + } + + /** + * Verify {@code createStatement(String)} API method with delete with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testCreateStatemenWithDeleteOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.inTransaction(tx -> tx + .createStatement(DELETE_POKEMON_ORDER_ARG) + .addParam(POKEMONS.get(BASE_ID+24).getId()).execute() + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+24)); + } + + /** + * Verify {@code namedStatement(String)} API method with delete with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testNamedStatemenWithDeleteOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.inTransaction(tx -> tx + .namedStatement("delete-pokemon-order-arg", POKEMONS.get(BASE_ID+25).getId()) + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+25)); + } + + /** + * Verify {@code statement(String)} API method with delete with ordered parameters. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testStatemenWithDeleteOrderArgs() throws ExecutionException, InterruptedException { + Long result = DB_CLIENT.inTransaction(tx -> tx + .statement(DELETE_POKEMON_ORDER_ARG, POKEMONS.get(BASE_ID+26).getId()) + ).toCompletableFuture().get().dmlFuture().toCompletableFuture().get(); + verifyDeletePokemon(result, POKEMONS.get(BASE_ID+26)); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionUpdateIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionUpdateIT.java new file mode 100644 index 000000000..dde83e276 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/transaction/TransactionUpdateIT.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.tests.transaction; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.LAST_POKEMON_ID; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.TYPES; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.UPDATE_POKEMON_NAMED_ARG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.UPDATE_POKEMON_ORDER_ARG; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyInsertPokemon; +import static io.helidon.tests.integration.dbclient.common.utils.Utils.verifyUpdatePokemon; + +/** + * Test set of basic JDBC updates in transaction. + */ +public class TransactionUpdateIT extends AbstractIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(TransactionUpdateIT.class.getName()); + + /** Maximum Pokemon ID. */ + private static final int BASE_ID = LAST_POKEMON_ID + 220; + + /** Map of pokemons for update tests. */ + private static final Map POKEMONS = new HashMap<>(); + + private static void addPokemon(Pokemon pokemon) throws ExecutionException, InterruptedException { + POKEMONS.put(pokemon.getId(), pokemon); + Long result = DB_CLIENT.execute(exec -> exec + .namedInsert("insert-pokemon", pokemon.getId(), pokemon.getName()) + ).toCompletableFuture().get(); + verifyInsertPokemon(result, pokemon); + } + + /** + * Initialize tests of basic JDBC updates. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @BeforeAll + public static void setup() throws ExecutionException, InterruptedException { + try { + int curId = BASE_ID; + addPokemon(new Pokemon(++curId, "Teddiursa", TYPES.get(1))); // BASE_ID+1 + addPokemon(new Pokemon(++curId, "Ursaring", TYPES.get(1))); // BASE_ID+2 + addPokemon(new Pokemon(++curId, "Slugma", TYPES.get(10))); // BASE_ID+3 + addPokemon(new Pokemon(++curId, "Magcargo", TYPES.get(6), TYPES.get(10))); // BASE_ID+4 + addPokemon(new Pokemon(++curId, "Lotad", TYPES.get(11), TYPES.get(12))); // BASE_ID+5 + addPokemon(new Pokemon(++curId, "Lombre", TYPES.get(11), TYPES.get(12))); // BASE_ID+6 + addPokemon(new Pokemon(++curId, "Ludicolo", TYPES.get(11), TYPES.get(12))); // BASE_ID+7 + } catch (Exception ex) { + LOGGER.warning(() -> String.format("Exception in setup: %s", ex)); + throw ex; + } + } + + /** + * Verify {@code createNamedUpdate(String, String)} API method with named parameters. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @Test + public void testCreateNamedUpdateStrStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+1); + Pokemon updatedPokemon = new Pokemon(BASE_ID+1, "Ursaring", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedUpdate("update-spearow", UPDATE_POKEMON_NAMED_ARG) + .addParam("name", updatedPokemon.getName()).addParam("id", updatedPokemon.getId()).execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createNamedUpdate(String)} API method with named parameters. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @Test + public void testCreateNamedUpdateStrNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+2); + Pokemon updatedPokemon = new Pokemon(BASE_ID+2, "Teddiursa", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedUpdate("update-pokemon-named-arg") + .addParam("name", updatedPokemon.getName()).addParam("id", updatedPokemon.getId()).execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createNamedUpdate(String)} API method with ordered parameters. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @Test + public void testCreateNamedUpdateStrOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+3); + Pokemon updatedPokemon = new Pokemon(BASE_ID+3, "Magcargo", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createNamedUpdate("update-pokemon-order-arg") + .addParam(updatedPokemon.getName()).addParam(updatedPokemon.getId()).execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createUpdate(String)} API method with named parameters. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @Test + public void testCreateUpdateNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+4); + Pokemon updatedPokemon = new Pokemon(BASE_ID+4, "Slugma", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createUpdate(UPDATE_POKEMON_NAMED_ARG) + .addParam("name", updatedPokemon.getName()).addParam("id", updatedPokemon.getId()).execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code createUpdate(String)} API method with ordered parameters. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @Test + public void testCreateUpdateOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+5); + Pokemon updatedPokemon = new Pokemon(BASE_ID+5, "Lombre", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.inTransaction(tx -> tx + .createUpdate(UPDATE_POKEMON_ORDER_ARG) + .addParam(updatedPokemon.getName()).addParam(updatedPokemon.getId()).execute() + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code namedUpdate(String)} API method with named parameters. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @Test + public void testNamedUpdateNamedArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+6); + Pokemon updatedPokemon = new Pokemon(BASE_ID+6, "Ludicolo", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.inTransaction(tx -> tx + .namedUpdate("update-pokemon-order-arg", updatedPokemon.getName(), updatedPokemon.getId()) + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + + /** + * Verify {@code update(String)} API method with ordered parameters. + * + * @throws InterruptedException if the current thread was interrupted + * @throws ExecutionException when database query failed + */ + @Test + public void testUpdateOrderArgs() throws ExecutionException, InterruptedException { + Pokemon srcPokemon = POKEMONS.get(BASE_ID+7); + Pokemon updatedPokemon = new Pokemon(BASE_ID+7, "Lotad", srcPokemon.getTypesArray()); + Long result = DB_CLIENT.inTransaction(tx -> tx + .update(UPDATE_POKEMON_ORDER_ARG, updatedPokemon.getName(), updatedPokemon.getId()) + ).toCompletableFuture().get(); + verifyUpdatePokemon(result, updatedPokemon); + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/utils/RangePoJo.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/utils/RangePoJo.java new file mode 100644 index 000000000..84a1d1abb --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/utils/RangePoJo.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.utils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.helidon.dbclient.DbMapper; +import io.helidon.dbclient.DbRow; + +/** + * PoJo used to define Pokemon IDs range in query statement tests. + */ +public class RangePoJo { + + public static final class Mapper implements DbMapper { + + public static final Mapper INSTANCE = new Mapper(); + + @Override + public RangePoJo read(DbRow row) { + throw new UnsupportedOperationException("Read operation is not implemented."); + } + + @Override + public Map toNamedParameters(RangePoJo value) { + Map params = new HashMap<>(2); + params.put("idmin", value.getIdMin()); + params.put("idmax", value.getIdMax()); + return params; + } + + @Override + public List toIndexedParameters(RangePoJo value) { + List params = new ArrayList<>(2); + params.add(value.getIdMin()); + params.add(value.getIdMax()); + return params; + } + + } + + /** Beginning of IDs range. */ + private final int idMin; + /** End of IDs range. */ + private final int idMax; + + /** + * Creates an instance of Range POJO. + * + * @param idMin beginning of IDs range + * @param idMax end of IDs range + */ + public RangePoJo(int idMin, int idMax) { + this.idMin = idMin; + this.idMax = idMax; + } + + /** + * Get beginning of IDs range. + * + * @return beginning of IDs range + */ + public int getIdMin() { + return idMin; + } + + /** + * Get end of IDs range. + * + * @return end of IDs range + */ + public int getIdMax() { + return idMax; + } + +} diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/utils/Utils.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/utils/Utils.java new file mode 100644 index 000000000..97c12cbc6 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/utils/Utils.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.common.utils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.tests.integration.dbclient.common.AbstractIT; +import io.helidon.tests.integration.dbclient.common.AbstractIT.Pokemon; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.POKEMONS; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Test utilities. + */ +public class Utils { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(Utils.class.getName()); + + private Utils() { + throw new IllegalStateException("No instances of this class are allowed!"); + } + + /** + * Verify that {@code DbRows rows} argument contains pokemons matching specified IDs range. + * @param rows database query result to verify + * @param idMin beginning of ID range + * @param idMax end of ID range + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + public static void verifyPokemonsIdRange( + DbRows rows, int idMin, int idMax + ) throws ExecutionException, InterruptedException { + assertThat(rows, notNullValue()); + List rowsList = rows.collect().toCompletableFuture().get(); + // Build Map of valid pokemons + Map valid = new HashMap<>(POKEMONS.size()); + for (Map.Entry entry : POKEMONS.entrySet()) { + int id = entry.getKey(); + Pokemon pokemon = entry.getValue(); + if (id > idMin && id < idMax) { + valid.put(id, pokemon); + } + } + // Compare result with valid pokemons + //assertThat(rowsList, hasSize(valid.size())); + for (DbRow row : rowsList) { + Integer id = row.column(1).as(Integer.class); + String name = row.column(2).as(String.class); + LOGGER.info(() -> String.format("Pokemon id=%d, name=%s", id, name)); + assertThat(valid.containsKey(id), equalTo(true)); + assertThat(name, equalTo( valid.get(id).getName())); + } + } + + /** + * Verify that {@code DbRows rows} argument contains single pokemon matching specified IDs range. + * @param maybeRow database query result to verify + * @param idMin beginning of ID range + * @param idMax end of ID range + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + public static void verifyPokemonsIdRange( + Optional maybeRow, int idMin, int idMax + ) throws ExecutionException, InterruptedException { + assertThat(maybeRow.isPresent(), equalTo(true)); + DbRow row = maybeRow.get(); + // Build Map of valid pokemons + Map valid = new HashMap<>(POKEMONS.size()); + for (Map.Entry entry : POKEMONS.entrySet()) { + int id = entry.getKey(); + Pokemon pokemon = entry.getValue(); + if (id > idMin && id < idMax) { + valid.put(id, pokemon); + } + } + Integer id = row.column(1).as(Integer.class); + String name = row.column(2).as(String.class); + assertThat(valid.containsKey(id), equalTo(true)); + assertThat(name, equalTo(valid.get(id).getName())); + } + + /** + * Verify that {@code DbRows rows} argument contains single record with provided pokemon. + * + * @param rows database query result to verify + * @param pokemon pokemon to compare with + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + public static void verifyPokemon(DbRows rows, AbstractIT.Pokemon pokemon) throws ExecutionException, InterruptedException { + assertThat(rows, notNullValue()); + List rowsList = rows.collect().toCompletableFuture().get(); + assertThat(rowsList, hasSize(1)); + DbRow row = rowsList.get(0); + Integer id = row.column(1).as(Integer.class); + String name = row.column(2).as(String.class); + assertThat(id, equalTo(pokemon.getId())); + assertThat(name, pokemon.getName().equals(name)); + } + + /** + * Verify that {@code DbRows rows} argument contains single record with provided pokemon. + * + * @param maybeRow database query result to verify + * @param pokemon pokemon to compare with + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + public static void verifyPokemon(Optional maybeRow, AbstractIT.Pokemon pokemon) throws ExecutionException, InterruptedException { + assertThat(maybeRow.isPresent(), equalTo(true)); + DbRow row = maybeRow.get(); + Integer id = row.column(1).as(Integer.class); + String name = row.column(2).as(String.class); + assertThat(id, equalTo(pokemon.getId())); + assertThat(name, pokemon.getName().equals(name)); + } + + /** + * Verify that {@code Pokemon result} argument contains single record with provided pokemon. + * + * @param result database query result mapped to Pokemon PoJo to verify + * @param pokemon pokemon to compare with + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + public static void verifyPokemon(AbstractIT.Pokemon result, AbstractIT.Pokemon pokemon) throws ExecutionException, InterruptedException { + assertThat(result.getId(), equalTo(pokemon.getId())); + assertThat(result.getName(), equalTo(pokemon.getName())); + } + + /** + * Verify that provided pokemon was successfully inserted into the database. + * + * @param result DML statement result + * @param pokemon pokemon to compare with + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + public static void verifyInsertPokemon(Long result, AbstractIT.Pokemon pokemon) throws ExecutionException, InterruptedException { + assertThat(result, equalTo(1L)); + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .namedGet("select-pokemon-by-id", pokemon.getId()) + ).toCompletableFuture().get(); + assertThat(maybeRow.isPresent(), equalTo(true)); + DbRow row = maybeRow.get(); + Integer id = row.column("id").as(Integer.class); + String name = row.column("name").as(String.class); + assertThat(id, equalTo(pokemon.getId())); + assertThat(name, pokemon.getName().equals(name)); + } + + /** + * Verify that provided pokemon was successfully updated in the database. + * + * @param result DML statement result + * @param pokemon pokemon to compare with + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + public static void verifyUpdatePokemon(Long result, AbstractIT.Pokemon pokemon) throws ExecutionException, InterruptedException { + assertThat(result, equalTo(1L)); + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .namedGet("select-pokemon-by-id", pokemon.getId()) + ).toCompletableFuture().get(); + assertThat(maybeRow.isPresent(), equalTo(true)); + DbRow row = maybeRow.get(); + Integer id = row.column(1).as(Integer.class); + String name = row.column(2).as(String.class); + assertThat(id, equalTo(pokemon.getId())); + assertThat(name, pokemon.getName().equals(name)); + } + + /** + * Verify that provided pokemon was successfully deleted from the database. + * + * @param result DML statement result + * @param pokemon pokemon to compare with + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + public static void verifyDeletePokemon(Long result, AbstractIT.Pokemon pokemon) throws ExecutionException, InterruptedException { + assertThat(result, equalTo(1L)); + Optional maybeRow = DB_CLIENT.execute(exec -> exec + .namedGet("select-pokemon-by-id", pokemon.getId()) + ).toCompletableFuture().get(); + assertThat(maybeRow.isPresent(), equalTo(false)); + } + +} diff --git a/tests/integration/dbclient/common/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbMapperProvider b/tests/integration/dbclient/common/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbMapperProvider new file mode 100644 index 000000000..eba308f77 --- /dev/null +++ b/tests/integration/dbclient/common/src/main/resources/META-INF/services/io.helidon.dbclient.spi.DbMapperProvider @@ -0,0 +1,17 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +io.helidon.tests.integration.dbclient.common.spi.MapperProvider diff --git a/tests/integration/dbclient/jdbc/pom.xml b/tests/integration/dbclient/jdbc/pom.xml new file mode 100644 index 000000000..a354770a7 --- /dev/null +++ b/tests/integration/dbclient/jdbc/pom.xml @@ -0,0 +1,285 @@ + + + + + 4.0.0 + + + io.helidon.tests.integration.dbclient + helidon-tests-integration-dbclient-project + 2.0-SNAPSHOT + ../pom.xml + + + io.helidon.tests.integration.dbclient + heldion-tests-integration-dbclient-jdbc + Integration Tests: DB Client JDBC + + + 3306 + 127.0.0.1 + test + helidon + h3l1d0n + I4mGr00t + + + + + io.helidon.tests.integration.dbclient + heldion-tests-integration-dbclient-common + ${project.version} + test + + + io.helidon.config + helidon-config + test + + + io.helidon.config + helidon-config-yaml + test + + + io.helidon.dbclient + helidon-dbclient-jdbc + test + + + com.zaxxer + HikariCP + test + + + org.slf4j + slf4j-jdk14 + test + + + mysql + mysql-connector-java + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + src/test/resources + true + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${version.plugin.surefire} + + true + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${version.plugin.surefire} + + methods + 10 + + + + + init + integration-test + + integration-test + + + + io.helidon.tests.integration.dbclient.jdbc.init.*IT + + + + + + test + integration-test + + integration-test + + + + io.helidon.tests.integration.dbclient:heldion-tests-integration-dbclient-common + + + io.helidon.tests.integration.dbclient.common.tests.simple.*IT + io.helidon.tests.integration.dbclient.common.tests.statement.*IT + io.helidon.tests.integration.dbclient.common.tests.transaction.*IT + io.helidon.tests.integration.dbclient.common.tests.interceptor.*IT + io.helidon.tests.integration.dbclient.common.tests.dbresult.*IT + io.helidon.tests.integration.dbclient.common.tests.mapping.*IT + io.helidon.tests.integration.dbclient.common.tests.health.*IT + io.helidon.tests.integration.dbclient.common.tests.metrics.*IT + + + + + + destroy + integration-test + + integration-test + + + + io.helidon.tests.integration.dbclient.jdbc.destroy.*IT + + + + + + + + + + + + + + debug + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${version.plugin.surefire} + + + test + + ${it.jdbc.debug} + ${it.jdbc.test} + + + + + + + + + test + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${version.plugin.surefire} + + + test + + ${it.jdbc.test} + + + + + + + + + docker + + + + io.fabric8 + docker-maven-plugin + 0.31.0 + + + start + pre-integration-test + + start + + + + stop + post-integration-test + + stop + + + + + + + mysql:8 + mysql + + + ${mysql.user} + ${mysql.password} + ${mysql.roootpw} + ${mysql.database} + + ${mysql.host} + + ${mysql.host}:${mysql.port}:3306 + + + MySQL server is up an running + + 127.0.0.1 + + ${mysql.port} + + + + + + + + true + false + + + + + + + + \ No newline at end of file diff --git a/tests/integration/dbclient/jdbc/src/test/java/io/helidon/tests/integration/dbclient/jdbc/destroy/DestroyIT.java b/tests/integration/dbclient/jdbc/src/test/java/io/helidon/tests/integration/dbclient/jdbc/destroy/DestroyIT.java new file mode 100644 index 000000000..a15f88f22 --- /dev/null +++ b/tests/integration/dbclient/jdbc/src/test/java/io/helidon/tests/integration/dbclient/jdbc/destroy/DestroyIT.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.jdbc.destroy; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Destroy database + */ +public class DestroyIT { + + /** Local logger instance. */ + static final Logger LOGGER = Logger.getLogger(DestroyIT.class.getName()); + + /** + * Delete database content. + * + * @param dbClient Helidon database client + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + private static void dropSchema(DbClient dbClient) throws ExecutionException, InterruptedException { + dbClient.execute(exec -> exec + .namedDml("drop-poketypes") + .thenCompose(result -> exec.namedDml("drop-pokemons")) + .thenCompose(result -> exec.namedDml("drop-types")) + ).toCompletableFuture().get(); + } + + + /** + * Destroy database after tests. + */ + @BeforeAll + public static void destroy() { + try { + dropSchema(DB_CLIENT); + } catch (ExecutionException | InterruptedException ex) { + fail("Database cleanup failed!", ex); + } + } + + /** + * Verify that table Types does not exist. + * + * @throws ExecutionException when database query failed + */ + @Test + public void testTypesDeleted() throws InterruptedException { + try { + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-types") + ).toCompletableFuture().get(); + if (rows != null) { + List rowsList = rows.collect().toCompletableFuture().get(); + LOGGER.warning(() -> String.format("Rows count: %d", rowsList.size())); + fail("No Types rows shall be returned after database cleanup!"); + } + } catch (ExecutionException ex) { + LOGGER.info(() -> String.format("Caught expected exception: %s", ex.getMessage())); + } + } + + /** + * Verify that table Pokemons does not exist. + * + * @throws ExecutionException when database query failed + */ + @Test + public void testPokemonsDeleted() throws InterruptedException { + try { + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-pokemons") + ).toCompletableFuture().get(); + if (rows != null) { + List rowsList = rows.collect().toCompletableFuture().get(); + LOGGER.warning(() -> String.format("Rows count: %d", rowsList.size())); + fail("No Pokemons rows shall be returned after database cleanup!"); + } + } catch (ExecutionException ex) { + LOGGER.info(() -> String.format("Caught expected exception: %s", ex.getMessage())); + } + } + + /** + * Verify that table PokemonTypes does not exist. + * + * @throws ExecutionException when database query failed + */ + @Test + public void testPokemonTypesDeleted() throws InterruptedException { + try { + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-poketypes-all") + ).toCompletableFuture().get(); + if (rows != null) { + List rowsList = rows.collect().toCompletableFuture().get(); + LOGGER.warning(() -> String.format("Rows count: %d", rowsList.size())); + fail("No PokemonTypes rows shall be returned after database cleanup!"); + } + } catch (ExecutionException ex) { + LOGGER.info(() -> String.format("Caught expected exception: %s", ex.getMessage())); + } + } + +} diff --git a/tests/integration/dbclient/jdbc/src/test/java/io/helidon/tests/integration/dbclient/jdbc/init/CheckIT.java b/tests/integration/dbclient/jdbc/src/test/java/io/helidon/tests/integration/dbclient/jdbc/init/CheckIT.java new file mode 100644 index 000000000..c7ec40ffe --- /dev/null +++ b/tests/integration/dbclient/jdbc/src/test/java/io/helidon/tests/integration/dbclient/jdbc/init/CheckIT.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.jdbc.init; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.function.Consumer; +import java.util.logging.Logger; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Check minimal functionality needed before running database schema initialization. + * First test class being executed after database startup. + */ +public class CheckIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(CheckIT.class.getName()); + + /** Test configuration. */ + public static final Config CONFIG = Config.create(ConfigSources.classpath("test.yaml")); + + /** Timeout in seconds to wait for database to come up. */ + private static final int TIMEOUT = 60; + + /** + * Wait until database starts up when its configuration node is available. + */ + private static final class ConnectionCheck implements Consumer { + + private boolean connected; + + private ConnectionCheck() { + connected = false; + } + + @Override + public void accept(Config config) { + String url = config.get("url").asString().get(); + String username = config.get("username").asString().get(); + String password = config.get("password").asString().get(); + long endTm = 1000 * TIMEOUT + System.currentTimeMillis(); + while (true) { + try { + DriverManager.getConnection(url, username, password); + connected = true; + return; + } catch (SQLException ex) { + if (System.currentTimeMillis() > endTm) { + return; + } + } + } + } + + private boolean connected() { + return connected; + } + + } + + /** + * Store database connection configuration and build {@link Connection} instance. + */ + private static final class ConnectionBuilder implements Consumer { + + private boolean hasConfig; + private String url; + private String username; + private String password; + + private ConnectionBuilder() { + hasConfig = false; + } + + @Override + public void accept(Config config) { + url = config.get("url").asString().get(); + username = config.get("username").asString().get(); + password = config.get("password").asString().get(); + hasConfig = true; + } + + private Connection createConnection() throws SQLException { + if (!hasConfig) { + fail("No db.connection configuration node was found."); + } + return DriverManager.getConnection(url, username, password); + } + + } + + /** + * Wait for database server to start. + * + * @param dbClient Helidon database client + */ + private static void waitForStart() { + ConnectionCheck check = new ConnectionCheck(); + CONFIG.get("db.connection").ifExists(check); + if (!check.connected()) { + fail("Database startup failed!"); + } + } + + /** + * Setup database for tests. + * Wait for database to start. Returns after ping query completed successfully or timeout passed. + */ + @BeforeAll + public static void setup() { + LOGGER.info(() -> String.format("Initializing Integration Tests")); + waitForStart(); + } + + /** + * Simple test to verify that DML query execution works. + * Used before running database schema initialization. + * + * @throws SQLException when database query failed + */ + @Test + public void testDmlStatementExecution() throws SQLException { + ConnectionBuilder builder = new ConnectionBuilder(); + String ping = CONFIG.get("db.statements.ping").asString().get(); + CONFIG.get("db.connection").ifExists(builder); + Connection conn = builder.createConnection(); + int result = conn.createStatement().executeUpdate(ping); + assertThat(result, equalTo(0)); + LOGGER.info(() -> String.format("Command ping result: %d", result)); + } + +} diff --git a/tests/integration/dbclient/jdbc/src/test/java/io/helidon/tests/integration/dbclient/jdbc/init/InitIT.java b/tests/integration/dbclient/jdbc/src/test/java/io/helidon/tests/integration/dbclient/jdbc/init/InitIT.java new file mode 100644 index 000000000..a8a7fe7a7 --- /dev/null +++ b/tests/integration/dbclient/jdbc/src/test/java/io/helidon/tests/integration/dbclient/jdbc/init/InitIT.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.jdbc.init; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Initialize database + */ +public class InitIT extends AbstractIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(InitIT.class.getName()); + + /** + * Initializes database schema (tables). + * + * @param dbClient Helidon database client + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + private static void initSchema(DbClient dbClient) throws ExecutionException, InterruptedException { + dbClient.execute(exec -> exec + .namedDml("create-types") + .thenCompose(result -> exec.namedDml("create-pokemons")) + .thenCompose(result -> exec.namedDml("create-poketypes")) + ).toCompletableFuture().get(); + } + + /** + * Initialize database content (rows in tables). + * + * @param dbClient Helidon database client + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + private static void initData(DbClient dbClient) throws InterruptedException, ExecutionException { + // Init pokemon types + dbClient.inTransaction(tx -> { + CompletionStage stage = null; + for (Map.Entry entry : TYPES.entrySet()) { + if (stage == null) { + stage = tx.namedDml("insert-type", entry.getKey(), entry.getValue().getName()); + } else { + stage = stage.thenCompose(result -> tx.namedDml( + "insert-type", entry.getKey(), entry.getValue().getName())); + } + } + return stage; + }).toCompletableFuture().get(); + // Init pokemons + dbClient.inTransaction(tx -> { + CompletionStage stage = null; + for (Map.Entry entry : POKEMONS.entrySet()) { + if (stage == null) { + stage = tx.namedDml("insert-pokemon", entry.getKey(), entry.getValue().getName()); + } else { + stage = stage.thenCompose(result -> tx.namedDml( + "insert-pokemon", entry.getKey(), entry.getValue().getName())); + } + } + return stage; + }).toCompletableFuture().get(); + // Init pokemon to type relation + dbClient.inTransaction(tx -> { + CompletionStage stage = null; + for (Map.Entry entry : POKEMONS.entrySet()) { + Pokemon pokemon = entry.getValue(); + LOGGER.info(() -> String.format("Pokemon: %s", pokemon.toString())); + for (Type type : pokemon.getTypes()) { + LOGGER.info(() -> String.format(" Type: %s", type.toString())); + if (stage == null) { + stage = tx.namedDml("insert-poketype", pokemon.getId(), type.getId()); + } else { + stage = stage.thenCompose(result -> tx.namedDml( + "insert-poketype", pokemon.getId(), type.getId())); + } + } + } + return stage; + }).toCompletableFuture().get(); + } + + /** + * Setup database for tests. + */ + @BeforeAll + public static void setup() { + LOGGER.info(() -> "Initializing Integration Tests"); + try { + initSchema(DB_CLIENT); + initData(DB_CLIENT); + } catch (ExecutionException | InterruptedException ex) { + fail("Database setup failed!", ex); + } + } + + /** + * Verify that database contains properly initialized pokemon types. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testListTypes() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-types") + ).toCompletableFuture().get(); + assertThat(rows, notNullValue()); + List rowsList = rows.collect().toCompletableFuture().get(); + assertThat(rowsList, not(empty())); + Set ids = new HashSet<>(TYPES.keySet()); + for (DbRow row : rowsList) { + Integer id = row.column(1).as(Integer.class); + String name = row.column(2).as(String.class); + assertThat(ids, hasItem(id)); + ids.remove(id); + assertThat(name, TYPES.get(id).getName().equals(name)); + LOGGER.info(() -> String.format("Type id=%d name=%s", id, name)); + } + } + + /** + * Verify that database contains properly initialized pokemons. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testListPokemons() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-pokemons") + ).toCompletableFuture().get(); + assertThat(rows, notNullValue()); + List rowsList = rows.collect().toCompletableFuture().get(); + assertThat(rowsList, not(empty())); + Set ids = new HashSet<>(POKEMONS.keySet()); + for (DbRow row : rowsList) { + Integer id = row.column(1).as(Integer.class); + String name = row.column(2).as(String.class); + assertThat(ids, hasItem(id)); + ids.remove(id); + assertThat(name, POKEMONS.get(id).getName().equals(name)); + LOGGER.info(() -> String.format("Type id=%d name=%s", id, name)); + } + } + + /** + * Verify that database contains properly initialized pokemon types relation. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testListPokemonTypes() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-pokemons") + ).toCompletableFuture().get(); + assertThat(rows, notNullValue()); + List rowsList = rows.collect().toCompletableFuture().get(); + assertThat(rowsList, not(empty())); + for (DbRow row : rowsList) { + Integer pokemonId = row.column(1).as(Integer.class); + String pokemonName = row.column(2).as(String.class); + Pokemon pokemon = POKEMONS.get(pokemonId); + assertThat(pokemonName, POKEMONS.get(pokemonId).getName().equals(pokemonName)); + LOGGER.info(() -> String.format("DB Pokemon id=%d name=%s", pokemonId, pokemonName)); + LOGGER.info(() -> String.format(" MAP %s", pokemon.toString())); + DbRows typeRows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-poketypes", pokemonId) + ).toCompletableFuture().get(); + List typeRowsList = typeRows.collect().toCompletableFuture().get(); + assertThat(typeRowsList.size(), equalTo(pokemon.getTypes().size())); + for (DbRow typeRow : typeRowsList) { + Integer typeId = typeRow.column(2).as(Integer.class); + LOGGER.info(() -> String.format(" DB Type ID: %d", typeId)); + assertThat(pokemon.getTypes(), hasItem(TYPES.get(typeId))); + } + } + } + +} diff --git a/tests/integration/dbclient/jdbc/src/test/resources/test.yaml b/tests/integration/dbclient/jdbc/src/test/resources/test.yaml new file mode 100644 index 000000000..6dd9cbedc --- /dev/null +++ b/tests/integration/dbclient/jdbc/src/test/resources/test.yaml @@ -0,0 +1,73 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +server: + port: 0 + host: 0.0.0.0 + +db: + source: jdbc + connection: + url: jdbc:mysql://${mysql.host}:${mysql.port}/${mysql.database}?useSSL=false&allowPublicKeyRetrieval=true + username: ${mysql.user} + password: ${mysql.password} + + statements: + # required ping statement + ping: "DO 0" + # database schema initialization statements + create-types: "CREATE TABLE Types (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(64) NOT NULL)" + create-pokemons: "CREATE TABLE Pokemons (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(64) NOT NULL)" + create-poketypes: "CREATE TABLE PokemonTypes (id_pokemon INTEGER NOT NULL REFERENCES Pokemon(id), id_type INTEGER NOT NULL REFERENCES Type(id))" + # database schema cleanup statements + drop-types: "DROP TABLE Types" + drop-pokemons: "DROP TABLE Pokemons" + drop-poketypes: "DROP TABLE PokemonTypes" + # data initialization statements + insert-type: "INSERT INTO Types(id, name) VALUES(?, ?)" + insert-pokemon: "INSERT INTO Pokemons(id, name) VALUES(?, ?)" + insert-poketype: "INSERT INTO PokemonTypes(id_pokemon, id_type) VALUES(?, ?)" + # data initialization verification statements + select-types: "SELECT id, name FROM Types" + select-pokemons: "SELECT id, name FROM Pokemons" + select-poketypes: "SELECT id_pokemon, id_type FROM PokemonTypes p WHERE id_pokemon = ?" + # data cleanup verification statements + select-poketypes-all: "SELECT id_pokemon, id_type FROM PokemonTypes" + # retrieve max. Pokemon ID + select-max-id: "SELECT MAX(id) FROM Pokemons" + # test queries + select-pokemon-named-arg: "SELECT id, name FROM Pokemons WHERE name=:name" + select-pokemon-order-arg: "SELECT id, name FROM Pokemons WHERE name=?" + # test DML insert + insert-pokemon-named-arg: "INSERT INTO Pokemons(id, name) VALUES(:id, :name)" + insert-pokemon-order-arg: "INSERT INTO Pokemons(id, name) VALUES(?, ?)" + # Pokemon mapper uses reverse order of indexed arguments + insert-pokemon-order-arg-rev: "INSERT INTO Pokemons(name, id) VALUES(?, ?)" + # test DML update + select-pokemon-by-id: "SELECT id, name FROM Pokemons WHERE id=?" + update-pokemon-named-arg: "UPDATE Pokemons SET name=:name WHERE id=:id" + update-pokemon-order-arg: "UPDATE Pokemons SET name=? WHERE id=?" + # test DML delete + delete-pokemon-named-arg: "DELETE FROM Pokemons WHERE id=:id" + delete-pokemon-order-arg: "DELETE FROM Pokemons WHERE id=?" + # Pokemon mapper uses full list of attributes + delete-pokemon-full-named-arg: "DELETE FROM Pokemons WHERE name=:name AND id=:id" + delete-pokemon-full-order-arg: "DELETE FROM Pokemons WHERE name=? AND id=?" + # test DbStatementQuery methods + select-pokemons-idrng-named-arg: "SELECT id, name FROM Pokemons WHERE id > :idmin AND id < :idmax" + select-pokemons-idrng-order-arg: "SELECT id, name FROM Pokemons WHERE id > ? AND id < ?" + # Test query with both named and ordered parameters (shall cause an exception) + select-pokemons-error-arg: "SELECT id, name FROM Pokemons WHERE id > :id AND name = ?" diff --git a/tests/integration/dbclient/mongodb/pom.xml b/tests/integration/dbclient/mongodb/pom.xml new file mode 100644 index 000000000..6d01e43d4 --- /dev/null +++ b/tests/integration/dbclient/mongodb/pom.xml @@ -0,0 +1,276 @@ + + + + + 4.0.0 + + + io.helidon.tests.integration.dbclient + helidon-tests-integration-dbclient-project + 2.0-SNAPSHOT + ../pom.xml + + + io.helidon.tests.integration.dbclient + heldion-tests-integration-dbclient-mongodb + Integration Tests: DB Client MongoDB + + + 27017 + 127.0.0.1 + test + helidon + h3l1d0n + root + I4mGr00t + + + + + io.helidon.tests.integration.dbclient + heldion-tests-integration-dbclient-common + ${project.version} + test + + + io.helidon.config + helidon-config + test + + + io.helidon.config + helidon-config-yaml + test + + + io.helidon.dbclient + helidon-dbclient-mongodb + test + + + org.mongodb + mongodb-driver-reactivestreams + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + src/test/resources + true + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${version.plugin.surefire} + + true + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${version.plugin.surefire} + + methods + 10 + + + + + init + integration-test + + integration-test + + + + io.helidon.tests.integration.dbclient.mongodb.init.*IT + + + + + + test + integration-test + + integration-test + + + + io.helidon.tests.integration.dbclient:heldion-tests-integration-dbclient-common + + + io.helidon.tests.integration.dbclient.common.tests.simple.*IT + io.helidon.tests.integration.dbclient.common.tests.statement.*IT + io.helidon.tests.integration.dbclient.common.tests.interceptor.*IT + io.helidon.tests.integration.dbclient.common.tests.dbresult.*IT + io.helidon.tests.integration.dbclient.common.tests.mapping.*IT + io.helidon.tests.integration.dbclient.common.tests.health.*IT + io.helidon.tests.integration.dbclient.common.tests.metrics.*IT + + + + + + + + destroy + integration-test + + integration-test + + + + io.helidon.tests.integration.dbclient.mongodb.destroy.*IT + + + + + + + + + + + + + + debug + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${version.plugin.surefire} + + + test + + ${it.jdbc.debug} + ${it.jdbc.test} + + + + + + + + + test + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${version.plugin.surefire} + + + test + + ${it.jdbc.test} + + + + + + + + + docker + + + + io.fabric8 + docker-maven-plugin + 0.31.0 + + + start + pre-integration-test + + start + + + + stop + post-integration-test + + stop + + + + + + + mongo:4.2 + mongo + + + ${mongo.roootuser} + ${mongo.roootpw} + ${mongo.database} + + ${mongo.host} + + ${mongo.host}:${mongo.port}:27017 + + + MySQL server is up an running + + 127.0.0.1 + + ${mongo.port} + + + + + + + + true + false + + + + + + + + \ No newline at end of file diff --git a/tests/integration/dbclient/mongodb/src/test/java/io/helidon/tests/integration/dbclient/mongodb/destroy/DestroyIT.java b/tests/integration/dbclient/mongodb/src/test/java/io/helidon/tests/integration/dbclient/mongodb/destroy/DestroyIT.java new file mode 100644 index 000000000..9eb8b9604 --- /dev/null +++ b/tests/integration/dbclient/mongodb/src/test/java/io/helidon/tests/integration/dbclient/mongodb/destroy/DestroyIT.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.mongodb.destroy; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Destroy database + */ +public class DestroyIT { + + /** Local logger instance. */ + static final Logger LOGGER = Logger.getLogger(DestroyIT.class.getName()); + + /** + * Delete database content. + * + * @param dbClient Helidon database client + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + private static void deleteSchema(DbClient dbClient) throws ExecutionException, InterruptedException { + dbClient.execute(exec -> exec + .namedDelete("delete-poketypes") + .thenCompose(result -> exec.namedDelete("delete-pokemons")) + .thenCompose(result -> exec.namedDelete("delete-types")) + ).toCompletableFuture().get(); + } + + + /** + * Destroy database after tests. + */ + @BeforeAll + public static void destroy() { + try { + deleteSchema(DB_CLIENT); + } catch (ExecutionException | InterruptedException ex) { + fail("Database cleanup failed!", ex); + } + } + + /** + * Verify that table Types does not exist. + * + * @throws ExecutionException when database query failed + */ + @Test + public void testTypesDeleted() throws InterruptedException { + try { + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-types") + ).toCompletableFuture().get(); + if (rows != null) { + List rowsList = rows.collect().toCompletableFuture().get(); + LOGGER.warning(() -> String.format("Rows count: %d", rowsList.size())); + assertThat(rowsList, empty()); + } + } catch (ExecutionException ex) { + LOGGER.info(() -> String.format("Caught expected exception: %s", ex.getMessage())); + } + } + + /** + * Verify that table Pokemons does not exist. + * + * @throws ExecutionException when database query failed + */ + @Test + public void testPokemonsDeleted() throws InterruptedException { + try { + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-pokemons") + ).toCompletableFuture().get(); + if (rows != null) { + List rowsList = rows.collect().toCompletableFuture().get(); + LOGGER.warning(() -> String.format("Rows count: %d", rowsList.size())); + assertThat(rowsList, empty()); + } + } catch (ExecutionException ex) { + LOGGER.info(() -> String.format("Caught expected exception: %s", ex.getMessage())); + } + } + + /** + * Verify that table PokemonTypes does not exist. + * + * @throws ExecutionException when database query failed + */ + @Test + public void testPokemonTypesDeleted() throws InterruptedException { + try { + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-poketypes-all") + ).toCompletableFuture().get(); + if (rows != null) { + List rowsList = rows.collect().toCompletableFuture().get(); + LOGGER.warning(() -> String.format("Rows count: %d", rowsList.size())); + assertThat(rowsList, empty()); + } + } catch (ExecutionException ex) { + LOGGER.info(() -> String.format("Caught expected exception: %s", ex.getMessage())); + } + } + +} diff --git a/tests/integration/dbclient/mongodb/src/test/java/io/helidon/tests/integration/dbclient/mongodb/init/CheckIT.java b/tests/integration/dbclient/mongodb/src/test/java/io/helidon/tests/integration/dbclient/mongodb/init/CheckIT.java new file mode 100644 index 000000000..f67d0b365 --- /dev/null +++ b/tests/integration/dbclient/mongodb/src/test/java/io/helidon/tests/integration/dbclient/mongodb/init/CheckIT.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.mongodb.init; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.config.Config; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.CONFIG; +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Check minimal functionality needed before running database schema initialization. + * First test class being executed after database startup. + */ +public class CheckIT extends AbstractIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(CheckIT.class.getName()); + + /** Timeout in seconds to wait for database to come up. */ + private static final int TIMEOUT = 60; + + /** Helidon DB client with admin database access. */ + public static final DbClient DB_ADMIN = initDbAdmin(); + + private static DbClient initDbAdmin() { + Config dbConfig = CONFIG.get("dbadmin"); + return DbClient.builder(dbConfig).build(); + } + + /** + * Wait for database server to start. + * + * @param dbClient Helidon database client + */ + private static void waitForStart(DbClient dbClient) { + long endTm = 1000 * TIMEOUT + System.currentTimeMillis(); + boolean retry = true; + while (retry) { + try { + dbClient.execute(exec -> exec.namedStatement("ping")) + .toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + retry = false; + } catch (ExecutionException | InterruptedException ex) { + if (System.currentTimeMillis() > endTm) { + fail("Database startup failed!", ex); + } else { + LOGGER.info(() -> String.format("Exception: %s", ex.getMessage())); + LOGGER.log(Level.INFO, "Exception details: ", ex); + } + } + } + } + + /** + * Initialize user for test database. + * + * @param dbClient Helidon database client + */ + private static void initUser(DbClient dbClient) { + try { + dbClient.execute(exec -> exec + .namedStatement("use") + .thenCompose(result -> exec.namedStatement("create-user")) + ).toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + } catch (ExecutionException | InterruptedException ex) { + LOGGER.warning(() -> String.format("Exception: %s", ex.getMessage())); + fail("Database user setup failed!", ex); + } + } + + /** + * Setup database for tests. + * Wait for database to start. Returns after ping query completed successfully or timeout passed. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @BeforeAll + public static void setup() throws ExecutionException, InterruptedException { + LOGGER.info(() -> String.format("Initializing Integration Tests")); + waitForStart(DB_ADMIN); + //initUser(DB_ADMIN); + } + + /** + * Simple test to verify that DML query execution works. + * Used before running database schema initialization. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testDmlStatementExecution() throws ExecutionException, InterruptedException { + DbRows result = DB_CLIENT.execute(exec -> exec.namedStatement("ping")) + .toCompletableFuture().get().rsFuture().toCompletableFuture().get(); + List rowsList = result.collect().toCompletableFuture().get(); + DbRow row = rowsList.get(0); + Double ok = row.column("ok").as(Double.class); + assertThat(ok, equalTo(1.0)); + LOGGER.info(() -> String.format("Command ping row: %s", row.toString())); + } + +} diff --git a/tests/integration/dbclient/mongodb/src/test/java/io/helidon/tests/integration/dbclient/mongodb/init/InitIT.java b/tests/integration/dbclient/mongodb/src/test/java/io/helidon/tests/integration/dbclient/mongodb/init/InitIT.java new file mode 100644 index 000000000..b4dd0876b --- /dev/null +++ b/tests/integration/dbclient/mongodb/src/test/java/io/helidon/tests/integration/dbclient/mongodb/init/InitIT.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.tests.integration.dbclient.mongodb.init; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbRow; +import io.helidon.dbclient.DbRows; +import io.helidon.tests.integration.dbclient.common.AbstractIT; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.dbclient.common.AbstractIT.DB_CLIENT; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Initialize database + */ +public class InitIT extends AbstractIT { + + /** Local logger instance. */ + private static final Logger LOGGER = Logger.getLogger(InitIT.class.getName()); + + /** + * Initialize database content (rows in tables). + * + * @param dbClient Helidon database client + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + private static void initData(DbClient dbClient) throws InterruptedException, ExecutionException { + // Init pokemon types + dbClient.execute(tx -> { + CompletionStage stage = null; + for (Map.Entry entry : TYPES.entrySet()) { + if (stage == null) { + stage = tx.namedInsert("insert-type", entry.getKey(), entry.getValue().getName()); + } else { + stage = stage.thenCompose(result -> tx.namedInsert( + "insert-type", entry.getKey(), entry.getValue().getName())); + } + } + return stage; + }).toCompletableFuture().get(); + // Init pokemons + dbClient.execute(tx -> { + CompletionStage stage = null; + for (Map.Entry entry : POKEMONS.entrySet()) { + if (stage == null) { + stage = tx.namedInsert("insert-pokemon", entry.getKey(), entry.getValue().getName()); + } else { + stage = stage.thenCompose(result -> tx.namedInsert( + "insert-pokemon", entry.getKey(), entry.getValue().getName())); + } + } + return stage; + }).toCompletableFuture().get(); + // Init pokemon to type relation + dbClient.execute(tx -> { + CompletionStage stage = null; + for (Map.Entry entry : POKEMONS.entrySet()) { + Pokemon pokemon = entry.getValue(); + for (Type type : pokemon.getTypes()) { + if (stage == null) { + stage = tx.namedInsert("insert-poketype", pokemon.getId(), type.getId()); + } else { + stage = stage.thenCompose(result -> tx.namedInsert( + "insert-poketype", pokemon.getId(), type.getId())); + } + } + } + return stage; + }).toCompletableFuture().get(); + } + + /** + * Setup database for tests. + */ + @BeforeAll + public static void setup() { + LOGGER.info(() -> "Initializing Integration Tests"); + try { + initData(DB_CLIENT); + } catch (ExecutionException | InterruptedException ex) { + fail("Database setup failed!", ex); + } + } + + /** + * Verify that database contains properly initialized pokemon types. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testListTypes() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-types") + ).toCompletableFuture().get(); + assertThat(rows, notNullValue()); + List rowsList = rows.collect().toCompletableFuture().get(); + assertThat(rowsList, not(empty())); + Set ids = new HashSet<>(TYPES.keySet()); + for (DbRow row : rowsList) { + Integer id = row.column(1).as(Integer.class); + String name = row.column(2).as(String.class); + assertThat(ids, hasItem(id)); + ids.remove(id); + assertThat(name, TYPES.get(id).getName().equals(name)); + LOGGER.info(() -> String.format("Type id=%d name=%s", id, name)); + } + } + + /** + * Verify that database contains properly initialized pokemons. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testListPokemons() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-pokemons") + ).toCompletableFuture().get(); + assertThat(rows, notNullValue()); + List rowsList = rows.collect().toCompletableFuture().get(); + assertThat(rowsList, not(empty())); + Set ids = new HashSet<>(POKEMONS.keySet()); + for (DbRow row : rowsList) { + Integer id = row.column(1).as(Integer.class); + String name = row.column(2).as(String.class); + assertThat(ids, hasItem(id)); + ids.remove(id); + assertThat(name, POKEMONS.get(id).getName().equals(name)); + LOGGER.info(() -> String.format("Pokemon id=%d name=%s", id, name)); + } + } + + /** + * Verify that database contains properly initialized pokemon types relation. + * + * @throws ExecutionException when database query failed + * @throws InterruptedException if the current thread was interrupted + */ + @Test + public void testListPokemonTypes() throws ExecutionException, InterruptedException { + DbRows rows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-pokemons") + ).toCompletableFuture().get(); + assertThat(rows, notNullValue()); + List rowsList = rows.collect().toCompletableFuture().get(); + assertThat(rowsList, not(empty())); + for (DbRow row : rowsList) { + Integer pokemonId = row.column(1).as(Integer.class); + String pokemonName = row.column(2).as(String.class); + Pokemon pokemon = POKEMONS.get(pokemonId); + assertThat(pokemonName, POKEMONS.get(pokemonId).getName().equals(pokemonName)); + LOGGER.info(() -> String.format("Pokemon id=%d name=%s", pokemonId, pokemonName)); + DbRows typeRows = DB_CLIENT.execute(exec -> exec + .namedQuery("select-poketypes", pokemonId) + ).toCompletableFuture().get(); + List typeRowsList = typeRows.collect().toCompletableFuture().get(); + assertThat(typeRowsList.size(), equalTo(pokemon.getTypes().size())); + for (DbRow typeRow : typeRowsList) { + Integer typeId = typeRow.column(2).as(Integer.class); + LOGGER.info(() -> String.format(" - Type id=%d name=%s", typeId, TYPES.get(typeId).getName())); + assertThat(pokemon.getTypes(), hasItem(TYPES.get(typeId))); + } + } + } + +} diff --git a/tests/integration/dbclient/mongodb/src/test/resources/test.yaml b/tests/integration/dbclient/mongodb/src/test/resources/test.yaml new file mode 100644 index 000000000..948518de4 --- /dev/null +++ b/tests/integration/dbclient/mongodb/src/test/resources/test.yaml @@ -0,0 +1,214 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +dbadmin: + source: mongoDb + connection: + url: mongodb://${mongo.host}:${mongo.port}/admin + username: ${mongo.roootuser} + password: ${mongo.roootpw} + + statements: + # required ping statement + ping: '{ + "operation": "command", + "query": { ping: 1 } + }' + use: '{ + "operation": "command", + "query": { use: "${mongo.database}" } + }' + create-user: '{ + "operation": "command", + "query": { + "createUser": "${mongo.user}", + "pwd": "${mongo.password}", + "roles": [ + { role: "readWrite", db: "${mongo.database}" } + ] + } + }' + + +db: + source: mongoDb + connection: + url: mongodb://${mongo.host}:${mongo.port}/admin + username: ${mongo.roootuser} + password: ${mongo.roootpw} +# url: mongodb://${mongo.host}:${mongo.port}/${mongo.database} +# username: ${mongo.user} +# password: ${mongo.password} + + statements: + # required ping statement + ping: '{ + "operation": "command", + "query": { ping: 1 } + }' + # database schema cleanup statements + delete-types: '{ + "collection": "types", + "operation": "delete", + "query": { } + }' + delete-pokemons: '{ + "collection": "pokemons", + "operation": "delete", + "query": { } + }' + delete-poketypes: '{ + "collection": "pokemon_types", + "operation": "delete", + "query": { } + }' + # data initialization statements + insert-type: '{ + "collection": "types", + "value": { + "id": ?, + "type": ? + } + }' + insert-pokemon: '{ + "collection": "pokemons", + "value": { + "id": ?, + "name": ? + } + }' + insert-poketype: '{ + "collection": "pokemon_types", + "value": { + "id_pokemon": ?, + "id_type": ? + } + }' + # data initialization verification statements + select-types: '{ + "collection": "types", + "projection": { id: 1, type: 1, _id: 0 }, + "query": {} + }' + select-pokemons: '{ + "collection": "pokemons", + "projection": { id: 1, name: 1, _id: 0 }, + "query": {} + }' + select-poketypes: '{ + "collection": "pokemon_types", + "projection": { id_pokemon: 1, id_type: 1, _id: 0 }, + "query": { id_pokemon: ? } + }' + # data cleanup verification statements + select-poketypes-all: '{ + "collection": "pokemon_types", + "projection": { id_pokemon: 1, id_type: 1, _id: 0 }, + "query": {} + }' + # test queries + select-pokemon-named-arg: '{ + "collection": "pokemons", + "operation": "query", + "projection": { id: 1, name: 1, _id: 0 }, + "query": { name: $name } + }' + select-pokemon-order-arg: '{ + "collection": "pokemons", + "operation": "query", + "projection": { id: 1, name: 1, _id: 0 }, + "query": { name: ? } + }' + # test DML insert + insert-pokemon-named-arg: '{ + "collection": "pokemons", + "operation": "insert", + "value": { + "id": $id, + "name": $name + } + }' + insert-pokemon-order-arg: '{ + "collection": "pokemons", + "operation": "insert", + "value": { + "id": ?, + "name": ? + } + }' + # Pokemon mapper uses reverse order of indexed arguments + insert-pokemon-order-arg-rev: '{ + "collection": "pokemons", + "operation": "insert", + "value": { + "name": ?, + "id": ? + } + }' + # test DML update + select-pokemon-by-id: '{ + "collection": "pokemons", + "operation": "query", + "projection": { id: 1, name: 1, _id: 0 }, + "query": { id: ? } + }' + update-pokemon-named-arg: '{ + "collection": "pokemons", + "operation": "update", + "value":{ $set: { "name": $name } }, + "query": { id: $id } + }' + update-pokemon-order-arg: '{ + "collection": "pokemons", + "operation": "update", + "value":{ $set: { "name": ? } }, + "query": { id: ? } + }' + # test DML delete + delete-pokemon-named-arg: '{ + "collection": "pokemons", + "operation": "delete", + "query": { id: $id } + }' + delete-pokemon-order-arg: '{ + "collection": "pokemons", + "operation": "delete", + "query": { id: ? } + }' + # Pokemon mapper uses full list of attributes + delete-pokemon-full-named-arg: '{ + "collection": "pokemons", + "operation": "delete", + "query": { $and: [ {name: $name }, { id: $id } ] } + }' + delete-pokemon-full-order-arg: '{ + "collection": "pokemons", + "operation": "delete", + "query": { $and: [ {name: ? }, { id: ? } ] } + }' + # test DbStatementQuery methods + select-pokemons-idrng-named-arg: '{ + "collection": "pokemons", + "operation": "query", + "projection": { id: 1, name: 1, _id: 0 }, + "query": { $and: [ { id: { $gt: $idmin } }, { id: { $lt: $idmax } } ] } + }' + select-pokemons-idrng-order-arg: '{ + "collection": "pokemons", + "operation": "query", + "projection": { id: 1, name: 1, _id: 0 }, + "query": { $and: [ { id: { $gt: ? } }, { id: { $lt: ? } } ] } + }' \ No newline at end of file diff --git a/tests/integration/dbclient/pom.xml b/tests/integration/dbclient/pom.xml new file mode 100644 index 000000000..9afbfe601 --- /dev/null +++ b/tests/integration/dbclient/pom.xml @@ -0,0 +1,45 @@ + + + + + 4.0.0 + + + io.helidon.tests.integration + helidon-tests-integration + 2.0-SNAPSHOT + ../pom.xml + + + pom + + io.helidon.tests.integration.dbclient + helidon-tests-integration-dbclient-project + Integration Tests: DB Client + + + A set of tests that validate DB Client + + + + common + jdbc + mongodb + + \ No newline at end of file diff --git a/tests/integration/pom.xml b/tests/integration/pom.xml index da26d0d2a..abdad7bc3 100644 --- a/tests/integration/pom.xml +++ b/tests/integration/pom.xml @@ -1,6 +1,6 @@