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 }