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.xbean.converter;
017
018import java.io.StringReader;
019import java.math.BigDecimal;
020import java.math.BigInteger;
021import java.util.Iterator;
022import java.util.Map;
023import java.util.ServiceLoader;
024import java.util.function.BiConsumer;
025import java.util.function.Consumer;
026import java.util.stream.Collectors;
027import java.util.stream.Stream;
028
029import javax.json.Json;
030import javax.json.JsonArray;
031import javax.json.JsonArrayBuilder;
032import javax.json.JsonNumber;
033import javax.json.JsonObject;
034import javax.json.JsonObjectBuilder;
035import javax.json.JsonString;
036import javax.json.JsonValue;
037import javax.json.spi.JsonProvider;
038
039import org.apache.xbean.propertyeditor.AbstractConverter;
040import org.talend.sdk.component.api.record.Schema;
041import org.talend.sdk.component.api.service.record.RecordBuilderFactory;
042import org.talend.sdk.component.runtime.manager.service.record.RecordBuilderFactoryProvider;
043import org.talend.sdk.component.runtime.record.RecordBuilderFactoryImpl;
044
045public class SchemaConverter extends AbstractConverter {
046
047    public static final String ELEMENT_SCHEMA = "elementSchema";
048
049    public static final String PROPS = "props";
050
051    public static final String TYPE = "type";
052
053    public static final String ENTRIES = "entries";
054
055    private final RecordBuilderFactory factory;
056
057    public SchemaConverter() {
058        super(Schema.class);
059        try {
060            final RecordBuilderFactoryProvider recordBuilderFactoryProvider;
061            final Iterator<RecordBuilderFactoryProvider> spiIterator =
062                    ServiceLoader.load(RecordBuilderFactoryProvider.class).iterator();
063            if (spiIterator.hasNext()) {
064                final RecordBuilderFactoryProvider spi = spiIterator.next();
065                recordBuilderFactoryProvider = spi;
066            } else {
067                recordBuilderFactoryProvider = RecordBuilderFactoryImpl::new;
068            }
069            factory = recordBuilderFactoryProvider.apply("null");
070        } catch (RuntimeException e) {
071            throw new IllegalStateException(e);
072        }
073    }
074
075    @Override
076    public Object toObjectImpl(final String s) {
077        if (!s.isEmpty()) {
078            final JsonObject json = JsonProvider.provider().createReader(new StringReader(s)).readObject();
079            return toSchema(json);
080        }
081        return null;
082    }
083
084    private Schema toSchema(final JsonObject json) {
085        if (json == null || json.isNull(TYPE)) {
086            return null;
087        }
088        final String type = json.getString(TYPE);
089        final Schema.Type schemaType = Enum.valueOf(Schema.Type.class, type);
090        final Schema.Builder builder = factory.newSchemaBuilder(schemaType);
091
092        if (schemaType == Schema.Type.RECORD) {
093            this.addEntries(builder, json.getJsonArray(ENTRIES));
094            this.addEntries(builder, json.getJsonArray("metadatas"));
095        } else if (schemaType == Schema.Type.ARRAY) {
096            this.treatElementSchema(json, builder::withElementSchema);
097        }
098        this.addProps(builder::withProp, json);
099
100        final JsonValue orderValue = json.get("order");
101        if (orderValue instanceof JsonString) {
102            final Schema.EntriesOrder order = Schema.EntriesOrder.of(((JsonString) orderValue).getString());
103            return builder.build(order);
104        } else {
105            return builder.build();
106        }
107    }
108
109    private void treatElementSchema(final JsonObject json,
110            final Consumer<Schema> setter) {
111        final JsonValue elementSchema = json.get(ELEMENT_SCHEMA);
112        if (elementSchema instanceof JsonObject) {
113            final Schema schema = this.toSchema((JsonObject) elementSchema);
114            setter.accept(schema);
115        } else if (elementSchema instanceof JsonString) {
116            final Schema.Type innerType = Schema.Type.valueOf(((JsonString) elementSchema).getString());
117            setter.accept(this.factory.newSchemaBuilder(innerType).build());
118        }
119    }
120
121    private void addEntries(final Schema.Builder schemaBuilder, final JsonArray entries) {
122        if (entries == null || entries.isEmpty()) {
123            return;
124        }
125
126        entries.stream()
127                .filter(JsonObject.class::isInstance)
128                .map(JsonObject.class::cast)
129                .map(this::jsonToEntry)
130                .forEach(schemaBuilder::withEntry);
131    }
132
133    private Schema.Entry jsonToEntry(final JsonObject jsonEntry) {
134        final Schema.Entry.Builder builder = factory.newEntryBuilder();
135        final Schema.Type schemaType = Enum.valueOf(Schema.Type.class, jsonEntry.getString(TYPE));
136        builder.withType(schemaType)
137                .withName(jsonEntry.getString("name"))
138                .withNullable(jsonEntry.getBoolean("nullable", true))
139                .withMetadata(jsonEntry.getBoolean("metadata", false));
140        final JsonString comment = jsonEntry.getJsonString("comment");
141        if (comment != null) {
142            builder.withComment(comment.getString());
143        }
144        final JsonString rawName = jsonEntry.getJsonString("rawName");
145        if (rawName != null) {
146            builder.withRawName(rawName.getString());
147        }
148        final JsonValue jsonValue = jsonEntry.get("defaultValue");
149        if (jsonValue != null) {
150            this.setDefaultValue(builder, jsonValue);
151        }
152
153        if (schemaType == Schema.Type.RECORD || schemaType == Schema.Type.ARRAY) {
154            this.treatElementSchema(jsonEntry, builder::withElementSchema);
155        }
156
157        this.addProps(builder::withProp, jsonEntry);
158        return builder.build();
159    }
160
161    private void addProps(final BiConsumer<String, String> setter,
162            final JsonObject object) {
163        if (object == null || object.get(PROPS) == null) {
164            return;
165        }
166        JsonObject props = object.getJsonObject(PROPS);
167        if (props.isEmpty()) {
168            return;
169        }
170        props.forEach(
171                (String name, JsonValue value) -> setter.accept(name, ((JsonString) value).getString()));
172    }
173
174    private void setDefaultValue(final Schema.Entry.Builder entry,
175            final JsonValue value) {
176        final JsonValue.ValueType valueType = value.getValueType();
177        if (valueType == JsonValue.ValueType.NUMBER) {
178            entry.withDefaultValue(((JsonNumber) value).numberValue());
179        } else if (valueType == JsonValue.ValueType.FALSE) {
180            entry.withDefaultValue(Boolean.FALSE);
181        } else if (valueType == JsonValue.ValueType.TRUE) {
182            entry.withDefaultValue(Boolean.TRUE);
183        } else if (valueType == JsonValue.ValueType.STRING) {
184            entry.withDefaultValue(((JsonString) value).getString());
185        }
186        // doesn't treat JsonArray nor JsonObject for default value.
187    }
188
189    public JsonObject toJson(final Schema schema) {
190        if (schema == null) {
191            return null;
192        }
193        final JsonObjectBuilder builder = Json.createObjectBuilder();
194        builder.add(TYPE, schema.getType().name());
195        if (schema.getType() == Schema.Type.RECORD) {
196            this.addEntries(builder, ENTRIES, schema.getAllEntries());
197        } else if (schema.getType() == Schema.Type.ARRAY) {
198            final JsonObject elementSchema = this.toJson(schema.getElementSchema());
199            if (elementSchema != null) {
200                builder.add(ELEMENT_SCHEMA, elementSchema);
201            }
202        }
203        this.addProps(builder, schema.getProps());
204
205        if (schema.getType() == Schema.Type.RECORD) {
206            final Schema.EntriesOrder order = schema.naturalOrder();
207            if (order != null) {
208                String orderValue = order.getFieldsOrder().collect(Collectors.joining(","));
209                builder.add("order", orderValue);
210            }
211        }
212        return builder.build();
213    }
214
215    private void addEntries(final JsonObjectBuilder objectBuilder,
216            final String name,
217            final Stream<Schema.Entry> entries) {
218        if (entries == null) {
219            return;
220        }
221        final JsonArrayBuilder jsonEntries = Json.createArrayBuilder();
222        objectBuilder.add(name, jsonEntries);
223        entries.map(this::entryToJson)
224                .forEach(jsonEntries::add);
225    }
226
227    private JsonObjectBuilder entryToJson(final Schema.Entry entry) {
228        final JsonObjectBuilder entryBuilder = Json.createObjectBuilder();
229        entryBuilder.add("name", entry.getName());
230        entryBuilder.add(TYPE, entry.getType().name());
231        entryBuilder.add("nullable", entry.isNullable());
232        if (entry.getComment() != null) {
233            entryBuilder.add("comment", entry.getComment());
234        }
235        if (entry.getRawName() != null) {
236            entryBuilder.add("rawName", entry.getRawName());
237        }
238        if (entry.isMetadata()) {
239            entryBuilder.add("metadata", true);
240        }
241        if (entry.getDefaultValue() != null) {
242            final JsonValue jsonValue = this.toValue(entry.getDefaultValue());
243            if (jsonValue != null) {
244                entryBuilder.add("defaultValue", jsonValue);
245            }
246        }
247        if (entry.getType() == Schema.Type.RECORD
248                || entry.getType() == Schema.Type.ARRAY) {
249
250            final Schema innerSchema = entry.getElementSchema();
251            if (innerSchema.getType() == Schema.Type.ARRAY
252                    || innerSchema.getType() == Schema.Type.RECORD) {
253                final JsonObject elementSchema = this.toJson(innerSchema);
254                entryBuilder.add(ELEMENT_SCHEMA, elementSchema);
255            } else {
256                entryBuilder.add(ELEMENT_SCHEMA, innerSchema.getType().name());
257            }
258        }
259        this.addProps(entryBuilder, entry.getProps());
260        return entryBuilder;
261    }
262
263    private void addProps(final JsonObjectBuilder builder,
264            final Map<String, String> properties) {
265        if (properties == null || properties.isEmpty()) {
266            return;
267        }
268        final JsonObjectBuilder jsonProps = Json.createObjectBuilder();
269        properties.entrySet()
270                .forEach(
271                        (Map.Entry<String, String> e) -> jsonProps.add(e.getKey(), e.getValue()));
272        builder.add(PROPS, jsonProps);
273    }
274
275    private JsonValue toValue(final Object object) {
276        if (object == null) {
277            return JsonValue.NULL;
278        }
279        if (object instanceof Integer) {
280            return Json.createValue((Integer) object);
281        }
282        if (object instanceof Long) {
283            return Json.createValue((Long) object);
284        }
285        if (object instanceof Double || object instanceof Float) {
286            return Json.createValue((Double) object);
287        }
288        if (object instanceof BigInteger) {
289            return Json.createValue((BigInteger) object);
290        }
291        if (object instanceof Boolean) {
292            if (object == Boolean.TRUE) {
293                return JsonValue.TRUE;
294            }
295            return JsonValue.FALSE;
296        }
297        if (object instanceof BigDecimal) {
298            return Json.createValue((BigDecimal) object);
299        }
300        if (object instanceof String) {
301            return Json.createValue((String) object);
302        }
303
304        return null;
305    }
306
307}