001/* 002 * Copyright (C) 2009 Christian Schulte <cs@schulte.it> 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without 006 * modification, are permitted provided that the following conditions 007 * are met: 008 * 009 * o Redistributions of source code must retain the above copyright 010 * notice, this list of conditions and the following disclaimer. 011 * 012 * o Redistributions in binary form must reproduce the above copyright 013 * notice, this list of conditions and the following disclaimer in 014 * the documentation and/or other materials provided with the 015 * distribution. 016 * 017 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 018 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 019 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 020 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, 021 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 022 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 023 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 024 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 025 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 026 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 027 * 028 * $JOMC: Jomc.java 5299 2016-08-30 01:50:13Z schulte $ 029 * 030 */ 031package org.jomc.cli; 032 033import java.io.BufferedReader; 034import java.io.IOException; 035import java.io.InputStreamReader; 036import java.io.PrintWriter; 037import java.io.StringReader; 038import java.io.StringWriter; 039import java.net.URL; 040import java.util.ArrayList; 041import java.util.Collections; 042import java.util.Date; 043import java.util.Enumeration; 044import java.util.List; 045import java.util.Locale; 046import java.util.logging.Level; 047import org.apache.commons.cli.CommandLine; 048import org.apache.commons.cli.GnuParser; 049import org.apache.commons.cli.HelpFormatter; 050import org.apache.commons.cli.Option; 051import org.apache.commons.cli.Options; 052import org.apache.commons.cli.ParseException; 053import org.apache.commons.lang.StringUtils; 054import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; 055 056/** 057 * JOMC command line interface. 058 * 059 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a> 060 */ 061public final class Jomc 062{ 063 064 /** 065 * Command line option. 066 */ 067 private static final Option DEBUG_OPTION; 068 069 /** 070 * Command line option. 071 */ 072 private static final Option VERBOSE_OPTION; 073 074 /** 075 * Command line option. 076 */ 077 private static final Option FAIL_ON_WARNINGS_OPTION; 078 079 static 080 { 081 DEBUG_OPTION = new Option( "D", Messages.getMessage( "debugOptionDescription" ) ); 082 DEBUG_OPTION.setLongOpt( "debug" ); 083 DEBUG_OPTION.setArgs( 1 ); 084 DEBUG_OPTION.setOptionalArg( true ); 085 DEBUG_OPTION.setArgName( Messages.getMessage( "debugOptionArgumentDescription" ) ); 086 087 VERBOSE_OPTION = new Option( "v", Messages.getMessage( "verboseOptionDescription" ) ); 088 VERBOSE_OPTION.setLongOpt( "verbose" ); 089 090 FAIL_ON_WARNINGS_OPTION = new Option( "fw", Messages.getMessage( "failOnWarningsOptionDescription" ) ); 091 FAIL_ON_WARNINGS_OPTION.setLongOpt( "fail-on-warnings" ); 092 } 093 094 /** 095 * Log level events are logged at by default. 096 * 097 * @see #getDefaultLogLevel() 098 */ 099 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}