Skip to content

Commit 95ddef2

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 (POST /file-format/create) allows to create a file format (DSL or YAML) from a JSON object. A second new API (POST /file-format/parse) 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 <[email protected]>
1 parent 003c6e1 commit 95ddef2

File tree

13 files changed

+833
-218
lines changed

13 files changed

+833
-218
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: 314 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: 67 additions & 33 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,
@@ -87,32 +89,52 @@ public ModelRepositoryImpl(final @Reference SafeEMF safeEmf) {
8789
if (!resource.getContents().isEmpty()) {
8890
return resource.getContents().getFirst();
8991
} else {
90-
logger.warn("Configuration model '{}' is either empty or cannot be parsed correctly!", name);
92+
logger.warn("DSL model '{}' is either empty or cannot be parsed correctly!", name);
9193
resourceSet.getResources().remove(resource);
9294
return null;
9395
}
9496
} else {
95-
logger.trace("Configuration model '{}' can not be found", name);
97+
logger.trace("DSL model '{}' can not be found", name);
9698
return null;
9799
}
98100
}
99101
}
100102

101103
@Override
102104
public boolean addOrRefreshModel(String name, final InputStream originalInputStream) {
103-
logger.info("Loading model '{}'", name);
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) {
110+
logger.info("Loading DSL 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+
logger.warn("DSL model '{}' has errors, therefore ignoring it: {}", name, String.join("\n", newErrors));
111126
removeModel(name);
112127
return false;
113128
}
129+
if (!newWarnings.isEmpty()) {
130+
logger.info("Validation issues found in DSL model '{}', using it anyway:\n{}", name,
131+
String.join("\n", newWarnings));
132+
}
114133
} catch (IOException e) {
115-
logger.warn("Configuration model '{}' cannot be parsed correctly!", name, e);
134+
if (errors != null) {
135+
errors.add("Model cannot be parsed correctly: %s".formatted(e.getMessage()));
136+
}
137+
logger.warn("DSL model '{}' cannot be parsed correctly!", name, e);
116138
return false;
117139
}
118140
try (InputStream inputStream = new ByteArrayInputStream(bytes)) {
@@ -144,7 +166,10 @@ public boolean addOrRefreshModel(String name, final InputStream originalInputStr
144166
}
145167
}
146168
} catch (IOException e) {
147-
logger.warn("Configuration model '{}' cannot be parsed correctly!", name, e);
169+
if (errors != null) {
170+
errors.add("Model cannot be parsed correctly: %s".formatted(e.getMessage()));
171+
}
172+
logger.warn("DSL model '{}' cannot be parsed correctly!", name, e);
148173
if (resource != null) {
149174
resourceSet.getResources().remove(resource);
150175
}
@@ -176,7 +201,7 @@ public Iterable<String> getAllModelNamesOfType(final String modelType) {
176201
return resourceListCopy.stream()
177202
.filter(input -> input.getURI().lastSegment().contains(".") && input.isLoaded()
178203
&& modelType.equalsIgnoreCase(input.getURI().fileExtension())
179-
&& !input.getURI().lastSegment().startsWith("tmp_"))
204+
&& !isTemporaryModel(input.getURI().lastSegment()))
180205
.map(from -> from.getURI().path()).toList();
181206
}
182207
}
@@ -189,7 +214,7 @@ public void reloadAllModelsOfType(final String modelType) {
189214
for (Resource resource : resourceListCopy) {
190215
if (resource.getURI().lastSegment().contains(".") && resource.isLoaded()
191216
&& modelType.equalsIgnoreCase(resource.getURI().fileExtension())
192-
&& !resource.getURI().lastSegment().startsWith("tmp_")) {
217+
&& !isTemporaryModel(resource.getURI().lastSegment())) {
193218
XtextResource xtextResource = (XtextResource) resource;
194219
// It's not sufficient to discard the derived state.
195220
// The quick & dirts solution is to reparse the whole resource.
@@ -211,7 +236,7 @@ public Set<String> removeAllModelsOfType(final String modelType) {
211236
for (Resource resource : resourceListCopy) {
212237
if (resource.getURI().lastSegment().contains(".") && resource.isLoaded()
213238
&& modelType.equalsIgnoreCase(resource.getURI().fileExtension())
214-
&& !resource.getURI().lastSegment().startsWith("tmp_")) {
239+
&& !isTemporaryModel(resource.getURI().lastSegment())) {
215240
logger.debug("Removing resource '{}'", resource.getURI().lastSegment());
216241
ret.add(resource.getURI().lastSegment());
217242
resourceSet.getResources().remove(resource);
@@ -232,16 +257,27 @@ public void removeModelRepositoryChangeListener(ModelRepositoryChangeListener li
232257
listeners.remove(listener);
233258
}
234259

260+
@Override
261+
public @Nullable String createTemporaryModel(String modelType, InputStream inputStream, List<String> errors,
262+
List<String> warnings) {
263+
String name = "%smodel_%d.%s".formatted(PREFIX_TMP_MODEL, ++counter, modelType);
264+
return addOrRefreshModel(name, inputStream, errors, warnings) ? name : null;
265+
}
266+
267+
private boolean isTemporaryModel(String modelName) {
268+
return modelName.startsWith(PREFIX_TMP_MODEL);
269+
}
270+
235271
@Override
236272
public void generateSyntaxFromModel(OutputStream out, String modelType, EObject modelContent) {
237273
synchronized (resourceSet) {
238-
String name = "tmp_generated_syntax_%d.%s".formatted(++counter, modelType);
274+
String name = "%sgenerated_syntax_%d.%s".formatted(PREFIX_TMP_MODEL, ++counter, modelType);
239275
Resource resource = resourceSet.createResource(URI.createURI(name));
240276
try {
241277
resource.getContents().add(modelContent);
242278
resource.save(out, Map.of(XtextResource.OPTION_ENCODING, StandardCharsets.UTF_8.name()));
243279
} catch (IOException e) {
244-
logger.warn("Exception when saving the model {}", resource.getURI().lastSegment());
280+
logger.warn("Exception when saving DSL model {}", resource.getURI().lastSegment());
245281
} finally {
246282
resourceSet.getResources().remove(resource);
247283
}
@@ -268,28 +304,28 @@ public void generateSyntaxFromModel(OutputStream out, String modelType, EObject
268304
* Validation will be done on a separate resource, in order to keep the original one intact in case its content
269305
* needs to be removed because of syntactical errors.
270306
*
271-
* @param name
272-
* @param inputStream
273-
* @return error messages as a String if any syntactical error were found, <code>null</code> otherwise
307+
* @param name the model name
308+
* @param inputStream an input stream with the model's content
309+
* @param errors the list to be used to fill the errors
310+
* @param warnings the list to be used to fill the warnings
311+
* @return false if any syntactical error were found, false otherwise
274312
* @throws IOException if there was an error with the given {@link InputStream}, loading the resource from there
275313
*/
276-
private @Nullable String validateModel(String name, InputStream inputStream) throws IOException {
314+
private boolean validateModel(String name, InputStream inputStream, List<String> errors, List<String> warnings)
315+
throws IOException {
277316
// 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));
317+
Resource resource = resourceSet.createResource(URI.createURI(PREFIX_TMP_MODEL + name));
279318
try {
280319
resource.load(inputStream, resourceOptions);
281-
StringBuilder criticalErrors = new StringBuilder();
282-
List<String> warnings = new LinkedList<>();
283320

284321
if (!resource.getContents().isEmpty()) {
285322
// Check for syntactical errors
286323
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()));
324+
errors.add(MessageFormat.format("[{0},{1}]: {2}", Integer.toString(diagnostic.getLine()),
325+
Integer.toString(diagnostic.getColumn()), diagnostic.getMessage()));
290326
}
291-
if (!criticalErrors.isEmpty()) {
292-
return criticalErrors.toString();
327+
if (!resource.getErrors().isEmpty()) {
328+
return false;
293329
}
294330

295331
// Check for validation errors, but log them only
@@ -299,10 +335,6 @@ public void generateSyntaxFromModel(OutputStream out, String modelType, EObject
299335
for (org.eclipse.emf.common.util.Diagnostic d : diagnostic.getChildren()) {
300336
warnings.add(d.getMessage());
301337
}
302-
if (!warnings.isEmpty()) {
303-
logger.info("Validation issues found in configuration model '{}', using it anyway:\n{}", name,
304-
String.join("\n", warnings));
305-
}
306338
} catch (NullPointerException e) {
307339
// see https://github.com/eclipse/smarthome/issues/3335
308340
logger.debug("Validation of '{}' skipped due to internal errors.", name);
@@ -311,12 +343,14 @@ public void generateSyntaxFromModel(OutputStream out, String modelType, EObject
311343
} finally {
312344
resourceSet.getResources().remove(resource);
313345
}
314-
return null;
346+
return true;
315347
}
316348

317349
private void notifyListeners(String name, EventType type) {
318-
for (ModelRepositoryChangeListener listener : listeners) {
319-
listener.modelChanged(name, type);
350+
if (!isTemporaryModel(name)) {
351+
for (ModelRepositoryChangeListener listener : listeners) {
352+
listener.modelChanged(name, type);
353+
}
320354
}
321355
}
322356
}

0 commit comments

Comments
 (0)