Helidon DB Client (#657)

* Helidon DB API and implementation for JDBC and MongoDB.

Signed-off-by: Tomas Kraus <Tomas.Kraus@oracle.com>
Signed-off-by: Tomas Langer <tomas.langer@oracle.com>

* Hikari CP metrics

Signed-off-by: Tomas Langer <tomas.langer@oracle.com>
This commit is contained in:
Tomas Langer
2020-01-23 18:42:21 +01:00
committed by GitHub
parent 115a822c02
commit c1e87abc62
217 changed files with 24370 additions and 21 deletions

View File

@@ -503,6 +503,75 @@
<artifactId>helidon-common-metrics</artifactId>
<version>${helidon.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common-mapper</artifactId>
<version>${project.version}</version>
</dependency>
<!-- db client -->
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-jdbc</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-mongodb</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-mongodb</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-health</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-jsonp</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-metrics</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-metrics-jdbc</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-tracing</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-webserver-jsonp</artifactId>
<version>${project.version}</version>
</dependency>
<!-- db client examples -->
<dependency>
<groupId>io.helidon.examples.dbclient</groupId>
<artifactId>helidon-examples-dbclient-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- tracing -->
<dependency>

View File

@@ -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 <K> type of the keys of the map
* @param <V> type of the values of the map
*/
public final class LruCache<K, V> {
/**
* Default capacity of the cache: {@value}.
*/
public static final int DEFAULT_CAPACITY = 10000;
private final LinkedHashMap<K, V> 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<K, V> builder) {
this.capacity = builder.capacity;
}
/**
* Create a new builder.
*
* @param <K> key type
* @param <V> value type
* @return a new fluent API builder instance
*/
public static <K, V> Builder<K, V> builder() {
return new Builder<>();
}
/**
* Create an instance with default configuration.
*
* @param <K> key type
* @param <V> value type
* @return a new cache instance
* @see #DEFAULT_CAPACITY
*/
public static <K, V> LruCache<K, V> create() {
Builder<K, V> builder = builder();
return builder.build();
}
/**
* Get a value from the cache.
*
* @param key key to retrieve
* @return value if present or empty
*/
public Optional<V> 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<V> 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<V> 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<V> 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<V> computeValue(K key, Supplier<Optional<V>> valueSupplier) {
// get is properly synchronized
Optional<V> currentValue = get(key);
if (currentValue.isPresent()) {
return currentValue;
}
Optional<V> 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 <K> type of keys
* @param <V> type of values
*/
public static class Builder<K, V> implements io.helidon.common.Builder<LruCache<K, V>> {
private int capacity = DEFAULT_CAPACITY;
@Override
public LruCache<K, V> build() {
return new LruCache<>(this);
}
/**
* Load configuration of this cache from configuration.
*
* @param config configuration
* @return updated builder instance
*/
public Builder<K, V> 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<K, V> capacity(int capacity) {
this.capacity = capacity;
return this;
}
}
}

View File

@@ -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<String, String> theCache = LruCache.create();
String value = "cached";
String key = "theKey";
String newValue = "not-cached";
Optional<String> 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<String, String> theCache = LruCache.create();
String value = "cached";
String key = "theKey";
String newValue = "not-cached";
Optional<String> 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<Integer, Integer> theCache = LruCache.<Integer, Integer>builder().capacity(10).build();
for (int i = 0; i < 10; i++) {
theCache.put(i, i);
}
for (int i = 0; i < 10; i++) {
Optional<Integer> integer = theCache.get(i);
assertThat(integer, is(Optional.of(i)));
}
theCache.put(10, 10);
Optional<Integer> res = theCache.get(0);
assertThat(res, is(Optional.empty()));
res = theCache.get(10);
assertThat(res, is(Optional.of(10)));
}
@Test
void testLruBehavior() {
LruCache<Integer, Integer> theCache = LruCache.<Integer, Integer>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> integer = theCache.get(i);
assertThat(integer, is(Optional.of(i)));
}
// now use 0
Optional<Integer> 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()));
}
}

View File

@@ -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 <T> type of the publisher
* @return a new empty publisher that just completes the subscriber
*/
public static <T> Flow.Publisher<T> 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 <T> type of the publisher
* @return a new publisher that publishes the single value and completes the subscriber
*/
public static <T> Flow.Publisher<T> 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() {
}
});
}
}

View File

@@ -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 <SOURCE> type of the publisher we subscribe to
* @param <TARGET> type of the publisher we expose
*/
public final class MappingProcessor<SOURCE, TARGET> implements Flow.Processor<SOURCE, TARGET> {
private final Function<SOURCE, TARGET> resultMapper;
private Flow.Subscriber<? super TARGET> mySubscriber;
private Flow.Subscription subscription;
private MappingProcessor(Function<SOURCE, TARGET> 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 <S> Source type
* @param <T> Target type
* @return a new mapping processor
*/
public static <S, T> MappingProcessor<S, T> create(Function<S, T> mappingFunction) {
return new MappingProcessor<>(mappingFunction);
}
@Override
public void subscribe(Flow.Subscriber<? super TARGET> 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();
}
}

View File

@@ -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 <T> return type of the asynchronous operation
*/
public interface OptionalCompletionStage<T> extends CompletionStage<Optional<T>> {
/**
* 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<T> 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<T> onValue(Consumer<? super T> action);
/**
* Creates a new instance of the completion stage that allows processing of cases when the element
* is present and when not.
*
* @param <T> return type of the asynchronous operation
* @param originalStage source completion stage instance
* @return the new {@code OptionalCompletionStage}
*/
static <T> OptionalCompletionStage<T> create(CompletionStage<Optional<T>> originalStage) {
return new OptionalCompletionStageImpl<>(originalStage);
}
}

View File

@@ -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 <T> return type of the asynchronous operation
*/
class OptionalCompletionStageImpl<T> implements OptionalCompletionStage<T> {
private final CompletionStage<Optional<T>> originalStage;
OptionalCompletionStageImpl(CompletionStage<Optional<T>> originalStag) {
this.originalStage = originalStag;
}
@Override
public OptionalCompletionStage<T> onEmpty(Runnable action) {
originalStage.thenAccept(original -> {
if (!original.isPresent()) {
action.run();
}
});
return this;
}
@Override
public OptionalCompletionStage<T> onValue(Consumer<? super T> action) {
originalStage.thenAccept(original -> {
original.ifPresent(action);
});
return this;
}
@Override
public <U> CompletionStage<U> thenApply(Function<? super Optional<T>, ? extends U> fn) {
return originalStage.thenApply(fn);
}
@Override
public <U> CompletionStage<U> thenApplyAsync(Function<? super Optional<T>, ? extends U> fn) {
return originalStage.thenApplyAsync(fn);
}
@Override
public <U> CompletionStage<U> thenApplyAsync(Function<? super Optional<T>, ? extends U> fn,
Executor executor) {
return originalStage.thenApplyAsync(fn, executor);
}
@Override
public CompletionStage<Void> thenAccept(Consumer<? super Optional<T>> action) {
return originalStage.thenAccept(action);
}
@Override
public CompletionStage<Void> thenAcceptAsync(Consumer<? super Optional<T>> action) {
return originalStage.thenAcceptAsync(action);
}
@Override
public CompletionStage<Void> thenAcceptAsync(Consumer<? super Optional<T>> action,
Executor executor) {
return originalStage.thenAcceptAsync(action, executor);
}
@Override
public CompletionStage<Void> thenRun(Runnable action) {
return originalStage.thenRun(action);
}
@Override
public CompletionStage<Void> thenRunAsync(Runnable action) {
return originalStage.thenRunAsync(action);
}
@Override
public CompletionStage<Void> thenRunAsync(Runnable action, Executor executor) {
return originalStage.thenRunAsync(action, executor);
}
@Override
public <U, V> CompletionStage<V> thenCombine(CompletionStage<? extends U> other,
BiFunction<? super Optional<T>, ? super U, ? extends V> fn) {
return originalStage.thenCombine(other, fn);
}
@Override
public <U, V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,
BiFunction<? super Optional<T>, ? super U, ? extends V> fn) {
return originalStage.thenCombineAsync(other, fn);
}
@Override
public <U, V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,
BiFunction<? super Optional<T>, ? super U, ? extends V> fn,
Executor executor) {
return originalStage.thenCombineAsync(other, fn, executor);
}
@Override
public <U> CompletionStage<Void> thenAcceptBoth(CompletionStage<? extends U> other,
BiConsumer<? super Optional<T>, ? super U> action) {
return originalStage.thenAcceptBoth(other, action);
}
@Override
public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,
BiConsumer<? super Optional<T>, ? super U> action) {
return originalStage.thenAcceptBothAsync(other, action);
}
@Override
public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,
BiConsumer<? super Optional<T>, ? super U> action,
Executor executor) {
return originalStage.thenAcceptBothAsync(other, action, executor);
}
@Override
public CompletionStage<Void> runAfterBoth(CompletionStage<?> other, Runnable action) {
return originalStage.runAfterBoth(other, action);
}
@Override
public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action) {
return originalStage.runAfterBothAsync(other, action);
}
@Override
public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action, Executor executor) {
return originalStage.runAfterBothAsync(other, action, executor);
}
@Override
public <U> CompletionStage<U> applyToEither(CompletionStage<? extends Optional<T>> other,
Function<? super Optional<T>, U> fn) {
return originalStage.applyToEither(other, fn);
}
@Override
public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends Optional<T>> other,
Function<? super Optional<T>, U> fn) {
return originalStage.applyToEitherAsync(other, fn);
}
@Override
public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends Optional<T>> other,
Function<? super Optional<T>, U> fn,
Executor executor) {
return originalStage.applyToEitherAsync(other, fn, executor);
}
@Override
public CompletionStage<Void> acceptEither(CompletionStage<? extends Optional<T>> other,
Consumer<? super Optional<T>> action) {
return originalStage.acceptEither(other, action);
}
@Override
public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends Optional<T>> other,
Consumer<? super Optional<T>> action) {
return originalStage.acceptEitherAsync(other, action);
}
@Override
public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends Optional<T>> other,
Consumer<? super Optional<T>> action,
Executor executor) {
return originalStage.acceptEitherAsync(other, action, executor);
}
@Override
public CompletionStage<Void> runAfterEither(CompletionStage<?> other, Runnable action) {
return originalStage.runAfterEither(other, action);
}
@Override
public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action) {
return originalStage.runAfterEitherAsync(other, action);
}
@Override
public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other,
Runnable action,
Executor executor) {
return originalStage.runAfterEitherAsync(other, action, executor);
}
@Override
public <U> CompletionStage<U> thenCompose(Function<? super Optional<T>, ? extends CompletionStage<U>> fn) {
return originalStage.thenCompose(fn);
}
@Override
public <U> CompletionStage<U> thenComposeAsync(Function<? super Optional<T>, ? extends CompletionStage<U>> fn) {
return originalStage.thenComposeAsync(fn);
}
@Override
public <U> CompletionStage<U> thenComposeAsync(Function<? super Optional<T>, ? extends CompletionStage<U>> fn,
Executor executor) {
return originalStage.thenComposeAsync(fn, executor);
}
@Override
public CompletionStage<Optional<T>> exceptionally(Function<Throwable, ? extends Optional<T>> fn) {
return originalStage.exceptionally(fn);
}
@Override
public CompletionStage<Optional<T>> whenComplete(BiConsumer<? super Optional<T>, ? super Throwable> action) {
return originalStage.whenComplete(action);
}
@Override
public CompletionStage<Optional<T>> whenCompleteAsync(BiConsumer<? super Optional<T>, ? super Throwable> action) {
return originalStage.whenCompleteAsync(action);
}
@Override
public CompletionStage<Optional<T>> whenCompleteAsync(BiConsumer<? super Optional<T>, ? super Throwable> action,
Executor executor) {
return originalStage.whenCompleteAsync(action, executor);
}
@Override
public <U> CompletionStage<U> handle(BiFunction<? super Optional<T>, Throwable, ? extends U> fn) {
return originalStage.handle(fn);
}
@Override
public <U> CompletionStage<U> handleAsync(BiFunction<? super Optional<T>, Throwable, ? extends U> fn) {
return originalStage.handleAsync(fn);
}
@Override
public <U> CompletionStage<U> handleAsync(BiFunction<? super Optional<T>, Throwable, ? extends U> fn,
Executor executor) {
return originalStage.handleAsync(fn, executor);
}
@Override
public CompletableFuture<Optional<T>> toCompletableFuture() {
return originalStage.toCompletableFuture();
}
}

View File

@@ -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<T> extends Subscribable<T> {
}
/**
* 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<T> extends Subscribable<T> {
}
}
/**
* Exposes this {@link Single} instance as a {@link CompletionStage} with {@code Optional<T>} 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<Optional<T>> toOptionalStage() {
try {
SingleToOptionalFuture<T> subscriber = new SingleToOptionalFuture<>();
this.subscribe(subscriber);
return subscriber;
} catch (Throwable ex) {
CompletableFuture<Optional<T>> future = new CompletableFuture<>();
future.completeExceptionally(ex);
return future;
}
}
/**
* Short-hand for {@code toFuture().toCompletableFuture().get()}.
* @return T

View File

@@ -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<T> extends CompletableFuture<Optional<T>> implements Subscriber<T> {
private final AtomicReference<Subscription> 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<T> 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");
}
}

View File

@@ -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();

51
dbclient/common/pom.xml Normal file
View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>helidon-dbclient-project</artifactId>
<groupId>io.helidon.dbclient</groupId>
<version>2.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>helidon-dbclient-common</artifactId>
<name>Helidon DB Client Common</name>
<dependencies>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common-configurable</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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.
* <p>
* 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;
}
}

View File

@@ -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 <S> type of a subclass
* @param <R> the result type of the statement as returned by {@link #execute()}
*/
public abstract class AbstractStatement<S extends DbStatement<S, R>, R> implements DbStatement<S, R> {
/** 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<R> execute() {
CompletableFuture<Long> queryFuture = new CompletableFuture<>();
CompletableFuture<Void> statementFuture = new CompletableFuture<>();
DbInterceptorContext dbContext = DbInterceptorContext.create(dbType())
.resultFuture(queryFuture)
.statementFuture(statementFuture);
update(dbContext);
CompletionStage<DbInterceptorContext> 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<DbInterceptorContext> invokeInterceptors(DbInterceptorContext dbContext) {
CompletableFuture<DbInterceptorContext> 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<R> doExecute(CompletionStage<DbInterceptorContext> dbContext,
CompletableFuture<Void> statementFuture,
CompletableFuture<Long> 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<String, ?> 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<String, Object> 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<Object> 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;
}
}
}

View File

@@ -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<Object> 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 <T> StatementParameters indexedParam(T parameters) {
Class<T> theClass = (Class<T>) parameters.getClass();
params(dbMapperManager.toIndexedParameters(parameters, theClass));
return this;
}
@Override
public List<Object> 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<String, ?> parameters) {
throw new DbClientException(CANT_USE_INDEXED_PARAMS);
}
@Override
public <T> 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<String, Object> namedParams() {
throw new DbClientException(CANT_USE_INDEXED_PARAMS);
}
}

View File

@@ -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<DbInterceptor> 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<InterceptorSupport> {
private final List<DbInterceptor> interceptors = new LinkedList<>();
private final Map<DbStatementType, List<DbInterceptor>> typeInterceptors = new EnumMap<>(DbStatementType.class);
private final Map<String, List<DbInterceptor>> 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<DbInterceptor> interceptors = new LinkedList<>(this.interceptors);
final Map<DbStatementType, List<DbInterceptor>> typeInterceptors = new EnumMap<>(this.typeInterceptors);
final Map<String, List<DbInterceptor>> namedStatementInterceptors = new HashMap<>(this.namedStatementInterceptors);
final LruCache<CacheKey, List<DbInterceptor>> cachedInterceptors = LruCache.create();
return new InterceptorSupport() {
@Override
public List<DbInterceptor> interceptors(DbStatementType dbStatementType, String statementName) {
// order is defined in DbInterceptor interface
return cachedInterceptors.computeValue(new CacheKey(dbStatementType, statementName), () -> {
List<DbInterceptor> 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<DbInterceptor> result, List<DbInterceptor> 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);
}
}
}
}

View File

@@ -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<String, Object> parameters = new HashMap<>();
private final DbMapperManager dbMapperManager;
NamedStatementParameters(DbMapperManager dbMapperManager) {
this.dbMapperManager = dbMapperManager;
}
@Override
public StatementParameters params(Map<String, ?> parameters) {
this.parameters.clear();
this.parameters.putAll(parameters);
return this;
}
@SuppressWarnings("unchecked")
@Override
public <T> StatementParameters namedParam(T parameters) {
Class<T> theClass = (Class<T>) 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<String, Object> 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 <T> 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<Object> indexedParams() {
throw new DbClientException(CANT_USE_NAMED_PARAMS);
}
}

View File

@@ -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
}

View File

@@ -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 <T> type of the array
* @return updated db statement
*/
default <T> 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<String, ?> 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 <T> type of the parameters
* @return updated db statement
*/
<T> 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 <T> type of the parameters
* @return updated db statement
*/
<T> 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 <T> type of the parameters
* @return updated db statement
*/
default <T> StatementParameters namedParam(T parameters, Function<T, Map<String, ?>> 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 <T> type of the parameters
* @return updated db statement
*/
default <T> StatementParameters indexedParam(T parameters, Function<T, List<?>> 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<String, Object> namedParams();
/**
* Return {@code List} containing all ordered parameters.
*
* @return {@code List} containing all ordered parameters
*/
List<Object> indexedParams();
}

View File

@@ -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;

View File

@@ -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;
}

60
dbclient/dbclient/pom.xml Normal file
View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>helidon-dbclient-project</artifactId>
<groupId>io.helidon.dbclient</groupId>
<version>2.0-SNAPSHOT</version>
</parent>
<artifactId>helidon-dbclient</artifactId>
<name>Helidon DB Client</name>
<description>Helidon Reactive Database Client API</description>
<dependencies>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common-service-loader</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common-context</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common-mapper</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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 <T> statement execution result type
* @param executor database statement executor, see {@link DbExecute}
* @return statement execution result
*/
<T> CompletionStage<T> inTransaction(Function<DbTransaction, CompletionStage<T>> executor);
/**
* Execute database statement.
*
* @param <T> statement execution result type
* @param executor database statement executor, see {@link DbExecute}
* @return statement execution result
*/
<T extends CompletionStage<?>> T execute(Function<DbExecute, T> executor);
/**
* Pings the database, completes when DB is up and ready, completes exceptionally if not.
*
* @return stage that completes when the ping finished
*/
CompletionStage<Void> 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.
* <p>Database driver is loaded as SPI provider which implements {@link io.helidon.dbclient.spi.DbClientProvider} interface.
* First provider on the class path is selected.</p>
*
* @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.
* <p>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.</p>
*
* @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<DbClient> {
static {
HelidonFeatures.register(HelidonFlavor.SE, "DbClient");
}
private final HelidonServiceLoader.Builder<DbInterceptorProvider> 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<DbInterceptorProvider> 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<List<String>> 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<List<String>> 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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<String, DbClientProvider> DB_SOURCES = new HashMap<>();
private static final String[] NAMES;
private static final DbClientProvider FIRST;
static {
HelidonServiceLoader<DbClientProvider> serviceLoader = HelidonServiceLoader
.builder(ServiceLoader.load(DbClientProvider.class))
.build();
List<DbClientProvider> 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<DbClientProvider> get(String name) {
return Optional.ofNullable(DB_SOURCES.get(name));
}
static String[] names() {
return NAMES;
}
}

View File

@@ -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 <T> 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> T as(Class<T> 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 <T> type of the returned value
* @return value mapped to the expected type if possible
* @throws MapperException in case the mapping cannot be done
*/
<T> T as(GenericType<T> 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.
* <p>
* 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.
* <p>
* Precision depends on data type:
* <ul>
* <li>Numeric: The maximal number of digits of the number</li>
* <li>String/Character: The maximal length</li>
* <li>Binary: The maximal number of bytes</li>
* <li>Other: Implementation specific</li>
* </ul>
*
* @return precision of this column or {@code empty} if precision is not available
*/
default Optional<Integer> precision() {
return Optional.empty();
}
/**
* Scale of this column.
* <p>
* 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<Integer> scale() {
return Optional.empty();
}
}

View File

@@ -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.
* <p>The database executor provides methods to create {@link DbStatement} instances for different types
* of database statements.</p>
* <p>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
* <p>There are five methods for each {@link DbStatementType}, example for query (the implementation
* detail is for the default implementation, providers may differ):
* <ol>
* <li>{@code DbStatement} {@link #createNamedQuery(String, String)} - full control over the name and content of the
* statement</li>
* <li>{@code DbStatement} {@link #createNamedQuery(String)} - use statement text from configuration</li>
* <li>{@code DbStatement} {@link #createQuery(String)} - use the provided statement, name is generated </li>
* <li>{@code DbRowResult} {@link #namedQuery(String, Object...)} - shortcut method to a named query with a list of
* parameters (or with no parameters at all)</li>
* <li>{@code DbRowResult} {@link #query(String, Object...)} - shortcut method to unnamed query with a list of parameters
* (or with no parameters at all)</li>
* </ol>
* 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<DbRows<DbRow>> 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<DbRows<DbRow>> 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<DbRow> 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<Optional<DbRow>> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<Long> 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<DbResult> 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<DbResult> statement(String statement, Object... parameters) {
return createStatement(statement).params(parameters).execute();
}
}

View File

@@ -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.
* <p>
* 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:
* <ol>
* <li>Named interceptors - if there are any interceptors configured for a specific statement, they are executed first</li>
* <li>Type interceptors - if there are any interceptors configured for a type of statement, they are executed next</li>
* <li>Global interceptors - if there are any interceptors configured globally, they are executed last</li>
* </ol>
* 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<DbInterceptorContext> statement(DbInterceptorContext context);
}

View File

@@ -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.
* <p>
* 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<Void> 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<Long> resultFuture();
/**
* Indexed parameters (if used).
*
* @return indexed parameters (empty if this statement parameters are not indexed)
*/
Optional<List<Object>> indexedParameters();
/**
* Named parameters (if used).
*
* @return named parameters (empty if this statement parameters are not named)
*/
Optional<Map<String, Object>> 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<Void> 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<Long> 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<Object> 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<String, Object> 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<Object> 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<String, Object> namedParameters);
/**
* Set the type of the statement.
*
* @param type statement type
* @return updated interceptor context
*/
DbInterceptorContext statementType(DbStatementType type);
}

View File

@@ -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<Void> statementFuture;
private CompletionStage<Long> queryFuture;
private List<Object> indexedParams;
private Map<String, Object> 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<Void> statementFuture() {
return statementFuture;
}
@Override
public Optional<List<Object>> indexedParameters() {
if (indexed) {
return Optional.of(indexedParams);
}
throw new IllegalStateException("Indexed parameters are not available for statement with named parameters");
}
@Override
public Optional<Map<String, Object>> 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<Void> statementFuture) {
this.statementFuture = statementFuture;
return this;
}
@Override
public CompletionStage<Long> resultFuture() {
return queryFuture;
}
@Override
public DbInterceptorContext resultFuture(CompletionStage<Long> resultFuture) {
this.queryFuture = resultFuture;
return this;
}
@Override
public DbInterceptorContext statement(String statement, List<Object> indexedParams) {
this.statement = statement;
this.indexedParams = indexedParams;
this.indexed = true;
return this;
}
@Override
public DbInterceptorContext statement(String statement, Map<String, Object> namedParams) {
this.statement = statement;
this.namedParams = namedParams;
this.indexed = false;
return this;
}
@Override
public DbInterceptorContext parameters(List<Object> 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<String, Object> 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;
}
}

View File

@@ -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.
* <p>
* 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 <T> target mapping type
*/
public interface DbMapper<T> {
/**
* 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<String, ?> toNamedParameters(T value);
/**
* Convert target type instance to a statement indexed parameters list.
* <p>
* 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);
}

View File

@@ -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<Object> TYPE_DB_ROW = GenericType.create(DbRow.class);
/**
* Generic type for the {@link Map} of String to value pairs for named parameters.
*/
GenericType<Map<String, ?>> TYPE_NAMED_PARAMS = new GenericType<Map<String, ?>>() { };
/**
* Generic type for the {@link List} of indexed parameters.
*/
GenericType<List<?>> TYPE_INDEXED_PARAMS = new GenericType<List<?>>() { };
/**
* 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<DbMapperProvider> 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 <T> 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> T read(DbRow row, Class<T> expectedType) throws MapperException;
/**
* Read database row into a typed value.
*
* @param row row from a database
* @param expectedType generic type of the response
* @param <T> 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> T read(DbRow row, GenericType<T> expectedType) throws MapperException;
/**
* Read object into a map of named parameters.
*
* @param value the typed value
* @param valueClass type of the value object
* @param <T> type of value
* @return map with the named parameters
* @see io.helidon.dbclient.DbStatement#namedParam(Object)
*/
<T> Map<String, ?> toNamedParameters(T value, Class<T> valueClass);
/**
* Read object into a list of indexed parameters.
*
* @param value the typed value
* @param valueClass type of the value object
* @param <T> type of value
* @return list with indexed parameters (in the order expected by statements using this object)
* @see io.helidon.dbclient.DbStatement#indexedParam(Object)
*/
<T> List<?> toIndexedParameters(T value, Class<T> valueClass);
/**
* Fluent API builder for {@link io.helidon.dbclient.DbMapperManager}.
*/
final class Builder implements io.helidon.common.Builder<DbMapperManager> {
private final HelidonServiceLoader.Builder<DbMapperProvider> providers = HelidonServiceLoader
.builder(ServiceLoader.load(DbMapperProvider.class));
private HelidonServiceLoader<DbMapperProvider> 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<DbMapperProvider> mapperProviders() {
if (null == providerLoader) {
return providers.build().asList();
} else {
return providerLoader.asList();
}
}
private Builder serviceLoader(HelidonServiceLoader<DbMapperProvider> serviceLoader) {
this.providerLoader = serviceLoader;
return this;
}
}
}

View File

@@ -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<DbMapperProvider> providers;
private final Map<Class<?>, DbMapper<?>> byClass = new ConcurrentHashMap<>();
private final Map<GenericType<?>, DbMapper<?>> byType = new ConcurrentHashMap<>();
DbMapperManagerImpl(Builder builder) {
this.providers = builder.mapperProviders();
}
@Override
public <T> T read(DbRow row, Class<T> expectedType) {
return executeMapping(() -> findMapper(expectedType, false)
.read(row),
row,
TYPE_DB_ROW,
GenericType.create(expectedType));
}
@Override
public <T> T read(DbRow row, GenericType<T> expectedType) {
return executeMapping(() -> findMapper(expectedType, false)
.read(row),
row,
TYPE_DB_ROW,
expectedType);
}
@Override
public <T> Map<String, ?> toNamedParameters(T value, Class<T> valueClass) {
return executeMapping(() -> findMapper(valueClass, false)
.toNamedParameters(value),
value,
GenericType.create(valueClass),
TYPE_NAMED_PARAMS);
}
@Override
public <T> List<?> toIndexedParameters(T value, Class<T> valueClass) {
return executeMapping(() -> findMapper(valueClass, false)
.toIndexedParameters(value),
value,
GenericType.create(valueClass),
TYPE_INDEXED_PARAMS);
}
private <T> T executeMapping(Supplier<T> 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 <T> DbMapper<T> findMapper(Class<T> type, boolean fromTypes) {
DbMapper<?> mapper = byClass.computeIfAbsent(type, aClass -> {
return fromProviders(type)
.orElseGet(() -> {
GenericType<T> targetType = GenericType.create(type);
if (fromTypes) {
return notFoundMapper(targetType);
}
return findMapper(targetType, true);
});
});
return (DbMapper<T>) mapper;
}
@SuppressWarnings("unchecked")
private <T> DbMapper<T> findMapper(GenericType<T> type, boolean fromClasses) {
DbMapper<?> mapper = byType.computeIfAbsent(type, aType -> {
return fromProviders(type)
.orElseGet(() -> {
if (!fromClasses && type.isClass()) {
return findMapper((Class<T>) type.rawType(), true);
}
return notFoundMapper(type);
});
});
return (DbMapper<T>) mapper;
}
private <T> Optional<DbMapper<T>> fromProviders(Class<T> type) {
return providers.stream()
.flatMap(provider -> provider.mapper(type).stream()).findFirst();
}
private <T> Optional<DbMapper<T>> fromProviders(GenericType<T> 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 <T> DbMapper<T> notFoundMapper(GenericType<T> type) {
return new DbMapper<T>() {
@Override
public T read(DbRow row) {
throw new MapperException(TYPE_DB_ROW, type, ERROR_NO_MAPPER_FOUND);
}
@Override
public Map<String, ?> 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);
}
};
}
}

View File

@@ -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).
* <p>
* 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}.
* <p>
* 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.
* <p>
* 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<Long> 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<DbRows<DbRow>> 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<Throwable> 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<Long> 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<DbRows<DbRow>> 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<Throwable> exceptionFuture();
}

View File

@@ -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<? super DbColumn> columnAction);
/**
* Get specific class instance representation of this row.
* Mapper for target class must be already registered.
*
* @param <T> 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> T as(Class<T> type) throws MapperException;
/**
* Map this row to an object using a {@link io.helidon.dbclient.DbMapper}.
*
* @param type type that supports generic declarations
* @param <T> 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> T as(GenericType<T> type) throws MapperException;
/**
* Get specific class instance representation of this row.
* Mapper for target class is provided as an argument.
*
* @param <T> 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> T as(Function<DbRow, T> mapper);
}

View File

@@ -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 <T> type of the result, starts as {@link io.helidon.dbclient.DbRow}
*/
public interface DbRows<T> {
/**
* Map this row result using a mapping function.
*
* @param mapper mapping function
* @param <U> new type of the row result
* @return row result of the correct type
*/
<U> DbRows<U> map(Function<T, U> mapper);
/**
* Map this row result using a configured {@link io.helidon.common.mapper.Mapper} that can
* map current type to the desired type.
*
* <p>
* 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 <U> new type of the row result
* @return row result of the correct type
*/
<U> DbRows<U> map(Class<U> type);
/**
* Map this row result using a configured {@link io.helidon.common.mapper.Mapper} that can
* map current type to the desired type.
* <p>
* 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 <U> new type of the row result
* @return row result of the target type
*/
<U> DbRows<U> map(GenericType<U> type);
/**
* Get this result as a publisher of rows mapped to the correct type.
*
* @return publisher
*/
Flow.Publisher<T> publisher();
/**
* Collect all the results into a list of rows mapped to the correct type.
* <p><b>This is a dangerous operation, as it collects all results in memory. Use with care.</b>
* @return future with the list
*/
CompletionStage<List<T>> collect();
}

View File

@@ -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.
* <p>
* 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.
* <p>
* Once parameters are set using one of the {@code params} methods, all other methods throw an
* {@link IllegalStateException}.
* <p>
* Once a parameter is added using {@link #addParam(Object)} or {@link #addParam(String, Object)}, all other
* {@code params} methods throw an {@link IllegalStateException}.
* <p>
* Once {@link #execute()} is called, all methods would throw an {@link IllegalStateException}.
*
* @param <R> Type of the result of this statement (e.g. a {@link java.util.concurrent.CompletionStage})
* @param <D> Type of the descendant of this class
*/
public interface DbStatement<D extends DbStatement<D, R>, 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<String, ?> 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<R> execute();
}

View File

@@ -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<DbStatementDml, Long> {
}

View File

@@ -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<DbStatementGeneric, DbResult> {
}

View File

@@ -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<DbStatementGet, Optional<DbRow>> {
}

View File

@@ -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<DbStatementQuery, DbRows<DbRow>> {
}

View File

@@ -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;
}
}

View File

@@ -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<DbStatements> {
private final Map<String, String> 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<String, String> 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;
}
};
}
}
}

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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 <T> type of the builder extending implementing this interface.
*/
public interface DbClientProviderBuilder<T extends DbClientProviderBuilder<T>> extends Builder<DbClient> {
/**
* 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> type of the supported class
* @return updated builder instance.
*/
<TYPE> T addMapper(DbMapper<TYPE> dbMapper, Class<TYPE> 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> type of the supported class
* @return updated builder instance.
*/
<TYPE> T addMapper(DbMapper<TYPE> dbMapper, GenericType<TYPE> 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.
* <p>
* Note the specific handling of the following types:
* <ul>
* <li>{@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)</li>
* <li>{@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)</li>
* </ul>
*
* @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();
}

View File

@@ -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:
* <ul>
* <li>{@code global}: the configuration key is used to get a new instance</li>
* <li>{code named}: for each configuration node with a list of nodes, a new instance is requested</li>
* <li>{code typed}: for each configuration node with a list of types, a new instance is requested</li>
* </ul>
* @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);
}

View File

@@ -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 <T> target mapping type
* @param type class of the returned mapper type
* @return a mapper for the specified type or empty
*/
<T> Optional<DbMapper<T>> mapper(Class<T> type);
/**
* Returns mapper for specific type supporting generic types as well.
* To get a list of strings: {@code mapper(new GenericType<List<String>>(){})}
*
* @param type type to find mapper for
* @param <T> type of the response
* @return a mapper for the specified type or empty
*/
default <T> Optional<DbMapper<T>> mapper(GenericType<T> type) {
return Optional.empty();
}
}

View File

@@ -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;

View File

@@ -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;
}

43
dbclient/health/pom.xml Normal file
View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-project</artifactId>
<version>2.0-SNAPSHOT</version>
</parent>
<artifactId>helidon-dbclient-health</artifactId>
<name>Helidon DB Client Health</name>
<description>Health check for Helidon DB Client</description>
<dependencies>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.health</groupId>
<artifactId>helidon-health</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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> 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<DbClientHealthCheck> {
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;
}
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<FindBugsFilter
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://github.com/spotbugs/filter/3.0.0"
xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd">
<Match>
<!-- False positive. See https://github.com/spotbugs/spotbugs/issues/756 -->
<Class name="io.helidon.dbclient.jdbc.JdbcStatementQuery"/>
<Bug pattern="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE"/>
</Match>
<Match>
<!-- False positive. See https://github.com/spotbugs/spotbugs/issues/756 -->
<Class name="io.helidon.dbclient.jdbc.JdbcStatementQuery$RowPublisher"/>
<Bug pattern="RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE"/>
</Match>
</FindBugsFilter>

64
dbclient/jdbc/pom.xml Normal file
View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>helidon-dbclient-project</artifactId>
<groupId>io.helidon.dbclient</groupId>
<version>2.0-SNAPSHOT</version>
</parent>
<artifactId>helidon-dbclient-jdbc</artifactId>
<name>Helidon DB Client JDBC</name>
<description>Helidon DB implementation for JDBC</description>
<properties>
<spotbugs.exclude>etc/spotbugs/exclude.xml</spotbugs.exclude>
</properties>
<dependencies>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-common</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common-configurable</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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.
* <table>
* <caption>Optional configuration parameters</caption>
* <tr>
* <th>key</th>
* <th>default value</th>
* <th>description</th>
* </tr>
* <tr>
* <td>url</td>
* <td>&nbsp;</td>
* <td>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}</td>
* </tr>
* <tr>
* <td>username</td>
* <td>&nbsp;</td>
* <td>Username used to connect to the database</td>
* </tr>
* <tr>
* <td>password</td>
* <td>&nbsp;</td>
* <td>Password used to connect to the database</td>
* </tr>
* </table>
*
* @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<ConnectionPool> {
/**
* 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<HikariCpExtensionProvider> 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<HikariCpExtension> 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<String, String> 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;
}
}
}

View File

@@ -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<HikariCpExtension> 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;
}
}

View File

@@ -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);
}

View File

@@ -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 <T> CompletionStage<T> inTransaction(Function<DbTransaction, CompletionStage<T>> executor) {
JdbcTxExecute execute = new JdbcTxExecute(
statements,
executorService,
interceptors,
connectionPool,
dbMapperManager,
mapperManager);
CompletionStage<T> 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 <T> statement execution result type
*/
private static final class RollbackHandler<T> implements Function<Throwable, T> {
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 extends CompletionStage<?>> T execute(Function<DbExecute, T> 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<Void> 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> 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<Connection> 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<Connection> 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> 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);
}
});
}
}
}

View File

@@ -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();
}
}

View File

@@ -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<JdbcDbClientProviderBuilder> {
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> 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<ExecutorService> 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 <TYPE> JdbcDbClientProviderBuilder addMapper(DbMapper<TYPE> dbMapper, Class<TYPE> mappedClass) {
this.dbMapperBuilder.addMapperProvider(new DbMapperProvider() {
@SuppressWarnings("unchecked")
@Override
public <T> Optional<DbMapper<T>> mapper(Class<T> type) {
if (type.equals(mappedClass)) {
return Optional.of((DbMapper<T>) dbMapper);
}
return Optional.empty();
}
});
return this;
}
@Override
public <TYPE> JdbcDbClientProviderBuilder addMapper(DbMapper<TYPE> dbMapper, GenericType<TYPE> mappedType) {
this.dbMapperBuilder.addMapperProvider(new DbMapperProvider() {
@Override
public <T> Optional<DbMapper<T>> mapper(Class<T> type) {
return Optional.empty();
}
@SuppressWarnings("unchecked")
@Override
public <T> Optional<DbMapper<T>> mapper(GenericType<T> type) {
if (type.equals(mappedType)) {
return Optional.of((DbMapper<T>) 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;
}
}

View File

@@ -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> connection;
private final ConcurrentHashMap.KeySetView<CompletableFuture<Long>, Boolean> futures = ConcurrentHashMap.newKeySet();
private JdbcExecuteContext(ExecutorService executorService,
InterceptorSupport interceptors,
DbMapperManager dbMapperManager,
MapperManager mapperManager,
String dbType,
CompletionStage<Connection> 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> 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> connection() {
return connection;
}
void addFuture(CompletableFuture<Long> queryFuture) {
this.futures.add(queryFuture);
}
public CompletionStage<Void> whenComplete() {
CompletionStage<?> overallStage = CompletableFuture.completedFuture(null);
for (CompletableFuture<Long> future : futures) {
overallStage = overallStage.thenCompose(o -> future);
}
return overallStage.thenAccept(it -> {
});
}
}

View File

@@ -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<StmtRunnable> 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<QueryProcessor> 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<QueryProcessor> 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);
}
}
}
}
}

View File

@@ -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 <S> subclass of this class
* @param <R> Statement execution result type
*/
abstract class JdbcStatement<S extends DbStatement<S, R>, R> extends AbstractStatement<S, R> {
/** 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> 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<PreparedStatement> 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> 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<String, Object> 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<String> 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<Object> 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<String> namesOrder, final Map<String, Object> parameters) {
// Parameters in query missing in parameters Map
List<String> 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<Parser> {}
/**
* 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<String> 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<String> namesOrder() {
return names;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<DbStatementDml, Long> implements DbStatementDml {
JdbcStatementDml(JdbcExecuteContext executeContext,
JdbcStatementContext statementContext) {
super(executeContext, statementContext);
}
@Override
protected CompletionStage<Long> doExecute(CompletionStage<DbInterceptorContext> dbContextFuture,
CompletableFuture<Void> statementFuture,
CompletableFuture<Long> 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;
});
});
}
}

View File

@@ -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<DbStatementGeneric, DbResult> 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<DbRows<DbRow>> queryResultFuture;
private final CompletableFuture<Long> dmlResultFuture;
private final CompletableFuture<Throwable> exceptionFuture;
GenericDbResult(
final CompletableFuture<DbRows<DbRow>> queryResultFuture,
final CompletableFuture<Long> dmlResultFuture,
final CompletableFuture<Throwable> exceptionFuture
) {
this.queryResultFuture = queryResultFuture;
this.dmlResultFuture = dmlResultFuture;
this.exceptionFuture = exceptionFuture;
}
@Override
public DbResult whenDml(Consumer<Long> consumer) {
dmlResultFuture.thenAccept(consumer);
return this;
}
@Override
public DbResult whenRs(Consumer<DbRows<DbRow>> consumer) {
queryResultFuture.thenAccept(consumer);
return this;
}
@Override
public DbResult exceptionally(Consumer<Throwable> exceptionHandler) {
exceptionFuture.thenAccept(exceptionHandler);
return this;
}
@Override
public CompletionStage<Long> dmlFuture() {
return dmlResultFuture;
}
@Override
public CompletionStage<DbRows<DbRow>> rsFuture() {
return queryResultFuture;
}
@Override
public CompletionStage<Throwable> exceptionFuture() {
return exceptionFuture;
}
}
JdbcStatementGeneric(JdbcExecuteContext executeContext,
JdbcStatementContext statementContext) {
super(executeContext, statementContext);
}
@Override
protected CompletionStage<DbResult> doExecute(CompletionStage<DbInterceptorContext> dbContextFuture,
CompletableFuture<Void> interceptorStatementFuture,
CompletableFuture<Long> interceptorQueryFuture) {
executeContext().addFuture(interceptorQueryFuture);
CompletableFuture<DbRows<DbRow>> queryResultFuture = new CompletableFuture<>();
CompletableFuture<Long> dmlResultFuture = new CompletableFuture<>();
CompletableFuture<Throwable> exceptionFuture = new CompletableFuture<>();
CompletableFuture<JdbcStatementQuery.ResultWithConn> 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<DbRow> 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);
});
}
}

View File

@@ -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<String, ?> 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<Optional<DbRow>> execute() {
return query.execute()
.thenApply(dbRows -> Single.from(dbRows.publisher()))
.thenCompose(Single::toOptionalStage);
}
}

View File

@@ -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<DbStatementQuery, DbRows<DbRow>> 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<DbRows<DbRow>> doExecute(CompletionStage<DbInterceptorContext> dbContextFuture,
CompletableFuture<Void> statementFuture,
CompletableFuture<Long> queryFuture) {
executeContext().addFuture(queryFuture);
CompletionStage<DbRows<DbRow>> 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<DbRow> processResultSet(
ExecutorService executorService,
DbMapperManager dbMapperManager,
MapperManager mapperManager,
CompletableFuture<Long> queryFuture,
ResultSet resultSet) {
return new JdbcDbRows<>(
resultSet,
executorService,
dbMapperManager,
mapperManager,
queryFuture,
DbRow.class);
}
static Map<Long, DbColumn> createMetadata(ResultSet rs) throws SQLException {
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
Map<Long, DbColumn> 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> T as(Class<T> type) {
return null;
}
@Override
public <T> T as(GenericType<T> 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<T> implements DbRows<T> {
private final AtomicBoolean resultRequested = new AtomicBoolean();
private final ExecutorService executorService;
private final DbMapperManager dbMapperManager;
private final MapperManager mapperManager;
private final CompletableFuture<Long> queryFuture;
private final ResultSet resultSet;
private final JdbcDbRows<?> parent;
private final GenericType<T> currentType;
private final Function<?, T> resultMapper;
private JdbcDbRows(ResultSet resultSet,
ExecutorService executorService,
DbMapperManager dbMapperManager,
MapperManager mapperManager,
CompletableFuture<Long> queryFuture,
Class<T> initialType) {
this(resultSet,
executorService,
dbMapperManager,
mapperManager,
queryFuture,
GenericType.create(initialType),
Function.identity(),
null);
}
private JdbcDbRows(ResultSet resultSet,
ExecutorService executorService,
DbMapperManager dbMapperManager,
MapperManager mapperManager,
CompletableFuture<Long> queryFuture,
GenericType<T> nextType,
Function<?, T> 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 <U> DbRows<U> map(Function<T, U> mapper) {
return new JdbcDbRows<>(resultSet,
executorService,
dbMapperManager,
mapperManager,
queryFuture,
null,
mapper,
this);
}
@Override
public <U> DbRows<U> map(Class<U> type) {
return map(GenericType.create(type));
}
@Override
public <U> DbRows<U> map(GenericType<U> type) {
GenericType<T> currentType = this.currentType;
Function<T, U> 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<U>) 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<T> publisher() {
checkResult();
return toPublisher();
}
@Override
public CompletionStage<List<T>> collect() {
checkResult();
return toFuture();
}
@SuppressWarnings("unchecked")
private Flow.Publisher<T> toPublisher() {
if (null == parent) {
// this is DbRow type
return (Flow.Publisher<T>) new RowPublisher(executorService,
resultSet,
queryFuture,
dbMapperManager,
mapperManager);
}
Function<Object, T> mappingFunction = (Function<Object, T>) resultMapper;
Multi<Object> parentMulti = (Multi<Object>) parent.multi();
return parentMulti.map(mappingFunction::apply);
}
private Multi<T> multi() {
return Multi.from(publisher());
}
private CompletionStage<List<T>> 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<DbRow> {
private final ExecutorService executorService;
private final ResultSet rs;
private final CompletableFuture<Long> queryFuture;
private final DbMapperManager dbMapperManager;
private final MapperManager mapperManager;
private RowPublisher(ExecutorService executorService,
ResultSet rs,
CompletableFuture<Long> 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<? super DbRow> subscriber) {
LinkedBlockingQueue<Long> 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<Long, DbColumn> 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<Long, DbColumn> metadata,
DbMapperManager dbMapperManager,
MapperManager mapperManager) throws SQLException {
// read whole row
// for each column
Map<String, DbColumn> byStringsWithValues = new HashMap<>();
Map<Integer, DbColumn> 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> T as(Class<T> type) {
if (null == value) {
return null;
}
if (type.isAssignableFrom(value.getClass())) {
return type.cast(value);
}
return map(value, type);
}
@SuppressWarnings("unchecked")
<SRC, T> T map(SRC value, Class<T> type) {
Class<SRC> theClass = (Class<SRC>) value.getClass();
return mapperManager.map(value, theClass, type);
}
@SuppressWarnings("unchecked")
<SRC, T> T map(SRC value, GenericType<T> type) {
Class<SRC> theClass = (Class<SRC>) value.getClass();
return mapperManager.map(value, GenericType.create(theClass), type);
}
@Override
public <T> T as(GenericType<T> 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<? super DbColumn> columnAction) {
byStringsWithValues.values()
.forEach(columnAction);
}
@Override
public <T> T as(Class<T> type) {
return dbMapperManager.read(this, type);
}
@Override
public <T> T as(GenericType<T> type) {
return dbMapperManager.read(this, type);
}
@Override
public <T> T as(Function<DbRow, T> 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;
}
}
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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<String> 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<String> namesExp = new ArrayList<>(2);
namesExp.add("n4m3");
namesExp.add("ag3");
Parser parser = new Parser(stmtIn);
String stmtOut = parser.convert();
List<String> 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<String> namesExp = new ArrayList<>(2);
namesExp.add("n4m3");
namesExp.add("ag3");
Parser parser = new Parser(stmtIn);
String stmtOut = parser.convert();
List<String> 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<String> namesExp = new ArrayList<>(2);
namesExp.add("myN4m3");
namesExp.add("ag3");
Parser parser = new Parser(stmtIn);
String stmtOut = parser.convert();
List<String> 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<String> namesExp = new ArrayList<>(2);
namesExp.add("zip");
Parser parser = new Parser(stmtIn);
String stmtOut = parser.convert();
List<String> names= parser.namesOrder();
assertEquals(stmtExp, stmtOut);
assertEquals(namesExp, names);
}
}

45
dbclient/jsonp/pom.xml Normal file
View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>helidon-dbclient-project</artifactId>
<groupId>io.helidon.dbclient</groupId>
<version>2.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>helidon-dbclient-jsonp</artifactId>
<name>Helidon DB Client JSON-Processing</name>
<description>
Support for JSON-Processing as parameters and responses in Helidon DB
</description>
<dependencies>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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<JsonObject> {
private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());
private static final Map<Class<?>, 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<String, Object> toNamedParameters(JsonObject value) {
Map<String, Object> result = new HashMap<>();
value.forEach((name, json) -> result.put(name, toObject(name, json, value)));
return result;
}
@Override
public List<Object> 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<Object> 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);
}
}

View File

@@ -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 <T> Optional<DbMapper<T>> mapper(Class<T> type) {
if (type.equals(JsonObject.class)) {
return Optional.of((DbMapper<T>) JsonProcessingMapper.create());
}
return Optional.empty();
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-project</artifactId>
<version>2.0-SNAPSHOT</version>
</parent>
<artifactId>helidon-dbclient-metrics-jdbc</artifactId>
<name>Helidon DB Client JDBC Metrics</name>
<description>Metrics support for Helidon DB implementation for JDBC</description>
<dependencies>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-jdbc</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-metrics</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.metrics</groupId>
<artifactId>helidon-metrics</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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 <T> the type of the metric's value
*/
public class JdbcMetricsGauge<T> implements Gauge<T> {
private final com.codahale.metrics.Gauge<T> gauge;
JdbcMetricsGauge(final com.codahale.metrics.Gauge<T> counter) {
this.gauge = counter;
}
@Override
public T getValue() {
return gauge.getValue();
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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> T time(Callable<T> 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());
}
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

43
dbclient/metrics/pom.xml Normal file
View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-project</artifactId>
<version>2.0-SNAPSHOT</version>
</parent>
<artifactId>helidon-dbclient-metrics</artifactId>
<name>Helidon DB Client Metrics</name>
<description>Metrics support for Helidon DB</description>
<dependencies>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.metrics</groupId>
<artifactId>helidon-metrics</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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<Counter> {
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.
* <p>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<Void> 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<Builder> implements io.helidon.common.Builder<DbCounter> {
@Override
public DbCounter build() {
return new DbCounter(this);
}
}
}

View File

@@ -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<Meter> {
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.
* <p>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<Void> 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<Builder> implements io.helidon.common.Builder<DbMeter> {
@Override
public DbMeter build() {
return new DbMeter(this);
}
}
}

View File

@@ -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<T extends Metric> implements DbInterceptor {
private final Metadata meta;
private final String description;
private final BiFunction<String, DbStatementType, String> nameFunction;
private final MetricRegistry registry;
private final ConcurrentHashMap<String, T> cache = new ConcurrentHashMap<>();
private final boolean measureErrors;
private final boolean measureSuccess;
protected DbMetric(DbMetricBuilder<?> builder) {
BiFunction<String, DbStatementType, String> 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<DbInterceptorContext> 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<Void> aFuture);
protected abstract MetricType metricType();
protected abstract T metric(MetricRegistry registry, Metadata meta);
}

Some files were not shown because too many files have changed in this diff Show More