001/* 002 * ============================================================================ 003 * Copyright © 2015 Square, Inc. 004 * Copyright for the modifications © 2018-2025 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.lang.Math.min; 023import static java.lang.String.format; 024import static java.util.stream.Collectors.toList; 025import static org.apiguardian.api.API.Status.INTERNAL; 026import static org.apiguardian.api.API.Status.STABLE; 027import static org.tquadrat.foundation.javacomposer.internal.Util.NULL_REFERENCE; 028import static org.tquadrat.foundation.javacomposer.internal.Util.createDebugOutput; 029import static org.tquadrat.foundation.lang.Objects.checkState; 030import static org.tquadrat.foundation.lang.Objects.isNull; 031import static org.tquadrat.foundation.lang.Objects.nonNull; 032import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument; 033import static org.tquadrat.foundation.lang.Objects.requireValidArgument; 034import static org.tquadrat.foundation.lang.Objects.requireValidNonNullArgument; 035import static org.tquadrat.foundation.util.StringUtils.isNotEmpty; 036import static org.tquadrat.foundation.util.StringUtils.isNotEmptyOrBlank; 037 038import javax.lang.model.element.Element; 039import javax.lang.model.type.TypeMirror; 040import java.io.UncheckedIOException; 041import java.lang.reflect.Type; 042import java.util.ArrayList; 043import java.util.Arrays; 044import java.util.Collection; 045import java.util.List; 046import java.util.Map; 047import java.util.Set; 048import java.util.TreeSet; 049import java.util.regex.Matcher; 050import java.util.regex.Pattern; 051import java.util.regex.PatternSyntaxException; 052import java.util.stream.Collector; 053import java.util.stream.IntStream; 054import java.util.stream.Stream; 055 056import org.apiguardian.api.API; 057import org.tquadrat.foundation.annotation.ClassVersion; 058import org.tquadrat.foundation.exception.UnexpectedExceptionError; 059import org.tquadrat.foundation.exception.ValidationException; 060import org.tquadrat.foundation.javacomposer.ClassName; 061import org.tquadrat.foundation.javacomposer.CodeBlock; 062import org.tquadrat.foundation.javacomposer.FieldSpec; 063import org.tquadrat.foundation.javacomposer.JavaComposer; 064import org.tquadrat.foundation.javacomposer.MethodSpec; 065import org.tquadrat.foundation.javacomposer.ParameterSpec; 066import org.tquadrat.foundation.javacomposer.TypeSpec; 067import org.tquadrat.foundation.lang.Lazy; 068import org.tquadrat.foundation.lang.Objects; 069 070/** 071 * The implementation of 072 * {@link CodeBlock} 073 * for a fragment of a {@code *.java} file. 074 * 075 * @author Square,Inc. 076 * @modified Thomas Thrien - thomas.thrien@tquadrat.org 077 * @version $Id: CodeBlockImpl.java 1151 2025-10-01 21:32:15Z tquadrat $ 078 * @since 0.0.5 079 * 080 * @UMLGraph.link 081 */ 082@ClassVersion( sourceVersion = "$Id: CodeBlockImpl.java 1151 2025-10-01 21:32:15Z tquadrat $" ) 083@API( status = INTERNAL, since = "0.0.5" ) 084public final class CodeBlockImpl implements CodeBlock 085{ 086 /*---------------*\ 087 ====** Inner Classes **==================================================== 088 \*---------------*/ 089 /** 090 * The implementation of 091 * {@link org.tquadrat.foundation.javacomposer.CodeBlock.Builder} 092 * as the builder for a new 093 * {@link CodeBlockImpl} 094 * instance. 095 * 096 * @author Square,Inc. 097 * @modified Thomas Thrien - thomas.thrien@tquadrat.org 098 * @version $Id: CodeBlockImpl.java 1151 2025-10-01 21:32:15Z tquadrat $ 099 * @since 0.0.5 100 * 101 * @UMLGraph.link 102 */ 103 @ClassVersion( sourceVersion = "$Id: CodeBlockImpl.java 1151 2025-10-01 21:32:15Z tquadrat $" ) 104 @API( status = INTERNAL, since = "0.0.5" ) 105 public static final class BuilderImpl implements CodeBlock.Builder 106 { 107 /*------------*\ 108 ====** Attributes **=================================================== 109 \*------------*/ 110 /** 111 * The arguments. 112 */ 113 private final Collection<Object> m_Args = new ArrayList<>(); 114 115 /** 116 * The reference to the factory. 117 */ 118 @SuppressWarnings( "UseOfConcreteClass" ) 119 private final JavaComposer m_Composer; 120 121 /** 122 * The format Strings. 123 */ 124 private final List<String> m_FormatParts = new ArrayList<>(); 125 126 /** 127 * The static imports. 128 */ 129 private final Collection<String> m_StaticImports = new TreeSet<>(); 130 131 /*--------------*\ 132 ====** Constructors **================================================= 133 \*--------------*/ 134 /** 135 * Creates a new {@code BuilderImpl} instance. 136 * 137 * @param composer The reference to the factory that created this 138 * builder instance. 139 */ 140 public BuilderImpl( @SuppressWarnings( "UseOfConcreteClass" ) final JavaComposer composer ) 141 { 142 m_Composer = requireNonNullArgument( composer, "composer" ); 143 } // BuilderImpl() 144 145 /** 146 * Creates a new {@code BuilderImpl} instance. 147 * 148 * @param composer The reference to the factory that created this 149 * builder instance. 150 * @param formatParts The format parts. 151 * @param args The arguments. 152 */ 153 public BuilderImpl( @SuppressWarnings( "UseOfConcreteClass" ) final JavaComposer composer, final List<String> formatParts, final List<Object> args ) 154 { 155 this( composer ); 156 m_FormatParts.addAll( requireNonNullArgument( formatParts, "formatParts" ) ); 157 m_Args.addAll( requireNonNullArgument( args, "args" ) ); 158 } // BuilderImpl() 159 160 /*---------*\ 161 ====** Methods **====================================================== 162 \*---------*/ 163 /** 164 * {@inheritDoc} 165 */ 166 @Override 167 public final BuilderImpl add( final CodeBlock codeBlock ) 168 { 169 addDebug(); 170 final var retValue = addWithoutDebugInfo( codeBlock ); 171 172 //---* Done *------------------------------------------------------ 173 return retValue; 174 } // add() 175 176 /** 177 * {@inheritDoc} 178 */ 179 @API( status = INTERNAL, since = "0.2.0" ) 180 @Override 181 public final BuilderImpl add( final String format, final Object... args ) 182 { 183 addDebug(); 184 185 final var retValue = addWithoutDebugInfo( format, args ); 186 187 //---* Done *---------------------------------------------------------- 188 return retValue; 189 } // add() 190 191 /** 192 * Adds the placeholder's argument. 193 * 194 * @param format The format. 195 * @param placeholder The placeholder character. 196 * @param arg The argument. 197 */ 198 private final void addArgument( final String format, final char placeholder, final Object arg ) 199 { 200 final var argument = switch( placeholder ) 201 { 202 case 'N' -> argToName( arg ); 203 case 'L' -> argToLiteral( arg ); 204 case 'S' -> argToString( arg ); 205 case 'T' -> argToType( arg ); 206 default -> throw new IllegalArgumentException( format( "invalid format string: '%s'", format ) ); 207 }; 208 m_Args.add( argument ); 209 } // addArgument() 210 211 /** 212 * Adds debug output. 213 */ 214 private final void addDebug() 215 { 216 createDebugOutput( m_Composer.addDebugOutput() ) 217 .ifPresent( v -> m_FormatParts.add( v.asComment() ) ); 218 } // addDebug() 219 220 /** 221 * {@inheritDoc} 222 */ 223 @API( status = INTERNAL, since = "0.2.0" ) 224 @Override 225 public final BuilderImpl addNamed( final String format, final Map<String,?> args ) 226 { 227 addDebug(); 228 229 for( final var argument : requireNonNullArgument( args, "args" ).keySet() ) 230 { 231 checkState( LOWERCASE.matcher( argument ).matches(), () -> new ValidationException( "argument '%s' must start with a lowercase character".formatted( argument ) ) ); 232 } 233 if( isNotEmpty( requireNonNullArgument( format, "format" ) ) ) 234 { 235 var currentPos = 0; 236 ParseLoop: while( currentPos < format.length() ) 237 { 238 final var nextPos = format.indexOf( "$", currentPos ); 239 if( nextPos == -1 ) 240 { 241 m_FormatParts.add( format.substring( currentPos ) ); 242 break ParseLoop; 243 } 244 245 if( currentPos != nextPos ) 246 { 247 m_FormatParts.add( format.substring( currentPos, nextPos ) ); 248 currentPos = nextPos; 249 } 250 251 Matcher matcher = null; 252 final var colon = format.indexOf( ':', currentPos ); 253 if( colon != -1 ) 254 { 255 final var endIndex = min( colon + 2, format.length() ); 256 matcher = NAMED_ARGUMENT.matcher( format.substring( currentPos, endIndex ) ); 257 } 258 if( nonNull( matcher ) && matcher.lookingAt() ) 259 { 260 final var argumentName = matcher.group( "argumentName" ); 261 checkState( args.containsKey( argumentName ), () -> new ValidationException( "Missing named argument for $%s".formatted( argumentName ) ) ); 262 final var formatChar = matcher.group( "typeChar" ).charAt( 0 ); 263 addArgument( format, formatChar, args.get( argumentName ) ); 264 m_FormatParts.add( "$" + formatChar ); 265 currentPos += matcher.regionEnd(); 266 } 267 else 268 { 269 checkState( currentPos < format.length() - 1, () -> new ValidationException( "dangling $ at end" ) ); 270 if( !isNoArgPlaceholder( format.charAt( currentPos + 1 ) ) ) 271 { 272 throw new ValidationException( "unknown format $%s at %s in '%s'".formatted( format.charAt( currentPos + 1 ), currentPos + 1, format ) ); 273 } 274 m_FormatParts.add( format.substring( currentPos, currentPos + 2 ) ); 275 currentPos += 2; 276 } 277 } // ParseLoop: 278 } 279 280 //---* Done *------------------------------------------------------ 281 return this; 282 } // addNamed() 283 284 /** 285 * {@inheritDoc} 286 */ 287 @API( status = STABLE, since = "0.2.0" ) 288 @Override 289 public final BuilderImpl addStatement( final String format, final Object... args ) 290 { 291 final var retValue = add( "$[" ) 292 .addWithoutDebugInfo( format, args ) 293 .addWithoutDebugInfo( ";\n$]" ); 294 295 //---* Done *---------------------------------------------------------- 296 return retValue; 297 } // addStatement() 298 299 /** 300 * {@inheritDoc} 301 */ 302 @API( status = STABLE, since = "0.2.0" ) 303 @Override 304 public final BuilderImpl addStaticImport( final Class<?> clazz, final String... names ) 305 { 306 return addStaticImport( ClassNameImpl.from( clazz ), names ); 307 } // addStaticImport() 308 309 /** 310 * {@inheritDoc} 311 */ 312 @API( status = STABLE, since = "0.2.0" ) 313 @Override 314 public final BuilderImpl addStaticImport( final ClassName className, final String... names ) 315 { 316 final var canonicalName = requireNonNullArgument( className, "className" ).canonicalName(); 317 for( final var name : requireValidNonNullArgument( names, "names", v -> v.length > 0, "%s array is empty"::formatted ) ) 318 { 319 m_StaticImports.add( 320 format( 321 "%s.%s", 322 canonicalName, 323 requireValidArgument( 324 name, 325 "name", 326 Objects::nonNull, 327 _ -> "null entry in names array: %s".formatted( Arrays.toString( names ) ) 328 ) 329 ) 330 ); 331 } 332 333 //---* Done *------------------------------------------------------ 334 return this; 335 } // addStaticImport() 336 337 /** 338 * {@inheritDoc} 339 */ 340 @API( status = STABLE, since = "0.2.0" ) 341 @Override 342 public final BuilderImpl addStaticImport( final Enum<?> constant ) 343 { 344 return addStaticImport( ClassNameImpl.from( requireNonNullArgument( constant, "constant" ).getDeclaringClass() ), constant.name() ); 345 } // addStaticImport() 346 347 /** 348 * Adds a 349 * {@link CodeBlock} 350 * instance without prepending any debug output. 351 * 352 * @param codeBlock The code block. 353 * @return This {@code Builder} instance. 354 */ 355 @SuppressWarnings( {"PublicMethodNotExposedInInterface"} ) 356 public final BuilderImpl addWithoutDebugInfo( final CodeBlock codeBlock ) 357 { 358 final var builder = (BuilderImpl) requireNonNullArgument( codeBlock, "codeBlock" ) 359 .toBuilder(); 360 m_FormatParts.addAll( builder.formatParts() ); 361 m_Args.addAll( builder.args() ); 362 m_StaticImports.addAll( ((CodeBlockImpl) codeBlock).getStaticImports() ); 363 364 //---* Done *------------------------------------------------------ 365 return this; 366 } // addWithoutDebugInfo() 367 368 /** 369 * <p>{@summary Adds code with positional or relative arguments, 370 * without prepending any debug output.}</p> 371 * <p>Relative arguments map 1:1 with the placeholders in the format 372 * string.</p> 373 * <p>Positional arguments use an index after the placeholder to 374 * identify which argument index to use. For example, for a literal to 375 * reference the 3<sup>rd</sup> argument, use {@code "$3L"} (1 based 376 * index).</p> 377 * <p>Mixing relative and positional arguments in a call to add is 378 * illegal and will result in an error.</p> 379 * 380 * @param format The format; may be empty. 381 * @param args The arguments. 382 * @return This {@code Builder} instance. 383 */ 384 @SuppressWarnings( {"PublicMethodNotExposedInInterface", "OverlyComplexMethod", "CharacterComparison"} ) 385 @API( status = INTERNAL, since = "0.2.0" ) 386 public final BuilderImpl addWithoutDebugInfo( final String format, final Object... args ) 387 { 388 var hasRelative = false; 389 var hasIndexed = false; 390 var relativeParameterCount = 0; 391 392 final var length = requireNonNullArgument( format, "format" ).length(); 393 final var indexedParameterCount = new int [requireNonNullArgument( args, "args" ).length]; 394 395 ParseLoop: 396 //noinspection ForLoopWithMissingComponent 397 for( var pos = 0; pos < length; /* Update is inside the loop body */ ) 398 { 399 if( format.charAt( pos ) != '$' ) 400 { 401 var nextPos = format.indexOf( '$', pos + 1 ); 402 if( nextPos == -1 ) nextPos = format.length(); 403 m_FormatParts.add( format.substring( pos, nextPos ) ); 404 pos = nextPos; 405 continue ParseLoop; 406 } 407 408 //---* The update for the for-loop … *------------------------- 409 ++pos ; // '$'. 410 411 /* 412 * Consume zero or more digits, leaving 'c' as the first 413 * non-digit char after the '$'. 414 */ 415 final var indexStart = pos; 416 char c; 417 do 418 { 419 checkState( pos < format.length(), () -> new ValidationException( "dangling format characters in '%s'".formatted( format ) ) ); 420 c = format.charAt( pos++ ); 421 } 422 while( c >= '0' && c <= '9' ); 423 final var indexEnd = pos - 1; 424 425 //---* If 'c' doesn't take an argument, we're done *----------- 426 if( isNoArgPlaceholder( c ) ) 427 { 428 checkState( indexStart == indexEnd, () -> new ValidationException( "$$, $>, $<, $[, $], $W, and $Z may not have an index" ) ); 429 m_FormatParts.add( "$" + c ); 430 continue ParseLoop; 431 } 432 433 /* 434 * Find either the indexed argument, or the relative argument 435 * (0-based). 436 */ 437 final int index; 438 if( indexStart < indexEnd ) 439 { 440 index = Integer.parseInt( format.substring( indexStart, indexEnd ) ) - 1; 441 hasIndexed = true; 442 if( args.length > 0 ) 443 { 444 //---* modulo is needed, checked below anyway *-------- 445 ++indexedParameterCount [index % args.length]; 446 } 447 } 448 else 449 { 450 index = relativeParameterCount++; 451 hasRelative = true; 452 } 453 454 checkState( index >= 0 && index < args.length, () -> new ValidationException( "index %d for '%s' not in range (received %s arguments)".formatted( index + 1, format.substring( indexStart - 1, indexEnd + 1 ), args.length ) ) ); 455 checkState( !hasIndexed || !hasRelative, () -> new ValidationException( "cannot mix indexed and positional parameters" ) ); 456 457 addArgument( format, c, args [index] ); 458 459 m_FormatParts.add( "$" + c ); 460 } // ParseLoop: 461 462 if( hasRelative && (relativeParameterCount < args.length) ) 463 { 464 throw new ValidationException( "unused arguments: expected %s, received %s".formatted( relativeParameterCount, args.length ) ); 465 } 466 if( hasIndexed ) 467 { 468 final Collection<String> unused = IntStream.range( 0, args.length ) 469 .filter( i -> indexedParameterCount[i] == 0 ) 470 .mapToObj( i -> "$" + (i + 1) ) 471 .collect( toList() ); 472 if( !unused.isEmpty() ) 473 { 474 throw new ValidationException( "unused argument%s: %s".formatted( unused.size() == 1 ? "" : "s", String.join( ", ", unused ) ) ); 475 } 476 } 477 478 //---* Done *------------------------------------------------------ 479 return this; 480 } // addWithoutDebugInfo() 481 482 /** 483 * Returns the arguments. 484 * 485 * @return The arguments. 486 */ 487 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 488 public final List<Object> args() { return List.copyOf( m_Args ); } 489 490 /** 491 * Returns the given object literally. 492 * 493 * @param o The object. 494 * @return The literal. 495 */ 496 private static final Object argToLiteral( final Object o ) 497 { 498 final var retValue = nonNull( o ) ? o : NULL_REFERENCE; 499 500 //---* Done *------------------------------------------------------ 501 return retValue; 502 } // argToLiteral() 503 504 /** 505 * Translates the given object to a name. 506 * 507 * @param o The object. 508 * @return The name. 509 */ 510 private static final Object argToName( final Object o ) 511 { 512 final var retValue = switch( o ) 513 { 514 case final CharSequence charSequence -> charSequence.toString(); 515 case final ParameterSpec parameterSpec -> parameterSpec.name(); 516 case final FieldSpec fieldSpec -> fieldSpec.name(); 517 case final MethodSpec methodSpec -> methodSpec.name(); 518 case final TypeSpec typeSpec -> 519 /* 520 * Does not work for anonymous types, so no check for the name 521 * is required. 522 */ 523 //noinspection OptionalGetWithoutIsPresent 524 typeSpec.name().get(); 525 case null, default -> throw new IllegalArgumentException( "expected name but was " + o ); 526 }; 527 528 //---* Done *------------------------------------------------------ 529 return retValue; 530 } // argToName() 531 532 /** 533 * Translates the given object to a String. 534 * 535 * @param o The object. 536 * @return The resulting String, or 537 * {@link Util#NULL_REFERENCE} 538 * if the object is 539 * {@code null}. 540 */ 541 private static final Object argToString( final Object o ) 542 { 543 final var retValue = isNull( o ) ? NULL_REFERENCE : Objects.toString( o ); 544 545 //---* Done *------------------------------------------------------ 546 return retValue; 547 } // argToString() 548 549 /** 550 * Translates the given object to a type. 551 * 552 * @param o The object. 553 * @return The resulting type. 554 */ 555 private static final TypeNameImpl argToType( final Object o ) 556 { 557 final var retValue = switch( o ) 558 { 559 case final TypeNameImpl typeName -> typeName; 560 case final TypeMirror typeMirror -> TypeNameImpl.from( typeMirror ); 561 case final Element element -> TypeNameImpl.from( element.asType() ); 562 case final Type type -> TypeNameImpl.from( type ); 563 case null, default -> throw new IllegalArgumentException( "expected type but was " + o ); 564 }; 565 566 //---* Done *------------------------------------------------------ 567 return retValue; 568 } // argToType() 569 570 /** 571 * {@inheritDoc} 572 */ 573 @API( status = INTERNAL, since = "0.0.5" ) 574 @Override 575 public final BuilderImpl beginControlFlow( final String controlFlow, final Object... args ) 576 { 577 addDebug(); 578 if( isNotEmptyOrBlank( requireNonNullArgument( controlFlow, "controlFlow" ) ) ) 579 { 580 addWithoutDebugInfo( controlFlow, args ); 581 if( !controlFlow.endsWith( "\n" ) ) addWithoutDebugInfo( " " ); 582 } 583 addWithoutDebugInfo( "{\n" ); 584 indent(); 585 586 //---* Done *------------------------------------------------------ 587 return this; 588 } // beginControlFlow() 589 590 /** 591 * {@inheritDoc} 592 */ 593 @Override 594 public final CodeBlockImpl build() { return new CodeBlockImpl( this ); } 595 596 /** 597 * {@inheritDoc} 598 */ 599 @Override 600 public final BuilderImpl endControlFlow() 601 { 602 addDebug(); 603 unindent(); 604 addWithoutDebugInfo( "}\n" ); 605 606 //---* Done *------------------------------------------------------ 607 return this; 608 } // endControlFlow() 609 610 /** 611 * {@inheritDoc} 612 */ 613 @API( status = INTERNAL, since = "0.0.5" ) 614 @Override 615 public final BuilderImpl endControlFlow( final String controlFlow, final Object... args ) 616 { 617 addDebug(); 618 unindent(); 619 addWithoutDebugInfo( "} " + requireNonNullArgument( controlFlow, "controlFlow" ) + ";\n", args ); 620 621 //---* Done *------------------------------------------------------ 622 return this; 623 } // endControlFlow() 624 625 /** 626 * Returns the format parts. 627 * 628 * @return The format parts. 629 */ 630 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 631 public final List<String> formatParts() { return List.copyOf( m_FormatParts ); } 632 633 /** 634 * {@inheritDoc} 635 */ 636 @Override 637 public final BuilderImpl indent() 638 { 639 m_FormatParts.add( "$>" ); 640 641 //---* Done *------------------------------------------------------ 642 return this; 643 } // indent() 644 645 /** 646 * {@inheritDoc} 647 */ 648 @Override 649 public final boolean isEmpty() { return m_FormatParts.isEmpty(); } 650 651 /** 652 * Checks whether the given placeholder character would expect an 653 * argument. 654 * 655 * @param placeholder The placeholder character. 656 * @return {@code true} if there is no argument expected, 657 * {@code false} otherwise. 658 */ 659 private static final boolean isNoArgPlaceholder( final char placeholder ) 660 { 661 final var retValue = IntStream.of( '$', '>', '<', '[', ']', 'W', 'Z' ) 662 .anyMatch( p -> p == placeholder ); 663 664 //---* Done *------------------------------------------------------ 665 return retValue; 666 } // isNoArgPlaceholder() 667 668 /** 669 * {@inheritDoc} 670 */ 671 @API( status = INTERNAL, since = "0.0.5" ) 672 @Override 673 public final BuilderImpl nextControlFlow( final String controlFlow, final Object... args ) 674 { 675 unindent(); 676 addWithoutDebugInfo( "}" ); 677 if( !requireNonNullArgument( controlFlow, "controlFlow" ).startsWith( "\n" ) ) addWithoutDebugInfo( " " ); 678 add( controlFlow, args ); 679 if( !controlFlow.endsWith( "\n" ) ) addWithoutDebugInfo(" " ); 680 addWithoutDebugInfo( "{\n" ); 681 indent(); 682 683 //---* Done *------------------------------------------------------ 684 return this; 685 } // nextControlFlow() 686 687 /** 688 * {@inheritDoc} 689 */ 690 @Override 691 public final BuilderImpl unindent() 692 { 693 m_FormatParts.add( "$<" ); 694 695 //---* Done *------------------------------------------------------ 696 return this; 697 } // unindent() 698 } 699 // class BuilderImpl 700 701 /** 702 * A helper class that supports to join code blocks. 703 * 704 * @author Thomas Thrien - thomas.thrien@tquadrat.org 705 * @version $Id: CodeBlockImpl.java 1151 2025-10-01 21:32:15Z tquadrat $ 706 * @since 0.0.5 707 * 708 * @UMLGraph.link 709 */ 710 @ClassVersion( sourceVersion = "$Id: CodeBlockImpl.java 1151 2025-10-01 21:32:15Z tquadrat $" ) 711 @API( status = INTERNAL, since = "0.0.5" ) 712 private static final class CodeBlockJoiner 713 { 714 /*------------*\ 715 ====** Attributes **=================================================== 716 \*------------*/ 717 /** 718 * The builder that is used to deliver the final code block. 719 */ 720 @SuppressWarnings( "UseOfConcreteClass" ) 721 private final BuilderImpl m_Builder; 722 723 /** 724 * The separator for the joined code blocks. 725 */ 726 private final String m_Delimiter; 727 728 /** 729 * Flag that indicates whether to add the delimiter on adding a new 730 * code block. 731 */ 732 private boolean m_First = true; 733 734 /*--------------*\ 735 ====** Constructors **================================================= 736 \*--------------*/ 737 /** 738 * Creates a new {@code CodeBlockJoiner} instance. 739 * 740 * @param delimiter The separator for the joined code blocks. 741 * @param builder The builder that is used to deliver the final code 742 * block. 743 */ 744 public CodeBlockJoiner( final String delimiter, @SuppressWarnings( "UseOfConcreteClass" ) final BuilderImpl builder ) 745 { 746 m_Delimiter = requireNonNullArgument( delimiter, "delimiter" ); 747 m_Builder = requireNonNullArgument( builder, "builder" ); 748 } // CodeBlockJoiner() 749 750 /*---------*\ 751 ====** Methods **====================================================== 752 \*---------*/ 753 /** 754 * Adds another code block. 755 * 756 * @param codeBlock The new code block. 757 * @return This {@code CodeBlockJoiner} instance. 758 */ 759 @SuppressWarnings( {"TypeMayBeWeakened", "UnusedReturnValue"} ) 760 public final CodeBlockJoiner add( @SuppressWarnings( "UseOfConcreteClass" ) final CodeBlockImpl codeBlock ) 761 { 762 if( !m_First ) m_Builder.addWithoutDebugInfo( m_Delimiter ); 763 m_First = false; 764 765 m_Builder.add( codeBlock ); 766 767 //---* Done *------------------------------------------------------ 768 return this; 769 } // add() 770 771 /** 772 * Returns the new code block with the joined ones. 773 * 774 * @return The new code block. 775 */ 776 public final CodeBlockImpl join() { return m_Builder.build(); } 777 778 /** 779 * Merges this code block joiner with the given other one. 780 * 781 * @param other The other code block joiner. 782 * @return This {@code CodeBlockJoiner} instance. 783 */ 784 public final CodeBlockJoiner merge( @SuppressWarnings( "UseOfConcreteClass" ) final CodeBlockJoiner other ) 785 { 786 final var otherBlock = requireNonNullArgument( other, "other" ).m_Builder.build(); 787 if( !otherBlock.isEmpty() ) add( otherBlock ); 788 789 //---* Done *------------------------------------------------------ 790 return this; 791 } // merge() 792 } 793 // class CodeBlockJoiner 794 795 /*------------*\ 796 ====** Attributes **======================================================= 797 \*------------*/ 798 /** 799 * The arguments. 800 */ 801 private final List<Object> m_Args; 802 803 /** 804 * Lazily initialised return value of 805 * {@link #toString()} 806 * for this code block. 807 */ 808 private final Lazy<String> m_CachedString; 809 810 /** 811 * The reference to the factory. 812 */ 813 @SuppressWarnings( "UseOfConcreteClass" ) 814 private final JavaComposer m_Composer; 815 816 /** 817 * A heterogeneous list containing string literals and value placeholders. 818 */ 819 private final List<String> m_FormatParts; 820 821 /** 822 * The static imports. 823 */ 824 private final Set<String> m_StaticImports; 825 826 /*------------------------*\ 827 ====** Static Initialisations **=========================================== 828 \*------------------------*/ 829 /** 830 * The regular expression that is used to determine whether a parameter 831 * name starts with a lowercase character. 832 */ 833 public static final Pattern LOWERCASE; 834 835 /** 836 * The regular expression that is used to obtain the argument name from 837 * a format string. 838 */ 839 public static final Pattern NAMED_ARGUMENT; 840 841 static 842 { 843 try 844 { 845 LOWERCASE = Pattern.compile( "[a-z]+[\\w_]*" ); 846 NAMED_ARGUMENT = Pattern.compile( "\\$(?<argumentName>[\\w_]+):(?<typeChar>\\w).*" ); 847 } 848 catch( final PatternSyntaxException e ) 849 { 850 throw new ExceptionInInitializerError( e ); 851 } 852 } 853 854 /*--------------*\ 855 ====** Constructors **===================================================== 856 \*--------------*/ 857 /** 858 * Creates a new {@code CodeBlockImpl} instance. 859 * 860 * @param builder The builder for this instance. 861 */ 862 @SuppressWarnings( {"AccessingNonPublicFieldOfAnotherObject"} ) 863 public CodeBlockImpl( @SuppressWarnings( "UseOfConcreteClass" ) final BuilderImpl builder ) 864 { 865 m_Composer = builder.m_Composer; 866 m_FormatParts = builder.formatParts(); 867 m_Args = builder.args(); 868 m_StaticImports = Set.copyOf( builder.m_StaticImports ); 869 870 m_CachedString = Lazy.use( this::initialiseCachedString ); 871 } // CodeBlockImpl() 872 873 /*---------*\ 874 ====** Methods **========================================================== 875 \*---------*/ 876 /** 877 * Returns the arguments. 878 * 879 * @return The arguments. 880 */ 881 /* 882 * Originally, this was the reference to the internal collection! 883 */ 884 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 885 public final List<Object> args() { return List.copyOf( m_Args ); } 886 887 /** 888 * {@inheritDoc} 889 */ 890 @Override 891 public final boolean equals( final Object o ) 892 { 893 var retValue = this == o; 894 if( !retValue && (o instanceof final CodeBlockImpl other) ) 895 { 896 retValue = m_Composer.equals( other.m_Composer ) && toString().equals( o.toString() ); 897 } 898 899 //---* Done *---------------------------------------------------------- 900 return retValue; 901 } // equals() 902 903 /** 904 * Returns the format parts from this code block. 905 * 906 * @return The format parts. 907 */ 908 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 909 public final List<String> formatParts() { return List.copyOf( m_FormatParts ); } 910 911 /** 912 * Returns the 913 * {@link JavaComposer} 914 * factory. 915 * 916 * @return The reference to the factory. 917 */ 918 @SuppressWarnings( {"PublicMethodNotExposedInInterface"} ) 919 public final JavaComposer getFactory() { return m_Composer; } 920 921 /** 922 * Returns the static imports for this code block. 923 * 924 * @return The static imports. 925 */ 926 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 927 @API( status = INTERNAL, since = "0.2.0" ) 928 public final Set<String> getStaticImports() { return m_StaticImports; } 929 930 /** 931 * {@inheritDoc} 932 */ 933 @Override 934 public final int hashCode() { return toString().hashCode(); } 935 936 /** 937 * The initializer for 938 * {@link #m_CachedString}. 939 * 940 * @return The return value for 941 * {@link #toString()}. 942 */ 943 private final String initialiseCachedString() 944 { 945 final var resultBuilder = new StringBuilder(); 946 final var codeWriter = new CodeWriter( m_Composer, resultBuilder ); 947 try 948 { 949 codeWriter.emit( this ); 950 } 951 catch( final UncheckedIOException e ) 952 { 953 throw new UnexpectedExceptionError( e.getCause() ); 954 } 955 final var retValue = resultBuilder.toString(); 956 957 //---* Done *---------------------------------------------------------- 958 return retValue; 959 } // initialiseCachedString() 960 961 /** 962 * {@inheritDoc} 963 */ 964 @Override 965 public final boolean isEmpty() { return m_FormatParts.isEmpty(); } 966 967 /** 968 * {@inheritDoc} 969 */ 970 @Override 971 public final CodeBlock join( final String separator, final CodeBlock... codeBlocks ) 972 { 973 final var retValue = makeCodeBlockStream( this, requireNonNullArgument( codeBlocks, "codeBlocks" ) ) 974 .collect( joining( separator ) ); 975 976 //---* Done *---------------------------------------------------------- 977 return retValue; 978 } // join() 979 980 /** 981 * <p>{@summary Joins this code block with the given code blocks into a 982 * single new {@code CodeBlock} instance, each separated by the given 983 * separator.} The given prefix will be prepended to the new 984 * {@code CodeBloc}, and the given suffix will be appended to it.</p> 985 * <p>For example, joining "{@code String s}", 986 * "{@code Object o}" and "{@code int i}" using 987 * "{@code , }" as the separator would produce 988 * "{@code String s, Object o, int i}".</p> 989 * 990 * @param separator The separator. 991 * @param prefix The prefix. 992 * @param suffix The suffix. 993 * @param codeBlocks The code blocks to join. 994 * @return The new code block. 995 */ 996 @Override 997 public final CodeBlock join( final String separator, final String prefix, final String suffix, final CodeBlock... codeBlocks ) 998 { 999 final var retValue = makeCodeBlockStream( this, requireNonNullArgument( codeBlocks, "codeBlocks" ) ) 1000 .collect( joining( separator, prefix, suffix ) ); 1001 1002 //---* Done *---------------------------------------------------------- 1003 return retValue; 1004 } // join() 1005 1006 /** 1007 * <p>{@summary A 1008 * {@link Collector} 1009 * implementation that joins {@code CodeBlock} instances together into one 1010 * new code block, separated by the given separator.}</p> 1011 * <p>For example, joining "{@code String s}", 1012 * "{@code Object o}" and "{@code int i}" using 1013 * "{@code , }" as the separator would produce 1014 * "{@code String s, Object o, int i}".</p> 1015 * 1016 * @param separator The separator. 1017 * @return The new collector. 1018 */ 1019 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 1020 public final Collector<CodeBlockImpl,?,CodeBlockImpl> joining( final String separator ) 1021 { 1022 final Collector<CodeBlockImpl,?,CodeBlockImpl> retValue = Collector.of 1023 ( 1024 () -> new CodeBlockJoiner( separator, new BuilderImpl( m_Composer ) ), CodeBlockJoiner::add, CodeBlockJoiner::merge, CodeBlockJoiner::join 1025 ); 1026 1027 //---* Done *---------------------------------------------------------- 1028 return retValue; 1029 } // joining() 1030 1031 /** 1032 * <p>{@summary A 1033 * {@link Collector} 1034 * implementation that joins {@code CodeBlock} instances together into one 1035 * new code block, separated by the given separator.} The given prefix 1036 * will be prepended to the new {@code CodeBloc}, and the given suffix 1037 * will be appended to it.</p> 1038 * <p>For example, joining "{@code String s}", 1039 * "{@code Object o}" and "{@code int i}" using 1040 * "{@code , }" as the separator, and 1041 * "{@code int func( }" as the prefix and " {@code )}" 1042 * as the suffix respectively would produce 1043 * "{@code int func( String s, Object o, int i )}".</p> 1044 * 1045 * @param separator The separator. 1046 * @param prefix The prefix. 1047 * @param suffix The suffix. 1048 * @return The new collector. 1049 */ 1050 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 1051 public final Collector<CodeBlockImpl,?,CodeBlockImpl> joining( final String separator, final String prefix, final String suffix ) 1052 { 1053 final var builder = new BuilderImpl( m_Composer ); 1054 builder.add( "$N", prefix ); 1055 final Collector<CodeBlockImpl,?,CodeBlockImpl> retValue = Collector.of 1056 ( 1057 () -> new CodeBlockJoiner( separator, builder ), CodeBlockJoiner::add, CodeBlockJoiner::merge, joiner -> 1058 { 1059 builder.add( m_Composer.codeBlockOf( "$N", suffix ) ); 1060 return joiner.join(); 1061 } 1062 ); 1063 1064 //---* Done *---------------------------------------------------------- 1065 return retValue; 1066 } // joining() 1067 1068 /** 1069 * Composes a stream from the given {@code CodeBlock} instances. 1070 * 1071 * @param head The first code block. 1072 * @param tail The other code blocks. 1073 * @return The 1074 * {@link Stream} 1075 * instance with the {@code CodeBlock} instances. 1076 */ 1077 private static final Stream<CodeBlockImpl> makeCodeBlockStream( final CodeBlock head, final CodeBlock... tail ) 1078 { 1079 final var builder = Stream.<CodeBlockImpl>builder(); 1080 builder.add( (CodeBlockImpl) requireNonNullArgument( head, "head" ) ); 1081 for( final var block : requireNonNullArgument( tail, "tail" ) ) 1082 { 1083 builder.add( (CodeBlockImpl) block ); 1084 } 1085 final var retValue = builder.build(); 1086 1087 //---* Done *---------------------------------------------------------- 1088 return retValue; 1089 } // makeCodeBlockStream() 1090 1091 /** 1092 * Creates a new builder that is initialised with the components of this 1093 * code block. 1094 * 1095 * @return The new builder. 1096 */ 1097 @SuppressWarnings( {"AccessingNonPublicFieldOfAnotherObject"} ) 1098 @Override 1099 public final BuilderImpl toBuilder() 1100 { 1101 final var retValue = new BuilderImpl( m_Composer, m_FormatParts, m_Args ); 1102 retValue.m_StaticImports.addAll( m_StaticImports ); 1103 1104 //---* Done *---------------------------------------------------------- 1105 return retValue; 1106 } // toBuilder() 1107 1108 /** 1109 * {@inheritDoc} 1110 */ 1111 @Override 1112 public final String toString() { return m_CachedString.get(); } 1113} 1114// class CodeBlockImpl 1115 1116/* 1117 * End of File 1118 */