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