/**
* Copyright 2011-2017 Asakusa Framework Team.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.asakusafw.windgate.jdbc;
import java.io.IOException;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.asakusafw.windgate.core.WindGateLogger;
import com.asakusafw.windgate.core.resource.ResourceProfile;
import com.asakusafw.windgate.core.util.PropertiesUtil;
/**
* A structured profile for {@link JdbcResourceMirror}.
* @since 0.2.2
* @since 0.9.0
*/
public class JdbcProfile {
static final WindGateLogger WGLOG = new JdbcLogger(JdbcProfile.class);
static final Logger LOG = LoggerFactory.getLogger(JdbcProfile.class);
/**
* The profile key of fully qualified JDBC driver class name.
*/
public static final String KEY_DRIVER = "driver";
/**
* The profile key of database URL.
*/
public static final String KEY_URL = "url";
/**
* The profile key of database user name.
*/
public static final String KEY_USER = "user";
/**
* The profile key of database connection password.
*/
public static final String KEY_PASSWORD = "password";
/**
* The profile key of {@link #getBatchPutUnit()}.
*/
public static final String KEY_BATCH_PUT_UNIT = "batchPutUnit";
/**
* The profile key of {@link #getBatchGetUnit()}.
* @since 0.2.4
*/
public static final String KEY_BATCH_GET_UNIT = "batchGetUnit";
/**
* The profile key of {@link #getConnectRetryCount()}.
* @since 0.2.4
*/
public static final String KEY_CONNECT_RETRY_COUNT = "connect.retryCount";
/**
* The profile key of {@link #getConnectRetryInterval()}.
* @since 0.2.4
*/
public static final String KEY_CONNECT_RETRY_INTERVAL = "connect.retryInterval";
/**
* The profile key of {@link #getTruncateStatement(String)}.
* @since 0.2.4
*/
public static final String KEY_TRUNCATE_STATEMENT = "statement.truncate";
/**
* The profile key of {@link #getConnectionProperties()}.
* @since 0.2.4
*/
public static final String KEY_PREFIX_PROPERTIES = "properties.";
/**
* The profile key of {@link #getOptimizations()}.
* @since 0.9.0
*/
public static final String KEY_OPTIMIZATIONS = "optimizations";
/**
* The default value of {@link #KEY_BATCH_GET_UNIT}.
* @since 0.2.4
*/
public static final int DEFAULT_BATCH_GET_UNIT = 0;
/**
* The default value of {@link #KEY_BATCH_PUT_UNIT}.
* @since 0.2.4
*/
public static final long DEFAULT_BATCH_PUT_UNIT = Long.MAX_VALUE;
/**
* The default value of {@link #KEY_CONNECT_RETRY_COUNT}.
* @since 0.2.4
*/
public static final int DEFAULT_CONNECT_RETRY_COUNT = 0;
/**
* The default value of {@link #KEY_CONNECT_RETRY_INTERVAL}.
* @since 0.2.4
*/
public static final int DEFAULT_CONNECT_RETRY_INTERVAL = 10;
/**
* The default value of {@link #KEY_TRUNCATE_STATEMENT}.
* @since 0.2.4
*/
public static final String DEFAULT_TRUNCATE_STATEMENT = "TRUNCATE TABLE {0}";
private final String resourceName;
private final ClassLoader classLoader;
private final String driver;
private final String url;
private final String user;
private final String password;
private final Map<String, String> connectionProperties;
private Set<String> optimizations = Collections.emptySet();
private volatile int batchGetUnit = DEFAULT_BATCH_GET_UNIT;
private volatile long batchPutUnit = DEFAULT_BATCH_PUT_UNIT;
private volatile int connectRetryCount = DEFAULT_CONNECT_RETRY_COUNT;
private volatile int connectRetryInterval = DEFAULT_CONNECT_RETRY_INTERVAL;
private volatile String truncateStatement = DEFAULT_TRUNCATE_STATEMENT;
/**
* Creates a new instance.
* @param resourceName the target resource name
* @param classLoader a class loader, or {@code null} to use the system class loader
* @param driver a fully qualified class name of JDBC Driver implementation
* @param url database URL
* @param user database connection user (nullable)
* @param password database connection password (nullable)
* @param batchPutUnit the number of rows on each batch insertion
* @throws IllegalArgumentException if some parameters were {@code null}
*/
public JdbcProfile(
String resourceName,
ClassLoader classLoader,
String driver,
String url,
String user,
String password,
long batchPutUnit) {
this(resourceName, classLoader, driver, url, user, password, Collections.emptyMap());
setBatchPutUnit0(batchPutUnit);
}
/**
* Creates a new instance.
* @param resourceName the target resource name
* @param classLoader a class loader, or {@code null} to use the system class loader
* @param driver a fully qualified class name of JDBC Driver implementation
* @param url database URL
* @param user database connection user (nullable)
* @param password database connection password (nullable)
* @param connectionProperties extra connection properties
* @throws IllegalArgumentException if some parameters were {@code null}
* @since 0.2.4
*/
public JdbcProfile(
String resourceName,
ClassLoader classLoader,
String driver,
String url,
String user,
String password,
Map<String, String> connectionProperties) {
if (resourceName == null) {
throw new IllegalArgumentException("resourceName must not be null"); //$NON-NLS-1$
}
if (driver == null) {
throw new IllegalArgumentException("driver must not be null"); //$NON-NLS-1$
}
if (url == null) {
throw new IllegalArgumentException("url must not be null"); //$NON-NLS-1$
}
if (connectionProperties == null) {
throw new IllegalArgumentException("connectProperties must not be null"); //$NON-NLS-1$
}
this.resourceName = resourceName;
this.classLoader = classLoader == null ? ClassLoader.getSystemClassLoader() : classLoader;
this.driver = driver;
this.url = url;
this.user = user;
this.password = password;
this.connectionProperties = Collections.unmodifiableMap(connectionProperties);
}
/**
* Converts {@link ResourceProfile} into {@link JdbcProfile}.
* @param profile target profile
* @return the converted profile
* @throws IllegalArgumentException if profile is not valid, or any parameter is {@code null}
*/
public static JdbcProfile convert(ResourceProfile profile) {
if (profile == null) {
throw new IllegalArgumentException("profile must not be null"); //$NON-NLS-1$
}
String resourceName = profile.getName();
ClassLoader classLoader = profile.getContext().getClassLoader();
String driver = extract(profile, KEY_DRIVER, true);
String url = extract(profile, KEY_URL, true);
String user = extract(profile, KEY_USER, false);
String password = extract(profile, KEY_PASSWORD, false);
Map<String, String> connectionProperties = extractConnectionProperties(profile);
JdbcProfile result = new JdbcProfile(
resourceName, classLoader, driver, url, user, password, connectionProperties);
int batchGetUnit = extractInt(profile, KEY_BATCH_GET_UNIT, 0, DEFAULT_BATCH_GET_UNIT);
long batchPutUnit = extractLong(profile, KEY_BATCH_PUT_UNIT, 1, DEFAULT_BATCH_PUT_UNIT);
int connectRetryCount = extractInt(profile, KEY_CONNECT_RETRY_COUNT, 0, DEFAULT_CONNECT_RETRY_COUNT);
int connectRetryInterval = extractInt(profile, KEY_CONNECT_RETRY_INTERVAL, 1, DEFAULT_CONNECT_RETRY_INTERVAL);
String truncateStatement = extract(profile, KEY_TRUNCATE_STATEMENT, false);
if (truncateStatement == null) {
truncateStatement = DEFAULT_TRUNCATE_STATEMENT;
}
Set<String> optimizations = extractSet(profile, KEY_OPTIMIZATIONS);
try {
MessageFormat.format(truncateStatement, "dummy");
} catch (IllegalArgumentException e) {
WGLOG.error("E00001",
profile.getName(),
KEY_TRUNCATE_STATEMENT,
truncateStatement);
throw new IllegalArgumentException(MessageFormat.format(
"The \"{1}\" must be a valid MessageFormat: {2} (resource={0})",
profile.getName(),
KEY_TRUNCATE_STATEMENT,
truncateStatement), e);
}
result.setBatchGetUnit(batchGetUnit);
result.setBatchPutUnit(batchPutUnit);
result.setConnectRetryCount(connectRetryCount);
result.setConnectRetryInterval(connectRetryInterval);
result.setTruncateStatement(truncateStatement);
result.setOptimizations(optimizations);
return result;
}
private static Map<String, String> extractConnectionProperties(ResourceProfile profile) {
assert profile != null;
Map<String, String> raw = PropertiesUtil.createPrefixMap(
profile.getConfiguration(),
KEY_PREFIX_PROPERTIES);
Map<String, String> results = new HashMap<>();
for (Map.Entry<String, String> entry : raw.entrySet()) {
String value = resolve(profile, KEY_PREFIX_PROPERTIES + entry.getKey(), entry.getValue());
results.put(entry.getKey(), value);
}
return results;
}
private static int extractInt(ResourceProfile profile, String key, int minimumValue, int defaultValue) {
assert profile != null;
assert key != null;
String valueString = extract(profile, key, false);
int value;
try {
if (valueString == null || valueString.trim().isEmpty()) {
value = defaultValue;
} else {
value = Integer.parseInt(valueString);
}
} catch (NumberFormatException e) {
WGLOG.error("E00001",
profile.getName(),
key,
valueString);
throw new IllegalArgumentException(MessageFormat.format(
"The \"{1}\" must be a valid number: {2} (resource={0})",
profile.getName(),
key,
valueString), e);
}
if (value < minimumValue) {
WGLOG.error("E00001",
profile.getName(),
key,
valueString);
throw new IllegalArgumentException(MessageFormat.format(
"The \"{1}\" must be > 0: {2} (resource={0})",
profile.getName(),
value,
valueString));
}
return value;
}
private static long extractLong(ResourceProfile profile, String key, long minimumValue, long defaultValue) {
assert profile != null;
assert key != null;
String valueString = extract(profile, key, false);
long value;
try {
if (valueString == null || valueString.isEmpty()) {
value = defaultValue;
} else {
value = Integer.parseInt(valueString);
}
} catch (NumberFormatException e) {
WGLOG.error("E00001",
profile.getName(),
key,
valueString);
throw new IllegalArgumentException(MessageFormat.format(
"The \"{1}\" must be a valid number: {2} (resource={0})",
profile.getName(),
key,
valueString), e);
}
if (value < minimumValue) {
WGLOG.error("E00001",
profile.getName(),
key,
valueString);
throw new IllegalArgumentException(MessageFormat.format(
"The \"{1}\" must be > 0: {2} (resource={0})",
profile.getName(),
value,
valueString));
}
return value;
}
private static Set<String> extractSet(ResourceProfile profile, String key) {
assert profile != null;
assert key != null;
String valueString = extract(profile, key, false);
if (valueString == null || valueString.isEmpty()) {
return Collections.emptySet();
}
return Stream.of(valueString.split(","))
.map(String::trim)
.filter(s -> s.isEmpty() == false)
.collect(Collectors.toSet());
}
private static String extract(ResourceProfile profile, String configKey, boolean mandatory) {
assert profile != null;
assert configKey != null;
String value = profile.getConfiguration().get(configKey);
if (value == null) {
if (mandatory == false) {
return null;
} else {
WGLOG.error("E00001",
profile.getName(),
configKey,
null);
throw new IllegalArgumentException(MessageFormat.format(
"Resource \"{0}\" must declare \"{1}\"",
profile.getName(),
configKey));
}
}
return resolve(profile, configKey, value.trim());
}
private static String resolve(ResourceProfile profile, String configKey, String value) {
assert profile != null;
assert configKey != null;
assert value != null;
try {
return profile.getContext().getContextParameters().replace(value, true);
} catch (IllegalArgumentException e) {
WGLOG.error(e, "E00001",
profile.getName(),
configKey,
value);
throw new IllegalArgumentException(MessageFormat.format(
"Failed to resolve environment variables: {2} (resource={0}, property={1})",
profile.getName(),
configKey,
value), e);
}
}
/**
* Returns the resource name.
* @return the resource name
*/
public String getResourceName() {
return resourceName;
}
/**
* Returns the class loader for the resources.
* @return the class loader
*/
public ClassLoader getClassLoader() {
return classLoader;
}
/**
* Creates a new connection using this configuration.
* @return the created connection
* @throws IOException if failed to create a new connection
*/
public Connection openConnection() throws IOException {
LOG.debug("Opening JDBC connection: {}",
url);
try {
Class<? extends Driver> driverClass = Class.forName(driver, true, classLoader).asSubclass(Driver.class);
Properties properties = new Properties();
properties.putAll(getConnectionProperties());
if (user != null) {
properties.put("user", user);
}
if (password != null) {
properties.put("password", password);
}
Connection conn = null;
try {
conn = openConnection(driverClass, properties);
} catch (Exception first) {
Exception last = first;
for (int i = 1, n = getConnectRetryCount(); i <= n; i++) {
WGLOG.warn(last, "W00001",
getResourceName(),
url,
i,
getConnectRetryCount());
try {
TimeUnit.SECONDS.sleep(getConnectRetryInterval());
conn = openConnection(driverClass, properties);
break;
} catch (Exception retry) {
last = retry;
}
}
if (conn == null) {
throw last;
}
}
boolean succeed = false;
try {
conn.setAutoCommit(false);
succeed = true;
} finally {
if (succeed == false) {
LOG.debug("Disposing JDBC connection: {}",
url);
conn.close();
}
}
return conn;
} catch (Exception e) {
WGLOG.error(e, "E00002",
getResourceName(),
url);
throw new IOException(MessageFormat.format(
"Failed to open connection: {0}",
url), e);
}
}
private Connection openConnection(Class<? extends Driver> driverClass, Properties properties) throws Exception {
assert properties != null;
try {
return DriverManager.getConnection(url, properties);
} catch (Exception e) {
// if the current class loader can not access the driver class, create driver class directly
try {
Driver driverObject = driverClass.getConstructor().newInstance();
Connection connection = driverObject.connect(url, properties);
if (connection == null) {
throw new IllegalStateException(MessageFormat.format(
"Driver class {0} may not support {1}",
driverClass.getName(),
url));
}
return connection;
} catch (RuntimeException inner) {
LOG.debug(MessageFormat.format(
"Failed to resolve driver class (internal error): {0} (on {1})",
driverClass.getName(),
driverClass.getClassLoader()), e);
} catch (Exception inner) {
LOG.debug(MessageFormat.format(
"Failed to resolve driver class: {0} (on {1})",
driverClass.getName(),
driverClass.getClassLoader()), e);
}
throw e;
}
}
/**
* Return extra configuration properties.
* If there is no extra configuration, this returns empty map.
* @return extra configuration properties
* @since 0.2.4
*/
public Map<String, String> getConnectionProperties() {
return connectionProperties;
}
/**
* Return the number of rows on each fetch ({@code fetch-size}).
* @return the number of rows on each fetch
* @since 0.2.4
*/
public int getBatchGetUnit() {
return batchGetUnit;
}
/**
* Configures {@link #KEY_BATCH_GET_UNIT}.
* @param value to set
* @throws IllegalArgumentException if {@code < 0}
*/
public void setBatchGetUnit(int value) {
if (value < 0L) {
throw new IllegalArgumentException("batchGetUnit must be >= 0"); //$NON-NLS-1$
}
this.batchGetUnit = value;
}
/**
* Return the number of rows on each batch insertion.
* @return the number of rows on each batch insertion
*/
public long getBatchPutUnit() {
return batchPutUnit;
}
/**
* Configures {@link #KEY_BATCH_PUT_UNIT}.
* @param value to set
* @throws IllegalArgumentException if {@code <= 0}
*/
public void setBatchPutUnit(long value) {
setBatchPutUnit0(value);
}
private void setBatchPutUnit0(long value) {
if (value <= 0L) {
throw new IllegalArgumentException("batchPutUnit must be > 0"); //$NON-NLS-1$
}
this.batchPutUnit = value;
}
/**
* Returns the retry count on create connection.
* @return the retry count, or {@code 0} for no retry
* @since 0.2.4
*/
public int getConnectRetryCount() {
return connectRetryCount;
}
/**
* Configures {@link #KEY_CONNECT_RETRY_COUNT}.
* @param value to set
* @throws IllegalArgumentException if {@code < 0}
*/
public void setConnectRetryCount(int value) {
if (value < 0) {
throw new IllegalArgumentException("connectionRetryCount must be >= 0"); //$NON-NLS-1$
}
this.connectRetryCount = value;
}
/**
* Returns the retry interval (in second).
* @return the connectionRetryInterval
* @see #getConnectRetryCount()
* @since 0.2.4
*/
public int getConnectRetryInterval() {
return connectRetryInterval;
}
/**
* Configures {@link #KEY_CONNECT_RETRY_INTERVAL}.
* @param value to set
* @throws IllegalArgumentException if {@code < 0}
*/
public void setConnectRetryInterval(int value) {
if (value < 0) {
throw new IllegalArgumentException("connectRetryInterval must be > 0"); //$NON-NLS-1$
}
this.connectRetryInterval = value;
}
/**
* Returns the truncate statement.
* @param tableName target table name
* @return the truncate statement
* @since 0.2.4
*/
public String getTruncateStatement(String tableName) {
return MessageFormat.format(truncateStatement, tableName);
}
/**
* Configures {@link #KEY_TRUNCATE_STATEMENT}.
* @param pattern to set
* @throws IllegalArgumentException if the pattern is not in form of message format
* @since 0.2.4
*/
public void setTruncateStatement(String pattern) {
if (pattern == null) {
throw new IllegalArgumentException("pattern must not be null"); //$NON-NLS-1$
}
MessageFormat.format(pattern, "example");
this.truncateStatement = pattern;
}
/**
* Returns the optimizations.
* @return the optimizations
* @see com.asakusafw.windgate.core.vocabulary.JdbcProcess.OptionSymbols
* @since 0.9.0
*/
public Set<String> getOptimizations() {
return optimizations;
}
/**
* Sets the enabled optimization symbols.
* @param optimizations the symbols
* @see com.asakusafw.windgate.core.vocabulary.JdbcProcess.OptionSymbols
* @since 0.9.0
*/
public void setOptimizations(Collection<String> optimizations) {
this.optimizations = Collections.unmodifiableSet(new LinkedHashSet<>(optimizations));
}
}