or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

authorization.mdcsrf-protection.mdexpression-handling.mdindex.mdmessage-matching.mdreactive-support.mdsecurity-context.md
tile.json

message-matching.mddocs/

Message Matching

Match messages based on destination patterns, message types, or custom criteria with support for path variables and composite matching logic.

Capabilities

MessageMatcher Interface

Core interface for determining if a message should be matched. Foundation for all message matching functionality.

/**
 * Strategy interface for matching messages.
 *
 * @param <T> the message payload type
 * @since 4.0
 */
public interface MessageMatcher<T> {

    /**
     * Returns true if this matcher matches the given message.
     *
     * @param message the message to match
     * @return true if message matches
     */
    boolean matches(Message<? extends T> message);

    /**
     * Returns match result with extracted path variables.
     *
     * @param message the message to match
     * @return match result including extracted variables
     * @since 6.5
     */
    default MatchResult matcher(Message<? extends T> message) {
        return matches(message) ? MatchResult.match() : MatchResult.notMatch();
    }

    /**
     * Matcher that matches every message.
     */
    MessageMatcher<Object> ANY_MESSAGE = (message) -> true;
}

Usage Example:

// Custom matcher implementation
MessageMatcher<Object> customMatcher = (message) -> {
    String destination = (String) message.getHeaders()
        .get("simpDestination");
    return destination != null && destination.startsWith("/app/admin");
};

// Use with authorization manager
MessageMatcherDelegatingAuthorizationManager authManager =
    MessageMatcherDelegatingAuthorizationManager.builder()
        .matchers(customMatcher).hasRole("ADMIN")
        .build();

MatchResult

Result of a message matching operation, including match status and any extracted path variables.

/**
 * Result of a message matching operation.
 *
 * @since 6.5
 */
public interface MatchResult {

    /**
     * Returns whether the message matched.
     *
     * @return true if matched
     */
    boolean isMatch();

    /**
     * Returns extracted path variables from pattern matching.
     *
     * @return map of variable names to values (empty if no variables)
     */
    Map<String, String> getVariables();

    /**
     * Creates a match result with no variables.
     *
     * @return match result
     */
    static MatchResult match() {
        return match(Collections.emptyMap());
    }

    /**
     * Creates a match result with extracted variables.
     *
     * @param variables the extracted path variables
     * @return match result
     */
    static MatchResult match(Map<String, String> variables);

    /**
     * Creates a non-match result.
     *
     * @return non-match result
     */
    static MatchResult notMatch();
}

PathPatternMessageMatcher

Matches messages by destination path patterns using Spring's PathPattern with support for path variable extraction.

/**
 * MessageMatcher that matches based on destination path patterns.
 * Supports path variables like /app/user/{userId}/*.
 *
 * @since 6.5
 */
public final class PathPatternMessageMatcher implements MessageMatcher<Object> {

    /**
     * Matcher for messages with null destination.
     */
    public static final MessageMatcher<Object> NULL_DESTINATION_MATCHER;

    /**
     * Creates builder with default PathPatternParser.
     *
     * @return builder instance
     */
    public static Builder withDefaults();

    /**
     * Creates builder with custom PathPatternParser.
     *
     * @param parser the path pattern parser to use
     * @return builder instance
     */
    public static Builder withPathPatternParser(PathPatternParser parser);

    /**
     * Returns true if message destination matches the pattern.
     *
     * @param message the message to match
     * @return true if destination matches pattern
     */
    public boolean matches(Message<?> message);

    /**
     * Returns match result with extracted path variables.
     *
     * @param message the message to match
     * @return match result with variables
     */
    public MatchResult matcher(Message<?> message);
}

Builder API:

public static final class Builder {

    /**
     * Creates matcher for the given destination pattern (any message type).
     *
     * @param pattern the destination pattern (e.g., "/app/user/**")
     * @return path pattern matcher
     */
    public PathPatternMessageMatcher matcher(String pattern);

    /**
     * Creates matcher for the given type and destination pattern.
     *
     * @param type the STOMP message type to match
     * @param pattern the destination pattern
     * @return path pattern matcher
     */
    public PathPatternMessageMatcher matcher(SimpMessageType type, String pattern);
}

Usage Examples:

// Match any message to /app/chat/**
PathPatternMessageMatcher chatMatcher =
    PathPatternMessageMatcher.withDefaults()
        .matcher("/app/chat/**");

// Match MESSAGE type to /app/user/**
PathPatternMessageMatcher userMessageMatcher =
    PathPatternMessageMatcher.withDefaults()
        .matcher(SimpMessageType.MESSAGE, "/app/user/**");

// Extract path variables
PathPatternMessageMatcher userMatcher =
    PathPatternMessageMatcher.withDefaults()
        .matcher("/app/user/{userId}/profile");

Message<?> message = // ... message with destination "/app/user/123/profile"
MatchResult result = userMatcher.matcher(message);
if (result.isMatch()) {
    String userId = result.getVariables().get("userId"); // "123"
}

// Match messages with null destination
MessageMatcher<Object> nullDestMatcher =
    PathPatternMessageMatcher.NULL_DESTINATION_MATCHER;

SimpMessageTypeMatcher

Matches messages by STOMP message type (CONNECT, SUBSCRIBE, MESSAGE, UNSUBSCRIBE, DISCONNECT).

/**
 * MessageMatcher that matches based on STOMP message type.
 *
 * @since 4.0
 */
public class SimpMessageTypeMatcher implements MessageMatcher<Object> {

    /**
     * Creates matcher for the specified message type.
     *
     * @param typeToMatch the STOMP message type to match
     */
    public SimpMessageTypeMatcher(SimpMessageType typeToMatch);

    /**
     * Returns true if message has the matching type.
     *
     * @param message the message to match
     * @return true if message type matches
     */
    public boolean matches(Message<?> message);
}

Usage Examples:

// Match CONNECT messages
MessageMatcher<Object> connectMatcher =
    new SimpMessageTypeMatcher(SimpMessageType.CONNECT);

// Match SUBSCRIBE messages
MessageMatcher<Object> subscribeMatcher =
    new SimpMessageTypeMatcher(SimpMessageType.SUBSCRIBE);

// Match MESSAGE type
MessageMatcher<Object> messageMatcher =
    new SimpMessageTypeMatcher(SimpMessageType.MESSAGE);

// Use with authorization
MessageMatcherDelegatingAuthorizationManager authManager =
    MessageMatcherDelegatingAuthorizationManager.builder()
        .matchers(connectMatcher).permitAll()
        .matchers(subscribeMatcher, messageMatcher).authenticated()
        .build();

AndMessageMatcher

Composite matcher that returns true only if ALL contained matchers match (AND logic).

/**
 * Composite MessageMatcher that matches if ALL matchers match.
 *
 * @param <T> the message payload type
 * @since 4.0
 */
public final class AndMessageMatcher<T> extends AbstractMessageMatcherComposite<T> {

    /**
     * Creates AND matcher from list of matchers.
     *
     * @param messageMatchers list of matchers to combine
     */
    public AndMessageMatcher(List<MessageMatcher<T>> messageMatchers);

    /**
     * Creates AND matcher from varargs matchers.
     *
     * @param messageMatchers matchers to combine
     */
    public AndMessageMatcher(MessageMatcher<T>... messageMatchers);

    /**
     * Returns true if ALL matchers match the message.
     *
     * @param message the message to match
     * @return true if all matchers match
     */
    public boolean matches(Message<? extends T> message);
}

Usage Example:

// Match messages that are both MESSAGE type AND to /app/admin/** destination
MessageMatcher<Object> typeMatcher =
    new SimpMessageTypeMatcher(SimpMessageType.MESSAGE);

PathPatternMessageMatcher destMatcher =
    PathPatternMessageMatcher.withDefaults()
        .matcher("/app/admin/**");

MessageMatcher<Object> andMatcher =
    new AndMessageMatcher<>(typeMatcher, destMatcher);

// Use with authorization
MessageMatcherDelegatingAuthorizationManager authManager =
    MessageMatcherDelegatingAuthorizationManager.builder()
        .matchers(andMatcher).hasRole("ADMIN")
        .build();

OrMessageMatcher

Composite matcher that returns true if ANY contained matcher matches (OR logic).

/**
 * Composite MessageMatcher that matches if ANY matcher matches.
 *
 * @param <T> the message payload type
 * @since 4.0
 */
public final class OrMessageMatcher<T> extends AbstractMessageMatcherComposite<T> {

    /**
     * Creates OR matcher from list of matchers.
     *
     * @param messageMatchers list of matchers to combine
     */
    public OrMessageMatcher(List<MessageMatcher<T>> messageMatchers);

    /**
     * Creates OR matcher from varargs matchers.
     *
     * @param messageMatchers matchers to combine
     */
    public OrMessageMatcher(MessageMatcher<T>... messageMatchers);

    /**
     * Returns true if ANY matcher matches the message.
     *
     * @param message the message to match
     * @return true if any matcher matches
     */
    public boolean matches(Message<? extends T> message);
}

Usage Example:

// Match messages to either /app/public/** OR /app/guest/**
PathPatternMessageMatcher publicMatcher =
    PathPatternMessageMatcher.withDefaults()
        .matcher("/app/public/**");

PathPatternMessageMatcher guestMatcher =
    PathPatternMessageMatcher.withDefaults()
        .matcher("/app/guest/**");

MessageMatcher<Object> orMatcher =
    new OrMessageMatcher<>(publicMatcher, guestMatcher);

// Use with authorization
MessageMatcherDelegatingAuthorizationManager authManager =
    MessageMatcherDelegatingAuthorizationManager.builder()
        .matchers(orMatcher).permitAll()
        .build();

AbstractMessageMatcherComposite

Base class for composite message matchers (And/Or). Provides common functionality.

/**
 * Abstract base for composite MessageMatcher implementations.
 *
 * @param <T> the message payload type
 * @since 4.0
 */
public abstract class AbstractMessageMatcherComposite<T> implements MessageMatcher<T> {

    /**
     * Returns the list of contained matchers.
     *
     * @return list of matchers
     */
    public final List<MessageMatcher<T>> getMessageMatchers();

    /**
     * Logger for subclasses.
     */
    protected final Log logger = LogFactory.getLog(getClass());

    /**
     * Deprecated logger field (use 'logger' instead).
     * @deprecated since 5.4, use 'logger' field instead
     */
    @Deprecated
    protected final Log LOGGER = LogFactory.getLog(getClass());
}

Matching Patterns

Pattern Syntax

PathPatternMessageMatcher uses Spring's PathPattern syntax:

  • ? - matches one character
  • * - matches zero or more characters within a path segment
  • ** - matches zero or more path segments
  • {name} - matches a path segment and captures it as a variable
  • {name:[regex]} - matches using regex and captures as variable

Examples:

"/app/chat"          // Exact match
"/app/chat/*"        // Matches /app/chat/room1, /app/chat/room2
"/app/chat/**"       // Matches /app/chat/room1/message
"/app/user/{userId}" // Matches /app/user/123, captures userId=123
"/app/{type}/{id}"   // Matches /app/user/123, captures type=user, id=123
"/app/file/*.{ext}"  // Matches /app/file/doc.pdf, captures ext=pdf

Common Matching Scenarios

Match by Destination Prefix

MessageMatcherDelegatingAuthorizationManager authManager =
    MessageMatcherDelegatingAuthorizationManager.builder()
        .simpDestMatchers("/app/admin/**").hasRole("ADMIN")
        .simpDestMatchers("/app/user/**").hasRole("USER")
        .simpDestMatchers("/app/public/**").permitAll()
        .build();

Match by Message Type

MessageMatcherDelegatingAuthorizationManager authManager =
    MessageMatcherDelegatingAuthorizationManager.builder()
        .simpTypeMatchers(SimpMessageType.CONNECT).permitAll()
        .simpTypeMatchers(SimpMessageType.MESSAGE).authenticated()
        .simpTypeMatchers(SimpMessageType.SUBSCRIBE).authenticated()
        .build();

Match Specific Type and Destination

MessageMatcherDelegatingAuthorizationManager authManager =
    MessageMatcherDelegatingAuthorizationManager.builder()
        // Only MESSAGE type to /app/chat/**
        .simpMessageDestMatchers("/app/chat/**").hasRole("USER")
        // Only SUBSCRIBE type to /topic/**
        .simpSubscribeDestMatchers("/topic/**").authenticated()
        .build();

Match with Path Variables

// Custom authorization using path variables
AuthorizationManager<MessageAuthorizationContext<?>> userAuthManager =
    (authentication, context) -> {
        String userId = context.getVariables().get("userId");
        String authenticatedUser = authentication.get().getName();

        return new AuthorizationDecision(authenticatedUser.equals(userId));
    };

MessageMatcherDelegatingAuthorizationManager authManager =
    MessageMatcherDelegatingAuthorizationManager.builder()
        .simpDestMatchers("/app/user/{userId}/**").access(userAuthManager)
        .build();

Composite Matching Logic

// Complex AND condition: MESSAGE type to admin destinations
MessageMatcher<Object> typeMatcher =
    new SimpMessageTypeMatcher(SimpMessageType.MESSAGE);

PathPatternMessageMatcher adminMatcher =
    PathPatternMessageMatcher.withDefaults()
        .matcher("/app/admin/**");

MessageMatcher<Object> combined = new AndMessageMatcher<>(typeMatcher, adminMatcher);

MessageMatcherDelegatingAuthorizationManager authManager =
    MessageMatcherDelegatingAuthorizationManager.builder()
        .matchers(combined).hasRole("ADMIN")
        .build();
// Complex OR condition: Public or guest destinations
PathPatternMessageMatcher publicMatcher =
    PathPatternMessageMatcher.withDefaults()
        .matcher("/app/public/**");

PathPatternMessageMatcher guestMatcher =
    PathPatternMessageMatcher.withDefaults()
        .matcher("/app/guest/**");

MessageMatcher<Object> publicOrGuest =
    new OrMessageMatcher<>(publicMatcher, guestMatcher);

MessageMatcherDelegatingAuthorizationManager authManager =
    MessageMatcherDelegatingAuthorizationManager.builder()
        .matchers(publicOrGuest).permitAll()
        .build();

Custom Matcher Implementation

Simple Custom Matcher

public class HeaderValueMatcher implements MessageMatcher<Object> {

    private final String headerName;
    private final String expectedValue;

    public HeaderValueMatcher(String headerName, String expectedValue) {
        this.headerName = headerName;
        this.expectedValue = expectedValue;
    }

    @Override
    public boolean matches(Message<?> message) {
        Object headerValue = message.getHeaders().get(headerName);
        return expectedValue.equals(headerValue);
    }
}

// Usage
MessageMatcher<Object> customMatcher =
    new HeaderValueMatcher("customHeader", "expectedValue");

MessageMatcherDelegatingAuthorizationManager authManager =
    MessageMatcherDelegatingAuthorizationManager.builder()
        .matchers(customMatcher).authenticated()
        .build();

Matcher with Path Variable Extraction

public class RegexDestinationMatcher implements MessageMatcher<Object> {

    private final Pattern pattern;

    public RegexDestinationMatcher(String regex) {
        this.pattern = Pattern.compile(regex);
    }

    @Override
    public boolean matches(Message<?> message) {
        String destination = (String) message.getHeaders()
            .get("simpDestination");
        return destination != null && pattern.matcher(destination).matches();
    }

    @Override
    public MatchResult matcher(Message<?> message) {
        String destination = (String) message.getHeaders()
            .get("simpDestination");

        if (destination == null) {
            return MatchResult.notMatch();
        }

        Matcher matcher = pattern.matcher(destination);
        if (!matcher.matches()) {
            return MatchResult.notMatch();
        }

        // Extract named groups as variables
        Map<String, String> variables = new HashMap<>();
        // ... extract groups from matcher
        return MatchResult.match(variables);
    }
}

Message Header Constants

Common message headers used in matching:

  • simpMessageType - The SimpMessageType (CONNECT, MESSAGE, SUBSCRIBE, etc.)
  • simpDestination - The destination path (e.g., "/app/chat")
  • simpUser - The authenticated user (Principal)
  • simpSessionId - The WebSocket session ID
  • simpSubscriptionId - The subscription ID (for SUBSCRIBE messages)

Example accessing headers:

MessageMatcher<Object> customMatcher = (message) -> {
    MessageHeaders headers = message.getHeaders();

    SimpMessageType type = (SimpMessageType) headers.get("simpMessageType");
    String destination = (String) headers.get("simpDestination");
    Principal user = (Principal) headers.get("simpUser");

    // Custom matching logic
    return type == SimpMessageType.MESSAGE &&
           destination != null &&
           destination.startsWith("/app/admin") &&
           user != null;
};

Error Handling

Null Safety in Matchers

Always handle null values when accessing message headers:

public class SafeDestinationMatcher implements MessageMatcher<Object> {
    
    @Override
    public boolean matches(Message<?> message) {
        if (message == null || message.getHeaders() == null) {
            return false;
        }
        
        String destination = (String) message.getHeaders().get("simpDestination");
        return destination != null && destination.startsWith("/app/admin");
    }
}

Exception Handling in Custom Matchers

Custom matchers should handle exceptions gracefully:

public class RobustMatcher implements MessageMatcher<Object> {
    
    @Override
    public boolean matches(Message<?> message) {
        try {
            // Matching logic
            return performMatch(message);
        } catch (Exception e) {
            logger.error("Matcher error", e);
            // Fail-secure: return false on error
            return false;
        }
    }
}

Troubleshooting

Issue: Matcher Not Matching Expected Messages

Causes:

  1. Pattern syntax incorrect
  2. Header name mismatch
  3. Message type not as expected

Solutions:

// Debug: Log actual message headers
MessageMatcher<Object> debugMatcher = (message) -> {
    logger.debug("Message headers: {}", message.getHeaders());
    logger.debug("Destination: {}", message.getHeaders().get("simpDestination"));
    logger.debug("Type: {}", message.getHeaders().get("simpMessageType"));
    return true;
};

Issue: Path Variables Not Extracted

Causes:

  1. Pattern doesn't use variable syntax
  2. Using wrong matcher

Solutions:

// Correct: Use {variable} syntax
PathPatternMessageMatcher.withDefaults()
    .matcher("/app/user/{userId}/**")

// Wrong: No variable extraction
PathPatternMessageMatcher.withDefaults()
    .matcher("/app/user/*/**")

Issue: Composite Matcher Not Working

Causes:

  1. Empty matcher list
  2. Null matchers in list

Solutions:

// Ensure all matchers are non-null
List<MessageMatcher<Object>> matchers = Arrays.asList(
    typeMatcher,
    destMatcher
);
// Remove any nulls
matchers.removeIf(Objects::isNull);

MessageMatcher<Object> composite = new AndMessageMatcher<>(matchers);