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; 017 018import static java.util.Optional.ofNullable; 019import static java.util.function.Function.identity; 020import static java.util.stream.Collectors.toConcurrentMap; 021import static java.util.stream.Collectors.toList; 022import static java.util.stream.Collectors.toMap; 023import static java.util.stream.Collectors.toSet; 024import static org.talend.sdk.component.runtime.manager.reflect.Constructors.findConstructor; 025 026import java.beans.ConstructorProperties; 027import java.io.Reader; 028import java.io.StringReader; 029import java.lang.annotation.Annotation; 030import java.lang.reflect.Array; 031import java.lang.reflect.Constructor; 032import java.lang.reflect.Executable; 033import java.lang.reflect.Field; 034import java.lang.reflect.ParameterizedType; 035import java.lang.reflect.Type; 036import java.util.AbstractMap; 037import java.util.ArrayList; 038import java.util.Collection; 039import java.util.Collections; 040import java.util.Comparator; 041import java.util.HashMap; 042import java.util.HashSet; 043import java.util.LinkedHashMap; 044import java.util.List; 045import java.util.Locale; 046import java.util.Map; 047import java.util.Set; 048import java.util.concurrent.ConcurrentHashMap; 049import java.util.concurrent.ConcurrentMap; 050import java.util.function.BiFunction; 051import java.util.function.BinaryOperator; 052import java.util.function.Function; 053import java.util.function.Predicate; 054import java.util.function.Supplier; 055import java.util.stream.Collector; 056import java.util.stream.Stream; 057 058import javax.json.Json; 059import javax.json.JsonArray; 060import javax.json.JsonNumber; 061import javax.json.JsonObject; 062import javax.json.JsonReader; 063import javax.json.JsonReaderFactory; 064import javax.json.JsonString; 065import javax.json.JsonValue; 066import javax.json.spi.JsonProvider; 067 068import org.apache.xbean.propertyeditor.PropertyEditorRegistry; 069import org.apache.xbean.recipe.ObjectRecipe; 070import org.apache.xbean.recipe.UnsetPropertiesRecipe; 071import org.mozilla.javascript.Context; 072import org.mozilla.javascript.Scriptable; 073import org.talend.sdk.component.api.record.Schema; 074import org.talend.sdk.component.api.service.configuration.Configuration; 075import org.talend.sdk.component.api.service.configuration.LocalConfiguration; 076import org.talend.sdk.component.runtime.internationalization.InternationalizationServiceFactory; 077import org.talend.sdk.component.runtime.manager.ParameterMeta; 078import org.talend.sdk.component.runtime.manager.reflect.parameterenricher.BaseParameterEnricher; 079import org.talend.sdk.component.runtime.manager.reflect.visibility.PayloadMapper; 080import org.talend.sdk.component.runtime.manager.reflect.visibility.VisibilityService; 081 082import lombok.RequiredArgsConstructor; 083import lombok.extern.slf4j.Slf4j; 084 085@Slf4j 086@RequiredArgsConstructor 087public class ReflectionService { 088 089 private final ParameterModelService parameterModelService; 090 091 private final PropertyEditorRegistry propertyEditorRegistry; 092 093 // note: we use xbean for now but we can need to add some caching inside if we 094 // abuse of it at runtime. 095 // not a concern for now. 096 // 097 // note2: compared to {@link ParameterModelService}, here we build the instance 098 // and we start from the config and not the 099 // model. 100 // 101 // IMPORTANT: ensure to be able to read all data (including collection) from a 102 // map to support system properties override 103 public Function<Map<String, String>, Object[]> parameterFactory(final Executable executable, 104 final Map<Class<?>, Object> precomputed, final List<ParameterMeta> metas) { 105 final ClassLoader loader = Thread.currentThread().getContextClassLoader(); 106 final Function<Supplier<Object>, Object> contextualSupplier = createContextualSupplier(loader); 107 final Collection<Function<Map<String, String>, Object>> factories = 108 Stream.of(executable.getParameters()).map(parameter -> { 109 final String name = parameterModelService.findName(parameter, parameter.getName()); 110 final Type parameterizedType = parameter.getParameterizedType(); 111 if (Class.class.isInstance(parameterizedType)) { 112 if (parameter.isAnnotationPresent(Configuration.class)) { 113 try { 114 final Class configClass = Class.class.cast(parameterizedType); 115 return createConfigFactory(precomputed, loader, contextualSupplier, parameter.getName(), 116 parameter.getAnnotation(Configuration.class), parameter.getAnnotations(), 117 configClass); 118 } catch (final NoSuchMethodException e) { 119 throw new IllegalArgumentException("No constructor for " + parameter); 120 } 121 } 122 final Object value = precomputed.get(parameterizedType); 123 if (value != null) { 124 if (Copiable.class.isInstance(value)) { 125 final Copiable copiable = Copiable.class.cast(value); 126 return (Function<Map<String, String>, Object>) config -> copiable.copy(value); 127 } 128 return (Function<Map<String, String>, Object>) config -> value; 129 } 130 final BiFunction<String, Map<String, Object>, Object> objectFactory = createObjectFactory( 131 loader, contextualSupplier, parameterizedType, translate(metas, name), precomputed); 132 return (Function<Map<String, String>, Object>) config -> objectFactory 133 .apply(name, Map.class.cast(config)); 134 } 135 136 if (ParameterizedType.class.isInstance(parameterizedType)) { 137 final ParameterizedType pt = ParameterizedType.class.cast(parameterizedType); 138 if (Class.class.isInstance(pt.getRawType())) { 139 if (Collection.class.isAssignableFrom(Class.class.cast(pt.getRawType()))) { 140 final Class<?> collectionType = Class.class.cast(pt.getRawType()); 141 final Type itemType = pt.getActualTypeArguments()[0]; 142 if (!Class.class.isInstance(itemType)) { 143 throw new IllegalArgumentException( 144 "For now we only support Collection<T> with T a Class<?>"); 145 } 146 final Class<?> itemClass = Class.class.cast(itemType); 147 148 // if we have services matching this type then we return the collection of 149 // services, otherwise 150 // we consider it is a config 151 final Collection<Object> services = precomputed 152 .entrySet() 153 .stream() 154 .sorted(Comparator.comparing(e -> e.getKey().getName())) 155 .filter(e -> itemClass.isAssignableFrom(e.getKey())) 156 .map(Map.Entry::getValue) 157 .collect(toList()); 158 159 // let's try to catch up built-in service 160 // here we have 1 entry per type and it is lazily created on get() 161 if (services.isEmpty()) { 162 final Object o = precomputed.get(itemClass); 163 if (o != null) { 164 services.add(o); 165 } 166 } 167 168 if (!services.isEmpty()) { 169 return (Function<Map<String, String>, Object>) config -> services; 170 } 171 172 // here we know we just want to instantiate a config list and not services 173 final Collector collector = Set.class == collectionType ? toSet() : toList(); 174 final List<ParameterMeta> parameterMetas = translate(metas, name); 175 final BiFunction<String, Map<String, Object>, Object> itemFactory = createObjectFactory( 176 loader, contextualSupplier, itemClass, parameterMetas, precomputed); 177 return (Function<Map<String, String>, Object>) config -> createList(loader, 178 contextualSupplier, name, collectionType, itemClass, collector, itemFactory, 179 Map.class.cast(config), parameterMetas, precomputed); 180 } 181 if (Map.class.isAssignableFrom(Class.class.cast(pt.getRawType()))) { 182 final Class<?> mapType = Class.class.cast(pt.getRawType()); 183 final Type keyItemType = pt.getActualTypeArguments()[0]; 184 final Type valueItemType = pt.getActualTypeArguments()[1]; 185 if (!Class.class.isInstance(keyItemType) || !Class.class.isInstance(valueItemType)) { 186 throw new IllegalArgumentException( 187 "For now we only support Map<A, B> with A and B a Class<?>"); 188 } 189 final Class<?> keyItemClass = Class.class.cast(keyItemType); 190 final Class<?> valueItemClass = Class.class.cast(valueItemType); 191 final List<ParameterMeta> parameterMetas = translate(metas, name); 192 final BiFunction<String, Map<String, Object>, Object> keyItemFactory = 193 createObjectFactory(loader, contextualSupplier, keyItemClass, parameterMetas, 194 precomputed); 195 final BiFunction<String, Map<String, Object>, Object> valueItemFactory = 196 createObjectFactory(loader, contextualSupplier, valueItemClass, parameterMetas, 197 precomputed); 198 final Collector collector = 199 createMapCollector(mapType, keyItemClass, valueItemClass, precomputed); 200 return (Function<Map<String, String>, Object>) config -> createMap(name, mapType, 201 keyItemFactory, valueItemFactory, collector, Map.class.cast(config)); 202 } 203 } 204 } 205 206 throw new IllegalArgumentException("Unsupported type: " + parameterizedType); 207 }).collect(toList()); 208 209 return config -> { 210 final Map<String, String> notNullConfig = ofNullable(config).orElseGet(Collections::emptyMap); 211 final PayloadValidator visitor = new PayloadValidator(); 212 if (!visitor.skip) { 213 visitor.globalPayload = new PayloadMapper((a, b) -> { 214 }).visitAndMap(metas, notNullConfig); 215 final PayloadMapper payloadMapper = new PayloadMapper(visitor); 216 payloadMapper.setGlobalPayload(visitor.globalPayload); 217 payloadMapper.visitAndMap(metas, notNullConfig); 218 visitor.throwIfFailed(); 219 } 220 return factories.stream().map(f -> f.apply(notNullConfig)).toArray(Object[]::new); 221 }; 222 } 223 224 public Function<Supplier<Object>, Object> createContextualSupplier(final ClassLoader loader) { 225 return supplier -> { 226 final Thread thread = Thread.currentThread(); 227 final ClassLoader old = thread.getContextClassLoader(); 228 thread.setContextClassLoader(loader); 229 try { 230 return supplier.get(); 231 } finally { 232 thread.setContextClassLoader(old); 233 } 234 }; 235 } 236 237 public Function<Map<String, String>, Object> createConfigFactory(final Map<Class<?>, Object> precomputed, 238 final ClassLoader loader, final Function<Supplier<Object>, Object> contextualSupplier, final String name, 239 final Configuration configuration, final Annotation[] allAnnotations, final Class<?> configClass) 240 throws NoSuchMethodException { 241 final Constructor constructor = configClass.getConstructor(); 242 final LocalConfiguration config = LocalConfiguration.class.cast(precomputed.get(LocalConfiguration.class)); 243 if (config == null) { 244 return c -> null; 245 } 246 247 final String prefix = configuration.value(); 248 final ParameterMeta objectMeta = 249 parameterModelService.buildParameter(prefix, prefix, new ParameterMeta.Source() { 250 251 @Override 252 public String name() { 253 return name; 254 } 255 256 @Override 257 public Class<?> declaringClass() { 258 return constructor.getDeclaringClass(); 259 } 260 }, configClass, allAnnotations, Stream 261 .of(ofNullable(constructor.getDeclaringClass().getPackage()).map(Package::getName).orElse("")) 262 .collect(toList()), true, new BaseParameterEnricher.Context(config)); 263 final BiFunction<String, Map<String, Object>, Object> objectFactory = createObjectFactory(loader, 264 contextualSupplier, configClass, objectMeta.getNestedParameters(), precomputed); 265 final Function<Map<String, Object>, Object> factory = c -> objectFactory.apply(prefix, c); 266 return ignoredDependentConfig -> { 267 final Map<String, Object> configMap = config 268 .keys() 269 .stream() 270 .filter(it -> objectMeta 271 .getNestedParameters() 272 .stream() 273 .anyMatch(p -> it.startsWith(prefix + '.' + p.getName()))) 274 .collect(toMap(identity(), config::get)); 275 return factory.apply(configMap); 276 }; 277 } 278 279 private List<ParameterMeta> translate(final List<ParameterMeta> metas, final String name) { 280 if (metas == null) { 281 return null; 282 } 283 return metas 284 .stream() 285 .filter(it -> it.getName().equals(name)) 286 .flatMap(it -> it.getNestedParameters().stream()) 287 .collect(toList()); 288 } 289 290 private Collector createMapCollector(final Class<?> mapType, final Class<?> keyItemClass, 291 final Class<?> valueItemClass, final Map<Class<?>, Object> precomputed) { 292 final Function<Map.Entry<?, ?>, Object> keyMapper = o -> doConvert(keyItemClass, o.getKey(), precomputed); 293 final Function<Map.Entry<?, ?>, Object> valueMapper = o -> doConvert(valueItemClass, o.getValue(), precomputed); 294 return ConcurrentMap.class.isAssignableFrom(mapType) ? toConcurrentMap(keyMapper, valueMapper) 295 : toMap(keyMapper, valueMapper); 296 } 297 298 private Object createList(final ClassLoader loader, final Function<Supplier<Object>, Object> contextualSupplier, 299 final String name, final Class<?> collectionType, final Class<?> itemClass, final Collector collector, 300 final BiFunction<String, Map<String, Object>, Object> itemFactory, final Map<String, Object> config, 301 final List<ParameterMeta> metas, final Map<Class<?>, Object> precomputed) { 302 final Object obj = config.get(name); 303 if (collectionType.isInstance(obj)) { 304 return Collection.class 305 .cast(obj) 306 .stream() 307 .map(o -> doConvert(itemClass, o, precomputed)) 308 .collect(collector); 309 } 310 311 // try to build it from the properties 312 // <value>[<index>] = xxxxx 313 // <value>[<index>].<property> = xxxxx 314 final Collection collection = List.class.isAssignableFrom(collectionType) ? new ArrayList<>() : new HashSet<>(); 315 final int maxLength = getArrayMaxLength(name, config); 316 int paramIdx = 0; 317 String[] args = null; 318 while (paramIdx < maxLength) { 319 final String configName = String.format("%s[%d]", name, paramIdx); 320 if (!config.containsKey(configName)) { 321 if (config.keySet().stream().anyMatch(k -> k.startsWith(configName + "."))) { // object 322 // mapping 323 if (paramIdx == 0) { 324 args = findArgsName(itemClass); 325 } 326 collection 327 .add(createObject(loader, contextualSupplier, itemClass, args, configName, config, metas, 328 precomputed)); 329 } else { 330 break; 331 } 332 } else { 333 collection.add(itemFactory.apply(configName, config)); 334 } 335 paramIdx++; 336 } 337 338 return collection; 339 } 340 341 private Integer getArrayMaxLength(final String prefix, final Map<String, Object> config) { 342 return ofNullable(config.get(prefix + "[length]")) 343 .map(String::valueOf) 344 .map(Integer::parseInt) 345 .orElse(Integer.MAX_VALUE); 346 } 347 348 private Object createMap(final String name, final Class<?> mapType, 349 final BiFunction<String, Map<String, Object>, Object> keyItemFactory, 350 final BiFunction<String, Map<String, Object>, Object> valueItemFactory, final Collector collector, 351 final Map<String, Object> config) { 352 final Object obj = config.get(name); 353 if (mapType.isInstance(obj)) { 354 return Map.class.cast(obj).entrySet().stream().collect(collector); 355 } 356 357 // try to build it from the properties 358 // <value>.key[<index>] = xxxxx 359 // <value>.key[<index>].<property> = xxxxx 360 // <value>.value[<index>] = xxxxx 361 // <value>.value[<index>].<property> = xxxxx 362 final Map map = ConcurrentMap.class.isAssignableFrom(mapType) ? new ConcurrentHashMap() : new HashMap(); 363 int paramIdx = 0; 364 do { 365 final String keyConfigName = String.format("%s.key[%d]", name, paramIdx); 366 final String valueConfigName = String.format("%s.value[%d]", name, paramIdx); 367 if (!config.containsKey(keyConfigName) || !config.containsKey(valueConfigName)) { // quick test first 368 if (config.keySet().stream().noneMatch(k -> k.startsWith(keyConfigName)) 369 && config.keySet().stream().noneMatch(k -> k.startsWith(valueConfigName))) { 370 break; 371 } 372 } 373 map.put(keyItemFactory.apply(keyConfigName, config), valueItemFactory.apply(valueConfigName, config)); 374 paramIdx++; 375 } while (true); 376 377 return map; 378 } 379 380 private BiFunction<String, Map<String, Object>, Object> createObjectFactory(final ClassLoader loader, 381 final Function<Supplier<Object>, Object> contextualSupplier, final Type type, 382 final List<ParameterMeta> metas, final Map<Class<?>, Object> precomputed) { 383 final Class clazz = Class.class.cast(type); 384 if (clazz.isPrimitive() || Primitives.unwrap(clazz) != clazz || String.class == clazz) { 385 return (name, config) -> doConvert(clazz, config.get(name), precomputed); 386 } 387 if (clazz.isEnum()) { 388 return (name, config) -> ofNullable(config.get(name)) 389 .map(String.class::cast) 390 .map(String::trim) 391 .filter(it -> !it.isEmpty()) 392 .map(v -> Enum.valueOf(clazz, v)) 393 .orElse(null); 394 } 395 396 final String[] args = findArgsName(clazz); 397 return (name, config) -> contextualSupplier 398 .apply(() -> createObject(loader, contextualSupplier, clazz, args, name, config, metas, precomputed)); 399 } 400 401 private String[] findArgsName(final Class clazz) { 402 return Stream 403 .of(clazz.getConstructors()) 404 .filter(c -> c.isAnnotationPresent(ConstructorProperties.class)) 405 .findFirst() 406 .map(c -> ConstructorProperties.class.cast(c.getAnnotation(ConstructorProperties.class)).value()) 407 .orElse(null); 408 } 409 410 private JsonValue createJsonValue(final Object value, final Map<Class<?>, Object> precomputed, 411 final Function<Reader, JsonReader> fallbackReaderCreator) { 412 final StringReader sr = new StringReader(String.valueOf(value).trim()); 413 try (final JsonReader reader = ofNullable(precomputed.get(JsonReaderFactory.class)) 414 .map(JsonReaderFactory.class::cast) 415 .map(f -> f.createReader(sr)) 416 .orElseGet(() -> fallbackReaderCreator.apply(sr))) { 417 return reader.read(); 418 } 419 } 420 421 private Object createObject(final ClassLoader loader, final Function<Supplier<Object>, Object> contextualSupplier, 422 final Class clazz, final String[] args, final String name, final Map<String, Object> config, 423 final List<ParameterMeta> metas, final Map<Class<?>, Object> precomputed) { 424 final Object potentialJsonValue = config.get(name); 425 if (JsonObject.class == clazz && String.class.isInstance(potentialJsonValue)) { 426 return createJsonValue(potentialJsonValue, precomputed, Json::createReader).asJsonObject(); 427 } 428 if (propertyEditorRegistry.findConverter(clazz) != null && Schema.class.isAssignableFrom(clazz)) { 429 final Object configValue = config.get(name); 430 if (String.class.isInstance(configValue)) { 431 return propertyEditorRegistry.getValue(clazz, String.class.cast(configValue)); 432 } 433 } 434 if (propertyEditorRegistry.findConverter(clazz) != null && config.size() == 1) { 435 final Object configValue = config.values().iterator().next(); 436 if (String.class.isInstance(configValue)) { 437 return propertyEditorRegistry.getValue(clazz, String.class.cast(configValue)); 438 } 439 } 440 441 final String prefix = name.isEmpty() ? "" : name + "."; 442 final ObjectRecipe recipe = newRecipe(clazz); 443 recipe.setProperty("rawProperties", new UnsetPropertiesRecipe()); // todo: log unused props? 444 ofNullable(args).ifPresent(recipe::setConstructorArgNames); 445 446 final Map<String, Object> specificMapping = config 447 .entrySet() 448 .stream() 449 .filter(e -> e.getKey().startsWith(prefix) || prefix.isEmpty()) 450 .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); 451 452 // extract map configuration 453 final Map<String, Object> mapEntries = specificMapping.entrySet().stream().filter(e -> { 454 final String key = e.getKey(); 455 final int idxStart = key.indexOf('[', prefix.length()); 456 return idxStart > 0 && ((idxStart > ".key".length() && key.startsWith(".key", idxStart - ".key".length())) 457 || (idxStart > ".value".length() && key.startsWith(".value", idxStart - ".value".length()))); 458 }) 459 .sorted(this::sortIndexEntry) 460 .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, noMerge(), LinkedHashMap::new)); 461 mapEntries.keySet().forEach(specificMapping::remove); 462 final Map<String, Object> preparedMaps = new HashMap<>(); 463 for (final Map.Entry<String, Object> entry : mapEntries.entrySet()) { 464 final String key = entry.getKey(); 465 final int idxStart = key.indexOf('[', prefix.length()); 466 String enclosingName = key.substring(prefix.length(), idxStart); 467 if (enclosingName.endsWith(".key")) { 468 enclosingName = enclosingName.substring(0, enclosingName.length() - ".key".length()); 469 } else if (enclosingName.endsWith(".value")) { 470 enclosingName = enclosingName.substring(0, enclosingName.length() - ".value".length()); 471 } else { 472 throw new IllegalArgumentException("'" + key + "' is not supported, it is considered as a map binding"); 473 } 474 if (preparedMaps.containsKey(enclosingName)) { 475 continue; 476 } 477 if (isUiParam(enclosingName)) { // normally cleaned up by the UI@back integration but safeguard here 478 continue; 479 } 480 481 final Type genericType = 482 findField(normalizeName(enclosingName.substring(enclosingName.indexOf('.') + 1), metas), clazz) 483 .getGenericType(); 484 final ParameterizedType pt = validateObject(clazz, enclosingName, genericType); 485 486 final Class<?> keyType = Class.class.cast(pt.getActualTypeArguments()[0]); 487 final Class<?> valueType = Class.class.cast(pt.getActualTypeArguments()[1]); 488 preparedMaps 489 .put(enclosingName, createMap(prefix + enclosingName, Map.class, 490 createObjectFactory(loader, contextualSupplier, keyType, metas, precomputed), 491 createObjectFactory(loader, contextualSupplier, valueType, metas, precomputed), 492 createMapCollector(Class.class.cast(pt.getRawType()), keyType, valueType, precomputed), 493 new HashMap<>(mapEntries))); 494 } 495 496 // extract list configuration 497 final Map<String, Object> listEntries = specificMapping.entrySet().stream().filter(e -> { 498 final String key = e.getKey(); 499 final int idxStart = key.indexOf('[', prefix.length()); 500 final int idxEnd = key.indexOf(']', prefix.length()); 501 final int sep = key.indexOf('.', prefix.length() + 1); 502 return idxStart > 0 && key.endsWith("]") && (sep > idxEnd || sep < 0); 503 }) 504 .sorted(this::sortIndexEntry) 505 .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, noMerge(), LinkedHashMap::new)); 506 listEntries.keySet().forEach(specificMapping::remove); 507 final Map<String, Object> preparedLists = new HashMap<>(); 508 for (final Map.Entry<String, Object> entry : listEntries.entrySet()) { 509 final String key = entry.getKey(); 510 final int idxStart = key.indexOf('[', prefix.length()); 511 final String enclosingName = key.substring(prefix.length(), idxStart); 512 if (preparedLists.containsKey(enclosingName)) { 513 continue; 514 } 515 if (isUiParam(enclosingName)) { 516 continue; 517 } 518 519 final Type genericType = findField(normalizeName(enclosingName, metas), clazz).getGenericType(); 520 if (Class.class.isInstance(genericType)) { 521 final Class<?> arrayClass = Class.class.cast(genericType); 522 if (arrayClass.isArray()) { 523 // we could use Array.newInstance but for now use the list, shouldn't impact 524 // much the perf 525 final Collection<?> list = Collection.class 526 .cast(createList(loader, contextualSupplier, prefix + enclosingName, List.class, 527 arrayClass.getComponentType(), toList(), createObjectFactory(loader, 528 contextualSupplier, arrayClass.getComponentType(), metas, precomputed), 529 new HashMap<>(listEntries), metas, precomputed)); 530 531 // we need that conversion to ensure the type matches 532 final Object array = Array.newInstance(arrayClass.getComponentType(), list.size()); 533 int idx = 0; 534 for (final Object o : list) { 535 Array.set(array, idx++, o); 536 } 537 preparedLists.put(enclosingName, array); 538 continue; 539 } // else let it fail with the "collection" error 540 } 541 542 // now we need an actual collection type 543 final ParameterizedType pt = validateCollection(clazz, enclosingName, genericType); 544 final Type itemType = pt.getActualTypeArguments()[0]; 545 preparedLists 546 .put(enclosingName, 547 createList(loader, contextualSupplier, prefix + enclosingName, 548 Class.class.cast(pt.getRawType()), Class.class.cast(itemType), toList(), 549 createObjectFactory(loader, contextualSupplier, itemType, metas, precomputed), 550 new HashMap<>(listEntries), metas, precomputed)); 551 } 552 553 // extract nested Object configurations 554 final Map<String, Object> objectEntries = specificMapping.entrySet().stream().filter(e -> { 555 final String key = e.getKey(); 556 return key.indexOf('.', prefix.length() + 1) > 0; 557 }).sorted((o1, o2) -> { 558 final String key1 = o1.getKey(); 559 final String key2 = o2.getKey(); 560 if (key1.equals(key2)) { 561 return 0; 562 } 563 564 final String nestedName1 = key1.substring(prefix.length(), key1.indexOf('.', prefix.length() + 1)); 565 final String nestedName2 = key2.substring(prefix.length(), key2.indexOf('.', prefix.length() + 1)); 566 567 final int idxStart1 = nestedName1.indexOf('['); 568 final int idxStart2 = nestedName2.indexOf('['); 569 if (idxStart1 > 0 && idxStart2 > 0 570 && nestedName1.substring(0, idxStart1).equals(nestedName2.substring(0, idxStart2))) { 571 final int idx1 = parseIndex(nestedName1.substring(idxStart1 + 1, nestedName1.length() - 1)); 572 final int idx2 = parseIndex(nestedName2.substring(idxStart2 + 1, nestedName2.length() - 1)); 573 return idx1 - idx2; 574 } 575 return key1.compareTo(key2); 576 }).collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (o, o2) -> { 577 throw new IllegalArgumentException("Can't merge " + o + " and " + o2); 578 }, LinkedHashMap::new)); 579 objectEntries.keySet().forEach(specificMapping::remove); 580 final Map<String, Object> preparedObjects = new HashMap<>(); 581 for (final Map.Entry<String, Object> entry : objectEntries.entrySet()) { 582 final String nestedName = 583 entry.getKey().substring(prefix.length(), entry.getKey().indexOf('.', prefix.length() + 1)); 584 if (isUiParam(nestedName)) { 585 continue; 586 } 587 if (nestedName.endsWith("]")) { // complex lists 588 final int idxStart = nestedName.indexOf('['); 589 if (idxStart > 0) { 590 final String listName = nestedName.substring(0, idxStart); 591 final Field field = findField(normalizeName(listName, metas), clazz); 592 if (ParameterizedType.class.isInstance(field.getGenericType())) { 593 final ParameterizedType pt = ParameterizedType.class.cast(field.getGenericType()); 594 if (Class.class.isInstance(pt.getRawType())) { 595 final Class<?> rawType = Class.class.cast(pt.getRawType()); 596 if (Set.class.isAssignableFrom(rawType)) { 597 addListElement(loader, contextualSupplier, config, prefix, preparedObjects, nestedName, 598 listName, pt, () -> new HashSet<>(2), translate(metas, listName), precomputed); 599 } else if (Collection.class.isAssignableFrom(rawType)) { 600 addListElement(loader, contextualSupplier, config, prefix, preparedObjects, nestedName, 601 listName, pt, () -> new ArrayList<>(2), translate(metas, listName), 602 precomputed); 603 } else { 604 throw new IllegalArgumentException("unsupported configuration type: " + pt); 605 } 606 continue; 607 } else { 608 throw new IllegalArgumentException("unsupported configuration type: " + pt); 609 } 610 } else { 611 throw new IllegalArgumentException("unsupported configuration type: " + field.getType()); 612 } 613 } else { 614 throw new IllegalArgumentException("unsupported configuration type: " + nestedName); 615 } 616 } 617 final String fieldName = normalizeName(nestedName, metas); 618 if (preparedObjects.containsKey(fieldName)) { 619 continue; 620 } 621 final Field field = findField(fieldName, clazz); 622 preparedObjects 623 .put(fieldName, 624 createObject(loader, contextualSupplier, field.getType(), findArgsName(field.getType()), 625 prefix + nestedName, config, translate(metas, nestedName), precomputed)); 626 } 627 628 // other entries can be directly set 629 final Map<String, Object> normalizedConfig = specificMapping 630 .entrySet() 631 .stream() 632 .filter(e -> e.getKey().startsWith(prefix) && e.getKey().substring(prefix.length()).indexOf('.') < 0) 633 .collect(toMap(e -> { 634 final String specificConfig = e.getKey().substring(prefix.length()); 635 final int index = specificConfig.indexOf('['); 636 if (index > 0) { 637 final int end = specificConfig.indexOf(']', index); 638 if (end > index) { // > 0 would work too 639 // here we need to normalize it to let xbean understand it 640 String leadingString = specificConfig.substring(0, index); 641 if (leadingString.endsWith(".key") || leadingString.endsWith(".value")) { // map 642 leadingString = leadingString.substring(0, leadingString.lastIndexOf('.')); 643 } 644 return leadingString + specificConfig.substring(end + 1); 645 } 646 } 647 return specificConfig; 648 }, Map.Entry::getValue)); 649 650 // now bind it all to the recipe and builder the instance 651 preparedMaps.forEach(recipe::setFieldProperty); 652 preparedLists.forEach(recipe::setFieldProperty); 653 preparedObjects.forEach(recipe::setFieldProperty); 654 if (!normalizedConfig.isEmpty()) { 655 normalizedConfig 656 .entrySet() 657 .stream() 658 .map(it -> normalize(it, metas)) 659 .forEach(e -> recipe.setFieldProperty(e.getKey(), e.getValue())); 660 } 661 return recipe.create(loader); 662 } 663 664 private ParameterizedType validateCollection(final Class clazz, final String enclosingName, 665 final Type genericType) { 666 if (!ParameterizedType.class.isInstance(genericType)) { 667 throw new IllegalArgumentException( 668 clazz + "#" + enclosingName + " should be a generic collection and not a " + genericType); 669 } 670 final ParameterizedType pt = ParameterizedType.class.cast(genericType); 671 if (pt.getActualTypeArguments().length != 1 || !Class.class.isInstance(pt.getActualTypeArguments()[0])) { 672 throw new IllegalArgumentException(clazz + "#" + enclosingName 673 + " should use concrete class items and not a " + pt.getActualTypeArguments()[0]); 674 } 675 return pt; 676 } 677 678 private ParameterizedType validateObject(final Class clazz, final String enclosingName, final Type genericType) { 679 if (!ParameterizedType.class.isInstance(genericType)) { 680 throw new IllegalArgumentException( 681 clazz + "#" + enclosingName + " should be a generic map and not a " + genericType); 682 } 683 final ParameterizedType pt = ParameterizedType.class.cast(genericType); 684 if (pt.getActualTypeArguments().length != 2 || !Class.class.isInstance(pt.getActualTypeArguments()[0]) 685 || !Class.class.isInstance(pt.getActualTypeArguments()[1])) { 686 throw new IllegalArgumentException(clazz + "#" + enclosingName 687 + " should be a generic map with a key and value class type (" + pt + ")"); 688 } 689 return pt; 690 } 691 692 private ObjectRecipe newRecipe(final Class clazz) { 693 final ObjectRecipe recipe = new ObjectRecipe(clazz); 694 recipe.setRegistry(propertyEditorRegistry); 695 recipe.allow(org.apache.xbean.recipe.Option.FIELD_INJECTION); 696 recipe.allow(org.apache.xbean.recipe.Option.PRIVATE_PROPERTIES); 697 recipe.allow(org.apache.xbean.recipe.Option.CASE_INSENSITIVE_PROPERTIES); 698 recipe.allow(org.apache.xbean.recipe.Option.IGNORE_MISSING_PROPERTIES); 699 return recipe; 700 } 701 702 private boolean isUiParam(final String name) { 703 final int dollar = name.indexOf('$'); 704 if (dollar >= 0 && name.indexOf("_name", dollar) > dollar) { 705 log.warn("{} is not a valid configuration, it shouldn't be passed to the runtime", name); 706 return true; 707 } 708 return false; 709 } 710 711 private BinaryOperator<Object> noMerge() { 712 return (a, b) -> { 713 throw new IllegalArgumentException("Conflict"); 714 }; 715 } 716 717 private Map.Entry<String, Object> normalize(final Map.Entry<String, Object> it, final List<ParameterMeta> metas) { 718 return metas == null ? it : metas.stream().filter(m -> m.getName().equals(it.getKey())).findFirst().map(m -> { 719 final String name = findName(m); 720 if (name.equals(it.getKey())) { 721 return it; 722 } 723 return new AbstractMap.SimpleEntry<>(name, it.getValue()); 724 }).orElse(it); 725 } 726 727 private String normalizeName(final String name, final List<ParameterMeta> metas) { 728 return metas == null ? name 729 : metas.stream().filter(m -> m.getName().equals(name)).findFirst().map(this::findName).orElse(name); 730 } 731 732 private String findName(final ParameterMeta m) { 733 return ofNullable(m.getSource()).map(ParameterMeta.Source::name).orElse(m.getName()); 734 } 735 736 // CHECKSTYLE:OFF 737 private void addListElement(final ClassLoader loader, final Function<Supplier<Object>, Object> contextualSupplier, 738 final Map<String, Object> config, final String prefix, final Map<String, Object> preparedObjects, 739 final String nestedName, final String listName, final ParameterizedType pt, final Supplier<?> init, 740 final List<ParameterMeta> metas, final Map<Class<?>, Object> precomputed) { 741 // CHECKSTYLE:ON 742 final Collection<Object> aggregator = 743 Collection.class.cast(preparedObjects.computeIfAbsent(listName, k -> init.get())); 744 final Class<?> itemType = Class.class.cast(pt.getActualTypeArguments()[0]); 745 final int index = parseIndex(nestedName.substring(listName.length() + 1, nestedName.length() - 1)); 746 final int maxSize = getArrayMaxLength(prefix + listName, config); 747 if (aggregator.size() <= index && index < maxSize) { 748 aggregator 749 .add(createObject(loader, contextualSupplier, itemType, findArgsName(itemType), prefix + nestedName, 750 config, metas, precomputed)); 751 } 752 } 753 754 private Field findField(final String name, final Class clazz) { 755 Class<?> type = clazz; 756 while (type != Object.class && type != null) { 757 try { 758 return type.getDeclaredField(name); 759 } catch (final NoSuchFieldException e) { 760 // no-op 761 } 762 type = type.getSuperclass(); 763 } 764 throw new IllegalArgumentException( 765 String.format("Unknown field: %s in class: %s.", name, clazz != null ? clazz.getName() : "null")); 766 } 767 768 private int sortIndexEntry(final Map.Entry<String, Object> e1, final Map.Entry<String, Object> e2) { 769 final String name1 = e1.getKey(); 770 final String name2 = e2.getKey(); 771 final int index1 = name1.indexOf('['); 772 final int index2 = name2.indexOf('['); 773 774 // same prefix -> sort specifically 775 if (index1 > 0 && index2 == index1 && name1.substring(0, index1).equals(name2.substring(0, index1))) { 776 final int end1 = name1.indexOf(']', index1); 777 final int end2 = name2.indexOf(']', index2); 778 if (end1 > index1 && end2 > index2) { 779 final String idx1 = name1.substring(index1 + 1, end1); 780 final String idx2 = name2.substring(index2 + 1, end2); 781 return parseIndex(idx1) - parseIndex(idx2); 782 } // else not matching so use default sorting 783 } 784 return name1.compareTo(name2); 785 } 786 787 private int parseIndex(final String name) { 788 if ("length".equals(name)) { 789 return -1; // not important, skipped anyway 790 } 791 return Integer.parseInt(name); 792 } 793 794 private Object doConvert(final Class<?> type, final Object value, final Map<Class<?>, Object> precomputed) { 795 if (value == null) { // get the primitive default 796 return getPrimitiveDefault(type); 797 } 798 if (type.isInstance(value)) { // no need of any conversion 799 return value; 800 } 801 if (JsonValue.class.isAssignableFrom(type)) { 802 return createJsonValue(value, precomputed, Json::createReader); 803 } 804 if (propertyEditorRegistry.findConverter(type) != null) { // go through string to convert the value 805 return propertyEditorRegistry.getValue(type, String.valueOf(value)); 806 } 807 throw new IllegalArgumentException("Can't convert '" + value + "' to " + type); 808 } 809 810 private Object getPrimitiveDefault(final Class<?> type) { 811 final Type convergedType = Primitives.unwrap(type); 812 if (char.class == convergedType || short.class == convergedType || byte.class == convergedType 813 || int.class == convergedType) { 814 return 0; 815 } 816 if (long.class == convergedType) { 817 return 0L; 818 } 819 if (boolean.class == convergedType) { 820 return false; 821 } 822 if (double.class == convergedType) { 823 return 0.; 824 } 825 if (float.class == convergedType) { 826 return 0f; 827 } 828 return null; 829 } 830 831 public static class JavascriptRegex implements Predicate<CharSequence> { 832 833 private final String regex; 834 835 private final String indicators; 836 837 private JavascriptRegex(final String regex) { 838 if (regex.startsWith("/") && regex.length() > 1) { 839 final int end = regex.lastIndexOf('/'); 840 if (end < 0) { 841 this.regex = regex; 842 indicators = ""; 843 } else { 844 this.regex = regex.substring(1, end); 845 indicators = regex.substring(end + 1); 846 } 847 } else { 848 this.regex = regex; 849 indicators = ""; 850 } 851 } 852 853 @Override 854 public boolean test(final CharSequence text) { 855 final String script = "new RegExp(regex, indicators).test(text)"; 856 final Context context = Context.enter(); 857 try { 858 final Scriptable scope = context.initStandardObjects(); 859 scope.put("text", scope, text); 860 scope.put("regex", scope, regex); 861 scope.put("indicators", scope, indicators); 862 return Context.toBoolean(context.evaluateString(scope, script, "test", 0, null)); 863 } catch (final Exception e) { 864 return false; 865 } finally { 866 Context.exit(); 867 } 868 } 869 } 870 871 public interface Messages { 872 873 String required(String property); 874 875 String min(String property, double bound, double value); 876 877 String max(String property, double bound, double value); 878 879 String minLength(String property, double bound, int value); 880 881 String maxLength(String property, double bound, int value); 882 883 String minItems(String property, double bound, int value); 884 885 String maxItems(String property, double bound, int value); 886 887 String uniqueItems(String property); 888 889 String pattern(String property, String pattern); 890 } 891 892 @RequiredArgsConstructor 893 private static class PayloadValidator implements PayloadMapper.OnParameter { 894 895 private static final VisibilityService VISIBILITY_SERVICE = new VisibilityService(JsonProvider.provider()); 896 897 private static final Messages MESSAGES = new InternationalizationServiceFactory(Locale::getDefault) 898 .create(Messages.class, PayloadValidator.class.getClassLoader()); 899 900 private final boolean skip = Boolean.getBoolean("talend.component.configuration.validation.skip"); 901 902 private final Collection<String> errors = new ArrayList<>(); 903 904 JsonObject globalPayload; 905 906 @Override 907 public void onParameter(final ParameterMeta meta, final JsonValue value) { 908 if (!VISIBILITY_SERVICE.build(meta).isVisible(globalPayload)) { 909 return; 910 } 911 912 if (Boolean.parseBoolean(meta.getMetadata().get("tcomp::validation::required")) 913 && value == JsonValue.NULL) { 914 errors.add(MESSAGES.required(meta.getPath())); 915 } 916 final Map<String, String> metadata = meta.getMetadata(); 917 { 918 final String min = metadata.get("tcomp::validation::min"); 919 if (min != null) { 920 final double bound = Double.parseDouble(min); 921 if (value.getValueType() == JsonValue.ValueType.NUMBER 922 && JsonNumber.class.cast(value).doubleValue() < bound) { 923 errors.add(MESSAGES.min(meta.getPath(), bound, JsonNumber.class.cast(value).doubleValue())); 924 } 925 } 926 } 927 { 928 final String max = metadata.get("tcomp::validation::max"); 929 if (max != null) { 930 final double bound = Double.parseDouble(max); 931 if (value.getValueType() == JsonValue.ValueType.NUMBER 932 && JsonNumber.class.cast(value).doubleValue() > bound) { 933 errors.add(MESSAGES.max(meta.getPath(), bound, JsonNumber.class.cast(value).doubleValue())); 934 } 935 } 936 } 937 { 938 final String min = metadata.get("tcomp::validation::minLength"); 939 if (min != null) { 940 final double bound = Double.parseDouble(min); 941 if (value.getValueType() == JsonValue.ValueType.STRING) { 942 final String val = JsonString.class.cast(value).getString(); 943 if (val.length() < bound) { 944 errors.add(MESSAGES.minLength(meta.getPath(), bound, val.length())); 945 } 946 } 947 } 948 } 949 { 950 final String max = metadata.get("tcomp::validation::maxLength"); 951 if (max != null) { 952 final double bound = Double.parseDouble(max); 953 if (value.getValueType() == JsonValue.ValueType.STRING) { 954 final String val = JsonString.class.cast(value).getString(); 955 if (val.length() > bound) { 956 errors.add(MESSAGES.maxLength(meta.getPath(), bound, val.length())); 957 } 958 } 959 } 960 } 961 { 962 final String min = metadata.get("tcomp::validation::minItems"); 963 if (min != null) { 964 final double bound = Double.parseDouble(min); 965 if (value.getValueType() == JsonValue.ValueType.ARRAY && value.asJsonArray().size() < bound) { 966 errors.add(MESSAGES.minItems(meta.getPath(), bound, value.asJsonArray().size())); 967 } 968 } 969 } 970 { 971 final String max = metadata.get("tcomp::validation::maxItems"); 972 if (max != null) { 973 final double bound = Double.parseDouble(max); 974 if (value.getValueType() == JsonValue.ValueType.ARRAY && value.asJsonArray().size() > bound) { 975 errors.add(MESSAGES.maxItems(meta.getPath(), bound, value.asJsonArray().size())); 976 } 977 } 978 } 979 { 980 final String unique = metadata.get("tcomp::validation::uniqueItems"); 981 if (unique != null) { 982 if (value.getValueType() == JsonValue.ValueType.ARRAY) { 983 final JsonArray array = value.asJsonArray(); 984 if (new HashSet<>(array).size() != array.size()) { 985 errors.add(MESSAGES.uniqueItems(meta.getPath())); 986 } 987 } 988 } 989 } 990 { 991 final String pattern = metadata.get("tcomp::validation::pattern"); 992 if (pattern != null && value.getValueType() == JsonValue.ValueType.STRING) { 993 final String val = JsonString.class.cast(value).getString(); 994 if (!new JavascriptRegex(pattern).test(CharSequence.class.cast(val))) { 995 errors.add(MESSAGES.pattern(meta.getPath(), pattern)); 996 } 997 } 998 } 999 } 1000 1001 private void throwIfFailed() { 1002 if (!errors.isEmpty()) { 1003 throw new IllegalArgumentException("- " + String.join("\n- ", errors)); 1004 } 1005 } 1006 } 1007 1008 /** 1009 * Helper function for creating an instance from a configuration map. 1010 * 1011 * @param clazz Class of the wanted instance. 1012 * @param <T> Type managed 1013 * @return function that generate the wanted instance when calling 1014 * {@link BiFunction#apply(java.lang.Object, java.lang.Object)} with a config name and configuration {@link Map}. 1015 */ 1016 public <T> BiFunction<String, Map<String, Object>, T> createObjectFactory(final Class<T> clazz) { 1017 final Map precomputed = Collections.emptyMap(); 1018 if (clazz.isPrimitive() || Primitives.unwrap(clazz) != clazz || clazz == String.class) { 1019 return (name, config) -> (T) doConvert(clazz, config.get(name), precomputed); 1020 } 1021 if (clazz.isEnum()) { 1022 return (name, 1023 config) -> (T) ofNullable(config.get(name)) 1024 .map(String.class::cast) 1025 .map(String::trim) 1026 .filter(it -> !it.isEmpty()) 1027 .map(v -> Enum.valueOf((Class<Enum>) clazz, v)) 1028 .orElse(null); 1029 } 1030 final ClassLoader loader = Thread.currentThread().getContextClassLoader(); 1031 final Function<Supplier<Object>, Object> contextualSupplier = createContextualSupplier(loader); 1032 final Constructor<?> c = findConstructor(clazz); 1033 final ParameterModelService s = new ParameterModelService(new PropertyEditorRegistry()); 1034 final List<ParameterMeta> metas = s.buildParameterMetas(c, c.getDeclaringClass().getPackage().getName(), null); 1035 final String[] args = findArgsName(clazz); 1036 return (name, config) -> (T) contextualSupplier 1037 .apply(() -> createObject(loader, contextualSupplier, clazz, args, name, config, metas, precomputed)); 1038 } 1039}