[rest] Add summary option to rules, things, UI components resources (#1827)

The /things, /rules, /ui/components endpoints retrieve all objects
in their entirety, which can become very big, i.e. channels, config
parameters, script rule modules or trees of UI components can
quickly add up to the size.

When the UI simply displays a list of those objects it retrieves all
this extra information but does nothing with it.

This introduces an optional ?summary=true query parameter for the
above resources to limit the output to pre-defined fields which are
deemed most relevant for displaying these lists, omitting the rest.

When the option is not set, the behavior remains unchanged so this
change is not API breaking. The API version has therefore not been
incremented. The client is responsible for adding the option to
retrieve summarized collections instead of the entire objects.

Signed-off-by: Yannick Schaus <github@schaus.net>
This commit is contained in:
Yannick Schaus 2020-11-21 18:40:48 +01:00 committed by GitHub
parent 5330de0473
commit 065177b730
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 48 additions and 9 deletions

View File

@ -21,7 +21,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.Consumes;
@ -65,9 +65,11 @@ import org.openhab.core.automation.util.ModuleBuilder;
import org.openhab.core.automation.util.RuleBuilder;
import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.rest.DTOMapper;
import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
import org.openhab.core.io.rest.Stream2JSONInputStream;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@ -114,6 +116,7 @@ public class RuleResource implements RESTResource {
private final Logger logger = LoggerFactory.getLogger(RuleResource.class);
private final DTOMapper dtoMapper;
private final RuleManager ruleManager;
private final RuleRegistry ruleRegistry;
private final ManagedRuleProvider managedRuleProvider;
@ -122,9 +125,11 @@ public class RuleResource implements RESTResource {
@Activate
public RuleResource( //
final @Reference DTOMapper dtoMapper, //
final @Reference RuleManager ruleManager, //
final @Reference RuleRegistry ruleRegistry, //
final @Reference ManagedRuleProvider managedRuleProvider) {
this.dtoMapper = dtoMapper;
this.ruleManager = ruleManager;
this.ruleRegistry = ruleRegistry;
this.managedRuleProvider = managedRuleProvider;
@ -135,7 +140,8 @@ public class RuleResource implements RESTResource {
@Operation(summary = "Get available rules, optionally filtered by tags and/or prefix.", responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedRuleDTO.class)))) })
public Response get(@QueryParam("prefix") final @Nullable String prefix,
@QueryParam("tags") final @Nullable List<String> tags) {
@QueryParam("tags") final @Nullable List<String> tags,
@QueryParam("summary") @Parameter(description = "summary fields only") @Nullable Boolean summary) {
// match all
Predicate<Rule> p = r -> true;
@ -149,10 +155,13 @@ public class RuleResource implements RESTResource {
// if tags is null or empty list returns all rules
p = p.and(hasAllTags(tags));
final Collection<EnrichedRuleDTO> rules = ruleRegistry.stream().filter(p) // filter according to Predicates
.map(rule -> EnrichedRuleDTOMapper.map(rule, ruleManager, managedRuleProvider)) // map matching rules
.collect(Collectors.toList());
return Response.ok(rules).build();
Stream<EnrichedRuleDTO> rules = ruleRegistry.stream().filter(p) // filter according to Predicates
.map(rule -> EnrichedRuleDTOMapper.map(rule, ruleManager, managedRuleProvider)); // map matching rules
if (summary != null && summary == true) {
rules = dtoMapper.limitToFields(rules, "uid,templateUID,name,visibility,description,status,tags,editable");
}
return Response.ok(new Stream2JSONInputStream(rules)).build();
}
@POST

View File

@ -56,6 +56,7 @@ import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.core.status.ConfigStatusInfo;
import org.openhab.core.config.core.status.ConfigStatusService;
import org.openhab.core.config.core.validation.ConfigValidationException;
import org.openhab.core.io.rest.DTOMapper;
import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.RESTConstants;
@ -148,6 +149,7 @@ public class ThingResource implements RESTResource {
/** The URI path to this resource */
public static final String PATH_THINGS = "things";
private final DTOMapper dtoMapper;
private final ChannelTypeRegistry channelTypeRegistry;
private final ConfigStatusService configStatusService;
private final ConfigDescriptionRegistry configDescRegistry;
@ -169,7 +171,7 @@ public class ThingResource implements RESTResource {
@Activate
public ThingResource( //
final @Reference ChannelTypeRegistry channelTypeRegistry,
final @Reference DTOMapper dtoMapper, final @Reference ChannelTypeRegistry channelTypeRegistry,
final @Reference ConfigStatusService configStatusService,
final @Reference ConfigDescriptionRegistry configDescRegistry,
final @Reference FirmwareRegistry firmwareRegistry,
@ -185,6 +187,7 @@ public class ThingResource implements RESTResource {
final @Reference ThingRegistry thingRegistry,
final @Reference ThingStatusInfoI18nLocalizationService thingStatusInfoI18nLocalizationService,
final @Reference ThingTypeRegistry thingTypeRegistry) {
this.dtoMapper = dtoMapper;
this.channelTypeRegistry = channelTypeRegistry;
this.configStatusService = configStatusService;
this.configDescRegistry = configDescRegistry;
@ -294,11 +297,16 @@ public class ThingResource implements RESTResource {
@Operation(summary = "Get all available things.", responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedThingDTO.class), uniqueItems = true))) })
public Response getAll(
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language) {
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
@QueryParam("summary") @Parameter(description = "summary fields only") @Nullable Boolean summary) {
final Locale locale = localeService.getLocale(language);
Stream<EnrichedThingDTO> thingStream = thingRegistry.stream().map(t -> convertToEnrichedThingDTO(t, locale))
.distinct();
if (summary != null && summary == true) {
thingStream = dtoMapper.limitToFields(thingStream,
"UID,label,bridgeUID,thingTypeUID,statusInfo,location,editable");
}
return Response.ok(new Stream2JSONInputStream(thingStream)).build();
}

View File

@ -13,6 +13,8 @@
package org.openhab.core.io.rest.ui.internal;
import java.security.InvalidParameterException;
import java.util.Date;
import java.util.Set;
import java.util.stream.Stream;
import javax.annotation.security.RolesAllowed;
@ -23,11 +25,13 @@ import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.auth.Role;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
@ -48,6 +52,7 @@ import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsName;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
@ -101,9 +106,26 @@ public class UIResource implements RESTResource {
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Get all registered UI components in the specified namespace.", responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = RootUIComponent.class)))) })
public Response getAllComponents(@PathParam("namespace") String namespace) {
public Response getAllComponents(@PathParam("namespace") String namespace,
@QueryParam("summary") @Parameter(description = "summary fields only") @Nullable Boolean summary) {
UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace);
Stream<RootUIComponent> components = registry.getAll().stream();
if (summary != null && summary == true) {
components = components.map(c -> {
RootUIComponent component = new RootUIComponent(c.getUID(), c.getType());
@Nullable
Set<String> tags = c.getTags();
if (tags != null) {
component.addTags(c.getTags());
}
@Nullable
Date timestamp = c.getTimestamp();
if (timestamp != null) {
component.setTimestamp(timestamp);
}
return component;
});
}
return Response.ok(new Stream2JSONInputStream(components)).build();
}