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