From 793c46861c2391b356a67b47e2ac67937957e0c2 Mon Sep 17 00:00:00 2001 From: Fred Rothganger Date: Fri, 8 Dec 2023 11:27:38 -0700 Subject: [PATCH] Headless import and export --- N2A/src/gov/sandia/n2a/Main.java | 162 ++++++++++++++---- N2A/src/gov/sandia/n2a/N2APlugin.java | 18 +- .../sandia/n2a/backend/c/ExportCbinary.java | 22 +++ .../sandia/n2a/backend/c/ExportCstatic.java | 149 ++++++++++++---- N2A/src/gov/sandia/n2a/backend/c/PluginC.java | 3 +- .../n2a/backend/neuroml/BackendNeuroML.java | 2 +- .../sandia/n2a/backend/neuroml/ExportJob.java | 7 + .../n2a/backend/neuroml/ExportNeuroML.java | 25 ++- .../n2a/backend/neuroml/ImportNeuroML.java | 12 +- .../n2a/backend/neuroml/PluginNeuroML.java | 6 +- .../n2a/backend/vensim/ImportVensim.java | 10 +- N2A/src/gov/sandia/n2a/host/Host.java | 5 + N2A/src/gov/sandia/n2a/host/SshPath.java | 7 +- N2A/src/gov/sandia/n2a/host/Unix.java | 8 +- N2A/src/gov/sandia/n2a/host/Windows.java | 6 + .../sandia/n2a/plugins/extpoints/Export.java | 7 +- .../sandia/n2a/plugins/extpoints/Import.java | 4 +- .../gov/sandia/n2a/transfer/ExportNative.java | 16 +- .../gov/sandia/n2a/transfer/ImportNative.java | 12 +- N2A/src/gov/sandia/n2a/ui/MainFrame.java | 5 +- N2A/src/gov/sandia/n2a/ui/NTextField.java | 4 +- N2A/src/gov/sandia/n2a/ui/Undoable.java | 4 +- N2A/src/gov/sandia/n2a/ui/eq/GraphNode.java | 6 +- N2A/src/gov/sandia/n2a/ui/eq/GraphParent.java | 2 +- .../sandia/n2a/ui/eq/PanelEquationGraph.java | 14 +- .../sandia/n2a/ui/eq/PanelEquationTree.java | 8 +- .../gov/sandia/n2a/ui/eq/PanelEquations.java | 92 ++++------ N2A/src/gov/sandia/n2a/ui/eq/PanelMRU.java | 2 +- N2A/src/gov/sandia/n2a/ui/eq/PanelModel.java | 55 +++++- N2A/src/gov/sandia/n2a/ui/eq/PanelSearch.java | 22 ++- .../sandia/n2a/ui/eq/search/NodeModel.java | 4 +- .../sandia/n2a/ui/eq/tree/NodeAnnotation.java | 4 +- .../gov/sandia/n2a/ui/eq/tree/NodeBase.java | 4 +- .../sandia/n2a/ui/eq/tree/NodeEquation.java | 6 +- .../sandia/n2a/ui/eq/tree/NodeInherit.java | 4 +- .../gov/sandia/n2a/ui/eq/tree/NodePart.java | 4 +- .../sandia/n2a/ui/eq/tree/NodeReference.java | 4 +- .../sandia/n2a/ui/eq/tree/NodeVariable.java | 2 +- N2A/src/gov/sandia/n2a/ui/eq/undo/AddDoc.java | 18 +- .../gov/sandia/n2a/ui/ref/ExportBibTeX.java | 28 ++- .../sandia/n2a/ui/ref/ExportBibliography.java | 13 +- .../sandia/n2a/ui/ref/ImportBibliography.java | 6 +- N2A/src/gov/sandia/n2a/ui/ref/PanelEntry.java | 84 +++++---- N2A/src/gov/sandia/n2a/ui/ref/PanelMRU.java | 2 +- .../gov/sandia/n2a/ui/ref/PanelSearch.java | 24 ++- .../gov/sandia/n2a/ui/ref/undo/AddEntry.java | 17 +- .../gov/sandia/n2a/ui/settings/PanelDiff.java | 8 +- .../sandia/n2a/ui/settings/SettingsRepo.java | 16 +- 48 files changed, 653 insertions(+), 290 deletions(-) create mode 100644 N2A/src/gov/sandia/n2a/backend/c/ExportCbinary.java diff --git a/N2A/src/gov/sandia/n2a/Main.java b/N2A/src/gov/sandia/n2a/Main.java index a86c5899..7dc9ae6e 100644 --- a/N2A/src/gov/sandia/n2a/Main.java +++ b/N2A/src/gov/sandia/n2a/Main.java @@ -16,10 +16,15 @@ import gov.sandia.n2a.eqset.MPart; import gov.sandia.n2a.host.Host; import gov.sandia.n2a.host.Remote; +import gov.sandia.n2a.plugins.ExtensionPoint; import gov.sandia.n2a.plugins.PluginManager; import gov.sandia.n2a.plugins.extpoints.Backend; +import gov.sandia.n2a.plugins.extpoints.Export; +import gov.sandia.n2a.plugins.extpoints.ExportModel; +import gov.sandia.n2a.plugins.extpoints.ShutdownHook; import gov.sandia.n2a.ui.MainFrame; import gov.sandia.n2a.ui.eq.PanelEquations; +import gov.sandia.n2a.ui.eq.PanelModel; import gov.sandia.n2a.ui.jobs.NodeJob; import gov.sandia.n2a.ui.jobs.OutputParser; import gov.sandia.n2a.ui.jobs.OutputParser.Column; @@ -41,6 +46,7 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Locale; import javax.swing.JFrame; @@ -56,26 +62,32 @@ public static void main (String[] args) ArrayList pluginDirs = new ArrayList (); MNode record = new MVolatile (); String headless = ""; + String format = null; + Path path = null; for (String arg : args) { if (arg.startsWith ("-plugin=" )) pluginClassNames.add (arg.substring (8)); else if (arg.startsWith ("-pluginDir=" )) pluginDirs .add (Paths.get (arg.substring (11)).toAbsolutePath ()); else if (arg.startsWith ("-param=" )) processParamFile (arg.substring (7), record); - else if (arg.startsWith ("-install" )) headless = "install"; - else if (arg.startsWith ("-csv" )) record.set (true, "$meta", "csv"); - else if (arg.startsWith ("-run=")) + else if (arg.equals ("-install" )) headless = "install"; + else if (arg.equals ("-csv" )) record.set (true, "$meta", "csv"); + else if (arg.equals ("-run" )) headless = "run"; + else if (arg.equals ("-study" )) headless = "study"; + else if (arg.startsWith ("-export" )) headless = "export"; + else if (arg.startsWith ("-import" )) headless = "import"; + else if (arg.startsWith ("-model=" )) { - MNode temp = new MVolatile ("", arg.substring (5)); + MNode temp = new MVolatile ("", arg.substring (7)); temp.merge (record); record = temp; - headless = "run"; } - else if (arg.startsWith ("-study=")) + else if (arg.startsWith ("-file=")) { - MNode temp = new MVolatile ("", arg.substring (7)); - temp.merge (record); - record = temp; - headless = "study"; + path = Paths.get (arg.substring (6)); + } + else if (arg.startsWith ("-format=")) + { + format = arg.substring (8); } else if (! arg.startsWith ("-")) { @@ -110,28 +122,47 @@ else if (! arg.startsWith ("-")) if (! headless.isEmpty ()) { - if (headless.equals ("run" )) runHeadless (record); - else if (headless.equals ("study" )) studyHeadless (record); - else if (headless.equals ("install")) + switch (headless) { - try - { - JobC jobC = new JobC (new MVolatile ()); - jobC.runtimeDir = resourceDir.resolve ("backend").resolve ("c"); - jobC.unpackRuntime (); - - JobPython jobPython = new JobPython (); - jobPython.runtimeDir = resourceDir.resolve ("backend").resolve ("python"); - jobPython.unpackRuntime (); - } - catch (Exception e) - { - e.printStackTrace (); - System.err.println ("Failed to unpack runtime resources."); - } + case "run": + runHeadless (record); + break; + case "study": + studyHeadless (record); + break; + case "import": + String name = record.key (); + if (name.isBlank ()) name = null; + importHeadless (path, format, name); + break; + case "export": + exportHeadless (record, format, path); + break; + case "install": + try + { + JobC jobC = new JobC (new MVolatile ()); + jobC.runtimeDir = resourceDir.resolve ("backend").resolve ("c"); + jobC.unpackRuntime (); - AppData.quit (); // Flush new DB data. + JobPython jobPython = new JobPython (); + jobPython.runtimeDir = resourceDir.resolve ("backend").resolve ("python"); + jobPython.unpackRuntime (); + } + catch (Exception e) + { + e.printStackTrace (); + System.err.println ("Failed to unpack runtime resources."); + } + break; } + + // Save all data and exit. + // See MainFrame window close listener + AppData.quit (); + List exps = PluginManager.getExtensionsForPoint (ShutdownHook.class); + for (ExtensionPoint exp : exps) ((ShutdownHook) exp).shutdown (); + Host.quit (); // Close down any ssh sessions. return; } @@ -298,7 +329,7 @@ public static void studyHeadless (MNode record) MPart collated = new MPart (record); if (! collated.containsKey ("study")) return; - // Start host monitor threads (see PanelRun constructor for non-headless procedure) + // Start host monitor threads (see PanelRun constructor for GUI procedure) Host.restartAssignmentThread (); for (Host h : Host.getHosts ()) h.restartMonitorThread (); @@ -352,10 +383,75 @@ public static void studyHeadless (MNode record) e.printStackTrace (); } } + } + + public static void importHeadless (Path path, String format, String name) + { + if (path == null) + { + System.err.println ("Import requires at least the path to the source file."); + return; + } + if (! path.isAbsolute ()) path = Paths.get (System.getProperty ("user.dir")).resolve (path); + + try + { + PanelModel.importFile (path, format, name); + } + catch (Exception e) + { + System.err.println ("Import failed"); + e.printStackTrace(); + } + } - // See MainFrame window close listener - AppData.quit (); // Save any modified data, particularly the study record. - Host.quit (); // Close down any ssh sessions. + public static void exportHeadless (MNode record, String format, Path path) + { + // See PanelEquations.listenerExport(). The code here is highly stripped down because we're not dealing with a GUI. + String key = record.key (); + MNode doc = AppData.docs.childOrEmpty ("models", key); + record.mergeUnder (doc); + + if (format == null) format = ""; + if (path == null) + { + path = Paths.get (System.getProperty ("user.dir")); + path = path.resolve (key); // The exporter will add an appropriate suffix to this. + } + else if (! path.isAbsolute ()) + { + path = Paths.get (System.getProperty ("user.dir")).resolve (path); + } + + Export exporter = null; + Export n2a = null; + List exps = PluginManager.getExtensionsForPoint (Export.class); + for (ExtensionPoint exp : exps) + { + if (! (exp instanceof ExportModel)) continue; + Export em = ((ExportModel) exp); + String name = em.getName (); + if (name.contains ("N2A")) n2a = em; + if (name.equalsIgnoreCase (format)) exporter = em; + if (exporter == null && em.accept (path)) exporter = em; // Suffix match + } + if (exporter == null) exporter = n2a; + if (exporter == null) + { + System.err.println ("No matching export method"); + return; + } + + try + { + Files.createDirectories (path.getParent ()); + exporter.process (record, path); + } + catch (Exception e) + { + System.err.println ("Export failed"); + e.printStackTrace (); + } } public static void setUncaughtExceptionHandler (final JFrame parent) diff --git a/N2A/src/gov/sandia/n2a/N2APlugin.java b/N2A/src/gov/sandia/n2a/N2APlugin.java index 393adb9d..6b7590b3 100644 --- a/N2A/src/gov/sandia/n2a/N2APlugin.java +++ b/N2A/src/gov/sandia/n2a/N2APlugin.java @@ -1,5 +1,5 @@ /* -Copyright 2013-2021 National Technology & Engineering Solutions of Sandia, LLC (NTESS). +Copyright 2013-2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software. */ @@ -98,18 +98,18 @@ public ExtensionPoint[] getExtensions () Windows.factory (), RemoteUnix.factory (), RemoteSlurm.factory (), - RemoteLSF.factory () + RemoteLSF.factory (), + new ExportNative (), + gov.sandia.n2a.ui.ref.PanelSearch.exportBibTeX, + new ImportNative (), + new ImportBibTeX (), + new ImportEndNote (), + new ImportPubMed (), + new ImportRIS () ); if (! AppData.properties.getBoolean ("headless")) { Collections.addAll (result, - new ExportNative (), - gov.sandia.n2a.ui.ref.PanelSearch.exportBibTeX, - new ImportNative (), - new ImportBibTeX (), - new ImportEndNote (), - new ImportPubMed (), - new ImportRIS (), new ActivityModel (), new ActivityRun (), new ActivityReference (), diff --git a/N2A/src/gov/sandia/n2a/backend/c/ExportCbinary.java b/N2A/src/gov/sandia/n2a/backend/c/ExportCbinary.java new file mode 100644 index 00000000..0d2b19bb --- /dev/null +++ b/N2A/src/gov/sandia/n2a/backend/c/ExportCbinary.java @@ -0,0 +1,22 @@ +/* +Copyright 2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS). +Under the terms of Contract DE-NA0003525 with NTESS, +the U.S. Government retains certain rights in this software. +*/ + +package gov.sandia.n2a.backend.c; + +public class ExportCbinary extends ExportCstatic +{ + public ExportCbinary () + { + shared = true; // Default choice for binaries, unless overridden by metadata in model itself. + lib = false; + } + + @Override + public String getName () + { + return "C binary"; + } +} diff --git a/N2A/src/gov/sandia/n2a/backend/c/ExportCstatic.java b/N2A/src/gov/sandia/n2a/backend/c/ExportCstatic.java index ac768d87..9633ca9a 100644 --- a/N2A/src/gov/sandia/n2a/backend/c/ExportCstatic.java +++ b/N2A/src/gov/sandia/n2a/backend/c/ExportCstatic.java @@ -10,6 +10,7 @@ import gov.sandia.n2a.db.MDoc; import gov.sandia.n2a.db.MNode; import gov.sandia.n2a.host.Host; +import gov.sandia.n2a.host.Remote; import gov.sandia.n2a.plugins.extpoints.ExportModel; import gov.sandia.n2a.ui.MainFrame; import gov.sandia.n2a.ui.MainTabbedPane; @@ -21,15 +22,16 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; - import javax.swing.tree.TreePath; public class ExportCstatic implements ExportModel { protected boolean shared; + protected boolean lib = true; @Override public String getName () @@ -38,7 +40,7 @@ public String getName () } @Override - public void export (MNode source, Path destination) throws Exception + public void process (MNode source, Path destination) throws Exception { // Approach: Create a proper C job, then copy specific resources to the destination. @@ -51,58 +53,97 @@ public void export (MNode source, Path destination) throws Exception job = (MDoc) AppData.runs.childOrCreate (jobKey); NodeJob.collectJobParameters (source, source.key (), job); job.save (); - NodeJob.saveSnapshot (source, job); + //NodeJob.saveSnapshot (source, job); // Currently no use for snapshot in exports. - final MDoc finalJob = job; - MainTabbedPane mtp = (MainTabbedPane) MainFrame.instance.tabs; - EventQueue.invokeLater (new Runnable () + boolean headless = AppData.properties.getFlag ("headless"); + if (! headless) { - public void run () + final MDoc finalJob = job; + EventQueue.invokeLater (new Runnable () { - mtp.setPreferredFocus (PanelRun.instance, PanelRun.instance.tree); - mtp.selectTab ("Runs"); - NodeJob jobNode = PanelRun.instance.addNewRun (finalJob, true); - Host h = Host.get (finalJob); - h.monitor (jobNode); - } - }); + public void run () + { + MainTabbedPane mtp = (MainTabbedPane) MainFrame.instance.tabs; + mtp.setPreferredFocus (PanelRun.instance, PanelRun.instance.tree); + mtp.selectTab ("Runs"); + NodeJob jobNode = PanelRun.instance.addNewRun (finalJob, true); + Host h = Host.get (finalJob); + h.monitor (jobNode); + } + }); + } String stem = destination.getFileName ().toString (); + int pos = stem.lastIndexOf ('.'); + if (pos > 0) stem = stem.substring (0, pos); // In case user accidentally selected an existing file with wrong suffix. JobC t = new JobC (job); - t.lib = true; // library flag. Will build a library then return (model not executed). + t.lib = lib; // library flag. Will build a library then return (model not executed). t.shared = shared; t.libStem = stem; t.run (); // Export is already on its own thread, so no need to start a new one for this. - // Move library resources to destination + // Move resources to destination // Notice that even though the export may be built on a remote host // (per host key in model), it will be copied to a local directory. CompilerFactory factory = BackendC.getFactory (t.env); // to get suffixes - Path parent = destination.getParent (); - String prefix = factory.prefixLibrary (shared); - String suffix = factory.suffixLibrary (shared); - Path libraryFrom = t.jobDir.resolve (prefix + stem + suffix); - Path libraryTo = parent .resolve (prefix + stem + suffix); - Path headerFrom = t.jobDir.resolve (stem + ".h"); - Path headerTo = parent .resolve (stem + ".h"); - Files.move (libraryFrom, libraryTo, StandardCopyOption.REPLACE_EXISTING); - Files.move (headerFrom, headerTo, StandardCopyOption.REPLACE_EXISTING); - if (shared) + Path parent = destination.getParent (); + if (lib) + { + String prefix = factory.prefixLibrary (shared); + String suffix = factory.suffixLibrary (shared); + Path libraryFrom = t.jobDir.resolve (prefix + stem + suffix); + Path libraryTo = parent .resolve (prefix + stem + suffix); + Path headerFrom = t.jobDir.resolve (stem + ".h"); + Path headerTo = parent .resolve (stem + ".h"); + Files.move (libraryFrom, libraryTo, StandardCopyOption.REPLACE_EXISTING); + Files.move (headerFrom, headerTo, StandardCopyOption.REPLACE_EXISTING); + if (shared) + { + if (factory.wrapperRequired ()) + { + String wrapper = factory.suffixLibraryWrapper (); + libraryFrom = t.jobDir.resolve (stem + wrapper); + libraryTo = parent .resolve (stem + wrapper); + Files.move (libraryFrom, libraryTo, StandardCopyOption.REPLACE_EXISTING); + } + if (t.debug && factory.debugRequired ()) + { + String debug = factory.suffixDebug (); + Path from = t.jobDir.resolve (stem + debug); + Path to = parent .resolve (stem + debug); + Files.move (from, to, StandardCopyOption.REPLACE_EXISTING); + } + } + } + else // binary { - if (factory.wrapperRequired ()) + // Kill job and wait for exit. On some platforms, the binary file is locked when the program is executing. + t.env.killJob (job, true); // force kill. No need for graceful shutdown. + boolean live = t.env.isAlive (job); // This test can potentially take a long time, even longer than 1 second. + for (int i = 0; i < 10 && live; i++) { - String wrapper = factory.suffixLibraryWrapper (); - libraryFrom = t.jobDir.resolve (stem + wrapper); - libraryTo = parent .resolve (stem + wrapper); - Files.move (libraryFrom, libraryTo, StandardCopyOption.REPLACE_EXISTING); + Thread.sleep (100); + live = t.env.isAlive (job); } - if (t.debug && factory.debugRequired ()) + + String suffixBinary = factory.suffixBinary (); + String suffixShell = t.env.shellSuffix (); + Path binaryFrom = t.jobDir.resolve ("model" + suffixBinary); + Path binaryTo = parent .resolve (stem + suffixBinary); + Path shellFrom = t.jobDir.resolve ("n2a_job" + suffixShell); + Path shellTo = parent .resolve (stem + suffixShell); + Files.move (binaryFrom, binaryTo, StandardCopyOption.REPLACE_EXISTING); + if (t.env instanceof Remote) { - String debug = factory.suffixDebug (); - Path from = t.jobDir.resolve (stem + debug); - Path to = parent .resolve (stem + debug); - Files.move (from, to, StandardCopyOption.REPLACE_EXISTING); + Files.move (shellFrom, shellTo, StandardCopyOption.REPLACE_EXISTING); + } + else // localhost. Rewrite paths to match destination dir + { + String shellScript = Files.readString (shellFrom); + shellScript = shellScript.replace (t.jobDir.toString (), parent.toString ()); + shellScript = shellScript.replace ("model" + suffixBinary, stem + suffixBinary); + Files.writeString (shellTo, shellScript, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); } } if (t.cli) @@ -114,6 +155,11 @@ public void run () // Cleanup if (source.getFlag ("$meta", "backend", "c", "keepExportJob")) return; + if (headless) + { + AppData.runs.clear (jobKey); + return; + } EventQueue.invokeLater (new Runnable () { public void run () @@ -122,6 +168,7 @@ public void run () synchronized (PanelRun.jobNodes) {jobNode = PanelRun.jobNodes.get (jobKey);} if (jobNode == null) return; + MainTabbedPane mtp = (MainTabbedPane) MainFrame.instance.tabs; int tabIndex = mtp.getSelectedIndex (); String tabName = ""; if (tabIndex >= 0) tabName = mtp.getTitleAt (tabIndex); @@ -148,4 +195,34 @@ public void run () throw e; } } + + @Override + public boolean accept (Path source) + { + if (Files.isDirectory (source)) return true; + String name = source.getFileName ().toString (); + int lastDot = name.lastIndexOf ('.'); + if (! lib && lastDot < 0) return true; // File without suffix could be a binary. + if (lastDot >= 0) + { + String suffix = name.substring (lastDot); + // TODO: add MacOS suffixes here + if (! lib) // binary + { + if (suffix.equalsIgnoreCase (".exe")) return true; + if (suffix.equalsIgnoreCase (".bin")) return true; + } + else if (shared) // shared library + { + if (suffix.equalsIgnoreCase (".dll")) return true; + if (suffix.equalsIgnoreCase (".so" )) return true; + } + else // static library + { + if (suffix.equalsIgnoreCase (".lib")) return true; + if (suffix.equalsIgnoreCase (".a" )) return true; + } + } + return false; + } } diff --git a/N2A/src/gov/sandia/n2a/backend/c/PluginC.java b/N2A/src/gov/sandia/n2a/backend/c/PluginC.java index 38cd2b08..fc688080 100644 --- a/N2A/src/gov/sandia/n2a/backend/c/PluginC.java +++ b/N2A/src/gov/sandia/n2a/backend/c/PluginC.java @@ -1,5 +1,5 @@ /* -Copyright 2013-2022 National Technology & Engineering Solutions of Sandia, LLC (NTESS). +Copyright 2013-2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software. */ @@ -62,6 +62,7 @@ public ExtensionPoint[] getExtensions () { List result = new ArrayList (); result.add (new BackendC ()); + result.add (new ExportCbinary ()); result.add (new ExportCstatic ()); result.add (new ExportCshared ()); if (! AppData.properties.getBoolean ("headless")) diff --git a/N2A/src/gov/sandia/n2a/backend/neuroml/BackendNeuroML.java b/N2A/src/gov/sandia/n2a/backend/neuroml/BackendNeuroML.java index 0346ccad..65e229bb 100644 --- a/N2A/src/gov/sandia/n2a/backend/neuroml/BackendNeuroML.java +++ b/N2A/src/gov/sandia/n2a/backend/neuroml/BackendNeuroML.java @@ -68,7 +68,7 @@ public void run () Host env = Host.get (job); Path jobDir = Host.getJobDir (env.getResourceDir (), job); Path modelPath = jobDir.resolve ("model.nml"); - ExportJob exportJob = PluginNeuroML.exporter.export (doc, modelPath, true); + ExportJob exportJob = PluginNeuroML.exporter.process (doc, modelPath, true); // Record metadata if (! exportJob.duration.isEmpty ()) job.set (exportJob.duration, "duration"); diff --git a/N2A/src/gov/sandia/n2a/backend/neuroml/ExportJob.java b/N2A/src/gov/sandia/n2a/backend/neuroml/ExportJob.java index bff22660..19e73fc6 100644 --- a/N2A/src/gov/sandia/n2a/backend/neuroml/ExportJob.java +++ b/N2A/src/gov/sandia/n2a/backend/neuroml/ExportJob.java @@ -172,6 +172,13 @@ public void process (MNode source, Path destination) if (! errors.isEmpty ()) { + if (AppData.properties.getFlag ("headless")) + { + System.err.println ("Export completed with warnings"); + System.err.println (errors); + return; + } + JTextArea textArea = new JTextArea (errors); JScrollPane scrollPane = new JScrollPane (textArea); scrollPane.setPreferredSize (new java.awt.Dimension (640, 480)); diff --git a/N2A/src/gov/sandia/n2a/backend/neuroml/ExportNeuroML.java b/N2A/src/gov/sandia/n2a/backend/neuroml/ExportNeuroML.java index 0bca5b32..ebf85184 100644 --- a/N2A/src/gov/sandia/n2a/backend/neuroml/ExportNeuroML.java +++ b/N2A/src/gov/sandia/n2a/backend/neuroml/ExportNeuroML.java @@ -10,7 +10,7 @@ This class uses XML schema files (.xsd) from the LEMS and NeuroML distributions should be copied from their respective distributions: https://github.com/NeuroML/NeuroML2/tree/master/Schemas/NeuroML2 https://github.com/LEMS/LEMS/blob/master/Schemas/LEMS -and placed into this directory. Also update export() below to reference the latest +and placed into this directory. Also update process() below to reference the latest files. Note that the software can work without these files, but the resulting XML may not be strictly correct. */ @@ -20,6 +20,7 @@ and placed into this directory. Also update export() below to reference the late import gov.sandia.n2a.db.MNode; import gov.sandia.n2a.plugins.extpoints.ExportModel; +import java.nio.file.Files; import java.nio.file.Path; public class ExportNeuroML implements ExportModel @@ -31,12 +32,12 @@ public String getName () } @Override - public void export (MNode source, Path destination) + public void process (MNode source, Path destination) { - export (source, destination, false); + process (source, destination, false); } - public ExportJob export (MNode source, Path destination, boolean forBackend) + public ExportJob process (MNode source, Path destination, boolean forBackend) { if (PluginNeuroML.partMap == null) PluginNeuroML.partMap = new PartMap (); if (PluginNeuroML.sequencer == null) @@ -51,4 +52,20 @@ public ExportJob export (MNode source, Path destination, boolean forBackend) job.process (source, destination); return job; } + + @Override + public boolean accept (Path source) + { + if (Files.isDirectory (source)) return true; + String name = source.getFileName ().toString (); + int lastDot = name.lastIndexOf ('.'); + if (lastDot >= 0) + { + String suffix = name.substring (lastDot); + if (suffix.equalsIgnoreCase (".xml" )) return true; + if (suffix.equalsIgnoreCase (".nml" )) return true; + if (suffix.equalsIgnoreCase (".lems")) return true; // Not sure "lems" is an official suffix, but if specified it seems very likely to be LEMS. + } + return false; + } } diff --git a/N2A/src/gov/sandia/n2a/backend/neuroml/ImportNeuroML.java b/N2A/src/gov/sandia/n2a/backend/neuroml/ImportNeuroML.java index 13a188bc..5a1700f6 100644 --- a/N2A/src/gov/sandia/n2a/backend/neuroml/ImportNeuroML.java +++ b/N2A/src/gov/sandia/n2a/backend/neuroml/ImportNeuroML.java @@ -1,5 +1,5 @@ /* -Copyright 2017-2021 National Technology & Engineering Solutions of Sandia, LLC (NTESS). +Copyright 2017-2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software. */ @@ -27,7 +27,7 @@ public String getName () } @Override - public void process (Path source) + public void process (Path source, String name) { if (PluginNeuroML.partMap == null) PluginNeuroML.partMap = new PartMap (); @@ -38,12 +38,16 @@ public void process (Path source) MNode mainModel = job.models.child (job.modelName); job.models.clear (job.modelName); - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; um.addEdit (new CompoundEdit ()); while (job.models.size () > 0) addModel (job.models.iterator ().next (), job.models, um); // Save the best for last. That is, ensure that the main model is the one selected in the UI // after all add operations are completed. - if (mainModel != null) um.apply (new AddDoc (job.modelName, mainModel)); + if (mainModel != null) + { + if (name == null) name = job.modelName; + um.apply (new AddDoc (name, mainModel)); + } um.endCompoundEdit (); } diff --git a/N2A/src/gov/sandia/n2a/backend/neuroml/PluginNeuroML.java b/N2A/src/gov/sandia/n2a/backend/neuroml/PluginNeuroML.java index 12a78f5e..659fd1c0 100644 --- a/N2A/src/gov/sandia/n2a/backend/neuroml/PluginNeuroML.java +++ b/N2A/src/gov/sandia/n2a/backend/neuroml/PluginNeuroML.java @@ -1,5 +1,5 @@ /* -Copyright 2016-2021 National Technology & Engineering Solutions of Sandia, LLC (NTESS). +Copyright 2016-2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software. */ @@ -54,10 +54,10 @@ public ExtensionPoint[] getExtensions () { List result = new ArrayList (); result.add (new BackendNeuroML ()); + result.add (exporter); + result.add (new ImportNeuroML ()); if (! AppData.properties.getBoolean ("headless")) { - result.add (exporter); - result.add (new ImportNeuroML ()); result.add (new SettingsNeuroML ()); } return result.toArray (new ExtensionPoint[result.size ()]); diff --git a/N2A/src/gov/sandia/n2a/backend/vensim/ImportVensim.java b/N2A/src/gov/sandia/n2a/backend/vensim/ImportVensim.java index 5e340ac3..2eca4dc8 100644 --- a/N2A/src/gov/sandia/n2a/backend/vensim/ImportVensim.java +++ b/N2A/src/gov/sandia/n2a/backend/vensim/ImportVensim.java @@ -1,5 +1,5 @@ /* -Copyright 2019-2021 National Technology & Engineering Solutions of Sandia, LLC (NTESS). +Copyright 2019-2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software. */ @@ -24,11 +24,15 @@ public String getName () } @Override - public void process (Path source) + public void process (Path source, String name) { ImportJob job = new ImportJob (); job.process (source); - if (job.model.size () > 0) MainFrame.instance.undoManager.apply (new AddDoc (job.modelName, job.model)); + if (job.model.size () > 0) + { + if (name == null) name = job.modelName; + MainFrame.undoManager.apply (new AddDoc (name, job.model)); + } } @Override diff --git a/N2A/src/gov/sandia/n2a/host/Host.java b/N2A/src/gov/sandia/n2a/host/Host.java index e148e4ce..24c61847 100644 --- a/N2A/src/gov/sandia/n2a/host/Host.java +++ b/N2A/src/gov/sandia/n2a/host/Host.java @@ -601,6 +601,11 @@ public boolean hasRun () return false; } + public String shellSuffix () + { + return ""; + } + public class ProcessInfo { public long pid; diff --git a/N2A/src/gov/sandia/n2a/host/SshPath.java b/N2A/src/gov/sandia/n2a/host/SshPath.java index 1c0cbc3c..c7036a8a 100644 --- a/N2A/src/gov/sandia/n2a/host/SshPath.java +++ b/N2A/src/gov/sandia/n2a/host/SshPath.java @@ -1,5 +1,5 @@ /* -Copyright 2020-2022 National Technology & Engineering Solutions of Sandia, LLC (NTESS). +Copyright 2020-2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software. */ @@ -355,4 +355,9 @@ public boolean existsResolved () throws IOException return false; } } + + public Host getHost () + { + return fileSystem.connection.host; + } } diff --git a/N2A/src/gov/sandia/n2a/host/Unix.java b/N2A/src/gov/sandia/n2a/host/Unix.java index 1cfe977b..963ab37a 100644 --- a/N2A/src/gov/sandia/n2a/host/Unix.java +++ b/N2A/src/gov/sandia/n2a/host/Unix.java @@ -126,7 +126,7 @@ public void submitJob (MNode job, boolean out2err, List> commands, } Path jobDir = Host.getJobDir (resourceDir, job); - Path script = jobDir.resolve ("n2a_job"); + Path script = jobDir.resolve ("n2a_job.sh"); String out = out2err ? "err" : "out"; String combined = ""; // The last assembled command-line. Used to find PID for single-command jobs (the usual case). try (BufferedWriter writer = Files.newBufferedWriter (script)) @@ -225,6 +225,12 @@ public void killJob (MNode job, boolean force) throws Exception try (AnyProcess proc = build (command).start ();) {} } + @Override + public String shellSuffix () + { + return ".sh"; + } + @Override public long getMemoryTotal () { diff --git a/N2A/src/gov/sandia/n2a/host/Windows.java b/N2A/src/gov/sandia/n2a/host/Windows.java index 9cf0026d..87ebf121 100644 --- a/N2A/src/gov/sandia/n2a/host/Windows.java +++ b/N2A/src/gov/sandia/n2a/host/Windows.java @@ -280,6 +280,12 @@ public void killJob (MNode job, boolean force) throws Exception } } + @Override + public String shellSuffix () + { + return ".bat"; + } + public void deleteTree (Path start) { // Hack to deal with file locks held by NIO. diff --git a/N2A/src/gov/sandia/n2a/plugins/extpoints/Export.java b/N2A/src/gov/sandia/n2a/plugins/extpoints/Export.java index 7f4d85c2..75692227 100644 --- a/N2A/src/gov/sandia/n2a/plugins/extpoints/Export.java +++ b/N2A/src/gov/sandia/n2a/plugins/extpoints/Export.java @@ -1,5 +1,5 @@ /* -Copyright 2013-2021 National Technology & Engineering Solutions of Sandia, LLC (NTESS). +Copyright 2013-2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software. */ @@ -14,6 +14,7 @@ public interface Export extends ExtensionPoint { - public String getName (); - public void export (MNode document, Path destination) throws Exception; + public String getName (); + public void process (MNode document, Path destination) throws Exception; + public boolean accept (Path source); // For the purpose of file dialog filtering. This should be a lightweight test, for example examining only the suffix. } diff --git a/N2A/src/gov/sandia/n2a/plugins/extpoints/Import.java b/N2A/src/gov/sandia/n2a/plugins/extpoints/Import.java index 6a9ee4b7..68d3f977 100644 --- a/N2A/src/gov/sandia/n2a/plugins/extpoints/Import.java +++ b/N2A/src/gov/sandia/n2a/plugins/extpoints/Import.java @@ -1,5 +1,5 @@ /* -Copyright 2013-2021 National Technology & Engineering Solutions of Sandia, LLC (NTESS). +Copyright 2013-2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software. */ @@ -13,7 +13,7 @@ public interface Import extends ExtensionPoint { public String getName (); - public void process (Path source); + public void process (Path source, String name) throws Exception; // "name" is a hint for the internal key of the created record. May be null. May be ignored by some importers. public float matches (Path source); // @return The probability that the file contains this format. public boolean accept (Path source); // For the purpose of file dialog filtering. This should be a lightweight test, for example examining only the suffix. } diff --git a/N2A/src/gov/sandia/n2a/transfer/ExportNative.java b/N2A/src/gov/sandia/n2a/transfer/ExportNative.java index 11ad6520..6532a287 100644 --- a/N2A/src/gov/sandia/n2a/transfer/ExportNative.java +++ b/N2A/src/gov/sandia/n2a/transfer/ExportNative.java @@ -1,5 +1,5 @@ /* -Copyright 2016-2021 National Technology & Engineering Solutions of Sandia, LLC (NTESS). +Copyright 2016-2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software. */ @@ -24,7 +24,7 @@ public String getName () } @Override - public void export (MNode source, Path destination) throws IOException + public void process (MNode source, Path destination) throws IOException { // Write a standard repository file. See MDoc.save() BufferedWriter writer = Files.newBufferedWriter (destination); @@ -33,4 +33,16 @@ public void export (MNode source, Path destination) throws IOException for (MNode n : source) schema.write (n, writer, ""); writer.close (); } + + @Override + public boolean accept (Path source) + { + if (Files.isDirectory (source)) return true; + String name = source.getFileName ().toString (); + int lastDot = name.lastIndexOf ('.'); + if (lastDot < 0) return true; // Assume a file with no suffix could be an n2a file. + String suffix = name.substring (lastDot); + if (suffix.equalsIgnoreCase (".n2a")) return true; + return false; + } } diff --git a/N2A/src/gov/sandia/n2a/transfer/ImportNative.java b/N2A/src/gov/sandia/n2a/transfer/ImportNative.java index 02e55487..322ff824 100644 --- a/N2A/src/gov/sandia/n2a/transfer/ImportNative.java +++ b/N2A/src/gov/sandia/n2a/transfer/ImportNative.java @@ -1,5 +1,5 @@ /* -Copyright 2016-2021 National Technology & Engineering Solutions of Sandia, LLC (NTESS). +Copyright 2016-2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software. */ @@ -26,13 +26,19 @@ public String getName () } @Override - public void process (Path source) + public void process (Path source, String name) { + if (name == null) + { + name = source.getFileName ().toString (); + if (name.endsWith (".n2a")) name = name.substring (0, name.length () - 4); + } + try (BufferedReader reader = Files.newBufferedReader (source)) { MVolatile doc = new MVolatile (); Schema.readAll (doc, reader); - MainFrame.instance.undoManager.apply (new AddDoc (source.getFileName ().toString (), doc)); + MainFrame.undoManager.apply (new AddDoc (name, doc)); } catch (IOException e) { diff --git a/N2A/src/gov/sandia/n2a/ui/MainFrame.java b/N2A/src/gov/sandia/n2a/ui/MainFrame.java index 80acb577..ab8cc45c 100644 --- a/N2A/src/gov/sandia/n2a/ui/MainFrame.java +++ b/N2A/src/gov/sandia/n2a/ui/MainFrame.java @@ -42,10 +42,10 @@ @SuppressWarnings("serial") public class MainFrame extends JFrame { - public static MainFrame instance; + public static MainFrame instance; + public static UndoManager undoManager = new UndoManager (); public MainTabbedPane tabs; - public UndoManager undoManager; public MainFrame () { @@ -71,7 +71,6 @@ public MainFrame () setIconImages (icons); // TODO: make icon appear on Mac. This requires accessing the "dock", which is an arbitrarily different concept than "tray" on every other desktop system, even though it does the same thing. - undoManager = new UndoManager (); tabs = new MainTabbedPane (); Lay.BLtg (this, diff --git a/N2A/src/gov/sandia/n2a/ui/NTextField.java b/N2A/src/gov/sandia/n2a/ui/NTextField.java index 28aaf0f5..7b8d8bc1 100644 --- a/N2A/src/gov/sandia/n2a/ui/NTextField.java +++ b/N2A/src/gov/sandia/n2a/ui/NTextField.java @@ -71,7 +71,7 @@ public void actionPerformed (ActionEvent evt) try { if (undoManager.canUndo ()) undoManager.undo (); - else MainFrame.instance.undoManager.undo (); + else MainFrame.undoManager.undo (); } catch (CannotUndoException e) {} } @@ -83,7 +83,7 @@ public void actionPerformed (ActionEvent evt) try { if (undoManager.canRedo ()) undoManager.redo(); - else MainFrame.instance.undoManager.redo (); + else MainFrame.undoManager.redo (); } catch (CannotRedoException e) {} } diff --git a/N2A/src/gov/sandia/n2a/ui/Undoable.java b/N2A/src/gov/sandia/n2a/ui/Undoable.java index 5910957d..7756e39d 100644 --- a/N2A/src/gov/sandia/n2a/ui/Undoable.java +++ b/N2A/src/gov/sandia/n2a/ui/Undoable.java @@ -1,5 +1,5 @@ /* -Copyright 2016,2020 National Technology & Engineering Solutions of Sandia, LLC (NTESS). +Copyright 2016-2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software. */ @@ -16,7 +16,7 @@ public class Undoable implements UndoableEdit { protected boolean hasBeenDone = false; // In the original AbstractUndoableEdit class, this was initialized true. We set it false and expect the caller to run redo() during creation of this object. protected boolean alive = true; - public Component tab = MainFrame.instance.tabs.getSelectedComponent (); + public Component tab = MainFrame.instance == null ? null : MainFrame.instance.tabs.getSelectedComponent (); public void die () { diff --git a/N2A/src/gov/sandia/n2a/ui/eq/GraphNode.java b/N2A/src/gov/sandia/n2a/ui/eq/GraphNode.java index 1cb38470..4b730526 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/GraphNode.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/GraphNode.java @@ -518,7 +518,7 @@ else if (this == gp.pinOut) ChangeAnnotations ca = new ChangeAnnotations (np, metadata); ca.graph = true; - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; if (selection.isEmpty ()) { um.apply (ca); @@ -1082,7 +1082,7 @@ public void actionPerformed (ActionEvent e) selection.remove (container.panelEquationGraph.graphPanel.pinOut); container.panelEquationGraph.clearSelection (); // In case pinIn or pinOut were selected. After delete, nothing should be selected. - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; if (selection.isEmpty ()) { um.apply (new DeletePart (node, false)); @@ -1681,7 +1681,7 @@ public void mouseReleased (MouseEvent me) } else if (SwingUtilities.isLeftMouseButton (me)) { - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; if (connect) { if (edge == null) // extend selection, because mouse never left the node boundary diff --git a/N2A/src/gov/sandia/n2a/ui/eq/GraphParent.java b/N2A/src/gov/sandia/n2a/ui/eq/GraphParent.java index 050b87df..b0e4fbe5 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/GraphParent.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/GraphParent.java @@ -276,7 +276,7 @@ public void mouseReleased (MouseEvent me) float em = SettingsLookAndFeel.em; if (now.width != old.width ) boundsParent.setTruncated (now.width / em, 2, "width"); if (now.height != old.height) boundsParent.setTruncated (now.height / em, 2, "height"); - if (boundsParent.size () > 0) MainFrame.instance.undoManager.apply (new ChangeAnnotations (part, metadata)); + if (boundsParent.size () > 0) MainFrame.undoManager.apply (new ChangeAnnotations (part, metadata)); } } diff --git a/N2A/src/gov/sandia/n2a/ui/eq/PanelEquationGraph.java b/N2A/src/gov/sandia/n2a/ui/eq/PanelEquationGraph.java index b1f820ce..d9444f56 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/PanelEquationGraph.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/PanelEquationGraph.java @@ -1131,7 +1131,7 @@ public void actionPerformed (ActionEvent e) { metadata.set (action, "gui", "arrow"); } - MainFrame.instance.undoManager.apply (new ChangeAnnotations (n, metadata)); + MainFrame.undoManager.apply (new ChangeAnnotations (n, metadata)); } }; @@ -1163,7 +1163,7 @@ public void actionPerformed (ActionEvent e) location = new Point (g.pinInBounds.x, g.pinInBounds.y + index * lineHeight); } - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; switch (e.getActionCommand ()) { case "Color": @@ -1332,7 +1332,7 @@ public void applyPinNameChange (String pinSide, String pinKey, String newKey) for (MNode p : pinData) if (newKey.equals (p.key ())) return; // Abort change if any key matches. If the name has not changed, this includes its own value. // Apply change - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; um.addEdit (new CompoundEdit ()); if (pinSide.equals ("in")) { @@ -1469,7 +1469,7 @@ public void applyPinTopicChange (GraphEdge e, String topic) { NodePart part = e.nodeFrom.node; if (topic.equals (part.source.get ("$meta", "gui", "pin", "topic"))) return; - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; if (topic.isEmpty ()) // reset to default { DeleteAnnotation da = DeleteAnnotation.withName (part, "$meta", "gui", "pin", "topic"); @@ -1638,7 +1638,7 @@ public boolean importData (TransferSupport xfer) public void exportDone (JComponent source, Transferable data, int action) { - MainFrame.instance.undoManager.endCompoundEdit (); + MainFrame.undoManager.endCompoundEdit (); } } @@ -1970,7 +1970,7 @@ public void mouseReleased (MouseEvent me) { edge.tipDrag = false; // For those cases where edge will continue to be used, rather than evaporate or be replaced. - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; um.addEdit (new CompoundEdit ()); // Everything is done inside a compound, even if it is a single edit. GraphNode nodeFrom = edge.nodeFrom; @@ -2456,7 +2456,7 @@ else if (orderPin != null) MNode metadata = new MVolatile (); for (int i = 0; i < count; i++) metadata.set (i, "gui", "pin", pinSide, pinOrder.get (i).key (), "order"); - MainFrame.instance.undoManager.apply (new ChangeAnnotations (container.part, metadata)); + MainFrame.undoManager.apply (new ChangeAnnotations (container.part, metadata)); } } } diff --git a/N2A/src/gov/sandia/n2a/ui/eq/PanelEquationTree.java b/N2A/src/gov/sandia/n2a/ui/eq/PanelEquationTree.java index 00e8093f..d0bba9e9 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/PanelEquationTree.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/PanelEquationTree.java @@ -912,7 +912,7 @@ else if (leadNode != null) } // Create transaction - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; CompoundEditView compound = null; int count = selection.size (); boolean multi = count > 1; @@ -957,7 +957,7 @@ public void watchSelected (boolean clearOthers) if (clearOthers) findExistingWatches (container.root, variables); - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; CompoundEditView compound = null; boolean multi = variables.size () > 1; if (multi) um.addEdit (compound = new CompoundEditView (CompoundEditView.CLEAR_TREE)); @@ -1043,7 +1043,7 @@ public void moveSelected (int direction) NodeBase nodeAfter = (NodeBase) model.getChild (parent, indexAfter); indexBefore = parent.getIndex (nodeBefore); indexAfter = parent.getIndex (nodeAfter); - MainFrame.instance.undoManager.apply (new ChangeOrder ((NodePart) parent, indexBefore, indexAfter)); + MainFrame.undoManager.apply (new ChangeOrder ((NodePart) parent, indexBefore, indexAfter)); } } } @@ -1071,7 +1071,7 @@ public static void outsource (NodePart part) data.clear ("$meta", "gui", "bounds"); // Create transaction - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; um.addEdit (new CompoundEdit ()); AddDoc a = new AddDoc (part.source.key (), data); a.setSilent (); // Necessary so that focus stays on part. Outsource ctor examines focus. diff --git a/N2A/src/gov/sandia/n2a/ui/eq/PanelEquations.java b/N2A/src/gov/sandia/n2a/ui/eq/PanelEquations.java index 619017fa..f2213fa8 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/PanelEquations.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/PanelEquations.java @@ -39,9 +39,7 @@ import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; -import java.io.PrintStream; import java.io.StringReader; import java.io.StringWriter; import java.nio.file.Files; @@ -66,7 +64,6 @@ import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JMenuItem; -import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JSplitPane; @@ -246,7 +243,7 @@ public void actionPerformed (ActionEvent e) { stopEditing (); AddDoc add = new AddDoc (); - MainFrame.instance.undoManager.apply (add); + MainFrame.undoManager.apply (add); } }); @@ -954,7 +951,7 @@ public void actionPerformed (ActionEvent e) if (record == null) { AddDoc add = new AddDoc (); - MainFrame.instance.undoManager.apply (add); + MainFrame.undoManager.apply (add); // After load(doc), active is null. // PanelEquationTree focusGained() will set active, but won't be called before the test below. active = getParentEquationTree (); @@ -1034,7 +1031,7 @@ else if (invoker == gp) if (context == null || context == part || context instanceof NodeIO) return; // Only process graph nodes, not parent node or IO pin blocks. // Bind part to an IO pin - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; if (pin == null) { if (context.source.child ("$meta", "gui", "pin") == null) @@ -1338,7 +1335,7 @@ class ExporterFilter extends FileFilter @Override public boolean accept (File f) { - return true; + return exporter.accept (f.toPath ()); } @Override @@ -1371,53 +1368,27 @@ public void actionPerformed (ActionEvent e) // Display chooser and collect result int result = fc.showSaveDialog (MainFrame.instance); + if (result != JFileChooser.APPROVE_OPTION) return; // Do export - if (result == JFileChooser.APPROVE_OPTION) + Path path = fc.getSelectedFile ().toPath (); + Export exporter = ((ExporterFilter) fc.getFileFilter ()).exporter; + Thread t = new Thread () { - Path path = fc.getSelectedFile ().toPath (); - Export exporter = ((ExporterFilter) fc.getFileFilter ()).exporter; - Thread t = new Thread () + public void run () { - public void run () + try { - try - { - exporter.export (record, path); - } - catch (Exception error) - { - File crashdump = new File (AppData.properties.get ("resourceDir"), "crashdump"); - try - { - PrintStream err = new PrintStream (crashdump); - error.printStackTrace (err); - err.close (); - } - catch (FileNotFoundException fnfe) {} - - EventQueue.invokeLater (new Runnable () - { - public void run () - { - JOptionPane.showMessageDialog - ( - MainFrame.instance, - "

" - + error.getMessage () + " Exception has been recorded in " - + crashdump.getAbsolutePath () - + "

", - "Export Failed", - JOptionPane.WARNING_MESSAGE - ); - } - }); - } + exporter.process (record, path); } - }; - t.setDaemon (true); - t.start (); - } + catch (Exception error) + { + PanelModel.fileImportExportException ("Export", error); + } + } + }; + t.setDaemon (true); + t.start (); } }; @@ -1464,13 +1435,26 @@ public void actionPerformed (ActionEvent e) // Display chooser and collect result int result = fc.showOpenDialog (MainFrame.instance); + if (result != JFileChooser.APPROVE_OPTION) return; // Do import - if (result == JFileChooser.APPROVE_OPTION) + Thread t = new Thread () { - Path path = fc.getSelectedFile ().toPath (); - PanelModel.importFile (path); - } + public void run () + { + try + { + Path path = fc.getSelectedFile ().toPath (); + PanelModel.importFile (path); + } + catch (Exception error) + { + PanelModel.fileImportExportException ("Import", error); + } + } + }; + t.setDaemon (true); + t.start (); } }; @@ -1750,7 +1734,7 @@ else if (comp instanceof PanelEquationGraph) } // Handle internal DnD as a node reordering. - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; if (xferNode != null && xferNode.panel == PanelEquations.this && xfer.isDrop ()) { if (path == null) return false; @@ -2232,7 +2216,7 @@ protected void exportDone (JComponent comp, Transferable data, int action) // as one of the conditions below for early-out.) We close the compound edit here // without adding anything further to it. We may create a compound delete below, // but it will be a separate action. - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; um.endCompoundEdit (); // This is safe, even if there is no compound edit in progress. TransferableNode tn = (TransferableNode) data; diff --git a/N2A/src/gov/sandia/n2a/ui/eq/PanelMRU.java b/N2A/src/gov/sandia/n2a/ui/eq/PanelMRU.java index 8534d331..41cd9c1c 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/PanelMRU.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/PanelMRU.java @@ -144,7 +144,7 @@ protected Transferable createTransferable (JComponent comp) protected void exportDone (JComponent source, Transferable data, int action) { list.clearSelection (); - MainFrame.instance.undoManager.endCompoundEdit (); // This is safe, even if there is no compound edit in progress. + MainFrame.undoManager.endCompoundEdit (); // This is safe, even if there is no compound edit in progress. } }); diff --git a/N2A/src/gov/sandia/n2a/ui/eq/PanelModel.java b/N2A/src/gov/sandia/n2a/ui/eq/PanelModel.java index a699eeb6..3b14896c 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/PanelModel.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/PanelModel.java @@ -13,7 +13,12 @@ import java.awt.FontMetrics; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintStream; import java.nio.file.Path; + +import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JSplitPane; import javax.swing.JTree; @@ -27,6 +32,7 @@ import gov.sandia.n2a.plugins.ExtensionPoint; import gov.sandia.n2a.plugins.PluginManager; import gov.sandia.n2a.plugins.extpoints.Import; +import gov.sandia.n2a.ui.MainFrame; import gov.sandia.n2a.ui.eq.PanelEquations.BreadcrumbRenderer; import gov.sandia.n2a.ui.settings.SettingsLookAndFeel; @@ -320,19 +326,62 @@ public void childChanged (String oldKey, String newKey) panelEquations.updateDoc (oldKey, newKey); } - public static void importFile (Path path) + public static void importFile (Path path) throws Exception + { + importFile (path, null, null); + } + + public static void importFile (Path path, String format, String name) throws Exception { Import bestImporter = null; float bestP = 0; for (ExtensionPoint exp : PluginManager.getExtensionsForPoint (Import.class)) { - float P = ((Import) exp).matches (path); + Import imp = (Import) exp; + if (format != null && imp.getName ().equalsIgnoreCase (format)) + { + bestImporter = imp; + break; + } + float P = imp.matches (path); if (P > bestP) { bestP = P; bestImporter = (Import) exp; } } - if (bestImporter != null) bestImporter.process (path); + if (bestImporter != null) bestImporter.process (path, name); + } + + /** + Report an import error to the user via UI and crashdump file. + **/ + public static void fileImportExportException (String direction, Exception error) + { + File crashdump = new File (AppData.properties.get ("resourceDir"), "crashdump"); + try + { + PrintStream err = new PrintStream (crashdump); + error.printStackTrace (err); + err.close (); + } + catch (FileNotFoundException fnfe) {} + + EventQueue.invokeLater (new Runnable () + { + public void run () + { + JOptionPane.showMessageDialog + ( + MainFrame.instance, + "

" + + error.getMessage () + " Exception has been recorded in " + + crashdump.getAbsolutePath () + + "

", + direction + " Failed", + JOptionPane.WARNING_MESSAGE + ); + } + }); } } diff --git a/N2A/src/gov/sandia/n2a/ui/eq/PanelSearch.java b/N2A/src/gov/sandia/n2a/ui/eq/PanelSearch.java index 3d2bb732..8904eb01 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/PanelSearch.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/PanelSearch.java @@ -142,7 +142,7 @@ public String getToolTipText (MouseEvent e) tree.addTreeSelectionListener (this); //tree.putClientProperty ("JTree.lineStyle", "None"); // Get rid of lines that connect children to parents. Also need to hide handles, but that is more difficult (no option in JTree). - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; InputMap inputMap = tree.getInputMap (); inputMap.put (KeyStroke.getKeyStroke ("INSERT"), "add"); @@ -280,9 +280,23 @@ public boolean importData (TransferSupport xfer) { @SuppressWarnings("unchecked") List files = (List) xferable.getTransferData (DataFlavor.javaFileListFlavor); + Exception error = null; um.addEdit (new CompoundEdit ()); // in case there is more than one file - for (File file : files) PanelModel.importFile (file.toPath ()); + // Ideally this would be on a separate thread, but since we are modifying a compound edit, + // we need to stay on EDT. + for (File file : files) + { + try + { + PanelModel.importFile (file.toPath ()); + } + catch (Exception e) + { + error = e; + } + } um.endCompoundEdit (); + if (error != null) PanelModel.fileImportExportException ("Import", error); return true; } else if (xfer.isDataFlavorSupported (DataFlavor.stringFlavor)) @@ -501,7 +515,7 @@ public void valueChanged (TreeSelectionEvent e) if (lastConnection == null) return; // Ensure that we just added a part. - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; UndoableEdit ue = um.editToBeUndone (); if (! (ue instanceof AddPart)) return; @@ -1343,7 +1357,7 @@ public void run () } AddPart ap = new AddPart (parent, parent.getChildCount (), data, c); - MainFrame.instance.undoManager.apply (ap); + MainFrame.undoManager.apply (ap); if (newRoot.getChildCount () > 1) lastConnection = ap.getCreatedNode ().getKeyPath (); } }); diff --git a/N2A/src/gov/sandia/n2a/ui/eq/search/NodeModel.java b/N2A/src/gov/sandia/n2a/ui/eq/search/NodeModel.java index 0fc60c48..e5665638 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/search/NodeModel.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/search/NodeModel.java @@ -166,11 +166,11 @@ public void applyEdit (JTree tree) existingDocument = models.child (input); } - MainFrame.instance.undoManager.apply (new ChangeDoc (key, input)); + MainFrame.undoManager.apply (new ChangeDoc (key, input)); } public void delete (JTree tree, boolean cancelled) { - MainFrame.instance.undoManager.apply (new DeleteDoc ((MDoc) AppData.docs.child ("models", key))); + MainFrame.undoManager.apply (new DeleteDoc ((MDoc) AppData.docs.child ("models", key))); } } diff --git a/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeAnnotation.java b/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeAnnotation.java index 2d81e917..46c14a48 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeAnnotation.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeAnnotation.java @@ -216,7 +216,7 @@ public void applyEdit (JTree tree) String input = (String) getUserObject (); if (input.isEmpty ()) { - boolean canceled = MainFrame.instance.undoManager.getPresentationName ().equals ("AddAnnotation"); + boolean canceled = MainFrame.undoManager.getPresentationName ().equals ("AddAnnotation"); delete (canceled); return; } @@ -268,7 +268,7 @@ public void applyEdit (JTree tree) return; } - MainFrame.instance.undoManager.apply (new ChangeAnnotation (this, name, value)); + MainFrame.undoManager.apply (new ChangeAnnotation (this, name, value)); } @Override diff --git a/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeBase.java b/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeBase.java index 39fda909..9c059199 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeBase.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeBase.java @@ -477,7 +477,7 @@ public NodeBase containerFor (String type) public NodeBase add (String type, JTree tree, MNode data, Point2D.Double location) { Undoable u = makeAdd (type, tree, data, location); - if (u != null) MainFrame.instance.undoManager.apply (u); + if (u != null) MainFrame.undoManager.apply (u); if (u instanceof AddEditable) return ((AddEditable) u).getCreatedNode (); return null; } @@ -500,7 +500,7 @@ public void applyEdit (JTree tree) public void delete (boolean canceled) { Undoable u = makeDelete (canceled); - if (u != null) MainFrame.instance.undoManager.apply (u); + if (u != null) MainFrame.undoManager.apply (u); } public Undoable makeDelete (boolean canceled) diff --git a/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeEquation.java b/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeEquation.java index c5fe2019..be98d5d4 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeEquation.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeEquation.java @@ -156,7 +156,7 @@ public void applyEdit (JTree tree) String input = (String) getUserObject (); if (input.isEmpty ()) { - boolean canceled = MainFrame.instance.undoManager.getPresentationName ().startsWith ("AddEquation"); + boolean canceled = MainFrame.undoManager.getPresentationName ().startsWith ("AddEquation"); delete (canceled); return; } @@ -190,7 +190,7 @@ public void applyEdit (JTree tree) piecesBefore.combiner = parent.source.get (); // The fact that we are modifying an existing equation node indicates that the variable (parent) should only contain a combiner. if (piecesAfter.combiner.isEmpty ()) piecesAfter.combiner = piecesBefore.combiner; - MainFrame.instance.undoManager.apply (new ChangeEquation (parent, piecesBefore.condition, piecesBefore.combiner, piecesBefore.expression, piecesAfter.condition, piecesAfter.combiner, piecesAfter.expression)); + MainFrame.undoManager.apply (new ChangeEquation (parent, piecesBefore.condition, piecesBefore.combiner, piecesBefore.expression, piecesAfter.condition, piecesAfter.combiner, piecesAfter.expression)); } @Override @@ -200,7 +200,7 @@ public Undoable makeDelete (boolean canceled) if (canceled) // Our delete is expected to neutralize an add. (But we still verify below that the add exists.) { // Hack to ensure unkill on variable gets reversed properly. - String last = MainFrame.instance.undoManager.getPresentationName (); + String last = MainFrame.undoManager.getPresentationName (); result.killedVariable = last.equals ("AddEquation killed"); } return result; diff --git a/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeInherit.java b/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeInherit.java index 228bda0a..f9404988 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeInherit.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeInherit.java @@ -96,7 +96,7 @@ public void applyEdit (JTree tree) String input = (String) getUserObject (); if (input.isEmpty ()) { - boolean canceled = MainFrame.instance.undoManager.getPresentationName ().equals ("AddInherit"); + boolean canceled = MainFrame.undoManager.getPresentationName ().equals ("AddInherit"); delete (canceled); return; } @@ -121,7 +121,7 @@ public void applyEdit (JTree tree) return; } - MainFrame.instance.undoManager.apply (new ChangeInherit (this, value)); + MainFrame.undoManager.apply (new ChangeInherit (this, value)); } @Override diff --git a/N2A/src/gov/sandia/n2a/ui/eq/tree/NodePart.java b/N2A/src/gov/sandia/n2a/ui/eq/tree/NodePart.java index f228139c..260a8e0d 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/tree/NodePart.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/tree/NodePart.java @@ -618,7 +618,7 @@ public static void suggestConnections (List fromParts, List if (u.candidates.size () == 0) continue; NodePart toPart = u.candidates.get (0); NodeVariable v = (NodeVariable) fromPart.child (u.alias); // This must exist. - MainFrame.instance.undoManager.apply (new ChangeVariable (v, u.alias, toPart.source.key ())); + MainFrame.undoManager.apply (new ChangeVariable (v, u.alias, toPart.source.key ())); toPart.connectionTarget = true; } } @@ -958,7 +958,7 @@ public void applyEdit (JTree tree) return; } - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; if (isTrueRoot ()) // Edits to root cause a rename of the document on disk { if (name.isEmpty ()) diff --git a/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeReference.java b/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeReference.java index 8ddce255..7fa38b48 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeReference.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeReference.java @@ -109,7 +109,7 @@ public void applyEdit (JTree tree) String input = (String) getUserObject (); if (input.isEmpty ()) { - boolean canceled = MainFrame.instance.undoManager.getPresentationName ().equals ("AddReference"); + boolean canceled = MainFrame.undoManager.getPresentationName ().equals ("AddReference"); delete (canceled); return; } @@ -145,7 +145,7 @@ public void applyEdit (JTree tree) return; } - MainFrame.instance.undoManager.apply (new ChangeReference (parent, oldName, oldValue, name, value)); + MainFrame.undoManager.apply (new ChangeReference (parent, oldName, oldValue, name, value)); } @Override diff --git a/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeVariable.java b/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeVariable.java index d5d88d17..521895a3 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeVariable.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/tree/NodeVariable.java @@ -532,7 +532,7 @@ public boolean allowEdit () public void applyEdit (JTree tree) { String input = toString (); - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; boolean added = um.getPresentationName ().equals ("AddVariable noname"); if (input.isEmpty ()) { diff --git a/N2A/src/gov/sandia/n2a/ui/eq/undo/AddDoc.java b/N2A/src/gov/sandia/n2a/ui/eq/undo/AddDoc.java index a71ca9e7..24f5ea2a 100644 --- a/N2A/src/gov/sandia/n2a/ui/eq/undo/AddDoc.java +++ b/N2A/src/gov/sandia/n2a/ui/eq/undo/AddDoc.java @@ -42,7 +42,13 @@ public AddDoc (String name, MNode saved) this.name = uniqueName (MDir.validFilenameFrom (name)); this.saved = saved; - PanelModel pm = PanelModel.instance; + // Insert ID, if given doc does not already have one. + MNode id = saved.childOrCreate ("$meta", "id"); + String idString = id.get (); + if (idString.isEmpty () || AppData.getModel (idString) != null) id.set (generateID ()); + + PanelModel pm = PanelModel.instance; + if (pm == null) return; // We are running headless. JTree tree = pm.panelSearch.tree; fromSearchPanel = tree.isFocusOwner (); // Otherwise, assume focus is on equation tree if (fromSearchPanel) // Otherwise, pathAfter is null and new entry will appear at top of uncategorized entries. @@ -64,11 +70,6 @@ public AddDoc (String name, MNode saved) if (! category.isEmpty ()) saved.set (category, "$meta", "gui", "category"); } } - - // Insert ID, if given doc does not already have one. - MNode id = saved.childOrCreate ("$meta", "id"); - String idString = id.get (); - if (idString.isEmpty () || AppData.getModel (idString) != null) id.set (generateID ()); } public void setSilent () @@ -127,13 +128,14 @@ public void redo () public static void create (String name, MNode saved, List pathAfter, boolean fromSearchPanel, boolean wasShowing) { - PanelModel pm = PanelModel.instance; - pm.panelSearch.insertNextAt (pathAfter); // Note that lastSelection will end up pointing to new entry, not pathAfter. + PanelModel pm = PanelModel.instance; // If null, then we are running headless. + if (pm != null) pm.panelSearch.insertNextAt (pathAfter); // Note that lastSelection will end up pointing to new entry, not pathAfter. MDoc doc = (MDoc) AppData.docs.childOrCreate ("models", name); // Triggers PanelModel.childAdded(name), which updates the select and MRU panels, but not the equation tree panel. doc.merge (saved); new MPart (doc).clearRedundantOverrides (); AppData.set (doc.get ("$meta", "id"), doc); + if (pm == null) return; if (doc.get ("$meta", "gui", "category").contains (",")) pm.panelSearch.search (); // update for multiple categories if (wasShowing) pm.panelEquations.load (doc); // Takes focus diff --git a/N2A/src/gov/sandia/n2a/ui/ref/ExportBibTeX.java b/N2A/src/gov/sandia/n2a/ui/ref/ExportBibTeX.java index fd0ae25f..d8e698b6 100644 --- a/N2A/src/gov/sandia/n2a/ui/ref/ExportBibTeX.java +++ b/N2A/src/gov/sandia/n2a/ui/ref/ExportBibTeX.java @@ -1,5 +1,5 @@ /* -Copyright 2021 National Technology & Engineering Solutions of Sandia, LLC (NTESS). +Copyright 2021-2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software. */ @@ -8,6 +8,9 @@ import java.io.IOException; import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; + import gov.sandia.n2a.db.MNode; public class ExportBibTeX extends ExportBibliography @@ -19,7 +22,7 @@ public String getName () } @Override - public void export (MNode references, Writer writer) throws IOException + public void process (MNode references, Writer writer) throws IOException { String nl = String.format ("%n"); @@ -30,4 +33,25 @@ public void export (MNode references, Writer writer) throws IOException writer.write ("}" + nl); } } + + @Override + public boolean accept (Path source) + { + if (Files.isDirectory (source)) return true; + String name = source.getFileName ().toString (); + int lastDot = name.lastIndexOf ('.'); + if (lastDot >= 0) + { + String suffix = name.substring (lastDot); + if (suffix.equalsIgnoreCase (".bib")) return true; + if (suffix.equalsIgnoreCase (".bibtex")) return true; + } + return false; + } + + @Override + public String suffix () + { + return ".bib"; + } } diff --git a/N2A/src/gov/sandia/n2a/ui/ref/ExportBibliography.java b/N2A/src/gov/sandia/n2a/ui/ref/ExportBibliography.java index e2797f1c..f6dedae4 100644 --- a/N2A/src/gov/sandia/n2a/ui/ref/ExportBibliography.java +++ b/N2A/src/gov/sandia/n2a/ui/ref/ExportBibliography.java @@ -20,7 +20,7 @@ public abstract class ExportBibliography implements Export { @Override - public void export (MNode document, Path destination) throws Exception + public void process (MNode document, Path destination) throws Exception { MNode references = new MVolatile (); if (document.child ("$meta") == null && document.child ("form") != null) // document is a reference. @@ -68,11 +68,18 @@ public boolean visit (MNode node) } } + String name = destination.getFileName ().toString (); + int pos = name.lastIndexOf ('.'); + if (pos > 0) name = name.substring (0, pos); + name += suffix (); + destination = destination.getParent ().resolve (name); + try (Writer writer = Files.newBufferedWriter (destination)) { - export (references, writer); + process (references, writer); } } - public abstract void export (MNode references, Writer writer) throws IOException; + public abstract void process (MNode references, Writer writer) throws IOException; + public abstract String suffix (); } diff --git a/N2A/src/gov/sandia/n2a/ui/ref/ImportBibliography.java b/N2A/src/gov/sandia/n2a/ui/ref/ImportBibliography.java index 4bbb5fec..267ba3f0 100644 --- a/N2A/src/gov/sandia/n2a/ui/ref/ImportBibliography.java +++ b/N2A/src/gov/sandia/n2a/ui/ref/ImportBibliography.java @@ -1,5 +1,5 @@ /* -Copyright 2021 National Technology & Engineering Solutions of Sandia, LLC (NTESS). +Copyright 2021-2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software. */ @@ -21,7 +21,7 @@ public abstract class ImportBibliography implements Import { @Override - public void process (Path path) + public void process (Path path, String name) { try (BufferedReader reader = Files.newBufferedReader (path)) { @@ -31,7 +31,7 @@ public void process (Path path) for (MNode n : data) // data can contain several entries { String key = MDir.validFilenameFrom (n.key ()); - MainFrame.instance.undoManager.apply (new AddEntry (key, n)); + MainFrame.undoManager.apply (new AddEntry (key, n)); } } catch (IOException e) diff --git a/N2A/src/gov/sandia/n2a/ui/ref/PanelEntry.java b/N2A/src/gov/sandia/n2a/ui/ref/PanelEntry.java index 42ae4826..e642e87a 100644 --- a/N2A/src/gov/sandia/n2a/ui/ref/PanelEntry.java +++ b/N2A/src/gov/sandia/n2a/ui/ref/PanelEntry.java @@ -43,9 +43,7 @@ import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; -import java.io.PrintStream; import java.io.StringReader; import java.nio.file.Path; import java.util.ArrayList; @@ -64,7 +62,6 @@ import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JLabel; -import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; @@ -223,7 +220,7 @@ public void actionPerformed (ActionEvent e) { public void actionPerformed (ActionEvent evt) { - try {MainFrame.instance.undoManager.undo ();} + try {MainFrame.undoManager.undo ();} catch (CannotUndoException e) {} catch (CannotRedoException e) {} } @@ -232,7 +229,7 @@ public void actionPerformed (ActionEvent evt) { public void actionPerformed (ActionEvent evt) { - try {MainFrame.instance.undoManager.redo();} + try {MainFrame.undoManager.redo();} catch (CannotUndoException e) {} catch (CannotRedoException e) {} } @@ -255,7 +252,7 @@ public void focusLost (FocusEvent e) } }); - gov.sandia.n2a.ui.UndoManager um = MainFrame.instance.undoManager; + gov.sandia.n2a.ui.UndoManager um = MainFrame.undoManager; table.setTransferHandler (new TransferHandler () { public boolean canImport (TransferSupport xfer) @@ -408,7 +405,7 @@ public void run () { public void actionPerformed (ActionEvent e) { - MainFrame.instance.undoManager.apply (new AddEntry ()); + MainFrame.undoManager.apply (new AddEntry ()); } }); @@ -477,14 +474,14 @@ public void addTag () { if (model.record == null) { - MainFrame.instance.undoManager.apply (new AddEntry ()); + MainFrame.undoManager.apply (new AddEntry ()); } else if (! model.locked) { int row = table.getSelectedRow (); if (row < 0) row = model.keys.size (); if (row < 3) row = 3; // keep id, form and title at the top - MainFrame.instance.undoManager.apply (new AddTag (model.record, row)); + MainFrame.undoManager.apply (new AddTag (model.record, row)); } } @@ -494,7 +491,7 @@ public void deleteTag () int row = table.getSelectedRow (); if (row < 3) return; // Protect id, form and title String tag = model.keys.get (row); - MainFrame.instance.undoManager.apply (new DeleteTag (model.record, tag)); + MainFrame.undoManager.apply (new DeleteTag (model.record, tag)); } /** @@ -564,39 +561,27 @@ public void actionPerformed (ActionEvent e) // Display chooser and collect result int result = fc.showSaveDialog (MainFrame.instance); + if (result != JFileChooser.APPROVE_OPTION) return; // Do export - if (result == JFileChooser.APPROVE_OPTION) + Path path = fc.getSelectedFile ().toPath (); + ExporterFilter filter = (ExporterFilter) fc.getFileFilter (); + Thread t = new Thread () { - Path path = fc.getSelectedFile ().toPath (); - ExporterFilter filter = (ExporterFilter) fc.getFileFilter (); - try - { - filter.exporter.export (model.record, path); - } - catch (Exception error) + public void run () { - File crashdump = new File (AppData.properties.get ("resourceDir"), "crashdump"); try { - PrintStream err = new PrintStream (crashdump); - error.printStackTrace (err); - err.close (); + filter.exporter.process (model.record, path); + } + catch (Exception error) + { + PanelModel.fileImportExportException ("Export", error); } - catch (FileNotFoundException fnfe) {} - - JOptionPane.showMessageDialog - ( - MainFrame.instance, - "

" - + error.getMessage () + " Exception has been recorded in " - + crashdump.getAbsolutePath () - + "

", - "Export Failed", - JOptionPane.WARNING_MESSAGE - ); } - } + }; + t.setDaemon (true); + t.start (); } }; @@ -644,13 +629,26 @@ public void actionPerformed (ActionEvent e) // Display chooser and collect result int result = fc.showOpenDialog (MainFrame.instance); + if (result != JFileChooser.APPROVE_OPTION) return; // Do import - if (result == JFileChooser.APPROVE_OPTION) + Thread t = new Thread () { - Path path = fc.getSelectedFile ().toPath (); - PanelModel.importFile (path); // This works for references too. - } + public void run () + { + try + { + Path path = fc.getSelectedFile ().toPath (); + PanelModel.importFile (path); // This works for references too. + } + catch (Exception error) + { + PanelModel.fileImportExportException ("Import", error); + } + } + }; + t.setDaemon (true); + t.start (); } }; @@ -846,7 +844,7 @@ public void setValueAt (Object value, int row, int column) if (newValue.equals (tag)) return; // nothing to do if (newValue.isEmpty ()) // tag is deleted. Most likely it was a new tag the user changed their mind about, but could also be an old tag. { - MainFrame.instance.undoManager.apply (new DeleteTag (record, tag)); + MainFrame.undoManager.apply (new DeleteTag (record, tag)); return; } if (record.child (newValue) != null) return; // not allowed, because another tag with that name already exists @@ -854,7 +852,7 @@ public void setValueAt (Object value, int row, int column) int exposedRow = keys.indexOf (newValue); // If non-negative, then we are about to overwrite a standard tag that isn't currently defined. if (isStandardTag (tag)) exposedRow = row; // About to expose a standard tag (which will become undefined). - MainFrame.instance.undoManager.apply (new RenameTag (record, exposedRow, tag, newValue)); + MainFrame.undoManager.apply (new RenameTag (record, exposedRow, tag, newValue)); } else if (column == 1) // value change { @@ -865,11 +863,11 @@ else if (column == 1) // value change if (newValue.isEmpty ()) return; // not allowed newValue = MDir.validFilenameFrom (newValue); if (AppData.docs.child ("references", newValue) != null) return; // not allowed, because another entry with that id already exists - MainFrame.instance.undoManager.apply (new ChangeEntry (record.key (), newValue)); + MainFrame.undoManager.apply (new ChangeEntry (record.key (), newValue)); } else { - MainFrame.instance.undoManager.apply (new ChangeTag (record, tag, newValue)); + MainFrame.undoManager.apply (new ChangeTag (record, tag, newValue)); } } } diff --git a/N2A/src/gov/sandia/n2a/ui/ref/PanelMRU.java b/N2A/src/gov/sandia/n2a/ui/ref/PanelMRU.java index 231ddc7a..99e9350a 100644 --- a/N2A/src/gov/sandia/n2a/ui/ref/PanelMRU.java +++ b/N2A/src/gov/sandia/n2a/ui/ref/PanelMRU.java @@ -106,7 +106,7 @@ protected Transferable createTransferable (JComponent comp) // Note that the output is a BibTeX entry, not the usual N2A schema. try (StringWriter writer = new StringWriter ()) { - PanelSearch.exportBibTeX.export (references, writer); + PanelSearch.exportBibTeX.process (references, writer); writer.close (); return new TransferableReference (writer.toString (), key); } diff --git a/N2A/src/gov/sandia/n2a/ui/ref/PanelSearch.java b/N2A/src/gov/sandia/n2a/ui/ref/PanelSearch.java index 43319100..7931ec04 100644 --- a/N2A/src/gov/sandia/n2a/ui/ref/PanelSearch.java +++ b/N2A/src/gov/sandia/n2a/ui/ref/PanelSearch.java @@ -99,7 +99,7 @@ public PanelSearch () { public void actionPerformed (ActionEvent e) { - MainFrame.instance.undoManager.apply (new AddEntry ()); + MainFrame.undoManager.apply (new AddEntry ()); } }); actionMap.put ("delete", new AbstractAction () @@ -110,7 +110,7 @@ public void actionPerformed (ActionEvent e) if (key == null) return; if (! ((MCombo) AppData.docs.child ("references")).isWriteable (key)) return; lastSelection = list.getSelectedIndex (); - MainFrame.instance.undoManager.apply (new DeleteEntry ((MDoc) AppData.docs.child ("references", key))); + MainFrame.undoManager.apply (new DeleteEntry ((MDoc) AppData.docs.child ("references", key))); } }); actionMap.put ("select", new AbstractAction () @@ -166,7 +166,7 @@ public void focusLost (FocusEvent e) } }); - UndoManager um = MainFrame.instance.undoManager; + UndoManager um = MainFrame.undoManager; transferHandler = new TransferHandler () { public boolean canImport (TransferSupport xfer) @@ -188,9 +188,23 @@ public boolean importData (TransferSupport xfer) { @SuppressWarnings("unchecked") List files = (List) xferable.getTransferData (DataFlavor.javaFileListFlavor); + Exception error = null; um.addEdit (new CompoundEdit ()); // in case there is more than one file - for (File file : files) PanelModel.importFile (file.toPath ()); + // Ideally this would be on a separate thread, but since we are modifying a compound edit, + // we need to stay on EDT. + for (File file : files) + { + try + { + PanelModel.importFile (file.toPath ()); + } + catch (Exception e) + { + error = e; + } + } um.endCompoundEdit (); + if (error != null) PanelModel.fileImportExportException ("Import", error); return true; } else if (xfer.isDataFlavorSupported (DataFlavor.stringFlavor)) @@ -250,7 +264,7 @@ protected Transferable createTransferable (JComponent comp) // Note that the output is a BibTeX entry, not the usual N2A schema. try (StringWriter writer = new StringWriter ()) { - exportBibTeX.export (references, writer); + exportBibTeX.process (references, writer); writer.close (); return new TransferableReference (writer.toString (), key); } diff --git a/N2A/src/gov/sandia/n2a/ui/ref/undo/AddEntry.java b/N2A/src/gov/sandia/n2a/ui/ref/undo/AddEntry.java index 6e543e1d..110bdbbf 100644 --- a/N2A/src/gov/sandia/n2a/ui/ref/undo/AddEntry.java +++ b/N2A/src/gov/sandia/n2a/ui/ref/undo/AddEntry.java @@ -29,10 +29,6 @@ public AddEntry (String id, MNode saved) { this.saved = saved; - PanelReference pr = PanelReference.instance; - fromSearchPanel = pr.panelSearch.list.isFocusOwner (); // Otherwise, assume focus is on the entry panel - keyAfter = pr.panelSearch.currentKey (); - // Determine unique name in database MNode references = AppData.docs.child ("references"); MNode existing = references.child (id); @@ -52,6 +48,11 @@ public AddEntry (String id, MNode saved) suffix++; } } + + PanelReference pr = PanelReference.instance; + if (pr == null) return; // We are running headless. + fromSearchPanel = pr.panelSearch.list.isFocusOwner (); // Otherwise, assume focus is on the entry panel + keyAfter = pr.panelSearch.currentKey (); } public void undo () @@ -77,9 +78,10 @@ public void redo () { super.redo (); int index = 0; - if (! keyAfter.isEmpty ()) + PanelReference pr = PanelReference.instance; // If null, then we are running headless. + if (pr != null && ! keyAfter.isEmpty ()) { - index = PanelReference.instance.panelSearch.indexOf (keyAfter); + index = pr.panelSearch.indexOf (keyAfter); if (index < 0) index = 0; } create (id, saved, index, fromSearchPanel, wasShowing, wasShowing); @@ -88,11 +90,12 @@ public void redo () public static void create (String id, MNode saved, int index, boolean fromSearchPanel, boolean wasShowing, boolean willEdit) { PanelReference pr = PanelReference.instance; - pr.panelSearch.insertNextAt (index); + if (pr != null) pr.panelSearch.insertNextAt (index); MNode doc = AppData.docs.childOrCreate ("references", id); // Triggers PanelReference.childAdded(name), which updates the select and MRU panels, but not the entry panel. doc.merge (saved); + if (pr == null) return; if (wasShowing) pr.panelEntry.model.setRecord (doc); if (willEdit || ! fromSearchPanel) { diff --git a/N2A/src/gov/sandia/n2a/ui/settings/PanelDiff.java b/N2A/src/gov/sandia/n2a/ui/settings/PanelDiff.java index 94e5b34e..ea64d9e0 100644 --- a/N2A/src/gov/sandia/n2a/ui/settings/PanelDiff.java +++ b/N2A/src/gov/sandia/n2a/ui/settings/PanelDiff.java @@ -1,5 +1,5 @@ /* -Copyright 2020 National Technology & Engineering Solutions of Sandia, LLC (NTESS). +Copyright 2020-2023 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software. */ @@ -69,7 +69,7 @@ public void actionPerformed (ActionEvent e) { if (lastDelta.deleted) // Force document to be restored in full, rather than allowing targeted undelete. { - MainFrame.instance.undoManager.apply (container.new RevertDelta (lastDelta)); + MainFrame.undoManager.apply (container.new RevertDelta (lastDelta)); return; } @@ -90,11 +90,11 @@ public void actionPerformed (ActionEvent e) // Guard against deleting entire document. Such changes must be relegated to RevertDelta. if (lastDelta.untracked && p == root) { - MainFrame.instance.undoManager.apply (container.new RevertDelta (lastDelta)); + MainFrame.undoManager.apply (container.new RevertDelta (lastDelta)); return; } - if (n != null) MainFrame.instance.undoManager.apply (new RevertDiff (n)); + if (n != null) MainFrame.undoManager.apply (new RevertDiff (n)); } }); diff --git a/N2A/src/gov/sandia/n2a/ui/settings/SettingsRepo.java b/N2A/src/gov/sandia/n2a/ui/settings/SettingsRepo.java index 6ec899b3..c78d53a4 100644 --- a/N2A/src/gov/sandia/n2a/ui/settings/SettingsRepo.java +++ b/N2A/src/gov/sandia/n2a/ui/settings/SettingsRepo.java @@ -795,7 +795,7 @@ public boolean isEmpty (String repoName) public void pull (GitWrapper gitRepo, String key) { - MainFrame.instance.undoManager.discardAllEdits (); // TODO: purge only edits related to the current repo. + MainFrame.undoManager.discardAllEdits (); // TODO: purge only edits related to the current repo. Thread thread = new Thread () { public void run () @@ -1006,7 +1006,7 @@ public boolean toggle (int row, int column) int oldRow = primaryRow; primaryRow = row; AppData.state.set (newKey, "Repos", "primary"); - MainFrame.instance.undoManager.discardAllEdits (); + MainFrame.undoManager.discardAllEdits (); needRebuild = true; fireTableCellUpdated (oldRow, 0); // Primary fireTableCellUpdated (row, 0); @@ -1017,7 +1017,7 @@ public boolean toggle (int row, int column) if (editable == 0) editable = 1; else editable = 0; repo.set (editable, "editable"); - MainFrame.instance.undoManager.discardAllEdits (); // TODO: It would be better to only purge edits related to the specific repo. + MainFrame.undoManager.discardAllEdits (); // TODO: It would be better to only purge edits related to the specific repo. needRebuild = true; fireTableCellUpdated (row, 1); return true; @@ -1026,7 +1026,7 @@ public boolean toggle (int row, int column) if (visible == 0) visible = 1; else visible = 0; repo.set (visible, "visible"); - MainFrame.instance.undoManager.discardAllEdits (); + MainFrame.undoManager.discardAllEdits (); needRebuild = true; fireTableCellUpdated (row, 2); return true; @@ -1217,12 +1217,12 @@ public void updateOrder () if (! newPrimary.equals (primary)) { AppData.state.set (newPrimary, "Repos", "primary"); - MainFrame.instance.undoManager.discardAllEdits (); + MainFrame.undoManager.discardAllEdits (); } } else { - MainFrame.instance.undoManager.discardAllEdits (); + MainFrame.undoManager.discardAllEdits (); } } } @@ -2264,7 +2264,7 @@ public synchronized void revert (int row) if (row < 0 || row >= deltas.size ()) return; Delta delta = deltas.get (row); if (delta.untracked) return; - MainFrame.instance.undoManager.apply (new RevertDelta (delta)); + MainFrame.undoManager.apply (new RevertDelta (delta)); } public int indexOf (String name) @@ -2317,7 +2317,7 @@ public synchronized void commit () deltas = newDeltas; refreshTable (); - MainFrame.instance.undoManager.discardAllEdits (); // TODO: purge only edits related to the current repo. + MainFrame.undoManager.discardAllEdits (); // TODO: purge only edits related to the current repo. Thread thread = new Thread () {