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    }