001/* 002 * ============================================================================ 003 * Copyright © 2015 Square, Inc. 004 * Copyright for the modifications © 2018-2023 by Thomas Thrien. 005 * ============================================================================ 006 * 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 */ 019 020package org.tquadrat.foundation.javacomposer.internal; 021 022import static java.util.Collections.unmodifiableMap; 023import static org.apiguardian.api.API.Status.INTERNAL; 024import static org.tquadrat.foundation.javacomposer.internal.Util.characterLiteralWithoutSingleQuotes; 025import static org.tquadrat.foundation.javacomposer.internal.Util.createDebugOutput; 026import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING; 027import static org.tquadrat.foundation.lang.Objects.hash; 028import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument; 029import static org.tquadrat.foundation.lang.Objects.requireValidArgument; 030 031import java.io.UncheckedIOException; 032import java.util.ArrayList; 033import java.util.LinkedHashMap; 034import java.util.List; 035import java.util.Map; 036 037import org.apiguardian.api.API; 038import org.tquadrat.foundation.annotation.ClassVersion; 039import org.tquadrat.foundation.exception.UnexpectedExceptionError; 040import org.tquadrat.foundation.javacomposer.AnnotationSpec; 041import org.tquadrat.foundation.javacomposer.CodeBlock; 042import org.tquadrat.foundation.javacomposer.JavaComposer; 043import org.tquadrat.foundation.javacomposer.TypeName; 044import org.tquadrat.foundation.lang.Lazy; 045import org.tquadrat.foundation.util.JavaUtils; 046 047/** 048 * The implementation of 049 * {@link AnnotationSpec} 050 * for a generated annotation on a declaration. 051 * 052 * @author Square,Inc. 053 * @modified Thomas Thrien - thomas.thrien@tquadrat.org 054 * @version $Id: AnnotationSpecImpl.java 1105 2024-02-28 12:58:46Z tquadrat $ 055 * @since 0.0.5 056 * 057 * @UMLGraph.link 058 */ 059@ClassVersion( sourceVersion = "$Id: AnnotationSpecImpl.java 1105 2024-02-28 12:58:46Z tquadrat $" ) 060@API( status = INTERNAL, since = "0.0.5" ) 061public final class AnnotationSpecImpl implements AnnotationSpec 062{ 063 /*---------------*\ 064 ====** Inner Classes **==================================================== 065 \*---------------*/ 066 /** 067 * The implementation of 068 * {@link org.tquadrat.foundation.javacomposer.AnnotationSpec.Builder} 069 * for a builder of an 070 * {@link AnnotationSpecImpl} 071 * instance. 072 * 073 * @author Square,Inc. 074 * @modified Thomas Thrien - thomas.thrien@tquadrat.org 075 * @version $Id: AnnotationSpecImpl.java 1105 2024-02-28 12:58:46Z tquadrat $ 076 * @since 0.0.5 077 * 078 * @UMLGraph.link 079 */ 080 @ClassVersion( sourceVersion = "$Id: AnnotationSpecImpl.java 1105 2024-02-28 12:58:46Z tquadrat $" ) 081 @API( status = INTERNAL, since = "0.0.5" ) 082 public static final class BuilderImpl implements AnnotationSpec.Builder 083 { 084 /*------------*\ 085 ====** Attributes **=================================================== 086 \*------------*/ 087 /** 088 * The building blocks. 089 */ 090 private final Map<String,List<CodeBlockImpl>> m_CodeBlocks = new LinkedHashMap<>(); 091 092 /** 093 * The reference to the factory. 094 */ 095 @SuppressWarnings( "UseOfConcreteClass" ) 096 private final JavaComposer m_Composer; 097 098 /** 099 * A flag that indicates whether the inline representation is forced 100 * for this annotation. 101 * 102 * @see org.tquadrat.foundation.javacomposer.AnnotationSpec.Builder#forceInline(boolean) 103 */ 104 private boolean m_ForceInline = false; 105 106 /** 107 * The name of the annotation type to build. 108 */ 109 @SuppressWarnings( "UseOfConcreteClass" ) 110 private final TypeNameImpl m_Type; 111 112 /*--------------*\ 113 ====** Constructors **================================================= 114 \*--------------*/ 115 /** 116 * Creates a new {@code BuilderImpl} instance. 117 * 118 * @param composer The reference to the factory that created this 119 * builder instance. 120 * @param type The name of the annotation type to build. 121 */ 122 @SuppressWarnings( "UseOfConcreteClass" ) 123 public BuilderImpl( final JavaComposer composer, final TypeName type ) 124 { 125 m_Composer = requireNonNullArgument( composer, "composer" ); 126 m_Type = (TypeNameImpl) requireNonNullArgument( type, "type" ); 127 } // BuilderImpl() 128 129 /*---------*\ 130 ====** Methods **====================================================== 131 \*---------*/ 132 /** 133 * {@inheritDoc} 134 */ 135 @Override 136 public final BuilderImpl addMember( final CharSequence name, final String format, final Object... args ) 137 { 138 final var codeBlock = ((CodeBlockImpl.BuilderImpl) m_Composer.codeBlockBuilder()) 139 .addWithoutDebugInfo( format, args ) 140 .build(); 141 final var retValue = addMember( name, codeBlock ); 142 143 //---* Done *------------------------------------------------------ 144 return retValue; 145 } // addMember() 146 147 /** 148 * {@inheritDoc} 149 */ 150 @Override 151 public final BuilderImpl addMember( final CharSequence name, final CodeBlock codeBlock ) 152 { 153 final var validatedName = requireValidArgument( name, "name", JavaUtils::isValidName, $ -> "not a valid name: %s".formatted( name ) ) 154 .toString() 155 .intern(); 156 requireNonNullArgument( codeBlock, "codeBlock" ); 157 final var values = m_CodeBlocks.computeIfAbsent( validatedName, $ -> new ArrayList<>() ); 158 values.add( createDebugOutput( m_Composer.addDebugOutput() ) 159 .map( output -> ((CodeBlockImpl.BuilderImpl) m_Composer.codeBlockBuilder()) 160 .addWithoutDebugInfo( output.asComment() ) 161 .addWithoutDebugInfo( codeBlock ) 162 .build() ) 163 .orElse( (CodeBlockImpl) codeBlock ) ); 164 165 //---* Done *------------------------------------------------------ 166 return this; 167 } // addMember() 168 169 /** 170 * Delegates to 171 * {@link #addMember(CharSequence,String,Object...)}, 172 * with parameter {@code format} depending on the given {@code value} 173 * object. Falls back to {@code "$L"} literal format if the class of 174 * the given {@code value} object is not supported. 175 * 176 * @param name The name for the new member. 177 * @param value The value for the new member. 178 * @return This {@code Builder} instance. 179 */ 180 @SuppressWarnings( {"PublicMethodNotExposedInInterface", "UnusedReturnValue", "IfStatementWithTooManyBranches", "ChainOfInstanceofChecks"} ) 181 public final BuilderImpl addMemberForValue( final String name, final Object value ) 182 { 183 requireValidArgument( name, "name", JavaUtils::isValidName, $ -> "not a valid name: %s".formatted( name ) ); 184 if( requireNonNullArgument( value, "value" ) instanceof Class<?> ) 185 { 186 addMember( name, "$T.class", value ); 187 } 188 else if( value instanceof final Enum<?> enumValue ) 189 { 190 addMember( name, "$T.$L", value.getClass(), enumValue.name() ); 191 } 192 else if( value instanceof String ) 193 { 194 addMember( name, "$S", value ); 195 } 196 else if( value instanceof Float ) 197 { 198 addMember( name, "$Lf", value ); 199 } 200 else if( value instanceof final Character charValue ) 201 { 202 addMember( name, "'$L'", characterLiteralWithoutSingleQuotes( charValue.charValue() ) ); 203 } 204 else 205 { 206 addMember( name, "$L", value ); 207 } 208 209 //---* Done *------------------------------------------------------ 210 return this; 211 } // addMemberForValue() 212 213 /** 214 * Creates the {@code AnnotationSpec} instance from the added members. 215 * 216 * @return The built instance. 217 */ 218 @Override 219 public final AnnotationSpecImpl build() { return new AnnotationSpecImpl( this ); } 220 221 /** 222 * {@inheritDoc} 223 */ 224 @Override 225 public final BuilderImpl forceInline( final boolean flag ) 226 { 227 m_ForceInline = flag; 228 229 //---* Done *------------------------------------------------------ 230 return this; 231 } // forceInline() 232 233 /** 234 * Returns the flag that indicates whether this annotation is 235 * presented inline or multiline. 236 * 237 * @return {@code true} for the inline presentation, {@code false} for 238 * multi-line. 239 */ 240 @SuppressWarnings( {"PublicMethodNotExposedInInterface", "BooleanMethodNameMustStartWithQuestion"} ) 241 public final boolean forceInline() { return m_ForceInline; } 242 243 /** 244 * Returns the 245 * {@link JavaComposer} 246 * factory. 247 * 248 * @return The reference to the factory. 249 */ 250 @SuppressWarnings( {"PublicMethodNotExposedInInterface"} ) 251 public final JavaComposer getFactory() { return m_Composer; } 252 253 /** 254 * Returns the members. 255 * 256 * @return The members. 257 */ 258 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 259 public final Map<String,List<CodeBlockImpl>> members() 260 { 261 final Map<String,List<CodeBlockImpl>> members = new LinkedHashMap<>(); 262 m_CodeBlocks.forEach( (key,value) -> members.put( key, List.copyOf( value ) ) ); 263 final var retValue = unmodifiableMap( members ); 264 265 //---* Done *------------------------------------------------------ 266 return retValue; 267 } // members() 268 269 /** 270 * Returns the type of the annotation. 271 * 272 * @return The type. 273 */ 274 @SuppressWarnings( {"PublicMethodNotExposedInInterface"} ) 275 public final TypeNameImpl type() { return m_Type; } 276 } 277 // class BuilderImpl 278 279 /*------------*\ 280 ====** Attributes **======================================================= 281 \*------------*/ 282 /** 283 * Lazily initialised return value of 284 * {@link #toString()} 285 * for this annotation. 286 */ 287 private final Lazy<String> m_CachedString; 288 289 /** 290 * The reference to the factory. 291 */ 292 @SuppressWarnings( "UseOfConcreteClass" ) 293 private final JavaComposer m_Composer; 294 295 /** 296 * A flag that indicates whether the inline representation is forced for 297 * this annotation. 298 * 299 * @see org.tquadrat.foundation.javacomposer.AnnotationSpec.Builder#forceInline(boolean) 300 */ 301 private final boolean m_ForceInline; 302 303 /** 304 * The code blocks that define this annotation. 305 */ 306 private final Map<String,List<CodeBlockImpl>> m_Members; 307 308 /** 309 * The name of this annotation. 310 */ 311 @SuppressWarnings( "UseOfConcreteClass" ) 312 private final TypeNameImpl m_Type; 313 314 /*--------------*\ 315 ====** Constructors **===================================================== 316 \*--------------*/ 317 /** 318 * Creates a new {@code AnnotationSpecImpl} instance. 319 * 320 * @param builder The builder for this instance. 321 */ 322 @SuppressWarnings( "UseOfConcreteClass" ) 323 public AnnotationSpecImpl( final BuilderImpl builder ) 324 { 325 m_Composer = builder.getFactory(); 326 m_Type = builder.type(); 327 m_Members = builder.members(); 328 m_ForceInline = builder.forceInline(); 329 330 m_CachedString = Lazy.use( this::initializeCachedString ); 331 } // AnnotationSpecImpl() 332 333 /*---------*\ 334 ====** Methods **========================================================== 335 \*---------*/ 336 /** 337 * Emits this annotation to the given code writer. 338 * 339 * @param codeWriter The code writer. 340 * @param inline {@code true} if the annotation should be placed on the 341 * same line as the annotated element, {@code false} otherwise. 342 * @throws UncheckedIOException A problem occurred when writing to the 343 * output target. 344 */ 345 @SuppressWarnings( {"PublicMethodNotExposedInInterface", "UseOfConcreteClass"} ) 346 public final void emit( final CodeWriter codeWriter, final boolean inline ) throws UncheckedIOException 347 { 348 final var layout = requireNonNullArgument( codeWriter, "codeWriter" ).layout(); 349 switch( layout ) 350 { 351 case LAYOUT_FOUNDATION -> emit4Foundation( codeWriter, inline ); 352 case LAYOUT_JAVAPOET -> emit4JavaPoet( codeWriter, inline ); 353 //case LAYOUT_DEFAULT -> 354 default -> emit4JavaPoet( codeWriter, inline ); 355 } 356 } // emit() 357 358 /** 359 * Emits this annotation to the given code writer. 360 * 361 * @param codeWriter The code writer. 362 * @param inline {@code true} if the annotation should be placed on the 363 * same line as the annotated element, {@code false} otherwise. 364 * @throws UncheckedIOException A problem occurred when writing to the 365 * output target. 366 */ 367 @SuppressWarnings( "UseOfConcreteClass" ) 368 private final void emit4Foundation( final CodeWriter codeWriter, final boolean inline ) throws UncheckedIOException 369 { 370 requireNonNullArgument( codeWriter, "codeWriter" ); 371 372 final var whitespace = inline || m_ForceInline ? " " : "\n"; 373 final var memberSeparator = inline || m_ForceInline ? ", " : ",\n"; 374 375 if( m_Members.isEmpty() ) 376 { 377 //---* @Singleton *------------------------------------------------ 378 codeWriter.emit( "@$T", m_Type ); 379 } 380 else if( m_Members.size() == 1 && m_Members.containsKey( "value" ) ) 381 { 382 //---* @Named("foo") *--------------------------------------------- 383 codeWriter.emit( "@$T( ", m_Type ); 384 emitAnnotationValues( codeWriter, whitespace, memberSeparator, m_Members.get( "value" ) ); 385 codeWriter.emit( " )" ); 386 } 387 else 388 { 389 /* 390 * Inline: 391 * @Column( name = "updated_at", nullable = false ) 392 * 393 * Not inline: 394 * @Column( 395 * name = "updated_at", 396 * nullable = false 397 * ) 398 */ 399 codeWriter.emit( "@$T(" + whitespace, m_Type ); 400 codeWriter.indent( 1 ); 401 for( final var iterator = m_Members.entrySet().iterator(); iterator.hasNext(); ) 402 { 403 final var entry = iterator.next(); 404 codeWriter.emit( "$L = ", entry.getKey() ); 405 emitAnnotationValues( codeWriter, whitespace, memberSeparator, entry.getValue() ); 406 if( iterator.hasNext() ) codeWriter.emit( memberSeparator ); 407 } 408 codeWriter.unindent( 1 ); 409 codeWriter.emit( whitespace + ")" ); 410 } 411 } // emit4Foundation() 412 413 /** 414 * Emits this annotation to the given code writer using the original 415 * JavaPoet layout. 416 * 417 * @param codeWriter The code writer. 418 * @param inline {@code true} if the annotation should be placed on the 419 * same line as the annotated element, {@code false} otherwise. 420 * @throws UncheckedIOException A problem occurred when writing to the 421 * output target. 422 */ 423 @SuppressWarnings( "UseOfConcreteClass" ) 424 private final void emit4JavaPoet( final CodeWriter codeWriter, final boolean inline ) throws UncheckedIOException 425 { 426 requireNonNullArgument( codeWriter, "codeWriter" ); 427 428 final var whitespace = inline || m_ForceInline ? EMPTY_STRING : "\n"; 429 final var memberSeparator = inline || m_ForceInline ? ", " : ",\n"; 430 431 if( m_Members.isEmpty() ) 432 { 433 //---* @Singleton *------------------------------------------------ 434 codeWriter.emit( "@$T", m_Type ); 435 } 436 else if( m_Members.size() == 1 && m_Members.containsKey( "value" ) ) 437 { 438 //---* @Named("foo") *--------------------------------------------- 439 codeWriter.emit( "@$T(", m_Type ); 440 emitAnnotationValues( codeWriter, whitespace, memberSeparator, m_Members.get( "value" ) ); 441 codeWriter.emit( ")" ); 442 } 443 else 444 { 445 /* 446 * Inline: 447 * @Column(name = "updated_at", nullable = false) 448 * 449 * Not inline: 450 * @Column( 451 * name = "updated_at", 452 * nullable = false 453 * ) 454 */ 455 codeWriter.emit( "@$T(" + whitespace, m_Type ); 456 codeWriter.indent( 2 ); 457 for( final var iterator = m_Members.entrySet().iterator(); iterator.hasNext(); ) 458 { 459 final var entry = iterator.next(); 460 codeWriter.emit( "$L = ", entry.getKey() ); 461 emitAnnotationValues( codeWriter, whitespace, memberSeparator, entry.getValue() ); 462 if( iterator.hasNext() ) codeWriter.emit( memberSeparator ); 463 } 464 codeWriter.unindent( 2 ); 465 codeWriter.emit( whitespace + ")" ); 466 } 467 } // emit4JavaPoet() 468 469 /** 470 * Emits the values of this annotation to the given code writer. 471 * 472 * @param codeWriter The code writer. 473 * @param whitespace The whitespace to emit. 474 * @param memberSeparator The separator for the members. 475 * @param values The members to emit. 476 * @throws UncheckedIOException A problem occurred when writing to the 477 * output target. 478 */ 479 @SuppressWarnings( "UseOfConcreteClass" ) 480 private static final void emitAnnotationValues( final CodeWriter codeWriter, final String whitespace, final String memberSeparator, final List<CodeBlockImpl> values ) throws UncheckedIOException 481 { 482 if( values.size() == 1 ) 483 { 484 codeWriter.indent( 2 ); 485 codeWriter.emit( values.getFirst() ); 486 codeWriter.unindent( 2 ); 487 } 488 else 489 { 490 codeWriter.emit( "{" + whitespace ); 491 codeWriter.indent( 2 ); 492 var first = true; 493 for( final var codeBlock : values ) 494 { 495 if( !first ) codeWriter.emit( memberSeparator ); 496 codeWriter.emit( codeBlock ); 497 first = false; 498 } 499 codeWriter.unindent( 2 ); 500 codeWriter.emit( whitespace + "}" ); 501 } 502 } // emitAnnotationValues() 503 504 /** 505 * {@inheritDoc} 506 */ 507 @Override 508 public final boolean equals( final Object o ) 509 { 510 var retValue = this == o; 511 if( !retValue && (o instanceof final AnnotationSpecImpl other) ) 512 { 513 retValue = m_Composer.equals( other.m_Composer ) && toString().equals( o.toString() ); 514 } 515 516 //---* Done *---------------------------------------------------------- 517 return retValue; 518 } // equals() 519 520 /** 521 * Returns the 522 * {@link JavaComposer} 523 * factory. 524 * 525 * @return The reference to the factory. 526 */ 527 @SuppressWarnings( {"PublicMethodNotExposedInInterface"} ) 528 public final JavaComposer getFactory() { return m_Composer; } 529 530 /** 531 * {@inheritDoc} 532 */ 533 @Override 534 public final int hashCode() { return hash( m_Composer, toString() ); } 535 536 /** 537 * The initializer for 538 * {@link #m_CachedString}. 539 * 540 * @return The return value for 541 * {@link #toString()}. 542 */ 543 private final String initializeCachedString() 544 { 545 final var resultBuilder = new StringBuilder(); 546 final var codeWriter = new CodeWriter( m_Composer, resultBuilder ); 547 try 548 { 549 codeWriter.emit( "$L", this ); 550 } 551 catch( final UncheckedIOException e ) 552 { 553 throw new UnexpectedExceptionError( e.getCause() ); 554 } 555 final var retValue = resultBuilder.toString(); 556 557 //---* Done *---------------------------------------------------------- 558 return retValue; 559 } // initializeCachedString() 560 561 /** 562 * Creates a new builder that is initialised with the components of this 563 * annotation. 564 * 565 * @return The new builder. 566 */ 567 @SuppressWarnings( "AccessingNonPublicFieldOfAnotherObject" ) 568 @Override 569 public final Builder toBuilder() 570 { 571 final var retValue = new BuilderImpl( m_Composer, m_Type ); 572 for( final var entry : m_Members.entrySet() ) 573 { 574 retValue.m_CodeBlocks.put( entry.getKey(), new ArrayList<>( entry.getValue() ) ); 575 } 576 577 //---* Done *---------------------------------------------------------- 578 return retValue; 579 } // toBuilder() 580 581 /** 582 * {@inheritDoc} 583 */ 584 @Override 585 public final String toString() { return m_CachedString.get(); } 586} 587// class AnnotationSpecImpl 588 589/* 590 * End of File 591 */