/* 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 net.maritimecloud.internal.mms.client.broadcast;
import static java.util.Objects.requireNonNull;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
import net.maritimecloud.core.id.MaritimeId;
import net.maritimecloud.internal.message.MessageHelper;
import net.maritimecloud.internal.message.text.json.JsonMessageReader;
import net.maritimecloud.internal.mms.client.ClientInfo;
import net.maritimecloud.internal.mms.client.MmsThreadManager;
import net.maritimecloud.internal.mms.client.connection.ClientConnection;
import net.maritimecloud.internal.net.messages.Broadcast;
import net.maritimecloud.internal.net.messages.BroadcastAck;
import net.maritimecloud.internal.net.messages.MessageHasher;
import net.maritimecloud.internal.net.util.DefaultAcknowledgement;
import net.maritimecloud.internal.net.util.DefaultMessageHeader;
import net.maritimecloud.internal.util.Coverage;
import net.maritimecloud.internal.util.MessageStore;
import net.maritimecloud.internal.util.concurrent.CompletableFuture;
import net.maritimecloud.internal.util.logging.Logger;
import net.maritimecloud.message.MessageSerializer;
import net.maritimecloud.net.BroadcastConsumer;
import net.maritimecloud.net.BroadcastMessage;
import net.maritimecloud.net.BroadcastSubscription;
import net.maritimecloud.net.DispatchedMessage;
import net.maritimecloud.net.MessageHeader;
import net.maritimecloud.net.mms.MmsBroadcastOptions;
import net.maritimecloud.net.mms.MmsClient;
import net.maritimecloud.net.mms.MmsClientClosedException;
import net.maritimecloud.util.Binary;
import net.maritimecloud.util.geometry.Area;
import net.maritimecloud.util.geometry.Circle;
import net.maritimecloud.util.geometry.PositionTime;
import org.cakeframework.container.concurrent.ScheduleWithFixedDelay;
import org.cakeframework.container.concurrent.ThreadManager;
import org.cakeframework.container.lifecycle.RunOnStop;
/**
* Manages sending and receiving of broadcasts.
*
* @author Kasper Nielsen
*/
public class ClientBroadcastManager {
/** The logger. */
static final Logger LOG = Logger.get(ClientBroadcastManager.class);
/** The network */
private final ClientConnection connection;
/** Broadcast that have been sent. Will be cleared regularly */
private final MessageStore<DispatchedBroadcast> dispatchedBroadcasts = new MessageStore<>();
volatile boolean isShutdown;
final ReentrantReadWriteLock sendLock = new ReentrantReadWriteLock();
final ReentrantReadWriteLock subscribeLock = new ReentrantReadWriteLock();
/** A map of local broadcast subscribers. */
final ConcurrentHashMap<String, SubscriptionSet> subscribers = new ConcurrentHashMap<>();
/** Thread manager takes care of asynchronous processing. */
private final MmsThreadManager threadManager;
private final ClientInfo info;
final ScheduledExecutorService ses;
public ClientBroadcastManager(ClientInfo info, MmsThreadManager threadManager, ClientConnection connection,
ThreadManager tmm) {
this.connection = requireNonNull(connection);
this.threadManager = requireNonNull(threadManager);
this.info = requireNonNull(info);
ses = tmm.getScheduledExecutor("");
connection.subscribe(BroadcastAck.class, (a, e) -> onBroadcastAck(e));
connection.subscribe(Broadcast.class, (a, e) -> onBroadcastMessage(e));
}
/**
* Sets up listeners for incoming broadcast messages.
*
* @param messageType
* the type of message to receive
* @param listener
* the callback listener
* @return a subscription
* @see MmsClient#broadcastSubscribe(Class, BroadcastConsumer)
*/
public <T extends BroadcastMessage> BroadcastSubscription broadcastSubscribe(Class<T> messageType,
BroadcastConsumer<T> listener, Area area) {
return broadcastSubscribe(BroadcastDeserializer.CLASSPATH_DESERIALIZER, MessageHelper.getName(messageType),
listener, area);
}
public <T extends BroadcastMessage> BroadcastSubscription broadcastSubscribe(BroadcastDeserializer bd, String type,
BroadcastConsumer<T> listener, Area area) {
subscribeLock.readLock().lock();
try {
if (isShutdown) {
throw new MmsClientClosedException("The mms client has been shutdown");
}
SubscriptionSet set = subscribers.computeIfAbsent(type, e -> new SubscriptionSet(this, type));
return set.newSubscription(bd, listener, area == null ? Coverage.ALL
: new Coverage.StaticAreaCoverage(area));
} finally {
subscribeLock.readLock().unlock();
}
}
DispatchedMessage brodcast(BroadcastMessage message, Area area, int radius,
Consumer<? super MessageHeader> ackConsumer) {
String broadcastType = MessageHelper.getName(message);
Broadcast broadcast = new Broadcast();
broadcast.setBroadcastType(broadcastType);
broadcast.setSenderId(info.getClientId().toString());
broadcast.setSenderTimestamp(info.currentTime());
Optional<PositionTime> r = info.getCurrentPosition();
if (r.isPresent()) {
broadcast.setSenderPosition(r.get());
}
Area broadcastArea = area;
// Fix this, must specify an area if stationary client
if (broadcastArea == null && r.isPresent()) {
broadcastArea = Circle.create(r.get(), radius);
}
broadcast.setArea(broadcastArea);
broadcast.setAckBroadcast(ackConsumer != null);
broadcast.setPayload(Binary.copyFromUtf8(MessageSerializer.writeToJSON(message,
MessageHelper.getSerializer(message))));
broadcast.setMessageId(MessageHasher.calculateSHA256(broadcast));
DefaultAcknowledgement ack = new DefaultAcknowledgement();
DispatchedBroadcast db = new DispatchedBroadcast(broadcast, ack, ackConsumer);
sendLock.readLock().lock();
try {
checkNotShutdown();
dispatchedBroadcasts.addMessage(db);
CompletableFuture<Void> om = connection.sendMessage(broadcast);
om.handle((ac, cause) -> {
if (cause == null) {
ack.complete();
} else {
ack.completeExceptionally(cause);
}
return null;
});
} finally {
sendLock.readLock().unlock();
}
return db;
}
/** A simple scheduled method that will clear old broadcasts. */
@ScheduleWithFixedDelay(value = 1, unit = TimeUnit.MINUTES)
public void clearOldMessages() {
dispatchedBroadcasts.pruneMessagesOldThan(System.nanoTime() - TimeUnit.NANOSECONDS.convert(1, TimeUnit.HOURS));
}
private void onBroadcastAck(BroadcastAck ack) {
DispatchedBroadcast f = dispatchedBroadcasts.find(ack.getAckForMessageId());
if (f != null) { // Just ignore the ack if we do not have a matching dispatched broadcast
f.acked(ack);
}
}
/**
* Invoked whenever a broadcast message is received from a remote actor.
*
* @param broadcast
* the broadcast that was received
*/
private void onBroadcastMessage(Broadcast broadcast) {
SubscriptionSet set = subscribers.get(broadcast.getBroadcastType());
if (set != null && !set.listeners.isEmpty()) {
MessageHeader header = new DefaultMessageHeader(MaritimeId.create(broadcast.getSenderId()),
broadcast.getMessageId(), broadcast.getSenderTimestamp(), broadcast.getSenderPosition());
// Deliver to each listener
for (SubscriptionSet.DefaultSubscription s : set.listeners) {
BroadcastMessage message;
JsonMessageReader r = new JsonMessageReader(broadcast.getPayload().toStringUtf8());
try {
message = s.bd.convert(broadcast.getBroadcastType(), r);
} catch (Exception e) {
e.printStackTrace();
return;
}
threadManager.broadcastReceived(() -> s.deliver(header, message));
}
}
}
private void checkNotShutdown() {
if (isShutdown) {
throw new MmsClientClosedException("The mms client has been shutdown");
}
}
@RunOnStop
public void stop() {
subscribeLock.writeLock().lock();
try {
sendLock.writeLock().lock();
try {
isShutdown = true;
// Cancel all acknowledgements
MmsClientClosedException e = new MmsClientClosedException("Client has been shutdown");
dispatchedBroadcasts.forEach(b -> b.shutdownClient(e));
dispatchedBroadcasts.clear();
subscribers.clear();
} finally {
sendLock.writeLock().unlock();
}
} finally {
subscribeLock.writeLock().unlock();
}
}
public void newSession() {
// send subscriptions.
// Spoergsmmalet er om vi skal serializer alle beskeder, dvs total order af beskeder?
// Taenker den skal gemmes et sted paa connectionen istedet for
// forEachEldestFirst(resend those that are not acked)
// resend unacked broadcasts
}
public DispatchedMessage broadcast(BroadcastMessage message, MmsBroadcastOptions options) {
MmsBroadcastOptions op = options.immutable();
return brodcast(message, op.getArea(), op.getRadius(), op.getRemoteReceive());
}
}