/*
* Copyright (C) 2012-2016 DuyHai DOAN
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 info.archinnov.achilles.script;
import static info.archinnov.achilles.internals.statement.StatementHelper.isDMLStatement;
import static info.archinnov.achilles.logger.AchillesLoggers.ACHILLES_DDL_SCRIPT;
import static info.archinnov.achilles.logger.AchillesLoggers.ACHILLES_DML_STATEMENT;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.SimpleStatement;
import com.datastax.driver.core.Statement;
import com.google.common.util.concurrent.MoreExecutors;
import info.archinnov.achilles.internals.futures.FutureUtils;
import info.archinnov.achilles.validation.Validator;
/**
* Facility class to execute a CQL script file or a plain CQL statement
*/
public class ScriptExecutor {
private static final Logger DML_LOGGER = LoggerFactory.getLogger(ACHILLES_DML_STATEMENT);
private static final Logger DDL_LOGGER = LoggerFactory.getLogger(ACHILLES_DDL_SCRIPT);
private static final String COMMA = ";";
private static final String BATCH_BEGIN = "BEGIN";
private static final String BATCH_APPLY = "APPLY";
private static final String CODE_DELIMITER_START = "^\\s*(?:AS)?\\s*\\$\\$\\s*$";
private static final String CODE_DELIMITER_END = "^\\s*\\$\\$\\s*;\\s*$";
private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\{([a-z][a-zA-Z0-9_]*)\\}");
private static final Pattern SPECIAL_REGEX_CHARS = Pattern.compile("[\\{\\}\\(\\)\\[\\]\\.\\+\\*\\?\\^\\$\\\\\\|]");
private static final Map<String, Object> EMPTY_MAP = new HashMap<>();
private final ExecutorService sameThreadExecutor = MoreExecutors.newDirectExecutorService();
private final Session session;
public ScriptExecutor(Session session) {
this.session = session;
}
/**
* Execute a CQL script file located in the class path
*
* @param scriptLocation the location of the script file in the class path
*/
public void executeScript(String scriptLocation) {
executeScriptTemplate(scriptLocation, EMPTY_MAP);
}
/**
* Execute a CQL script template located in the class path and
* inject provided values into the template to produce the actual script
*
* @param scriptTemplateLocation the location of the script template in the class path
* @param values template values
*/
public void executeScriptTemplate(String scriptTemplateLocation, Map<String, Object> values) {
final List<SimpleStatement> statements = buildStatements(loadScriptAsLines(scriptTemplateLocation, values));
for (SimpleStatement statement : statements) {
if (isDMLStatement(statement)) {
DML_LOGGER.debug("\tSCRIPT : {}\n", statement.getQueryString());
} else {
DDL_LOGGER.debug("\tSCRIPT : {}\n", statement.getQueryString());
}
session.execute(statement);
}
}
/**
* Execute a plain CQL string statement
* @param statement
* plain CQL string statement
*
* @return the resultSet
*
*/
public ResultSet execute(String statement) {
return session.execute(statement);
}
/**
* Execute a CQL statement
* @param statement
* CQL statement
*
* @return the resultSet
*
*/
public ResultSet execute(Statement statement) {
return session.execute(statement);
}
/**
* Execute a plain CQL string statement asynchronously
*
* @param statement the CQL string statement
* @return CompletableFuture<ResultSet>
*/
public CompletableFuture<ResultSet> executeAsync(String statement) {
return FutureUtils.toCompletableFuture(session.executeAsync(statement), sameThreadExecutor);
}
/**
* Execute a CQL statement asynchronously
*
* @param statement CQL statement
* @return CompletableFuture<ResultSet>
*/
public CompletableFuture<ResultSet> executeAsync(Statement statement) {
return FutureUtils.toCompletableFuture(session.executeAsync(statement), sameThreadExecutor);
}
protected List<String> loadScriptAsLines(String scriptLocation) {
return loadScriptAsLines(scriptLocation, EMPTY_MAP);
}
protected List<String> loadScriptAsLines(String scriptLocation, Map<String, Object> variables) {
InputStream inputStream = this.getClass().getResourceAsStream("/" + scriptLocation);
Validator.validateNotNull(inputStream, "Cannot find CQL script file at location '%s'", scriptLocation);
Scanner scanner = new Scanner(inputStream);
List<String> lines = new ArrayList<>();
while (scanner.hasNextLine()) {
String nextLine = maybeReplaceVariables(scanner, variables);
if (isNotBlank(nextLine)) {
lines.add(nextLine);
}
}
return lines;
}
private String maybeReplaceVariables(Scanner scanner, Map<String, Object> variables) {
String nextLine = scanner.nextLine().trim();
if (isNotBlank(nextLine) && !variables.isEmpty()) {
final Matcher matcher = VARIABLE_PATTERN.matcher(nextLine);
while (matcher.find()) {
final String group = matcher.group(1);
Validator.validateTrue(variables.containsKey(group),
"Cannot find value for variable ${%s} in the variable map provided to ScriptExecutor", group);
final String replacement = SPECIAL_REGEX_CHARS.matcher(variables.get(group).toString()).replaceAll("\\\\$0");
nextLine = nextLine.replaceFirst("\\$\\{" + group + "\\}", replacement);
}
}
return nextLine;
}
protected List<SimpleStatement> buildStatements(List<String> lines) {
List<SimpleStatement> statements = new ArrayList<>();
StringBuilder statement = new StringBuilder();
boolean batch = false;
boolean codeBlock = false;
StringBuilder batchStatement = new StringBuilder();
for (String line : lines) {
if (line.trim().startsWith(BATCH_BEGIN)) {
batch = true;
}
if (line.trim().matches(CODE_DELIMITER_START)) {
if(codeBlock) {
codeBlock = false;
} else {
codeBlock = true;
}
}
if (batch) {
batchStatement.append(line);
if (line.trim().startsWith(BATCH_APPLY)) {
batch = false;
statements.add(new SimpleStatement(batchStatement.toString()));
batchStatement = new StringBuilder();
}
} else if(codeBlock) {
statement.append(line);
if (line.trim().matches(CODE_DELIMITER_END)) {
codeBlock = false;
statements.add(new SimpleStatement(statement.toString()));
statement = new StringBuilder();
}
}
else {
statement.append(line);
if (line.trim().endsWith(COMMA)) {
statements.add(new SimpleStatement(statement.toString()));
statement = new StringBuilder();
} else {
statement.append(" ");
}
}
}
return statements;
}
public Session getSession() {
return session;
}
}