/**
* Copyright 2014-2017 Linagora, Université Joseph Fourier, Floralis
*
* The present code is developed in the scope of the joint LINAGORA -
* Université Joseph Fourier - Floralis research program and is designated
* as a "Result" pursuant to the terms and conditions of the LINAGORA
* - Université Joseph Fourier - Floralis research program. Each copyright
* holder of Results enumerated here above fully & independently holds complete
* ownership of the complete Intellectual Property rights applicable to the whole
* of said Results, and may freely exploit it in any manner which does not infringe
* the moral rights of the other copyright holders.
*
* 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.roboconf.messaging.http.internal;
import static net.roboconf.messaging.http.HttpConstants.DEFAULT_IP;
import static net.roboconf.messaging.http.HttpConstants.HTTP_SERVER_IP;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import net.roboconf.core.utils.Utils;
import net.roboconf.messaging.api.MessagingConstants;
import net.roboconf.messaging.api.extensions.AbstractRoutingClient.RoutingContext;
import net.roboconf.messaging.api.extensions.IMessagingClient;
import net.roboconf.messaging.api.extensions.MessagingContext.RecipientKind;
import net.roboconf.messaging.api.factory.IMessagingClientFactory;
import net.roboconf.messaging.api.reconfigurables.ReconfigurableClient;
import net.roboconf.messaging.http.HttpConstants;
import net.roboconf.messaging.http.internal.clients.HttpAgentClient;
import net.roboconf.messaging.http.internal.clients.HttpDmClient;
import net.roboconf.messaging.http.internal.sockets.DmWebSocketServlet;
import org.eclipse.jetty.websocket.api.Session;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.http.HttpService;
/**
* Messaging client factory for HTTP.
* <p>
* In this factory, there is a singleton instance of the client for
* the DM. This is because of the web sockets. We do not want to lost
* and/or confuse message routing. So, there is no reconfiguration for
* this client and we keep a single instance.
* </p>
*
* @author Pierre-Yves Gibello - Linagora
* @author Vincent Zurczak - Linagora
*/
public class HttpClientFactory implements IMessagingClientFactory {
/**
* @author Vincent Zurczak - Linagora
*/
public static class HttpRoutingContext extends RoutingContext {
public final Map<String,Session> ctxToSession = new ConcurrentHashMap<> ();
}
// The created CLIENTS.
// References to the CLIENTS are *weak*, so we never prevent their garbage collection.
final Set<HttpAgentClient> agentClients = Collections.newSetFromMap( new WeakHashMap<HttpAgentClient,Boolean> ());
private final Logger logger = Logger.getLogger( getClass().getName());
private final HttpRoutingContext routingContext = new HttpRoutingContext();
private final HttpDmClient dmClient = new HttpDmClient( this.routingContext );
// Injected by iPojo
BundleContext bundleContext;
HttpService httpService;
String httpServerIp;
int httpPort;
/**
* Constructor.
*/
public HttpClientFactory() {
// nothing
}
/**
* Constructor with the bundle context.
* <p>
* This constructor is automatically invoked by iPojo.
* </p>
*
* @param context
*/
public HttpClientFactory( BundleContext bundleContext ) {
this.bundleContext = bundleContext;
}
// Getters and Setters
public synchronized void setHttpServerIp( final String serverIp ) {
this.httpServerIp = serverIp;
this.dmClient.setHttpServerIp( serverIp );
this.logger.finer( "Server IP set to " + this.httpServerIp );
}
public synchronized void setHttpPort( final int port ) {
this.httpPort = port;
this.dmClient.setHttpPort( port );
this.logger.finer( "Server port set to " + this.httpPort );
}
public HttpDmClient getDmClient() {
return this.dmClient;
}
// iPojo methods
/**
* The method to use when all the dependencies are resolved.
* <p>
* It means iPojo guarantees that both the manager and the HTTP
* service are not null.
* </p>
*
* @throws Exception
*/
public void start() throws Exception {
// Is the DM part of the distribution?
boolean found = false;
for( Bundle b : this.bundleContext.getBundles()) {
if( "net.roboconf.dm".equals( b.getSymbolicName())) {
found = true;
break;
}
}
// If we are on an agent, we have nothing to do.
// Otherwise, we must register a servlet.
if( found ) {
this.logger.fine( "iPojo registers a servlet for HTTP messaging." );
Hashtable<String,String> initParams = new Hashtable<String,String> ();
initParams.put( "servlet-name", "Roboconf DM (HTTP messaging)" );
DmWebSocketServlet messagingServlet = new DmWebSocketServlet( this );
this.httpService.registerServlet( HttpConstants.DM_SOCKET_PATH, messagingServlet, initParams, null );
} else {
this.logger.warning( "Roboconf's DM bundle was not found. No servlet will be registered." );
}
}
/**
* Stops all the agent clients.
* <p>
* Invoked by iPojo.
* </p>
*/
public void stop() {
this.logger.fine( "iPojo unregisters a servlet for HTTP messaging." );
resetClients( true );
}
/**
* Stops all the clients (agents and DM).
* <p>
* Mostly for tests.
* </p>
*/
void stopAll() {
try {
this.dmClient.closeConnection();
stop();
} catch( Throwable t ) {
this.logger.warning( "An error occurred while closing the connection of the DM client." );
Utils.logException( this.logger, new RuntimeException( t ));
}
}
@Override
public IMessagingClient createClient( ReconfigurableClient<?> parent ) {
this.logger.fine( "Creating a new HTTP client with owner = " + parent.getOwnerKind());
IMessagingClient client;
if( parent.getOwnerKind() == RecipientKind.DM ) {
client = this.dmClient;
} else {
synchronized( this ) {
client = new HttpAgentClient( parent, this.httpServerIp, this.httpPort );
}
this.agentClients.add((HttpAgentClient) client);
}
return client;
}
@Override
public String getType() {
return HttpConstants.FACTORY_HTTP;
}
@Override
public boolean setConfiguration( final Map<String, String> configuration ) {
boolean valid = HttpConstants.FACTORY_HTTP.equals( configuration.get( MessagingConstants.MESSAGING_TYPE_PROPERTY ));
if( valid ) {
boolean hasChanged = false;
// Get the new values
String ip = Utils.getValue( configuration, HTTP_SERVER_IP, DEFAULT_IP );
String portAS = configuration.get( HttpConstants.HTTP_SERVER_PORT );
int port = portAS == null ? HttpConstants.DEFAULT_PORT : Integer.parseInt( portAS );
// Avoid unnecessary (and potentially problematic) reconfiguration if nothing has changed.
// First we detect for changes, and set the parameters accordingly.
synchronized( this ) {
if( ! Objects.equals( this.httpServerIp, ip )) {
this.httpServerIp = ip;
hasChanged = true;
}
if( this.httpPort != port ) {
this.httpPort = port;
hasChanged = true;
}
}
// Then, if changes has occurred, we reconfigure the factory. This will invalidate every created client.
// Otherwise, if nothing has changed, we do nothing. Thus we avoid invalidating clients uselessly, and
// prevent any message loss.
if( hasChanged )
reconfigure();
}
return valid;
}
public void reconfigure() {
this.logger.fine( "HTTP clients are about to be reconfigured." );
resetClients( false );
}
/**
* Closes messaging clients or requests a replacement to the reconfigurable client.
* @param shutdown true to close, false to request...
*/
private void resetClients( boolean shutdown ) {
// Only agent clients need to be reconfigured.
// Make fresh snapshots of the CLIENTS, as we don't want to reconfigure them while holding the lock.
final List<HttpAgentClient> clients;
synchronized( this ) {
// Get the snapshot.
clients = new ArrayList<>( this.agentClients );
// Remove the clients, new ones will be created if necessary.
this.agentClients.clear();
}
// Now reconfigure all the CLIENTS.
for( HttpAgentClient client : clients ) {
try {
final ReconfigurableClient<?> reconfigurable = client.getReconfigurableClient();
if (shutdown)
reconfigurable.closeConnection();
else
reconfigurable.switchMessagingType( HttpConstants.FACTORY_HTTP );
} catch( Throwable t ) {
// Warn but continue to reconfigure the next CLIENTS!
this.logger.warning( "A client has thrown an exception on reconfiguration: " + client );
Utils.logException( this.logger, new RuntimeException( t ));
}
}
}
}