001/** 002 * Copyright (C) 2006-2018 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.junit; 017 018import static java.lang.Math.abs; 019import static java.util.Collections.emptyMap; 020import static java.util.concurrent.TimeUnit.MINUTES; 021import static java.util.concurrent.TimeUnit.SECONDS; 022import static java.util.stream.Collectors.joining; 023import static java.util.stream.Collectors.toList; 024import static org.apache.ziplock.JarLocation.jarLocation; 025import static org.junit.Assert.fail; 026import static org.talend.sdk.component.junit.SimpleFactory.configurationByExample; 027 028import java.util.ArrayList; 029import java.util.Collection; 030import java.util.HashMap; 031import java.util.Iterator; 032import java.util.List; 033import java.util.Map; 034import java.util.Objects; 035import java.util.Optional; 036import java.util.Queue; 037import java.util.Set; 038import java.util.Spliterator; 039import java.util.Spliterators; 040import java.util.concurrent.ConcurrentLinkedQueue; 041import java.util.concurrent.CountDownLatch; 042import java.util.concurrent.ExecutionException; 043import java.util.concurrent.ExecutorService; 044import java.util.concurrent.Executors; 045import java.util.concurrent.Future; 046import java.util.concurrent.Semaphore; 047import java.util.concurrent.TimeoutException; 048import java.util.concurrent.atomic.AtomicInteger; 049import java.util.stream.Stream; 050import java.util.stream.StreamSupport; 051 052import javax.json.JsonObject; 053import javax.json.bind.Jsonb; 054import javax.json.bind.JsonbConfig; 055 056import org.apache.xbean.finder.filter.Filter; 057import org.talend.sdk.component.junit.lang.StreamDecorator; 058import org.talend.sdk.component.runtime.base.Lifecycle; 059import org.talend.sdk.component.runtime.input.Input; 060import org.talend.sdk.component.runtime.input.Mapper; 061import org.talend.sdk.component.runtime.manager.ComponentFamilyMeta; 062import org.talend.sdk.component.runtime.manager.ComponentManager; 063import org.talend.sdk.component.runtime.manager.ContainerComponentRegistry; 064import org.talend.sdk.component.runtime.manager.chain.Job; 065import org.talend.sdk.component.runtime.manager.json.PreComputedJsonpProvider; 066import org.talend.sdk.component.runtime.output.Processor; 067 068import lombok.RequiredArgsConstructor; 069import lombok.extern.slf4j.Slf4j; 070 071@Slf4j 072public class BaseComponentsHandler implements ComponentsHandler { 073 074 static final ThreadLocal<State> STATE = new ThreadLocal<>(); 075 076 private final ThreadLocal<PreState> initState = ThreadLocal.withInitial(PreState::new); 077 078 protected String packageName; 079 080 protected Collection<String> isolatedPackages; 081 082 public BaseComponentsHandler withIsolatedPackage(final String packageName, final String... packages) { 083 isolatedPackages = 084 Stream.concat(Stream.of(packageName), Stream.of(packages)).filter(Objects::nonNull).collect(toList()); 085 if (isolatedPackages.isEmpty()) { 086 isolatedPackages = null; 087 } 088 return this; 089 } 090 091 public EmbeddedComponentManager start() { 092 final EmbeddedComponentManager embeddedComponentManager = new EmbeddedComponentManager(packageName) { 093 094 @Override 095 protected boolean isContainerClass(final Filter filter, final String name) { 096 if (name == null) { 097 return super.isContainerClass(filter, null); 098 } 099 return (isolatedPackages == null || isolatedPackages.stream().noneMatch(name::startsWith)) 100 && super.isContainerClass(filter, name); 101 } 102 103 @Override 104 public void close() { 105 try { 106 final State state = STATE.get(); 107 if (state.jsonb != null) { 108 try { 109 state.jsonb.close(); 110 } catch (final Exception e) { 111 // no-op: not important 112 } 113 } 114 STATE.remove(); 115 initState.remove(); 116 } finally { 117 super.close(); 118 } 119 } 120 }; 121 122 STATE.set(new State(embeddedComponentManager, new ArrayList<>(), initState.get().emitter)); 123 return embeddedComponentManager; 124 } 125 126 @Override 127 public Outputs collect(final Processor processor, final ControllableInputFactory inputs) { 128 return collect(processor, inputs, 10); 129 } 130 131 /** 132 * Collects all outputs of a processor. 133 * 134 * @param processor the processor to run while there are inputs. 135 * @param inputs the input factory, when an input will return null it will stop the 136 * processing. 137 * @param bundleSize the bundle size to use. 138 * @return a map where the key is the output name and the value a stream of the 139 * output values. 140 */ 141 @Override 142 public Outputs collect(final Processor processor, final ControllableInputFactory inputs, final int bundleSize) { 143 final AutoChunkProcessor autoChunkProcessor = new AutoChunkProcessor(bundleSize, processor); 144 autoChunkProcessor.start(); 145 final Outputs outputs = new Outputs(); 146 try { 147 while (inputs.hasMoreData()) { 148 autoChunkProcessor.onElement(inputs, name -> value -> { 149 final List aggregator = outputs.data.computeIfAbsent(name, n -> new ArrayList<>()); 150 aggregator.add(value); 151 }); 152 } 153 } finally { 154 autoChunkProcessor.stop(); 155 } 156 return outputs; 157 } 158 159 @Override 160 public <T> Stream<T> collect(final Class<T> recordType, final Mapper mapper, final int maxRecords) { 161 return collect(recordType, mapper, maxRecords, Runtime.getRuntime().availableProcessors()); 162 } 163 164 /** 165 * Collects data emitted from this mapper. If the split creates more than one 166 * mapper, it will create as much threads as mappers otherwise it will use the 167 * caller thread. 168 * 169 * IMPORTANT: don't forget to consume all the stream to ensure the underlying 170 * { @see org.talend.sdk.component.runtime.input.Input} is closed. 171 * 172 * @param recordType the record type to use to type the returned type. 173 * @param mapper the mapper to go through. 174 * @param maxRecords maximum number of records, allows to stop the source when 175 * infinite. 176 * @param concurrency requested (1 can be used instead if <= 0) concurrency for the reader execution. 177 * @param <T> the returned type of the records of the mapper. 178 * @return all the records emitted by the mapper. 179 */ 180 @Override 181 public <T> Stream<T> collect(final Class<T> recordType, final Mapper mapper, final int maxRecords, 182 final int concurrency) { 183 mapper.start(); 184 185 final State state = STATE.get(); 186 final long assess = mapper.assess(); 187 final int proc = Math.max(1, concurrency); 188 final List<Mapper> mappers = mapper.split(Math.max(assess / proc, 1)); 189 switch (mappers.size()) { 190 case 0: 191 return Stream.empty(); 192 case 1: 193 return StreamDecorator.decorate( 194 asStream(asIterator(mappers.iterator().next().create(), new AtomicInteger(maxRecords))), 195 collect -> { 196 try { 197 collect.run(); 198 } finally { 199 mapper.stop(); 200 } 201 }); 202 default: // N producers-1 consumer pattern 203 final AtomicInteger threadCounter = new AtomicInteger(0); 204 final ExecutorService es = Executors.newFixedThreadPool(mappers.size(), r -> new Thread(r) { 205 206 { 207 setName(BaseComponentsHandler.this.getClass().getSimpleName() + "-pool-" + abs(mapper.hashCode()) 208 + "-" + threadCounter.incrementAndGet()); 209 } 210 }); 211 final AtomicInteger recordCounter = new AtomicInteger(maxRecords); 212 final Semaphore permissions = new Semaphore(0); 213 final Queue<T> records = new ConcurrentLinkedQueue<>(); 214 final CountDownLatch latch = new CountDownLatch(mappers.size()); 215 final List<? extends Future<?>> tasks = mappers 216 .stream() 217 .map(Mapper::create) 218 .map(input -> (Iterator<T>) asIterator(input, recordCounter)) 219 .map(it -> es.submit(() -> { 220 try { 221 while (it.hasNext()) { 222 final T next = it.next(); 223 records.add(next); 224 permissions.release(); 225 } 226 } finally { 227 latch.countDown(); 228 } 229 })) 230 .collect(toList()); 231 es.shutdown(); 232 233 final int timeout = Integer.getInteger("talend.component.junit.timeout", 5); 234 new Thread() { 235 236 { 237 setName(BaseComponentsHandler.class.getSimpleName() + "-monitor_" + abs(mapper.hashCode())); 238 } 239 240 @Override 241 public void run() { 242 try { 243 latch.await(timeout, MINUTES); 244 } catch (final InterruptedException e) { 245 Thread.interrupted(); 246 } finally { 247 permissions.release(); 248 } 249 } 250 }.start(); 251 return StreamDecorator.decorate(asStream(new Iterator<T>() { 252 253 @Override 254 public boolean hasNext() { 255 try { 256 permissions.acquire(); 257 } catch (final InterruptedException e) { 258 Thread.interrupted(); 259 fail(e.getMessage()); 260 } 261 return !records.isEmpty(); 262 } 263 264 @Override 265 public T next() { 266 T poll = records.poll(); 267 if (poll != null) { 268 return mapRecord(state, recordType, poll); 269 } 270 return null; 271 } 272 }), task -> { 273 try { 274 task.run(); 275 } finally { 276 tasks.forEach(f -> { 277 try { 278 f.get(5, SECONDS); 279 } catch (final InterruptedException e) { 280 Thread.interrupted(); 281 } catch (final ExecutionException | TimeoutException e) { 282 // no-op 283 } finally { 284 if (!f.isDone() && !f.isCancelled()) { 285 f.cancel(true); 286 } 287 } 288 }); 289 } 290 }); 291 } 292 } 293 294 private <T> Stream<T> asStream(final Iterator<T> iterator) { 295 return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.IMMUTABLE), false); 296 } 297 298 private <T> Iterator<T> asIterator(final Input input, final AtomicInteger counter) { 299 input.start(); 300 return new Iterator<T>() { 301 302 private boolean closed; 303 304 private Object next; 305 306 @Override 307 public boolean hasNext() { 308 final int remaining = counter.get(); 309 if (remaining <= 0) { 310 return false; 311 } 312 313 final boolean hasNext = (next = input.next()) != null; 314 if (!hasNext && !closed) { 315 closed = true; 316 input.stop(); 317 } 318 if (hasNext) { 319 counter.decrementAndGet(); 320 } 321 return hasNext; 322 } 323 324 @Override 325 public T next() { 326 return (T) next; 327 } 328 }; 329 } 330 331 @Override 332 public <T> List<T> collectAsList(final Class<T> recordType, final Mapper mapper) { 333 return collectAsList(recordType, mapper, 1000); 334 } 335 336 @Override 337 public <T> List<T> collectAsList(final Class<T> recordType, final Mapper mapper, final int maxRecords) { 338 return collect(recordType, mapper, maxRecords).collect(toList()); 339 } 340 341 @Override 342 public Mapper createMapper(final Class<?> componentType, final Object configuration) { 343 return create(Mapper.class, componentType, configuration); 344 } 345 346 @Override 347 public Processor createProcessor(final Class<?> componentType, final Object configuration) { 348 return create(Processor.class, componentType, configuration); 349 } 350 351 private <C, T, A> A create(final Class<A> api, final Class<T> componentType, final C configuration) { 352 final ComponentFamilyMeta.BaseMeta<? extends Lifecycle> meta = findMeta(componentType); 353 return api.cast(meta 354 .getInstantiator() 355 .apply(configuration == null || meta.getParameterMetas().isEmpty() ? emptyMap() 356 : configurationByExample(configuration, meta 357 .getParameterMetas() 358 .stream() 359 .filter(p -> p.getName().equals(p.getPath())) 360 .findFirst() 361 .map(p -> p.getName() + '.') 362 .orElseThrow(() -> new IllegalArgumentException("Didn't find any option and therefore " 363 + "can't convert the configuration instance to a configuration"))))); 364 } 365 366 private <T> ComponentFamilyMeta.BaseMeta<? extends Lifecycle> findMeta(final Class<T> componentType) { 367 return asManager() 368 .find(c -> c.get(ContainerComponentRegistry.class).getComponents().values().stream()) 369 .flatMap(f -> Stream.concat(f.getProcessors().values().stream(), 370 f.getPartitionMappers().values().stream())) 371 .filter(m -> m.getType().getName().equals(componentType.getName())) 372 .findFirst() 373 .orElseThrow(() -> new IllegalArgumentException("No component " + componentType)); 374 } 375 376 @Override 377 public <T> List<T> collect(final Class<T> recordType, final String family, final String component, 378 final int version, final Map<String, String> configuration) { 379 Job 380 .components() 381 .component("in", 382 family + "://" + component + "?__version=" + version 383 + configuration 384 .entrySet() 385 .stream() 386 .map(entry -> entry.getKey() + "=" + entry.getValue()) 387 .collect(joining("&", "&", ""))) 388 .component("collector", "test://collector") 389 .connections() 390 .from("in") 391 .to("collector") 392 .build() 393 .run(); 394 395 return getCollectedData(recordType); 396 } 397 398 @Override 399 public <T> void process(final Iterable<T> inputs, final String family, final String component, final int version, 400 final Map<String, String> configuration) { 401 setInputData(inputs); 402 403 Job 404 .components() 405 .component("emitter", "test://emitter") 406 .component("out", 407 family + "://" + component + "?__version=" + version 408 + configuration 409 .entrySet() 410 .stream() 411 .map(entry -> entry.getKey() + "=" + entry.getValue()) 412 .collect(joining("&", "&", ""))) 413 .connections() 414 .from("emitter") 415 .to("out") 416 .build() 417 .run(); 418 419 } 420 421 @Override 422 public ComponentManager asManager() { 423 return STATE.get().manager; 424 } 425 426 public <T> T findService(final String plugin, final Class<T> serviceClass) { 427 return serviceClass.cast(asManager() 428 .findPlugin(plugin) 429 .orElseThrow(() -> new IllegalArgumentException("cant find plugin '" + plugin + "'")) 430 .get(ComponentManager.AllServices.class) 431 .getServices() 432 .get(serviceClass)); 433 } 434 435 public <T> T findService(final Class<T> serviceClass) { 436 return findService( 437 Optional.of(getTestPlugins()).filter(c -> !c.isEmpty()).map(c -> c.iterator().next()).orElseThrow( 438 () -> new IllegalStateException("No component plugin found")), 439 serviceClass); 440 } 441 442 public Set<String> getTestPlugins() { 443 return EmbeddedComponentManager.class.cast(asManager()).testPlugins; 444 } 445 446 @Override 447 public <T> void setInputData(final Iterable<T> data) { 448 initState.get().emitter = data.iterator(); 449 } 450 451 @Override 452 public <T> List<T> getCollectedData(final Class<T> recordType) { 453 final State state = STATE.get(); 454 return state.collector 455 .stream() 456 .filter(r -> recordType.isInstance(r) || JsonObject.class.isInstance(r)) 457 .map(r -> mapRecord(state, recordType, r)) 458 .collect(toList()); 459 } 460 461 private <T> T mapRecord(final State state, final Class<T> recordType, final Object r) { 462 if (recordType.isInstance(r)) { 463 return recordType.cast(r); 464 } 465 if (JsonObject.class.isInstance(r)) { 466 final Jsonb jsonb = state.jsonb(); 467 return jsonb.fromJson(jsonb.toJson(r), recordType); 468 } 469 throw new IllegalArgumentException("Unsupported record: " + r); 470 } 471 472 static class PreState { 473 474 Iterator<?> emitter; 475 } 476 477 @RequiredArgsConstructor 478 static class State { 479 480 final ComponentManager manager; 481 482 final Collection<Object> collector; 483 484 final Iterator<?> emitter; 485 486 volatile Jsonb jsonb; 487 488 synchronized Jsonb jsonb() { 489 if (jsonb == null) { 490 jsonb = manager 491 .getJsonbProvider() 492 .create() 493 .withProvider(new PreComputedJsonpProvider("test", manager.getJsonpProvider(), 494 manager.getJsonpParserFactory(), manager.getJsonpWriterFactory(), 495 manager.getJsonpBuilderFactory(), manager.getJsonpGeneratorFactory(), 496 manager.getJsonpReaderFactory())) // reuses 497 // the 498 // same 499 // memory 500 // buffering 501 .withConfig(new JsonbConfig().setProperty("johnzon.cdi.activated", false)) 502 .build(); 503 } 504 return jsonb; 505 } 506 } 507 508 public static class EmbeddedComponentManager extends ComponentManager { 509 510 private final ComponentManager oldInstance; 511 512 private final Set<String> testPlugins; 513 514 private EmbeddedComponentManager(final String componentPackage) { 515 super(findM2(), "TALEND-INF/dependencies.txt", "org.talend.sdk.component:type=component,value=%s"); 516 testPlugins = addJarContaining(Thread.currentThread().getContextClassLoader(), 517 componentPackage.replace('.', '/')); 518 container 519 .builder("component-runtime-junit.jar", jarLocation(SimpleCollector.class).getAbsolutePath()) 520 .create(); 521 oldInstance = CONTEXTUAL_INSTANCE.get(); 522 CONTEXTUAL_INSTANCE.set(this); 523 } 524 525 @Override 526 public void close() { 527 try { 528 super.close(); 529 } finally { 530 CONTEXTUAL_INSTANCE.compareAndSet(this, oldInstance); 531 } 532 } 533 534 @Override 535 protected boolean isContainerClass(final Filter filter, final String name) { 536 // embedded mode (no plugin structure) so just run with all classes in parent classloader 537 return true; 538 } 539 } 540 541 public static class Outputs { 542 543 private final Map<String, List<?>> data = new HashMap<>(); 544 545 public int size() { 546 return data.size(); 547 } 548 549 public Set<String> keys() { 550 return data.keySet(); 551 } 552 553 public <T> List<T> get(final Class<T> type, final String name) { 554 return (List<T>) data.get(name); 555 } 556 } 557}