/* 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.mms.server.connection.client;
import static java.util.Objects.requireNonNull;
import java.util.Collections;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.stream.Stream;
import net.maritimecloud.core.id.MaritimeId;
import net.maritimecloud.internal.mms.messages.Connected;
import net.maritimecloud.internal.mms.messages.Hello;
import net.maritimecloud.internal.mms.messages.spi.MmsMessage;
import net.maritimecloud.message.Message;
import net.maritimecloud.mms.server.MmsServer;
import net.maritimecloud.mms.server.connection.client.Client.State;
import net.maritimecloud.mms.server.connection.transport.ServerTransport;
import net.maritimecloud.net.mms.MmsConnectionClosingCode;
/**
*
* @author Kasper Nielsen
*/
public class ClientManager implements Iterable<Client> {
/** A list of all currently connected clients. Clients will be removed after xx time */
final ConcurrentHashMap<String, Client> clients = new ConcurrentHashMap<>();
/** The MMS Server */
final MmsServer mmsServer;
/**
* Creates a new ClientManager
*
* @param mmsServer
* the mms server
*/
public ClientManager(MmsServer mmsServer) {
this.mmsServer = requireNonNull(mmsServer);
}
public void forEachTarget(Consumer<Client> consumer) {
clients.forEachValue(10, requireNonNull(consumer));
}
/**
* Returns any client with the specified id
*
* @param id
* the id of the client
* @return any client with the specified id
*/
public Client get(MaritimeId id) {
return clients.get(id.toString());
}
/** {@inheritDoc} */
@Override
public Iterator<Client> iterator() {
return Collections.unmodifiableCollection(clients.values()).iterator();
}
/**
* A new client if successfully connected.
*
* @param hello
* the clients hello message
* @param transport
* the transport used for sending and receiving messages
* @return a new client
*/
Client onHello(Hello hello, ServerTransport transport) {
MaritimeId mid = MaritimeId.create(hello.getClientId());
String id = mid.toString();
// this method is written is typical non-blocking style, with an infinite loop.
// Where each invocation is live free lock bla bla.
for (;;) {
Client c = clients.get(id);
if (c == null) { // no existing client
c = new Client(this, transport, id);
c.latestPositionAndTime = hello.getPositionTime();
// we need to lock it before we insert it into the hash map so other threads won't attempt to send any
// messages before we have sent a Connected message.
// We can probably remove it at some point, just need to figure out how?
c.lock.writeLock().lock();
try {
// Try and see if we can insert as current client. Otherwise let for(;;) loop retry
if (clients.putIfAbsent(id, c) == null) {
return c.connectWithWriteLock(transport);
}
} finally {
c.lock.writeLock().unlock();
}
} else {
// A client is already connecting, connected or is stale. Since we will most likely change state.
// we start by locking the client, preparing an update.
c.lock.writeLock().lock();
try {
c.latestPositionAndTime = hello.getPositionTime(); // lets start by updating the latest timestamp
ClientInternalState state = c.state;
if (state.state == State.CONNECTING) {
// The current implementation does not allow this state.
// Because right now the above code will always hold the write lock
// while the state is InternalStateConnecting.
// This might change with a distributed implementation.
throw new IllegalStateException();
} else if (state.state == State.TERMINATED) {
clients.remove(id, c);// remove it, and let for(;;) handle the new connection
} else {
Session existingSession = state.session;
if (state.state == State.CONNECTED) {
state.transport.close(MmsConnectionClosingCode.DUPLICATE_CONNECT);
}
if (!existingSession.getSessionId().equals(hello.getSessionId())) {
existingSession.disconnectedWithWriteLock(true);
// Create a new session
return c.connectWithWriteLock(transport);
} else {
c.state = new ClientInternalState(State.CONNECTED, transport, existingSession);
MmsMessage mm = new MmsMessage(new Connected().setSessionId(existingSession.getSessionId())
.setLastReceivedMessageId(existingSession.latestMessageIdReceivedByRemote));
transport.sendMessage(mm); // Send connected message
existingSession.onConnectWithWriteLock(transport, hello.getLastReceivedMessageId());
return c;
}
}
} finally {
c.lock.writeLock().unlock();
}
}
}
}
/**
* Returns a parallel stream of all connected clients.
*
* @return a parallel stream of all connected clients
*/
public Stream<Client> parallelStream() {
return clients.values().parallelStream();
}
SessionMessageFuture sendMessage(String destinationId, Message m) {
Client ic = clients.get(destinationId);
if (ic == null) {
return SessionMessageFuture.notConnected(m);
}
return ic.sendMessage(null, m);
}
/**
* Returns a stream of all connected clients.
*
* @return a stream of all connected clients
*/
public Stream<Client> stream() {
return clients.values().stream();
}
}