/** * 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.api.extensions; import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; import net.roboconf.core.model.beans.Application; import net.roboconf.messaging.api.MessagingConstants; import net.roboconf.messaging.api.extensions.MessagingContext.RecipientKind; import net.roboconf.messaging.api.messages.Message; /** * A class to dispatch messages directly into message queues. * <p> * This class can be used to replace a messaging server. * It will directly route messages to the right "recipients". * </p> * * @param <T> a handler class where messages will be directed to * @author Vincent Zurczak - Linagora */ public abstract class AbstractRoutingClient<T> implements IMessagingClient { /** * A bean to wrap routing contexts. * <p> * At the beginning, routing contexts were stored as static final maps. * Sub-classes were thus all sharing the same routing information. When used * in stand-alone mode, it was fine. However, Roboconf allows to switch messaging * type and such an organization would have had side effects. * </p> * <p> * So, we replaced the static maps by a class. * Each messaging factory should extend this class and pass it as an * arguments to the agents it creates. This way, messaging clients that * extends this class are isolated from other implementations. * </p> * * @author Vincent Zurczak - Linagora */ public static abstract class RoutingContext { public final Map<String,Set<MessagingContext>> subscriptions = new ConcurrentHashMap<> (); } protected final RoutingContext routingContext; protected final AtomicBoolean connected = new AtomicBoolean( false ); protected final Logger logger = Logger.getLogger( getClass().getName()); protected String ownerId, applicationName, scopedInstancePath, domain; protected boolean connectionIsRequired = true; /** * Constructor. * @param routingContext * @param ownerKind */ public AbstractRoutingClient( RoutingContext routingContext, RecipientKind ownerKind ) { this.routingContext = routingContext; setOwnerProperties( ownerKind, null, null, null ); } @Override public void closeConnection() throws IOException { this.logger.fine( getOwnerId() + " is closing its connection." ); this.connected.set( false ); } @Override public void openConnection() throws IOException { this.logger.fine( getOwnerId() + " is opening a connection." ); this.connected.set( true ); } @Override public Map<String,String> getConfiguration() { return Collections.singletonMap(MessagingConstants.MESSAGING_TYPE_PROPERTY, getMessagingType()); } @Override public void deleteMessagingServerArtifacts( Application application ) throws IOException { this.logger.fine( getOwnerId() + " is deleting server artifacts for " + application ); getStaticContextToObject().remove( this.ownerId ); this.routingContext.subscriptions.remove( this.ownerId ); } @Override public boolean isConnected() { return this.connected.get(); } @Override public void subscribe( MessagingContext ctx ) throws IOException { this.logger.fine( getOwnerId() + " is subscribing to " + buildOwnerId( ctx )); subscribe( this.ownerId, ctx ); } @Override public void unsubscribe( MessagingContext ctx ) throws IOException { this.logger.fine( getOwnerId() + " is unsubscribing to " + buildOwnerId( ctx )); unsubscribe( this.ownerId, ctx ); } @Override public void publish( MessagingContext ctx, Message msg ) throws IOException { this.logger.fine( getOwnerId() + " is publishing message (" + msg + ") to " + buildOwnerId( ctx )); if( ! canProceed()) { this.logger.fine( getOwnerId() + " is dropping message (" + msg + ") for " + buildOwnerId( ctx )); return; } for( Map.Entry<String,Set<MessagingContext>> entry : this.routingContext.subscriptions.entrySet()) { if( ! entry.getValue().contains( ctx )) continue; T obj = getStaticContextToObject().get( entry.getKey()); if( obj != null ) process( obj, msg ); } } @Override public void setOwnerProperties( RecipientKind ownerKind, String domain, String applicationName, String scopedInstancePath ) { // Store the fields (the owner kind is not supposed to change) this.applicationName = applicationName; this.scopedInstancePath = scopedInstancePath; this.domain = domain; // Update the client's owner ID String newOwnerId = buildOwnerId( ownerKind, applicationName, scopedInstancePath ); this.logger.fine( "New owner ID in " + getMessagingType() + " client: " + newOwnerId ); if( this.ownerId == null) { this.ownerId = newOwnerId; } else if( ! newOwnerId.equals( this.ownerId )) { // Switch the owner ID first. // Other method calls will thus use the new version. String oldOwnerId = this.ownerId; this.ownerId = newOwnerId; // Remove old values and associate them with the new key. // FIXME: I am wondering whether we should not have synchronized accesses to the static maps. // This could be a problem with dynamic reconfiguration of agents. T obj = getStaticContextToObject().remove( oldOwnerId ); if( obj != null ) getStaticContextToObject().put( newOwnerId, obj ); Set<MessagingContext> subscriptions = this.routingContext.subscriptions.remove( oldOwnerId ); if( subscriptions != null ) this.routingContext.subscriptions.put( newOwnerId, subscriptions ); } } /** * @return the routing context */ public RoutingContext getRoutingContext() { return this.routingContext; } /** * @return the owner ID */ public String getOwnerId() { return this.ownerId; } /** * Builds a unique ID. * @param ownerKind the owner kind (not null) * @param applicationName the application name (can be null) * @param scopedInstancePath the scoped instance path (can be null) * @return a non-null string */ public static String buildOwnerId( RecipientKind ownerKind, String applicationName, String scopedInstancePath ) { StringBuilder sb = new StringBuilder(); if( ownerKind == RecipientKind.DM ) { sb.append( "@DM@" ); } else { if( scopedInstancePath !=null ) { sb.append( scopedInstancePath ); sb.append( " " ); } if( applicationName != null ) { sb.append( "@ " ); sb.append( applicationName ); } } // The "domain" is not used here. // "domain" was not designed for self-hosted messaging but for "real" messaging servers. return sb.toString().trim(); } /** * Builds a unique ID. * @param ownerKind the owner kind (not null) * @param applicationName the application name (can be null) * @param scopedInstancePath the scoped instance path (can be null) * @return a non-null string */ public static String buildOwnerId( MessagingContext ctx ) { return ctx == null ? null : buildOwnerId( ctx.getKind(), ctx.getApplicationName(), ctx.getComponentOrFacetName()); } /** * Registers a subscription between an ID and a context. * @param id a client ID * @param ctx a messaging context * @throws IOException */ protected void subscribe( String id, MessagingContext ctx ) throws IOException { if( ! canProceed()) return; Set<MessagingContext> sub = this.routingContext.subscriptions.get( id ); if( sub == null ) { sub = new HashSet<> (); this.routingContext.subscriptions.put( id, sub ); } sub.add( ctx ); } /** * Unregisters a subscription between an ID and a context. * @param id a client ID * @param ctx a messaging context * @throws IOException */ protected void unsubscribe( String id, MessagingContext ctx ) throws IOException { if( ! canProceed()) return; Set<MessagingContext> sub = this.routingContext.subscriptions.get( id ); if( sub != null ) { sub.remove( ctx ); if( sub.isEmpty()) this.routingContext.subscriptions.remove( id ); } } /** * Determines whether a messaging operation can be done. * <p> * A messaging operation can be publishing a message or * dealing with subscriptions. * </p> * <p> * Example: verify a connection/login was established. * </p> * * @return true if we can proceed, false otherwise */ protected boolean canProceed() { return ! this.connectionIsRequired || this.connected.get(); } protected abstract Map<String,T> getStaticContextToObject(); protected abstract void process( T obj, Message message ) throws IOException; }