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