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-2024.
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 static java.lang.String.format;
41  import static java.util.Collections.sort;
42  import static picocli.CommandLine.usage;
43  
44  import groovy.lang.Binding;
45  import groovy.lang.Script;
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.Enumeration;
55  import java.util.List;
56  import java.util.jar.JarEntry;
57  import java.util.jar.JarFile;
58  import java.util.logging.Level;
59  import java.util.logging.Logger;
60  import java.util.zip.ZipEntry;
61  
62  import picocli.CommandLine;
63  import picocli.CommandLine.Help.Ansi;
64  import picocli.CommandLine.Option;
65  import picocli.CommandLine.ParseResult;
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    * @param binding a {@link groovy.lang.Binding} object
121    */
122   public FFXScript(Binding binding) {
123     super(binding);
124     if (GraphicsEnvironment.isHeadless()) {
125       color = Ansi.ON;
126     } else {
127       color = Ansi.OFF;
128     }
129   }
130 
131   /**
132    * Use the System ClassLoader to find the requested script.
133    *
134    * @param name Name of the script to load (e.g. Energy).
135    * @return The Script, if found, or null.
136    */
137   public static Class<? extends FFXScript> getScript(String name) {
138     ClassLoader loader = FFXScript.class.getClassLoader();
139     String pathName = name;
140     Class<?> script;
141     try {
142       // First try to load the class directly.
143       script = loader.loadClass(pathName);
144     } catch (ClassNotFoundException e) {
145       // Next, try to load a script from the potential package.
146       pathName = "ffx.potential.groovy." + name;
147       try {
148         script = loader.loadClass(pathName);
149       } catch (ClassNotFoundException e2) {
150         // Next, try to load a script from the algorithm package.
151         pathName = "ffx.algorithms.groovy." + name;
152         try {
153           script = loader.loadClass(pathName);
154         } catch (ClassNotFoundException e3) {
155           if (name.startsWith("xray.")) {
156             // Finally, try to load a script from the xray package.
157             pathName = "ffx.xray.groovy." + name.replaceAll("xray.", "");
158           } else if (name.startsWith("realspace.")) {
159             pathName = "ffx.realspace.groovy." + name.replaceAll("realspace.", "");
160           } else {
161             pathName = "ffx." + name;
162           }
163           try {
164             script = loader.loadClass(pathName);
165           } catch (ClassNotFoundException e4) {
166             logger.warning(format(" %s was not found.", name));
167             return null;
168           }
169         }
170       }
171     }
172     return script.asSubclass(FFXScript.class);
173   }
174 
175   /**
176    * List the embedded FFX Groovy Scripts.
177    *
178    * @param logScripts     List Scripts.
179    * @param logTestScripts List Test Scripts.
180    */
181   public static void listGroovyScripts(boolean logScripts, boolean logTestScripts) {
182     ClassLoader classLoader = ClassLoader.getSystemClassLoader();
183     try {
184       logger.info("\n  Potential Package Commands:");
185       URL url = classLoader.getResource("ffx/potential");
186       listScriptsForPackage(url, logScripts, logTestScripts);
187       logger.info("\n  Algorithms Package Commands:");
188       url = classLoader.getResource("ffx/algorithms");
189       listScriptsForPackage(url, logScripts, logTestScripts);
190       logger.info("\n  Refinement Package Commands:");
191       url = classLoader.getResource("ffx/xray");
192       listScriptsForPackage(url, logScripts, logTestScripts);
193     } catch (Exception e) {
194       logger.info(" The ffx resource could not be found by the classloader.");
195     }
196   }
197 
198   /**
199    * List the embedded FFX Groovy Scripts.
200    *
201    * @param scriptURL      URL of package with scripts.
202    * @param logScripts     List Scripts.
203    * @param logTestScripts List Test Scripts.
204    */
205   private static void listScriptsForPackage(URL scriptURL, boolean logScripts, boolean logTestScripts) {
206     String scriptPath = scriptURL.getPath();
207     String ffx = scriptPath.substring(5, scriptURL.getPath().indexOf("!"));
208     List<String> scripts = new ArrayList<>();
209     List<String> testScripts = new ArrayList<>();
210 
211     try (JarFile jar = new JarFile(URLDecoder.decode(ffx, StandardCharsets.UTF_8))) {
212       // Iterates over Jar entries.
213       Enumeration<JarEntry> enumeration = jar.entries();
214       while (enumeration.hasMoreElements()) {
215         ZipEntry zipEntry = enumeration.nextElement();
216         String className = zipEntry.getName();
217         if (className.startsWith("ffx")
218             && className.contains("groovy")
219             && className.endsWith(".class")
220             && !className.contains("$")) {
221           className = className.replace("/", ".");
222           className = className.replace(".class", "");
223           // Present the classes using "short-cut" names.
224           className = className.replace("ffx.potential.groovy.", "");
225           className = className.replace("ffx.algorithms.groovy.", "");
226           className = className.replace("ffx.realspace.groovy", "realspace");
227           className = className.replace("ffx.xray.groovy", "xray");
228           if (className.toUpperCase().contains("TEST")) {
229             testScripts.add(className);
230           } else {
231             scripts.add(className);
232           }
233         }
234       }
235     } catch (Exception e) {
236       logger.info(format(" The %s resource could not be decoded.", scriptPath));
237       return;
238     }
239 
240     // Sort the scripts alphabetically.
241     sort(scripts);
242     sort(testScripts);
243 
244     // Log the script names.
245     if (logTestScripts) {
246       for (String script : testScripts) {
247         logger.info("   " + script);
248       }
249     }
250     if (logScripts) {
251       for (String script : scripts) {
252         logger.info("   " + script);
253       }
254     }
255   }
256 
257   /**
258    * Default help information.
259    *
260    * @return String describing how to use this command.
261    */
262   public String helpString() {
263     try {
264       StringOutputStream sos = new StringOutputStream(new ByteArrayOutputStream());
265       usage(this, sos, color);
266       return " " + sos;
267     } catch (UnsupportedEncodingException e) {
268       logger.log(Level.WARNING, e.toString());
269       return null;
270     }
271   }
272 
273   /**
274    * Initialize this Script based on the specified command line arguments.
275    *
276    * @return boolean Returns true if the script should continue and false to exit.
277    */
278   public boolean init() {
279     Binding binding = getBinding();
280 
281     // The args property could either be a list or an array of String arguments.
282     Object arguments = binding.getProperty("args");
283     if (arguments instanceof List<?> list) {
284       int numArgs = list.size();
285       args = new String[numArgs];
286       for (int i = 0; i < numArgs; i++) {
287         args[i] = (String) list.get(i);
288       }
289     } else {
290       args = (String[]) binding.getProperty("args");
291     }
292 
293     CommandLine commandLine = new CommandLine(this);
294     try {
295       parseResult = commandLine.parseArgs(args);
296     } catch (CommandLine.UnmatchedArgumentException uae) {
297       logger.warning(
298           " 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).");
299       throw uae;
300     }
301 
302     // Print help info exit.
303     if (help) {
304       logger.info(helpString());
305       return false;
306     }
307 
308     // Version info is printed by default.
309     // This should not be reached, due to the FFX Main class handling the "-V, --version" flag and
310     // exiting.
311     return !version;
312   }
313 
314   /**
315    * {@inheritDoc}
316    *
317    * <p>Execute the script.
318    */
319   @Override
320   public FFXScript run() {
321     logger.info(helpString());
322     return this;
323   }
324 }