/* Copyright (c) 2011 Danish Maritime Authority.
*
* Licensed 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 dk.dma.ais.packet;
import static java.util.Objects.requireNonNull;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The default implements of {@link AisPacketStream}.
*
* @author Kasper Nielsen
*/
class AisPacketStreamImpl extends AisPacketStream {
/** The logger */
static final Logger LOG = LoggerFactory.getLogger(AisPacketStreamImpl.class);
/** A lock we use to make sure packets are delivered in order. */
private final Object deliveryLock = new Object();
final ConcurrentHashMap<SubscriptionImpl, SubscriptionImpl> subscriptions;
final Predicate<? super AisPacket> predicate;
final AisPacketStreamImpl root;
AisPacketStreamImpl() {
predicate = null;
subscriptions = new ConcurrentHashMap<>();
root = null;
}
AisPacketStreamImpl(AisPacketStreamImpl parent, Predicate<? super AisPacket> predicate) {
this.root = requireNonNull(parent);
this.predicate = requireNonNull(predicate);
this.subscriptions = parent.subscriptions;
}
public void add(AisPacket p) {
requireNonNull(p);
if (root != null) {
throw new UnsupportedOperationException("Can only add elements to the root stream");
}
synchronized (deliveryLock) {
for (SubscriptionImpl s : subscriptions.keySet()) {
s.packets.add(p);
s.deliver();
}
}
// This approach does not work, as we cannot guarantee in-order delivery
// map.forEachKeyInParallel(new Action<SubscriptionImpl>() {
// @Override
// public void apply(SubscriptionImpl s) {
// s.deliver();
// }
// });
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public AisPacketStream filter(Predicate<? super AisPacket> predicate) {
requireNonNull(predicate);
return new AisPacketStreamImpl(root == null ? this : root,
(Predicate<? super AisPacket>) (this.predicate == null ? predicate
: this.predicate.and((Predicate) predicate)));
}
void handlePacket(AisPacket p, SubscriptionImpl s) {
if (s.predicate == null || s.predicate.test(p)) {
s.consumer.accept(p);
}
}
/** {@inheritDoc} */
@Override
public Subscription subscribe(Consumer<AisPacket> c) {
SubscriptionImpl s = new SubscriptionImpl(predicate, c);
subscriptions.put(s, s);
return s;
}
class SubscriptionImpl implements Subscription {
final CountDownLatch cancelled = new CountDownLatch(1);
final Consumer<? super AisPacket> consumer;
final AtomicLong count = new AtomicLong();
final ReentrantLock lock = new ReentrantLock();
final ConcurrentLinkedQueue<AisPacket> packets = new ConcurrentLinkedQueue<>();
final Predicate<? super AisPacket> predicate;
SubscriptionImpl(Predicate<? super AisPacket> predicate, Consumer<? super AisPacket> consumer) {
this.predicate = predicate;
this.consumer = requireNonNull(consumer);
}
/** {@inheritDoc} */
@Override
public void awaitCancelled() throws InterruptedException {
cancelled.await();
}
/** {@inheritDoc} */
@Override
public boolean awaitCancelled(long timeout, TimeUnit unit) throws InterruptedException {
return cancelled.await(timeout, unit);
}
/** {@inheritDoc} */
@Override
public void cancel() {
cancel(null);
}
/** {@inheritDoc} */
synchronized void cancel(Throwable e) {
lock.lock();
try {
if (e != null) {
LOG.error("Cancelling subscription, because of error", e);
}
if (cancelled.getCount() > 0) {
subscriptions.remove(this);
cancelled.countDown();
if (consumer instanceof AisPacketStream.StreamConsumer) {
try {
((AisPacketStream.StreamConsumer<?>) consumer).end(e);
} catch (RuntimeException ex) {
if (e == null) {
LOG.error("Failed to write footer", ex);
}
}
}
}
} finally {
lock.unlock();
}
}
/** This method delivers the actual event. */
void deliver() {
while (lock.tryLock()) {
try {
if (packets.isEmpty()) {
return;
}
for (AisPacket p = packets.poll(); p != null; p = packets.poll()) {
try {
if (predicate == null || predicate.test(p)) {
if (count.getAndIncrement() == 0 && consumer instanceof AisPacketStream.StreamConsumer) {
((AisPacketStream.StreamConsumer<?>) consumer).begin();
}
consumer.accept(p);
}
} catch (RuntimeException e) {
try {
cancel(e == AisPacketStream.CANCEL ? null : e);
} catch (RuntimeException ignore) {}
return;
}
}
} finally {
lock.unlock();
}
}
}
/** {@inheritDoc} */
@Override
public boolean isCancelled() {
return !subscriptions.containsKey(this);
}
}
}