Skip to content

Commit 98c2406

Browse files
committed
[REST] New REST APIs to generate DSL syntax for items and things
Related to openhab#4509 Signed-off-by: Laurent Garnier <[email protected]>
1 parent ce37425 commit 98c2406

File tree

19 files changed

+1308
-16
lines changed

19 files changed

+1308
-16
lines changed

bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/discovery/InboxResource.java

+81-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
*/
1313
package org.openhab.core.io.rest.core.internal.discovery;
1414

15+
import static org.openhab.core.config.discovery.inbox.InboxPredicates.forThingUID;
16+
17+
import java.net.URI;
18+
import java.util.HashMap;
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.Set;
1522
import java.util.stream.Stream;
1623

1724
import javax.annotation.security.RolesAllowed;
@@ -33,6 +40,11 @@
3340
import org.eclipse.jdt.annotation.NonNullByDefault;
3441
import org.eclipse.jdt.annotation.Nullable;
3542
import org.openhab.core.auth.Role;
43+
import org.openhab.core.config.core.ConfigDescription;
44+
import org.openhab.core.config.core.ConfigDescriptionParameter;
45+
import org.openhab.core.config.core.ConfigDescriptionRegistry;
46+
import org.openhab.core.config.core.ConfigUtil;
47+
import org.openhab.core.config.core.Configuration;
3648
import org.openhab.core.config.discovery.DiscoveryResult;
3749
import org.openhab.core.config.discovery.DiscoveryResultFlag;
3850
import org.openhab.core.config.discovery.dto.DiscoveryResultDTO;
@@ -44,6 +56,10 @@
4456
import org.openhab.core.io.rest.Stream2JSONInputStream;
4557
import org.openhab.core.thing.Thing;
4658
import org.openhab.core.thing.ThingUID;
59+
import org.openhab.core.thing.binding.ThingFactory;
60+
import org.openhab.core.thing.syntaxgenerator.SyntaxGeneratorsService;
61+
import org.openhab.core.thing.type.ThingType;
62+
import org.openhab.core.thing.type.ThingTypeRegistry;
4763
import org.osgi.service.component.annotations.Activate;
4864
import org.osgi.service.component.annotations.Component;
4965
import org.osgi.service.component.annotations.Reference;
@@ -96,10 +112,18 @@ public class InboxResource implements RESTResource {
96112
public static final String PATH_INBOX = "inbox";
97113

98114
private final Inbox inbox;
115+
private final ThingTypeRegistry thingTypeRegistry;
116+
private final ConfigDescriptionRegistry configDescRegistry;
117+
private final SyntaxGeneratorsService syntaxGeneratorsService;
99118

100119
@Activate
101-
public InboxResource(final @Reference Inbox inbox) {
120+
public InboxResource(final @Reference Inbox inbox, final @Reference ThingTypeRegistry thingTypeRegistry,
121+
final @Reference ConfigDescriptionRegistry configDescRegistry,
122+
final @Reference SyntaxGeneratorsService syntaxGeneratorsService) {
102123
this.inbox = inbox;
124+
this.thingTypeRegistry = thingTypeRegistry;
125+
this.configDescRegistry = configDescRegistry;
126+
this.syntaxGeneratorsService = syntaxGeneratorsService;
103127
}
104128

105129
@POST
@@ -182,4 +206,60 @@ public Response unignore(@PathParam("thingUID") @Parameter(description = "thingU
182206
inbox.setFlag(new ThingUID(thingUID), DiscoveryResultFlag.NEW);
183207
return Response.ok(null, MediaType.TEXT_PLAIN).build();
184208
}
209+
210+
@GET
211+
@Path("/{thingUID}/filesyntax")
212+
@Produces(MediaType.TEXT_PLAIN)
213+
@Operation(operationId = "generateSyntaxForDiscoveryResult", summary = "Generate file syntax for the thing associated to the discovery result.", responses = {
214+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
215+
@ApiResponse(responseCode = "400", description = "Unsupported syntax format."),
216+
@ApiResponse(responseCode = "404", description = "Discovery result not found in the inbox or thing type not found.") })
217+
public Response generateSyntaxForDiscoveryResult(
218+
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
219+
@PathParam("thingUID") @Parameter(description = "thingUID") String thingUID,
220+
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) {
221+
if (!syntaxGeneratorsService.isGeneratorForThingAvailable(format)) {
222+
String message = "No syntax available for format " + format + "!";
223+
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
224+
}
225+
226+
List<DiscoveryResult> results = inbox.getAll().stream().filter(forThingUID(new ThingUID(thingUID))).toList();
227+
if (results.isEmpty()) {
228+
String message = "Discovery result for thing with UID " + thingUID + " not found in the inbox!";
229+
return Response.status(Response.Status.NOT_FOUND).entity(message).build();
230+
}
231+
DiscoveryResult result = results.get(0);
232+
ThingType thingType = thingTypeRegistry.getThingType(result.getThingTypeUID());
233+
if (thingType == null) {
234+
String message = "Thing type with UID " + result.getThingTypeUID() + " does not exist!";
235+
return Response.status(Response.Status.NOT_FOUND).entity(message).build();
236+
}
237+
Configuration config = buildThingConfiguration(thingType, result.getProperties());
238+
Thing thing = ThingFactory.createThing(thingType, result.getThingUID(), config, result.getBridgeUID(),
239+
configDescRegistry);
240+
241+
return Response.ok(syntaxGeneratorsService.generateSyntaxForThings(format, Set.of(thing), false)).build();
242+
}
243+
244+
private Configuration buildThingConfiguration(ThingType thingType, Map<String, Object> properties) {
245+
Map<String, Object> configParams = new HashMap<>();
246+
for (ConfigDescriptionParameter param : getConfigDescriptionParameters(thingType)) {
247+
Object value = properties.get(param.getName());
248+
if (value != null) {
249+
configParams.put(param.getName(), ConfigUtil.normalizeType(value, param));
250+
}
251+
}
252+
return new Configuration(configParams);
253+
}
254+
255+
private List<ConfigDescriptionParameter> getConfigDescriptionParameters(ThingType thingType) {
256+
URI descURI = thingType.getConfigDescriptionURI();
257+
if (descURI != null) {
258+
ConfigDescription desc = configDescRegistry.getConfigDescription(descURI);
259+
if (desc != null) {
260+
return desc.getParameters();
261+
}
262+
}
263+
return List.of();
264+
}
185265
}

bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java

+50-1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
import org.openhab.core.library.types.UpDownType;
9090
import org.openhab.core.semantics.SemanticTagRegistry;
9191
import org.openhab.core.semantics.SemanticsPredicates;
92+
import org.openhab.core.thing.syntaxgenerator.SyntaxGeneratorsService;
9293
import org.openhab.core.types.Command;
9394
import org.openhab.core.types.State;
9495
import org.openhab.core.types.TypeParser;
@@ -183,6 +184,7 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context
183184
private final MetadataSelectorMatcher metadataSelectorMatcher;
184185
private final SemanticTagRegistry semanticTagRegistry;
185186
private final TimeZoneProvider timeZoneProvider;
187+
private final SyntaxGeneratorsService syntaxGeneratorsService;
186188

187189
private final RegistryChangedRunnableListener<Item> resetLastModifiedItemChangeListener = new RegistryChangedRunnableListener<>(
188190
() -> lastModified = null);
@@ -202,7 +204,8 @@ public ItemResource(//
202204
final @Reference MetadataRegistry metadataRegistry,
203205
final @Reference MetadataSelectorMatcher metadataSelectorMatcher,
204206
final @Reference SemanticTagRegistry semanticTagRegistry,
205-
final @Reference TimeZoneProvider timeZoneProvider) {
207+
final @Reference TimeZoneProvider timeZoneProvider,
208+
final @Reference SyntaxGeneratorsService syntaxGeneratorsService) {
206209
this.dtoMapper = dtoMapper;
207210
this.eventPublisher = eventPublisher;
208211
this.itemBuilderFactory = itemBuilderFactory;
@@ -213,6 +216,7 @@ public ItemResource(//
213216
this.metadataSelectorMatcher = metadataSelectorMatcher;
214217
this.semanticTagRegistry = semanticTagRegistry;
215218
this.timeZoneProvider = timeZoneProvider;
219+
this.syntaxGeneratorsService = syntaxGeneratorsService;
216220

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

908+
@GET
909+
@RolesAllowed({ Role.ADMIN })
910+
@Path("/filesyntax")
911+
@Produces(MediaType.TEXT_PLAIN)
912+
@Operation(operationId = "generateSyntaxForAllItems", summary = "Generate file syntax for all items.", security = {
913+
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
914+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
915+
@ApiResponse(responseCode = "400", description = "Unsupported syntax format.") })
916+
public Response generateSyntaxForAllItems(
917+
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
918+
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) {
919+
if (!syntaxGeneratorsService.isGeneratorForItemAvailable(format)) {
920+
String message = "No syntax available for format " + format + "!";
921+
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
922+
}
923+
return Response.ok(syntaxGeneratorsService.generateSyntaxForItems(format, itemRegistry.getAll())).build();
924+
}
925+
926+
@GET
927+
@RolesAllowed({ Role.ADMIN })
928+
@Path("/{itemname: [a-zA-Z_0-9]+}/filesyntax")
929+
@Produces(MediaType.TEXT_PLAIN)
930+
@Operation(operationId = "generateSyntaxForItem", summary = "Generate file syntax for an item.", security = {
931+
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
932+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
933+
@ApiResponse(responseCode = "400", description = "Unsupported syntax format."),
934+
@ApiResponse(responseCode = "404", description = "Item not found.") })
935+
public Response generateSyntaxForItem(
936+
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
937+
@PathParam("itemname") @Parameter(description = "item name") String itemname,
938+
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) {
939+
if (!syntaxGeneratorsService.isGeneratorForItemAvailable(format)) {
940+
String message = "No syntax available for format " + format + "!";
941+
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
942+
}
943+
944+
Item item = getItem(itemname);
945+
if (item == null) {
946+
String message = "Item " + itemname + " does not exist!";
947+
return Response.status(Response.Status.NOT_FOUND).entity(message).build();
948+
}
949+
950+
return Response.ok(syntaxGeneratorsService.generateSyntaxForItems(format, Set.of(item))).build();
951+
}
952+
904953
private JsonObject buildStatusObject(String itemName, String status, @Nullable String message) {
905954
JsonObject jo = new JsonObject();
906955
jo.addProperty("name", itemName);

bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/thing/ThingResource.java

+53-1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
import org.openhab.core.thing.i18n.ThingStatusInfoI18nLocalizationService;
9696
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
9797
import org.openhab.core.thing.link.ManagedItemChannelLinkProvider;
98+
import org.openhab.core.thing.syntaxgenerator.SyntaxGeneratorsService;
9899
import org.openhab.core.thing.type.BridgeType;
99100
import org.openhab.core.thing.type.ChannelType;
100101
import org.openhab.core.thing.type.ChannelTypeRegistry;
@@ -171,6 +172,7 @@ public class ThingResource implements RESTResource {
171172
private final ThingTypeRegistry thingTypeRegistry;
172173
private final RegistryChangedRunnableListener<Thing> resetLastModifiedChangeListener = new RegistryChangedRunnableListener<>(
173174
() -> lastModified = null);
175+
private final SyntaxGeneratorsService syntaxGeneratorsService;
174176

175177
private @Context @NonNullByDefault({}) UriInfo uriInfo;
176178
private @Nullable Date lastModified = null;
@@ -192,7 +194,8 @@ public ThingResource( //
192194
final @Reference ThingManager thingManager, //
193195
final @Reference ThingRegistry thingRegistry,
194196
final @Reference ThingStatusInfoI18nLocalizationService thingStatusInfoI18nLocalizationService,
195-
final @Reference ThingTypeRegistry thingTypeRegistry) {
197+
final @Reference ThingTypeRegistry thingTypeRegistry,
198+
final @Reference SyntaxGeneratorsService syntaxGeneratorsService) {
196199
this.dtoMapper = dtoMapper;
197200
this.channelTypeRegistry = channelTypeRegistry;
198201
this.configStatusService = configStatusService;
@@ -206,6 +209,7 @@ public ThingResource( //
206209
this.thingRegistry = thingRegistry;
207210
this.thingStatusInfoI18nLocalizationService = thingStatusInfoI18nLocalizationService;
208211
this.thingTypeRegistry = thingTypeRegistry;
212+
this.syntaxGeneratorsService = syntaxGeneratorsService;
209213

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

727+
@GET
728+
@RolesAllowed({ Role.ADMIN })
729+
@Path("/filesyntax")
730+
@Produces(MediaType.TEXT_PLAIN)
731+
@Operation(operationId = "generateSyntaxForAllThings", summary = "Generate file syntax for all things.", security = {
732+
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
733+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
734+
@ApiResponse(responseCode = "400", description = "Unsupported syntax format.") })
735+
public Response generateSyntaxForAllThings(
736+
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
737+
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format,
738+
@DefaultValue("false") @QueryParam("preferPresentationAsTree") @Parameter(description = "prefer a presentation as a tree if supported by the generator") boolean preferPresentationAsTree) {
739+
if (!syntaxGeneratorsService.isGeneratorForThingAvailable(format)) {
740+
String message = "No syntax available for format " + format + "!";
741+
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
742+
}
743+
return Response.ok(syntaxGeneratorsService.generateSyntaxForThings(format, thingRegistry.getAll(),
744+
preferPresentationAsTree)).build();
745+
}
746+
747+
@GET
748+
@RolesAllowed({ Role.ADMIN })
749+
@Path("/{thingUID}/filesyntax")
750+
@Produces(MediaType.TEXT_PLAIN)
751+
@Operation(operationId = "generateSyntaxForThing", summary = "Generate file syntax for a thing.", security = {
752+
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
753+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))),
754+
@ApiResponse(responseCode = "400", description = "Unsupported syntax format."),
755+
@ApiResponse(responseCode = "404", description = "Thing not found.") })
756+
public Response generateSyntaxForThing(
757+
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
758+
@PathParam("thingUID") @Parameter(description = "thingUID") String thingUID,
759+
@DefaultValue("DSL") @QueryParam("format") @Parameter(description = "syntax format") String format) {
760+
if (!syntaxGeneratorsService.isGeneratorForThingAvailable(format)) {
761+
String message = "No syntax available for format " + format + "!";
762+
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
763+
}
764+
765+
ThingUID aThingUID = new ThingUID(thingUID);
766+
Thing thing = thingRegistry.get(aThingUID);
767+
if (thing == null) {
768+
String message = "Thing " + thingUID + " does not exist!";
769+
return Response.status(Response.Status.NOT_FOUND).entity(message).build();
770+
}
771+
772+
return Response.ok(syntaxGeneratorsService.generateSyntaxForThings(format, Set.of(thing), false)).build();
773+
}
774+
723775
private FirmwareDTO convertToFirmwareDTO(Firmware firmware) {
724776
return new FirmwareDTO(firmware.getThingTypeUID().getAsString(), firmware.getVendor(), firmware.getModel(),
725777
firmware.isModelRestricted(), firmware.getDescription(), firmware.getVersion(),

bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/ModelRepository.java

+9
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,13 @@ public interface ModelRepository {
9292
* @param listener the listener to remove
9393
*/
9494
void removeModelRepositoryChangeListener(ModelRepositoryChangeListener listener);
95+
96+
/**
97+
* Generate the syntax from a provided model content.
98+
*
99+
* @param extension the kind of model ("items", "things", ...)
100+
* @param content the content of the model
101+
* @return the corresponding syntax
102+
*/
103+
String generateSyntaxFromModelContent(String extension, EObject content);
95104
}

bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/ModelRepositoryImpl.java

+18
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
package org.openhab.core.model.core.internal;
1414

1515
import java.io.ByteArrayInputStream;
16+
import java.io.ByteArrayOutputStream;
1617
import java.io.IOException;
1718
import java.io.InputStream;
1819
import java.nio.charset.StandardCharsets;
@@ -227,6 +228,23 @@ public void removeModelRepositoryChangeListener(ModelRepositoryChangeListener li
227228
listeners.remove(listener);
228229
}
229230

231+
@Override
232+
public String generateSyntaxFromModelContent(String extension, EObject content) {
233+
String result = "";
234+
Resource resource = resourceSet.createResource(URI.createURI("tmp_generated_syntax." + extension));
235+
try {
236+
resource.getContents().add(content);
237+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
238+
resource.save(outputStream, Map.of(XtextResource.OPTION_ENCODING, StandardCharsets.UTF_8.name()));
239+
result = new String(outputStream.toByteArray());
240+
} catch (IOException e) {
241+
logger.warn("Exception when saving the model {}", resource.getURI().lastSegment());
242+
} finally {
243+
resourceSet.getResources().remove(resource);
244+
}
245+
return result;
246+
}
247+
230248
private @Nullable Resource getResource(String name) {
231249
return resourceSet.getResource(URI.createURI(name), false);
232250
}

bundles/org.openhab.core.model.item/src/org/openhab/core/model/ItemsRuntimeModule.xtend

+7-2
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,24 @@ org.openhab.core.model
1818

1919
import com.google.inject.Binder
2020
import com.google.inject.name.Names
21+
import org.openhab.core.model.formatting.ItemsFormatter
2122
import org.openhab.core.model.internal.valueconverter.ItemValueConverters
2223
import org.eclipse.xtext.conversion.IValueConverterService
24+
import org.eclipse.xtext.formatting.IFormatter
2325
import org.eclipse.xtext.linking.lazy.LazyURIEncoder
2426

2527
/**
2628
* Use this class to register components to be used at runtime / without the Equinox extension registry.
2729
*/
2830
class ItemsRuntimeModule extends AbstractItemsRuntimeModule {
29-
3031
override Class<? extends IValueConverterService> bindIValueConverterService() {
3132
return ItemValueConverters
3233
}
33-
34+
35+
override Class<? extends IFormatter> bindIFormatter() {
36+
return ItemsFormatter
37+
}
38+
3439
override void configureUseIndexFragmentsForLazyLinking(Binder binder) {
3540
binder.bind(Boolean.TYPE).annotatedWith(Names.named(LazyURIEncoder.USE_INDEXED_FRAGMENTS_BINDING)).toInstance(
3641
Boolean.FALSE)

0 commit comments

Comments
 (0)