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}