Added debezium-core and MySQL binary log reading tests.

This commit is contained in:
Randall Hauch 2015-11-24 15:54:37 -06:00
parent 42926f17f3
commit dffdfd8049
56 changed files with 6199 additions and 37 deletions

54
debezium-core/pom.xml Normal file
View File

@ -0,0 +1,54 @@
<?xml version="1.0"?>
<!--
~ Copyright 2014 Red Hat, Inc. and/or its affiliates.
~
~ Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
-->
<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>
<groupId>io.debezium</groupId>
<artifactId>debezium-parent</artifactId>
<version>0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>debezium-core</artifactId>
<name>Debezium Core</name>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.easytesting</groupId>
<artifactId>fest-assert</artifactId>
</dependency>
</dependencies>
<build>
<resources>
<!-- Apply the properties set in the POM to the resource files -->
<resource>
<filtering>true</filtering>
<directory>src/main/resources</directory>
<includes>
<include>**/build.properties</include>
</includes>
</resource>
</resources>
</build>
</project>

View File

@ -0,0 +1,28 @@
package io.debezium.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Copyright (c) 2005 Brian Goetz and Tim Peierls.<br />
* Released under the Creative Commons Attribution License<br />
* (http://creativecommons.org/licenses/by/2.5)<br />
* Official home: http://www.jcip.net<br />
* Adopted from Java Concurrency in Practice.
* <p>
* An annotation that describes the monitor protecting the annotated field or method. For example, <code>@GuardedBy("this")</code>
* specifies that the lock is the object in whose class the field or method is defined, while <code>@GuardedBy("lock")</code>
* specifies that the method or field is guarded by a lock held in the "lock" field.
* </p>
*
* @see ThreadSafe
* @see NotThreadSafe
* @see Immutable
*/
@Target( {ElementType.FIELD, ElementType.METHOD} )
@Retention( RetentionPolicy.SOURCE )
public @interface GuardedBy {
String value();
}

View File

@ -0,0 +1,30 @@
package io.debezium.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Copyright (c) 2005 Brian Goetz and Tim Peierls.<br />
* Released under the Creative Commons Attribution License<br />
* (http://creativecommons.org/licenses/by/2.5)<br />
* Official home: http://www.jcip.net<br />
* Adopted from Java Concurrency in Practice.
* <p>
* This annotation documents that instances of the annotated class are immutable. This means that its state is seen to others as
* never being changed, even though the actual private internal state may indeed change. Therefore, in an immutable class:
* <ul>
* <li>all public fields are final; and</li>
* <li>all public final reference fields refer to other immutable objects; and</li>
* <li>constructors and methods do not publish references to any internal state which is potentially mutable by the
* implementation.</li>
* </ul>
* </p>
*/
@Documented
@Target( ElementType.TYPE )
@Retention( RetentionPolicy.RUNTIME )
public @interface Immutable {
}

View File

@ -0,0 +1,26 @@
package io.debezium.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Copyright (c) 2005 Brian Goetz and Tim Peierls.<br />
* Released under the Creative Commons Attribution License<br />
* (http://creativecommons.org/licenses/by/2.5)<br />
* Official home: http://www.jcip.net<br />
* Adopted from Java Concurrency in Practice.
* <p>
* This annotation documents the class as <i>not</i> being thread-safe, meaning the caller is expected to properly handle and
* guard all concurrent operations on an instance.
* </p>
*
* @see ThreadSafe
*/
@Documented
@Target( ElementType.TYPE )
@Retention( RetentionPolicy.RUNTIME )
public @interface NotThreadSafe {
}

View File

@ -0,0 +1,24 @@
package io.debezium.annotation;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PACKAGE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
* Annotation that can be used to specify that the target field, method, constructor, package or type is read-only.
*
* @see Immutable
*/
@Documented
@Retention( RUNTIME )
@Target( {FIELD, METHOD, CONSTRUCTOR, PACKAGE, TYPE} )
public @interface ReadOnly {
}

View File

@ -0,0 +1,27 @@
package io.debezium.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Copyright (c) 2005 Brian Goetz and Tim Peierls.<br />
* Released under the Creative Commons Attribution License<br />
* (http://creativecommons.org/licenses/by/2.5)<br />
* Official home: http://www.jcip.net<br />
* Adopted from Java Concurrency in Practice.
* <p>
* This annotation documents the class as being thread-safe. This means that no sequences of accesses (reads and writes to public
* fields, calls to public methods) may put the object into an invalid state, regardless of the interleaving of those actions by
* the runtime, and without requiring any additional synchronization or coordination on the part of the caller.
* </p>
*
* @see NotThreadSafe
*/
@Documented
@Target( {ElementType.TYPE, ElementType.FIELD} )
@Retention( RetentionPolicy.RUNTIME )
public @interface ThreadSafe {
}

View File

@ -0,0 +1,694 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.config;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntSupplier;
import java.util.function.LongSupplier;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.debezium.annotation.Immutable;
import io.debezium.util.Collect;
import io.debezium.util.IoUtil;
/**
* An immutable representation of a Debezium configuration. A {@link Configuration} instance can be obtained
* {@link #from(Properties) from Properties} or loaded from a {@link #load(File) file}, {@link #load(InputStream) stream},
* {@link #load(Reader) reader}, {@link #load(URL) URL}, or from a {@link #load(String, ClassLoader) resource on the classpath}.
* <p>
* A Configuration object is basically a decorator around a {@link Properties} object. It has methods to get and convert
* individual property values to numeric, boolean and String types, optionally using a default value if the given property value
* does not exist. However, it is {@link Immutable immutable}, so it does not have any methods to set property values, allowing
* it to be passed around and reused without concern that other components might change the underlying property values.
*
* @author Randall Hauch
*/
@Immutable
public interface Configuration {
/**
* The basic interface for configuration builders.
* @param <C> the type of configuration
* @param <B> the type of builder
*/
public static interface ConfigBuilder<C extends Configuration, B extends ConfigBuilder<C,B>> {
/**
* Associated the given value with the specified key.
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
B with( String key, String value );
/**
* Associated the given value with the specified key.
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with( String key, int value ) {
return with(key, Integer.toString(value));
}
/**
* Associated the given value with the specified key.
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with( String key, float value ) {
return with(key, Float.toString(value));
}
/**
* Associated the given value with the specified key.
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with( String key, double value ) {
return with(key, Double.toString(value));
}
/**
* Associated the given value with the specified key.
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with( String key, long value ) {
return with(key, Long.toString(value));
}
/**
* Associated the given value with the specified key.
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with( String key, boolean value ) {
return with(key, Boolean.toString(value));
}
/**
* Build and return the immutable configuration.
* @return the immutable configuration; never null
*/
C build();
}
/**
* A builder of Configuration objects.
*/
public static class Builder implements ConfigBuilder<Configuration,Builder> {
private final Properties props = new Properties();
protected Builder() {
}
protected Builder( Properties props ) {
this.props.putAll(props);
}
@Override
public Builder with(String key, String value) {
props.setProperty(key, value);
return this;
}
@Override
public Configuration build() {
return Configuration.from(props);
}
}
/**
* Create a new {@link Builder configuration builder}.
* @return the configuration builder
*/
public static Builder create() {
return new Builder();
}
/**
* Create a new {@link Builder configuration builder} that starts with a copy of the supplied configuration.
* @param config the configuration to copy
* @return the configuration builder
*/
public static Builder copy( Configuration config) {
return new Builder(config.asProperties());
}
/**
* Create a Configuration object that is populated by system properties, per {@link #withSystemProperties(String)}.
* @param prefix the required prefix for the system properties; may not be null but may be empty
* @return the configuration
*/
public static Configuration fromSystemProperties( String prefix ) {
return empty().withSystemProperties(prefix);
}
/**
* Obtain an empty configuration.
*
* @return an empty configuration; never null
*/
public static Configuration empty() {
return new Configuration() {
@Override
public Set<String> keys() {
return Collections.emptySet();
}
@Override
public String getString(String key) {
return null;
}
};
}
/**
* Obtain a configuration instance by copying the supplied Properties object. The supplied {@link Properties} object is
* copied so that the resulting Configuration cannot be modified.
*
* @param properties the properties; may be null or empty
* @return the configuration; never null
*/
public static Configuration from(Properties properties) {
Properties props = new Properties();
if (properties != null) props.putAll(properties);
return new Configuration() {
@Override
public String getString(String key) {
return properties.getProperty(key);
}
@Override
public Set<String> keys() {
return properties.stringPropertyNames();
}
@Override
public String toString() {
return props.toString();
}
};
}
/**
* Obtain a configuration instance by loading the Properties from the supplied URL.
*
* @param url the URL to the stream containing the configuration properties; may not be null
* @return the configuration; never null
* @throws IOException if there is an error reading the stream
*/
public static Configuration load(URL url) throws IOException {
try (InputStream stream = url.openStream()) {
return load(stream);
}
}
/**
* Obtain a configuration instance by loading the Properties from the supplied file.
*
* @param file the file containing the configuration properties; may not be null
* @return the configuration; never null
* @throws IOException if there is an error reading the stream
*/
public static Configuration load(File file) throws IOException {
try (InputStream stream = new FileInputStream(file)) {
return load(stream);
}
}
/**
* Obtain a configuration instance by loading the Properties from the supplied stream.
*
* @param stream the stream containing the properties; may not be null
* @return the configuration; never null
* @throws IOException if there is an error reading the stream
*/
public static Configuration load(InputStream stream) throws IOException {
try {
Properties properties = new Properties();
properties.load(stream);
return from(properties);
} finally {
stream.close();
}
}
/**
* Obtain a configuration instance by loading the Properties from the supplied reader.
*
* @param reader the reader containing the properties; may not be null
* @return the configuration; never null
* @throws IOException if there is an error reading the stream
*/
public static Configuration load(Reader reader) throws IOException {
try {
Properties properties = new Properties();
properties.load(reader);
return from(properties);
} finally {
reader.close();
}
}
/**
* Obtain a configuration instance by loading the Properties from a file on the file system or classpath given by the supplied
* path.
*
* @param path the path to the file containing the configuration properties; may not be null
* @param clazz the class whose classpath is to be used to find the file; may be null
* @return the configuration; never null but possibly empty
* @throws IOException if there is an error reading the stream
*/
public static Configuration load(String path, Class<?> clazz) throws IOException {
return load(path, clazz.getClassLoader());
}
/**
* Obtain a configuration instance by loading the Properties from a file on the file system or classpath given by the supplied
* path.
*
* @param path the path to the file containing the configuration properties; may not be null
* @param classLoader the class loader to use; may be null
* @return the configuration; never null but possibly empty
* @throws IOException if there is an error reading the stream
*/
public static Configuration load(String path, ClassLoader classLoader) throws IOException {
Logger logger = LoggerFactory.getLogger(Configuration.class);
return load(path, classLoader, logger::debug);
}
/**
* Obtain a configuration instance by loading the Properties from a file on the file system or classpath given by the supplied
* path.
*
* @param path the path to the file containing the configuration properties; may not be null
* @param classLoader the class loader to use; may be null
* @param logger the function that will be called with status updates; may be null
* @return the configuration; never null but possibly empty
* @throws IOException if there is an error reading the stream
*/
public static Configuration load(String path, ClassLoader classLoader, Consumer<String> logger) throws IOException {
try (InputStream stream = IoUtil.getResourceAsStream(path, classLoader, null, null, logger)) {
Properties props = new Properties();
if (stream != null) {
props.load(stream);
}
return from(props);
}
}
/**
* Determine whether this configuration contains a key-value pair with the given key and the value is non-null
*
* @param key the key
* @return true if the configuration contains the key, or false otherwise
*/
default boolean hasKey(String key) {
return getString(key) != null;
}
/**
* Get the set of keys in this configuration.
*
* @return the set of keys; never null but possibly empty
*/
public Set<String> keys();
/**
* Get the string value associated with the given key.
*
* @param key the key for the configuration property
* @return the value, or null if the key is null or there is no such key-value pair in the configuration
*/
public String getString(String key);
/**
* Get the string value associated with the given key, returning the default value if there is no such key-value pair.
*
* @param key the key for the configuration property
* @param defaultValue the value that should be returned by default if there is no such key-value pair in the configuration;
* may be null
* @return the configuration value, or the {@code defaultValue} if there is no such key-value pair in the configuration
*/
default String getString(String key, String defaultValue) {
return getString(key, () -> defaultValue);
}
/**
* Get the string value associated with the given key, returning the default value if there is no such key-value pair.
*
* @param key the key for the configuration property
* @param defaultValueSupplier the supplier of value that should be returned by default if there is no such key-value pair in
* the configuration; may be null and may return null
* @return the configuration value, or the {@code defaultValue} if there is no such key-value pair in the configuration
*/
default String getString(String key, Supplier<String> defaultValueSupplier) {
String value = getString(key);
return value != null ? value : (defaultValueSupplier != null ? defaultValueSupplier.get() : null);
}
/**
* Get the string value(s) associated with the given key, where the supplied regular expression is used to parse the single
* string value into multiple values.
*
* @param key the key for the configuration property
* @param regex the delimiting regular expression
* @return the list of string values; null only if there is no such key-value pair in the configuration
* @see String#split(String)
*/
default List<String> getStrings(String key, String regex) {
String value = getString(key);
if (value == null) return null;
return Collect.arrayListOf(value.split(regex));
}
/**
* Get the integer value associated with the given key.
*
* @param key the key for the configuration property
* @return the integer value, or null if the key is null, there is no such key-value pair in the configuration, or the value
* could not be parsed as an integer
*/
default Integer getInteger(String key) {
return getInteger(key, null);
}
/**
* Get the long value associated with the given key.
*
* @param key the key for the configuration property
* @return the integer value, or null if the key is null, there is no such key-value pair in the configuration, or the value
* could not be parsed as an integer
*/
default Long getLong(String key) {
return getLong(key, null);
}
/**
* Get the boolean value associated with the given key.
*
* @param key the key for the configuration property
* @return the boolean value, or null if the key is null, there is no such key-value pair in the configuration, or the value
* could not be parsed as a boolean value
*/
default Boolean getBoolean(String key) {
return getBoolean(key, null);
}
/**
* Get the integer value associated with the given key, returning the default value if there is no such key-value pair or
* if the value could not be {@link Integer#parseInt(String) parsed} as an integer.
*
* @param key the key for the configuration property
* @param defaultValue the default value
* @return the integer value, or null if the key is null, there is no such key-value pair in the configuration, or the value
* could not be parsed as an integer
*/
default int getInteger(String key, int defaultValue) {
return getInteger(key, () -> defaultValue).intValue();
}
/**
* Get the long value associated with the given key, returning the default value if there is no such key-value pair or
* if the value could not be {@link Long#parseLong(String) parsed} as a long.
*
* @param key the key for the configuration property
* @param defaultValue the default value
* @return the long value, or null if the key is null, there is no such key-value pair in the configuration, or the value
* could not be parsed as a long
*/
default long getLong(String key, long defaultValue) {
return getLong(key, () -> defaultValue).longValue();
}
/**
* Get the boolean value associated with the given key, returning the default value if there is no such key-value pair or
* if the value could not be {@link Boolean#parseBoolean(String) parsed} as a boolean value.
*
* @param key the key for the configuration property
* @param defaultValue the default value
* @return the boolean value, or null if the key is null, there is no such key-value pair in the configuration, or the value
* could not be parsed as a boolean value
*/
default boolean getBoolean(String key, boolean defaultValue) {
return getBoolean(key, () -> defaultValue).booleanValue();
}
/**
* Get the integer value associated with the given key, using the given supplier to obtain a default value if there is no such
* key-value pair.
*
* @param key the key for the configuration property
* @param defaultValueSupplier the supplier for the default value; may be null
* @return the integer value, or null if the key is null, there is no such key-value pair in the configuration, the
* {@code defaultValueSupplier} reference is null, or there is a key-value pair in the configuration but the value
* could not be parsed as an integer
*/
default Integer getInteger(String key, IntSupplier defaultValueSupplier) {
String value = getString(key);
if (value != null) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
}
}
return defaultValueSupplier != null ? defaultValueSupplier.getAsInt() : null;
}
/**
* Get the long value associated with the given key, using the given supplier to obtain a default value if there is no such
* key-value pair.
*
* @param key the key for the configuration property
* @param defaultValueSupplier the supplier for the default value; may be null
* @return the long value, or null if the key is null, there is no such key-value pair in the configuration, the
* {@code defaultValueSupplier} reference is null, or there is a key-value pair in the configuration but the value
* could not be parsed as a long
*/
default Long getLong(String key, LongSupplier defaultValueSupplier) {
String value = getString(key);
if (value != null) {
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
}
}
return defaultValueSupplier != null ? defaultValueSupplier.getAsLong() : null;
}
/**
* Get the boolean value associated with the given key, using the given supplier to obtain a default value if there is no such
* key-value pair.
*
* @param key the key for the configuration property
* @param defaultValueSupplier the supplier for the default value; may be null
* @return the boolean value, or null if the key is null, there is no such key-value pair in the configuration, the
* {@code defaultValueSupplier} reference is null, or there is a key-value pair in the configuration but the value
* could not be parsed as a boolean value
*/
default Boolean getBoolean(String key, BooleanSupplier defaultValueSupplier) {
String value = getString(key);
if (value != null) {
value = value.trim().toLowerCase();
if (Boolean.valueOf(value)) return Boolean.TRUE;
if (value.equals("false")) return false;
}
return defaultValueSupplier != null ? defaultValueSupplier.getAsBoolean() : null;
}
/**
* Get an instance of the class given by the value in the configuration associated with the given key.
*
* @param key the key for the configuration property
* @param clazz the Class of which the resulting object is expected to be an instance of; may not be null
* @return the new instance, or null if there is no such key-value pair in the configuration or if there is a key-value
* configuration but the value could not be converted to an existing class with a zero-argument constructor
*/
default <T> T getInstance(String key, Class<T> clazz) {
return getInstance(key, clazz, () -> getClass().getClassLoader());
}
/**
* Get an instance of the class given by the value in the configuration associated with the given key.
*
* @param key the key for the configuration property
* @param clazz the Class of which the resulting object is expected to be an instance of; may not be null
* @param classloaderSupplier the supplier of the ClassLoader to be used to load the resulting class; may be null if this
* class' ClassLoader should be used
* @return the new instance, or null if there is no such key-value pair in the configuration or if there is a key-value
* configuration but the value could not be converted to an existing class with a zero-argument constructor
*/
@SuppressWarnings("unchecked")
default <T> T getInstance(String key, Class<T> clazz, Supplier<ClassLoader> classloaderSupplier) {
String className = getString(key);
if (className != null) {
ClassLoader classloader = classloaderSupplier != null ? classloaderSupplier.get() : getClass().getClassLoader();
try {
return (T) classloader.loadClass(className);
} catch (ClassNotFoundException e) {
}
}
return null;
}
/**
* Return a new {@link Configuration} that contains only the subset of keys that match the given prefix.
* If desired, the keys in the resulting Configuration will have the prefix (plus any terminating "{@code .}" character if
* needed) removed.
* <p>
* This method returns this Configuration instance if the supplied {@code prefix} is null or empty.
*
* @param prefix the prefix
* @param removePrefix true if the prefix (and any subsequent "{@code .}" character) should be removed from the keys in the
* resulting Configuration, or false if the keys in this Configuration should be used as-is in the resulting
* Configuration
* @return the subset of this Configuration; never null
*/
default Configuration subset(String prefix, boolean removePrefix) {
if (prefix == null) return this;
prefix = prefix.trim();
if (prefix.isEmpty()) return this;
String prefixWithSeparator = prefix.endsWith(".") ? prefix : prefix + ".";
int minLength = prefixWithSeparator.length();
Function<String, String> prefixRemover = removePrefix ? key -> key.substring(minLength) : key -> key;
return filter(key->key.startsWith(prefixWithSeparator)).map(prefixRemover);
}
/**
* Return a new {@link Configuration} that contains only the subset of keys that satisfy the given predicate.
*
* @param mapper that function that transforms keys
* @return the subset Configuration; never null
*/
default Configuration map(Function<String, String> mapper) {
if (mapper == null) return this;
Map<String,String> newToOld = new HashMap<>();
keys().stream().filter(k -> k != null).forEach(oldKey -> {
String newKey = mapper.apply(oldKey);
if (newKey != null) {
newToOld.put(newKey, oldKey);
}
});
return new Configuration() {
@Override
public Set<String> keys() {
return Collect.unmodifiableSet(newToOld.keySet());
}
@Override
public String getString(String key) {
String oldKey = newToOld.get(key);
return Configuration.this.getString(oldKey);
}
};
}
/**
* Return a new {@link Configuration} that contains only the subset of keys that satisfy the given predicate.
*
* @param matcher the function that determines whether a key should be included in the subset
* @return the subset Configuration; never null
*/
default Configuration filter(Predicate<? super String> matcher) {
if (matcher == null) return this;
return new Configuration() {
@Override
public Set<String> keys() {
return Collect.unmodifiableSet(Configuration.this.keys().stream()
.filter(k -> k != null)
.filter(matcher)
.collect(Collectors.toSet()));
}
@Override
public String getString(String key) {
return matcher.test(key) ? Configuration.this.getString(key) : null;
}
};
}
/**
* Determine if this configuration is empty and has no properties.
*
* @return {@code true} if empty, or {@code false} otherwise
*/
default boolean isEmpty() {
return keys().isEmpty();
}
/**
* Get a copy of these configuration properties as a Properties object.
*
* @return the properties object; never null
*/
default Properties asProperties() {
Properties props = new Properties();
keys().forEach(key -> {
String value = getString(key);
if (key != null && value != null) props.setProperty(key, value);
});
return props;
}
/**
* Return a copy of this configuration except where acceptable system properties are used to overwrite properties copied from
* this configuration. All system properties whose name has the given prefix are added, where the prefix is removed from the
* system property name, it is converted to lower case, and each underscore character ('{@code _}') are replaced with a
* period ('{@code .}').
*
* @param prefix the required prefix for the system properties
* @return the resulting properties converted from the system properties; never null, but possibly empty
*/
default Configuration withSystemProperties(String prefix) {
int prefixLength = prefix.length();
return withSystemProperties(input -> {
if (input.startsWith(prefix)) {
String withoutPrefix = input.substring(prefixLength).trim();
if (withoutPrefix.length() > 0) {
// Convert to a properties format ...
return withoutPrefix.toLowerCase().replaceAll("[_]", ".");
}
}
return null;
});
}
/**
* Return a copy of this configuration except where acceptable system properties are used to overwrite properties copied from
* this configuration. Each of the system properties is examined and passed to the supplied function; if the result of the
* function is a non-null string, then a property with that string as the name and the system property value are added to the
* returned configuration.
*
* @param propertyNameConverter the function that will convert the name of each system property to an applicable property name
* (or null if the system property name does not apply); may not be null
* @return the resulting properties filtered from the input properties; never null, but possibly empty
*/
default Configuration withSystemProperties(Function<String, String> propertyNameConverter) {
Properties props = asProperties();
Properties systemProperties = System.getProperties();
for (String key : systemProperties.stringPropertyNames()) {
String propName = propertyNameConverter.apply(key);
if (propName != null && propName.length() > 0) {
String value = systemProperties.getProperty(key);
props.setProperty(propName, value);
}
}
return from(props);
}
}

View File

@ -0,0 +1,96 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.crdt;
/**
* Conflict-free Replicated Data Types (CRDT)s. Unless otherwise noted, the implementations of these interfaces are not
* thread-safe since they are expected to be used within a thread-safe process, and sent across a network to another thread-safe
* process where they will be merged together.
*
* @author Randall Hauch
*/
public final class CRDT {
/**
* Create a new CRDT counter. The operations on this counter are not threadsafe.
*
* @return the new counter; never null
*/
public static GCounter newGCounter() {
return new StateBasedGCounter();
}
/**
* Create a new CRDT counter pre-populated with the given value. The operations on this counter are not threadsafe.
*
* @param adds the number of adds
* @return the new counter; never null
*/
public static GCounter newGCounter(long adds) {
return new StateBasedGCounter(adds);
}
/**
* Create a new CRDT counter. The operations on this counter are not threadsafe.
*
* @return the new counter; never null
*/
public static PNCounter newPNCounter() {
return new StateBasedPNCounter();
}
/**
* Create a new CRDT counter pre-populated with the given values. The operations on this counter are not threadsafe.
*
* @param adds the number of adds
* @param removes the number of removes
* @return the new counter; never null
*/
public static PNCounter newPNCounter(long adds, long removes) {
return new StateBasedPNCounter(adds, removes);
}
/**
* Create a new CRDT counter that records how much the value has changed since last reset. The operations on this counter are
* not threadsafe.
*
* @return the new counter; never null
*/
public static DeltaCounter newDeltaCounter() {
return new StateBasedPNDeltaCounter();
}
/**
* Create a new CRDT counter that records how much the value has changed since last reset. The operations on this counter are
* not threadsafe.
*
* @param totalAdds the total number of adds
* @param totalRemoves the total number of removes
* @param recentAdds the recent number of adds since last {@link DeltaCounter#reset()}
* @param recentRemoves the recent number of removes since last {@link DeltaCounter#reset()}
* @return the new counter; never null
*/
public static DeltaCounter newDeltaCounter(long totalAdds, long totalRemoves, long recentAdds, long recentRemoves) {
return new StateBasedPNDeltaCounter(totalAdds, totalRemoves, recentAdds, recentRemoves);
}
/**
* Create a new CRDT counter that records how much the value has changed since last reset. The operations on this counter are
* not threadsafe.
*
* @param count the {@link DeltaCount} instance that should be used to pre-populate the new counter; may be null
* @return the new counter; never null
*/
public static DeltaCounter newDeltaCounter(DeltaCount count) {
if (count == null) return new StateBasedPNDeltaCounter();
return new StateBasedPNDeltaCounter(count.getIncrement(), count.getDecrement(),
count.getChanges().getIncrement(), count.getChanges().getDecrement());
}
private CRDT() {
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.crdt;
/**
* A read-only result of a counter. The value may or may not change depending upon how the value was obtained.
*/
public interface Count {
/**
* Get the current value.
*
* @return the current value
*/
long get();
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.crdt;
/**
* A {@link Count} that also tracks changes to the value within the last interval.
*/
public interface DeltaCount extends PNCount {
/**
* Get the changes in the current value during the last interval.
*
* @return the changes in the value during the last interval; never null
*/
PNCount getChanges();
/**
* Determine if there are any changes in this count.
* @return {@code true} if there are non-zero {@link #getChanges() changes}, or {@code false} otherwise
*/
default boolean hasChanges() {
PNCount changes = getChanges();
return changes.getIncrement() != 0 || changes.getDecrement() != 0;
}
/**
* Get the value of this count prior to the {@link #getChanges() changes}.
* @return the prior count; never null
*/
Count getPriorCount();
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.crdt;
import io.debezium.annotation.NotThreadSafe;
/**
* A simple counter that maintains a single changing value by separately tracking the positive and negative changes, and by
* tracking recent {@link #getChanges() changes} in this value since last {@link #reset() reset}. It is
* inspired by the conflict-free replicated data type (CRDT) P-N Counter. The semantics ensure the value converges toward the
* global number of increments minus the number of decrements. The global total can be calculated by {@link #merge(Count) merging}
* all the replicated instances, without regard to order of merging.
*/
@NotThreadSafe
public interface DeltaCounter extends PNCounter, DeltaCount {
/**
* Increment the counter and get the result.
*
* @return this instance so methods can be chained together; never null
*/
@Override
DeltaCounter increment();
/**
* Decrement the counter and get the result.
*
* @return this instance so methods can be chained together; never null
*/
@Override
DeltaCounter decrement();
/**
* Merge the supplied counter into this counter.
*
* @param other the other counter to merge into this one; may be null
* @return this counter so that methods can be chained
*/
@Override
DeltaCounter merge(Count other);
/**
* Start a new interval and reset the {@link #getChanges()} to initial values.
*/
void reset();
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.crdt;
/**
* A read-only result of the state of a grow-only {@link GCounter}. The value may or may not change depending upon how the value
* was obtained.
*/
public interface GCount extends Count {
/**
* Get the amount that the value incremented.
*
* @return the incremented value
*/
long getIncrement();
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.crdt;
import io.debezium.annotation.NotThreadSafe;
/**
* A simple grow-only counter that maintains a single changing value by tracking the positive changes to the value. It is
* inspired by the conflict-free replicated data type (CRDT) G Counter.
*/
@NotThreadSafe
public interface GCounter extends GCount {
/**
* Increment the counter and get the result.
*
* @return this instance so methods can be chained together; never null
*/
GCounter increment();
/**
* Increment the counter and get the result.
*
* @return the current result after incrementing
*/
long incrementAndGet();
/**
* Increment the counter and get the result.
*
* @return the current result before incrementing
*/
long getAndIncrement();
/**
* Merge the supplied counter into this counter.
*
* @param other the other counter to merge into this one; may be null
* @return this counter so that methods can be chained
*/
GCounter merge(Count other);
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.crdt;
/**
* A read-only result of the state of a {@link PNCounter}. The value may or may not change depending upon how the value was
* obtained.
*/
public interface PNCount extends GCount {
/**
* Get the current value.
*
* @return the current value
*/
@Override
default long get() {
return getIncrement() - getDecrement();
}
/**
* Get the amount that the value decremented. The {@link #get() value} is the {@link #getIncrement() total
* increments} minus the {@link #getDecrement() total decrements}
*
* @return the decremented value
*/
long getDecrement();
}

View File

@ -0,0 +1,56 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.crdt;
import io.debezium.annotation.NotThreadSafe;
/**
* A simple counter that maintains a single changing value by separately tracking the positive and negative changes. It is
* inspired by the conflict-free replicated data type (CRDT) P-N Counter. The semantics ensure the value converges toward the
* global number of increments minus the number of decrements. The global total can be calculated by {@link #merge(Count) merging}
* all the replicated instances, without regard to order of merging.
*/
@NotThreadSafe
public interface PNCounter extends PNCount, GCounter {
/**
* Increment the counter and get the result.
*
* @return this instance so methods can be chained together; never null
*/
@Override
PNCounter increment();
/**
* Decrement the counter and get the result.
*
* @return this instance so methods can be chained together; never null
*/
PNCounter decrement();
/**
* Decrement the counter and get the result.
*
* @return the current result after decrementing
*/
long decrementAndGet();
/**
* Decrement the counter and get the result.
*
* @return the current result before decrementing
*/
long getAndDecrement();
/**
* Merge the supplied counter into this counter.
*
* @param other the other counter to merge into this one; may be null
* @return this counter so that methods can be chained
*/
@Override
PNCounter merge(Count other);
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.crdt;
import io.debezium.annotation.NotThreadSafe;
@NotThreadSafe
class StateBasedGCounter implements GCounter {
private long adds;
protected StateBasedGCounter() {
this(0L);
}
protected StateBasedGCounter(long adds) {
this.adds = adds;
}
@Override
public GCounter increment() {
++adds;
return this;
}
@Override
public long incrementAndGet() {
return ++adds;
}
@Override
public long getAndIncrement() {
return adds++;
}
@Override
public long get() {
return adds;
}
@Override
public long getIncrement() {
return adds;
}
@Override
public GCounter merge(Count other) {
if (other instanceof GCount) {
GCount changes = (GCount)other;
this.adds += changes.getIncrement();
} else if (other instanceof Count) {
Count changes = other;
this.adds += changes.get();
}
return this;
}
@Override
public String toString() {
return "+" + adds;
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.crdt;
import io.debezium.annotation.NotThreadSafe;
@NotThreadSafe
class StateBasedPNCounter implements PNCounter {
private long adds;
private long removes;
protected StateBasedPNCounter() {
this(0L, 0L);
}
protected StateBasedPNCounter(long adds, long removes) {
this.adds = adds;
this.removes = removes;
}
@Override
public PNCounter increment() {
++adds;
return this;
}
@Override
public PNCounter decrement() {
++removes;
return this;
}
@Override
public long incrementAndGet() {
return ++adds - removes;
}
@Override
public long decrementAndGet() {
return adds - ++removes;
}
@Override
public long getAndIncrement() {
return adds++ - removes;
}
@Override
public long getAndDecrement() {
return adds - removes++;
}
@Override
public long get() {
return adds - removes;
}
@Override
public long getIncrement() {
return adds;
}
@Override
public long getDecrement() {
return removes;
}
@Override
public PNCounter merge(Count other) {
if (other instanceof PNCount) {
PNCount changes = (PNCount)other;
this.adds += changes.getIncrement();
this.removes += changes.getDecrement();
} else if (other instanceof GCount) {
GCount changes = (GCount)other;
this.adds += changes.getIncrement();
} else if (other instanceof Count) {
Count changes = other;
this.adds += changes.get();
}
return this;
}
@Override
public String toString() {
return "+" + adds + " -" + removes;
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.crdt;
import io.debezium.annotation.NotThreadSafe;
@NotThreadSafe class StateBasedPNDeltaCounter extends StateBasedPNCounter implements DeltaCounter {
private PNCounter delta;
protected StateBasedPNDeltaCounter() {
this(0L, 0L, 0L, 0L);
}
protected StateBasedPNDeltaCounter(long totalAdds, long totalRemoves, long recentAdds, long recentRemoves) {
super(totalAdds, totalRemoves);
delta = new StateBasedPNCounter(recentAdds, recentRemoves);
}
@Override
public DeltaCounter increment() {
super.increment();
delta.increment();
return this;
}
@Override
public DeltaCounter decrement() {
super.decrement();
delta.decrement();
return this;
}
@Override
public long incrementAndGet() {
delta.incrementAndGet();
return super.incrementAndGet();
}
@Override
public long decrementAndGet() {
delta.decrementAndGet();
return super.decrementAndGet();
}
@Override
public long getAndIncrement() {
delta.getAndIncrement();
return super.getAndIncrement();
}
@Override
public long getAndDecrement() {
delta.getAndDecrement();
return super.getAndDecrement();
}
@Override
public PNCount getChanges() {
return delta;
}
@Override
public boolean hasChanges() {
return delta.getIncrement() != 0 || delta.getDecrement() != 0;
}
@Override
public Count getPriorCount() {
long value = super.get() - delta.get();
return new Count() {
@Override
public long get() {
return value;
}
};
}
@Override
public void reset() {
delta = new StateBasedPNCounter();
}
@Override
public DeltaCounter merge(Count other) {
if (other instanceof DeltaCount) {
// Just merge in the *changes* ...
DeltaCount that = (DeltaCount) other;
this.delta.merge(that.getChanges());
super.merge(that.getChanges());
} else {
super.merge(other);
}
return this;
}
@Override
public String toString() {
return super.toString() + " (changes " + this.delta + ")";
}
}

View File

@ -0,0 +1,11 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.function;
@FunctionalInterface
public interface Callable {
void call();
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.function;
import java.util.function.Predicate;
/**
* @author Randall Hauch
*
*/
public class Predicates {
public static <T> Predicate<T> notNull() {
return new Predicate<T>() {
@Override
public boolean test(T t) {
return t != null;
}
};
}
private Predicates() {
}
}

View File

@ -0,0 +1,275 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.jdbc;
import java.util.Properties;
import java.util.Set;
import java.util.function.Predicate;
import io.debezium.annotation.Immutable;
import io.debezium.config.Configuration;
import io.debezium.util.Collect;
/**
* A specialized configuration for the Debezium driver.
*
* @author Randall Hauch
*/
@Immutable
public interface JdbcConfiguration extends Configuration {
public static interface Builder extends Configuration.ConfigBuilder<JdbcConfiguration, Builder> {
default Builder withUser(String username) {
return with(Field.USER, username);
}
default Builder withPassword(String password) {
return with(Field.PASSWORD, password);
}
default Builder withHostname(String hostname) {
return with(Field.HOSTNAME, hostname);
}
default Builder withDatabase(String databaseName) {
return with(Field.DATABASE, databaseName);
}
default Builder withPort(int port) {
return with(Field.PORT, port);
}
}
/**
* Create a new {@link Builder configuration builder} that starts with a copy of the supplied configuration.
*
* @param config the configuration to copy
* @return the configuration builder
*/
public static Builder copy(Configuration config) {
return new Builder() {
private Properties props = config.asProperties();
@Override
public Builder with(String key, String value) {
props.setProperty(key, value);
return this;
}
@Override
public JdbcConfiguration build() {
return JdbcConfiguration.adapt(Configuration.from(props));
}
@Override
public String toString() {
return props.toString();
}
};
}
/**
* Create a new {@link Builder configuration builder} that starts with an empty configuration.
*
* @return the configuration builder
*/
public static Builder create() {
return new Builder() {
private Properties props = new Properties();
@Override
public Builder with(String key, String value) {
props.setProperty(key, value);
return this;
}
@Override
public JdbcConfiguration build() {
return JdbcConfiguration.adapt(Configuration.from(props));
}
@Override
public String toString() {
return props.toString();
}
};
}
/**
* The pre-defined fields for JDBC configurations.
*/
public static interface Field {
public static final String USER = "user";
public static final String PASSWORD = "password";
public static final String DATABASE = "dbname";
public static final String HOSTNAME = "hostname";
public static final String PORT = "port";
}
/**
* The set of pre-defined fields for JDBC configurations.
*/
public static Set<String> ALL_KNOWN_FIELDS = Collect.unmodifiableSet(Field.DATABASE, Field.USER, Field.PASSWORD, Field.HOSTNAME,
Field.PORT);
/**
* Get a predicate that determines if supplied keys are {@link Field pre-defined field names}.
*
* @return the predicate; never null
*/
default Predicate<String> knownFieldNames() {
return ALL_KNOWN_FIELDS::contains;
}
/**
* Get a view of this configuration that does not contain the {@link #knownFieldNames() known fields}.
*
* @return the filtered view of this configuration; never null
*/
default Configuration withoutKnownFields() {
return filter(knownFieldNames().negate());
}
/**
* Obtain a {@link JdbcConfiguration} adapter for the given {@link Configuration}.
*
* @param config the configuration; may not be null
* @return the ClientConfiguration; never null
*/
public static JdbcConfiguration adapt(Configuration config) {
if (config instanceof JdbcConfiguration) return (JdbcConfiguration) config;
return new JdbcConfiguration() {
@Override
public Set<String> keys() {
return config.keys();
}
@Override
public String getString(String key) {
return config.getString(key);
}
@Override
public String toString() {
return config.toString();
}
};
}
/**
* Get the hostname property from the configuration.
*
* @return the host name, or null if there is none.
*/
default String getHostname() {
return getHostname(null);
}
/**
* Get the hostname property from the configuration.
*
* @param defaultValue the value to use by default
* @return the host name, or the {@code defaultValue} if there is no such property
*/
default String getHostname(String defaultValue) {
return getString(Field.HOSTNAME, defaultValue);
}
/**
* Get the port property from the configuration.
*
* @return the port number, or null if there is none.
*/
default String getPortAsString() {
return getPort(null);
}
/**
* Get the port property from the configuration.
*
* @param defaultValue the value to use by default
* @return the port, or the {@code defaultValue} if there is no such property
*/
default String getPort(String defaultValue) {
return getString(Field.PORT, defaultValue);
}
/**
* Get the port property from the configuration.
*
* @return the port number, or null if there is none.
*/
default Integer getPort() {
return getInteger(Field.PORT);
}
/**
* Get the port property from the configuration.
*
* @param defaultValue the value to use by default
* @return the port, or the {@code defaultValue} if there is no such property
*/
default int getPort(int defaultValue) {
return getInteger(Field.PORT, defaultValue);
}
/**
* Get the database name property from the configuration.
*
* @return the database name, or null if there is none.
*/
default String getDatabase() {
return getDatabase(null);
}
/**
* Get the database name property from the configuration.
*
* @param defaultValue the value to use by default
* @return the database name, or the {@code defaultValue} if there is no such property
*/
default String getDatabase(String defaultValue) {
return getString(Field.DATABASE, defaultValue);
}
/**
* Get the user property from the configuration.
*
* @return the username, or null if there is none.
*/
default String getUser() {
return getUser(null);
}
/**
* Get the user property from the configuration.
*
* @param defaultValue the value to use by default
* @return the username, or the {@code defaultValue} if there is no such property
*/
default String getUser(String defaultValue) {
return getString(Field.USER, defaultValue);
}
/**
* Get the password property from the configuration.
*
* @return the password value, or null if there is none.
*/
default String getPassword() {
return getPassword(null);
}
/**
* Get the password property from the configuration.
*
* @param defaultValue the value to use by default
* @return the password, or the {@code defaultValue} if there is no such property
*/
default String getPassword(String defaultValue) {
return getString(Field.PASSWORD, defaultValue);
}
}

View File

@ -0,0 +1,286 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.debezium.annotation.ThreadSafe;
import io.debezium.config.Configuration;
import io.debezium.util.Strings;
/**
* A utility that simplifies using a JDBC connection and executing transactions composed of multiple statements.
*
* @author Randall Hauch
*/
public class JdbcConnection implements AutoCloseable {
private final static Logger LOGGER = LoggerFactory.getLogger(JdbcConnection.class);
/**
* Establishes JDBC connections.
*/
@FunctionalInterface
@ThreadSafe
public static interface ConnectionFactory {
/**
* Establish a connection to the database denoted by the given configuration.
*
* @param config the configuration with JDBC connection information
* @return the JDBC connection; may not be null
* @throws SQLException if there is an error connecting to the database
*/
Connection connect(JdbcConfiguration config) throws SQLException;
}
/**
* Defines multiple JDBC operations.
*/
@FunctionalInterface
public static interface Operations {
/**
* Apply a series of operations against the given JDBC statement.
*
* @param statement the JDBC statement to use to execute one or more operations
* @throws SQLException if there is an error connecting to the database or executing the statements
*/
void apply(Statement statement) throws SQLException;
}
/**
* Create a {@link ConnectionFactory} that replaces variables in the supplied URL pattern. Variables include:
* <ul>
* <li><code>${hostname}</code></li>
* <li><code>${port}</code></li>
* <li><code>${dbname}</code></li>
* <li><code>${username}</code></li>
* <li><code>${password}</code></li>
* </ul>
*
* @param urlPattern the URL pattern string; may not be null
* @return the connection factory
*/
protected static ConnectionFactory patternBasedFactory(String urlPattern) {
return (config) -> {
LOGGER.trace("Config: {}", config.asProperties());
Properties props = config.asProperties();
String url = findAndReplace(urlPattern, props,
JdbcConfiguration.Field.HOSTNAME,
JdbcConfiguration.Field.PORT,
JdbcConfiguration.Field.USER,
JdbcConfiguration.Field.PASSWORD,
JdbcConfiguration.Field.DATABASE);
LOGGER.trace("Props: {}", props);
LOGGER.trace("URL: {}", url);
Connection conn = DriverManager.getConnection(url, props);
LOGGER.debug("Connected to {} with {}", url, props);
return conn;
};
}
private static String findAndReplace(String url, Properties props, String... variableNames) {
for (String variable : variableNames) {
if (url.contains("${" + variable + "}")) {
// Otherwise, we have to remove it from the properties ...
String value = props.getProperty(variable);
props.remove(variable);
// And replace the variable ...
url = url.replaceAll("\\$\\{" + variable + "\\}", value);
}
}
return url;
}
private final Configuration config;
private final ConnectionFactory factory;
private final Operations initialOps;
private volatile Connection conn;
/**
* Create a new instance with the given configuration and connection factory.
*
* @param config the configuration; may not be null
* @param connectionFactory the connection factory; may not be null
*/
public JdbcConnection(Configuration config, ConnectionFactory connectionFactory) {
this(config, connectionFactory, null);
}
/**
* Create a new instance with the given configuration and connection factory, and specify the operations that should be
* run against each newly-established connection.
*
* @param config the configuration; may not be null
* @param connectionFactory the connection factory; may not be null
* @param initialOperations the initial operations that should be run on each new connection; may be null
*/
public JdbcConnection(Configuration config, ConnectionFactory connectionFactory, Operations initialOperations) {
this.config = config;
this.factory = connectionFactory;
this.initialOps = initialOperations;
this.conn = null;
}
/**
* Ensure a connection to the database is established.
*
* @return this object for chaining methods together
* @throws SQLException if there is an error connecting to the database
*/
public JdbcConnection connect() throws SQLException {
connection();
return this;
}
/**
* Execute a series of SQL statements as a single transaction.
*
* @param sqlStatements the SQL statements that are to be performed as a single transaction
* @return this object for chaining methods together
* @throws SQLException if there is an error connecting to the database or executing the statements
* @see #execute(Operations)
*/
public JdbcConnection execute(String... sqlStatements) throws SQLException {
return execute(statement -> {
for (String sqlStatement : sqlStatements) {
if (sqlStatement != null) statement.execute(sqlStatement);
}
});
}
/**
* Execute a series of operations as a single transaction.
*
* @param operations the function that will be called with a newly-created {@link Statement}, and that performs
* one or more operations on that statement object
* @return this object for chaining methods together
* @throws SQLException if there is an error connecting to the database or executing the statements
*/
public JdbcConnection execute(Operations operations) throws SQLException {
Connection conn = connection();
conn.setAutoCommit(false);
try (Statement statement = conn.createStatement();) {
operations.apply(statement);
conn.commit();
}
return this;
}
/**
* Execute a SQL query.
*
* @param query the SQL query
* @param resultConsumer the consumer of the query results
* @return this object for chaining methods together
* @throws SQLException if there is an error connecting to the database or executing the statements
* @see #execute(Operations)
*/
public JdbcConnection query(String query, Consumer<ResultSet> resultConsumer) throws SQLException {
Connection conn = connection();
conn.setAutoCommit(false);
try (Statement statement = conn.createStatement();) {
ResultSet resultSet = statement.executeQuery(query);
if (resultConsumer != null) resultConsumer.accept(resultSet);
}
return this;
}
public void print(ResultSet resultSet ) {
// CHECKSTYLE:OFF
print(resultSet,System.out::println);
// CHECKSTYLE:ON
}
public void print(ResultSet resultSet, Consumer<String> lines ) {
try {
ResultSetMetaData rsmd = resultSet.getMetaData();
int columnCount = rsmd.getColumnCount();
int[] columnSizes = findMaxLength(resultSet);
lines.accept(delimiter(columnCount, columnSizes));
StringBuilder sb = new StringBuilder();
for ( int i=1; i<=columnCount; i++ ) {
if (i > 1) sb.append(" | ");
sb.append(Strings.setLength(rsmd.getColumnLabel(i),columnSizes[i],' '));
}
lines.accept(sb.toString());
sb.setLength(0);
lines.accept(delimiter(columnCount, columnSizes));
while (resultSet.next()) {
sb.setLength(0);
for (int i = 1; i <= columnCount; i++) {
if (i > 1) sb.append(" | ");
sb.append(Strings.setLength(resultSet.getString(i),columnSizes[i],' '));
}
lines.accept(sb.toString());
sb.setLength(0);
}
lines.accept(delimiter(columnCount, columnSizes));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private String delimiter( int columnCount, int[] columnSizes ) {
StringBuilder sb = new StringBuilder();
for ( int i=1; i<=columnCount; i++ ) {
if (i > 1) sb.append("---");
sb.append(Strings.createString('-',columnSizes[i]));
}
return sb.toString();
}
private int[] findMaxLength( ResultSet resultSet ) throws SQLException {
ResultSetMetaData rsmd = resultSet.getMetaData();
int columnCount = rsmd.getColumnCount();
int[] columnSizes = new int[columnCount+1];
for ( int i=1; i<=columnCount; i++ ) {
columnSizes[i] = Math.max(columnSizes[i], rsmd.getColumnLabel(i).length());
}
while (resultSet.next()) {
for (int i = 1; i <= columnCount; i++) {
String value = resultSet.getString(i);
if ( value != null ) columnSizes[i] = Math.max(columnSizes[i], value.length());
}
}
resultSet.beforeFirst();
return columnSizes;
}
protected synchronized Connection connection() throws SQLException {
if (conn == null) {
conn = factory.connect(JdbcConfiguration.adapt(config));
if (conn == null) throw new SQLException("Unable to obtain a JDBC connection");
// Always run the initial operations on this new connection
if (initialOps != null) execute(initialOps);
}
return conn;
}
/**
* Close the connection and release any resources.
*/
@Override
public synchronized void close() throws SQLException {
if (conn != null) {
try {
conn.close();
} finally {
conn = null;
}
}
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.util;
/**
* @author Randall Hauch
*
*/
public interface Clock {
public static final Clock SYSTEM = new Clock() {
@Override
public long currentTimeInMillis() {
return System.currentTimeMillis();
}
@Override
public long currentTimeInNanos() {
return System.nanoTime();
}
};
public static Clock system() {
return SYSTEM;
}
public long currentTimeInNanos();
public long currentTimeInMillis();
}

View File

@ -0,0 +1,116 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A set of utilities for more easily creating various kinds of collections.
*/
public class Collect {
/**
* Create a fixed sized Map that removes the least-recently used entry when the map becomes too large. The supplied
* {@code maximumNumberOfEntries} should be a power of 2 to efficiently make efficient use of memory. If not, the resulting
* map will be able to contain no more than {@code maximumNumberOfEntries} entries, but the underlying map will have a
* capacity that is the next power of larger than the supplied {@code maximumNumberOfEntries} value so that it can hold
* the required number of entries.
*
* @param maximumNumberOfEntries the maximum number of entries allowed in the map; should be a power of 2
* @return the map that is limited in size by the specified number of entries; never null
*/
public static <K, V> Map<K, V> fixedSizeMap(int maximumNumberOfEntries) {
return new LinkedHashMap<K, V>(maximumNumberOfEntries + 1, .75F, true) { // throws illegal argument if < 0
private static final long serialVersionUID = 1L;
final int evictionSize = maximumNumberOfEntries - 1;
@Override
public boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > evictionSize;
}
};
}
public static <T> Set<T> unmodifiableSet(@SuppressWarnings("unchecked") T... values) {
return unmodifiableSet(arrayListOf(values));
}
public static <T> Set<T> unmodifiableSet(Collection<T> values) {
return Collections.unmodifiableSet(new HashSet<T>(values));
}
public static <T> Set<T> unmodifiableSet(Set<T> values) {
return Collections.unmodifiableSet(values);
}
public static <T> List<T> arrayListOf(T[] values) {
List<T> result = new ArrayList<>();
for (T value : values) {
if (value != null) result.add(value);
}
return result;
}
public static <T> List<T> arrayListOf(T first, @SuppressWarnings("unchecked") T... additional) {
List<T> result = new ArrayList<>();
result.add(first);
for (T another : additional) {
if (another != null) result.add(another);
}
return result;
}
public static <T> List<T> arrayListOf(Iterable<T> values) {
List<T> result = new ArrayList<>();
values.forEach((value) -> result.add(value));
return result;
}
public static <K, V> Map<K, V> mapOf(K key, V value) {
return Collections.singletonMap(key, value);
}
public static <K, V> Map<K, V> hashMapOf(K key, V value) {
Map<K, V> map = new HashMap<>();
map.put(key, value);
return map;
}
public static <K, V> Map<K, V> hashMapOf(K key1, V value1, K key2, V value2) {
Map<K, V> map = new HashMap<>();
map.put(key1, value1);
map.put(key2, value2);
return map;
}
public static <K, V> Map<K, V> hashMapOf(K key1, V value1, K key2, V value2, K key3, V value3) {
Map<K, V> map = new HashMap<>();
map.put(key1, value1);
map.put(key2, value2);
map.put(key3, value3);
return map;
}
public static <K, V> Map<K, V> hashMapOf(K key1, V value1, K key2, V value2, K key3, V value3, K key4, V value4) {
Map<K, V> map = new HashMap<>();
map.put(key1, value1);
map.put(key2, value2);
map.put(key3, value3);
map.put(key4, value4);
return map;
}
private Collect() {
}
}

View File

@ -0,0 +1,319 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import io.debezium.annotation.Immutable;
/**
* Utility for parsing and accessing the command line options and parameters.
*
* @author Randall Hauch
*/
@Immutable
public class CommandLineOptions {
/**
* Parse the array of arguments passed to a Java {@code main} method and create a {@link CommandLineOptions} instance.
*
* @param args the {@code main} method's parameters; may not be null
* @return the representation of the command line options and parameters; never null
*/
public static CommandLineOptions parse(String[] args) {
Map<String, String> options = new HashMap<>();
List<String> params = new ArrayList<>();
List<String> optionNames = new ArrayList<>();
String optionName = null;
for (String value : args) {
value = value.trim();
if (value.startsWith("-")) {
optionName = value;
options.put(optionName, "true");
optionNames.add(optionName);
} else {
if (optionName != null)
options.put(optionName, value);
else
params.add(value);
}
}
return new CommandLineOptions(options, params, optionNames);
}
private final Map<String, String> options;
private final List<String> params;
private final List<String> orderedOptionNames;
private CommandLineOptions(Map<String, String> options, List<String> params, List<String> orderedOptionNames) {
this.options = options;
this.params = params;
this.orderedOptionNames = orderedOptionNames;
}
/**
* Determine if the option with the given name was used on the command line.
* <p>
* The supplied option name is trimmed before matching against the command line arguments. Note that any special characters
* such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
*
* @param name the name for the option (e.g., "-v" or "--verbose")
* @return true if the exact option was used, or false otherwise or if the supplied option name is null
* @see #hasOption(String, String)
*/
public boolean hasOption(String name) {
return hasOption(name, null);
}
/**
* Determine if the option with one of the given names was used on the command line.
* <p>
* The supplied option name and alternative names are trimmed before matching against the command line arguments. Note that
* any special characters such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
*
* @param name the name for the option (e.g., "-v" or "--verbose")
* @param alternativeName an alternative name for the option; may be null
* @return true if the exact option was used, or false otherwise or if both the supplied option name and alternative name are
* null
* @see #hasOption(String)
*/
public boolean hasOption(String name, String alternativeName) {
return getOption(name, alternativeName, null) != null;
}
/**
* Obtain the value associated with the named option.
* <p>
* The supplied option name is trimmed before matching against the command line arguments. Note that any special characters
* such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
*
* @param name the name for the option (e.g., "-v" or "--verbose")
* @return the value associated with the option, or null if no option was found or the name is null
* @see #getOption(String, String)
* @see #getOption(String, String, String)
*/
public String getOption(String name) {
return getOption(name, null);
}
/**
* Obtain the value associated with the named option, using the supplied default value if none is found.
* <p>
* The supplied option name is trimmed before matching against the command line arguments. Note that any special characters
* such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
*
* @param name the name for the option (e.g., "-v" or "--verbose")
* @param defaultValue the value that should be returned no named option was found
* @return the value associated with the option, or the default value if none was found or the name is null
* @see #getOption(String)
* @see #getOption(String, String, String)
*/
public String getOption(String name, String defaultValue) {
return getOption(name, null, defaultValue);
}
/**
* Obtain the value associated with the option given the name and alternative name of the option, using the supplied default
* value if none is found.
* <p>
* The supplied option name and alternative names are trimmed before matching against the command line arguments. Note that
* any special characters such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
*
* @param name the name for the option (e.g., "-v" or "--verbose")
* @param alternativeName an alternative name for the option; may be null
* @param defaultValue the value that should be returned no named option was found
* @return the value associated with the option, or the default value if none was found or if both the name and alternative
* name are null
* @see #getOption(String)
* @see #getOption(String, String)
*/
public String getOption(String name, String alternativeName, String defaultValue) {
recordOptionUsed(name, alternativeName);
String result = options.get(name.trim());
if (result == null && alternativeName != null) result = options.get(alternativeName.trim());
return result != null ? result : defaultValue;
}
protected void recordOptionUsed(String name, String alternativeName) {
orderedOptionNames.remove(name.trim());
if (alternativeName != null) orderedOptionNames.remove(alternativeName.trim());
}
/**
* Obtain the long value associated with the named option, using the supplied default value if none is found or if the value
* cannot be parsed as a long value.
* <p>
* The supplied option name is trimmed before matching against the command line arguments. Note that any special characters
* such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
*
* @param name the name for the option (e.g., "-v" or "--verbose")
* @param defaultValue the value that should be returned no named option was found
* @return the value associated with the option, or the default value if none was found or the name is null
* @see #getOption(String, String, long)
*/
public long getOption(String name, long defaultValue) {
return getOption(name, null, defaultValue);
}
/**
* Obtain the long value associated with the option given the name and alternative name of the option, using the supplied
* default value if none is found or if the value cannot be parsed as a long value.
* <p>
* The supplied option name and alternative names are trimmed before matching against the command line arguments. Note that
* any special characters such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
*
* @param name the name for the option (e.g., "-v" or "--verbose")
* @param alternativeName an alternative name for the option; may be null
* @param defaultValue the value that should be returned no named option was found
* @return the value associated with the option, or the default value if none was found or if both the name and alternative
* name are null
* @see #getOption(String, long)
*/
public long getOption(String name, String alternativeName, long defaultValue) {
return Strings.asLong(getOption(name, alternativeName, null), defaultValue);
}
/**
* Obtain the integer value associated with the named option, using the supplied default value if none is found or if the
* value
* cannot be parsed as an integer value.
* <p>
* The supplied option name is trimmed before matching against the command line arguments. Note that any special characters
* such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
*
* @param name the name for the option (e.g., "-v" or "--verbose")
* @param defaultValue the value that should be returned no named option was found
* @return the value associated with the option, or the default value if none was found or the name is null
* @see #getOption(String, String, int)
*/
public int getOption(String name, int defaultValue) {
return getOption(name, null, defaultValue);
}
/**
* Obtain the integer value associated with the option given the name and alternative name of the option, using the supplied
* default value if none is found or if the value cannot be parsed as an integer value.
* <p>
* The supplied option name and alternative names are trimmed before matching against the command line arguments. Note that
* any special characters such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
*
* @param name the name for the option (e.g., "-v" or "--verbose")
* @param alternativeName an alternative name for the option; may be null
* @param defaultValue the value that should be returned no named option was found
* @return the value associated with the option, or the default value if none was found or if both the name and alternative
* name are null
* @see #getOption(String, int)
*/
public int getOption(String name, String alternativeName, int defaultValue) {
return Strings.asInt(getOption(name, alternativeName, null), defaultValue);
}
/**
* Obtain the boolean value associated with the named option, using the supplied default value if none is found or if the
* value
* cannot be parsed as a boolean value.
* <p>
* The supplied option name is trimmed before matching against the command line arguments. Note that any special characters
* such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
*
* @param name the name for the option (e.g., "-v" or "--verbose")
* @param defaultValue the value that should be returned no named option was found
* @return the value associated with the option, or the default value if none was found or the name is null
* @see #getOption(String, String, boolean)
*/
public boolean getOption(String name, boolean defaultValue) {
return getOption(name, null, defaultValue);
}
/**
* Obtain the boolean value associated with the option given the name and alternative name of the option, using the supplied
* default value if none is found or if the value cannot be parsed as a boolean value.
* <p>
* The supplied option name and alternative names are trimmed before matching against the command line arguments. Note that
* any special characters such as prefixes (e.g., '{@code -}' or '{@code --}') must be included in the name.
*
* @param name the name for the option (e.g., "-v" or "--verbose")
* @param alternativeName an alternative name for the option; may be null
* @param defaultValue the value that should be returned no named option was found
* @return the value associated with the option, or the default value if none was found or if both the name and alternative
* name are null
* @see #getOption(String, boolean)
*/
public boolean getOption(String name, String alternativeName, boolean defaultValue) {
return Strings.asBoolean(getOption(name, alternativeName, null), defaultValue);
}
/**
* Obtain the parameter at the given index. Parameters are those arguments that are not preceded by an option name.
*
* @param index the index of the parameter
* @return the parameter value, or null if there was no parameter at the given index
*/
public String getParameter(int index) {
return index < 0 || index >= params.size() ? null : params.get(index);
}
/**
* Determine whether the specified value matches one of the parameters.
*
* @param value the parameter value to match
* @return true if one of the parameter matches the value, or false otherwise
*/
public boolean hasParameter(String value) {
return value == null || params.isEmpty() ? false : params.contains(value);
}
/**
* Determine whether there were any unknown option names after all possible options have been checked via one of the
* {@code getOption(String,...)} methods.
*
* @return true if there was at least one option (e.g., beginning with '{@code -}') that was not checked, or false
* if there were no unknown options on the command line
* @see #getFirstUnknownOptionName()
* @see #hasUnknowns()
*/
public boolean hasUnknowns() {
return !orderedOptionNames.isEmpty();
}
/**
* Get the list of unknown option names after all possible options have been checked via one of the
* {@code getOption(String,...)} methods.
*
* @return the list of options (e.g., beginning with '{@code -}') that were not checked; never null but possible empty
* @see #getFirstUnknownOptionName()
* @see #hasUnknowns()
*/
public List<String> getUnknownOptionNames() {
return Collections.unmodifiableList(orderedOptionNames);
}
/**
* If there were {@link #hasUnknowns() unknown options}, return the first one that appeared on the command line.
* @return the first unknown option, or null if there were no {@link #hasUnknowns() unknown options}
* @see #getUnknownOptionNames()
* @see #getFirstUnknownOptionName()
*/
public String getFirstUnknownOptionName() {
return orderedOptionNames.isEmpty() ? null : orderedOptionNames.get(0);
}
@Override
public String toString() {
StringJoiner joiner = new StringJoiner(" ");
options.forEach((opt, val) -> {
joiner.add("-" + opt).add(val);
});
params.forEach(joiner::add);
return joiner.toString();
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.util;
import java.util.Arrays;
import io.debezium.annotation.Immutable;
/**
* Utilities for easily computing hash codes. The algorithm should generally produce good distributions for use in hash-based
* containers or collections, but as expected does always result in repeatable hash codes given the inputs.
*/
@Immutable
public class HashCode {
// Prime number used in improving distribution
private static final int PRIME = 103;
/**
* Compute a combined hash code from the supplied objects. This method always returns 0 if no objects are supplied.
*
* @param objects the objects that should be used to compute the hash code
* @return the hash code
*/
public static int compute(Object... objects) {
return _compute(0, objects);
}
/**
* Compute a combined hash code from the supplied objects using the supplied seed.
*
* @param seed a value upon which the hash code will be based; may be 0
* @param objects the objects that should be used to compute the hash code
* @return the hash code
*/
private static int _compute(int seed,
Object... objects) {
if (objects == null || objects.length == 0) {
return seed * HashCode.PRIME;
}
// Compute the hash code for all of the objects ...
int hc = seed;
for (Object object : objects) {
hc = HashCode.PRIME * hc;
if (object instanceof byte[]) {
hc += Arrays.hashCode((byte[]) object);
} else if (object instanceof boolean[]) {
hc += Arrays.hashCode((boolean[]) object);
} else if (object instanceof short[]) {
hc += Arrays.hashCode((short[]) object);
} else if (object instanceof int[]) {
hc += Arrays.hashCode((int[]) object);
} else if (object instanceof long[]) {
hc += Arrays.hashCode((long[]) object);
} else if (object instanceof float[]) {
hc += Arrays.hashCode((float[]) object);
} else if (object instanceof double[]) {
hc += Arrays.hashCode((double[]) object);
} else if (object instanceof char[]) {
hc += Arrays.hashCode((char[]) object);
} else if (object instanceof Object[]) {
hc += Arrays.hashCode((Object[]) object);
} else if (object != null) {
hc += object.hashCode();
}
}
return hc;
}
private HashCode() {
}
}

View File

@ -0,0 +1,428 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.util;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.URL;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;
import java.util.Set;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.debezium.annotation.Immutable;
/**
* A set of utilities for more easily performing I/O.
*/
@Immutable
public class IoUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(IoUtil.class);
/**
* Read and return the entire contents of the supplied {@link InputStream stream}. This method always closes the stream when
* finished reading.
*
* @param stream the stream to the contents; may be null
* @return the contents, or an empty byte array if the supplied reader is null
* @throws IOException if there is an error reading the content
*/
public static byte[] readBytes(InputStream stream) throws IOException {
if (stream == null) return new byte[] {};
byte[] buffer = new byte[1024];
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
int numRead = 0;
while ((numRead = stream.read(buffer)) > -1) {
output.write(buffer, 0, numRead);
}
output.flush();
return output.toByteArray();
}
}
/**
* Read and return the entire contents of the supplied {@link File file}.
*
* @param file the file containing the contents; may be null
* @return the contents, or an empty byte array if the supplied file is null
* @throws IOException if there is an error reading the content
*/
public static byte[] readBytes(File file) throws IOException {
if (file == null) return new byte[] {};
try (InputStream stream = new BufferedInputStream(new FileInputStream(file))) {
return readBytes(stream);
}
}
/**
* Read the lines from the content of the resource file at the given path on the classpath.
*
* @param resourcePath the logical path to the classpath, file, or URL resource
* @param classLoader the classloader that should be used to load the resource as a stream; may be null
* @param clazz the class that should be used to load the resource as a stream; may be null
* @param lineProcessor the function that this method calls for each line read from the supplied stream; may not be null
* @throws IOException if an I/O error occurs
*/
public static void readLines(String resourcePath, ClassLoader classLoader, Class<?> clazz, Consumer<String> lineProcessor)
throws IOException {
try (InputStream stream = IoUtil.getResourceAsStream(resourcePath, classLoader, clazz, null, null)) {
IoUtil.readLines(stream, lineProcessor);
}
}
/**
* Read the lines from the supplied stream. This function completely reads the stream and therefore closes the stream.
*
* @param stream the stream with the contents to be read; may not be null
* @param lineProcessor the function that this method calls for each line read from the supplied stream; may not be null
* @throws IOException if an I/O error occurs
*/
public static void readLines(InputStream stream, Consumer<String> lineProcessor) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
String line = null;
while ((line = reader.readLine()) != null) {
lineProcessor.accept(line);
}
}
}
/**
* Read the lines from the supplied stream. This function completely reads the stream and therefore closes the stream.
*
* @param path path to the file with the contents to be read; may not be null
* @param lineProcessor the function that this method calls for each line read from the supplied stream; may not be null
* @throws IOException if an I/O error occurs
*/
public static void readLines(Path path, Consumer<String> lineProcessor) throws IOException {
Files.lines(path).forEach(lineProcessor);
}
/**
* Read and return the entire contents of the supplied {@link Reader}. This method always closes the reader when finished
* reading.
*
* @param reader the reader of the contents; may be null
* @return the contents, or an empty string if the supplied reader is null
* @throws IOException if there is an error reading the content
*/
public static String read(Reader reader) throws IOException {
if (reader == null) return "";
StringBuilder sb = new StringBuilder();
try (Reader r = reader) {
int numRead = 0;
char[] buffer = new char[1024];
while ((numRead = reader.read(buffer)) > -1) {
sb.append(buffer, 0, numRead);
}
}
return sb.toString();
}
/**
* Read and return the entire contents of the supplied {@link InputStream}. This method always closes the stream when finished
* reading.
*
* @param stream the streamed contents; may be null
* @return the contents, or an empty string if the supplied stream is null
* @throws IOException if there is an error reading the content
*/
public static String read(InputStream stream) throws IOException {
return stream == null ? "" : read(new InputStreamReader(stream));
}
/**
* Read and return the entire contents of the supplied {@link InputStream}. This method always closes the stream when finished
* reading.
*
* @param stream the streamed contents; may be null
* @param charset charset of the stream data; may not be null
* @return the contents, or an empty string if the supplied stream is null
* @throws IOException if there is an error reading the content
*/
public static String read(InputStream stream,
String charset) throws IOException {
return stream == null ? "" : read(new InputStreamReader(stream, charset));
}
/**
* Read and return the entire contents of the supplied {@link File}.
*
* @param file the file containing the information to be read; may be null
* @return the contents, or an empty string if the supplied reader is null
* @throws IOException if there is an error reading the content
*/
public static String read(File file) throws IOException {
if (file == null) return "";
StringBuilder sb = new StringBuilder();
try (Reader reader = new FileReader(file)) {
int numRead = 0;
char[] buffer = new char[1024];
while ((numRead = reader.read(buffer)) > -1) {
sb.append(buffer, 0, numRead);
}
}
return sb.toString();
}
/**
* Get the {@link InputStream input stream} to the resource given by the supplied path. This method performs these operations
* in order, returning as soon as a file is found:
* <ol>
* <li>look for a file on the file system at the given absolute path; otherwise</li>
* <li>look for a file on the file system at the given path relative to the JVM process; otherwise</li>
* <li>if a {@code classloader} is supplied, use it to load the file on the classpath at the given path; otherwise</li>
* <li>if a {@code clazz} is supplied, use it to load the file on its classpath at the given path; otherwise</li>
* <li>try to convert the path to a URL and obtain the referenced resource</li>
* </ol>
* If all of these fail, this method returns null.
*
* @param resourcePath the logical path to the classpath, file, or URL resource
* @param classLoader the classloader that should be used to load the resource as a stream; may be null
* @param clazz the class that should be used to load the resource as a stream; may be null
* @param resourceDesc the description of the resource to be used in messages sent to {@code logger}; may be null
* @param logger a function that is to be called with log messages describing what is being tried; may be null
* @return an input stream to the resource; or null if the resource could not be found
* @throws IllegalArgumentException if the resource path is null or empty
*/
public static InputStream getResourceAsStream(String resourcePath,
ClassLoader classLoader,
Class<?> clazz, String resourceDesc, Consumer<String> logger) {
if (resourcePath == null) throw new IllegalArgumentException("resourcePath may not be null");
if (resourceDesc == null && logger != null) resourceDesc = resourcePath;
InputStream result = null;
if (result == null) {
try {
// Try absolute path ...
Path filePath = FileSystems.getDefault().getPath(resourcePath).toAbsolutePath();
File f = filePath.toFile();
if (f.exists() && f.isFile() && f.canRead()) {
result = new BufferedInputStream(new FileInputStream(f));
}
logMessage(result, logger, resourceDesc, "on filesystem at " + filePath);
} catch (InvalidPathException e) {
// just continue ...
} catch (FileNotFoundException e) {
// just continue ...
}
}
if (result == null) {
try {
// Try relative to current working directory ...
Path current = FileSystems.getDefault().getPath(".").toAbsolutePath();
Path absolute = current.resolve(Paths.get(resourcePath)).toAbsolutePath();
File f = absolute.toFile();
if (f.exists() && f.isFile() && f.canRead()) {
result = new BufferedInputStream(new FileInputStream(f));
}
logMessage(result, logger, resourceDesc, "on filesystem relative to '" + current + "' at '" + absolute + "'");
} catch (InvalidPathException e) {
// just continue ...
} catch (FileNotFoundException e) {
// just continue ...
}
}
if (result == null && classLoader != null) {
// Try using the class loader ...
result = classLoader.getResourceAsStream(resourcePath);
logMessage(result, logger, resourceDesc, "on classpath");
}
if (result == null && clazz != null) {
// Not yet found, so try the class ...
result = clazz.getResourceAsStream(resourcePath);
if (result == null) {
// Not yet found, so try the class's class loader ...
result = clazz.getClassLoader().getResourceAsStream(resourcePath);
}
logMessage(result, logger, resourceDesc, "on classpath");
}
if (result == null) {
// Still not found, so try to construct a URL out of it ...
try {
URL url = new URL(resourcePath);
result = url.openStream();
logMessage(result, logger, resourceDesc, "at URL " + url.toExternalForm());
} catch (MalformedURLException e) {
// just continue ...
} catch (IOException err) {
// just continue ...
}
}
// May be null ...
return result;
}
/**
* Create a directory at the given absolute or relative path.
*
* @param path the relative or absolute path of the directory; may not be null
* @return the reference to the existing readable and writable directory
*/
public static File createDirectory(Path path) {
File dir = path.toAbsolutePath().toFile();
if (dir.exists() && dir.canRead() && dir.canWrite()) {
if (dir.isDirectory()) return dir;
throw new IllegalStateException("Expecting '" + path + "' to be a directory but found a file");
}
dir.mkdirs();
return dir;
}
/**
* Create a file at the given absolute or relative path.
*
* @param path the relative or absolute path of the file to create; may not be null
* @return the reference to the existing readable and writable file
*/
public static File createFile(Path path) {
File file = path.toAbsolutePath().toFile();
if (file.exists() && file.canRead() && file.canWrite()) {
if (file.isFile()) return file;
throw new IllegalStateException("Expecting '" + path + "' to be a file but found a directory");
}
file.getParentFile().mkdirs();
return file;
}
/**
* Create a directory at the given absolute or relative path, removing any existing content beforehand.
*
* @param path the relative or absolute path of the directory to recreate; may not be null
* @param removeExistingContent true if any existing content should be removed
* @return the reference to the existing readable and writable directory
* @throws IOException if there is a problem deleting the files at this path
*/
public static File createDirectory(Path path, boolean removeExistingContent) throws IOException {
File dir = path.toAbsolutePath().toFile();
if (dir.exists() && dir.canRead() && dir.canWrite()) {
if (dir.isDirectory()) {
delete(path);
return dir;
}
throw new IllegalStateException("Expecting '" + path + "' to be a directory but found a file");
}
dir.mkdirs();
return dir;
}
/**
* A method that will delete a file or folder only if it is within the 'target' directory (for safety).
* Folders are removed recursively.
*
* @param path the path to the file or folder in the target directory
* @throws IOException if there is a problem deleting the file at the given path
*/
public static void delete(String path) throws IOException {
if (path != null) delete(Paths.get(path));
}
/**
* A method that will delete a file or folder. Folders are removed recursively.
*
* @param fileOrFolder the file or folder to be deleted
* @throws IOException if there is a problem deleting the file at the given path
*/
public static void delete(File fileOrFolder) throws IOException {
if (fileOrFolder != null) delete(fileOrFolder.toPath());
}
/**
* A method that will delete multiple file and/or folders. Folders are removed recursively.
*
* @param filesOrFolder the files and folders to be deleted
* @throws IOException if there is a problem deleting the file at the given path
*/
public static void delete(File... filesOrFolder) throws IOException {
for (File fileOrFolder : filesOrFolder) {
delete(fileOrFolder);
}
}
/**
* A method that will recursively delete a file or folder.
*
* @param path the path to the file or folder in the target directory
* @throws IOException if there is a problem deleting the file at the given path
*/
public static void delete(Path path) throws IOException {
if (path != null) {
if (path.toAbsolutePath().toFile().exists()) {
LOGGER.info("Deleting '" + path + "'...");
Set<FileVisitOption> options = EnumSet.noneOf(FileVisitOption.class);
int maxDepth = 10;
FileVisitor<Path> removingVisitor = new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
java.nio.file.Files.delete(file);
return FileVisitResult.SKIP_SUBTREE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
java.nio.file.Files.delete(dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
LOGGER.error("Unable to remove '{}'", file.getFileName(), exc);
return FileVisitResult.CONTINUE;
}
};
java.nio.file.Files.walkFileTree(path, options, maxDepth, removingVisitor);
}
}
}
/**
* Find a port that is available. This method starts a {@link ServerSocket} and obtains the port on which the socket is
* listening, and then shuts down the socket so the port becomes available.
*
* @return the number of the now-available port
* @throws IllegalStateException if it cannot find an available port
*/
public static int getAvailablePort() {
try (ServerSocket socket = new ServerSocket(0)) {
return socket.getLocalPort();
} catch (IOException e) {
throw new IllegalStateException("Cannot find available port: " + e.getMessage(), e);
}
}
private static void logMessage(InputStream stream, Consumer<String> logger, String resourceDesc, String msg) {
if (stream != null && logger != null) {
logger.accept("Found " + resourceDesc + " " + msg);
}
}
private IoUtil() {
// Prevent construction
}
}

View File

@ -0,0 +1,316 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.util;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.function.BiFunction;
import java.util.function.Function;
import io.debezium.annotation.Immutable;
/**
* A utility for creating iterators.
*
* @author Randall Hauch
*/
@Immutable
public class Iterators {
public static <T> Iterator<T> empty() {
return new Iterator<T>() {
@Override
public boolean hasNext() {
return false;
}
@Override
public T next() {
throw new NoSuchElementException();
}
};
}
public static <T> Iterator<T> with(final T value) {
return new Iterator<T>() {
private boolean finished = false;
@Override
public boolean hasNext() {
return !finished;
}
@Override
public T next() {
if (finished) {
throw new NoSuchElementException();
}
finished = true;
return value;
}
};
}
public static <T> Iterator<T> with(T value1, T value2) {
return new Iterator<T>() {
private int remaining = 2;
@Override
public boolean hasNext() {
return remaining > 0;
}
@Override
public T next() {
if (remaining == 2) {
--remaining;
return value1;
}
if (remaining == 1) {
--remaining;
return value2;
}
throw new NoSuchElementException();
}
};
}
public static <T> Iterator<T> with(T value1, T value2, T value3) {
return new Iterator<T>() {
private int remaining = 3;
@Override
public boolean hasNext() {
return remaining > 0;
}
@Override
public T next() {
if (remaining == 3) {
--remaining;
return value1;
}
if (remaining == 2) {
--remaining;
return value2;
}
if (remaining == 1) {
--remaining;
return value3;
}
throw new NoSuchElementException();
}
};
}
@SafeVarargs
public static <T> Iterator<T> with(T value1, T value2, T value3, T... additional) {
return new Iterator<T>() {
private int index = 0;
@Override
public boolean hasNext() {
return index < additional.length + 3;
}
@Override
public T next() {
try {
if (index == 0) return value1;
if (index == 1) return value2;
if (index == 2) return value3;
if (index < additional.length + 3) return additional[index - 3];
--index;
throw new NoSuchElementException();
} finally {
++index;
}
}
};
}
public static <T> Iterator<T> with(T[] values) {
return new Iterator<T>() {
private int index = 0;
@Override
public boolean hasNext() {
return index < values.length;
}
@Override
public T next() {
try {
if (index < values.length) return values[index];
--index;
throw new NoSuchElementException();
} finally {
++index;
}
}
};
}
public static <T, U, V> Iterator<V> around(Iterable<? extends T> first,
Iterable<? extends U> second,
BiFunction<T, U, V> conversion) {
return around(first.iterator(), second.iterator(), conversion);
}
public static <T, U, V> Iterator<V> around(final Iterator<? extends T> first,
final Iterator<? extends U> second,
final BiFunction<T, U, V> combineFirstAndSecond) {
return new Iterator<V>() {
@Override
public boolean hasNext() {
return second.hasNext();
}
@Override
public V next() {
return combineFirstAndSecond.apply(first.next(), second.next());
}
};
}
public static <V, T> Iterator<T> around(final Iterable<? extends V> iterable, Function<V, T> conversion) {
return around(iterable.iterator(), conversion);
}
public static <V, T> Iterator<T> around(final Iterator<? extends V> iterator, Function<V, T> conversion) {
return new Iterator<T>() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public T next() {
return conversion.apply(iterator.next());
}
@Override
public void remove() {
iterator.remove();
}
};
}
public static <T> Iterable<T> around(final Iterator<T> iterator) {
return new Iterable<T>() {
@Override
public Iterator<T> iterator() {
return iterator;
}
};
}
public static <T> Iterator<T> readOnly(final Iterator<T> iterator) {
return new Iterator<T>() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public T next() {
return iterator.next();
}
};
}
public static <V, T> Iterator<T> readOnly(final Iterator<? extends V> iterator, Function<V, T> conversion) {
return new Iterator<T>() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public T next() {
return conversion.apply(iterator.next());
}
};
}
public static <T> Iterator<T> readOnly(final Iterable<T> iterable) {
return readOnly(iterable.iterator());
}
public static <V, T> Iterator<T> readOnly(final Iterable<V> iterable, Function<V, T> conversion) {
return readOnly(iterable.iterator(), conversion);
}
public static <T> Iterable<T> readOnlyIterable(final Iterable<T> iterable) {
return new Iterable<T>() {
@Override
public Iterator<T> iterator() {
return readOnly(iterable.iterator());
}
};
}
public static <V, T> Iterable<T> readOnlyIterable(final Iterable<? extends V> iterable, Function<V, T> conversion) {
return new Iterable<T>() {
@Override
public Iterator<T> iterator() {
return readOnly(iterable.iterator(), conversion);
}
};
}
public static <T> Iterator<T> join(Iterable<T> first, T last) {
return join(first.iterator(), with(last));
}
public static <T> Iterator<T> join(Iterable<T> first, T last1, T last2) {
return join(first.iterator(), with(last1, last2));
}
public static <T> Iterator<T> join(Iterable<T> first, T last1, T last2, T last3) {
return join(first.iterator(), with(last1, last2, last3));
}
public static <T> Iterator<T> join(Iterable<T> first, T last1, T last2, T last3, T last4) {
return join(first.iterator(), with(last1, last2, last3, last4));
}
public static <T> Iterator<T> join(Iterable<T> first, Iterable<T> second) {
return join(first.iterator(), second.iterator());
}
public static <T> Iterator<T> join(Iterator<T> first, Iterator<T> second) {
return new Iterator<T>() {
private boolean completedFirst = false;
@Override
public boolean hasNext() {
if (!completedFirst) {
if (first.hasNext()) return true;
completedFirst = true;
}
return second.hasNext();
}
@Override
public T next() {
if (!completedFirst) {
if (first.hasNext()) return first.next();
completedFirst = true;
}
return second.next();
}
@Override
public void remove() {
if (!completedFirst) {
first.remove();
}
second.remove();
}
};
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.util;
import java.util.Iterator;
import java.util.StringJoiner;
import io.debezium.annotation.Immutable;
/**
* A utility for joining multiple {@link CharSequence character sequences} together.
*
* @author Randall Hauch
*/
@Immutable
public final class Joiner {
public static Joiner on(CharSequence delimiter) {
return new Joiner(new StringJoiner(delimiter));
}
public static Joiner on(CharSequence prefix, CharSequence delimiter) {
return new Joiner(new StringJoiner(delimiter, prefix, ""));
}
public static Joiner on(CharSequence prefix, CharSequence delimiter, CharSequence suffix) {
return new Joiner(new StringJoiner(delimiter, prefix, suffix));
}
private final StringJoiner joiner;
private Joiner(StringJoiner joiner) {
this.joiner = joiner;
}
public String join(Object[] values) {
for (Object value : values) {
if (value != null) joiner.add(value.toString());
}
return joiner.toString();
}
public String join(CharSequence firstValue, CharSequence... additionalValues) {
if (firstValue != null) joiner.add(firstValue);
for (CharSequence value : additionalValues) {
if (value != null) joiner.add(value);
}
return joiner.toString();
}
public String join(Iterable<?> values) {
for (Object value : values) {
if (value != null) joiner.add(value.toString());
}
return joiner.toString();
}
public String join(Iterable<?> values, CharSequence nextValue, CharSequence... additionalValues) {
for (Object value : values) {
if (value != null) joiner.add(value.toString());
}
if (nextValue != null) joiner.add(nextValue);
for (CharSequence value : additionalValues) {
if (value != null) joiner.add(value);
}
return joiner.toString();
}
public String join(Iterator<?> values) {
while (values.hasNext()) {
Object value = values.next();
if (value != null) joiner.add(value.toString());
}
return joiner.toString();
}
}

View File

@ -0,0 +1,81 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.util;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
import io.debezium.annotation.ThreadSafe;
/**
* An atomic reference that atomically uses a supplier to lazily accesses the referenced object the first time it is needed.
*
* @author Randall Hauch
* @param <T> the type of referenced object
*/
@ThreadSafe
public final class LazyReference<T> {
public static <T> LazyReference<T> create(Supplier<T> supplier) {
return new LazyReference<T>(supplier);
}
private final AtomicReference<T> ref = new AtomicReference<>();
private final Supplier<T> supplier;
private LazyReference(Supplier<T> supplier) {
this.supplier = supplier;
}
/**
* Determine if the referenced object has been created and accessed.
*
* @return {@code true} if the object has been created, or false otherwise
*/
public boolean isInitialized() {
return ref.get() != null;
}
/**
* If the referenced object has been {@link #isInitialized() initialized}, then release it.
* This method does nothing if the reference has not yet been accessed or {@link #isInitialized() initialized}.
*/
public void release() {
release(null);
}
/**
* If the referenced object has been {@link #isInitialized() initialized}, then release it and call the supplied function with
* the reference. This method does nothing if the reference has not yet been accessed or {@link #isInitialized() initialized}.
*
* @param finalizer the function that should be called when the previously-{@link #isInitialized() initialized} referenced
* object is released; may be null
*/
public void release(Consumer<T> finalizer) {
ref.updateAndGet(existing->{
if ( existing != null && finalizer != null ) finalizer.accept(existing);
return null;
});
}
/**
* Get the referenced value (creating it if required) and call the supplied function.
*
* @param consumer the function that operates on the value; may not be null
* @return true if the function was called on the referenced value, or false if there is no referenced value
*/
public boolean execute(Consumer<T> consumer) {
T value = get();
if (value == null) return false;
consumer.accept(value);
return true;
}
public T get() {
return ref.updateAndGet(existing->existing != null ? existing : supplier.get());
}
}

View File

@ -0,0 +1,721 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.util;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import io.debezium.annotation.Immutable;
/**
* Utilities for performing math operations with mixed native and advanced numeric types.
*
* @author Randall Hauch
*/
@Immutable
public final class MathOps {
public static Number add(Number first, Number second) {
if (second == null)
return first;
else if (first == null) return second;
if (first instanceof Short) return add((Short) first, second);
if (first instanceof Integer) return add((Integer) first, second);
if (first instanceof Long) return add((Long) first, second);
if (first instanceof Float) return add((Float) first, second);
if (first instanceof Double) return add((Double) first, second);
if (first instanceof BigInteger) return add((BigInteger) first, second);
if (first instanceof BigDecimal) return add((BigDecimal) first, second);
if (first instanceof AtomicLong) return add((AtomicLong) first, second);
if (first instanceof AtomicInteger) return add((AtomicInteger) first, second);
throw new IllegalArgumentException();
}
public static Number add(Short first, Number second) {
if (second instanceof Short) return add(first, (Short) second);
if (second instanceof Integer) return add(first, (Integer) second);
if (second instanceof Long) return add(first, (Long) second);
if (second instanceof Float) return add(first, (Float) second);
if (second instanceof Double) return add(first, (Double) second);
if (second instanceof BigInteger) return add(first, (BigInteger) second);
if (second instanceof BigDecimal) return add(first, (BigDecimal) second);
if (second instanceof AtomicInteger) return add(first, (AtomicInteger) second);
if (second instanceof AtomicLong) return add(first, (AtomicLong) second);
throw new IllegalArgumentException();
}
public static Number add(Short first, short second) {
int sum = first.shortValue() + second;
if (Short.MAX_VALUE >= sum && Short.MIN_VALUE <= sum) return new Short((short) sum);
return new Integer(sum);
}
public static Number add(Short first, int second) {
long sum = first.longValue() + second;
if (Short.MAX_VALUE >= sum && Short.MIN_VALUE <= sum) return new Short((short) sum);
if (Integer.MAX_VALUE >= sum && Integer.MIN_VALUE <= sum) return new Integer((int) sum);
return new Long(sum);
}
public static Number add(Short first, long second) {
long sum = first.longValue() + second;
if (Short.MAX_VALUE >= sum && Short.MIN_VALUE <= sum) return new Short((short) sum);
if (Integer.MAX_VALUE >= sum && Integer.MIN_VALUE <= sum) return new Integer((int) sum);
return new Long(sum);
}
public static Number add(Short first, float second) {
double sum = first.floatValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Short first, double second) {
double sum = first.doubleValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Short first, Short second) {
return add(first, second.shortValue());
}
public static Number add(Short first, Integer second) {
return add(first, second.intValue());
}
public static Number add(Short first, Long second) {
return add(first, second.longValue());
}
public static Number add(Short first, Float second) {
return add(first, second.floatValue());
}
public static Number add(Short first, Double second) {
return add(first, second.doubleValue());
}
public static Number add(Short first, BigDecimal second) {
return second.add(BigDecimal.valueOf(first.longValue()));
}
public static Number add(Short first, BigInteger second) {
return second.add(BigInteger.valueOf(first.longValue()));
}
public static Number add(Short first, AtomicInteger second) {
return add(first, second.intValue());
}
public static Number add(Short first, AtomicLong second) {
return add(first, second.longValue());
}
public static Number add(Integer first, Number second) {
if (second instanceof Short) return add(first, (Short) second);
if (second instanceof Integer) return add(first, (Integer) second);
if (second instanceof Long) return add(first, (Long) second);
if (second instanceof Float) return add(first, (Float) second);
if (second instanceof Double) return add(first, (Double) second);
if (second instanceof BigInteger) return add(first, (BigInteger) second);
if (second instanceof BigDecimal) return add(first, (BigDecimal) second);
if (second instanceof AtomicInteger) return add(first, (AtomicInteger) second);
if (second instanceof AtomicLong) return add(first, (AtomicLong) second);
throw new IllegalArgumentException();
}
public static Number add(Integer first, short second) {
long sum = first.longValue() + second;
if (Short.MAX_VALUE >= sum && Short.MIN_VALUE <= sum) return new Short((short) sum);
if (Integer.MAX_VALUE >= sum && Integer.MIN_VALUE <= sum) return new Integer((int) sum);
return new Long(sum);
}
public static Number add(Integer first, int second) {
long sum = first.longValue() + second;
if (Short.MAX_VALUE >= sum && Short.MIN_VALUE <= sum) return new Short((short) sum);
if (Integer.MAX_VALUE >= sum && Integer.MIN_VALUE <= sum) return new Integer((int) sum);
return new Long(sum);
}
public static Number add(Integer first, long second) {
long sum = first.longValue() + second;
if (Short.MAX_VALUE >= sum && Short.MIN_VALUE <= sum) return new Short((short) sum);
if (Integer.MAX_VALUE >= sum && Integer.MIN_VALUE <= sum) return new Integer((int) sum);
return new Long(sum);
}
public static Number add(Integer first, float second) {
double sum = first.floatValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Integer first, double second) {
double sum = first.doubleValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Integer first, Short second) {
return add(first, second.shortValue());
}
public static Number add(Integer first, Integer second) {
return add(first, second.intValue());
}
public static Number add(Integer first, Long second) {
return add(first, second.longValue());
}
public static Number add(Integer first, Float second) {
return add(first, second.floatValue());
}
public static Number add(Integer first, Double second) {
return add(first, second.doubleValue());
}
public static Number add(Integer first, BigDecimal second) {
return second.add(BigDecimal.valueOf(first.longValue()));
}
public static Number add(Integer first, BigInteger second) {
return second.add(BigInteger.valueOf(first.longValue()));
}
public static Number add(Integer first, AtomicInteger second) {
return add(first, second.intValue());
}
public static Number add(Integer first, AtomicLong second) {
return add(first, second.longValue());
}
public static Number add(Long first, Number second) {
if (second instanceof Short) return add(first, (Short) second);
if (second instanceof Integer) return add(first, (Integer) second);
if (second instanceof Long) return add(first, (Long) second);
if (second instanceof Float) return add(first, (Float) second);
if (second instanceof Double) return add(first, (Double) second);
if (second instanceof BigInteger) return add(first, (BigInteger) second);
if (second instanceof BigDecimal) return add(first, (BigDecimal) second);
if (second instanceof AtomicInteger) return add(first, (AtomicInteger) second);
if (second instanceof AtomicLong) return add(first, (AtomicLong) second);
throw new IllegalArgumentException();
}
public static Number add(Long first, short second) {
long sum = first.longValue() + second;
if (Short.MAX_VALUE >= sum && Short.MIN_VALUE <= sum) return new Short((short) sum);
if (Integer.MAX_VALUE >= sum && Integer.MIN_VALUE <= sum) return new Integer((int) sum);
return new Long(sum);
}
public static Number add(Long first, int second) {
long sum = first.longValue() + second;
if (Short.MAX_VALUE >= sum && Short.MIN_VALUE <= sum) return new Short((short) sum);
if (Integer.MAX_VALUE >= sum && Integer.MIN_VALUE <= sum) return new Integer((int) sum);
return new Long(sum);
}
public static Number add(Long first, long second) {
long sum = first.longValue() + second;
if (Short.MAX_VALUE >= sum && Short.MIN_VALUE <= sum) return new Short((short) sum);
if (Integer.MAX_VALUE >= sum && Integer.MIN_VALUE <= sum) return new Integer((int) sum);
return new Long(sum);
}
public static Number add(Long first, float second) {
double sum = first.doubleValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Long first, double second) {
double sum = first.doubleValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Long first, Short second) {
return add(first, second.shortValue());
}
public static Number add(Long first, Integer second) {
return add(first, second.intValue());
}
public static Number add(Long first, Long second) {
return add(first, second.longValue());
}
public static Number add(Long first, Float second) {
return add(first, second.floatValue());
}
public static Number add(Long first, Double second) {
return add(first, second.doubleValue());
}
public static Number add(Long first, BigDecimal second) {
return second.add(BigDecimal.valueOf(first.longValue()));
}
public static Number add(Long first, BigInteger second) {
return second.add(BigInteger.valueOf(first.longValue()));
}
public static Number add(Long first, AtomicInteger second) {
return add(first, second.intValue());
}
public static Number add(Long first, AtomicLong second) {
return add(first, second.longValue());
}
public static Number add(Float first, Number second) {
if (second instanceof Short) return add(first, (Short) second);
if (second instanceof Integer) return add(first, (Integer) second);
if (second instanceof Long) return add(first, (Long) second);
if (second instanceof Float) return add(first, (Float) second);
if (second instanceof Double) return add(first, (Double) second);
if (second instanceof BigInteger) return add(first, (BigInteger) second);
if (second instanceof BigDecimal) return add(first, (BigDecimal) second);
if (second instanceof AtomicInteger) return add(first, (AtomicInteger) second);
if (second instanceof AtomicLong) return add(first, (AtomicLong) second);
throw new IllegalArgumentException();
}
public static Number add(Float first, short second) {
double sum = first.floatValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Float first, int second) {
double sum = first.floatValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Float first, long second) {
double sum = first.floatValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Float first, float second) {
double sum = first.floatValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Float first, double second) {
double sum = first.floatValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Float first, Short second) {
return add(first, second.shortValue());
}
public static Number add(Float first, Integer second) {
return add(first, second.intValue());
}
public static Number add(Float first, Long second) {
return add(first, second.longValue());
}
public static Number add(Float first, Float second) {
return add(first, second.floatValue());
}
public static Number add(Float first, Double second) {
return add(first, second.doubleValue());
}
public static Number add(Float first, BigDecimal second) {
return second.add(BigDecimal.valueOf(first.longValue()));
}
public static Number add(Float first, BigInteger second) {
return second.add(BigInteger.valueOf(first.longValue()));
}
public static Number add(Float first, AtomicInteger second) {
return add(first, second.intValue());
}
public static Number add(Float first, AtomicLong second) {
return add(first, second.longValue());
}
public static Number add(Double first, Number second) {
if (second instanceof Short) return add(first, (Short) second);
if (second instanceof Integer) return add(first, (Integer) second);
if (second instanceof Long) return add(first, (Long) second);
if (second instanceof Float) return add(first, (Float) second);
if (second instanceof Double) return add(first, (Double) second);
if (second instanceof BigInteger) return add(first, (BigInteger) second);
if (second instanceof BigDecimal) return add(first, (BigDecimal) second);
if (second instanceof AtomicInteger) return add(first, (AtomicInteger) second);
if (second instanceof AtomicLong) return add(first, (AtomicLong) second);
throw new IllegalArgumentException();
}
public static Number add(Double first, short second) {
double sum = first.floatValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Double first, int second) {
double sum = first.floatValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Double first, long second) {
double sum = first.floatValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Double first, float second) {
double sum = first.floatValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Double first, double second) {
double sum = first.floatValue() + second;
if (Float.MAX_VALUE >= sum && Float.MIN_VALUE <= sum) return new Float((float) sum);
return new Double(sum);
}
public static Number add(Double first, Short second) {
return add(first, second.shortValue());
}
public static Number add(Double first, Integer second) {
return add(first, second.intValue());
}
public static Number add(Double first, Long second) {
return add(first, second.longValue());
}
public static Number add(Double first, Float second) {
return add(first, second.floatValue());
}
public static Number add(Double first, Double second) {
return add(first, second.doubleValue());
}
public static Number add(Double first, BigDecimal second) {
return second.add(BigDecimal.valueOf(first.longValue()));
}
public static Number add(Double first, BigInteger second) {
return second.add(BigInteger.valueOf(first.longValue()));
}
public static Number add(Double first, AtomicInteger second) {
return add(first, second.intValue());
}
public static Number add(Double first, AtomicLong second) {
return add(first, second.longValue());
}
public static Number add(BigInteger first, Number second) {
if (second instanceof Short) return add(first, (Short) second);
if (second instanceof Integer) return add(first, (Integer) second);
if (second instanceof Long) return add(first, (Long) second);
if (second instanceof Float) return add(first, (Float) second);
if (second instanceof Double) return add(first, (Double) second);
if (second instanceof BigInteger) return add(first, (BigInteger) second);
if (second instanceof BigDecimal) return add(first, (BigDecimal) second);
if (second instanceof AtomicInteger) return add(first, (AtomicInteger) second);
if (second instanceof AtomicLong) return add(first, (AtomicLong) second);
throw new IllegalArgumentException();
}
public static Number add(BigInteger first, short second) {
return first.add(BigInteger.valueOf(second));
}
public static Number add(BigInteger first, int second) {
return first.add(BigInteger.valueOf(second));
}
public static Number add(BigInteger first, long second) {
return first.add(BigInteger.valueOf(second));
}
public static Number add(BigInteger first, float second) {
return new BigDecimal(first).add(BigDecimal.valueOf(second));
}
public static Number add(BigInteger first, double second) {
return new BigDecimal(first).add(BigDecimal.valueOf(second));
}
public static Number add(BigInteger first, Short second) {
return add(first, second.shortValue());
}
public static Number add(BigInteger first, Integer second) {
return add(first, second.intValue());
}
public static Number add(BigInteger first, Long second) {
return add(first, second.longValue());
}
public static Number add(BigInteger first, Float second) {
return add(first, second.floatValue());
}
public static Number add(BigInteger first, Double second) {
return add(first, second.doubleValue());
}
public static Number add(BigInteger first, BigDecimal second) {
return second.add(new BigDecimal(first));
}
public static Number add(BigInteger first, BigInteger second) {
return second.add(second);
}
public static Number add(BigInteger first, AtomicInteger second) {
return add(first, second.intValue());
}
public static Number add(BigInteger first, AtomicLong second) {
return add(first, second.longValue());
}
public static Number add(BigDecimal first, Number second) {
if (second instanceof Short) return add(first, (Short) second);
if (second instanceof Integer) return add(first, (Integer) second);
if (second instanceof Long) return add(first, (Long) second);
if (second instanceof Float) return add(first, (Float) second);
if (second instanceof Double) return add(first, (Double) second);
if (second instanceof BigInteger) return add(first, (BigInteger) second);
if (second instanceof BigDecimal) return add(first, (BigDecimal) second);
if (second instanceof AtomicInteger) return add(first, (AtomicInteger) second);
if (second instanceof AtomicLong) return add(first, (AtomicLong) second);
throw new IllegalArgumentException();
}
public static Number add(BigDecimal first, short second) {
return first.add(BigDecimal.valueOf(second));
}
public static Number add(BigDecimal first, int second) {
return first.add(BigDecimal.valueOf(second));
}
public static Number add(BigDecimal first, long second) {
return first.add(BigDecimal.valueOf(second));
}
public static Number add(BigDecimal first, float second) {
return first.add(BigDecimal.valueOf(second));
}
public static Number add(BigDecimal first, double second) {
return first.add(BigDecimal.valueOf(second));
}
public static Number add(BigDecimal first, Short second) {
return add(first, second.shortValue());
}
public static Number add(BigDecimal first, Integer second) {
return add(first, second.intValue());
}
public static Number add(BigDecimal first, Long second) {
return add(first, second.longValue());
}
public static Number add(BigDecimal first, Float second) {
return add(first, second.floatValue());
}
public static Number add(BigDecimal first, Double second) {
return add(first, second.doubleValue());
}
public static Number add(BigDecimal first, BigDecimal second) {
return second.add(first);
}
public static Number add(BigDecimal first, BigInteger second) {
return second.add(second);
}
public static Number add(BigDecimal first, AtomicInteger second) {
return add(first, second.intValue());
}
public static Number add(BigDecimal first, AtomicLong second) {
return add(first, second.longValue());
}
public static Number add(AtomicInteger first, Number second) {
if (second instanceof Short) return add(first, (Short) second);
if (second instanceof Integer) return add(first, (Integer) second);
if (second instanceof Long) return add(first, (Long) second);
if (second instanceof Float) return add(first, (Float) second);
if (second instanceof Double) return add(first, (Double) second);
if (second instanceof BigInteger) return add(first, (BigInteger) second);
if (second instanceof BigDecimal) return add(first, (BigDecimal) second);
if (second instanceof AtomicInteger) return add(first, (AtomicInteger) second);
if (second instanceof AtomicLong) return add(first, (AtomicLong) second);
throw new IllegalArgumentException();
}
public static Number add(AtomicInteger first, short second) {
return add(new Integer(first.intValue()), second);
}
public static Number add(AtomicInteger first, int second) {
return add(new Integer(first.intValue()), second);
}
public static Number add(AtomicInteger first, long second) {
return add(new Integer(first.intValue()), second);
}
public static Number add(AtomicInteger first, float second) {
return add(new Integer(first.intValue()), second);
}
public static Number add(AtomicInteger first, double second) {
return add(new Integer(first.intValue()), second);
}
public static Number add(AtomicInteger first, Short second) {
return add(first, second.shortValue());
}
public static Number add(AtomicInteger first, Integer second) {
return add(first, second.intValue());
}
public static Number add(AtomicInteger first, Long second) {
return add(first, second.longValue());
}
public static Number add(AtomicInteger first, Float second) {
return add(first, second.floatValue());
}
public static Number add(AtomicInteger first, Double second) {
return add(first, second.doubleValue());
}
public static Number add(AtomicInteger first, BigDecimal second) {
return add(second, first);
}
public static Number add(AtomicInteger first, BigInteger second) {
return add(second, first);
}
public static Number add(AtomicInteger first, AtomicInteger second) {
return add(first, second.intValue());
}
public static Number add(AtomicInteger first, AtomicLong second) {
return add(first, second.longValue());
}
public static Number add(AtomicLong first, Number second) {
if (second instanceof Short) return add(first, (Short) second);
if (second instanceof Integer) return add(first, (Integer) second);
if (second instanceof Long) return add(first, (Long) second);
if (second instanceof Float) return add(first, (Float) second);
if (second instanceof Double) return add(first, (Double) second);
if (second instanceof BigInteger) return add(first, (BigInteger) second);
if (second instanceof BigDecimal) return add(first, (BigDecimal) second);
if (second instanceof AtomicInteger) return add(first, (AtomicInteger) second);
if (second instanceof AtomicLong) return add(first, (AtomicLong) second);
throw new IllegalArgumentException();
}
public static Number add(AtomicLong first, short second) {
return add(new Long(first.longValue()), second);
}
public static Number add(AtomicLong first, int second) {
return add(new Long(first.longValue()), second);
}
public static Number add(AtomicLong first, long second) {
return add(new Long(first.longValue()), second);
}
public static Number add(AtomicLong first, float second) {
return add(new Long(first.longValue()), second);
}
public static Number add(AtomicLong first, double second) {
return add(new Long(first.longValue()), second);
}
public static Number add(AtomicLong first, Short second) {
return add(first, second.shortValue());
}
public static Number add(AtomicLong first, Integer second) {
return add(first, second.intValue());
}
public static Number add(AtomicLong first, Long second) {
return add(first, second.longValue());
}
public static Number add(AtomicLong first, Float second) {
return add(first, second.floatValue());
}
public static Number add(AtomicLong first, Double second) {
return add(first, second.doubleValue());
}
public static Number add(AtomicLong first, BigDecimal second) {
return add(second, first);
}
public static Number add(AtomicLong first, BigInteger second) {
return add(second, first);
}
public static Number add(AtomicLong first, AtomicInteger second) {
return add(first, second.intValue());
}
public static Number add(AtomicLong first, AtomicLong second) {
return add(first, second.longValue());
}
private MathOps() {
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.util;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import io.debezium.annotation.ThreadSafe;
/**
* A simple named thread factory that creates threads named "{@code $PREFIX$-$NAME$-thread-$NUMBER$}".
*
* @author Randall Hauch
*/
@ThreadSafe
public final class NamedThreadFactory implements ThreadFactory {
private static final boolean DEFAULT_DAEMON_THREAD = true;
private static final int DEFAULT_STACK_SIZE = 0;
private final boolean daemonThreads;
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
private final int stackSize;
private final Consumer<String> afterThreadCreation;
public NamedThreadFactory(String prefix) {
this(prefix, DEFAULT_DAEMON_THREAD, DEFAULT_STACK_SIZE);
}
public NamedThreadFactory(String prefix, boolean daemonThreads) {
this(prefix, daemonThreads, DEFAULT_STACK_SIZE);
}
public NamedThreadFactory(String prefix, boolean daemonThreads, int stackSize) {
this(prefix, daemonThreads, stackSize, null);
}
public NamedThreadFactory(String prefix, boolean daemonThreads, int stackSize, Consumer<String> afterThreadCreation) {
if ( prefix == null ) throw new IllegalArgumentException("The thread prefix may not be null");
final SecurityManager s = System.getSecurityManager();
this.group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
this.namePrefix = prefix;
this.daemonThreads = daemonThreads;
this.stackSize = stackSize;
this.afterThreadCreation = afterThreadCreation;
}
@Override
public Thread newThread(final Runnable runnable) {
String threadName = namePrefix + threadNumber.getAndIncrement();
final Thread t = new Thread(group, runnable, threadName, stackSize);
t.setDaemon(daemonThreads);
if (t.getPriority() != Thread.NORM_PRIORITY) {
t.setPriority(Thread.NORM_PRIORITY);
}
if (afterThreadCreation != null) {
try {
afterThreadCreation.accept(threadName);
} catch (Throwable e) {
// do nothing
}
}
return t;
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.util;
import java.util.Iterator;
import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.IntStream;
import io.debezium.annotation.Immutable;
/**
* Utility methods for obtaining streams of integers.
*
* @author Randall Hauch
*/
@Immutable
public class Sequences {
/**
* Create a stream of <em>number</em> monotonically increasing numbers starting at 0, useful when performing an operation
* <em>number</em> times.
*
* @param number the number of values to include in the stream; must be positive
* @return the sequence; never null
*/
public static IntStream times(int number) {
return IntStream.range(0, number);
}
/**
* Create an iterator over an infinite number of monotonically increasing numbers starting at 0, useful when performing an
* operation an unknown number of times.
*
* @return the sequence; never null
*/
public static Iterable<Integer> infiniteIntegers() {
return infiniteIntegers(0);
}
/**
* Create an iterator over an infinite number monotonically increasing numbers starting at the given number, useful when
* performing an operation an unknown number of times.
*
* @param startingAt the first number to include in the resulting stream
* @return the sequence; never null
*/
public static Iterable<Integer> infiniteIntegers(int startingAt) {
return Iterators.around(new Iterator<Integer>() {
private int counter = startingAt;
@Override
public boolean hasNext() {
return true;
}
@Override
public Integer next() {
return Integer.valueOf(counter++);
}
});
}
/**
* Obtain a supplier function that randomly selects from the given values. If the supplied values contain nulls, then
* the resulting supplier function may return null values.
*
* @param first the first value that may be randomly picked
* @param additional the additional values to randomly pick from; may be null or empty
* @return the supplier function; never null
*/
@SafeVarargs
public static <T> Supplier<T> randomlySelect(T first, T... additional) {
if (additional == null || additional.length == 0) return () -> first;
Random rng = new Random(System.currentTimeMillis());
int max = additional.length + 1;
return () -> {
int index = rng.nextInt(max);
return index == 0 ? first : additional[index-1];
};
}
/**
* Obtain a supplier function that randomly selects from the given values. If the supplied values contain nulls, then
* the resulting supplier function may return null values.
*
* @param values the values to randomly pick from; may not be null, should not be empty
* @return the supplier function; never null
*/
@SafeVarargs
public static <T> Supplier<T> randomlySelect(T... values) {
if (values == null || values.length == 0) throw new IllegalArgumentException("The values array may not be null or empty");
Random rng = new Random(System.currentTimeMillis());
return () -> values[rng.nextInt(values.length)];
}
}

View File

@ -0,0 +1,521 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.util;
import java.text.DecimalFormat;
import java.time.Duration;
import java.util.LongSummaryStatistics;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import io.debezium.annotation.ThreadSafe;
/**
* A stopwatch for measuring durations. All NewStopwatch implementations are threadsafe, although using a single stopwatch
* object across threads requires caution and care.
*
* @author Randall Hauch
*/
@ThreadSafe
public abstract class Stopwatch {
/**
* Start the stopwatch. Calling this method on an already-started stopwatch has no effect.
*
* @return this object to enable chaining methods
* @see #stop
*/
public abstract Stopwatch start();
/**
* Stop the stopwatch. Calling this method on an already-stopped stopwatch has no effect.
*
* @return this object to enable chaining methods
* @see #start()
*/
public abstract Stopwatch stop();
/**
* Get the total and average durations measured by this stopwatch.
*
* @return the durations; never null
*/
public abstract Durations durations();
/**
* The average and total durations as measured by one or more stopwatches.
*/
@ThreadSafe
public static interface Durations {
/**
* Get the statistics for the durations in nanoseconds.
*
* @return the statistics; never null
*/
Statistics statistics();
}
/**
* The timing statistics for a recorded set of samples.
*
* @author Randall Hauch
*
*/
public static interface Statistics {
/**
* Returns the count of durations recorded.
*
* @return the count of durations
*/
public long getCount();
/**
* Returns the total of all recorded durations.
*
* @return The total duration; never null but possibly {@link Duration#ZERO}.
*/
public Duration getTotal();
/**
* Returns the minimum of all recorded durations.
*
* @return The minimum duration; never null but possibly {@link Duration#ZERO}.
*/
public Duration getMinimum();
/**
* Returns the maximum of all recorded durations.
*
* @return The maximum duration; never null but possibly {@link Duration#ZERO}.
*/
public Duration getMaximum();
/**
* Returns the arithmetic mean of all recorded durations.
*
* @return The average duration; never null but possibly {@link Duration#ZERO}.
*/
public Duration getAverage();
/**
* Returns a string representation of the total of all recorded durations.
*
* @return the string representation of the total duration; never null but possibly {@link Duration#ZERO}.
*/
default public String getTotalAsString() {
return asString(getTotal());
}
/**
* Returns a string representation of the minimum of all recorded durations.
*
* @return the string representation of the minimum duration; never null but possibly {@link Duration#ZERO}.
*/
default public String getMinimumAsString() {
return asString(getMinimum());
}
/**
* Returns a string representation of the maximum of all recorded durations.
*
* @return the string representation of the maximum duration; never null but possibly {@link Duration#ZERO}.
*/
default public String getMaximumAsString() {
return asString(getMaximum());
}
/**
* Returns a string representation of the arithmetic mean of all recorded durations.
*
* @return the string representation of the average duration; never null but possibly {@link Duration#ZERO}.
*/
default public String getAverageAsString() {
return asString(getAverage());
}
}
private static Statistics createStatistics(LongSummaryStatistics stats) {
boolean some = stats.getCount() > 0L;
return new Statistics() {
@Override
public long getCount() {
return stats.getCount();
}
@Override
public Duration getMaximum() {
return some ? Duration.ofNanos(stats.getMax()) : Duration.ZERO;
}
@Override
public Duration getMinimum() {
return some ? Duration.ofNanos(stats.getMin()) : Duration.ZERO;
}
@Override
public Duration getTotal() {
return some ? Duration.ofNanos(stats.getSum()) : Duration.ZERO;
}
@Override
public Duration getAverage() {
return some ? Duration.ofNanos((long) stats.getAverage()) : Duration.ZERO;
}
private String fixedLengthSeconds(Duration duration) {
double seconds = duration.toNanos() * 1e-9;
String result = new DecimalFormat("##0.00000").format(seconds) + "s";
if (result.length() == 8) return " " + result;
if (result.length() == 9) return " " + result;
return result;
}
private String fixedLength(long count) {
String result = new DecimalFormat("###0").format(count);
if (result.length() == 1) return " " + result;
if (result.length() == 2) return " " + result;
if (result.length() == 3) return " " + result;
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(fixedLengthSeconds(getTotal()) + " total;");
sb.append(fixedLength(getCount()) + " samples;");
sb.append(fixedLengthSeconds(getAverage()) + " avg;");
sb.append(fixedLengthSeconds(getMinimum()) + " min;");
sb.append(fixedLengthSeconds(getMaximum()) + " max");
return sb.toString();
}
};
}
/**
* Create a new {@link Stopwatch} that can be reused. The resulting {@link Stopwatch#durations()}, however,
* only reflect the most recently completed stopwatch interval.
* <p>
* For example, the following code shows this behavior:
*
* <pre>
* Stopwatch sw = Stopwatch.reusable();
* sw.start();
* sleep(3000); // sleep 3 seconds
* sw.stop();
* print(sw.durations()); // total and average duration are each 3 seconds
*
* sw.start();
* sleep(2000); // sleep 2 seconds
* sw.stop();
* print(sw.durations()); // total and average duration are each 2 seconds
* </pre>
*
* @return the new stopwatch; never null
*/
public static Stopwatch reusable() {
return createWith(new SingleDuration(), null, null);
}
/**
* Create a new {@link Stopwatch} that records all of the measured durations of the stopwatch.
* <p>
* For example, the following code shows this behavior:
*
* <pre>
* Stopwatch sw = Stopwatch.accumulating();
* sw.start();
* sleep(3000); // sleep 3 seconds
* sw.stop();
* print(sw.durations()); // total and average duration are each 3 seconds
*
* sw.start();
* sleep(2000); // sleep 2 seconds
* sw.stop();
* print(sw.durations()); // total duration is now 5 seconds, average is 2.5 seconds
* </pre>
*
* @return the new stopwatch; never null
*/
public static Stopwatch accumulating() {
return createWith(new MultipleDurations(), null, null);
}
/**
* A set of stopwatches whose durations are combined. New stopwatches can be created at any time, and when
* {@link Stopwatch#stop()} will always record their duration with this set.
* <p>
* This set is threadsafe, meaning that multiple threads can {@link #create()} new stopwatches concurrently, and each
* stopwatch's duration is measured separately. Additionally, all of the other methods of this interface are also threadsafe.
* </p>
*
* @author Randall Hauch
*/
@ThreadSafe
public static interface StopwatchSet extends Durations {
/**
* Create a new stopwatch that records durations with this set.
*
* @return the new stopwatch; never null
*/
Stopwatch create();
/**
* Time the given function.
*
* @param runnable the function to call
*/
default public void time(Runnable runnable) {
time(1, runnable);
}
/**
* Time the given function.
*
* @param runnable the function that is to be executed; may not be null
* @return the result of the operation
*/
default public <T> T time(Callable<T> runnable) {
Stopwatch sw = create().start();
try {
return runnable.call();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
sw.stop();
}
}
/**
* Time the given function multiple times.
*
* @param repeat the number of times to repeat the function call; must be positive
* @param runnable the function to call; may not be null
*/
default public void time(int repeat, Runnable runnable) {
for (int i = 0; i != repeat; ++i) {
Stopwatch sw = create().start();
try {
runnable.run();
} finally {
sw.stop();
}
}
}
/**
* Time the given function multiple times.
*
* @param repeat the number of times to repeat the function call; must be positive
* @param runnable the function that is to be executed a number of times; may not be null
* @param cleanup the function that is to be called after each time call to the runnable function, and not included
* in the time measurements; may be null
* @throws Exception the exception thrown by the runnable function
*/
default public <T> void time(int repeat, Callable<T> runnable, Consumer<T> cleanup) throws Exception {
for (int i = 0; i != repeat; ++i) {
T result = null;
Stopwatch sw = create().start();
try {
result = runnable.call();
} finally {
sw.stop();
if (cleanup != null) {
cleanup.accept(result);
}
}
}
}
/**
* Block until all running stopwatches have been {@link Stopwatch#stop() stopped}. This means that if a stopwatch
* is {@link #create() created} but never started, this method will not wait for it. Likewise, if a stopwatch
* is {@link #create() created} and started, then this method will block until the stopwatch is {@link Stopwatch#stop()
* stopped} (even if the same stopwatch is started multiple times).
* are stopped.
*
* @throws InterruptedException if the thread is interrupted before unblocking
*/
void await() throws InterruptedException;
/**
* Block until all stopwatches that have been {@link #create() created} and {@link Stopwatch#start() started} are
* stopped.
*
* @param timeout the maximum length of time that this method should block
* @param unit the unit for the timeout; may not be null
* @throws InterruptedException if the thread is interrupted before unblocking
*/
void await(long timeout, TimeUnit unit) throws InterruptedException;
}
/**
* Create a new set of stopwatches. The resulting object is threadsafe, and each {@link Stopwatch} created by
* {@link StopwatchSet#create()} is also threadsafe.
*
* @return the stopwatches set; never null
*/
public static StopwatchSet multiple() {
MultipleDurations durations = new MultipleDurations();
VariableLatch latch = new VariableLatch(0);
return new StopwatchSet() {
@Override
public Statistics statistics() {
return durations.statistics();
}
@Override
public Stopwatch create() {
return createWith(durations, latch::countUp, latch::countDown);
}
@Override
public void await() throws InterruptedException {
latch.await();
}
@Override
public void await(long timeout, TimeUnit unit) throws InterruptedException {
latch.await(timeout, unit);
}
@Override
public String toString() {
return statistics().toString();
}
};
}
/**
* Create a new stopwatch that updates the given {@link BaseDurations duration}, and optionally has functions to
* be called after the stopwatch is started and stopped.
* <p>
* The resulting stopwatch is threadsafe.
* </p>
*
* @param duration the duration that should be updated; may not be null
* @param uponStart the function that should be called when the stopwatch is successfully started (after not running); may be
* null
* @param uponStop the function that should be called when the stopwatch is successfully stopped (after it was running); may
* be null
* @return the new stopwatch
*/
protected static Stopwatch createWith(BaseDurations duration, Runnable uponStart, Runnable uponStop) {
return new Stopwatch() {
private final AtomicLong started = new AtomicLong(0L);
@Override
public Stopwatch start() {
started.getAndUpdate(existing -> {
if (existing == 0L) {
// Has not yet been started ...
existing = System.nanoTime();
if (uponStart != null) uponStart.run();
}
return existing;
});
return this;
}
@Override
public Stopwatch stop() {
started.getAndUpdate(existing -> {
if (existing != 0L) {
// Is running but has not yet been stopped ...
duration.add(Duration.ofNanos(System.nanoTime() - existing));
if (uponStop != null) uponStop.run();
return 0L;
}
return existing;
});
return this;
}
@Override
public Durations durations() {
return duration;
}
@Override
public String toString() {
return durations().toString();
}
};
}
/**
* Compute the readable string representation of the supplied duration.
*
* @param duration the duration; may not be null
* @return the string representation; never null
*/
protected static String asString(Duration duration) {
return duration.toString().substring(2);
}
/**
* Abstract base class for {@link Durations} implementations.
*
* @author Randall Hauch
*/
@ThreadSafe
protected static abstract class BaseDurations implements Durations {
public abstract void add(Duration duration);
@Override
public String toString() {
return statistics().toString();
}
}
/**
* A {@link Durations} implementation that only remembers the most recently {@link #add(Duration) added} duration.
*
* @author Randall Hauch
*/
@ThreadSafe
private static final class SingleDuration extends BaseDurations {
private final AtomicReference<Statistics> stats = new AtomicReference<>();
@Override
public Statistics statistics() {
return stats.get();
}
@Override
public void add(Duration duration) {
LongSummaryStatistics stats = new LongSummaryStatistics();
if (duration != null) stats.accept(duration.toNanos());
this.stats.set(createStatistics(stats));
}
}
/**
* A {@link Durations} implementation that accumulates all {@link #add(Duration) added} durations.
*
* @author Randall Hauch
*/
@ThreadSafe
private static final class MultipleDurations extends BaseDurations {
private final ConcurrentLinkedQueue<Duration> durations = new ConcurrentLinkedQueue<>();
@Override
public Statistics statistics() {
return createStatistics(durations.stream().mapToLong(Duration::toNanos).summaryStatistics());
}
@Override
public void add(Duration duration) {
durations.add(duration);
}
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.util;
import java.util.StringJoiner;
/**
* Interface that allows a class to more efficiently generate a string that is to be added to other strings via a
* {@link StringJoiner}.
*
* @author Randall Hauch
*/
@FunctionalInterface
public interface Stringifiable {
/**
* Used the supplied {@link StringJoiner} to record string representations of this object. The supplied joiner may concatenate
* these strings together using custom delimiter strings.
*
* @param joiner the string joiner instance that will accumulate the
*/
void asString(StringJoiner joiner);
}

View File

@ -0,0 +1,364 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.util;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import io.debezium.annotation.ThreadSafe;
/**
* String-related utility methods.
*
* @author Randall Hauch
*/
@ThreadSafe
public final class Strings {
/**
* Represents a predicate (boolean-valued function) of one character argument.
*/
@FunctionalInterface
public static interface CharacterPredicate {
/**
* Evaluates this predicate on the given character argument.
*
* @param c the input argument
* @return {@code true} if the input argument matches the predicate, or {@code false} otherwise
*/
boolean test(char c);
}
/**
* Compare two {@link CharSequence} instances.
*
* @param str1 the first character sequence; may be null
* @param str2 the second character sequence; may be null
* @return a negative integer if the first sequence is less than the second, zero if the sequence are equivalent (including if
* both are null), or a positive integer if the first sequence is greater than the second
*/
public static int compareTo(CharSequence str1, CharSequence str2) {
if (str1 == str2) return 0;
if (str1 == null) return -1;
if (str2 == null) return 1;
return str1.toString().compareTo(str2.toString());
}
/**
* Trim away any leading or trailing whitespace characters.
* <p>
* This is semantically equivalent to {@link String#trim()} but instead uses {@link #trim(String, CharacterPredicate)}.
*
* @param str the string to be trimmed; may not be null
* @return the trimmed string; never null
* @see #trim(String,CharacterPredicate)
*/
public static String trim(String str) {
return trim(str, c -> c <= ' '); // same logic as String.trim()
}
/**
* Trim away any leading or trailing characters that satisfy the supplied predicate
*
* @param str the string to be trimmed; may not be null
* @param predicate the predicate function; may not be null
* @return the trimmed string; never null
* @see #trim(String)
*/
public static String trim(String str, CharacterPredicate predicate) {
int len = str.length();
if (len == 0) return str;
int st = 0;
while ((st < len) && predicate.test(str.charAt(st))) {
st++;
}
while ((st < len) && predicate.test(str.charAt(len - 1))) {
len--;
}
return ((st > 0) || (len < str.length())) ? str.substring(st, len) : str;
}
/**
* Create a new string containing the specified character repeated a specific number of times.
*
* @param charToRepeat the character to repeat
* @param numberOfRepeats the number of times the character is to repeat in the result; must be greater than 0
* @return the resulting string
*/
public static String createString( final char charToRepeat,
int numberOfRepeats ) {
assert numberOfRepeats >= 0;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < numberOfRepeats; ++i) {
sb.append(charToRepeat);
}
return sb.toString();
}
/**
* Set the length of the string, padding with the supplied character if the supplied string is shorter than desired, or
* truncating the string if it is longer than desired. Unlike {@link #justifyLeft(String, int, char)}, this method does not
* remove leading and trailing whitespace.
*
* @param original the string for which the length is to be set; may not be null
* @param length the desired length; must be positive
* @param padChar the character to use for padding, if the supplied string is not long enough
* @return the string of the desired length
* @see #justifyLeft(String, int, char)
*/
public static String setLength( String original,
int length,
char padChar ) {
return justifyLeft(original, length, padChar, false);
}
public static enum Justify {
LEFT,
RIGHT,
CENTER;
}
/**
* Justify the contents of the string.
*
* @param justify the way in which the string is to be justified
* @param str the string to be right justified; if null, an empty string is used
* @param width the desired width of the string; must be positive
* @param padWithChar the character to use for padding, if needed
* @return the right justified string
*/
public static String justify( Justify justify,
String str,
final int width,
char padWithChar ) {
switch (justify) {
case LEFT:
return justifyLeft(str, width, padWithChar);
case RIGHT:
return justifyRight(str, width, padWithChar);
case CENTER:
return justifyCenter(str, width, padWithChar);
}
assert false;
return null;
}
/**
* Right justify the contents of the string, ensuring that the string ends at the last character. If the supplied string is
* longer than the desired width, the leading characters are removed so that the last character in the supplied string at the
* last position. If the supplied string is shorter than the desired width, the padding character is inserted one or more
* times such that the last character in the supplied string appears as the last character in the resulting string and that
* the length matches that specified.
*
* @param str the string to be right justified; if null, an empty string is used
* @param width the desired width of the string; must be positive
* @param padWithChar the character to use for padding, if needed
* @return the right justified string
*/
public static String justifyRight( String str,
final int width,
char padWithChar ) {
assert width > 0;
// Trim the leading and trailing whitespace ...
str = str != null ? str.trim() : "";
final int length = str.length();
int addChars = width - length;
if (addChars < 0) {
// truncate the first characters, keep the last
return str.subSequence(length - width, length).toString();
}
// Prepend the whitespace ...
final StringBuilder sb = new StringBuilder();
while (addChars > 0) {
sb.append(padWithChar);
--addChars;
}
// Write the content ...
sb.append(str);
return sb.toString();
}
/**
* Left justify the contents of the string, ensuring that the supplied string begins at the first character and that the
* resulting string is of the desired length. If the supplied string is longer than the desired width, it is truncated to the
* specified length. If the supplied string is shorter than the desired width, the padding character is added to the end of
* the string one or more times such that the length is that specified. All leading and trailing whitespace is removed.
*
* @param str the string to be left justified; if null, an empty string is used
* @param width the desired width of the string; must be positive
* @param padWithChar the character to use for padding, if needed
* @return the left justified string
* @see #setLength(String, int, char)
*/
public static String justifyLeft( String str,
final int width,
char padWithChar ) {
return justifyLeft(str, width, padWithChar, true);
}
protected static String justifyLeft( String str,
final int width,
char padWithChar,
boolean trimWhitespace ) {
// Trim the leading and trailing whitespace ...
str = str != null ? (trimWhitespace ? str.trim() : str) : "";
int addChars = width - str.length();
if (addChars < 0) {
// truncate
return str.subSequence(0, width).toString();
}
// Write the content ...
final StringBuilder sb = new StringBuilder();
sb.append(str);
// Append the whitespace ...
while (addChars > 0) {
sb.append(padWithChar);
--addChars;
}
return sb.toString();
}
/**
* Center the contents of the string. If the supplied string is longer than the desired width, it is truncated to the
* specified length. If the supplied string is shorter than the desired width, padding characters are added to the beginning
* and end of the string such that the length is that specified; one additional padding character is prepended if required.
* All leading and trailing whitespace is removed before centering.
*
* @param str the string to be left justified; if null, an empty string is used
* @param width the desired width of the string; must be positive
* @param padWithChar the character to use for padding, if needed
* @return the left justified string
* @see #setLength(String, int, char)
*/
public static String justifyCenter( String str,
final int width,
char padWithChar ) {
// Trim the leading and trailing whitespace ...
str = str != null ? str.trim() : "";
int addChars = width - str.length();
if (addChars < 0) {
// truncate
return str.subSequence(0, width).toString();
}
// Write the content ...
int prependNumber = addChars / 2;
int appendNumber = prependNumber;
if ((prependNumber + appendNumber) != addChars) {
++prependNumber;
}
final StringBuilder sb = new StringBuilder();
// Prepend the pad character(s) ...
while (prependNumber > 0) {
sb.append(padWithChar);
--prependNumber;
}
// Add the actual content
sb.append(str);
// Append the pad character(s) ...
while (appendNumber > 0) {
sb.append(padWithChar);
--appendNumber;
}
return sb.toString();
}
/**
* Get the stack trace of the supplied exception.
*
* @param throwable the exception for which the stack trace is to be returned
* @return the stack trace, or null if the supplied exception is null
*/
public static String getStackTrace(Throwable throwable) {
if (throwable == null) return null;
final ByteArrayOutputStream bas = new ByteArrayOutputStream();
final PrintWriter pw = new PrintWriter(bas);
throwable.printStackTrace(pw);
pw.close();
return bas.toString();
}
/**
* Parse the supplied string as a integer value.
*
* @param value the string representation of a integer value
* @param defaultValue the value to return if the string value is null or cannot be parsed as an int
* @return the int value
*/
public static int asInt(String value, int defaultValue) {
if (value != null) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
}
}
return defaultValue;
}
/**
* Parse the supplied string as a long value.
*
* @param value the string representation of a long value
* @param defaultValue the value to return if the string value is null or cannot be parsed as a long
* @return the long value
*/
public static long asLong(String value, long defaultValue) {
if (value != null) {
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
}
}
return defaultValue;
}
/**
* Parse the supplied string as a double value.
*
* @param value the string representation of a double value
* @param defaultValue the value to return if the string value is null or cannot be parsed as a double
* @return the double value
*/
public static double asDouble(String value, double defaultValue) {
if (value != null) {
try {
return Double.parseDouble(value);
} catch (NumberFormatException e) {
}
}
return defaultValue;
}
/**
* Parse the supplied string as a boolean value.
*
* @param value the string representation of a boolean value
* @param defaultValue the value to return if the string value is null or cannot be parsed as a boolean
* @return the boolean value
*/
public static boolean asBoolean(String value, boolean defaultValue) {
if (value != null) {
try {
return Boolean.parseBoolean(value);
} catch (NumberFormatException e) {
}
}
return defaultValue;
}
private Strings() {
}
}

View File

@ -0,0 +1,234 @@
/*
* Based on java.util.concurrent.CountDownLatch, which was
* Written by Doug Lea with assistance from members of JCP JSR-166
* Expert Group and released to the public domain, as explained at
* http://creativecommons.org/publicdomain/zero/1.0/
*
* Any changes relative to CountDownLatch are also released to the
* public domain, as explained at
* http://creativecommons.org/publicdomain/zero/1.0/
*/
package io.debezium.util;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import io.debezium.annotation.ThreadSafe;
/**
* A latch that works similarly to {@link CountDownLatch} except that it can also increase the count dynamically.
*/
@ThreadSafe
public class VariableLatch {
/**
* Create a new variable latch.
*
* @return the variable latch; never null
*/
public static VariableLatch create() {
return create(0);
}
/**
* Create a new variable latch.
*
* @param initialValue the initial number of latches
* @return the variable latch; never null
*/
public static VariableLatch create(int initialValue) {
return new VariableLatch(0);
}
/**
* Synchronization control For CountDownLatch.
* Uses AQS state to represent count.
*/
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
@Override
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
@Override
protected boolean tryReleaseShared(int releases) {
// Increment or decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0 && releases >= 0) return false;
int nextc = c - releases;
if (nextc < 0) nextc = 0;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
private final Sync sync;
/**
* Constructs a {@code CountDownLatch} initialized with the given count.
*
* @param count the number of times {@link #countDown} must be invoked
* before threads can pass through {@link #await}
* @throws IllegalArgumentException if {@code count} is negative
*/
public VariableLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
/**
* Causes the current thread to wait until the latch has counted down to
* zero, unless the thread is {@linkplain Thread#interrupt interrupted}.
*
* <p>
* If the current count is zero then this method returns immediately.
*
* <p>
* If the current count is greater than zero then the current thread becomes disabled for thread scheduling purposes and lies
* dormant until one of two things happen:
* <ul>
* <li>The count reaches zero due to invocations of the {@link #countDown} method; or
* <li>Some other thread {@linkplain Thread#interrupt interrupts} the current thread.
* </ul>
*
* <p>
* If the current thread:
* <ul>
* <li>has its interrupted status set on entry to this method; or
* <li>is {@linkplain Thread#interrupt interrupted} while waiting,
* </ul>
* then {@link InterruptedException} is thrown and the current thread's interrupted status is cleared.
*
* @throws InterruptedException if the current thread is interrupted
* while waiting
*/
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
/**
* Causes the current thread to wait until the latch has counted down to
* zero, unless the thread is {@linkplain Thread#interrupt interrupted},
* or the specified waiting time elapses.
*
* <p>
* If the current count is zero then this method returns immediately with the value {@code true}.
*
* <p>
* If the current count is greater than zero then the current thread becomes disabled for thread scheduling purposes and lies
* dormant until one of three things happen:
* <ul>
* <li>The count reaches zero due to invocations of the {@link #countDown} method; or
* <li>Some other thread {@linkplain Thread#interrupt interrupts} the current thread; or
* <li>The specified waiting time elapses.
* </ul>
*
* <p>
* If the count reaches zero then the method returns with the value {@code true}.
*
* <p>
* If the current thread:
* <ul>
* <li>has its interrupted status set on entry to this method; or
* <li>is {@linkplain Thread#interrupt interrupted} while waiting,
* </ul>
* then {@link InterruptedException} is thrown and the current thread's interrupted status is cleared.
*
* <p>
* If the specified waiting time elapses then the value {@code false} is returned. If the time is less than or equal to zero,
* the method will not wait at all.
*
* @param timeout the maximum time to wait
* @param unit the time unit of the {@code timeout} argument
* @return {@code true} if the count reached zero and {@code false} if the waiting time elapsed before the count reached zero
* @throws InterruptedException if the current thread is interrupted
* while waiting
*/
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
/**
* Decrements the count of the latch, releasing all waiting threads if the count reaches zero.
*
* <p>
* If the current count is greater than zero then it is decremented. If the new count is zero then all waiting threads are
* re-enabled for thread scheduling purposes.
*
* <p>
* If the current count equals zero then nothing happens.
*/
public void countDown() {
sync.releaseShared(1);
}
/**
* Decrements the count of the latch, releasing all waiting threads if the count reaches zero.
*
* <p>
* If the current count is greater than zero then it is decremented. If the new count is zero then all waiting threads are
* re-enabled for thread scheduling purposes.
*
* <p>
* If the current count equals zero then nothing happens.
*
* @param count the number of counts to decrease
*/
public void countDown(int count) {
sync.releaseShared(1 * (Math.abs(count)));
}
/**
* Increments the count of the latch by one.
*/
public void countUp() {
sync.releaseShared(-1);
}
/**
* Increments the count of the latch by a positive number.
*
* @param count the number of counts to increase
*/
public void countUp(int count) {
sync.releaseShared(-1 * (Math.abs(count)));
}
/**
* Returns the current count.
*
* <p>
* This method is typically used for debugging and testing purposes.
*
* @return the current count
*/
public long getCount() {
return sync.getCount();
}
/**
* Returns a string identifying this latch, as well as its state.
* The state, in brackets, includes the String {@code "Count ="} followed by the current count.
*
* @return a string identifying this latch, as well as its state
*/
@Override
public String toString() {
return super.toString() + "[Count = " + sync.getCount() + "]";
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.jdbc;
import io.debezium.config.Configuration;
public class TestDatabase {
public static JdbcConfiguration testConfig( String databaseName ) {
return buildTestConfig().withDatabase(databaseName).build();
}
public static JdbcConfiguration.Builder buildTestConfig() {
return JdbcConfiguration.copy(Configuration.fromSystemProperties("database."));
}
}

View File

@ -1,6 +1,6 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.injest.jdbc.util;

View File

@ -19,7 +19,7 @@
<dependencies>
<dependency>
<groupId>io.debezium</groupId>
<artifactId>debezium-ingest-jdbc</artifactId>
<artifactId>debezium-core</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
@ -36,7 +36,7 @@
<!-- Testing -->
<dependency>
<groupId>io.debezium</groupId>
<artifactId>debezium-ingest-jdbc</artifactId>
<artifactId>debezium-core</artifactId>
<type>test-jar</type>
</dependency>
<dependency>
@ -63,7 +63,7 @@
work on all platforms. We'll set some of these as system properties during integration testing.
-->
<database.port>3306</database.port>
<database.username>mysqluser</database.username>
<database.user>mysqluser</database.user>
<database.password>mysqlpw</database.password>
</properties>
<build>
@ -97,7 +97,7 @@
<env>
<MYSQL_ROOT_PASSWORD>debezium-rocks</MYSQL_ROOT_PASSWORD>
<MYSQL_DATABASE>mysql</MYSQL_DATABASE> <!-- database created upon init -->
<MYSQL_USER>${database.username}</MYSQL_USER>
<MYSQL_USER>${database.user}</MYSQL_USER>
<MYSQL_PASSWORD>${database.password}</MYSQL_PASSWORD>
</env>
<ports>
@ -165,7 +165,7 @@
<!-- Make these available to the tests via system properties -->
<database.hostname>${dockerhost.ip}</database.hostname>
<database.port>${database.port}</database.port>
<database.username>${database.username}</database.username>
<database.user>${database.user}</database.user>
<database.password>${database.password}</database.password>
</systemPropertyVariables>
</configuration>
@ -211,6 +211,6 @@
<include>**/*</include>
</includes>
</testResource>
</testResources>
</testResources>
</build>
</project></project>

View File

@ -0,0 +1,38 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.ingest.mysql;
import io.debezium.config.Configuration;
import io.debezium.jdbc.JdbcConnection;
/**
* A utility for working with MySQL connections.
* @author Randall Hauch
*/
public class MySQLConnection extends JdbcConnection {
protected static ConnectionFactory FACTORY = JdbcConnection.patternBasedFactory("jdbc:mysql://${hostname}:${port}/${dbname}");
/**
* Create a new instance with the given configuration and connection factory.
*
* @param config the configuration; may not be null
*/
public MySQLConnection(Configuration config) {
super(config, FACTORY);
}
/**
* Create a new instance with the given configuration and connection factory, and specify the operations that should be
* run against each newly-established connection.
*
* @param config the configuration; may not be null
* @param initialOperations the initial operations that should be run on each new connection; may be null
*/
public MySQLConnection(Configuration config, Operations initialOperations) {
super(config, FACTORY, initialOperations);
}
}

View File

@ -0,0 +1,10 @@
# In production you would almost certainly limit the replication user must be on the follower (slave) machine,
# to prevent other clients accessing the log from other machines. For example, 'replicator'@'follower.acme.com'.
#
# However, this grant is equivalent to specifying *any* hosts, which makes this easier since the docker host
# is not easily known to the Docker container
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'replicator' IDENTIFIED BY 'replpass';
# Create the database that we'll use to populate data and watch the effect in the binlog
CREATE DATABASE readbinlog_test;
GRANT ALL PRIVILEGES ON readbinlog_test.* TO 'mysqluser'@'%';

View File

@ -1,28 +1,29 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.ingest.mysql;
import java.sql.Connection;
import java.sql.SQLException;
import org.junit.Test;
import io.debezium.injest.jdbc.util.TestDatabase;
import io.debezium.jdbc.TestDatabase;
public class ConnectionIT {
@Test
public void shouldConnectToDefaulDatabase() throws SQLException {
try (Connection conn = TestDatabase.connect("jdbc:mysql://${hostname}:${port}/mysql");) {
try (MySQLConnection conn = new MySQLConnection( TestDatabase.testConfig("mysql") );) {
conn.connect();
}
}
@Test
public void shouldConnectToEmptyDatabase() throws SQLException {
try (Connection conn = TestDatabase.connect("jdbc:mysql://${hostname}:${port}/emptydb");) {
try (MySQLConnection conn = new MySQLConnection( TestDatabase.testConfig("emptydb") );) {
conn.connect();
}
}
}

View File

@ -0,0 +1,468 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.ingest.mysql;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.shyiko.mysql.binlog.BinaryLogClient;
import com.github.shyiko.mysql.binlog.BinaryLogClient.EventListener;
import com.github.shyiko.mysql.binlog.BinaryLogClient.LifecycleListener;
import com.github.shyiko.mysql.binlog.event.DeleteRowsEventData;
import com.github.shyiko.mysql.binlog.event.Event;
import com.github.shyiko.mysql.binlog.event.EventData;
import com.github.shyiko.mysql.binlog.event.EventType;
import com.github.shyiko.mysql.binlog.event.QueryEventData;
import com.github.shyiko.mysql.binlog.event.TableMapEventData;
import com.github.shyiko.mysql.binlog.event.UpdateRowsEventData;
import com.github.shyiko.mysql.binlog.event.WriteRowsEventData;
import com.github.shyiko.mysql.binlog.event.XidEventData;
import static org.fest.assertions.Assertions.assertThat;
import io.debezium.jdbc.JdbcConfiguration;
import io.debezium.jdbc.TestDatabase;
public class ReadBinLogIT {
protected static final Logger LOGGER = LoggerFactory.getLogger(ReadBinLogIT.class);
protected static final long DEFAULT_TIMEOUT = TimeUnit.SECONDS.toMillis(3);
private JdbcConfiguration config;
private EventCounters counters;
private BinaryLogClient client;
private MySQLConnection conn;
private List<Event> events = new ArrayList<>();
@Before
public void beforeEach() throws TimeoutException, IOException, SQLException, InterruptedException {
events.clear();
config = TestDatabase.buildTestConfig().withDatabase("readbinlog_test").build();
// Connect the normal SQL client ...
conn = new MySQLConnection(config);
conn.connect();
// Connect the bin log client ...
counters = new EventCounters();
client = new BinaryLogClient(config.getHostname(), config.getPort(), "replicator", "replpass");
client.setServerId(client.getServerId() - 1); // avoid clashes between BinaryLogClient instances
client.setKeepAlive(false);
client.registerEventListener(this::logEvent);
client.registerEventListener(counters);
client.registerEventListener(this::recordEvent);
client.registerLifecycleListener(new TraceLifecycleListener());
client.connect(DEFAULT_TIMEOUT); // does not block
// Set up the table as one transaction and wait to see the events ...
conn.execute("DROP TABLE IF EXISTS person",
"CREATE TABLE person (name VARCHAR(255) primary key)");
counters.waitFor(2, EventType.QUERY, DEFAULT_TIMEOUT);
counters.reset();
}
@After
public void afterEach() throws IOException, SQLException {
events.clear();
try {
if (client != null) client.disconnect();
} finally {
client = null;
try {
if (conn != null) conn.close();
} finally {
conn = null;
}
}
}
@Test
public void shouldCaptureSingleWriteUpdateDeleteEvents() throws Exception {
// write/insert
conn.execute("INSERT INTO person VALUES ('Georgia')");
counters.waitFor(1, WriteRowsEventData.class, DEFAULT_TIMEOUT);
List<WriteRowsEventData> writeRowEvents = recordedEventData(WriteRowsEventData.class, 1);
assertRows(writeRowEvents.get(0), rows().insertedRow("Georgia"));
// update
conn.execute("UPDATE person SET name = 'Maggie' WHERE name = 'Georgia'");
counters.waitFor(1, UpdateRowsEventData.class, DEFAULT_TIMEOUT);
List<UpdateRowsEventData> updateRowEvents = recordedEventData(UpdateRowsEventData.class, 1);
assertRows(updateRowEvents.get(0), rows().changeRow("Georgia").to("Maggie"));
// delete
conn.execute("DELETE FROM person WHERE name = 'Maggie'");
counters.waitFor(1, DeleteRowsEventData.class, DEFAULT_TIMEOUT);
List<DeleteRowsEventData> deleteRowEvents = recordedEventData(DeleteRowsEventData.class, 1);
assertRows(deleteRowEvents.get(0), rows().removedRow("Maggie"));
}
@Test
public void shouldCaptureMultipleWriteUpdateDeleteEvents() throws Exception {
// write/insert as a single transaction
conn.execute("INSERT INTO person VALUES ('Georgia')",
"INSERT INTO person VALUES ('Janice')");
counters.waitFor(1, QueryEventData.class, DEFAULT_TIMEOUT); // BEGIN
counters.waitFor(1, TableMapEventData.class, DEFAULT_TIMEOUT);
counters.waitFor(2, WriteRowsEventData.class, DEFAULT_TIMEOUT);
counters.waitFor(1, XidEventData.class, DEFAULT_TIMEOUT); // COMMIT
List<WriteRowsEventData> writeRowEvents = recordedEventData(WriteRowsEventData.class, 2);
assertRows(writeRowEvents.get(0), rows().insertedRow("Georgia"));
assertRows(writeRowEvents.get(1), rows().insertedRow("Janice"));
counters.reset();
// update as a single transaction
conn.execute("UPDATE person SET name = 'Maggie' WHERE name = 'Georgia'",
"UPDATE person SET name = 'Jamie' WHERE name = 'Janice'");
counters.waitFor(1, QueryEventData.class, DEFAULT_TIMEOUT); // BEGIN
counters.waitFor(1, TableMapEventData.class, DEFAULT_TIMEOUT);
counters.waitFor(2, UpdateRowsEventData.class, DEFAULT_TIMEOUT);
counters.waitFor(1, XidEventData.class, DEFAULT_TIMEOUT); // COMMIT
List<UpdateRowsEventData> updateRowEvents = recordedEventData(UpdateRowsEventData.class, 2);
assertRows(updateRowEvents.get(0), rows().changeRow("Georgia").to("Maggie"));
assertRows(updateRowEvents.get(1), rows().changeRow("Janice").to("Jamie"));
counters.reset();
// delete as a single transaction
conn.execute("DELETE FROM person WHERE name = 'Maggie'",
"DELETE FROM person WHERE name = 'Jamie'");
counters.waitFor(1, QueryEventData.class, DEFAULT_TIMEOUT); // BEGIN
counters.waitFor(1, TableMapEventData.class, DEFAULT_TIMEOUT);
counters.waitFor(2, DeleteRowsEventData.class, DEFAULT_TIMEOUT);
counters.waitFor(1, XidEventData.class, DEFAULT_TIMEOUT); // COMMIT
List<DeleteRowsEventData> deleteRowEvents = recordedEventData(DeleteRowsEventData.class, 2);
assertRows(deleteRowEvents.get(0), rows().removedRow("Maggie"));
assertRows(deleteRowEvents.get(1), rows().removedRow("Jamie"));
}
@Test
public void shouldCaptureMultipleWriteUpdateDeletesInSingleEvents() throws Exception {
// write/insert as a single statement/transaction
conn.execute("INSERT INTO person VALUES ('Georgia'),('Janice')");
counters.waitFor(1, QueryEventData.class, DEFAULT_TIMEOUT); // BEGIN
counters.waitFor(1, TableMapEventData.class, DEFAULT_TIMEOUT);
counters.waitFor(1, WriteRowsEventData.class, DEFAULT_TIMEOUT);
counters.waitFor(1, XidEventData.class, DEFAULT_TIMEOUT); // COMMIT
List<WriteRowsEventData> writeRowEvents = recordedEventData(WriteRowsEventData.class, 1);
assertRows(writeRowEvents.get(0), rows().insertedRow("Georgia").insertedRow("Janice"));
counters.reset();
// update as a single statement/transaction
conn.execute("UPDATE person SET name = CASE " +
" WHEN name = 'Georgia' THEN 'Maggie' " +
" WHEN name = 'Janice' THEN 'Jamie' " +
" END " +
"WHERE name IN ('Georgia','Janice')");
counters.waitFor(1, QueryEventData.class, DEFAULT_TIMEOUT); // BEGIN
counters.waitFor(1, TableMapEventData.class, DEFAULT_TIMEOUT);
counters.waitFor(1, UpdateRowsEventData.class, DEFAULT_TIMEOUT);
counters.waitFor(1, XidEventData.class, DEFAULT_TIMEOUT); // COMMIT
List<UpdateRowsEventData> updateRowEvents = recordedEventData(UpdateRowsEventData.class, 1);
assertRows(updateRowEvents.get(0), rows().changeRow("Georgia").to("Maggie").changeRow("Janice").to("Jamie"));
counters.reset();
// delete as a single statement/transaction
conn.execute("DELETE FROM person WHERE name IN ('Maggie','Jamie')");
counters.waitFor(1, QueryEventData.class, DEFAULT_TIMEOUT); // BEGIN
counters.waitFor(1, TableMapEventData.class, DEFAULT_TIMEOUT);
counters.waitFor(1, DeleteRowsEventData.class, DEFAULT_TIMEOUT);
counters.waitFor(1, XidEventData.class, DEFAULT_TIMEOUT); // COMMIT
List<DeleteRowsEventData> deleteRowEvents = recordedEventData(DeleteRowsEventData.class, 1);
assertRows(deleteRowEvents.get(0), rows().removedRow("Maggie").removedRow("Jamie"));
}
@Test
public void shouldQueryInformationSchema() throws Exception {
// long tableId = writeRows.getTableId();
// BitSet columnIds = writeRows.getIncludedColumns();
//
// conn.query("select TABLE_NAME, ROW_FORMAT, TABLE_ROWS, AVG_ROW_LENGTH, DATA_LENGTH, MAX_DATA_LENGTH, INDEX_LENGTH, DATA_FREE, " +
// "AUTO_INCREMENT, CREATE_TIME, UPDATE_TIME, CHECK_TIME, TABLE_COLLATION, CHECKSUM, CREATE_OPTIONS, TABLE_COMMENT " +
// "from INFORMATION_SCHEMA.TABLES " +
// "where TABLE_SCHEMA like 'readbinlog_test' and TABLE_NAME like 'person'", conn::print);
// conn.query("select TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, " +
// "DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, CHARACTER_OCTET_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE, " +
// "CHARACTER_SET_NAME, COLLATION_NAME from INFORMATION_SCHEMA.COLUMNS " +
// "where TABLE_SCHEMA like 'readbinlog_test' and TABLE_NAME like 'person'", conn::print);
}
protected void logEvent(Event event) {
LOGGER.info("Received event: " + event);
}
protected void recordEvent(Event event) {
synchronized (events) {
events.add(event);
}
}
protected <T extends EventData> List<T> recordedEventData(Class<T> eventType, int expectedCount) {
List<T> results = null;
synchronized (events) {
results = events.stream().map(Event::getData).filter(eventType::isInstance).map(eventType::cast).collect(Collectors.toList());
}
if (expectedCount > -1) {
assertThat(results.size()).isEqualTo(expectedCount);
}
return results;
}
protected void assertRow(Serializable[] data, Serializable... expected) {
assertThat(data.length).isEqualTo(expected.length);
assertThat(data).contains((Object[]) expected);
}
protected void assertRows( WriteRowsEventData eventData, int numRowsInEvent, Serializable... expectedValuesInRows ) {
assertThat(eventData.getRows().size()).isEqualTo(numRowsInEvent);
int valuePosition = 0;
for (Serializable[] row : eventData.getRows() ) {
for ( Serializable value : row ) {
assertThat(value).isEqualTo(expectedValuesInRows[valuePosition++]);
}
}
}
public static class Row {
public Serializable[] fromValues;
public Serializable[] toValues;
}
public static interface UpdateBuilder {
RowBuilder to( Serializable...values );
}
public static class RowBuilder {
private List<Row> rows = new ArrayList<>();
private Row nextRow = null;
public RowBuilder insertedRow( Serializable...values ) {
maybeAddRow();
return changeRow().to(values);
}
public RowBuilder removedRow( Serializable...values ) {
maybeAddRow();
return changeRow(values).to(values);
}
public UpdateBuilder changeRow( Serializable...values ) {
maybeAddRow();
nextRow = new Row();
nextRow.fromValues = values;
return new UpdateBuilder() {
@Override
public RowBuilder to(Serializable... values) {
nextRow.toValues = values;
return RowBuilder.this;
}
};
}
protected void maybeAddRow() {
if ( nextRow != null ) {
rows.add(nextRow);
nextRow = null;
}
}
protected List<Row> rows() {
maybeAddRow();
return rows;
}
protected boolean findInsertedRow( Serializable[] values ) {
maybeAddRow();
for ( Iterator<Row> iter = rows.iterator(); iter.hasNext(); ) {
Row expectedRow = iter.next();
if ( Arrays.deepEquals(expectedRow.toValues,values)) {
iter.remove();
return true;
}
}
return false;
}
protected boolean findDeletedRow( Serializable[] values ) {
maybeAddRow();
for ( Iterator<Row> iter = rows.iterator(); iter.hasNext(); ) {
Row expectedRow = iter.next();
if ( Arrays.deepEquals(expectedRow.fromValues,values)) {
iter.remove();
return true;
}
}
return false;
}
protected boolean findUpdatedRow( Serializable[] oldValues, Serializable[] newValues ) {
maybeAddRow();
for ( Iterator<Row> iter = rows.iterator(); iter.hasNext(); ) {
Row expectedRow = iter.next();
if ( Arrays.deepEquals(expectedRow.fromValues,oldValues) && Arrays.deepEquals(expectedRow.toValues,newValues)) {
iter.remove();
return true;
}
}
return false;
}
}
protected RowBuilder rows() {
return new RowBuilder();
}
protected void assertRows( UpdateRowsEventData eventData, RowBuilder rows ) {
assertThat(eventData.getRows().size()).isEqualTo(rows.rows().size());
for (Map.Entry<Serializable[], Serializable[]> row : eventData.getRows() ) {
if ( !rows.findUpdatedRow(row.getKey(), row.getValue()) ) {
fail("Failed to find updated row: " + eventData );
}
}
}
protected void assertRows( WriteRowsEventData eventData, RowBuilder rows ) {
assertThat(eventData.getRows().size()).isEqualTo(rows.rows().size());
for (Serializable[] removedRow : eventData.getRows() ) {
if ( !rows.findInsertedRow(removedRow) ) {
fail("Failed to find inserted row: " + eventData );
}
}
}
protected void assertRows( DeleteRowsEventData eventData, RowBuilder rows ) {
assertThat(eventData.getRows().size()).isEqualTo(rows.rows().size());
for (Serializable[] removedRow : eventData.getRows() ) {
if ( !rows.findDeletedRow(removedRow) ) {
fail("Failed to find removed row: " + eventData );
}
}
}
protected static class EventCounters implements EventListener {
/*
* VariableLatch instances count down when receiving an event, and thus are negative. When callers wait for a specified
* number of events to occur, the latch's count is incremented by the expected count. If the latch's resulting count is
* less than or equal to 0, then the caller does not wait.
*/
private final ConcurrentMap<EventType, AtomicInteger> counterByType = new ConcurrentHashMap<>();
private final ConcurrentMap<Class<? extends EventData>, AtomicInteger> counterByDataClass = new ConcurrentHashMap<>();
@Override
public void onEvent(Event event) {
counterByType.compute(event.getHeader().getEventType(), this::increment);
EventData data = event.getData();
if (data != null) {
counterByDataClass.compute(data.getClass(), this::increment);
}
}
protected <K> AtomicInteger increment(K key, AtomicInteger counter) {
if (counter == null) return new AtomicInteger(1);
synchronized (counter) {
counter.incrementAndGet();
counter.notify();
}
return counter;
}
/**
* Blocks until the listener has seen the specified number of events with the given type.
*
* @param eventCount the number of events
* @param type the type of event
* @param timeoutMillis the maximum amount of time in milliseconds that this method should block
* @throws InterruptedException if the thread was interrupted while waiting
* @throws TimeoutException if the waiting timed out before the expected number of events were received
*/
public void waitFor(int eventCount, EventType type, long timeoutMillis) throws InterruptedException, TimeoutException {
waitFor(type.name(), () -> counterByType.get(type), eventCount, timeoutMillis);
}
/**
* Blocks until the listener has seen the specified number of events with the given type.
*
* @param eventCount the number of events
* @param eventDataClass the EventData subclass
* @param timeoutMillis the maximum amount of time in milliseconds that this method should block
* @throws InterruptedException if the thread was interrupted while waiting
* @throws TimeoutException if the waiting timed out before the expected number of events were received
*/
public void waitFor(int eventCount, Class<? extends EventData> eventDataClass, long timeoutMillis)
throws InterruptedException, TimeoutException {
waitFor(eventDataClass.getSimpleName(), () -> counterByDataClass.get(eventDataClass), eventCount, timeoutMillis);
}
private void waitFor(String eventTypeName, Supplier<AtomicInteger> counterGetter, int eventCount, long timeoutMillis)
throws InterruptedException, TimeoutException {
// Get the counter, and prepare for it to be null ...
AtomicInteger counter = null;
long stopTime = System.currentTimeMillis() + timeoutMillis;
do {
counter = counterGetter.get();
} while (counter == null && System.currentTimeMillis() <= stopTime);
if (counter == null) {
// Did not even find a counter in this timeframe ...
throw new TimeoutException("Timed out while waiting for " + eventCount + " " + eventTypeName + " events");
}
synchronized (counter) {
counter.addAndGet(-eventCount);
if (counter.get() != 0) {
counter.wait(timeoutMillis);
if (counter.get() != 0) {
throw new TimeoutException("Timed out while waiting for " + eventCount + " " + eventTypeName + " events");
}
}
}
}
/**
* Clear all counters.
*/
public void reset() {
counterByDataClass.clear();
counterByType.clear();
}
}
protected static class TraceLifecycleListener implements LifecycleListener {
@Override
public void onDisconnect(BinaryLogClient client) {
LOGGER.info("Client disconnected");
}
@Override
public void onConnect(BinaryLogClient client) {
LOGGER.info("Client connected");
}
@Override
public void onCommunicationFailure(BinaryLogClient client, Exception ex) {
LOGGER.error("Client communication failure", ex);
}
@Override
public void onEventDeserializationFailure(BinaryLogClient client, Exception ex) {
LOGGER.error("Client received event deserialization failure", ex);
}
}
}

View File

@ -19,7 +19,7 @@
<dependencies>
<dependency>
<groupId>io.debezium</groupId>
<artifactId>debezium-ingest-jdbc</artifactId>
<artifactId>debezium-core</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
@ -32,7 +32,7 @@
<!-- Testing -->
<dependency>
<groupId>io.debezium</groupId>
<artifactId>debezium-ingest-jdbc</artifactId>
<artifactId>debezium-core</artifactId>
<type>test-jar</type>
</dependency>
<dependency>
@ -59,7 +59,7 @@
work on all platforms. We'll set some of these as system properties during integration testing.
-->
<database.port>5432</database.port>
<database.username>postgres</database.username>
<database.user>postgres</database.user>
<database.password>postgres</database.password>
</properties>
<build>
@ -91,7 +91,7 @@
</bind>
</volumes-->
<env>
<POSTGRES_USER>${database.username}</POSTGRES_USER>
<POSTGRES_USER>${database.user}</POSTGRES_USER>
<POSTGRES_PASSWORD>${database.password}</POSTGRES_PASSWORD>
</env>
<ports>
@ -159,7 +159,7 @@
<!-- Make these available to the tests via system properties -->
<database.hostname>${dockerhost.ip}</database.hostname>
<database.port>${database.port}</database.port>
<database.username>${database.username}</database.username>
<database.user>${database.user}</database.user>
<database.password>${database.password}</database.password>
</systemPropertyVariables>
</configuration>

View File

@ -0,0 +1,38 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.ingest.postgres;
import io.debezium.config.Configuration;
import io.debezium.jdbc.JdbcConnection;
/**
* A utility for working with MySQL connections.
* @author Randall Hauch
*/
public class PostgresConnection extends JdbcConnection {
protected static ConnectionFactory FACTORY = JdbcConnection.patternBasedFactory("jdbc:postgresql://${hostname}:${port}/${dbname}");
/**
* Create a new instance with the given configuration and connection factory.
*
* @param config the configuration; may not be null
*/
public PostgresConnection(Configuration config) {
super(config, FACTORY);
}
/**
* Create a new instance with the given configuration and connection factory, and specify the operations that should be
* run against each newly-established connection.
*
* @param config the configuration; may not be null
* @param initialOperations the initial operations that should be run on each new connection; may be null
*/
public PostgresConnection(Configuration config, Operations initialOperations) {
super(config, FACTORY, initialOperations);
}
}

View File

@ -1,28 +1,30 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.ingest.postgresql;
import java.sql.Connection;
import java.sql.SQLException;
import org.junit.Test;
import io.debezium.injest.jdbc.util.TestDatabase;
import io.debezium.ingest.postgres.PostgresConnection;
import io.debezium.jdbc.TestDatabase;
public class ConnectionIT {
@Test
public void shouldConnectToDefaultDatabase() throws SQLException {
try (Connection conn = TestDatabase.connect("jdbc:postgresql://${hostname}:${port}/postgres");) {
public void shouldConnectToDefaulDatabase() throws SQLException {
try (PostgresConnection conn = new PostgresConnection( TestDatabase.testConfig("postgres") );) {
conn.connect();
}
}
@Test
public void shouldConnectToEmptyDatabase() throws SQLException {
try (Connection conn = TestDatabase.connect("jdbc:postgresql://${hostname}:${port}/emptydb");) {
try (PostgresConnection conn = new PostgresConnection( TestDatabase.testConfig("emptydb") );) {
conn.connect();
}
}
}

13
pom.xml
View File

@ -81,6 +81,7 @@
</properties>
<modules>
<module>support/checkstyle</module>
<module>debezium-core</module>
<module>debezium-ingest-jdbc</module>
<module>debezium-ingest-postgres</module>
<module>debezium-ingest-mysql</module>
@ -147,6 +148,11 @@
</dependency>
<!-- Debezium artifacts -->
<dependency>
<groupId>io.debezium</groupId>
<artifactId>debezium-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.debezium</groupId>
<artifactId>debezium-ingest-jdbc</artifactId>
@ -169,6 +175,13 @@
</dependency>
<!-- Debezium test artifacts -->
<dependency>
<groupId>io.debezium</groupId>
<artifactId>debezium-core</artifactId>
<version>${project.version}</version>
<scope>test</scope>
<type>test-jar</type>
</dependency>
<dependency>
<groupId>io.debezium</groupId>
<artifactId>debezium-ingest-jdbc</artifactId>

View File

@ -1,6 +1,6 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.checkstyle;

View File

@ -1,6 +1,6 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.checkstyle;

View File

@ -1,6 +1,6 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.checkstyle;

View File

@ -1,6 +1,6 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.checkstyle;

View File

@ -1,6 +1,6 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.checkstyle;

View File

@ -14,9 +14,9 @@
<!--property name="fileExtensions" value="java,cnd"/-->
<property name="fileExtensions" value="java"/>
<!-- Exclude any classes that keep their own copyright and header -->
<property name="excludedClasses" value=""/>
<property name="excludedClasses" value="io.debezium.util.VariableLatch"/>
<!-- Exclude any directories that contain completely different headers -->
<!-- property name="excludedFilesRegex" value="io/debezium/something"/-->
<property name="excludedFilesRegex" value="io/debezium/annotation/.*"/>
</module>
<module name="TreeWalker">
@ -103,7 +103,7 @@
<!-- Required to get SuppressionCommentFilter to work -->
<module name="FileContentsHolder" />
<!-- Checks that ModeShape is expressly NOT using -->
<!-- Checks that Debezium is expressly NOT using -->
<!--module name="ModifiedControlVariable" /-->
<!--module name="DeclarationOrder" /-->
<!--module name="InnerTypeLast" /-->

View File

@ -1,5 +1,5 @@
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/

View File

@ -6,10 +6,10 @@
enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.filecomment"
name="filecomment">
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
</template>
</templates>