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