Index: log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/StrSubstitutor.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/StrSubstitutor.java (revision 1534018) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/StrSubstitutor.java (working copy) @@ -25,6 +25,7 @@ import java.util.Properties; import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.helpers.Strings; /** * Substitutes variables within a string by values. @@ -62,6 +63,26 @@ * The quick brown fox jumped over the lazy dog. * *

+ * Also, this class allows to set a default value for unresolved variables. + * The default value for a variable can be appended to the variable name after the variable + * default value delimiter. The default value of the variable default value delimiter is ':-', + * as in bash and other *nix shells, as those are arguably where the default ${} delimiter set originated. + * The variable default value delimiter can be manually set by calling {@link #setValueDelimiterMatcher(StrMatcher)}, + * {@link #setValueDelimiter(char)} or {@link #setValueDelimiter(String)}. + * The following shows an example with varialbe default value settings: + *

+ * Map valuesMap = HashMap();
+ * valuesMap.put("animal", "quick brown fox");
+ * valuesMap.put("target", "lazy dog");
+ * String templateString = "The ${animal} jumped over the ${target}. ${undefined.number:-1234567890}.";
+ * StrSubstitutor sub = new StrSubstitutor(valuesMap);
+ * String resolvedString = sub.replace(templateString);
+ * 
+ * yielding: + *
+ *      The quick brown fox jumped over the lazy dog. 1234567890.
+ * 
+ *

* In addition to this usage pattern there are some static convenience methods that * cover the most common use cases. These methods can be used without the need of * manually creating an instance. However if multiple replace operations are to be @@ -115,6 +136,10 @@ * Constant for the default variable suffix. */ public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}"); + /** + * Constant for the default value delimiter of a variable. + */ + public static final StrMatcher DEFAULT_VALUE_DELIMITER = StrMatcher.stringMatcher(":-"); private static final int BUF_SIZE = 256; @@ -131,6 +156,10 @@ */ private StrMatcher suffixMatcher; /** + * Stores the default variable value delimiter + */ + private StrMatcher valueDelimiterMatcher; + /** * Variable resolution is delegated to an implementer of VariableResolver. */ private StrLookup variableResolver; @@ -186,6 +215,21 @@ /** * Creates a new instance and initializes it. * + * @param valueMap the map with the variables' values, may be null + * @param prefix the prefix for variables, not null + * @param suffix the suffix for variables, not null + * @param escape the escape character + * @param valueDelimiter the variable default value delimiter, may be null + * @throws IllegalArgumentException if the prefix or suffix is null + */ + public StrSubstitutor(final Map valueMap, final String prefix, final String suffix, + final char escape, final String valueDelimiter) { + this(new MapLookup(valueMap), prefix, suffix, escape, valueDelimiter); + } + + /** + * Creates a new instance and initializes it. + * * @param variableResolver the variable resolver, may be null */ public StrSubstitutor(final StrLookup variableResolver) { @@ -213,6 +257,24 @@ * Creates a new instance and initializes it. * * @param variableResolver the variable resolver, may be null + * @param prefix the prefix for variables, not null + * @param suffix the suffix for variables, not null + * @param escape the escape character + * @param valueDelimiter the variable default value delimiter string, may be null + * @throws IllegalArgumentException if the prefix or suffix is null + */ + public StrSubstitutor(final StrLookup variableResolver, final String prefix, final String suffix, final char escape, final String valueDelimiter) { + this.setVariableResolver(variableResolver); + this.setVariablePrefix(prefix); + this.setVariableSuffix(suffix); + this.setEscapeChar(escape); + this.setValueDelimiter(valueDelimiter); + } + + /** + * Creates a new instance and initializes it. + * + * @param variableResolver the variable resolver, may be null * @param prefixMatcher the prefix for variables, not null * @param suffixMatcher the suffix for variables, not null * @param escape the escape character @@ -221,11 +283,28 @@ public StrSubstitutor(final StrLookup variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher, final char escape) { + this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER); + } + + /** + * Creates a new instance and initializes it. + * + * @param variableResolver the variable resolver, may be null + * @param prefixMatcher the prefix for variables, not null + * @param suffixMatcher the suffix for variables, not null + * @param escape the escape character + * @param valueDelimiterMatcher the variable default value delimiter matcher, may be null + * @throws IllegalArgumentException if the prefix or suffix is null + */ + public StrSubstitutor( + final StrLookup variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher, final char escape, final StrMatcher valueDelimiterMatcher) { this.setVariableResolver(variableResolver); this.setVariablePrefixMatcher(prefixMatcher); this.setVariableSuffixMatcher(suffixMatcher); this.setEscapeChar(escape); + this.setValueDelimiterMatcher(valueDelimiterMatcher); } + //----------------------------------------------------------------------- /** * Replaces all the occurrences of variables in the given source object with @@ -755,6 +834,8 @@ final StrMatcher prefixMatcher = getVariablePrefixMatcher(); final StrMatcher suffixMatcher = getVariableSuffixMatcher(); final char escape = getEscapeChar(); + final StrMatcher valueDelimiterMatcher = getValueDelimiterMatcher(); + final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables(); final boolean top = (priorVariables == null); boolean altered = false; @@ -783,7 +864,7 @@ int endMatchLen = 0; int nestedVarCount = 0; while (pos < bufEnd) { - if (isEnableSubstitutionInVariables() + if (substitutionInVariablesEnabled && (endMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd)) != 0) { // found a nested variable start @@ -799,17 +880,37 @@ } else { // found variable end marker if (nestedVarCount == 0) { - String varName = new String(chars, startPos + String varNameExpr = new String(chars, startPos + startMatchLen, pos - startPos - startMatchLen); - if (isEnableSubstitutionInVariables()) { - final StringBuilder bufName = new StringBuilder(varName); + if (substitutionInVariablesEnabled) { + final StringBuilder bufName = new StringBuilder(varNameExpr); substitute(event, bufName, 0, bufName.length()); - varName = bufName.toString(); + varNameExpr = bufName.toString(); } pos += endMatchLen; final int endPos = pos; + String varName = varNameExpr; + String varDefaultValue = null; + + if (valueDelimiterMatcher != null) { + final char [] varNameExprChars = varNameExpr.toCharArray(); + int valueDelimiterMatchLen = 0; + for (int i = 0; i < varNameExprChars.length; i++) { + // if there's any nested variable when nested variable substitution disabled, then stop resolving name and default value. + if (!substitutionInVariablesEnabled + && prefixMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0) { + break; + } + if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) { + varName = varNameExpr.substring(0, i); + varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen); + break; + } + } + } + // on the first call initialize priorVariables if (priorVariables == null) { priorVariables = new ArrayList(); @@ -822,8 +923,11 @@ priorVariables.add(varName); // resolve the variable - final String varValue = resolveVariable(event, varName, buf, + String varValue = resolveVariable(event, varName, buf, startPos, endPos); + if (varValue == null) { + varValue = varDefaultValue; + } if (varValue != null) { // recursive replace final int varLen = varValue.length(); @@ -1056,6 +1160,76 @@ return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix)); } + // Variable Default Value Delimiter + //----------------------------------------------------------------------- + /** + * Gets the variable default value delimiter matcher currently in use. + *

+ * The variable default value delimiter is the characer or characters that delimite the + * variable name and the variable default value. This delimiter is expressed in terms of a matcher + * allowing advanced variable default value delimiter matches. + *

+ * If it returns null, then the variable default value resolution is disabled. + * + * @return the variable default value delimiter matcher in use, may be null + */ + public StrMatcher getValueDelimiterMatcher() { + return valueDelimiterMatcher; + } + + /** + * Sets the variable default value delimiter matcher to use. + *

+ * The variable default value delimiter is the characer or characters that delimite the + * variable name and the variable default value. This delimiter is expressed in terms of a matcher + * allowing advanced variable default value delimiter matches. + *

+ * If the valueDelimiterMatcher is null, then the variable default value resolution + * becomes disabled. + * + * @param valueDelimiterMatcher variable default value delimiter matcher to use, may be null + * @return this, to enable chaining + */ + public StrSubstitutor setValueDelimiterMatcher(final StrMatcher valueDelimiterMatcher) { + this.valueDelimiterMatcher = valueDelimiterMatcher; + return this; + } + + /** + * Sets the variable default value delimiter to use. + *

+ * The variable default value delimiter is the characer or characters that delimite the + * variable name and the variable default value. This method allows a single character + * variable default value delimiter to be easily set. + * + * @param valueDelimiter the variable default value delimiter character to use + * @return this, to enable chaining + */ + public StrSubstitutor setValueDelimiter(final char valueDelimiter) { + return setValueDelimiterMatcher(StrMatcher.charMatcher(valueDelimiter)); + } + + /** + * Sets the variable default value delimiter to use. + *

+ * The variable default value delimiter is the characer or characters that delimite the + * variable name and the variable default value. This method allows a string + * variable default value delimiter to be easily set. + *

+ * If the valueDelimiter is null or empty string, then the variable default + * value resolution becomes disabled. + * + * @param valueDelimiter the variable default value delimiter string to use, may be null or empty + * @return this, to enable chaining + */ + public StrSubstitutor setValueDelimiter(final String valueDelimiter) { + if (Strings.isEmpty(valueDelimiter)) { + setValueDelimiterMatcher(null); + return this; + } + return setValueDelimiterMatcher(StrMatcher.stringMatcher(valueDelimiter)); + } + // Resolver //----------------------------------------------------------------------- /** Index: log4j-core/src/test/java/org/apache/logging/log4j/core/appender/routing/RoutingAppenderWithJndiTest.java =================================================================== --- log4j-core/src/test/java/org/apache/logging/log4j/core/appender/routing/RoutingAppenderWithJndiTest.java (revision 1534018) +++ log4j-core/src/test/java/org/apache/logging/log4j/core/appender/routing/RoutingAppenderWithJndiTest.java (working copy) @@ -90,7 +90,7 @@ // default route when there's no jndi resource StructuredDataMessage msg = new StructuredDataMessage("Test", "This is a message from unknown context", "Context"); EventLogger.logEvent(msg); - File defaultLogFile = new File("target/routingbyjndi/routingbyjnditest-default.log"); + File defaultLogFile = new File("target/routingbyjndi/routingbyjnditest-unknown.log"); assertTrue("The default log file was not created", defaultLogFile.exists()); // now set jndi resource to Application1 @@ -116,5 +116,21 @@ assertNotNull("No events generated", listAppender2.getEvents()); assertTrue("Incorrect number of events. Expected 2, got " + listAppender2.getEvents().size(), listAppender2.getEvents().size() == 2); assertTrue("Incorrect number of events. Expected 1, got " + listAppender1.getEvents().size(), listAppender1.getEvents().size() == 1); + + // now set jndi resource to Application3. + // The context name, 'Application3', will be used as log file name by the default route. + context.rebind("java:comp/env/logging/context-name", "Application3"); + msg = new StructuredDataMessage("Test", "This is a message from Application3", "Context"); + EventLogger.logEvent(msg); + File application3LogFile = new File("target/routingbyjndi/routingbyjnditest-Application3.log"); + assertTrue("The Application3 log file was not created", application3LogFile.exists()); + + // now set jndi resource to Application4 + // The context name, 'Application4', will be used as log file name by the default route. + context.rebind("java:comp/env/logging/context-name", "Application4"); + msg = new StructuredDataMessage("Test", "This is a message from Application4", "Context"); + EventLogger.logEvent(msg); + File application4LogFile = new File("target/routingbyjndi/routingbyjnditest-Application4.log"); + assertTrue("The Application3 log file was not created", application4LogFile.exists()); } } Index: log4j-core/src/test/java/org/apache/logging/log4j/core/config/XMLLoggerPropsTest.java =================================================================== --- log4j-core/src/test/java/org/apache/logging/log4j/core/config/XMLLoggerPropsTest.java (revision 1534018) +++ log4j-core/src/test/java/org/apache/logging/log4j/core/config/XMLLoggerPropsTest.java (working copy) @@ -16,6 +16,12 @@ */ package org.apache.logging.log4j.core.config; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.Map; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.Appender; @@ -26,12 +32,6 @@ import org.junit.BeforeClass; import org.junit.Test; -import java.util.List; -import java.util.Map; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - /** * */ @@ -76,7 +76,16 @@ final List events = app.getMessages(); assertTrue("No events", events.size() > 0); assertTrue("Incorrect number of events", events.size() == 2); + assertTrue("Incorrect value", events.get(0).contains("user=")); + assertTrue("Incorrect value", events.get(0).contains("phrasex=****")); assertTrue("Incorrect value", events.get(0).contains("test=test")); + assertTrue("Incorrect value", events.get(0).contains("test2=test2default")); + assertTrue("Incorrect value", events.get(0).contains("test3=Unknown")); + assertTrue("Incorrect value", events.get(1).contains("user=")); + assertTrue("Incorrect value", events.get(1).contains("phrasex=****")); + assertTrue("Incorrect value", events.get(1).contains("test=test")); + assertTrue("Incorrect value", events.get(1).contains("test2=test2default")); + assertTrue("Incorrect value", events.get(1).contains("test3=Unknown")); } finally { System.clearProperty("test"); } Index: log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/StrSubstitutorTest.java =================================================================== --- log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/StrSubstitutorTest.java (revision 1534018) +++ log4j-core/src/test/java/org/apache/logging/log4j/core/lookup/StrSubstitutorTest.java (working copy) @@ -16,22 +16,22 @@ */ package org.apache.logging.log4j.core.lookup; -import org.apache.logging.log4j.ThreadContext; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; +import static org.junit.Assert.assertEquals; import java.util.HashMap; import java.util.Map; -import static org.junit.Assert.assertEquals; +import org.apache.logging.log4j.ThreadContext; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; /** * */ public class StrSubstitutorTest { - private static final String TESTKEY = "TestKey"; + private static final String TESTKEY = "TestKey"; private static final String TESTVAL = "TestValue"; @@ -57,5 +57,12 @@ assertEquals("TestValue-TestValue-TestValue", value); value = subst.replace("${BadKey}"); assertEquals("${BadKey}", value); + + value = subst.replace("${BadKey:-Unknown}-${ctx:BadKey:-Unknown}-${sys:BadKey:-Unknown}"); + assertEquals("Unknown-Unknown-Unknown", value); + value = subst.replace("${BadKey:-Unknown}-${ctx:BadKey}-${sys:BadKey:-Unknown}"); + assertEquals("Unknown-${ctx:BadKey}-Unknown", value); + value = subst.replace("${BadKey:-Unknown}-${ctx:BadKey:-}-${sys:BadKey:-Unknown}"); + assertEquals("Unknown--Unknown", value); } } Index: log4j-core/src/test/resources/log4j-loggerprops.xml =================================================================== --- log4j-core/src/test/resources/log4j-loggerprops.xml (revision 1534018) +++ log4j-core/src/test/resources/log4j-loggerprops.xml (working copy) @@ -1,19 +1,26 @@ + + + test2default + + - + $${sys:user.name} + ${sys:user.phrasex:-****} ${sys:test} ${sys:user.name} + ${sys:user.phrasex:-****} ${sys:test} Index: log4j-core/src/test/resources/log4j-routing-by-jndi.xml =================================================================== --- log4j-core/src/test/resources/log4j-routing-by-jndi.xml (revision 1534018) +++ log4j-core/src/test/resources/log4j-routing-by-jndi.xml (working copy) @@ -32,9 +32,8 @@ - - + %d %p %C{1.} [%t] %m%n