diff --git a/core/src/main/java/io/jstach/rainbowgum/LogEvent.java b/core/src/main/java/io/jstach/rainbowgum/LogEvent.java
index 9a341f75..24e47bb8 100644
--- a/core/src/main/java/io/jstach/rainbowgum/LogEvent.java
+++ b/core/src/main/java/io/jstach/rainbowgum/LogEvent.java
@@ -212,6 +212,13 @@ default void formattedMessage(Appendable a) {
*/
public LogEvent freeze();
+ /**
+ * Freeze and replace with the given timestamp.
+ * @param timestamp instant to replace timestamp in this.
+ * @return a copy of this with the given timestamp.
+ */
+ public LogEvent freeze(Instant timestamp);
+
}
enum EmptyLogEvent implements LogEvent {
@@ -276,6 +283,11 @@ public LogEvent freeze() {
return this;
}
+ @Override
+ public LogEvent freeze(Instant timestamp) {
+ return this;
+ }
+
}
record OneArgLogEvent(Instant timestamp, String threadName, long threadId, System.Logger.Level level, String loggerName,
@@ -295,6 +307,10 @@ public int argCount() {
}
public LogEvent freeze() {
+ return freeze(timestamp);
+ }
+
+ public LogEvent freeze(Instant timestamp) {
StringBuilder sb = new StringBuilder(message.length());
formattedMessage(sb);
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, sb.toString(),
@@ -320,6 +336,10 @@ public int argCount() {
}
public LogEvent freeze() {
+ return freeze(timestamp);
+ }
+
+ public LogEvent freeze(Instant timestamp) {
StringBuilder sb = new StringBuilder(message.length());
formattedMessage(sb);
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, sb.toString(),
@@ -344,11 +364,16 @@ public int argCount() {
}
public LogEvent freeze() {
+ return freeze(timestamp);
+ }
+
+ public LogEvent freeze(Instant timestamp) {
StringBuilder sb = new StringBuilder(message.length());
formattedMessage(sb);
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, sb.toString(),
keyValues.freeze(), null);
}
+
}
record DefaultLogEvent(Instant timestamp, String threadName, long threadId, System.Logger.Level level,
@@ -379,4 +404,13 @@ public LogEvent freeze() {
return this;
}
+ public LogEvent freeze(Instant timestamp) {
+ if (keyValues instanceof MutableKeyValues mkvs) {
+ return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, formattedMessage, mkvs,
+ throwable);
+ }
+ return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, formattedMessage, keyValues,
+ throwable);
+ }
+
}
diff --git a/core/src/main/java/io/jstach/rainbowgum/LogProperties.java b/core/src/main/java/io/jstach/rainbowgum/LogProperties.java
index 14f1c192..c89a365e 100644
--- a/core/src/main/java/io/jstach/rainbowgum/LogProperties.java
+++ b/core/src/main/java/io/jstach/rainbowgum/LogProperties.java
@@ -1,5 +1,8 @@
package io.jstach.rainbowgum;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.UncheckedIOException;
import java.lang.System.Logger.Level;
import java.net.URI;
import java.nio.charset.StandardCharsets;
@@ -13,6 +16,7 @@
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
+import java.util.Properties;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
@@ -128,11 +132,43 @@ public interface LogProperties {
/**
* Analogous to {@link System#getProperty(String)}.
- * @param key property key.
+ * @param key property name.
* @return property value.
*/
public @Nullable String valueOrNull(String key);
+ /**
+ * Gets a list or null if the key is missing and by default uses
+ * {@link #parseList(String)}.
+ * @param key property name.
+ * @return list or null
.
+ * @apiNote the reason empty list is not returned for missing key is that it creates
+ * ambiguity so null is returned when key is missing.
+ */
+ default @Nullable List listOrNull(String key) {
+ String s = valueOrNull(key);
+ if (s == null) {
+ return null;
+ }
+ return parseList(s);
+ }
+
+ /**
+ * Gets a map or null if the key is missing and by default uses
+ * {@link #parseMap(String)}.
+ * @param key property name.
+ * @return map or null
.
+ * @apiNote the reason empty map is not returned for missing key is that it creates
+ * ambiguity so null is returned when key is missing.
+ */
+ default Map mapOrNull(String key) {
+ String s = valueOrNull(key);
+ if (s == null) {
+ return null;
+ }
+ return parseMap(s);
+ }
+
/**
* Searches up a path using this properties to check for values.
* @param root prefix.
@@ -162,6 +198,121 @@ default int order() {
return 0;
}
+ /**
+ * Builder for properties.
+ */
+ public final static class Builder {
+
+ private String description = "custom";
+
+ private int order = 0;
+
+ private Function function;
+
+ private Builder() {
+ }
+
+ /**
+ * Description for properties.
+ * @param description not null.
+ * @return this.
+ */
+ public Builder description(String description) {
+ this.description = description;
+ return this;
+ }
+
+ /**
+ * When log properties are coalesced this method is used to resolve order. A
+ * higher number gives higher precedence.
+ * @param order order.
+ * @return this.
+ */
+ public Builder order(int order) {
+ this.order = order;
+ return this;
+ }
+
+ /**
+ * Parses a string as {@link Properties}.
+ * @param properties properties as a string.
+ * @return this.
+ */
+ public Builder fromProperties(String properties) {
+ function = Format.PROPERTIES.parse(properties);
+ return this;
+ }
+
+ /**
+ * Parses a string as a URI query string.
+ * @param query uri percent encoded uri with separator as "&
" and
+ * key value separator of "=
".
+ * @return this.
+ */
+ public Builder fromURIQuery(String query) {
+ function = Format.URI_QUERY.parse(query);
+ return this;
+ }
+
+ /**
+ * Builds LogProperties based on builder config.
+ * @return this.
+ */
+ public LogProperties build() {
+ if (function == null) {
+ throw new IllegalStateException("function is was not set");
+ }
+ return new DefaultLogProperties(function, description, order);
+ }
+
+ private record DefaultLogProperties(Function func, String description,
+ int order) implements LogProperties {
+
+ @Override
+ public @Nullable String valueOrNull(String key) {
+ return func.apply(key);
+ }
+
+ }
+
+ enum Format {
+
+ PROPERTIES() {
+ @Override
+ Function parse(String content) {
+ var m = new LinkedHashMap();
+ StringReader reader = new StringReader(content);
+ try {
+ PropertiesParser.readProperties(reader, m::put);
+ }
+ catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return m::get;
+ }
+ },
+ URI_QUERY() {
+ @Override
+ Function parse(String content) {
+ var m = parseMap(content);
+ return m::get;
+ }
+ };
+
+ abstract Function parse(String content);
+
+ }
+
+ }
+
+ /**
+ * LogProperties builder.
+ * @return builder.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
/**
* Creates log properties from many log properties.
* @param logProperties list of properties.
@@ -395,12 +546,69 @@ private static void sneakyThrow(final Throwable x) throws
}
- private static RuntimeException throwPropertyError(String key, Exception e) {
- throw new RuntimeException("Error for property. key: " + key, e);
+ /**
+ * Parent interface for property exceptions.
+ */
+ sealed interface PropertyProblem {
+
+ }
+
+ /**
+ * Thrown if an error happens while converting a property.
+ */
+ final static class PropertyConvertException extends RuntimeException implements PropertyProblem {
+
+ private static final long serialVersionUID = -6260241455268426342L;
+
+ /**
+ * Key.
+ */
+ private final String key;
+
+ /**
+ * Creates convert exception.
+ * @param key property key.
+ * @param message error message
+ * @param cause maybe null.
+ */
+ PropertyConvertException(String key, String message, @Nullable Throwable cause) {
+ super(message, cause);
+ this.key = key;
+ }
+
+ /**
+ * Property key.
+ * @return key.
+ */
+ public String key() {
+ return this.key;
+ }
+
+ }
+
+ /**
+ * Throw if property is missing.
+ */
+ final static class PropertyMissingException extends NoSuchElementException implements PropertyProblem {
+
+ private static final long serialVersionUID = 4203076848052565692L;
+
+ /**
+ * Creates a missing propety exception.
+ * @param s error message.
+ */
+ PropertyMissingException(String s) {
+ super(s);
+ }
+
+ }
+
+ private static PropertyConvertException throwPropertyError(String key, Exception e) {
+ throw new PropertyConvertException(key, "Error for property. key: " + key, e);
}
private static RuntimeException throwMissingError(List keys) {
- throw new NoSuchElementException("Property missing. key: " + keys);
+ throw new PropertyMissingException("Property missing. key: " + keys);
}
/**
@@ -771,6 +979,16 @@ public Property map(PropertyFunction super T, ? extends U, ? super Exce
return new Property<>(propertyGetter.map(mapper), keys);
}
+ /**
+ * Converts the value into a String that can be parsed by the builtin properties
+ * parsing of types. Supported types: String, Boolean, Integer, URI, Map, List.
+ * @param value to be converted to string.
+ * @return property representation of value.
+ */
+ public String propertyString(T value) {
+ return propertyGetter.propertyString(value);
+ }
+
/**
* Set a property if its not null.
* @param value value to set.
@@ -792,6 +1010,35 @@ public static RootPropertyGetter builder() {
}
}
+ // enum PropertyType {
+ // STRING,
+ // INTEGER,
+ // BOOLEAN,
+ // URI,
+ // MAP,
+ // LIST,
+ // UNKNOWN;
+ //
+ // public static PropertyType of(Object value) {
+ // return switch(value) {
+ // case String s -> STRING;
+ // case Integer i -> INTEGER;
+ // case Boolean b -> BOOLEAN;
+ // case Map,?> m -> MAP;
+ // default -> UNKNOWN;
+ // };
+ // }
+ //
+ // public static PropertyType of(Class> c) {
+ // return switch (c) {
+ // case Class s -> STRING;
+ // default -> throw new IllegalArgumentException("Unexpected value: " + c);
+ //
+ // };
+ // }
+ //
+ // }
+
/**
* Extracts and converts from {@link LogProperties}.
*
@@ -808,6 +1055,14 @@ sealed interface PropertyGetter {
@Nullable
T valueOrNull(LogProperties props, String key);
+ /**
+ * Converts the value into a String that can be parsed by the builtin properties
+ * parsing of types. Supported types: String, Boolean, Integer, URI, Map, List.
+ * @param value to be converted to string.
+ * @return property representation of value.
+ */
+ String propertyString(T value);
+
/**
* Value or null.
* @param props log properties.
@@ -1014,7 +1269,12 @@ record RootPropertyGetter(String prefix, boolean search) implements PropertyGett
if (search) {
return props.search(prefix, key);
}
- return props.valueOrNull(LogProperties.concatKey(prefix, key));
+ return props.valueOrNull(fullyQualifiedKey(key));
+ }
+
+ @Override
+ public String propertyString(String value) {
+ return value;
}
RuntimeException throwError(LogProperties props, String key, Exception e) {
@@ -1058,7 +1318,7 @@ public String fullyQualifiedKey(String key) {
* @return getter that will convert to integers.
*/
public PropertyGetter toInt() {
- return this.map(Integer::parseInt);
+ return new FuncGetter<>(this, Integer::parseInt, String::valueOf);
}
/**
@@ -1066,7 +1326,7 @@ public PropertyGetter toInt() {
* @return getter that will convert to boolean.
*/
public PropertyGetter toBoolean() {
- return this.map(Boolean::parseBoolean);
+ return new FuncGetter<>(this, Boolean::parseBoolean, String::valueOf);
}
/**
@@ -1076,7 +1336,7 @@ public PropertyGetter toBoolean() {
* @return getter that will convert to enum type.
*/
public > PropertyGetter toEnum(Class enumClass) {
- return this.map(s -> Enum.valueOf(enumClass, s));
+ return new FuncGetter<>(this, s -> Enum.valueOf(enumClass, s), String::valueOf);
}
/**
@@ -1084,7 +1344,7 @@ public > PropertyGetter toEnum(Class enumClass) {
* @return getter that will parse URIs.
*/
public PropertyGetter toURI() {
- return this.map(URI::new);
+ return new FuncGetter<>(this, URI::new, String::valueOf);
}
}
@@ -1102,6 +1362,18 @@ public sealed interface ChildPropertyGetter extends PropertyGetter {
*/
PropertyGetter> parent();
+ @Override
+ default String propertyString(T value) {
+ return switch (value) {
+ case String s -> s;
+ case Boolean b -> String.valueOf(b);
+ case Integer i -> String.valueOf(i);
+ case URI u -> String.valueOf(u);
+ case Map, ?> m -> MapGetter._propertyString(m);
+ default -> throw new RuntimeException("Unable to convert to property string. value = " + value);
+ };
+ }
+
}
/**
@@ -1111,7 +1383,7 @@ public sealed interface ChildPropertyGetter extends PropertyGetter {
* @return new property getter.
*/
default PropertyGetter map(PropertyFunction super T, ? extends U, ? super Exception> mapper) {
- return new MapExtractor(this, mapper);
+ return new FuncGetter(this, mapper, null);
}
/**
@@ -1166,8 +1438,54 @@ public T valueOrNull(LogProperties props, String key) {
}
-record MapExtractor(PropertyGetter parent,
- PropertyFunction super T, ? extends R, ? super Exception> mapper) implements ChildPropertyGetter {
+record BooleanGetter(RootPropertyGetter parent) implements ChildPropertyGetter {
+
+ @Override
+ public @Nullable Boolean valueOrNull(LogProperties props, String key) {
+ var v = parent.valueOrNull(props, key);
+ return Boolean.parseBoolean(v);
+ }
+
+}
+
+record MapGetter(RootPropertyGetter parent) implements ChildPropertyGetter