001/* 002 * ============================================================================ 003 * Copyright © 2002-2024 by Thomas Thrien. 004 * All Rights Reserved. 005 * ============================================================================ 006 * Licensed to the public under the agreements of the GNU Lesser General Public 007 * License, version 3.0 (the "License"). You may obtain a copy of the License at 008 * 009 * http://www.gnu.org/licenses/lgpl.html 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 014 * License for the specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package org.tquadrat.foundation.config.ap.impl.codebuilders; 019 020import static java.lang.String.format; 021import static javax.lang.model.element.Modifier.FINAL; 022import static javax.lang.model.element.Modifier.PRIVATE; 023import static javax.lang.model.element.Modifier.PUBLIC; 024import static org.apiguardian.api.API.Status.INTERNAL; 025import static org.apiguardian.api.API.Status.MAINTAINED; 026import static org.tquadrat.foundation.config.ap.ConfigAnnotationProcessor.MSG_INIGroupMissing; 027import static org.tquadrat.foundation.config.ap.ConfigAnnotationProcessor.MSG_INIKeyMissing; 028import static org.tquadrat.foundation.config.ap.ConfigAnnotationProcessor.MSG_INIPathMissing; 029import static org.tquadrat.foundation.config.ap.ConfigAnnotationProcessor.MSG_MissingStringConverter; 030import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.ALLOWS_INIFILE; 031import static org.tquadrat.foundation.config.ap.impl.CodeBuilder.StandardField.STD_FIELD_INIFile; 032import static org.tquadrat.foundation.config.ap.impl.CodeBuilder.StandardField.STD_FIELD_ReadLock; 033import static org.tquadrat.foundation.config.ap.impl.CodeBuilder.StandardField.STD_FIELD_WriteLock; 034import static org.tquadrat.foundation.javacomposer.Primitives.VOID; 035import static org.tquadrat.foundation.javacomposer.SuppressableWarnings.THROW_CAUGHT_LOCALLY; 036import static org.tquadrat.foundation.javacomposer.SuppressableWarnings.createSuppressWarningsAnnotation; 037import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING; 038import static org.tquadrat.foundation.util.JavaUtils.composeGetterName; 039import static org.tquadrat.foundation.util.Template.hasVariables; 040 041import java.io.FileNotFoundException; 042import java.io.IOException; 043import java.nio.file.Files; 044import java.nio.file.Path; 045import java.util.LinkedList; 046import java.util.Map; 047import java.util.Optional; 048 049import org.apiguardian.api.API; 050import org.tquadrat.foundation.annotation.ClassVersion; 051import org.tquadrat.foundation.ap.CodeGenerationError; 052import org.tquadrat.foundation.config.spi.prefs.PreferencesException; 053import org.tquadrat.foundation.inifile.INIFile; 054import org.tquadrat.foundation.javacomposer.ParameterizedTypeName; 055import org.tquadrat.foundation.lang.CommonConstants; 056import org.tquadrat.foundation.lang.Lazy; 057import org.tquadrat.foundation.util.Template; 058import org.tquadrat.foundation.util.stringconverter.PathStringConverter; 059 060/** 061 * The 062 * {@linkplain org.tquadrat.foundation.config.ap.impl.CodeBuilder code builder implementation} 063 * that connects the configuration bean to 064 * {@link INIFile}, 065 * as defined in 066 * {@link org.tquadrat.foundation.config.INIBeanSpec}. 067 * 068 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 069 * @version $Id: INIBeanBuilder.java 1129 2024-04-13 17:35:56Z tquadrat $ 070 * @UMLGraph.link 071 * @since 0.1.0 072 */ 073@SuppressWarnings( "OverlyCoupledClass" ) 074@ClassVersion( sourceVersion = "$Id: INIBeanBuilder.java 1129 2024-04-13 17:35:56Z tquadrat $" ) 075@API( status = MAINTAINED, since = "0.1.0" ) 076public final class INIBeanBuilder extends CodeBuilderBase 077{ 078 /*---------------*\ 079 ====** Inner Classes **==================================================== 080 \*---------------*/ 081 /** 082 * The various types for the initialisation of the field holding the 083 * backing 084 * {@link Path} 085 * for the 086 * {@link INIFile} 087 * instance. 088 * 089 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 090 * @version $Id: INIBeanBuilder.java 1129 2024-04-13 17:35:56Z tquadrat $ 091 * @UMLGraph.link 092 * @since 0.1.0 093 */ 094 @ClassVersion( sourceVersion = "$Id: INIBeanBuilder.java 1129 2024-04-13 17:35:56Z tquadrat $" ) 095 @API( status = INTERNAL, since = "0.1.0" ) 096 private static enum InitType 097 { 098 /** 099 * The given filename denotes an absolute path. 100 */ 101 INIT_ABSOLUTE, 102 103 /** 104 * The given filename starts with {@code $USER} and stands for a path 105 * that is to be resolved against the user's home folder. 106 */ 107 INIT_HOME, 108 109 /** 110 * The given filename starts with <code>${…}</code> and stands for a 111 * path that will be resolved against a property. 112 */ 113 INIT_PROPERTY, 114 115 /** 116 * The given filename will be resolved against the current working 117 * directory. 118 */ 119 INIT_CWD 120 } 121 // enum InitType 122 123 /*--------------*\ 124 ====** Constructors **===================================================== 125 \*--------------*/ 126 /** 127 * Creates a new instance of {@code INIBeanBuilder}. 128 * 129 * @param context The code generator context. 130 */ 131 public INIBeanBuilder( final CodeGeneratorContext context ) 132 { 133 super( context ); 134 } // INIBeanBuilder() 135 136 /*---------*\ 137 ====** Methods **========================================================== 138 \*---------*/ 139 /** 140 * {@inheritDoc} 141 */ 142 @SuppressWarnings( {"OverlyCoupledMethod", "OverlyLongMethod", "OverlyComplexMethod"} ) 143 @Override 144 public final void build() 145 { 146 //---* Parse the filename for the INIFile *---------------------------- 147 final InitType initType; 148 final Path iniFilePath; 149 String propertyName = null; 150 final var rawINIFilePath = getConfiguration().getINIFilePath() 151 .orElseThrow( () -> new CodeGenerationError( MSG_INIPathMissing ) ); 152 153 if( rawINIFilePath.startsWith( "$USER" ) ) 154 { 155 initType = InitType.INIT_HOME; 156 iniFilePath = PathStringConverter.INSTANCE.fromString( rawINIFilePath.substring( 5 ) ); 157 } 158 else if( rawINIFilePath.startsWith( "${" ) && hasVariables( rawINIFilePath ) ) 159 { 160 initType = InitType.INIT_PROPERTY; 161 final var template = new Template( rawINIFilePath ); 162 final var variables = new LinkedList<>( template.findVariables() ); 163 propertyName = variables.getFirst(); 164 iniFilePath = PathStringConverter.INSTANCE.fromString( template.replaceVariable( Map.of( propertyName, EMPTY_STRING ) ) ); 165 } 166 else 167 { 168 iniFilePath = PathStringConverter.INSTANCE.fromString( rawINIFilePath ); 169 if( iniFilePath.isAbsolute() ) 170 { 171 initType = InitType.INIT_ABSOLUTE; 172 } 173 else 174 { 175 initType = InitType.INIT_CWD; 176 } 177 } 178 179 /* 180 * Add the method that retrieves the path for the file that backs the 181 * INIFile. 182 */ 183 final var retrievePathBuilder = getComposer().methodBuilder( "retrieveINIFilePath" ) 184 .addModifiers( PRIVATE, FINAL ) 185 .addJavadoc( 186 """ 187 Returns the path for the INIFile backing file. 188 """, ExceptionInInitializerError.class 189 ) 190 .returns( Path.class, 191 """ 192 The path for the file that backs the {@code INIFile} instance.\ 193 """ ) 194 .addCode( switch( initType ) 195 { 196 case INIT_CWD -> getComposer().codeBlockBuilder() 197 .addStatement( "final var retValue = $1T.of( $2S, $3S ).toAbsolutePath()", Path.class, ".", PathStringConverter.INSTANCE.toString( iniFilePath ) ) 198 .build(); 199 case INIT_HOME -> getComposer().codeBlockBuilder() 200 .addStatement( "final var retValue = $1T.of( getProperty( PROPERTY_USER_HOME ), $2S ).toAbsolutePath()", Path.class, PathStringConverter.INSTANCE.toString( iniFilePath ) ) 201 .addStaticImport( System.class, "getProperty" ) 202 .addStaticImport( CommonConstants.class, "PROPERTY_USER_HOME" ) 203 .build(); 204 case INIT_ABSOLUTE -> getComposer().codeBlockBuilder() 205 .addStatement( "final var retValue = $1T.of( $2S ).toAbsolutePath()", Path.class, PathStringConverter.INSTANCE.toString( iniFilePath ) ) 206 .build(); 207 case INIT_PROPERTY -> getComposer().codeBlockBuilder() 208 .addStatement( "final var basePath = $1L().toAbsolutePath()", composeGetterName( propertyName ) ) 209 .addStatement( "final var retValue = basePath.resolve( $1S ).toAbsolutePath()", PathStringConverter.INSTANCE.toString( iniFilePath ) ) 210 .build(); 211 } ) 212 .addCode( getComposer().createReturnStatement() ); 213 final var retrievePathMethod = retrievePathBuilder.build(); 214 addMethod( retrievePathMethod ); 215 216 //---* Add the method that creates the INIFile instance *-------------- 217 final var createINIFileBuilder = getComposer().methodBuilder( "createINIFile" ) 218 .addModifiers( PRIVATE, FINAL ) 219 .addAnnotation( createSuppressWarningsAnnotation( getComposer(), THROW_CAUGHT_LOCALLY ) ) 220 .addJavadoc( 221 """ 222 Creates the 223 {@link INIFile} 224 instance that is connected with this configuration bean. 225 226 @throws $T Something went wrong on creating/opening the INI file.\ 227 """, ExceptionInInitializerError.class 228 ) 229 .returns( INIFile.class, 230 """ 231 The {@code INIFile} instance.\ 232 """ ) 233 .addException( ExceptionInInitializerError.class ) 234 .addStatement( "final $T retValue", INIFile.class ) 235 .addStatement( "final var path = $N()", retrievePathMethod ) 236 .beginControlFlow( 237 """ 238 try 239 """ 240 ); 241 if( getConfiguration().getINIFileMustExist() ) 242 { 243 createINIFileBuilder.beginControlFlow( 244 """ 245 if( !exists( path ) ) 246 """ 247 ) 248 .addStaticImport( Files.class, "exists" ) 249 .addStatement( "throw new $1T( path.toString() )", FileNotFoundException.class ) 250 .endControlFlow() 251 .addStatement( "retValue = $1T.open( path )", INIFile.class ); 252 } 253 else 254 { 255 final var fileComment = getConfiguration().getINIFileComment(); 256 if( fileComment.isPresent() ) 257 { 258 createINIFileBuilder.addStatement( 259 """ 260 final var isNew = !exists( path )\ 261 """ ) 262 .addStaticImport( Files.class, "exists" ) 263 .addStatement( "retValue = $T.open( path )", INIFile.class ) 264 .beginControlFlow( 265 """ 266 if( isNew ) 267 """ ) 268 .addStatement( "retValue.setComment( $S )", fileComment.get() ) 269 .endControlFlow(); 270 } 271 else 272 { 273 createINIFileBuilder.addStatement( 274 """ 275 retValue = $1T.open( path )\ 276 """, INIFile.class ); 277 } 278 } 279 createINIFileBuilder.nextControlFlow( 280 """ 281 282 catch( final $T e ) 283 """, IOException.class ) 284 .addStatement( "throw new $T( e )", ExceptionInInitializerError.class ) 285 .endControlFlow() 286 .addCode( "\n" ) 287 .addComment( "Sets the structure of the INIFile" ); 288 for( final var group : getConfiguration().getINIGroups().entrySet() ) 289 { 290 createINIFileBuilder.addStatement( "retValue.setComment( $S, $S )", group.getKey(), group.getValue() ); 291 } 292 PropertiesLoop: 293 for( var iterator = getConfiguration().propertyIterator(); iterator.hasNext(); ) 294 { 295 final var property = iterator.next(); 296 297 if( !property.hasFlag( ALLOWS_INIFILE ) ) continue PropertiesLoop; 298 if( property.getINIComment().isEmpty() ) continue PropertiesLoop; 299 final var group = property.getINIGroup().orElseThrow( () -> new CodeGenerationError( format( MSG_INIGroupMissing, property.getPropertyName() ) ) ); 300 final var key = property.getINIKey().orElseThrow( () -> new CodeGenerationError( format( MSG_INIKeyMissing, property.getPropertyName() ) ) ); 301 final var comment = property.getINIComment().orElseThrow(); 302 createINIFileBuilder.addStatement( "retValue.setComment( $S, $S, $S )", group, key, comment ); 303 } // PropertiesLoop: 304 305 final var createINIFile = createINIFileBuilder.addCode( getComposer().createReturnStatement() ) 306 .build(); 307 addMethod( createINIFile ); 308 309 //---* Add the field for the INI file *-------------------------------- 310 final var iniFileClass = ParameterizedTypeName.from( Lazy.class, INIFile.class ); 311 final var iniFile = getComposer().fieldBuilder( iniFileClass, STD_FIELD_INIFile.toString(), PRIVATE, FINAL ) 312 .addJavadoc( 313 """ 314 The INIFile instance that is used by this configuration bean to 315 persist (some of) its properties. 316 """ 317 ) 318 .build(); 319 addField( STD_FIELD_INIFile, iniFile ); 320 321 //---* Initialise the INI file *--------------------------------------- 322 final var constructorCode = getComposer().codeBlockBuilder() 323 .add( 324 """ 325 326 //---* Initialise the INI file *---------------------------------------- 327 """ 328 ) 329 .addStatement( "$1N = $2T.use( this::$3N )", iniFile, Lazy.class, createINIFile ) 330 .build(); 331 addConstructorCode( constructorCode ); 332 333 //---* Create the method that returns the INIFile *-------------------- 334 final var returnType = ParameterizedTypeName.from( Optional.class, INIFile.class ); 335 final var method = getComposer().methodBuilder( "obtainINIFile" ) 336 .addModifiers( PUBLIC, FINAL ) 337 .addAnnotation( Override.class ) 338 .returns( returnType ) 339 .addJavadoc( getComposer().createInheritDocComment() ) 340 .addStatement( "return $T.of( $N.get() )", Optional.class, iniFile ) 341 .build(); 342 addMethod( method ); 343 344 //---* The builder for the code of the loadINIFile() method *---------- 345 final var loadCodeBuilder = getComposer().codeBlockBuilder(); 346 if( isSynchronized() ) 347 { 348 loadCodeBuilder.beginControlFlow( 349 """ 350 try( final var ignore = $N.lock() ) 351 """, getField( STD_FIELD_WriteLock ) ); 352 } 353 else 354 { 355 loadCodeBuilder.beginControlFlow( 356 """ 357 try 358 """ ); 359 } 360 loadCodeBuilder.addStatement( "final var iniFile = $1N.get()", iniFile ) 361 .addStatement( "iniFile.refresh()" ) 362 .add( 363 """ 364 365 /* 366 * Load the data. 367 */ 368 """ 369 ); 370 371 //---* The builder for the code of the updateINIFile() method *-------- 372 final var updateCodeBuilder = getComposer().codeBlockBuilder(); 373 if( isSynchronized() ) 374 { 375 updateCodeBuilder.beginControlFlow( 376 """ 377 try( final var ignore = $N.lock() ) 378 """, getField( STD_FIELD_ReadLock ) ); 379 } 380 else 381 { 382 updateCodeBuilder.beginControlFlow( 383 """ 384 try 385 """ ); 386 } 387 updateCodeBuilder.addStatement( "final var iniFile = $1N.get()", iniFile ) 388 .add( 389 """ 390 391 /* 392 * Write the data. 393 */ 394 """ 395 ); 396 397 //---* Process the properties *---------------------------------------- 398 PropertiesLoop: 399 for( final var iterator = getProperties(); iterator.hasNext(); ) 400 { 401 final var propertySpec = iterator.next(); 402 /* 403 * Skip the properties that do not have a tie to the INI file. 404 */ 405 if( !propertySpec.hasFlag( ALLOWS_INIFILE ) ) continue PropertiesLoop; 406 407 final var name = propertySpec.getPropertyName(); 408 final var group = propertySpec.getINIGroup().orElseThrow( () -> new CodeGenerationError( format( MSG_INIGroupMissing, name ) ) ); 409 final var key = propertySpec.getINIKey().orElseThrow( () -> new CodeGenerationError( format( MSG_INIKeyMissing, name ) ) ); 410 final var field = propertySpec.getFieldName(); 411 final var stringConverterType = propertySpec.getStringConverterClass() 412 .orElseThrow( () -> new CodeGenerationError( format( MSG_MissingStringConverter, name ) ) ); 413 final var stringConverterCode = 414 switch( determineStringConverterInstantiation( stringConverterType, propertySpec.isEnum() ) ) 415 { 416 case BY_INSTANCE -> getComposer().statementOf( "final var stringConverter = $T.INSTANCE", stringConverterType ); 417 case THROUGH_CONSTRUCTOR -> getComposer().statementOf( "final var stringConverter = new $T()", stringConverterType ); 418 case AS_ENUM -> getComposer().statementOf( "final var stringConverter = new $1T<>( $2T.class )", stringConverterType, propertySpec.getPropertyType() ); 419 }; 420 421 //---* Load the value *-------------------------------------------- 422 loadCodeBuilder.beginControlFlow( EMPTY_STRING ) 423 .add( stringConverterCode ) 424 .addStatement( "$1N = iniFile.getValue( $2S, $3S, stringConverter ).orElse( $1N )", field, group, key ) 425 .endControlFlow(); 426 427 //---* Write the value *------------------------------------------- 428 updateCodeBuilder.beginControlFlow( EMPTY_STRING ) 429 .add( stringConverterCode ) 430 .addStatement( "iniFile.setValue( $2S, $3S, $1N, stringConverter )", field, group, key ) 431 .endControlFlow(); 432 } // PropertiesLoop: 433 434 //---* Create the loadINIFile() method *------------------------------- 435 loadCodeBuilder.nextControlFlow( 436 """ 437 438 catch( final $T e ) 439 """, IOException.class ) 440 .addStatement( "throw new $T( e )", PreferencesException.class ) 441 .endControlFlow(); 442 final var loadMethod = getComposer().methodBuilder( "loadINIFile" ) 443 .addModifiers( PUBLIC, FINAL ) 444 .addAnnotation( Override.class ) 445 .returns( VOID ) 446 .addJavadoc( getComposer().createInheritDocComment() ) 447 .addCode( loadCodeBuilder.build() ) 448 .build(); 449 addMethod( loadMethod ); 450 451 //---* Create the updateINIFile() method *----------------------------- 452 updateCodeBuilder.add( "\n" ) 453 .addStatement( "iniFile.save()" ) 454 .nextControlFlow( 455 """ 456 457 catch( final $T e ) 458 """, IOException.class ) 459 .addStatement( "throw new $T( e )", PreferencesException.class ) 460 .endControlFlow(); 461 final var updateMethod = getComposer().methodBuilder( "updateINIFile" ) 462 .addModifiers( PUBLIC, FINAL ) 463 .addAnnotation( Override.class ) 464 .returns( VOID ) 465 .addJavadoc( getComposer().createInheritDocComment() ) 466 .addCode( updateCodeBuilder.build() ) 467 .build(); 468 addMethod( updateMethod ); 469 } // build() 470} 471// class INIBeanBuilder 472 473/* 474 * End of File 475 */