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 &lt;= 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}