/*******************************************************************************
* Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com)
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License v3
* which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt
******************************************************************************/
package com.opendoorlogistics.studio.scripts.execution;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import com.opendoorlogistics.api.ExecutionReport;
import com.opendoorlogistics.api.ODLApi;
import com.opendoorlogistics.api.tables.ODLDatastore;
import com.opendoorlogistics.api.tables.ODLDatastoreUndoable;
import com.opendoorlogistics.api.tables.ODLTable;
import com.opendoorlogistics.api.tables.ODLTableAlterable;
import com.opendoorlogistics.api.ui.Disposable;
import com.opendoorlogistics.core.api.impl.ODLApiImpl;
import com.opendoorlogistics.core.scripts.elements.Script;
import com.opendoorlogistics.core.scripts.execution.ScriptExecutor;
import com.opendoorlogistics.core.scripts.utils.ScriptUtils;
import com.opendoorlogistics.core.utils.LoggerUtils;
import com.opendoorlogistics.core.utils.strings.StandardisedStringSet;
import com.opendoorlogistics.core.utils.strings.Strings;
import com.opendoorlogistics.studio.appframe.AbstractAppFrame;
/**
* The script runner exists only whilst the current spreadsheet is open. It is closed when the spreadsheet is closed.
* It holds an exuector service for the refreshes
* @author Phil
*
*/
public final class ScriptsRunner implements ReporterFrame.OnRefreshReport, Disposable {
private final static Logger LOGGER = Logger.getLogger(ScriptsRunner.class.getName());
private static class RefreshQueue{
private final LinkedList<RefreshItem> queue = new LinkedList<>();
private final ScriptsRunner runner;
private final ODLApi api = new ODLApiImpl();
public RefreshQueue(ScriptsRunner runner) {
this.runner = runner;
}
synchronized void post(RefreshItem item){
queue.addLast(item);
}
// synchronized int size(){
// return queue.size();
// }
synchronized boolean isEmpty(){
return queue.size()==0;
}
synchronized ScriptExecutionTask pop(){
ArrayList<RefreshItem> itemList = new ArrayList<>();
if(queue.size()==0){
return null;
}
// get the top item
RefreshItem top = queue.removeFirst();
itemList.add(top);
// get any other items from the same script also in the queue
Iterator<RefreshItem> it = queue.iterator();
while(it.hasNext()){
RefreshItem item = it.next();
// Check same script
if(!Strings.equalsStd(top.getFrameIdentifier().getScriptId(), item.getFrameIdentifier().getScriptId())){
continue;
}
// Check both have, or don't have, a parameter table
boolean topNull = top.getParametersTable()!=null;
boolean otherNull = item.getParametersTable()!=null;
if(topNull!=otherNull){
continue;
}
// Check parameter tables are the same if we have them
if(!topNull){
if(!api.tables().isIdentical(top.getParametersTable().getTableAt(0), item.getParametersTable().getTableAt(0))){
continue;
}
}
itemList.add(item);
it.remove();
}
// get all instruction ids and all reporter frame ids
StandardisedStringSet instructionIdsToRefresh = new StandardisedStringSet(false);
HashSet<ReporterFrameIdentifier> frameIdentifiers = new HashSet<>();
for(RefreshItem item:itemList){
instructionIdsToRefresh.add(item.getFrameIdentifier().getInstructionId());
frameIdentifiers.add(item.getFrameIdentifier());
}
// now get the options to execute these instructions
String[]optionIds = ScriptUtils.getOptionIdsByInstructionIds(top.getUnfilteredScript(), instructionIdsToRefresh);
// take deep copy of parameters table if we have one, to avoid threading issues etc
ODLDatastore<? extends ODLTable> parameters = top.getParametersTable();
if(parameters!=null){
parameters = api.tables().copyDs(parameters);
}
// call execute which will run the script in the background
ScriptExecutionTask task = new ScriptExecutionTask(runner.getAppFrame(),top.getUnfilteredScript(), optionIds, "Refresh open report", true, parameters);
task.setReporterFrameIds(frameIdentifiers);
return task;
}
}
private class RefreshItem{
private final ReporterFrameIdentifier frameIdentifier;
private final boolean isAutomaticRefresh;
private final Script unfilteredScript;
private final ODLDatastore<? extends ODLTable> parametersTable;
public RefreshItem(Script script, ReporterFrameIdentifier frameIdentifier, boolean isAutomaticRefresh, ODLDatastore<? extends ODLTable> parametersTable) {
this.unfilteredScript = script;
this.frameIdentifier = frameIdentifier;
this.isAutomaticRefresh = isAutomaticRefresh;
this.parametersTable = parametersTable;
}
public ReporterFrameIdentifier getFrameIdentifier() {
return frameIdentifier;
}
public Script getUnfilteredScript() {
return unfilteredScript;
}
public boolean isAutomaticRefresh() {
return isAutomaticRefresh;
}
public ODLDatastore<? extends ODLTable> getParametersTable() {
return parametersTable;
}
}
private final AbstractAppFrame appFrame;
private final ExecutorService executorService;
private final ODLDatastoreUndoable<? extends ODLTableAlterable> ds;
private final RefreshQueue reportRefreshQueue = new RefreshQueue(this);
// /**
// * Execute on the EDT thread without a progress dialog
// *
// * @param filteredScript
// * @param name
// */
// private RunResult executeOnEDT(Script filteredScript, String[] optionIds, String name, ScriptsDependencyInjector guiFascade) {
// boolean TEST_HACK = true;
// if (TEST_HACK) {
// TEST_BACKGROUND_EXECUTION(filteredScript, optionIds, name, guiFascade);
// return RunResult.EXECUTING_IN_BACKGROUND;
// }
// throwIfNotOnEDT();
//
// ScriptExecutor executor = new ScriptExecutor(false, guiFascade);
// ds.startTransaction();
// ExecutionReport result = executor.execute(filteredScript, wrapDsWithEditableFlags(ds));
// if (result.isFailed()) {
// ds.rollbackTransaction();
// } else {
// ds.endTransaction();
// }
//
// // don't show script failure box until transaction as finished as this runs the EDT
// // and another refreshreport - with another transaction - can be triggered...
// if (result.isFailed()) {
// showScriptFailureBox(false, name, result);
// }
//
// return result.isFailed() ? RunResult.FAILED : RunResult.SUCCEEDED;
// }
public ScriptsRunner(AbstractAppFrame parentFrame, ODLDatastoreUndoable<? extends ODLTableAlterable> ds) {
this.appFrame = parentFrame;
this.ds = ds;
// Have only 1 execution service thread so control updates finish processing in the order
// they're submitted (otherwise wrong results may appear on-screen).
this.executorService = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(), new ThreadFactory() {
ThreadFactory dFactory = Executors.defaultThreadFactory();
@Override
public Thread newThread(Runnable r) {
Thread ret = dFactory.newThread(r);
ret.setName("ScriptRunnerThread-" + UUID.randomUUID().toString());
return ret;
}
});
}
/**
* Compile on the EDT thread without a progress dialog
*
* @param script
* @param name
*/
void compileOnEDT(Script script, String[] optionIds, String name) {
ExecutionUtils.throwIfNotOnEDT();
script = ExecutionUtils.getFilteredCollapsedScript(appFrame,script, optionIds, name);
if (script == null) {
return;
}
ScriptExecutor executor = new ScriptExecutor(appFrame.getApi(),true, null);
ExecutionReport result = executor.execute(script, ds);
if (result.isFailed()) {
ExecutionUtils.showScriptFailureBox(appFrame,true, name, result);
} else {
JOptionPane.showMessageDialog(appFrame, "Script compiled successfully.");
}
}
// /***
// * Execute the script in a background thread and show a progress dialog
// *
// * @param script
// * @param name
// * @param guiFascade
// */
// private void executeInBackgroundWithProgessDlg(final Script script, final String[] optionIds, final String name, ScriptsDependencyInjector guiFascade) {
// throwIfNotOnEDT();
//
// // Create progress dialog. This prevents the EDT datastore from being modified during the execution.
// final ProgressDialog<ExecutionReport> progressDialog = new ProgressDialog<>(appFrame, Strings.isEmpty(name) == false ? "Running " + name : "Running script", true);
// progressDialog.setLocationRelativeTo(appFrame);
//
// // Copy the datastore in EDT so we never get a half-written copy
// // The UI should not be allowed to edit data in this copy as it won't be written back to the main datastore,
// // so edit permission flags are removed from all tables.
// ODLDatastore<ODLTableAlterable> copy = ds.deepCopyDataOnly();
// copy.setFlags(TableFlags.removeFlags(copy.getFlags(), TableFlags.UI_EDIT_PERMISSION_FLAGS));
// for (int i = 0; i < copy.getTableCount(); i++) {
// ODLTableDefinitionAlterable table = copy.getTableAt(i);
// table.setFlags(TableFlags.removeFlags(table.getFlags(), TableFlags.UI_EDIT_PERMISSION_FLAGS));
// }
//
// // wrap in a decorator which records the writes (needed for merging later)
// final WriteRecorderDecorator<ODLTableAlterable> writeRecorder = new WriteRecorderDecorator<>(ODLTableAlterable.class, copy);
//
// // create run method
// Callable<ExecutionReport> run = new Callable<ExecutionReport>() {
//
// @Override
// public ExecutionReport call() throws Exception {
// ScriptExecutor executor = new ScriptExecutor(false, progressDialog.getGuiFascade());
// return executor.execute(script, writeRecorder);
// }
// };
//
// // and on finished
// OnFinishedSwingThreadCB<ExecutionReport> onFinished = new OnFinishedSwingThreadCB<ExecutionReport>() {
//
// @Override
// public OnFinishedOption onFinished(ExecutionReport result, boolean userCancelled, boolean userFinishedNow) {
// throwIfNotOnEDT();
//
// // Try merging the script result back into the primary datastore.
// // The primary datastore is only modified on the EDT.
// if (!result.isFailed()) {
//
// // merge has a transaction so don't need to start one here
// if (!MergeBranchedDatastore.merge(writeRecorder, ds)) {
// result.setFailed("Failed to merge the script result with the primary datastore." + System.lineSeparator() + "Has the data structure changed?");
// }
// }
//
// // show message if failed
// if (result.isFailed()) {
// showScriptFailureBox(false, name, result);
// }
//
// return OnFinishedOption.NONE;
// }
// };
//
// // go!!!
// progressDialog.start(run, onFinished, guiFascade);
//
// }
Future<Void> execute(final Script script,final String[] optionIds,final String name) {
//new ScriptExecutionTask(this,script, optionIds, name, false).start();
ExecutionUtils.throwIfNotOnEDT();
class FinishedTester implements Future<Void>{
volatile boolean isDone;
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
throw new UnsupportedOperationException();
}
@Override
public boolean isCancelled() {
throw new UnsupportedOperationException();
}
@Override
public boolean isDone() {
return isDone;
}
@Override
public Void get() throws InterruptedException, ExecutionException {
throw new UnsupportedOperationException();
}
@Override
public Void get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
throw new UnsupportedOperationException();
}
}
FinishedTester finishedTester = new FinishedTester();
// run as many concurrent tasks as the user wants as they have been called manually
new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
try {
new ScriptExecutionTask(appFrame,script, optionIds, name, false,null).executeNonEDT();
} catch (Exception e) {
}
finishedTester.isDone = true;
return null;
}
}.execute();
return finishedTester;
}
AbstractAppFrame getAppFrame() {
return appFrame;
}
ODLDatastoreUndoable<? extends ODLTableAlterable> getDs(){
return ds;
}
/*
* Post a report refresh to be processed by a separate single report update thread...
*/
@Override
public void postReportRefreshRequest(Script unfilteredScript,ReporterFrameIdentifier frameIdentifier, boolean isAutomaticRefresh,ODLDatastore<? extends ODLTable> parametersTable) {
ExecutionUtils.throwIfNotOnEDT();
LOGGER.info(LoggerUtils.prefix() + " - received refresh report request for frame " + (frameIdentifier!=null ? frameIdentifier.getCombinedId():""));
// Post to the queue
reportRefreshQueue.post(new RefreshItem(unfilteredScript, frameIdentifier, isAutomaticRefresh,parametersTable));
// Submit a task to process it (may process more than one item from queue at a time).
// We submit the task to the background thread *after* all pending swing events have been processed as:
// this allows for the pooling of updates from multiple open windows in the same script.
if(!executorService.isShutdown()){
invokeTaskSubmissionLater();
}
}
/**
*
*/
private void invokeTaskSubmissionLater() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
executorService.submit(new Runnable() {
@Override
public void run() {
// try to get a task
ScriptExecutionTask task = reportRefreshQueue.pop();
if(task!=null){
// keep on invoking whilst not empty (invoking will happen later)
if(!reportRefreshQueue.isEmpty()){
invokeTaskSubmissionLater();
}
// execute the task
task.executeNonEDT();
}
}
});
}
});
}
@Override
public void dispose() {
if(!executorService.isShutdown()){
executorService.shutdownNow();
}
}
}