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