Skip to content

Commit c8fb28f

Browse files
committed
[REST] New API for conversion between file formats
Related to #4585 This PR only supports management of things. It supports DSL and YAML file formats. A first new API allows to create a file format (DSL or YAML) from a JSON object. A second new API allows to parse a file format (DSL or YAML) to a JSON object. These 2 APIs should help Main UI displaying DSL and YAML file formats for things. Signed-off-by: Laurent Garnier <lg.hc@free.fr>
1 parent 003c6e1 commit c8fb28f

File tree

13 files changed

+816
-208
lines changed

13 files changed

+816
-208
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (c) 2010-2025 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.core.io.rest.core.fileformat;
14+
15+
import java.util.List;
16+
17+
import org.openhab.core.thing.dto.ThingDTO;
18+
19+
/**
20+
* This is a data transfer object to serialize the different components that can be contained
21+
* in a file format (items, things, ...).
22+
*
23+
* @author Laurent Garnier - Initial contribution
24+
*/
25+
public class FileFormatDTO {
26+
27+
public List<ThingDTO> things;
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (c) 2010-2025 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.core.io.rest.core.fileformat;
14+
15+
import java.util.List;
16+
17+
/**
18+
* This is a data transfer object to serialize the different components that can be contained
19+
* in a file format (items, things, ...) including an optional list of warnings.
20+
*
21+
* @author Laurent Garnier - Initial contribution
22+
*/
23+
public class extendedFileFormatDTO extends FileFormatDTO {
24+
25+
public List<String> warnings;
26+
}

bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/fileformat/FileFormatResource.java

Lines changed: 311 additions & 1 deletion
Large diffs are not rendered by default.

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import java.io.InputStream;
1616
import java.io.OutputStream;
17+
import java.util.List;
1718
import java.util.Set;
1819

1920
import org.eclipse.emf.ecore.EObject;
@@ -28,6 +29,7 @@
2829
*
2930
* @author Kai Kreuzer - Initial contribution
3031
* @author Laurent Garnier - Added method generateSyntaxFromModel
32+
* @author Laurent Garnier - Added method createTemporaryModel
3133
*/
3234
@NonNullByDefault
3335
public interface ModelRepository {
@@ -95,6 +97,21 @@ public interface ModelRepository {
9597
*/
9698
void removeModelRepositoryChangeListener(ModelRepositoryChangeListener listener);
9799

100+
/**
101+
* Creates a temporary model in the repository
102+
*
103+
* A temporary model is not attached to a file on disk.
104+
* A temporary model will be loaded without impacting any object registry.
105+
*
106+
* @param modelType the model type
107+
* @param inputStream an input stream with the model's content
108+
* @param errors the list to be used to fill the errors
109+
* @param warnings the list to be used to fill the warnings
110+
* @return the created model name if it was successfully processed, null otherwise
111+
*/
112+
@Nullable
113+
String createTemporaryModel(String modelType, InputStream inputStream, List<String> errors, List<String> warnings);
114+
98115
/**
99116
* Generate the syntax from a provided model content.
100117
*

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

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.text.MessageFormat;
2121
import java.util.ArrayList;
2222
import java.util.HashSet;
23-
import java.util.LinkedList;
2423
import java.util.List;
2524
import java.util.Map;
2625
import java.util.Set;
@@ -52,11 +51,14 @@
5251
* @author Oliver Libutzki - Added reloadAllModelsOfType method
5352
* @author Simon Kaufmann - added validation of models before loading them
5453
* @author Laurent Garnier - Added method generateSyntaxFromModel
54+
* @author Laurent Garnier - Added method createTemporaryModel
5555
*/
5656
@Component(immediate = true)
5757
@NonNullByDefault
5858
public class ModelRepositoryImpl implements ModelRepository {
5959

60+
private static final String PREFIX_TMP_MODEL = "tmp_";
61+
6062
private final Logger logger = LoggerFactory.getLogger(ModelRepositoryImpl.class);
6163
private final ResourceSet resourceSet;
6264
private final Map<String, String> resourceOptions = Map.of(XtextResource.OPTION_ENCODING,
@@ -100,17 +102,37 @@ public ModelRepositoryImpl(final @Reference SafeEMF safeEmf) {
100102

101103
@Override
102104
public boolean addOrRefreshModel(String name, final InputStream originalInputStream) {
105+
return addOrRefreshModel(name, originalInputStream, null, null);
106+
}
107+
108+
public boolean addOrRefreshModel(String name, final InputStream originalInputStream, @Nullable List<String> errors,
109+
@Nullable List<String> warnings) {
103110
logger.info("Loading model '{}'", name);
104111
Resource resource = null;
105112
byte[] bytes;
106113
try (InputStream inputStream = originalInputStream) {
107114
bytes = inputStream.readAllBytes();
108-
String validationResult = validateModel(name, new ByteArrayInputStream(bytes));
109-
if (validationResult != null) {
110-
logger.warn("Configuration model '{}' has errors, therefore ignoring it: {}", name, validationResult);
115+
List<String> newErrors = new ArrayList<>();
116+
List<String> newWarnings = new ArrayList<>();
117+
boolean valid = validateModel(name, new ByteArrayInputStream(bytes), newErrors, newWarnings);
118+
if (errors != null) {
119+
errors.addAll(newErrors);
120+
}
121+
if (warnings != null) {
122+
warnings.addAll(newWarnings);
123+
}
124+
if (!valid) {
125+
if (!isTemporaryModel(name)) {
126+
logger.warn("Configuration model '{}' has errors, therefore ignoring it: {}", name,
127+
String.join("\n", newErrors));
128+
}
111129
removeModel(name);
112130
return false;
113131
}
132+
if (!isTemporaryModel(name) && !newWarnings.isEmpty()) {
133+
logger.info("Validation issues found in configuration model '{}', using it anyway:\n{}", name,
134+
String.join("\n", newWarnings));
135+
}
114136
} catch (IOException e) {
115137
logger.warn("Configuration model '{}' cannot be parsed correctly!", name, e);
116138
return false;
@@ -176,7 +198,7 @@ public Iterable<String> getAllModelNamesOfType(final String modelType) {
176198
return resourceListCopy.stream()
177199
.filter(input -> input.getURI().lastSegment().contains(".") && input.isLoaded()
178200
&& modelType.equalsIgnoreCase(input.getURI().fileExtension())
179-
&& !input.getURI().lastSegment().startsWith("tmp_"))
201+
&& !isTemporaryModel(input.getURI().lastSegment()))
180202
.map(from -> from.getURI().path()).toList();
181203
}
182204
}
@@ -189,7 +211,7 @@ public void reloadAllModelsOfType(final String modelType) {
189211
for (Resource resource : resourceListCopy) {
190212
if (resource.getURI().lastSegment().contains(".") && resource.isLoaded()
191213
&& modelType.equalsIgnoreCase(resource.getURI().fileExtension())
192-
&& !resource.getURI().lastSegment().startsWith("tmp_")) {
214+
&& !isTemporaryModel(resource.getURI().lastSegment())) {
193215
XtextResource xtextResource = (XtextResource) resource;
194216
// It's not sufficient to discard the derived state.
195217
// The quick & dirts solution is to reparse the whole resource.
@@ -211,7 +233,7 @@ public Set<String> removeAllModelsOfType(final String modelType) {
211233
for (Resource resource : resourceListCopy) {
212234
if (resource.getURI().lastSegment().contains(".") && resource.isLoaded()
213235
&& modelType.equalsIgnoreCase(resource.getURI().fileExtension())
214-
&& !resource.getURI().lastSegment().startsWith("tmp_")) {
236+
&& !isTemporaryModel(resource.getURI().lastSegment())) {
215237
logger.debug("Removing resource '{}'", resource.getURI().lastSegment());
216238
ret.add(resource.getURI().lastSegment());
217239
resourceSet.getResources().remove(resource);
@@ -232,10 +254,21 @@ public void removeModelRepositoryChangeListener(ModelRepositoryChangeListener li
232254
listeners.remove(listener);
233255
}
234256

257+
@Override
258+
public @Nullable String createTemporaryModel(String modelType, InputStream inputStream, List<String> errors,
259+
List<String> warnings) {
260+
String name = "%smodel_%d.%s".formatted(PREFIX_TMP_MODEL, ++counter, modelType);
261+
return addOrRefreshModel(name, inputStream, errors, warnings) ? name : null;
262+
}
263+
264+
private boolean isTemporaryModel(String modelName) {
265+
return modelName.startsWith(PREFIX_TMP_MODEL);
266+
}
267+
235268
@Override
236269
public void generateSyntaxFromModel(OutputStream out, String modelType, EObject modelContent) {
237270
synchronized (resourceSet) {
238-
String name = "tmp_generated_syntax_%d.%s".formatted(++counter, modelType);
271+
String name = "%sgenerated_syntax_%d.%s".formatted(PREFIX_TMP_MODEL, ++counter, modelType);
239272
Resource resource = resourceSet.createResource(URI.createURI(name));
240273
try {
241274
resource.getContents().add(modelContent);
@@ -268,28 +301,28 @@ public void generateSyntaxFromModel(OutputStream out, String modelType, EObject
268301
* Validation will be done on a separate resource, in order to keep the original one intact in case its content
269302
* needs to be removed because of syntactical errors.
270303
*
271-
* @param name
272-
* @param inputStream
273-
* @return error messages as a String if any syntactical error were found, <code>null</code> otherwise
304+
* @param name the model name
305+
* @param inputStream an input stream with the model's content
306+
* @param errors the list to be used to fill the errors
307+
* @param warnings the list to be used to fill the warnings
308+
* @return false if any syntactical error were found, false otherwise
274309
* @throws IOException if there was an error with the given {@link InputStream}, loading the resource from there
275310
*/
276-
private @Nullable String validateModel(String name, InputStream inputStream) throws IOException {
311+
private boolean validateModel(String name, InputStream inputStream, List<String> errors, List<String> warnings)
312+
throws IOException {
277313
// use another resource for validation in order to keep the original one for emergency-removal in case of errors
278-
Resource resource = resourceSet.createResource(URI.createURI("tmp_" + name));
314+
Resource resource = resourceSet.createResource(URI.createURI(PREFIX_TMP_MODEL + name));
279315
try {
280316
resource.load(inputStream, resourceOptions);
281-
StringBuilder criticalErrors = new StringBuilder();
282-
List<String> warnings = new LinkedList<>();
283317

284318
if (!resource.getContents().isEmpty()) {
285319
// Check for syntactical errors
286320
for (Diagnostic diagnostic : resource.getErrors()) {
287-
criticalErrors
288-
.append(MessageFormat.format("[{0},{1}]: {2}\n", Integer.toString(diagnostic.getLine()),
289-
Integer.toString(diagnostic.getColumn()), diagnostic.getMessage()));
321+
errors.add(MessageFormat.format("[{0},{1}]: {2}", Integer.toString(diagnostic.getLine()),
322+
Integer.toString(diagnostic.getColumn()), diagnostic.getMessage()));
290323
}
291-
if (!criticalErrors.isEmpty()) {
292-
return criticalErrors.toString();
324+
if (!resource.getErrors().isEmpty()) {
325+
return false;
293326
}
294327

295328
// Check for validation errors, but log them only
@@ -299,10 +332,6 @@ public void generateSyntaxFromModel(OutputStream out, String modelType, EObject
299332
for (org.eclipse.emf.common.util.Diagnostic d : diagnostic.getChildren()) {
300333
warnings.add(d.getMessage());
301334
}
302-
if (!warnings.isEmpty()) {
303-
logger.info("Validation issues found in configuration model '{}', using it anyway:\n{}", name,
304-
String.join("\n", warnings));
305-
}
306335
} catch (NullPointerException e) {
307336
// see https://github.com/eclipse/smarthome/issues/3335
308337
logger.debug("Validation of '{}' skipped due to internal errors.", name);
@@ -311,12 +340,14 @@ public void generateSyntaxFromModel(OutputStream out, String modelType, EObject
311340
} finally {
312341
resourceSet.getResources().remove(resource);
313342
}
314-
return null;
343+
return true;
315344
}
316345

317346
private void notifyListeners(String name, EventType type) {
318-
for (ModelRepositoryChangeListener listener : listeners) {
319-
listener.modelChanged(name, type);
347+
if (!isTemporaryModel(name)) {
348+
for (ModelRepositoryChangeListener listener : listeners) {
349+
listener.modelChanged(name, type);
350+
}
320351
}
321352
}
322353
}

bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/GenericThingProvider.xtend

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,9 @@ import org.slf4j.LoggerFactory
7171
* factory cannot load a thing yet (bug 470368),
7272
* added delay until ThingTypes are fully loaded
7373
* @author Markus Rathgeb - Add locale provider support
74+
* @author Laurent Garnier - Add method getThingsFromModel
7475
*/
75-
@Component(immediate=true, service=ThingProvider)
76+
@Component(immediate=true, service=#[ ThingProvider, GenericThingProvider ])
7677
class GenericThingProvider extends AbstractProviderLazyNullness<Thing> implements ThingProvider, ModelRepositoryChangeListener, ReadyService.ReadyTracker {
7778

7879
static final String XML_THING_TYPE = "openhab.xmlThingTypes";
@@ -122,25 +123,7 @@ class GenericThingProvider extends AbstractProviderLazyNullness<Thing> implement
122123
return
123124
}
124125
flattenModelThings(model.things).map [
125-
// Get the ThingHandlerFactories
126-
val ThingUID thingUID = constructThingUID
127-
if (thingUID === null) {
128-
// ignore the Thing because its definition is broken
129-
return null
130-
}
131-
val thingTypeUID = constructThingTypeUID(thingUID)
132-
if (thingTypeUID === null) {
133-
// ignore the Thing because its definition is broken
134-
return null
135-
}
136-
val factory = thingHandlerFactories.findFirst [
137-
supportsThingType(thingTypeUID)
138-
]
139-
if (factory === null && modelLoaded) {
140-
logger.info("No ThingHandlerFactory found for thing {} (thing-type is {}). Deferring initialization.",
141-
thingUID, thingTypeUID)
142-
}
143-
return factory
126+
return getThingHandlerFactory
144127
]?.filter [
145128
// Drop it if there is no ThingHandlerFactory yet which can handle it
146129
it !== null
@@ -151,6 +134,27 @@ class GenericThingProvider extends AbstractProviderLazyNullness<Thing> implement
151134
}
152135
}
153136

137+
def private ThingHandlerFactory getThingHandlerFactory(ModelThing modelThing) {
138+
val ThingUID thingUID = constructThingUID(modelThing)
139+
if (thingUID === null) {
140+
// ignore the Thing because its definition is broken
141+
return null
142+
}
143+
val thingTypeUID = constructThingTypeUID(modelThing, thingUID)
144+
if (thingTypeUID === null) {
145+
// ignore the Thing because its definition is broken
146+
return null
147+
}
148+
val factory = thingHandlerFactories.findFirst [
149+
supportsThingType(thingTypeUID)
150+
]
151+
if (factory == null && modelLoaded) {
152+
logger.info("No ThingHandlerFactory found for thing {} (thing-type is {}). Deferring initialization.",
153+
thingUID, thingTypeUID)
154+
}
155+
return factory
156+
}
157+
154158
def private ThingUID constructThingUID(ModelThing modelThing) {
155159
if (modelThing.id !== null) {
156160
return new ThingUID(modelThing.id)
@@ -487,6 +491,23 @@ class GenericThingProvider extends AbstractProviderLazyNullness<Thing> implement
487491
}
488492
}
489493

494+
def public List<Thing> getThingsFromModel(String modelName) {
495+
val things = newArrayList()
496+
if (modelRepository !== null && modelName.endsWith("things")) {
497+
logger.debug("Read things from model '{}'", modelName);
498+
val model = modelRepository.getModel(modelName) as ThingModel
499+
if (model !== null) {
500+
flattenModelThings(model.things).forEach [
501+
val factory = getThingHandlerFactory
502+
if (factory !== null && loadedXmlThingTypes.contains(factory.bundleName)) {
503+
createThing(things, factory)
504+
}
505+
]
506+
}
507+
}
508+
return things
509+
}
510+
490511
def private Set<ThingUID> getAllThingUIDs(ThingModel model) {
491512
return getAllThingUIDs(model.things, null)
492513
}

0 commit comments

Comments
 (0)