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