Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(TCOMP-2337): Validation on Array #712

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -890,7 +890,7 @@ public interface Messages {
}

@RequiredArgsConstructor
private static class PayloadValidator implements PayloadMapper.OnParameter {
protected static class PayloadValidator implements PayloadMapper.OnParameter {

private static final VisibilityService VISIBILITY_SERVICE = new VisibilityService(JsonProvider.provider());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,16 @@
import static lombok.AccessLevel.PRIVATE;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import javax.json.JsonArrayBuilder;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonPointer;
Expand All @@ -40,6 +44,7 @@

import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

@RequiredArgsConstructor
public class VisibilityService {
Expand All @@ -51,7 +56,8 @@ public class VisibilityService {
public ConditionGroup build(final ParameterMeta param) {
final boolean and =
"AND".equalsIgnoreCase(param.getMetadata().getOrDefault("tcomp::condition::ifs::operator", "AND"));
return new ConditionGroup(param
Map<String, Condition> conditions = new HashMap<>();
ConditionGroup group = new ConditionGroup(param
.getMetadata()
.entrySet()
.stream()
Expand All @@ -63,12 +69,32 @@ public ConditionGroup build(final ParameterMeta param) {
final String negateKey = "tcomp::condition::if::negate" + index;
final String evaluationStrategyKey = "tcomp::condition::if::evaluationStrategy" + index;
final String absoluteTargetPath = pathResolver.resolveProperty(param.getPath(), meta.getValue());
return new Condition(toPointer(absoluteTargetPath),
Boolean.parseBoolean(param.getMetadata().getOrDefault(negateKey, "false")),
param.getMetadata().getOrDefault(evaluationStrategyKey, "DEFAULT").toUpperCase(ROOT),
param.getMetadata().getOrDefault(valueKey, "true").split(","));
Condition condition = conditions.get(absoluteTargetPath);
if (condition == null) {
condition = new Condition('/' + absoluteTargetPath.replace('.', '/'),
toPointer(absoluteTargetPath),
Boolean.parseBoolean(param.getMetadata().getOrDefault(negateKey, "false")),
param.getMetadata().getOrDefault(evaluationStrategyKey, "DEFAULT").toUpperCase(ROOT),
param.getMetadata().getOrDefault(valueKey, "true").split(","));
conditions.put(absoluteTargetPath, condition);
} else {
List<String> collection = Arrays.stream(condition.values).collect(toList());
collection.add(String.valueOf(param.getMetadata().getOrDefault(valueKey, "true")));
condition = new Condition('/' + absoluteTargetPath.replace('.', '/'),
toPointer(absoluteTargetPath),
Boolean.parseBoolean(param.getMetadata().getOrDefault(negateKey, "false")),
param.getMetadata().getOrDefault(evaluationStrategyKey, "DEFAULT").toUpperCase(ROOT),
collection.toArray(new String[0]));
conditions.replace(absoluteTargetPath, condition);
}
return condition;
})
.collect(toList()), and ? stream -> stream.allMatch(i -> i) : stream -> stream.anyMatch(i -> i));
if (and) {
group.conditions.clear();
group.conditions.addAll(conditions.values());
}
return group;
}

private JsonPointer toPointer(final String absoluteTargetPath) {
Expand Down Expand Up @@ -119,13 +145,16 @@ private String doResolveProperty(final String propPath, final String paramRef) {
}

@RequiredArgsConstructor(access = PRIVATE)
@ToString
public static class Condition {

private static final Function<Object, String> TO_STRING = v -> v == null ? null : String.valueOf(v);

private static final Function<Object, String> TO_LOWERCASE =
v -> v == null ? null : String.valueOf(v).toLowerCase(ROOT);

private final String path;

private final JsonPointer pointer;

private final boolean negation;
Expand All @@ -141,97 +170,120 @@ boolean evaluateCondition(final JsonObject payload) {
private boolean evaluate(final String expected, final JsonObject payload) {
final Object actual = extractValue(payload);
switch (evaluationStrategy) {
case "DEFAULT":
return expected.equals(TO_STRING.apply(actual));
case "LENGTH":
if (actual == null) {
return "0".equals(expected);
}
final int expectedSize = Integer.parseInt(expected);
if (Collection.class.isInstance(actual)) {
return expectedSize == Collection.class.cast(actual).size();
}
if (actual.getClass().isArray()) {
return expectedSize == Array.getLength(actual);
}
if (String.class.isInstance(actual)) {
return expectedSize == String.class.cast(actual).length();
}
return false;
default:
Function<Object, String> preprocessor = TO_STRING;
if (evaluationStrategy.startsWith("CONTAINS")) {
final int start = evaluationStrategy.indexOf('(');
if (start >= 0) {
final int end = evaluationStrategy.indexOf(')', start);
if (end >= 0) {
final Map<String, String> configuration = Stream
.of(evaluationStrategy.substring(start + 1, end).split(","))
.map(String::trim)
.filter(it -> !it.isEmpty())
.map(it -> {
final int sep = it.indexOf('=');
if (sep > 0) {
return new String[] { it.substring(0, sep), it.substring(sep + 1) };
}
return new String[] { "value", it };
})
.collect(toMap(a -> a[0], a -> a[1]));
if (Boolean.parseBoolean(configuration.getOrDefault("lowercase", "false"))) {
preprocessor = TO_LOWERCASE;
case "DEFAULT":
if (Collection.class.isInstance(actual)) {
//if values >= actual, return true, it actual contains any element out of values, return false;
for (Object s : Collection.class.cast(actual)) {
if (!Arrays.stream(values).collect(toList()).contains(s)) {
return false;
}
}
return true;
}
return expected.equals(TO_STRING.apply(actual));
case "LENGTH":
if (actual == null) {
return false;
}
if (CharSequence.class.isInstance(actual)) {
return ofNullable(preprocessor.apply(TO_STRING.apply(actual)))
.map(it -> it.contains(expected))
.orElse(false);
return "0".equals(expected);
}
final int expectedSize = Integer.parseInt(expected);
if (Collection.class.isInstance(actual)) {
final Collection<?> collection = Collection.class.cast(actual);
return collection.stream().map(preprocessor).anyMatch(it -> it.contains(expected));
return expectedSize == Collection.class.cast(actual).size();
}
if (actual.getClass().isArray()) {
return IntStream
.range(0, Array.getLength(actual))
.mapToObj(i -> Array.get(actual, i))
.map(preprocessor)
.anyMatch(it -> it.contains(expected));
return expectedSize == Array.getLength(actual);
}
if (String.class.isInstance(actual)) {
return expectedSize == String.class.cast(actual).length();
}
return false;
}
throw new IllegalArgumentException("Not supported operation '" + evaluationStrategy + "'");
default:
Function<Object, String> preprocessor = TO_STRING;
if (evaluationStrategy.startsWith("CONTAINS")) {
final int start = evaluationStrategy.indexOf('(');
if (start >= 0) {
final int end = evaluationStrategy.indexOf(')', start);
if (end >= 0) {
final Map<String, String> configuration = Stream
.of(evaluationStrategy.substring(start + 1, end).split(","))
.map(String::trim)
.filter(it -> !it.isEmpty())
.map(it -> {
final int sep = it.indexOf('=');
if (sep > 0) {
return new String[]{it.substring(0, sep), it.substring(sep + 1)};
}
return new String[]{"value", it};
})
.collect(toMap(a -> a[0], a -> a[1]));
if (Boolean.parseBoolean(configuration.getOrDefault("lowercase", "false"))) {
preprocessor = TO_LOWERCASE;
}
}
}
if (actual == null) {
return false;
}
if (CharSequence.class.isInstance(actual)) {
return ofNullable(preprocessor.apply(TO_STRING.apply(actual)))
.map(it -> it.contains(expected))
.orElse(false);
}
if (Collection.class.isInstance(actual)) {
final Collection<?> collection = Collection.class.cast(actual);
return collection.stream().map(preprocessor).anyMatch(it -> it.contains(expected));
}
if (actual.getClass().isArray()) {
return IntStream
.range(0, Array.getLength(actual))
.mapToObj(i -> Array.get(actual, i))
.map(preprocessor)
.anyMatch(it -> it.contains(expected));
}
return false;
}
throw new IllegalArgumentException("Not supported operation '" + evaluationStrategy + "'");
}
}

private Object extractValue(final JsonObject payload) {
if (!pointer.containsValue(payload)) {
if (path.contains("[${index}]")) {
final JsonPointer ptr = JsonProvider.provider()
.createPointer(path.substring(0, path.indexOf("[${index}]")));
final JsonPointer subptr = JsonProvider.provider()
.createPointer(path.substring(path.indexOf("]") + 1));
final JsonArrayBuilder builder = JsonProvider.provider().createArrayBuilder();
ptr.getValue(payload)
.asJsonArray()
.stream()
.forEach(j -> {
JsonValue value = subptr.getValue(j.asJsonObject());
builder.add(value);
});
return ofNullable(builder.build()).map(this::mapValue).orElse(null);
}
return null;
}
return ofNullable(pointer.getValue(payload)).map(this::mapValue).orElse(null);

}

private Object mapValue(final JsonValue value) {
switch (value.getValueType()) {
case ARRAY:
return value.asJsonArray().stream().map(this::mapValue).collect(toList());
case STRING:
return JsonString.class.cast(value).getString();
case TRUE:
return true;
case FALSE:
return false;
case NUMBER:
return JsonNumber.class.cast(value).doubleValue();
case NULL:
return null;
case OBJECT:
default:
return value;
case ARRAY:
return value.asJsonArray().stream().map(this::mapValue).collect(toList());
case STRING:
return JsonString.class.cast(value).getString();
case TRUE:
return true;
case FALSE:
return false;
case NUMBER:
return JsonNumber.class.cast(value).doubleValue();
case NULL:
return null;
case OBJECT:
default:
return value;
}
}
}
Expand Down
Loading