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 ("Variables") in the form 064 * <code>${<<i>name</i>>}</code>, where <<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 * ('&').</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 * ('&').</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>${<<i>name</i>>}</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><name></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 * ('&').</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><name></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 * ('&').</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><name></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><name></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><name></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 * "${…}". 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><name></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>${<<i>name</i>>}</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 * ('&').</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>${<<i>name</i>>}</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 * ('&').</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>${<<i>name</i>>}</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 * ('&').</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>${<<i>name</i>>}</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 * ('&').</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>${<<i>name</i>>}</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 * ('&').</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><name></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 */