View Javadoc

1   /*
2    * Client
3    *
4    * $Id: Client.java,v 1.15 2005/10/03 20:07:45 stack-sf Exp $
5    *
6    * Created on Nov 12, 2004
7    *
8    * Copyright (C) 2004 Internet Archive.
9    *
10   * This file is part of the Heritrix web crawler (crawler.archive.org).
11   *
12   * Heritrix is free software; you can redistribute it and/or modify
13   * it under the terms of the GNU Lesser Public License as published by
14   * the Free Software Foundation; either version 2.1 of the License, or
15   * any later version.
16   *
17   * Heritrix is distributed in the hope that it will be useful,
18   * but WITHOUT ANY WARRANTY; without even the implied warranty of
19   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20   * GNU Lesser Public License for more details.
21   *
22   * You should have received a copy of the GNU Lesser Public License
23   * along with Heritrix; if not, write to the Free Software
24   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
25   */
26  package org.archive.jmx;
27  
28  import java.io.IOException;
29  import java.io.PrintWriter;
30  import java.io.StringWriter;
31  import java.text.FieldPosition;
32  import java.text.ParseException;
33  import java.text.SimpleDateFormat;
34  import java.util.Date;
35  import java.util.HashMap;
36  import java.util.Iterator;
37  import java.util.Map;
38  import java.util.Set;
39  import java.util.logging.ConsoleHandler;
40  import java.util.logging.Handler;
41  import java.util.logging.LogRecord;
42  import java.util.logging.Logger;
43  import java.util.logging.SimpleFormatter;
44  import java.util.regex.Matcher;
45  import java.util.regex.Pattern;
46  
47  import javax.management.Attribute;
48  import javax.management.AttributeList;
49  import javax.management.InstanceNotFoundException;
50  import javax.management.IntrospectionException;
51  import javax.management.MBeanAttributeInfo;
52  import javax.management.MBeanFeatureInfo;
53  import javax.management.MBeanInfo;
54  import javax.management.MBeanOperationInfo;
55  import javax.management.MBeanParameterInfo;
56  import javax.management.MBeanServerConnection;
57  import javax.management.MalformedObjectNameException;
58  import javax.management.ObjectInstance;
59  import javax.management.ObjectName;
60  import javax.management.ReflectionException;
61  import javax.management.openmbean.CompositeData;
62  import javax.management.openmbean.TabularData;
63  import javax.management.remote.JMXConnector;
64  import javax.management.remote.JMXConnectorFactory;
65  import javax.management.remote.JMXServiceURL;
66  
67  
68  /***
69   * A Simple Command-Line JMX Client.
70   * Tested against the JDK 1.5.0 JMX Agent.
71   * See <a href="http://java.sun.com/j2se/1.5.0/docs/guide/management/agent.html">Monitoring
72   * and Management Using JMX</a>.
73   * <p>Can supply credentials and do primitive string representation of tabular
74   * and composite openmbeans.
75   * @author stack
76   */
77  public class Client {
78      private static final Logger logger =
79          Logger.getLogger(Client.class.getName());
80      
81      /***
82       * Usage string.
83       */
84      private static final String USAGE = "Usage: java -jar" +
85          " cmdline-jmxclient.jar USER:PASS HOST:PORT [BEAN] [COMMAND]\n" +
86          "Options:\n" +
87          " USER:PASS Username and password. Required. If none, pass '-'.\n" +
88          "           E.g. 'controlRole:secret'\n" +
89          " HOST:PORT Hostname and port to connect to. Required." +
90          " E.g. localhost:8081.\n" +
91          "           Lists registered beans if only USER:PASS and this" +
92          " argument.\n" +
93          " BEAN      Optional target bean name. If present we list" +
94          " available operations\n" +
95          "           and attributes.\n" +
96          " COMMAND   Optional operation to run or attribute to fetch. If" +
97          " none supplied,\n" +
98          "           all operations and attributes are listed. Attributes" +
99          " begin with a\n" +
100         "           capital letter: e.g. 'Status' or 'Started'." +
101         " Operations do not.\n" +
102         "           Operations can take arguments by adding an '=' " +
103         "followed by\n" +
104         "           comma-delimited params. Pass multiple " +
105         "attributes/operations to run\n" +
106         "           more than one per invocation. Use commands 'create' and " +
107         "'destroy'\n" +
108         "           to instantiate and unregister beans ('create' takes name " +
109         "of class).\n" +
110         "           Pass 'Attributes' to get listing of all attributes and " +
111         "and their\n" +
112         "           values.\n" +
113         "Requirements:\n" +
114         " JDK1.5.0. If connecting to a SUN 1.5.0 JDK JMX Agent, remote side" +
115         " must be\n" +
116         " started with system properties such as the following:\n" +
117         "     -Dcom.sun.management.jmxremote.port=PORT\n" +
118         "     -Dcom.sun.management.jmxremote.authenticate=false\n" +
119         "     -Dcom.sun.management.jmxremote.ssl=false\n" +
120         " The above will start the remote server with no password. See\n" +
121         " http://java.sun.com/j2se/1.5.0/docs/guide/management/agent.html" +
122         " for more on\n" +
123         " 'Monitoring and Management via JMX'.\n" +
124         "Client Use Examples:\n" +
125         " To list MBeans on a non-password protected remote agent:\n" +
126         "     % java -jar cmdline-jmxclient-X.X.jar - localhost:8081 //\n" +
127         "         org.archive.crawler:name=Heritrix,type=Service\n" +
128         " To list attributes and attributes of the Heritrix MBean:\n" +
129         "     % java -jar cmdline-jmxclient-X.X.jar - localhost:8081 //\n" +
130         "         org.archive.crawler:name=Heritrix,type=Service //\n" +
131         "         schedule=http://www.archive.org\n" +
132         " To set set logging level to FINE on a password protected JVM:\n" +
133         "     % java -jar cmdline-jmxclient-X.X.jar controlRole:secret" +
134         " localhost:8081 //\n" +
135         "         java.util.logging:type=Logging //\n" +
136         "         setLoggerLevel=org.archive.crawler.Heritrix,FINE";
137     
138     /***
139      * Pattern that matches a command name followed by
140      * an optional equals and optional comma-delimited list
141      * of arguments.
142      */
143     protected static final Pattern CMD_LINE_ARGS_PATTERN =
144         Pattern.compile("^([^=]+)(?:(?://=)(.+))?$");
145     
146     private static final String CREATE_CMD_PREFIX = "create=";
147     
148 	public static void main(String[] args) throws Exception {
149         Client client = new Client();
150         // Set the logger to use our all-on-one-line formatter.
151         Logger l = Logger.getLogger("");
152         Handler [] hs = l.getHandlers();
153         for (int i = 0; i < hs.length; i++) {
154             Handler h = hs[0];
155             if (h instanceof ConsoleHandler) {
156                 h.setFormatter(client.new OneLineSimpleLogger());
157             }
158         }
159         client.execute(args);
160 	}
161     
162     protected static void usage() {
163         usage(0, null);
164     }
165     
166     protected static void usage(int exitCode, String message) {
167         if (message != null && message.length() > 0) {
168             System.out.println(message);
169         }
170         System.out.println(USAGE);
171         System.exit(exitCode);
172     }
173 
174     /***
175      * Constructor.
176      */
177     public Client() {
178         super();
179     }
180     
181     /***
182      * Parse a 'login:password' string.  Assumption is that no
183      * colon in the login name.
184      * @param userpass
185      * @return Array of strings with login in first position.
186      */
187     protected String [] parseUserpass(final String userpass) {
188         if (userpass == null || userpass.equals("-")) {
189             return null;
190         }
191         int index = userpass.indexOf(':');
192         if (index <= 0) {
193             throw new RuntimeException("Unable to parse: " +userpass);
194         }
195         return new String [] {userpass.substring(0, index),
196             userpass.substring(index + 1)};
197     }
198     
199     /***
200      * @param login
201      * @param password
202      * @return Credentials as map for RMI.
203      */
204     protected Map formatCredentials(final String login,
205             final String password) {
206         Map env = null;
207         String[] creds = new String[] {login, password};
208         env = new HashMap(1);
209         env.put(JMXConnector.CREDENTIALS, creds);
210         return env;
211     }
212     
213     protected JMXConnector getJMXConnector(final String hostport,
214             final String login, final String password)
215     throws IOException {
216         // Make up the jmx rmi URL and get a connector.
217         JMXServiceURL rmiurl = new JMXServiceURL("service:jmx:rmi://"
218             + hostport + "/jndi/rmi://" + hostport + "/jmxrmi");
219         return JMXConnectorFactory.connect(rmiurl,
220             formatCredentials(login, password));
221     }
222     
223     protected ObjectName getObjectName(final String beanname)
224     throws MalformedObjectNameException, NullPointerException {
225         return notEmpty(beanname)? new ObjectName(beanname): null;
226     }
227     
228     /***
229      * Version of execute called from the cmdline.
230      * Prints out result of execution on stdout.
231      * Parses cmdline args.  Then calls {@link #execute(String, String,
232      * String, String, String[], boolean)}.
233      * @param args Cmdline args.
234      * @throws Exception
235      */
236     protected void execute(final String [] args)
237     throws Exception {
238         // Process command-line.
239         if (args.length == 0 || args.length == 1) {
240             usage();
241         }
242         String userpass = args[0];
243         String hostport = args[1];
244         String beanname = null;
245         String [] command = null;
246         if (args.length > 2) {
247             beanname = args[2];
248         }
249         if (args.length > 3) {
250             command = new String [args.length - 3];
251             for (int i = 3; i < args.length; i++) {
252                 command[i - 3] = args[i];
253             }
254         }
255         String [] loginPassword = parseUserpass(userpass);
256         Object [] result = execute(hostport,
257             ((loginPassword == null)? null: loginPassword[0]),
258             ((loginPassword == null)? null: loginPassword[1]), beanname,
259             command);
260         // Print out results on stdout. Only log if a result.
261         if (result != null) {
262             for (int i = 0; i < result.length; i++) {
263                 if (result[i] != null && result[i].toString().length() > 0) {
264                     if (command != null) {
265                         logger.info(command[i] + ": " + result[i]);
266                     } else {
267                         logger.info("\n" + result[i].toString());
268                     }
269                 }
270             }
271         }
272     }
273     
274     protected Object [] execute(final String hostport, final String login,
275             final String password, final String beanname,
276             final String [] command)
277     throws Exception {
278         return execute(hostport, login, password, beanname, command, false);
279     }
280 
281     public Object [] executeOneCmd(final String hostport, final String login,
282             final String password, final String beanname,
283             final String command)
284     throws Exception {
285         return execute(hostport, login, password, beanname,
286             new String[] {command}, true);
287     }
288     
289     /***
290      * Execute command against remote JMX agent.
291      * @param hostport 'host:port' combination.
292      * @param login RMI login to use.
293      * @param password RMI password to use.
294      * @param beanname Name of remote bean to run command against.
295      * @param command Array of commands to run.
296      * @param oneBeanOnly Set true if passed <code>beanname</code> is
297      * an exact name and the query for a bean is only supposed to return
298      * one bean instance. If not, we raise an exception (Otherwise, if false,
299      * then we deal with possibility of multiple bean instances coming back
300      * from query). Set to true when want to get an attribute or run an
301      * operation.
302      * @return Array of results -- one per command.
303      * @throws Exception
304      */
305     protected Object [] execute(final String hostport, final String login,
306             final String password, final String beanname,
307             final String [] command, final boolean oneBeanOnly)
308     throws Exception {
309         JMXConnector jmxc = getJMXConnector(hostport, login, password);
310         Object [] result = null;
311         try {
312             result = doBeans(jmxc.getMBeanServerConnection(),
313                 getObjectName(beanname), command, oneBeanOnly);
314         } finally {
315             jmxc.close();
316         }
317         return result;
318     }
319     
320     protected boolean notEmpty(String s) {
321         return s != null && s.length() > 0;
322     }
323         
324     protected Object [] doBeans(final MBeanServerConnection mbsc,
325         final ObjectName objName, final String[] command,
326         final boolean oneBeanOnly)
327     throws Exception {
328         Object [] result = null;
329         Set beans = mbsc.queryMBeans(objName, null);
330         if (beans.size() == 0) {
331             // No bean found. Check if we are to create a bean?
332             if (command.length == 1 && notEmpty(command[0])
333                     && command[0].startsWith(CREATE_CMD_PREFIX)) {
334                 String className =
335                     command[0].substring(CREATE_CMD_PREFIX.length());
336                 mbsc.createMBean(className, objName);
337             } else {
338                 // TODO: Is there a better JMX exception that RE for this
339                 // scenario?
340                 throw new RuntimeException(objName.getCanonicalName() +
341                     " not registered.");
342             }
343         } else if (beans.size() == 1) {
344             result = doBean(mbsc, (ObjectInstance) beans.iterator().next(),
345                 command);
346         } else {
347             if (oneBeanOnly) {
348                 throw new RuntimeException("Only supposed to be one bean " +
349                     "query result");
350             }
351             // This is case of multiple beans in query results.
352             // Print name of each into a StringBuffer.  Return as one
353             // result.
354             StringBuffer buffer = new StringBuffer();
355             for (Iterator i = beans.iterator(); i.hasNext();) {
356                 Object obj = i.next();
357                 if (obj instanceof ObjectName) {
358                     buffer.append((((ObjectName) obj).getCanonicalName()));
359                 } else if (obj instanceof ObjectInstance) {
360                     buffer.append((((ObjectInstance) obj).getObjectName()
361                         .getCanonicalName()));
362                 } else {
363                     throw new RuntimeException("Unexpected object type: " + obj);
364                 }
365                 buffer.append("\n");
366             }
367             result = new String [] {buffer.toString()};
368         }
369         return result;
370     }
371     
372     /***
373      * Get attribute or run operation against passed bean <code>instance</code>.
374      * 
375      * @param mbsc Server connection.
376      * @param instance Bean instance we're to get attributes from or run
377      * operation against.
378      * @param command Command to run (May be null).
379      * @return Result.  If multiple commands, multiple results.
380      * @throws Exception
381      */
382     protected Object [] doBean(MBeanServerConnection mbsc,
383         ObjectInstance instance, String [] command)
384     throws Exception {
385         // If no command, then print out list of attributes and operations.
386         if (command == null || command.length <= 0) {
387             return new String [] {listOptions(mbsc, instance)};
388         }
389         
390         // Maybe multiple attributes/operations listed on one command line.
391         Object [] result = new Object[command.length];
392         for (int i = 0; i < command.length; i++) {
393             result[i] = doSubCommand(mbsc, instance, command[i]);
394         }
395         return result;
396     }
397     
398     public Object doSubCommand(MBeanServerConnection mbsc,
399         ObjectInstance instance, String subCommand)
400     throws Exception {
401         // First, handle special case of our being asked to destroy a bean.
402         if (subCommand.equals("destroy")) {
403             mbsc.unregisterMBean(instance.getObjectName());
404             return null;
405         } else if (subCommand.startsWith(CREATE_CMD_PREFIX)) {
406             throw new IllegalArgumentException("You cannot call create " +
407                     "on an already existing bean.");
408         }
409         
410         // Get attribute and operation info.
411         MBeanAttributeInfo [] attributeInfo =
412             mbsc.getMBeanInfo(instance.getObjectName()).getAttributes();
413         MBeanOperationInfo [] operationInfo =
414             mbsc.getMBeanInfo(instance.getObjectName()).getOperations();
415         // Now, bdbje JMX bean doesn't follow the convention of attributes
416         // having uppercase first letter and operations having lowercase
417         // first letter.  But most beans do. Be prepared to handle the bdbje
418         // case.
419         Object result = null;
420         if (Character.isUpperCase(subCommand.charAt(0))) {
421             // Probably an attribute.
422             if (!isFeatureInfo(attributeInfo, subCommand) &&
423                     isFeatureInfo(operationInfo, subCommand)) {
424                 // Its not an attribute name. Looks like its name of an
425                 // operation.  Try it.
426                 result =
427                     doBeanOperation(mbsc, instance, subCommand, operationInfo);
428             } else {
429                 // Then it is an attribute OR its not an attribute name nor
430                 // operation name and the below invocation will throw a
431                 // AttributeNotFoundException.
432                 result = doAttributeOperation(mbsc, instance, subCommand,
433                     attributeInfo);
434             }
435         } else {
436             // Must be an operation.
437             if (!isFeatureInfo(operationInfo, subCommand) &&
438                     isFeatureInfo(attributeInfo, subCommand)) {
439                 // Its not an operation name but looks like it could be an
440                 // attribute name. Try it.
441                 result = doAttributeOperation(mbsc, instance, subCommand,
442                     attributeInfo);        
443             } else {
444                 // Its an operation name OR its neither operation nor attribute
445                 // name and the below will throw a NoSuchMethodException.
446                 result =
447                     doBeanOperation(mbsc, instance, subCommand, operationInfo);
448             }
449         }
450         
451         // Look at the result.  Is it of composite or tabular type?
452         // If so, convert to a String representation.
453         if (result instanceof CompositeData) {
454             result = recurseCompositeData(new StringBuffer("\n"), "", "",
455                 (CompositeData)result);
456         } else if (result instanceof TabularData) {
457             result = recurseTabularData(new StringBuffer("\n"), "", "",
458                  (TabularData)result);
459         } else if (result instanceof String []) {
460             String [] strs = (String [])result;
461             StringBuffer buffer = new StringBuffer("\n");
462             for (int i = 0; i < strs.length; i++) {
463                 buffer.append(strs[i]);
464                 buffer.append("\n");
465             }
466             result = buffer;
467         } else if (result instanceof AttributeList) {
468             AttributeList list = (AttributeList)result;
469             if (list.size() <= 0) {
470                 result = null;
471             } else {
472                 StringBuffer buffer = new StringBuffer("\n");
473                 for (Iterator ii = list.iterator(); ii.hasNext();) {
474                     Attribute a = (Attribute)ii.next();
475                     buffer.append(a.getName());
476                     buffer.append(": ");
477                     buffer.append(a.getValue());
478                     buffer.append("\n");
479                 }
480                 result = buffer;
481             }
482         }
483         return result;
484     }
485     
486     protected boolean isFeatureInfo(MBeanFeatureInfo [] infos, String cmd) {
487         return getFeatureInfo(infos, cmd) != null;
488     }
489     
490     protected MBeanFeatureInfo getFeatureInfo(MBeanFeatureInfo [] infos,
491             String cmd) {
492         // Cmd may be carrying arguments.  Don't count them in the compare.
493         int index = cmd.indexOf('=');
494         String name = (index > 0)? cmd.substring(0, index): cmd;
495         for (int i = 0; i < infos.length; i++) {
496             if (infos[i].getName().equals(name)) {
497                 return infos[i];
498             }
499         }
500         return null;
501     }
502     
503     protected StringBuffer recurseTabularData(StringBuffer buffer,
504             String indent, String name, TabularData data) {
505         addNameToBuffer(buffer, indent, name);
506         java.util.Collection c = data.values();
507         for (Iterator i = c.iterator(); i.hasNext();) {
508             Object obj = i.next();
509             if (obj instanceof CompositeData) {
510                 recurseCompositeData(buffer, indent + " ", "",
511                     (CompositeData)obj);
512             } else if (obj instanceof TabularData) {
513                 recurseTabularData(buffer, indent, "",
514                     (TabularData)obj);
515             } else {
516                 buffer.append(obj);
517             }
518         }
519         return buffer;
520     }
521     
522     protected StringBuffer recurseCompositeData(StringBuffer buffer,
523             String indent, String name, CompositeData data) {
524         indent = addNameToBuffer(buffer, indent, name);
525         for (Iterator i = data.getCompositeType().keySet().iterator();
526                 i.hasNext();) {
527             String key = (String)i.next();
528             Object o = data.get(key);
529             if (o instanceof CompositeData) {
530                 recurseCompositeData(buffer, indent + " ", key,
531                     (CompositeData)o);
532             } else if (o instanceof TabularData) {
533                 recurseTabularData(buffer, indent, key, (TabularData)o);
534             } else {
535                 buffer.append(indent);
536                 buffer.append(key);
537                 buffer.append(": ");
538                 buffer.append(o);
539                 buffer.append("\n");
540             }
541         }
542         return buffer;
543     }
544     
545     protected String addNameToBuffer(StringBuffer buffer, String indent,
546             String name) {
547         if (name == null || name.length() == 0) {
548             return indent;
549         }
550         buffer.append(indent);
551         buffer.append(name);
552         buffer.append(":\n");
553         // Move all that comes under this 'name' over by one space.
554         return indent + " ";
555     }
556     
557     /***
558      * Class that parses commandline arguments.
559      * Expected format is 'operationName=arg0,arg1,arg2...'. We are assuming no
560      * spaces nor comma's in argument values.
561      */
562     protected class CommandParse {
563         private String cmd;
564         private String [] args;
565         
566         protected CommandParse(String command) throws ParseException {
567             parse(command);
568         }
569         
570         private void parse(String command) throws ParseException {
571             Matcher m = CMD_LINE_ARGS_PATTERN.matcher(command);
572             if (m == null || !m.matches()) {
573                 throw new ParseException("Failed parse of " + command, 0);
574             }
575 
576             this.cmd = m.group(1);
577             if (m.group(2) != null && m.group(2).length() > 0) {
578                 this.args = m.group(2).split(",");
579             } else {
580                 this.args = null;
581             }
582         }
583         
584         protected String getCmd() {
585             return this.cmd;
586         }
587         
588         protected String [] getArgs() {
589             return this.args;
590         }
591     }
592     
593     protected Object doAttributeOperation(MBeanServerConnection mbsc,
594         ObjectInstance instance, String command, MBeanAttributeInfo [] infos)
595     throws Exception {
596         // Usually we get attributes. If an argument, then we're being asked
597         // to set attribute.
598         CommandParse parse = new CommandParse(command);
599         if (parse.getArgs() == null || parse.getArgs().length == 0) {
600             // Special-casing.  If the subCommand is 'Attributes', then return
601             // list of all attributes.
602             if (command.equals("Attributes")) {
603                 String [] names = new String[infos.length];
604                 for (int i = 0; i < infos.length; i++) {
605                     names[i] = infos[i].getName();
606                 }
607                 return mbsc.getAttributes(instance.getObjectName(), names);
608             }
609             return mbsc.getAttribute(instance.getObjectName(), parse.getCmd());
610         }
611         if (parse.getArgs().length != 1) {
612             throw new IllegalArgumentException("One only argument setting " +
613                 "attribute values: " + parse.getArgs());
614         }
615         // Get first attribute of name 'cmd'. Assumption is no method
616         // overrides.  Then, look at the attribute and use its type.
617         MBeanAttributeInfo info =
618             (MBeanAttributeInfo)getFeatureInfo(infos, parse.getCmd());
619         java.lang.reflect.Constructor c = Class.forName(
620              info.getType()).getConstructor(new Class[] {String.class});
621         Attribute a = new Attribute(parse.getCmd(),
622             c.newInstance(new Object[] {parse.getArgs()[0]}));
623         mbsc.setAttribute(instance.getObjectName(), a);
624         return null;
625     }
626 
627     protected Object doBeanOperation(MBeanServerConnection mbsc,
628         ObjectInstance instance, String command, MBeanOperationInfo [] infos)
629     throws Exception {
630         // Parse command line.
631         CommandParse parse = new CommandParse(command);
632         
633         // Get first method of name 'cmd'. Assumption is no method
634         // overrides.  Then, look at the method and use its signature
635         // to make sure client sends over parameters of the correct type.
636         MBeanOperationInfo op =
637             (MBeanOperationInfo)getFeatureInfo(infos, parse.getCmd());
638         Object result = null;
639         if (op == null) {
640             result = "Operation " + parse.getCmd() + " not found.";
641         } else {
642             MBeanParameterInfo [] paraminfos = op.getSignature();
643             int paraminfosLength = (paraminfos == null)? 0: paraminfos.length;
644             int objsLength = (parse.getArgs() == null)?
645                 0: parse.getArgs().length;
646             if (paraminfosLength != objsLength) {
647                 result = "Passed param count does not match signature count";
648             } else {
649                 String [] signature = new String[paraminfosLength];
650                 Object [] params = (paraminfosLength == 0)? null
651                         : new Object[paraminfosLength];
652                 for (int i = 0; i < paraminfosLength; i++) {
653                     MBeanParameterInfo paraminfo = paraminfos[i];
654                     java.lang.reflect.Constructor c = Class.forName(
655                         paraminfo.getType()).getConstructor(
656                             new Class[] {String.class});
657                     params[i] =
658                         c.newInstance(new Object[] {parse.getArgs()[i]});
659                     signature[i] = paraminfo.getType();
660                 }
661                 result = mbsc.invoke(instance.getObjectName(), parse.getCmd(),
662                     params, signature);
663             }
664         }
665         return result;
666     }
667 
668     protected String listOptions(MBeanServerConnection mbsc,
669             ObjectInstance instance)
670     throws InstanceNotFoundException, IntrospectionException,
671             ReflectionException, IOException {
672         StringBuffer result = new StringBuffer();
673         MBeanInfo info = mbsc.getMBeanInfo(instance.getObjectName());
674         MBeanAttributeInfo [] attributes  = info.getAttributes();
675         if (attributes.length > 0) {
676             result.append("Attributes:");
677             result.append("\n");
678             for (int i = 0; i < attributes.length; i++) {
679                 result.append(' ' + attributes[i].getName() +
680                     ": " + attributes[i].getDescription() +
681                     " (type=" + attributes[i].getType() +
682                     ")");
683                 result.append("\n");
684             }
685         }
686         MBeanOperationInfo [] operations = info.getOperations();
687         if (operations.length > 0) {
688             result.append("Operations:");
689             result.append("\n");
690             for (int i = 0; i < operations.length; i++) {
691                 MBeanParameterInfo [] params = operations[i].getSignature();
692                 StringBuffer paramsStrBuffer = new StringBuffer();
693                 if (params != null) {
694                     for (int j = 0; j < params.length; j++) {
695                         paramsStrBuffer.append("\n   name=");
696                         paramsStrBuffer.append(params[j].getName());
697                         paramsStrBuffer.append(" type=");
698                         paramsStrBuffer.append(params[j].getType());
699                         paramsStrBuffer.append(" ");
700                         paramsStrBuffer.append(params[j].getDescription());
701                     }
702                 }
703                 result.append(' ' + operations[i].getName() +              
704                     ": " + operations[i].getDescription() +
705                     "\n  Parameters " + params.length +
706                     ", return type=" + operations[i].getReturnType() +
707                     paramsStrBuffer.toString());
708                 result.append("\n");
709             }
710         }
711         return result.toString();
712     }
713     
714     /***
715      * Logger that writes entry on one line with less verbose date.
716      * Modelled on the OneLineSimpleLogger from Heritrix.
717      * 
718      * @author stack
719      * @version $Revision: 1.15 $, $Date: 2005/10/03 20:07:45 $
720      */
721     private class OneLineSimpleLogger extends SimpleFormatter {
722         /***
723          * Date instance.
724          * 
725          * Keep around instance of date.
726          */
727         private Date date = new Date();
728         
729         /***
730          * Field position instance.
731          * 
732          * Keep around this instance.
733          */
734         private FieldPosition position = new FieldPosition(0);
735         
736         /***
737          * MessageFormatter for date.
738          */
739         private SimpleDateFormat formatter =
740             new SimpleDateFormat("MM/dd/yyyy HH:mm:ss Z");
741         
742         /***
743          * Persistent buffer in which we conjure the log.
744          */
745         private StringBuffer buffer = new StringBuffer();
746         
747 
748         public OneLineSimpleLogger() {
749             super();
750         }
751         
752         public synchronized String format(LogRecord record) {
753             this.buffer.setLength(0);
754             this.date.setTime(record.getMillis());
755             this.position.setBeginIndex(0);
756             this.formatter.format(this.date, this.buffer, this.position);
757             this.buffer.append(' ');
758             if (record.getSourceClassName() != null) {
759                 this.buffer.append(record.getSourceClassName());
760             } else {
761                 this.buffer.append(record.getLoggerName());
762             }
763             this.buffer.append(' ');
764             this.buffer.append(formatMessage(record));
765             this.buffer.append(System.getProperty("line.separator"));
766             if (record.getThrown() != null) {
767                 try {
768                     StringWriter writer = new StringWriter();
769                     PrintWriter printer = new PrintWriter(writer);
770                     record.getThrown().printStackTrace(printer);
771                     writer.close();
772                     this.buffer.append(writer.toString());
773                 } catch (Exception e) {
774                     this.buffer.append("Failed to get stack trace: " +
775                         e.getMessage());
776                 }
777             }
778             return this.buffer.toString();
779         }
780     }
781 }