/*
*
* * Copyright 2014 Orient Technologies LTD (info(at)orientechnologies.com)
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
* *
* * For more information: http://www.orientechnologies.com
*
*/
package com.orientechnologies.orient.core.command.script;
import com.orientechnologies.common.collection.OMultiValue;
import com.orientechnologies.common.concur.ONeedRetryException;
import com.orientechnologies.common.concur.resource.OPartitionedObjectPool;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.io.OIOUtils;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.parser.OContextVariableResolver;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.command.*;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.exception.ORecordNotFoundException;
import com.orientechnologies.orient.core.exception.OTransactionException;
import com.orientechnologies.orient.core.serialization.serializer.OStringSerializerHelper;
import com.orientechnologies.orient.core.sql.OCommandSQL;
import com.orientechnologies.orient.core.sql.OCommandSQLParsingException;
import com.orientechnologies.orient.core.sql.OSQLEngine;
import com.orientechnologies.orient.core.sql.OTemporaryRidGenerator;
import com.orientechnologies.orient.core.sql.filter.OSQLFilter;
import com.orientechnologies.orient.core.sql.filter.OSQLPredicate;
import com.orientechnologies.orient.core.sql.parser.OIfStatement;
import com.orientechnologies.orient.core.sql.parser.OStatement;
import com.orientechnologies.orient.core.sql.parser.OrientSql;
import com.orientechnologies.orient.core.sql.parser.ParseException;
import com.orientechnologies.orient.core.sql.query.OResultSet;
import com.orientechnologies.orient.core.storage.ORecordDuplicatedException;
import com.orientechnologies.orient.core.tx.OTransaction;
import javax.script.*;
import java.io.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Executes Script Commands.
*
* @author Luca Garulli
* @see OCommandScript
*/
public class OCommandExecutorScript extends OCommandExecutorAbstract implements OCommandDistributedReplicateRequest, OTemporaryRidGenerator {
private static final int MAX_DELAY = 100;
protected OCommandScript request;
protected DISTRIBUTED_EXECUTION_MODE executionMode = DISTRIBUTED_EXECUTION_MODE.LOCAL;
protected AtomicInteger serialTempRID = new AtomicInteger(0);
public OCommandExecutorScript() {
}
@SuppressWarnings("unchecked")
public OCommandExecutorScript parse(final OCommandRequest iRequest) {
request = (OCommandScript) iRequest;
executionMode = ((OCommandScript) iRequest).getExecutionMode();
return this;
}
public OCommandDistributedReplicateRequest.DISTRIBUTED_EXECUTION_MODE getDistributedExecutionMode() {
return executionMode;
}
public Object execute(final Map<Object, Object> iArgs) {
if (context == null)
context = new OBasicCommandContext();
return executeInContext(context, iArgs);
}
public Object executeInContext(final OCommandContext iContext, final Map<Object, Object> iArgs) {
final String language = request.getLanguage();
parserText = request.getText();
parameters = iArgs;
parameters = iArgs;
if (language.equalsIgnoreCase("SQL")) {
// SPECIAL CASE: EXECUTE THE COMMANDS IN SEQUENCE
try {
parserText = preParse(parserText, iArgs);
} catch (ParseException e) {
throw new OCommandExecutionException("Invalid script:" + e.getMessage());
}
return executeSQL();
} else {
return executeJsr223Script(language, iContext, iArgs);
}
}
private String preParse(String parserText, final Map<Object, Object> iArgs) throws ParseException {
final boolean strict = getDatabase().getStorage().getConfiguration().isStrictSql();
if (strict) {
parserText = addSemicolons(parserText);
InputStream is = new ByteArrayInputStream(parserText.getBytes());
OrientSql osql = new OrientSql(is);
List<OStatement> statements = osql.parseScript();
StringBuilder result = new StringBuilder();
for (OStatement stm : statements) {
stm.toString(iArgs, result);
if (!(stm instanceof OIfStatement)) {
result.append(";");
}
result.append("\n");
}
return result.toString();
} else {
return parserText;
}
}
private String addSemicolons(String parserText) {
String[] rows = parserText.split("\n");
StringBuilder builder = new StringBuilder();
for (String row : rows) {
row = row.trim();
builder.append(row);
if (!(row.endsWith(";") || row.endsWith("{"))) {
builder.append(";");
}
builder.append("\n");
}
return builder.toString();
}
public boolean isIdempotent() {
return false;
}
protected Object executeJsr223Script(final String language, final OCommandContext iContext, final Map<Object, Object> iArgs) {
ODatabaseDocumentInternal db = ODatabaseRecordThreadLocal.INSTANCE.get();
final OScriptManager scriptManager = Orient.instance().getScriptManager();
CompiledScript compiledScript = request.getCompiledScript();
final OPartitionedObjectPool.PoolEntry<ScriptEngine> entry = scriptManager.acquireDatabaseEngine(db.getName(), language);
final ScriptEngine scriptEngine = entry.object;
try {
if (compiledScript == null) {
if (!(scriptEngine instanceof Compilable))
throw new OCommandExecutionException("Language '" + language + "' does not support compilation");
final Compilable c = (Compilable) scriptEngine;
try {
compiledScript = c.compile(parserText);
} catch (ScriptException e) {
scriptManager.throwErrorMessage(e, parserText);
}
request.setCompiledScript(compiledScript);
}
final Bindings binding = scriptManager.bind(compiledScript.getEngine().getBindings(ScriptContext.ENGINE_SCOPE),
(ODatabaseDocumentTx) db, iContext, iArgs);
try {
final Object ob = compiledScript.eval(binding);
return OCommandExecutorUtility.transformResult(ob);
} catch (ScriptException e) {
throw OException.wrapException(
new OCommandScriptException("Error on execution of the script", request.getText(), e.getColumnNumber()), e);
} finally {
scriptManager.unbind(binding, iContext, iArgs);
}
} finally {
scriptManager.releaseDatabaseEngine(language, db.getName(), entry);
}
}
// TODO: CREATE A REGULAR JSR223 SCRIPT IMPL
protected Object executeSQL() {
ODatabaseDocument db = ODatabaseRecordThreadLocal.INSTANCE.getIfDefined();
try {
return executeSQLScript(parserText, db);
} catch (IOException e) {
throw OException.wrapException(new OCommandExecutionException("Error on executing command: " + parserText), e);
}
}
@Override
protected void throwSyntaxErrorException(String iText) {
throw new OCommandScriptException("Error on execution of the script: " + iText, request.getText(), 0);
}
protected Object executeSQLScript(final String iText, final ODatabaseDocument db) throws IOException {
Object lastResult = null;
int maxRetry = 1;
context.setVariable("transactionRetries", 0);
context.setVariable("parentQuery", this);
for (int retry = 1; retry <= maxRetry; retry++) {
try {
try {
int txBegunAtLine = -1;
int txBegunAtPart = -1;
lastResult = null;
int nestedLevel = 0;
int skippingScriptsAtNestedLevel = -1;
final BufferedReader reader = new BufferedReader(new StringReader(iText));
int line = 0;
int linePart = 0;
String lastLine;
boolean txBegun = false;
for (; line < txBegunAtLine; ++line)
// SKIP PREVIOUS COMMAND AND JUMP TO THE BEGIN IF ANY
reader.readLine();
for (; (lastLine = reader.readLine()) != null; ++line) {
lastLine = lastLine.trim();
// this block is here (and not below, with the other conditions)
// just because of the smartSprit() that does not parse correctly a single bracket
// final List<String> lineParts = OStringSerializerHelper.smartSplit(lastLine, ';', true);
final List<String> lineParts = splitBySemicolon(lastLine);
if (line == txBegunAtLine)
// SKIP PREVIOUS COMMAND PART AND JUMP TO THE BEGIN IF ANY
linePart = txBegunAtPart;
else
linePart = 0;
boolean breakReturn = false;
for (; linePart < lineParts.size(); ++linePart) {
final String lastCommand = lineParts.get(linePart);
if (isIfCondition(lastCommand)) {
nestedLevel++;
if (skippingScriptsAtNestedLevel >= 0) {
continue; // I'm in an (outer) IF that did not match the condition
}
boolean ifResult = evaluateIfCondition(lastCommand);
if (!ifResult) {
// if does not match the condition, skip all the inner statements
skippingScriptsAtNestedLevel = nestedLevel;
}
continue;
} else if (lastCommand.equals("}")) {
nestedLevel--;
if (skippingScriptsAtNestedLevel > nestedLevel) {
skippingScriptsAtNestedLevel = -1;
}
continue;
} else if (skippingScriptsAtNestedLevel >= 0) {
continue; // I'm in an IF that did not match the condition
} else if (OStringSerializerHelper.startsWithIgnoreCase(lastCommand, "let ")) {
lastResult = executeLet(lastCommand, db);
} else if (OStringSerializerHelper.startsWithIgnoreCase(lastCommand, "begin")) {
if (txBegun)
throw new OCommandSQLParsingException("Transaction already begun");
if (db.getTransaction().isActive())
// COMMIT ANY ACTIVE TX
db.commit();
txBegun = true;
txBegunAtLine = line;
txBegunAtPart = linePart;
db.begin();
if (lastCommand.length() > "begin ".length()) {
String next = lastCommand.substring("begin ".length()).trim();
if (OStringSerializerHelper.startsWithIgnoreCase(next, "isolation ")) {
next = next.substring("isolation ".length()).trim();
db.getTransaction().setIsolationLevel(OTransaction.ISOLATION_LEVEL.valueOf(next.toUpperCase()));
}
}
} else if ("rollback".equalsIgnoreCase(lastCommand)) {
if (!txBegun)
throw new OCommandSQLParsingException("Transaction not begun");
db.rollback();
txBegun = false;
txBegunAtLine = -1;
txBegunAtPart = -1;
} else if (OStringSerializerHelper.startsWithIgnoreCase(lastCommand, "commit")) {
if (txBegunAtLine < 0)
throw new OCommandSQLParsingException("Transaction not begun");
if (retry == 1 && lastCommand.length() > "commit ".length()) {
// FIRST CYCLE: PARSE RETRY TIMES OVERWRITING DEFAULT = 1
String next = lastCommand.substring("commit ".length()).trim();
if (OStringSerializerHelper.startsWithIgnoreCase(next, "retry ")) {
next = next.substring("retry ".length()).trim();
maxRetry = Integer.parseInt(next);
}
}
db.commit();
txBegun = false;
txBegunAtLine = -1;
txBegunAtPart = -1;
} else if (OStringSerializerHelper.startsWithIgnoreCase(lastCommand, "sleep ")) {
executeSleep(lastCommand);
} else if (OStringSerializerHelper.startsWithIgnoreCase(lastCommand, "console.log ")) {
executeConsoleLog(lastCommand, db);
} else if (OStringSerializerHelper.startsWithIgnoreCase(lastCommand, "console.output ")) {
executeConsoleOutput(lastCommand, db);
} else if (OStringSerializerHelper.startsWithIgnoreCase(lastCommand, "console.error ")) {
executeConsoleError(lastCommand, db);
} else if (OStringSerializerHelper.startsWithIgnoreCase(lastCommand, "return ")) {
lastResult = getValue(lastCommand.substring("return ".length()), db);
// END OF SCRIPT
breakReturn = true;
break;
} else if (lastCommand != null && lastCommand.length() > 0)
lastResult = executeCommand(lastCommand, db);
}
if (breakReturn) {
break;
}
}
} catch (RuntimeException ex) {
if (db.getTransaction().isActive())
db.rollback();
throw ex;
}
// COMPLETED
break;
} catch (OTransactionException e) {
// THIS CASE IS ON UPSERT
context.setVariable("retries", retry);
getDatabase().getLocalCache().clear();
if (retry >= maxRetry)
throw e;
waitForNextRetry();
} catch (ORecordDuplicatedException e) {
// THIS CASE IS ON UPSERT
context.setVariable("retries", retry);
getDatabase().getLocalCache().clear();
if (retry >= maxRetry)
throw e;
waitForNextRetry();
} catch (ORecordNotFoundException e) {
// THIS CASE IS ON UPSERT
context.setVariable("retries", retry);
getDatabase().getLocalCache().clear();
if (retry >= maxRetry)
throw e;
} catch (ONeedRetryException e) {
context.setVariable("retries", retry);
getDatabase().getLocalCache().clear();
if (retry >= maxRetry)
throw e;
waitForNextRetry();
}
}
return lastResult;
}
private List<String> splitBySemicolon(String lastLine) {
if (lastLine == null) {
return Collections.EMPTY_LIST;
}
List<String> result = new ArrayList<String>();
Character prev = null;
Character lastQuote = null;
StringBuilder buffer = new StringBuilder();
for (char c : lastLine.toCharArray()) {
if (c == ';' && lastQuote == null) {
if (buffer.toString().trim().length() > 0) {
result.add(buffer.toString().trim());
}
buffer = new StringBuilder();
prev = null;
continue;
}
if ((c == '"' || c == '\'') && (prev == null || !prev.equals('\\'))) {
if (lastQuote != null && lastQuote.equals(c)) {
lastQuote = null;
} else if (lastQuote == null) {
lastQuote = c;
}
}
buffer.append(c);
prev = c;
}
if (buffer.toString().trim().length() > 0) {
result.add(buffer.toString().trim());
}
return result;
}
private boolean evaluateIfCondition(String lastCommand) {
String cmd = lastCommand;
cmd = cmd.trim().substring(2);// remove IF
cmd = cmd.trim().substring(0, cmd.trim().length() - 1); // remove {
OSQLFilter condition = OSQLEngine.getInstance().parseCondition(cmd, getContext(), "IF");
Object result = null;
try {
result = condition.evaluate(null, null, getContext());
} catch (Exception e) {
throw new OCommandExecutionException("Could not evaluate IF condition: " + cmd + " - " + e.getMessage());
}
if (Boolean.TRUE.equals(result)) {
return true;
}
return false;
}
private boolean isIfCondition(String iCommand) {
if (iCommand == null) {
return false;
}
String cmd = iCommand.trim();
if (cmd.length() < 3) {
return false;
}
if (!((OStringSerializerHelper.startsWithIgnoreCase(cmd, "if ")) || OStringSerializerHelper.startsWithIgnoreCase(cmd, "if("))) {
return false;
}
if (!cmd.endsWith("{")) {
return false;
}
return true;
}
/**
* Wait before to retry
*/
protected void waitForNextRetry() {
try {
Thread.sleep(new Random().nextInt(MAX_DELAY - 1) + 1);
} catch (InterruptedException e) {
OLogManager.instance().error(this, "Wait was interrupted", e);
}
}
private Object executeCommand(final String lastCommand, final ODatabaseDocument db) {
final OCommandSQL command = new OCommandSQL(lastCommand);
Object result = db.command(command.setContext(getContext())).execute(toMap(parameters));
request.setFetchPlan(command.getFetchPlan());
return result;
}
private Object toMap(Object parameters) {
if (parameters instanceof SimpleBindings) {
HashMap<Object, Object> result = new LinkedHashMap<Object, Object>();
result.putAll((SimpleBindings) parameters);
return result;
}
return parameters;
}
private Object getValue(final String iValue, final ODatabaseDocument db) {
Object lastResult = null;
boolean recordResultSet = true;
if (iValue.equalsIgnoreCase("NULL"))
lastResult = null;
else if (iValue.startsWith("[") && iValue.endsWith("]")) {
// ARRAY - COLLECTION
final List<String> items = new ArrayList<String>();
OStringSerializerHelper.getCollection(iValue, 0, items);
final List<Object> result = new ArrayList<Object>(items.size());
for (int i = 0; i < items.size(); ++i) {
String item = items.get(i);
result.add(getValue(item, db));
}
lastResult = result;
checkIsRecordResultSet(lastResult);
} else if (iValue.startsWith("{") && iValue.endsWith("}")) {
// MAP
final Map<String, String> map = OStringSerializerHelper.getMap(iValue);
final Map<Object, Object> result = new HashMap<Object, Object>(map.size());
for (Map.Entry<String, String> entry : map.entrySet()) {
// KEY
String stringKey = entry.getKey();
if (stringKey == null)
continue;
stringKey = stringKey.trim();
Object key;
if (stringKey.startsWith("$"))
key = getContext().getVariable(stringKey);
else
key = stringKey;
if (OMultiValue.isMultiValue(key) && OMultiValue.getSize(key) == 1)
key = OMultiValue.getFirstValue(key);
// VALUE
String stringValue = entry.getValue();
if (stringValue == null)
continue;
stringValue = stringValue.trim();
Object value;
if (stringValue.toString().startsWith("$"))
value = getContext().getVariable(stringValue);
else
value = stringValue;
result.put(key, value);
}
lastResult = result;
checkIsRecordResultSet(lastResult);
} else if (iValue.startsWith("\"") && iValue.endsWith("\"") || iValue.startsWith("'") && iValue.endsWith("'")) {
lastResult = new OContextVariableResolver(context).parse(OIOUtils.getStringContent(iValue));
checkIsRecordResultSet(lastResult);
} else if (iValue.startsWith("(") && iValue.endsWith(")"))
lastResult = executeCommand(iValue.substring(1, iValue.length() - 1), db);
else {
lastResult = new OSQLPredicate(iValue).evaluate(context);
}
// END OF THE SCRIPT
return lastResult;
}
private void checkIsRecordResultSet(Object result) {
if (!(result instanceof OIdentifiable) && !(result instanceof OResultSet)) {
if (!OMultiValue.isMultiValue(result)) {
request.setRecordResultSet(false);
} else {
for (Object val : OMultiValue.getMultiValueIterable(result)) {
if (!(val instanceof OIdentifiable))
request.setRecordResultSet(false);
}
}
}
}
private void executeSleep(String lastCommand) {
final String sleepTimeInMs = lastCommand.substring("sleep ".length()).trim();
try {
Thread.sleep(Integer.parseInt(sleepTimeInMs));
} catch (InterruptedException e) {
OLogManager.instance().debug(this, "Sleep was interrupted in SQL batch");
}
}
private void executeConsoleLog(final String lastCommand, final ODatabaseDocument db) {
final String value = lastCommand.substring("console.log ".length()).trim();
OLogManager.instance().info(this, "%s", getValue(OIOUtils.wrapStringContent(value, '\''), db));
}
private void executeConsoleOutput(final String lastCommand, final ODatabaseDocument db) {
final String value = lastCommand.substring("console.output ".length()).trim();
System.out.println(getValue(OIOUtils.wrapStringContent(value, '\''), db));
}
private void executeConsoleError(final String lastCommand, final ODatabaseDocument db) {
final String value = lastCommand.substring("console.error ".length()).trim();
System.err.println(getValue(OIOUtils.wrapStringContent(value, '\''), db));
}
private Object executeLet(final String lastCommand, final ODatabaseDocument db) {
final int equalsPos = lastCommand.indexOf('=');
final String variable = lastCommand.substring("let ".length(), equalsPos).trim();
final String cmd = lastCommand.substring(equalsPos + 1).trim();
if (cmd == null)
return null;
Object lastResult = null;
if (cmd.equalsIgnoreCase("NULL") || cmd.startsWith("$") || (cmd.startsWith("[") && cmd.endsWith("]"))
|| (cmd.startsWith("{") && cmd.endsWith("}"))
|| (cmd.startsWith("\"") && cmd.endsWith("\"") || cmd.startsWith("'") && cmd.endsWith("'"))
|| (cmd.startsWith("(") && cmd.endsWith(")")))
lastResult = getValue(cmd, db);
else
lastResult = executeCommand(cmd, db);
// PUT THE RESULT INTO THE CONTEXT
getContext().setVariable(variable, lastResult);
return lastResult;
}
@Override
public QUORUM_TYPE getQuorumType() {
return QUORUM_TYPE.WRITE;
}
@Override
public int getTemporaryRIDCounter(OCommandContext iContext) {
return serialTempRID.incrementAndGet();
}
}