AbstractCommand.java

/*
 * Copyright (C) 2009 Christian Schulte <cs@schulte.it>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   o Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   o Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in
 *     the documentation and/or other materials provided with the
 *     distribution.
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * $JOMC: AbstractCommand.java 5299 2016-08-30 01:50:13Z schulte $
 *
 */
package org.jomc.cli.commands;

import java.util.List;
import java.util.Locale;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import org.apache.commons.cli.CommandLine;
import org.jomc.cli.Command;

/**
 * Base {@code Command} implementation.
 *
 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
 */
public abstract class AbstractCommand implements Command
{

    /**
     * Default log level.
     */
    private static volatile Level defaultLogLevel;

    /**
     * Log level of the instance.
     */
    private volatile Level logLevel;

    /**
     * The listeners of the instance.
     */
    private volatile List<Listener> listeners = new CopyOnWriteArrayList<Listener>();

    /**
     * The {@code ExecutorService} of the command.
     *
     * @since 1.10
     */
    private volatile ExecutorService executorService;

    /**
     * Creates a new {@code AbstractCommand} instance.
     */
    public AbstractCommand()
    {
        super();
    }

    /**
     * Gets the default log level events are logged at.
     * <p>
     * The default log level is controlled by system property
     * {@code org.jomc.cli.commands.AbstractCommand.defaultLogLevel} holding the log level to log events at by
     * default. If that property is not set, the {@code WARNING} default is returned.
     * </p>
     *
     * @return The log level events are logged at by default.
     *
     * @see #getLogLevel()
     * @see Level#parse(java.lang.String)
     */
    public static Level getDefaultLogLevel()
    {
        if ( defaultLogLevel == null )
        {
            defaultLogLevel = Level.parse( System.getProperty(
                "org.jomc.cli.commands.AbstractCommand.defaultLogLevel", Level.WARNING.getName() ) );

        }

        return defaultLogLevel;
    }

    /**
     * Sets the default log level events are logged at.
     *
     * @param value The new default level events are logged at or {@code null}.
     *
     * @see #getDefaultLogLevel()
     */
    public static void setDefaultLogLevel( final Level value )
    {
        defaultLogLevel = value;
    }

    /**
     * Gets the log level of the instance.
     *
     * @return The log level of the instance.
     *
     * @see #getDefaultLogLevel()
     * @see #setLogLevel(java.util.logging.Level)
     * @see #isLoggable(java.util.logging.Level)
     */
    public final Level getLogLevel()
    {
        if ( this.logLevel == null )
        {
            this.logLevel = getDefaultLogLevel();

            if ( this.isLoggable( Level.CONFIG ) )
            {
                this.log( Level.CONFIG, Messages.getMessage( "defaultLogLevelInfo", this.logLevel.getLocalizedName() ),
                          null );

            }
        }

        return this.logLevel;
    }

    /**
     * Sets the log level of the instance.
     *
     * @param value The new log level of the instance or {@code null}.
     *
     * @see #getLogLevel()
     * @see #isLoggable(java.util.logging.Level)
     */
    public final void setLogLevel( final Level value )
    {
        this.logLevel = value;
    }

    /**
     * Gets the list of registered listeners.
     * <p>
     * This accessor method returns a reference to the live list, not a snapshot. Therefore any modification you make
     * to the returned list will be present inside the object. This is why there is no {@code set} method for the
     * listeners property.
     * </p>
     *
     * @return The list of registered listeners.
     *
     * @see #log(java.util.logging.Level, java.lang.String, java.lang.Throwable)
     */
    public final List<Listener> getListeners()
    {
        return this.listeners;
    }

    /**
     * Checks if a message at a given level is provided to the listeners of the instance.
     *
     * @param level The level to test.
     *
     * @return {@code true}, if messages at {@code level} are provided to the listeners of the instance;
     * {@code false}, if messages at {@code level} are not provided to the listeners of the instance.
     *
     * @throws NullPointerException if {@code level} is {@code null}.
     *
     * @see #getLogLevel()
     * @see #setLogLevel(java.util.logging.Level)
     */
    protected boolean isLoggable( final Level level )
    {
        if ( level == null )
        {
            throw new NullPointerException( "level" );
        }

        return level.intValue() >= this.getLogLevel().intValue();
    }

    /**
     * Notifies registered listeners.
     *
     * @param level The level of the event.
     * @param message The message of the event or {@code null}.
     * @param throwable The throwable of the event {@code null}.
     *
     * @throws NullPointerException if {@code level} is {@code null}.
     *
     * @see #getListeners()
     * @see #isLoggable(java.util.logging.Level)
     */
    protected void log( final Level level, final String message, final Throwable throwable )
    {
        if ( level == null )
        {
            throw new NullPointerException( "level" );
        }

        if ( this.isLoggable( level ) )
        {
            for ( final Listener l : this.getListeners() )
            {
                l.onLog( level, message, throwable );
            }
        }
    }

    @Override
    public org.apache.commons.cli.Options getOptions()
    {
        final org.apache.commons.cli.Options options = new org.apache.commons.cli.Options();
        options.addOption( Options.THREADS_OPTION );
        return options;
    }

    /**
     * Gets the {@code ExecutorService} used to run tasks in parallel.
     *
     * @param commandLine The {@code CommandLine} to use for setting up an executor service when not already created.
     *
     * @return The {@code ExecutorService} used to run tasks in parallel or {@code null}.
     *
     * @since 1.10
     */
    protected final ExecutorService getExecutorService( final CommandLine commandLine )
    {
        if ( this.executorService == null )
        {
            final String formular =
                commandLine.hasOption( Options.THREADS_OPTION.getOpt() )
                    ? commandLine.getOptionValue( Options.THREADS_OPTION.getOpt() ).toLowerCase( new Locale( "" ) )
                    : "1.0c";

            final Double parallelism =
                formular.contains( "c" )
                    ? Double.valueOf( formular.replace( "c", "" ) ) * Runtime.getRuntime().availableProcessors()
                    : Double.valueOf( formular );

            if ( parallelism.intValue() > 1 )
            {
                this.executorService = Executors.newFixedThreadPool(
                    parallelism.intValue(), new ThreadFactory()
                {

                    private final ThreadGroup group;

                    private final AtomicInteger threadNumber = new AtomicInteger( 1 );


                    {
                        final SecurityManager s = System.getSecurityManager();
                        this.group = s != null
                                         ? s.getThreadGroup()
                                         : Thread.currentThread().getThreadGroup();

                    }

                    @Override
                    public Thread newThread( final Runnable r )
                    {
                        final Thread t =
                            new Thread( this.group, r, "jomc-cli-" + this.threadNumber.getAndIncrement(), 0 );

                        if ( t.isDaemon() )
                        {
                            t.setDaemon( false );
                        }
                        if ( t.getPriority() != Thread.NORM_PRIORITY )
                        {
                            t.setPriority( Thread.NORM_PRIORITY );
                        }

                        return t;
                    }

                } );
            }
        }

        return this.executorService;
    }

    @Override
    public final int execute( final CommandLine commandLine )
    {
        if ( commandLine == null )
        {
            throw new NullPointerException( "commandLine" );
        }

        int status = STATUS_FAILURE;

        try
        {
            if ( this.isLoggable( Level.INFO ) )
            {
                this.log( Level.INFO, Messages.getMessage( "separator" ), null );
                this.log( Level.INFO, Messages.getMessage( "applicationTitle" ), null );
                this.log( Level.INFO, Messages.getMessage( "separator" ), null );
                this.log( Level.INFO, Messages.getMessage( "commandInfo", this.getName() ), null );
            }

            this.preExecuteCommand( commandLine );
            this.executeCommand( commandLine );
            status = STATUS_SUCCESS;
        }
        catch ( final Throwable t )
        {
            this.log( Level.SEVERE, null, t );
            status = STATUS_FAILURE;
        }
        finally
        {
            try
            {
                this.postExecuteCommand( commandLine );
            }
            catch ( final Throwable t )
            {
                this.log( Level.SEVERE, null, t );
                status = STATUS_FAILURE;
            }
            finally
            {
                if ( this.executorService != null )
                {
                    this.executorService.shutdown();
                    this.executorService = null;
                }
            }
        }

        if ( this.isLoggable( Level.INFO ) )
        {
            if ( status == STATUS_SUCCESS )
            {
                this.log( Level.INFO, Messages.getMessage( "commandSuccess", this.getName() ), null );
            }
            else if ( status == STATUS_FAILURE )
            {
                this.log( Level.INFO, Messages.getMessage( "commandFailure", this.getName() ), null );
            }

            this.log( Level.INFO, Messages.getMessage( "separator" ), null );
        }

        return status;
    }

    /**
     * Called by the {@code execute} method prior to the {@code executeCommand} method.
     *
     * @param commandLine The command line to execute.
     *
     * @throws NullPointerException if {@code commandLine} is {@code null}.
     * @throws CommandExecutionException if executing the command fails.
     *
     * @see #execute(org.apache.commons.cli.CommandLine)
     */
    protected void preExecuteCommand( final CommandLine commandLine ) throws CommandExecutionException
    {
        if ( commandLine == null )
        {
            throw new NullPointerException( "commandLine" );
        }
    }

    /**
     * Called by the {@code execute} method prior to the {@code postExecuteCommand} method.
     *
     * @param commandLine The command line to execute.
     *
     * @throws CommandExecutionException if executing the command fails.
     *
     * @see #execute(org.apache.commons.cli.CommandLine)
     */
    protected abstract void executeCommand( final CommandLine commandLine ) throws CommandExecutionException;

    /**
     * Called by the {@code execute} method after the {@code preExecuteCommand}/{@code executeCommand} methods even if
     * those methods threw an exception.
     *
     * @param commandLine The command line to execute.
     *
     * @throws NullPointerException if {@code commandLine} is {@code null}.
     * @throws CommandExecutionException if executing the command fails.
     *
     * @see #execute(org.apache.commons.cli.CommandLine)
     */
    protected void postExecuteCommand( final CommandLine commandLine ) throws CommandExecutionException
    {
        if ( commandLine == null )
        {
            throw new NullPointerException( "commandLine" );
        }
    }

}