/* * RHQ Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.enterprise.communications.command.client; import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import mazz.i18n.Logger; import org.jboss.remoting.InvokerLocator; import org.rhq.enterprise.communications.command.Command; import org.rhq.enterprise.communications.command.CommandResponse; import org.rhq.enterprise.communications.command.CommandType; import org.rhq.enterprise.communications.command.impl.generic.GenericCommandClient; import org.rhq.enterprise.communications.i18n.CommI18NFactory; import org.rhq.enterprise.communications.i18n.CommI18NResourceKeys; /** * A command line client that can be used to issue a command from a shell or script. * * <p>Note that using this type of client limits the types of data that can be sent to the server; specifically * parameters to the command. Since arguments are parsed from the command line, only data that can be represented as a * <code>String</code> can be passed to the server as command parameters.</p> * * @author John Mazzitelli */ public class CmdlineClient { /** * Logger */ private static final Logger LOG = CommI18NFactory.getLogger(CmdlineClient.class); /** * the name of the command to execute */ private String m_commandName; /** * the version of the command to execute (default is 1) */ private int m_commandVersion = 1; /** * the set of parameters to pass to the command */ private Map<String, Object> m_params; /** * array of packages to search when looking for the concrete command client classes */ private String[] m_packages; /** * the name of the command client class that was specifically defined by a cmdline argument */ private String m_classname; /** * the remoting invoker locator URI */ private String m_locatorUri; /** * the subsystem of the remoting invocation handler (aka command processor) */ private String m_subsystem = JBossRemotingRemoteCommunicator.DEFAULT_SUBSYSTEM; /** * Main entry point to the command line client. * * @param args specifies the command to invoke and its parameters */ public static void main(String[] args) { try { CmdlineClient client = new CmdlineClient(); CommandResponse response = client.issueCommand(args); LOG.debug(CommI18NResourceKeys.CMDLINE_CLIENT_RESPONSE, response); } catch (Throwable e) { String cmdline = ""; for (int i = 0; i < args.length; i++) { cmdline += ("'" + args[i] + "'"); if ((i + 1) < args.length) { cmdline += " "; } } LOG.error(e, CommI18NResourceKeys.CMDLINE_CLIENT_EXECUTE_FAILURE, cmdline); } return; } /** * Simply builds and returns a command client defined by the given cmdline arguments. It does not send the command. * * <p>Given the command type information and the optional package locations where command clients can be found, this * will attempt to instantiate the specific client for the specific command. If the client cannot be found, an * attempt will be made to issue the command using the {@link GenericCommandClient generic client}.</p> * * @param args cmdline arguments * * @return the command client * * @throws IllegalArgumentException if a failure occurred while processing the cmdline arguments * @throws ClassNotFoundException if failed to find a valid command client class * @throws IllegalAccessException if failed to instantiate the command's client class * @throws InstantiationException if failed to instantiate the command's client class * @throws MalformedURLException if the given URL is invalid and cannot be used to locate an invoker */ public CommandClient buildCommandClient(String[] args) throws IllegalArgumentException, ClassNotFoundException, InstantiationException, IllegalAccessException, MalformedURLException { if (processCommandLine(args) != -1) { // this client cannot be used to invoke the command throw new IllegalArgumentException(LOG .getMsgString(CommI18NResourceKeys.CMDLINE_CLIENT_PROCESS_ARGS_FAILURE)); } // determine the name of the command client class and instantiate a new instance of it CommandClient commandClient; try { Class commandClientClazz = findCommandClientClass(); commandClient = instantiateCommandClient(commandClientClazz); } catch (ClassNotFoundException cnfe) { // can't find the command in question - assume the user knows best and just issue a generic command // cross your fingers and hope that the command type defined by the user is processable on the server-side LOG.debug(CommI18NResourceKeys.CMDLINE_CLIENT_USING_GENERIC_CLIENT, cnfe.getMessage()); try { GenericCommandClient customClient = new GenericCommandClient(); customClient.setCommandType(new CommandType(m_commandName, m_commandVersion)); commandClient = customClient; } catch (Exception e) { // any exception that occurred while trying to build a generic client should throw a ClassNotFound // to alert the caller that the original client class was not found and our workaround didn't work throw new ClassNotFoundException(cnfe.toString(), e); } } return commandClient; } /** * Simply builds and returns a command defined by the given cmdline arguments. It does not send the command. * * <p>Given the command type information and the optional package locations where command clients can be found, this * will attempt to instantiate the specific client for the specific command. If the client cannot be found, an * attempt will be made to issue the command using the {@link GenericCommandClient generic client}.</p> * * @param args cmdline arguments * * @return the command * * @throws IllegalArgumentException if a failure occurred while processing the cmdline arguments * @throws ClassNotFoundException if failed to find a valid command client class * @throws IllegalAccessException if failed to instantiate the command's client class * @throws InstantiationException if failed to instantiate the command's client class * @throws MalformedURLException if the given URL is invalid and cannot be used to locate an invoker */ public Command buildCommand(String[] args) throws IllegalArgumentException, ClassNotFoundException, InstantiationException, IllegalAccessException, MalformedURLException { CommandClient commandClient = buildCommandClient(args); return commandClient.createNewCommand(m_params); } /** * Issues a command defined by the given cmdline arguments. * * <p>Given the command type information and the optional package locations where command clients can be found, this * will attempt to instantiate the specific client for the specific command. If the client cannot be found, an * attempt will be made to issue the command using the {@link GenericCommandClient generic client}.</p> * * @param args cmdline arguments * * @return the response of the command * * @throws IllegalArgumentException if a failure occurred while processing the cmdline arguments * @throws ClassNotFoundException if failed to find a valid command client class * @throws IllegalAccessException if failed to instantiate the command's client class * @throws InstantiationException if failed to instantiate the command's client class * @throws MalformedURLException if the given URL is invalid and cannot be used to locate an invoker * @throws Throwable any other error that was due to a failure during the invocation of the command */ public CommandResponse issueCommand(String[] args) throws IllegalArgumentException, ClassNotFoundException, InstantiationException, IllegalAccessException, MalformedURLException, Throwable { CommandClient commandClient = buildCommandClient(args); // tell the new concrete command client instance to connect to the desired remote server if (m_locatorUri == null) { throw new MalformedURLException(LOG.getMsgString(CommI18NResourceKeys.CMDLINE_CLIENT_NULL_URI)); } InvokerLocator invokerLocator = new InvokerLocator(m_locatorUri); JBossRemotingRemoteCommunicator communicator = new JBossRemotingRemoteCommunicator(invokerLocator, m_subsystem, null); commandClient.setRemoteCommunicator(communicator); // tell the concrete command client instance to invoke the command on the remote server CommandResponse response = commandClient.invoke(m_params); commandClient.disconnectRemoteCommunicator(); return response; } /** * Convienence method that invokes a given command type (with the given set of parameter values) on the remote * server found at the given URI. * * <p>Note that this method may or may not be the most appropriate to use. Use this method if you do not know ahead * of time the type of command you are going to issue. If you already know the type of command you are going to * issue, it is best to use the command's more strongly typed client subclass method that implements * {@link CommandClient#invoke(Command)}.</p> * * @param commandType the type of command to issue * @param locatorURI location of the remote server * @param params set of name/value parameters sent along with the command (may be <code>null</code>) * * @return the command response * * @throws IllegalArgumentException if a failure occurred while processing the cmdline arguments * @throws ClassNotFoundException if failed to find the command's client class * @throws IllegalAccessException if failed to instantiate the command's client class * @throws InstantiationException if failed to instantiate the command's client class * @throws MalformedURLException if the given URL is invalid and cannot be used to locate an invoker * @throws Throwable any other error that was due to a failure during the invocation of the command */ public CommandResponse issueCommand(CommandType commandType, String locatorURI, Map<String, String> params) throws IllegalArgumentException, MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException, Throwable { ArrayList<String> args = new ArrayList<String>(); args.add("-c"); args.add(commandType.getName()); args.add("-v"); args.add("" + commandType.getVersion()); args.add("-u"); args.add(locatorURI); if (params != null) { for (Iterator iter = params.entrySet().iterator(); iter.hasNext();) { Map.Entry entry = (Map.Entry) iter.next(); Object paramName = entry.getKey(); Object paramValue = entry.getValue(); String paramNVP = paramName.toString(); if (paramValue != null) { paramNVP += ("=" + paramValue); } args.add(paramNVP); } } return issueCommand(args.toArray(new String[args.size()])); } /** * Convienence method that invokes a given command type on the remote server found at the given URI. Note that this * method is only useful if the command to be issued does not require any parameters. * * @param commandType the type of command to issue * @param locatorURI location of the remote server * * @return the command response * * @throws IllegalArgumentException if a failure occurred while processing the cmdline arguments * @throws ClassNotFoundException if failed to find the command's client class * @throws IllegalAccessException if failed to instantiate the command's client class * @throws InstantiationException if failed to instantiate the command's client class * @throws MalformedURLException if the given URL is invalid and cannot be used to locate an invoker * @throws Throwable any other error that was due to a failure during the invocation of the command */ public CommandResponse issueCommand(CommandType commandType, String locatorURI) throws IllegalArgumentException, MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException, Throwable { return issueCommand(commandType, locatorURI, null); } /** * Returns the help usage string. * * @return help text */ public String getUsage() { return LOG.getMsgString(CommI18NResourceKeys.CMDLINE_CLIENT_USAGE); } /** * Processes the command line arguments in a generic enough way for most if not all command clients to be able to * use. * * @param args the full set of command line arguments given to the client main method * * @return -1 if everything is OK, anything else means a problem occurred or not enough information was provided on * the cmdline to be able to continue. A 0 or higher means that the client cannot be used to invoke commands * (in that case, the number can be used as the exit code should we want the JVM to exit) * * @throws Error this should rarely, if ever, occur - it is usually due to a bug in getopt library */ private int processCommandLine(final String[] args) { int exitCode = -1; m_params = new HashMap<String, Object>(); // set this from a system property or default to the client classname String programName = System.getProperty("program.name", this.getClass().getName()); String sopts = "-:hv:p:c:l:u:s:"; LongOpt[] lopts = { new LongOpt("help", LongOpt.NO_ARGUMENT, null, 'h'), new LongOpt("pkgs", LongOpt.REQUIRED_ARGUMENT, null, 'p'), new LongOpt("cmd", LongOpt.REQUIRED_ARGUMENT, null, 'c'), new LongOpt("version", LongOpt.REQUIRED_ARGUMENT, null, 'v'), new LongOpt("class", LongOpt.REQUIRED_ARGUMENT, null, 'l'), new LongOpt("uri", LongOpt.REQUIRED_ARGUMENT, null, 'u'), new LongOpt("subsystem", LongOpt.REQUIRED_ARGUMENT, null, 's'), }; Getopt getopt = new Getopt(programName, args, sopts, lopts); int code; String arg; while ((code = getopt.getopt()) != -1) { switch (code) { case ':': case '?': { // for now both of these should exit with error status return 1; } case 1: { // this will catch non-option arguments (which will be command params for a particular command) arg = getopt.getOptarg(); String paramName; String paramValue; int i = arg.indexOf("="); if (i == -1) { paramName = arg; paramValue = "true"; } else { paramName = arg.substring(0, i); paramValue = arg.substring(i + 1, arg.length()); } // add the parameter to the returned map m_params.put(paramName, paramValue); LOG.debug(CommI18NResourceKeys.CMDLINE_CLIENT_CMDLINE_PARAM, programName, paramName, ((!"password" .equalsIgnoreCase(paramName)) ? paramValue : "*")); break; } case 'h': { // show command line help System.out.println(programName + " " + getUsage()); System.out.println(); return 0; } case 'v': { arg = getopt.getOptarg(); try { m_commandVersion = new Integer(arg).intValue(); } catch (NumberFormatException nfe) { LOG.error(CommI18NResourceKeys.CMDLINE_CLIENT_INVALID_CMD_VERSION, arg, nfe); return 1; } break; } case 'p': { arg = getopt.getOptarg(); LOG.debug(CommI18NResourceKeys.CMDLINE_CLIENT_PACKAGES, programName, arg); ArrayList<String> packageList = new ArrayList<String>(); StringTokenizer strtok = new StringTokenizer(arg, ":"); while (strtok.hasMoreTokens()) { packageList.add(strtok.nextToken()); } m_packages = packageList.toArray(new String[packageList.size()]); break; } case 'l': { m_classname = getopt.getOptarg(); LOG.debug(CommI18NResourceKeys.CMDLINE_CLIENT_CLASSNAME, programName, m_classname); break; } case 'c': { m_commandName = getopt.getOptarg(); LOG.debug(CommI18NResourceKeys.CMDLINE_CLIENT_COMMAND, programName, m_commandName); break; } case 'u': { m_locatorUri = getopt.getOptarg(); LOG.debug(CommI18NResourceKeys.CMDLINE_CLIENT_LOCATOR_URI, programName, m_locatorUri); break; } case 's': { m_subsystem = getopt.getOptarg(); LOG.debug(CommI18NResourceKeys.CMDLINE_CLIENT_SUBSYSTEM, programName, m_subsystem); break; } default: { // this should never happen, if it does its an error in getopt; throw an error so we know about it throw new Error(LOG.getMsgString(CommI18NResourceKeys.CMDLINE_CLIENT_UNHANDLED_OPTION, code)); } } } return exitCode; } /** * Attempts to find the concrete command client class implementation for the command to be issued. This will search * the list of packages defined on the command line; if no packages were specified, see below for the algorithm * used. * * <p>When searching the set of packages for the command client class, the client class must be located in a * subpackage with the same name as the command name with a class name equal to the command name (capitalized) * followed by "CommandClient".</p> * * <p>As an example, if the package to search is called "org.foo", and the command to invoke is called "bar", the * following class name will be used as the command client class: <b><code> * org.foo.bar.BarCommandClient</code></b>.</p> * * <p>A subpackage named "v#" (where # is the command's version number) will be searched if the command client can't * be found using the above algorithm: <b><code>org.foo.bar.v1.BarCommandClient</code></b>.</p> * * <p>If no packages are defined on the command line, the default will be to look in these package (in this * order):</p> * * <ul> * <li><code>org.rhq.enterprise.communications.[prefix.]command.impl</code></li> * <li><code>this.getClass().getPackage().getName()</code> [i.e. this class' package]</li> * </ul> * * <p>The above <i>[prefix.]</i> is called a command prefix. It is defined as a string followed by a dot (.) * followed by the command name. A command may or may not have a prefix. The command prefix and the command will be * used as-is, unless searching in the default org.jboss.on package structure - in which case the prefix will be * inserted after org.rhq.enterprise.communications. when searching. As an example, if the command to execute is * version 1 of "hello.world" and no packages are defined on the command line, the packages will be searched for the * following classes (assume this class is in package com.abc.command.client):</p> * * <ul> * <li><code>org.rhq.enterprise.communications.command.impl.hello.world.WorldCommandClient</code></li> * <li><code>org.rhq.enterprise.communications.command.impl.hello.world.v1.WorldCommandClient</code></li> * <li><code>org.rhq.enterprise.communications.hello.command.impl.world.WorldCommandClient</code></li> * <li><code>org.rhq.enterprise.communications.hello.command.impl.world.v1.WorldCommandClient</code></li> * <li><code>com.abc.command.client.hello.world.WorldCommandClient</code></li> * <li><code>com.abc.command.client.hello.world.v1.WorldCommandClient</code></li> * </ul> * * @return the class of the command client to use to invoke the command * * @throws ClassNotFoundException if the command client class was not found */ private Class findCommandClientClass() throws ClassNotFoundException { Class clazz = null; if (m_commandName == null) { throw new ClassNotFoundException(LOG.getMsgString(CommI18NResourceKeys.CMDLINE_CLIENT_CANNOT_FIND_CLIENT)); } if (m_classname != null) { clazz = Class.forName(m_classname); } else { if ((m_packages == null) || (m_packages.length == 0)) { m_packages = new String[] { "org.rhq.enterprise.communications.command.impl", this.getClass().getPackage().getName() }; } // a command can be something like "doSomething" or "prefix.doSomething" or "prefix.xyz.doSomething" int lastDot = m_commandName.lastIndexOf('.'); String lastElementCapitalized = Character.toUpperCase(m_commandName.charAt(lastDot + 1)) + m_commandName.substring(lastDot + 2); String commandClientClassName = lastElementCapitalized + "CommandClient"; String classPostfix = "." + m_commandName.toLowerCase() + "." + commandClientClassName; String versionedClassPostfix = "." + m_commandName.toLowerCase() + ".v" + m_commandVersion + "." + commandClientClassName; // use this string in case we fail to find the class - each location searched will go in here String searchFailures = LOG.getMsgString(CommI18NResourceKeys.CMDLINE_CLIENT_CANNOT_FIND_CLIENT_SEARCHED); // determine the list of fully-qualified class names that we will search in order to find the command client class List<String> classesToCheck = new ArrayList<String>(); for (int i = 0; (i < m_packages.length) && (clazz == null); i++) { String pkg = m_packages[i]; // clear the classes from the previous iteration classesToCheck.clear(); // we have special naming rules if looking up in our internal package and we have a command prefix if ("org.rhq.enterprise.communications.command.impl".equals(pkg)) { int firstDot = m_commandName.indexOf('.'); if (firstDot > -1) { String prefix = m_commandName.substring(0, firstDot); String classPostfixWithoutPrefix = classPostfix.substring(firstDot + 1); String versionedClassPostfixWithoutPrefix = versionedClassPostfix.substring(firstDot + 1); classesToCheck.add("org.rhq.enterprise.communications." + prefix + ".command.impl" + classPostfixWithoutPrefix); classesToCheck.add("org.rhq.enterprise.communications." + prefix + ".command.impl" + versionedClassPostfixWithoutPrefix); } } // ignoring our special rules, look for the class that is the package appended with the classPostfix classesToCheck.add(pkg + classPostfix); classesToCheck.add(pkg + versionedClassPostfix); // for the set of classes in the current package we are checking // doing the check here allows us to quit the package for-loop once we find the class for (Iterator iter = classesToCheck.iterator(); iter.hasNext() && (clazz == null);) { String classToFind = (String) iter.next(); try { clazz = Class.forName(classToFind); } catch (ClassNotFoundException cnfe) { // remember this location in case of error but move on to the next check searchFailures += (" : " + classToFind); } } } if (clazz == null) { throw new ClassNotFoundException(searchFailures); } } return clazz; } /** * Instantiates the given command client class and returns the new instance. Assumes a no-arg constructor exists for * the given class. * * @param commandClientClass the command client class to instantiate * * @return the new instance * * @throws IllegalAccessException if failed to create the new instance * @throws InstantiationException if failed to create the new instance */ private CommandClient instantiateCommandClient(Class commandClientClass) throws InstantiationException, IllegalAccessException { return (CommandClient) commandClientClass.newInstance(); } }