001/*
002 * ============================================================================
003 *  Copyright © 2002-2026 by Thomas Thrien.
004 *  All Rights Reserved.
005 * ============================================================================
006 *  Licensed to the public under the agreements of the GNU Lesser General Public
007 *  License, version 3.0 (the "License"). You may obtain a copy of the License at
008 *
009 *       http://www.gnu.org/licenses/lgpl.html
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 *  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 *  License for the specific language governing permissions and limitations
015 *  under the License.
016 */
017
018package org.tquadrat.foundation.util;
019
020import static java.lang.String.format;
021import static java.lang.System.getProperties;
022import static java.lang.System.getenv;
023import static java.util.Arrays.asList;
024import static java.util.regex.Pattern.compile;
025import static org.apiguardian.api.API.Status.INTERNAL;
026import static org.apiguardian.api.API.Status.STABLE;
027import static org.tquadrat.foundation.lang.Objects.isNull;
028import static org.tquadrat.foundation.lang.Objects.nonNull;
029import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
030import static org.tquadrat.foundation.util.StringUtils.isNotEmpty;
031import static org.tquadrat.foundation.util.StringUtils.isNotEmptyOrBlank;
032import static org.tquadrat.foundation.util.SystemUtils.determineIPAddress;
033import static org.tquadrat.foundation.util.SystemUtils.getMACAddress;
034import static org.tquadrat.foundation.util.SystemUtils.getNodeId;
035import static org.tquadrat.foundation.util.SystemUtils.getPID;
036
037import java.io.Serial;
038import java.io.Serializable;
039import java.net.SocketException;
040import java.time.Instant;
041import java.util.Collection;
042import java.util.Formattable;
043import java.util.Formatter;
044import java.util.HashMap;
045import java.util.HashSet;
046import java.util.LinkedList;
047import java.util.Map;
048import java.util.Optional;
049import java.util.SequencedCollection;
050import java.util.Set;
051import java.util.function.Function;
052import java.util.regex.Pattern;
053import java.util.regex.PatternSyntaxException;
054
055import org.apiguardian.api.API;
056import org.tquadrat.foundation.annotation.ClassVersion;
057import org.tquadrat.foundation.annotation.MountPoint;
058import org.tquadrat.foundation.exception.ImpossibleExceptionError;
059import org.tquadrat.foundation.lang.StringConverter;
060
061/**
062 *  <p>{@summary An instance of this class is basically a wrapper around a
063 *  String that contains placeholders (&quot;Variables&quot;) in the form
064 *  <code>${&lt;<i>name</i>&gt;}</code>, where &lt;<i>name</i> is the variable
065 *  name.}</p>
066 *  <p>The variables names are case-sensitive.</p>
067 *  <p>Valid variable names may not contain other characters than the letters
068 *  from 'a' to 'z' (upper case and lower case), the digits from '0' to '9' and
069 *  the special characters underscore ('_') and dot ('.'), after an optional
070 *  prefix character.</p>
071 *  <p>Allowed prefixes are the tilde ('~'), the slash ('/'), the equal sign
072 *  ('='), the colon (':'), the per cent sign ('%'), and the ampersand
073 *  ('&amp;').</p>
074 *  <p>The prefix character is part of the name.</p>
075 *  <p>Finally, there is the single underscore that is allowed as a special
076 *  variable.</p>
077 *  <p>When the system data is added as source (see
078 *  {@link #replaceVariableFromSystemData(CharSequence, Map[]) replaceVariableFromSystemData()}
079 *  and
080 *  {@link #replaceVariable(boolean, Map[]) replaceVariable()}
081 *  with {@code addSystemData} set to {@code true}), some additional variables
082 *  are available:</p>
083 *  <dl>
084 *      <dt>{@value #VARNAME_IPAddress}</dt>
085 *      <dd>One of the outbound IP addresses of the machine that executes the
086 *      current program, if network is configured at all.</dd>
087 *      <dt>{@value #VARNAME_MACAddress}</dt>
088 *      <dd>The MAC address of the machine that executes the current program;
089 *      if no network is configured, a dummy address is used.</dd>
090 *      <dt>{@value #VARNAME_NodeId}</dt>
091 *      <dd>The node id of the machines that executes the current program; if
092 *      no network is configured, a pseudo node id is used.</dd>
093 *      <dt>{@value #VARNAME_Now}</dt>
094 *      <dd>The current data and time in UTC time zone.</dd>
095 *      <dt>{@value #VARNAME_pid}</dt>
096 *      <dd>The process id of the current program.</dd>
097 *  </dl>
098 *
099 *  @see #VARNAME_IPAddress
100 *  @see #VARNAME_MACAddress
101 *  @see #VARNAME_NodeId
102 *  @see #VARNAME_Now
103 *  @see #VARNAME_pid
104 *
105 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
106 *  @version $Id: Template.java 1251 2026-05-25 20:08:13Z tquadrat $
107 *
108 *  @UMLGraph.link
109 *  @since 0.1.0
110 */
111@SuppressWarnings( "ClassWithTooManyMethods" )
112@ClassVersion( sourceVersion = "$Id: Template.java 1251 2026-05-25 20:08:13Z tquadrat $" )
113@API( status = STABLE, since = "0.1.0" )
114public class Template implements Serializable
115{
116        /*-----------*\
117    ====** Constants **========================================================
118        \*-----------*/
119    /**
120     *  The variable name for the IP address of the executing machine:
121     *  {@value}.
122     *
123     *  @since 0.1.0
124     */
125    @API( status = STABLE, since = "0.1.0" )
126    public static final String VARNAME_IPAddress = "org.tquadrat.ipaddress";
127
128    /**
129     *  The variable name for the MAC address of the executing machine:
130     *  {@value}.
131     *
132     *  @since 0.1.0
133     */
134    @API( status = STABLE, since = "0.1.0" )
135    public static final String VARNAME_MACAddress = "org.tquadrat.macaddress";
136
137    /**
138     *  The variable name for the node id of the executing machine: {@value}.
139     *
140     *  @since 0.1.0
141     */
142    @API( status = STABLE, since = "0.1.0" )
143    public static final String VARNAME_NodeId = "org.tquadrat.nodeid";
144
145    /**
146     *  The variable name for the current date and time: {@value}.
147     *
148     * @see Instant#now()
149     *
150     *  @since 0.1.0
151     */
152    @API( status = STABLE, since = "0.1.0" )
153    public static final String VARNAME_Now = "org.tquadrat.now";
154
155    /**
156     *  The variable name for the id of the current process, executing this
157     *  program: {@value}.
158     *
159     *  @since 0.1.0
160     */
161    @API( status = STABLE, since = "0.1.0" )
162    public static final String VARNAME_pid = "org.tquadrat.pid";
163
164    /**
165     *  The regular expression to identify a variable in a char sequence:
166     *  {@value}.
167     *
168     *  @see #findVariables(CharSequence)
169     *  @see #findVariables()
170     *  @see #isValidVariableName(CharSequence)
171     *  @see #replaceVariable(CharSequence,Map...)
172     *  @see #replaceVariable(Map...)
173     *  @see #replaceVariable(CharSequence, Function)
174     *  @see #replaceVariable(Function)
175     *
176     *  @since 0.1.0
177     */
178    @SuppressWarnings( "RegExpUnnecessaryNonCapturingGroup" )
179    @API( status = STABLE, since = "0.1.0" )
180    public static final String VARIABLE_PATTERN = "\\$\\{((?:_)|(?:[~/=%:&]?\\p{IsAlphabetic}(?:\\p{IsAlphabetic}|\\d|_|.)*?))}";
181
182    /**
183     *  <p>{@summary The template for variables: {@value}.} The argument is the
184     *  name of the variable itself; after an optional prefix character, it may
185     *  not contain other characters than the letters from 'a' to 'z' (upper
186     *  case and lower case), the digits from '0' to '9' and the special
187     *  characters underscore ('_') and dot ('.').</p>
188     *  <p>Allowed prefixes are the tilde ('~'), the slash ('/'), the equal
189     *  sign ('='), the colon (':'), the per cent sign ('%'), and the ampersand
190     *  ('&amp;').</p>
191     *  <p>The prefix character is part of the name.</p>
192     *  <p>Finally, there is the single underscore that is allowed as a
193     *  special variable.</p>
194     *
195     *  @see #VARIABLE_PATTERN
196     *
197     *  @since 0.1.0
198     */
199    @API( status = STABLE, since = "0.1.0" )
200    public static final String VARIABLE_TEMPLATE = "${%1$s}";
201
202        /*------------*\
203    ====** Attributes **=======================================================
204        \*------------*/
205    /**
206     *  The template text.
207     *
208     *  @serial
209     */
210    private final String m_TemplateText;
211
212    /**
213     *  The additional
214     *  {@link StringConverter}
215     *  implementations.
216     *
217     *  @serial
218     */
219    private final Map<Class<?>,StringConverter<?>> m_StringConverters = new HashMap<>();
220
221        /*------------------------*\
222    ====** Static Initialisations **===========================================
223        \*------------------------*/
224    /**
225     *  The pattern that is used to identify a variable in a char sequence.
226     *
227     *  @see #replaceVariable(CharSequence, Map...)
228     *  @see #replaceVariable(Map...)
229     *  @see #findVariables(CharSequence)
230     *  @see #findVariables()
231     *  @see #VARIABLE_PATTERN
232     */
233    private static final Pattern m_VariablePattern;
234
235    /**
236     *  The serial version UID for objects of this class: {@value}.
237     *
238     *  @hidden
239     */
240    @Serial
241    private static final long serialVersionUID = 1L;
242
243    static
244    {
245        //---* The regex patterns *--------------------------------------------
246        try
247        {
248            m_VariablePattern = compile( VARIABLE_PATTERN );
249        }
250        catch( final PatternSyntaxException e )
251        {
252            throw new ImpossibleExceptionError( "The patterns are constant values that have been tested", e );
253        }
254    }
255
256        /*--------------*\
257    ====** Constructors **=====================================================
258        \*--------------*/
259    /**
260     *  Creates a new instance of {@code Template}.
261     *
262     *  @param  templateText    The template text, containing variable in the
263     *      form <code>${&lt;<i>name</i>&gt;}</code>.
264     */
265    public Template( final CharSequence templateText )
266    {
267        m_TemplateText = requireNonNullArgument( templateText, "templateText" ).toString();
268    }   //  Template()
269
270        /*---------*\
271    ====** Methods **==========================================================
272        \*---------*/
273    /**
274     *  <p>{@summary The mount point for template manipulations in derived
275     *  classes.}</p>
276     *  <p>The default implementation will just return the argument.</p>
277     *
278     *  @param  templateText    The template text, as it was given to the
279     *      constructor on creation of the object instance.
280     *  @return The adjusted template text.
281     */
282    @SuppressWarnings( "static-method" )
283    @MountPoint
284    protected String adjustTemplate( final String templateText )
285    {
286        @SuppressWarnings( "UnnecessaryLocalVariable" )
287        final var retValue = templateText;
288
289        //---* Done *----------------------------------------------------------
290        return retValue;
291    }   //  adjustTemplate()
292
293    /**
294     *  <p>{@summary Applies the given
295     *  {@link StringConverter}
296     *  to the given value.}</p>
297     *  <p>This method is required to resolve an issue with the not so
298     *  compatible generics.</p>
299     *
300     *  @param  valueClass  The class of the value.
301     *  @param  stringConverter The {@code StringConverter} to use.
302     *  @param  value   The value to convert to a String.
303     *  @return The resulting String value.
304     *
305     *  @since 2.25.4
306     */
307    @SuppressWarnings( "rawtypes" )
308    private final String applyStringConverter( final Class valueClass, final StringConverter stringConverter, final Object value )
309    {
310        @SuppressWarnings( "unchecked" )
311        final var retValue = stringConverter.toString( valueClass.cast( value ) );
312
313        //---* Done *----------------------------------------------------------
314        return retValue;
315    }   //  applyStringConverter()
316
317    /**
318     *  Builds the source map with the additional data.
319     *
320     *  @return The source map.
321     *
322     *  @see #VARNAME_IPAddress
323     *  @see #VARNAME_MACAddress
324     *  @see #VARNAME_NodeId
325     *  @see #VARNAME_Now
326     *  @see #VARNAME_pid
327     */
328    private static final Map<String,Object> createAdditionalSource()
329    {
330        final Map<String,Object> retValue = new HashMap<>(
331            Map.of(
332                VARNAME_MACAddress, getMACAddress(),
333                VARNAME_pid, Long.valueOf( getPID() ),
334                VARNAME_NodeId, Long.valueOf( getNodeId() ),
335                VARNAME_Now, Instant.now() )
336        );
337        try
338        {
339            determineIPAddress().ifPresent( inetAddress -> retValue.put( VARNAME_IPAddress, inetAddress ) );
340        }
341        catch( final SocketException ignored ) { /* Deliberately ignored */ }
342
343        //---* Done *----------------------------------------------------------
344        return retValue;
345    }   //  createAdditionalSource()
346
347    /**
348     *  Escapes backslash ('\') and dollar sign ('$') for regex replacements.
349     *
350     *  @param  input   The source string.
351     *  @return The string with the escaped characters.
352     *
353     *  @see java.util.regex.Matcher#appendReplacement(StringBuffer,String)
354     *
355     *  @since 0.1.0
356     */
357    @API( status = INTERNAL, since = "0.1.0" )
358    private static String escapeRegexReplacement( final CharSequence input )
359    {
360        assert nonNull( input ) : "input is null";
361
362        //---* Escape the backslashes and dollar signs *-------------------
363        final var len = input.length();
364        final var buffer = new StringBuilder( (len * 12) / 10 );
365        char c;
366        EscapeLoop: for( var i = 0; i < len; ++i )
367        {
368            c = input.charAt( i );
369            switch( c )
370            {
371                case '\\':
372                case '$':
373                    buffer.append( '\\' ); // The fall through is intended here!
374                    //$FALL-THROUGH$
375                default: // Do nothing ...
376            }
377            buffer.append( c );
378        }   //  EscapeLoop:
379
380        final var retValue = buffer.toString();
381
382        //---* Done *----------------------------------------------------------
383        return retValue;
384    }   //  escapeRegexReplacement()
385
386    /**
387     *  <p>{@summary Collects all the variables of the form
388     *  <code>${<i>&lt;name&gt;</i>}</code> in the given String.}</p>
389     *  <p>If there are not any variables in the given String, an empty
390     *  {@link Set}
391     *  will be returned.</p>
392     *  <p>A valid variable name may not contain any other characters than the
393     *  letters from 'a' to 'z' (upper case and lower case), the digits from
394     *  '0' to '9' and the special characters underscore ('_') and dot ('.'),
395     *  after an optional prefix character.</p>
396     *  <p>Allowed prefixes are the tilde ('~'), the slash ('/'), the equal
397     *  sign ('='), the colon (':'), the per cent sign ('%'), and the ampersand
398     *  ('&amp;').</p>
399     *  <p>Finally, there is the single underscore that is allowed as a
400     *  special variable.</p>
401     *
402     *  @param  text    The text with the variables; may be {@code null}.
403     *  @return A {@code Collection} with the variable (names).
404     *
405     *  @see #VARIABLE_PATTERN
406     *
407     *  @since 0.0.5
408     */
409    @API( status = STABLE, since = "0.0.5" )
410    public static final Set<String> findVariables( final CharSequence text )
411    {
412        final Collection<String> buffer = new HashSet<>();
413        if( nonNull( text ) )
414        {
415            final var matcher = m_VariablePattern.matcher( text );
416            while( matcher.find() )
417            {
418                final var foundVariable = matcher.group( 1 );
419                buffer.add( foundVariable );
420            }
421        }
422        final var retValue = Set.copyOf( buffer );
423
424        //---* Done *----------------------------------------------------------
425        return retValue;
426    }   //  findVariables()
427
428    /**
429     *  <p>{@summary Collects all the variables of the form
430     *  <code>${<i>&lt;name&gt;</i>}</code> in the adjusted template.}</p>
431     *  <p>If there are not any variables in there, an empty
432     *  {@link Collection}
433     *  will be returned.</p>
434     *  <p>A valid variable name may not contain any other characters than the
435     *  letters from 'a' to 'z' (upper case and lower case), the digits from
436     *  '0' to '9' and the special characters underscore ('_') and dot ('.'),
437     *  after an optional prefix character.</p>
438     *  <p>Allowed prefixes are the tilde ('~'), the slash ('/'), the equal
439     *  sign ('='), the colon (':'), the per cent sign ('%'), and the ampersand
440     *  ('&amp;').</p>
441     *  <p>Finally, there is the single underscore that is allowed as a
442     *  special variable.</p>
443     *
444     *  @return A {@code Collection} with the variable (names).
445     *
446     *  @see #VARIABLE_PATTERN
447     */
448    public final Set<String> findVariables()
449    {
450        final var retValue = findVariables( getTemplateText() );
451
452        //---* Done *----------------------------------------------------------
453        return retValue;
454    }   //  findVariables()
455
456    /**
457     *  <p>{@summary Mountpoint for the formatting of the result after the
458     *  variables have been replaced.}</p>
459     *  <p>The default implementation just returns the result.</p>
460     *
461     *  @param  text    The result from replacing the variables in the template
462     *      text.
463     *  @return The reformatted result.
464     */
465    @SuppressWarnings( "static-method" )
466    @MountPoint
467    protected String formatResult( final String text )
468    {
469        @SuppressWarnings( "UnnecessaryLocalVariable" )
470        final var retValue = text;
471
472        //---* Done *----------------------------------------------------------
473        return retValue;
474    }   //  formatResult()
475
476    /**
477     *  Returns the template text after it has been processed by
478     *  {@link #adjustTemplate(String)}.
479     *
480     *  @return The adjusted template text.
481     */
482    protected final String getTemplateText() { return adjustTemplate( m_TemplateText ); }
483
484    /**
485     *  Checks whether the adjusted template contains the variable of
486     *  the form <code>${<i>&lt;name&gt;</i>}</code> (matching the pattern
487     *  given in
488     *  {@link #VARIABLE_PATTERN})
489     *  with the given name.
490     *
491     *  @param  name    The name of the variable to look for.
492     *  @return {@code true} if the template contains the variable,
493     *      {@code false} otherwise.
494     *  @throws IllegalArgumentException    The given argument is not valid as
495     *      a variable name.
496     *
497     *  @see #VARIABLE_PATTERN
498     */
499    public final boolean hasVariable( final String name )
500    {
501        if( !isValidVariableName( name ) )
502            throw new IllegalArgumentException( "%s is not a valid variable name".formatted( name ) );
503
504        final var retValue = findVariables().contains( name );
505
506        //---* Done *----------------------------------------------------------
507        return retValue;
508    }   //  hasVariable()
509
510    /**
511     *  Checks whether the given String contains at least one variable of the
512     *  form <code>${<i>&lt;name&gt;</i>}</code> (matching the pattern given in
513     *  {@link #VARIABLE_PATTERN}).
514     *
515     *  @param  input   The String to test; can be {@code null}.
516     *  @return {@code true} if the String contains at least one variable,
517     *      {@code false} otherwise.
518     *
519     *  @see #VARIABLE_PATTERN
520     *
521     *  @since 0.1.0
522     */
523    @API( status = STABLE, since = "0.1.0" )
524    public static final boolean hasVariables( final CharSequence input )
525    {
526        final var retValue = isNotEmptyOrBlank( input ) && m_VariablePattern.matcher( input ).find();
527
528        //---* Done *----------------------------------------------------------
529        return retValue;
530    }   //  hasVariables()
531
532    /**
533     *  Checks whether the adjusted template contains at least one variable of
534     *  the form <code>${<i>&lt;name&gt;</i>}</code> (matching the pattern
535     *  given in
536     *  {@link #VARIABLE_PATTERN}).
537     *
538     *  @return {@code true} if the template contains at least one variable,
539     *      {@code false} otherwise.
540     *
541     *  @see #VARIABLE_PATTERN
542     */
543    public final boolean hasVariables() { return hasVariables( getTemplateText() ); }
544
545    /**
546     *  Test whether the given String is a valid variable name.
547     *
548     *  @param  name    The bare variable name, without the surrounding
549     *      &quot;${&hellip;}&quot;.
550     *  @return {@code true} if the given name is valid for a variable name,
551     *      {@code false} otherwise.
552     *
553     *  @see #VARIABLE_PATTERN
554     *  @see #findVariables(CharSequence)
555     *  @see #replaceVariable(CharSequence, Map...)
556     *
557     *  @since 0.1.0
558     */
559    @API( status = STABLE, since = "0.1.0" )
560    public static final boolean isValidVariableName( final CharSequence name )
561    {
562        var retValue = isNotEmptyOrBlank( requireNonNullArgument( name, "name" ) );
563        if( retValue )
564        {
565            final var text = format( VARIABLE_TEMPLATE, name );
566            retValue = m_VariablePattern.matcher( text ).matches();
567        }
568
569        //---* Done *----------------------------------------------------------
570        return retValue;
571    }   //  isValidVariableName()
572
573    /**
574     *  Checks whether the given String is a variable in the form
575     *  <code>${<i>&lt;name&gt;</i>}</code>, according to the pattern provided
576     *  in
577     *  {@link #VARIABLE_PATTERN}.
578     *
579     *  @param  input   The String to test; can be {@code null}.
580     *  @return {@code true} if the given String is not {@code null}, not the
581     *      empty String, and it matches the given pattern, {@code false}
582     *      otherwise.
583     *
584     *  @since 0.1.0
585     */
586    @API( status = STABLE, since = "0.1.0" )
587    public static final boolean isVariable( final CharSequence input )
588    {
589        final var retValue = isNotEmptyOrBlank( input ) && m_VariablePattern.matcher( input ).matches();
590
591        //---* Done *----------------------------------------------------------
592        return retValue;
593    }   //  isVariable()
594
595    /**
596     *  <p>{@summary Registers an additional
597     *  {@link StringConverter}
598     *  that is used to convert the replacement value to a String.}</p>
599     *  <p>The additional {@code StringConverter}s will be tried first before
600     *  the system provided implementations are applied.</p>
601     *
602     *  @param  <T> The type of the subject class.
603     *  @param  subjectClass    The class of the objects that are handled by
604     *      the given {@code StringConverter}.
605     *  @param  stringConverter The instance of {@code StringConverter} that
606     *      does the conversion.
607     *
608     *  @since 0.25.4
609     */
610    @API( status = STABLE, since = "0.25.4" )
611    public final <T> void registerStringConverter( final Class<T> subjectClass, final StringConverter<T> stringConverter )
612    {
613        m_StringConverters.put( requireNonNullArgument( subjectClass, "subjectClass" ), requireNonNullArgument( stringConverter, "stringConverter" ) );
614    }   //  registerStringConverter()
615
616    /**
617     *  <p>{@summary Replaces the variables of the form
618     *  <code>${&lt;<i>name</i>&gt;}</code> in the given String with values
619     *  from the given maps.} The method will try the maps in the given
620     *  sequence, it stops after the first match.</p>
621     *  <p>If no replacement value could be found, the variable will not be
622     *  replaced at all.</p>
623     *  <p>If a value from one of the maps contains a variable itself, this
624     *  will not be replaced.</p>
625     *  <p>The variables names are case-sensitive.</p>
626     *  <p>Valid variable names may not contain other characters than the
627     *  letters from 'a' to 'z' (upper case and lower case), the digits from
628     *  '0' to '9' and the special characters underscore ('_') and dot ('.'),
629     *  after an optional prefix character.</p>
630     *  <p>Allowed prefixes are the tilde ('~'), the slash ('/'), the equal
631     *  sign ('='), the colon (':'), the per cent sign ('%'), and the ampersand
632     *  ('&amp;').</p>
633     *  <p>The prefix character is part of the name.</p>
634     *  <p>Finally, there is the single underscore that is allowed as a
635     *  special variable.</p>
636     *
637     *  @param  text    The text with the variables; may be {@code null}.
638     *  @param  sources The maps with the replacement values.
639     *  @return The new text, or {@code null} if the provided value for
640     *      {@code text} was already {@code null}.
641     *
642     *  @see #VARIABLE_PATTERN
643     *
644     *  @since 0.1.0
645     */
646    @SuppressWarnings( "TypeParameterExplicitlyExtendsObject" )
647    @SafeVarargs
648    @API( status = STABLE, since = "0.1.0" )
649    public static final String replaceVariable( final CharSequence text, final Map<String,? extends Object>... sources )
650    {
651        String retValue = null;
652        if( nonNull( text ) )
653        {
654            final var effectiveSources = asList( requireNonNullArgument( sources, "sources" ) );
655            final var template = new Template( text );
656            retValue = template.replaceVariable( variable -> template.retrieveVariableValue( variable, effectiveSources ) );
657        }
658
659        //---* Done *----------------------------------------------------------
660        return retValue;
661    }   //  replaceVariable()
662
663    /**
664     *  <p>{@summary Replaces the variables of the form
665     *  <code>${&lt;<i>name</i>&gt;}</code> in the adjusted template with the
666     *  String representations of values from the given maps and returns the
667     *  result after formatting the updated contents.} The method will try the
668     *  maps in the given sequence, it stops after the first match.</p>
669     *  <p>The found values will be converted to Strings by using the
670     *  {@link StringConverter}
671     *  that was registered for the class of the value. If the class implements
672     *  {@link Formattable},
673     *  {@link Formattable#formatTo(Formatter,int,int,int) formatTo(Formatter, 0, -1, -1)}
674     *  will be called. If there is no matching {@code StringConverter}, the
675     *  value will be converted by calling
676     *  {@link org.tquadrat.foundation.lang.Objects#toString(Object)}
677     *  with the value as argument.</p>
678     *  <p>If no replacement value could be found, the variable will not be
679     *  replaced at all.</p>
680     *  <p>If a value from one of the maps contains a variable itself, this
681     *  will not be replaced.</p>
682     *  <p>The variables names are case-sensitive.</p>
683     *  <p>Valid variable names may not contain other characters than the
684     *  letters from 'a' to 'z' (upper case and lower case), the digits from
685     *  '0' to '9' and the special characters underscore ('_') and dot ('.'),
686     *  after an optional prefix character.</p>
687     *  <p>Allowed prefixes are the tilde ('~'), the slash ('/'), the equal
688     *  sign ('='), the colon (':'), the per cent sign ('%'), and the ampersand
689     *  ('&amp;').</p>
690     *  <p>The prefix character is part of the name.</p>
691     *  <p>Finally, there is the single underscore that is allowed as a
692     *  special variable.</p>
693     *
694     *  @param  sources The maps with the replacement values.
695     *  @return The new text, or {@code null} if the provided value for
696     *      {@code text} was already {@code null}.
697     *
698     *  @see #VARIABLE_PATTERN
699     */
700    @SuppressWarnings( "TypeParameterExplicitlyExtendsObject" )
701    @SafeVarargs
702    public final String replaceVariable( final Map<String,? extends Object>... sources )
703    {
704        return replaceVariable( false, sources );
705    }   //  replaceVariable()
706
707    /**
708     *  <p>{@summary Replaces the variables of the form
709     *  <code>${&lt;<i>name</i>&gt;}</code> in the adjusted template with the
710     *  String reprensentations of the values from the given maps and returns
711     *  the result after formatting the updated contents.}
712     *  The method will try the maps in the given sequence, it stops after the
713     *  first match.</p>
714     *  <p>If {@code addSystemData} is provided as {@code true}, the
715     *  {@linkplain System#getProperties() system properties}
716     *  and
717     *  {@linkplain System#getenv() system environment}
718     *  will be searched for replacement values before any other source.</p>
719     *  <p>In addition, five more variables are recognised:</p>
720     *  <dl>
721     *      <dt><b><code>{@value #VARNAME_IPAddress}</code></b></dt>
722     *      <dd>The first IP address for the machine that executes this Java
723     *      virtual machine.</dd>
724     *      <dt><b><code>{@value #VARNAME_MACAddress}</code></b></dt>
725     *      <dd>The MAC address of the first NIC in this machine.</dd>
726     *      <dt><b><code>{@value #VARNAME_NodeId}</code></b></dt>
727     *      <dd>The node id from the first NIC in this machine.</dd>
728     *      <dt><b><code>{@value #VARNAME_Now}</code></b></dt>
729     *      <dd>The current date and time as returned by
730     *      {@link Instant#now}.</dd>
731     *      <dt><b><code>{@value #VARNAME_pid}</code></b></dt>
732     *      <dd>The process id of this Java virtual machine.</dd>
733     *  </dl>
734     *  <p>The found values will be converted to Strings by using the
735     *  {@link StringConverter}
736     *  that was registered for the class of the value. If the class implements
737     *  {@link Formattable},
738     *  {@link Formattable#formatTo(Formatter,int,int,int) formatTo(Formatter, 0, -1, -1)}
739     *  will be called. If there is no matching {@code StringConverter}, the
740     *  value will be converted by calling
741     *  {@link org.tquadrat.foundation.lang.Objects#toString(Object)}
742     *  with the value as argument.</p>
743     *  <p>If no replacement value could be found, the variable will not be
744     *  replaced at all.</p>
745     *  <p>If a value from one of the maps contains a variable itself, this
746     *  will not be replaced.</p>
747     *  <p>The variables names are case-sensitive.</p>
748     *  <p>Valid variable names may not contain other characters than the
749     *  letters from 'a' to 'z' (upper case and lower case), the digits from
750     *  '0' to '9' and the special characters underscore ('_') and dot ('.'),
751     *  after an optional prefix character.</p>
752     *  <p>Allowed prefixes are the tilde ('~'), the slash ('/'), the equal
753     *  sign ('='), the colon (':'), the per cent sign ('%'), and the ampersand
754     *  ('&amp;').</p>
755     *  <p>The prefix character is part of the name.</p>
756     *  <p>Finally, there is the single underscore that is allowed as a
757     *  special variable.</p>
758     *
759     *  @param  addSystemData   {@code true} if the system properties and the
760     *      system environment should be searched for replacement values, too,
761     *      otherwise {@code false}.
762     *  @param  sources The maps with the replacement values.
763     *  @return The new text, or {@code null} if the provided value for
764     *      {@code text} was already {@code null}.
765     *
766     *  @see #VARIABLE_PATTERN
767     *  @see #replaceVariableFromSystemData(CharSequence, Map[])
768     */
769    @SuppressWarnings( "TypeParameterExplicitlyExtendsObject" )
770    @SafeVarargs
771    public final String replaceVariable( final boolean addSystemData, final Map<String,? extends Object>... sources )
772    {
773        final SequencedCollection<Map<String,? extends Object>> effectiveSources = new LinkedList<>( asList( requireNonNullArgument( sources, "sources" ) ) );
774
775        if( addSystemData )
776        {
777            @SuppressWarnings( {"unchecked", "rawtypes"} )
778            final Map<String,? extends Object> systemProperties = (Map) getProperties();
779
780            effectiveSources.addFirst( getenv() );
781            effectiveSources.addFirst( systemProperties );
782        }
783
784        effectiveSources.addFirst( createAdditionalSource() );
785        final var retValue = replaceVariable( variable -> retrieveVariableValue( variable, effectiveSources ) );
786
787        //---* Done *----------------------------------------------------------
788        return retValue;
789    }   //  replaceVariable()
790
791    /**
792     *  <p>{@summary Replaces the variables of the form
793     *  <code>${&lt;<i>name</i>&gt;}</code> in the adjusted template with the
794     *  String representations of the values returned by the given retriever
795     *  function for the variable name, and returns the result after formatting
796     *  the updated contents.}</p>
797     *  <p>If no replacement value could be found, the variable will not be
798     *  replaced at all.</p>
799     *  <p>If the retriever function returns a value that contains a variable
800     *  itself, this will not be replaced.</p>
801     *  <p>The retriever function will be called only once for each variable
802     *  name; if the text contains the same variable multiple times, it will
803     *  always be replaced with the same value.</p>
804     *  <p>The variables names are case-sensitive.</p>
805     *  <p>Valid variable names may not contain other characters than the
806     *  letters from 'a' to 'z' (upper case and lower case), the digits from
807     *  '0' to '9' and the special characters underscore ('_') and dot ('.'),
808     *  after an optional prefix character.</p>
809     *  <p>Allowed prefixes are the tilde ('~'), the slash ('/'), the equal
810     *  sign ('='), the colon (':'), the per cent sign ('%'), and the ampersand
811     *  ('&amp;').</p>
812     *  <p>The prefix character is part of the name.</p>
813     *  <p>Finally, there is the single underscore that is allowed as a
814     *  special variable.</p>
815     *
816     *  @param  retriever   The function that will retrieve the replacement
817     *      values for the given variable names.
818     *  @return The new text, or {@code null} if the provided value for
819     *      {@code text} was already {@code null}.
820     *
821     *  @see #VARIABLE_PATTERN
822     */
823    public final String replaceVariable( final Function<? super String, Optional<String>> retriever )
824    {
825        requireNonNullArgument( retriever, "retriever" );
826
827        final Map<String,String> cache = new HashMap<>();
828
829        final var text = getTemplateText();
830        final var buffer = new StringBuilder();
831        if( isNotEmpty( text ) )
832        {
833            final var matcher = m_VariablePattern.matcher( text );
834            while( matcher.find() )
835            {
836                final var variable = matcher.group( 0 );
837                final var replacement = cache.computeIfAbsent( variable, v -> escapeRegexReplacement( retriever.apply( matcher.group( 1 ) ).orElse( v ) ) );
838                matcher.appendReplacement( buffer, replacement );
839            }
840            matcher.appendTail( buffer );
841        }
842
843        final var retValue = formatResult( buffer.toString() );
844
845        //---* Done *----------------------------------------------------------
846        return retValue;
847    }   //  replaceVariable()
848
849    /**
850     *  <p>{@summary Replaces the variables of the form
851     *  <code>${&lt;<i>name</i>&gt;}</code> in the given String with the String
852     *  representation of the values returned by the given retriever function
853     *  for the variable name.}</p>
854     *  <p>If no replacement value could be found, the variable will not be
855     *  replaced at all.</p>
856     *  <p>If the retriever function returns a value that contains a variable
857     *  itself, this will not be replaced.</p>
858     *  <p>The retriever function will be called only once for each variable
859     *  name; if the text contains the same variable multiple times, it will
860     *  always be replaced with the same value.</p>
861     *  <p>The variables names are case-sensitive.</p>
862     *  <p>Valid variable name may not contain other characters than the
863     *  letters from 'a' to 'z' (upper case and lower case), the digits from
864     *  '0' to '9' and the special characters underscore ('_') and dot ('.'),
865     *  after an optional prefix character.</p>
866     *  <p>Allowed prefixes are the tilde ('~'), the slash ('/'), the equal
867     *  sign ('='), the colon (':'), the per cent sign ('%'), and the ampersand
868     *  ('&amp;').</p>
869     *  <p>The prefix character is part of the name.</p>
870     *  <p>Finally, there is the single underscore that is allowed as a
871     *  special variable.</p>
872     *
873     *  @param  text    The text with the variables; may be {@code null}.
874     *  @param  retriever   The function that will retrieve the replacement
875     *      values for the given variable names.
876     *  @return The new text, or {@code null} if the provided value for
877     *      {@code text} was already {@code null}.
878     *
879     *  @see #VARIABLE_PATTERN
880     *
881     *  @since 0.1.0
882     */
883    @API( status = STABLE, since = "0.1.0" )
884    public static final String replaceVariable( final CharSequence text, final Function<? super String, Optional<String>> retriever )
885    {
886        final var retValue = isNull( text )
887            ? null
888            : new Template( text ).replaceVariable( requireNonNullArgument( retriever, "retriever" ) );
889
890        //---* Done *----------------------------------------------------------
891        return retValue;
892    }   //  replaceVariable()
893
894    /**
895     *  <p>{@summary Replaces the variables of the form
896     *  <code>${<i>&lt;name&gt;</i>}</code> in the given String with values
897     *  from the
898     *  {@linkplain System#getProperties() system properties},
899     *  the
900     *  {@linkplain System#getenv() system environment}
901     *  and the given maps.} The method will try the maps in the given
902     *  sequence, it stops after the first match.</p>
903     *  <p>In addition, five more variables are recognised:</p>
904     *  <dl>
905     *      <dt><b><code>{@value #VARNAME_IPAddress}</code></b></dt>
906     *      <dd>The first IP address for the machine that executes this Java
907     *      virtual machine.</dd>
908     *      <dt><b><code>{@value #VARNAME_MACAddress}</code></b></dt>
909     *      <dd>The MAC address of the first NIC in this machine.</dd>
910     *      <dt><b><code>{@value #VARNAME_NodeId}</code></b></dt>
911     *      <dd>The node id from the first NIC in this machine.</dd>
912     *      <dt><b><code>{@value #VARNAME_Now}</code></b></dt>
913     *      <dd>The current date and time as returned by
914     *      {@link Instant#now}.</dd>
915     *      <dt><b><code>{@value #VARNAME_pid}</code></b></dt>
916     *      <dd>The process id of this Java virtual machine.</dd>
917     *  </dl>
918     *  <p>If no replacement value could be found, the variable will not be
919     *  replaced at all; no exception will be thrown.</p>
920     *  <p>If a value from one of the maps contains a variable itself, this
921     *  will not be replaced.</p>
922     *  <p>The variables names are case-sensitive.</p>
923     *
924     *  @param  text    The text with the variables; can be {@code null}.
925     *  @param  sources The maps with the replacement values, in addition to
926     *      the system variables.
927     *  @return The new text, or {@code null} if the provided value for
928     *      {@code text} was already {@code null}.
929     *
930     *  @see #VARIABLE_PATTERN
931     *  @see #replaceVariable(CharSequence, Map...)
932     */
933    @SuppressWarnings( "TypeParameterExplicitlyExtendsObject" )
934    @SafeVarargs
935    @API( status = STABLE, since = "0.1.0" )
936    public static final String replaceVariableFromSystemData( final CharSequence text, final Map<String,? extends Object>... sources )
937    {
938        final var retValue = isNull( text )
939            ? null
940            : new Template( text ).replaceVariable( true, requireNonNullArgument( sources, "sources" ) );
941
942        //---* Done *----------------------------------------------------------
943        return retValue;
944    }   //  replaceVariableFromSystemData()
945
946    /**
947     *  <p>{@summary Tries to obtain a value for the given key from one of the
948     *  given sources that will be searched in the given sequence order.}</p>
949     *  <p>The method stops searching once an entry for {@code code} was
950     *  found.</p>
951     *  <p>The found value will be converted to a String by using the
952     *  {@link StringConverter}
953     *  that was registered for the class of the value. If the class implements
954     *  {@link Formattable},
955     *  {@link Formattable#formatTo(Formatter,int,int,int) formatTo(Formatter, 0, -1, -1)}
956     *  will be called. If there is no matching {@code StringConverter}, the
957     *  value will be converted by calling
958     *  {@link org.tquadrat.foundation.lang.Objects#toString(Object)}
959     *  with the value as argument.</p>
960     *
961     *  @param  name    The name of the value.
962     *  @param  sources The maps with the values.
963     *  @return An instance of
964     *      {@link Optional}
965     *      that holds the value from one of the sources.
966     */
967    @SuppressWarnings( {"TypeParameterExplicitlyExtendsObject", "BoundedWildcard"} )
968    private final Optional<String> retrieveVariableValue( final String name, final Iterable<Map<String,? extends Object>> sources )
969    {
970        assert nonNull( name ) : "name is null";
971        assert nonNull( sources ) : "sources is null";
972
973        //---* Search the sources *--------------------------------------------
974        Object value = null;
975        SearchLoop: for( final var map : sources )
976        {
977            value = map.get( name );
978            if( nonNull( value ) ) break SearchLoop;
979        }   //  SearchLoop:
980
981        final var replacement = switch( value )
982        {
983            case null -> null;
984            case final CharSequence charSequence -> charSequence.toString();
985            case final Formattable formattable ->
986            {
987                final var formatter = new Formatter();
988                formattable.formatTo( formatter, 0, -1, -1 );
989                yield formatter.toString();
990            }
991            default ->
992            {
993                final var valueClass = value.getClass();
994                final var foundValue = valueClass.cast( value );
995                Optional<? extends StringConverter<?>> stringConverter = Optional.ofNullable( m_StringConverters.get( valueClass ) );
996                if( stringConverter.isEmpty() ) stringConverter = StringConverter.forClass( valueClass );
997                yield stringConverter.map( converter -> applyStringConverter( valueClass, converter, foundValue ) ).orElse( foundValue.toString() );
998            }
999        };
1000        final Optional<String> retValue = isNull( replacement ) ? Optional.empty() : Optional.of( replacement );
1001
1002        //---* Done *----------------------------------------------------------
1003        return retValue;
1004    }   //  retrieveVariableValue()
1005}
1006//  class Template
1007
1008/*
1009 *  End of File
1010 */