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