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.Boolean.FALSE;
021import static java.lang.String.format;
022import static java.util.Arrays.asList;
023import static java.util.Collections.list;
024import static java.util.stream.Collectors.joining;
025import static javax.lang.model.element.ElementKind.METHOD;
026import static javax.lang.model.element.Modifier.DEFAULT;
027import static javax.lang.model.element.Modifier.PUBLIC;
028import static javax.lang.model.element.Modifier.STATIC;
029import static javax.tools.Diagnostic.Kind.ERROR;
030import static javax.tools.Diagnostic.Kind.NOTE;
031import static org.apiguardian.api.API.Status.INTERNAL;
032import static org.apiguardian.api.API.Status.STABLE;
033import static org.tquadrat.foundation.config.ap.CollectionKind.LIST;
034import static org.tquadrat.foundation.config.ap.CollectionKind.MAP;
035import static org.tquadrat.foundation.config.ap.CollectionKind.NO_COLLECTION;
036import static org.tquadrat.foundation.config.ap.CollectionKind.SET;
037import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.ALLOWS_INIFILE;
038import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.ALLOWS_PREFERENCES;
039import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.ELEMENTTYPE_IS_ENUM;
040import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.EXEMPT_FROM_TOSTRING;
041import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.GETTER_IS_DEFAULT;
042import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.GETTER_ON_MAP;
043import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.GETTER_RETURNS_OPTIONAL;
044import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.PROPERTY_CLI_MANDATORY;
045import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.PROPERTY_CLI_MULTIVALUED;
046import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.PROPERTY_IS_ARGUMENT;
047import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.PROPERTY_IS_MUTABLE;
048import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.PROPERTY_IS_OPTION;
049import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.PROPERTY_IS_SPECIAL;
050import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.PROPERTY_REQUIRES_SYNCHRONIZATION;
051import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.SETTER_CHECK_EMPTY;
052import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.SETTER_CHECK_NULL;
053import static org.tquadrat.foundation.config.ap.PropertySpec.PropertyFlag.SETTER_IS_DEFAULT;
054import static org.tquadrat.foundation.javacomposer.Layout.LAYOUT_FOUNDATION;
055import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING;
056import static org.tquadrat.foundation.lang.CommonConstants.UTF8;
057import static org.tquadrat.foundation.lang.DebugOutput.ifDebug;
058import static org.tquadrat.foundation.lang.Objects.isNull;
059import static org.tquadrat.foundation.lang.Objects.nonNull;
060import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
061import static org.tquadrat.foundation.lang.StringConverter.METHOD_NAME_GetSubjectClass;
062import static org.tquadrat.foundation.util.JavaUtils.PREFIX_GET;
063import static org.tquadrat.foundation.util.JavaUtils.PREFIX_IS;
064import static org.tquadrat.foundation.util.JavaUtils.isAddMethod;
065import static org.tquadrat.foundation.util.JavaUtils.isGetter;
066import static org.tquadrat.foundation.util.JavaUtils.isSetter;
067import static org.tquadrat.foundation.util.JavaUtils.loadClass;
068import static org.tquadrat.foundation.util.StringUtils.capitalize;
069import static org.tquadrat.foundation.util.StringUtils.decapitalize;
070import static org.tquadrat.foundation.util.StringUtils.isEmpty;
071import static org.tquadrat.foundation.util.StringUtils.isEmptyOrBlank;
072import static org.tquadrat.foundation.util.StringUtils.isNotEmptyOrBlank;
073
074import javax.annotation.processing.RoundEnvironment;
075import javax.annotation.processing.SupportedOptions;
076import javax.annotation.processing.SupportedSourceVersion;
077import javax.lang.model.SourceVersion;
078import javax.lang.model.element.Element;
079import javax.lang.model.element.ElementKind;
080import javax.lang.model.element.ExecutableElement;
081import javax.lang.model.element.Name;
082import javax.lang.model.element.NestingKind;
083import javax.lang.model.element.PackageElement;
084import javax.lang.model.element.TypeElement;
085import javax.lang.model.type.ArrayType;
086import javax.lang.model.type.TypeMirror;
087import javax.lang.model.util.SimpleTypeVisitor14;
088import java.io.BufferedReader;
089import java.io.IOException;
090import java.io.InputStream;
091import java.io.InputStreamReader;
092import java.lang.annotation.Annotation;
093import java.lang.reflect.InvocationTargetException;
094import java.util.ArrayList;
095import java.util.Collection;
096import java.util.HashMap;
097import java.util.HashSet;
098import java.util.List;
099import java.util.Map;
100import java.util.NoSuchElementException;
101import java.util.Optional;
102import java.util.Set;
103import java.util.stream.Collectors;
104import java.util.stream.DoubleStream;
105import java.util.stream.IntStream;
106import java.util.stream.LongStream;
107import java.util.stream.Stream;
108
109import org.apiguardian.api.API;
110import org.tquadrat.foundation.annotation.ClassVersion;
111import org.tquadrat.foundation.annotation.PropertyName;
112import org.tquadrat.foundation.ap.APBase;
113import org.tquadrat.foundation.ap.CodeGenerationError;
114import org.tquadrat.foundation.ap.IllegalAnnotationError;
115import org.tquadrat.foundation.config.Argument;
116import org.tquadrat.foundation.config.CheckEmpty;
117import org.tquadrat.foundation.config.CheckNull;
118import org.tquadrat.foundation.config.ConfigurationBeanSpecification;
119import org.tquadrat.foundation.config.EnvironmentVariable;
120import org.tquadrat.foundation.config.ExemptFromToString;
121import org.tquadrat.foundation.config.I18nSupport;
122import org.tquadrat.foundation.config.INIFileConfig;
123import org.tquadrat.foundation.config.INIGroup;
124import org.tquadrat.foundation.config.INIValue;
125import org.tquadrat.foundation.config.NoPreference;
126import org.tquadrat.foundation.config.Option;
127import org.tquadrat.foundation.config.Preference;
128import org.tquadrat.foundation.config.PreferencesRoot;
129import org.tquadrat.foundation.config.SpecialProperty;
130import org.tquadrat.foundation.config.SpecialPropertyType;
131import org.tquadrat.foundation.config.StringConversion;
132import org.tquadrat.foundation.config.SystemPreference;
133import org.tquadrat.foundation.config.SystemProperty;
134import org.tquadrat.foundation.config.ap.impl.CodeGenerator;
135import org.tquadrat.foundation.config.ap.impl.PropertySpecImpl;
136import org.tquadrat.foundation.config.cli.CmdLineValueHandler;
137import org.tquadrat.foundation.config.internal.ClassRegistry;
138import org.tquadrat.foundation.config.spi.prefs.EnumAccessor;
139import org.tquadrat.foundation.config.spi.prefs.ListAccessor;
140import org.tquadrat.foundation.config.spi.prefs.MapAccessor;
141import org.tquadrat.foundation.config.spi.prefs.PreferenceAccessor;
142import org.tquadrat.foundation.config.spi.prefs.SetAccessor;
143import org.tquadrat.foundation.config.spi.prefs.SimplePreferenceAccessor;
144import org.tquadrat.foundation.exception.UnexpectedExceptionError;
145import org.tquadrat.foundation.exception.UnsupportedEnumError;
146import org.tquadrat.foundation.i18n.BaseBundleName;
147import org.tquadrat.foundation.i18n.MessagePrefix;
148import org.tquadrat.foundation.javacomposer.ClassName;
149import org.tquadrat.foundation.javacomposer.JavaComposer;
150import org.tquadrat.foundation.javacomposer.ParameterizedTypeName;
151import org.tquadrat.foundation.javacomposer.TypeName;
152import org.tquadrat.foundation.lang.Objects;
153import org.tquadrat.foundation.lang.StringConverter;
154import org.tquadrat.foundation.util.JavaUtils;
155import org.tquadrat.foundation.util.LazyMap;
156import org.tquadrat.foundation.util.stringconverter.EnumStringConverter;
157
158/**
159 *  The annotation processor for the {@code org.tquadrat.foundation.config}
160 *  module.
161 *
162 *  @version $Id: ConfigAnnotationProcessor.java 1105 2024-02-28 12:58:46Z tquadrat $
163 *
164 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
165 *  @UMLGraph.link
166 *  @since 0.1.0
167 */
168@SuppressWarnings( {"OverlyCoupledClass", "OverlyComplexClass", "ClassWithTooManyMethods"} )
169@ClassVersion( sourceVersion = "$Id: ConfigAnnotationProcessor.java 1105 2024-02-28 12:58:46Z tquadrat $" )
170@API( status = STABLE, since = "0.1.0" )
171@SupportedSourceVersion( SourceVersion.RELEASE_17 )
172@SupportedOptions( { APBase.ADD_DEBUG_OUTPUT, APBase.MAVEN_GOAL } )
173public class ConfigAnnotationProcessor extends APBase
174{
175        /*-----------*\
176    ====** Constants **========================================================
177        \*-----------*/
178    /**
179     *  The name for
180     *  {@link org.tquadrat.foundation.config.ConfigBeanSpec#addListener(org.tquadrat.foundation.config.ConfigurationChangeListener)}: {@value}.
181     */
182    public static final String METHODNAME_ConfigBeanSpec_AddListener = "addListener";
183
184    /**
185     *  The name for
186     *  {@link org.tquadrat.foundation.config.ConfigBeanSpec#getResourceBundle()}: {@value}.
187     */
188    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
189    public static final String METHODNAME_ConfigBeanSpec_GetResourceBundle = "getResourceBundle";
190
191    /**
192     *  The name for the optional {@code initData } method: {@value}.
193     */
194    public static final String METHODNAME_ConfigBeanSpec_InitData = "initData";
195
196    /**
197     *  The name for
198     *  {@link Map#isEmpty()}: {@value}.
199     */
200    public static final String METHODNAME_Map_IsEmpty = "isEmpty";
201
202    /**
203     *  The message that indicates that no accessor class is given for the
204     *  {@link SystemPreference @SystemPreference} annotation: {@value}.
205     */
206    public static final String MSG_AccessorMissing = "No accessor is given for the @SystemPreference annotation on '%1$s'";
207
208    /**
209     *  The message that indicates the erroneous attempt to define an 'add'
210     *  method for a property that is not a collection.
211     */
212    public static final String MSG_AddMethodNotAllowed = "The method '%1$s' is not allowed, as the property type is not a collection";
213
214    /**
215     *  The message that indicates that a mirror cannot be retrieved: {@value}.
216     */
217    public static final String MSG_CannotRetrieveMirror = "Cannot retrieve Mirror for '%1$s'";
218
219    /**
220     *  The message that indicates a clash of CLI annotations.
221     */
222    public static final String MSG_CLIAnnotationClash = "Annotations @Argument and @Option are mutually exclusive";
223
224    /**
225     *  The message that indicates the failure of the code generation for the
226     *  configuration bean specification: {@value}.
227     */
228    public static final String MSG_CodeGenerationFailed = "Code generation for '%1$s.%2$s' failed";
229
230    /**
231     *  The message that indicates that a property was specified twice:
232     *  {@value}.
233     */
234    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
235    public static final String MSG_DuplicateProperty = "Duplicate property: %1$s";
236
237    /**
238     *  The message that indicates that an option name was used twice:
239     *  {@value}.
240     */
241    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
242    public static final String MSG_DuplicateOptionName = "Option name '%s' for property '%s' is already used";
243
244    /**
245     *  The message that indicates a missing default getter for the message
246     *  prefix: {@value}.
247     */
248    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
249    public static final String MSG_GetterMustBeDEFAULT = "The getter for the message prefix must be defined as default";
250
251    /**
252     *  The message that indicates that an illegal annotation had been applied
253     *  to an 'add' method: {@value}.
254     */
255    public static final String MSG_IllegalAnnotationOnAddMethod = "Invalid annotation '%1$s' on 'add' method '%2$s'";
256
257    /**
258     *  The message that indicates that an illegal annotation had been applied
259     *  to a getter: {@value}.
260     */
261    public static final String MSG_IllegalAnnotationOnGetter = "Invalid annotation '%1$s' on getter '%2$s'";
262
263    /**
264     *  The message that indicates that an illegal annotation had been applied
265     *  to a setter: {@value}.
266     */
267    public static final String MSG_IllegalAnnotationOnSetter = "Illegal annotation '%1$s' on setter '%2$s'";
268
269    /**
270     *  The message that indicates that an invalid implementation for an
271     *  interface was used: {@value}.
272     */
273    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
274    public static final String MSG_IllegalImplementation = "Illegal implementation for '%1$s': %2$s";
275
276    /**
277     *  The message that indicates that a mutator was provided for an immutable
278     *  property: {@value}.
279     */
280    public static final String MSG_IllegalMutator = "No mutator allowed for property '%1$s'";
281
282    /**
283     *  The message that indicates that the attribute
284     *  {@link INIValue#group()}
285     *  was not properly populated: {@value}.
286     */
287    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
288    public static final String MSG_INIGroupMissing = "The group for @INIValue on '%s' is missing";
289
290    /**
291     *  The message that indicates that the attribute
292     *  {@link INIValue#key()}
293     *  was not properly populated: {@value}.
294     */
295    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
296    public static final String MSG_INIKeyMissing = "The key for @INIValue on '%s' is missing";
297
298    /**
299     *  The message that indicates that the attribute
300     *  {@link INIFileConfig#path()}
301     *  was not properly populated: {@value}.
302     */
303    public static final String MSG_INIPathMissing = "The path for @INIFileConfig is not set properly";
304
305    /**
306     *  The message that indicates that an annotation is valid only for
307     *  interfaces: {@value}.
308     */
309    public static final String MSG_InterfacesOnly = "Only allowed for interfaces";
310
311    /**
312     *  The message that indicates that a CLI property is invalid: {@value}.
313     */
314    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
315    public static final String MSG_InvalidCLIType = "Property '%s' is neither argument nor option";
316
317    /**
318     *  The message that indicates a missing environment variable name for a
319     *  property: {@value}.
320     */
321    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
322    public static final String MSG_MissingEnvironmentVar = "The name of the environment variable is missing for property '%1$s'";
323
324    /**
325     *  The message that indicates a missing getter for a property: {@value}.
326     */
327    public static final String MSG_MissingGetter = "A getter method for the property '%1$s' is missing";
328
329    /**
330     *  The message that indicates that the configuration bean specification
331     *  does not extend
332     *  {@link org.tquadrat.foundation.config.ConfigBeanSpec}:
333     *  {@value}.
334     */
335    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
336    public static final String MSG_MissingInterface = "The configuration bean specification '%1$s' does not extend '%2$s'";
337
338    /**
339     *  The message that indicates a missing definition for a property:
340     *  {@value}.
341     */
342    public static final String MSG_MissingPropertyDefinition = "There is neither a getter nor a setter method for the property '%1$s', first introduced by the 'add' method '%2$s'";
343
344    /**
345     *  The message that indicates a missing
346     *  {@link org.tquadrat.foundation.lang.StringConverter}
347     *  for a property: {@value}.
348     */
349    public static final String MSG_MissingStringConverter = "There is no StringConverter for the property '%1$s'";
350
351    /**
352     *  The message that indicates a missing
353     *  {@link org.tquadrat.foundation.lang.StringConverter}
354     *  for a property: {@value}.
355     */
356    public static final String MSG_MissingStringConverterWithType = "There is no StringConverter for the property '%1$s' than converts '%2$s'";
357
358    /**
359     *  The message that indicates a missing system property name for a
360     *  property: {@value}.
361     */
362    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
363    public static final String MSG_MissingSystemProp = "The name of the system property is missing for property '%1$s'";
364
365    /**
366     *  The message that indicates a missing argument index for a CLI argument
367     *  property: {@value}.
368     */
369    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
370    public static final String MSG_NoArgumentIndex = "No argument index for property '%s'";
371
372    /**
373     *  The message that indicates a missing base bundle name configuration: {@value}.
374     */
375    public static final String MSG_NoBaseBundleName = "There is no public static field providing the base bundle name";
376
377    /**
378     *  The message that indicates that an 'add' method was provided for a non-collection property.
379     */
380    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
381    public static final String MSG_NoCollection = "Method '%1$s' not allowed for property '%2$s': property type is not List, Set or Map";
382
383    /**
384     *  The message that indicates a missing message prefix field: {@value}.
385     */
386    public static final String MSG_NoMessagePrefix = "There is no public static field providing the message prefix";
387
388    /**
389     *  The message that indicates a missing property name for a CLI option
390     *  property: {@value}.
391     */
392    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
393    public static final String MSG_NoOptionName = "No option name for property '%s'";
394
395    /**
396     *   The message that indicates that a given method is not a setter:
397     *   {@value}.
398     */
399    public static final String MSG_NoSetter = "The method '%1$s' is neither a setter nor an add method";
400
401    /**
402     *  The message that indicates a missing property type.
403     */
404    public static final String MSG_NoType = "Type for property '%2$s' is missing and cannot be inferred (method: %1$s)";
405
406    /**
407     *  The message that indicates that a value cannot be retrieved from a
408     *  mirror: {@value}.
409     */
410    public static final String MSG_NoValueForMirror = "Cannot get value for '%2$s' from Annotation Mirror for '%1$s'";
411
412    /**
413     *  The message that indicates a clash of preference annotations: {@value}.
414     */
415    public static final String MSG_PreferenceAnnotationClash = "Annotations @Preference and @NoPreference are mutually exclusive";
416
417    /**
418     *  The message that indicates invalid "preferences"
419     *  configuration for a property: {@value}.
420     */
421    public static final String MSG_PreferencesNotConfigured = "The 'preferences' configuration for '%1$s' is invalid";
422
423    /**
424     *  The message that indicates that no {@code Preferences} key is given for
425     *  the
426     *  {@link SystemPreference @SystemPreference} annotation: {@value}.
427     */
428    public static final String MSG_PrefsKeyMissing = "No key is given for the @SystemPreference annotation on '%1$s'";
429
430    /**
431     *  The message that indicates a wrong return type for
432     *  {@link org.tquadrat.foundation.config.ConfigBeanSpec#getResourceBundle()}:
433     *  {@value}.
434     */
435    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
436    public static final String MSG_ResourceBundleWrongReturnType = "The return type of getResourceBundle() must be Optional(ResourceBundle)";
437
438    /**
439     *  The message that indicates that the session property was not defined:
440     *  {@value}.
441     *
442     *  @see    org.tquadrat.foundation.config.SessionBeanSpec#getSessionKey()
443     *  @see    SpecialPropertyType#CONFIG_PROPERTY_SESSION
444     */
445    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
446    public static final String MSG_SessionPropertyMissing = "Session property was not defined";
447
448    /**
449     *  The message that indicates a mismatch of the values for the
450     *  {@link SpecialProperty @SpecialProperty}
451     *  annotation: {@value}.
452     */
453    public static final String MSG_SpecialPropertyMismatch = "%1$s annotation value for '%2$s' is '%3$s', but '%4$s' was expected";
454
455    /**
456     *  The message that indicates that the
457     *  {@link org.tquadrat.foundation.lang.StringConverter}
458     *  inferred from the setter does not match with that from the getter.
459     */
460    public static final String MSG_StringConverterMismatch = "Inferred StringConverter does not match the previously determined one: %1$s";
461
462    /**
463     *  The message that indicates that the
464     *  {@link org.tquadrat.foundation.lang.StringConverter}
465     *  is invalid for the property type.
466     */
467    @SuppressWarnings( "StaticMethodOnlyUsedInOneClass" )
468    public static final String MSG_StringConverterNotCompatible = "StringConverter '%2$s' cannot handle '%1$s'";
469
470    /**
471     *  The message that indicates a mismatch of the types for the setter
472     *  argument and the property itself: {@value}.
473     */
474    public static final String MSG_TypeMismatch = "Parameter type '%1$s' of setter '%2$s' does not match with property type '%3$s'";
475
476        /*------------*\
477    ====** Attributes **=======================================================
478        \*------------*/
479    /**
480     *  <p>{@summary The base bundle name.} The value will be set in
481     *  {@link #process(Set, RoundEnvironment)}
482     *  from a String constant that is annotated with the annotation
483     *  {@link BaseBundleName &#64;BaseBundleName}.</p>
484     */
485    @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" )
486    private Optional<String> m_BaseBundleName;
487
488    /**
489     *  <p>{@summary The message prefix.} The value will be set in
490     *  {@link #process(Set, RoundEnvironment)}
491     *  from a String constant that is annotated with the annotation
492     *  {@link MessagePrefix &#64;MessagePrefix}.</p>
493     */
494    @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" )
495    private Optional<String> m_MessagePrefix;
496
497    /**
498     *  The preferences accessor classes.
499     */
500    @API( status = INTERNAL, since = "0.1.0" )
501    private final Map<TypeName,ClassName> m_PrefsAccessorClasses;
502
503    /**
504     *  A map of
505     *  {@link StringConverter}
506     *  implementations that use instances of
507     *  {@link TypeName}
508     *  as keys.
509     */
510    private final LazyMap<TypeName,ClassName> m_StringConvertersForTypeNames;
511
512        /*--------------*\
513    ====** Constructors **=====================================================
514        \*--------------*/
515    /**
516     *  Creates a new {@code ConfigAnnotationProcessor} instance.
517     */
518    public ConfigAnnotationProcessor()
519    {
520        super();
521
522        m_StringConvertersForTypeNames = LazyMap.use( true, this::initStringConvertersForTypeNames );
523
524        final var buffer = new HashMap<TypeName,ClassName>();
525        for( final var entry : ClassRegistry.m_PrefsAccessorClasses.entrySet() )
526        {
527            buffer.put( TypeName.from( entry.getKey() ), ClassName.from( entry.getValue() ) );
528        }
529        m_PrefsAccessorClasses = Map.copyOf( buffer );
530    }   //  ConfigAnnotationProcessor()
531
532        /*------------------------*\
533    ====** Static Initialisations **===========================================
534        \*------------------------*/
535    /**
536     *  The type name for the class
537     *  {@link org.tquadrat.foundation.config.spi.prefs.SimplePreferenceAccessor}.
538     */
539    @API( status = INTERNAL, since = "0.1.0" )
540    public static final TypeName DEFAULT_ACCESSOR_TYPE;
541
542    /**
543     *  The type name for the class
544     *  {@link EnumAccessor}.
545     */
546    @API( status = INTERNAL, since = "0.0.1" )
547    public static final TypeName ENUM_ACCESSOR_TYPE;
548
549    /**
550     *  The type name for the class
551     *  {@link ListAccessor}.
552     */
553    @API( status = INTERNAL, since = "0.1.0" )
554    public static final TypeName LIST_ACCESSOR_TYPE;
555
556    /**
557     *  The type name for the class
558     *  {@link MapAccessor}.
559     */
560    @API( status = INTERNAL, since = "0.1.0" )
561    public static final TypeName MAP_ACCESSOR_TYPE;
562
563    /**
564     *  The type name for the class
565     *  {@link PreferenceAccessor}.
566     */
567    @API( status = INTERNAL, since = "0.0.1" )
568    public static final TypeName PREFS_ACCESSOR_TYPE;
569
570    /**
571     *  The type name for the class
572     *  {@link SetAccessor}.
573     */
574    @API( status = INTERNAL, since = "0.1.0" )
575    public static final TypeName SET_ACCESSOR_TYPE;
576
577    static
578    {
579        DEFAULT_ACCESSOR_TYPE = ClassName.from( SimplePreferenceAccessor.class );
580        ENUM_ACCESSOR_TYPE = ClassName.from( EnumAccessor.class );
581        LIST_ACCESSOR_TYPE = ClassName.from( ListAccessor.class );
582        MAP_ACCESSOR_TYPE = ClassName.from( MapAccessor.class );
583        PREFS_ACCESSOR_TYPE = ClassName.from( PreferenceAccessor.class );
584        SET_ACCESSOR_TYPE = ClassName.from( SetAccessor.class );
585    }
586
587        /*---------*\
588    ====** Methods **==========================================================
589        \*---------*/
590    /**
591     *  Checks whether the given type is inappropriate for a configuration bean
592     *  property and therefore deserves that an
593     *  {@link InappropriateTypeError}
594     *  is thrown.
595     *
596     *  @param  typeName    The type to check.
597     *  @throws InappropriateTypeError  The given type may not be chosen for
598     *      a configuration bean property.
599     *
600     *  @see    InputStream
601     *  @see    Stream
602     *  @see    DoubleStream
603     *  @see    IntStream
604     *  @see    LongStream
605     */
606    private final void checkAppropriate( final TypeMirror typeName ) throws InappropriateTypeError
607    {
608        KindSwitch: switch( requireNonNullArgument( typeName, "type" ).getKind() )
609        {
610            case ARRAY ->
611            {
612                //---* Check the component type *------------------------------
613                /*
614                 * Currently (as of 2023-03-08 and for Java 17), the class
615                 * SimpleTypeVisitor14 is the latest incarnation of this type.
616                 */
617                @SuppressWarnings( {"AnonymousInnerClass"} )
618                final var componentType = typeName.accept( new SimpleTypeVisitor14<TypeMirror,Void>()
619                {
620                    /**
621                     *  {@inheritDoc}
622                     */
623                    @Override
624                    public final TypeMirror visitArray( final ArrayType arrayType, final Void ignore )
625                    {
626                        return arrayType.getComponentType();
627                    }   //  visitArray()
628                }, null );
629
630                if( nonNull( componentType ) ) checkAppropriate( componentType );
631            }
632
633            case BOOLEAN, BYTE, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT ->
634            { /* The primitives work just fine */}
635
636            case DECLARED ->
637            {
638                final var erasure = getTypeUtils().erasure( typeName );
639                final var unwantedTypes = List.of( DoubleStream.class, IntStream.class, LongStream.class, InputStream.class, Stream.class );
640                final var isInappropriate = unwantedTypes.stream()
641                    .map( Class::getName )
642                    .map( s -> getElementUtils().getTypeElement( s ) )
643                    .map( Element::asType )
644                    .map( e -> getTypeUtils().erasure( e ) )
645                    .anyMatch( t -> getTypeUtils().isAssignable( erasure, t ) );
646                if( isInappropriate ) throw new InappropriateTypeError( TypeName.from( typeName ) );
647            }
648
649            default -> throw new UnsupportedEnumError( typeName.getKind() );
650        }   //  KindSwitch:
651    }   //  checkAppropriate()
652
653    /**
654     *  <p>Composes the name of the field from the given property name.</p>
655     *
656     *  @param  propertyName    The name of the property.
657     *  @return The name of the field.
658     */
659    private static final String composeFieldName( final String propertyName )
660    {
661        final var retValue = format( "m_%s", capitalize( propertyName ) );
662
663        //---* Done *----------------------------------------------------------
664        return retValue;
665    }   //  composeFieldName()
666
667    /**
668     *  <p>{@summary Creates a registry of the known
669     *  {@link StringConverter}
670     *  implementations.}</p>
671     *  <p>The
672     *  {@link TypeName}
673     *  of the subject class is the key for that map, the {@code TypeName} for
674     *  the {@code Class} implementing the {@code StringConverter} is the
675     *  value.</p>
676     *
677     *  @return An immutable map of
678     *      {@link StringConverter}
679     *      implementations.
680     *
681     *  @throws IOException Failed to read the resource files with the
682     *      {@code StringConverter} implementations.
683     */
684    @SuppressWarnings( "NestedTryStatement" )
685    @API( status = INTERNAL, since = "0.1.0" )
686    public static final Map<TypeName,ClassName> createStringConverterRegistry() throws IOException
687    {
688        final Map<TypeName,ClassName> buffer = new HashMap<>();
689
690        /*
691         * For some reason, the original code for this method does not work:
692         *
693         * for( final var c : StringConverter.list() )
694         * {
695         *    final var container = StringConverter.forClass( c );
696         *    if( container.isPresent() )
697         *    {
698         *       final var stringConverterClass = TypeName.from( container.get().getClass() );
699         *       final var key = TypeName.from( c );
700         *       buffer.put( key, stringConverterClass );
701         *       ifDebug( "StringConverters: %1$s => %2$s"::formatted, key, stringConverterClass );
702         *    }
703         * }
704         *
705         * This code relies on the code in the foundation-util module, so I
706         * assumed the problem there and move the code to here:
707         *
708         * final var moduleLayer = StringConverter.class.getModule().getLayer();
709         * final var converters = isNull( moduleLayer )
710         *   ? ServiceLoader.load( StringConverter.class )
711         *   : ServiceLoader.load( moduleLayer, StringConverter.class );
712         *
713         * for( final StringConverter<?> c : converters )
714         * {
715         *   StringConverter<?> converter;
716         *   try
717         *   {
718         *     final var providerMethod = c.getClass().getMethod( METHOD_NAME_Provider );
719         *     converter = (StringConverter<?>) providerMethod.invoke( null );
720         *   }
721         *   catch( final NoSuchMethodException | IllegalAccessException | InvocationTargetException e )
722         *   {
723         *     converter = c;
724         *   }
725         *
726         *   for( final var subjectClass : retrieveSubjectClasses( converter ) )
727         *   {
728         *     buffer.put( TypeName.from( subjectClass ), TypeName.from( converter.getClass() ) );
729         *   }
730         * }
731         *
732         * I raised a question on StackOverflow regarding this issue:
733         *   https://stackoverflow.com/questions/70861635/java-util-serviceloader-does-not-work-inside-of-an-annotationprocessor
734         */
735
736        final var classLoader = CodeGenerationConfiguration.class.getClassLoader();
737        final var resources = classLoader.getResources( "META-INF/services/%s".formatted( StringConverter.class.getName() ) );
738        for( final var file : list( resources ) )
739        {
740            try( final var reader = new BufferedReader( new InputStreamReader( file.openStream(), UTF8 ) ) )
741            {
742                final var converterClasses = reader.lines()
743                    .map( String::trim )
744                    .filter( s -> !s.startsWith( "#" ) )
745                    .map( s -> loadClass( classLoader, s, StringConverter.class ) )
746                    .filter( Optional::isPresent )
747                    .map( Optional::get )
748                    .toList();
749                CreateLoop: for( final var aClass : converterClasses )
750                {
751                    try
752                    {
753                        final var constructor = aClass.getConstructor();
754                        final var instance = constructor.newInstance();
755                        for( final var subjectClass : retrieveSubjectClasses( instance ) )
756                        {
757                            buffer.put( TypeName.from( subjectClass ), ClassName.from( aClass ) );
758                        }
759                    }
760                    catch( final InvocationTargetException | NoSuchMethodException |InstantiationException | IllegalAccessException e )
761                    {
762                        ifDebug( e );
763
764                        //---* Deliberately ignored! *-------------------------
765                        continue CreateLoop;
766                    }
767                }   //  CreateLoop:
768            }
769        }
770
771        final var retValue = Map.copyOf( buffer );
772
773        //---* Done *----------------------------------------------------------
774        return retValue;
775    }   //  createStringConverterRegistry()
776
777    /**
778     *  Determines whether the given
779     *  {@link TypeMirror type}
780     *  is a collection of some type and returns the respective kind.
781     *
782     *  @param  type The type to check.
783     *  @return The collection kind.
784     */
785    private final CollectionKind determineCollectionKind( final TypeMirror type )
786    {
787        final var focusType = getTypeUtils().erasure( requireNonNullArgument( type, "type" ) );
788
789        final var listType = getTypeUtils().erasure( getElementUtils().getTypeElement( List.class.getName() ).asType() );
790        final var mapType = getTypeUtils().erasure( getElementUtils().getTypeElement( Map.class.getName() ).asType() );
791        final var setType = getTypeUtils().erasure( getElementUtils().getTypeElement( Set.class.getName() ).asType() );
792
793        var retValue = NO_COLLECTION;
794        if( getTypeUtils().isAssignable( focusType, listType ) ) retValue = LIST;
795        if( getTypeUtils().isAssignable( focusType, mapType ) ) retValue = MAP;
796        if( getTypeUtils().isAssignable( focusType, setType ) ) retValue = SET;
797
798        //---* Done *----------------------------------------------------------
799        return retValue;
800    }   //  determineCollectionKind()
801
802    /**
803     *  <p>{@summary Determines the element type from the given
804     *  {@link TypeMirror}
805     *  instance representing a collection.}</p>
806     *
807     *  @param  type   The type.
808     *  @return An instance of
809     *      {@link Optional}
810     *      that holds the element type.
811     */
812    private final Optional<TypeMirror> determineElementType( final TypeMirror type )
813    {
814        final var retValue = switch( determineCollectionKind( requireNonNullArgument( type, "type" ) ) )
815            {
816                case LIST, SET ->
817                {
818                    final var genericTypes = retrieveGenericTypes( type );
819                    yield genericTypes.size() == 1 ? Optional.of( genericTypes.getFirst() ) : Optional.<TypeMirror>empty();
820                }
821
822                default -> Optional.<TypeMirror>empty();
823            };
824
825        //---* Done *----------------------------------------------------------
826        return retValue;
827    }   //  determineElementType()
828
829    /**
830     *  <p>{@summary Retrieves the name of the property from the name of the
831     *  given executable element for a method that is either a getter, a setter
832     *  or an 'add' method.}</p>
833     *  <p>Alternatively the method has an annotation that provides the name of
834     *  the property.</p>
835     *
836     *  @param  method  The method.
837     *  @return The name of the property.
838     *
839     *  @see PropertyName
840     *  @see SpecialProperty
841     *  @see SpecialPropertyType
842     */
843    private final String determinePropertyNameFromMethod( @SuppressWarnings( "TypeMayBeWeakened" ) final ExecutableElement method )
844    {
845        final String retValue;
846        final var specialPropertyAnnotation = method.getAnnotation( SpecialProperty.class );
847        final var propertyNameAnnotation = method.getAnnotation( PropertyName.class );
848        if( nonNull( specialPropertyAnnotation ) )
849        {
850            if( nonNull( propertyNameAnnotation ) )
851            {
852                throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnGetter, PropertyName.class.getName(), method.getSimpleName() ) );
853            }
854            final var specialPropertyType = specialPropertyAnnotation.value();
855            retValue = specialPropertyType.getPropertyName();
856        }
857        else
858        {
859            if( nonNull( propertyNameAnnotation ) )
860            {
861                retValue = propertyNameAnnotation.value();
862            }
863            else
864            {
865                final var methodName = method.getSimpleName().toString();
866
867                /*
868                 * We know that the method is either a getter, a setter or an 'add'
869                 * method. Therefore, we know also that the name starts either with
870                 * "get", "set", "add" or "is".
871                 */
872                final var pos = methodName.startsWith( PREFIX_IS ) ? PREFIX_IS.length() : PREFIX_GET.length();
873                retValue = decapitalize( methodName.substring( pos ) );
874            }
875        }
876
877        //---* Done *----------------------------------------------------------
878        return retValue;
879    }   //  determinePropertyNameFromMethod()
880
881    /**
882     *  <p>{@summary Determines the property type from the given
883     *  {@link TypeMirror}
884     *  instance.} This is either the return type of a
885     *  getter or the argument type of a setter.</p>
886     *  <p>Usually the property type will be the respective
887     *  {@link TypeMirror}
888     *  for the given type as is, only in case of
889     *  {@link Optional},
890     *  it will be the parameter type.</p>
891     *
892     *  @param  type   The type.
893     *  @return The property type.
894     */
895    private final TypeMirror determinePropertyType( final TypeMirror type )
896    {
897        var retValue = requireNonNullArgument( type, "type" );
898        final var optionalType = getTypeUtils().erasure( getElementUtils().getTypeElement( Optional.class.getName() ).asType() );
899        if( getTypeUtils().isAssignable( getTypeUtils().erasure( retValue ), optionalType ) )
900        {
901            /*
902             * The return type is Optional, we need to get the type parameter
903             * and have to look at that.
904             */
905            final var genericTypes = retrieveGenericTypes( retValue );
906            if( genericTypes.size() == 1 ) retValue = genericTypes.getFirst();
907        }
908
909        //---* Check whether the property type is appropriate *----------------
910        checkAppropriate( retValue );
911
912        //---* Done *----------------------------------------------------------
913        return retValue;
914    }   //  determinePropertyType()
915
916    /**
917     *  Determines the implementation of
918     *  {@link StringConverter}
919     *  that can translate a String into an instance of the given type and
920     *  vice-versa.
921     *
922     *  @param  method  The annotated method; it is only used to get the
923     *      instance of
924     *      {@link StringConversion &#64;StringConversion}
925     *      from it.
926     *  @param  type    The target type.
927     *  @param  isEnum  {@code true} if the target type is an
928     *      {@link Enum enum}
929     *      type, {@code false} otherwise.
930     *  @return An instance of
931     *      {@link Optional}
932     *      that holds the determined class.
933     */
934    private final Optional<ClassName> determineStringConverterClass( final ExecutableElement method, final TypeName type, final boolean isEnum )
935    {
936        requireNonNullArgument( type, "type" );
937        requireNonNullArgument( method, "method" );
938        ifDebug( a -> "Method: %2$s%n\tType for StringConverter request: %1$s%n\tisEnum: %3$b".formatted( a [0].toString(), ((Element) a[1]).getSimpleName(), a [2] ), type, method, Boolean.valueOf( isEnum ) );
939
940        //---* Retrieve the StringConverter from the annotation *--------------
941        final var retValue = extractStringConverterClass( method )
942            .or( () -> Optional.ofNullable( isEnum ? ClassName.from( EnumStringConverter.class ) : m_StringConvertersForTypeNames.get( type ) ) );
943        //noinspection unchecked
944        ifDebug( a -> ((Optional<TypeName>) a [0]).map( "Detected StringConverter: %1$s"::formatted ).orElse( "Could not find a StringConverter" ), retValue );
945
946        //---* Done *----------------------------------------------------------
947        return retValue;
948    }   //  determineStringConverterClass
949
950    /**
951     *  <p>{@summary Retrieves the value for the
952     *  {@link StringConversion &#64;StringConversion}
953     *  annotation from the given method.}</p>
954     *  <p>The type for the annotation value is an instance of
955     *  {@link Class Class&lt;? extends StringConverter&gt;},
956     *  so it cannot be retrieved directly. Therefore this method will return
957     *  the
958     *  {@link TypeName}
959     *  for the
960     *  {@link StringConverter}
961     *  implementation class.</p>
962     *
963     *  @param  method  The annotated method.
964     *  @return An instance of
965     *      {@link Optional}
966     *      holding the type name that represents the annotation value
967     *      &quot;<i>stringConverter</i>&quot;.
968     */
969    public final Optional<ClassName> extractStringConverterClass( final ExecutableElement method )
970    {
971        final var retValue = getAnnotationMirror( requireNonNullArgument( method, "method" ), StringConversion.class )
972            .flatMap( this::getAnnotationValue )
973            .map( annotationValue -> TypeName.from( (TypeMirror) annotationValue.getValue() ) )
974            .map( TypeName::toString )
975            .map( JavaUtils::loadClass )
976            .filter( Optional::isPresent )
977            .map( Optional::get )
978            .map( ClassName::from );
979
980        //---* Done *----------------------------------------------------------
981        return retValue;
982    }   //  extractStringConverterClass()
983
984    /**
985     *  {@inheritDoc}
986     */
987    @Override
988    protected final Collection<Class<? extends Annotation>> getSupportedAnnotationClasses()
989    {
990        final Collection<Class<? extends Annotation>> retValue =
991            List.of( ConfigurationBeanSpecification.class );
992
993        //---* Done *----------------------------------------------------------
994        return retValue;
995    }   //  getSupportedAnnotationClasses()
996
997    /**
998     *  Processes the given
999     *  {@link ExecutableElement}
1000     *  instance for an 'add' method.
1001     *
1002     *  @param  configuration   The code generation configuration.
1003     *  @param  addMethod   The 'add' method.
1004     */
1005    @SuppressWarnings( {"UseOfConcreteClass", "OverlyCoupledMethod", "OverlyComplexMethod"} )
1006    private final void handleAddMethod( final CodeGenerationConfiguration configuration, final ExecutableElement addMethod )
1007    {
1008        //---* Get the method name *-------------------------------------------
1009        final var addMethodName = addMethod.getSimpleName();
1010
1011        //---* Check for unwanted annotations *--------------------------------
1012        @SuppressWarnings( "unchecked" )
1013        Class<? extends Annotation> [] unwantedAnnotations = new Class[]
1014        {
1015            SystemProperty.class,
1016            EnvironmentVariable.class,
1017            SystemPreference.class,
1018            Preference.class,
1019            NoPreference.class,
1020            Argument.class,
1021            Option.class,
1022            INIValue.class
1023        };
1024        for( final var annotationClass : unwantedAnnotations )
1025        {
1026            if( nonNull( addMethod.getAnnotation( annotationClass ) ) )
1027            {
1028                throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnAddMethod, annotationClass.getName(), addMethodName ) );
1029            }
1030        }
1031
1032        /*
1033         * If the 'add' method is default, our job is mostly done already.
1034         */
1035        final var isDefault = addMethod.getModifiers().contains( DEFAULT );
1036        if( isDefault )
1037        {
1038            //noinspection unchecked
1039            unwantedAnnotations = new Class[]
1040                {
1041                    CheckEmpty.class,
1042                    CheckNull.class,
1043                    SpecialProperty.class
1044                };
1045            for( final var annotationClass : unwantedAnnotations )
1046            {
1047                if( nonNull( addMethod.getAnnotation( annotationClass ) ) )
1048                {
1049                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnAddMethod, annotationClass.getName(), addMethodName ) );
1050                }
1051            }
1052        }
1053        else
1054        {
1055            //---* Get the property name *-------------------------------------
1056            final var propertyName = determinePropertyNameFromMethod( addMethod );
1057
1058            /*
1059             * As we cannot infer the property type from an add method, it is
1060             * required that we already have a property specification for this
1061             * property.
1062             */
1063            final var property = (PropertySpecImpl) configuration.getProperty( propertyName )
1064                .orElseThrow( () -> new org.tquadrat.foundation.ap.CodeGenerationError( format( MSG_MissingPropertyDefinition, propertyName, addMethodName ) ) );
1065
1066            //---* Only collections may have 'add' methods *-------------------
1067            if( property.getCollectionKind() == NO_COLLECTION )
1068            {
1069                throw new CodeGenerationError( format( MSG_AddMethodNotAllowed, addMethodName ) );
1070            }
1071
1072            //---* Immutable special properties may not have an add method *---
1073            if( property.hasFlag( PROPERTY_IS_SPECIAL ) )
1074            {
1075                property.getSpecialPropertyType()
1076                    .map( CodeGenerator::getSpecialPropertySpecification )
1077                    .filter( spec -> spec.hasFlag( PROPERTY_IS_MUTABLE ) )
1078                    .orElseThrow( () -> new CodeGenerationError( format( MSG_IllegalMutator, propertyName ) ) );
1079            }
1080
1081            //---* Save the method name and variable name *--------------------
1082            property.setAddMethodName( addMethodName );
1083            property.setAddMethodArgumentName( retrieveSetterArgumentName( addMethod ) );
1084
1085            //---* We have an add method, so the property is mutable *---------
1086            property.setFlag( PROPERTY_IS_MUTABLE );
1087            if( configuration.getSynchronizationRequired() ) property.setFlag( PROPERTY_REQUIRES_SYNCHRONIZATION );
1088
1089            /*
1090             * For a special property, all the definitions are made there, so
1091             * nothing to do here …
1092             */
1093            if( !property.hasFlag( PROPERTY_IS_SPECIAL ) )
1094            {
1095                final var propertyType = property.getPropertyType();
1096                if( isNull( propertyType ) )
1097                {
1098                    throw new CodeGenerationError( format( MSG_NoType, addMethodName, propertyName ) );
1099                }
1100
1101                /*
1102                 * Get the StringConverter; as this cannot be inferred it has
1103                 * to be taken from the annotation @StringConversion, if
1104                 * present. And then it will override the already set one.
1105                 */
1106                extractStringConverterClass( addMethod )
1107                    .ifPresent( property::setStringConverterClass );
1108            }
1109
1110            /*
1111             * Shall the property be added to the result of toString()?
1112             */
1113            if( nonNull( addMethod.getAnnotation( ExemptFromToString.class ) ) )
1114            {
1115                property.setFlag( EXEMPT_FROM_TOSTRING );
1116            }
1117
1118            //---* … and now we create the method spec *-----------------------
1119            final var methodBuilder = configuration.getComposer()
1120                .overridingMethodBuilder( addMethod );
1121            property.setAddMethodBuilder( methodBuilder );
1122        }
1123    }   //  handleAddMethod()
1124
1125    /**
1126     *  Processes the given
1127     *  {@link ExecutableElement}
1128     *  instance for a getter method.
1129     *
1130     *  @param  configuration   The code generation configuration.
1131     *  @param  getter  The getter method.
1132     */
1133    @SuppressWarnings( {"UseOfConcreteClass", "OverlyCoupledMethod", "OverlyLongMethod", "OverlyComplexMethod"} )
1134    private final void handleGetter( final CodeGenerationConfiguration configuration, final ExecutableElement getter )
1135    {
1136        //---* Get the property name *-----------------------------------------
1137        final var propertyName = determinePropertyNameFromMethod( getter );
1138
1139        /*
1140         * Create the new property spec and store it. The getters will be
1141         * created first, so a property with the same name, defined by another
1142         * getter, may not exist already. The respective check is made on
1143         * storing.
1144         */
1145        final var property = new PropertySpecImpl( propertyName );
1146        configuration.addProperty( property );
1147
1148        //---* Keep the name of the getter method *----------------------------
1149        final var getterMethodName = getter.getSimpleName();
1150        property.setGetterMethodName( getterMethodName );
1151
1152        //---* Shall the property be added to the result of toString()? *------
1153        if( nonNull( getter.getAnnotation( ExemptFromToString.class ) ) )
1154        {
1155            property.setFlag( EXEMPT_FROM_TOSTRING );
1156        }
1157
1158        //---* Default getters are handled differently *-----------------------
1159        final var isDefault = getter.getModifiers().contains( DEFAULT );
1160        if( isDefault )
1161        {
1162            /*
1163             * Several annotations are not allowed for a default getter.
1164             */
1165            @SuppressWarnings( "unchecked" )
1166            final Class<? extends Annotation> [] unwantedAnnotations = new Class[]
1167            {
1168                SystemProperty.class,
1169                EnvironmentVariable.class,
1170                SystemPreference.class,
1171                Argument.class,
1172                Option.class,
1173                Preference.class,
1174                SpecialProperty.class,
1175                INIValue.class
1176            };
1177            for( final var annotationClass : unwantedAnnotations )
1178            {
1179                if( nonNull( getter.getAnnotation( annotationClass ) ) )
1180                {
1181                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnGetter, annotationClass.getName(), getter.getSimpleName() ) );
1182                }
1183            }
1184            property.setFlag( GETTER_IS_DEFAULT, GETTER_ON_MAP );
1185        }
1186        var allowsPreferences = !isDefault;
1187
1188        //---* Determine the property type *-----------------------------------
1189        final var rawReturnType = getter.getReturnType();
1190        final var rawPropertyType = determinePropertyType( rawReturnType );
1191        final var propertyType = TypeName.from( rawPropertyType );
1192        final var returnType = TypeName.from( rawReturnType );
1193        property.setPropertyType( propertyType );
1194        final var collectionKind = determineCollectionKind( rawPropertyType );
1195        property.setCollectionKind( collectionKind );
1196        final var isEnum = isEnumType( rawPropertyType );
1197        property.setIsEnum( isEnum );
1198        switch( collectionKind )
1199        {
1200            case LIST, SET ->
1201                determineElementType( rawPropertyType )
1202                    .filter( this::isEnumType )
1203                    .ifPresent( $ -> property.setFlag( ELEMENTTYPE_IS_ENUM ) );
1204
1205            case MAP ->
1206            {
1207                // Does nothing currently
1208            }
1209
1210            case NO_COLLECTION -> { /* Nothing to do */ }
1211        }
1212
1213        /*
1214         * Some properties are 'special', and that is reflected by the
1215         * annotation @SpecialProperty.
1216         */
1217        final var specialPropertyAnnotation = getter.getAnnotation( SpecialProperty.class );
1218        if( nonNull( specialPropertyAnnotation ) )
1219        {
1220            /*
1221             * No further analysis required because everything is determined by
1222             * the special property spec, even the property name.
1223             */
1224            property.setSpecialPropertyType( specialPropertyAnnotation.value() );
1225        }
1226        else
1227        {
1228            //---* Keep the return type *--------------------------------------
1229            property.setGetterReturnType( returnType );
1230
1231            /*
1232             * Check whether the return type is Optional; only when the return
1233             * type is Optional, it can be different from the property type.
1234             */
1235            if( !returnType.equals( propertyType ) ) property.setFlag( GETTER_RETURNS_OPTIONAL );
1236
1237            /*
1238             * Determine the string converter instance, either from the
1239             * annotation or guess it from the property type.
1240             */
1241            determineStringConverterClass( getter, propertyType, isEnum ).ifPresent( property::setStringConverterClass );
1242
1243            if( !isDefault )
1244            {
1245                //---* Set the field name *------------------------------------
1246                property.setFieldName( composeFieldName( propertyName ) );
1247            }
1248
1249            //---* Additional annotations *------------------------------------
1250            final Optional<INIValue> iniValue = property.getStringConverterClass().isPresent()
1251                ? Optional.ofNullable( getter.getAnnotation( INIValue.class ) )
1252                : Optional.empty();
1253            iniValue.ifPresent( a ->
1254                {
1255                    property.setINIConfiguration( a );
1256                    property.setFlag( ALLOWS_INIFILE, PROPERTY_IS_MUTABLE );
1257                } );
1258            //noinspection NonShortCircuitBooleanExpression
1259            allowsPreferences &= iniValue.isEmpty();
1260
1261            final var systemPropertyAnnotation = getter.getAnnotation( SystemProperty.class );
1262            if( nonNull( systemPropertyAnnotation ) )
1263            {
1264                if( iniValue.isPresent() )
1265                {
1266                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnGetter, INIValue.class.getName(), getter.getSimpleName() ) );
1267                }
1268
1269                parseSystemPropertyAnnotation( systemPropertyAnnotation, property );
1270
1271                /*
1272                 * System properties may not be stored to/retrieved from
1273                 * preferences or an INIFile.
1274                 */
1275                allowsPreferences = false;
1276            }
1277
1278            final var environmentVariableAnnotation = getter.getAnnotation( EnvironmentVariable.class );
1279            if( nonNull( environmentVariableAnnotation ) )
1280            {
1281                if( iniValue.isPresent() )
1282                {
1283                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnGetter, INIValue.class.getName(), getter.getSimpleName() ) );
1284                }
1285
1286                /*
1287                 * @SystemProperty and @EnvironmentVariable are mutual
1288                 * exclusive.
1289                 */
1290                if( nonNull( systemPropertyAnnotation ) )
1291                {
1292                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnGetter, EnvironmentVariable.class.getName(), getter.getSimpleName() ) );
1293                }
1294
1295                parseEnvironmentVariableAnnotation( environmentVariableAnnotation, property );
1296
1297                /*
1298                 * Environment variable values may not be stored
1299                 * to/retrieved from preferences.
1300                 */
1301                allowsPreferences = false;
1302            }
1303
1304            final var systemPrefsAnnotation = getter.getAnnotation( SystemPreference.class );
1305            if( nonNull( systemPrefsAnnotation ) )
1306            {
1307                /*
1308                 * @INIValue, @SystemProperty, @EnvironmentVariable and
1309                 * @SystemPreference are mutual exclusive.
1310                 */
1311                if( nonNull( systemPropertyAnnotation ) || nonNull( environmentVariableAnnotation ) || iniValue.isPresent() )
1312                {
1313                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnGetter, SystemPreference.class.getName(), getter.getSimpleName() ) );
1314                }
1315
1316                /*
1317                 * The property will be initialised from a system preference
1318                 * with the name given in the annotation.
1319                 */
1320                property.setSystemPrefsPath( systemPrefsAnnotation.path() );
1321                final var prefsKey = systemPrefsAnnotation.key();
1322                if( isNotEmptyOrBlank( prefsKey ) )
1323                {
1324                    property.setPrefsKey( prefsKey );
1325                }
1326                else
1327                {
1328                    throw new IllegalAnnotationError( format( MSG_PrefsKeyMissing, propertyName ) );
1329                }
1330
1331                final var accessorClass = getAnnotationMirror( getter, SystemPreference.class )
1332                    .flatMap( mirror -> getAnnotationValue( mirror, "accessor" ) )
1333                    .map( annotationValue -> TypeName.from( (TypeMirror) annotationValue.getValue() ) )
1334                    .orElseThrow( () -> new IllegalAnnotationError( format( MSG_AccessorMissing, propertyName ) ) );
1335                property.setPrefsAccessorClass( accessorClass );
1336
1337                /*
1338                 * System preference values may not be stored to/retrieved from
1339                 * preferences.
1340                 */
1341                allowsPreferences = false;
1342            }
1343
1344            //---* Process the CLI annotations *-------------------------------
1345            final var argumentAnnotation = getter.getAnnotation( Argument.class );
1346            final var optionAnnotation = getter.getAnnotation( Option.class );
1347            if( nonNull( argumentAnnotation) && nonNull( optionAnnotation ) )
1348            {
1349                throw new IllegalAnnotationError( MSG_CLIAnnotationClash, optionAnnotation );
1350            }
1351            if( nonNull( argumentAnnotation ) )
1352            {
1353                parseArgumentAnnotation( argumentAnnotation, getter, property );
1354            }
1355            if( nonNull( optionAnnotation ) )
1356            {
1357                parseOptionAnnotation( optionAnnotation, getter, property );
1358            }
1359
1360            //---* Process the preferences annotations *-----------------------
1361            final var noPreferenceAnnotation = getter.getAnnotation( NoPreference.class );
1362            final var preferenceAnnotation = getter.getAnnotation( Preference.class );
1363            if( !allowsPreferences && nonNull( preferenceAnnotation ) )
1364            {
1365                throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnGetter, Preference.class.getName(), getter.getSimpleName() ) );
1366            }
1367            if( nonNull(  preferenceAnnotation ) && nonNull( noPreferenceAnnotation ) )
1368            {
1369                throw new IllegalAnnotationError( MSG_PreferenceAnnotationClash, noPreferenceAnnotation );
1370            }
1371            if( nonNull( noPreferenceAnnotation ) ) allowsPreferences = false;
1372            if( allowsPreferences )
1373            {
1374                property.setFlag( ALLOWS_PREFERENCES );
1375
1376                //---* The default values *------------------------------------
1377                var preferenceKey = propertyName;
1378                var accessorClass = PREFS_ACCESSOR_TYPE;
1379
1380                //---* Check the annotation *----------------------------------
1381                if( nonNull( preferenceAnnotation ) )
1382                {
1383                    if( isNotEmptyOrBlank( preferenceAnnotation.key() ) ) preferenceKey = preferenceAnnotation.key();
1384
1385                    try
1386                    {
1387                        final var name = "accessor";
1388                        accessorClass = getTypeMirrorFromAnnotationValue( getter, Preference.class, name )
1389                            .map( TypeName::from )
1390                            .orElseThrow( () -> new CodeGenerationError( format( MSG_NoValueForMirror, Preference.class.getName(), name ) ) );
1391                    }
1392                    catch( final NoSuchElementException e )
1393                    {
1394                        throw new CodeGenerationError( format( MSG_CannotRetrieveMirror, Preference.class.getName() ), e );
1395                    }
1396                }
1397
1398                //---* Translate the default, if required *--------------------
1399                accessorClass = retrieveAccessorClass( accessorClass, rawPropertyType, collectionKind );
1400
1401                //---* Keep the values *---------------------------------------
1402                property.setPrefsKey( preferenceKey );
1403                property.setPrefsAccessorClass( accessorClass );
1404            }
1405
1406            //---* … and now we create the method spec *-------------------
1407            final var methodBuilder = configuration.getComposer()
1408                .overridingMethodBuilder( getter );
1409            property.setGetterBuilder( methodBuilder );
1410        }
1411    }   //  handleGetter()
1412
1413    /**
1414     *  Processes the given
1415     *  {@link ExecutableElement}
1416     *  instance for a setter method.
1417     *
1418     *  @param  configuration   The code generation configuration.
1419     *  @param  setter  The setter method.
1420     */
1421    @SuppressWarnings( {"UseOfConcreteClass", "OverlyCoupledMethod", "OverlyLongMethod", "OverlyComplexMethod"} )
1422    private final void handleSetter( final CodeGenerationConfiguration configuration, final ExecutableElement setter )
1423    {
1424        //---* Get the method name *-------------------------------------------
1425        final var setterMethodName = setter.getSimpleName();
1426
1427        //---* Check for unwanted annotations *--------------------------------
1428        @SuppressWarnings( "unchecked" )
1429        final Class<? extends Annotation> [] unwantedAnnotations = new Class[]
1430        {
1431            SystemProperty.class,
1432            EnvironmentVariable.class,
1433            SystemPreference.class,
1434            Argument.class,
1435            Option.class,
1436            INIValue.class
1437        };
1438        for( final var annotationClass : unwantedAnnotations )
1439        {
1440            if( nonNull( setter.getAnnotation( annotationClass ) ) )
1441            {
1442                throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnSetter, annotationClass.getName(), setterMethodName ) );
1443            }
1444        }
1445
1446        //---* Get the property name *-----------------------------------------
1447        final var propertyName = determinePropertyNameFromMethod( setter );
1448
1449        /*
1450         * Let's see if we have such a property already; if not, we have to
1451         * create it.
1452         */
1453        final PropertySpecImpl property;
1454        final TypeName propertyType;
1455        final CollectionKind collectionKind;
1456        ifDebug( "propertyName: %s"::formatted, propertyName );
1457        final var rawArgumentType = setter.getParameters().getFirst().asType();
1458        final boolean isEnum;
1459        if( configuration.hasProperty( propertyName ) )
1460        {
1461            ifDebug( "property '%s' exists already"::formatted, propertyName );
1462            try
1463            {
1464                property = (PropertySpecImpl) configuration.getProperty( propertyName ).orElseThrow();
1465            }
1466            catch( final NoSuchElementException e )
1467            {
1468                throw new UnexpectedExceptionError( e );
1469            }
1470            ifDebug( property.hasFlag( PROPERTY_IS_SPECIAL ), "property '%s' is special"::formatted, propertyName );
1471
1472            /*
1473             *  The setter's argument type must match the property type, also
1474             *  for special properties – even when this is checked elsewhere
1475             *  again.
1476             */
1477            propertyType = property.getPropertyType();
1478            if( !propertyType.equals( TypeName.from( setter.getParameters().getFirst().asType() ) ) )
1479            {
1480                throw new CodeGenerationError( format( MSG_TypeMismatch, TypeName.from( setter.getParameters().getFirst().asType() ).toString(), setterMethodName, propertyType.toString() ) );
1481            }
1482            collectionKind = property.getCollectionKind();
1483            isEnum = property.isEnum();
1484        }
1485        else
1486        {
1487            //---* Create the new property *-----------------------------------
1488            property = new PropertySpecImpl( propertyName );
1489            configuration.addProperty( property );
1490            checkAppropriate( rawArgumentType );
1491            propertyType = TypeName.from( rawArgumentType );
1492            property.setPropertyType( propertyType );
1493            collectionKind = determineCollectionKind( rawArgumentType );
1494            property.setCollectionKind( collectionKind );
1495            isEnum = isEnumType( rawArgumentType );
1496            property.setIsEnum( isEnum );
1497        }
1498
1499        //---* Immutable special properties may not have a setter *------------
1500        if( property.hasFlag( PROPERTY_IS_SPECIAL ) )
1501        {
1502            property.getSpecialPropertyType()
1503                .map( CodeGenerator::getSpecialPropertySpecification )
1504                .filter( spec -> spec.hasFlag( PROPERTY_IS_MUTABLE ) )
1505                .orElseThrow( () -> new CodeGenerationError( format( MSG_IllegalMutator, propertyName ) ) );
1506        }
1507
1508        //---* There is a setter, so the property is mutable *-----------------
1509        property.setFlag( PROPERTY_IS_MUTABLE );
1510        if( configuration.getSynchronizationRequired() ) property.setFlag( PROPERTY_REQUIRES_SYNCHRONIZATION );
1511
1512        //---* Keep the name of the setter method *----------------------------
1513        property.setSetterMethodName( setterMethodName );
1514
1515        //---* Shall the property be added to the result of toString()? *------
1516        if( nonNull( setter.getAnnotation( ExemptFromToString.class ) ) )
1517        {
1518            property.setFlag( EXEMPT_FROM_TOSTRING );
1519        }
1520
1521        /*
1522         * Default setters are handled differently, and they must have a
1523         * corresponding default getter.
1524         */
1525        final var isDefault = setter.getModifiers().contains( DEFAULT );
1526        if( isDefault)
1527        {
1528            if( property.getGetterMethodName().isEmpty() || !property.hasFlag( GETTER_IS_DEFAULT ) )
1529            {
1530                throw new CodeGenerationError( format( MSG_MissingGetter, propertyName ) );
1531            }
1532            property.setFlag( SETTER_IS_DEFAULT );
1533        }
1534
1535        /*
1536         * If this setter is for a special property, we need to check whether
1537         * we have a getter, and whether that getter is for the same special
1538         * property.
1539         */
1540        final var specialPropertyAnnotation = setter.getAnnotation( SpecialProperty.class );
1541        if( nonNull( specialPropertyAnnotation ) )
1542        {
1543            /*
1544             * A default setter cannot be a setter for a special property.
1545             */
1546            if( isDefault ) throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnSetter, SpecialProperty.class.getName(), setterMethodName ) );
1547
1548            final var specialPropertyType = specialPropertyAnnotation.value();
1549            final var specialPropertyTypeOptional = property.getSpecialPropertyType();
1550            if( specialPropertyTypeOptional.isEmpty() )
1551            {
1552                /*
1553                 * No further analysis required because everything is
1554                 * determined by the special property spec, even the property
1555                 * name.
1556                 */
1557                property.setSpecialPropertyType( specialPropertyType );
1558            }
1559            else
1560            {
1561                if( specialPropertyTypeOptional.get() != specialPropertyType )
1562                {
1563                    throw new IllegalAnnotationError( format( MSG_SpecialPropertyMismatch, SpecialProperty.class.getName(), setterMethodName, specialPropertyType.name(), specialPropertyTypeOptional.get().name() ) );
1564                }
1565            }
1566        }
1567        else
1568        {
1569            //---* Process the preferences annotations *-----------------------
1570            final var noPreferenceAnnotation = setter.getAnnotation( NoPreference.class );
1571            final var preferenceAnnotation = setter.getAnnotation( Preference.class );
1572            final var allowsPreferences = isNull( noPreferenceAnnotation );
1573
1574            if( nonNull(  preferenceAnnotation ) && nonNull( noPreferenceAnnotation ) )
1575            {
1576                throw new IllegalAnnotationError( MSG_PreferenceAnnotationClash, noPreferenceAnnotation );
1577            }
1578
1579            /*
1580             * If there is a getter for the property, the configuration for
1581             * preferences is already made there.
1582             * For a setter, the preferences annotations are only allowed if
1583             * there is no getter.
1584             */
1585            if( property.getGetterMethodName().isPresent() )
1586            {
1587                /*
1588                 * For a setter, the preferences annotations are only allowed
1589                 * if there is no getter.
1590                 */
1591                if( nonNull( preferenceAnnotation ) || nonNull( noPreferenceAnnotation ) )
1592                {
1593                    final var currentAnnotation = nonNull( preferenceAnnotation ) ? preferenceAnnotation : noPreferenceAnnotation;
1594                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnSetter, currentAnnotation.getClass().getName(), setterMethodName ) );
1595                }
1596            }
1597            else
1598            {
1599                if( allowsPreferences )
1600                {
1601                    property.setFlag( ALLOWS_PREFERENCES );
1602
1603                    //---* The default values *--------------------------------
1604                    var preferenceKey = propertyName;
1605                    var accessorClass = PREFS_ACCESSOR_TYPE;
1606
1607                    //---* Check the annotation *------------------------------
1608                    if( nonNull( preferenceAnnotation ) )
1609                    {
1610                        if( isNotEmptyOrBlank( preferenceAnnotation.key() ) ) preferenceKey = preferenceAnnotation.key();
1611
1612                        try
1613                        {
1614                            final var name = "accessor";
1615                            accessorClass = getTypeMirrorFromAnnotationValue( setter, Preference.class, name )
1616                                .map( TypeName::from )
1617                                .orElseThrow( () -> new CodeGenerationError( format( MSG_NoValueForMirror, Preference.class.getName(), name ) ) );
1618                        }
1619                        catch( final NoSuchElementException e )
1620                        {
1621                            throw new CodeGenerationError( format( MSG_CannotRetrieveMirror, Preference.class.getName() ), e );
1622                        }
1623                    }
1624
1625                    //---* Translate the default, if required *--------------------
1626                    accessorClass = retrieveAccessorClass( accessorClass, rawArgumentType, collectionKind );
1627
1628                    //---* Keep the values *-----------------------------------
1629                    property.setPrefsKey( preferenceKey );
1630                    property.setPrefsAccessorClass( accessorClass );
1631                }
1632            }
1633
1634            /*
1635             * Determine the string converter instance, either from the
1636             * annotation or guess it from the property type.
1637             */
1638            final var stringConverterOptional = determineStringConverterClass( setter, propertyType, isEnum );
1639            property.getStringConverterClass()
1640                .ifPresentOrElse( stringConverterClass ->
1641                {
1642                    final var error = new CodeGenerationError( format( MSG_StringConverterMismatch, setterMethodName ) );
1643                    if( !stringConverterClass.equals( stringConverterOptional.orElseThrow( () -> error ) ) ) throw error;
1644                },
1645                () -> stringConverterOptional.ifPresent( property::setStringConverterClass ) );
1646
1647            //---* Configure the setter *--------------------------------------
1648            final var checkEmptyAnnotation = setter.getAnnotation( CheckEmpty.class );
1649            final var checkNullAnnotation = setter.getAnnotation( CheckNull.class );
1650
1651            /*
1652             * The annotations @CheckEmpty and @CheckNull are mutually
1653             * exclusive, although non-empty forces also not-null.
1654             */
1655            if( nonNull( checkNullAnnotation ) && nonNull( checkEmptyAnnotation ) )
1656            {
1657                throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnSetter, CheckNull.class.getName(), setterMethodName ) );
1658            }
1659            if( nonNull( checkEmptyAnnotation ) )
1660            {
1661                if( isDefault )
1662                {
1663                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnSetter, CheckEmpty.class.getName(), setter.getSimpleName() ) );
1664                }
1665                property.setFlag( SETTER_CHECK_EMPTY );
1666            }
1667            if( nonNull( checkNullAnnotation ) )
1668            {
1669                if( isDefault )
1670                {
1671                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnSetter, CheckNull.class.getName(), setter.getSimpleName() ) );
1672                }
1673                property.setFlag( SETTER_CHECK_NULL );
1674            }
1675        }
1676
1677        //---* Create the setter's argument *----------------------------------
1678        property.setSetterArgumentName( retrieveSetterArgumentName( setter ) );
1679
1680        //---* … and now we create the method spec *---------------------------
1681        final var methodBuilder = configuration.getComposer()
1682            .overridingMethodBuilder( setter );
1683        property.setSetterBuilder( methodBuilder );
1684    }   //  handleSetter()
1685
1686    /**
1687     *  Initialises the internal attribute
1688     *  {@link #m_StringConvertersForTypeNames}.
1689     *
1690     *  @return The map of
1691     *      {@link StringConverter}
1692     *      implementations.
1693     */
1694    @API( status = INTERNAL, since = "0.1.0" )
1695    private final Map<TypeName,ClassName> initStringConvertersForTypeNames()
1696    {
1697        final Map<TypeName,ClassName> retValue;
1698        try
1699        {
1700            retValue = createStringConverterRegistry();
1701        }
1702        catch( final IOException e )
1703        {
1704            throw new ExceptionInInitializerError( e );
1705        }
1706        ifDebug( retValue.isEmpty(), $ -> "No StringConverters??" );
1707
1708        //---* Done *----------------------------------------------------------
1709        return retValue;
1710    }   //  initStringConvertersForTypeNames()
1711
1712    /**
1713     *  Parses the given annotation and updates the given property accordingly.
1714     *
1715     *  @param  annotation  The annotation.
1716     *  @param  method  The annotated method.
1717     *  @param  property    The property.
1718     */
1719    @SuppressWarnings( "UseOfConcreteClass" )
1720    private final void parseArgumentAnnotation( final Argument annotation, final ExecutableElement method, final PropertySpecImpl property )
1721    {
1722        property.setFlag( PROPERTY_IS_ARGUMENT );
1723
1724        //---* The index *-----------------------------------------------------
1725        property.setCLIArgumentIndex( annotation.index() );
1726
1727        //---* The other fields *----------------------------------------------
1728        parseCLIAnnotation( annotation, method, property );
1729    }   //  parseArgumentAnnotation()
1730
1731    /**
1732     *  <p>{@summary Parses the given CLI annotation and updates the given
1733     *  property accordingly.}</p>
1734     *  <p>{@code annotation} may be only of type
1735     *  {@link Argument}
1736     *  or
1737     *  {@link Option},
1738     *  although this is not explicitly checked (the method is private!).</p>
1739     *  <p>Usually, the method is only called by the methods
1740     *  {@link #parseArgumentAnnotation(Argument, ExecutableElement, PropertySpecImpl)}
1741     *  and
1742     *  {@link #parseOptionAnnotation(Option, ExecutableElement, PropertySpecImpl)},
1743     *  with the proper arguments.</p>
1744     *
1745     *  @param  annotation  The annotation.
1746     *  @param  method  The annotated method.
1747     *  @param  property    The property.
1748     */
1749    @SuppressWarnings( "UseOfConcreteClass" )
1750    private final void parseCLIAnnotation( final Annotation annotation, @SuppressWarnings( "TypeMayBeWeakened" ) final ExecutableElement method, final PropertySpecImpl property )
1751    {
1752        final var annotationMirror = getAnnotationMirror( method, annotation.getClass() )
1753            .orElseThrow( () -> new CodeGenerationError( format( MSG_CannotRetrieveMirror, annotation.getClass().getName() ) ) );
1754
1755        //---* The value handler class *---------------------------------------
1756        {
1757            final var name = "handler";
1758            final var annotationValue = getAnnotationValue( annotationMirror, name )
1759                .orElseThrow( () -> new CodeGenerationError( format( MSG_NoValueForMirror, annotation.getClass().getName(), name ) ) );
1760            final var defaultClass = getTypeUtils().erasure( getElementUtils().getTypeElement( CmdLineValueHandler.class.getName() ).asType() );
1761            final var handlerClass = getTypeUtils().erasure( (TypeMirror) annotationValue.getValue() );
1762            if( !getTypeUtils().isSameType( defaultClass, handlerClass ) )
1763            {
1764                property.setCLIValueHandlerClass( TypeName.from( handlerClass ) );
1765            }
1766        }
1767
1768        //---* The format *----------------------------------------------------
1769        {
1770            final var name = "format";
1771            getAnnotationValue( annotationMirror, name )
1772                .map( v -> (String) v.getValue() )
1773                .ifPresent( v -> property.setCLIFormat( isNotEmptyOrBlank( v ) ? v : null ) );
1774        }
1775
1776        //---* The meta var *--------------------------------------------------
1777        {
1778            final var name = "metaVar";
1779            getAnnotationValue( annotationMirror, name )
1780                .map( v -> (String) v.getValue() )
1781                .ifPresent( v -> property.setCLIMetaVar( isNotEmptyOrBlank( v ) ? v : null ) );
1782        }
1783
1784        //---* The multi-valued flag *-----------------------------------------
1785        {
1786            final var name = "multiValued";
1787            final var flag = getAnnotationValue( annotationMirror, name )
1788                .map( v -> (Boolean) v.getValue() )
1789                .orElse( FALSE )
1790                .booleanValue();
1791            if( flag ) property.setFlag( PROPERTY_CLI_MULTIVALUED );
1792        }
1793
1794        //---* The required flag *---------------------------------------------
1795        {
1796            final var name = "required";
1797            final var flag = getAnnotationValue( annotationMirror, name )
1798                .map( v -> (Boolean) v.getValue() )
1799                .orElse( FALSE )
1800                .booleanValue();
1801            if( flag ) property.setFlag( PROPERTY_CLI_MANDATORY );
1802        }
1803
1804        //---* The usage text *------------------------------------------------
1805        {
1806            final var name = "usage";
1807            getAnnotationValue( annotationMirror, name )
1808                .map( v -> (String) v.getValue() )
1809                .ifPresent( v -> property.setCLIUsage( isNotEmptyOrBlank( v ) ? v : null ) );
1810        }
1811
1812        //---* The usage text *------------------------------------------------
1813        //noinspection UnnecessaryCodeBlock
1814        {
1815            final var name = "usageKey";
1816            getAnnotationValue( annotationMirror, name )
1817                .map( v -> (String) v.getValue() )
1818                .ifPresent( v -> property.setCLIUsageKey( isNotEmptyOrBlank( v ) ? v : null ) );
1819        }
1820    }   //  parseCLIAnnotation()
1821
1822    /**
1823     *  Parses the given annotation and updates the given property accordingly.
1824     *
1825     *  @param  annotation  The annotation.
1826     *  @param  property    The property.
1827     */
1828    @SuppressWarnings( "UseOfConcreteClass" )
1829    private final void parseEnvironmentVariableAnnotation( final EnvironmentVariable annotation, final PropertySpecImpl property )
1830    {
1831        /*
1832         * The property will be initialised from an environment
1833         * variable with the name given in the annotation.
1834         */
1835        property.setEnvironmentVariableName( annotation.value() );
1836
1837        //---* Set the default value *-----------------------------------------
1838        property.setEnvironmentDefaultValue( annotation.defaultValue() );
1839    }   //  parseEnvironmentVariableAnnotation()
1840
1841    /**
1842     *  Parses the given annotation and updates the given property accordingly.
1843     *
1844     *  @param  annotation  The annotation.
1845     *  @param  method  The annotated method.
1846     *  @param  property    The property.
1847     */
1848    @SuppressWarnings( "UseOfConcreteClass" )
1849    private final void parseOptionAnnotation( final Option annotation, final ExecutableElement method, final PropertySpecImpl property )
1850    {
1851        property.setFlag( PROPERTY_IS_OPTION );
1852
1853        //---* The name and aliases *------------------------------------------
1854        final List<String> names = new ArrayList<>();
1855        names.add( annotation.name() );
1856        names.addAll( asList( annotation.aliases() ) );
1857        property.setCLIOptionNames( names );
1858
1859        //---* The other fields *----------------------------------------------
1860        parseCLIAnnotation( annotation, method, property );
1861    }   //  parseOptionAnnotation()
1862
1863    /**
1864     *  Parses the given annotation and updates the given property accordingly.
1865     *
1866     *  @param  annotation  The annotation.
1867     *  @param  property    The property.
1868     */
1869    @SuppressWarnings( "UseOfConcreteClass" )
1870    private final void parseSystemPropertyAnnotation( final SystemProperty annotation, final PropertySpecImpl property )
1871    {
1872        /*
1873         * The property will be initialised from a system property with
1874         * the name given in the annotation.
1875         */
1876        property.setSystemPropertyName( annotation.value() );
1877
1878        //---* Set the default value *-----------------------------------------
1879        property.setEnvironmentDefaultValue( annotation.defaultValue() );
1880    }   //  parseSystemPropertyAnnotation()
1881
1882    /**
1883     *  {@inheritDoc}
1884     */
1885    @SuppressWarnings( "OverlyNestedMethod" )
1886    @Override
1887    public final boolean process( final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnvironment )
1888    {
1889        //---* Tell them who we are *------------------------------------------
1890        final var message = annotations.isEmpty() ? "No annotations to process" : annotations.stream()
1891            .map( TypeElement::getQualifiedName )
1892            .collect( joining( "', '", "Processing the annotation" + (annotations.size() > 1 ? "s '" : " '"), "'" ) );
1893        printMessage( NOTE, message );
1894
1895        final var retValue = !roundEnvironment.errorRaised() && !annotations.isEmpty();
1896        if( retValue )
1897        {
1898            if( !annotations.isEmpty() )
1899            {
1900                /*
1901                 * Get the values for the i18n annotations and keep them.
1902                 */
1903                //---* Get the message prefix *--------------------------------
1904                m_MessagePrefix = retrieveAnnotatedField( roundEnvironment, MessagePrefix.class )
1905                    .filter( variableElement -> variableElement.getModifiers().containsAll( Set.of( PUBLIC, STATIC ) ) )
1906                    .map( variableElement -> Objects.toString( variableElement.getConstantValue(), EMPTY_STRING ) );
1907
1908                //---* Get the base bundle name *------------------------------
1909                m_BaseBundleName = retrieveAnnotatedField( roundEnvironment, BaseBundleName.class )
1910                    .filter( variableElement -> variableElement.getModifiers().containsAll( Set.of( PUBLIC, STATIC ) ) )
1911                    .map( variableElement -> Objects.toString( variableElement.getConstantValue(), EMPTY_STRING ) );
1912
1913                /*
1914                 * Process the elements that are annotated as configuration
1915                 * bean specification. Although not more than one per
1916                 * application seems logical, it could easily be more than one.
1917                 */
1918                ScanLoop: for( final var element : roundEnvironment.getElementsAnnotatedWith( ConfigurationBeanSpecification.class ) )
1919                {
1920                    /*
1921                     * We are only interested in elements that are type
1922                     * elements, and to be honest, we only want interfaces.
1923                     */
1924                    if( element instanceof final TypeElement typeElement )
1925                    {
1926                        if( typeElement.getKind() == ElementKind.INTERFACE )
1927                        {
1928                            //---* Create the configuration bean *-------------
1929                            try
1930                            {
1931                                processConfigurationBeanSpecification( typeElement );
1932                            }
1933                            catch( final IOException e )
1934                            {
1935                                printMessage( ERROR, e.toString(), element );
1936                            }
1937                        }
1938                        else
1939                        {
1940                            printMessage( ERROR, "%s: Only interfaces may be annotated with '%s'".formatted( typeElement.getQualifiedName().toString(), ConfigurationBeanSpecification.class.getSimpleName() ), element );
1941                            throw new IllegalAnnotationError( MSG_InterfacesOnly, ConfigurationBeanSpecification.class );
1942                        }
1943                    }
1944                    else
1945                    {
1946                        printMessage( ERROR, format( MSG_IllegalAnnotationUse, element.getSimpleName().toString(), ConfigurationBeanSpecification.class.getSimpleName() ), element );
1947                        throw new IllegalAnnotationError( ConfigurationBeanSpecification.class );
1948                    }
1949                }   //  ScanLoop:
1950            }
1951        }
1952
1953        //---* Done *----------------------------------------------------------
1954        return retValue;
1955    }   //  process()
1956
1957    /**
1958     *  Processes the given configuration bean specification and generates the
1959     *  source for the so specified configuration bean.
1960     *
1961     *  @param  specification   The specification interface.
1962     *  @throws IOException A problem occurred when writing the source file.
1963     */
1964    @SuppressWarnings( {"OverlyCoupledMethod", "OverlyComplexMethod"} )
1965    private final void processConfigurationBeanSpecification( final TypeElement specification ) throws IOException
1966    {
1967        //---* Create the composer *-------------------------------------------
1968        final var composer = new JavaComposer( LAYOUT_FOUNDATION, addDebugOutput() );
1969
1970        final var specificationAnnotation = specification.getAnnotation( ConfigurationBeanSpecification.class );
1971
1972        //---* Determine the simple name of the bean class *-------------------
1973        final var configurationBeanClassName = getElementUtils().getName( isEmpty( specificationAnnotation.name() ) ? format( "%sImpl", specification.getSimpleName() ) : specificationAnnotation.name() );
1974        final var isSamePackage = specificationAnnotation.samePackage();
1975
1976        //---* Do we need to synchronise the access to the properties? *-------
1977        final var synchronizeAccess = specificationAnnotation.synchronizeAccess();
1978
1979        //---* Get the base class *--------------------------------------------
1980        TypeName baseClass = null;
1981        try
1982        {
1983            final var annotationValueName = "baseClass";
1984            baseClass = getTypeMirrorFromAnnotationValue( specification, ConfigurationBeanSpecification.class, annotationValueName )
1985                .map( TypeName::from )
1986                .orElseThrow( () -> new CodeGenerationError( format( MSG_NoValueForMirror, ConfigurationBeanSpecification.class.getName(), annotationValueName ) ) );
1987        }
1988        catch( final NoSuchElementException e )
1989        {
1990            throw new CodeGenerationError( format( MSG_CannotRetrieveMirror, ConfigurationBeanSpecification.class.getName() ), e );
1991        }
1992        if( TypeName.from( Object.class ).equals( baseClass ) )
1993        {
1994            //noinspection AssignmentToNull
1995            baseClass = null;
1996        }
1997
1998        /*
1999         * Determine the name of the package for the bean class. As the
2000         * interface may be an inner type, we cannot just take the next best
2001         * enclosing element.
2002         */
2003        var parentElement = specification;
2004        while( parentElement.getNestingKind() != NestingKind.TOP_LEVEL )
2005        {
2006            parentElement = (TypeElement) specification.getEnclosingElement();
2007        }
2008        final var packageElement = (PackageElement) specification.getEnclosingElement();
2009        final var specificationPackageName = packageElement.getQualifiedName().toString();
2010        final var configurationBeanPackageName = getElementUtils().getName
2011            (
2012                isSamePackage
2013                ? specificationPackageName
2014                : packageElement.isUnnamed()
2015                  ? PACKAGE_NAME
2016                  : format( "%s.%s", specificationPackageName, PACKAGE_NAME )
2017            );
2018
2019        //---* Create the configuration for the code generation *--------------
2020        final var specificationClass = ClassName.from( specification );
2021        final var configuration = new CodeGenerationConfiguration( this, composer, specificationClass, configurationBeanClassName, configurationBeanPackageName, baseClass, synchronizeAccess );
2022
2023        //---* Determine the name for the initialisation data resource *-------
2024        var initDataResource = specificationAnnotation.initDataResource();
2025        if( isNotEmptyOrBlank( initDataResource ) )
2026        {
2027            if( "=".equals( initDataResource ) ) initDataResource = format( "%s.properties", specification.getSimpleName() );
2028            configuration.setInitDataResource( initDataResource );
2029        }
2030
2031        //---* Retrieve the method for the initialisation *--------------------
2032        retrieveInitDataMethod( configuration, specification );
2033
2034        /*
2035         * Retrieve all the interfaces that are implemented by the
2036         * specification.
2037         */
2038        final Set<TypeElement> interfaces = new HashSet<>();
2039        retrieveInterfaces( specification, interfaces );
2040        final var interfacesTypes = interfaces.stream()
2041            .map( element -> TypeName.from( element.asType() ) )
2042            .collect( Collectors.toList() );
2043        configuration.addInterfacesToImplement( interfacesTypes );
2044
2045        //---* Retrieve the properties *---------------------------------------
2046        retrieveProperties( configuration, interfaces );
2047
2048        //---* Add the settings for the I18nSupport *--------------------------
2049        if( configuration.implementInterface( I18nSupport.class ) )
2050        {
2051            configuration.setI18NParameters( m_MessagePrefix.orElseThrow( () -> new CodeGenerationError( MSG_NoMessagePrefix ) ),
2052                m_BaseBundleName.orElseThrow( () -> new CodeGenerationError( MSG_NoBaseBundleName ) ) );
2053        }
2054
2055        //---* Determine the settings for the preferences stuff *--------------
2056        final var preferencesRootAnnotation = specification.getAnnotation( PreferencesRoot.class );
2057        if( nonNull( preferencesRootAnnotation ) )
2058        {
2059            configuration.setPreferencesRoot( preferencesRootAnnotation.nodeName() );
2060            try
2061            {
2062                getTypeMirrorFromAnnotationValue( specification, PreferencesRoot.class, "changeListenerClass" )
2063                    .map( TypeName::from )
2064                    .ifPresent( configuration::setPreferenceChangeListenerClass );
2065            }
2066            catch( final NoSuchElementException e )
2067            {
2068                throw new CodeGenerationError( format( MSG_CannotRetrieveMirror, PreferencesRoot.class.getName() ), e );
2069            }
2070        }
2071
2072        //---* Determine the settings for the {@code INI} file stuff *---------
2073        final var iniFileConfig = specification.getAnnotation( INIFileConfig.class );
2074        if( nonNull( iniFileConfig ) )
2075        {
2076            final var filename = iniFileConfig.path();
2077            if( isEmptyOrBlank( filename ) )
2078            {
2079                throw new CodeGenerationError( MSG_INIPathMissing );
2080            }
2081            configuration.setINIFileConfig( filename, iniFileConfig.mustExist(), iniFileConfig.comment() );
2082            for( final var group : specification.getAnnotationsByType( INIGroup.class ) ) configuration.addINIGroup( group );
2083        }
2084
2085        //---* Create the source code *----------------------------------------
2086        try
2087        {
2088            final var generator = new CodeGenerator( configuration );
2089            final var javaFile = generator.createCode();
2090
2091            //---* Write the source file *-------------------------------------
2092            javaFile.writeTo( getFiler() );
2093        }
2094        catch( @SuppressWarnings( "OverlyBroadCatchBlock" ) final Exception e )
2095        {
2096            /*
2097             * Any exception that makes it to this point indicates a failure of
2098             * the code generation process.
2099             */
2100            printMessage( ERROR, format( "Code Generation failed: %s", e.getMessage() ), specification );
2101            throw new CodeGenerationError( format( MSG_CodeGenerationFailed, configurationBeanPackageName, configurationBeanClassName ), e );
2102        }
2103    }   //  processConfigurationBeanSpecification()
2104
2105    /**
2106     *  Retrieves the class for the preference accessor.
2107     *
2108     *  @param  accessorType    The accessor class as defined in the
2109     *      annotation; if this is
2110     *      {@link PreferenceAccessor PreferenceAccessor.class},
2111     *      the effective handler class has to inferred from the
2112     *      {@code propertyType}.
2113     *  @param  propertyType    The type of the property that should be
2114     *      accessed.
2115     *  @param collectionKind   The kind of collection that is represented by
2116     *      the property type.
2117     *  @return The effective accessor class.
2118     *  @throws IllegalAnnotationError  There is no accessor for the given
2119     *      property type.
2120     */
2121    @API( status = INTERNAL, since = "0.0.1" )
2122    private final TypeName retrieveAccessorClass( final TypeName accessorType, final TypeMirror propertyType, final CollectionKind collectionKind ) throws IllegalAnnotationError
2123    {
2124        var retValue = accessorType;
2125        if( isNull( accessorType ) || accessorType.equals( PREFS_ACCESSOR_TYPE ) )
2126        {
2127            //---* Infer the effective accessor class from the property type *-
2128            if( isEnumType( propertyType ) )
2129            {
2130                retValue = ENUM_ACCESSOR_TYPE;
2131            }
2132            else
2133            {
2134                retValue = switch( collectionKind )
2135                {
2136                    case NO_COLLECTION -> m_PrefsAccessorClasses.getOrDefault( TypeName.from( propertyType ), (ClassName) DEFAULT_ACCESSOR_TYPE );
2137                    case LIST -> LIST_ACCESSOR_TYPE;
2138                    case MAP -> MAP_ACCESSOR_TYPE;
2139                    case SET -> SET_ACCESSOR_TYPE;
2140                };
2141            }
2142        }
2143
2144        //---* Done *----------------------------------------------------------
2145        return retValue;
2146    }   //  retrieveAccessorClass()
2147
2148    /**
2149     *  <p>{@summary This methods checks whether the configuration bean
2150     *  specification specifies an {@code initData()} method.} This method has
2151     *  to meet the requirements below:</p>
2152     *  <ul>
2153     *  <li>the name has to be
2154     *  {@value #METHODNAME_ConfigBeanSpec_InitData}</li>
2155     *  <li>it does not take any arguments</li>
2156     *  <li>it returns an instance of {@code Map<String,Object>}</li>
2157     *  <li>it is either {@code static} or {@code default} or implemented in a
2158     *  base class, although this is not checked here</li>
2159     *  </ul>
2160     *  <p>Is such a method exists, a
2161     *  {@link org.tquadrat.foundation.javacomposer.MethodSpec}
2162     *  for it will be created and added to the configuration.</p>
2163     *
2164     *  @param  configuration   The configuration for the code generation.
2165     *  @param  specification   The configuration bean specification interface.
2166     *
2167     *  @note   If a base class for the new configuration bean is defined, the
2168     *      method may be abstract, but if that base class does not implement
2169     *      the method it will be detected only by the final compiler run, not
2170     *      by the code generation here.
2171     */
2172    @SuppressWarnings( {"TypeMayBeWeakened", "UseOfConcreteClass"} )
2173    private final void retrieveInitDataMethod( final CodeGenerationConfiguration configuration, final TypeElement specification )
2174    {
2175        final var hasBaseClass = configuration.getBaseClass().isPresent();
2176        final var returnType = ParameterizedTypeName.from( Map.class, String.class, Object.class );
2177        final var method = specification.getEnclosedElements().stream()
2178            // We are only interested in methods.
2179            .filter( e -> e.getKind() == METHOD )
2180            // Methods are executable elements
2181            .map( e -> (ExecutableElement) e )
2182            /*
2183             * The method has to be either default or static; these modifiers
2184             * are mutually exclusive, so the method has either one or the
2185             * other, or it is abstract, having neither of default or static.
2186             */
2187            .filter( e -> hasBaseClass || e.getModifiers().contains( DEFAULT ) || e.getModifiers().contains( STATIC ) )
2188            // The name of the method should be "initData"
2189            .filter( e -> e.getSimpleName().contentEquals( METHODNAME_ConfigBeanSpec_InitData ) )
2190            // The method may not take any arguments
2191            .filter( e -> e.getParameters().isEmpty() )
2192            // The return type has to be Map<String,Object>
2193            .filter( e -> TypeName.from( e.getReturnType() ) instanceof ParameterizedTypeName )
2194            .filter( e -> returnType.equals( TypeName.from( e.getReturnType() ) ) )
2195            .findFirst();
2196
2197        method.map( methodSpec -> configuration.getComposer().createMethod( methodSpec ) )
2198            .ifPresent( configuration::setInitDataMethod );
2199    }   //  retrieveInitDataMethod()
2200
2201    /**
2202     *  <p>{@summary Scans the configuration bean specification for the
2203     *  properties and stores the result to the configuration.}</p>
2204     *  <p>A property is defined primarily by the respective
2205     *  {@linkplain org.tquadrat.foundation.function.Getter getter}
2206     *  method with its annotations, but it is also possible to define it by a
2207     *  {@linkplain org.tquadrat.foundation.function.Setter setter}
2208     *  method only – especially when the configuration bean specification
2209     *  extends the interface
2210     *  {@link java.util.Map}.</p>
2211     *
2212     *  @param  configuration   The code generation configuration.
2213     *  @param  interfaces  The interfaces that have to implemented by the new
2214     *      configuration bean.
2215     */
2216    @SuppressWarnings( "UseOfConcreteClass" )
2217    private final void retrieveProperties( final CodeGenerationConfiguration configuration, final Collection<? extends TypeElement> interfaces )
2218    {
2219        final var isMap = configuration.implementInterface( Map.class );
2220
2221        /*
2222         * Retrieve getters, setters and 'add' methods from the interfaces.
2223         */
2224        final Collection<ExecutableElement> getters = new ArrayList<>();
2225        final Collection<ExecutableElement> setters = new ArrayList<>();
2226        final Collection<ExecutableElement> addMethods = new ArrayList<>();
2227        //noinspection OverlyLongLambda
2228        interfaces.stream()
2229            .flatMap( element -> element.getEnclosedElements().stream() )
2230            .filter( e -> e.getKind() == METHOD )
2231            .map( e -> (ExecutableElement) e )
2232            .forEach( element ->
2233            {
2234                if( isGetter( element ) )
2235                {
2236                    /*
2237                     * There is a method Map.isEmpty(); if the configuration
2238                     * bean specification extends java.util.Map, this is not a
2239                     * property getter for the 'empty' property.
2240                     * If isMap == true, the method isEmpty() will be
2241                     * implemented elsewhere.
2242                     */
2243                    if( !element.getSimpleName().toString().equals( METHODNAME_Map_IsEmpty ) || !isMap )
2244                    {
2245                        getters.add( element );
2246                    }
2247                }
2248                else if( isSetter( element ) )
2249                {
2250                    setters.add( element );
2251                }
2252                else if( isAddMethod( element ) )
2253                {
2254                    /*
2255                     * There is a method ConfigBeanSpec.addListener() that is
2256                     * necessary for the listener management, and not an 'add'
2257                     * method for a property. Therefore, it will be implemented
2258                     * elsewhere.
2259                     */
2260                    if( !element.getSimpleName().toString().equals( METHODNAME_ConfigBeanSpec_AddListener ) )
2261                    {
2262                        addMethods.add( element );
2263                    }
2264                }
2265            } );
2266
2267        /*
2268         *  Getters are to be processed first.
2269         *  This will create the properties that are stored in the
2270         *  configuration.
2271         */
2272        getters.forEach( getter -> handleGetter( configuration, getter ) );
2273
2274        //---* Process the setters *-------------------------------------------
2275        setters.forEach( setter -> handleSetter( configuration, setter ) );
2276
2277        //---* Process the 'add' methods *-------------------------------------
2278        addMethods.forEach( addMethod -> handleAddMethod( configuration, addMethod ) );
2279    }   //  retrieveProperties()
2280
2281    /**
2282     *  <p>{@summary Retrieves the name of the single argument of a setter
2283     *  method.}</p>
2284     *  <p>This method will return the name of the argument as defined in the
2285     *  configuration bean specification if the compiler flag
2286     *  {@code -parameters} is set; otherwise, the arguments are just counted
2287     *  ({@code arg0}, {@code arg1}, {@code arg2}, …).</p>
2288     *
2289     *  @param  setter  The setter method.
2290     *  @return The name of the argument as defined in the configuration bean
2291     *      specification, or &quot;arg0&quot;
2292     */
2293    private final Name retrieveSetterArgumentName( final ExecutableElement setter )
2294    {
2295        final var parameters = retrieveArgumentNames( setter );
2296        if( parameters.size() != 1 ) throw new CodeGenerationError( format( MSG_NoSetter, setter.getSimpleName() ) );
2297        final var retValue = parameters.getFirst();
2298
2299        //---* Done *----------------------------------------------------------
2300        return retValue;
2301    }   //  retrieveSetterArgumentName()
2302
2303    /**
2304     *  <p>{@summary Determines the key class for the given instance of
2305     *  {@link StringConverter}.}</p>
2306     *
2307     *  @note   This method was copied from
2308     *      {@code org.tquadrat.foundation.base/org.tquadrat.foundation.lang.internal.StringConverterService}.
2309     *
2310     *  @param  converter   The converter instance.
2311     *  @return The subject class.
2312     */
2313    @SuppressWarnings( {"NestedTryStatement", "unchecked"} )
2314    private static final Collection<Class<?>> retrieveSubjectClasses( final StringConverter<?> converter )
2315    {
2316        final var converterClass = requireNonNullArgument( converter, "converter" ).getClass();
2317        Collection<Class<?>> retValue;
2318        try
2319        {
2320            try
2321            {
2322                final var getSubjectClassMethod = converterClass.getMethod( METHOD_NAME_GetSubjectClass );
2323                //noinspection unchecked
2324                retValue = (Collection<Class<?>>) getSubjectClassMethod.invoke( converter );
2325            }
2326            catch( @SuppressWarnings( "unused" ) final NoSuchMethodException ignored )
2327            {
2328                final var fromStringMethod = converterClass.getMethod( "fromString", CharSequence.class );
2329                retValue = List.of( fromStringMethod.getReturnType() );
2330            }
2331        }
2332        catch( final NoSuchMethodException | SecurityException | IllegalAccessException | InvocationTargetException e )
2333        {
2334            throw new UnexpectedExceptionError( e );
2335        }
2336
2337        //---* Done *----------------------------------------------------------
2338        return retValue;
2339    }   //  retrieveSubjectClass()
2340}
2341//  class ConfigAnnotationProcessor
2342
2343/*
2344 *  End of File
2345 */