Skip to content

Commit

Permalink
feature: Write "regex:" in front of value to indicate, that this is a…
Browse files Browse the repository at this point in the history
… pattern. (#9)

* fix: throw a more speaking exception when regex in query was wrong

* feat: write "regex:" in query-param to indicate that the query contains a regExp

* feat: implemented change requests

* fix: move test-package
  • Loading branch information
levinkerschberger authored Oct 31, 2024
1 parent 5456fbc commit 24b4720
Show file tree
Hide file tree
Showing 11 changed files with 486 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
package rocks.inspectit.gepard.agentmanager.connection.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import java.util.List;
import java.util.UUID;
Expand All @@ -14,6 +19,7 @@
import rocks.inspectit.gepard.agentmanager.connection.model.dto.CreateConnectionRequest;
import rocks.inspectit.gepard.agentmanager.connection.model.dto.QueryConnectionRequest;
import rocks.inspectit.gepard.agentmanager.connection.service.ConnectionService;
import rocks.inspectit.gepard.agentmanager.exception.ApiError;

/**
* Controller for handling agent connection requests. Holds the POST endpoint for handling
Expand Down Expand Up @@ -45,7 +51,31 @@ public ResponseEntity<List<ConnectionDto>> getConnections() {
}

@PostMapping("/query")
@Operation(summary = "Query connections")
@Operation(
summary = "Query connections with support for exact and regex matching",
description =
"""
Query connections using a combination of exact matches and regex patterns.
For regex matching, prefix the pattern with 'regex:'.
All fields are optional - omitted fields will not be considered in the query.
""")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "Successfully retrieved matching connections",
content =
@Content(
mediaType = "application/json",
array = @ArraySchema(schema = @Schema(implementation = ConnectionDto.class)))),
@ApiResponse(
responseCode = "400",
description = "Invalid query parameters or regex pattern",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ApiError.class)))
})
public ResponseEntity<List<ConnectionDto>> queryConnections(
@Valid @RequestBody QueryConnectionRequest query) {
return ResponseEntity.ok(connectionService.queryConnections(query));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agentmanager.connection.model.dto;

import jakarta.validation.Valid;
import java.util.Map;
import rocks.inspectit.gepard.agentmanager.connection.validation.ValidRegexPattern;

/**
* Represents a request against the {@code ConnectionController} Query-Endpoint.
*
* <p>All fields are optional. If a field is not set, it is not considered in the query.
*/
public record QueryConnectionRequest(String id, String registrationTime, QueryAgentRequest agent) {
public record QueryConnectionRequest(
@ValidRegexPattern(message = "Invalid connection ID pattern") String id,
@ValidRegexPattern(message = "Invalid registration time pattern") String registrationTime,
@Valid QueryAgentRequest agent) {

public record QueryAgentRequest(
String serviceName,
Long pid,
String gepardVersion,
String otelVersion,
Long startTime,
String javaVersion,
@ValidRegexPattern(message = "Invalid service name pattern") String serviceName,
@ValidRegexPattern(message = "Invalid process ID pattern")
String pid, // pid just has to be a number
@ValidRegexPattern(message = "Invalid Gepard version pattern") String gepardVersion,
@ValidRegexPattern(message = "Invalid OpenTelemetry version pattern") String otelVersion,
@ValidRegexPattern(message = "Invalid start time pattern")
String startTime, // startTime just has to be a number
@ValidRegexPattern(message = "Invalid Java version pattern") String javaVersion,
Map<String, String> attributes) {}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agentmanager.connection.service;

import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
Expand All @@ -15,6 +12,7 @@
import rocks.inspectit.gepard.agentmanager.connection.model.dto.ConnectionDto;
import rocks.inspectit.gepard.agentmanager.connection.model.dto.CreateConnectionRequest;
import rocks.inspectit.gepard.agentmanager.connection.model.dto.QueryConnectionRequest;
import rocks.inspectit.gepard.agentmanager.connection.validation.RegexQueryService;

/** Service-Implementation for handling agent connection requests. */
@Slf4j
Expand All @@ -24,6 +22,8 @@ public class ConnectionService {

private final ConcurrentHashMap<UUID, Connection> connectionCache;

private final RegexQueryService regexQueryService;

/**
* Handles a connection request from an agent.
*
Expand Down Expand Up @@ -81,20 +81,22 @@ public ConnectionDto getConnection(UUID id) {
* @return true if the connection matches the query, false otherwise.
*/
private boolean matchesConnection(Connection connection, QueryConnectionRequest query) {
if (query.id() != null && !connection.getId().toString().matches(query.id())) {
return false;
}
boolean matches = true;

if (query.registrationTime() != null
&& !connection.getRegistrationTime().toString().matches(query.registrationTime())) {
return false;
}
matches &= regexQueryService.matches(connection.getId().toString(), query.id());
matches &=
regexQueryService.matches(
connection.getRegistrationTime().toString(), query.registrationTime());

if (query.agent() != null) {
return matchesAgent(connection.getAgent(), query.agent());

QueryConnectionRequest.QueryAgentRequest queryAgent = query.agent();
Agent agent = connection.getAgent();

matches &= matchesAgent(agent, queryAgent);
}

return true;
return matches;
}

/**
Expand All @@ -105,29 +107,20 @@ private boolean matchesConnection(Connection connection, QueryConnectionRequest
* @return true if the agent matches the query, false otherwise.
*/
private boolean matchesAgent(Agent agent, QueryConnectionRequest.QueryAgentRequest query) {
if (query.serviceName() != null && !agent.getServiceName().matches(query.serviceName())) {
return false;
}

if (query.gepardVersion() != null && !agent.getGepardVersion().matches(query.gepardVersion())) {
return false;
}

if (query.otelVersion() != null && !agent.getOtelVersion().matches(query.otelVersion())) {
return false;
}
boolean matches = true;

if (query.javaVersion() != null && !agent.getJavaVersion().matches(query.javaVersion())) {
return false;
}
matches &= regexQueryService.matches(agent.getServiceName(), query.serviceName());
matches &= regexQueryService.matchesLong(agent.getPid(), query.pid());
matches &= regexQueryService.matches(agent.getGepardVersion(), query.gepardVersion());
matches &= regexQueryService.matches(agent.getOtelVersion(), query.otelVersion());
matches &= regexQueryService.matchesInstant(agent.getStartTime(), query.startTime());
matches &= regexQueryService.matches(agent.getJavaVersion(), query.javaVersion());

if (query.attributes() != null && !query.attributes().isEmpty()) {
Map<String, String> agentAttributes = agent.getAttributes();

return matchesAttributes(agentAttributes, query.attributes());
matches &= matchesAttributes(agent.getAttributes(), query.attributes());
}

return true;
return matches;
}

/**
Expand All @@ -141,11 +134,10 @@ private boolean matchesAttributes(
Map<String, String> agentAttributes, Map<String, String> queryAttributes) {
return queryAttributes.entrySet().stream()
.allMatch(
entry -> {
String key = entry.getKey();
String pattern = entry.getValue();
String agentValue = agentAttributes.get(key);
return agentValue != null && agentValue.matches(pattern);
queryEntry -> {
String actualValue = agentAttributes.get(queryEntry.getKey());
return actualValue != null
&& regexQueryService.matches(actualValue, queryEntry.getValue());
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agentmanager.connection.validation;

import static rocks.inspectit.gepard.agentmanager.connection.validation.RegexQueryService.REGEX_INDICATOR;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
* Validates that a given string is a valid regex pattern.
*
* <p>Used in conjunction with the {@link ValidRegexPattern} annotation.
*/
public class RegexPatternValidator implements ConstraintValidator<ValidRegexPattern, String> {

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
if (value.startsWith(REGEX_INDICATOR)) {
try {
Pattern.compile(value.substring(REGEX_INDICATOR.length()));
return true;
} catch (PatternSyntaxException e) {
return false;
}
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agentmanager.connection.validation;

import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.regex.Pattern;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

/**
* Service for querying values against regex patterns.
*
* <p>Used for filtering entities based on regex patterns.
*/
@Service
@Validated
public class RegexQueryService {

public static final String REGEX_INDICATOR = "regex:";

/**
* Checks if the given value matches the given pattern. If the pattern starts with "regex:", the
* pattern is treated as a regex pattern. Otherwise, the pattern is treated as an exact match. If
* the pattern or value is null, the method returns true, because this means: No filtering.
*
* @param value the value to match
* @param pattern the pattern to match against
* @return true if the value matches the pattern or is an exact match, false otherwise
*/
public boolean matches(String value, String pattern) {

if (pattern == null || value == null) {
return true;
}

if (pattern.startsWith(REGEX_INDICATOR)) {
String regexPattern = pattern.substring(REGEX_INDICATOR.length());
return Pattern.compile(regexPattern).matcher(value).matches();
} else {
return value.equals(pattern);
}
}

/**
* Checks if the given Long matches the given pattern. If the pattern starts with "regex:", the
* pattern is treated as a regex pattern. Otherwise, the pattern is treated as an exact match. If
* the pattern or value is null, the method returns true, because this means: No filtering.
*
* @param value the value to match
* @param pattern the pattern to match against
* @return true if the value matches the pattern or is an exact match, false otherwise
*/
public boolean matchesLong(Long value, String pattern) {
if (pattern == null || value == null) {
return true;
}

if (pattern.startsWith(REGEX_INDICATOR)) {
String regexPattern = pattern.substring(REGEX_INDICATOR.length());
return Pattern.compile(regexPattern).matcher(value.toString()).matches();
} else {
return value.toString().equals(pattern);
}
}

/**
* Checks if the given Instant matches the given pattern. If the pattern starts with "regex:", the
* pattern is treated as a regex pattern. Otherwise, the pattern is treated as an exact match. If
* the pattern or value is null, the method returns true, because this means: No filtering.
*
* @param value The value to match
* @param pattern The pattern to match against
* @return true if the value matches the pattern or is an exact match, false otherwise
*/
public boolean matchesInstant(Instant value, String pattern) {
if (pattern == null || value == null) {
return true;
}

if (pattern.startsWith(REGEX_INDICATOR)) {
String regexPattern = pattern.substring(REGEX_INDICATOR.length());
// Convert Instant to ISO-8601 string for regex matching
String instantString = value.truncatedTo(ChronoUnit.MILLIS).toString();
return Pattern.compile(regexPattern).matcher(instantString).matches();
} else {
try {
// For exact matching, parse the pattern as Instant and compare
Instant patternInstant = Instant.parse(pattern);
return value
.truncatedTo(ChronoUnit.MILLIS)
.equals(patternInstant.truncatedTo(ChronoUnit.MILLIS));
} catch (DateTimeParseException e) {
throw new IllegalArgumentException(
"Invalid timestamp format. Expected ISO-8601 format (e.g., 2024-10-29T10:15:30Z)", e);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agentmanager.connection.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = RegexPatternValidator.class)
public @interface ValidRegexPattern {
String message() default "Invalid regex pattern";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import java.time.LocalDateTime;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.regex.PatternSyntaxException;
import org.eclipse.jgit.errors.InvalidPatternException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
Expand Down Expand Up @@ -129,4 +131,30 @@ public ResponseEntity<ApiError> handleUnrecognizedProperty(

return new ResponseEntity<>(apiError, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(InvalidPatternException.class)
public ResponseEntity<ApiError> handleInvalidPattern(
InvalidPatternException ex, HttpServletRequest request) {
ApiError apiError =
new ApiError(
request.getRequestURI(),
List.of(ex.getMessage()),
HttpStatus.BAD_REQUEST.value(),
LocalDateTime.now());

return new ResponseEntity<>(apiError, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(PatternSyntaxException.class)
public ResponseEntity<ApiError> handleInvalidPatternSyntax(
PatternSyntaxException ex, HttpServletRequest request) {
ApiError apiError =
new ApiError(
request.getRequestURI(),
List.of(ex.getMessage()),
HttpStatus.BAD_REQUEST.value(),
LocalDateTime.now());

return new ResponseEntity<>(apiError, HttpStatus.BAD_REQUEST);
}
}
Loading

0 comments on commit 24b4720

Please sign in to comment.