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.chain.internal; 017 018import static java.util.Collections.singletonList; 019import static java.util.stream.Collectors.toList; 020import static java.util.stream.Collectors.toMap; 021import static java.util.stream.Collectors.toSet; 022 023import java.io.BufferedReader; 024import java.io.IOException; 025import java.io.InputStream; 026import java.io.InputStreamReader; 027import java.lang.reflect.InvocationTargetException; 028import java.util.AbstractMap; 029import java.util.ArrayList; 030import java.util.Collection; 031import java.util.HashMap; 032import java.util.HashSet; 033import java.util.Iterator; 034import java.util.List; 035import java.util.Map; 036import java.util.ServiceLoader; 037import java.util.Set; 038import java.util.TreeMap; 039import java.util.concurrent.atomic.AtomicBoolean; 040import java.util.concurrent.atomic.AtomicInteger; 041import java.util.concurrent.atomic.AtomicLong; 042import java.util.concurrent.atomic.AtomicReference; 043import java.util.function.Function; 044import java.util.function.Supplier; 045 046import javax.json.bind.Jsonb; 047import javax.json.bind.JsonbBuilder; 048import javax.json.bind.JsonbConfig; 049 050import org.talend.sdk.component.api.processor.OutputEmitter; 051import org.talend.sdk.component.api.record.Record; 052import org.talend.sdk.component.api.service.record.RecordBuilderFactory; 053import org.talend.sdk.component.runtime.base.Lifecycle; 054import org.talend.sdk.component.runtime.input.Input; 055import org.talend.sdk.component.runtime.input.Mapper; 056import org.talend.sdk.component.runtime.manager.ComponentManager; 057import org.talend.sdk.component.runtime.manager.chain.AutoChunkProcessor; 058import org.talend.sdk.component.runtime.manager.chain.ChainedMapper; 059import org.talend.sdk.component.runtime.manager.chain.GroupKeyProvider; 060import org.talend.sdk.component.runtime.manager.chain.Job; 061import org.talend.sdk.component.runtime.output.InputFactory; 062import org.talend.sdk.component.runtime.output.OutputFactory; 063import org.talend.sdk.component.runtime.output.Processor; 064import org.talend.sdk.component.runtime.output.ProcessorImpl; 065import org.talend.sdk.component.runtime.record.RecordBuilderFactoryImpl; 066import org.talend.sdk.component.runtime.record.RecordConverters; 067 068import lombok.AllArgsConstructor; 069import lombok.Data; 070import lombok.Getter; 071import lombok.RequiredArgsConstructor; 072import lombok.extern.slf4j.Slf4j; 073 074public class JobImpl implements Job { 075 076 public static class NodeBuilderImpl implements NodeBuilder { 077 078 private final List<Component> nodes = new ArrayList<>(); 079 080 private final Map<String, Map<String, Object>> properties = new HashMap<>(); 081 082 @Override 083 public NodeBuilder property(final String name, final Object value) { 084 final Component lastComponent = nodes.get(nodes.size() - 1); 085 properties.computeIfAbsent(lastComponent.getId(), s -> new HashMap<>()); 086 properties.get(lastComponent.getId()).put(name, value); 087 return this; 088 } 089 090 @Override 091 public NodeBuilder component(final String id, final String uri) { 092 nodes.add(new Component(id, DSLParser.parse(uri))); 093 return this; 094 } 095 096 @Override 097 public LinkBuilder connections() { 098 return new LinkBuilder(nodes, properties); 099 } 100 101 } 102 103 @Slf4j 104 @RequiredArgsConstructor 105 public static class LinkBuilder implements Job.FromBuilder, Builder { 106 107 private final List<Component> nodes; 108 109 private final Map<String, Map<String, Object>> properties; 110 111 private final List<Edge> edges = new ArrayList<>(); 112 113 private final Map<Integer, Set<Component>> levels = new TreeMap<>(); 114 115 @Override 116 public ToBuilder from(final String id, final String branch) { 117 final Component from = nodes 118 .stream() 119 .filter(node -> node.getId().equals(id)) 120 .findFirst() 121 .orElseThrow( 122 () -> new IllegalStateException("No component with id '" + id + "' in created components")); 123 124 edges 125 .stream() 126 .filter(edge -> edge.getFrom().getNode().getId().equals(id) 127 && edge.getFrom().getBranch().equals(branch)) 128 .findFirst() 129 .ifPresent(edge -> { 130 throw new IllegalStateException( 131 "(" + id + "," + branch + ") node is already connected : " + edge); 132 }); 133 134 return new To(nodes, edges, new Connection(from, branch), this); 135 } 136 137 public void doBuild() { 138 final List<Component> orphans = nodes 139 .stream() 140 .filter(n -> edges 141 .stream() 142 .noneMatch(l -> l.getFrom().getNode().equals(n) || l.getTo().getNode().equals(n))) 143 .collect(toList()); 144 orphans.forEach(o -> log.warn("component '" + o + "' is orphan in this graph. it will be ignored.")); 145 nodes.removeAll(orphans); 146 147 // set up sources 148 nodes 149 .stream() 150 .filter(node -> edges.stream().noneMatch(l -> l.getTo().getNode().equals(node))) 151 .forEach(component -> component.setSource(true)); 152 calculateGraphOrder(0, new HashSet<>(nodes), new ArrayList<>(edges), levels); 153 } 154 155 private void calculateGraphOrder(final int order, final Set<Component> nodes, final List<Edge> edges, 156 final Map<Integer, Set<Component>> orderedGraph) { 157 if (edges.isEmpty()) { 158 orderedGraph.put(order, nodes); // last nodes 159 return; 160 } 161 final Set<Component> startingNodes = nodes 162 .stream() 163 .filter(node -> edges.stream().noneMatch(l -> l.getTo().getNode().equals(node))) 164 .collect(toSet()); 165 if (order == 0 && startingNodes.isEmpty()) { 166 throw new IllegalStateException("There is no starting component in this graph."); 167 } 168 final List<Edge> level = edges 169 .stream() 170 .filter(edge -> startingNodes.contains(edge.getFrom().getNode())) 171 .filter(edge -> edges 172 .stream() 173 .filter(others -> edge.getTo().getNode().equals(others.getTo().getNode())) 174 .map(others -> others.getFrom().getNode()) 175 .allMatch(startingNodes::contains)) 176 .collect(toList()); 177 if (level.isEmpty()) { 178 throw new IllegalStateException("the job pipeline has cyclic connection"); 179 } 180 final Set<Component> components = level.stream().map(edge -> edge.getFrom().getNode()).collect(toSet()); 181 orderedGraph.put(order, components); 182 edges.removeAll(level); 183 nodes.removeAll(components); 184 calculateGraphOrder(order + 1, nodes, edges, orderedGraph); 185 } 186 187 @Override 188 public JobExecutor build() { 189 doBuild(); 190 return new JobExecutor(levels, edges, properties); 191 } 192 } 193 194 @RequiredArgsConstructor 195 private static class To implements ToBuilder { 196 197 private final List<Component> nodes; 198 199 private final List<Edge> edges; 200 201 private final Connection from; 202 203 private final Builder builder; 204 205 @Override 206 public Builder to(final String id, final String branch) { 207 final Component to = nodes 208 .stream() 209 .filter(node -> node.getId().equals(id)) 210 .findFirst() 211 .orElseThrow(() -> new IllegalStateException("No component with id '" + id + "' in created nodes")); 212 213 edges 214 .stream() 215 .filter(edge -> edge.getTo().getNode().getId().equals(id) 216 && edge.getTo().getBranch().equals(branch)) 217 .findFirst() 218 .ifPresent(edge -> { 219 throw new IllegalStateException( 220 "(" + id + "," + branch + ") node is already connected : " + edge); 221 }); 222 edges.add(new Edge(from, new Connection(to, branch))); 223 return builder; 224 } 225 } 226 227 @Getter 228 @Slf4j 229 @RequiredArgsConstructor 230 public static class JobExecutor implements Job.ExecutorBuilder { 231 232 private final Map<Integer, Set<Component>> levels; 233 234 private final List<Edge> edges; 235 236 private final Map<String, Map<String, Object>> componentProperties; 237 238 private final Map<String, Object> jobProperties = new HashMap<>(); 239 240 private final ComponentManager manager = ComponentManager.instance(); 241 242 @Override 243 public ExecutorBuilder property(final String name, final Object value) { 244 jobProperties.put(name, value); 245 return this; 246 } 247 248 @Override 249 public void run() { 250 ExecutorBuilder runner = this; 251 final Object o = jobProperties.get(ExecutorBuilder.class.getName()); 252 if (ExecutorBuilder.class.isInstance(o)) { 253 runner = ExecutorBuilder.class.cast(o); 254 } else if (Class.class.isInstance(o)) { 255 runner = newRunner(Class.class.cast(o)); 256 } else if (String.class.isInstance(o)) { 257 final String name = String.class.cast(o).trim(); 258 if (!"standalone".equalsIgnoreCase(name) && !"default".equalsIgnoreCase(name) 259 && !"local".equalsIgnoreCase(name)) { 260 if ("beam".equalsIgnoreCase(name)) { 261 try { 262 runner = newRunner(Thread.currentThread().getContextClassLoader(), 263 "org.talend.sdk.component.runtime.beam.chain.impl.BeamExecutor"); 264 } catch (final RuntimeException re) { 265 log 266 .error("Can't instantiate beam job integration, " 267 + "did you add org.talend.sdk.component:component-runtime-beam in your dependencies", 268 re); 269 } 270 } else { 271 runner = newRunner(Thread.currentThread().getContextClassLoader(), name); 272 } 273 } 274 } else if (o != null) { 275 throw new IllegalArgumentException(o + " is not an ExecutionBuilder"); 276 } else { 277 final ClassLoader loader = Thread.currentThread().getContextClassLoader(); 278 try (final InputStream stream = 279 loader.getResourceAsStream("META-INF/services/" + ExecutorBuilder.class.getName())) { 280 if (stream != null) { 281 runner = new BufferedReader(new InputStreamReader(stream)) 282 .lines() 283 .map(String::trim) 284 .filter(s -> !s.startsWith("#") && !s.isEmpty()) 285 .findFirst() 286 .map(clazz -> newRunner(loader, clazz)) 287 .orElse(this); 288 } 289 } catch (final IOException e) { 290 log.debug(e.getMessage(), e); 291 } 292 } 293 294 if (runner == this) { 295 JobExecutor.class.cast(runner).localRun(); 296 } else { 297 runner.run(); 298 } 299 } 300 301 private ExecutorBuilder newRunner(final ClassLoader loader, final String clazz) { 302 try { 303 final Class<? extends ExecutorBuilder> aClass = 304 (Class<? extends ExecutorBuilder>) loader.loadClass(clazz); 305 return newRunner(aClass); 306 } catch (final ClassNotFoundException e) { 307 throw new IllegalArgumentException(e); 308 } 309 } 310 311 private ExecutorBuilder newRunner(final Class<? extends ExecutorBuilder> runnerType) { 312 try { 313 try { 314 return runnerType.getConstructor(JobExecutor.class).newInstance(JobExecutor.this); 315 } catch (final NoSuchMethodException e) { 316 return runnerType.getConstructor().newInstance(); 317 } 318 } catch (final NoSuchMethodException | InstantiationException | IllegalAccessException e1) { 319 throw new IllegalArgumentException(e1); 320 } catch (InvocationTargetException e1) { 321 throw new IllegalArgumentException(e1.getTargetException()); 322 } 323 } 324 325 private void localRun() { 326 final long maxRecords = 327 Long.parseLong(String.valueOf(getJobProperties().getOrDefault("streaming.maxRecords", "-1"))); 328 final Map<String, InputRunner> inputs = 329 levels.values().stream().flatMap(Collection::stream).filter(Component::isSource).map(n -> { 330 final Mapper mapper = manager 331 .findMapper(n.getNode().getFamily(), n.getNode().getComponent(), 332 n.getNode().getVersion(), n.getNode().getConfiguration()) 333 .orElseThrow(() -> new IllegalStateException("No mapper found for: " + n.getNode())); 334 return new AbstractMap.SimpleEntry<>(n.getId(), new InputRunner(mapper, maxRecords)); 335 }).collect(toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); 336 337 final Map<String, AutoChunkProcessor> processors = levels 338 .values() 339 .stream() 340 .flatMap(Collection::stream) 341 .filter(component -> !component.isSource()) 342 .map(component -> { 343 final Processor processor = manager 344 .findProcessor(component.getNode().getFamily(), component.getNode().getComponent(), 345 component.getNode().getVersion(), component.getNode().getConfiguration()) 346 .orElseThrow(() -> new IllegalStateException( 347 "No processor found for:" + component.getNode())); 348 final AtomicInteger maxBatchSize = new AtomicInteger(1); 349 if (ProcessorImpl.class.isInstance(processor)) { 350 ProcessorImpl.class 351 .cast(processor) 352 .getInternalConfiguration() 353 .entrySet() 354 .stream() 355 .filter(it -> it.getKey().endsWith("$maxBatchSize") && it.getValue() != null 356 && !it.getValue().trim().isEmpty()) 357 .findFirst() 358 .ifPresent(val -> { 359 try { 360 maxBatchSize.set(Integer.parseInt(val.getValue().trim())); 361 } catch (final NumberFormatException nfe) { 362 throw new IllegalArgumentException("Invalid configuratoin: " + val); 363 } 364 }); 365 } 366 return new AbstractMap.SimpleEntry<>(component.getId(), 367 new AutoChunkProcessor(maxBatchSize.get(), processor)); 368 }) 369 .collect(toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); 370 371 final RecordConverters.MappingMetaRegistry registry = new RecordConverters.MappingMetaRegistry(); 372 final AtomicReference<DataOutputFactory> outs = new AtomicReference<>(); 373 try { 374 final Map<String, AtomicBoolean> sourcesWithData = levels 375 .values() 376 .stream() 377 .flatMap(Collection::stream) 378 .filter(Component::isSource) 379 .map(component -> new AbstractMap.SimpleEntry<>(component.getId(), new AtomicBoolean(true))) 380 .collect(toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); 381 processors.values().forEach(Lifecycle::start); // start processor 382 final Map<String, Map<String, Map<String, Collection<Record>>>> flowData = new HashMap<>(); 383 final AtomicBoolean running = new AtomicBoolean(true); 384 do { 385 levels.forEach((level, components) -> components.forEach((Component component) -> { 386 if (component.isSource()) { 387 final InputRunner source = inputs.get(component.getId()); 388 final Record data = source.next(); 389 if (data == null) { 390 sourcesWithData.get(component.getId()).set(false); 391 return; 392 } 393 final String key = getKeyProvider(component.getId()) 394 .apply(new GroupContextImpl(data, component.getId(), "__default__")); 395 flowData.computeIfAbsent(component.getId(), s -> new HashMap<>()); 396 flowData.get(component.getId()).computeIfAbsent("__default__", s -> new TreeMap<>()); 397 flowData 398 .get(component.getId()) 399 .get("__default__") 400 .computeIfAbsent(key, k -> new ArrayList<>()) 401 .add(data); 402 } else { 403 final List<Edge> connections = 404 getConnections(getEdges(), component, e -> e.getTo().getNode()); 405 final DataInputFactory dataInputFactory = new DataInputFactory(); 406 if (connections.size() == 1) { 407 final Edge edge = connections.get(0); 408 final String fromId = edge.getFrom().getNode().getId(); 409 final String fromBranch = edge.getFrom().getBranch(); 410 final String toBranch = edge.getTo().getBranch(); 411 412 final Map<String, Map<String, Collection<Record>>> idData = flowData.get(fromId); 413 final Record data = idData == null ? null : pollFirst(idData.get(fromBranch)); 414 if (data != null) { 415 dataInputFactory.withInput(toBranch, singletonList(data)); 416 } 417 } else { // need grouping 418 final Map<String, Map<String, Collection<Record>>> availableDataForStep = 419 new HashMap<>(); 420 connections.forEach(edge -> { 421 final String fromId = edge.getFrom().getNode().getId(); 422 final String fromBranch = edge.getFrom().getBranch(); 423 final String toBranch = edge.getTo().getBranch(); 424 final Map<String, Collection<Record>> data = 425 flowData.get(fromId) == null ? null : flowData.get(fromId).get(fromBranch); 426 if (data != null && !data.isEmpty()) { 427 availableDataForStep.put(toBranch, data); 428 } 429 }); 430 431 final Map<String, String> joined = joinWithFusionSort(availableDataForStep); 432 if (!joined.isEmpty() && connections.size() == joined.size()) { 433 joined.forEach((k, v) -> { 434 final Collection data = availableDataForStep.get(k).remove(v); 435 dataInputFactory.withInput(k, data); 436 }); 437 } 438 } 439 if (dataInputFactory.inputs.isEmpty()) { 440 if (level.equals(levels.size() - 1) 441 && sourcesWithData.entrySet().stream().noneMatch(e -> e.getValue().get())) { 442 running.set(false); 443 } 444 return; 445 } 446 final AutoChunkProcessor processor = processors.get(component.getId()); 447 448 final DataOutputFactory dataOutputFactory = new DataOutputFactory(getManager() 449 .findPlugin(processor.plugin()) 450 .get() 451 .get(ComponentManager.AllServices.class) 452 .getServices(), registry); 453 processor.onElement(dataInputFactory, dataOutputFactory); 454 dataOutputFactory.getOutputs().forEach((branch, data) -> data.forEach(item -> { 455 final String key = getKeyProvider(component.getId()) 456 .apply(new GroupContextImpl(item, component.getId(), branch)); 457 flowData.computeIfAbsent(component.getId(), s -> new HashMap<>()); 458 flowData.get(component.getId()).computeIfAbsent(branch, s -> new TreeMap<>()); 459 flowData 460 .get(component.getId()) 461 .get(branch) 462 .computeIfAbsent(key, k -> new ArrayList<>()) 463 .add(item); 464 })); 465 outs.set(dataOutputFactory); 466 } 467 })); 468 } while (running.get()); 469 } finally { 470 if (outs.get() != null) { 471 processors.values().forEach(p -> p.flush(outs.get())); 472 } 473 processors.values().forEach(Lifecycle::stop); 474 inputs.values().forEach(InputRunner::stop); 475 levels 476 .values() 477 .stream() 478 .flatMap(Collection::stream) 479 .map(Component::getId) 480 .forEach(LocalSequenceHolder::clean); 481 } 482 } 483 484 private Map<String, String> 485 joinWithFusionSort(final Map<String, Map<String, Collection<Record>>> dataByBranch) { 486 final Map<String, String> join = new HashMap<>(); 487 dataByBranch.forEach((branch1, records1) -> { 488 dataByBranch.forEach((branch2, records2) -> { 489 if (!branch1.equals(branch2)) { 490 for (final String key1 : records1.keySet()) { 491 for (final String key2 : records2.keySet()) { 492 if (key1.equals(key2)) { 493 join.putIfAbsent(branch1, key1); 494 join.putIfAbsent(branch2, key2); 495 } else if (key1.compareTo(key2) < 0) { 496 break;// see fusion sort 497 } 498 } 499 } 500 } 501 }); 502 }); 503 return join; 504 } 505 506 private Record pollFirst(final Map<String, Collection<Record>> data) { 507 if (data == null || data.isEmpty()) { 508 return null; 509 } 510 while (!data.isEmpty()) { 511 final String key = data.keySet().iterator().next(); 512 final Collection<Record> items = data.get(key); 513 if (!items.isEmpty()) { 514 final Iterator<Record> iterator = items.iterator(); 515 final Record item = iterator.next(); 516 iterator.remove(); 517 return item; 518 } else { 519 data.remove(key); 520 } 521 } 522 return null; 523 } 524 525 private List<Job.Edge> getConnections(final List<Job.Edge> edges, final Job.Component step, 526 final Function<Edge, Component> direction) { 527 return edges.stream().filter(edge -> direction.apply(edge).equals(step)).collect(toList()); 528 } 529 530 public GroupKeyProvider getKeyProvider(final String componentId) { 531 if (componentProperties.get(componentId) != null) { 532 final Object o = componentProperties.get(componentId).get(GroupKeyProvider.class.getName()); 533 if (GroupKeyProvider.class.isInstance(o)) { 534 return new GroupKeyProviderImpl(GroupKeyProvider.class.cast(o)); 535 } 536 } 537 538 final Object o = jobProperties.get(GroupKeyProvider.class.getName()); 539 if (GroupKeyProvider.class.isInstance(o)) { 540 return new GroupKeyProviderImpl(GroupKeyProvider.class.cast(o)); 541 } 542 543 final ServiceLoader<GroupKeyProvider> services = ServiceLoader.load(GroupKeyProvider.class); 544 if (services.iterator().hasNext()) { 545 return services.iterator().next(); 546 } 547 548 return LocalSequenceHolder.cleanAndGet(componentId); 549 } 550 } 551 552 @Data 553 private static class GroupContextImpl implements GroupKeyProvider.GroupContext { 554 555 private final Record data; 556 557 private final String componentId; 558 559 private final String branchName; 560 } 561 562 public static class LocalSequenceHolder { 563 564 private static final Map<String, AtomicLong> GENERATORS = new HashMap<>(); 565 566 public static GroupKeyProvider cleanAndGet(final String name) { 567 GENERATORS.put(name, new AtomicLong(0)); 568 return c -> Long.toString(GENERATORS.get(name).incrementAndGet()); 569 } 570 571 public static void clean(final String name) { 572 GENERATORS.remove(name); 573 } 574 } 575 576 @Slf4j 577 private static class InputRunner { 578 579 private final Mapper chainedMapper; 580 581 private final Input input; 582 583 private final long maxRecords; 584 585 private long currentRecords; 586 587 private InputRunner(final Mapper mapper, final long maxRecords) { 588 this.maxRecords = maxRecords; 589 RuntimeException error = null; 590 try { 591 mapper.start(); 592 chainedMapper = new ChainedMapper(mapper, mapper.split(mapper.assess()).iterator()); 593 chainedMapper.start(); 594 input = chainedMapper.create(); 595 input.start(); 596 } catch (final RuntimeException re) { 597 error = re; 598 throw re; 599 } finally { 600 try { 601 mapper.stop(); 602 } catch (final RuntimeException re) { 603 if (error == null) { 604 throw re; 605 } 606 log.error(re.getMessage(), re); 607 } 608 } 609 } 610 611 public Record next() { 612 if (maxRecords > 0 && currentRecords >= maxRecords) { 613 return null; 614 } 615 final Object next = input.next(); 616 if (next == null) { 617 return null; 618 } 619 currentRecords++; 620 return Record.class.cast(next); 621 } 622 623 public void stop() { 624 RuntimeException error = null; 625 try { 626 if (input != null) { 627 input.stop(); 628 } 629 } catch (final RuntimeException re) { 630 error = re; 631 throw re; 632 } finally { 633 try { 634 if (chainedMapper != null) { 635 chainedMapper.stop(); 636 } 637 } catch (final RuntimeException re) { 638 if (error == null) { 639 throw re; 640 } 641 log.error(re.getMessage(), re); 642 } 643 } 644 } 645 } 646 647 @Data 648 private static class DataOutputFactory implements OutputFactory { 649 650 private final Map<Class<?>, Object> services; 651 652 private final RecordConverters.MappingMetaRegistry registry; 653 654 private final Map<String, Collection<Record>> outputs = new HashMap<>(); 655 656 @Override 657 public OutputEmitter create(final String name) { 658 return new OutputEmitterImpl(name, registry); 659 } 660 661 @AllArgsConstructor 662 private class OutputEmitterImpl implements OutputEmitter { 663 664 private final String name; 665 666 private final RecordConverters.MappingMetaRegistry registry; 667 668 @Override 669 public void emit(final Object value) { 670 outputs 671 .computeIfAbsent(name, k -> new ArrayList<>()) 672 .add(new RecordConverters() 673 .toRecord(registry, value, () -> Jsonb.class.cast(services.get(Jsonb.class)), 674 () -> RecordBuilderFactory.class 675 .cast(services.get(RecordBuilderFactory.class)))); 676 } 677 } 678 } 679 680 private static class DataInputFactory implements InputFactory { 681 682 private final Map<String, Iterator<Object>> inputs = new HashMap<>(); 683 684 private volatile Jsonb jsonb; 685 686 private volatile RecordBuilderFactory factory; 687 688 private volatile RecordConverters.MappingMetaRegistry registry; 689 690 private DataInputFactory withInput(final String branch, final Collection<Object> branchData) { 691 inputs.put(branch, branchData.iterator()); 692 return this; 693 } 694 695 @Override 696 public Object read(final String name) { 697 final Iterator<?> iterator = inputs.get(name); 698 if (iterator != null && iterator.hasNext()) { 699 return map(iterator.next()); 700 } 701 return null; 702 } 703 704 private Object map(final Object next) { 705 if (next == null || Record.class.isInstance(next)) { 706 return next; 707 } 708 709 final String str = jsonb().get().toJson(next); 710 // primitives mainly, not that accurate in main code but for now not forbidden 711 if (str.equals(next.toString())) { 712 return next; 713 } 714 if (registry == null) { 715 synchronized (this) { 716 if (registry == null) { 717 registry = new RecordConverters.MappingMetaRegistry(); 718 } 719 } 720 } 721 // pojo 722 return new RecordConverters().toRecord(registry, next, jsonb(), () -> { 723 if (factory == null) { 724 synchronized (this) { 725 if (factory == null) { 726 factory = new RecordBuilderFactoryImpl("test"); 727 } 728 } 729 } 730 return factory; 731 }); 732 } 733 734 private Supplier<Jsonb> jsonb() { 735 return () -> { 736 if (jsonb == null) { 737 synchronized (this) { 738 if (jsonb == null) { 739 jsonb = JsonbBuilder.create(new JsonbConfig().setProperty("johnzon.cdi.activated", false)); 740 } 741 } 742 } 743 return jsonb; 744 }; 745 } 746 } 747 748 @AllArgsConstructor 749 protected static class GroupKeyProviderImpl implements GroupKeyProvider { 750 751 private final GroupKeyProvider delegate; 752 753 @Override 754 public String apply(final GroupKeyProvider.GroupContext context) { 755 return delegate.apply(context); 756 } 757 } 758}