001/**
002 * Copyright (C) 2006-2022 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.record;
017
018import static java.util.Collections.unmodifiableList;
019import static java.util.stream.Collectors.joining;
020import static java.util.stream.Collectors.toList;
021import static java.util.stream.Collectors.toMap;
022
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.Comparator;
026import java.util.LinkedHashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Objects;
030import java.util.stream.Stream;
031
032import javax.json.bind.annotation.JsonbTransient;
033
034import org.talend.sdk.component.api.record.OrderedMap;
035import org.talend.sdk.component.api.record.Schema;
036
037import lombok.EqualsAndHashCode;
038import lombok.Getter;
039import lombok.ToString;
040
041@ToString
042public class SchemaImpl implements Schema {
043
044    @Getter
045    private final Type type;
046
047    @Getter
048    private final Schema elementSchema;
049
050    @Getter
051    private final List<Entry> entries;
052
053    @JsonbTransient
054    private final List<Entry> metadataEntries;
055
056    @Getter
057    private final Map<String, String> props;
058
059    @JsonbTransient
060    private final EntriesOrder entriesOrder;
061
062    public static final String ENTRIES_ORDER_PROP = "talend.fields.order";
063
064    SchemaImpl(final SchemaImpl.BuilderImpl builder) {
065        this.type = builder.type;
066        this.elementSchema = builder.elementSchema;
067        this.entries = unmodifiableList(builder.entries.streams().collect(toList()));
068        this.metadataEntries = unmodifiableList(builder.metadataEntries.streams().collect(toList()));
069        this.props = builder.props;
070        entriesOrder = EntriesOrder.of(getFieldsOrder());
071    }
072
073    /**
074     * Optimized hashcode method (do not enter inside field hashcode, just getName, ignore props fields).
075     *
076     * @return hashcode.
077     */
078    @Override
079    public int hashCode() {
080        final String e1 =
081                this.entries != null ? this.entries.stream().map(Entry::getName).collect(joining(",")) : "";
082        final String m1 = this.metadataEntries != null
083                ? this.metadataEntries.stream().map(Entry::getName).collect(joining(","))
084                : "";
085
086        return Objects.hash(this.type, this.elementSchema, e1, m1);
087    }
088
089    @Override
090    public boolean equals(final Object obj) {
091        if (obj == this) {
092            return true;
093        }
094        if (!(obj instanceof SchemaImpl)) {
095            return false;
096        }
097        final SchemaImpl other = (SchemaImpl) obj;
098        if (!other.canEqual(this)) {
099            return false;
100        }
101        return Objects.equals(this.type, other.type)
102                && Objects.equals(this.elementSchema, other.elementSchema)
103                && Objects.equals(this.entries, other.entries)
104                && Objects.equals(this.metadataEntries, other.metadataEntries)
105                && Objects.equals(this.props, other.props);
106    }
107
108    protected boolean canEqual(final SchemaImpl other) {
109        return true;
110    }
111
112    @Override
113    public String getProp(final String property) {
114        return props.get(property);
115    }
116
117    @Override
118    public List<Entry> getMetadata() {
119        return this.metadataEntries;
120    }
121
122    @Override
123    @JsonbTransient
124    public Stream<Entry> getAllEntries() {
125        return Stream.concat(this.metadataEntries.stream(), this.entries.stream());
126    }
127
128    @Override
129    public Builder toBuilder() {
130        final Builder builder = new BuilderImpl()
131                .withType(this.type)
132                .withElementSchema(this.elementSchema)
133                .withProps(this.props
134                        .entrySet()
135                        .stream()
136                        .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)));
137        getEntriesOrdered().forEach(builder::withEntry);
138        return builder;
139    }
140
141    @Override
142    @JsonbTransient
143    public List<Entry> getEntriesOrdered() {
144        return getAllEntries().sorted(entriesOrder).collect(toList());
145    }
146
147    @Override
148    @JsonbTransient
149    public EntriesOrder naturalOrder() {
150        return entriesOrder;
151    }
152
153    private String getFieldsOrder() {
154        String fields = getProp(ENTRIES_ORDER_PROP);
155        if (fields == null || fields.isEmpty()) {
156            fields = getAllEntries().map(Entry::getName).collect(joining(","));
157            props.put(ENTRIES_ORDER_PROP, fields);
158        }
159        return fields;
160    }
161
162    public static class BuilderImpl implements Builder {
163
164        private Type type;
165
166        private Schema elementSchema;
167
168        private final OrderedMap<Schema.Entry> entries = new OrderedMap<>(Schema.Entry::getName);
169
170        private final OrderedMap<Schema.Entry> metadataEntries = new OrderedMap<>(Schema.Entry::getName);
171
172        private Map<String, String> props = new LinkedHashMap<>(0);
173
174        private List<String> entriesOrder = new ArrayList<>();
175
176        @Override
177        public Builder withElementSchema(final Schema schema) {
178            if (type != Type.ARRAY && schema != null) {
179                throw new IllegalArgumentException("elementSchema is only valid for ARRAY type of schema");
180            }
181            elementSchema = schema;
182            return this;
183        }
184
185        @Override
186        public Builder withType(final Type type) {
187            this.type = type;
188            return this;
189        }
190
191        @Override
192        public Builder withEntry(final Entry entry) {
193            if (type != Type.RECORD) {
194                throw new IllegalArgumentException("entry is only valid for RECORD type of schema");
195            }
196            final Entry entryToAdd = Schema.avoidCollision(entry,
197                    this::getEntry,
198                    this::replaceEntry);
199            if (entryToAdd == null) {
200                // mean try to add entry with same name.
201                throw new IllegalArgumentException("Entry with name " + entry.getName() + " already exist in schema");
202            }
203            if (entry.isMetadata()) {
204                this.metadataEntries.addValue(entryToAdd);
205            } else {
206                this.entries.addValue(entryToAdd);
207            }
208
209            entriesOrder.add(entry.getName());
210            return this;
211        }
212
213        @Override
214        public Builder withEntryAfter(final String after, final Entry entry) {
215            withEntry(entry);
216            return moveAfter(after, entry.getName());
217        }
218
219        @Override
220        public Builder withEntryBefore(final String before, final Entry entry) {
221            withEntry(entry);
222            return moveBefore(before, entry.getName());
223        }
224
225        private void replaceEntry(final String name, final Schema.Entry entry) {
226            if (this.entries.getValue(entry.getName()) != null) {
227                this.entries.replace(name, entry);
228            } else if (this.metadataEntries.getValue(name) != null) {
229                this.metadataEntries.replace(name, entry);
230            }
231        }
232
233        private Stream<Entry> getAllEntries() {
234            return Stream.concat(this.entries.streams(), this.metadataEntries.streams());
235        }
236
237        @Override
238        public Builder withProp(final String key, final String value) {
239            props.put(key, value);
240            return this;
241        }
242
243        @Override
244        public Builder withProps(final Map<String, String> props) {
245            if (props != null) {
246                this.props = props;
247            }
248            return this;
249        }
250
251        @Override
252        public Builder remove(final String name) {
253            final Entry entry = this.getEntry(name);
254            if (entry == null) {
255                throw new IllegalArgumentException(String.format("%s not in schema", name));
256            }
257            return this.remove(entry);
258        }
259
260        @Override
261        public Builder remove(final Entry entry) {
262            if (entry != null) {
263                if (entry.isMetadata()) {
264                    if (this.metadataEntries.getValue(entry.getName()) != null) {
265                        metadataEntries.removeValue(entry);
266                    }
267                } else if (this.entries.getValue(entry.getName()) != null) {
268                    entries.removeValue(entry);
269                }
270                entriesOrder.remove(entry.getName());
271            }
272            return this;
273        }
274
275        @Override
276        public Builder moveAfter(final String after, final String name) {
277            if (entriesOrder.indexOf(after) == -1) {
278                throw new IllegalArgumentException(String.format("%s not in schema", after));
279            }
280            entriesOrder.remove(name);
281            int destination = entriesOrder.indexOf(after) + 1;
282            if (destination < entriesOrder.size()) {
283                entriesOrder.add(destination, name);
284            } else {
285                entriesOrder.add(name);
286            }
287            return this;
288        }
289
290        @Override
291        public Builder moveBefore(final String before, final String name) {
292            if (entriesOrder.indexOf(before) == -1) {
293                throw new IllegalArgumentException(String.format("%s not in schema", before));
294            }
295            entriesOrder.remove(name);
296            entriesOrder.add(entriesOrder.indexOf(before), name);
297            return this;
298        }
299
300        @Override
301        public Builder swap(final String name, final String with) {
302            Collections.swap(entriesOrder, entriesOrder.indexOf(name), entriesOrder.indexOf(with));
303            return this;
304        }
305
306        @Override
307        public Schema build() {
308            if (this.entriesOrder != null && !this.entriesOrder.isEmpty()) {
309                this.props.put(ENTRIES_ORDER_PROP, entriesOrder.stream().collect(joining(",")));
310            }
311            return new SchemaImpl(this);
312        }
313
314        @Override
315        public Schema build(final Comparator<Entry> order) {
316            final String entriesOrderProp =
317                    this.getAllEntries().sorted(order).map(Entry::getName).collect(joining(","));
318            this.props.put(ENTRIES_ORDER_PROP, entriesOrderProp);
319
320            return new SchemaImpl(this);
321        }
322
323        private Schema.Entry getEntry(final String name) {
324            Entry entry = this.entries.getValue(name);
325            if (entry == null) {
326                entry = this.metadataEntries.getValue(name);
327            }
328            return entry;
329        }
330    }
331
332    /**
333     * {@link org.talend.sdk.component.api.record.Schema.Entry} implementation.
334     */
335    @EqualsAndHashCode
336    @ToString
337    public static class EntryImpl implements org.talend.sdk.component.api.record.Schema.Entry {
338
339        private EntryImpl(final EntryImpl.BuilderImpl builder) {
340            this.name = builder.name;
341            this.rawName = builder.rawName;
342            this.type = builder.type;
343            this.nullable = builder.nullable;
344            this.metadata = builder.metadata;
345            this.defaultValue = builder.defaultValue;
346            this.elementSchema = builder.elementSchema;
347            this.comment = builder.comment;
348            this.props.putAll(builder.props);
349        }
350
351        /**
352         * The name of this entry.
353         */
354        private final String name;
355
356        /**
357         * The raw name of this entry.
358         */
359        private final String rawName;
360
361        /**
362         * Type of the entry, this determine which other fields are populated.
363         */
364        private final Schema.Type type;
365
366        /**
367         * Is this entry nullable or always valued.
368         */
369        private final boolean nullable;
370
371        /**
372         * Is this entry a metadata entry.
373         */
374        private final boolean metadata;
375
376        /**
377         * Default value for this entry.
378         */
379        private final Object defaultValue;
380
381        /**
382         * For type == record, the element type.
383         */
384        private final Schema elementSchema;
385
386        /**
387         * Allows to associate to this field a comment - for doc purposes, no use in the runtime.
388         */
389        private final String comment;
390
391        /**
392         * metadata
393         */
394        private final Map<String, String> props = new LinkedHashMap<>(0);
395
396        @Override
397        @JsonbTransient
398        public String getOriginalFieldName() {
399            return rawName != null ? rawName : name;
400        }
401
402        @Override
403        public String getProp(final String property) {
404            return this.props.get(property);
405        }
406
407        @Override
408        public Entry.Builder toBuilder() {
409            return new EntryImpl.BuilderImpl(this);
410        }
411
412        @Override
413        public String getName() {
414            return this.name;
415        }
416
417        @Override
418        public String getRawName() {
419            return this.rawName;
420        }
421
422        @Override
423        public Type getType() {
424            return this.type;
425        }
426
427        @Override
428        public boolean isNullable() {
429            return this.nullable;
430        }
431
432        @Override
433        public boolean isMetadata() {
434            return this.metadata;
435        }
436
437        @Override
438        public Object getDefaultValue() {
439            return this.defaultValue;
440        }
441
442        @Override
443        public Schema getElementSchema() {
444            return this.elementSchema;
445        }
446
447        @Override
448        public String getComment() {
449            return this.comment;
450        }
451
452        @Override
453        public Map<String, String> getProps() {
454            return this.props;
455        }
456
457        /**
458         * Plain builder matching {@link Entry} structure.
459         */
460        public static class BuilderImpl implements Entry.Builder {
461
462            private String name;
463
464            private String rawName;
465
466            private Schema.Type type;
467
468            private boolean nullable;
469
470            private boolean metadata = false;
471
472            private Object defaultValue;
473
474            private Schema elementSchema;
475
476            private String comment;
477
478            private final Map<String, String> props = new LinkedHashMap<>(0);
479
480            public BuilderImpl() {
481            }
482
483            private BuilderImpl(final Entry entry) {
484                this.name = entry.getName();
485                this.rawName = entry.getRawName();
486                this.nullable = entry.isNullable();
487                this.type = entry.getType();
488                this.comment = entry.getComment();
489                this.elementSchema = entry.getElementSchema();
490                this.defaultValue = entry.getDefaultValue();
491                this.metadata = entry.isMetadata();
492                this.props.putAll(entry.getProps());
493            }
494
495            public Builder withName(final String name) {
496                this.name = Schema.sanitizeConnectionName(name);
497                // if raw name is changed as follow name rule, use label to store raw name
498                // if not changed, not set label to save space
499                if (!name.equals(this.name)) {
500                    this.rawName = name;
501                }
502                return this;
503            }
504
505            @Override
506            public Builder withRawName(final String rawName) {
507                this.rawName = rawName;
508                return this;
509            }
510
511            @Override
512            public Builder withType(final Type type) {
513                this.type = type;
514                return this;
515            }
516
517            @Override
518            public Builder withNullable(final boolean nullable) {
519                this.nullable = nullable;
520                return this;
521            }
522
523            @Override
524            public Builder withMetadata(final boolean metadata) {
525                this.metadata = metadata;
526                return this;
527            }
528
529            @Override
530            public <T> Builder withDefaultValue(final T value) {
531                defaultValue = value;
532                return this;
533            }
534
535            @Override
536            public Builder withElementSchema(final Schema schema) {
537                elementSchema = schema;
538                return this;
539            }
540
541            @Override
542            public Builder withComment(final String comment) {
543                this.comment = comment;
544                return this;
545            }
546
547            @Override
548            public Builder withProp(final String key, final String value) {
549                props.put(key, value);
550                return this;
551            }
552
553            @Override
554            public Builder withProps(final Map props) {
555                if (props == null) {
556                    return this;
557                }
558                this.props.putAll(props);
559                return this;
560            }
561
562            public Entry build() {
563                return new EntryImpl(this);
564            }
565
566        }
567    }
568
569}