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.rmi;
029
030import java.io.FileInputStream;
031import java.io.IOException;
032import java.io.InputStream;
033import java.io.InputStreamReader;
034import java.io.LineNumberReader;
035import java.io.PrintStream;
036import java.io.StreamTokenizer;
037import java.io.StringReader;
038import java.rmi.RemoteException;
039import java.rmi.registry.LocateRegistry;
040import java.rmi.registry.Registry;
041import java.util.ArrayList;
042import java.util.Arrays;
043import java.util.HashMap;
044import java.util.HashSet;
045import java.util.List;
046import java.util.Map;
047import java.util.Set;
048
049/**
050 * Client application used to connect locally to the CmsShell server.<p>
051 */
052public class CmsRemoteShellClient {
053
054    /** Command parameter for passing an additional shell commands class name. */
055    public static final String PARAM_ADDITIONAL = "additional";
056
057    /** Command parameter for controlling the port to use for the initial RMI lookup. */
058    public static final String PARAM_REGISTRY_PORT = "registryPort";
059
060    /** Command parameter for passing a shell script file name. */
061    public static final String PARAM_SCRIPT = "script";
062
063    /** The name of the additional commands class. */
064    private String m_additionalCommands;
065
066    /** True if echo mode is turned on. */
067    private boolean m_echo;
068
069    /** The error code which should be returned in case of errors. */
070    private int m_errorCode;
071
072    /** True if exit was called. */
073    private boolean m_exitCalled;
074
075    /** True if an error occurred. */
076    private boolean m_hasError;
077
078    /** The input stream to read the commands from. */
079    private InputStream m_input;
080
081    /** Controls whether shell is interactive. */
082    private boolean m_interactive;
083
084    /** The output stream. */
085    private PrintStream m_out;
086
087    /** The prompt. */
088    private String m_prompt;
089
090    /** The port used for the RMI registry. */
091    private int m_registryPort;
092
093    /** The RMI referencce to the shell server. */
094    private I_CmsRemoteShell m_remoteShell;
095
096    /**
097     * Creates a new instance.<p>
098     *
099     * @param args the parameters
100     * @throws IOException if something goes wrong
101     */
102    public CmsRemoteShellClient(String[] args)
103    throws IOException {
104        Map<String, String> params = parseArgs(args);
105        String script = params.get(PARAM_SCRIPT);
106        if (script == null) {
107            m_interactive = true;
108            m_input = System.in;
109        } else {
110            m_input = new FileInputStream(script);
111        }
112        m_additionalCommands = params.get(PARAM_ADDITIONAL);
113        String port = params.get(PARAM_REGISTRY_PORT);
114        m_registryPort = CmsRemoteShellConstants.DEFAULT_PORT;
115        if (port != null) {
116            try {
117                m_registryPort = Integer.parseInt(port);
118                if (m_registryPort < 0) {
119                    System.out.println("Invalid port: " + port);
120                    System.exit(1);
121                }
122            } catch (NumberFormatException e) {
123                System.out.println("Invalid port: " + port);
124                System.exit(1);
125            }
126        }
127    }
128
129    /**
130     * Main method, which starts the shell client.<p>
131     *
132     * @param args the command line arguments
133     * @throws Exception if something goes wrong
134     */
135    public static void main(String[] args) throws Exception {
136
137        CmsRemoteShellClient client = new CmsRemoteShellClient(args);
138        client.run();
139    }
140
141    /**
142     * Validates, parses and returns the command line arguments.<p>
143     *
144     * @param args the command line arguments
145     * @return the map of parsed arguments
146     */
147    public Map<String, String> parseArgs(String[] args) {
148
149        Map<String, String> result = new HashMap<String, String>();
150        Set<String> allowedKeys = new HashSet<String>(
151            Arrays.asList(PARAM_ADDITIONAL, PARAM_SCRIPT, PARAM_REGISTRY_PORT));
152        for (String arg : args) {
153            if (arg.startsWith("-")) {
154                int eqPos = arg.indexOf("=");
155                if (eqPos >= 0) {
156                    String key = arg.substring(1, eqPos);
157                    if (!allowedKeys.contains(key)) {
158                        wrongUsage();
159                    }
160                    String val = arg.substring(eqPos + 1);
161                    result.put(key, val);
162                } else {
163                    wrongUsage();
164                }
165            } else {
166                wrongUsage();
167            }
168        }
169        return result;
170    }
171
172    /**
173     * Main loop of the shell server client.<p>
174     *
175     * Reads commands from either stdin or a file, executes them remotely and displays the results.
176     *
177     * @throws Exception if something goes wrong
178     */
179    public void run() throws Exception {
180
181        Registry registry = LocateRegistry.getRegistry(m_registryPort);
182        I_CmsRemoteShellProvider provider = (I_CmsRemoteShellProvider)(registry.lookup(
183            CmsRemoteShellConstants.PROVIDER));
184        m_remoteShell = provider.createShell(m_additionalCommands);
185        m_prompt = m_remoteShell.getPrompt();
186        m_out = new PrintStream(System.out);
187        try {
188            LineNumberReader lnr = new LineNumberReader(new InputStreamReader(m_input, "UTF-8"));
189            while (!exitCalled()) {
190                if (m_interactive || isEcho()) {
191                    // print the prompt in front of the commands to process only when 'interactive'
192                    printPrompt();
193                }
194                String line = lnr.readLine();
195                if (line == null) {
196                    break;
197                }
198                if (line.trim().startsWith("#")) {
199                    m_out.println(line);
200                    continue;
201                }
202                StringReader lineReader = new StringReader(line);
203                StreamTokenizer st = new StreamTokenizer(lineReader);
204                st.eolIsSignificant(true);
205                st.wordChars('*', '*');
206                // put all tokens into a List
207                List<String> parameters = new ArrayList<String>();
208                while (st.nextToken() != StreamTokenizer.TT_EOF) {
209                    if (st.ttype == StreamTokenizer.TT_NUMBER) {
210                        parameters.add(Integer.toString(new Double(st.nval).intValue()));
211                    } else {
212                        parameters.add(st.sval);
213                    }
214                }
215                lineReader.close();
216
217                if (parameters.size() == 0) {
218                    // empty line, just need to check if echo is on
219                    if (isEcho()) {
220                        m_out.println();
221                    }
222                    continue;
223                }
224
225                // extract command and arguments
226                String command = parameters.get(0);
227                List<String> arguments = new ArrayList<String>(parameters.subList(1, parameters.size()));
228
229                // execute the command with the given arguments
230                executeCommand(command, arguments);
231
232            }
233            exit(0);
234        } catch (Throwable t) {
235            t.printStackTrace();
236            if (m_errorCode != -1) {
237                exit(m_errorCode);
238            }
239        }
240    }
241
242    /**
243     * Executes a command remotely, displays the command output and updates the internal state.<p>
244     *
245     * @param command the command
246     * @param arguments the arguments
247     */
248    private void executeCommand(String command, List<String> arguments) {
249
250        try {
251            CmsShellCommandResult result = m_remoteShell.executeCommand(command, arguments);
252            m_out.print(result.getOutput());
253            updateState(result);
254            if (m_exitCalled) {
255                exit(0);
256            } else if (m_hasError && (m_errorCode != -1)) {
257                exit(m_errorCode);
258            }
259        } catch (RemoteException r) {
260            r.printStackTrace(System.err);
261            exit(1);
262        }
263    }
264
265    /**
266     * Exits the shell with an error code, and if possible, notifies the remote shell that it is exiting.<p>
267     *
268     * @param errorCode the error code
269     */
270    private void exit(int errorCode) {
271
272        try {
273            m_remoteShell.end();
274        } catch (Exception e) {
275            e.printStackTrace();
276        }
277        System.exit(errorCode);
278    }
279
280    /**
281     * Returns true if the exit command has been called.<p>
282     *
283     * @return true if the exit command has been called
284     */
285    private boolean exitCalled() {
286
287        return m_exitCalled;
288    }
289
290    /**
291     * Returns true if echo mode is enabled.<p>
292     *
293     * @return true if echo mode is enabled
294     */
295    private boolean isEcho() {
296
297        return m_echo;
298    }
299
300    /**
301     * Prints the prompt.<p>
302     */
303    private void printPrompt() {
304
305        System.out.print(m_prompt);
306    }
307
308    /**
309     * Updates the internal client state based on the state received from the server.<p>
310     *
311     * @param result the result of the last shell command execution
312     */
313    private void updateState(CmsShellCommandResult result) {
314
315        m_errorCode = result.getErrorCode();
316        m_prompt = result.getPrompt();
317        m_exitCalled = result.isExitCalled();
318        m_hasError = result.hasError();
319        m_echo = result.hasEcho();
320    }
321
322    /**
323     * Displays text which shows the valid command line parameters, and then exits.
324     */
325    private void wrongUsage() {
326
327        String usage = "Usage: java -cp $PATH_TO_OPENCMS_JAR org.opencms.rmi.CmsRemoteShellClient\n"
328            + "    -script=[path to script] (optional) \n"
329            + "    -registryPort=[port of RMI registry] (optional, default is "
330            + CmsRemoteShellConstants.DEFAULT_PORT
331            + ")\n"
332            + "    -additional=[additional commands class name] (optional)";
333        System.out.println(usage);
334        System.exit(1);
335    }
336
337}