View Javadoc
1   // ******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2025.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  // ******************************************************************************
38  package ffx;
39  
40  import edu.rit.pj.Comm;
41  import edu.rit.pj.cluster.Configuration;
42  import ffx.potential.Utilities;
43  import ffx.ui.LogHandler;
44  import ffx.ui.MainPanel;
45  import ffx.ui.ModelingShell;
46  import ffx.ui.OSXAdapter;
47  import ffx.utilities.FFXScript;
48  import groovy.lang.Script;
49  import org.apache.commons.io.FilenameUtils;
50  import org.apache.commons.lang3.SystemUtils;
51  import org.apache.commons.lang3.builder.ToStringBuilder;
52  import org.apache.commons.lang3.time.StopWatch;
53  import org.apache.commons.logging.impl.LogFactoryImpl;
54  
55  import javax.annotation.Nullable;
56  import javax.swing.ImageIcon;
57  import javax.swing.JFrame;
58  import javax.swing.SwingUtilities;
59  import javax.swing.UIManager;
60  import java.awt.GraphicsEnvironment;
61  import java.awt.Toolkit;
62  import java.awt.event.WindowAdapter;
63  import java.awt.event.WindowEvent;
64  import java.io.File;
65  import java.io.IOException;
66  import java.net.InetAddress;
67  import java.net.URL;
68  import java.net.UnknownHostException;
69  import java.util.ArrayList;
70  import java.util.Arrays;
71  import java.util.Date;
72  import java.util.List;
73  import java.util.Properties;
74  import java.util.concurrent.TimeUnit;
75  import java.util.logging.Handler;
76  import java.util.logging.Level;
77  import java.util.logging.LogManager;
78  import java.util.logging.Logger;
79  
80  import static java.lang.String.format;
81  
82  /**
83   * The Main class is the entry point to the graphical user interface version of Force Field X.
84   *
85   * @author Michael J. Schnieders
86   * @since 1.0
87   */
88  public final class Main extends JFrame {
89  
90    private static final Logger logger = Logger.getLogger(Main.class.getName());
91    /**
92     * This is the main application wrapper.
93     */
94    public static MainPanel mainPanel;
95    /**
96     * Constant <code>stopWatch</code>
97     */
98    private static StopWatch stopWatch = new StopWatch();
99    /**
100    * Handle FFX logging.
101    */
102   private static LogHandler logHandler;
103   /**
104    * The configured Scheduler port.
105    */
106   private static int schedulerPort;
107   /**
108    * Parallel Java Configuration instance.
109    */
110   private static Configuration configuration = null;
111   /**
112    * Parallel Java World Communicator.
113    */
114   private static Comm world = null;
115   /**
116    * Name of the machine FFX is running on.
117    */
118   private static String hostName = null;
119   /**
120    * Force Field X process ID.
121    */
122   private static int procID = -1;
123   /**
124    * Print version and exit
125    */
126   private static boolean printVersionAndExit = false;
127   /**
128    * This Runnable is used to init the GUI using SwingUtilities.
129    */
130   private final Runnable initGUI =
131       new Runnable() {
132         // Create the MainPanel and MainMenu, then add them to the JFrame.
133         public void run() {
134 
135           Toolkit.getDefaultToolkit().setDynamicLayout(true);
136 
137           // Set the Title and Icon
138           Main.this.setTitle("Force Field X");
139           URL iconURL = getClass().getClassLoader().getResource("ffx/ui/icons/icon64.png");
140           if (iconURL != null) {
141             ImageIcon icon = new ImageIcon(iconURL);
142             Main.this.setIconImage(icon.getImage());
143           }
144           Main.this.addWindowListener(
145               new WindowAdapter() {
146                 @Override
147                 public void windowClosing(WindowEvent e) {
148                   if (mainPanel != null) {
149                     mainPanel.exit();
150                   }
151                   System.exit(0);
152                 }
153               });
154 
155           mainPanel = new MainPanel(Main.this);
156           mainPanel.setVisible(false);
157           logHandler.setMainPanel(mainPanel);
158           Main.this.add(mainPanel);
159           mainPanel.initialize();
160           Main.this.setVisible(true);
161           mainPanel.setVisible(true);
162           Main.this.setJMenuBar(mainPanel.getMainMenu());
163 
164           // This is a hack to get GraphicsCanvis to initialize on some platform/Java3D
165           // combinations.
166           // mainPanel.setPanel(MainPanel.KEYWORDS);
167           // mainPanel.setVisible(true);
168           // mainPanel.setPanel(MainPanel.GRAPHICS);
169 
170           // Mac OS X specific features that help Force Field X look native
171           // on Macs. This needs to be done after the MainPanel is created.
172           if (SystemUtils.IS_OS_MAC_OSX) {
173             OSXAdapter osxAdapter = new OSXAdapter(mainPanel);
174           }
175 
176           SwingUtilities.updateComponentTreeUI(SwingUtilities.getRoot(Main.this));
177         }
178       };
179 
180   /**
181    * Main does some window initializations.
182    *
183    * @param commandLineFile a {@link java.io.File} object.
184    * @param argList         a {@link java.util.List} object.
185    */
186   public Main(@Nullable File commandLineFile, List<String> argList) {
187     super("Force Field X");
188     // Start the clock.
189     stopWatch.start();
190 
191     // Init the GUI.
192     try {
193       SwingUtilities.invokeAndWait(initGUI);
194     } catch (Exception e) {
195       logger.warning(" Exception initializing the GUI.\n" + e);
196     }
197 
198     // Run the supplied command or file.
199     if (commandLineFile != null) {
200       runScript(mainPanel.getModelingShell(), commandLineFile, argList);
201     }
202 
203     if (logger.isLoggable(Level.FINE)) {
204       StringBuilder sb = new StringBuilder();
205       sb.append(format("\n Start-up Time (msec): %s.", stopWatch.getTime(TimeUnit.MILLISECONDS)));
206       Runtime runtime = Runtime.getRuntime();
207       runtime.gc();
208       long occupiedMemory = runtime.totalMemory() - runtime.freeMemory();
209       long KB = 1024;
210       sb.append(format("\n In-Use Memory   (Kb): %d", occupiedMemory / KB));
211       sb.append(format("\n Free Memory     (Kb): %d", runtime.freeMemory() / KB));
212       sb.append(format("\n Total Memory    (Kb): %d", runtime.totalMemory() / KB));
213       logger.fine(sb.toString());
214     }
215   }
216 
217   /**
218    * Create an instance of Force Field X
219    *
220    * @param args an array of {@link java.lang.String} objects.
221    */
222   public static void main(String[] args) {
223 
224     List<String> argList = new ArrayList<>();
225     File commandLineFile = initMain(args, argList);
226 
227     try {
228       // Start up the GUI or CLI version of Force Field X.
229       if (!GraphicsEnvironment.isHeadless()) {
230         startGraphicalUserInterface(commandLineFile, argList);
231       } else {
232         if (commandLineFile == null) {
233           logger.info(" No command line file supplied.");
234         } else {
235           startCommandLineInterface(commandLineFile, argList);
236         }
237       }
238     } catch (Throwable t) {
239       int statusCode = 1;
240       logger.info(" Uncaught exception: exiting with status code " + statusCode);
241       logger.info(Utilities.stackTraceToString(t));
242       System.exit(statusCode);
243     }
244   }
245 
246   /**
247    * Process the input arguments into a List, start the logging, start Parallel Java and process the
248    * input command.
249    *
250    * @param args    an array of {@link java.lang.String} objects.
251    * @param argList List is filled with processed arguments.
252    * @return A file for FFX to operate on.
253    */
254   public static File initMain(String[] args, List<String> argList) {
255     // Process any "-D" command line flags.
256     args = processProperties(args);
257 
258     // Start up the Parallel Java communication layer.
259     startParallelJava(args);
260 
261     // Configure our logging.
262     startLogging();
263 
264     // Determine host name and process ID.
265     environment();
266 
267     // Print the header.
268     header(args);
269 
270     // Print out help for the command line interface.
271     if (GraphicsEnvironment.isHeadless() && args.length < 2) {
272       if (args.length == 1 && args[0].toUpperCase().contains("TEST")) {
273         commandLineInterfaceHelp(true);
274       }
275       commandLineInterfaceHelp(false);
276     }
277 
278     // Parse the specified command or structure file.
279     File commandLineFile = null;
280     int nArgs = args.length;
281     if (nArgs > 0) {
282       commandLineFile = new File(args[0]);
283       // Resolve a relavtive path
284       if (commandLineFile.exists()) {
285         commandLineFile = new File(FilenameUtils.normalize(commandLineFile.getAbsolutePath()));
286       }
287     }
288 
289     // Convert the args to a List<String>.
290     if (nArgs > 1) {
291       argList.addAll(Arrays.asList(args).subList(1, nArgs));
292     }
293 
294     return commandLineFile;
295   }
296 
297   /**
298    * A main entry point that runs a script and return a refernce to the result.
299    *
300    * @param args an array of {@link java.lang.String} objects.
301    * @return A Groovy Script instance.
302    */
303   public static Script ffxScript(String[] args) {
304     List<String> argList = new ArrayList<>();
305     File commandLineFile = initMain(args, argList);
306     try {
307       if (commandLineFile == null) {
308         logger.info(" No command line file supplied.");
309       } else {
310         return startCommandLineInterface(commandLineFile, argList);
311       }
312     } catch (Throwable t) {
313       int statusCode = 1;
314       logger.info(" Uncaught exception: exiting with status code " + statusCode);
315       logger.info(Utilities.stackTraceToString(t));
316       System.exit(statusCode);
317     }
318     return null;
319   }
320 
321   /**
322    * Print out help for the command line version of Force Field X.
323    */
324   private static void commandLineInterfaceHelp(boolean listTestScripts) {
325     logger.info(" usage: ffxc [-D<property=value>] <command> [-options] <PDB|XYZ>");
326     if (listTestScripts) {
327       FFXScript.listGroovyScripts(false, true);
328       logger.info("\n For help on an experimental or test command use:  ffxc <command> -h\n");
329     } else {
330       FFXScript.listGroovyScripts(true, false);
331       logger.info("\n To list experimental & test scripts: ffxc --test");
332       logger.info(" For help on a specific command use:  ffxc <command> -h\n");
333     }
334     System.exit(0);
335   }
336 
337   /**
338    * Determine the host name, process ID, and FFX base directory.
339    */
340   private static void environment() {
341     try {
342       InetAddress addr = InetAddress.getLocalHost();
343       hostName = addr.getHostName();
344     } catch (UnknownHostException e) {
345       // Do nothing.
346     }
347 
348     String procString = System.getProperty("app.pid");
349     if (procString != null) {
350       procID = Integer.parseInt(procString);
351     } else {
352       procID = 0;
353     }
354 
355     String dirString = System.getProperty("basedir");
356     File ffxDirectory;
357     if (dirString != null) {
358       ffxDirectory = new File(dirString);
359     } else {
360       ffxDirectory = new File(".");
361     }
362 
363     try {
364       logger.fine(format(" Force Field X directory is %s", ffxDirectory.getCanonicalPath()));
365     } catch (Exception e) {
366       // Do Nothing.
367     }
368   }
369 
370   /**
371    * Print out a promo.
372    */
373   private static void header(String[] args) {
374     StringBuilder sb = new StringBuilder();
375     sb.append(MainPanel.border).append("\n");
376     sb.append(MainPanel.title).append("\n");
377     sb.append(MainPanel.aboutString).append("\n");
378     sb.append(MainPanel.border);
379 
380     // Print the FFX version and exit.
381     if (printVersionAndExit && GraphicsEnvironment.isHeadless()) {
382       logger.info(sb.toString());
383       System.exit(0);
384     }
385 
386     sb.append("\n ").append(new Date());
387     sb.append(format("\n Process ID %d on %s.", procID, hostName));
388 
389     // Print out command line arguments if the array is not null.
390     if (args != null && args.length > 0) {
391       sb.append("\n\n Command line arguments:\n ");
392       sb.append(Arrays.toString(args));
393       sb.append("\n");
394     }
395 
396     if (schedulerPort > 1) {
397       sb.append(format("\n Parallel Java:\n %s", world.toString()));
398       if (configuration != null) {
399         sb.append(
400             format(
401                 "\n Scheduler %s on port %d.\n", configuration.getSchedulerHost(), schedulerPort));
402       }
403     }
404 
405     logger.info(sb.toString());
406   }
407 
408   /**
409    * Process any "-D" command line flags.
410    */
411   private static String[] processProperties(String[] args) {
412     List<String> newArgs = new ArrayList<>();
413     for (String arg : args) {
414       arg = arg.trim();
415 
416       if (arg.equals("-V") || arg.equals("--version")) {
417         printVersionAndExit = true;
418       }
419 
420       if (arg.startsWith("-D")) {
421         // Remove -D from the front of String.
422         arg = arg.substring(2);
423         // Split at the first equals if it exists.
424         if (arg.contains("=")) {
425           int equalsPosition = arg.indexOf("=");
426           String key = arg.substring(0, equalsPosition);
427           String value = arg.substring(equalsPosition + 1);
428           // Set the system property.
429           System.setProperty(key, value);
430         } else {
431           if (!arg.isEmpty()) {
432             System.setProperty(arg, "");
433           }
434         }
435       } else {
436         // Collect non "-D" arguments.
437         newArgs.add(arg);
438       }
439     }
440     // Return the remaining arguments.
441     args = new String[newArgs.size()];
442     newArgs.toArray(args);
443     return args;
444   }
445 
446   /**
447    * Start the Force Field X command line interface.
448    *
449    * @param commandLineFile The command line file.
450    * @param argList         The command line argument list.
451    */
452   private static Script startCommandLineInterface(File commandLineFile, List<String> argList) {
453     if (configuration == null) {
454       logger.info(" Starting up the command line interface.\n");
455     }
456     HeadlessMain m = new HeadlessMain(logHandler);
457     mainPanel = m.mainPanel;
458     return runScript(mainPanel.getModelingShell(), commandLineFile, argList);
459   }
460 
461   /**
462    * Start the Force Field X graphical user interface.
463    *
464    * @param commandLineFile The command line file.
465    * @param argList         The command line argument list.
466    */
467   private static void startGraphicalUserInterface(File commandLineFile, List<String> argList) {
468     logger.info(" Starting up the graphical user interface.");
469 
470     // Set some Swing Constants.
471     UIManager.put("swing.boldMetal", Boolean.FALSE);
472     setDefaultLookAndFeelDecorated(false);
473 
474     /*
475      Some Mac OS X specific features that help FFX look native. These need
476      to be set before the MainPanel is created.
477     */
478     if (SystemUtils.IS_OS_MAC_OSX) {
479       OSXAdapter.setOSXProperties();
480       try {
481         UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
482       } catch (Exception e) {
483         //
484       }
485     }
486 
487     // Initialize the Main frame and Force Field X MainPanel.
488     new Main(commandLineFile, argList);
489   }
490 
491   /**
492    * Replace the default console handler with our custom FFX handler.
493    */
494   private static void startLogging() {
495     // Remove all log handlers from the default logger.
496     try {
497       Logger defaultLogger = LogManager.getLogManager().getLogger("");
498       Handler[] defaultHandlers = defaultLogger.getHandlers();
499       for (Handler h : defaultHandlers) {
500         defaultLogger.removeHandler(h);
501       }
502     } catch (Exception e) {
503       String error = e.toString();
504       System.err.println(error);
505     }
506 
507     // Suppress various loggers used by FFX dependencies.
508     // Attempt to suppress Log4j version 1.x.
509     // https://logging.apache.org/log4j/1.x/manual.html
510     System.setProperty("log4j.threshold", "OFF");
511     System.setProperty("log4j.rootLogger", "OFF");
512     System.setProperty("log4j1.compatibility", "true");
513     Properties properties = new Properties();
514     properties.setProperty("log4j.threshold", "OFF");
515     properties.setProperty("log4j.rootLogger", "OFF");
516     properties.setProperty("log4j1.compatibility", "true");
517     org.apache.log4j.PropertyConfigurator.configure(properties);
518     org.apache.log4j.LogManager.getRootLogger().setLevel(org.apache.log4j.Level.OFF);
519 
520     // Attempt to suppress Log4j2 version 2.x.
521     // https://logging.apache.org/log4j/2.x/manual
522     System.setProperty("log4j2.level", "OFF");
523     System.setProperty("log4j2.simplelogLevel", "OFF");
524     System.setProperty("log4j2.disable.jmx", "true");
525     org.apache.logging.log4j.LogManager.getRootLogger().atLevel(org.apache.logging.log4j.Level.OFF);
526 
527     // Attempt to suppress SLF4J loggers.
528     // https://www.slf4j.org/manual.html
529     System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "off");
530     // The statement below causes a warning to be printed to the console for SLF4J 2.0.16.
531     // System.setProperty(LoggerFactory.PROVIDER_PROPERTY_KEY, "org.slf4j.nop.NOPServiceProvider");
532     // org.slf4j.Logger rootLogger = org.slf4j.LoggerFactory.getILoggerFactory().getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
533 
534     // Attempt to suppress Apache Commons loggers.
535     // https://commons.apache.org/proper/commons-logging/guide.html
536     System.setProperty(LogFactoryImpl.LOG_PROPERTY, "org.apache.commons.logging.impl.NoOpLog");
537     org.apache.commons.logging.LogFactory.getFactory()
538         .setAttribute(LogFactoryImpl.LOG_PROPERTY, "org.apache.commons.logging.impl.NoOpLog");
539 
540     /*
541     // The following message is logged in Jupyter notebooks. It would be nice to determine how to suppress it.
542     // The bug is described here: https://issues.apache.org/jira/browse/BEANUTILS-477
543     // INFO org.apache.commons.beanutils.FluentPropertyBeanIntrospector -
544     // Error when creating PropertyDescriptor for
545     // public final void org.apache.commons.configuration2.AbstractConfiguration.setProperty(java.lang.String,java.lang.Object)!
546     // Ignoring this property.
547      */
548 
549     Logger ffxLogger = Logger.getLogger("ffx");
550     // Remove any existing handlers.
551     for (Handler handler : ffxLogger.getHandlers()) {
552       ffxLogger.removeHandler(handler);
553     }
554 
555     // Retrieve the log level from the ffx.log system property.
556     String logLevel = System.getProperty("ffx.log", "info");
557     Level tempLevel;
558     try {
559       tempLevel = Level.parse(logLevel.toUpperCase());
560     } catch (Exception e) {
561       tempLevel = Level.INFO;
562     }
563 
564     Level level = tempLevel;
565     logHandler = new LogHandler();
566     logHandler.setLevel(level);
567     ffxLogger.addHandler(logHandler);
568     ffxLogger.setLevel(level);
569   }
570 
571   /**
572    * Start up the Parallel Java communication layer.
573    */
574   private static void startParallelJava(String[] args) {
575 
576     // Try to read the PJ configuration file.
577     try {
578       configuration = new Configuration("cluster.txt");
579     } catch (IOException e) {
580       // No cluster configuration file was found.
581     }
582 
583     // Attempt to read the "pj.nn" System property.
584     String numNodes = System.getProperty("pj.nn");
585     int requestedNodes = -1;
586     if (numNodes != null) {
587       try {
588         requestedNodes = Integer.parseInt(numNodes);
589       } catch (Exception e) {
590         // Error parsing the the pj.nn property.
591       }
592     }
593 
594     // Attempt to read the "pj.port" System property.
595     String portString = System.getProperty("pj.port");
596     schedulerPort = -1;
597     if (portString != null) {
598       try {
599         schedulerPort = Integer.parseInt(portString);
600       } catch (Exception e) {
601         // Error parsing the the pj.nn property.
602       }
603     }
604 
605     // Configure the scheduler port if it wasn't set on the command line.
606     if (schedulerPort <= 0) {
607       if (requestedNodes <= 0) {
608         // If the "pj.nn" property is not set, configure the port to 1 (no scheduler).
609         schedulerPort = 1;
610       } else if (configuration != null) {
611         // Configure the port using the Configuration.
612         schedulerPort = configuration.getSchedulerPort();
613       } else {
614         // Set the port to the PJ default.
615         schedulerPort = 20617; // Must sync this value with the Scheduler.groovy script.
616       }
617       System.setProperty("pj.port", Integer.toString(schedulerPort));
618     }
619 
620     try {
621       Comm.init(args);
622       world = Comm.world();
623     } catch (Exception e) {
624       // Log the exception to System.err because the logging subsystem has not be initialized yet.
625       String message = " Exception starting up the Parallel Java communication layer.";
626       System.err.println(message);
627       String error = e.toString();
628       System.err.println(error);
629     }
630   }
631 
632   public static Script runScript(ModelingShell shell, File commandLineFile, List<String> argList) {
633     // Attempt to run a supplied script.
634     if (commandLineFile.exists()) {
635       return shell.runFFXScript(commandLineFile, argList);
636     } else {
637       // See if the commandLineFile is an embedded script.
638       String name = commandLineFile.getName();
639       Class<? extends Script> ffxScript = FFXScript.getScript(name);
640       if (ffxScript != null) {
641         return shell.runFFXScript(ffxScript, argList);
642       }
643     }
644     return null;
645   }
646 
647   /**
648    * {@inheritDoc}
649    *
650    * <p>Commons.Lang Style toString.
651    */
652   @Override
653   public String toString() {
654     ToStringBuilder toStringBuilder =
655         new ToStringBuilder(this)
656             .append("Up Time: " + stopWatch)
657             .append("Logger: " + logger.getName());
658     return toStringBuilder.toString();
659   }
660 }