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: AbstractCommand.java 5299 2016-08-30 01:50:13Z schulte $
29   *
30   */
31  package org.jomc.cli.commands;
32  
33  import java.util.List;
34  import java.util.Locale;
35  import java.util.concurrent.CopyOnWriteArrayList;
36  import java.util.concurrent.ExecutorService;
37  import java.util.concurrent.Executors;
38  import java.util.concurrent.ThreadFactory;
39  import java.util.concurrent.atomic.AtomicInteger;
40  import java.util.logging.Level;
41  import org.apache.commons.cli.CommandLine;
42  import org.jomc.cli.Command;
43  
44  /**
45   * Base {@code Command} implementation.
46   *
47   * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
48   */
49  public abstract class AbstractCommand implements Command
50  {
51  
52      /**
53       * Default log level.
54       */
55      private static volatile Level defaultLogLevel;
56  
57      /**
58       * Log level of the instance.
59       */
60      private volatile Level logLevel;
61  
62      /**
63       * The listeners of the instance.
64       */
65      private volatile List<Listener> listeners = new CopyOnWriteArrayList<Listener>();
66  
67      /**
68       * The {@code ExecutorService} of the command.
69       *
70       * @since 1.10
71       */
72      private volatile ExecutorService executorService;
73  
74      /**
75       * Creates a new {@code AbstractCommand} instance.
76       */
77      public AbstractCommand()
78      {
79          super();
80      }
81  
82      /**
83       * Gets the default log level events are logged at.
84       * <p>
85       * The default log level is controlled by system property
86       * {@code org.jomc.cli.commands.AbstractCommand.defaultLogLevel} holding the log level to log events at by
87       * default. If that property is not set, the {@code WARNING} default is returned.
88       * </p>
89       *
90       * @return The log level events are logged at by default.
91       *
92       * @see #getLogLevel()
93       * @see Level#parse(java.lang.String)
94       */
95      public static Level getDefaultLogLevel()
96      {
97          if ( defaultLogLevel == null )
98          {
99              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 }