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: AbstractCommand.java 5299 2016-08-30 01:50:13Z schulte $
029 *
030 */
031package org.jomc.cli.commands;
032
033import java.util.List;
034import java.util.Locale;
035import java.util.concurrent.CopyOnWriteArrayList;
036import java.util.concurrent.ExecutorService;
037import java.util.concurrent.Executors;
038import java.util.concurrent.ThreadFactory;
039import java.util.concurrent.atomic.AtomicInteger;
040import java.util.logging.Level;
041import org.apache.commons.cli.CommandLine;
042import org.jomc.cli.Command;
043
044/**
045 * Base {@code Command} implementation.
046 *
047 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
048 */
049public abstract class AbstractCommand implements Command
050{
051
052    /**
053     * Default log level.
054     */
055    private static volatile Level defaultLogLevel;
056
057    /**
058     * Log level of the instance.
059     */
060    private volatile Level logLevel;
061
062    /**
063     * The listeners of the instance.
064     */
065    private volatile List<Listener> listeners = new CopyOnWriteArrayList<Listener>();
066
067    /**
068     * The {@code ExecutorService} of the command.
069     *
070     * @since 1.10
071     */
072    private volatile ExecutorService executorService;
073
074    /**
075     * Creates a new {@code AbstractCommand} instance.
076     */
077    public AbstractCommand()
078    {
079        super();
080    }
081
082    /**
083     * Gets the default log level events are logged at.
084     * <p>
085     * The default log level is controlled by system property
086     * {@code org.jomc.cli.commands.AbstractCommand.defaultLogLevel} holding the log level to log events at by
087     * default. If that property is not set, the {@code WARNING} default is returned.
088     * </p>
089     *
090     * @return The log level events are logged at by default.
091     *
092     * @see #getLogLevel()
093     * @see Level#parse(java.lang.String)
094     */
095    public static Level getDefaultLogLevel()
096    {
097        if ( defaultLogLevel == null )
098        {
099            defaultLogLevel = Level.parse( System.getProperty(
100                "org.jomc.cli.commands.AbstractCommand.defaultLogLevel", Level.WARNING.getName() ) );
101
102        }
103
104        return defaultLogLevel;
105    }
106
107    /**
108     * Sets the default log level events are logged at.
109     *
110     * @param value The new default level events are logged at or {@code null}.
111     *
112     * @see #getDefaultLogLevel()
113     */
114    public static void setDefaultLogLevel( final Level value )
115    {
116        defaultLogLevel = value;
117    }
118
119    /**
120     * Gets the log level of the instance.
121     *
122     * @return The log level of the instance.
123     *
124     * @see #getDefaultLogLevel()
125     * @see #setLogLevel(java.util.logging.Level)
126     * @see #isLoggable(java.util.logging.Level)
127     */
128    public final Level getLogLevel()
129    {
130        if ( this.logLevel == null )
131        {
132            this.logLevel = getDefaultLogLevel();
133
134            if ( this.isLoggable( Level.CONFIG ) )
135            {
136                this.log( Level.CONFIG, Messages.getMessage( "defaultLogLevelInfo", this.logLevel.getLocalizedName() ),
137                          null );
138
139            }
140        }
141
142        return this.logLevel;
143    }
144
145    /**
146     * Sets the log level of the instance.
147     *
148     * @param value The new log level of the instance or {@code null}.
149     *
150     * @see #getLogLevel()
151     * @see #isLoggable(java.util.logging.Level)
152     */
153    public final void setLogLevel( final Level value )
154    {
155        this.logLevel = value;
156    }
157
158    /**
159     * Gets the list of registered listeners.
160     * <p>
161     * This accessor method returns a reference to the live list, not a snapshot. Therefore any modification you make
162     * to the returned list will be present inside the object. This is why there is no {@code set} method for the
163     * listeners property.
164     * </p>
165     *
166     * @return The list of registered listeners.
167     *
168     * @see #log(java.util.logging.Level, java.lang.String, java.lang.Throwable)
169     */
170    public final List<Listener> getListeners()
171    {
172        return this.listeners;
173    }
174
175    /**
176     * Checks if a message at a given level is provided to the listeners of the instance.
177     *
178     * @param level The level to test.
179     *
180     * @return {@code true}, if messages at {@code level} are provided to the listeners of the instance;
181     * {@code false}, if messages at {@code level} are not provided to the listeners of the instance.
182     *
183     * @throws NullPointerException if {@code level} is {@code null}.
184     *
185     * @see #getLogLevel()
186     * @see #setLogLevel(java.util.logging.Level)
187     */
188    protected boolean isLoggable( final Level level )
189    {
190        if ( level == null )
191        {
192            throw new NullPointerException( "level" );
193        }
194
195        return level.intValue() >= this.getLogLevel().intValue();
196    }
197
198    /**
199     * Notifies registered listeners.
200     *
201     * @param level The level of the event.
202     * @param message The message of the event or {@code null}.
203     * @param throwable The throwable of the event {@code null}.
204     *
205     * @throws NullPointerException if {@code level} is {@code null}.
206     *
207     * @see #getListeners()
208     * @see #isLoggable(java.util.logging.Level)
209     */
210    protected void log( final Level level, final String message, final Throwable throwable )
211    {
212        if ( level == null )
213        {
214            throw new NullPointerException( "level" );
215        }
216
217        if ( this.isLoggable( level ) )
218        {
219            for ( final Listener l : this.getListeners() )
220            {
221                l.onLog( level, message, throwable );
222            }
223        }
224    }
225
226    @Override
227    public org.apache.commons.cli.Options getOptions()
228    {
229        final org.apache.commons.cli.Options options = new org.apache.commons.cli.Options();
230        options.addOption( Options.THREADS_OPTION );
231        return options;
232    }
233
234    /**
235     * Gets the {@code ExecutorService} used to run tasks in parallel.
236     *
237     * @param commandLine The {@code CommandLine} to use for setting up an executor service when not already created.
238     *
239     * @return The {@code ExecutorService} used to run tasks in parallel or {@code null}.
240     *
241     * @since 1.10
242     */
243    protected final ExecutorService getExecutorService( final CommandLine commandLine )
244    {
245        if ( this.executorService == null )
246        {
247            final String formular =
248                commandLine.hasOption( Options.THREADS_OPTION.getOpt() )
249                    ? commandLine.getOptionValue( Options.THREADS_OPTION.getOpt() ).toLowerCase( new Locale( "" ) )
250                    : "1.0c";
251
252            final Double parallelism =
253                formular.contains( "c" )
254                    ? Double.valueOf( formular.replace( "c", "" ) ) * Runtime.getRuntime().availableProcessors()
255                    : Double.valueOf( formular );
256
257            if ( parallelism.intValue() > 1 )
258            {
259                this.executorService = Executors.newFixedThreadPool(
260                    parallelism.intValue(), new ThreadFactory()
261                {
262
263                    private final ThreadGroup group;
264
265                    private final AtomicInteger threadNumber = new AtomicInteger( 1 );
266
267
268                    {
269                        final SecurityManager s = System.getSecurityManager();
270                        this.group = s != null
271                                         ? s.getThreadGroup()
272                                         : Thread.currentThread().getThreadGroup();
273
274                    }
275
276                    @Override
277                    public Thread newThread( final Runnable r )
278                    {
279                        final Thread t =
280                            new Thread( this.group, r, "jomc-cli-" + this.threadNumber.getAndIncrement(), 0 );
281
282                        if ( t.isDaemon() )
283                        {
284                            t.setDaemon( false );
285                        }
286                        if ( t.getPriority() != Thread.NORM_PRIORITY )
287                        {
288                            t.setPriority( Thread.NORM_PRIORITY );
289                        }
290
291                        return t;
292                    }
293
294                } );
295            }
296        }
297
298        return this.executorService;
299    }
300
301    @Override
302    public final int execute( final CommandLine commandLine )
303    {
304        if ( commandLine == null )
305        {
306            throw new NullPointerException( "commandLine" );
307        }
308
309        int status = STATUS_FAILURE;
310
311        try
312        {
313            if ( this.isLoggable( Level.INFO ) )
314            {
315                this.log( Level.INFO, Messages.getMessage( "separator" ), null );
316                this.log( Level.INFO, Messages.getMessage( "applicationTitle" ), null );
317                this.log( Level.INFO, Messages.getMessage( "separator" ), null );
318                this.log( Level.INFO, Messages.getMessage( "commandInfo", this.getName() ), null );
319            }
320
321            this.preExecuteCommand( commandLine );
322            this.executeCommand( commandLine );
323            status = STATUS_SUCCESS;
324        }
325        catch ( final Throwable t )
326        {
327            this.log( Level.SEVERE, null, t );
328            status = STATUS_FAILURE;
329        }
330        finally
331        {
332            try
333            {
334                this.postExecuteCommand( commandLine );
335            }
336            catch ( final Throwable t )
337            {
338                this.log( Level.SEVERE, null, t );
339                status = STATUS_FAILURE;
340            }
341            finally
342            {
343                if ( this.executorService != null )
344                {
345                    this.executorService.shutdown();
346                    this.executorService = null;
347                }
348            }
349        }
350
351        if ( this.isLoggable( Level.INFO ) )
352        {
353            if ( status == STATUS_SUCCESS )
354            {
355                this.log( Level.INFO, Messages.getMessage( "commandSuccess", this.getName() ), null );
356            }
357            else if ( status == STATUS_FAILURE )
358            {
359                this.log( Level.INFO, Messages.getMessage( "commandFailure", this.getName() ), null );
360            }
361
362            this.log( Level.INFO, Messages.getMessage( "separator" ), null );
363        }
364
365        return status;
366    }
367
368    /**
369     * Called by the {@code execute} method prior to the {@code executeCommand} method.
370     *
371     * @param commandLine The command line to execute.
372     *
373     * @throws NullPointerException if {@code commandLine} is {@code null}.
374     * @throws CommandExecutionException if executing the command fails.
375     *
376     * @see #execute(org.apache.commons.cli.CommandLine)
377     */
378    protected void preExecuteCommand( final CommandLine commandLine ) throws CommandExecutionException
379    {
380        if ( commandLine == null )
381        {
382            throw new NullPointerException( "commandLine" );
383        }
384    }
385
386    /**
387     * Called by the {@code execute} method prior to the {@code postExecuteCommand} method.
388     *
389     * @param commandLine The command line to execute.
390     *
391     * @throws CommandExecutionException if executing the command fails.
392     *
393     * @see #execute(org.apache.commons.cli.CommandLine)
394     */
395    protected abstract void executeCommand( final CommandLine commandLine ) throws CommandExecutionException;
396
397    /**
398     * Called by the {@code execute} method after the {@code preExecuteCommand}/{@code executeCommand} methods even if
399     * those methods threw an exception.
400     *
401     * @param commandLine The command line to execute.
402     *
403     * @throws NullPointerException if {@code commandLine} is {@code null}.
404     * @throws CommandExecutionException if executing the command fails.
405     *
406     * @see #execute(org.apache.commons.cli.CommandLine)
407     */
408    protected void postExecuteCommand( final CommandLine commandLine ) throws CommandExecutionException
409    {
410        if ( commandLine == null )
411        {
412            throw new NullPointerException( "commandLine" );
413        }
414    }
415
416}