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