001/* 002 * ============================================================================ 003 * Copyright © 2015 Square, Inc. 004 * Copyright for the modifications © 2018-2026 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.UnlikelyExceptionError; 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 1258 2026-06-04 18:33:06Z tquadrat $ 078 * @since 0.0.5 079 * 080 * @UMLGraph.link 081 */ 082@ClassVersion( sourceVersion = "$Id: CodeBlockImpl.java 1258 2026-06-04 18:33:06Z 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 1258 2026-06-04 18:33:06Z tquadrat $ 099 * @since 0.0.5 100 * 101 * @UMLGraph.link 102 */ 103 @ClassVersion( sourceVersion = "$Id: CodeBlockImpl.java 1258 2026-06-04 18:33:06Z 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 * <p>{@summary {@constructor}}</p> 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 * <p>{@summary {@constructor}}</p> 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 //noinspection StandardVariableNames 318 for( final var name : requireValidNonNullArgument( names, "names", v -> v.length > 0, (n,_) -> "%s array is empty".formatted( n ) ) ) 319 { 320 m_StaticImports.add( 321 format( 322 "%s.%s", 323 canonicalName, 324 requireValidArgument( 325 name, 326 "name", 327 Objects::nonNull, 328 (_,_) -> "null entry in names array: %s".formatted( Arrays.toString( names ) ) 329 ) 330 ) 331 ); 332 } 333 334 //---* Done *------------------------------------------------------ 335 return this; 336 } // addStaticImport() 337 338 /** 339 * {@inheritDoc} 340 */ 341 @API( status = STABLE, since = "0.2.0" ) 342 @Override 343 public final BuilderImpl addStaticImport( final Enum<?> constant ) 344 { 345 return addStaticImport( ClassNameImpl.from( requireNonNullArgument( constant, "constant" ).getDeclaringClass() ), constant.name() ); 346 } // addStaticImport() 347 348 /** 349 * Adds a 350 * {@link CodeBlock} 351 * instance without prepending any debug output. 352 * 353 * @param codeBlock The code block. 354 * @return This {@code Builder} instance. 355 */ 356 @SuppressWarnings( {"PublicMethodNotExposedInInterface"} ) 357 public final BuilderImpl addWithoutDebugInfo( final CodeBlock codeBlock ) 358 { 359 final var builder = (BuilderImpl) requireNonNullArgument( codeBlock, "codeBlock" ) 360 .toBuilder(); 361 m_FormatParts.addAll( builder.formatParts() ); 362 m_Args.addAll( builder.args() ); 363 m_StaticImports.addAll( ((CodeBlockImpl) codeBlock).getStaticImports() ); 364 365 //---* Done *------------------------------------------------------ 366 return this; 367 } // addWithoutDebugInfo() 368 369 /** 370 * <p>{@summary Adds code with positional or relative arguments, 371 * without prepending any debug output.}</p> 372 * <p>Relative arguments map 1:1 with the placeholders in the format 373 * string.</p> 374 * <p>Positional arguments use an index after the placeholder to 375 * identify which argument index to use. For example, for a literal to 376 * reference the 3<sup>rd</sup> argument, use {@code "$3L"} (1 based 377 * index).</p> 378 * <p>Mixing relative and positional arguments in a call to add is 379 * illegal and will result in an error.</p> 380 * 381 * @param format The format; may be empty. 382 * @param args The arguments. 383 * @return This {@code Builder} instance. 384 */ 385 @SuppressWarnings( {"PublicMethodNotExposedInInterface", "OverlyComplexMethod", "CharacterComparison"} ) 386 @API( status = INTERNAL, since = "0.2.0" ) 387 public final BuilderImpl addWithoutDebugInfo( final String format, final Object... args ) 388 { 389 var hasRelative = false; 390 var hasIndexed = false; 391 @SuppressWarnings( "LocalVariableNamingConvention" ) 392 var relativeParameterCount = 0; 393 394 final var length = requireNonNullArgument( format, "format" ).length(); 395 @SuppressWarnings( "LocalVariableNamingConvention" ) 396 final var indexedParameterCount = new int [requireNonNullArgument( args, "args" ).length]; 397 398 ParseLoop: 399 //noinspection ForLoopWithMissingComponent 400 for( var pos = 0; pos < length; /* Update is inside the loop body */ ) 401 { 402 if( format.charAt( pos ) != '$' ) 403 { 404 var nextPos = format.indexOf( '$', pos + 1 ); 405 if( nextPos == -1 ) nextPos = format.length(); 406 m_FormatParts.add( format.substring( pos, nextPos ) ); 407 pos = nextPos; 408 continue ParseLoop; 409 } 410 411 //---* The update for the for-loop … *------------------------- 412 ++pos ; // '$'. 413 414 /* 415 * Consume zero or more digits, leaving 'c' as the first 416 * non-digit char after the '$'. 417 */ 418 final var indexStart = pos; 419 char c; 420 do 421 { 422 checkState( pos < format.length(), () -> new ValidationException( "dangling format characters in '%s'".formatted( format ) ) ); 423 c = format.charAt( pos++ ); 424 } 425 while( c >= '0' && c <= '9' ); 426 final var indexEnd = pos - 1; 427 428 //---* If 'c' doesn't take an argument, we're done *----------- 429 if( isNoArgPlaceholder( c ) ) 430 { 431 checkState( indexStart == indexEnd, () -> new ValidationException( "$$, $>, $<, $[, $], $W, and $Z may not have an index" ) ); 432 m_FormatParts.add( "$" + c ); 433 continue ParseLoop; 434 } 435 436 /* 437 * Find either the indexed argument, or the relative argument 438 * (0-based). 439 */ 440 final int index; 441 if( indexStart < indexEnd ) 442 { 443 index = Integer.parseInt( format.substring( indexStart, indexEnd ) ) - 1; 444 hasIndexed = true; 445 if( args.length > 0 ) 446 { 447 //---* modulo is needed, checked below anyway *-------- 448 ++indexedParameterCount [index % args.length]; 449 } 450 } 451 else 452 { 453 index = relativeParameterCount++; 454 hasRelative = true; 455 } 456 457 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 ) ) ); 458 checkState( !hasIndexed || !hasRelative, () -> new ValidationException( "cannot mix indexed and positional parameters" ) ); 459 460 addArgument( format, c, args [index] ); 461 462 m_FormatParts.add( "$" + c ); 463 } // ParseLoop: 464 465 if( hasRelative && (relativeParameterCount < args.length) ) 466 { 467 throw new ValidationException( "unused arguments: expected %s, received %s".formatted( relativeParameterCount, args.length ) ); 468 } 469 if( hasIndexed ) 470 { 471 final Collection<String> unused = IntStream.range( 0, args.length ) 472 .filter( i -> indexedParameterCount[i] == 0 ) 473 .mapToObj( i -> "$" + (i + 1) ) 474 .collect( toList() ); 475 if( !unused.isEmpty() ) 476 { 477 throw new ValidationException( "unused argument%s: %s".formatted( unused.size() == 1 ? "" : "s", String.join( ", ", unused ) ) ); 478 } 479 } 480 481 //---* Done *------------------------------------------------------ 482 return this; 483 } // addWithoutDebugInfo() 484 485 /** 486 * Returns the arguments. 487 * 488 * @return The arguments. 489 */ 490 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 491 public final List<Object> args() { return List.copyOf( m_Args ); } 492 493 /** 494 * Returns the given object literally. 495 * 496 * @param o The object. 497 * @return The literal. 498 */ 499 private static final Object argToLiteral( final Object o ) 500 { 501 final var retValue = nonNull( o ) ? o : NULL_REFERENCE; 502 503 //---* Done *------------------------------------------------------ 504 return retValue; 505 } // argToLiteral() 506 507 /** 508 * Translates the given object to a name. 509 * 510 * @param o The object. 511 * @return The name. 512 */ 513 private static final Object argToName( final Object o ) 514 { 515 final var retValue = switch( o ) 516 { 517 case final CharSequence charSequence -> charSequence.toString(); 518 case final ParameterSpec parameterSpec -> parameterSpec.name(); 519 case final FieldSpec fieldSpec -> fieldSpec.name(); 520 case final MethodSpec methodSpec -> methodSpec.name(); 521 case final TypeSpec typeSpec -> 522 /* 523 * Does not work for anonymous types, so no check for the name 524 * is required. 525 */ 526 //noinspection OptionalGetWithoutIsPresent 527 typeSpec.name().get(); 528 case null, default -> throw new IllegalArgumentException( "expected name but was " + o ); 529 }; 530 531 //---* Done *------------------------------------------------------ 532 return retValue; 533 } // argToName() 534 535 /** 536 * Translates the given object to a String. 537 * 538 * @param o The object. 539 * @return The resulting String, or 540 * {@link Util#NULL_REFERENCE} 541 * if the object is 542 * {@null}. 543 */ 544 private static final Object argToString( final Object o ) 545 { 546 final var retValue = isNull( o ) ? NULL_REFERENCE : Objects.toString( o ); 547 548 //---* Done *------------------------------------------------------ 549 return retValue; 550 } // argToString() 551 552 /** 553 * Translates the given object to a type. 554 * 555 * @param o The object. 556 * @return The resulting type. 557 */ 558 private static final TypeNameImpl argToType( final Object o ) 559 { 560 final var retValue = switch( o ) 561 { 562 case final TypeNameImpl typeName -> typeName; 563 case final TypeMirror typeMirror -> TypeNameImpl.from( typeMirror ); 564 case final Element element -> TypeNameImpl.from( element.asType() ); 565 case final Type type -> TypeNameImpl.from( type ); 566 case null, default -> throw new IllegalArgumentException( "expected type but was " + o ); 567 }; 568 569 //---* Done *------------------------------------------------------ 570 return retValue; 571 } // argToType() 572 573 /** 574 * {@inheritDoc} 575 */ 576 @API( status = INTERNAL, since = "0.0.5" ) 577 @Override 578 public final BuilderImpl beginControlFlow( final String controlFlow, final Object... args ) 579 { 580 addDebug(); 581 if( isNotEmptyOrBlank( requireNonNullArgument( controlFlow, "controlFlow" ) ) ) 582 { 583 addWithoutDebugInfo( controlFlow, args ); 584 if( !controlFlow.endsWith( "\n" ) ) addWithoutDebugInfo( " " ); 585 } 586 addWithoutDebugInfo( "{\n" ); 587 indent(); 588 589 //---* Done *------------------------------------------------------ 590 return this; 591 } // beginControlFlow() 592 593 /** 594 * {@inheritDoc} 595 */ 596 @Override 597 public final CodeBlockImpl build() { return new CodeBlockImpl( this ); } 598 599 /** 600 * {@inheritDoc} 601 */ 602 @Override 603 public final BuilderImpl endControlFlow() 604 { 605 addDebug(); 606 unindent(); 607 addWithoutDebugInfo( "}\n" ); 608 609 //---* Done *------------------------------------------------------ 610 return this; 611 } // endControlFlow() 612 613 /** 614 * {@inheritDoc} 615 */ 616 @API( status = INTERNAL, since = "0.0.5" ) 617 @Override 618 public final BuilderImpl endControlFlow( final String controlFlow, final Object... args ) 619 { 620 addDebug(); 621 unindent(); 622 addWithoutDebugInfo( "} " + requireNonNullArgument( controlFlow, "controlFlow" ) + ";\n", args ); 623 624 //---* Done *------------------------------------------------------ 625 return this; 626 } // endControlFlow() 627 628 /** 629 * Returns the format parts. 630 * 631 * @return The format parts. 632 */ 633 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 634 public final List<String> formatParts() { return List.copyOf( m_FormatParts ); } 635 636 /** 637 * {@inheritDoc} 638 */ 639 @Override 640 public final BuilderImpl indent() 641 { 642 m_FormatParts.add( "$>" ); 643 644 //---* Done *------------------------------------------------------ 645 return this; 646 } // indent() 647 648 /** 649 * {@inheritDoc} 650 */ 651 @Override 652 public final boolean isEmpty() { return m_FormatParts.isEmpty(); } 653 654 /** 655 * Checks whether the given placeholder character would expect an 656 * argument. 657 * 658 * @param placeholder The placeholder character. 659 * @return {@true} if there is no argument expected, 660 * {@false} otherwise. 661 */ 662 private static final boolean isNoArgPlaceholder( final char placeholder ) 663 { 664 final var retValue = IntStream.of( '$', '>', '<', '[', ']', 'W', 'Z' ) 665 .anyMatch( p -> p == placeholder ); 666 667 //---* Done *------------------------------------------------------ 668 return retValue; 669 } // isNoArgPlaceholder() 670 671 /** 672 * {@inheritDoc} 673 */ 674 @API( status = INTERNAL, since = "0.0.5" ) 675 @Override 676 public final BuilderImpl nextControlFlow( final String controlFlow, final Object... args ) 677 { 678 unindent(); 679 addWithoutDebugInfo( "}" ); 680 if( !requireNonNullArgument( controlFlow, "controlFlow" ).startsWith( "\n" ) ) addWithoutDebugInfo( " " ); 681 add( controlFlow, args ); 682 if( !controlFlow.endsWith( "\n" ) ) addWithoutDebugInfo(" " ); 683 addWithoutDebugInfo( "{\n" ); 684 indent(); 685 686 //---* Done *------------------------------------------------------ 687 return this; 688 } // nextControlFlow() 689 690 /** 691 * {@inheritDoc} 692 */ 693 @Override 694 public final BuilderImpl unindent() 695 { 696 m_FormatParts.add( "$<" ); 697 698 //---* Done *------------------------------------------------------ 699 return this; 700 } // unindent() 701 } 702 // class BuilderImpl 703 704 /** 705 * A helper class that supports to join code blocks. 706 * 707 * @author Thomas Thrien - thomas.thrien@tquadrat.org 708 * @version $Id: CodeBlockImpl.java 1258 2026-06-04 18:33:06Z tquadrat $ 709 * @since 0.0.5 710 * 711 * @UMLGraph.link 712 */ 713 @ClassVersion( sourceVersion = "$Id: CodeBlockImpl.java 1258 2026-06-04 18:33:06Z tquadrat $" ) 714 @API( status = INTERNAL, since = "0.0.5" ) 715 private static final class CodeBlockJoiner 716 { 717 /*------------*\ 718 ====** Attributes **=================================================== 719 \*------------*/ 720 /** 721 * The builder that is used to deliver the final code block. 722 */ 723 @SuppressWarnings( "UseOfConcreteClass" ) 724 private final BuilderImpl m_Builder; 725 726 /** 727 * The separator for the joined code blocks. 728 */ 729 private final String m_Delimiter; 730 731 /** 732 * Flag that indicates whether to add the delimiter on adding a new 733 * code block. 734 */ 735 private boolean m_First = true; 736 737 /*--------------*\ 738 ====** Constructors **================================================= 739 \*--------------*/ 740 /** 741 * <p>{@summary {@constructor}}</p> 742 * 743 * @param delimiter The separator for the joined code blocks. 744 * @param builder The builder that is used to deliver the final code 745 * block. 746 */ 747 public CodeBlockJoiner( final String delimiter, @SuppressWarnings( "UseOfConcreteClass" ) final BuilderImpl builder ) 748 { 749 m_Delimiter = requireNonNullArgument( delimiter, "delimiter" ); 750 m_Builder = requireNonNullArgument( builder, "builder" ); 751 } // CodeBlockJoiner() 752 753 /*---------*\ 754 ====** Methods **====================================================== 755 \*---------*/ 756 /** 757 * Adds another code block. 758 * 759 * @param codeBlock The new code block. 760 * @return This {@code CodeBlockJoiner} instance. 761 */ 762 @SuppressWarnings( {"TypeMayBeWeakened", "UnusedReturnValue"} ) 763 public final CodeBlockJoiner add( @SuppressWarnings( "UseOfConcreteClass" ) final CodeBlockImpl codeBlock ) 764 { 765 if( !m_First ) m_Builder.addWithoutDebugInfo( m_Delimiter ); 766 m_First = false; 767 768 m_Builder.add( codeBlock ); 769 770 //---* Done *------------------------------------------------------ 771 return this; 772 } // add() 773 774 /** 775 * Returns the new code block with the joined ones. 776 * 777 * @return The new code block. 778 */ 779 public final CodeBlockImpl join() { return m_Builder.build(); } 780 781 /** 782 * Merges this code block joiner with the given other one. 783 * 784 * @param other The other code block joiner. 785 * @return This {@code CodeBlockJoiner} instance. 786 */ 787 public final CodeBlockJoiner merge( @SuppressWarnings( "UseOfConcreteClass" ) final CodeBlockJoiner other ) 788 { 789 final var otherBlock = requireNonNullArgument( other, "other" ).m_Builder.build(); 790 if( !otherBlock.isEmpty() ) add( otherBlock ); 791 792 //---* Done *------------------------------------------------------ 793 return this; 794 } // merge() 795 } 796 // class CodeBlockJoiner 797 798 /*------------*\ 799 ====** Attributes **======================================================= 800 \*------------*/ 801 /** 802 * The arguments. 803 */ 804 private final List<Object> m_Args; 805 806 /** 807 * Lazily initialised return value of 808 * {@link #toString()} 809 * for this code block. 810 */ 811 private final Lazy<String> m_CachedString; 812 813 /** 814 * The reference to the factory. 815 */ 816 @SuppressWarnings( "UseOfConcreteClass" ) 817 private final JavaComposer m_Composer; 818 819 /** 820 * A heterogeneous list containing string literals and value placeholders. 821 */ 822 private final List<String> m_FormatParts; 823 824 /** 825 * The static imports. 826 */ 827 private final Set<String> m_StaticImports; 828 829 /*------------------------*\ 830 ====** Static Initialisations **=========================================== 831 \*------------------------*/ 832 /** 833 * The regular expression that is used to determine whether a parameter 834 * name starts with a lowercase character. 835 */ 836 public static final Pattern LOWERCASE; 837 838 /** 839 * The regular expression that is used to obtain the argument name from 840 * a format string. 841 */ 842 public static final Pattern NAMED_ARGUMENT; 843 844 static 845 { 846 try 847 { 848 LOWERCASE = Pattern.compile( "[a-z]+[\\w_]*" ); 849 NAMED_ARGUMENT = Pattern.compile( "\\$(?<argumentName>[\\w_]+):(?<typeChar>\\w).*" ); 850 } 851 catch( final PatternSyntaxException e ) 852 { 853 throw new ExceptionInInitializerError( e ); 854 } 855 } 856 857 /*--------------*\ 858 ====** Constructors **===================================================== 859 \*--------------*/ 860 /** 861 * <p>{@summary {@constructor}}</p> 862 * 863 * @param builder The builder for this instance. 864 */ 865 @SuppressWarnings( {"AccessingNonPublicFieldOfAnotherObject"} ) 866 public CodeBlockImpl( @SuppressWarnings( "UseOfConcreteClass" ) final BuilderImpl builder ) 867 { 868 m_Composer = builder.m_Composer; 869 m_FormatParts = builder.formatParts(); 870 m_Args = builder.args(); 871 m_StaticImports = Set.copyOf( builder.m_StaticImports ); 872 873 m_CachedString = Lazy.use( this::initialiseCachedString ); 874 } // CodeBlockImpl() 875 876 /*---------*\ 877 ====** Methods **========================================================== 878 \*---------*/ 879 /** 880 * Returns the arguments. 881 * 882 * @return The arguments. 883 */ 884 /* 885 * Originally, this was the reference to the internal collection! 886 */ 887 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 888 public final List<Object> args() { return List.copyOf( m_Args ); } 889 890 /** 891 * {@inheritDoc} 892 */ 893 @Override 894 public final boolean equals( final Object o ) 895 { 896 var retValue = this == o; 897 if( !retValue && (o instanceof final CodeBlockImpl other) ) 898 { 899 retValue = m_Composer.equals( other.m_Composer ) && toString().equals( o.toString() ); 900 } 901 902 //---* Done *---------------------------------------------------------- 903 return retValue; 904 } // equals() 905 906 /** 907 * Returns the format parts from this code block. 908 * 909 * @return The format parts. 910 */ 911 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 912 public final List<String> formatParts() { return List.copyOf( m_FormatParts ); } 913 914 /** 915 * Returns the 916 * {@link JavaComposer} 917 * factory. 918 * 919 * @return The reference to the factory. 920 */ 921 @SuppressWarnings( {"PublicMethodNotExposedInInterface"} ) 922 public final JavaComposer getFactory() { return m_Composer; } 923 924 /** 925 * Returns the static imports for this code block. 926 * 927 * @return The static imports. 928 */ 929 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 930 @API( status = INTERNAL, since = "0.2.0" ) 931 public final Set<String> getStaticImports() { return m_StaticImports; } 932 933 /** 934 * {@inheritDoc} 935 */ 936 @Override 937 public final int hashCode() { return toString().hashCode(); } 938 939 /** 940 * <p>{@summary The initialiser for 941 * {@link #m_CachedString}.}</p> 942 * 943 * @return The return value for 944 * {@link #toString()}. 945 */ 946 private final String initialiseCachedString() 947 { 948 final var resultBuilder = new StringBuilder(); 949 final var codeWriter = new CodeWriter( m_Composer, resultBuilder ); 950 try 951 { 952 codeWriter.emit( this ); 953 } 954 catch( final UncheckedIOException e ) 955 { 956 throw new UnlikelyExceptionError( e.getCause() ); 957 } 958 final var retValue = resultBuilder.toString(); 959 960 //---* Done *---------------------------------------------------------- 961 return retValue; 962 } // initialiseCachedString() 963 964 /** 965 * {@inheritDoc} 966 */ 967 @Override 968 public final boolean isEmpty() { return m_FormatParts.isEmpty(); } 969 970 /** 971 * {@inheritDoc} 972 */ 973 @Override 974 public final CodeBlock join( final String separator, final CodeBlock... codeBlocks ) 975 { 976 final var retValue = makeCodeBlockStream( this, requireNonNullArgument( codeBlocks, "codeBlocks" ) ) 977 .collect( joining( separator ) ); 978 979 //---* Done *---------------------------------------------------------- 980 return retValue; 981 } // join() 982 983 /** 984 * <p>{@summary Joins this code block with the given code blocks into a 985 * single new {@code CodeBlock} instance, each separated by the given 986 * separator.} The given prefix will be prepended to the new 987 * {@code CodeBloc}, and the given suffix will be appended to it.</p> 988 * <p>For example, joining "{@code String s}", 989 * "{@code Object o}" and "{@code int i}" using 990 * "{@code , }" as the separator would produce 991 * "{@code String s, Object o, int i}".</p> 992 * 993 * @param separator The separator. 994 * @param prefix The prefix. 995 * @param suffix The suffix. 996 * @param codeBlocks The code blocks to join. 997 * @return The new code block. 998 */ 999 @Override 1000 public final CodeBlock join( final String separator, final String prefix, final String suffix, final CodeBlock... codeBlocks ) 1001 { 1002 final var retValue = makeCodeBlockStream( this, requireNonNullArgument( codeBlocks, "codeBlocks" ) ) 1003 .collect( joining( separator, prefix, suffix ) ); 1004 1005 //---* Done *---------------------------------------------------------- 1006 return retValue; 1007 } // join() 1008 1009 /** 1010 * <p>{@summary A 1011 * {@link Collector} 1012 * implementation that joins {@code CodeBlock} instances together into one 1013 * new code block, separated by the given separator.}</p> 1014 * <p>For example, joining "{@code String s}", 1015 * "{@code Object o}" and "{@code int i}" using 1016 * "{@code , }" as the separator would produce 1017 * "{@code String s, Object o, int i}".</p> 1018 * 1019 * @param separator The separator. 1020 * @return The new collector. 1021 */ 1022 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 1023 public final Collector<CodeBlockImpl,?,CodeBlockImpl> joining( final String separator ) 1024 { 1025 final Collector<CodeBlockImpl,?,CodeBlockImpl> retValue = Collector.of 1026 ( 1027 () -> new CodeBlockJoiner( separator, new BuilderImpl( m_Composer ) ), CodeBlockJoiner::add, CodeBlockJoiner::merge, CodeBlockJoiner::join 1028 ); 1029 1030 //---* Done *---------------------------------------------------------- 1031 return retValue; 1032 } // joining() 1033 1034 /** 1035 * <p>{@summary A 1036 * {@link Collector} 1037 * implementation that joins {@code CodeBlock} instances together into one 1038 * new code block, separated by the given separator.} The given prefix 1039 * will be prepended to the new {@code CodeBloc}, and the given suffix 1040 * will be appended to it.</p> 1041 * <p>For example, joining "{@code String s}", 1042 * "{@code Object o}" and "{@code int i}" using 1043 * "{@code , }" as the separator, and 1044 * "{@code int func( }" as the prefix and " {@code )}" 1045 * as the suffix respectively would produce 1046 * "{@code int func( String s, Object o, int i )}".</p> 1047 * 1048 * @param separator The separator. 1049 * @param prefix The prefix. 1050 * @param suffix The suffix. 1051 * @return The new collector. 1052 */ 1053 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 1054 public final Collector<CodeBlockImpl,?,CodeBlockImpl> joining( final String separator, final String prefix, final String suffix ) 1055 { 1056 final var builder = new BuilderImpl( m_Composer ); 1057 builder.add( "$N", prefix ); 1058 final Collector<CodeBlockImpl,?,CodeBlockImpl> retValue = Collector.of 1059 ( 1060 () -> new CodeBlockJoiner( separator, builder ), CodeBlockJoiner::add, CodeBlockJoiner::merge, joiner -> 1061 { 1062 builder.add( m_Composer.codeBlockOf( "$N", suffix ) ); 1063 return joiner.join(); 1064 } 1065 ); 1066 1067 //---* Done *---------------------------------------------------------- 1068 return retValue; 1069 } // joining() 1070 1071 /** 1072 * Composes a stream from the given {@code CodeBlock} instances. 1073 * 1074 * @param head The first code block. 1075 * @param tail The other code blocks. 1076 * @return The 1077 * {@link Stream} 1078 * instance with the {@code CodeBlock} instances. 1079 */ 1080 private static final Stream<CodeBlockImpl> makeCodeBlockStream( final CodeBlock head, final CodeBlock... tail ) 1081 { 1082 final var builder = Stream.<CodeBlockImpl>builder(); 1083 builder.add( (CodeBlockImpl) requireNonNullArgument( head, "head" ) ); 1084 for( final var block : requireNonNullArgument( tail, "tail" ) ) 1085 { 1086 builder.add( (CodeBlockImpl) block ); 1087 } 1088 final var retValue = builder.build(); 1089 1090 //---* Done *---------------------------------------------------------- 1091 return retValue; 1092 } // makeCodeBlockStream() 1093 1094 /** 1095 * Creates a new builder that is initialised with the components of this 1096 * code block. 1097 * 1098 * @return The new builder. 1099 */ 1100 @SuppressWarnings( {"AccessingNonPublicFieldOfAnotherObject"} ) 1101 @Override 1102 public final BuilderImpl toBuilder() 1103 { 1104 final var retValue = new BuilderImpl( m_Composer, m_FormatParts, m_Args ); 1105 retValue.m_StaticImports.addAll( m_StaticImports ); 1106 1107 //---* Done *---------------------------------------------------------- 1108 return retValue; 1109 } // toBuilder() 1110 1111 /** 1112 * {@inheritDoc} 1113 */ 1114 @Override 1115 public final String toString() { return m_CachedString.get(); } 1116} 1117// class CodeBlockImpl 1118 1119/* 1120 * End of File 1121 */