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}