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, 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.rmi;
029
030import org.opencms.file.CmsObject;
031import org.opencms.main.CmsException;
032import org.opencms.main.CmsLog;
033import org.opencms.main.CmsShell;
034import org.opencms.main.CmsShellCommandException;
035import org.opencms.main.I_CmsShellCommands;
036import org.opencms.main.OpenCms;
037import org.opencms.util.CmsStringUtil;
038
039import java.io.ByteArrayOutputStream;
040import java.io.PrintStream;
041import java.io.UnsupportedEncodingException;
042import java.lang.reflect.InvocationTargetException;
043import java.rmi.RemoteException;
044import java.rmi.server.UnicastRemoteObject;
045import java.util.ArrayList;
046import java.util.List;
047
048import org.apache.commons.lang3.RandomStringUtils;
049import org.apache.commons.logging.Log;
050
051import com.google.common.collect.Lists;
052
053/**
054 * RMI object which wraps a CmsShell and can be used for shell command execution.
055 */
056public class CmsRemoteShell extends UnicastRemoteObject implements I_CmsRemoteShell {
057
058    /**
059     * Stores remote shell instances which haven't been unregistered yet.<p>
060     */
061    static class InstanceStore {
062
063        /** The list of shell instances. */
064        private List<CmsRemoteShell> m_instances = Lists.newArrayList();
065
066        /**
067         * Adds a new instance.<p>
068         *
069         * @param shell the instance to add
070         */
071        public synchronized void add(CmsRemoteShell shell) {
072
073            m_instances.add(shell);
074        }
075
076        /**
077         * Removes and unexports an instance.<p>
078         *
079         * @param cmsRemoteShell the instance to remove
080         */
081        @SuppressWarnings("synthetic-access")
082        public synchronized void remove(CmsRemoteShell cmsRemoteShell) {
083
084            try {
085                UnicastRemoteObject.unexportObject(cmsRemoteShell, true);
086            } catch (Exception e) {
087                LOG.error(e.getLocalizedMessage(), e);
088            }
089            m_instances.remove(cmsRemoteShell);
090        }
091
092        /**
093         * Removes and unexports all instances.<p>
094         */
095        @SuppressWarnings("synthetic-access")
096        public synchronized void removeAll() {
097
098            for (CmsRemoteShell shell : m_instances) {
099                try {
100                    UnicastRemoteObject.unexportObject(shell, true);
101                } catch (Exception e) {
102                    LOG.error(e.getLocalizedMessage(), e);
103                }
104            }
105            m_instances.clear();
106
107        }
108
109    }
110
111    /** The log instance for this class. */
112    private static final Log LOG = CmsLog.getLog(CmsRemoteShell.class);
113
114    /** Serial version id. */
115    private static final long serialVersionUID = -243325251951003282L;
116
117    /** Stores instances which have yet to be unexported. */
118    private static InstanceStore m_instanceStore = new InstanceStore();
119
120    /** Byte array stream used to capture output of shell commands; will be cleared for each individual command. */
121    private ByteArrayOutputStream m_baos = new ByteArrayOutputStream();
122
123    /** Random id string for debugging purposes. */
124    private String m_id;
125
126    /** The output stream used to capture the shell command output. */
127    private PrintStream m_out;
128
129    /** The wrapped shell instance. */
130    private CmsShell m_shell;
131
132    /**
133     * Creates a new instance.<p>
134     *
135     * @param additionalCommandsNames comma separated list of full qualified names of classes with additional shell
136     *                               commands (may be null)
137     * @param port the port to use
138     *
139     * @throws CmsException if something goes wrong
140     * @throws RemoteException if RMI stuff goes wrong
141     */
142    public CmsRemoteShell(String additionalCommandsNames, int port)
143    throws CmsException, RemoteException {
144
145        super(port);
146        m_id = RandomStringUtils.randomAlphanumeric(8);
147        List<I_CmsShellCommands> additionalCommands = new ArrayList<>();
148        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(additionalCommandsNames)) {
149            String[] classNames = additionalCommandsNames.split(",");
150            for (String className : classNames) {
151                try {
152                    className = className.trim();
153
154                    Class<?> commandsCls = Class.forName(className);
155                    if (I_CmsShellCommands.class.isAssignableFrom(commandsCls)) {
156                        additionalCommands.add((I_CmsShellCommands) commandsCls.getDeclaredConstructor().newInstance());
157                        LOG.info("Class " + className + " has been loaded and added to additional commands.");
158                    } else {
159                        LOG.error("Error: Class " + className + " does not implement I_CmsShellCommands");
160                    }
161                } catch (ClassNotFoundException e) {
162                    final String errMsg = "Error: Could not find the class " + className;
163                    LOG.error(errMsg, e);
164                    throw new IllegalArgumentException(errMsg, e);
165                } catch (InstantiationException | IllegalAccessException | NoSuchMethodException |
166                         InvocationTargetException e) {
167                    final String errMsg = "Error instantiating the class " + className + ". " + e.getLocalizedMessage();
168                    LOG.error(errMsg, e);
169                    throw new IllegalArgumentException(errMsg, e);
170                }
171            }
172        }
173
174        CmsObject cms = OpenCms.initCmsObject("Guest");
175        m_out = new PrintStream(m_baos, true);
176        m_shell = new CmsShell(cms, "${user}@${project}:${siteroot}|${uri}>", additionalCommands, m_out, m_out);
177        m_instanceStore.add(this);
178    }
179
180    /**
181     * Removes and unexports all instances.<p>
182     */
183    public static void unregisterAll() {
184
185        m_instanceStore.removeAll();
186    }
187
188    /**
189     * @see org.opencms.rmi.I_CmsRemoteShell#end()
190     */
191    public void end() {
192
193        m_instanceStore.remove(this);
194    }
195
196    /**
197     * @see org.opencms.rmi.I_CmsRemoteShell#executeCommand(java.lang.String, java.util.List)
198     */
199    public CmsShellCommandResult executeCommand(String cmd, List<String> params) {
200
201        LOG.debug(m_id + " executing " + cmd + " " + params);
202        CmsShellCommandResult result = new CmsShellCommandResult();
203        m_baos.reset();
204        boolean hasError = false;
205        try {
206            CmsShell.pushShell(m_shell);
207            m_shell.executeCommand(cmd, params);
208        } catch (CmsShellCommandException e) {
209            hasError = true;
210            LOG.warn(m_id + " " + e.getLocalizedMessage(), e);
211        } finally {
212            CmsShell.popShell();
213            m_out.flush();
214        }
215        hasError |= m_shell.hasReportError();
216        result.setExitCalled(m_shell.isExitCalled());
217        result.setHasError(hasError);
218        result.setErrorCode(m_shell.getErrorCode());
219        result.setPrompt(m_shell.getPrompt());
220        result.setEcho(m_shell.hasEcho());
221        try {
222            String outputString = new String(m_baos.toByteArray(), "UTF-8");
223            result.setOutput(outputString);
224        } catch (UnsupportedEncodingException e) {
225            e.printStackTrace();
226        }
227        return result;
228    }
229
230    /**
231     * @see org.opencms.rmi.I_CmsRemoteShell#getPrompt()
232     */
233    public String getPrompt() {
234
235        return m_shell.getPrompt();
236    }
237
238}