/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.component.reactive.streams.engine;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.component.reactive.streams.ReactiveStreamsBackpressureStrategy;
import org.apache.camel.component.reactive.streams.ReactiveStreamsComponent;
import org.apache.camel.component.reactive.streams.ReactiveStreamsEndpoint;
import org.apache.camel.component.reactive.streams.ReactiveStreamsHelper;
import org.apache.camel.component.reactive.streams.ReactiveStreamsProducer;
import org.apache.camel.component.reactive.streams.api.DispatchCallback;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The Camel publisher. It forwards Camel exchanges to external reactive-streams subscribers.
*/
public class CamelPublisher implements Publisher<Exchange>, AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(CamelPublisher.class);
private ExecutorService workerPool;
private String name;
private ReactiveStreamsBackpressureStrategy backpressureStrategy;
private List<CamelSubscription> subscriptions = new CopyOnWriteArrayList<>();
private ReactiveStreamsProducer producer;
public CamelPublisher(ExecutorService workerPool, CamelContext context, String name) {
this.workerPool = workerPool;
this.backpressureStrategy = ((ReactiveStreamsComponent) context.getComponent("reactive-streams")).getBackpressureStrategy();
this.name = name;
}
@Override
public void subscribe(Subscriber<? super Exchange> subscriber) {
Objects.requireNonNull(subscriber, "subscriber must not be null");
CamelSubscription sub = new CamelSubscription(UUID.randomUUID().toString(), workerPool, this, name, this.backpressureStrategy, subscriber);
this.subscriptions.add(sub);
subscriber.onSubscribe(sub);
}
public void unsubscribe(CamelSubscription subscription) {
subscriptions.remove(subscription);
}
public void publish(Exchange data) {
// freeze the subscriptions
List<CamelSubscription> subs = new LinkedList<>(subscriptions);
DispatchCallback<Exchange> originalCallback = ReactiveStreamsHelper.getCallback(data);
DispatchCallback<Exchange> callback = originalCallback;
if (originalCallback != null && subs.size() > 0) {
// When multiple subscribers have an active subscription,
// we acknowledge the exchange once it has been delivered to every
// subscriber (or their subscription is cancelled)
AtomicInteger counter = new AtomicInteger(subs.size());
// Use just the first exception in the callback when multiple exceptions are thrown
AtomicReference<Throwable> thrown = new AtomicReference<>(null);
callback = ReactiveStreamsHelper.attachCallback(data, (exchange, error) -> {
thrown.compareAndSet(null, error);
if (counter.decrementAndGet() == 0) {
originalCallback.processed(exchange, thrown.get());
}
});
}
if (subs.size() > 0) {
if (LOG.isDebugEnabled()) {
LOG.debug("Exchange published to {} subscriptions for the stream {}: {}", subs.size(), name, data);
}
// at least one subscriber
for (CamelSubscription sub : subs) {
sub.publish(data);
}
} else if (callback != null) {
callback.processed(data, new IllegalStateException("The stream has no active subscriptions"));
}
}
public void attachProducer(ReactiveStreamsProducer producer) {
Objects.requireNonNull(producer, "producer cannot be null, use the detach method");
if (this.producer != null) {
throw new IllegalStateException("A producer is already attached to the stream '" + name + "'");
}
this.producer = producer;
// Apply endpoint options if available
ReactiveStreamsEndpoint endpoint = producer.getEndpoint();
if (endpoint.getBackpressureStrategy() != null) {
this.backpressureStrategy = endpoint.getBackpressureStrategy();
for (CamelSubscription sub : this.subscriptions) {
sub.setBackpressureStrategy(endpoint.getBackpressureStrategy());
}
}
}
public void detachProducer() {
this.producer = null;
}
@Override
public void close() throws Exception {
for (CamelSubscription sub : subscriptions) {
sub.signalCompletion();
}
subscriptions.clear();
}
public List<CamelSubscription> getSubscriptions() {
return subscriptions;
}
}