Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion docs/modules/servers/partials/operate/webadmin.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5251,6 +5251,7 @@ Will return a description and statistics for channels of a user:
]
....

The same optional query parameters as `GET /servers/channels` are supported (see below).

=== Listing all channels

Expand Down Expand Up @@ -5288,4 +5289,26 @@ Will return a description and statistics for channels of all users:
]
....

Be warned that the output can be very large if a significant count of channels is opened.
Be warned that the output can be very large if a significant count of channels is opened.

The following optional query parameters are supported to sort and paginate results:

[cols="~,~,~", options="header"]
|===
| Parameter | Default | Description
| `limit` | unlimited | Maximum number of results to return.
| `offset` | `0` | Number of results to skip before returning.
| `sortBy` | none (no sort) | JSON path of the field to sort by. Supports top-level fields (`protocol`, `endpoint`, `remoteAddress`, `connectionDate`, `isActive`, `isOpen`, `isWritable`, `isEncrypted`, `username`) and nested keys inside `protocolSpecificInformation` using dot notation (e.g. `protocolSpecificInformation.cumulativeWrittenBytes`). Unknown paths are treated as an empty value without error.
| `sortDirection` | `asc` | Sort direction. Accepted values: `asc`, `desc` (case-insensitive). Only applied when `sortBy` is set.
| `sortType` | `alphabetical` | How to compare values. `alphabetical` uses natural string order; `numerical` parses values as long integers (non-numeric values sort as `0`). Only applied when `sortBy` is set.
|===

Example — list the 10 most data-hungry IMAP connections:

....
curl -XGET "/servers/channels?sortBy=protocolSpecificInformation.cumulativeWrittenBytes&sortType=numerical&sortDirection=desc&limit=10"
....

Return codes:

- 200: the list of channels, possibly empty
44 changes: 44 additions & 0 deletions server/protocols/webadmin/webadmin-protocols/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,57 @@
<description>Finner grained management for protocols</description>

<dependencies>
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>apache-james-mailbox-api</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>apache-james-mailbox-memory</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>apache-james-mailbox-memory</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>event-bus-api</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>james-server-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>james-server-data-api</artifactId>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>james-server-protocols-imap4</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>james-server-protocols-library</artifactId>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>james-server-protocols-library</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>james-server-testing</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
import static org.apache.james.DisconnectorNotifier.AllUsersRequest.ALL_USERS_REQUEST;

import java.time.Instant;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;

import jakarta.inject.Inject;

Expand Down Expand Up @@ -155,15 +157,19 @@ public void define(Service service) {
return Responses.returnNoContent(response);
});

service.get(SERVERS + "/channels", (request, response) -> OBJECT_MAPPER.writeValueAsString(connectionDescriptionSupplier.describeConnections()
.map(ConnectionDescriptionDTO::from)
.toList()));
service.get(SERVERS + "/channels", (request, response) -> {
ChannelsQueryParameters params = ChannelsQueryParameters.from(request);
return OBJECT_MAPPER.writeValueAsString(params.apply(connectionDescriptionSupplier.describeConnections()
.map(ConnectionDescriptionDTO::from))
.toList());
});

service.get(SERVERS + "/channels/:user", (request, response) -> {
Username username = Username.of(request.params("user"));
return OBJECT_MAPPER.writeValueAsString(connectionDescriptionSupplier.describeConnections()
ChannelsQueryParameters params = ChannelsQueryParameters.from(request);
return OBJECT_MAPPER.writeValueAsString(params.apply(connectionDescriptionSupplier.describeConnections()
.filter(connectionDescription -> connectionDescription.username().map(username::equals).orElse(false))
.map(ConnectionDescriptionDTO::from)
.map(ConnectionDescriptionDTO::from))
.toList());
});

Expand All @@ -174,6 +180,100 @@ public void define(Service service) {
.toList()));
}

private static class ChannelsQueryParameters {
enum SortDirection {
ASC,
DESC
}

enum SortType {
NUMERICAL,
ALPHABETICAL
}

private final Optional<Long> limit;
private final long offset;
private final Optional<String> sortBy;
private final SortDirection sortDirection;
private final SortType sortType;

private ChannelsQueryParameters(Optional<Long> limit, long offset, Optional<String> sortBy,
SortDirection sortDirection, SortType sortType) {
this.limit = limit;
this.offset = offset;
this.sortBy = sortBy;
this.sortDirection = sortDirection;
this.sortType = sortType;
}

static ChannelsQueryParameters from(Request request) {
Optional<Long> limit = Optional.ofNullable(request.queryParams("limit")).map(Long::parseUnsignedLong);
long offset = Optional.ofNullable(request.queryParams("offset")).map(Long::parseUnsignedLong).orElse(0L);
Optional<String> sortBy = Optional.ofNullable(request.queryParams("sortBy"));
SortDirection sortDirection = Optional.ofNullable(request.queryParams("sortDirection"))
.map(s -> SortDirection.valueOf(s.toUpperCase()))
.orElse(SortDirection.ASC);
SortType sortType = Optional.ofNullable(request.queryParams("sortType"))
.map(s -> SortType.valueOf(s.toUpperCase()))
.orElse(SortType.ALPHABETICAL);
return new ChannelsQueryParameters(limit, offset, sortBy, sortDirection, sortType);
}

Stream<ConnectionDescriptionDTO> apply(Stream<ConnectionDescriptionDTO> stream) {
Stream<ConnectionDescriptionDTO> result = stream;
if (sortBy.isPresent()) {
result = result.sorted(buildComparator(sortBy.get()));
}
if (offset > 0) {
result = result.skip(offset);
}
if (limit.isPresent()) {
result = result.limit(limit.get());
}
return result;
}

private Comparator<ConnectionDescriptionDTO> buildComparator(String field) {
Comparator<ConnectionDescriptionDTO> comparator;
if (sortType == SortType.NUMERICAL) {
comparator = Comparator.comparingLong(a -> extractNumeric(a, field));
} else {
comparator = Comparator.comparing(a -> extractString(a, field));
}
if (sortDirection == SortDirection.DESC) {
comparator = comparator.reversed();
}
return comparator;
}

private static long extractNumeric(ConnectionDescriptionDTO dto, String field) {
try {
return Long.parseLong(extractString(dto, field));
} catch (NumberFormatException e) {
return 0L;
}
}

private static String extractString(ConnectionDescriptionDTO dto, String field) {
if (field.startsWith("protocolSpecificInformation.")) {
String key = field.substring("protocolSpecificInformation.".length());
return Optional.ofNullable(dto.protocolSpecificInformation().get(key)).orElse("");
}
return switch (field) {
case "protocol" -> dto.protocol();
case "endpoint" -> dto.endpoint();
case "remoteAddress" -> dto.remoteAddress().orElse("");
case "connectionDate" -> dto.connectionDate().map(Instant::toString).orElse("");
case "isActive" -> String.valueOf(dto.isActive());
case "isOpen" -> String.valueOf(dto.isOpen());
case "isWritable" -> String.valueOf(dto.isWritable());
case "isEncrypted" -> String.valueOf(dto.isEncrypted());
case "username" -> dto.username().orElse("");
default -> "";
};
}
}

private Predicate<CertificateReloadable> filters(Request request) {
Optional<Port> port = Optional.ofNullable(request.queryParams("port")).map(Integer::parseUnsignedInt).map(Port::of);

Expand Down
Loading