001/**
002 * Copyright (C) 2006-2025 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}