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 */