/******************************************************************************* * 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.io.File; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.logging.Logger; import javax.swing.JInternalFrame; import javax.swing.JPanel; import javax.swing.SwingUtilities; import com.opendoorlogistics.api.ExecutionReport; import com.opendoorlogistics.api.IO; import com.opendoorlogistics.api.ODLApi; import com.opendoorlogistics.api.components.ComponentControlLauncherApi; import com.opendoorlogistics.api.components.ComponentExecutionApi.ModalDialogResult; import com.opendoorlogistics.api.components.ODLComponent; import com.opendoorlogistics.api.components.ProcessingApi; import com.opendoorlogistics.api.scripts.parameters.Parameters.TableType; import com.opendoorlogistics.api.standardcomponents.map.MapSelectionList.MapSelectionListRegister; 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.tables.ODLTableDefinition; import com.opendoorlogistics.api.ui.Disposable; import com.opendoorlogistics.core.api.impl.IODecorator; import com.opendoorlogistics.core.api.impl.ODLApiDecorator; import com.opendoorlogistics.core.scripts.elements.Option; import com.opendoorlogistics.core.scripts.elements.Script; import com.opendoorlogistics.core.scripts.execution.ScriptExecutionBlackboardImpl; import com.opendoorlogistics.core.scripts.execution.ScriptExecutor; import com.opendoorlogistics.core.scripts.io.ScriptIO; import com.opendoorlogistics.core.scripts.utils.ScriptUtils; import com.opendoorlogistics.core.scripts.utils.ScriptUtils.OptionVisitor; import com.opendoorlogistics.core.tables.concurrency.MergeBranchedDatastore; import com.opendoorlogistics.core.tables.concurrency.WriteRecorderDecorator; import com.opendoorlogistics.core.tables.decorators.datastores.SimpleDecorator; import com.opendoorlogistics.core.tables.decorators.datastores.dependencies.DataDependencies; import com.opendoorlogistics.core.tables.utils.DatastoreComparer; import com.opendoorlogistics.core.utils.LoggerUtils; import com.opendoorlogistics.core.utils.strings.Strings; import com.opendoorlogistics.core.utils.ui.ModalDialog; import com.opendoorlogistics.core.utils.ui.SwingUtils; import com.opendoorlogistics.studio.appframe.AbstractAppFrame; import com.opendoorlogistics.studio.internalframes.HasInternalFrames; import com.opendoorlogistics.studio.internalframes.HasInternalFrames.FramePlacement; import com.opendoorlogistics.studio.scripts.execution.ReporterFrame.RefreshMode; import com.opendoorlogistics.studio.scripts.execution.ScriptsDependencyInjector.RecordedLauncherCallback; class ScriptExecutionTask extends DatastoreModifierTask{ private final static Logger LOGGER = Logger.getLogger(ScriptExecutionTask.class.getName()); private final AbstractAppFrame appFrame; private final ODLApi api; private final Script unfiltered; private final String[] optionIds; private final String scriptName; private final boolean isScriptRefresh; private final ODLDatastore<? extends ODLTable> parametersDs; private volatile Script filtered; private volatile ScriptsDependencyInjector guiFascade; private volatile Set<ReporterFrameIdentifier> reporterFrameIds; private volatile DataDependencies wholeScriptDependencies; private volatile SimpleDecorator<ODLTableAlterable> simple; private volatile WriteRecorderDecorator<ODLTableAlterable> writeRecorder; private volatile boolean showingModalPanel = false; ScriptExecutionTask(AbstractAppFrame appFrame, final Script script, String[] optionIds, final String scriptName, boolean isScriptRefresh,ODLDatastore<? extends ODLTable> parametersTable) { super(appFrame); this.appFrame = appFrame; this.api = appFrame.getApi(); this.unfiltered = script; this.optionIds = optionIds; this.scriptName = scriptName; this.isScriptRefresh = isScriptRefresh; this.parametersDs = parametersTable; } @Override protected String getProgressTitle(){ // get option name if this is a multi-option script and we're running a single option String optionName=null; if(api.properties().isTrue("scripts.progressbar.title.useoptionname") && unfiltered!=null && optionIds!=null && optionIds.length==1 && optionIds[0]!=null){ class OptionCount implements OptionVisitor{ int count=0; String foundName; @Override public boolean visitOption(Option parent, Option option, int depth) { if(api.stringConventions().equalStandardised(option.getOptionId(), optionIds[0])){ foundName = option.getName(); } count++; return true; } } OptionCount counter = new OptionCount(); ScriptUtils.visitOptions(unfiltered, counter); if(counter.count>1){ optionName = counter.foundName; } } if(optionName!=null){ return optionName; } String title = Strings.isEmpty(scriptName) == false ? "Running " + scriptName : "Running script"; return title; } private ReporterFrameIdentifier getReporterFrameId(String instructionId, String panelId) { return new ReporterFrameIdentifier(getScriptId(), instructionId, panelId); } /** * @param unfilteredScript * @return */ private String getScriptId() { return unfiltered.getUuid().toString(); } @Override protected ExecutionReport executeNonEDTAfterInitialisation() { // Wrap in a decorator which records the writes (needed for merging later) writeRecorder = new WriteRecorderDecorator<>(ODLTableAlterable.class, getNonEDTDatastoreCopy()); // Place within a simple decorator where we can switch back to the main ds afterwards for launched controls simple = new SimpleDecorator<>(ODLTableAlterable.class, writeRecorder); // Decorate the API so we can return the reference file (if available) ODLApi decorated = createDecoratedApi(); // Execute and get all dependencies afterwards ScriptExecutor executor = new ScriptExecutor(decorated,false, guiFascade); if(parametersDs!=null){ executor.setInitialParametersTable(decorated.scripts().parameters().findTable(parametersDs, TableType.PARAMETERS)); } ExecutionReport result = executor.execute(filtered, simple); if (!result.isFailed()) { wholeScriptDependencies = executor.extractDependencies((ScriptExecutionBlackboardImpl) result); } return result; } private ODLApi createDecoratedApi() { ODLApi undecorated = appFrame.getApi(); ODLApi decorated = new ODLApiDecorator(undecorated){ @Override public IO io() { return new IODecorator(api.io()){ @Override public File getLoadedExcelFile() { return getReferenceFile(); } }; } }; return decorated; } @Override protected boolean initialiseNonEDTExecution() { // Filter the script for the options filtered = ExecutionUtils.getFilteredCollapsedScript(appFrame, unfiltered, optionIds, scriptName); if (filtered == null) { return false; } // Copy over the override use prompt information Option mainOption = null; if(optionIds==null || optionIds.length==0){ mainOption = unfiltered; }else if (optionIds!=null && optionIds.length==1){ mainOption = ScriptUtils.getOption(unfiltered, optionIds[0]); } if(mainOption!=null){ filtered.setOverrideVisibleParameters(mainOption.isOverrideVisibleParameters()); filtered.setVisibleParametersOverride(mainOption.getVisibleParametersOverride()); } // Create the execution api we give to the script executor to allow it interact with the UI initDependencyEjector(); return true; } /** * */ private void initDependencyEjector() { ProcessingApi papi = createProcessingApi(); guiFascade = new ScriptsDependencyInjector(appFrame,createDecoratedApi()) { @Override public boolean isCancelled() { return papi.isCancelled(); } @Override public boolean isFinishNow() { return papi.isFinishNow(); } @Override public void postStatusMessage(final String s) { papi.postStatusMessage(s); } @Override protected ModalDialogResult showModal(ModalDialog md) { class MyRunnable implements Runnable{ volatile ModalDialogResult result; @Override public void run() { result = showModalOnEDT(md); } } MyRunnable runnable = new MyRunnable(); SwingUtils.runAndWaitOnEDT(runnable); return runnable.result; } private ModalDialogResult showModalOnEDT(ModalDialog md) { ExecutionUtils.throwIfNotOnEDT(); ModalDialogResult result = null; showingModalPanel = true; // get rid of progress if (progressReporter != null) { progressReporter.dispose(); progressReporter = null; } try { result = super.showModal(md); } catch (Exception e) { throw new RuntimeException(e); } finally { showingModalPanel = false; SwingUtilities.invokeLater(new Runnable() { @Override public void run() { startProgress(); } }); } return result; } }; } @Override protected int getDelayMillisBeforeProgress(){ return isScriptRefresh ? 1000 : 200; } @Override protected String getFailureWindowTitle(){ return scriptName; } @Override protected void finishOnEDTBeforeDatastoreMerge(ExecutionReport result) { // close any outdated controls before merging the datastore so we don't // trigger an unwanted refresh update which starts them up again closeOutdatedControls(); } @Override protected void finishOnEDTAfterDatastoreMerge(ExecutionReport result) { closeOutdatedControls(); // process controls after merging back to main datastore HashSet<ReporterFrame<?>> allProcessedFrames = new HashSet<>(); if (result.isFailed() == false) { // give the scripts execution the main datastore instead of the copy so it can interact directly simple.replaceDecorated(getEDTDatastore()); // process all the launch control callbacks Iterator<RecordedLauncherCallback> it = guiFascade.getControlLauncherCallbacks().iterator(); int count=0; while (it.hasNext() && result.isFailed() == false) { if(progressReporter!=null){ progressReporter.getProgressPanel().setText("Launching controls : " + count); } final RecordedLauncherCallback cb = it.next(); final HashSet<ReporterFrame<?>> frames = new HashSet<>(); try { cb.getCb().launchControls(createComponentControlLauncherApi(cb, frames)); } catch (Throwable e) { result.setFailed(e); } // post-process the reporter frames allProcessedFrames.addAll(frames); if (!result.isFailed()) { for (ReporterFrame<?> frame : frames) { // set dependencies DataDependencies dependencies = guiFascade.getDependenciesByInstructionId(cb.getInstructionId()); // give it a parameters table if we have one and can refresh ODLDatastore<? extends ODLTable> parameters = cb.getParamsDs(); if( parameters!=null){ parameters = api.tables().copyDs(parameters); } frame.setTopLabel(cb.getReportTopLabel()); frame.setDependencies(getEDTDatastore(), unfiltered, dependencies, parameters,result); frame.setRefresherCB(appFrame.getLoadedDatastore().getRunner()); } } count++; } } // Close all processed controls if we had a problem if (result.isFailed()) { for (ReporterFrame<?> frame : allProcessedFrames) { frame.dispose(); } } // Also close any controls with identifiers associated to the task but which were not processed (they must be old) if (reporterFrameIds != null) { for (ReporterFrameIdentifier id : reporterFrameIds) { ReporterFrame<?> frame = getReporterFrame(appFrame,id); if (frame != null && allProcessedFrames.contains(frame) == false) { frame.dispose(); } } } // Set open controls to be dirty if the datastore changed during the script's execution if (!result.isFailed() && allProcessedFrames.size()>0) { // Check read tables in the main datastore against the working copy to determine if anything changed boolean dataChanged = false; for (int tableId : wholeScriptDependencies.getReadTableIds()) { if(wholeScriptDependencies.hasTableValueRead(tableId)){ // structure and data if (!DatastoreComparer.isSame(getEDTDatastore().getTableByImmutableId(tableId), getNonEDTDatastoreCopy().getTableByImmutableId(tableId), DatastoreComparer.CHECK_ALL|DatastoreComparer.CHECK_ROW_SELECTION_STATE)) { dataChanged = true; break; } }else{ // structure only if (!DatastoreComparer.isSameStructure(getEDTDatastore().getTableByImmutableId(tableId), getNonEDTDatastoreCopy().getTableByImmutableId(tableId), DatastoreComparer.CHECK_ALL)) { dataChanged = true; break; } } } if (dataChanged) { for (ReporterFrame<?> frame : allProcessedFrames) { if (!frame.isDisposed()) { // System.out.println("Setting " + frame.getTitle() + " dirty after its script execution..."); frame.setDirty(); } } } // log this check StringBuilder logMsg = new StringBuilder(); logMsg.append(LoggerUtils.prefix()); logMsg.append(" - processed reporter frames "); int frameCount=0; for (ReporterFrame<?> frame : allProcessedFrames) { if(frameCount>0){ logMsg.append(","); } logMsg.append("["); logMsg.append(frame.getId().getCombinedId()); logMsg.append("]"); frameCount++; } logMsg.append(" with read tables "); int logTableCount=0; for(int tableId : wholeScriptDependencies.getReadTableIds()){ ODLTableDefinition dfn = getNonEDTDatastoreCopy()!=null ? getNonEDTDatastoreCopy().getTableByImmutableId(tableId):null; if(logTableCount>0){ logMsg.append(", "); } logMsg.append(dfn!=null ? dfn.getName() : "N/A"); logTableCount++; } if(dataChanged){ logMsg.append(" data changed during running"); }else{ logMsg.append(" no data changed during running"); } LOGGER.info(logMsg.toString()); } } private void closeOutdatedControls() { // If we're not auto-refreshing, close any controls which used an old version of the script as they will be out-of-date // providing they're not an 'never refresh' control which has a null script if(!isScriptRefresh){ String myXML = ScriptIO.instance().toXMLString(unfiltered); for(ReporterFrame<?> rf:getReporterFrames(appFrame)){ if(getScriptId().equals(rf.getId().getScriptId()) && rf.getUnfilteredScript()!=null){ String otherXML = ScriptIO.instance().toXMLString(rf.getUnfilteredScript()); if(!Strings.equalsStd(myXML, otherXML)){ rf.dispose(); } } } } } @Override protected boolean isProgressMinimised(){ // Minimise by default if we're refreshing a script, but not on the 1st launch // as often this can take longer - e.g. loading and caching a file for the first time (e.g. spatial query postcodes gdf file) // and the progress is only intrusive when we're auto-refreshing anyway.. return isScriptRefresh; } /** * @param cb * @param frames * @return */ private ComponentControlLauncherApi createComponentControlLauncherApi(final RecordedLauncherCallback cb, final HashSet<ReporterFrame<?>> frames) { return new ComponentControlLauncherApi() { @Override public <T extends JPanel & Disposable> boolean registerPanel(final String panelId, final String title, T panel, boolean refreshable) { // get the option from the unfiltered script String optId = ScriptUtils.getOptionIdByInstructionId(unfiltered, cb.getInstructionId()); if (optId == null) { throw new RuntimeException("Cannot find instruction in script."); } Option option = ScriptUtils.getOption(unfiltered, optId); // work out the refresh mode (this can change between automatic and manual on updating a frame) RefreshMode refreshMode; if (refreshable && option.isSynchronised()) { refreshMode = RefreshMode.AUTOMATIC; } else if (refreshable) { if(option.isRefreshButtonAlwaysEnabled()){ refreshMode = RefreshMode.MANUAL_ALWAYS_AVAILABLE; }else{ refreshMode = RefreshMode.MANUAL_AUTO_DISABLE; } } else { refreshMode = RefreshMode.NEVER; } // try to get it first in-case already registered ReporterFrameIdentifier id = getReporterFrameId( cb.getInstructionId(), panelId); @SuppressWarnings("unchecked") ReporterFrame<T> frame = (ReporterFrame<T>) ScriptExecutionTask.getReporterFrame(appFrame,id); if (frame != null && frame.getRefreshMode() != refreshMode) { // wrong refresh mode; get rid of it frame.dispose(); frame = null; } if (frame != null) { // update the user panel in the reporter frame frame.setUserPanel(panel); frame.toFront(); frames.add(frame); } else { // Get frame title String frameTitle = ""; Boolean optionNameOnly=api.properties().getBool("scripts.reports.title.optionnameonly"); if(optionNameOnly!=null && optionNameOnly){ if(option!=null && option.getName()!=null){ frameTitle = option.getName(); } }else{ // window title is (a) name given by component, (b) option name (multi option scripts only), (c) filename/scriptName class Adder{ String add(String s1, String s2){ if(!Strings.isEmpty(s2)){ if(s1.length()>0){ return s1 + " - " + s2; }else{ return s2; } } return s1; } } Adder adder = new Adder(); frameTitle = adder.add(frameTitle, title); if(ScriptUtils.getOptionsCount(unfiltered)>1 && option!=null){ frameTitle = adder.add(frameTitle, option.getName()); } frameTitle = adder.add(frameTitle , scriptName); } frame = new ReporterFrame<T>(panel, id, frameTitle,cb.getComponent(), refreshMode,option.isShowLastRefreshedTime(), appFrame.getLoadedDatastore()); frames.add(frame); appFrame.addInternalFrame(frame, FramePlacement.AUTOMATIC); } return true; } @Override public JPanel getRegisteredPanel(String panelId) { ReporterFrameIdentifier id = getReporterFrameId( cb.getInstructionId(), panelId); ReporterFrame<?> rf = ScriptExecutionTask.getReporterFrame(appFrame,id); if (rf != null) { if(!isScriptRefresh){ // bring this frame to the front if we've actually clicked on to execute this script rf.toFront(); } frames.add(rf); return rf.getUserPanel(); } return null; } @Override public ODLApi getApi() { return guiFascade.getApi(); } @Override public List<JPanel> getRegisteredPanels() { ReporterFrameIdentifier id = getReporterFrameId( cb.getInstructionId(), ""); ArrayList<JPanel> ret = new ArrayList<JPanel>(); for(ReporterFrame<?> rf:getReporterFrames(appFrame,id.getScriptId(), id.getInstructionId())){ ret.add(rf.getUserPanel()); } return ret; } @Override public void disposeRegisteredPanel(JPanel panel) { ReporterFrame<?> rf = getReporterFrame(panel); if(rf!=null){ rf.dispose(); } } private ReporterFrame<?> getReporterFrame(JPanel panel) { for(ReporterFrame<?> rf : new ArrayList<ReporterFrame<?>>(getReporterFrames(appFrame))){ if(rf.getUserPanel() == panel){ return rf; } } return null; } @Override public void setTitle(JPanel panel, String title) { ReporterFrame<?> rf = getReporterFrame(panel); if(rf!=null){ rf.setTitle(title); } } @Override public void toFront(JPanel panel) { ReporterFrame<?> rf = getReporterFrame(panel); if(rf!=null){ rf.toFront(); } } @Override public ODLDatastoreUndoable<? extends ODLTableAlterable> getGlobalDatastore() { return getEDTDatastore(); } @Override public MapSelectionListRegister getMapSelectionListRegister() { return appFrame.getLoadedDatastore(); } }; } @Override protected boolean isAllowsUserInteraction() { return ScriptUtils.hasComponentFlag(guiFascade.getApi(), filtered, ODLComponent.FLAG_ALLOW_USER_INTERACTION_WHEN_RUNNING); } // public Set<ReporterFrameIdentifier> getReporterFrameIds() { // return reporterFrameIds; // } public void setReporterFrameIds(Set<ReporterFrameIdentifier> reporterFrameIds) { this.reporterFrameIds = reporterFrameIds; } private static List<ReporterFrame<?>> getReporterFrames(HasInternalFrames appFrame){ ArrayList<ReporterFrame<?>> ret = new ArrayList<>(); for(JInternalFrame frame : appFrame.getInternalFrames()){ if(ReporterFrame.class.isInstance(frame)){ ret.add((ReporterFrame<?>)frame); } } return ret; } private static ReporterFrame<?> getReporterFrame(HasInternalFrames appFrame,ReporterFrameIdentifier id) { for(ReporterFrame<?> rf : getReporterFrames(appFrame)){ if (rf.getId().equals(id)) { return rf; } } return null; } private static List<ReporterFrame<?>> getReporterFrames(HasInternalFrames appFrame,String scriptId, String instructionId){ ArrayList<ReporterFrame<?>> ret = new ArrayList<ReporterFrame<?>>(); for(ReporterFrame<?> rf : getReporterFrames(appFrame)){ ReporterFrameIdentifier id = rf.getId(); if(Strings.equals(id.getScriptId(), scriptId) && Strings.equals(id.getInstructionId(), instructionId)){ ret.add(rf); } } return ret; } @Override protected boolean mergeResultWithEDTDatastore() { return MergeBranchedDatastore.merge(writeRecorder,getEDTDatastore()); } @Override protected boolean isProgressShowable(){ return !showingModalPanel; } }