001/*
002 * ============================================================================
003 *  Copyright © 2002-2025 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 1151 2025-10-01 21:32:15Z 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 1151 2025-10-01 21:32:15Z 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    @SuppressWarnings( "GrazieInspection" )
896    private final TypeMirror determinePropertyType( final TypeMirror type )
897    {
898        var retValue = requireNonNullArgument( type, "type" );
899        final var optionalType = getTypeUtils().erasure( getElementUtils().getTypeElement( Optional.class.getName() ).asType() );
900        if( getTypeUtils().isAssignable( getTypeUtils().erasure( retValue ), optionalType ) )
901        {
902            /*
903             * The return type is Optional, we need to get the type parameter
904             * and have to look at that.
905             */
906            final var genericTypes = retrieveGenericTypes( retValue );
907            if( genericTypes.size() == 1 ) retValue = genericTypes.getFirst();
908        }
909
910        //---* Check whether the property type is appropriate *----------------
911        checkAppropriate( retValue );
912
913        //---* Done *----------------------------------------------------------
914        return retValue;
915    }   //  determinePropertyType()
916
917    /**
918     *  Determines the implementation of
919     *  {@link StringConverter}
920     *  that can translate a String into an instance of the given type and
921     *  vice versa.
922     *
923     *  @param  method  The annotated method; it is only used to get the
924     *      instance of
925     *      {@link StringConversion &#64;StringConversion}
926     *      from it.
927     *  @param  type    The target type.
928     *  @param  isEnum  {@code true} if the target type is an
929     *      {@link Enum enum}
930     *      type, {@code false} otherwise.
931     *  @return An instance of
932     *      {@link Optional}
933     *      that holds the determined class.
934     */
935    private final Optional<ClassName> determineStringConverterClass( final ExecutableElement method, final TypeName type, final boolean isEnum )
936    {
937        requireNonNullArgument( type, "type" );
938        requireNonNullArgument( method, "method" );
939        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 ) );
940
941        //---* Retrieve the StringConverter from the annotation *--------------
942        final var retValue = extractStringConverterClass( method )
943            .or( () -> Optional.ofNullable( isEnum ? ClassName.from( EnumStringConverter.class ) : m_StringConvertersForTypeNames.get( type ) ) );
944        //noinspection unchecked
945        ifDebug( a -> ((Optional<TypeName>) a [0]).map( "Detected StringConverter: %1$s"::formatted ).orElse( "Could not find a StringConverter" ), retValue );
946
947        //---* Done *----------------------------------------------------------
948        return retValue;
949    }   //  determineStringConverterClass
950
951    /**
952     *  <p>{@summary Retrieves the value for the
953     *  {@link StringConversion &#64;StringConversion}
954     *  annotation from the given method.}</p>
955     *  <p>The type for the annotation value is an instance of
956     *  {@link Class Class&lt;? extends StringConverter&gt;},
957     *  so it cannot be retrieved directly. Therefore, this method will return
958     *  the
959     *  {@link TypeName}
960     *  for the
961     *  {@link StringConverter}
962     *  implementation class.</p>
963     *
964     *  @param  method  The annotated method.
965     *  @return An instance of
966     *      {@link Optional}
967     *      holding the type name that represents the annotation value
968     *      &quot;<i>stringConverter</i>&quot;.
969     */
970    public final Optional<ClassName> extractStringConverterClass( final ExecutableElement method )
971    {
972        final var retValue = getAnnotationMirror( requireNonNullArgument( method, "method" ), StringConversion.class )
973            .flatMap( this::getAnnotationValue )
974            .map( annotationValue -> TypeName.from( (TypeMirror) annotationValue.getValue() ) )
975            .map( TypeName::toString )
976            .map( JavaUtils::loadClass )
977            .filter( Optional::isPresent )
978            .map( Optional::get )
979            .map( ClassName::from );
980
981        //---* Done *----------------------------------------------------------
982        return retValue;
983    }   //  extractStringConverterClass()
984
985    /**
986     *  {@inheritDoc}
987     */
988    @Override
989    protected final Collection<Class<? extends Annotation>> getSupportedAnnotationClasses()
990    {
991        final Collection<Class<? extends Annotation>> retValue =
992            List.of( ConfigurationBeanSpecification.class );
993
994        //---* Done *----------------------------------------------------------
995        return retValue;
996    }   //  getSupportedAnnotationClasses()
997
998    /**
999     *  Processes the given
1000     *  {@link ExecutableElement}
1001     *  instance for an 'add' method.
1002     *
1003     *  @param  configuration   The code generation configuration.
1004     *  @param  addMethod   The 'add' method.
1005     */
1006    @SuppressWarnings( {"UseOfConcreteClass", "OverlyCoupledMethod", "OverlyComplexMethod"} )
1007    private final void handleAddMethod( final CodeGenerationConfiguration configuration, final ExecutableElement addMethod )
1008    {
1009        //---* Get the method name *-------------------------------------------
1010        final var addMethodName = addMethod.getSimpleName();
1011
1012        //---* Check for unwanted annotations *--------------------------------
1013        @SuppressWarnings( "unchecked" )
1014        Class<? extends Annotation> [] unwantedAnnotations = new Class[]
1015        {
1016            SystemProperty.class,
1017            EnvironmentVariable.class,
1018            SystemPreference.class,
1019            Preference.class,
1020            NoPreference.class,
1021            Argument.class,
1022            Option.class,
1023            INIValue.class
1024        };
1025        for( final var annotationClass : unwantedAnnotations )
1026        {
1027            if( nonNull( addMethod.getAnnotation( annotationClass ) ) )
1028            {
1029                throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnAddMethod, annotationClass.getName(), addMethodName ) );
1030            }
1031        }
1032
1033        /*
1034         * If the 'add' method is default, our job is mostly done already.
1035         */
1036        final var isDefault = addMethod.getModifiers().contains( DEFAULT );
1037        if( isDefault )
1038        {
1039            //noinspection unchecked
1040            unwantedAnnotations = new Class[]
1041                {
1042                    CheckEmpty.class,
1043                    CheckNull.class,
1044                    SpecialProperty.class
1045                };
1046            for( final var annotationClass : unwantedAnnotations )
1047            {
1048                if( nonNull( addMethod.getAnnotation( annotationClass ) ) )
1049                {
1050                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnAddMethod, annotationClass.getName(), addMethodName ) );
1051                }
1052            }
1053        }
1054        else
1055        {
1056            //---* Get the property name *-------------------------------------
1057            final var propertyName = determinePropertyNameFromMethod( addMethod );
1058
1059            /*
1060             * As we cannot infer the property type from an add method, it is
1061             * required that we already have a property specification for this
1062             * property.
1063             */
1064            final var property = (PropertySpecImpl) configuration.getProperty( propertyName )
1065                .orElseThrow( () -> new org.tquadrat.foundation.ap.CodeGenerationError( format( MSG_MissingPropertyDefinition, propertyName, addMethodName ) ) );
1066
1067            //---* Only collections may have 'add' methods *-------------------
1068            if( property.getCollectionKind() == NO_COLLECTION )
1069            {
1070                throw new CodeGenerationError( format( MSG_AddMethodNotAllowed, addMethodName ) );
1071            }
1072
1073            //---* Immutable special properties may not have an add method *---
1074            if( property.hasFlag( PROPERTY_IS_SPECIAL ) )
1075            {
1076                property.getSpecialPropertyType()
1077                    .map( CodeGenerator::getSpecialPropertySpecification )
1078                    .filter( spec -> spec.hasFlag( PROPERTY_IS_MUTABLE ) )
1079                    .orElseThrow( () -> new CodeGenerationError( format( MSG_IllegalMutator, propertyName ) ) );
1080            }
1081
1082            //---* Save the method name and variable name *--------------------
1083            property.setAddMethodName( addMethodName );
1084            property.setAddMethodArgumentName( retrieveSetterArgumentName( addMethod ) );
1085
1086            //---* We have an add method, so the property is mutable *---------
1087            property.setFlag( PROPERTY_IS_MUTABLE );
1088            if( configuration.getSynchronizationRequired() ) property.setFlag( PROPERTY_REQUIRES_SYNCHRONIZATION );
1089
1090            /*
1091             * For a special property, all the definitions are made there, so
1092             * nothing to do here …
1093             */
1094            if( !property.hasFlag( PROPERTY_IS_SPECIAL ) )
1095            {
1096                final var propertyType = property.getPropertyType();
1097                if( isNull( propertyType ) )
1098                {
1099                    throw new CodeGenerationError( format( MSG_NoType, addMethodName, propertyName ) );
1100                }
1101
1102                /*
1103                 * Get the StringConverter; as this cannot be inferred it has
1104                 * to be taken from the annotation @StringConversion, if
1105                 * present. And then it will override the already set one.
1106                 */
1107                extractStringConverterClass( addMethod )
1108                    .ifPresent( property::setStringConverterClass );
1109            }
1110
1111            /*
1112             * Shall the property be added to the result of toString()?
1113             */
1114            if( nonNull( addMethod.getAnnotation( ExemptFromToString.class ) ) )
1115            {
1116                property.setFlag( EXEMPT_FROM_TOSTRING );
1117            }
1118
1119            //---* … and now we create the method spec *-----------------------
1120            final var methodBuilder = configuration.getComposer()
1121                .overridingMethodBuilder( addMethod );
1122            property.setAddMethodBuilder( methodBuilder );
1123        }
1124    }   //  handleAddMethod()
1125
1126    /**
1127     *  Processes the given
1128     *  {@link ExecutableElement}
1129     *  instance for a getter method.
1130     *
1131     *  @param  configuration   The code generation configuration.
1132     *  @param  getter  The getter method.
1133     */
1134    @SuppressWarnings( {"UseOfConcreteClass", "OverlyCoupledMethod", "OverlyLongMethod", "OverlyComplexMethod"} )
1135    private final void handleGetter( final CodeGenerationConfiguration configuration, final ExecutableElement getter )
1136    {
1137        //---* Get the property name *-----------------------------------------
1138        final var propertyName = determinePropertyNameFromMethod( getter );
1139
1140        /*
1141         * Create the new property spec and store it. The getters will be
1142         * created first, so a property with the same name, defined by another
1143         * getter, may not exist already. The respective check is made on
1144         * storing.
1145         */
1146        final var property = new PropertySpecImpl( propertyName );
1147        configuration.addProperty( property );
1148
1149        //---* Keep the name of the getter method *----------------------------
1150        final var getterMethodName = getter.getSimpleName();
1151        property.setGetterMethodName( getterMethodName );
1152
1153        //---* Shall the property be added to the result of toString()? *------
1154        if( nonNull( getter.getAnnotation( ExemptFromToString.class ) ) )
1155        {
1156            property.setFlag( EXEMPT_FROM_TOSTRING );
1157        }
1158
1159        //---* Default getters are handled differently *-----------------------
1160        final var isDefault = getter.getModifiers().contains( DEFAULT );
1161        if( isDefault )
1162        {
1163            /*
1164             * Several annotations are not allowed for a default getter.
1165             */
1166            @SuppressWarnings( "unchecked" )
1167            final Class<? extends Annotation> [] unwantedAnnotations = new Class[]
1168            {
1169                SystemProperty.class,
1170                EnvironmentVariable.class,
1171                SystemPreference.class,
1172                Argument.class,
1173                Option.class,
1174                Preference.class,
1175                SpecialProperty.class,
1176                INIValue.class
1177            };
1178            for( final var annotationClass : unwantedAnnotations )
1179            {
1180                if( nonNull( getter.getAnnotation( annotationClass ) ) )
1181                {
1182                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnGetter, annotationClass.getName(), getter.getSimpleName() ) );
1183                }
1184            }
1185            property.setFlag( GETTER_IS_DEFAULT, GETTER_ON_MAP );
1186        }
1187        var allowsPreferences = !isDefault;
1188
1189        //---* Determine the property type *-----------------------------------
1190        final var rawReturnType = getter.getReturnType();
1191        final var rawPropertyType = determinePropertyType( rawReturnType );
1192        final var propertyType = TypeName.from( rawPropertyType );
1193        final var returnType = TypeName.from( rawReturnType );
1194        property.setPropertyType( propertyType );
1195        final var collectionKind = determineCollectionKind( rawPropertyType );
1196        property.setCollectionKind( collectionKind );
1197        final var isEnum = isEnumType( rawPropertyType );
1198        property.setIsEnum( isEnum );
1199        switch( collectionKind )
1200        {
1201            case LIST, SET ->
1202                determineElementType( rawPropertyType )
1203                    .filter( this::isEnumType )
1204                    .ifPresent( _ -> property.setFlag( ELEMENTTYPE_IS_ENUM ) );
1205
1206            case MAP ->
1207            {
1208                // Does nothing currently
1209            }
1210
1211            case NO_COLLECTION -> { /* Nothing to do */ }
1212        }
1213
1214        /*
1215         * Some properties are 'special', and that is reflected by the
1216         * annotation @SpecialProperty.
1217         */
1218        final var specialPropertyAnnotation = getter.getAnnotation( SpecialProperty.class );
1219        if( nonNull( specialPropertyAnnotation ) )
1220        {
1221            /*
1222             * No further analysis required because everything is determined by
1223             * the special property spec, even the property name.
1224             */
1225            property.setSpecialPropertyType( specialPropertyAnnotation.value() );
1226        }
1227        else
1228        {
1229            //---* Keep the return type *--------------------------------------
1230            property.setGetterReturnType( returnType );
1231
1232            /*
1233             * Check whether the return type is Optional; only when the return
1234             * type is Optional, it can be different from the property type.
1235             */
1236            if( !returnType.equals( propertyType ) ) property.setFlag( GETTER_RETURNS_OPTIONAL );
1237
1238            /*
1239             * Determine the string converter instance, either from the
1240             * annotation or guess it from the property type.
1241             */
1242            determineStringConverterClass( getter, propertyType, isEnum ).ifPresent( property::setStringConverterClass );
1243
1244            if( !isDefault )
1245            {
1246                //---* Set the field name *------------------------------------
1247                property.setFieldName( composeFieldName( propertyName ) );
1248            }
1249
1250            //---* Additional annotations *------------------------------------
1251            final Optional<INIValue> iniValue = property.getStringConverterClass().isPresent()
1252                ? Optional.ofNullable( getter.getAnnotation( INIValue.class ) )
1253                : Optional.empty();
1254            iniValue.ifPresent( a ->
1255                {
1256                    property.setINIConfiguration( a );
1257                    property.setFlag( ALLOWS_INIFILE, PROPERTY_IS_MUTABLE );
1258                } );
1259            //noinspection NonShortCircuitBooleanExpression
1260            allowsPreferences &= iniValue.isEmpty();
1261
1262            final var systemPropertyAnnotation = getter.getAnnotation( SystemProperty.class );
1263            if( nonNull( systemPropertyAnnotation ) )
1264            {
1265                if( iniValue.isPresent() )
1266                {
1267                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnGetter, INIValue.class.getName(), getter.getSimpleName() ) );
1268                }
1269
1270                parseSystemPropertyAnnotation( systemPropertyAnnotation, property );
1271
1272                /*
1273                 * System properties may not be stored to/retrieved from
1274                 * preferences or an INIFile.
1275                 */
1276                allowsPreferences = false;
1277            }
1278
1279            final var environmentVariableAnnotation = getter.getAnnotation( EnvironmentVariable.class );
1280            if( nonNull( environmentVariableAnnotation ) )
1281            {
1282                if( iniValue.isPresent() )
1283                {
1284                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnGetter, INIValue.class.getName(), getter.getSimpleName() ) );
1285                }
1286
1287                /*
1288                 * @SystemProperty and @EnvironmentVariable are mutual
1289                 * exclusive.
1290                 */
1291                if( nonNull( systemPropertyAnnotation ) )
1292                {
1293                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnGetter, EnvironmentVariable.class.getName(), getter.getSimpleName() ) );
1294                }
1295
1296                parseEnvironmentVariableAnnotation( environmentVariableAnnotation, property );
1297
1298                /*
1299                 * Environment variable values may not be stored
1300                 * to/retrieved from preferences.
1301                 */
1302                allowsPreferences = false;
1303            }
1304
1305            final var systemPrefsAnnotation = getter.getAnnotation( SystemPreference.class );
1306            if( nonNull( systemPrefsAnnotation ) )
1307            {
1308                /*
1309                 * @INIValue, @SystemProperty, @EnvironmentVariable and
1310                 * @SystemPreference are mutual exclusive.
1311                 */
1312                if( nonNull( systemPropertyAnnotation ) || nonNull( environmentVariableAnnotation ) || iniValue.isPresent() )
1313                {
1314                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnGetter, SystemPreference.class.getName(), getter.getSimpleName() ) );
1315                }
1316
1317                /*
1318                 * The property will be initialised from a system preference
1319                 * with the name given in the annotation.
1320                 */
1321                property.setSystemPrefsPath( systemPrefsAnnotation.path() );
1322                final var prefsKey = systemPrefsAnnotation.key();
1323                if( isNotEmptyOrBlank( prefsKey ) )
1324                {
1325                    property.setPrefsKey( prefsKey );
1326                }
1327                else
1328                {
1329                    throw new IllegalAnnotationError( format( MSG_PrefsKeyMissing, propertyName ) );
1330                }
1331
1332                final var accessorClass = getAnnotationMirror( getter, SystemPreference.class )
1333                    .flatMap( mirror -> getAnnotationValue( mirror, "accessor" ) )
1334                    .map( annotationValue -> TypeName.from( (TypeMirror) annotationValue.getValue() ) )
1335                    .orElseThrow( () -> new IllegalAnnotationError( format( MSG_AccessorMissing, propertyName ) ) );
1336                property.setPrefsAccessorClass( accessorClass );
1337
1338                /*
1339                 * System preference values may not be stored to/retrieved from
1340                 * preferences.
1341                 */
1342                allowsPreferences = false;
1343            }
1344
1345            //---* Process the CLI annotations *-------------------------------
1346            final var argumentAnnotation = getter.getAnnotation( Argument.class );
1347            final var optionAnnotation = getter.getAnnotation( Option.class );
1348            if( nonNull( argumentAnnotation) && nonNull( optionAnnotation ) )
1349            {
1350                throw new IllegalAnnotationError( MSG_CLIAnnotationClash, optionAnnotation );
1351            }
1352            if( nonNull( argumentAnnotation ) )
1353            {
1354                parseArgumentAnnotation( argumentAnnotation, getter, property );
1355            }
1356            if( nonNull( optionAnnotation ) )
1357            {
1358                parseOptionAnnotation( optionAnnotation, getter, property );
1359            }
1360
1361            //---* Process the preferences annotations *-----------------------
1362            final var noPreferenceAnnotation = getter.getAnnotation( NoPreference.class );
1363            final var preferenceAnnotation = getter.getAnnotation( Preference.class );
1364            if( !allowsPreferences && nonNull( preferenceAnnotation ) )
1365            {
1366                throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnGetter, Preference.class.getName(), getter.getSimpleName() ) );
1367            }
1368            if( nonNull(  preferenceAnnotation ) && nonNull( noPreferenceAnnotation ) )
1369            {
1370                throw new IllegalAnnotationError( MSG_PreferenceAnnotationClash, noPreferenceAnnotation );
1371            }
1372            if( nonNull( noPreferenceAnnotation ) ) allowsPreferences = false;
1373            if( allowsPreferences )
1374            {
1375                property.setFlag( ALLOWS_PREFERENCES );
1376
1377                //---* The default values *------------------------------------
1378                var preferenceKey = propertyName;
1379                var accessorClass = PREFS_ACCESSOR_TYPE;
1380
1381                //---* Check the annotation *----------------------------------
1382                if( nonNull( preferenceAnnotation ) )
1383                {
1384                    if( isNotEmptyOrBlank( preferenceAnnotation.key() ) ) preferenceKey = preferenceAnnotation.key();
1385
1386                    try
1387                    {
1388                        final var name = "accessor";
1389                        accessorClass = getTypeMirrorFromAnnotationValue( getter, Preference.class, name )
1390                            .map( TypeName::from )
1391                            .orElseThrow( () -> new CodeGenerationError( format( MSG_NoValueForMirror, Preference.class.getName(), name ) ) );
1392                    }
1393                    catch( final NoSuchElementException e )
1394                    {
1395                        throw new CodeGenerationError( format( MSG_CannotRetrieveMirror, Preference.class.getName() ), e );
1396                    }
1397                }
1398
1399                //---* Translate the default, if required *--------------------
1400                accessorClass = retrieveAccessorClass( accessorClass, rawPropertyType, collectionKind );
1401
1402                //---* Keep the values *---------------------------------------
1403                property.setPrefsKey( preferenceKey );
1404                property.setPrefsAccessorClass( accessorClass );
1405            }
1406
1407            //---* … and now we create the method spec *-------------------
1408            final var methodBuilder = configuration.getComposer()
1409                .overridingMethodBuilder( getter );
1410            property.setGetterBuilder( methodBuilder );
1411        }
1412    }   //  handleGetter()
1413
1414    /**
1415     *  Processes the given
1416     *  {@link ExecutableElement}
1417     *  instance for a setter method.
1418     *
1419     *  @param  configuration   The code generation configuration.
1420     *  @param  setter  The setter method.
1421     */
1422    @SuppressWarnings( {"UseOfConcreteClass", "OverlyCoupledMethod", "OverlyLongMethod", "OverlyComplexMethod"} )
1423    private final void handleSetter( final CodeGenerationConfiguration configuration, final ExecutableElement setter )
1424    {
1425        //---* Get the method name *-------------------------------------------
1426        final var setterMethodName = setter.getSimpleName();
1427
1428        //---* Check for unwanted annotations *--------------------------------
1429        @SuppressWarnings( "unchecked" )
1430        final Class<? extends Annotation> [] unwantedAnnotations = new Class[]
1431        {
1432            SystemProperty.class,
1433            EnvironmentVariable.class,
1434            SystemPreference.class,
1435            Argument.class,
1436            Option.class,
1437            INIValue.class
1438        };
1439        for( final var annotationClass : unwantedAnnotations )
1440        {
1441            if( nonNull( setter.getAnnotation( annotationClass ) ) )
1442            {
1443                throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnSetter, annotationClass.getName(), setterMethodName ) );
1444            }
1445        }
1446
1447        //---* Get the property name *-----------------------------------------
1448        final var propertyName = determinePropertyNameFromMethod( setter );
1449
1450        /*
1451         * Let's see if we have such a property already; if not, we have to
1452         * create it.
1453         */
1454        final PropertySpecImpl property;
1455        final TypeName propertyType;
1456        final CollectionKind collectionKind;
1457        ifDebug( "propertyName: %s"::formatted, propertyName );
1458        final var rawArgumentType = setter.getParameters().getFirst().asType();
1459        final boolean isEnum;
1460        if( configuration.hasProperty( propertyName ) )
1461        {
1462            ifDebug( "property '%s' exists already"::formatted, propertyName );
1463            try
1464            {
1465                property = (PropertySpecImpl) configuration.getProperty( propertyName ).orElseThrow();
1466            }
1467            catch( final NoSuchElementException e )
1468            {
1469                throw new UnexpectedExceptionError( e );
1470            }
1471            ifDebug( property.hasFlag( PROPERTY_IS_SPECIAL ), "property '%s' is special"::formatted, propertyName );
1472
1473            /*
1474             *  The setter's argument type must match the property type, also
1475             *  for special properties – even when this is checked elsewhere
1476             *  again.
1477             */
1478            propertyType = property.getPropertyType();
1479            if( !propertyType.equals( TypeName.from( setter.getParameters().getFirst().asType() ) ) )
1480            {
1481                throw new CodeGenerationError( format( MSG_TypeMismatch, TypeName.from( setter.getParameters().getFirst().asType() ).toString(), setterMethodName, propertyType.toString() ) );
1482            }
1483            collectionKind = property.getCollectionKind();
1484            isEnum = property.isEnum();
1485        }
1486        else
1487        {
1488            //---* Create the new property *-----------------------------------
1489            property = new PropertySpecImpl( propertyName );
1490            configuration.addProperty( property );
1491            checkAppropriate( rawArgumentType );
1492            propertyType = TypeName.from( rawArgumentType );
1493            property.setPropertyType( propertyType );
1494            collectionKind = determineCollectionKind( rawArgumentType );
1495            property.setCollectionKind( collectionKind );
1496            isEnum = isEnumType( rawArgumentType );
1497            property.setIsEnum( isEnum );
1498        }
1499
1500        //---* Immutable special properties may not have a setter *------------
1501        if( property.hasFlag( PROPERTY_IS_SPECIAL ) )
1502        {
1503            property.getSpecialPropertyType()
1504                .map( CodeGenerator::getSpecialPropertySpecification )
1505                .filter( spec -> spec.hasFlag( PROPERTY_IS_MUTABLE ) )
1506                .orElseThrow( () -> new CodeGenerationError( format( MSG_IllegalMutator, propertyName ) ) );
1507        }
1508
1509        //---* There is a setter, so the property is mutable *-----------------
1510        property.setFlag( PROPERTY_IS_MUTABLE );
1511        if( configuration.getSynchronizationRequired() ) property.setFlag( PROPERTY_REQUIRES_SYNCHRONIZATION );
1512
1513        //---* Keep the name of the setter method *----------------------------
1514        property.setSetterMethodName( setterMethodName );
1515
1516        //---* Shall the property be added to the result of toString()? *------
1517        if( nonNull( setter.getAnnotation( ExemptFromToString.class ) ) )
1518        {
1519            property.setFlag( EXEMPT_FROM_TOSTRING );
1520        }
1521
1522        /*
1523         * Default setters are handled differently, and they must have a
1524         * corresponding default getter.
1525         */
1526        final var isDefault = setter.getModifiers().contains( DEFAULT );
1527        if( isDefault)
1528        {
1529            if( property.getGetterMethodName().isEmpty() || !property.hasFlag( GETTER_IS_DEFAULT ) )
1530            {
1531                throw new CodeGenerationError( format( MSG_MissingGetter, propertyName ) );
1532            }
1533            property.setFlag( SETTER_IS_DEFAULT );
1534        }
1535
1536        /*
1537         * If this setter is for a special property, we need to check whether
1538         * we have a getter, and whether that getter is for the same special
1539         * property.
1540         */
1541        final var specialPropertyAnnotation = setter.getAnnotation( SpecialProperty.class );
1542        if( nonNull( specialPropertyAnnotation ) )
1543        {
1544            /*
1545             * A default setter cannot be a setter for a special property.
1546             */
1547            if( isDefault ) throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnSetter, SpecialProperty.class.getName(), setterMethodName ) );
1548
1549            final var specialPropertyType = specialPropertyAnnotation.value();
1550            final var specialPropertyTypeOptional = property.getSpecialPropertyType();
1551            if( specialPropertyTypeOptional.isEmpty() )
1552            {
1553                /*
1554                 * No further analysis required because everything is
1555                 * determined by the special property spec, even the property
1556                 * name.
1557                 */
1558                property.setSpecialPropertyType( specialPropertyType );
1559            }
1560            else
1561            {
1562                if( specialPropertyTypeOptional.get() != specialPropertyType )
1563                {
1564                    throw new IllegalAnnotationError( format( MSG_SpecialPropertyMismatch, SpecialProperty.class.getName(), setterMethodName, specialPropertyType.name(), specialPropertyTypeOptional.get().name() ) );
1565                }
1566            }
1567        }
1568        else
1569        {
1570            //---* Process the preferences annotations *-----------------------
1571            final var noPreferenceAnnotation = setter.getAnnotation( NoPreference.class );
1572            final var preferenceAnnotation = setter.getAnnotation( Preference.class );
1573            final var allowsPreferences = isNull( noPreferenceAnnotation );
1574
1575            if( nonNull(  preferenceAnnotation ) && nonNull( noPreferenceAnnotation ) )
1576            {
1577                throw new IllegalAnnotationError( MSG_PreferenceAnnotationClash, noPreferenceAnnotation );
1578            }
1579
1580            /*
1581             * If there is a getter for the property, the configuration for
1582             * preferences is already made there.
1583             * For a setter, the preferences annotations are only allowed if
1584             * there is no getter.
1585             */
1586            if( property.getGetterMethodName().isPresent() )
1587            {
1588                /*
1589                 * For a setter, the preferences annotations are only allowed
1590                 * if there is no getter.
1591                 */
1592                if( nonNull( preferenceAnnotation ) || nonNull( noPreferenceAnnotation ) )
1593                {
1594                    final var currentAnnotation = nonNull( preferenceAnnotation ) ? preferenceAnnotation : noPreferenceAnnotation;
1595                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnSetter, currentAnnotation.getClass().getName(), setterMethodName ) );
1596                }
1597            }
1598            else
1599            {
1600                if( allowsPreferences )
1601                {
1602                    property.setFlag( ALLOWS_PREFERENCES );
1603
1604                    //---* The default values *--------------------------------
1605                    var preferenceKey = propertyName;
1606                    var accessorClass = PREFS_ACCESSOR_TYPE;
1607
1608                    //---* Check the annotation *------------------------------
1609                    if( nonNull( preferenceAnnotation ) )
1610                    {
1611                        if( isNotEmptyOrBlank( preferenceAnnotation.key() ) ) preferenceKey = preferenceAnnotation.key();
1612
1613                        try
1614                        {
1615                            final var name = "accessor";
1616                            accessorClass = getTypeMirrorFromAnnotationValue( setter, Preference.class, name )
1617                                .map( TypeName::from )
1618                                .orElseThrow( () -> new CodeGenerationError( format( MSG_NoValueForMirror, Preference.class.getName(), name ) ) );
1619                        }
1620                        catch( final NoSuchElementException e )
1621                        {
1622                            throw new CodeGenerationError( format( MSG_CannotRetrieveMirror, Preference.class.getName() ), e );
1623                        }
1624                    }
1625
1626                    //---* Translate the default, if required *--------------------
1627                    accessorClass = retrieveAccessorClass( accessorClass, rawArgumentType, collectionKind );
1628
1629                    //---* Keep the values *-----------------------------------
1630                    property.setPrefsKey( preferenceKey );
1631                    property.setPrefsAccessorClass( accessorClass );
1632                }
1633            }
1634
1635            /*
1636             * Determine the string converter instance, either from the
1637             * annotation or guess it from the property type.
1638             */
1639            final var stringConverterOptional = determineStringConverterClass( setter, propertyType, isEnum );
1640            property.getStringConverterClass()
1641                .ifPresentOrElse( stringConverterClass ->
1642                {
1643                    final var error = new CodeGenerationError( format( MSG_StringConverterMismatch, setterMethodName ) );
1644                    if( !stringConverterClass.equals( stringConverterOptional.orElseThrow( () -> error ) ) ) throw error;
1645                },
1646                () -> stringConverterOptional.ifPresent( property::setStringConverterClass ) );
1647
1648            //---* Configure the setter *--------------------------------------
1649            final var checkEmptyAnnotation = setter.getAnnotation( CheckEmpty.class );
1650            final var checkNullAnnotation = setter.getAnnotation( CheckNull.class );
1651
1652            /*
1653             * The annotations @CheckEmpty and @CheckNull are mutually
1654             * exclusive, although non-empty forces also not-null.
1655             */
1656            if( nonNull( checkNullAnnotation ) && nonNull( checkEmptyAnnotation ) )
1657            {
1658                throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnSetter, CheckNull.class.getName(), setterMethodName ) );
1659            }
1660            if( nonNull( checkEmptyAnnotation ) )
1661            {
1662                if( isDefault )
1663                {
1664                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnSetter, CheckEmpty.class.getName(), setter.getSimpleName() ) );
1665                }
1666                property.setFlag( SETTER_CHECK_EMPTY );
1667            }
1668            if( nonNull( checkNullAnnotation ) )
1669            {
1670                if( isDefault )
1671                {
1672                    throw new IllegalAnnotationError( format( MSG_IllegalAnnotationOnSetter, CheckNull.class.getName(), setter.getSimpleName() ) );
1673                }
1674                property.setFlag( SETTER_CHECK_NULL );
1675            }
1676        }
1677
1678        //---* Create the setter's argument *----------------------------------
1679        property.setSetterArgumentName( retrieveSetterArgumentName( setter ) );
1680
1681        //---* … and now we create the method spec *---------------------------
1682        final var methodBuilder = configuration.getComposer()
1683            .overridingMethodBuilder( setter );
1684        property.setSetterBuilder( methodBuilder );
1685    }   //  handleSetter()
1686
1687    /**
1688     *  Initialises the internal attribute
1689     *  {@link #m_StringConvertersForTypeNames}.
1690     *
1691     *  @return The map of
1692     *      {@link StringConverter}
1693     *      implementations.
1694     */
1695    @API( status = INTERNAL, since = "0.1.0" )
1696    private final Map<TypeName,ClassName> initStringConvertersForTypeNames()
1697    {
1698        final Map<TypeName,ClassName> retValue;
1699        try
1700        {
1701            retValue = createStringConverterRegistry();
1702        }
1703        catch( final IOException e )
1704        {
1705            throw new ExceptionInInitializerError( e );
1706        }
1707        ifDebug( retValue.isEmpty(), _ -> "No StringConverters??" );
1708
1709        //---* Done *----------------------------------------------------------
1710        return retValue;
1711    }   //  initStringConvertersForTypeNames()
1712
1713    /**
1714     *  Parses the given annotation and updates the given property accordingly.
1715     *
1716     *  @param  annotation  The annotation.
1717     *  @param  method  The annotated method.
1718     *  @param  property    The property.
1719     */
1720    @SuppressWarnings( "UseOfConcreteClass" )
1721    private final void parseArgumentAnnotation( final Argument annotation, final ExecutableElement method, final PropertySpecImpl property )
1722    {
1723        property.setFlag( PROPERTY_IS_ARGUMENT );
1724
1725        //---* The index *-----------------------------------------------------
1726        property.setCLIArgumentIndex( annotation.index() );
1727
1728        //---* The other fields *----------------------------------------------
1729        parseCLIAnnotation( annotation, method, property );
1730    }   //  parseArgumentAnnotation()
1731
1732    /**
1733     *  <p>{@summary Parses the given CLI annotation and updates the given
1734     *  property accordingly.}</p>
1735     *  <p>{@code annotation} may be only of type
1736     *  {@link Argument}
1737     *  or
1738     *  {@link Option},
1739     *  although this is not explicitly checked (the method is private!).</p>
1740     *  <p>Usually, the method is only called by the methods
1741     *  {@link #parseArgumentAnnotation(Argument, ExecutableElement, PropertySpecImpl)}
1742     *  and
1743     *  {@link #parseOptionAnnotation(Option, ExecutableElement, PropertySpecImpl)},
1744     *  with the proper arguments.</p>
1745     *
1746     *  @param  annotation  The annotation.
1747     *  @param  method  The annotated method.
1748     *  @param  property    The property.
1749     */
1750    @SuppressWarnings( "UseOfConcreteClass" )
1751    private final void parseCLIAnnotation( final Annotation annotation, @SuppressWarnings( "TypeMayBeWeakened" ) final ExecutableElement method, final PropertySpecImpl property )
1752    {
1753        final var annotationMirror = getAnnotationMirror( method, annotation.getClass() )
1754            .orElseThrow( () -> new CodeGenerationError( format( MSG_CannotRetrieveMirror, annotation.getClass().getName() ) ) );
1755
1756        //---* The value handler class *---------------------------------------
1757        {
1758            final var name = "handler";
1759            final var annotationValue = getAnnotationValue( annotationMirror, name )
1760                .orElseThrow( () -> new CodeGenerationError( format( MSG_NoValueForMirror, annotation.getClass().getName(), name ) ) );
1761            final var defaultClass = getTypeUtils().erasure( getElementUtils().getTypeElement( CmdLineValueHandler.class.getName() ).asType() );
1762            final var handlerClass = getTypeUtils().erasure( (TypeMirror) annotationValue.getValue() );
1763            if( !getTypeUtils().isSameType( defaultClass, handlerClass ) )
1764            {
1765                property.setCLIValueHandlerClass( TypeName.from( handlerClass ) );
1766            }
1767        }
1768
1769        //---* The format *----------------------------------------------------
1770        {
1771            final var name = "format";
1772            getAnnotationValue( annotationMirror, name )
1773                .map( v -> (String) v.getValue() )
1774                .ifPresent( v -> property.setCLIFormat( isNotEmptyOrBlank( v ) ? v : null ) );
1775        }
1776
1777        //---* The meta var *--------------------------------------------------
1778        {
1779            final var name = "metaVar";
1780            getAnnotationValue( annotationMirror, name )
1781                .map( v -> (String) v.getValue() )
1782                .ifPresent( v -> property.setCLIMetaVar( isNotEmptyOrBlank( v ) ? v : null ) );
1783        }
1784
1785        //---* The multi-valued flag *-----------------------------------------
1786        {
1787            final var name = "multiValued";
1788            final var flag = getAnnotationValue( annotationMirror, name )
1789                .map( v -> (Boolean) v.getValue() )
1790                .orElse( FALSE )
1791                .booleanValue();
1792            if( flag ) property.setFlag( PROPERTY_CLI_MULTIVALUED );
1793        }
1794
1795        //---* The required flag *---------------------------------------------
1796        {
1797            final var name = "required";
1798            final var flag = getAnnotationValue( annotationMirror, name )
1799                .map( v -> (Boolean) v.getValue() )
1800                .orElse( FALSE )
1801                .booleanValue();
1802            if( flag ) property.setFlag( PROPERTY_CLI_MANDATORY );
1803        }
1804
1805        //---* The usage text *------------------------------------------------
1806        {
1807            final var name = "usage";
1808            getAnnotationValue( annotationMirror, name )
1809                .map( v -> (String) v.getValue() )
1810                .ifPresent( v -> property.setCLIUsage( isNotEmptyOrBlank( v ) ? v : null ) );
1811        }
1812
1813        //---* The usage text *------------------------------------------------
1814        //noinspection UnnecessaryCodeBlock
1815        {
1816            final var name = "usageKey";
1817            getAnnotationValue( annotationMirror, name )
1818                .map( v -> (String) v.getValue() )
1819                .ifPresent( v -> property.setCLIUsageKey( isNotEmptyOrBlank( v ) ? v : null ) );
1820        }
1821    }   //  parseCLIAnnotation()
1822
1823    /**
1824     *  Parses the given annotation and updates the given property accordingly.
1825     *
1826     *  @param  annotation  The annotation.
1827     *  @param  property    The property.
1828     */
1829    @SuppressWarnings( "UseOfConcreteClass" )
1830    private final void parseEnvironmentVariableAnnotation( final EnvironmentVariable annotation, final PropertySpecImpl property )
1831    {
1832        /*
1833         * The property will be initialised from an environment
1834         * variable with the name given in the annotation.
1835         */
1836        property.setEnvironmentVariableName( annotation.value() );
1837
1838        //---* Set the default value *-----------------------------------------
1839        property.setEnvironmentDefaultValue( annotation.defaultValue() );
1840    }   //  parseEnvironmentVariableAnnotation()
1841
1842    /**
1843     *  Parses the given annotation and updates the given property accordingly.
1844     *
1845     *  @param  annotation  The annotation.
1846     *  @param  method  The annotated method.
1847     *  @param  property    The property.
1848     */
1849    @SuppressWarnings( "UseOfConcreteClass" )
1850    private final void parseOptionAnnotation( final Option annotation, final ExecutableElement method, final PropertySpecImpl property )
1851    {
1852        property.setFlag( PROPERTY_IS_OPTION );
1853
1854        //---* The name and aliases *------------------------------------------
1855        final List<String> names = new ArrayList<>();
1856        names.add( annotation.name() );
1857        names.addAll( asList( annotation.aliases() ) );
1858        property.setCLIOptionNames( names );
1859
1860        //---* The other fields *----------------------------------------------
1861        parseCLIAnnotation( annotation, method, property );
1862    }   //  parseOptionAnnotation()
1863
1864    /**
1865     *  Parses the given annotation and updates the given property accordingly.
1866     *
1867     *  @param  annotation  The annotation.
1868     *  @param  property    The property.
1869     */
1870    @SuppressWarnings( "UseOfConcreteClass" )
1871    private final void parseSystemPropertyAnnotation( final SystemProperty annotation, final PropertySpecImpl property )
1872    {
1873        /*
1874         * The property will be initialised from a system property with
1875         * the name given in the annotation.
1876         */
1877        property.setSystemPropertyName( annotation.value() );
1878
1879        //---* Set the default value *-----------------------------------------
1880        property.setEnvironmentDefaultValue( annotation.defaultValue() );
1881    }   //  parseSystemPropertyAnnotation()
1882
1883    /**
1884     *  {@inheritDoc}
1885     */
1886    @SuppressWarnings( "OverlyNestedMethod" )
1887    @Override
1888    public final boolean process( final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnvironment )
1889    {
1890        //---* Tell them who we are *------------------------------------------
1891        final var message = annotations.isEmpty() ? "No annotations to process" : annotations.stream()
1892            .map( TypeElement::getQualifiedName )
1893            .collect( joining( "', '", "Processing the annotation" + (annotations.size() > 1 ? "s '" : " '"), "'" ) );
1894        printMessage( NOTE, message );
1895
1896        final var retValue = !roundEnvironment.errorRaised() && !annotations.isEmpty();
1897        if( retValue )
1898        {
1899            //noinspection ConstantExpression
1900            if( !annotations.isEmpty() )
1901            {
1902                /*
1903                 * Get the values for the i18n annotations and keep them.
1904                 */
1905                //---* Get the message prefix *--------------------------------
1906                m_MessagePrefix = retrieveAnnotatedField( roundEnvironment, MessagePrefix.class )
1907                    .filter( variableElement -> variableElement.getModifiers().containsAll( Set.of( PUBLIC, STATIC ) ) )
1908                    .map( variableElement -> Objects.toString( variableElement.getConstantValue(), EMPTY_STRING ) );
1909
1910                //---* Get the base bundle name *------------------------------
1911                m_BaseBundleName = retrieveAnnotatedField( roundEnvironment, BaseBundleName.class )
1912                    .filter( variableElement -> variableElement.getModifiers().containsAll( Set.of( PUBLIC, STATIC ) ) )
1913                    .map( variableElement -> Objects.toString( variableElement.getConstantValue(), EMPTY_STRING ) );
1914
1915                /*
1916                 * Process the elements that are annotated as configuration
1917                 * bean specification. Although not more than one per
1918                 * application seems logical, it could easily be more than one.
1919                 */
1920                ScanLoop: for( final var element : roundEnvironment.getElementsAnnotatedWith( ConfigurationBeanSpecification.class ) )
1921                {
1922                    /*
1923                     * We are only interested in elements that are type
1924                     * elements, and to be honest, we only want interfaces.
1925                     */
1926                    if( element instanceof final TypeElement typeElement )
1927                    {
1928                        if( typeElement.getKind() == ElementKind.INTERFACE )
1929                        {
1930                            //---* Create the configuration bean *-------------
1931                            try
1932                            {
1933                                processConfigurationBeanSpecification( typeElement );
1934                            }
1935                            catch( final IOException e )
1936                            {
1937                                printMessage( ERROR, e.toString(), element );
1938                            }
1939                        }
1940                        else
1941                        {
1942                            printMessage( ERROR, "%s: Only interfaces may be annotated with '%s'".formatted( typeElement.getQualifiedName().toString(), ConfigurationBeanSpecification.class.getSimpleName() ), element );
1943                            throw new IllegalAnnotationError( MSG_InterfacesOnly, ConfigurationBeanSpecification.class );
1944                        }
1945                    }
1946                    else
1947                    {
1948                        printMessage( ERROR, format( MSG_IllegalAnnotationUse, element.getSimpleName().toString(), ConfigurationBeanSpecification.class.getSimpleName() ), element );
1949                        throw new IllegalAnnotationError( ConfigurationBeanSpecification.class );
1950                    }
1951                }   //  ScanLoop:
1952            }
1953        }
1954
1955        //---* Done *----------------------------------------------------------
1956        return retValue;
1957    }   //  process()
1958
1959    /**
1960     *  Processes the given configuration bean specification and generates the
1961     *  source for the so specified configuration bean.
1962     *
1963     *  @param  specification   The specification interface.
1964     *  @throws IOException A problem occurred when writing the source file.
1965     */
1966    @SuppressWarnings( {"OverlyCoupledMethod", "OverlyComplexMethod"} )
1967    private final void processConfigurationBeanSpecification( final TypeElement specification ) throws IOException
1968    {
1969        //---* Create the composer *-------------------------------------------
1970        final var composer = new JavaComposer( LAYOUT_FOUNDATION, addDebugOutput() );
1971
1972        final var specificationAnnotation = specification.getAnnotation( ConfigurationBeanSpecification.class );
1973
1974        //---* Determine the simple name of the bean class *-------------------
1975        final var configurationBeanClassName = getElementUtils().getName( isEmpty( specificationAnnotation.name() ) ? format( "%sImpl", specification.getSimpleName() ) : specificationAnnotation.name() );
1976        final var isSamePackage = specificationAnnotation.samePackage();
1977
1978        //---* Do we need to synchronise the access to the properties? *-------
1979        final var synchronizeAccess = specificationAnnotation.synchronizeAccess();
1980
1981        //---* Get the base class *--------------------------------------------
1982        TypeName baseClass = null;
1983        try
1984        {
1985            final var annotationValueName = "baseClass";
1986            baseClass = getTypeMirrorFromAnnotationValue( specification, ConfigurationBeanSpecification.class, annotationValueName )
1987                .map( TypeName::from )
1988                .orElseThrow( () -> new CodeGenerationError( format( MSG_NoValueForMirror, ConfigurationBeanSpecification.class.getName(), annotationValueName ) ) );
1989        }
1990        catch( final NoSuchElementException e )
1991        {
1992            throw new CodeGenerationError( format( MSG_CannotRetrieveMirror, ConfigurationBeanSpecification.class.getName() ), e );
1993        }
1994        if( TypeName.from( Object.class ).equals( baseClass ) )
1995        {
1996            //noinspection AssignmentToNull
1997            baseClass = null;
1998        }
1999
2000        /*
2001         * Determine the name of the package for the bean class. As the
2002         * interface may be an inner type, we cannot just take the next best
2003         * enclosing element.
2004         */
2005        var parentElement = specification;
2006        while( parentElement.getNestingKind() != NestingKind.TOP_LEVEL )
2007        {
2008            parentElement = (TypeElement) specification.getEnclosingElement();
2009        }
2010        final var packageElement = (PackageElement) specification.getEnclosingElement();
2011        final var specificationPackageName = packageElement.getQualifiedName().toString();
2012        final var configurationBeanPackageName = getElementUtils().getName
2013            (
2014                isSamePackage
2015                ? specificationPackageName
2016                : packageElement.isUnnamed()
2017                  ? PACKAGE_NAME
2018                  : format( "%s.%s", specificationPackageName, PACKAGE_NAME )
2019            );
2020
2021        //---* Create the configuration for the code generation *--------------
2022        final var specificationClass = ClassName.from( specification );
2023        final var configuration = new CodeGenerationConfiguration( this, composer, specificationClass, configurationBeanClassName, configurationBeanPackageName, baseClass, synchronizeAccess );
2024
2025        //---* Determine the name for the initialisation data resource *-------
2026        var initDataResource = specificationAnnotation.initDataResource();
2027        if( isNotEmptyOrBlank( initDataResource ) )
2028        {
2029            if( "=".equals( initDataResource ) ) initDataResource = format( "%s.properties", specification.getSimpleName() );
2030            configuration.setInitDataResource( initDataResource );
2031        }
2032
2033        //---* Retrieve the method for the initialisation *--------------------
2034        retrieveInitDataMethod( configuration, specification );
2035
2036        /*
2037         * Retrieve all the interfaces that are implemented by the
2038         * specification.
2039         */
2040        final Set<TypeElement> interfaces = new HashSet<>();
2041        retrieveInterfaces( specification, interfaces );
2042        final var interfacesTypes = interfaces.stream()
2043            .map( element -> TypeName.from( element.asType() ) )
2044            .collect( Collectors.toList() );
2045        configuration.addInterfacesToImplement( interfacesTypes );
2046
2047        //---* Retrieve the properties *---------------------------------------
2048        retrieveProperties( configuration, interfaces );
2049
2050        //---* Add the settings for the I18nSupport *--------------------------
2051        if( configuration.implementInterface( I18nSupport.class ) )
2052        {
2053            configuration.setI18NParameters( m_MessagePrefix.orElseThrow( () -> new CodeGenerationError( MSG_NoMessagePrefix ) ),
2054                m_BaseBundleName.orElseThrow( () -> new CodeGenerationError( MSG_NoBaseBundleName ) ) );
2055        }
2056
2057        //---* Determine the settings for the preferences stuff *--------------
2058        final var preferencesRootAnnotation = specification.getAnnotation( PreferencesRoot.class );
2059        if( nonNull( preferencesRootAnnotation ) )
2060        {
2061            configuration.setPreferencesRoot( preferencesRootAnnotation.nodeName() );
2062            try
2063            {
2064                getTypeMirrorFromAnnotationValue( specification, PreferencesRoot.class, "changeListenerClass" )
2065                    .map( TypeName::from )
2066                    .ifPresent( configuration::setPreferenceChangeListenerClass );
2067            }
2068            catch( final NoSuchElementException e )
2069            {
2070                throw new CodeGenerationError( format( MSG_CannotRetrieveMirror, PreferencesRoot.class.getName() ), e );
2071            }
2072        }
2073
2074        //---* Determine the settings for the {@code INI} file stuff *---------
2075        final var iniFileConfig = specification.getAnnotation( INIFileConfig.class );
2076        if( nonNull( iniFileConfig ) )
2077        {
2078            final var filename = iniFileConfig.path();
2079            if( isEmptyOrBlank( filename ) )
2080            {
2081                throw new CodeGenerationError( MSG_INIPathMissing );
2082            }
2083            configuration.setINIFileConfig( filename, iniFileConfig.mustExist(), iniFileConfig.comment() );
2084            for( final var group : specification.getAnnotationsByType( INIGroup.class ) ) configuration.addINIGroup( group );
2085        }
2086
2087        //---* Create the source code *----------------------------------------
2088        try
2089        {
2090            final var generator = new CodeGenerator( configuration );
2091            final var javaFile = generator.createCode();
2092
2093            //---* Write the source file *-------------------------------------
2094            javaFile.writeTo( getFiler() );
2095        }
2096        catch( @SuppressWarnings( "OverlyBroadCatchBlock" ) final Exception e )
2097        {
2098            /*
2099             * Any exception that makes it to this point indicates a failure of
2100             * the code generation process.
2101             */
2102            printMessage( ERROR, format( "Code Generation failed: %s", e.getMessage() ), specification );
2103            throw new CodeGenerationError( format( MSG_CodeGenerationFailed, configurationBeanPackageName, configurationBeanClassName ), e );
2104        }
2105    }   //  processConfigurationBeanSpecification()
2106
2107    /**
2108     *  Retrieves the class for the preference accessor.
2109     *
2110     *  @param  accessorType    The accessor class as defined in the
2111     *      annotation; if this is
2112     *      {@link PreferenceAccessor PreferenceAccessor.class},
2113     *      the effective handler class has to inferred from the
2114     *      {@code propertyType}.
2115     *  @param  propertyType    The type of the property that should be
2116     *      accessed.
2117     *  @param collectionKind   The kind of collection that is represented by
2118     *      the property type.
2119     *  @return The effective accessor class.
2120     *  @throws IllegalAnnotationError  There is no accessor for the given
2121     *      property type.
2122     */
2123    @API( status = INTERNAL, since = "0.0.1" )
2124    private final TypeName retrieveAccessorClass( final TypeName accessorType, final TypeMirror propertyType, final CollectionKind collectionKind ) throws IllegalAnnotationError
2125    {
2126        var retValue = accessorType;
2127        if( isNull( accessorType ) || accessorType.equals( PREFS_ACCESSOR_TYPE ) )
2128        {
2129            //---* Infer the effective accessor class from the property type *-
2130            if( isEnumType( propertyType ) )
2131            {
2132                retValue = ENUM_ACCESSOR_TYPE;
2133            }
2134            else
2135            {
2136                retValue = switch( collectionKind )
2137                {
2138                    case NO_COLLECTION -> m_PrefsAccessorClasses.getOrDefault( TypeName.from( propertyType ), (ClassName) DEFAULT_ACCESSOR_TYPE );
2139                    case LIST -> LIST_ACCESSOR_TYPE;
2140                    case MAP -> MAP_ACCESSOR_TYPE;
2141                    case SET -> SET_ACCESSOR_TYPE;
2142                };
2143            }
2144        }
2145
2146        //---* Done *----------------------------------------------------------
2147        return retValue;
2148    }   //  retrieveAccessorClass()
2149
2150    /**
2151     *  <p>{@summary This methods checks whether the configuration bean
2152     *  specification specifies an {@code initData()} method.} This method has
2153     *  to meet the requirements below:</p>
2154     *  <ul>
2155     *  <li>the name has to be
2156     *  {@value #METHODNAME_ConfigBeanSpec_InitData}</li>
2157     *  <li>it does not take any arguments</li>
2158     *  <li>it returns an instance of {@code Map<String,Object>}</li>
2159     *  <li>it is either {@code static} or {@code default} or implemented in a
2160     *  base class, although this is not checked here</li>
2161     *  </ul>
2162     *  <p>Is such a method exists, a
2163     *  {@link org.tquadrat.foundation.javacomposer.MethodSpec}
2164     *  for it will be created and added to the configuration.</p>
2165     *
2166     *  @param  configuration   The configuration for the code generation.
2167     *  @param  specification   The configuration bean specification interface.
2168     *
2169     *  @note   If a base class for the new configuration bean is defined, the
2170     *      method may be abstract, but if that base class does not implement
2171     *      the method it will be detected only by the final compiler run, not
2172     *      by the code generation here.
2173     */
2174    @SuppressWarnings( {"TypeMayBeWeakened", "UseOfConcreteClass"} )
2175    private final void retrieveInitDataMethod( final CodeGenerationConfiguration configuration, final TypeElement specification )
2176    {
2177        final var hasBaseClass = configuration.getBaseClass().isPresent();
2178        final var returnType = ParameterizedTypeName.from( Map.class, String.class, Object.class );
2179        final var method = specification.getEnclosedElements().stream()
2180            // We are only interested in methods.
2181            .filter( e -> e.getKind() == METHOD )
2182            // Methods are executable elements
2183            .map( e -> (ExecutableElement) e )
2184            /*
2185             * The method has to be either default or static; these modifiers
2186             * are mutually exclusive, so the method has either one or the
2187             * other, or it is abstract, having neither of default or static.
2188             */
2189            .filter( e -> hasBaseClass || e.getModifiers().contains( DEFAULT ) || e.getModifiers().contains( STATIC ) )
2190            // The name of the method should be "initData"
2191            .filter( e -> e.getSimpleName().contentEquals( METHODNAME_ConfigBeanSpec_InitData ) )
2192            // The method may not take any arguments
2193            .filter( e -> e.getParameters().isEmpty() )
2194            // The return type has to be Map<String,Object>
2195            .filter( e -> TypeName.from( e.getReturnType() ) instanceof ParameterizedTypeName )
2196            .filter( e -> returnType.equals( TypeName.from( e.getReturnType() ) ) )
2197            .findFirst();
2198
2199        method.map( methodSpec -> configuration.getComposer().createMethod( methodSpec ) )
2200            .ifPresent( configuration::setInitDataMethod );
2201    }   //  retrieveInitDataMethod()
2202
2203    /**
2204     *  <p>{@summary Scans the configuration bean specification for the
2205     *  properties and stores the result to the configuration.}</p>
2206     *  <p>A property is defined primarily by the respective
2207     *  {@linkplain org.tquadrat.foundation.function.Getter getter}
2208     *  method with its annotations, but it is also possible to define it by a
2209     *  {@linkplain org.tquadrat.foundation.function.Setter setter}
2210     *  method only – especially when the configuration bean specification
2211     *  extends the interface
2212     *  {@link java.util.Map}.</p>
2213     *
2214     *  @param  configuration   The code generation configuration.
2215     *  @param  interfaces  The interfaces that have to implemented by the new
2216     *      configuration bean.
2217     */
2218    @SuppressWarnings( "UseOfConcreteClass" )
2219    private final void retrieveProperties( final CodeGenerationConfiguration configuration, final Collection<? extends TypeElement> interfaces )
2220    {
2221        final var isMap = configuration.implementInterface( Map.class );
2222
2223        /*
2224         * Retrieve getters, setters and 'add' methods from the interfaces.
2225         */
2226        final Collection<ExecutableElement> getters = new ArrayList<>();
2227        final Collection<ExecutableElement> setters = new ArrayList<>();
2228        final Collection<ExecutableElement> addMethods = new ArrayList<>();
2229        //noinspection OverlyLongLambda
2230        interfaces.stream()
2231            .flatMap( element -> element.getEnclosedElements().stream() )
2232            .filter( e -> e.getKind() == METHOD )
2233            .map( e -> (ExecutableElement) e )
2234            .forEach( element ->
2235            {
2236                if( isGetter( element ) )
2237                {
2238                    /*
2239                     * There is a method Map.isEmpty(); if the configuration
2240                     * bean specification extends java.util.Map, this is not a
2241                     * property getter for the 'empty' property.
2242                     * If isMap == true, the method isEmpty() will be
2243                     * implemented elsewhere.
2244                     */
2245                    if( !element.getSimpleName().toString().equals( METHODNAME_Map_IsEmpty ) || !isMap )
2246                    {
2247                        getters.add( element );
2248                    }
2249                }
2250                else if( isSetter( element ) )
2251                {
2252                    setters.add( element );
2253                }
2254                else if( isAddMethod( element ) )
2255                {
2256                    /*
2257                     * There is a method ConfigBeanSpec.addListener() that is
2258                     * necessary for the listener management, and not an 'add'
2259                     * method for a property. Therefore, it will be implemented
2260                     * elsewhere.
2261                     */
2262                    if( !element.getSimpleName().toString().equals( METHODNAME_ConfigBeanSpec_AddListener ) )
2263                    {
2264                        addMethods.add( element );
2265                    }
2266                }
2267            } );
2268
2269        /*
2270         *  Getters are to be processed first.
2271         *  This will create the properties that are stored in the
2272         *  configuration.
2273         */
2274        getters.forEach( getter -> handleGetter( configuration, getter ) );
2275
2276        //---* Process the setters *-------------------------------------------
2277        setters.forEach( setter -> handleSetter( configuration, setter ) );
2278
2279        //---* Process the 'add' methods *-------------------------------------
2280        addMethods.forEach( addMethod -> handleAddMethod( configuration, addMethod ) );
2281    }   //  retrieveProperties()
2282
2283    /**
2284     *  <p>{@summary Retrieves the name of the single argument of a setter
2285     *  method.}</p>
2286     *  <p>This method will return the name of the argument as defined in the
2287     *  configuration bean specification if the compiler flag
2288     *  {@code -parameters} is set; otherwise, the arguments are just counted
2289     *  ({@code arg0}, {@code arg1}, {@code arg2}, …).</p>
2290     *
2291     *  @param  setter  The setter method.
2292     *  @return The name of the argument as defined in the configuration bean
2293     *      specification, or &quot;arg0&quot;
2294     */
2295    private final Name retrieveSetterArgumentName( final ExecutableElement setter )
2296    {
2297        final var parameters = retrieveArgumentNames( setter );
2298        if( parameters.size() != 1 ) throw new CodeGenerationError( format( MSG_NoSetter, setter.getSimpleName() ) );
2299        final var retValue = parameters.getFirst();
2300
2301        //---* Done *----------------------------------------------------------
2302        return retValue;
2303    }   //  retrieveSetterArgumentName()
2304
2305    /**
2306     *  <p>{@summary Determines the key class for the given instance of
2307     *  {@link StringConverter}.}</p>
2308     *
2309     *  @note   This method was copied from
2310     *      {@code org.tquadrat.foundation.base/org.tquadrat.foundation.lang.internal.StringConverterService}.
2311     *
2312     *  @param  converter   The converter instance.
2313     *  @return The subject class.
2314     */
2315    @SuppressWarnings( {"NestedTryStatement", "unchecked"} )
2316    private static final Collection<Class<?>> retrieveSubjectClasses( final StringConverter<?> converter )
2317    {
2318        final var converterClass = requireNonNullArgument( converter, "converter" ).getClass();
2319        Collection<Class<?>> retValue;
2320        try
2321        {
2322            try
2323            {
2324                final var getSubjectClassMethod = converterClass.getMethod( METHOD_NAME_GetSubjectClass );
2325                //noinspection unchecked
2326                retValue = (Collection<Class<?>>) getSubjectClassMethod.invoke( converter );
2327            }
2328            catch( @SuppressWarnings( "unused" ) final NoSuchMethodException ignored )
2329            {
2330                final var fromStringMethod = converterClass.getMethod( "fromString", CharSequence.class );
2331                retValue = List.of( fromStringMethod.getReturnType() );
2332            }
2333        }
2334        catch( final NoSuchMethodException | SecurityException | IllegalAccessException | InvocationTargetException e )
2335        {
2336            throw new UnexpectedExceptionError( e );
2337        }
2338
2339        //---* Done *----------------------------------------------------------
2340        return retValue;
2341    }   //  retrieveSubjectClass()
2342}
2343//  class ConfigAnnotationProcessor
2344
2345/*
2346 *  End of File
2347 */