/** * Copyright 2015-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.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import net.roboconf.core.model.beans.ImportedVariable; import net.roboconf.core.model.beans.Instance; import net.roboconf.core.model.helpers.ComponentHelpers; import net.roboconf.core.model.helpers.VariableHelpers; import net.roboconf.core.utils.Utils; /** * A context that identifies messages recipients and topic names. * <p> * Three properties are part of this class.<br /> * The kind identifies the message recipient. It can either be the DM, * or agents or inter-application exchanges. The application name indicates * an application scope for the listeners. And the topic name indicates more * precise bindings. For agents, the topic name can be defined from a component * or facet name and a "direction". * </p> * <p> * When using the {@link RecipientKind #DM} kind, the topic name is ignored. The application * name is kept to be reused as a topic name. In terms of messaging, there is only one DM. * Even if we had to deploy several DM (for any reason), DM messages should be routed to all of them. * </p> * <p> * Using the {@link RecipientKind #AGENTS} kind means we target a specific topic name * in a given application. Both the topic name and the application name matter. Generally, * we use an indirection to build the topic name. It is based on a component or facet name * and a "direction" (target those that import a variable, or those that export it). * </p> * <p> * Eventually, the {@link RecipientKind #INTER_APP} kind means exchanges between agents * but without application scope (it is ignored). Only the topic name matters. We could have * implemented this kind with the AGENTS one and a null application name. But having a distinct * value for this use case will simplify debug. Besides, inter-application dependencies are * a real Roboconf feature. It is not only a view of the messaging. * </p> * * @author Vincent Zurczak - Linagora */ public class MessagingContext implements Serializable { private static final long serialVersionUID = 5529159467155629784L; private final RecipientKind kind; private final String domain, componentOrFacetName, applicationName; private final ThoseThat thoseThat; /** * Constructor. * @param kind the recipient kind * @param domain the domain * @param applicationName an application name */ public MessagingContext( RecipientKind kind, String domain, String applicationName ) { this( kind, domain, null, null, applicationName ); } /** * Constructor. * @param kind the recipient kind * @param domain the domain * @param topicName a topic name * @param applicationName an application name */ public MessagingContext( RecipientKind kind, String domain, String topicName, String applicationName ) { this( kind, domain, topicName, null, applicationName ); } /** * Constructor. * <p> * This is a convenience constructor that builds a topic name * from a component or facet name and an agent direction. This constructor * should be used when sending messages to agents. * </p> * * @param kind the recipient kind * @param domain the domain * @param componentOrFacetName the component or facet name * @param thoseThat do we target "those that export" or "those that import" <code>componentOrFacetName</code>? * @param applicationName an application name */ public MessagingContext( RecipientKind kind, String domain, String componentOrFacetName, ThoseThat thoseThat, String applicationName ) { this.kind = kind; this.domain = domain; if( kind == RecipientKind.DM ) { // The application name will be used to build a topic name. this.componentOrFacetName = null; this.applicationName = applicationName; this.thoseThat = null; } else if( kind == RecipientKind.INTER_APP ) { this.componentOrFacetName = componentOrFacetName; this.thoseThat = thoseThat; this.applicationName = null; } else { this.thoseThat = thoseThat; this.componentOrFacetName = componentOrFacetName; this.applicationName = applicationName; } } public RecipientKind getKind() { return this.kind; } public String getApplicationName() { return this.applicationName; } public String getComponentOrFacetName() { return this.componentOrFacetName; } public ThoseThat getAgentDirection() { return this.thoseThat; } public String getDomain() { return this.domain; } /** * @return a topic name, or an empty string if none was specified */ public String getTopicName() { StringBuilder sb = new StringBuilder(); if( this.kind == RecipientKind.DM ) { if( this.applicationName != null ) sb.append( this.applicationName ); } else { if( this.thoseThat != null ) sb.append( this.thoseThat ); if( this.componentOrFacetName != null ) sb.append( this.componentOrFacetName ); } return sb.toString(); } @Override public int hashCode() { String topicName = getTopicName(); int backup = this.kind.hashCode(); return Utils.isEmptyOrWhitespaces( topicName ) ? backup : topicName.hashCode(); } @Override public boolean equals( Object obj ) { return obj instanceof MessagingContext && this.kind == ((MessagingContext ) obj).kind && this.thoseThat == ((MessagingContext ) obj).thoseThat && Objects.equals( this.componentOrFacetName, ((MessagingContext ) obj).componentOrFacetName ) && Objects.equals( this.applicationName, ((MessagingContext ) obj).applicationName ); } @Override public String toString() { StringBuilder sb = new StringBuilder( getTopicName()); if( this.applicationName != null && this.kind != RecipientKind.DM ) { sb.append( " @ " ); sb.append( this.applicationName ); } sb.append( " (" ); sb.append( this.kind ); sb.append( ")" ); return sb.toString().trim(); } /** * Builds a list of messaging contexts. * @param domain the domain * @param applicationName the name of the agent's application * @param instance the current instance * @param thoseThat whether we target "those that import" or "those that export" * @return a non-null list */ public static Collection<MessagingContext> forImportedVariables( String domain, String applicationName, Instance instance, ThoseThat thoseThat ) { Map<String,MessagingContext> result = new HashMap<> (); for( ImportedVariable var : ComponentHelpers.findAllImportedVariables( instance.getComponent()).values()) { String componentOrApplicationTemplateName = VariableHelpers.parseVariableName( var.getName()).getKey(); if( result.containsKey( componentOrApplicationTemplateName )) continue; // When we import a variable, it is either internal or external, but not both! RecipientKind kind = var.isExternal() ? RecipientKind.INTER_APP : RecipientKind.AGENTS; MessagingContext ctx = new MessagingContext( kind, domain, componentOrApplicationTemplateName, thoseThat, applicationName ); result.put( componentOrApplicationTemplateName, ctx ); } return result.values(); } /** * Builds a list of messaging contexts. * @param domain the domain * @param applicationName the name of the agent's application * @param instance the current instance * @param externalExports a non-null map that associates internal exported variables with global ones * @param thoseThat whether we target "those that import" or "those that export" * @return a non-null list */ public static List<MessagingContext> forExportedVariables( String domain, String applicationName, Instance instance, Map<String,String> externalExports, ThoseThat thoseThat ) { List<MessagingContext> result = new ArrayList<> (); // For inter-app messages, the real question is about whether we need // to create a context for the application template. Set<String> externalExportPrefixes = new HashSet<> (); for( String varName : externalExports.keySet()) { String prefix = VariableHelpers.parseVariableName( varName ).getKey(); externalExportPrefixes.add( prefix ); } // Internal variables boolean publishExternal = false; for( String facetOrComponentName : VariableHelpers.findPrefixesForExportedVariables( instance )) { MessagingContext ctx = new MessagingContext( RecipientKind.AGENTS, domain, facetOrComponentName, thoseThat, applicationName ); result.add( ctx ); if( externalExportPrefixes.contains( facetOrComponentName )) publishExternal = true; } // External variables - they all have the same prefix, the application template's name if( publishExternal ) { String varName = externalExports.values().iterator().next(); String prefix = VariableHelpers.parseVariableName( varName ).getKey(); // We indicate the application name, but it will most likely not be used // for inter-application messages. MessagingContext ctx = new MessagingContext( RecipientKind.INTER_APP, domain, prefix, thoseThat, applicationName ); result.add( ctx ); } return result; } /** * @author Vincent Zurczak - Linagora */ public static enum RecipientKind { INTER_APP, DM, AGENTS; } /** * @author Vincent Zurczak - Linagora */ public static enum ThoseThat { EXPORT( "those.that.export." ), IMPORT( "those.that.import." ); private String string; /** * Constructor. * @param string */ private ThoseThat( String string ) { this.string = string; } @Override public String toString() { return this.string; } } }