View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.frontend.cli;
20  
21  import java.io.BufferedReader;
22  import java.io.File;
23  import java.io.FileNotFoundException;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.InputStreamReader;
27  import java.io.PrintWriter;
28  import java.net.URL;
29  import java.net.URLClassLoader;
30  import java.nio.charset.Charset;
31  import java.nio.charset.StandardCharsets;
32  import java.nio.file.Files;
33  import java.nio.file.Paths;
34  import java.util.ArrayList;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.Vector;
38  import java.util.concurrent.atomic.AtomicInteger;
39  import java.util.stream.Stream;
40  
41  import org.apache.commons.text.StringSubstitutor;
42  import org.apache.logging.log4j.LogManager;
43  import org.apache.logging.log4j.Logger;
44  import org.jdom2.Element;
45  import org.mycore.common.MCRClassTools;
46  import org.mycore.common.MCRSession;
47  import org.mycore.common.MCRSessionMgr;
48  import org.mycore.common.MCRSystemUserInformation;
49  import org.mycore.common.MCRTransactionHelper;
50  import org.mycore.common.config.MCRConfiguration2;
51  import org.mycore.common.content.MCRJDOMContent;
52  import org.mycore.common.events.MCRStartupHandler;
53  import org.mycore.common.xml.MCRURIResolver;
54  
55  /**
56   * The main class implementing the MyCoRe command line interface. With the
57   * command line interface, you can import, export, update and delete documents
58   * and other data from/to the filesystem. Metadata is imported from and exported
59   * to XML files. The command line interface is for administrative purposes and
60   * to be used on the server side. It implements an interactive command prompt
61   * and understands a set of commands. Each command is an instance of the class
62   * <code>MCRCommand</code>.
63   * 
64   * @see MCRCommand
65   * 
66   * @author Frank Lützenkirchen
67   * @author Detlev Degenhardt
68   * @author Jens Kupferschmidt
69   * @author Thomas Scheffler (yagee)
70   */
71  public class MCRCommandLineInterface {
72  
73      private static final Logger LOGGER = LogManager.getLogger();
74  
75      /** The name of the system */
76      private static String system = null;
77  
78      /** A queue of commands waiting to be executed */
79      protected static Vector<String> commandQueue = new Vector<>();
80  
81      protected static Vector<String> failedCommands = new Vector<>();
82  
83      private static boolean interactiveMode = true;
84  
85      private static boolean SKIP_FAILED_COMMAND = false;
86  
87      private static MCRCommandManager knownCommands;
88  
89      private static ThreadLocal<String> sessionId = new ThreadLocal<>();
90  
91      /**
92       * The main method that either shows up an interactive command prompt or
93       * reads a file containing a list of commands to be processed
94       */
95      public static void main(String[] args) {
96          if (!(MCRCommandLineInterface.class.getClassLoader() instanceof URLClassLoader)) {
97              System.out.println("Current ClassLoader is not extendable at runtime. Using workaround.");
98              Thread.currentThread().setContextClassLoader(new CLIURLClassLoader(new URL[0]));
99          }
100         MCRStartupHandler.startUp(null/*no servlet context here*/);
101         system = MCRConfiguration2.getStringOrThrow("MCR.CommandLineInterface.SystemName") + ":";
102 
103         initSession();
104 
105         MCRSession session = MCRSessionMgr.getSession(sessionId.get());
106         MCRSessionMgr.setCurrentSession(session);
107 
108         try {
109             output("");
110             output("Command Line Interface.");
111             output("");
112 
113             output("Initializing...");
114             knownCommands = new MCRCommandManager();
115             output("Initialization done.");
116 
117             output("Type 'help' to list all commands!");
118             output("");
119         } finally {
120             MCRSessionMgr.releaseCurrentSession();
121         }
122 
123         readCommandFromArguments(args);
124 
125         String command = null;
126 
127         MCRCommandPrompt prompt = new MCRCommandPrompt(system);
128         while (true) {
129             if (commandQueue.isEmpty()) {
130                 if (interactiveMode) {
131                     command = prompt.readCommand();
132                 } else if (MCRConfiguration2.getString("MCR.CommandLineInterface.unitTest").orElse("false")
133                     .equals("true")) {
134                     break;
135                 } else {
136                     exit();
137                 }
138             } else {
139                 command = commandQueue.firstElement();
140                 commandQueue.removeElementAt(0);
141                 System.out.println(system + "> " + command);
142             }
143 
144             processCommand(command);
145         }
146     }
147 
148     private static void readCommandFromArguments(String[] args) {
149         if (args.length > 0) {
150             interactiveMode = false;
151 
152             String line = readLineFromArguments(args);
153             addCommands(line);
154         }
155     }
156 
157     private static void addCommands(String line) {
158         Stream.of(line.split(";;"))
159             .map(String::trim)
160             .filter(s -> !s.isEmpty())
161             .forEachOrdered(commandQueue::add);
162     }
163 
164     private static String readLineFromArguments(String[] args) {
165         StringBuilder sb = new StringBuilder();
166         for (String arg : args) {
167             sb.append(arg).append(" ");
168         }
169         return sb.toString();
170     }
171 
172     private static void initSession() {
173         MCRSessionMgr.unlock();
174         MCRSession session = MCRSessionMgr.getCurrentSession();
175         session.setCurrentIP("127.0.0.1");
176         session.setUserInformation(MCRSystemUserInformation.getSuperUserInstance());
177         MCRSessionMgr.setCurrentSession(session);
178         sessionId.set(session.getID());
179         MCRSessionMgr.releaseCurrentSession();
180     }
181 
182     /**
183      * Processes a command entered by searching a matching command in the list
184      * of known commands and executing its method.
185      * 
186      * @param command
187      *            The command string to be processed
188      */
189     protected static void processCommand(String command) {
190 
191         MCRSession session = MCRSessionMgr.getSession(sessionId.get());
192         MCRSessionMgr.setCurrentSession(session);
193 
194         try {
195             MCRTransactionHelper.beginTransaction();
196             List<String> commandsReturned = knownCommands.invokeCommand(expandCommand(command));
197             MCRTransactionHelper.commitTransaction();
198             addCommandsToQueue(commandsReturned);
199         } catch (Exception ex) {
200             MCRCLIExceptionHandler.handleException(ex);
201             rollbackTransaction(session);
202             if (SKIP_FAILED_COMMAND) {
203                 saveFailedCommand(command);
204             } else {
205                 saveQueue(command);
206                 if (!interactiveMode) {
207                     System.exit(1);
208                 }
209                 commandQueue.clear();
210             }
211         } finally {
212             MCRSessionMgr.releaseCurrentSession();
213         }
214     }
215 
216     /**
217      * Expands variables in a command.
218      * Replaces any variables in form ${propertyName} to the value defined by
219      * {@link MCRConfiguration2#getString(String)}.
220      * If the property is not defined no variable replacement takes place.
221      * @param command a CLI command that should be expanded
222      * @return expanded command
223      */
224     public static String expandCommand(final String command) {
225         StringSubstitutor strSubstitutor = new StringSubstitutor(MCRConfiguration2.getPropertiesMap());
226         String expandedCommand = strSubstitutor.replace(command);
227         if (!expandedCommand.equals(command)) {
228             LOGGER.info("{} --> {}", command, expandedCommand);
229         }
230         return expandedCommand;
231     }
232 
233     private static void rollbackTransaction(MCRSession session) {
234         output("Command failed. Performing transaction rollback...");
235 
236         if (MCRTransactionHelper.isTransactionActive()) {
237             try {
238                 MCRTransactionHelper.rollbackTransaction();
239             } catch (Exception ex2) {
240                 MCRCLIExceptionHandler.handleException(ex2);
241             }
242         }
243     }
244 
245     private static void addCommandsToQueue(List<String> commandsReturned) {
246         if (commandsReturned.size() > 0) {
247             output("Queueing " + commandsReturned.size() + " commands to process");
248 
249             for (int i = 0; i < commandsReturned.size(); i++) {
250                 commandQueue.insertElementAt(commandsReturned.get(i), i);
251             }
252         }
253     }
254 
255     protected static void saveQueue(String lastCommand) {
256         output("");
257         output("The following command failed:");
258         output(lastCommand);
259         if (!commandQueue.isEmpty()) {
260             System.out.printf(Locale.ROOT, "%s There are %s other commands still unprocessed.%n", system,
261                 commandQueue.size());
262         } else if (interactiveMode) {
263             return;
264         }
265         commandQueue.add(0, lastCommand);
266         saveCommandQueueToFile(commandQueue, "unprocessed-commands.txt");
267     }
268 
269     private static void saveCommandQueueToFile(final Vector<String> queue, String fname) {
270         output("Writing unprocessed commands to file " + fname);
271         try (PrintWriter pw = new PrintWriter(new File(fname), Charset.defaultCharset().name())) {
272             for (String command : queue) {
273                 pw.println(command);
274             }
275             pw.close();
276         } catch (IOException ex) {
277             MCRCLIExceptionHandler.handleException(ex);
278         }
279     }
280 
281     protected static void saveFailedCommand(String lastCommand) {
282         output("");
283         output("The following command failed:");
284         output(lastCommand);
285         if (!commandQueue.isEmpty()) {
286             System.out.printf(Locale.ROOT, "%s There are %s other commands still unprocessed.%n", system,
287                 commandQueue.size());
288         }
289         failedCommands.add(lastCommand);
290     }
291 
292     protected static void handleFailedCommands() {
293         if (failedCommands.size() > 0) {
294             System.err.println(system + " Several command failed.");
295             saveCommandQueueToFile(failedCommands, "failed-commands.txt");
296         }
297     }
298 
299     /**
300      * Show contents of a local text file, including line numbers.
301      * 
302      * @param fname
303      *            the filename
304      */
305     public static void show(String fname) throws Exception {
306         AtomicInteger ln = new AtomicInteger();
307         System.out.println();
308         Files.readAllLines(Paths.get(fname), Charset.defaultCharset())
309             .forEach(l -> System.out.printf(Locale.ROOT, "%04d: %s", ln.incrementAndGet(), l));
310         System.out.println();
311     }
312 
313     /**
314      * Reads XML content from URIResolver and sends output to a local file.
315      */
316     public static void getURI(String uri, String file) throws Exception {
317         Element resolved = MCRURIResolver.instance().resolve(uri);
318         Element cloned = resolved.clone();
319         new MCRJDOMContent(cloned).sendTo(new File(file));
320     }
321 
322     /**
323      * Reads a file containing a list of commands to be executed and adds them
324      * to the commands queue for processing. This method implements the
325      * "process ..." command.
326      * 
327      * @param file
328      *            The file holding the commands to be processed
329      * @throws IOException
330      *             when the file could not be read
331      * @throws FileNotFoundException
332      *             when the file was not found
333      */
334     public static List<String> readCommandsFile(String file) throws IOException {
335         try (BufferedReader reader = Files.newBufferedReader(new File(file).toPath(), Charset.defaultCharset())) {
336             output("Reading commands from file " + file);
337             return readCommandsFromBufferedReader(reader);
338         }
339     }
340 
341     public static List<String> readCommandsRessource(String resource) throws IOException {
342         final URL resourceURL = MCRClassTools.getClassLoader().getResource(resource);
343         if (resourceURL == null) {
344             throw new IOException("Resource URL is null!");
345         }
346         try (InputStream is = resourceURL.openStream();
347             InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
348             BufferedReader reader = new BufferedReader(isr)) {
349             output("Reading commands from resource " + resource);
350             return readCommandsFromBufferedReader(reader);
351         }
352     }
353 
354     private static List<String> readCommandsFromBufferedReader(BufferedReader reader) throws IOException {
355         String line;
356         List<String> list = new ArrayList<>();
357         while ((line = reader.readLine()) != null) {
358             line = line.trim();
359 
360             if (line.startsWith("#") || line.isEmpty()) {
361                 continue;
362             }
363             list.add(line);
364         }
365         return list;
366     }
367 
368     /**
369      * Executes simple shell commands from inside the command line interface and
370      * shows their output. This method implements commands entered beginning
371      * with exclamation mark, like "! ls -l /temp"
372      * 
373      * @param command
374      *            the shell command to be executed
375      * @throws IOException
376      *             when an IO error occured while catching the output returned
377      *             by the command
378      * @throws SecurityException
379      *             when the command could not be executed for security reasons
380      */
381     public static void executeShellCommand(String command) throws Exception {
382         MCRExternalProcess process = new MCRExternalProcess(command);
383         process.run();
384         output(process.getOutput().asString());
385         output(process.getErrors());
386     }
387 
388     /**
389      * Prints out the current user.
390      */
391     public static void whoami() {
392         MCRSession session = MCRSessionMgr.getCurrentSession();
393         String userName = session.getUserInformation().getUserID();
394         output("You are user " + userName);
395     }
396 
397     public static void cancelOnError() {
398         SKIP_FAILED_COMMAND = false;
399     }
400 
401     public static void skipOnError() {
402         SKIP_FAILED_COMMAND = true;
403     }
404 
405     /**
406      * Exits the command line interface. This method implements the "exit" and
407      * "quit" commands.
408      */
409     public static void exit() {
410         showSessionDuration();
411         handleFailedCommands();
412         System.exit(0);
413     }
414 
415     private static void showSessionDuration() {
416         MCRSessionMgr.unlock();
417         MCRSession session = MCRSessionMgr.getCurrentSession();
418         long duration = System.currentTimeMillis() - session.getLoginTime();
419         output("Session duration: " + duration + " ms");
420         MCRSessionMgr.releaseCurrentSession();
421         session.close();
422     }
423 
424     static void output(String message) {
425         System.out.println(system + " " + message);
426     }
427 
428     private static class CLIURLClassLoader extends URLClassLoader {
429 
430         CLIURLClassLoader(URL[] urls) {
431             super(urls);
432         }
433 
434         @Override
435         protected void addURL(URL url) {
436             //make this callable via reflection later;
437             super.addURL(url);
438         }
439     }
440 
441 }