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