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 &quot;{@code String s}&quot;,
986     * &quot;{@code Object o}&quot; and &quot;{@code int i}&quot; using
987     * &quot;{@code , }&quot; as the separator would produce
988     * &quot;{@code String s, Object o, int i}&quot;.</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 &quot;{@code String s}&quot;,
1012     *  &quot;{@code Object o}&quot; and &quot;{@code int i}&quot; using
1013     *  &quot;{@code , }&quot; as the separator would produce
1014     *  &quot;{@code String s, Object o, int i}&quot;.</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 &quot;{@code String s}&quot;,
1039     *  &quot;{@code Object o}&quot; and &quot;{@code int i}&quot; using
1040     *  &quot;{@code , }&quot; as the separator, and
1041     *  &quot;{@code int func( }&quot; as the prefix and &quot; {@code )}&quot;
1042     *  as the suffix respectively would produce
1043     *  &quot;{@code int func( String s, Object o, int i )}&quot;.</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 */