View Javadoc
1   /*
2    * Copyright (C) 2009 Christian Schulte <cs@schulte.it>
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *   o Redistributions of source code must retain the above copyright
10   *     notice, this list of conditions and the following disclaimer.
11   *
12   *   o Redistributions in binary form must reproduce the above copyright
13   *     notice, this list of conditions and the following disclaimer in
14   *     the documentation and/or other materials provided with the
15   *     distribution.
16   *
17   * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
18   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
19   * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
20   * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
21   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24   * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26   * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27   *
28   * $JOMC: Jomc.java 5299 2016-08-30 01:50:13Z schulte $
29   *
30   */
31  package org.jomc.cli;
32  
33  import java.io.BufferedReader;
34  import java.io.IOException;
35  import java.io.InputStreamReader;
36  import java.io.PrintWriter;
37  import java.io.StringReader;
38  import java.io.StringWriter;
39  import java.net.URL;
40  import java.util.ArrayList;
41  import java.util.Collections;
42  import java.util.Date;
43  import java.util.Enumeration;
44  import java.util.List;
45  import java.util.Locale;
46  import java.util.logging.Level;
47  import org.apache.commons.cli.CommandLine;
48  import org.apache.commons.cli.GnuParser;
49  import org.apache.commons.cli.HelpFormatter;
50  import org.apache.commons.cli.Option;
51  import org.apache.commons.cli.Options;
52  import org.apache.commons.cli.ParseException;
53  import org.apache.commons.lang.StringUtils;
54  import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
55  
56  /**
57   * JOMC command line interface.
58   *
59   * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
60   */
61  public final class Jomc
62  {
63  
64      /**
65       * Command line option.
66       */
67      private static final Option DEBUG_OPTION;
68  
69      /**
70       * Command line option.
71       */
72      private static final Option VERBOSE_OPTION;
73  
74      /**
75       * Command line option.
76       */
77      private static final Option FAIL_ON_WARNINGS_OPTION;
78  
79      static
80      {
81          DEBUG_OPTION = new Option( "D", Messages.getMessage( "debugOptionDescription" ) );
82          DEBUG_OPTION.setLongOpt( "debug" );
83          DEBUG_OPTION.setArgs( 1 );
84          DEBUG_OPTION.setOptionalArg( true );
85          DEBUG_OPTION.setArgName( Messages.getMessage( "debugOptionArgumentDescription" ) );
86  
87          VERBOSE_OPTION = new Option( "v", Messages.getMessage( "verboseOptionDescription" ) );
88          VERBOSE_OPTION.setLongOpt( "verbose" );
89  
90          FAIL_ON_WARNINGS_OPTION = new Option( "fw", Messages.getMessage( "failOnWarningsOptionDescription" ) );
91          FAIL_ON_WARNINGS_OPTION.setLongOpt( "fail-on-warnings" );
92      }
93  
94      /**
95       * Log level events are logged at by default.
96       *
97       * @see #getDefaultLogLevel()
98       */
99      private static final Level DEFAULT_LOG_LEVEL = Level.WARNING;
100 
101     /**
102      * Default log level.
103      */
104     private static volatile Level defaultLogLevel;
105 
106     /**
107      * Print writer of the instance.
108      */
109     private volatile PrintWriter printWriter;
110 
111     /**
112      * Log level of the instance.
113      */
114     private volatile Level logLevel;
115 
116     /**
117      * Greatest severity logged by the command.
118      */
119     private volatile Level severity = Level.ALL;
120 
121     /**
122      * Creates a new {@code Jomc} instance.
123      */
124     public Jomc()
125     {
126         super();
127     }
128 
129     /**
130      * Gets the print writer of the instance.
131      *
132      * @return The print writer of the instance.
133      *
134      * @see #setPrintWriter(java.io.PrintWriter)
135      */
136     @IgnoreJRERequirement
137     public PrintWriter getPrintWriter()
138     {
139         if ( this.printWriter == null )
140         {
141             try
142             {
143                 // As of Java 6, "System.console()", if any.
144                 Class.forName( "java.io.Console" );
145                 this.printWriter = System.console() != null
146                                        ? System.console().writer()
147                                        : new PrintWriter( System.out, true );
148 
149             }
150             catch ( final ClassNotFoundException e )
151             {
152                 if ( this.isLoggable( Level.FINEST ) )
153                 {
154                     this.log( Level.FINEST, Messages.getMessage( e ), e );
155                 }
156 
157                 this.printWriter = new PrintWriter( System.out, true );
158             }
159         }
160 
161         return this.printWriter;
162     }
163 
164     /**
165      * Sets the print writer of the instance.
166      *
167      * @param value The new print writer of the instance or {@code null}.
168      *
169      * @see #getPrintWriter()
170      */
171     public void setPrintWriter( final PrintWriter value )
172     {
173         this.printWriter = value;
174     }
175 
176     /**
177      * Gets the default log level events are logged at.
178      * <p>
179      * The default log level is controlled by system property {@code org.jomc.cli.Jomc.defaultLogLevel} holding
180      * the log level to log events at by default. If that property is not set, the {@code WARNING} default is returned.
181      * </p>
182      *
183      * @return The log level events are logged at by default.
184      *
185      * @see #getLogLevel()
186      * @see Level#parse(java.lang.String)
187      */
188     public static Level getDefaultLogLevel()
189     {
190         if ( defaultLogLevel == null )
191         {
192             defaultLogLevel = Level.parse( System.getProperty(
193                 "org.jomc.cli.Jomc.defaultLogLevel", DEFAULT_LOG_LEVEL.getName() ) );
194 
195         }
196 
197         return defaultLogLevel;
198     }
199 
200     /**
201      * Sets the default log level events are logged at.
202      *
203      * @param value The new default level events are logged at or {@code null}.
204      *
205      * @see #getDefaultLogLevel()
206      */
207     public static void setDefaultLogLevel( final Level value )
208     {
209         defaultLogLevel = value;
210     }
211 
212     /**
213      * Gets the log level of the instance.
214      *
215      * @return The log level of the instance.
216      *
217      * @see #getDefaultLogLevel()
218      * @see #setLogLevel(java.util.logging.Level)
219      * @see #isLoggable(java.util.logging.Level)
220      */
221     public Level getLogLevel()
222     {
223         if ( this.logLevel == null )
224         {
225             this.logLevel = getDefaultLogLevel();
226 
227             if ( this.isLoggable( Level.CONFIG ) )
228             {
229                 this.log( Level.CONFIG, Messages.getMessage( "defaultLogLevelInfo", this.logLevel.getLocalizedName() ),
230                           null );
231 
232             }
233         }
234 
235         return this.logLevel;
236     }
237 
238     /**
239      * Sets the log level of the instance.
240      *
241      * @param value The new log level of the instance or {@code null}.
242      *
243      * @see #getLogLevel()
244      * @see #isLoggable(java.util.logging.Level)
245      */
246     public void setLogLevel( final Level value )
247     {
248         this.logLevel = value;
249     }
250 
251     /**
252      * Checks if a message at a given level is provided to the listeners of the instance.
253      *
254      * @param level The level to test.
255      *
256      * @return {@code true}, if messages at {@code level} are provided to the listeners of the instance;
257      * {@code false}, if messages at {@code level} are not provided to the listeners of the instance.
258      *
259      * @throws NullPointerException if {@code level} is {@code null}.
260      *
261      * @see #getLogLevel()
262      * @see #setLogLevel(java.util.logging.Level)
263      */
264     public boolean isLoggable( final Level level )
265     {
266         if ( level == null )
267         {
268             throw new NullPointerException( "level" );
269         }
270 
271         return level.intValue() >= this.getLogLevel().intValue();
272     }
273 
274     /**
275      * Processes the given arguments and executes the corresponding command.
276      *
277      * @param args Arguments to process.
278      *
279      * @return Status code.
280      *
281      * @see Command#STATUS_SUCCESS
282      * @see Command#STATUS_FAILURE
283      */
284     public int jomc( final String[] args )
285     {
286         Command cmd = null;
287         this.severity = Level.ALL;
288 
289         try
290         {
291             final StringBuilder commandInfo = new StringBuilder( 1024 );
292 
293             for ( final Command c : this.getCommands() )
294             {
295                 if ( cmd == null && args != null && args.length > 0
296                          && ( args[0].equals( c.getName() ) || args[0].equals( c.getAbbreviatedName() ) ) )
297                 {
298                     cmd = c;
299                 }
300 
301                 commandInfo.append( StringUtils.rightPad( c.getName(), 25 ) ).
302                     append( " : " ).
303                     append( c.getShortDescription( Locale.getDefault() ) ).
304                     append( " (" ).
305                     append( c.getAbbreviatedName() ).
306                     append( ")" ).
307                     append( System.getProperty( "line.separator", "\n" ) );
308 
309             }
310 
311             if ( cmd == null )
312             {
313                 this.getPrintWriter().println( Messages.getMessage( "usage", "help" ) );
314                 this.getPrintWriter().println();
315                 this.getPrintWriter().println( commandInfo.toString() );
316                 return Command.STATUS_FAILURE;
317             }
318 
319             final String[] commandArguments = new String[ args.length - 1 ];
320             System.arraycopy( args, 1, commandArguments, 0, commandArguments.length );
321 
322             final Options options = cmd.getOptions();
323             options.addOption( DEBUG_OPTION );
324             options.addOption( VERBOSE_OPTION );
325             options.addOption( FAIL_ON_WARNINGS_OPTION );
326 
327             if ( commandArguments.length > 0 && "help".equals( commandArguments[0] ) )
328             {
329                 final StringWriter usage = new StringWriter();
330                 final StringWriter opts = new StringWriter();
331                 final HelpFormatter formatter = new HelpFormatter();
332 
333                 PrintWriter pw = new PrintWriter( usage );
334                 formatter.printUsage( pw, 80, cmd.getName(), options );
335                 pw.close();
336                 assert !pw.checkError() : "Unexpected error printing usage.";
337 
338                 pw = new PrintWriter( opts );
339                 formatter.printOptions( pw, 80, options, 2, 2 );
340                 pw.close();
341                 assert !pw.checkError() : "Unexpected error printing options.";
342 
343                 this.getPrintWriter().println( cmd.getShortDescription( Locale.getDefault() ) );
344                 this.getPrintWriter().println();
345                 this.getPrintWriter().println( usage.toString() );
346                 this.getPrintWriter().println( opts.toString() );
347                 this.getPrintWriter().println();
348 
349                 if ( cmd.getLongDescription( Locale.getDefault() ) != null )
350                 {
351                     this.getPrintWriter().println( cmd.getLongDescription( Locale.getDefault() ) );
352                     this.getPrintWriter().println();
353                 }
354 
355                 return Command.STATUS_SUCCESS;
356             }
357 
358             cmd.getListeners().add( new Command.Listener()
359             {
360 
361                 public void onLog( final Level level, final String message, final Throwable t )
362                 {
363                     log( level, message, t );
364                 }
365 
366             } );
367 
368             // https://issues.apache.org/jira/browse/CLI-255
369             final CommandLine commandLine = new GnuParser().parse( options, commandArguments );
370             final boolean debug = commandLine.hasOption( DEBUG_OPTION.getOpt() );
371             final boolean verbose = commandLine.hasOption( VERBOSE_OPTION.getOpt() );
372             Level debugLevel = Level.ALL;
373 
374             if ( debug )
375             {
376                 final String debugOption = commandLine.getOptionValue( DEBUG_OPTION.getOpt() );
377 
378                 if ( debugOption != null )
379                 {
380                     debugLevel = Level.parse( debugOption );
381                 }
382             }
383 
384             if ( debug || verbose )
385             {
386                 this.setLogLevel( debug ? debugLevel : Level.INFO );
387             }
388 
389             cmd.setLogLevel( this.getLogLevel() );
390 
391             if ( this.isLoggable( Level.FINER ) )
392             {
393                 for ( int i = 0; i < args.length; i++ )
394                 {
395                     this.log( Level.FINER, new StringBuilder( 128 ).append( "[" ).append( i ).append( "] -> '" ).
396                               append( args[i] ).append( "'" ).append( System.getProperty( "line.separator", "\n" ) ).
397                               toString(), null );
398 
399                 }
400             }
401 
402             final boolean failOnWarnings = commandLine.hasOption( FAIL_ON_WARNINGS_OPTION.getOpt() );
403 
404             final int status = cmd.execute( commandLine );
405 
406             if ( status == Command.STATUS_SUCCESS && failOnWarnings
407                      && this.severity.intValue() >= Level.WARNING.intValue() )
408             {
409                 return Command.STATUS_FAILURE;
410             }
411 
412             return status;
413         }
414         catch ( final ParseException e )
415         {
416             this.log( Level.SEVERE, Messages.getMessage( "illegalArgumentsInformation", cmd.getName(), "help" ), e );
417             return Command.STATUS_FAILURE;
418         }
419         catch ( final Throwable t )
420         {
421             this.log( Level.SEVERE, null, t );
422             return Command.STATUS_FAILURE;
423         }
424         finally
425         {
426             this.getPrintWriter().flush();
427             this.severity = Level.ALL;
428         }
429     }
430 
431     /**
432      * Main entry point.
433      *
434      * @param args The application arguments.
435      */
436     public static void main( final String[] args )
437     {
438         System.exit( run( args ) );
439     }
440 
441     /**
442      * Main entry point without exiting the VM.
443      *
444      * @param args The application arguments.
445      *
446      * @return Status code.
447      *
448      * @see Command#STATUS_SUCCESS
449      * @see Command#STATUS_FAILURE
450      */
451     public static int run( final String[] args )
452     {
453         return new Jomc().jomc( args );
454     }
455 
456     /**
457      * Logs to the print writer of the instance.
458      *
459      * @param level The level of the event.
460      * @param message The message of the event or {@code null}.
461      * @param throwable The throwable of the event {@code null}.
462      *
463      * @throws NullPointerException if {@code level} is {@code null}.
464      */
465     private void log( final Level level, final String message, final Throwable throwable )
466     {
467         if ( level == null )
468         {
469             throw new NullPointerException( "level" );
470         }
471 
472         if ( this.severity.intValue() < level.intValue() )
473         {
474             this.severity = level;
475         }
476 
477         if ( this.isLoggable( level ) )
478         {
479             if ( message != null )
480             {
481                 this.getPrintWriter().print( this.formatLogLines( level, "" ) );
482                 this.getPrintWriter().print( this.formatLogLines( level, message ) );
483             }
484 
485             if ( throwable != null )
486             {
487                 this.getPrintWriter().print( this.formatLogLines( level, "" ) );
488                 final String m = Messages.getMessage( throwable );
489 
490                 if ( m != null && m.length() > 0 )
491                 {
492                     this.getPrintWriter().print( this.formatLogLines( level, m ) );
493                 }
494                 else
495                 {
496                     this.getPrintWriter().print( this.formatLogLines(
497                         level, Messages.getMessage( "defaultExceptionMessage" ) ) );
498 
499                 }
500 
501                 if ( this.getLogLevel().intValue() < Level.INFO.intValue() )
502                 {
503                     final StringWriter stackTrace = new StringWriter();
504                     final PrintWriter pw = new PrintWriter( stackTrace );
505                     throwable.printStackTrace( pw );
506                     pw.flush();
507                     this.getPrintWriter().print( this.formatLogLines( level, stackTrace.toString() ) );
508                 }
509             }
510         }
511 
512         this.getPrintWriter().flush();
513     }
514 
515     private String formatLogLines( final Level level, final String text )
516     {
517         BufferedReader reader = null;
518 
519         try
520         {
521             final StringBuilder lines = new StringBuilder( text.length() );
522             reader = new BufferedReader( new StringReader( text ) );
523 
524             for ( String line = reader.readLine(); line != null; line = reader.readLine() )
525             {
526                 final boolean debug = this.getLogLevel().intValue() < Level.INFO.intValue();
527                 lines.append( "[" ).append( level.getLocalizedName() );
528 
529                 if ( debug )
530                 {
531                     lines.append( "|" ).append( Thread.currentThread().getName() ).append( "|" ).
532                         append( Messages.getMessage( "timePattern", new Date( System.currentTimeMillis() ) ) );
533 
534                 }
535 
536                 lines.append( "] " ).append( line ).append( System.getProperty( "line.separator", "\n" ) );
537             }
538 
539             reader.close();
540             reader = null;
541 
542             return lines.toString();
543         }
544         catch ( final IOException e )
545         {
546             throw new AssertionError( e );
547         }
548         finally
549         {
550             try
551             {
552                 if ( reader != null )
553                 {
554                     reader.close();
555                 }
556             }
557             catch ( final IOException e )
558             {
559                 this.log( Level.SEVERE, Messages.getMessage( e ), e );
560             }
561         }
562     }
563 
564     /**
565      * Gets the {@code Command}s of the instance.
566      *
567      * @return The {@code Command}s of the instance.
568      *
569      * @throws IOException if discovering {@code Command} implementations fails.
570      */
571     private List<Command> getCommands() throws IOException
572     {
573         final List<Command> commands = new ArrayList<Command>();
574 
575         final Enumeration<URL> serviceResources =
576             this.getClass().getClassLoader().getResources( "META-INF/services/org.jomc.cli.Command" );
577 
578         if ( serviceResources != null )
579         {
580             for ( final URL serviceResource : Collections.list( serviceResources ) )
581             {
582                 BufferedReader reader = null;
583                 try
584                 {
585                     reader = new BufferedReader( new InputStreamReader( serviceResource.openStream(), "UTF-8" ) );
586 
587                     for ( String line = reader.readLine(); line != null; line = reader.readLine() )
588                     {
589                         if ( !line.contains( "#" ) )
590                         {
591                             commands.add( Class.forName( line.trim() ).asSubclass( Command.class ).newInstance() );
592                         }
593                     }
594                 }
595                 catch ( final ClassNotFoundException e )
596                 {
597                     throw new AssertionError( e );
598                 }
599                 catch ( final InstantiationException e )
600                 {
601                     throw new AssertionError( e );
602                 }
603                 catch ( final IllegalAccessException e )
604                 {
605                     throw new AssertionError( e );
606                 }
607                 finally
608                 {
609                     try
610                     {
611                         if ( reader != null )
612                         {
613                             reader.close();
614                         }
615                     }
616                     catch ( final IOException e )
617                     {
618                         this.log( Level.WARNING, Messages.getMessage( e ), e );
619                     }
620                 }
621             }
622         }
623 
624         return Collections.unmodifiableList( commands );
625     }
626 
627 }