001/* 002 * ============================================================================ 003 * Copyright © 2002-2024 by Thomas Thrien. 004 * All Rights Reserved. 005 * ============================================================================ 006 * 007 * Licensed to the public under the agreements of the GNU Lesser General Public 008 * License, version 3.0 (the "License"). You may obtain a copy of the License at 009 * 010 * http://www.gnu.org/licenses/lgpl.html 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 014 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 015 * License for the specific language governing permissions and limitations 016 * under the License. 017 */ 018 019package org.tquadrat.foundation.config; 020 021import static java.lang.System.err; 022import static java.util.Comparator.comparing; 023import static org.apiguardian.api.API.Status.STABLE; 024import static org.tquadrat.foundation.config.internal.CLIDefinitionParser.parse; 025import static org.tquadrat.foundation.i18n.I18nUtil.resolveText; 026import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_Object_ARRAY; 027import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING; 028import static org.tquadrat.foundation.lang.CommonConstants.UTF8; 029import static org.tquadrat.foundation.lang.Objects.nonNull; 030import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument; 031import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument; 032import static org.tquadrat.foundation.util.JavaUtils.findMainClass; 033import static org.tquadrat.foundation.util.JavaUtils.loadClass; 034 035import javax.xml.stream.XMLStreamException; 036import java.io.IOException; 037import java.io.InputStream; 038import java.io.OutputStream; 039import java.io.OutputStreamWriter; 040import java.io.Writer; 041import java.lang.reflect.InvocationTargetException; 042import java.nio.charset.Charset; 043import java.util.ArrayList; 044import java.util.Collection; 045import java.util.HashMap; 046import java.util.List; 047import java.util.Map; 048import java.util.Optional; 049import java.util.ResourceBundle; 050import java.util.concurrent.locks.ReentrantLock; 051 052import org.apiguardian.api.API; 053import org.tquadrat.foundation.annotation.ClassVersion; 054import org.tquadrat.foundation.annotation.UtilityClass; 055import org.tquadrat.foundation.config.internal.ArgumentParser; 056import org.tquadrat.foundation.config.internal.UsageBuilder; 057import org.tquadrat.foundation.config.spi.CLIArgumentDefinition; 058import org.tquadrat.foundation.config.spi.CLIDefinition; 059import org.tquadrat.foundation.config.spi.CLIOptionDefinition; 060import org.tquadrat.foundation.exception.PrivateConstructorForStaticClassCalledError; 061import org.tquadrat.foundation.exception.ValidationException; 062import org.tquadrat.foundation.function.tce.TCEBiFunction; 063import org.tquadrat.foundation.function.tce.TCEFunction; 064import org.tquadrat.foundation.lang.AutoLock; 065 066/** 067 * <p>{@summary Utility methods that can be used to handle configuration 068 * beans.}</p> 069 * <p>The main API is defined by the two methods:</p> 070 * <ul> 071 * <li>{@link #getConfiguration(Class, TCEFunction)}</li> 072 * <li>{@link #getConfiguration(Class, String, TCEBiFunction)}</li> 073 * </ul> 074 * <p>They are used to generate and return the configuration bean instance for 075 * the given configuration bean specification. For the same specification, it 076 * returns always the same instance (for an implementation of 077 * {@link SessionBeanSpec}, 078 * it will be the same instance when specification and session key are the 079 * same).</p> 080 * <p>The {@code factory} argument for 081 * {@link #getConfiguration(Class, TCEFunction)} 082 * can be a lambda like this:</p> 083 * <blockquote><pre><code>c -> c.getConstructor().newInstance()</code></pre></blockquote> 084 * <p>and for</p> 085 * {@link #getConfiguration(Class, String, TCEBiFunction)}, 086 * <p>it could be:</p> 087 * <blockquote><pre><code>(c,s) -> c.getConstructor( String.class ).newInstance( s )</code></pre></blockquote> 088 * <p>The factory is required because the code in the module 089 * {@code org.tquadrat.foundation.ui} cannot access classes in the 090 * {@code ~.generated} package of the module that uses the configuration, and 091 * that holds the generated configuration beans.</p> 092 * 093 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 094 * @version $Id: ConfigUtil.java 1105 2024-02-28 12:58:46Z tquadrat $ 095 * @since 0.0.1 096 * 097 * @UMLGraph.link 098 */ 099@UtilityClass 100@ClassVersion( sourceVersion = "$Id: ConfigUtil.java 1105 2024-02-28 12:58:46Z tquadrat $" ) 101@API( status = STABLE, since = "0.0.1" ) 102public final class ConfigUtil 103{ 104 /*------------*\ 105 ====** Attributes **======================================================= 106 \*------------*/ 107 /** 108 * <p>{@summary The registry for global configuration beans.}</p> 109 * <p>The key is the configuration bean specification interface, the 110 * value is the initialised instance of the related configuration 111 * bean.</p> 112 */ 113 @SuppressWarnings( "StaticCollection" ) 114 private static final Map<Class<? extends ConfigBeanSpec>,ConfigBeanSpec> m_ConfigurationBeanRegistry = new HashMap<>(); 115 116 /** 117 * <p>{@summary The registry for session configuration beans.}</p> 118 * <p>The key is the configuration bean specification interface, the 119 * value is a map holding the initialised instances of the related 120 * configuration beans, indexed by the session identifier.</p> 121 */ 122 @SuppressWarnings( "StaticCollection" ) 123 private static final Map<Class<? extends SessionBeanSpec>,Map<String,? extends SessionBeanSpec>> m_SessionConfigBeanRegistry = new HashMap<>(); 124 125 /*------------------------*\ 126 ====** Static Initialisations **=========================================== 127 \*------------------------*/ 128 /** 129 * The lock for the 130 * {@link #m_ConfigurationBeanRegistry}. 131 */ 132 private static final AutoLock m_ConfigurationBeanRegistryLock; 133 134 /** 135 * The lock for the 136 * {@link #m_SessionConfigBeanRegistry}. 137 */ 138 private static final AutoLock m_SessionConfigBeanRegistryLock; 139 140 static 141 { 142 //---* Creating the locks *-------------------------------------------- 143 m_ConfigurationBeanRegistryLock = AutoLock.of( new ReentrantLock() ); 144 m_SessionConfigBeanRegistryLock = AutoLock.of( new ReentrantLock() ); 145 } 146 147 /*--------------*\ 148 ====** Constructors **===================================================== 149 \*--------------*/ 150 /** 151 * No instance allowed for this class. 152 */ 153 private ConfigUtil() { throw new PrivateConstructorForStaticClassCalledError( ConfigUtil.class ); } 154 155 /*---------*\ 156 ====** Methods **========================================================== 157 \*---------*/ 158 /** 159 * <p>{@summary Drops the configuration bean for the given specification 160 * and the given session key.}</p> 161 * <p>The "session key" can be any arbitrary kind of a unique 162 * identifier: a user id, a session id, a URI, or a UUID.</p> 163 * <p>Nothing happens if the there is not configuration bean for the 164 * given specification and/or session key.</p> 165 * 166 * @param <T> The type of the configuration bean specification. 167 * @param specification The specification interface for the 168 * configuration bean. 169 * @param sessionKey The session key. 170 */ 171 @API( status = STABLE, since = "0.0.1" ) 172 public static final <T extends SessionBeanSpec> void dropConfiguration( final Class<T> specification, final String sessionKey ) 173 { 174 requireNonNullArgument( specification, "specification" ); 175 requireNotEmptyArgument( sessionKey, "sessionKey" ); 176 177 try( @SuppressWarnings( "unused" ) final var ignored = m_SessionConfigBeanRegistryLock.lock() ) 178 { 179 @SuppressWarnings( "unchecked" ) 180 final var beans = (Map<String,SessionBeanSpec>) m_SessionConfigBeanRegistry.get( specification ); 181 if( nonNull( beans ) ) beans.remove( sessionKey ); 182 } 183 } // dropConfiguration() 184 185 /** 186 * Dumps a parameter file template for the provided command line 187 * definition to the given 188 * {@link OutputStream}. 189 * 190 * @param cmdLineDefinition The command line definition. 191 * @param outputStream The target output stream. 192 * @throws IOException Something went wrong when writing to the output 193 * stream. 194 */ 195 @API( status = STABLE, since = "0.0.2" ) 196 public static final void dumpParamFileTemplate( final List<? extends CLIDefinition> cmdLineDefinition, final OutputStream outputStream ) throws IOException 197 { 198 try( final Writer writer = new OutputStreamWriter( requireNonNullArgument( outputStream, "outputStream" ), UTF8 ) ) 199 { 200 final List<CLIOptionDefinition> options = new ArrayList<>(); 201 final List<CLIArgumentDefinition> arguments = new ArrayList<>(); 202 203 for( final var definition : requireNonNullArgument( cmdLineDefinition, "cmdLineDefinition" ) ) 204 { 205 if( definition.isArgument() ) 206 { 207 arguments.add( (CLIArgumentDefinition) definition ); 208 } 209 else 210 { 211 options.add( (CLIOptionDefinition) definition ); 212 } 213 } 214 215 options.sort( comparing( CLIOptionDefinition::getSortKey ) ); 216 arguments.sort( comparing( CLIArgumentDefinition::getSortKey ) ); 217 218 for( final var definition : options ) 219 { 220 writer.append( definition.name() ) 221 .append( ' ' ) 222 .append( definition.metaVar() ) 223 .append( '\n' ); 224 } 225 for( final var definition : arguments ) 226 { 227 writer.append( definition.metaVar() ).append( '\n' ); 228 } 229 writer.flush(); 230 } 231 } // dumpParamFileTemplate() 232 233 /** 234 * Retrieves the configuration bean for the given specification. 235 * 236 * @param <T> The type of the configuration bean specification. 237 * @param specification The specification interface for the 238 * configuration bean. 239 * @param factory The factory that instantiates the configuration bean. 240 * @return The configuration bean. 241 */ 242 @API( status = STABLE, since = "0.0.1" ) 243 public static final <T extends ConfigBeanSpec> T getConfiguration( final Class<? extends T> specification, final TCEFunction<Class<T>,T> factory ) 244 { 245 requireNonNullArgument( factory, "factory" ); 246 247 final T retValue; 248 try( @SuppressWarnings( "unused" ) final var ignored = m_ConfigurationBeanRegistryLock.lock() ) 249 { 250 @SuppressWarnings( "unchecked" ) 251 final var bean = (T) m_ConfigurationBeanRegistry.computeIfAbsent( requireNonNullArgument( specification, "specification" ), aClass -> loadConfigurationBean( aClass, factory ) ); 252 retValue = bean; 253 } 254 255 //---* Done *---------------------------------------------------------- 256 return retValue; 257 } // getConfiguration() 258 259 /** 260 * <p>{@summary Retrieves the configuration bean for the given 261 * specification and the given session key.}</p> 262 * <p>The "session key" can be any arbitrary kind of a unique 263 * identifier: a user id, a session id, a URI, or a UUID.</p> 264 * 265 * @param <T> The type of the configuration bean specification. 266 * @param specification The specification interface for the 267 * configuration bean. 268 * @param sessionKey The session key. 269 * @param factory The factory that instantiates the configuration bean. 270 * @return The configuration bean. 271 */ 272 @API( status = STABLE, since = "0.0.1" ) 273 public static final <T extends SessionBeanSpec> T getConfiguration( final Class<? extends T> specification, final String sessionKey, final TCEBiFunction<Class<T>, ? super String, T> factory ) 274 { 275 requireNonNullArgument( factory, "factory" ); 276 277 final T retValue; 278 try( @SuppressWarnings( "unused" ) final var ignored = m_SessionConfigBeanRegistryLock.lock() ) 279 { 280 @SuppressWarnings( "unchecked" ) 281 final var beans = (Map<String,SessionBeanSpec>) m_SessionConfigBeanRegistry.computeIfAbsent( requireNonNullArgument( specification, "specification" ), $ -> new HashMap<>() ); 282 @SuppressWarnings( "unchecked" ) 283 final var bean = (T) beans.computeIfAbsent( requireNotEmptyArgument( sessionKey, "sessionKey" ), s -> loadSessionBean( specification, s, factory ) ); 284 retValue = bean; 285 } 286 287 //---* Done *---------------------------------------------------------- 288 return retValue; 289 } // getConfiguration() 290 291 /** 292 * Retrieves the configuration bean class and loads it. 293 * 294 * @param <T> The type of the configuration bean specification. 295 * @param specification The specification interface for the 296 * configuration bean. 297 * @param factory The factory that instantiates the configuration bean. 298 * @return The configuration bean. 299 */ 300 private static final <T extends ConfigBeanSpec> T loadConfigurationBean( final Class<? extends ConfigBeanSpec> specification, final TCEFunction<? super Class<T>, T> factory ) 301 { 302 final var className = retrieveClassName( specification ); 303 T retValue = null; 304 try 305 { 306 @SuppressWarnings( "unchecked" ) 307 final var beanClass = (Class<T>) loadClass( className, specification ).orElseThrow( () -> new ClassNotFoundException( className ) ); 308 retValue = factory.apply( beanClass ); 309 } 310 catch( final ClassNotFoundException e ) 311 { 312 throw new ValidationException( "Invalid configuration bean specification: %s".formatted( specification.getName() ), e ); 313 } 314 catch( final InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e ) 315 { 316 throw new ValidationException( "Unable to instantiate configuration bean for specification: %s".formatted( specification.getName() ), e ); 317 } 318 catch( final Exception e ) 319 { 320 throw new ValidationException( "Problems to instantiate configuration bean for specification: %s".formatted( specification.getName() ), e ); 321 } 322 323 //---* Done *---------------------------------------------------------- 324 return retValue; 325 } // loadConfigurationBean() 326 327 /** 328 * Retrieves the configuration bean class for a session bean and loads it. 329 * 330 * @param <T> The type of the configuration bean specification. 331 * @param specification The specification interface for the 332 * configuration bean. 333 * @param sessionKey The session key. 334 * @param factory The factory that instantiates the configuration bean. 335 * @return The configuration bean. 336 */ 337 private static final <T extends ConfigBeanSpec> T loadSessionBean( final Class<? extends T> specification, final String sessionKey, final TCEBiFunction<? super Class<T>, ? super String, T> factory ) 338 { 339 final var className = retrieveClassName( specification ); 340 T retValue = null; 341 try 342 { 343 @SuppressWarnings( "unchecked" ) 344 final var beanClass = (Class<T>) loadClass( className, specification ).orElseThrow( () -> new ClassNotFoundException( "className" ) ); 345 retValue = factory.apply( beanClass, sessionKey ); 346 } 347 catch( final ClassNotFoundException e ) 348 { 349 throw new ValidationException( "Invalid configuration bean specification: %s".formatted( specification.getName() ), e ); 350 } 351 catch( final InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e ) 352 { 353 throw new ValidationException( "Unable to instantiate configuration bean for specification: %s".formatted( specification.getName() ), e ); 354 } 355 catch( final Exception e ) 356 { 357 throw new ValidationException( "Problems to instantiate configuration bean for specification: %s".formatted( specification.getName() ), e ); 358 } 359 360 //---* Done *---------------------------------------------------------- 361 return retValue; 362 } // loadSessionBean() 363 364 /** 365 * Parses the given command line arguments based on the provided list of 366 * {@link CLIDefinition} 367 * instances. 368 * 369 * @param cmdLineDefinition The definition for the expected/allowed 370 * command line options and arguments. 371 * @param args The command line arguments. 372 * @throws CmdLineException The parsing failed for some reason. 373 */ 374 @API( status = STABLE, since = "0.0.1" ) 375 public static final void parseCommandLine( final Collection<? extends CLIDefinition> cmdLineDefinition, final String... args ) throws CmdLineException 376 { 377 final var parser = new ArgumentParser( cmdLineDefinition ); 378 parser.parse( args ); 379 } // parseCommandLine() 380 381 /** 382 * Parses the given command line arguments based on the given instance 383 * of 384 * {@link InputStream} 385 * that provides the XML CLI definition. In case of an invalid entry on 386 * the command line, an error message will be printed to 387 * {@link System#err}. 388 * 389 * @param cmdLineDefinition The definition for the expected/allowed 390 * command line options and arguments. 391 * @param validate {@code true} if the given XML should be validated 392 * against the schema {@code CLIDefinition.xsd} previous to parsing 393 * it, {@code false} if the validation can be omitted. 394 * @param args The command line arguments. 395 * @return The command line values; the key for the result map is the 396 * value from the 397 * <code>{@value org.tquadrat.foundation.config.internal.CLIDefinitionParser#XMLATTRIBUTE_PropertyName}</code> 398 * property. 399 * @throws CmdLineException The parsing of the command line failed for 400 * some reason. 401 * @throws XMLStreamException The parsing for the XML CLI definition 402 * failed for some reason. 403 * @throws IOException Reading the XML CLI definition failed. 404 */ 405 @API( status = STABLE, since = "0.0.1" ) 406 public static final Map<String,Object> parseCommandLine( final InputStream cmdLineDefinition, final boolean validate, final String... args ) throws CmdLineException, XMLStreamException, IOException 407 { 408 final var retValue = parseCommandLine( null, cmdLineDefinition, validate, args ); 409 410 //---* Done *---------------------------------------------------------- 411 return retValue; 412 } // parseCommandLine() 413 414 /** 415 * Parses the given command line arguments based on the given instance 416 * of 417 * {@link InputStream} 418 * that provides the XML CLI definition. In case of an invalid entry on 419 * the command line, an error message will be printed to 420 * {@link System#err}. 421 * 422 * @param resourceBundle The 423 * {@link ResourceBundle} 424 * for the messages. 425 * @param cmdLineDefinition The definition for the expected/allowed 426 * command line options and arguments. 427 * @param validate {@code true} if the given XML should be validated 428 * against the schema {@code CLIDefinition.xsd} previous to parsing 429 * it, {@code false} if the validation can be omitted. 430 * @param args The command line arguments. 431 * @return The command line values; the key for the result map is the 432 * value from the 433 * <code>{@value org.tquadrat.foundation.config.internal.CLIDefinitionParser#XMLATTRIBUTE_PropertyName}</code> 434 * property. 435 * @throws CmdLineException The parsing of the command line failed for 436 * some reason. 437 * @throws XMLStreamException The parsing for the XML CLI definition 438 * failed for some reason. 439 * @throws IOException Reading the XML CLI definition failed. 440 */ 441 @API( status = STABLE, since = "0.0.2" ) 442 public static final Map<String,Object> parseCommandLine( final ResourceBundle resourceBundle, final InputStream cmdLineDefinition, final boolean validate, final String... args ) throws CmdLineException, XMLStreamException, IOException 443 { 444 final Map<String,Object> retValue = new HashMap<>(); 445 final var cliDefinitions = parse( cmdLineDefinition, retValue, validate ); 446 final var cliParser = new ArgumentParser( cliDefinitions ); 447 try 448 { 449 cliParser.parse( args ); 450 } 451 catch( final CmdLineException e ) 452 { 453 final var command = findMainClass().orElse( "<MainClass>" ); 454 //noinspection UseOfSystemOutOrSystemErr 455 err.println( e.getMessage() ); 456 //noinspection UseOfSystemOutOrSystemErr 457 printUsage( err, Optional.ofNullable( resourceBundle ), command, cliDefinitions ); 458 throw e; 459 } 460 461 //---* Done *---------------------------------------------------------- 462 return retValue; 463 } // parseCommandLine() 464 465 /** 466 * Prints a <i>usage</i> message to the given 467 * {@link OutputStream}. 468 * 469 * @param outputStream The output stream. 470 * @param resources The resource bundle that is used for translation. 471 * @param command The command used to start the program. 472 * @param definitions The CLI definitions. 473 * @throws IOException A problem occurred on writing to the output stream. 474 */ 475 @SuppressWarnings( {"OptionalUsedAsFieldOrParameterType"} ) 476 @API( status = STABLE, since = "0.0.1" ) 477 public static final void printUsage( final OutputStream outputStream, final Optional<ResourceBundle> resources, final CharSequence command, final Collection<? extends CLIDefinition> definitions ) throws IOException 478 { 479 final var builder = new UsageBuilder( resources ); 480 final var usage = builder.build( command, definitions ); 481 requireNonNullArgument( outputStream, "outputStream" ).write( usage.getBytes( Charset.defaultCharset() ) ); 482 } // printUsage() 483 484 /** 485 * Returns the message from the given 486 * {@link CLIDefinition}. 487 * 488 * @param bundle The resource bundle. 489 * @param definition The {@code CLIDefinition}. 490 * @return The resolved message. 491 */ 492 @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" ) 493 @API( status = STABLE, since = "0.0.2" ) 494 public static final String resolveMessage( final Optional<ResourceBundle> bundle, final CLIDefinition definition ) 495 { 496 final var retValue = resolveText( bundle, requireNonNullArgument( definition, "definition" ).usage(), definition.usageKey(), EMPTY_Object_ARRAY ); 497 498 //---* Done *---------------------------------------------------------- 499 return retValue; 500 } // resolveMessage() 501 502 /** 503 * Retrieves the class name for the configuration bean from the given 504 * specification. 505 * 506 * @param specification The specification. 507 * @return The class name. 508 */ 509 private static final String retrieveClassName( final Class<? extends ConfigBeanSpec> specification ) 510 { 511 final var annotation = specification.getAnnotation( ConfigurationBeanSpecification.class ); 512 final var simpleName = annotation.name().isEmpty() ? specification.getSimpleName() + "Impl" : annotation.name(); 513 514 final var packageName = specification.getPackageName(); 515 516 final var retValue = (packageName.isEmpty() ? EMPTY_STRING : packageName + ".") + (annotation.samePackage() ? EMPTY_STRING : "generated.") + simpleName; 517 518 //---* Done *---------------------------------------------------------- 519 return retValue; 520 } // retrieveClassName() 521} 522// class ConfigUtil 523 524/* 525 * End of File 526 */