001/**
002 * Copyright (C) 2006-2024 Talend Inc. - www.talend.com
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.talend.sdk.component.runtime.manager.reflect.visibility;
017
018import static java.util.Locale.ROOT;
019import static java.util.Optional.ofNullable;
020import static java.util.stream.Collectors.joining;
021import static java.util.stream.Collectors.toList;
022import static java.util.stream.Collectors.toMap;
023import static lombok.AccessLevel.PRIVATE;
024
025import java.lang.reflect.Array;
026import java.util.Collection;
027import java.util.Map;
028import java.util.function.Function;
029import java.util.stream.IntStream;
030import java.util.stream.Stream;
031
032import javax.json.JsonNumber;
033import javax.json.JsonObject;
034import javax.json.JsonPointer;
035import javax.json.JsonString;
036import javax.json.JsonValue;
037import javax.json.spi.JsonProvider;
038
039import org.talend.sdk.component.runtime.manager.ParameterMeta;
040
041import lombok.NoArgsConstructor;
042import lombok.RequiredArgsConstructor;
043
044@RequiredArgsConstructor
045public class VisibilityService {
046
047    private final AbsolutePathResolver pathResolver = new AbsolutePathResolver();
048
049    private final JsonProvider jsonProvider;
050
051    public ConditionGroup build(final ParameterMeta param) {
052        final boolean and =
053                "AND".equalsIgnoreCase(param.getMetadata().getOrDefault("tcomp::condition::ifs::operator", "AND"));
054        return new ConditionGroup(param
055                .getMetadata()
056                .entrySet()
057                .stream()
058                .filter(meta -> meta.getKey().startsWith("tcomp::condition::if::target"))
059                .map(meta -> {
060                    final String[] split = meta.getKey().split("::");
061                    final String index = split.length == 5 ? "::" + split[split.length - 1] : "";
062                    final String valueKey = "tcomp::condition::if::value" + index;
063                    final String negateKey = "tcomp::condition::if::negate" + index;
064                    final String evaluationStrategyKey = "tcomp::condition::if::evaluationStrategy" + index;
065                    final String absoluteTargetPath = pathResolver.resolveProperty(param.getPath(), meta.getValue());
066                    return new Condition(toPointer(absoluteTargetPath),
067                            Boolean.parseBoolean(param.getMetadata().getOrDefault(negateKey, "false")),
068                            param.getMetadata().getOrDefault(evaluationStrategyKey, "DEFAULT").toUpperCase(ROOT),
069                            param.getMetadata().getOrDefault(valueKey, "true").split(","));
070                })
071                .collect(toList()), and ? stream -> stream.allMatch(i -> i) : stream -> stream.anyMatch(i -> i));
072    }
073
074    private JsonPointer toPointer(final String absoluteTargetPath) {
075        return jsonProvider.createPointer('/' + absoluteTargetPath.replace('.', '/'));
076    }
077
078    // check org.talend.sdk.component.form.internal.converter.impl.widget.path.AbsolutePathResolver,
079    // we don't want component-form-core dependency here, we can move it to another module later if relevant
080    @NoArgsConstructor(access = PRIVATE)
081    private static class AbsolutePathResolver {
082
083        private String resolveProperty(final String propPath, final String paramRef) {
084            return doResolveProperty(propPath, normalizeParamRef(paramRef));
085        }
086
087        private String normalizeParamRef(final String paramRef) {
088            return (!paramRef.contains(".") ? "../" : "") + paramRef;
089        }
090
091        private String doResolveProperty(final String propPath, final String paramRef) {
092            if (".".equals(paramRef)) {
093                return propPath;
094            }
095            if (paramRef.startsWith("..")) {
096                String current = propPath;
097                String ref = paramRef;
098                while (ref.startsWith("..")) {
099                    int lastDot = current.lastIndexOf('.');
100                    if (lastDot < 0) {
101                        lastDot = 0;
102                    }
103                    current = current.substring(0, lastDot);
104                    ref = ref.substring("..".length(), ref.length());
105                    if (ref.startsWith("/")) {
106                        ref = ref.substring(1);
107                    }
108                    if (current.isEmpty()) {
109                        break;
110                    }
111                }
112                return Stream.of(current, ref.replace('/', '.')).filter(it -> !it.isEmpty()).collect(joining("."));
113            }
114            if (paramRef.startsWith(".") || paramRef.startsWith("./")) {
115                return propPath + '.' + paramRef.replaceFirst("\\./?", "").replace('/', '.');
116            }
117            return paramRef;
118        }
119    }
120
121    @RequiredArgsConstructor(access = PRIVATE)
122    public static class Condition {
123
124        private static final Function<Object, String> TO_STRING = v -> v == null ? null : String.valueOf(v);
125
126        private static final Function<Object, String> TO_LOWERCASE =
127                v -> v == null ? null : String.valueOf(v).toLowerCase(ROOT);
128
129        private final JsonPointer pointer;
130
131        private final boolean negation;
132
133        private final String evaluationStrategy;
134
135        private final String[] values;
136
137        boolean evaluateCondition(final JsonObject payload) {
138            return negation != Stream.of(values).anyMatch(val -> evaluate(val, payload));
139        }
140
141        private boolean evaluate(final String expected, final JsonObject payload) {
142            final Object actual = extractValue(payload);
143            switch (evaluationStrategy) {
144            case "DEFAULT":
145                return expected.equals(TO_STRING.apply(actual));
146            case "LENGTH":
147                if (actual == null) {
148                    return "0".equals(expected);
149                }
150                final int expectedSize = Integer.parseInt(expected);
151                if (Collection.class.isInstance(actual)) {
152                    return expectedSize == Collection.class.cast(actual).size();
153                }
154                if (actual.getClass().isArray()) {
155                    return expectedSize == Array.getLength(actual);
156                }
157                if (String.class.isInstance(actual)) {
158                    return expectedSize == String.class.cast(actual).length();
159                }
160                return false;
161            default:
162                Function<Object, String> preprocessor = TO_STRING;
163                if (evaluationStrategy.startsWith("CONTAINS")) {
164                    final int start = evaluationStrategy.indexOf('(');
165                    if (start >= 0) {
166                        final int end = evaluationStrategy.indexOf(')', start);
167                        if (end >= 0) {
168                            final Map<String, String> configuration = Stream
169                                    .of(evaluationStrategy.substring(start + 1, end).split(","))
170                                    .map(String::trim)
171                                    .filter(it -> !it.isEmpty())
172                                    .map(it -> {
173                                        final int sep = it.indexOf('=');
174                                        if (sep > 0) {
175                                            return new String[] { it.substring(0, sep), it.substring(sep + 1) };
176                                        }
177                                        return new String[] { "value", it };
178                                    })
179                                    .collect(toMap(a -> a[0], a -> a[1]));
180                            if (Boolean.parseBoolean(configuration.getOrDefault("lowercase", "false"))) {
181                                preprocessor = TO_LOWERCASE;
182                            }
183                        }
184                    }
185                    if (actual == null) {
186                        return false;
187                    }
188                    if (CharSequence.class.isInstance(actual)) {
189                        return ofNullable(preprocessor.apply(TO_STRING.apply(actual)))
190                                .map(it -> it.contains(expected))
191                                .orElse(false);
192                    }
193                    if (Collection.class.isInstance(actual)) {
194                        final Collection<?> collection = Collection.class.cast(actual);
195                        return collection.stream().map(preprocessor).anyMatch(it -> it.contains(expected));
196                    }
197                    if (actual.getClass().isArray()) {
198                        return IntStream
199                                .range(0, Array.getLength(actual))
200                                .mapToObj(i -> Array.get(actual, i))
201                                .map(preprocessor)
202                                .anyMatch(it -> it.contains(expected));
203                    }
204                    return false;
205                }
206                throw new IllegalArgumentException("Not supported operation '" + evaluationStrategy + "'");
207            }
208        }
209
210        private Object extractValue(final JsonObject payload) {
211            if (!pointer.containsValue(payload)) {
212                return null;
213            }
214            return ofNullable(pointer.getValue(payload)).map(this::mapValue).orElse(null);
215
216        }
217
218        private Object mapValue(final JsonValue value) {
219            switch (value.getValueType()) {
220            case ARRAY:
221                return value.asJsonArray().stream().map(this::mapValue).collect(toList());
222            case STRING:
223                return JsonString.class.cast(value).getString();
224            case TRUE:
225                return true;
226            case FALSE:
227                return false;
228            case NUMBER:
229                return JsonNumber.class.cast(value).doubleValue();
230            case NULL:
231                return null;
232            case OBJECT:
233            default:
234                return value;
235            }
236        }
237    }
238
239    @RequiredArgsConstructor(access = PRIVATE)
240    public static class ConditionGroup {
241
242        private final Collection<Condition> conditions;
243
244        private final Function<Stream<Boolean>, Boolean> aggregator;
245
246        public boolean isVisible(final JsonObject payload) {
247            return conditions
248                    .stream()
249                    .allMatch(group -> aggregator.apply(conditions.stream().map(c -> c.evaluateCondition(payload))));
250        }
251    }
252
253}