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.Collections.emptyMap;
019import static java.util.Comparator.comparing;
020import static java.util.Optional.ofNullable;
021import static javax.json.stream.JsonCollectors.toJsonArray;
022
023import java.util.Collection;
024import java.util.Collections;
025import java.util.Map;
026import java.util.stream.Stream;
027
028import javax.json.JsonArray;
029import javax.json.JsonBuilderFactory;
030import javax.json.JsonObject;
031import javax.json.JsonObjectBuilder;
032import javax.json.JsonValue;
033import javax.json.spi.JsonProvider;
034
035import org.talend.sdk.component.runtime.manager.ParameterMeta;
036
037import lombok.RequiredArgsConstructor;
038
039@RequiredArgsConstructor
040public class PayloadMapper {
041
042    // we don't need the runtime one here
043    private final JsonProvider jsonp = JsonProvider.provider();
044
045    private static final VisibilityService VISIBILITY_SERVICE = new VisibilityService(JsonProvider.provider());
046
047    private final JsonBuilderFactory factory = jsonp.createBuilderFactory(emptyMap());
048
049    private final OnParameter parameterVisitor;
050
051    private JsonObject globalPayload = null;
052
053    public JsonObject visitAndMap(final Collection<ParameterMeta> parameters, final Map<String, String> payload) {
054        return unflatten("", ofNullable(parameters).orElseGet(Collections::emptyList),
055                payload == null ? emptyMap() : payload);
056    }
057
058    public void setGlobalPayload(final JsonObject payload) {
059        globalPayload = payload;
060    }
061
062    private JsonObject unflatten(final String contextualPrefix, final Collection<ParameterMeta> definitions,
063            final Map<String, String> config) {
064        final JsonObjectBuilder json = factory.createObjectBuilder();
065        ofNullable(definitions)
066                .map(Collection::stream)
067                .orElseGet(Stream::empty)
068                .sorted(comparing(ParameterMeta::getName))
069                .forEach(meta -> onProperty(contextualPrefix, meta.getNestedParameters(), config, json, meta));
070        return json.build();
071    }
072
073    private void onProperty(final String contextualPrefix, final Collection<ParameterMeta> definitions,
074            final Map<String, String> config, final JsonObjectBuilder json, final ParameterMeta definition) {
075        final String name = definition.getName();
076        final String newPath = contextualPrefix + (contextualPrefix.isEmpty() ? "" : ".") + name;
077        switch (definition.getType()) {
078        case OBJECT: {
079            onObject(definitions, definition, config, json, name, newPath);
080            break;
081        }
082        case ARRAY: {
083            onArray(definition.getNestedParameters(), definition, config, newPath, json, name);
084            break;
085        }
086        case BOOLEAN:
087            final String boolValue = config.get(newPath);
088            if (boolValue == null || boolValue.isEmpty()) {
089                parameterVisitor.onParameter(definition, JsonValue.NULL);
090            } else {
091                final boolean value = Boolean.parseBoolean(boolValue.trim());
092                parameterVisitor.onParameter(definition, value ? JsonValue.TRUE : JsonValue.FALSE);
093                json.add(name, value);
094            }
095            ofNullable(boolValue)
096                    .map(String::trim)
097                    .filter(v -> !v.isEmpty())
098                    .ifPresent(v -> json.add(name, Boolean.parseBoolean(v)));
099            break;
100        case NUMBER:
101            final String numberValue = config.get(newPath);
102            if (numberValue == null || numberValue.isEmpty()) {
103                parameterVisitor.onParameter(definition, JsonValue.NULL);
104            } else {
105                final Double value = Double.valueOf(numberValue.trim());
106                parameterVisitor.onParameter(definition, jsonp.createValue(value));
107                final long asLong = value.longValue();
108                if (value == asLong) {
109                    json.add(name, asLong);
110                } else {
111                    json.add(name, value);
112                }
113            }
114            break;
115        case ENUM:
116        case STRING: {
117            final String value = config.get(newPath);
118            parameterVisitor.onParameter(definition, value == null ? JsonValue.NULL : jsonp.createValue(value));
119            ofNullable(value).ifPresent(v -> json.add(name, v));
120            break;
121        }
122        default:
123        }
124    }
125
126    private void onObject(final Collection<ParameterMeta> definitions, final ParameterMeta meta,
127            final Map<String, String> config, final JsonObjectBuilder json, final String name,
128            final String currentPath) {
129        if (!isVisible(meta)) {
130            return;
131        }
132        final JsonObject unflatten = unflatten(currentPath, definitions, config);
133        if (!unflatten.isEmpty()) {
134            json.add(name, unflatten);
135            parameterVisitor.onParameter(meta, unflatten);
136        } else {
137            parameterVisitor.onParameter(meta, JsonValue.NULL);
138        }
139    }
140
141    private boolean isVisible(final ParameterMeta meta) {
142        return globalPayload == null ? true : VISIBILITY_SERVICE.build(meta).isVisible(globalPayload);
143    }
144
145    private void onArray(final Collection<ParameterMeta> definitions, final ParameterMeta definition,
146            final Map<String, String> config, final String currentPrefix, final JsonObjectBuilder json,
147            final String name) {
148        final JsonArray array;
149        if (definitions.size() == 1 && definitions.iterator().next().getPath().endsWith("[${index}]")) { // primitive
150            final ParameterMeta primitiveMeta = definitions.stream().iterator().next();
151            array = config
152                    .entrySet()
153                    .stream()
154                    .filter(it -> it.getKey().startsWith(currentPrefix + '['))
155                    .map(e -> new ArrayEntry(e, currentPrefix))
156                    .distinct()
157                    // sort by index
158                    .sorted(comparing(it -> it.index))
159                    .map(entry -> onArrayPrimitive(primitiveMeta, entry))
160                    .collect(toJsonArray());
161        } else {
162            array = config
163                    .entrySet()
164                    .stream()
165                    .filter(it -> it.getKey().startsWith(currentPrefix + '['))
166                    .map(e -> new ArrayEntry(e, currentPrefix).index)
167                    .distinct()
168                    // sort by index
169                    .sorted(comparing(it -> it))
170                    .map(index -> unflatten(currentPrefix + '[' + index + ']', definitions, config))
171                    .collect(toJsonArray());
172        }
173        if (!array.isEmpty()) {
174            json.add(name, array);
175            parameterVisitor.onParameter(definition, array);
176        } else {
177            parameterVisitor.onParameter(definition, JsonValue.NULL);
178        }
179    }
180
181    private JsonValue onArrayPrimitive(final ParameterMeta itemDef, final ArrayEntry e) {
182        final String value = e.entry.getValue();
183        switch (itemDef.getType()) {
184        case BOOLEAN:
185            return Boolean.parseBoolean(value.trim()) ? JsonValue.TRUE : JsonValue.FALSE;
186        case NUMBER:
187            final Double number = Double.valueOf(value.trim());
188            return number == number.longValue() ? jsonp.createValue(number.longValue()) : jsonp.createValue(number);
189        case ENUM:
190        case STRING:
191            return jsonp.createValue(value);
192        default:
193            throw new IllegalArgumentException("Unsupported structure in " + "array: " + itemDef.getType());
194        }
195    }
196
197    private static class ArrayEntry {
198
199        private final Map.Entry<String, String> entry;
200
201        private final int index;
202
203        private ArrayEntry(final Map.Entry<String, String> entry, final String name) {
204            this.entry = entry;
205
206            final String indexStr =
207                    entry.getKey().substring(name.length() + 1, entry.getKey().indexOf(']', name.length()));
208            this.index = Integer.parseInt(indexStr);
209        }
210    }
211
212    public interface OnParameter {
213
214        void onParameter(ParameterMeta meta, JsonValue value);
215
216    }
217}