001/**
002 * Copyright (C) 2006-2021 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.api.record;
017
018import java.io.StringReader;
019import java.nio.charset.Charset;
020import java.nio.charset.CharsetEncoder;
021import java.nio.charset.StandardCharsets;
022import java.time.temporal.Temporal;
023import java.util.Base64;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.Date;
027import java.util.List;
028import java.util.Map;
029import java.util.Objects;
030import java.util.Optional;
031
032import javax.json.Json;
033import javax.json.JsonValue;
034
035public interface Schema {
036
037    /**
038     * @return the type of this schema.
039     */
040    Type getType();
041
042    /**
043     * @return the nested element schema for arrays.
044     */
045    Schema getElementSchema();
046
047    /**
048     * @return the entries for records.
049     */
050    List<Entry> getEntries();
051
052    default Entry getEntry(final String name) {
053        return Optional
054                .ofNullable(getEntries()) //
055                .orElse(Collections.emptyList()) //
056                .stream() //
057                .filter((Entry e) -> Objects.equals(e.getName(), name)) //
058                .findFirst() //
059                .orElse(null);
060    }
061
062    /**
063     * @return the metadata props
064     */
065    Map<String, String> getProps();
066
067    /**
068     * @param property : property name.
069     * @return the requested metadata prop
070     */
071    String getProp(String property);
072
073    /**
074     * Get a property values from schema with its name.
075     * 
076     * @param name : property's name.
077     * @return property's value.
078     */
079    default JsonValue getJsonProp(final String name) {
080        final String prop = this.getProp(name);
081        if (prop == null) {
082            return null;
083        }
084        try {
085            return Json.createParser(new StringReader(prop)).getValue();
086        } catch (RuntimeException ex) {
087            return Json.createValue(prop);
088        }
089    }
090
091    enum Type {
092        RECORD(new Class<?>[] { Record.class }),
093        ARRAY(new Class<?>[] { Collection.class }),
094        STRING(new Class<?>[] { String.class }),
095        BYTES(new Class<?>[] { byte[].class, Byte[].class }),
096        INT(new Class<?>[] { Integer.class }),
097        LONG(new Class<?>[] { Long.class }),
098        FLOAT(new Class<?>[] { Float.class }),
099        DOUBLE(new Class<?>[] { Double.class }),
100        BOOLEAN(new Class<?>[] { Boolean.class }),
101        DATETIME(new Class<?>[] { Long.class, Date.class, Temporal.class });
102
103        /** All compatibles Java classes */
104        private final Class<?>[] classes;
105
106        Type(final Class<?>[] classes) {
107            this.classes = classes;
108        }
109
110        /**
111         * Check if input can be affected to an entry of this type.
112         * 
113         * @param input : object.
114         * @return true if input is null or ok.
115         */
116        public boolean isCompatible(final Object input) {
117            if (input == null) {
118                return true;
119            }
120            for (final Class<?> clazz : classes) {
121                if (clazz.isInstance(input)) {
122                    return true;
123                }
124            }
125            return false;
126        }
127    }
128
129    interface Entry {
130
131        /**
132         * @return The name of this entry.
133         */
134        String getName();
135
136        /**
137         * @return The raw name of this entry.
138         */
139        String getRawName();
140
141        /**
142         * @return the raw name of this entry if exists, else return name.
143         */
144        String getOriginalFieldName();
145
146        /**
147         * @return Type of the entry, this determine which other fields are populated.
148         */
149        Type getType();
150
151        /**
152         * @return Is this entry nullable or always valued.
153         */
154        boolean isNullable();
155
156        /**
157         * @param <T> the default value type.
158         * @return Default value for this entry.
159         */
160        <T> T getDefaultValue();
161
162        /**
163         * @return For type == record, the element type.
164         */
165        Schema getElementSchema();
166
167        /**
168         * @return Allows to associate to this field a comment - for doc purposes, no use in the runtime.
169         */
170        String getComment();
171
172        /**
173         * @return the metadata props
174         */
175        Map<String, String> getProps();
176
177        /**
178         * @param property : property name.
179         * @return the requested metadata prop
180         */
181        String getProp(String property);
182
183        /**
184         * Get a property values from entry with its name.
185         * 
186         * @param name : property's name.
187         * @return property's value.
188         */
189        default JsonValue getJsonProp(final String name) {
190            final String prop = this.getProp(name);
191            if (prop == null) {
192                return null;
193            }
194            try {
195                return Json.createParser(new StringReader(prop)).getValue();
196            } catch (RuntimeException ex) {
197                return Json.createValue(prop);
198            }
199        }
200
201        // Map<String, Object> metadata <-- DON'T DO THAT, ENSURE ANY META IS TYPED!
202
203        /**
204         * Plain builder matching {@link Entry} structure.
205         */
206        interface Builder {
207
208            Builder withName(String name);
209
210            Builder withRawName(String rawName);
211
212            Builder withType(Type type);
213
214            Builder withNullable(boolean nullable);
215
216            <T> Builder withDefaultValue(T value);
217
218            Builder withElementSchema(Schema schema);
219
220            Builder withComment(String comment);
221
222            Builder withProps(Map<String, String> props);
223
224            Builder withProp(String key, String value);
225
226            Entry build();
227
228        }
229    }
230
231    /**
232     * Allows to build a schema.
233     */
234    interface Builder {
235
236        /**
237         * @param type schema type.
238         * @return this builder.
239         */
240        Builder withType(Type type);
241
242        /**
243         * @param entry element for either an array or record type.
244         * @return this builder.
245         */
246        Builder withEntry(Entry entry);
247
248        /**
249         * @param schema nested element schema.
250         * @return this builder.
251         */
252        Builder withElementSchema(Schema schema);
253
254        /**
255         * @param props schema properties
256         * @return this builder
257         */
258        Builder withProps(Map<String, String> props);
259
260        /**
261         *
262         * @param key the prop key name
263         * @param value the prop value
264         * @return this builder
265         */
266        Builder withProp(String key, String value);
267
268        /**
269         * @return the described schema.
270         */
271        Schema build();
272    }
273
274    /**
275     * Sanitize name to be avro compatible.
276     * 
277     * @param name : original name.
278     * @return avro compatible name.
279     */
280    static String sanitizeConnectionName(final String name) {
281        if (name == null || name.isEmpty()) {
282            return name;
283        }
284
285        char current = name.charAt(0);
286        final CharsetEncoder ascii = Charset.forName(StandardCharsets.US_ASCII.name()).newEncoder();
287        final boolean skipFirstChar = ((!ascii.canEncode(current)) || (!Character.isLetter(current) && current != '_'))
288                && name.length() > 1 && (!Character.isDigit(name.charAt(1)));
289
290        final StringBuilder sanitizedBuilder = new StringBuilder();
291
292        if (!skipFirstChar) {
293            if (((!Character.isLetter(current)) && current != '_') || (!ascii.canEncode(current))) {
294                sanitizedBuilder.append('_');
295            } else {
296                sanitizedBuilder.append(current);
297            }
298        }
299        for (int i = 1; i < name.length(); i++) {
300            current = name.charAt(i);
301            if (!ascii.canEncode(current)) {
302                if (Character.isLowerCase(current) || Character.isUpperCase(current)) {
303                    sanitizedBuilder.append('_');
304                } else {
305                    final byte[] encoded =
306                            Base64.getEncoder().encode(name.substring(i, i + 1).getBytes(StandardCharsets.UTF_8));
307                    final String enc = new String(encoded);
308                    if (sanitizedBuilder.length() == 0 && Character.isDigit(enc.charAt(0))) {
309                        sanitizedBuilder.append('_');
310                    }
311                    for (int iter = 0; iter < enc.length(); iter++) {
312                        if (Character.isLetterOrDigit(enc.charAt(iter))) {
313                            sanitizedBuilder.append(enc.charAt(iter));
314                        } else {
315                            sanitizedBuilder.append('_');
316                        }
317                    }
318                }
319            } else if (Character.isLetterOrDigit(current)) {
320                sanitizedBuilder.append(current);
321            } else {
322                sanitizedBuilder.append('_');
323            }
324
325        }
326        return sanitizedBuilder.toString();
327    }
328
329}