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