Skip to content

Commit

Permalink
[REST] New REST APIs to generate DSL syntax for items and things
Browse files Browse the repository at this point in the history
Related to #4509

Signed-off-by: Laurent Garnier <[email protected]>
  • Loading branch information
lolodomo committed Jan 21, 2025
1 parent ce37425 commit 98c2406
Show file tree
Hide file tree
Showing 19 changed files with 1,308 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
*/
package org.openhab.core.io.rest.core.internal.discovery;

import static org.openhab.core.config.discovery.inbox.InboxPredicates.forThingUID;

import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import javax.annotation.security.RolesAllowed;
Expand All @@ -33,6 +40,11 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.auth.Role;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.config.core.ConfigDescriptionParameter;
import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultFlag;
import org.openhab.core.config.discovery.dto.DiscoveryResultDTO;
Expand All @@ -44,6 +56,10 @@
import org.openhab.core.io.rest.Stream2JSONInputStream;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingFactory;
import org.openhab.core.thing.syntaxgenerator.SyntaxGeneratorsService;
import org.openhab.core.thing.type.ThingType;
import org.openhab.core.thing.type.ThingTypeRegistry;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
Expand Down Expand Up @@ -96,10 +112,18 @@ public class InboxResource implements RESTResource {
public static final String PATH_INBOX = "inbox";

private final Inbox inbox;
private final ThingTypeRegistry thingTypeRegistry;
private final ConfigDescriptionRegistry configDescRegistry;
private final SyntaxGeneratorsService syntaxGeneratorsService;

@Activate
public InboxResource(final @Reference Inbox inbox) {
public InboxResource(final @Reference Inbox inbox, final @Reference ThingTypeRegistry thingTypeRegistry,
final @Reference ConfigDescriptionRegistry configDescRegistry,
final @Reference SyntaxGeneratorsService syntaxGeneratorsService) {
this.inbox = inbox;
this.thingTypeRegistry = thingTypeRegistry;
this.configDescRegistry = configDescRegistry;
this.syntaxGeneratorsService = syntaxGeneratorsService;
}

@POST
Expand Down Expand Up @@ -182,4 +206,60 @@ public Response unignore(@PathParam("thingUID") @Parameter(description = "thingU
inbox.setFlag(new ThingUID(thingUID), DiscoveryResultFlag.NEW);
return Response.ok(null, MediaType.TEXT_PLAIN).build();
}

@GET
@Path("/{thingUID}/filesyntax")
@Produces(MediaType.TEXT_PLAIN)
@Operation(operationId = "generateSyntaxForDiscoveryResult", summary = "Generate file syntax for the thing associated to the discovery result.", responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "400", description = "Unsupported syntax format."),
@ApiResponse(responseCode = "404", description = "Discovery result not found in the inbox or thing type not found.") })
public Response generateSyntaxForDiscoveryResult(
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
@PathParam("thingUID") @Parameter(description = "thingUID") String thingUID,
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) {
if (!syntaxGeneratorsService.isGeneratorForThingAvailable(format)) {
String message = "No syntax available for format " + format + "!";
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
}

List<DiscoveryResult> results = inbox.getAll().stream().filter(forThingUID(new ThingUID(thingUID))).toList();
if (results.isEmpty()) {
String message = "Discovery result for thing with UID " + thingUID + " not found in the inbox!";
return Response.status(Response.Status.NOT_FOUND).entity(message).build();
}
DiscoveryResult result = results.get(0);
ThingType thingType = thingTypeRegistry.getThingType(result.getThingTypeUID());
if (thingType == null) {
String message = "Thing type with UID " + result.getThingTypeUID() + " does not exist!";
return Response.status(Response.Status.NOT_FOUND).entity(message).build();
}
Configuration config = buildThingConfiguration(thingType, result.getProperties());
Thing thing = ThingFactory.createThing(thingType, result.getThingUID(), config, result.getBridgeUID(),
configDescRegistry);

return Response.ok(syntaxGeneratorsService.generateSyntaxForThings(format, Set.of(thing), false)).build();
}

private Configuration buildThingConfiguration(ThingType thingType, Map<String, Object> properties) {
Map<String, Object> configParams = new HashMap<>();
for (ConfigDescriptionParameter param : getConfigDescriptionParameters(thingType)) {
Object value = properties.get(param.getName());
if (value != null) {
configParams.put(param.getName(), ConfigUtil.normalizeType(value, param));
}
}
return new Configuration(configParams);
}

private List<ConfigDescriptionParameter> getConfigDescriptionParameters(ThingType thingType) {
URI descURI = thingType.getConfigDescriptionURI();
if (descURI != null) {
ConfigDescription desc = configDescRegistry.getConfigDescription(descURI);
if (desc != null) {
return desc.getParameters();
}
}
return List.of();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.semantics.SemanticTagRegistry;
import org.openhab.core.semantics.SemanticsPredicates;
import org.openhab.core.thing.syntaxgenerator.SyntaxGeneratorsService;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.TypeParser;
Expand Down Expand Up @@ -183,6 +184,7 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context
private final MetadataSelectorMatcher metadataSelectorMatcher;
private final SemanticTagRegistry semanticTagRegistry;
private final TimeZoneProvider timeZoneProvider;
private final SyntaxGeneratorsService syntaxGeneratorsService;

private final RegistryChangedRunnableListener<Item> resetLastModifiedItemChangeListener = new RegistryChangedRunnableListener<>(
() -> lastModified = null);
Expand All @@ -202,7 +204,8 @@ public ItemResource(//
final @Reference MetadataRegistry metadataRegistry,
final @Reference MetadataSelectorMatcher metadataSelectorMatcher,
final @Reference SemanticTagRegistry semanticTagRegistry,
final @Reference TimeZoneProvider timeZoneProvider) {
final @Reference TimeZoneProvider timeZoneProvider,
final @Reference SyntaxGeneratorsService syntaxGeneratorsService) {
this.dtoMapper = dtoMapper;
this.eventPublisher = eventPublisher;
this.itemBuilderFactory = itemBuilderFactory;
Expand All @@ -213,6 +216,7 @@ public ItemResource(//
this.metadataSelectorMatcher = metadataSelectorMatcher;
this.semanticTagRegistry = semanticTagRegistry;
this.timeZoneProvider = timeZoneProvider;
this.syntaxGeneratorsService = syntaxGeneratorsService;

this.itemRegistry.addRegistryChangeListener(resetLastModifiedItemChangeListener);
this.metadataRegistry.addRegistryChangeListener(resetLastModifiedMetadataChangeListener);
Expand Down Expand Up @@ -901,6 +905,51 @@ public Response getSemanticItem(final @Context UriInfo uriInfo, final @Context H
return JSONResponse.createResponse(Status.OK, dto, null);
}

@GET
@RolesAllowed({ Role.ADMIN })
@Path("/filesyntax")
@Produces(MediaType.TEXT_PLAIN)
@Operation(operationId = "generateSyntaxForAllItems", summary = "Generate file syntax for all items.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "400", description = "Unsupported syntax format.") })
public Response generateSyntaxForAllItems(
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) {
if (!syntaxGeneratorsService.isGeneratorForItemAvailable(format)) {
String message = "No syntax available for format " + format + "!";
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
}
return Response.ok(syntaxGeneratorsService.generateSyntaxForItems(format, itemRegistry.getAll())).build();
}

@GET
@RolesAllowed({ Role.ADMIN })
@Path("/{itemname: [a-zA-Z_0-9]+}/filesyntax")
@Produces(MediaType.TEXT_PLAIN)
@Operation(operationId = "generateSyntaxForItem", summary = "Generate file syntax for an item.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "400", description = "Unsupported syntax format."),
@ApiResponse(responseCode = "404", description = "Item not found.") })
public Response generateSyntaxForItem(
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
@PathParam("itemname") @Parameter(description = "item name") String itemname,
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) {
if (!syntaxGeneratorsService.isGeneratorForItemAvailable(format)) {
String message = "No syntax available for format " + format + "!";
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
}

Item item = getItem(itemname);
if (item == null) {
String message = "Item " + itemname + " does not exist!";
return Response.status(Response.Status.NOT_FOUND).entity(message).build();
}

return Response.ok(syntaxGeneratorsService.generateSyntaxForItems(format, Set.of(item))).build();
}

private JsonObject buildStatusObject(String itemName, String status, @Nullable String message) {
JsonObject jo = new JsonObject();
jo.addProperty("name", itemName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
import org.openhab.core.thing.i18n.ThingStatusInfoI18nLocalizationService;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.link.ManagedItemChannelLinkProvider;
import org.openhab.core.thing.syntaxgenerator.SyntaxGeneratorsService;
import org.openhab.core.thing.type.BridgeType;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeRegistry;
Expand Down Expand Up @@ -171,6 +172,7 @@ public class ThingResource implements RESTResource {
private final ThingTypeRegistry thingTypeRegistry;
private final RegistryChangedRunnableListener<Thing> resetLastModifiedChangeListener = new RegistryChangedRunnableListener<>(
() -> lastModified = null);
private final SyntaxGeneratorsService syntaxGeneratorsService;

private @Context @NonNullByDefault({}) UriInfo uriInfo;
private @Nullable Date lastModified = null;
Expand All @@ -192,7 +194,8 @@ public ThingResource( //
final @Reference ThingManager thingManager, //
final @Reference ThingRegistry thingRegistry,
final @Reference ThingStatusInfoI18nLocalizationService thingStatusInfoI18nLocalizationService,
final @Reference ThingTypeRegistry thingTypeRegistry) {
final @Reference ThingTypeRegistry thingTypeRegistry,
final @Reference SyntaxGeneratorsService syntaxGeneratorsService) {
this.dtoMapper = dtoMapper;
this.channelTypeRegistry = channelTypeRegistry;
this.configStatusService = configStatusService;
Expand All @@ -206,6 +209,7 @@ public ThingResource( //
this.thingRegistry = thingRegistry;
this.thingStatusInfoI18nLocalizationService = thingStatusInfoI18nLocalizationService;
this.thingTypeRegistry = thingTypeRegistry;
this.syntaxGeneratorsService = syntaxGeneratorsService;

this.thingRegistry.addRegistryChangeListener(resetLastModifiedChangeListener);
}
Expand Down Expand Up @@ -720,6 +724,54 @@ public Response getFirmwares(@PathParam("thingUID") @Parameter(description = "th
return Response.ok().entity(new Stream2JSONInputStream(firmwareStream)).build();
}

@GET
@RolesAllowed({ Role.ADMIN })
@Path("/filesyntax")
@Produces(MediaType.TEXT_PLAIN)
@Operation(operationId = "generateSyntaxForAllThings", summary = "Generate file syntax for all things.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "400", description = "Unsupported syntax format.") })
public Response generateSyntaxForAllThings(
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format,
@DefaultValue("false") @QueryParam("preferPresentationAsTree") @Parameter(description = "prefer a presentation as a tree if supported by the generator") boolean preferPresentationAsTree) {
if (!syntaxGeneratorsService.isGeneratorForThingAvailable(format)) {
String message = "No syntax available for format " + format + "!";
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
}
return Response.ok(syntaxGeneratorsService.generateSyntaxForThings(format, thingRegistry.getAll(),
preferPresentationAsTree)).build();
}

@GET
@RolesAllowed({ Role.ADMIN })
@Path("/{thingUID}/filesyntax")
@Produces(MediaType.TEXT_PLAIN)
@Operation(operationId = "generateSyntaxForThing", summary = "Generate file syntax for a thing.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "400", description = "Unsupported syntax format."),
@ApiResponse(responseCode = "404", description = "Thing not found.") })
public Response generateSyntaxForThing(
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
@PathParam("thingUID") @Parameter(description = "thingUID") String thingUID,
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) {
if (!syntaxGeneratorsService.isGeneratorForThingAvailable(format)) {
String message = "No syntax available for format " + format + "!";
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
}

ThingUID aThingUID = new ThingUID(thingUID);
Thing thing = thingRegistry.get(aThingUID);
if (thing == null) {
String message = "Thing " + thingUID + " does not exist!";
return Response.status(Response.Status.NOT_FOUND).entity(message).build();
}

return Response.ok(syntaxGeneratorsService.generateSyntaxForThings(format, Set.of(thing), false)).build();
}

private FirmwareDTO convertToFirmwareDTO(Firmware firmware) {
return new FirmwareDTO(firmware.getThingTypeUID().getAsString(), firmware.getVendor(), firmware.getModel(),
firmware.isModelRestricted(), firmware.getDescription(), firmware.getVersion(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,13 @@ public interface ModelRepository {
* @param listener the listener to remove
*/
void removeModelRepositoryChangeListener(ModelRepositoryChangeListener listener);

/**
* Generate the syntax from a provided model content.
*
* @param extension the kind of model ("items", "things", ...)
* @param content the content of the model
* @return the corresponding syntax
*/
String generateSyntaxFromModelContent(String extension, EObject content);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package org.openhab.core.model.core.internal;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -227,6 +228,23 @@ public void removeModelRepositoryChangeListener(ModelRepositoryChangeListener li
listeners.remove(listener);
}

@Override
public String generateSyntaxFromModelContent(String extension, EObject content) {
String result = "";
Resource resource = resourceSet.createResource(URI.createURI("tmp_generated_syntax." + extension));
try {
resource.getContents().add(content);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
resource.save(outputStream, Map.of(XtextResource.OPTION_ENCODING, StandardCharsets.UTF_8.name()));
result = new String(outputStream.toByteArray());
} catch (IOException e) {
logger.warn("Exception when saving the model {}", resource.getURI().lastSegment());
} finally {
resourceSet.getResources().remove(resource);
}
return result;
}

private @Nullable Resource getResource(String name) {
return resourceSet.getResource(URI.createURI(name), false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,24 @@ org.openhab.core.model

import com.google.inject.Binder
import com.google.inject.name.Names
import org.openhab.core.model.formatting.ItemsFormatter
import org.openhab.core.model.internal.valueconverter.ItemValueConverters
import org.eclipse.xtext.conversion.IValueConverterService
import org.eclipse.xtext.formatting.IFormatter
import org.eclipse.xtext.linking.lazy.LazyURIEncoder

/**
* Use this class to register components to be used at runtime / without the Equinox extension registry.
*/
class ItemsRuntimeModule extends AbstractItemsRuntimeModule {

override Class<? extends IValueConverterService> bindIValueConverterService() {
return ItemValueConverters
}


override Class<? extends IFormatter> bindIFormatter() {
return ItemsFormatter
}

override void configureUseIndexFragmentsForLazyLinking(Binder binder) {
binder.bind(Boolean.TYPE).annotatedWith(Names.named(LazyURIEncoder.USE_INDEXED_FRAGMENTS_BINDING)).toInstance(
Boolean.FALSE)
Expand Down
Loading

0 comments on commit 98c2406

Please sign in to comment.