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}