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;
019
020import static java.lang.String.format;
021import static java.util.Collections.unmodifiableMap;
022import static org.apiguardian.api.API.Status.MAINTAINED;
023import static org.tquadrat.foundation.config.ap.ConfigAnnotationProcessor.MSG_DuplicateProperty;
024import static org.tquadrat.foundation.config.ap.ConfigAnnotationProcessor.MSG_IllegalImplementation;
025import static org.tquadrat.foundation.lang.Objects.isNull;
026import static org.tquadrat.foundation.lang.Objects.nonNull;
027import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
028import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument;
029import static org.tquadrat.foundation.util.Comparators.caseInsensitiveComparator;
030import static org.tquadrat.foundation.util.StringUtils.isEmptyOrBlank;
031import static org.tquadrat.foundation.util.StringUtils.isNotEmptyOrBlank;
032
033import javax.lang.model.element.Name;
034import javax.lang.model.util.Elements;
035import java.lang.reflect.Type;
036import java.time.Instant;
037import java.util.Collection;
038import java.util.HashSet;
039import java.util.Iterator;
040import java.util.List;
041import java.util.Map;
042import java.util.Optional;
043import java.util.SortedMap;
044import java.util.TreeMap;
045
046import org.apiguardian.api.API;
047import org.tquadrat.foundation.annotation.ClassVersion;
048import org.tquadrat.foundation.ap.APHelper;
049import org.tquadrat.foundation.ap.CodeGenerationError;
050import org.tquadrat.foundation.config.INIGroup;
051import org.tquadrat.foundation.config.ap.impl.PropertySpecImpl;
052import org.tquadrat.foundation.javacomposer.ClassName;
053import org.tquadrat.foundation.javacomposer.JavaComposer;
054import org.tquadrat.foundation.javacomposer.MethodSpec;
055import org.tquadrat.foundation.javacomposer.TypeName;
056
057/**
058 *  An instance of this class provides the configuration for the code
059 *  generation, and it collects the results from the different code generators.
060 *
061 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
062 *  @version $Id: CodeGenerationConfiguration.java 1105 2024-02-28 12:58:46Z tquadrat $
063 *  @UMLGraph.link
064 *  @since 0.1.0
065 */
066@SuppressWarnings( {"ClassWithTooManyFields", "ClassWithTooManyMethods"} )
067@ClassVersion( sourceVersion = "$Id: CodeGenerationConfiguration.java 1105 2024-02-28 12:58:46Z tquadrat $" )
068@API( status = MAINTAINED, since = "0.1.0" )
069public final class CodeGenerationConfiguration
070{
071        /*------------*\
072    ====** Attributes **=======================================================
073        \*------------*/
074    /**
075     *  The base bundle name for the resource bundle if i18n support is
076     *  required.
077     *
078     *  @note   This is <i>not</i> the value but the fully qualified field name
079     *      where to find that value!
080     */
081    private String m_BaseBundleName = null;
082
083    /**
084     *  The optional base class for the new configuration bean.
085     */
086    private final TypeName m_BaseClass;
087
088    /**
089     *  The build time; this is provided by the configuration as this allows
090     *  easier testing.
091     */
092    private final Instant m_BuildTime = Instant.now();
093
094    /**
095     * The class name for the configuration bean class, without the package.
096     */
097    private final Name m_ClassName;
098
099    /**
100     * The
101     * {@link JavaComposer}
102     * instance that is used for the code generation.
103     */
104    @SuppressWarnings( "UseOfConcreteClass" )
105    private final JavaComposer m_Composer;
106
107    /**
108     *  The processing environment.
109     */
110    private final APHelper m_Environment;
111
112    /**
113     *  The comment for the configuration file.
114     */
115    private String m_INIFileComment = null;
116
117    /**
118     *  The flag that indicates whether the configuration must exist prior to
119     *  the first open attempt.
120     */
121    private boolean m_INIFileMustExist;
122
123    /**
124     *  The name for the configuration file.
125     */
126    private String m_INIFilePath = null;
127
128    /**
129     *  The {@code INI} file groups. The key is the name of the group, the
130     *  value is the respective comment for the group.
131     */
132    private final Map<String,String> m_INIGroups = new TreeMap<>();
133
134    /**
135     *  The method that is provided as a source for the initialisation of the
136     *  properties of the configuration bean.
137     */
138    private MethodSpec m_InitDataMethod = null;
139
140    /**
141     * The name of the resource that is used to initialise the properties of
142     * the configuration bean.
143     */
144    private String m_InitDataResource = null;
145
146    /**
147     *  The interfaces that are extended by the configuration bean
148     *  specification.
149     */
150    private final Collection<TypeName> m_InterfacesToImplement = new HashSet<>();
151
152    /**
153     *  The message prefix for the i18n support.
154     *
155     *  @note   This is <i>not</i> the value but the fully qualified field name
156     *      where to find that value!
157     */
158    private String m_MessagePrefix = null;
159
160    /**
161     * The name of the package for the configuration bean.
162     */
163    private final Name m_PackageName;
164
165    /**
166     *  The change listener class for the {@code Preferences}.
167     */
168    private TypeName m_PreferenceChangeListenerClass;
169
170    /**
171     *  <p>{@summary The properties for the configuration bean.} The name of
172     *  the property is the key.</p>
173     */
174    private final SortedMap<String, PropertySpecImpl> m_Properties = new TreeMap<>( caseInsensitiveComparator() );
175
176    /**
177     *  The configuration bean specification.
178     */
179    private final ClassName m_Specification;
180
181    /**
182     *  The name of the preferences root node.
183     */
184    private String m_PreferencesRoot = null;
185
186    /**
187     *  This flag indicates whether the access to the configuration bean
188     *  properties must be thread-safe.
189     */
190    private final boolean m_SynchronizeAccess;
191
192        /*--------------*\
193    ====** Constructors **=====================================================
194        \*--------------*/
195    /**
196     *  Creates an instance of {@code CodeGenerationConfiguration}.
197     *
198     *  @param  environment The access to the processing environment.
199     *  @param  composer    The
200     *      {@link JavaComposer}
201     *      instance that is used for the code generation.
202     *  @param  specification   The configuration specification, the interface
203     *      that defines the configuration bean.
204     *  @param  className   The name for the configuration bean class, without
205     *      the package name.
206     *  @param  packageName The name of the package for the configuration bean
207     *      class.
208     *  @param  baseClass   The optional base class for the new configuration
209     *      bean class.
210     *  @param  synchronizeAccess   {@code true} if the access to the
211     *      configuration bean properties should be thread safe, {@code false}
212     *      if a synchronisation/locking is not required.
213     */
214    @SuppressWarnings( {"UseOfConcreteClass", "ConstructorWithTooManyParameters"} )
215    public CodeGenerationConfiguration( final APHelper environment, final JavaComposer composer, final ClassName specification, final Name className, final Name packageName, final TypeName baseClass, final boolean synchronizeAccess )
216    {
217        m_Environment = requireNonNullArgument( environment, "environment" );
218        m_Composer = requireNonNullArgument( composer, "composer" );
219        m_ClassName = requireNotEmptyArgument( className, "className" );
220        m_PackageName = requireNonNullArgument( packageName, "packageName" );
221        m_Specification = requireNonNullArgument( specification, "specification" );
222        m_BaseClass = baseClass;
223        m_SynchronizeAccess = synchronizeAccess;
224    }   //  CodeGenerationConfiguration()
225
226        /*---------*\
227    ====** Methods **==========================================================
228        \*---------*/
229    /**
230     *  Adds an {@code INI} file group.
231     *
232     *  @param  name    The name for the group.
233     *  @param  comment The comment for the group.
234     */
235    public final void addINIGroup( final String name, final String comment )
236    {
237        if( isNotEmptyOrBlank( name ) )
238        {
239            if( isEmptyOrBlank( comment ) )
240            {
241                m_INIGroups.remove( name );
242            }
243            else
244            {
245                m_INIGroups.put( name, comment );
246            }
247        }
248    }   //  addINIGroup()
249
250    /**
251     *  Adds an {@code INI} file group.
252     *
253     *  @param  group   The group definition.
254     */
255    public final void addINIGroup( final INIGroup group )
256    {
257        addINIGroup( requireNonNullArgument( group, "group" ).name(), group.comment() );
258    }   //  addINIGroup()
259
260    /**
261     *  Adds some interfaces that have to be implemented by the new
262     *  configuration beam.
263     *
264     *  @param  interfacesToImplement   The classes for the interfaces to
265     *      implement.
266     */
267    public final void addInterfacesToImplement( final Collection<? extends TypeName> interfacesToImplement )
268    {
269        m_InterfacesToImplement.addAll( requireNotEmptyArgument( interfacesToImplement, "interfacesToImplement" ) );
270    }   //  addInterfaceToImplement()
271
272    /**
273     *  <p>{@summary Adds a property to the new configuration bean.}</p>
274     *  <p>If a property with the same name as the new one already exists, a
275     *  {@link CodeGenerationError}
276     *  will be thrown.</p>
277     *
278     *  @param  property    The property
279     *  @throws CodeGenerationError There is a duplicate property.
280     */
281    public final void addProperty( final PropertySpec property ) throws CodeGenerationError
282    {
283        if( requireNonNullArgument( property, "property" ) instanceof final PropertySpecImpl propertyImpl )
284        {
285            final var propertyName = propertyImpl.getPropertyName();
286            if( nonNull( m_Properties.put( propertyName, propertyImpl ) ) )
287            {
288                throw new CodeGenerationError( format( MSG_DuplicateProperty, propertyName ) );
289            }
290        }
291        else
292        {
293            throw new CodeGenerationError( format( MSG_IllegalImplementation, PropertySpec.class.getName(), property.getClass().getName() ) );
294        }
295    }   //  addProperty()
296
297    /**
298     *  Returns the name of the field that holds the base bundle name for the
299     *  resource bundle, in case i18n support is configured.
300     *
301     *  @return An instance of
302     *      {@link Optional}
303     *      holding the fully qualified field name.
304     */
305    public final Optional<String> getBaseBundleName() { return Optional.ofNullable( m_BaseBundleName ); }
306
307    /**
308     *  Returns the base class for the new configuration bean.
309     *
310     *  @return An instance of
311     *      {@link Optional}
312     *      that holds the base class.
313     */
314    public final Optional<TypeName> getBaseClass() { return Optional.ofNullable( m_BaseClass ); }
315
316    /**
317     *  Returns the build time for the new configuration bean.
318     *
319     *  @return The build time.
320     */
321    public final Instant getBuildTime() { return m_BuildTime; }
322
323    /**
324     *  Returns the name of the class for the new configuration bean.
325     *
326     *  @return The class name.
327     */
328    public final Name getClassName() { return m_ClassName; }
329
330    /**
331     *  Returns the
332     *  {@link JavaComposer}
333     *  instance that is used for the code generation.
334     *
335     *  @return The composer instance.
336     */
337    public final JavaComposer getComposer() { return m_Composer; }
338
339    /**
340     *  Returns an implementation of some utility methods for operating on
341     *  elements.
342     *
343     *  @return The element utilities.
344     */
345    public final Elements getElementUtils() { return getEnvironment().getElementUtils(); }
346
347    /**
348     *  Returns the processing environment.
349     *
350     *  @return The processing environment.
351     */
352    public final APHelper getEnvironment() { return m_Environment; }
353
354    /**
355     *  Returns the comment for the {@code INI} file.
356     *
357     *  @return The comment for the configuration file.
358     */
359    public final Optional<String> getINIFileComment() { return Optional.ofNullable( m_INIFileComment ); }
360
361    /**
362     *  Returns the flag that indicates whether the configuration file must
363     *  exist before the program starts.
364     *
365     *  @return {@code true} if the file must exist already, {@code false} if
366     *      it will be created on startup.
367     *
368     * @see org.tquadrat.foundation.config.INIFileConfig#mustExist()
369     */
370    @SuppressWarnings( "BooleanMethodNameMustStartWithQuestion" )
371    public final boolean getINIFileMustExist() { return m_INIFileMustExist; }
372
373    /**
374     *  Returns the name for the file that backs the
375     *  {@link org.tquadrat.foundation.inifile.INIFile}
376     *  instance used by the generated configuration bean.
377     *
378     *  @return An instance of
379     *      {@link Optional}
380     *      that holds the filename.
381     */
382    public final Optional<String> getINIFilePath() { return Optional.ofNullable( m_INIFilePath ); }
383
384    /**
385     *  Returns the {@code INI} file groups.
386     *
387     *  @return The group names and the related comments.
388     */
389    public final Map<String,String> getINIGroups() { return unmodifiableMap( m_INIGroups ); }
390
391    /**
392     *  <p>{@summary Returns the method that is provided as a source for the
393     *  initialisation of the properties of the configuration bean.}</p>
394     *  <p>It it is {@code default} or {@code static}, the implementation of
395     *  the method has to be part of the configuration bean specification
396     *  interface itself, otherwise it has to be implemented in base class.</p>
397     *  <p>It is an error if there is an {@code initData()} method that is
398     *  neither {@code default} nor {@code static}, and no base class is
399     *  defined with the
400     *  {@link org.tquadrat.foundation.config.ConfigurationBeanSpecification &#64;ConfigurationBeanSpecificatgion}
401     *  annotation.</p>
402     *  <p>The signature for the method has to be</p>
403     *  <pre><code>public Map&lt;String,Object&gt; initData() throws Exception</code></pre>
404     *  <p>and the return value is a map with the initialisation values, where
405     *  the property names are the keys.</p>
406     *
407     *  @return An instance of
408     *      {@link Optional}
409     *      that holds the method.
410     *
411     *  @see org.tquadrat.foundation.config.ConfigurationBeanSpecification#baseClass()
412     */
413    public final Optional<MethodSpec> getInitDataMethod() { return Optional.ofNullable( m_InitDataMethod ); }
414
415    /**
416     *  Returns the name of the resource that is used to initialise the
417     *  properties of the configuration bean.
418     *
419     *  @return An instance of
420     *      {@link Optional}
421     *      that holds the resource name.
422     */
423    public final Optional<String> getInitDataResource() { return Optional.ofNullable( m_InitDataResource ); }
424
425    /**
426     *  Returns the interfaces that have to be implemented by the new
427     *  configuration bean.
428     *
429     *  @return The interfaces to implement.
430     */
431    public final Collection<TypeName> getInterfacesToImplement() { return List.copyOf( m_InterfacesToImplement ); }
432
433    /**
434     *  Returns the name of the field that holds the message prefix, in case
435     *  i18n support is configured.
436     *
437     *  @return An instance of
438     *      {@link Optional}
439     *      holding the fully qualified field name.
440     */
441    public final Optional<String> getMessagePrefix() { return Optional.ofNullable( m_MessagePrefix ); }
442
443    /**
444     *  Returns the name of the package for the new configuration bean.
445     *
446     *  @return The package name.
447     */
448    public final Name getPackageName() { return m_PackageName; }
449
450    /**
451     *  <p>{@summary Returns the class for the {@code Preferences} change
452     *  listener.} If no listener class is defined, the change listener support
453     *  for the {@code Preferences} will be omitted.
454     *
455     *  @return An instance of
456     *      {@link Optional}
457     *      that holds the listener class.
458     *
459     *  @see org.tquadrat.foundation.config.PreferencesRoot#changeListenerClass()
460     */
461    public final Optional<TypeName> getPreferenceChangeListenerClass()
462    {
463        final var retValue = Optional.ofNullable( m_PreferenceChangeListenerClass );
464
465        //---* Done *----------------------------------------------------------
466        return retValue;
467    }   //  getPreferenceChangeListenerClass()
468
469    /**
470     *  Returns the name for the {@code Preferences} root node.
471     *
472     *  @return The name for the {@code Preferences} root node.
473     *
474     *  @see org.tquadrat.foundation.config.PreferencesRoot#nodeName()
475     */
476    public final String getPreferencesRoot()
477    {
478        final var retValue = isNotEmptyOrBlank( m_PreferencesRoot) ? m_PreferencesRoot : m_Specification.canonicalName();
479
480        //---* Done *----------------------------------------------------------
481        return retValue;
482    }   //  getPreferencesRoot()
483    /**
484     *  Returns the property with the given name.
485     *
486     *  @param  propertyName    The name of the property.
487     *  @return An instance of
488     *      {@link Optional}
489     *      that holds the retrieved property.
490     */
491    public final Optional<PropertySpec> getProperty( final String propertyName )
492    {
493        final Optional<PropertySpec> retValue = Optional.ofNullable( m_Properties.get( requireNotEmptyArgument( propertyName, "propertyName" ) ) );
494
495        //---* Done *----------------------------------------------------------
496        return retValue;
497    }   //  getProperty()
498
499    /**
500     *  Returns the configuration bean specification.
501     *
502     *  @return The configuration bean specification.
503     */
504    public final ClassName getSpecification() { return m_Specification;}
505
506    /**
507     *  Returns the flag that controls whether the generated code for the
508     *  access to the configuration bean properties has to be thread-safe.
509     *
510     *  @return {@code true} if synchronisation/locking is required,
511     *      {@code false} if not.
512     */
513    @SuppressWarnings( "BooleanMethodNameMustStartWithQuestion" )
514    public final boolean getSynchronizationRequired() { return m_SynchronizeAccess; }
515
516    /**
517     *  Checks whether a property with the given name does already exist.
518     *
519     *  @param  propertyName    The name of the property.
520     *  @return {@code true} if the property with the given name already
521     *      exists, {@code false} otherwise.
522     */
523    public final boolean hasProperty( final String propertyName ) { return m_Properties.containsKey( propertyName ); }
524
525    /**
526     *  Checks whether the given interface must be implemented by the new
527     *  configuration bean.
528     *
529     *  @param  interfaceToImplement    The class for the interface that has to
530     *      be implemented.
531     *  @return {@code true} if the given interface must be implemented,
532     *      {@code false} otherwise.
533     */
534    @SuppressWarnings( "BooleanMethodNameMustStartWithQuestion" )
535    public final boolean implementInterface( final TypeName interfaceToImplement )
536    {
537        final var retValue = m_InterfacesToImplement.contains( requireNonNullArgument( interfaceToImplement, "interfaceToImplement" ) );
538
539        //---* Done *----------------------------------------------------------
540        return retValue;
541    }   //  implementInterface()
542
543    /**
544     *  Checks whether the given interface must be implemented by the new
545     *  configuration bean.
546     *
547     *  @param  interfaceToImplement    The class for the interface that has to
548     *      be implemented.
549     *  @return {@code true} if the given interface must be implemented,
550     *      {@code false} otherwise.
551     */
552    @SuppressWarnings( "BooleanMethodNameMustStartWithQuestion" )
553    public final boolean implementInterface( final Type interfaceToImplement )
554    {
555        final var element = TypeName.from( interfaceToImplement );
556        final var retValue = implementInterface( element );
557
558        //---* Done *----------------------------------------------------------
559        return retValue;
560    }   //  implementInterface()
561
562    /**
563     *  Returns an
564     *  {@link java.util.Iterator}
565     *  over the defined properties.
566     *
567     *  @return The iterator.
568     */
569    public final Iterator<PropertySpec> propertyIterator()
570    {
571        final var iterator = m_Properties.values().iterator();
572        @SuppressWarnings( {"AnonymousInnerClass"} )
573        final Iterator<PropertySpec> retValue = new Iterator<>()
574        {
575            /**
576             *  {@inheritDoc}
577             */
578            @Override
579            public final boolean hasNext() { return iterator.hasNext(); }
580
581            /**
582             *  {@inheritDoc}
583             */
584            @Override
585            public final PropertySpec next() { return iterator.next(); }
586        };
587
588        //---* Done *----------------------------------------------------------
589        return retValue;
590    }   //  propertyIterator()
591
592    /**
593     *  Sets the i18n parameters.
594     *
595     *  @param  messagePrefix   The value for the message prefix.
596     *  @param  baseBundleName  The name of the base bundle.
597     */
598    public final void setI18NParameters( final String messagePrefix, final String baseBundleName )
599    {
600        m_MessagePrefix = messagePrefix;
601        m_BaseBundleName = baseBundleName;
602    }   //  setI18NParameters()
603
604    /**
605     *  Sets the configuration for the {@code INI} file.
606     *
607     *  @param  filename    The path; can be {@code null}.
608     *  @param  flag    The flag that indicates whether the configuration file
609     *      must exist before the program starts.{@code true} if the file must
610     *      exist, {@code false} if it will be created on startup.
611     *  @param  comment The comment; can be {@code null}.
612     */
613    public final void setINIFileConfig( final String filename, final boolean flag,  final String comment )
614    {
615        m_INIFilePath = filename;
616        m_INIFileComment = isNull( m_INIFilePath ) ? null : comment;
617        m_INIFileMustExist = flag;
618    }   //  setINIFileConfig()
619
620    /**
621     *  Set the method that is provided as a source for the initialisation of
622     *  the properties of the configuration bean.
623     *
624     *  @param  method  The method; can be {@code null}.
625     */
626    public final void setInitDataMethod( final MethodSpec method ) { m_InitDataMethod = method; }
627
628    /**
629     *  Set the name of the resource that is used to initialise the
630     *  properties of the configuration bean.
631     *
632     *  @param  initDataResource    The resource name; can be {@code null}.
633     */
634    public final void setInitDataResource( final String initDataResource ) { m_InitDataResource = initDataResource; }
635
636    /**
637     *  Sets the class for the {@code Preferences} change listener.
638     *
639     *  @param  listenerClass   The listener class; can be {@code null}.
640     *
641     *  @see org.tquadrat.foundation.config.PreferencesRoot#changeListenerClass()
642     */
643    public final void setPreferenceChangeListenerClass( final TypeName listenerClass )
644    {
645        m_PreferenceChangeListenerClass = listenerClass;
646    }   //  setPreferenceChangeListenerClass()
647
648    /**
649     *  Sets the name for the preferences root node.
650     *
651     *  @param  name    The name for the preferences root node.
652     */
653    public final void setPreferencesRoot( final String name )
654    {
655        m_PreferencesRoot = requireNotEmptyArgument( name, "name" );
656    }   //  setPreferencesRoot()
657}
658//  class CodeGenerationConfiguration
659
660/*
661 *  End of File
662 */