Skip to content

Commit

Permalink
[#3517] Support filtering for gateway or edge devices only
Browse files Browse the repository at this point in the history
The MongoDB based registry has been extended to support the isGateway
query parameter of the Registry Management API's search devices
operation.
  • Loading branch information
sophokles73 committed Jan 15, 2024
1 parent 94ba665 commit 70e77fa
Show file tree
Hide file tree
Showing 16 changed files with 515 additions and 143 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2020, 2023 Contributors to the Eclipse Foundation
* Copyright (c) 2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
Expand Down Expand Up @@ -225,8 +225,8 @@ protected Future<Result<Void>> processDeleteDevicesOfTenant(final String tenantI
* retrieve the whole result set page by page.
* @param filters A list of filters. The filters are predicates that objects in the result set must match.
* @param sortOptions A list of sort options. The sortOptions specify properties to sort the result set by.
* @param isGateway Optional filter for searching only gateways or only devices.
* If given parameter is Optional.empty() result will contain both gateways and devices.
* @param isGateway A filter for restricting the search to gateway ({@code True}) or edge ({@code False} devices only.
* If <em>empty</em>, the search will not be restricted.
* @param span The active OpenTracing span to use for tracking this operation.
* <p>
* Implementations <em>must not</em> invoke the {@link Span#finish()} nor the {@link Span#finish(long)}
Expand Down Expand Up @@ -417,6 +417,7 @@ public final Future<OperationResult<SearchResult<DeviceWithId>>> searchDevices(
Objects.requireNonNull(tenantId);
Objects.requireNonNull(filters);
Objects.requireNonNull(sortOptions);
Objects.requireNonNull(isGateway);
Objects.requireNonNull(span);

if (pageSize <= 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2020, 2022 Contributors to the Eclipse Foundation
* Copyright (c) 2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
Expand All @@ -21,6 +21,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;

import io.quarkus.runtime.annotations.RegisterForReflection;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.pointer.JsonPointer;

/**
Expand All @@ -37,27 +38,59 @@ public final class Filter {
private Operator operator = Operator.eq;

/**
* An enum defining supported filter operators.
* Supported filter operators.
*/
public enum Operator {
eq
eq,
in,
not_in
}

private Filter(final String field, final Object value, final Operator op) {
Objects.requireNonNull(field);
Objects.requireNonNull(value);
this.field = JsonPointer.from(field);
this.value = value;
this.operator = Optional.ofNullable(op).orElse(Operator.eq);
}

/**
* Creates a filter for a field and value using the equals operator.
* Creates a filter for a field and value using the <em>equals</em> operator.
*
* @param field The field to use for filtering.
* @param field A JSON Pointer to the field to use for filtering.
* @param value The value corresponding to the field to use for filtering.
* @throws IllegalArgumentException if the field is not a valid pointer.
* @throws IllegalArgumentException if the field is not a valid JSON pointer.
* @throws NullPointerException if any of the parameters is {@code null}.
*/
public Filter(@JsonProperty(value = RegistryManagementConstants.FIELD_FILTER_FIELD, required = true) final String field,
@JsonProperty(value = RegistryManagementConstants.FIELD_FILTER_VALUE, required = true) final Object value) {
Objects.requireNonNull(field);
Objects.requireNonNull(value);
this(field, value, Operator.eq);
}

this.field = JsonPointer.from(field);
this.value = value;
/**
* Creates a filter for a field and value list using the <em>in</em> operator.
*
* @param field A JSON Pointer to the field to use for filtering.
* @param valueList The list of values to match.
* @return The filter.
* @throws IllegalArgumentException if the field is not a valid pointer.
* @throws NullPointerException if any of the parameters are {@code null}.
*/
public static Filter inFilter(final String field, final JsonArray valueList) {
return new Filter(field, valueList, Operator.in);
}

/**
* Creates a filter for a field and value list using the <em>not in</em> operator.
*
* @param field A JSON Pointer to the field to use for filtering.
* @param valueList The list of values to match.
* @return The filter.
* @throws IllegalArgumentException if the field is not a valid pointer.
* @throws NullPointerException if any of the parameters are {@code null}.
*/
public static Filter notInFilter(final String field, final JsonArray valueList) {
return new Filter(field, valueList, Operator.not_in);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2016, 2023 Contributors to the Eclipse Foundation
* Copyright (c) 2016 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
Expand Down Expand Up @@ -102,14 +102,14 @@ public interface DeviceManagementService {
* Implementations <em>must not</em> invoke the {@link Span#finish()} nor the {@link Span#finish(long)}
* methods. However,implementations may log (error) events on this span, set tags and use this span
* as the parent for additional spans created as part of this method's execution.
* @param isGateway Optional filter for searching only gateways or only devices.
* If given parameter is Optional.empty() result will contain both gateways and devices.
* @param isGateway A filter for restricting the search to gateway ({@code True}) or edge ({@code False} devices only.
* If <em>empty</em>, the search will not be restricted.
* @return A future indicating the outcome of the operation.
* <p>
* The future will be succeeded with a result containing the matching devices. Otherwise, the future will
* be failed with a {@link org.eclipse.hono.client.ServiceInvocationException} containing an error code
* as specified in the Device Registry Management API.
* @throws NullPointerException if any of filters, sort options or tracing span are {@code null}.
* @throws NullPointerException if any of filters, sort options, gateway filter or tracing span are {@code null}.
* @throws IllegalArgumentException if page size is &lt;= 0 or page offset is &lt; 0.
* @see <a href="https://www.eclipse.org/hono/docs/api/management/#/devices/searchDevicesForTenant"> Device Registry
* Management API - Search Devices</a>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, 2022 Contributors to the Eclipse Foundation
* Copyright (c) 2021 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
Expand All @@ -13,6 +13,8 @@

package org.eclipse.hono.deviceregistry.service.device;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
Expand All @@ -23,6 +25,7 @@
import static com.google.common.truth.Truth.assertThat;

import java.net.HttpURLConnection;
import java.util.List;
import java.util.Optional;

import org.eclipse.hono.notification.NotificationEventBusSupport;
Expand Down Expand Up @@ -67,6 +70,31 @@ void setUp() {
deviceManagementService = new TestDeviceManagementService(vertx);
}

/**
* Verifies that the search devices operation verifies that all required parameters
* are non {@code null}.
*/
@Test
public void testSearchDevicesRejectsNullParameters() {
assertAll(
() -> assertThrows(
NullPointerException.class,
() -> deviceManagementService.searchDevices(
DEFAULT_TENANT_ID, 10, 0, null, List.of(), Optional.empty(), SPAN)
),
() -> assertThrows(
NullPointerException.class,
() -> deviceManagementService.searchDevices(
DEFAULT_TENANT_ID, 10, 0, List.of(), null, Optional.empty(), SPAN)
),
() -> assertThrows(
NullPointerException.class,
() -> deviceManagementService.searchDevices(
DEFAULT_TENANT_ID, 10, 0, List.of(), List.of(), null, SPAN)
)
);
}

/**
* Verifies that {@link AbstractDeviceManagementService#createDevice(String, Optional, Device, Span)} publishes the
* expected notification.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import org.eclipse.hono.service.management.SearchResult;
import org.eclipse.hono.service.management.Sort;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import io.opentracing.noop.NoopSpan;
import io.vertx.core.Future;
Expand Down Expand Up @@ -108,6 +110,53 @@ default void testSearchDevicesWithAFilterSucceeds(final VertxTestContext ctx) {
}));
}

/**
* Verifies that a request to search gateway devices succeeds and matching devices are found.
*
* @param isGateway {@code true} if only gateways should be searched for.
* @param expectedFirstDeviceId The identifier that the first device in the result set is expected to have.
* @param expectedSecondDeviceId The identifier that the second device in the result set is expected to have.
* @param ctx The vert.x test context.
*/
@ParameterizedTest
@CsvSource(value = { "true, testDevice2, testDevice3", "false, testDevice0, testDevice1" })
default void testSearchGatewayDevicesWithFilterSucceeds(
final boolean isGateway,
final String expectedFirstDeviceId,
final String expectedSecondDeviceId,
final VertxTestContext ctx) {
final String tenantId = DeviceRegistryUtils.getUniqueIdentifier();
final int pageSize = 10;
final int pageOffset = 0;
final var filter = List.of(new Filter("/enabled", false));
final var sortOptions = List.of(new Sort("/id"));

createDevices(tenantId, Map.of(
"testDevice0", new Device().setEnabled(false).setMemberOf(List.of()),
"testDevice1", new Device().setEnabled(false).setVia(List.of("testDevice2")),
"testDevice2", new Device().setEnabled(false),
"testDevice3", new Device().setEnabled(false).setMemberOf(List.of("gwGroup"))))
.compose(ok -> getDeviceManagementService().searchDevices(
tenantId,
pageSize,
pageOffset,
filter,
sortOptions,
Optional.of(isGateway),
NoopSpan.INSTANCE))
.onComplete(ctx.succeeding(s -> {
ctx.verify(() -> {
assertThat(s.getStatus()).isEqualTo(HttpURLConnection.HTTP_OK);

final SearchResult<DeviceWithId> searchResult = s.getPayload();
assertThat(searchResult.getTotal()).isEqualTo(2);
assertThat(searchResult.getResult().get(0).getId()).isEqualTo(expectedFirstDeviceId);
assertThat(searchResult.getResult().get(1).getId()).isEqualTo(expectedSecondDeviceId);
});
ctx.completeNow();
}));
}

/**
* Verifies that a request to search devices with multiple filters succeeds and matching devices are found.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2020, 2023 Contributors to the Eclipse Foundation
* Copyright (c) 2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
Expand Down Expand Up @@ -54,7 +54,10 @@
import org.eclipse.hono.service.tenant.TenantService;
import org.h2.tools.RunScript;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
Expand All @@ -75,6 +78,7 @@ enum DatabaseType {
}
protected static final Span SPAN = NoopSpan.INSTANCE;

private static final Logger LOG = LoggerFactory.getLogger(AbstractJdbcRegistryTest.class);
private static final DatabaseType DEFAULT_DATABASE_TYPE = DatabaseType.H2;
private static final DatabaseType DATABASE_TYPE = DatabaseType.valueOf(System.getProperty(AbstractJdbcRegistryTest.class.getSimpleName()
+ ".databaseType", DEFAULT_DATABASE_TYPE.name()).toUpperCase());
Expand All @@ -99,6 +103,16 @@ enum DatabaseType {
protected DeviceServiceOptions properties;
protected TenantInformationService tenantInformationService;

/**
* Prints the test name.
*
* @param testInfo Test case meta information.
*/
@BeforeEach
public void setup(final TestInfo testInfo) {
LOG.info("running {}", testInfo.getDisplayName());
}

@BeforeEach
void startDevices(final Vertx vertx) throws IOException, SQLException {
final var jdbc = resolveJdbcProperties();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,53 +337,4 @@ void testSearchDevicesWithStringOneCharWildcardsFilter(final VertxTestContext ct
ctx.completeNow();
}));
}


@Test
void testSearchAllGateways(final VertxTestContext ctx) {
final String tenantId = DeviceRegistryUtils.getUniqueIdentifier();
final int pageSize = 10;
final int pageOffset = 0;

createDevices(tenantId, Map.of(
"testDevice1", new Device().setVia(List.of("testDevice2")),
"testDevice2", new Device(),
"testDevice3", new Device().setVia(List.of("testDevice2"))))
.compose(ok -> getDeviceManagementService()
.searchDevices(tenantId, pageSize, pageOffset, List.of(), List.of(), Optional.of(true), NoopSpan.INSTANCE))
.onComplete(ctx.succeeding(s -> {
ctx.verify(() -> {
assertThat(s.getStatus()).isEqualTo(HttpURLConnection.HTTP_OK);
assertThat(s.getPayload().getTotal()).isEqualTo(1);
assertThat(s.getPayload().getResult()).hasSize(1);
assertThat(s.getPayload().getResult().get(0).getId()).isEqualTo("testDevice2");
});
ctx.completeNow();
}));
}


@Test
void testSearchOnlyDevices(final VertxTestContext ctx) {
final String tenantId = DeviceRegistryUtils.getUniqueIdentifier();
final int pageSize = 10;
final int pageOffset = 0;

createDevices(tenantId, Map.of(
"testDevice1", new Device().setVia(List.of("testDevice2")),
"testDevice2", new Device(),
"testDevice3", new Device().setVia(List.of("testDevice2"))))
.compose(ok -> getDeviceManagementService()
.searchDevices(tenantId, pageSize, pageOffset, List.of(), List.of(), Optional.of(false), NoopSpan.INSTANCE))
.onComplete(ctx.succeeding(s -> {
ctx.verify(() -> {
assertThat(s.getStatus()).isEqualTo(HttpURLConnection.HTTP_OK);
assertThat(s.getPayload().getTotal()).isEqualTo(2);
assertThat(s.getPayload().getResult()).hasSize(2);
assertThat(s.getPayload().getResult().get(0).getId()).isEqualTo("testDevice1");
assertThat(s.getPayload().getResult().get(1).getId()).isEqualTo("testDevice3");
});
ctx.completeNow();
}));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ public interface DeviceDao {
* retrieve the whole result set page by page.
* @param filters A list of filters. The filters are predicates that objects in the result set must match.
* @param sortOptions A list of sort options. The sortOptions specify properties to sort the result set by.
* @param isGateway A filter for restricting the search to gateway ({@code True}) or edge ({@code False} devices only.
* If <em>empty</em>, the search will not be restricted.
* @param tracingContext The context to track the processing of the request in
* or {@code null} if no such context exists.
* @return A future indicating the outcome of the operation.
Expand All @@ -105,6 +107,7 @@ Future<SearchResult<DeviceWithId>> find(
int pageOffset,
List<Filter> filters,
List<Sort> sortOptions,
Optional<Boolean> isGateway,
SpanContext tracingContext);

/**
Expand Down
Loading

0 comments on commit 70e77fa

Please sign in to comment.