001 /*
002 *
003 * $Revision: 14364 $ $Date: 2008-11-07 17:29:41 +0100 (Fr, 07 Nov 2008) $
004 *
005 * This file is part of M y C o R e See http://www.mycore.de/ for details.
006 *
007 * This program is free software; you can use it, redistribute it and / or modify it under the terms of the GNU General Public License (GPL) as published by the
008 * Free Software Foundation; either version 2 of the License or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
011 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
012 *
013 * You should have received a copy of the GNU General Public License along with this program, in a file called gpl.txt or license.txt. If not, write to the Free
014 * Software Foundation Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307 USA
015 */
016
017 package org.mycore.frontend.cli;
018
019 import java.io.BufferedReader;
020 import java.io.FileNotFoundException;
021 import java.io.FileOutputStream;
022 import java.io.FileReader;
023 import java.io.IOException;
024 import java.io.InputStream;
025 import java.io.InputStreamReader;
026 import java.io.OutputStreamWriter;
027 import java.io.PrintStream;
028 import java.io.PrintWriter;
029 import java.lang.reflect.InvocationTargetException;
030 import java.util.ArrayList;
031 import java.util.HashMap;
032 import java.util.Iterator;
033 import java.util.List;
034 import java.util.Map;
035 import java.util.StringTokenizer;
036 import java.util.Vector;
037 import java.util.concurrent.ConcurrentLinkedQueue;
038
039 import org.apache.log4j.Logger;
040 import org.hibernate.Transaction;
041
042 import org.mycore.backend.hibernate.MCRHIBConnection;
043 import org.mycore.common.MCRConfiguration;
044 import org.mycore.common.MCRException;
045 import org.mycore.common.MCRSession;
046 import org.mycore.common.MCRSessionMgr;
047 import org.mycore.common.MCRUsageException;
048 import org.mycore.datamodel.common.MCRActiveLinkException;
049 import org.mycore.user.MCRUserMgr;
050
051 /**
052 * The main class implementing the MyCoRe command line interface. With the command line interface, you can import, export, update and delete documents and other
053 * data from/to the filesystem. Metadata is imported from and exported to XML files. The command line interface is for administrative purposes and to be used on
054 * the server side. It implements an interactive command prompt and understands a set of commands. Each command is an instance of the class
055 * <code>MCRCommand</code>.
056 *
057 * @see MCRCommand
058 * @author Frank Lützenkirchen
059 * @author Detlev Degenhardt
060 * @author Jens Kupferschmidt
061 * @author Thomas Scheffler (yagee)
062 * @version $Revision: 14364 $ $Date: 2008-11-07 17:29:41 +0100 (Fr, 07 Nov 2008) $
063 */
064 public class MCRCommandLineInterface {
065 /** The Logger */
066 static Logger logger;
067
068 /** The name of the system */
069 private static String system = null;
070
071 /** The configuration */
072 private static MCRConfiguration config = null;
073
074 /** The array holding all known commands */
075 protected static ArrayList<MCRCommand> knownCommands = new ArrayList<MCRCommand>();
076
077 /** A queue of commands waiting to be executed */
078 protected static Vector<String> commandQueue = new Vector<String>();
079
080 protected static Vector<String> failedCommands = new Vector<String>();
081
082 /** The standard input console where the user enters commands */
083 protected static BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
084
085 protected static ConcurrentLinkedQueue<Number> benchList = new ConcurrentLinkedQueue<Number>();
086
087 /** The current session */
088 private static MCRSession session = null;
089
090 private static boolean interactiveMode = true;
091
092 private static boolean SKIP_FAILED_COMMAND = false;
093
094 /**
095 * Reads command definitions from a configuration file and builds the MCRCommand instances
096 */
097 protected static void initCommands() {
098 // **************************************
099 // Built-in commands
100 // **************************************
101 knownCommands.add(new MCRCommand("process {0}", "org.mycore.frontend.cli.MCRCommandLineInterface.readCommandsFile String",
102 "Execute the commands listed in the text file {0}."));
103 knownCommands.add(new MCRCommand("help {0}", "org.mycore.frontend.cli.MCRCommandLineInterface.showCommandsHelp String",
104 "Show the help text for the commands beginning with {0}."));
105 knownCommands.add(new MCRCommand("help", "org.mycore.frontend.cli.MCRCommandLineInterface.listKnownCommands", "List all possible commands."));
106 knownCommands.add(new MCRCommand("exit", "org.mycore.frontend.cli.MCRCommandLineInterface.exit", "Stop and exit the commandline tool."));
107 knownCommands.add(new MCRCommand("quit", "org.mycore.frontend.cli.MCRCommandLineInterface.exit", "Stop and exit the commandline tool."));
108 knownCommands.add(new MCRCommand("! {0}", "org.mycore.frontend.cli.MCRCommandLineInterface.executeShellCommand String",
109 "Execute the shell command {0}, for example '! ls' or '! cmd /c dir'"));
110 knownCommands.add(new MCRCommand("show file {0}", "org.mycore.frontend.cli.MCRCommandLineInterface.show String", "Show contents of local file {0}"));
111 knownCommands.add(new MCRCommand("change to user {0} with {1}", "org.mycore.frontend.cli.MCRCommandLineInterface.changeToUser String String",
112 "Change the user {0} with the given password in {1}."));
113 knownCommands.add(new MCRCommand("login {0}", "org.mycore.frontend.cli.MCRCommandLineInterface.login String",
114 "Start the login dialog for the user {0}."));
115 knownCommands.add(new MCRCommand("whoami", "org.mycore.frontend.cli.MCRCommandLineInterface.whoami", "Print the current user."));
116 knownCommands.add(new MCRCommand("show command statistics", "org.mycore.frontend.cli.MCRCommandLineInterface.showCommandStatistics",
117 "Show statistics on number of commands processed and execution time needed per command"));
118 knownCommands.add(new MCRCommand("cancel on error", "org.mycore.frontend.cli.MCRCommandLineInterface.cancelOnError",
119 "Cancel execution of further commands in case of error"));
120 knownCommands.add(new MCRCommand("skip on error", "org.mycore.frontend.cli.MCRCommandLineInterface.skipOnError",
121 "Skip execution of failed command in case of error"));
122
123 // **************************************
124 // Read internal and/or external commands
125 // **************************************
126 readCommands("MCR.CLI.Classes.Internal", "internal");
127 readCommands("MCR.CLI.Classes.External", "external");
128 }
129
130 private static void readCommands(String property, String type) {
131 String classes = config.getString(property, "");
132
133 for (StringTokenizer st = new StringTokenizer(classes, ","); st.hasMoreTokens();) {
134 String classname = st.nextToken();
135 logger.debug("Will load commands from the " + type + " class " + classname);
136
137 Object obj;
138 try {
139 obj = Class.forName(classname).newInstance();
140 } catch (Exception e) {
141 String msg = "Could not instantiate class " + classname;
142 throw new org.mycore.common.MCRConfigurationException(msg, e);
143 }
144 ArrayList<MCRCommand> commands = ((MCRExternalCommandInterface) obj).getPossibleCommands();
145 knownCommands.addAll(commands);
146 }
147 }
148
149 /**
150 * The main method that either shows up an interactive command prompt or reads a file containing a list of commands to be processed
151 */
152 public static void main(String[] args) {
153 config = MCRConfiguration.instance();
154 logger = Logger.getLogger(MCRCommandLineInterface.class);
155 session = MCRSessionMgr.getCurrentSession();
156 session.setCurrentIP(MCRSession.getLocalIP());
157 session.setCurrentUserID(config.getString("MCR.Users.Superuser.UserName", "administrator"));
158 MCRSessionMgr.setCurrentSession(session);
159 system = config.getString("MCR.CommandLineInterface.SystemName", "MyCoRe") + ":";
160
161 System.out.println();
162 System.out.println(system + " Command Line Interface.");
163 System.out.println(system);
164 System.out.println(system + " Initializing...");
165
166 try {
167 initCommands();
168 } catch (Exception ex) {
169 showException(ex);
170 System.exit(1);
171 }
172
173 System.out.println(system + " Initialization done.");
174 System.out.println(system + " Type 'help' to list all commands!");
175 System.out.println(system);
176
177 if (args.length > 0) {
178 interactiveMode = false;
179
180 StringBuffer sb = new StringBuffer();
181 for (int i = 0; i < args.length; i++)
182 sb.append(args[i]).append(" ");
183 String line = sb.toString().trim();
184 String[] cmds = line.split(";;");
185 for (String cmd : cmds) {
186 cmd = cmd.trim();
187 if (cmd.length() > 0)
188 commandQueue.add(cmd);
189 }
190 }
191
192 String command = null;
193 String firstCommand = null;
194
195 while (true) {
196 if (commandQueue.isEmpty()) {
197 if (interactiveMode) {
198 command = readCommandFromPrompt();
199 } else {
200 if (firstCommand != null && config.getBoolean("MCR.CLI.SaveRuntimeStatistics", false))
201 try {
202 saveMillis(firstCommand);
203 } catch (IOException e) {
204 // TODO Auto-generated catch block
205 e.printStackTrace();
206 }
207 exit();
208 // break;
209 }
210 } else {
211 command = (String) commandQueue.firstElement();
212 if (firstCommand == null)
213 firstCommand = command;
214 commandQueue.removeElementAt(0);
215 System.out.println(system + "> " + command);
216 }
217
218 processCommand(command);
219 }
220 }
221
222 /**
223 * Shows up a command prompt.
224 *
225 * @return The command entered by the user at stdin
226 */
227 protected static String readCommandFromPrompt() {
228 String line = "";
229
230 do {
231 System.out.print(system + "> ");
232
233 try {
234 line = console.readLine();
235 } catch (IOException ex) {
236 }
237 } while ((line = line.trim()).length() == 0);
238
239 return line;
240 }
241
242 /** Stores total time needed for all executions of the given command */
243 protected static HashMap<String, Long> timeNeeded = new HashMap<String, Long>();
244
245 /** Stores total number of executions for each command */
246 protected static HashMap<String, Integer> numInvocations = new HashMap<String, Integer>();
247
248 /**
249 * Processes a command entered by searching a matching command in the list of known commands and executing its method.
250 *
251 * @param command
252 * The command string to be processed
253 */
254 protected static void processCommand(String command) {
255 long start = 0, end = 0;
256 Transaction tx = MCRHIBConnection.instance().getSession().beginTransaction();
257 List<String> commandsReturned = null;
258 String invokedCommand = null;
259
260 try {
261 for (MCRCommand currentCommand : knownCommands) {
262 start = System.currentTimeMillis();
263 commandsReturned = currentCommand.invoke(command);
264
265 if (commandsReturned != null) // Command was executed
266 {
267 end = System.currentTimeMillis();
268 invokedCommand = currentCommand.showSyntax();
269
270 long sum = (timeNeeded.containsKey(invokedCommand) ? timeNeeded.get(invokedCommand) : 0L);
271 sum += end - start;
272 timeNeeded.put(invokedCommand, sum);
273
274 int num = 1 + (numInvocations.containsKey(invokedCommand) ? numInvocations.get(invokedCommand) : 0);
275 numInvocations.put(invokedCommand, num);
276
277 // Add commands to queue
278 if (commandsReturned.size() > 0) {
279 System.out.println(system + " Queueing " + commandsReturned.size() + " commands to process");
280
281 for (int i = 0; i < commandsReturned.size(); i++)
282 commandQueue.insertElementAt(commandsReturned.get(i), i);
283 }
284
285 break;
286 }
287 }
288 tx.commit();
289 if (commandsReturned != null) {
290 System.out.printf("%s Command processed (%d ms)\n", system, (end - start));
291 addMillis(end - start);
292 } else {
293 if (interactiveMode)
294 System.out.printf("%s Command not understood. Enter 'help' to get a list of commands.\n", system);
295 else
296 throw new MCRUsageException("Command not understood: " + command);
297 }
298 } catch (Exception ex) {
299 showException(ex);
300 System.out.printf("%s Command failed. Performing transaction rollback...\n", system);
301 try {
302 tx.rollback();
303 } catch (Exception ex2) {
304 showException(ex2);
305 }
306 if (SKIP_FAILED_COMMAND) {
307 saveFailedCommand(command);
308 } else {
309 saveQueue(command);
310 if (!interactiveMode)
311 System.exit(1);
312 }
313 } finally {
314 tx = MCRHIBConnection.instance().getSession().beginTransaction();
315 MCRHIBConnection.instance().getSession().clear();
316 tx.commit();
317 }
318 }
319
320 /**
321 * Shows statistics on number of invocations and time needed for each command successfully executed.
322 */
323 public static void showCommandStatistics() {
324 System.out.println();
325 for (Object key : timeNeeded.keySet().toArray()) {
326 long tn = timeNeeded.get(key);
327 int num = numInvocations.get(key);
328
329 System.out.println(key);
330 System.out.println(" total: " + tn + " ms, average: " + (tn / num) + " ms, " + num + " invocations.");
331 }
332 System.out.println();
333 }
334
335 protected static void saveQueue(String lastCommand) {
336 System.out.println(system);
337 System.out.println(system + " The following command failed: ");
338 System.out.println(system + " " + lastCommand);
339 if (!commandQueue.isEmpty())
340 System.out.printf("%s There are %s other commands still unprocessed.\n", system, commandQueue.size());
341 else if (interactiveMode)
342 return;
343 commandQueue.add(0, lastCommand);
344 saveCommandQueueToFile(commandQueue, "unprocessed-commands.txt");
345 }
346
347 private static void saveCommandQueueToFile(final Vector<String> queue, String fname) {
348 System.out.println(system + " Writing unprocessed commands to file " + fname);
349
350 try {
351 PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(fname)));
352 for (String command : queue)
353 pw.println(command);
354 pw.close();
355 } catch (IOException ex) {
356 showException(ex);
357 }
358 }
359
360 protected static void saveFailedCommand(String lastCommand) {
361 System.out.println(system);
362 System.out.println(system + " The following command failed: ");
363 System.out.println(system + " " + lastCommand);
364 if (!commandQueue.isEmpty())
365 System.out.printf("%s There are %s other commands still unprocessed.\n", system, commandQueue.size());
366 failedCommands.add(lastCommand);
367 }
368
369 protected static void handleFailedCommands() {
370 if (failedCommands.size() > 0) {
371 System.err.println(system + " Several command failed.");
372 saveCommandQueueToFile(failedCommands, "failed-commands.txt");
373 }
374 }
375
376 /**
377 * Show contents of a local text file, including line numbers.
378 *
379 * @param fname
380 * the filename
381 * @throws Exception
382 */
383 public static void show(String fname) throws Exception {
384 BufferedReader br = new BufferedReader(new FileReader(fname));
385 System.out.println();
386 String line;
387 int i = 1;
388 while ((line = br.readLine()) != null)
389 System.out.printf("%04d: %s\n", i++, line);
390 br.close();
391 System.out.println();
392 }
393
394 /**
395 * Shows details about an exception that occured during command processing
396 *
397 * @param ex
398 * The exception that was catched while processing a command
399 */
400 protected static void showException(Throwable ex) {
401 if (ex instanceof InvocationTargetException) {
402 ex = ((InvocationTargetException) ex).getTargetException();
403 showException(ex);
404 return;
405 } else if (ex instanceof ExceptionInInitializerError) {
406 ex = ((ExceptionInInitializerError) ex).getCause();
407 showException(ex);
408 return;
409 }
410
411 System.out.println(system);
412 System.out.println(system + " Exception occured: " + ex.getClass().getName());
413 System.out.println(system + " Exception message: " + ex.getLocalizedMessage());
414 System.out.println(system);
415
416 if (ex instanceof MCRActiveLinkException) {
417 MCRActiveLinkException activeLinks = (MCRActiveLinkException) ex;
418 StringBuffer msgBuf = new StringBuffer(system);
419 msgBuf.append(" There are links active preventing the commit of work, see error message for details. The following links where affected:");
420 Map links = activeLinks.getActiveLinks();
421 Iterator destIt = links.keySet().iterator();
422 String curDest;
423 int count = 0;
424 while (destIt.hasNext()) {
425 count++;
426 curDest = destIt.next().toString();
427 List sources = (List) links.get(curDest);
428 Iterator sourceIt = sources.iterator();
429 while (sourceIt.hasNext()) {
430 msgBuf.append("\n\t").append(count).append(".) ").append(sourceIt.next().toString()).append("==>").append(curDest);
431 }
432 }
433 msgBuf.append('\n');
434 System.out.println(msgBuf.toString());
435 }
436
437 String trace = MCRException.getStackTraceAsString(ex);
438 if (logger.isDebugEnabled())
439 logger.debug(trace);
440 else
441 System.out.println(trace);
442
443 if (ex instanceof MCRActiveLinkException) {
444 MCRActiveLinkException activeLinks = (MCRActiveLinkException) ex;
445 StringBuffer msgBuf = new StringBuffer();
446 msgBuf.append("\nThere are links active preventing the commit of work, see error message for details. The following links where affected:");
447 Map links = activeLinks.getActiveLinks();
448 Iterator destIt = links.keySet().iterator();
449 String curDest;
450 while (destIt.hasNext()) {
451 curDest = destIt.next().toString();
452 logger.debug("Current Destination: " + curDest);
453 List sources = (List) links.get(curDest);
454 Iterator sourceIt = sources.iterator();
455 while (sourceIt.hasNext()) {
456 msgBuf.append('\n').append(sourceIt.next().toString()).append("==>").append(curDest);
457 }
458 }
459 }
460 if (ex instanceof MCRException) {
461 ex = ((MCRException) ex).getException();
462 if (ex != null) {
463 System.out.println(system);
464 System.out.println(system + " This exception was caused by:");
465 showException(ex);
466 }
467 }
468 }
469
470 /**
471 * Reads a file containing a list of commands to be executed and adds them to the commands queue for processing. This method implements the "process ..."
472 * command.
473 *
474 * @param file
475 * The file holding the commands to be processed
476 * @throws IOException
477 * when the file could not be read
478 * @throws FileNotFoundException
479 * when the file was not found
480 */
481 public static List<String> readCommandsFile(String file) throws IOException, FileNotFoundException {
482 BufferedReader reader = new BufferedReader(new FileReader(file));
483 System.out.println(system + " Reading commands from file " + file);
484
485 String line;
486 List<String> list = new ArrayList<String>();
487 while ((line = reader.readLine()) != null) {
488 line = line.trim();
489
490 if (line.startsWith("#") || (line.length() == 0)) {
491 continue;
492 }
493 list.add(line);
494 }
495
496 reader.close();
497 return list;
498 }
499
500 /**
501 * Shows a list of commands understood by the command line interface and shows their input syntax. This method implements the "help" command
502 */
503 public static void listKnownCommands() {
504 System.out.println(system + " The following " + knownCommands.size() + " commands can be used:");
505 System.out.println(system);
506
507 for (int i = 0; i < knownCommands.size(); i++) {
508 System.out.println(system + " " + ((MCRCommand) knownCommands.get(i)).showSyntax());
509 }
510 }
511
512 /**
513 * Shows the help text of one or more commands.
514 *
515 * @param com
516 * the command
517 */
518 public static void showCommandsHelp(String com) {
519 boolean test = false;
520
521 for (int i = 0; i < knownCommands.size(); i++) {
522 if (((MCRCommand) knownCommands.get(i)).showSyntax().indexOf(com) != -1) {
523 System.out.println(system + " " + ((MCRCommand) knownCommands.get(i)).showSyntax());
524 System.out.println(system + " " + ((MCRCommand) knownCommands.get(i)).getHelpText());
525 System.out.println(system);
526 test = true;
527 }
528 }
529
530 if (!test) {
531 System.out.println(system + " Unknown command.");
532 }
533 }
534
535 /**
536 * Executes simple shell commands from inside the command line interface and shows their output. This method implements commands entered beginning with
537 * exclamation mark, like "! ls -l /temp"
538 *
539 * @param command
540 * the shell command to be executed
541 * @throws IOException
542 * when an IO error occured while catching the output returned by the command
543 * @throws SecurityException
544 * when the command could not be executed for security reasons
545 */
546 public static void executeShellCommand(String command) throws IOException, SecurityException {
547 Process p = Runtime.getRuntime().exec(command);
548 showOutput(p.getInputStream());
549 showOutput(p.getErrorStream());
550 }
551
552 /**
553 * The method print the current user.
554 */
555 public static void whoami() {
556 System.out.println(system + " You are user " + session.getCurrentUserID());
557 }
558
559 /**
560 * This command changes the user of the session context to a new user.
561 *
562 * @param user
563 * the new user ID
564 * @param password
565 * the password of the new user
566 */
567 public static void changeToUser(String user, String password) {
568 if (userExists(user)) {
569 System.out.println(system + " The old user ID is " + session.getCurrentUserID());
570
571 if (org.mycore.user.MCRUserMgr.instance().login(user.trim(), password.trim())) {
572 session.setCurrentUserID(user);
573 session.setLoginTime();
574 System.out.println(system + " The new user ID is " + session.getCurrentUserID());
575 } else {
576 String msg = "Wrong password, no changes of user ID in session context!";
577 if (logger.isDebugEnabled())
578 logger.debug(msg);
579 else
580 System.out.println(system + " " + msg);
581 }
582 }
583 }
584
585 /**
586 * This command changes the user of the session context to a new user.
587 *
588 * @param user
589 * the new user ID
590 */
591 public static void login(String user) {
592 if (userExists(user)) {
593 System.out.println(system + " The old user ID is " + session.getCurrentUserID());
594
595 String password = "";
596
597 do {
598 System.out.print(system + " Enter the password for user " + user + ":> ");
599
600 try {
601 password = console.readLine();
602 } catch (IOException ex) {
603 }
604 } while ((password = password.trim()).length() == 0);
605
606 changeToUser(user, password);
607 }
608 }
609
610 private static boolean userExists(String user) {
611 if (MCRUserMgr.instance().existUser(user))
612 return true;
613 System.out.println(system + " User does not exists: " + user);
614 return false;
615 }
616
617 /**
618 * Catches the output read from an input stream and prints it line by line on standard out. This is used to catch the stdout and stderr stream output when
619 * executing an external shell command.
620 */
621 protected static void showOutput(InputStream in) throws IOException {
622 int c;
623 StringBuffer sb = new StringBuffer(1024);
624
625 while ((c = in.read()) != -1) {
626 sb.append((char) c);
627 }
628
629 System.out.println(system + " " + sb.toString());
630 }
631
632 public static void cancelOnError() {
633 SKIP_FAILED_COMMAND = false;
634 }
635
636 public static void skipOnError() {
637 SKIP_FAILED_COMMAND = true;
638 }
639
640 public static void addMillis(long l) {
641 benchList.add(l);
642 }
643
644 public static void clearMillis() {
645 benchList.clear();
646 }
647
648 public static void saveMillis(String fileBaseName) throws IOException {
649 PrintStream fout = new PrintStream(fileBaseName + ".dat");
650
651 for (int i = 1; !benchList.isEmpty(); i++) {
652 fout.printf("%d %d\n", i, benchList.poll().intValue());
653 }
654 fout.close();
655 }
656
657 /**
658 * Exits the command line interface. This method implements the "exit" and "quit" commands.
659 */
660 public static void exit() {
661 System.out.println(system + " Session time: " + (System.currentTimeMillis() - session.getLoginTime()) + " ms");
662 handleFailedCommands();
663 System.exit(0);
664 }
665 }