// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.messagebus.routing;
import java.util.ArrayList;
import java.util.List;
/**
* Along with the {@link RoutingSpec}, {@link RoutingTableSpec} and {@link RouteSpec}, this holds the routing
* specifications for all protocols. The only way a client can configure or alter the settings of a message bus instance
* is through these classes.
*
* This class contains the spec for a single hop.
*
* @author Simon Thoresen Hult
*/
public class HopSpec {
private final String name;
private final String selector;
private final List recipients = new ArrayList<>();
private final boolean verify;
private boolean ignoreResult = false;
/**
* Creates a new named hop specification.
*
* @param name A protocol-unique name for this hop.
* @param selector A string that represents the selector for this hop.
*/
public HopSpec(String name, String selector) {
this(name, selector, true);
}
/**
* Creates a new named hop specification.
*
* @param name A protocol-unique name for this hop.
* @param selector A string that represents the selector for this hop.
* @param verify Whether or not this should be verified.
*/
public HopSpec(String name, String selector, boolean verify) {
this.name = name;
this.selector = selector;
this.verify = verify;
}
/**
* Implements the copy constructor.
*
* @param obj The object to copy.
*/
public HopSpec(HopSpec obj) {
this.name = obj.name;
this.selector = obj.selector;
this.verify = obj.verify;
for (String recipient : obj.recipients) {
recipients.add(recipient);
}
this.ignoreResult = obj.ignoreResult;
}
/**
* Returns the protocol-unique name of this hop.
*
* @return The name.
*/
public String getName() {
return name;
}
/**
* Returns the string selector that resolves the recipients of this hop.
*
* @return The selector.
*/
public String getSelector() {
return selector;
}
/**
* Returns whether there are any recipients that the selector can choose from.
*
* @return True if there is at least one recipient.
*/
public boolean hasRecipients() {
return !recipients.isEmpty();
}
/**
* Returns the number of recipients that the selector can choose from.
*
* @return The number of recipients.
*/
public int getNumRecipients() {
return recipients.size();
}
/**
* Returns the recipients at the given index.
*
* @param i The index of the recipient to return.
* @return The recipient at the given index.
*/
public String getRecipient(int i) {
return recipients.get(i);
}
/**
* Adds the given recipient to this.
*
* @param recipient The recipient to add.
* @return This, to allow chaining.
*/
public HopSpec addRecipient(String recipient) {
recipients.add(recipient);
return this;
}
/**
* Adds the given recipients to this.
*
* @param recipients The recipients to add.
* @return This, to allow chaining.
*/
public HopSpec addRecipients(List recipients) {
this.recipients.addAll(recipients);
return this;
}
/**
* Sets the recipient at the given index.
*
* @param i The index at which to set the recipient.
* @param recipient The recipient to set.
* @return This, to allow chaining.
*/
public HopSpec setRecipient(int i, String recipient) {
recipients.set(i, recipient);
return this;
}
/**
* Removes the recipient at the given index.
*
* @param i The index of the recipient to remove.
* @return The removed recipient.
*/
public String removeRecipient(int i) {
return recipients.remove(i);
}
/**
* Clears the list of recipients that the selector may choose from.
*
* @return This, to allow chaining.
*/
public HopSpec clearRecipients() {
recipients.clear();
return this;
}
/**
* Returns whether to ignore the result when routing through this hop.
*
* @return True to ignore the result.
*/
public boolean getIgnoreResult() {
return ignoreResult;
}
/**
* Sets whether to ignore the result when routing through this hop.
*
* @param ignoreResult Whether to ignore the result.
* @return This, to allow chaining.
*/
public HopSpec setIgnoreResult(boolean ignoreResult) {
this.ignoreResult = ignoreResult;
return this;
}
/**
* Verifies the content of this against the given application.
*
* @param app The application to verify against.
* @param table The routing table to verify against.
* @param errors The list of errors found.
* @return True if no errors where found.
*/
public boolean verify(ApplicationSpec app, RoutingTableSpec table, List errors) {
if (verify) {
verify(app, table, null, recipients, selector, errors,
"hop '" + name + "' in routing table '" + table.getProtocol() + "'");
}
return errors.isEmpty();
}
/**
* Verifies that the hop given by the given selector and children is valid.
*
* @param app The application to verify against.
* @param table The routing table to verify against.
* @param parent The parent hop that the selector must match.
* @param selector The selector to verify.
* @param children The children to verify, may be null or empty.
* @param errors The list of errors found.
* @param context The context to use if adding an error.
* @return True if no errors where found.
*/
static boolean verify(ApplicationSpec app, RoutingTableSpec table, Hop parent, List children,
String selector, List errors, String context) {
// Verify that the selector can be parsed.
Hop hop = Hop.parse(selector);
for (int i = 0, len = hop.getNumDirectives(); i < len; ++i) {
HopDirective dir = hop.getDirective(i);
if (dir instanceof ErrorDirective) {
errors.add("For " + context + "; " + ((ErrorDirective)dir).getMessage());
return false;
}
}
// Verify that the parent matches this, if any.
if (parent != null) {
if (parent.getNumDirectives() == 1) {
// hops that contain a single policy directive will typically be magic
} else if (!parent.matches(hop)) {
errors.add("Selector '" + parent.getServiceName() + "' does not match " + context + ".");
return false;
}
}
// Traverse and verify the directives of the hop.
boolean verifyServiceName = true;
boolean allowRecipients = false;
for (int i = 0, len = hop.getNumDirectives(); i < len; ++i) {
HopDirective dir = hop.getDirective(i);
if (dir instanceof ErrorDirective) {
// caught above
} else if (dir instanceof PolicyDirective) {
allowRecipients = true;
verifyServiceName = false;
} else if (dir instanceof RouteDirective) {
String routeName = ((RouteDirective)dir).getName();
if (!table.hasRoute(routeName)) {
errors.add(capitalize(context) + " references route '" + routeName + "' which does not exist.");
return false;
}
verifyServiceName = false;
} else if (dir instanceof TcpDirective) {
verifyServiceName = false;
} else if (dir instanceof VerbatimDirective) {
// will be verified below
}
}
// Verify that the service referenced by the hop exists, if required.
if (verifyServiceName) {
String serviceName = hop.getServiceName();
if (table.hasRoute(serviceName)) {
// all good
} else if (table.hasHop(serviceName)) {
// also good
} else if (!app.isService(table.getProtocol(), serviceName)) {
errors.add(capitalize(context) + " references '" + serviceName + "' which is neither a service," +
" a route nor another hop.");
return false;
}
}
// Verify that recipients are allowed and that they are valid themselves.
if (children != null && children.size() > 0) {
if (!allowRecipients) {
errors.add(capitalize(context) + " has recipients but no policy directive.");
return false;
}
for (String child : children) {
verify(app, table, hop, null, child, errors, "recipient '" + child + "' in " + context);
}
}
// All ok.
return true;
}
/**
* Appends the content of this to the given config string builder.
*
* @param cfg The config to add to.
* @param prefix The prefix to use for each add.
*/
public void toConfig(StringBuilder cfg, String prefix) {
cfg.append(prefix).append("name ").append(RoutingSpec.toConfigString(name)).append("\n");
cfg.append(prefix).append("selector ").append(RoutingSpec.toConfigString(selector)).append("\n");
if (ignoreResult) {
cfg.append(prefix).append("ignoreresult true\n");
}
int numRecipients = recipients.size();
if (numRecipients > 0) {
cfg.append(prefix).append("recipient[").append(numRecipients).append("]\n");
for (int i = 0; i < numRecipients; ++i) {
cfg.append(prefix).append("recipient[").append(i).append("] ");
cfg.append(RoutingSpec.toConfigString(recipients.get(i))).append("\n");
}
}
}
/**
* Capitalizes the given string by upper-casing its first letter.
*
* @param str The string to capitalize.
* @return The capitalized string.
*/
private static String capitalize(String str) {
return Character.toUpperCase(str.charAt(0)) + str.substring(1);
}
// Overrides Object.
@Override
public String toString() {
StringBuilder ret = new StringBuilder();
toConfig(ret, "");
return ret.toString();
}
// Overrides Object.
@Override
public boolean equals(Object obj) {
if (!(obj instanceof HopSpec)) {
return false;
}
HopSpec rhs = (HopSpec)obj;
if (!name.equals(rhs.name)) {
return false;
}
if (!selector.equals(rhs.selector)) {
return false;
}
if (!recipients.equals(rhs.recipients)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + (selector != null ? selector.hashCode() : 0);
result = 31 * result + (recipients != null ? recipients.hashCode() : 0);
result = 31 * result + (verify ? 1 : 0);
result = 31 * result + (ignoreResult ? 1 : 0);
return result;
}
}