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 javax.lang.model.element.Modifier.STATIC;
025import static org.apiguardian.api.API.Status.MAINTAINED;
026import static org.tquadrat.foundation.config.ap.ConfigAnnotationProcessor.DEFAULT_ACCESSOR_TYPE;
027import static org.tquadrat.foundation.config.ap.ConfigAnnotationProcessor.ENUM_ACCESSOR_TYPE;
028import static org.tquadrat.foundation.config.ap.ConfigAnnotationProcessor.LIST_ACCESSOR_TYPE;
029import static org.tquadrat.foundation.config.ap.ConfigAnnotationProcessor.MAP_ACCESSOR_TYPE;
030import static org.tquadrat.foundation.config.ap.ConfigAnnotationProcessor.MSG_MissingStringConverter;
031import static org.tquadrat.foundation.config.ap.ConfigAnnotationProcessor.MSG_MissingStringConverterWithType;
032import static org.tquadrat.foundation.config.ap.ConfigAnnotationProcessor.MSG_PreferencesNotConfigured;
033import static org.tquadrat.foundation.config.ap.ConfigAnnotationProcessor.SET_ACCESSOR_TYPE;
034import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.ALLOWS_PREFERENCES;
035import static org.tquadrat.foundation.config.ap.impl.CodeBuilder.StandardField.STD_FIELD_Accessors;
036import static org.tquadrat.foundation.config.ap.impl.CodeBuilder.StandardField.STD_FIELD_PreferenceChangeListener;
037import static org.tquadrat.foundation.config.ap.impl.CodeBuilder.StandardField.STD_FIELD_PreferencesRoot;
038import static org.tquadrat.foundation.config.ap.impl.CodeBuilder.StandardField.STD_FIELD_UserPreferences;
039import static org.tquadrat.foundation.config.ap.impl.CodeBuilder.StandardField.STD_FIELD_WriteLock;
040import static org.tquadrat.foundation.javacomposer.Primitives.VOID;
041import static org.tquadrat.foundation.javacomposer.SuppressableWarnings.USE_OF_CONCRETE_CLASS;
042import static org.tquadrat.foundation.javacomposer.SuppressableWarnings.createSuppressWarningsAnnotation;
043import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING;
044
045import java.util.HashMap;
046import java.util.Map;
047import java.util.Optional;
048import java.util.prefs.BackingStoreException;
049import java.util.prefs.Preferences;
050
051import org.apiguardian.api.API;
052import org.tquadrat.foundation.annotation.ClassVersion;
053import org.tquadrat.foundation.ap.CodeGenerationError;
054import org.tquadrat.foundation.config.spi.prefs.PreferenceAccessor;
055import org.tquadrat.foundation.config.spi.prefs.PreferencesException;
056import org.tquadrat.foundation.javacomposer.ClassName;
057import org.tquadrat.foundation.javacomposer.ParameterizedTypeName;
058import org.tquadrat.foundation.javacomposer.TypeName;
059import org.tquadrat.foundation.javacomposer.WildcardTypeName;
060import org.tquadrat.foundation.lang.Objects;
061
062/**
063 *  The
064 *  {@linkplain org.tquadrat.foundation.config.ap.impl.CodeBuilder code builder implementation}
065 *  that connects the configuration bean to
066 *  {@link java.util.prefs.Preferences},
067 *  as defined in
068 *  {@link org.tquadrat.foundation.config.PreferencesBeanSpec}.
069 *
070 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
071 *  @version $Id: PreferencesBeanBuilder.java 1105 2024-02-28 12:58:46Z tquadrat $
072 *  @UMLGraph.link
073 *  @since 0.1.0
074 */
075@ClassVersion( sourceVersion = "$Id: PreferencesBeanBuilder.java 1105 2024-02-28 12:58:46Z tquadrat $" )
076@API( status = MAINTAINED, since = "0.1.0" )
077public final class PreferencesBeanBuilder extends CodeBuilderBase
078{
079        /*--------------*\
080    ====** Constructors **=====================================================
081        \*--------------*/
082    /**
083     *  Creates a new instance of {@code PreferencesBeanBuilder}.
084     *
085     *  @param  context The code generator context.
086     */
087    public PreferencesBeanBuilder( final CodeGeneratorContext context )
088    {
089        super( context );
090    }   //  PreferencesBeanBuilder()
091
092        /*---------*\
093    ====** Methods **==========================================================
094        \*---------*/
095    /**
096     *  {@inheritDoc}
097     */
098    @SuppressWarnings( {"IfStatementWithTooManyBranches", "OverlyCoupledMethod", "OverlyLongMethod", "OverlyComplexMethod"} )
099    @Override
100    public final void build()
101    {
102        //---* Add the field for the name of the preferences root *------------
103        final var preferencesRoot = getComposer().fieldBuilder( String.class, STD_FIELD_PreferencesRoot.toString(), PUBLIC, FINAL, STATIC )
104            .addJavadoc(
105                """
106                The name for the Preferences instance: {@value}.
107                """ )
108            .initializer( "$S", getConfiguration().getPreferencesRoot() )
109            .build();
110        addField( STD_FIELD_PreferencesRoot, preferencesRoot );
111
112        //---* Create the field for the preferences root node *----------------
113        final var userPreference = getComposer().fieldBuilder( Preferences.class, STD_FIELD_UserPreferences.toString(), PRIVATE, FINAL )
114            .addJavadoc(
115                """
116                The Preferences root node.
117                """ )
118            .build();
119        addField( STD_FIELD_UserPreferences, userPreference );
120
121        //---* Add the registry for the accessor instances *-------------------
122        final var preferenceAccessorType = ParameterizedTypeName.from( ClassName.from( PreferenceAccessor.class ), WildcardTypeName.subtypeOf( Object.class ) );
123        final var registryType = ParameterizedTypeName.from( ClassName.from( Map.class ), TypeName.from( String.class ), preferenceAccessorType );
124        final var accessorRegistry = getComposer().fieldBuilder( registryType, STD_FIELD_Accessors.toString(), PRIVATE, FINAL )
125            .addJavadoc(
126                """
127                The registry for the preferences accessors.
128                """ )
129            .initializer( "new $T<>()", HashMap.class )
130            .build();
131        addField( STD_FIELD_Accessors, accessorRegistry );
132
133        final var writeLock = getField( STD_FIELD_WriteLock );
134
135        //---* Initialise the field for the preferences root node *------------
136        addConstructorCode( getComposer().codeBlockBuilder()
137            .add(
138                """
139
140                /*
141                 * Retrieve the USER Preferences.
142                 */
143                """ )
144            .addStatement( "$N = userRoot().node( $N )", userPreference, preferencesRoot )
145            .addStaticImport( Preferences.class, "userRoot" )
146            .build()
147        );
148
149        //---* Create the method that returns the preference *-----------------
150        final var returnType = ParameterizedTypeName.from( Optional.class, Preferences.class );
151        final var method = getComposer().methodBuilder( "obtainPreferencesNode" )
152            .addModifiers( PUBLIC, FINAL )
153            .addAnnotation( Override.class )
154            .returns( returnType )
155            .addJavadoc( getComposer().createInheritDocComment() )
156            .addStatement( "return $T.of( $N )", Optional.class, userPreference )
157            .build();
158        addMethod( method );
159
160        //---* The builder for the code of the loadPreferences() method *------
161        final var loadPrefsCodeBuilder = getComposer().codeBlockBuilder()
162            .beginControlFlow( """
163                try( final var ignore = $N.lock() )
164                """, writeLock );
165
166        //---* Add the preference change listener support *--------------------
167        final var prefsChangeListener = getConfiguration().getPreferenceChangeListenerClass();
168        if( prefsChangeListener.isPresent() )
169        {
170            //---* Create the field *------------------------------------------
171            final var changeListener = getComposer().fieldBuilder( prefsChangeListener.get(), STD_FIELD_PreferenceChangeListener.toString(), PRIVATE )
172                .addJavadoc(
173                    """
174                    The listener for preference changes.
175                    """ )
176                .addAnnotation( createSuppressWarningsAnnotation( getComposer(), USE_OF_CONCRETE_CLASS ) )
177                .initializer( "$L", "null" )
178                .build();
179            addField( STD_FIELD_PreferenceChangeListener, changeListener );
180
181            /*
182             * The listener itself will be instantiated only when
183             * loadPreferences() is called the first time.
184             */
185            loadPrefsCodeBuilder.add(
186                    """
187                    /*
188                     * Create the preference change listener.
189                     */
190                    """ )
191                .beginControlFlow(
192                    """
193                    if( isNull( $N ) )
194                    """, changeListener
195                )
196                .addStaticImport( Objects.class, "isNull" )
197                .addStatement( "$N = new $T( $N, $N )", changeListener, prefsChangeListener.get(), accessorRegistry, writeLock )
198                .addStatement( "$N.addPreferenceChangeListener( $N )", userPreference, changeListener )
199                .endControlFlow();
200        }
201        loadPrefsCodeBuilder.add(
202                """
203                /*
204                 * Synchronise the preferences backing store with the memory.
205                 */
206                """
207            )
208            .addStatement( "$N.sync()", userPreference )
209            .add(
210                """
211                
212                /*
213                 * Load the data.
214                 */
215                """
216            );
217
218        //---* The builder for the code of the updatePreferences() method *----
219        final var updatePrefsCodeBuilder = getComposer().codeBlockBuilder()
220            .beginControlFlow( """
221                try( final var ignore = $N.lock() )
222                """, writeLock );
223
224        addConstructorCode( getComposer().codeBlockOf( """
225
226            /*
227             * Initialise the registry for the preference accessor instances.
228             */
229            """ ) );
230
231        //---* Process the properties *----------------------------------------
232        PropertiesLoop: for( final var iterator = getProperties(); iterator.hasNext(); )
233        {
234            final var propertySpec = iterator.next();
235            /*
236             * Skip the properties that do not have a tie to the preferences.
237             */
238            if( !propertySpec.hasFlag( ALLOWS_PREFERENCES ) ) continue PropertiesLoop;
239
240            final var name = propertySpec.getPropertyName();
241            final var key = propertySpec.getPrefsKey().orElseThrow( () -> new CodeGenerationError( format( MSG_PreferencesNotConfigured, name ) ) );
242            final var accessorClass = propertySpec.getPrefsAccessorClass().orElseThrow( () -> new CodeGenerationError( format( MSG_PreferencesNotConfigured, name ) ) );
243            final var field = propertySpec.getFieldName();
244
245            //---* Add the code for the Constructor *--------------------------
246            final var getter = getComposer().lambdaBuilder()
247                .addCode( "$N", field )
248                .build();
249            final var setter = getComposer().lambdaBuilder()
250                .addParameter( "p" )
251                .addCode( "$N = p", field )
252                .build();
253            final var codeBlockBuilder = getComposer().codeBlockBuilder();
254
255            if( accessorClass.equals( ENUM_ACCESSOR_TYPE ) )
256            {
257                final var propertyType = propertySpec.getPropertyType();
258                codeBlockBuilder.addStatement( "$1N.put( $2S, new $3T<>( $2S, $4T.class, $5L, $6L ) )", accessorRegistry, key, accessorClass, propertyType, getter, setter );
259            }
260            else if( accessorClass.equals( LIST_ACCESSOR_TYPE ) || accessorClass.equals( SET_ACCESSOR_TYPE ) )
261            {
262                final var propertyType = (ParameterizedTypeName) propertySpec.getPropertyType();
263                final var argumentType = propertyType.typeArguments().getFirst();
264                final var stringConverterType = getStringConverter( argumentType )
265                    .orElseThrow( () -> new CodeGenerationError( format( MSG_MissingStringConverterWithType, name, argumentType.toString() ) ) );
266                switch( determineStringConverterInstantiation( stringConverterType, false ) )
267                {
268                    case BY_INSTANCE -> codeBlockBuilder.addStatement( "$1N.put( $2S, new $3T<>( $2S, $4T.INSTANCE, $5L, $6L ) )", accessorRegistry, key, accessorClass, stringConverterType, getter, setter );
269                    case THROUGH_CONSTRUCTOR -> codeBlockBuilder.addStatement( "$1N.put( $2S, new $3T<>( $2S, new $4T(), $5L, $6L ) )", accessorRegistry, key, accessorClass, stringConverterType, getter, setter );
270                    case AS_ENUM -> codeBlockBuilder.addStatement( "$1N.put( $2S, new $3T<>( $2S, new $4T( $7T), $5L, $6L ) )", accessorRegistry, key, accessorClass, stringConverterType, getter, setter, propertyType );
271                }
272            }
273            else if( accessorClass.equals( MAP_ACCESSOR_TYPE ) )
274            {
275                final var propertyType = (ParameterizedTypeName) propertySpec.getPropertyType();
276                final var argumentTypes = propertyType.typeArguments();
277                final var keyStringConverterType = getStringConverter( argumentTypes.getFirst() )
278                    .orElseThrow( () -> new CodeGenerationError( format( MSG_MissingStringConverterWithType, name, argumentTypes.getFirst().toString() ) ) );
279                final var keySnippet =
280                    switch( determineStringConverterInstantiation( keyStringConverterType, false ) )
281                    {
282                        case BY_INSTANCE -> "$4T.INSTANCE";
283                        case THROUGH_CONSTRUCTOR -> "new $4T()";
284                        case AS_ENUM -> EMPTY_STRING;
285                    };
286                final var valueStringConverterType = getStringConverter( argumentTypes.get( 1 ) )
287                    .orElseThrow( () -> new CodeGenerationError( format( MSG_MissingStringConverterWithType, name, argumentTypes.get( 1 ).toString() ) ) );
288                final var valueSnippet =
289                    switch( determineStringConverterInstantiation( valueStringConverterType, false ) )
290                    {
291                        case BY_INSTANCE -> "$5T.INSTANCE";
292                        case THROUGH_CONSTRUCTOR -> "new $5T()";
293                        case AS_ENUM -> EMPTY_STRING;
294                    };
295                codeBlockBuilder.addStatement( "$1N.put( $2S, new $3T<>( $2S, %1$s, %2$s, $6L, $7L ) )".formatted( keySnippet, valueSnippet ), accessorRegistry, key, accessorClass, keyStringConverterType, valueStringConverterType, getter, setter );
296            }
297            else if( accessorClass.equals( DEFAULT_ACCESSOR_TYPE ) )
298            {
299                final var stringConverterType = propertySpec.getStringConverterClass()
300                    .orElseThrow( () -> new CodeGenerationError( format( MSG_MissingStringConverter, name ) ) );
301                switch( determineStringConverterInstantiation( stringConverterType, propertySpec.isEnum() ) )
302                {
303                    case BY_INSTANCE -> codeBlockBuilder.addStatement( "$1N.put( $2S, new $3T<>( $2S, $5L, $6L, $4T.INSTANCE ) )", accessorRegistry, key, accessorClass, stringConverterType, getter, setter );
304                    case THROUGH_CONSTRUCTOR -> codeBlockBuilder.addStatement( "$1N.put( $2S, new $3T<>( $2S, $5L, $6L, new $4T() ) )", accessorRegistry, key, accessorClass, stringConverterType, getter, setter );
305                    case AS_ENUM -> codeBlockBuilder.addStatement( "$1N.put( $2S, new $3T<>( $2S, $5L, $6L, new $4T( $7T ) ) )", accessorRegistry, key, accessorClass, stringConverterType, getter, setter, propertySpec.getPropertyType() );
306                }
307            }
308            else
309            {
310                codeBlockBuilder.addStatement( "$1N.put( $2S, new $3T( $2S, $4L, $5L ) )", accessorRegistry, key, accessorClass, getter, setter );
311            }
312            addConstructorCode( codeBlockBuilder.build() );
313
314            //---* Add the code for loadPreferences() *------------------------
315            loadPrefsCodeBuilder.addStatement( "$N.get( $S ).readPreference( $N )", accessorRegistry, key, userPreference );
316
317            //---* Add the code for updatePreferences() *----------------------
318            updatePrefsCodeBuilder.addStatement( "$N.get( $S ).writePreference( $N )", accessorRegistry, key, userPreference );
319        }   //  PropertiesLoop:
320
321        //---* Create the loadPreferences() method *---------------------------
322        loadPrefsCodeBuilder.nextControlFlow(
323            """
324
325            catch( final $T e )
326            """, BackingStoreException.class )
327            .addStatement( "throw new $T( e )", PreferencesException.class )
328            .endControlFlow();
329        final var loadPrefsMethod = getComposer().methodBuilder( "loadPreferences" )
330            .addModifiers( PUBLIC, FINAL )
331            .addAnnotation( Override.class )
332            .returns( VOID )
333            .addJavadoc( getComposer().createInheritDocComment() )
334            .addCode( loadPrefsCodeBuilder.build() )
335            .build();
336        addMethod( loadPrefsMethod );
337
338        //---* Create the updatePreferences() method *-------------------------
339        updatePrefsCodeBuilder.add( "\n" )
340            .addStatement( "$N.flush()", userPreference )
341            .nextControlFlow(
342                """
343
344                catch( final $T e )
345                """, BackingStoreException.class )
346            .addStatement( "throw new $T( e )", PreferencesException.class )
347            .endControlFlow();
348        final var updatePrefsMethod = getComposer().methodBuilder( "updatePreferences" )
349            .addModifiers( PUBLIC, FINAL )
350            .addAnnotation( Override.class )
351            .returns( VOID )
352            .addJavadoc( getComposer().createInheritDocComment() )
353            .addCode( updatePrefsCodeBuilder.build() )
354            .build();
355        addMethod( updatePrefsMethod );
356    }   //  build()
357}
358//  class PreferencesBeanBuilder
359
360/*
361 *  End of File
362 */