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 @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 @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 @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 @StringConversion} 954 * annotation from the given method.}</p> 955 * <p>The type for the annotation value is an instance of 956 * {@link Class Class<? extends StringConverter>}, 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 * "<i>stringConverter</i>". 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 "arg0" 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 */