mirror of
https://github.com/elastic/logstash.git
synced 2025-04-24 22:57:16 -04:00
WIP removing the need to specify argument positions explicitly like (String) arg[0]
for constructors.
This commit is contained in:
parent
4770bd9bac
commit
b656f2dce3
10 changed files with 353 additions and 255 deletions
|
@ -1,14 +1,12 @@
|
|||
package org.logstash.common.parser;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -21,29 +19,101 @@ import java.util.stream.Collectors;
|
|||
* @param <Value> The object type to construct when `parse` is called.
|
||||
*/
|
||||
public class ConstructingObjectParser<Value> implements ObjectParser<Value> {
|
||||
private final Logger logger = LogManager.getLogger();
|
||||
private final Function<Object[], Value> builder;
|
||||
private final Map<String, FieldDefinition<Value>> parsers = new HashMap<>();
|
||||
private final Map<String, FieldDefinition<Object[]>> constructorArgs;
|
||||
|
||||
private final Map<String, BiConsumer<Value, Object>> parsers = new HashMap<>();
|
||||
private List<Field<?>> constructorFields = null;
|
||||
private Function<Map<String, Object>, Value> builder;
|
||||
/**
|
||||
* Zero-argument object constructor (A Supplier)
|
||||
* @param supplier The supplier which produces an object instance.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public ConstructingObjectParser(Supplier<Value> supplier) {
|
||||
this.builder = args -> supplier.get();
|
||||
|
||||
// Reject any attempts to add constructor fields with an immutable map.
|
||||
constructorArgs = Collections.emptyMap();
|
||||
this(config -> supplier.get());
|
||||
}
|
||||
/**
|
||||
* One-argument object constructor
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public <Arg0> ConstructingObjectParser(Function<Arg0, Value> function, Field<Arg0> arg0) {
|
||||
this(config -> function.apply(arg0.apply(config)));
|
||||
constructorFields = Arrays.asList(arg0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param builder A function which takes an Object[] as argument and returns a Value instance
|
||||
* Two-argument object constructor
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public ConstructingObjectParser(Function<Object[], Value> builder) {
|
||||
public <Arg0, Arg1> ConstructingObjectParser(BiFunction<Arg0, Arg1, Value> function, Field<Arg0> arg0, Field<Arg1> arg1) {
|
||||
this(config -> function.apply(arg0.apply(config), arg1.apply(config)));
|
||||
constructorFields = Arrays.asList(arg0, arg1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Three-argument object constructor
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public <Arg0, Arg1, Arg2> ConstructingObjectParser(Function3<Arg0, Arg1, Arg2, Value> function, Field<Arg0> arg0, Field<Arg1> arg1, Field<Arg2> arg2) {
|
||||
this(config -> function.apply(arg0.apply(config), arg1.apply(config), arg2.apply(config)));
|
||||
constructorFields = Arrays.asList(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public <Arg0, Arg1, Arg2, Arg3> ConstructingObjectParser(Function4<Arg0, Arg1, Arg2, Arg3, Value> function, Field<Arg0> arg0, Field<Arg1> arg1, Field<Arg2> arg2, Field<Arg3> arg3) {
|
||||
this(config -> function.apply(arg0.apply(config), arg1.apply(config), arg2.apply(config), arg3.apply(config)));
|
||||
constructorFields = Arrays.asList(arg0, arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public <Arg0, Arg1, Arg2, Arg3, Arg4> ConstructingObjectParser(Function5<Arg0, Arg1, Arg2, Arg3, Arg4, Value> function, Field<Arg0> arg0, Field<Arg1> arg1, Field<Arg2> arg2, Field<Arg3> arg3, Field<Arg4> arg4) {
|
||||
this(config -> function.apply(arg0.apply(config), arg1.apply(config), arg2.apply(config), arg3.apply(config), arg4.apply(config)));
|
||||
constructorFields = Arrays.asList(arg0, arg1, arg2, arg3, arg4);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public <Arg0, Arg1, Arg2, Arg3, Arg4, Arg5> ConstructingObjectParser(Function6<Arg0, Arg1, Arg2, Arg3, Arg4, Arg5, Value> function, Field<Arg0> arg0, Field<Arg1> arg1, Field<Arg2> arg2, Field<Arg3> arg3, Field<Arg4> arg4, Field<Arg5> arg5) {
|
||||
this(config -> function.apply(arg0.apply(config), arg1.apply(config), arg2.apply(config), arg3.apply(config), arg4.apply(config), arg5.apply(config)));
|
||||
constructorFields = Arrays.asList(arg0, arg1, arg2, arg3, arg4, arg5);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public <Arg0, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6> ConstructingObjectParser(Function7<Arg0, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Value> function, Field<Arg0> arg0, Field<Arg1> arg1, Field<Arg2> arg2, Field<Arg3> arg3, Field<Arg4> arg4, Field<Arg5> arg5, Field<Arg6> arg6) {
|
||||
this(config -> function.apply(arg0.apply(config), arg1.apply(config), arg2.apply(config), arg3.apply(config), arg4.apply(config), arg5.apply(config), arg6.apply(config)));
|
||||
constructorFields = Arrays.asList(arg0, arg1, arg2, arg3, arg4, arg5, arg6);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public <Arg0, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7> ConstructingObjectParser(Function8<Arg0, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Value> function, Field<Arg0> arg0, Field<Arg1> arg1, Field<Arg2> arg2, Field<Arg3> arg3, Field<Arg4> arg4, Field<Arg5> arg5, Field<Arg6> arg6, Field<Arg7> arg7) {
|
||||
this(config -> function.apply(arg0.apply(config), arg1.apply(config), arg2.apply(config), arg3.apply(config), arg4.apply(config), arg5.apply(config), arg6.apply(config), arg7.apply(config)));
|
||||
constructorFields = Arrays.asList(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public <Arg0, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8> ConstructingObjectParser(Function9<Arg0, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8, Value> function, Field<Arg0> arg0, Field<Arg1> arg1, Field<Arg2> arg2, Field<Arg3> arg3, Field<Arg4> arg4, Field<Arg5> arg5, Field<Arg6> arg6, Field<Arg7> arg7, Field<Arg8> arg8) {
|
||||
this(config -> function.apply(arg0.apply(config), arg1.apply(config), arg2.apply(config), arg3.apply(config), arg4.apply(config), arg5.apply(config), arg6.apply(config), arg7.apply(config), arg8.apply(config)));
|
||||
constructorFields = Arrays.asList(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);
|
||||
}
|
||||
|
||||
private ConstructingObjectParser(Function<Map<String, Object>, Value> builder) {
|
||||
this.builder = builder;
|
||||
constructorArgs = new HashMap<>();
|
||||
}
|
||||
|
||||
private static <Value> Value construct(Map<String, Object> config, Function<Object[], Value> builder, Field<?>... constructorFields) throws IllegalArgumentException {
|
||||
Object[] builderArgs = new Object[constructorFields.length];
|
||||
int i = 0;
|
||||
for (Field<?> field : constructorFields) {
|
||||
final String name = field.getName();
|
||||
|
||||
if (config.containsKey(name)) {
|
||||
builderArgs[i] = field.apply(config.get(name));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Missing required argument '" + name + "'");
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return builder.apply(builderArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -78,49 +148,8 @@ public class ConstructingObjectParser<Value> implements ObjectParser<Value> {
|
|||
}
|
||||
|
||||
BiConsumer<Value, Object> objConsumer = (value, object) -> consumer.accept(value, transform.apply(object));
|
||||
FieldDefinition<Value> field = new FieldDefinition<>(objConsumer, FieldUsage.Field);
|
||||
parsers.put(name, field);
|
||||
return field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare a constructor argument. Constructor arguments are implicitly ordered by the order they are executed.
|
||||
* <p>
|
||||
* When calling `apply`, all constructor arguments are considered required. If missing, `apply` will throw an exception.
|
||||
* <p>
|
||||
* <code>{@code
|
||||
* ConstructingObjectParser<Integer> c = new ConstructingObjectParser<>(args -> new Integer((int) args[0])0;
|
||||
* c.declareInteger("number");
|
||||
* <p>
|
||||
* // alternately, the longer way:
|
||||
* //c.declareConstructorArg("number", ObjectTransform::transformInteger)
|
||||
* <p>
|
||||
* Integer i = c.apply(Collections.singletonMap("number", 100);
|
||||
* // i == 100
|
||||
* }</code>
|
||||
*
|
||||
* @param name The name of this constructor argument
|
||||
* @param transform The function to transform an Object to the specified T type
|
||||
* @param <T> the type of value expected for this constructor arg.
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public <T> Field declareConstructorArg(String name, Function<Object, T> transform) {
|
||||
if (isKnownField(name)) {
|
||||
throw new IllegalArgumentException("Duplicate field defined '" + name + "'");
|
||||
}
|
||||
|
||||
final int position = constructorArgs.size();
|
||||
BiConsumer<Object[], Object> objConsumer = (array, object) -> array[position] = transform.apply(object);
|
||||
FieldDefinition<Object[]> field = new FieldDefinition<>(objConsumer, FieldUsage.Constructor);
|
||||
try {
|
||||
constructorArgs.put(name, field);
|
||||
} catch (UnsupportedOperationException e) {
|
||||
// This will be thrown when this ConstructingObjectParser is created with a Supplier (which takes no arguments)
|
||||
// for example, new ConstructingObjectParser<>((Supplier<String>) String::new)
|
||||
throw new UnsupportedOperationException("Cannot add constructor args because the constructor doesn't take any arguments!");
|
||||
}
|
||||
FieldDefinition<T> field = new FieldDefinition<T>(name, transform);
|
||||
parsers.put(name, (value, input) -> consumer.accept(value, transform.apply(input)));
|
||||
return field;
|
||||
}
|
||||
|
||||
|
@ -139,21 +168,20 @@ public class ConstructingObjectParser<Value> implements ObjectParser<Value> {
|
|||
public Value apply(Map<String, Object> config) {
|
||||
rejectUnknownFields(config.keySet());
|
||||
|
||||
Value value = construct(config);
|
||||
Value value = this.builder.apply(config);
|
||||
|
||||
// Now call all the object setters/etc
|
||||
for (Map.Entry<String, Object> entry : config.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
if (constructorArgs.containsKey(name)) {
|
||||
// Skip constructor arguments
|
||||
if (isConstructorField(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FieldDefinition<Value> field = parsers.get(name);
|
||||
assert field != null;
|
||||
BiConsumer<Value, Object> parser = parsers.get(name);
|
||||
assert parser != null;
|
||||
|
||||
try {
|
||||
field.accept(value, entry.getValue());
|
||||
parser.accept(value, entry.getValue());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("Field " + name + ": " + e.getMessage(), e);
|
||||
}
|
||||
|
@ -162,6 +190,38 @@ public class ConstructingObjectParser<Value> implements ObjectParser<Value> {
|
|||
return value;
|
||||
}
|
||||
|
||||
private boolean isConstructorField(String name) {
|
||||
return constructorFields.stream().anyMatch(f -> f.getName().equals(name));
|
||||
}
|
||||
|
||||
private boolean isKnownField(String name) {
|
||||
return parsers.containsKey(name) || isConstructorField(name);
|
||||
}
|
||||
|
||||
public interface Function3<Arg0, Arg1, Arg2, Value> {
|
||||
Value apply(Arg0 arg0, Arg1 arg1, Arg2 arg2);
|
||||
}
|
||||
|
||||
public interface Function4<Arg0, Arg1, Arg2, Arg3, Value> {
|
||||
Value apply(Arg0 arg0, Arg1 arg1, Arg2 arg2, Arg3 arg3);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param builder A function which takes an Object[] as argument and returns a Value instance
|
||||
*/
|
||||
//@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
//public ConstructingObjectParser(Function<Object[], Value> builder, Field<?>... fields) {
|
||||
//this(config -> construct(config, builder, fields), fields);
|
||||
//}
|
||||
|
||||
public interface Function5<Arg0, Arg1, Arg2, Arg3, Arg4, Value> {
|
||||
Value apply(Arg0 arg0, Arg1 arg1, Arg2 arg2, Arg3 arg3, Arg4 arg4);
|
||||
}
|
||||
|
||||
public interface Function6<Arg0, Arg1, Arg2, Arg3, Arg4, Arg5, Value> {
|
||||
Value apply(Arg0 arg0, Arg1 arg1, Arg2 arg2, Arg3 arg3, Arg4 arg4, Arg5 arg5);
|
||||
}
|
||||
|
||||
private void rejectUnknownFields(Set<String> configNames) throws IllegalArgumentException {
|
||||
// Check for any unknown parameters.
|
||||
List<String> unknown = configNames.stream().filter(name -> !isKnownField(name)).collect(Collectors.toList());
|
||||
|
@ -171,31 +231,15 @@ public class ConstructingObjectParser<Value> implements ObjectParser<Value> {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isKnownField(String name) {
|
||||
return (parsers.containsKey(name) || constructorArgs.containsKey(name));
|
||||
public interface Function7<Arg0, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Value> {
|
||||
Value apply(Arg0 arg0, Arg1 arg1, Arg2 arg2, Arg3 arg3, Arg4 arg4, Arg5 arg5, Arg6 arg6);
|
||||
}
|
||||
|
||||
private Value construct(Map<String, Object> config) throws IllegalArgumentException {
|
||||
Object[] args = new Object[constructorArgs.size()];
|
||||
public interface Function8<Arg0, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Value> {
|
||||
Value apply(Arg0 arg0, Arg1 arg1, Arg2 arg2, Arg3 arg3, Arg4 arg4, Arg5 arg5, Arg6 arg6, Arg7 arg7);
|
||||
}
|
||||
|
||||
// Constructor arguments. Any constructor argument is a *required* setting.
|
||||
for (Map.Entry<String, FieldDefinition<Object[]>> argInfo : constructorArgs.entrySet()) {
|
||||
String name = argInfo.getKey();
|
||||
FieldDefinition<Object[]> field = argInfo.getValue();
|
||||
|
||||
if (config.containsKey(name)) {
|
||||
if (field.isObsolete()) {
|
||||
throw new IllegalArgumentException("Field '" + name + "' is obsolete and may not be used. " + field.getDetails());
|
||||
} else if (field.isDeprecated()) {
|
||||
logger.warn("Field '" + name + "' is deprecated and should be avoided. " + field.getDetails());
|
||||
}
|
||||
|
||||
field.accept(args, config.get(name));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Missing required argument '" + name);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.apply(args);
|
||||
public interface Function9<Arg0, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8, Value> {
|
||||
Value apply(Arg0 arg0, Arg1 arg1, Arg2 arg2, Arg3 arg3, Arg4 arg4, Arg5 arg5, Arg6 arg6, Arg7 arg7, Arg8 arg8);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,59 @@
|
|||
package org.logstash.common.parser;
|
||||
|
||||
public interface Field {
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
public interface Field<Value> extends Function<Object, Value> {
|
||||
Field setDeprecated(String details);
|
||||
|
||||
static Field<String> declareString(String name) {
|
||||
return declareField(name, ObjectTransforms::transformString);
|
||||
}
|
||||
Field setObsolete(String details);
|
||||
|
||||
static Field<Float> declareFloat(String name) {
|
||||
return declareField(name, ObjectTransforms::transformFloat);
|
||||
}
|
||||
|
||||
static Field<Long> declareLong(String name) {
|
||||
return declareField(name, ObjectTransforms::transformLong);
|
||||
}
|
||||
|
||||
static Field<Double> declareDouble(String name) {
|
||||
return declareField(name, ObjectTransforms::transformDouble);
|
||||
}
|
||||
|
||||
static Field<Boolean> declareBoolean(String name) {
|
||||
return declareField(name, ObjectTransforms::transformBoolean);
|
||||
}
|
||||
|
||||
static Field<Integer> declareInteger(String name) {
|
||||
return declareField(name, ObjectTransforms::transformInteger);
|
||||
}
|
||||
|
||||
static Field<Map<String, Object>> declareMap(String name) {
|
||||
return declareField(name, ObjectTransforms::transformMap);
|
||||
}
|
||||
|
||||
static <V> Field<List<V>> declareList(String name, Function<Object, V> transform) {
|
||||
return new FieldDefinition<>(name, (object) -> ObjectTransforms.transformList(object, transform));
|
||||
}
|
||||
|
||||
static <V> Field<V> declareField(String name, Function<Object, V> transform) {
|
||||
return new FieldDefinition<>(name, transform);
|
||||
}
|
||||
|
||||
boolean isDeprecated();
|
||||
|
||||
boolean isObsolete();
|
||||
|
||||
String getName();
|
||||
|
||||
String getDetails();
|
||||
|
||||
default Value apply(Map<String, Object> map) {
|
||||
return apply(map.get(getName()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
package org.logstash.common.parser;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
class FieldDefinition<Value> implements Field, BiConsumer<Value, Object> {
|
||||
private final FieldUsage usage;
|
||||
private final BiConsumer<Value, Object> consumer;
|
||||
class FieldDefinition<Value> implements Field<Value> {
|
||||
private final Function<Object, Value> transform;
|
||||
|
||||
private String details;
|
||||
private String name;
|
||||
|
||||
// This is only set if deprecated or obsolete
|
||||
// XXX: Move this concept separately to DeprecatedFieldDefinition and ObsoleteFieldDefinition
|
||||
private FieldStatus status;
|
||||
private String details;
|
||||
|
||||
FieldDefinition(BiConsumer<Value, Object> consumer, FieldUsage usage) {
|
||||
this.consumer = consumer;
|
||||
this.usage = usage;
|
||||
FieldDefinition(String name, Function<Object, Value> transform) {
|
||||
this.name = name;
|
||||
this.transform = transform;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -22,9 +25,6 @@ class FieldDefinition<Value> implements Field, BiConsumer<Value, Object> {
|
|||
|
||||
@Override
|
||||
public Field setObsolete(String details) {
|
||||
if (usage == FieldUsage.Constructor) {
|
||||
throw new IllegalArgumentException("Constructor arguments cannot be made obsolete.");
|
||||
}
|
||||
setStatus(FieldStatus.Obsolete, details);
|
||||
return this;
|
||||
}
|
||||
|
@ -35,19 +35,26 @@ class FieldDefinition<Value> implements Field, BiConsumer<Value, Object> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void accept(Value value, Object object) {
|
||||
consumer.accept(value, object);
|
||||
public Value apply(Object object) {
|
||||
return transform.apply(object);
|
||||
}
|
||||
|
||||
boolean isDeprecated() {
|
||||
@Override
|
||||
public boolean isDeprecated() {
|
||||
return status == FieldStatus.Deprecated;
|
||||
}
|
||||
|
||||
boolean isObsolete() {
|
||||
@Override
|
||||
public boolean isObsolete() {
|
||||
return status == FieldStatus.Obsolete;
|
||||
}
|
||||
|
||||
String getDetails() {
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDetails() {
|
||||
return details;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import java.util.function.Function;
|
|||
public interface ObjectParser<Value> extends Function<Map<String, Object>, Value> {
|
||||
<T> Field declareField(String name, BiConsumer<Value, T> consumer, Function<Object, T> transform);
|
||||
|
||||
<T> Field declareConstructorArg(String name, Function<Object, T> transform);
|
||||
//<T> Field declareConstructorArg(String name, Function<Object, T> transform);
|
||||
|
||||
/**
|
||||
* Add an field with an long value.
|
||||
|
@ -20,15 +20,6 @@ public interface ObjectParser<Value> extends Function<Map<String, Object>, Value
|
|||
return declareField(name, consumer, ObjectTransforms::transformLong);
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare an long constructor argument.
|
||||
*
|
||||
* @param name the name of the field.
|
||||
*/
|
||||
default Field declareLong(String name) {
|
||||
return declareConstructorArg(name, ObjectTransforms::transformLong);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an field with an integer value.
|
||||
*
|
||||
|
@ -39,15 +30,6 @@ public interface ObjectParser<Value> extends Function<Map<String, Object>, Value
|
|||
return declareField(name, consumer, ObjectTransforms::transformInteger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare an integer constructor argument.
|
||||
*
|
||||
* @param name the name of the field.
|
||||
*/
|
||||
default Field declareInteger(String name) {
|
||||
return declareConstructorArg(name, ObjectTransforms::transformInteger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a field with a string value.
|
||||
*
|
||||
|
@ -58,15 +40,6 @@ public interface ObjectParser<Value> extends Function<Map<String, Object>, Value
|
|||
return declareField(name, consumer, ObjectTransforms::transformString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare a constructor argument that is a string.
|
||||
*
|
||||
* @param name the name of this field.
|
||||
*/
|
||||
default Field declareString(String name) {
|
||||
return declareConstructorArg(name, ObjectTransforms::transformString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare a field with a List containing T instances
|
||||
*
|
||||
|
@ -79,42 +52,14 @@ public interface ObjectParser<Value> extends Function<Map<String, Object>, Value
|
|||
return declareField(name, consumer, object -> ObjectTransforms.transformList(object, transform));
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare a constructor argument which is a List
|
||||
*
|
||||
* @param name The name of the argument.
|
||||
* @param transform The object -> T transform function
|
||||
* @param <T> The type of object contained in the list.
|
||||
*/
|
||||
default <T> Field declareList(String name, Function<Object, T> transform) {
|
||||
return declareConstructorArg(name, (object) -> ObjectTransforms.transformList(object, transform));
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare a constructor argument that is a float.
|
||||
*
|
||||
* @param name the name of the argument
|
||||
*/
|
||||
default Field declareFloat(String name) {
|
||||
return declareConstructorArg(name, ObjectTransforms::transformFloat);
|
||||
}
|
||||
|
||||
default Field declareFloat(String name, BiConsumer<Value, Float> consumer) {
|
||||
return declareField(name, consumer, ObjectTransforms::transformFloat);
|
||||
}
|
||||
|
||||
default Field declareDouble(String name) {
|
||||
return declareConstructorArg(name, ObjectTransforms::transformDouble);
|
||||
}
|
||||
|
||||
default Field declareDouble(String name, BiConsumer<Value, Double> consumer) {
|
||||
return declareField(name, consumer, ObjectTransforms::transformDouble);
|
||||
}
|
||||
|
||||
default Field declareBoolean(String name) {
|
||||
return declareConstructorArg(name, ObjectTransforms::transformBoolean);
|
||||
}
|
||||
|
||||
default Field declareBoolean(String name, BiConsumer<Value, Boolean> consumer) {
|
||||
return declareField(name, consumer, ObjectTransforms::transformBoolean);
|
||||
}
|
||||
|
@ -130,16 +75,4 @@ public interface ObjectParser<Value> extends Function<Map<String, Object>, Value
|
|||
default <T> Field declareObject(String name, BiConsumer<Value, T> consumer, ConstructingObjectParser<T> parser) {
|
||||
return declareField(name, consumer, (t) -> ObjectTransforms.transformObject(t, parser));
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare a constructor argument that is an object.
|
||||
*
|
||||
* @param name the name of the field which represents this constructor argument
|
||||
* @param parser the ConstructingObjectParser that builds the object
|
||||
* @param <T> The type of object created by the parser.
|
||||
*/
|
||||
default <T> Field declareObject(String name, ConstructingObjectParser<T> parser) {
|
||||
return declareConstructorArg(name, (t) -> ObjectTransforms.transformObject(t, parser));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -108,4 +108,13 @@ public class ObjectTransforms {
|
|||
throw new IllegalArgumentException("Object value must be a List, but is a " + object.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, Object> transformMap(Object object) {
|
||||
if (object instanceof Map) {
|
||||
// XXX: Validate all entries in this map for the cast?
|
||||
return (Map<String, Object>) object;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Expected a map `{ ... }` but got a " + object.getClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import java.util.HashMap;
|
|||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
@ -27,16 +28,35 @@ public class ConstructingObjectParserTest {
|
|||
// Exists to do return type compile-time checks.
|
||||
// no body is needed
|
||||
}
|
||||
|
||||
public static class MixedUsageTest {
|
||||
private Map<String, Object> config = new HashMap<>();
|
||||
private int foo = 1000; // XXX: randomize
|
||||
private String bar = "hello"; // XXX: randomize
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
config.put("foo", foo);
|
||||
config.put("bar", bar);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGoodConstruction() {
|
||||
MixedExample example = MixedExample.BUILDER.apply(config);
|
||||
assertEquals(foo, example.getFoo());
|
||||
assertEquals(bar, example.getBar());
|
||||
}
|
||||
}
|
||||
|
||||
public static class FieldIntegrationTest {
|
||||
private final ConstructingObjectParser<Example> EXAMPLE_BUILDER = new ConstructingObjectParser<>(Example::new);
|
||||
private final ConstructingObjectParser<Path> PATH_BUILDER = new ConstructingObjectParser<>(args -> Paths.get((String) args[0]));
|
||||
private final ConstructingObjectParser<Path> PATH_BUILDER = new ConstructingObjectParser<Path>(Paths::get, Field.declareString("path"));
|
||||
|
||||
private final Map<String, Object> config = new HashMap<>();
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
check(PATH_BUILDER.declareString("path"));
|
||||
check(EXAMPLE_BUILDER.declareFloat("float", Example::setF));
|
||||
check(EXAMPLE_BUILDER.declareInteger("integer", Example::setI));
|
||||
check(EXAMPLE_BUILDER.declareLong("long", Example::setL));
|
||||
|
@ -46,10 +66,10 @@ public class ConstructingObjectParserTest {
|
|||
check(EXAMPLE_BUILDER.declareList("stringList", Example::setStringList, ObjectTransforms::transformString));
|
||||
|
||||
// Custom transform (Object => Path)
|
||||
check(EXAMPLE_BUILDER.declareString("path", (example, path) -> example.setP1(Paths.get(path))));
|
||||
check(EXAMPLE_BUILDER.declareString("path", (example, path) -> example.setP(Paths.get(path))));
|
||||
|
||||
// Custom nested object constructor: { "object": { "path": "some path" } }
|
||||
check(EXAMPLE_BUILDER.declareObject("object", Example::setP2, PATH_BUILDER));
|
||||
//check(EXAMPLE_BUILDER.declareObject("object", Example::setP2, PATH_BUILDER));
|
||||
|
||||
config.put("float", 1F);
|
||||
config.put("integer", 1);
|
||||
|
@ -72,29 +92,21 @@ public class ConstructingObjectParserTest {
|
|||
assertEquals(Collections.singletonList("hello"), e.getStringList());
|
||||
|
||||
// because they are not set and the default in the Example class is null.
|
||||
assertNull(e.getP1());
|
||||
assertNull(e.getP2());
|
||||
assertNull(e.getP());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomTransform() {
|
||||
config.put("path", "example");
|
||||
Example e = EXAMPLE_BUILDER.apply(config);
|
||||
assertEquals(Paths.get("example"), e.getP1());
|
||||
assertEquals(Paths.get("example"), e.getP());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNestedObject() {
|
||||
config.put("object", Collections.singletonMap("path", "example"));
|
||||
Example e = EXAMPLE_BUILDER.apply(config);
|
||||
assertEquals(Paths.get("example"), e.getP2());
|
||||
}
|
||||
|
||||
@Test(expected = UnsupportedOperationException.class)
|
||||
public void testInvalidConstructor() {
|
||||
// EXAMPLE_BUILDER gives a Supplier (not a Function<Object[], ...>), so constructor arguments
|
||||
// should be disabled and this call should fail.
|
||||
check(EXAMPLE_BUILDER.declareString("invalid"));
|
||||
//assertEquals(Paths.get("example"), e.getP2());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
|
@ -106,30 +118,24 @@ public class ConstructingObjectParserTest {
|
|||
}
|
||||
|
||||
public static class ConstructorIntegrationTest {
|
||||
private final ConstructingObjectParser<Example> EXAMPLE_BUILDER = new ConstructingObjectParser<>(args -> new Example((int) args[0], (float) args[1], (long) args[2], (double) args[3], (boolean) args[4], (String) args[5], (Path) args[6], (Path) args[7], (List<String>) args[8]));
|
||||
private final ConstructingObjectParser<Path> PATH_BUILDER = new ConstructingObjectParser<>(args -> Paths.get((String) args[0]));
|
||||
private final ConstructingObjectParser<Example> EXAMPLE_BUILDER = new ConstructingObjectParser<Example>(
|
||||
Example::new,
|
||||
Field.declareInteger("integer"), // arg0
|
||||
Field.declareFloat("float"), // arg0
|
||||
Field.declareLong("long"), // arg2 ...
|
||||
Field.declareDouble("double"),
|
||||
Field.declareBoolean("boolean"),
|
||||
Field.declareString("string"),
|
||||
Field.declareField("path", object -> Paths.get(ObjectTransforms.transformString(object))),
|
||||
Field.declareList("strings", ObjectTransforms::transformString)
|
||||
);
|
||||
|
||||
private final ConstructingObjectParser<Path> PATH_BUILDER = new ConstructingObjectParser<>(Paths::get, Field.declareString("path"));
|
||||
|
||||
private final Map<String, Object> config = new LinkedHashMap<>();
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
check(PATH_BUILDER.declareString("path"));
|
||||
|
||||
check(EXAMPLE_BUILDER.declareInteger("integer"));
|
||||
check(EXAMPLE_BUILDER.declareFloat("float"));
|
||||
check(EXAMPLE_BUILDER.declareLong("long"));
|
||||
check(EXAMPLE_BUILDER.declareDouble("double"));
|
||||
check(EXAMPLE_BUILDER.declareBoolean("boolean"));
|
||||
check(EXAMPLE_BUILDER.declareString("string"));
|
||||
|
||||
// Custom transform (Object => Path)
|
||||
check(EXAMPLE_BUILDER.declareConstructorArg("path", (object) -> Paths.get((String) object)));
|
||||
|
||||
// Custom nested object constructor: { "object": { "path": "some path" } }
|
||||
check(EXAMPLE_BUILDER.declareObject("object", PATH_BUILDER));
|
||||
|
||||
check(EXAMPLE_BUILDER.declareList("stringList", ObjectTransforms::transformString));
|
||||
|
||||
config.put("float", 1F);
|
||||
config.put("integer", 1);
|
||||
config.put("long", 1L);
|
||||
|
@ -150,8 +156,7 @@ public class ConstructingObjectParserTest {
|
|||
assertEquals(1L, e.getL());
|
||||
assertEquals(true, e.isB());
|
||||
assertEquals("hello", e.getS());
|
||||
assertEquals(Paths.get("path1"), e.getP1());
|
||||
assertEquals(Paths.get("path2"), e.getP2());
|
||||
assertEquals(Paths.get("path"), e.getP());
|
||||
|
||||
assertEquals(Collections.singletonList("hello"), e.getStringList());
|
||||
}
|
||||
|
@ -165,8 +170,8 @@ public class ConstructingObjectParserTest {
|
|||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testDuplicateConstructorFieldsAreRejected() {
|
||||
// field 'float' is already defined, so this should fail.
|
||||
check(EXAMPLE_BUILDER.declareString("float"));
|
||||
String name = "foo";
|
||||
new ConstructingObjectParser<Path>(Paths::get, Field.declareString(name), Field.declareString(name));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -224,13 +229,16 @@ public class ConstructingObjectParserTest {
|
|||
}
|
||||
|
||||
public static class DeprecationsAndObsoletes {
|
||||
final ConstructingObjectParser<Example> c = new ConstructingObjectParser<>((args) -> new Example());
|
||||
final ConstructingObjectParser<Example> c = new ConstructingObjectParser<>(Example::new);
|
||||
final BiConsumer<Example, Integer> noOp = (a, b) -> { /* empty */ };
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
check(c.declareInteger("deprecated").setDeprecated("This setting will warn the user when used."));
|
||||
check(c.declareInteger("obsolete", (a, b) -> {
|
||||
}).setObsolete("This setting should cause a failure when someone uses it."));
|
||||
check(c.declareInteger("deprecated", noOp).setDeprecated("This setting will warn the user when used."));
|
||||
check(c.declareInteger("obsolete", noOp).setObsolete("This setting should cause a failure when someone uses it."));
|
||||
}
|
||||
|
||||
private static class Example {
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -239,12 +247,5 @@ public class ConstructingObjectParserTest {
|
|||
c.apply(Collections.singletonMap("deprecated", 1));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void obsoletesOnConstructorArgsIsInvalid() {
|
||||
c.declareInteger("fail").setObsolete("...");
|
||||
}
|
||||
|
||||
private static class Example {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,22 +11,20 @@ class Example {
|
|||
private boolean b;
|
||||
private long l;
|
||||
private String s;
|
||||
private Path p1;
|
||||
private Path p2;
|
||||
private Path p;
|
||||
private List<String> stringList;
|
||||
|
||||
Example() {
|
||||
}
|
||||
|
||||
Example(int i, float f, long l, double d, boolean b, String s, Path p1, Path p2, List<String> stringList) {
|
||||
Example(int i, float f, long l, double d, boolean b, String s, Path p, List<String> stringList) {
|
||||
this.i = i;
|
||||
this.f = f;
|
||||
this.l = l;
|
||||
this.d = d;
|
||||
this.b = b;
|
||||
this.s = s;
|
||||
this.p1 = p1;
|
||||
this.p2 = p2;
|
||||
this.p = p;
|
||||
this.stringList = stringList;
|
||||
}
|
||||
|
||||
|
@ -78,20 +76,12 @@ class Example {
|
|||
this.s = s;
|
||||
}
|
||||
|
||||
Path getP1() {
|
||||
return p1;
|
||||
Path getP() {
|
||||
return p;
|
||||
}
|
||||
|
||||
void setP1(Path p1) {
|
||||
this.p1 = p1;
|
||||
}
|
||||
|
||||
Path getP2() {
|
||||
return p2;
|
||||
}
|
||||
|
||||
void setP2(Path p2) {
|
||||
this.p2 = p2;
|
||||
void setP(Path p) {
|
||||
this.p = p;
|
||||
}
|
||||
|
||||
List<String> getStringList() {
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package org.logstash.common.parser;
|
||||
|
||||
class MixedExample {
|
||||
static final ConstructingObjectParser<MixedExample> BUILDER = new ConstructingObjectParser<MixedExample>(MixedExample::new, Field.declareInteger("foo"));
|
||||
|
||||
static {
|
||||
BUILDER.declareString("bar", MixedExample::setBar);
|
||||
}
|
||||
|
||||
private int foo;
|
||||
private String bar;
|
||||
|
||||
public MixedExample(int foo) {
|
||||
this.foo = foo;
|
||||
}
|
||||
|
||||
|
||||
public int getFoo() {
|
||||
return foo;
|
||||
}
|
||||
|
||||
public String getBar() {
|
||||
return bar;
|
||||
}
|
||||
|
||||
public void setBar(String bar) {
|
||||
this.bar = bar;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package org.logstash.plugin;
|
|||
|
||||
import org.logstash.Event;
|
||||
import org.logstash.common.parser.ConstructingObjectParser;
|
||||
import org.logstash.common.parser.Field;
|
||||
import org.logstash.common.parser.ObjectTransforms;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
@ -11,18 +12,21 @@ import java.util.Map;
|
|||
import java.util.function.Function;
|
||||
|
||||
class TranslateFilterPlugin {
|
||||
private final static ConstructingObjectParser<TranslateFilter> TRANSLATE = new ConstructingObjectParser<>(TranslateFilterPlugin::newFilter);
|
||||
final static ConstructingObjectParser<TranslateFilter> TRANSLATE = new ConstructingObjectParser<TranslateFilter>(
|
||||
TranslateFilterPlugin::newFilter,
|
||||
Field.declareString("field"),
|
||||
|
||||
// Things get a bit weird here. I can explain. The translate filter has two modes of operation:
|
||||
// One, where the user specifies a 'dictionary'. Two, where the user specifies 'dictionary_path'.
|
||||
// These two modes are mutually exclusive, so it makes sense to have a custom builder method to
|
||||
// determine which mode to use based on the given arguments. See TranslateFilter.newFilter.
|
||||
// It's unclear if this modality is a desirable feature, but I am only porting the translate filter
|
||||
// in this scenario as a a way to demonstrate and test the capabilities of ConstructingObjectParser.
|
||||
Field.declareMap("dictionary"),
|
||||
Field.declareField("dictionary_path", TranslateFilterPlugin::parsePath)
|
||||
);
|
||||
|
||||
static {
|
||||
TRANSLATE.declareString("field");
|
||||
|
||||
// Things get a bit weird here. I can explain. The translate filter has two modes of operation:
|
||||
// One, where the user specifies a 'dictionary'. Two, where the user specifies 'dictionary_path'.
|
||||
// These two modes are mutually exclusive, so it makes sense to have a custom builder method to
|
||||
// determine which mode to use based on the given arguments. See TranslateFilter.newFilter.
|
||||
TRANSLATE.declareConstructorArg("dictionary", (config) -> config);
|
||||
TRANSLATE.declareConstructorArg("dictionary_path", (object) -> Paths.get(ObjectTransforms.transformString(object)));
|
||||
|
||||
// These are all nice settings that are optional in the plugin.
|
||||
TRANSLATE.declareString("destination", TranslateFilter::setDestination);
|
||||
TRANSLATE.declareBoolean("exact", TranslateFilter::setExact);
|
||||
|
@ -34,18 +38,27 @@ class TranslateFilterPlugin {
|
|||
TRANSLATE.declareInteger("refresh_interval", TranslateFilterPlugin::setRefreshInterval);
|
||||
}
|
||||
|
||||
private static TranslateFilter newFilter(Object[] args) {
|
||||
String source = (String) args[0];
|
||||
if (args[1] != null && args[2] != null) {
|
||||
private static Path parsePath(Object input) {
|
||||
Path path = Paths.get(ObjectTransforms.transformString(input));
|
||||
|
||||
if (!path.toFile().exists()) {
|
||||
throw new IllegalArgumentException("The given path does not exist: " + path);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private static TranslateFilter newFilter(String source, Map<String, Object> map, Path path) {
|
||||
if (map != null && path != null) {
|
||||
throw new IllegalArgumentException("You must specify either dictionary or dictionary_path, not both. Both are set.");
|
||||
}
|
||||
|
||||
if (args[1] != null) {
|
||||
if (map != null) {
|
||||
// "dictionary" field was set, so args[1] is a map.
|
||||
return new TranslateFilter(source, (Map<String, Object>) args[1]);
|
||||
return new TranslateFilter(source, map);
|
||||
} else {
|
||||
// dictionary_path set, so let's use a file-backed translate filter.
|
||||
return new FileBackedTranslateFilter(source, (Path) args[2]);
|
||||
return new FileBackedTranslateFilter(source, path);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,6 +83,8 @@ class TranslateFilterPlugin {
|
|||
private boolean override = false;
|
||||
private boolean regex = false;
|
||||
|
||||
TranslateFilter(String source, Map<String, Object> map, Path path) { /* ... */ }
|
||||
|
||||
TranslateFilter(String source, Map<String, Object> map) {
|
||||
this(source);
|
||||
this.map = map;
|
|
@ -0,0 +1,18 @@
|
|||
package org.logstash.plugin;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class TranslateFilterPluginTest {
|
||||
@Test
|
||||
public void exampleConfig() {
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
config.put("dictionary", Collections.emptyMap());
|
||||
config.put("field", "fancy");
|
||||
|
||||
TranslateFilterPlugin.TranslateFilter filter = TranslateFilterPlugin.TRANSLATE.apply(config);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue