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.utilities;
39  
40  import picocli.CommandLine;
41  import picocli.CommandLine.Help.Ansi;
42  import picocli.CommandLine.Option;
43  import picocli.CommandLine.ParseResult;
44  
45  import java.awt.GraphicsEnvironment;
46  import java.io.ByteArrayOutputStream;
47  import java.io.UnsupportedEncodingException;
48  import java.net.URL;
49  import java.net.URLDecoder;
50  import java.nio.charset.StandardCharsets;
51  import java.util.ArrayList;
52  import java.util.Arrays;
53  import java.util.Enumeration;
54  import java.util.List;
55  import java.util.jar.JarEntry;
56  import java.util.jar.JarFile;
57  import java.util.logging.Level;
58  import java.util.logging.Logger;
59  import java.util.zip.ZipEntry;
60  
61  import static java.lang.String.format;
62  import static java.util.Collections.sort;
63  import static picocli.CommandLine.usage;
64  
65  /**
66   * Base FFX Command class.
67   *
68   * @author Michael J. Schnieders
69   */
70  public abstract class FFXCommand {
71  
72    /**
73     * The logger for this class.
74     */
75    public static final Logger logger = Logger.getLogger(FFXCommand.class.getName());
76  
77    /**
78     * Unix shells are able to evaluate PicoCLI ANSI color codes, but right now the FFX GUI Shell does
79     * not.
80     *
81     * <p>In a headless environment, color will be ON for command line help, but OFF for the GUI.
82     */
83    public final Ansi color;
84  
85    /**
86     * The array of args passed into the Script.
87     */
88    public String[] args;
89  
90    /**
91     * Parse Result.
92     */
93    public ParseResult parseResult = null;
94  
95    /**
96     * -V or --version Prints the FFX version and exits.
97     */
98    @Option(
99        names = {"-V", "--version"},
100       versionHelp = true,
101       defaultValue = "false",
102       description = "Print the Force Field X version and exit.")
103   public boolean version;
104 
105   /**
106    * -h or --help Prints a help message.
107    */
108   @Option(
109       names = {"-h", "--help"},
110       usageHelp = true,
111       defaultValue = "false",
112       description = "Print command help and exit.")
113   public boolean help;
114 
115   /**
116    * The Binding that provides variables to this Command.
117    */
118   public FFXBinding binding;
119 
120   /**
121    * Default constructor for an FFX Command.
122    */
123   public FFXCommand() {
124     this(new FFXBinding());
125   }
126 
127   /**
128    * Create an FFX Command using the supplied command line arguments.
129    *
130    * @param args The command line arguments.
131    */
132   public FFXCommand(String[] args) {
133     this(new FFXBinding());
134     binding.setVariable("args", Arrays.asList(args));
135   }
136 
137   /**
138    * Default constructor for an FFX Command.
139    *
140    * @param binding the Binding that provides variables to this Command.
141    */
142   public FFXCommand(FFXBinding binding) {
143     this.binding = binding;
144     if (GraphicsEnvironment.isHeadless()) {
145       color = Ansi.ON;
146     } else {
147       color = Ansi.OFF;
148     }
149   }
150 
151   /**
152    * Set the Binding that provides variables to this Command.
153    *
154    * @param binding The Binding to use.
155    */
156   public void setBinding(FFXBinding binding) {
157     this.binding = binding;
158   }
159 
160   /**
161    * Use the System ClassLoader to find the requested Command.
162    *
163    * @param name Name of the Command to load (e.g., Energy).
164    * @return The Command, if found, or null.
165    */
166   public static Class<? extends FFXCommand> getCommand(String name) {
167     ClassLoader loader = FFXCommand.class.getClassLoader();
168     String pathName = name;
169     Class<?> command;
170     try {
171       // First try to load the class directly.
172       command = loader.loadClass(pathName);
173     } catch (ClassNotFoundException e) {
174       // Next, try to load a Command from the Potential commands package.
175       pathName = "ffx.potential.commands." + name;
176       try {
177         command = loader.loadClass(pathName);
178       } catch (ClassNotFoundException e2) {
179         // Next, try to load a Command from the Algorithms commands package.
180         pathName = "ffx.algorithms.commands." + name;
181         try {
182           command = loader.loadClass(pathName);
183         } catch (ClassNotFoundException e2b) {
184           if (name.startsWith("xray.")) {
185             // Finally, try to load a Command from the Refinement package.
186             pathName = "ffx.xray.commands." + name.replaceAll("xray.", "");
187           } else if (name.startsWith("realspace.")) {
188             pathName = "ffx.realspace.commands." + name.replaceAll("realspace.", "");
189           } else {
190             pathName = "ffx." + name;
191           }
192           try {
193             command = loader.loadClass(pathName);
194           } catch (ClassNotFoundException e4) {
195             logger.warning(format(" %s was not found.", name));
196             return null;
197           }
198         }
199       }
200     }
201     return command.asSubclass(FFXCommand.class);
202   }
203 
204   /**
205    * List the embedded FFX Commands.
206    *
207    * @param logCommands     List Commands.
208    * @param logTestCommands List Test Commands.
209    */
210   public static void listCommands(boolean logCommands, boolean logTestCommands) {
211     ClassLoader classLoader = ClassLoader.getSystemClassLoader();
212     try {
213       logger.info("\n  Potential Package Commands:");
214       URL url = classLoader.getResource("ffx/potential");
215       listCommandsForPackage(url, logCommands, logTestCommands);
216       logger.info("\n  Algorithms Package Commands:");
217       url = classLoader.getResource("ffx/algorithms");
218       listCommandsForPackage(url, logCommands, logTestCommands);
219       logger.info("\n  Refinement Package Commands:");
220       url = classLoader.getResource("ffx/xray");
221       listCommandsForPackage(url, logCommands, logTestCommands);
222     } catch (Exception e) {
223       logger.info(" The FFX resource could not be found by the classloader.");
224     }
225   }
226 
227   /**
228    * List the embedded FFX Commands.
229    *
230    * @param commandURL      URL of package with Commands.
231    * @param logCommands     List Commands.
232    * @param logTestCommands List Test Commands.
233    */
234   private static void listCommandsForPackage(URL commandURL, boolean logCommands, boolean logTestCommands) {
235     String commandPath = commandURL.getPath();
236     String ffx = commandPath.substring(5, commandURL.getPath().indexOf("!"));
237     List<String> commands = new ArrayList<>();
238     List<String> testCommands = new ArrayList<>();
239 
240     try (JarFile jar = new JarFile(URLDecoder.decode(ffx, StandardCharsets.UTF_8))) {
241       // Iterates over Jar entries.
242       Enumeration<JarEntry> enumeration = jar.entries();
243       while (enumeration.hasMoreElements()) {
244         ZipEntry zipEntry = enumeration.nextElement();
245         String className = zipEntry.getName();
246         if (className.startsWith("ffx")
247             && className.endsWith(".class")
248             && !className.contains("$")
249             && className.contains("commands")) {
250           className = className.replace("/", ".");
251           className = className.replace(".class", "");
252           // Present the classes using "short-cut" names.
253           className = className.replace("ffx.potential.commands.", "");
254           className = className.replace("ffx.algorithms.commands.", "");
255           className = className.replace("ffx.realspace.commands", "realspace");
256           className = className.replace("ffx.xray.commands", "xray");
257           if (className.toUpperCase().contains("TEST")) {
258             testCommands.add(className);
259           } else {
260             commands.add(className);
261           }
262         }
263       }
264     } catch (Exception e) {
265       logger.info(format(" The %s resource could not be decoded.", commandPath));
266       return;
267     }
268 
269     // Sort the scripts alphabetically.
270     sort(commands);
271     sort(testCommands);
272 
273     // Log the script names.
274     if (logTestCommands) {
275       for (String command : testCommands) {
276         logger.info("   " + command);
277       }
278     }
279     if (logCommands) {
280       for (String command : commands) {
281         logger.info("   " + command);
282       }
283     }
284   }
285 
286   /**
287    * Default help information.
288    *
289    * @return String describing how to use this command.
290    */
291   public String helpString() {
292     try {
293       StringOutputStream sos = new StringOutputStream(new ByteArrayOutputStream());
294       usage(this, sos, color);
295       return " " + sos;
296     } catch (UnsupportedEncodingException e) {
297       logger.log(Level.WARNING, e.toString());
298       return null;
299     }
300   }
301 
302   /**
303    * Initialize this Command based on the specified command line arguments.
304    *
305    * @return boolean Returns true if the script should continue and false to exit.
306    */
307   public boolean init() {
308     // The args property could either be a list or an array of String arguments.
309     Object arguments = binding.getVariable("args");
310 
311     if (arguments instanceof List<?> list) {
312       int numArgs = list.size();
313       args = new String[numArgs];
314       for (int i = 0; i < numArgs; i++) {
315         args[i] = (String) list.get(i);
316       }
317     } else if (arguments instanceof String[]) {
318       args = (String[]) arguments;
319     } else if (arguments instanceof String) {
320       args = new String[]{(String) arguments};
321     } else {
322       args = new String[0];
323     }
324 
325     CommandLine commandLine = new CommandLine(this);
326     try {
327       parseResult = commandLine.parseArgs(args);
328     } catch (CommandLine.UnmatchedArgumentException uae) {
329       logger.warning(
330           " The usual source of this exception is when long-form arguments (such as --uaA) are only preceded by one dash (such as -uaA, which is an error).");
331       throw uae;
332     }
333 
334     // Print help info exit.
335     if (help) {
336       logger.info(helpString());
337       return false;
338     }
339 
340     // Version info is printed by default.
341     // This should not be reached, due to the FFX Main class handling the "-V, --version" flag and
342     // exiting.
343     return !version;
344   }
345 
346 
347   /**
348    * Execute this Command.
349    *
350    * @return The current FFXCommand.
351    */
352   public FFXCommand run() {
353     logger.info(helpString());
354     return this;
355   }
356 }