View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership. The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License. You may obtain a copy of the License at
9    * 
10   *      http://www.apache.org/licenses/LICENSE-2.0
11   * 
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.river.tool.envcheck;
19  
20  import org.apache.river.resource.Service;
21  
22  import java.io.ByteArrayOutputStream;
23  import java.io.File;
24  import java.io.InputStream;
25  import java.io.IOException;
26  import java.io.ObjectInputStream;
27  import java.io.PrintStream;
28  
29  import java.lang.reflect.Modifier;
30  
31  import java.net.MalformedURLException;
32  import java.net.URL;
33  import java.net.URLClassLoader;
34  
35  import java.util.ArrayList;
36  import java.util.Enumeration;
37  import java.util.Iterator;
38  import java.util.Properties;
39  import java.util.ResourceBundle;
40  
41  import net.jini.config.Configuration;
42  import net.jini.config.ConfigurationException;
43  import net.jini.config.ConfigurationProvider;
44  
45  import org.apache.river.start.ServiceDescriptor;
46  import org.apache.river.start.NonActivatableServiceDescriptor;
47  import org.apache.river.start.SharedActivatableServiceDescriptor;
48  import org.apache.river.start.SharedActivationGroupDescriptor;
49  
50  import org.apache.river.tool.envcheck.Reporter.Message;
51  import org.apache.river.api.net.RFC3986URLClassLoader;
52  
53  /**
54   * Tool used to perform validity checks on the run-time environment of a client
55   * or service. The output of this tool is a report; command-line options
56   * control the verbosity and severity level at which report entries are
57   * generated. A simple plugin architecture is implemented; a set of plugins
58   * implementing a variety of checks is bundled with the tool, and support is
59   * provided to allow additional plugins to be supplied by the user.
60   * <p>
61   * The following items are discussed below:
62   * <ul>
63   * <li><a href="#running">Running the Tool</a>
64   * <li><a href="#processing">Processing Options</a>
65   * <li><a href="#examples">Examples</a>
66   * <li><a href="#plugins">Bundled Plugins</a>
67   * </ul>
68   *
69   * <a name="running"></a>
70   * <h3>Running the Tool</h3>
71   *
72   * This tool primarily validates the system properties and configuration
73   * files used when starting the target client or service. This is accomplished
74   * by providing the unmodified command line for launching the component as
75   * arguments to the tool. Thus, for a service designed to be run by the
76   * service starter having the hypothetical original command line:
77   * <blockquote><pre>
78   * java -Djava.security.policy=<var><b>my_policy</b></var> \
79   *      -jar <var><b>install_dir</b></var>/lib/start.jar mystart.config
80   * </pre></blockquote>
81   * the simplest invocation of the tool would be:
82   * <blockquote><pre>
83   * java -jar <var><b>install_dir</b></var>/lib/envcheck.jar \
84   *      java -Djava.security.policy=<var><b>my_policy</b></var> \
85   *           -jar <var><b>install_dir</b></var>/lib/start.jar mystart.config
86   * </pre></blockquote>
87   * Note that the entire command line, including the <code>java</code> command,
88   * is supplied as arguments to the tool. The <code>java</code> command used to
89   * run the tool may be different than the <code>java</code> command invoked by
90   * the command line under test. The first token in the command line being
91   * analyzed must not begin with a '-' and must end with the string "java".
92   *
93   * <a name="processing"></a>
94   * <h3>Processing Options</h3>
95   * <p>
96   * <dl>
97   * <dt><b><code>-traces</code></b>
98   * <dd>The implementation of a validity check may detect success or failure by
99   *     handling an expected exception. By default, an error message will be
100  *     generated in these cases, but the stack trace will be inhibited. This
101  *     option is a hint that stack traces are desired. It is the responsibility
102  *     of the individual plugin implementation to honor this option.
103  * </dd>
104  * 
105  * <dt><b><code>-explain</code></b>
106  * <dd>By default, the output of a validity check will be a short message with
107  *     enough detail to allow a knowledgeable user to interpret it; however, it
108  *     may not be understandable to a novice user. The <code>-explain</code>
109  *     option is a hint that may result in the generation of additional output
110  *     describing the purpose and context of the check. An explanation is output
111  *     the first time its associated message is output, and is not repeated. It
112  *     is the responsibility of the individual plugin implementation to honor
113  *     this option.
114  * </dd>
115  * 
116  * <dt><b><code>-level</code> <var>info|warning|error</var></b>
117  * <dd>The tool supports three severity levels for message generation. 
118  * <p>
119  *     <dl>
120  *     <dt><var>info</var>
121  *     <dd>'success' messages or other non-failure oriented configuration data
122  *     <dt><var>warning</var>
123  *     <dd>a condition or value has been detected that is 'legal', but which
124  *         could result in unintended behavior. An example is the use of
125  *         <code>localhost</code> in a codebase annotation.
126  *     <dt><var>error</var>
127  *     <dd>a condition has been detected that is likely due to an error
128  *         on the command line or in a configuration file. An example is
129  *         assigning the value of a non-existant file name to the
130  *         <code>java.util.logging.config.file</code> system property.
131  *     </dl>
132  * <p>
133  *     This option is used to set the level at which message records are
134  *     generated. The default value is <var>warning</var>.
135  * </dd>
136  * 
137  * <dt><b><code>-plugin</code> <var>file</var></b>
138 
139  * <dd>Identifies a JAR file containing user supplied plugins that will be run
140  *     after the standard plugins are run. All of the necessary support classes
141  *     and resources required to support the plugins must be included in this
142  *     file. The file must also include a resource named
143  *     <code>META-INF/services/org.apache.river.tool.envcheck.Plugin</code>, which
144  *     contains the class names of the plugins to run listed one per line.
145  *     Every class listed must implement the
146  *     <code>org.apache.river.tool.envcheck.Plugin</code> interface. This option
147  *     may be supplied zero or more times.
148  * </dd>
149  * 
150  * <dt><b><code>-security</code></b>
151  * <dd>A plugin specific option that is recognized by one of the bundled
152  *     plugins.  Specifying this option will activate a number of JAAS and JSSE
153  *     checks. User supplied plugins wishing to recognize this option must
154  *     implement the <code>isPlugOption</code> method of the 
155  *     <code>Plugin</code> interface.
156  * </dd>
157  * </dl>
158  * <p>
159  * <a name="examples"></a>
160  * <h3>Examples</h3>
161 
162  * The following example will analyze a command line used to start a user
163  * service. The command being analyzed defines a classpath and two system
164  * properties, and names a class containing a <code>main</code> method:
165 
166  * <p>
167  * <blockquote><pre>
168  * java -jar <var><b>install_dir</b></var>/lib/envcheck.jar -level info -explain -traces \
169  *      java -cp <var><b>mylib_dir</b></var>/myservice.jar:/jini/lib/jsk-platform.jar \
170  *           -Djava.security.policy=<var><b>my_policy</b></var> \
171  *           -Djava.server.rmi.codebase=http://myhost/myservice-dl.jar \
172  *           myservice.MyServiceImpl
173  * </pre></blockquote>
174  * In this case, the tool is limited to performing validity checks on the
175  * classpath, policy, and codebase values identified by the system properties
176  * and options provided on the service command line. The <code>-level</code>,
177  * <code>-explain</code>, and <code>-traces</code> options supplied will result
178  * in the most verbose output possible from the tool.
179  * <p>
180  * The following example will analyze a command line used to start reggie using
181  * the service starter. The command being analyzed uses a policy and service
182  * starter configuration located in the working directory:
183  * <p>
184  * <blockquote><pre>
185  * java -jar <var><b>install_dir</b></var>/lib/envcheck.jar -level error \
186  *      java -Djava.security.policy=<var><b>my_starterpolicy</b></var> \
187  *           -jar <var><b>install_dir</b></var>/lib/start.jar reggie.config
188  * </pre></blockquote>
189  * The tool can perform many more checks in this case because the
190  * bundled plugins include built-in knowledge about the service starter
191  * and its public configuration entries. The tool options used will minimize
192  * the output produced by the tool.
193  * <p>
194  * <a name="plugins"></a>
195  * <h3>Bundled Plugins</h3>
196  * A set of plugins are loaded automatically by the tool to perform some
197  * basic analysis. 
198  * <p>
199  * If the command line being analyzed invokes the service starter, the tool will
200  * create a <code>Configuration</code> from the arguments of the command line
201  * being analyzed. Failure to create the <code>Configuration</code> will result
202  * in termination of the tool, otherwise the
203  * <code>org.apache.river.start.ServiceDescriptor</code>s provided by the
204  * <code>Configuration</code> are examined. The following checks are done for
205  * each <code>ServiceDescriptor</code>:
206  * <ul>
207  *  <li>Verify that the <code>getPolicy</code> method returns a reference to a
208  *      policy file that is valid and accessible
209  *  <li>Check whether that policy grants <code>AllPermissions</code> to all
210  *      protection domains
211  * </ul>
212  * The following checks are done for each
213  * <code>NonActivatableServiceDescriptor</code> and each
214  * <code>SharedActivatableServiceDescriptor</code>
215  * <ul>
216  *  <li>Verify that calling <code>getServerConfigArgs</code> does not return
217  *      <code>null</code> or an empty array
218  *  <li>Verify that a <code>Configuration</code> can be constructed from those
219  *      args
220  *  <li>Verify that any entry in that <code>Configuration</code> named
221  *      <code>initialLookupGroups</code> does not have a value of
222  *      <code>ALL_GROUPS</code>
223  *  <li>Verify that the export codebase is defined. For each component in the
224  *      export codebase:
225  *   <ul>
226  *     <li>Verify that the URL is not malformed
227  *     <li>If it is an HTTPMD URL, verify that the necessary protocol handler
228  *         is installed
229  *     <li>Check that domain names are fully qualified
230  *     <li>Warn if an md5 HTTPMD URL is being used (md5 has a security hole)
231  *     <li>Verify that the host name in the URL can be resolved
232  *     <li>Verify that the host name does not resolve to a loopback address
233  *     <li>Verify that it's possible to open a connection using the URL
234  *   </ul>
235  *   <li>If the <code>-security</code> option was specified:
236  *     <ul>
237  *        <li>Verify that <code>javax.net.ssl.trustStore</code> is defined and
238  *            that the trust store it references is accessible
239  *        <li>Check whether <code>org.apache.river.discovery.x500.trustStore</code>
240  *            is defined, and if so that the trust store it references is 
241  *            accessible
242  *        <li>Check whether <code>javax.net.ssl.keyStore</code> is defined, and
243  *            if so that the key store it references is accessible
244  *        <li>Verify that a login configuration is defined and that it is 
245  *            accessible and syntactically correct
246  *      </ul>
247  * </ul>
248  * The following checks are done for each 
249  * <code>SharedActivatableServiceDescriptor</code>:
250  * <ul>
251  *   <li>Verify that any entry in that <code>Configuration</code> named
252  *       <code>persistenceDirectory</code> refers to either an empty directory 
253  *       or a non-existant directory
254  * </ul>
255  * The following checks are done for each 
256  * <code>SharedActivationGroupDescriptor</code>:
257  * <ul>
258  *   <li>Verify that the activation system is running
259  *   <li>Verify that the virtual machine (VM) created by that command is at 
260  *       least version 1.4
261  *   <li>Verify that <code>jsk-policy.jar</code> is loaded from the extensions
262  *       directory
263  *     
264  *   <li>Verify that <code>jsk-platform.jar</code> is in the classpath 
265  *
266  * 
267  *   <li>If <code>java.util.logging.config.file</code> is defined in the
268  *       properties returned by calling <code>getServerProperties</code> then 
269  *       verify that the file it references is accessible
270  * </ul>
271  *  A subset of these checks are performed on the command line being analyzed:
272  *  <ul>
273  *    <li>Codebase/URL checks based on the value of
274  *        <code>java.rmi.server.codebase</code>
275  *    <li>Policy file checks based on the value of
276  *        <code>java.security.policy</code>
277  *    <li>Check for <code>jsk-policy</code> being loaded by the extension
278  *        class loader
279  *    <li>Check for <code>jsk-platform</code> in the classpath 
280  *    <li>Security checks if <code>-security</code> was specified
281  *    <li>The logging config file check
282  * </ul>
283  * In all cases, check that the local host name does not resolve to the 
284  * loopback address.
285  */
286 
287 public class EnvCheck {
288 
289     /** the list of plugins instances to run */
290     private ArrayList pluginList = new ArrayList();
291 
292     /** flag controlling the display of stack traces in the output */
293     private boolean printStackTraces = false;
294 
295     /** the command line arguments of the command being analyzed */
296     private String[] args;
297 
298     /** the <code>ServiceDescriptor</code>s obtained from the starter config */
299     private ServiceDescriptor[] descriptors = new  ServiceDescriptor[0];
300 
301     /** the localization resource bundle */
302     private static ResourceBundle bundle;
303 
304     /** the java command on the command line being checked */
305     private String javaCmd;
306 
307     /** the options on the command line being checked (never null) */
308     String[] options = new String[0];
309 
310     /** the properties on the command line being checked (never null) */
311     Properties properties = new Properties();
312 
313     /** the classpath on the command line being checked */
314     String classpath = null;
315 
316     /** the main class on the command line being checked */
317     String mainClass = null;
318 
319     /** the executable JAR file on the command line being checked */
320     String jarToRun = null;
321 
322     /** the list of plugins supplied via the -plugin option */
323     static ArrayList pluginJarList = new ArrayList();
324 
325     /** the class loader for loading plugins */
326     ClassLoader pluginLoader = EnvCheck.class.getClassLoader();
327 
328     /** the classpath of the tool, including the plugins (updated in run) */
329     static String combinedClasspath = System.getProperty("java.class.path");
330 
331     /**
332      * The entry point for the tool. The localization resource bundle is
333      * located, the plugins are loaded, and the checks are performed. The system
334      * property <code>java.protocol.handler.pkgs</code> for the tool VM is set
335      * to <code>net.jini.url</code> to ensure that the tool can manipulate
336      * HTTPMD URLs.
337      *
338      * @param args the command line arguments
339      */
340     public static void main(String[] args) {
341 	System.setProperty("java.protocol.handler.pkgs", "net.jini.url");
342 	bundle = Util.getResourceBundle(EnvCheck.class);
343 	if (args.length == 0) {
344 	    usage();
345 	    System.exit(1);
346 	}
347         findPlugins(args);
348 	new EnvCheck().run(args);
349     }
350 
351     /** 
352      * Output the usage message.
353      */
354     private static void usage(){
355 	System.err.println(Util.getString("envcheck.usage", bundle));
356     }
357 
358     /**
359      * Helper to print a localized string
360      *
361      * @param key the resource key
362      */
363     private String getString(String key) {
364 	return Util.getString(key, bundle);
365     }
366 
367     /**
368      * Helper to print a localized string
369      *
370      * @param key the resource key
371      * @param val the value parameter expected by the message
372      */
373     private String getString(String key, String val) {
374 	return Util.getString(key, bundle, val);
375     }
376 
377     /**
378      * Search the command line for user supplied plugin definitions
379      * and place them in the internal plugin list.
380      *
381      * @param cmdLine the original command line args
382      */
383     private static void findPlugins(String[] cmdLine) {
384 	int index = 0;
385 	String arg;
386 	while ((arg = cmdLine[index++]).startsWith("-")) {
387 	    if (arg.equals("-plugin")) {
388 		String pluginName = cmdLine[index++];
389 		if (!(new File(pluginName).exists())) {
390 		    System.err.println(Util.getString("envcheck.noplugin", 
391 				       bundle,
392 				       pluginName));
393 		    System.exit(1);
394 		}
395 		combinedClasspath += File.pathSeparator + pluginName;
396 		try {
397 		    pluginJarList.add(new URL("file:" + pluginName));
398 		} catch (MalformedURLException e) { // should never happen
399 		    e.printStackTrace();
400 		    System.exit(1);
401 		}
402 	    } else if (arg.equals("-level")) {
403 		index++;
404 	    }
405 	}
406     }
407 
408     /**
409      * Parse the command line, identifying options for the tool and parsing
410      * the VM, system properties, options, JAR/main class and arguments
411      * for the command line being tested. The <code>-plugin</code> option
412      * is ignored by this parser since they must have been processed
413      * earlier.
414      *
415      * @param cmdLine the original command line arguments
416      */
417     private void parseArgs(String[] cmdLine) {
418 	int index = 0;
419 	try {
420 	    while (true) {
421 		String arg = cmdLine[index++];
422 		if (arg.equals("-traces")) {
423 		    printStackTraces = true;
424 		    Reporter.setPrintTraces(true);
425 		} else if (arg.equals("-explain")) {
426 		    Reporter.setExplanation(true);
427 		} else if (arg.equals("-level")) {
428 		    String val = cmdLine[index++];
429 		    if (val.equals("info")) {
430 			Reporter.setLevel(Reporter.INFO);
431 		    } else if (val.equals("warning")) {
432 			Reporter.setLevel(Reporter.WARNING);
433 		    } else if (val.equals("error")) {
434 			Reporter.setLevel(Reporter.ERROR);
435 		    } else {
436 			System.err.println(getString("envcheck.badlevel"));
437 			usage();
438 			System.exit(1);
439 		    }
440 	        } else if (arg.equals("-plugin")) {
441 		    index++; // already processed the plugin
442 		} else if (isPluginOption(arg)) { //no additional work to do
443 		} else if (arg.startsWith("-")) {
444 		    System.err.println(getString("envcheck.illegalopt", arg));
445 		    usage();
446 		    System.exit(1);
447 		} else if (arg.endsWith("java")) {
448 		    javaCmd = arg;
449 		    break;
450 		} else {
451 		    System.err.println(getString("envcheck.nojavacmd"));
452 		    usage();
453 		    System.exit(1);
454 		}
455 	    }
456 	} catch (ArrayIndexOutOfBoundsException e) {
457 	    System.err.println(getString("envcheck.nocmdargs"));
458 	    usage();
459 	    System.exit(1);
460 	}
461 	try {
462 	    ArrayList optList = new ArrayList();
463 	    while (true) {
464 		String opt = cmdLine[index++];
465 		if (!opt.startsWith("-")) {
466 		    mainClass = opt;
467 		    break;
468 		} else if (opt.startsWith("-D")) {
469 		    int valueIndex = opt.indexOf("=");
470 		    String key = opt.substring(2, valueIndex);
471 		    String value = opt.substring(valueIndex + 1);
472 		    properties.put(key, value);
473 		} else if (opt.equals("-cp") || opt.equals("-classpath")) {
474 		    classpath = cmdLine[index++];
475 		} else if (opt.equals("-jar")) {
476 		    jarToRun = cmdLine[index++];
477 		    break;
478 		} else {
479 		    optList.add(opt);
480 		}
481 	    }
482 	    options = 
483 		(String[])  optList.toArray(new String[optList.size()]);
484 	} catch (ArrayIndexOutOfBoundsException e) {
485 	    System.err.println(getString("noexecutable"));
486 	    usage();
487 	    System.exit(1);
488 	}
489 	ArrayList argList = new ArrayList();
490 	while (index < cmdLine.length) {
491 	    argList.add(cmdLine[index++]);
492 	}
493 	args = (String[]) argList.toArray(new String[argList.size()]);
494     }
495 	    
496     /**
497      * Return the <code>ServiceDescriptor</code>s contained in the service
498      * starter configuration. If the command being analyzed does not invoke the
499      * service starter, a zero-length array will be returned.
500      * 
501      * @return the descriptors in the starter configuration or an empty array
502      */
503     public ServiceDescriptor[] getDescriptors() {
504 	return descriptors;
505     }
506 
507     /**
508      * Return the <code>SharedActivationGroupDescriptor</code> contained in the
509      * service starter configuration. Returns <code>null</code> if there is no
510      * such descriptor, or if the command being analyzed does not invoke the
511      * service starter.
512      *
513      * @return the <code>SharedActivationGroupDescriptor</code> or 
514      *         <code>null</code>
515      */
516     public SharedActivationGroupDescriptor getGroupDescriptor() {
517 	for (int i = 0; i < descriptors.length; i++) {
518 	    if (descriptors[i] instanceof SharedActivationGroupDescriptor) {
519 		return (SharedActivationGroupDescriptor) descriptors[i];
520 	    }
521 	}
522 	return null;
523     }
524 
525     /**
526      * Perform the runtime checks. If any user plugins were supplied, construct
527      * a class loader capable of loading them and modify
528      * <code>combinedClasspath</code> to make the classes available to subtasks.
529      * Load the plugins and parse the command line. The plugins must be
530      * available to the parser so that plugin specific options can be checked
531      * for. Load the service starter configuration if the service starter is
532      * being invoked. Execute all of the plugin classes in the order loaded.
533      *
534      * @param cmdLine the original command line arguments
535      */
536     private void run(String[] cmdLine) {
537 	if (pluginJarList.size() > 0) {
538 	    URL[] urls = 
539 		(URL[]) pluginJarList.toArray(new URL[pluginJarList.size()]);
540 	    pluginLoader = new RFC3986URLClassLoader(urls, pluginLoader);
541 	}
542 	loadPlugins();
543 	parseArgs(cmdLine);
544 	if (jarToRun == null && classpath == null) {
545 	    System.err.println(getString("envcheck.missingclasspath"));
546 	    System.exit(1);
547 	}
548 	loadConfiguration();
549 	Iterator plugins = pluginList.iterator();
550 	while (plugins.hasNext()) {
551 	    Plugin plugin = (Plugin) plugins.next();
552 	    try {
553 		plugin.run(this);
554 	    } catch (SecurityException e) { // limp on if permission check fails
555 		e.printStackTrace();
556 	    }
557 	}
558 	int warningCount = Reporter.getWarningCount();
559 	int errorCount = Reporter.getErrorCount();
560 	if ((warningCount + errorCount) > 0) {
561 	    System.out.println();
562 	    System.out.println();
563 	    System.out.println(getString("envcheck.summaryseparator"));
564 	}
565 	if (warningCount > 0) {
566 	    System.out.println(getString("envcheck.warningheader")
567 			       + warningCount);
568 	}
569 	if (errorCount > 0) {
570 	    System.out.println(getString("envcheck.errorheader") + errorCount);
571 	}
572 	    
573     }
574 
575     /**
576      * Check whether <code>arg</code> is a plugin specific option. The
577      * <code>isPluginOption</code> method of every plugin is called in
578      * case an option is shared among multiple plugins. 
579      *
580      * @param arg the argument to check
581      * @return <code>true</code> if any plugin's <code>isPluginOption</code> 
582      *         method returns <code>true</code>
583      */
584     private boolean isPluginOption(String arg) {
585 	boolean gotOne = false;
586 	Iterator plugins = pluginList.iterator();
587 	while (plugins.hasNext()) {
588 	    Plugin plugin = (Plugin) plugins.next();
589 	    if (plugin.isPluginOption(arg)) {
590 		gotOne = true;
591 	    }
592 	}
593 	return gotOne;
594     }
595 	    
596     /**
597      * Instantiate the service starter configuration if the command line being
598      * analyzed runs the service starter.  This is assumed to be true if the
599      * command line specified an executable JAR file named <code>start.jar</code> and
600      * if that file resides in the same directory as a file named
601      * <code>jsk-platform.jar</code>. Extract and save all of the service
602      * descriptors and service destructors from the configuration. If a
603      * <code>ConfigurationException</code> is thrown the tool will exit. If
604      * there are no <code>ServiceDescriptor</code> or
605      * <code>ServiceDestructor</code> entries supplied by the configuration a
606      * error is generated and a normal return is done. The instantiation of
607      * the configuration is done in a child process using the VM, properties,
608      * options and arguments supplied in the command line under test.
609      */
610     private void loadConfiguration() {
611 	if (jarToRun != null) {
612 	    if (!jarToRun.endsWith("start.jar")) {
613 		return;
614 	    }
615 	    //XXX do existence check in parser
616 	    File starterFile = new File(jarToRun);
617 	    File jskLibDir = starterFile.getParentFile();
618 	    File jskplatform = new File(jskLibDir, "jsk-platform.jar");
619 	    if (!jskplatform.exists()) {
620 		return; // presumably a user file coincidentally named start.jar
621 	    }
622 	} else {
623 	    if (!mainClass.equals("org.apache.river.start.ServiceStarter")) {
624 		return;
625 	    }
626 	}
627 	//XXX need to check validity of javaCmd and arg list length
628 	Object lobj = 
629 	    launch("org.apache.river.tool.envcheck.EnvCheck$GetDescriptors", args);
630 	if (lobj instanceof ServiceDescriptor[]) {
631 	    descriptors = (ServiceDescriptor[]) lobj;
632 	    if (descriptors.length == 0) {
633 		Reporter.print(new Message(Reporter.ERROR, 
634 					   getString("envcheck.emptyconfig"), 
635 					   null));
636 	    }
637 	} else if (lobj instanceof ConfigurationException) {
638 	    System.err.println(getString("envcheck.ssdescfailed"));
639 	    ((Throwable) lobj).printStackTrace();
640 	    System.exit(1);
641 	} else {
642 	    System.err.println(getString("envcheck.subtaskex", lobj.toString()));
643 	    ((Throwable) lobj).printStackTrace();
644 	    System.exit(1);
645 	}
646     }
647 
648     /**
649      * Load the plugin classes. All of the resources named
650      * <code>META-INF/services/org.apache.river.tool.envcheck.Plugin</code> are
651      * read; each such resource is expected to contain a list of plugin class
652      * names, one per line (white space is trimmed). The corresponding classes
653      * are loaded and saved. Any other exceptions thrown while processing
654      * the plugin JAR files will cause the tool to exit.
655      */
656     private void loadPlugins() {
657 	Package pkg = getClass().getPackage();
658 	String pkgName = "";
659 	if (pkg != null) {
660 	    pkgName = pkg.getName();
661 	}
662 	pkgName = pkgName.replace('.', '/');
663 	if (pkgName.length() > 0) {
664 	    pkgName += "/";
665 	}
666 	Iterator plugins = 
667 	    Service.providers(org.apache.river.tool.envcheck.Plugin.class,
668 			      pluginLoader);
669 	while (plugins.hasNext()) {
670 	    pluginList.add(plugins.next());
671 	}
672     }
673 
674     /**
675      * Get the command line arguments of the command being analyzed.
676      *
677      * @return the args
678      */
679     // unused, but supplied to support user developed plugins
680     public String[] getArgs() {
681 	return args;
682     }
683 
684     /**
685      * Return the flag indicating whether to output stack traces that
686      * result from a check.
687      */
688     public boolean printStacks() {
689 	return printStackTraces;
690     }
691 
692     /**
693      * Launch a child VM using the <code>java</code> command, properties, and
694      * options supplied on the command line being analyzed. If an executable JAR
695      * file was specified, the classpath of the child VM consists of the JAR
696      * file name augmented with the classpath of the tool and plugins. If a main
697      * class was specified, the classpath of the child VM consists of the
698      * <code>-cp/-classpath</code> option value of the command line being
699      * analyzed augmented with the classpath of the tool and plugins.
700      *
701      * @param task the class name of the task to launch, which must implement
702      *             the <code>SubVMTask</code> interface
703      * @param args the arguments to pass to the main method of the task
704      * @return the result or exception returned by the subtask supplied as a
705      *         serialized object written on the subtask's
706      *         <code>System.out</code> stream.
707      */
708     public Object launch(String task, String[] args) {
709 	String cp;
710 	if (jarToRun != null) {
711 	    cp = jarToRun + File.pathSeparator;
712 	} else {
713 	    cp = classpath + File.pathSeparator;
714 	}
715 	cp += combinedClasspath;
716 	String[] opts = new String[options.length + 2];
717 	opts[0] = "-cp";
718 	opts[1] = cp;
719 	System.arraycopy(options, 0, opts, 2, options.length);
720 	if (args == null) {
721 	    args = new String[0];
722 	}
723 	String[] subvmArgs = new String[args.length + 1];
724 	subvmArgs[0] = task;
725 	System.arraycopy(args, 0, subvmArgs, 1, args.length);
726 	return launch(javaCmd, properties, opts, subvmArgs);
727     }
728 
729     /**
730      * Launch a child VM using the <code>java</code> command, properties, and
731      * options supplied on the command line. If an executable JAR file was
732      * specified, the classpath of the child VM consists of the JAR file name
733      * augmented with the classpath of the tool and plugins. If a main class was
734      * specified, the classpath of the child VM consists of the
735      * <code>-cp/-classpath</code> option value of the command line being
736      * analyzed augmented with the classpath of the tool and plugins.
737      *
738      * @param task the class name of the task to launch, which must implement
739      *             the <code>SubVMTask</code> interface
740      * @return the result or exception returned by the subtask supplied as a
741      *         serialized object written on the subtask's
742      *         <code>System.out</code> stream.
743      */
744     public Object launch(String task) {
745 	return launch(task, null);
746     }
747 
748     /**
749      * Return a property value that was specified on the command line being
750      * analyzed. Only properties explicitly defined on the command line will
751      * resolve to a value.
752      *
753      * @param key the name of the property
754      * @return the property value, or <code>null</code> if undefined
755      */
756     public String getProperty(String key) {
757 	return properties.getProperty(key);
758     }
759 
760     /**
761      * Return a copy of the properties that were specified on the
762      * command line being analyzed. The caller may modify the returned
763      * properties object.
764      * 
765      * @return the properties, which may be empty
766      */
767     public Properties getProperties() {
768 	return (Properties) properties.clone();
769     }
770 
771     /**
772      * Launch a subtask VM using the <code>java</code> command given by
773      * <code>javaCmd</code>.  If <code>javaCmd</code> is <code>null</code>, the
774      * command to run is derived from the value of the <code>java.home</code>
775      * property of the tool VM.  The first value in <code>args</code> must name
776      * a class that implements the <code>SubVMTask</code> interface. The
777      * <code>props</code> and <code>opts</code> arrays must contain fully
778      * formatted command line values for properties and options
779      * (i.e. "-Dfoo=bar" or "-opt"). <code>opts</code> must include a
780      * <code>-cp</code> or <code>-classpath</code> option and its value must
781      * completely specify the classpath required to run the subtask.
782      *
783      * @param javaCmd the <code>java</code> command to execute, or
784      *                <code>null</code> to create another instance of the
785      *                tool VM
786      * @param props   properties to define, which may be <code>null</code> or
787      *                empty
788      * @param opts    options to define, which must include a classpath 
789      *                definition
790      * @param args    arguments to pass to the child VM
791      * @return        the result or exception returned by the subtask supplied
792      *                as a serialized object written on the subtask's
793      *                <code>System.out</code> stream.
794      *
795      * @throws IllegalArgumentException if <code>opts</code> does not
796      *         include a classpath definition, or if <code>args[0]</code>
797      *         does not contain the name of a class that implements 
798      *         <code>SubVMTask</code>
799      */
800     public Object launch(String javaCmd, 
801 			 Properties props, 
802 			 String[] opts,
803 			 String[] args)
804     {
805 	if (args == null || args.length == 0) {
806 	    throw new IllegalArgumentException("No class name in args[0]");
807 	}
808 	// make sure subtask is valid to avoid obscure failures at exec time
809 	String taskName = args[0];
810 	try {
811 	    Class taskClass = Class.forName(taskName, true, pluginLoader);
812 	    if (!SubVMTask.class.isAssignableFrom(taskClass)) {
813 		throw new IllegalArgumentException(taskName 
814 						   + " does not implement "
815 						   + "SubVMTask");
816 	    }
817 	    // make sure inner classes are static
818 	    if (taskClass.getDeclaringClass() != null) {
819 		if ((taskClass.getModifiers() & Modifier.STATIC) == 0) {
820 		    throw new IllegalArgumentException(taskName
821 						   + " must be a static class");
822 		}
823 	    }
824 	} catch (ClassNotFoundException e) {
825 	    throw new IllegalArgumentException("Class not found: " + taskName);
826 	}
827 	if (javaCmd == null) {
828 	    javaCmd = System.getProperty("java.home");
829 	    javaCmd += File.separator + "bin" + File.separator + "java";
830 	}
831 	ArrayList cmdList = new ArrayList();
832 	cmdList.add(javaCmd);
833 	boolean gotClasspath = false;
834 	if (opts != null) {
835 	    for (int i = 0; i < opts.length; i++) {
836 		if (opts[i].equals("-cp") || opts[i].equals("-classpath")) 
837 		    if (i + 1 == opts.length) {
838 			throw new IllegalArgumentException("classpath option "
839 							   + "does not have "
840 							   + "a value");
841 		    }
842 		gotClasspath = true;
843 		cmdList.add(opts[i]);
844 	    }
845 	}
846 	if (!gotClasspath) {
847 	    throw new IllegalArgumentException("no classpath defined");
848 	}
849 	if (props != null) {
850 	    Enumeration en = props.propertyNames();
851 	    while (en.hasMoreElements()) {
852 		String key = (String) en.nextElement();
853 		String value = props.getProperty(key);
854 		cmdList.add("-D" + key + "=" + value);
855 	    }
856 	}
857 	cmdList.add("org.apache.river.tool.envcheck.SubVM");
858 	for (int i = 0; i < args.length; i++) {
859 	    cmdList.add(args[i]);
860 	}
861 	try {
862 	    String[] argList = 
863 		(String[]) cmdList.toArray(new String[cmdList.size()]);
864 //  	    for (int i = 0; i < argList.length; i++) {
865 //  		System.out.print(argList[i] + " ");
866 //  	    }
867 //  	    System.out.println();
868 	    Process p = Runtime.getRuntime().exec(argList);
869 	    Pipe pipe = new Pipe("errorPipe", 
870 				 p.getErrorStream(), 
871 				 System.err, 
872 				 "Child VM: ");
873 	    ObjectInputStream s = new ObjectInputStream(p.getInputStream());
874 	    Object o = s.readObject();
875 	    pipe.waitTillEmpty(5000);
876 	    return o;
877 	} catch (Exception e) {
878 	    return e;
879 	}
880     }
881 
882     /**
883      * Launch a subtask using the environment defined by the given service
884      * descriptors. Calling this method is equivalent to calling
885      * <code>launch(d, g, taskName, null)</code>.
886      *
887      * @param d the services descriptor, which may be <code>null</code>
888      * @param gd the group descriptor, which may be <code>null</code>
889      * @param taskName the name of the subtask to run
890      * @return the result or exception returned by the subtask supplied as a
891      *         serialized object written on the subtask's
892      *         <code>System.out</code> stream.
893      */
894     public Object launch(NonActivatableServiceDescriptor d, 
895 			 SharedActivationGroupDescriptor gd,
896 			 String taskName)
897     {
898 	return launch(d, gd, taskName, null);
899     }
900 
901     /**
902      * Launch a subtask using the environment defined by the given service
903      * descriptors. 
904      * <p>
905      * If <code>d</code> and <code>gd</code> are both <code>null</code>, then
906      * calling this method is equivalent to calling
907      * <code>launch(taskName, args)</code>.
908      * <p>
909 
910      * If <code>d</code> is <code>null</code> and <code>gd</code> is
911      * non-<code>null</code> then the properties are taken from
912      * <code>gd.getServerProperties()</code> and the
913      * <code>java.security.policy</code> property is added or replaced with the
914      * value of <code>gd.getPolicy()</code>.  The options are taken from
915      * <code>gd.getServerOptions()</code>, but any <code>-cp/-classpath</code>
916      * option is discarded; a <code>-cp</code> option is added that is the value
917      * of <code>gd.getClasspath()</code> augmented with the classpath of the
918      * tool and plugins.  If <code>gd.getServerCommand()</code> is
919      * non-<code>null</code>, its value is used to invoke the child VM;
920      * otherwise the <code>java</code> command of the command line being
921      * analyzed is used. The arguments passed to the child VM consist of an
922      * array whose first element is <code>taskName</code> and whose remaining
923      * elements are taken from <code>args</code>.
924 
925      * <p>
926 
927      * If <code>d</code> is not <code>null</code>, but <code>gd</code> is
928      * <code>null</code>, then if <code>d</code> is an instance of
929      * <code>SharedActivatableServiceDescriptor</code> an
930      * <code>IllegalArgumentException</code> is thrown. Otherwise the properties
931      * and options are taken from the command line being analyzed. The
932      * <code>java.security.policy</code> property is added or replaced using the
933      * value of <code>d.getPolicy()</code>.  The <code>-cp/-classpath</code>
934      * option is replaced with the value of <code>d.getImportCodebase()</code>
935      * augmented with the classpath of the tool and plugins.  The arguments
936      * passed to the child VM consist of <code>taskName</code> followed by
937      * <code>args</code> if <code>args</code> is non-<code>null</code>, or
938      * followed by <code>d.getServerConfigArgs()</code> otherwise.  The VM is
939      * invoked using the <code>java</code> command of the command line being
940      * analyzed.
941 
942      * <p>
943 
944      * if <code>d</code> and <code>gd</code> are both non-<code>null</code> then
945      * if <code>d</code> is an instance of
946      * <code>SharedActivatableServiceDescriptor</code> then the properties,
947      * options, and <code>java</code> command are taken from
948      * <code>gd.getServerProperties()</code>,
949      * <code>gd.getServerOptions()</code>, and
950      * <code>gd.getServerCommand()</code>; however, if the value of
951      * <code>gd.getServerCommand()</code> is <code>null</code>, the
952      * <code>java</code> command is taken from the command line being
953      * analysed. If <code>d</code> is not an instance of
954      * <code>SharedActivatableServiceDescriptor</code> then the properties,
955      * options, and <code>java</code> command are taken from the command line
956      * being analyzed.  In all cases the <code>java.security.policy</code>
957      * property is added or replaced using the value of
958      * <code>d.getPolicy()</code>.  The <code>-cp/-classpath</code> option is
959      * added or replaced with the value of <code>d.getImportCodebase()</code>
960      * augmented with the value of the classpath of the tool and plugins.  The
961      * arguments passed to the child VM consist of <code>taskName</code>
962      * followed by <code>args</code> if <code>args</code> is
963      * non-<code>null</code>, or followed by
964      * <code>d.getServerConfigArgs()</code> otherwise.
965 
966      * <p>
967      *
968      * @param d the service descriptor, which may be <code>null</code>
969      * @param gd the group descriptor, which may be <code>null</code>
970      * @param taskName the name of the subtask to run
971      * @param args the arguments to pass to the child VM, which may be 
972      *             <code>null</code>
973      * @return the result or exception returned by the subtask supplied as a
974      *         serialized object written on the subtask's
975      *         <code>System.out</code> stream.
976      */
977     public Object launch(NonActivatableServiceDescriptor d, 
978 			 SharedActivationGroupDescriptor gd,
979 			 String taskName,
980 			 String[] args)
981     {
982 	if (d == null && gd == null) {
983 	    return launch(taskName, args);
984 	}
985 	//build taskArgs array
986 	if (args == null) {
987 	    if (d != null) {
988 		args = d.getServerConfigArgs();
989 		if (args == null || args.length == 0) {
990 		    return new IllegalArgumentException("No configuration args "
991 							+ "in descriptor");
992 		}
993 	    } else {
994 		args = new String[0];
995 	    }
996 	}
997 	String[] taskArgs = new String[args.length + 1];
998 	taskArgs[0] = taskName;
999 	System.arraycopy(args, 0, taskArgs, 1, args.length);
1000 	
1001 	if (d == null && gd != null) {
1002 	    Properties props = gd.getServerProperties();
1003 	    props.put("java.security.policy", gd.getPolicy());
1004 	    ArrayList l = new ArrayList();
1005 	    String[] opts = gd.getServerOptions();
1006 	    if (opts != null) {
1007 		for (int i = 0; i < opts.length; i++ ) {
1008 		    if (opts[i].equals("-cp")) {
1009 			i++; // bump past value
1010 		    } else {
1011 			l.add(opts[i]);
1012 		    }
1013 		}
1014 	    }
1015 	    l.add("-cp");
1016 	    l.add(gd.getClasspath() + File.pathSeparator + combinedClasspath);
1017 	    opts = (String[]) l.toArray(new String[l.size()]);
1018 	    String cmd = gd.getServerCommand();
1019 	    if (cmd == null) {
1020 		cmd = javaCmd;
1021 	    }
1022 	    return launch(cmd, props, opts, taskArgs);
1023 	} else if (d != null && gd == null) {
1024 	    if (d instanceof SharedActivatableServiceDescriptor) {
1025 		throw new IllegalArgumentException("no group for service");
1026 	    }
1027 	    Properties props = getProperties();
1028 	    props.put("java.security.policy", d.getPolicy());
1029 	    ArrayList l = new ArrayList();
1030 	    for (int i = 0; i < options.length; i++ ) {
1031 		if (options[i].equals("-cp") 
1032 		    || options[i].equals("-classpath")) {
1033 		    i++; // bump past value
1034 		} else {
1035 		    l.add(options[i]);
1036 		}
1037 	    }
1038 	    l.add("-cp");
1039 	    l.add(d.getImportCodebase() 
1040 		    + File.pathSeparator 
1041 		    + combinedClasspath);
1042 	    String[] opts = (String[]) l.toArray(new String[l.size()]);
1043 	    return launch(javaCmd, props, opts, taskArgs);
1044 	} else if (d != null && gd != null) {
1045 	    Properties props = getProperties();
1046 	    String[] opts = options;
1047 	    String vm = null;
1048 	    if (d instanceof SharedActivatableServiceDescriptor) {
1049 		props = gd.getServerProperties();
1050 		if (props == null) {
1051 		    props = new Properties();
1052 		}
1053 		opts = gd.getServerOptions();
1054 		vm = gd.getServerCommand();
1055 	    }
1056 	    if (vm == null) {
1057 		vm = javaCmd;
1058 	    }
1059 	    props.put("java.security.policy", d.getPolicy());
1060 	    ArrayList l = new ArrayList();
1061 	    if (opts != null) {
1062 		for (int i = 0; i < opts.length; i++ ) {
1063 		    if (opts[i].equals("-cp")
1064 		     || opts[i].equals("-classpath")) {
1065 			i++; // bump past value
1066 		    } else {
1067 			l.add(opts[i]);
1068 		    }
1069 		}
1070 	    }
1071 	    l.add("-cp");
1072 	    l.add(d.getImportCodebase() 
1073 		  + File.pathSeparator 
1074 		  + combinedClasspath);
1075 	    opts = (String[]) l.toArray(new String[l.size()]);
1076 	    return launch(vm, props, opts, taskArgs);
1077 	} else {
1078 	    throw new IllegalStateException("Should never get here");
1079 	}
1080     }
1081 
1082     /** 
1083      * Return the <code>java</code> command for the command line being analyzed.
1084      *
1085      * @return the <code>java</code> command
1086      */
1087     public String getJavaCmd() {
1088 	return javaCmd;
1089     }
1090 
1091     /**
1092      * Check for the existence of a file identified by a property
1093      * supplied on the command line being analyzed.
1094      *
1095      * @param prop the name of the property
1096      * @param desc a brief description of the file
1097      * @return the property value, or <code>null</code> if undefined
1098      */
1099     public String checkFile(String prop, String desc) {
1100 	String name = getProperty(prop);
1101 	if (name == null) {
1102 	    return getString("util.undef", desc);
1103 	}
1104 	return Util.checkFileName(name, desc);
1105     }
1106 
1107     /**
1108      * Return the name of the executable JAR file supplied on the
1109      * command line being analyzed.
1110      *
1111      * @return the JAR file name, or <code>null</code> if the command line
1112      *         did not specify one.
1113      */
1114     public String getJarToRun() {
1115         return jarToRun;
1116     }
1117 
1118     /**
1119      * Get the classpath provided by the command line being analyzed.  If
1120      * <code>getJarToRun()</code> returns a non-<code>null</code> value then
1121      * its value is returned. Otherwise the value supplied by the command
1122      * line <code>-cp/-classpath</code> option is returned.
1123      *
1124      * @return the classpath supplied on the command line being analyzed.
1125      */
1126     public String getClasspath() {
1127 	if (jarToRun != null) {
1128 	    return jarToRun;
1129 	}
1130 	return classpath;
1131     }
1132 
1133     /**
1134      * A subtask which returns the service descriptors and service
1135      * destructors supplied by the service starter configuration constructed
1136      * from <code>args</code>.
1137      */
1138      static class GetDescriptors implements SubVMTask {
1139 
1140 	public Object run(String[] args) {
1141 	    Configuration starterConfig = null;
1142 	    try {
1143 		starterConfig = ConfigurationProvider.getInstance(args);
1144 	    } catch (ConfigurationException e) {
1145 		return e;
1146 	    }
1147 	    try {
1148 		ServiceDescriptor[] d1 =  (ServiceDescriptor[])
1149 		    starterConfig.getEntry("org.apache.river.start",
1150 					   "serviceDescriptors",
1151 					   ServiceDescriptor[].class, 
1152 					   new ServiceDescriptor[0]);
1153 		ServiceDescriptor[] d2 =  (ServiceDescriptor[])
1154 		    starterConfig.getEntry("org.apache.river.start",
1155 					   "serviceDestructors",
1156 					   ServiceDescriptor[].class, 
1157 					   new ServiceDescriptor[0]);
1158 		ServiceDescriptor[] descriptors = 
1159 		    new ServiceDescriptor[d1.length + d2.length];
1160 		System.arraycopy(d1, 0, descriptors, 0, d1.length);
1161 		System.arraycopy(d2, 0, descriptors, d1.length, d2.length);
1162 		return descriptors;
1163 	    } catch (ConfigurationException e) {
1164 		return e;
1165 	    }
1166 	}
1167     }
1168 
1169     /**
1170      * An I/O redirection pipe. A daemon thread copies data from an input
1171      * stream to an output stream. An optional annotation may be provided
1172      * which will prefix each line of the copied data with a label which
1173      * can be used to identify the source. 
1174      */
1175     private class Pipe implements Runnable {
1176 
1177 	/** the line separator character */
1178 	private final static byte SEPARATOR = (byte) '\n';
1179 
1180 	/** output line buffer */
1181 	private ByteArrayOutputStream bufOut = new ByteArrayOutputStream();
1182 
1183 	/** the input stream */
1184 	private InputStream in;
1185 
1186 	/** the output PrintStream */
1187 	private PrintStream stream;
1188 
1189 	/** the output stream annotation */
1190 	private String annotation;
1191 
1192 	/** the thread to process the data */
1193 	private Thread outThread;
1194 	
1195 	/**
1196 	 * Create a new Pipe object and start the thread to handle the data.
1197 	 *
1198 	 * @param name the name to assign to the thread
1199 	 * @param in input stream from which pipe input flows
1200 	 * @param stream the stream to which output will be sent
1201 	 * @param a the annotation for prepending text to logged lines
1202 	 */
1203 	Pipe(String name, 
1204 		    InputStream in, 
1205 		    PrintStream stream, 
1206 		    String a) 
1207 	{
1208 	    this.in = in;
1209 	    this.stream = stream;
1210 	    this.annotation = a;
1211 	    outThread = new Thread(this, name);
1212 	    outThread.setDaemon(true);
1213 	    outThread.start();
1214 	}
1215 
1216 	/**
1217 	 * Wait until the run method terminates due to reading EOF on input
1218 	 *
1219 	 * @param timeout max time to wait for the thread to terminate
1220 	 */
1221 	void waitTillEmpty(int timeout) {
1222 	    try {
1223 		outThread.join(timeout);
1224 	    } catch (InterruptedException ignore) {
1225 	    }
1226 	}
1227 
1228 	/**
1229 	 * Read and write data until EOF is detected. Flush any remaining data to
1230 	 * the output steam and return, terminating the thread.
1231 	 */
1232 	public void run() {
1233 	    byte[] buf = new byte[256];
1234 	    int count;
1235 	    try {
1236 		/* read bytes till there are no more. */
1237 		while ((count = in.read(buf)) != -1) {
1238 		    write(buf, count);
1239 		}
1240 
1241 		/*  If annotating, flush internal buffer... may not have ended on a
1242 		 *  line separator, we also need a last annotation if 
1243 		 *  something was left.
1244 		 */
1245 		String lastInBuffer = bufOut.toString();
1246 		bufOut.reset();
1247 		if (lastInBuffer.length() > 0) {
1248 		    if (annotation != null) {
1249 			stream.print(annotation);
1250 		    }
1251 		    stream.println(lastInBuffer);
1252 		}
1253 	    } catch (IOException e) {
1254 	    }
1255 	}
1256 	
1257 	/**
1258 	 * Write each byte in the give byte array.
1259 	 *
1260 	 * @param b the array of input bytes
1261 	 * @param len the number data bytes in the array
1262 	 */
1263 	private void write(byte b[], int len) throws IOException {
1264 	    if (len < 0) {
1265 		throw new ArrayIndexOutOfBoundsException(len);
1266 	    }
1267 	    for (int i = 0; i < len; i++) {
1268 		write(b[i]);
1269 	    }
1270 	}
1271 
1272 	/**
1273 	 * If not annotated, write the byte to the stream immediately. Otherwise,
1274 	 * write a byte of data to the internal buffer. If we have matched a line
1275 	 * separator, then the currently buffered line is sent to the output writer
1276 	 * with a prepended annotation string.
1277 	 */
1278 	private void write(byte b) throws IOException {
1279 
1280 	    bufOut.write(b);
1281 	    
1282 	    // write buffered line if line separator detected
1283 	    if (b == SEPARATOR) {
1284 		String s = bufOut.toString();
1285 		bufOut.reset();
1286 		if (annotation != null) {
1287 		    stream.print(annotation);
1288 		}
1289 		stream.print(s);
1290 	    }
1291 	}
1292     }
1293 }