001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.main;
029
030import org.opencms.configuration.CmsParameterConfiguration;
031import org.opencms.db.CmsUserSettings;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsUser;
034import org.opencms.i18n.CmsLocaleManager;
035import org.opencms.i18n.CmsMessages;
036import org.opencms.security.CmsRole;
037import org.opencms.util.CmsDataTypeUtil;
038import org.opencms.util.CmsFileUtil;
039import org.opencms.util.CmsStringUtil;
040
041import java.awt.event.KeyEvent;
042import java.io.FileDescriptor;
043import java.io.FileInputStream;
044import java.io.IOException;
045import java.io.InputStream;
046import java.io.InputStreamReader;
047import java.io.LineNumberReader;
048import java.io.PrintStream;
049import java.io.Reader;
050import java.io.StreamTokenizer;
051import java.io.StringReader;
052import java.lang.reflect.InvocationTargetException;
053import java.lang.reflect.Method;
054import java.lang.reflect.Modifier;
055import java.util.ArrayList;
056import java.util.Collection;
057import java.util.Iterator;
058import java.util.List;
059import java.util.Locale;
060import java.util.Map;
061import java.util.TreeMap;
062
063/**
064 * A command line interface to access OpenCms functions which
065 * is used for the initial setup and also can be used for scripting access to the OpenCms
066 * repository without the Workplace.<p>
067 *
068 * The CmsShell has direct access to all methods in the "command objects".
069 * Currently the following classes are used as command objects:
070 * <code>{@link org.opencms.main.CmsShellCommands}</code>,
071 * <code>{@link org.opencms.file.CmsRequestContext}</code> and
072 * <code>{@link org.opencms.file.CmsObject}</code>.<p>
073 *
074 * It is also possible to add a custom command object when calling the script API,
075 * like in {@link CmsShell#CmsShell(String, String, String, String, I_CmsShellCommands, PrintStream, PrintStream, boolean)}.<p>
076 *
077 * Only public methods in the command objects that use supported data types
078 * as parameters can be called from the shell. Supported data types are:
079 * <code>String, {@link org.opencms.util.CmsUUID}, boolean, int, long, double, float</code>.<p>
080 *
081 * If a method name is ambiguous, i.e. the method name with the same number of parameter exist
082 * in more then one of the command objects, the method is only executed on the first matching method object.<p>
083 *
084 * @since 6.0.0
085 *
086 * @see org.opencms.main.CmsShellCommands
087 * @see org.opencms.file.CmsRequestContext
088 * @see org.opencms.file.CmsObject
089 */
090public class CmsShell {
091
092    /**
093     * Command object class.<p>
094     */
095    private class CmsCommandObject {
096
097        /** The list of methods. */
098        private Map<String, List<Method>> m_methods;
099
100        /** The object to execute the methods on. */
101        private Object m_object;
102
103        /**
104         * Creates a new command object.<p>
105         *
106         * @param object the object to execute the methods on
107         */
108        protected CmsCommandObject(Object object) {
109
110            m_object = object;
111            initShellMethods();
112        }
113
114        /**
115         * Tries to execute a method for the provided parameters on this command object.<p>
116         *
117         * If methods with the same name and number of parameters exist in this command object,
118         * the given parameters are tried to be converted from String to matching types.<p>
119         *
120         * @param command the command entered by the user in the shell
121         * @param parameters the parameters entered by the user in the shell
122         * @return true if a method was executed, false otherwise
123         */
124        protected boolean executeMethod(String command, List<String> parameters) {
125
126            // build the method lookup
127            String lookup = buildMethodLookup(command, parameters.size());
128
129            // try to look up the methods of this command object
130            List<Method> possibleMethods = m_methods.get(lookup);
131            if (possibleMethods == null) {
132                return false;
133            }
134
135            // a match for the method name was found, now try to figure out if the parameters are ok
136            Method onlyStringMethod = null;
137            Method foundMethod = null;
138            Object[] params = null;
139            Iterator<Method> i;
140
141            // first check if there is one method with only has String parameters, make this the fall back
142            i = possibleMethods.iterator();
143            while (i.hasNext()) {
144                Method method = i.next();
145                Class<?>[] clazz = method.getParameterTypes();
146                boolean onlyString = true;
147                for (int j = 0; j < clazz.length; j++) {
148                    if (!(clazz[j].equals(String.class))) {
149                        onlyString = false;
150                        break;
151                    }
152                }
153                if (onlyString) {
154                    onlyStringMethod = method;
155                    break;
156                }
157            }
158
159            // now check a method matches the provided parameters
160            // if so, use this method, else continue searching
161            i = possibleMethods.iterator();
162            while (i.hasNext()) {
163                Method method = i.next();
164                if (method == onlyStringMethod) {
165                    // skip the String only signature because this would always match
166                    continue;
167                }
168                // now try to convert the parameters to the required types
169                Class<?>[] clazz = method.getParameterTypes();
170                Object[] converted = new Object[clazz.length];
171                boolean match = true;
172                for (int j = 0; j < clazz.length; j++) {
173                    String value = parameters.get(j);
174                    try {
175                        converted[j] = CmsDataTypeUtil.parse(value, clazz[j]);
176                    } catch (Throwable t) {
177                        match = false;
178                        break;
179                    }
180                }
181                if (match) {
182                    // we found a matching method signature
183                    params = converted;
184                    foundMethod = method;
185                    break;
186                }
187
188            }
189
190            if ((foundMethod == null) && (onlyStringMethod != null)) {
191                // no match found but String only signature available, use this
192                params = parameters.toArray();
193                foundMethod = onlyStringMethod;
194            }
195
196            if ((params == null) || (foundMethod == null)) {
197                // no match found at all
198                return false;
199            }
200
201            // now try to invoke the method
202            try {
203                Object result = foundMethod.invoke(m_object, params);
204                if (result != null) {
205                    if (result instanceof Collection<?>) {
206                        Collection<?> c = (Collection<?>)result;
207                        m_out.println(c.getClass().getName() + " (size: " + c.size() + ")");
208                        int count = 0;
209                        if (result instanceof Map<?, ?>) {
210                            Map<?, ?> m = (Map<?, ?>)result;
211                            Iterator<?> j = m.entrySet().iterator();
212                            while (j.hasNext()) {
213                                Map.Entry<?, ?> entry = (Map.Entry<?, ?>)j.next();
214                                m_out.println(count++ + ": " + entry.getKey() + "= " + entry.getValue());
215                            }
216                        } else {
217                            Iterator<?> j = c.iterator();
218                            while (j.hasNext()) {
219                                m_out.println(count++ + ": " + j.next());
220                            }
221                        }
222                    } else {
223                        m_out.println(result.toString());
224                    }
225                }
226            } catch (InvocationTargetException ite) {
227                m_out.println(
228                    Messages.get().getBundle(getLocale()).key(
229                        Messages.GUI_SHELL_EXEC_METHOD_1,
230                        new Object[] {foundMethod.getName()}));
231                ite.getTargetException().printStackTrace(m_out);
232            } catch (Throwable t) {
233                m_out.println(
234                    Messages.get().getBundle(getLocale()).key(
235                        Messages.GUI_SHELL_EXEC_METHOD_1,
236                        new Object[] {foundMethod.getName()}));
237                t.printStackTrace(m_out);
238            }
239
240            return true;
241        }
242
243        /**
244         * Returns a signature overview of all methods containing the given search String.<p>
245         *
246         * If no method name matches the given search String, the empty String is returned.<p>
247         *
248         * @param searchString the String to search for, if null all methods are shown
249         *
250         * @return a signature overview of all methods containing the given search String
251         */
252        protected String getMethodHelp(String searchString) {
253
254            StringBuffer buf = new StringBuffer(512);
255            Iterator<String> i = m_methods.keySet().iterator();
256            while (i.hasNext()) {
257                List<Method> l = m_methods.get(i.next());
258                Iterator<Method> j = l.iterator();
259                while (j.hasNext()) {
260                    Method method = j.next();
261                    if ((searchString == null)
262                        || (method.getName().toLowerCase().indexOf(searchString.toLowerCase()) > -1)) {
263                        buf.append("* ");
264                        buf.append(method.getName());
265                        buf.append("(");
266                        Class<?>[] params = method.getParameterTypes();
267                        for (int k = 0; k < params.length; k++) {
268                            String par = params[k].getName();
269                            par = par.substring(par.lastIndexOf('.') + 1);
270                            if (k != 0) {
271                                buf.append(", ");
272                            }
273                            buf.append(par);
274                        }
275                        buf.append(")\n");
276                    }
277                }
278            }
279            return buf.toString();
280        }
281
282        /**
283         * Returns the object to execute the methods on.<p>
284         *
285         * @return the object to execute the methods on
286         */
287        protected Object getObject() {
288
289            return m_object;
290        }
291
292        /**
293         * Builds a method lookup String.<p>
294         *
295         * @param methodName the name of the method
296         * @param paramCount the parameter count of the method
297         *
298         * @return a method lookup String
299         */
300        private String buildMethodLookup(String methodName, int paramCount) {
301
302            StringBuffer buf = new StringBuffer(32);
303            buf.append(methodName.toLowerCase());
304            buf.append(" [");
305            buf.append(paramCount);
306            buf.append("]");
307            return buf.toString();
308        }
309
310        /**
311         * Initializes the map of accessible methods.<p>
312         */
313        private void initShellMethods() {
314
315            Map<String, List<Method>> result = new TreeMap<String, List<Method>>();
316
317            Method[] methods = m_object.getClass().getMethods();
318            for (int i = 0; i < methods.length; i++) {
319                // only public methods directly declared in the base class can be used in the shell
320                if ((methods[i].getDeclaringClass() == m_object.getClass())
321                    && (methods[i].getModifiers() == Modifier.PUBLIC)) {
322
323                    // check if the method signature only uses primitive data types
324                    boolean onlyPrimitive = true;
325                    Class<?>[] clazz = methods[i].getParameterTypes();
326                    for (int j = 0; j < clazz.length; j++) {
327                        if (!CmsDataTypeUtil.isParseable(clazz[j])) {
328                            // complex data type methods can not be called from the shell
329                            onlyPrimitive = false;
330                            break;
331                        }
332                    }
333
334                    if (onlyPrimitive) {
335                        // add this method to the set of methods that can be called from the shell
336                        String lookup = buildMethodLookup(methods[i].getName(), methods[i].getParameterTypes().length);
337                        List<Method> l;
338                        if (result.containsKey(lookup)) {
339                            l = result.get(lookup);
340                        } else {
341                            l = new ArrayList<Method>(1);
342                        }
343                        l.add(methods[i]);
344                        result.put(lookup, l);
345                    }
346                }
347            }
348            m_methods = result;
349        }
350    }
351
352    /** Prefix for "additional" parameter. */
353    public static final String SHELL_PARAM_ADDITIONAL_COMMANDS = "-additional=";
354
355    /** Prefix for "base" parameter. */
356    public static final String SHELL_PARAM_BASE = "-base=";
357
358    /** Prefix for "servletMapping" parameter. */
359    public static final String SHELL_PARAM_DEFAULT_WEB_APP = "-defaultWebApp=";
360
361    /** Prefix for "script" parameter. */
362    public static final String SHELL_PARAM_SCRIPT = "-script=";
363
364    /** Prefix for "servletMapping" parameter. */
365    public static final String SHELL_PARAM_SERVLET_MAPPING = "-servletMapping=";
366
367    /** The OpenCms context object. */
368    protected CmsObject m_cms;
369
370    /** Additional shell commands object. */
371    private I_CmsShellCommands m_additionalShellCommands;
372
373    /** All shell callable objects. */
374    private List<CmsCommandObject> m_commandObjects;
375
376    /** If set to true, all commands are echoed. */
377    private boolean m_echo;
378
379    /** Indicates if the 'exit' command has been called. */
380    private boolean m_exitCalled;
381
382    /** The messages object. */
383    private CmsMessages m_messages;
384
385    /** The OpenCms system object. */
386    private OpenCmsCore m_opencms;
387
388    /** The shell prompt format. */
389    private String m_prompt;
390
391    /** The current users settings. */
392    private CmsUserSettings m_settings;
393
394    /** Internal shell command object. */
395    private I_CmsShellCommands m_shellCommands;
396
397    /** Stream to write the regular output messages to. */
398    protected PrintStream m_out;
399
400    /** Stream to write the error messages output to. */
401    protected PrintStream m_err;
402
403    /** Indicates if this is an interactive session with a user sitting on a console. */
404    private boolean m_interactive;
405
406    /**
407     * Creates a new CmsShell.<p>
408     *
409     * @param cms the user context to run the shell from
410     * @param prompt the prompt format to set
411     * @param additionalShellCommands optional object for additional shell commands, or null
412     * @param out stream to write the regular output messages to
413     * @param err stream to write the error messages output to
414     */
415    public CmsShell(
416        CmsObject cms,
417        String prompt,
418        I_CmsShellCommands additionalShellCommands,
419        PrintStream out,
420        PrintStream err) {
421
422        setPrompt(prompt);
423        try {
424            // has to be initialized already if this constructor is used
425            m_opencms = null;
426            Locale locale = getLocale();
427            m_messages = Messages.get().getBundle(locale);
428            m_cms = cms;
429
430            // initialize the shell
431            initShell(additionalShellCommands, out, err);
432        } catch (Throwable t) {
433            t.printStackTrace(m_err);
434        }
435    }
436
437    /**
438     * Creates a new CmsShell using System.out and System.err for output of the messages.<p>
439     *
440     * @param webInfPath the path to the 'WEB-INF' folder of the OpenCms installation
441     * @param servletMapping the mapping of the servlet (or <code>null</code> to use the default <code>"/opencms/*"</code>)
442     * @param defaultWebAppName the name of the default web application (or <code>null</code> to use the default <code>"ROOT"</code>)
443     * @param prompt the prompt format to set
444     * @param additionalShellCommands optional object for additional shell commands, or null
445     */
446    public CmsShell(
447        String webInfPath,
448        String servletMapping,
449        String defaultWebAppName,
450        String prompt,
451        I_CmsShellCommands additionalShellCommands) {
452
453        this(
454            webInfPath,
455            servletMapping,
456            defaultWebAppName,
457            prompt,
458            additionalShellCommands,
459            System.out,
460            System.err,
461            false);
462    }
463
464    /**
465     * Creates a new CmsShell.<p>
466     *
467     * @param webInfPath the path to the 'WEB-INF' folder of the OpenCms installation
468     * @param servletMapping the mapping of the servlet (or <code>null</code> to use the default <code>"/opencms/*"</code>)
469     * @param defaultWebAppName the name of the default web application (or <code>null</code> to use the default <code>"ROOT"</code>)
470     * @param prompt the prompt format to set
471     * @param additionalShellCommands optional object for additional shell commands, or null
472     * @param out stream to write the regular output messages to
473     * @param err stream to write the error messages output to
474     * @param interactive if <code>true</code> this is an interactive session with a user sitting on a console
475     */
476    public CmsShell(
477        String webInfPath,
478        String servletMapping,
479        String defaultWebAppName,
480        String prompt,
481        I_CmsShellCommands additionalShellCommands,
482        PrintStream out,
483        PrintStream err,
484        boolean interactive) {
485
486        setPrompt(prompt);
487        setInteractive(interactive);
488        if (CmsStringUtil.isEmpty(servletMapping)) {
489            servletMapping = "/opencms/*";
490        }
491        if (CmsStringUtil.isEmpty(defaultWebAppName)) {
492            defaultWebAppName = "ROOT";
493        }
494        try {
495            // first initialize runlevel 1
496            m_opencms = OpenCmsCore.getInstance();
497            // Externalization: get Locale: will be the System default since no CmsObject is up  before
498            // runlevel 2
499            Locale locale = getLocale();
500            m_messages = Messages.get().getBundle(locale);
501            // search for the WEB-INF folder
502            if (CmsStringUtil.isEmpty(webInfPath)) {
503                out.println(m_messages.key(Messages.GUI_SHELL_NO_HOME_FOLDER_SPECIFIED_0));
504                out.println();
505                webInfPath = CmsFileUtil.searchWebInfFolder(System.getProperty("user.dir"));
506                if (CmsStringUtil.isEmpty(webInfPath)) {
507                    err.println(m_messages.key(Messages.GUI_SHELL_HR_0));
508                    err.println(m_messages.key(Messages.GUI_SHELL_NO_HOME_FOLDER_FOUND_0));
509                    err.println();
510                    err.println(m_messages.key(Messages.GUI_SHELL_START_DIR_LINE1_0));
511                    err.println(m_messages.key(Messages.GUI_SHELL_START_DIR_LINE2_0));
512                    err.println(m_messages.key(Messages.GUI_SHELL_HR_0));
513                    return;
514                }
515            }
516            out.println(Messages.get().getBundle(locale).key(Messages.GUI_SHELL_WEB_INF_PATH_1, webInfPath));
517            // set the path to the WEB-INF folder (the 2nd and 3rd parameters are just reasonable dummies)
518            CmsServletContainerSettings settings = new CmsServletContainerSettings(
519                webInfPath,
520                defaultWebAppName,
521                servletMapping,
522                null,
523                null);
524            m_opencms.getSystemInfo().init(settings);
525            // now read the configuration properties
526            String propertyPath = m_opencms.getSystemInfo().getConfigurationFileRfsPath();
527            out.println(m_messages.key(Messages.GUI_SHELL_CONFIG_FILE_1, propertyPath));
528            out.println();
529            CmsParameterConfiguration configuration = new CmsParameterConfiguration(propertyPath);
530
531            // now upgrade to runlevel 2
532            m_opencms = m_opencms.upgradeRunlevel(configuration);
533
534            // create a context object with 'Guest' permissions
535            m_cms = m_opencms.initCmsObject(m_opencms.getDefaultUsers().getUserGuest());
536
537            // initialize the shell
538            initShell(additionalShellCommands, out, err);
539        } catch (Throwable t) {
540            t.printStackTrace(err);
541        }
542    }
543
544    /**
545     * Main program entry point when started via the command line.<p>
546     *
547     * @param args parameters passed to the application via the command line
548     */
549    public static void main(String[] args) {
550
551        boolean wrongUsage = false;
552        String webInfPath = null;
553        String script = null;
554        String servletMapping = null;
555        String defaultWebApp = null;
556        String additional = null;
557
558        if (args.length > 4) {
559            wrongUsage = true;
560        } else {
561            for (int i = 0; i < args.length; i++) {
562                String arg = args[i];
563                if (arg.startsWith(SHELL_PARAM_BASE)) {
564                    webInfPath = arg.substring(SHELL_PARAM_BASE.length());
565                } else if (arg.startsWith(SHELL_PARAM_SCRIPT)) {
566                    script = arg.substring(SHELL_PARAM_SCRIPT.length());
567                } else if (arg.startsWith(SHELL_PARAM_SERVLET_MAPPING)) {
568                    servletMapping = arg.substring(SHELL_PARAM_SERVLET_MAPPING.length());
569                } else if (arg.startsWith(SHELL_PARAM_DEFAULT_WEB_APP)) {
570                    defaultWebApp = arg.substring(SHELL_PARAM_DEFAULT_WEB_APP.length());
571                } else if (arg.startsWith(SHELL_PARAM_ADDITIONAL_COMMANDS)) {
572                    additional = arg.substring(SHELL_PARAM_ADDITIONAL_COMMANDS.length());
573                } else {
574                    System.out.println(Messages.get().getBundle().key(Messages.GUI_SHELL_WRONG_USAGE_0));
575                    wrongUsage = true;
576                }
577            }
578        }
579        if (wrongUsage) {
580            System.out.println(Messages.get().getBundle().key(Messages.GUI_SHELL_USAGE_1, CmsShell.class.getName()));
581        } else {
582
583            I_CmsShellCommands additionalCommands = null;
584            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(additional)) {
585                try {
586                    Class<?> commandClass = Class.forName(additional);
587                    additionalCommands = (I_CmsShellCommands)commandClass.newInstance();
588                } catch (Exception e) {
589                    System.out.println(
590                        Messages.get().getBundle().key(Messages.GUI_SHELL_ERR_ADDITIONAL_COMMANDS_1, additional));
591                    e.printStackTrace();
592                    return;
593                }
594            }
595            boolean interactive = true;
596            FileInputStream stream = null;
597            if (script != null) {
598                try {
599                    stream = new FileInputStream(script);
600                } catch (IOException exc) {
601                    System.out.println(Messages.get().getBundle().key(Messages.GUI_SHELL_ERR_SCRIPTFILE_1, script));
602                }
603            }
604            if (stream == null) {
605                // no script-file, use standard input stream
606                stream = new FileInputStream(FileDescriptor.in);
607                interactive = true;
608            }
609            CmsShell shell = new CmsShell(
610                webInfPath,
611                servletMapping,
612                defaultWebApp,
613                "${user}@${project}:${siteroot}|${uri}>",
614                additionalCommands,
615                System.out,
616                System.err,
617                interactive);
618            shell.execute(stream);
619            try {
620                stream.close();
621            } catch (IOException e) {
622                e.printStackTrace();
623            }
624        }
625    }
626
627    /**
628     * Executes the commands from the given input stream in this shell.<p>
629     *
630     * <ul>
631     * <li>Commands in the must be separated with a line break '\n'.
632     * <li>Only one command per line is allowed.
633     * <li>String parameters must be quoted like this: <code>'string value'</code>.
634     * </ul>
635     *
636     * @param inputStream the input stream from which the commands are read
637     */
638    public void execute(InputStream inputStream) {
639
640        execute(new InputStreamReader(inputStream));
641    }
642
643    /**
644     * Executes the commands from the given reader in this shell.<p>
645     *
646     * <ul>
647     * <li>Commands in the must be separated with a line break '\n'.
648     * <li>Only one command per line is allowed.
649     * <li>String parameters must be quoted like this: <code>'string value'</code>.
650     * </ul>
651     *
652     * @param reader the reader from which the commands are read
653     */
654    public void execute(Reader reader) {
655
656        try {
657            LineNumberReader lnr = new LineNumberReader(reader);
658            while (!m_exitCalled) {
659                if (m_interactive || m_echo) {
660                    // print the prompt in front of the commands to process only when 'interactive'
661                    printPrompt();
662                }
663                String line = lnr.readLine();
664                if (line == null) {
665                    // if null the file has been read to the end
666                    try {
667                        Thread.sleep(500);
668                    } catch (Throwable t) {
669                        // noop
670                    }
671                    // end the while loop
672                    break;
673                }
674                if (line.trim().startsWith("#")) {
675                    m_out.println(line);
676                    continue;
677                }
678                // In linux, the up and down arrows generate escape sequences that cannot be properly handled.
679                // If a escape sequence is detected, OpenCms prints a warning message
680                if (line.indexOf(KeyEvent.VK_ESCAPE) != -1) {
681                    m_out.println(
682                            m_messages.key(Messages.GUI_SHELL_ESCAPE_SEQUENCES_NOT_SUPPORTED_0));
683                    continue;
684                }
685                StringReader lineReader = new StringReader(line);
686                StreamTokenizer st = new StreamTokenizer(lineReader);
687                st.eolIsSignificant(true);
688                st.wordChars('*', '*');
689                // put all tokens into a List
690                List<String> parameters = new ArrayList<String>();
691                while (st.nextToken() != StreamTokenizer.TT_EOF) {
692                    if (st.ttype == StreamTokenizer.TT_NUMBER) {
693                        parameters.add(Integer.toString(new Double(st.nval).intValue()));
694                    } else {
695                        if (null != st.sval) {
696                            parameters.add(st.sval);
697                        }
698                    }
699                }
700                lineReader.close();
701
702                if (parameters.size() == 0) {
703                    // empty line, just need to check if echo is on
704                    if (m_echo) {
705                        m_out.println();
706                    }
707                    continue;
708                }
709
710                // extract command and arguments
711                String command = parameters.get(0);
712                List<String> arguments = parameters.subList(1, parameters.size());
713
714                // execute the command with the given arguments
715                executeCommand(command, arguments);
716            }
717        } catch (Throwable t) {
718            t.printStackTrace(m_err);
719        }
720    }
721
722    /**
723     * Executes the commands from the given string in this shell.<p>
724     *
725     * <ul>
726     * <li>Commands in the must be separated with a line break '\n'.
727     * <li>Only one command per line is allowed.
728     * <li>String parameters must be quoted like this: <code>'string value'</code>.
729     * </ul>
730     *
731     * @param commands the string from which the commands are read
732     */
733    public void execute(String commands) {
734
735        execute(new StringReader(commands));
736    }
737
738    /**
739     * Exits this shell and destroys the OpenCms instance.<p>
740     */
741    public void exit() {
742
743        if (m_exitCalled) {
744            return;
745        }
746        m_exitCalled = true;
747        try {
748            if (m_additionalShellCommands != null) {
749                m_additionalShellCommands.shellExit();
750            } else {
751                m_shellCommands.shellExit();
752            }
753        } catch (Throwable t) {
754            t.printStackTrace();
755        }
756        if (m_opencms != null) {
757            // if called by an in line script we don't want to kill the whole instance
758            try {
759                m_opencms.shutDown();
760            } catch (Throwable t) {
761                t.printStackTrace();
762            }
763        }
764    }
765
766    /**
767     * Returns the stream this shell writes its error messages to.<p>
768     *
769     * @return the stream this shell writes its error messages to
770     */
771    public PrintStream getErr() {
772
773        return m_err;
774    }
775
776    /**
777     * Private internal helper for localization to the current user's locale
778     * within OpenCms. <p>
779     *
780     * @return the current user's <code>Locale</code>.
781     */
782    public Locale getLocale() {
783
784        if (getSettings() == null) {
785            return CmsLocaleManager.getDefaultLocale();
786        }
787        return getSettings().getLocale();
788    }
789
790    /**
791     * Returns the localized messages object for the current user.<p>
792     *
793     * @return the localized messages object for the current user
794     */
795    public CmsMessages getMessages() {
796
797        return m_messages;
798    }
799
800    /**
801     * Returns the stream this shell writes its regular messages to.<p>
802     *
803     * @return the stream this shell writes its regular messages to
804     */
805    public PrintStream getOut() {
806
807        return m_out;
808    }
809
810    /**
811     * Obtain the additional settings related to the current user.
812     *
813     * @return the additional settings related to the current user.
814     */
815    public CmsUserSettings getSettings() {
816
817        return m_settings;
818    }
819
820    /**
821     * Initializes the CmsShell.<p>
822     *
823     * @param additionalShellCommands optional object for additional shell commands, or null
824     * @param out stream to write the regular output messages to
825     * @param err stream to write the error messages output to
826     */
827    public void initShell(I_CmsShellCommands additionalShellCommands, PrintStream out, PrintStream err) {
828
829        // set the output streams
830        m_out = out;
831        m_err = err;
832
833        // initialize the settings of the user
834        m_settings = initSettings();
835
836        // initialize shell command object
837        m_shellCommands = new CmsShellCommands();
838        m_shellCommands.initShellCmsObject(m_cms, this);
839
840        // initialize additional shell command object
841        if (additionalShellCommands != null) {
842            m_additionalShellCommands = additionalShellCommands;
843            m_additionalShellCommands.initShellCmsObject(m_cms, this);
844            m_additionalShellCommands.shellStart();
845        } else {
846            m_shellCommands.shellStart();
847        }
848
849        m_commandObjects = new ArrayList<CmsCommandObject>();
850        if (m_additionalShellCommands != null) {
851            // get all shell callable methods from the additional shell command object
852            m_commandObjects.add(new CmsCommandObject(m_additionalShellCommands));
853        }
854        // get all shell callable methods from the CmsShellCommands
855        m_commandObjects.add(new CmsCommandObject(m_shellCommands));
856        // get all shell callable methods from the CmsRequestContext
857        m_commandObjects.add(new CmsCommandObject(m_cms.getRequestContext()));
858        // get all shell callable methods from the CmsObject
859        m_commandObjects.add(new CmsCommandObject(m_cms));
860    }
861
862    /**
863     * If <code>true</code> this is an interactive session with a user sitting on a console.<p>
864     *
865     * @return <code>true</code> if this is an interactive session with a user sitting on a console
866     */
867    public boolean isInteractive() {
868
869        return m_interactive;
870    }
871
872    /**
873     * Prints the shell prompt.<p>
874     */
875    public void printPrompt() {
876
877        String prompt = m_prompt;
878        try {
879            prompt = CmsStringUtil.substitute(prompt, "${user}", m_cms.getRequestContext().getCurrentUser().getName());
880            prompt = CmsStringUtil.substitute(prompt, "${siteroot}", m_cms.getRequestContext().getSiteRoot());
881            prompt = CmsStringUtil.substitute(
882                prompt,
883                "${project}",
884                m_cms.getRequestContext().getCurrentProject().getName());
885            prompt = CmsStringUtil.substitute(prompt, "${uri}", m_cms.getRequestContext().getUri());
886        } catch (Throwable t) {
887            // ignore
888        }
889        m_out.print(prompt);
890        m_out.flush();
891    }
892
893    /**
894     * Set <code>true</code> if this is an interactive session with a user sitting on a console.<p>
895     *
896     * This controls the output of the prompt and some other info that is valuable
897     * on the console, but not required in an automatic session.<p>
898     *
899     * @param interactive if <code>true</code> this is an interactive session with a user sitting on a console
900     */
901    public void setInteractive(boolean interactive) {
902
903        m_interactive = interactive;
904    }
905
906    /**
907     * Sets the locale of the current user.<p>
908     *
909     * @param locale the locale to set
910     *
911     * @throws CmsException in case the locale of the current user can not be stored
912     */
913    public void setLocale(Locale locale) throws CmsException {
914
915        CmsUserSettings settings = getSettings();
916        if (settings != null) {
917            settings.setLocale(locale);
918            settings.save(m_cms);
919            m_messages = Messages.get().getBundle(locale);
920        }
921    }
922
923    /**
924     * Reads the given stream and executes the commands in this shell.<p>
925     *
926     * @param inputStream an input stream from which commands are read
927     * @deprecated use {@link #execute(InputStream)} instead
928     */
929    @Deprecated
930    public void start(FileInputStream inputStream) {
931
932        // in the old behavior 'interactive' was always true
933        setInteractive(true);
934        execute(inputStream);
935    }
936
937    /**
938     * Validates the given user and password and checks if the user has the requested role.<p>
939     *
940     * @param userName the user name
941     * @param password the password
942     * @param requiredRole the required role
943     *
944     * @return <code>true</code> if the user is valid
945     */
946    public boolean validateUser(String userName, String password, CmsRole requiredRole) {
947
948        boolean result = false;
949
950        try {
951            CmsUser user = m_cms.readUser(userName, password);
952            result = OpenCms.getRoleManager().hasRole(m_cms, user.getName(), requiredRole);
953        } catch (@SuppressWarnings("unused") CmsException e) {
954            // nothing to do
955        }
956        return result;
957    }
958
959    /**
960     * Shows the signature of all methods containing the given search String.<p>
961     *
962     * @param searchString the String to search for in the methods, if null all methods are shown
963     */
964    protected void help(String searchString) {
965
966        String commandList;
967        boolean foundSomething = false;
968        m_out.println();
969
970        Iterator<CmsCommandObject> i = m_commandObjects.iterator();
971        while (i.hasNext()) {
972            CmsCommandObject cmdObj = i.next();
973            commandList = cmdObj.getMethodHelp(searchString);
974            if (!CmsStringUtil.isEmpty(commandList)) {
975                m_out.println(
976                    m_messages.key(Messages.GUI_SHELL_AVAILABLE_METHODS_1, cmdObj.getObject().getClass().getName()));
977                m_out.println(commandList);
978                foundSomething = true;
979            }
980        }
981
982        if (!foundSomething) {
983            m_out.println(m_messages.key(Messages.GUI_SHELL_MATCH_SEARCHSTRING_1, searchString));
984        }
985    }
986
987    /**
988     * Initializes the internal <code>CmsWorkplaceSettings</code> that contain (amongst other
989     * information) important information additional information about the current user
990     * (an instance of {@link CmsUserSettings}).<p>
991     *
992     * This step is performed within the <code>CmsShell</code> constructor directly after
993     * switching to run-level 2 and obtaining the <code>CmsObject</code> for the guest user as
994     * well as when invoking the CmsShell command <code>login</code>.<p>
995     *
996     * @return the user settings for the current user.
997     */
998    protected CmsUserSettings initSettings() {
999
1000        m_settings = new CmsUserSettings(m_cms);
1001        return m_settings;
1002    }
1003
1004    /**
1005     * Sets the echo status.<p>
1006     *
1007     * @param echo the echo status to set
1008     */
1009    protected void setEcho(boolean echo) {
1010
1011        m_echo = echo;
1012    }
1013
1014    /**
1015     * Sets the current shell prompt.<p>
1016     *
1017     * To set the prompt, the following variables are available:<p>
1018     *
1019     * <code>$u</code> the current user name<br>
1020     * <code>$s</code> the current site root<br>
1021     * <code>$p</code> the current project name<p>
1022     *
1023     * @param prompt the prompt to set
1024     */
1025    protected void setPrompt(String prompt) {
1026
1027        m_prompt = prompt;
1028    }
1029
1030    /**
1031     * Executes a shell command with a list of parameters.<p>
1032     *
1033     * @param command the command to execute
1034     * @param parameters the list of parameters for the command
1035     */
1036    private void executeCommand(String command, List<String> parameters) {
1037        if (null == command) {
1038            return;
1039        }
1040
1041        if (m_echo) {
1042            // echo the command to STDOUT
1043            m_out.print(command);
1044            for (int i = 0; i < parameters.size(); i++) {
1045                m_out.print(" '");
1046                m_out.print(parameters.get(i));
1047                m_out.print("'");
1048            }
1049            m_out.println();
1050        }
1051
1052        // prepare to lookup a method in CmsObject or CmsShellCommands
1053        boolean executed = false;
1054        Iterator<CmsCommandObject> i = m_commandObjects.iterator();
1055        while (!executed && i.hasNext()) {
1056            CmsCommandObject cmdObj = i.next();
1057            executed = cmdObj.executeMethod(command, parameters);
1058        }
1059
1060        if (!executed) {
1061            // method not found
1062            m_out.println();
1063            StringBuffer commandMsg = new StringBuffer(command).append("(");
1064            for (int j = 0; j < parameters.size(); j++) {
1065                commandMsg.append("value");
1066                if (j < (parameters.size() - 1)) {
1067                    commandMsg.append(", ");
1068                }
1069            }
1070            commandMsg.append(")");
1071
1072            m_out.println(m_messages.key(Messages.GUI_SHELL_METHOD_NOT_FOUND_1, commandMsg.toString()));
1073            m_out.println(m_messages.key(Messages.GUI_SHELL_HR_0));
1074            ((CmsShellCommands)m_shellCommands).help();
1075        }
1076    }
1077}