Skip to content

Commit cb4bcc8

Browse files
committed
[REST] New API to transform to/from DSL file format (items & things)
Related to #4585 POST /file-format/parse to parse file format POST /file-format/create to create file format Signed-off-by: Laurent Garnier <[email protected]>
1 parent 67303fa commit cb4bcc8

File tree

14 files changed

+887
-57
lines changed

14 files changed

+887
-57
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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.items.dto.ItemDTO;
18+
import org.openhab.core.thing.dto.ThingDTO;
19+
import org.openhab.core.thing.link.dto.ItemChannelLinkDTO;
20+
21+
/**
22+
* This is a data transfer object that is used to serialize different components that can be embedded
23+
* inside a file format, like items, metadata, channel links, things, ...
24+
*
25+
* @author Laurent Garnier - Initial contribution
26+
*/
27+
public class FileFormatDTO {
28+
29+
public List<ItemDTO> items;
30+
public List<MetadataDTO> metadata;
31+
public List<ItemChannelLinkDTO> channelLinks;
32+
public List<ThingDTO> things;
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.Map;
16+
17+
/**
18+
* This is a data transfer object that is used to serialize a metadata.
19+
*
20+
* @author Laurent Garnier - Initial contribution
21+
*/
22+
public class MetadataDTO {
23+
24+
public String itemName;
25+
public String namespace;
26+
public String value;
27+
public Map<String, Object> configuration;
28+
29+
public MetadataDTO(String itemName, String namespace, String value, Map<String, Object> configuration) {
30+
this.itemName = itemName;
31+
this.namespace = namespace;
32+
this.value = value;
33+
this.configuration = configuration;
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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 that is used to serialize the result of file format parsing.
19+
*
20+
* @author Laurent Garnier - Initial contribution
21+
*/
22+
public class ParsedFileFormatDTO extends FileFormatDTO {
23+
24+
public List<String> warnings;
25+
}

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

+406-4
Large diffs are not rendered by default.

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

+23-1
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 methods addStandaloneModel and removeStandaloneModel
3133
*/
3234
@NonNullByDefault
3335
public interface ModelRepository {
@@ -95,13 +97,33 @@ public interface ModelRepository {
9597
*/
9698
void removeModelRepositoryChangeListener(ModelRepositoryChangeListener listener);
9799

100+
/**
101+
* Adds a standalone model to the repository
102+
* A standalone model will be loaded without triggering any listener.
103+
*
104+
* @param modelType the model type
105+
* @param inputStream an input stream with the model's content
106+
* @param errors the list to be used to fill the errors
107+
* @param warnings the list to be used to fill the warnings
108+
* @return the created model name if it was successfully processed, null otherwise
109+
*/
110+
@Nullable
111+
String addStandaloneModel(String modelType, InputStream inputStream, List<String> errors, List<String> warnings);
112+
113+
/**
114+
* Removes a standalone model from the repository
115+
*
116+
* @param name the name of the model to remove
117+
* @return true, if model was removed, false, if it did not exist
118+
*/
119+
boolean removeStandaloneModel(String name);
120+
98121
/**
99122
* Generate the syntax from a provided model content.
100123
*
101124
* @param out the output stream to write the generated syntax to
102125
* @param modelType the model type
103126
* @param modelContent the content of the model
104-
* @return the corresponding syntax
105127
*/
106128
void generateSyntaxFromModel(OutputStream out, String modelType, EObject modelContent);
107129
}

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

+61-23
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,6 +51,7 @@
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 methods addStandaloneModel and removeStandaloneModel
5555
*/
5656
@Component(immediate = true)
5757
@NonNullByDefault
@@ -100,17 +100,37 @@ public ModelRepositoryImpl(final @Reference SafeEMF safeEmf) {
100100

101101
@Override
102102
public boolean addOrRefreshModel(String name, final InputStream originalInputStream) {
103+
return addOrRefreshModel(name, originalInputStream, null, null, false);
104+
}
105+
106+
public boolean addOrRefreshModel(String name, final InputStream originalInputStream, @Nullable List<String> errors,
107+
@Nullable List<String> warnings, boolean standalone) {
103108
logger.info("Loading model '{}'", name);
104109
Resource resource = null;
105110
byte[] bytes;
106111
try (InputStream inputStream = originalInputStream) {
107112
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);
113+
List<String> newErrors = new ArrayList<>();
114+
List<String> newWarnings = new ArrayList<>();
115+
boolean valid = validateModel(name, new ByteArrayInputStream(bytes), newErrors, newWarnings);
116+
if (errors != null) {
117+
errors.addAll(newErrors);
118+
}
119+
if (warnings != null) {
120+
warnings.addAll(newWarnings);
121+
}
122+
if (!valid) {
123+
if (!standalone) {
124+
logger.warn("Configuration model '{}' has errors, therefore ignoring it: {}", name,
125+
String.join("\n", newErrors));
126+
}
111127
removeModel(name);
112128
return false;
113129
}
130+
if (!standalone && !newWarnings.isEmpty()) {
131+
logger.info("Validation issues found in configuration model '{}', using it anyway:\n{}", name,
132+
String.join("\n", newWarnings));
133+
}
114134
} catch (IOException e) {
115135
logger.warn("Configuration model '{}' cannot be parsed correctly!", name, e);
116136
return false;
@@ -128,7 +148,9 @@ public boolean addOrRefreshModel(String name, final InputStream originalInputStr
128148
resource = resourceSet.createResource(URI.createURI(name));
129149
if (resource != null) {
130150
resource.load(inputStream, resourceOptions);
131-
notifyListeners(name, EventType.ADDED);
151+
if (!standalone) {
152+
notifyListeners(name, EventType.ADDED);
153+
}
132154
return true;
133155
} else {
134156
logger.warn("Ignoring file '{}' as we do not have a parser for it.", name);
@@ -139,7 +161,9 @@ public boolean addOrRefreshModel(String name, final InputStream originalInputStr
139161
synchronized (resourceSet) {
140162
resource.unload();
141163
resource.load(inputStream, resourceOptions);
142-
notifyListeners(name, EventType.MODIFIED);
164+
if (!standalone) {
165+
notifyListeners(name, EventType.MODIFIED);
166+
}
143167
return true;
144168
}
145169
}
@@ -154,11 +178,17 @@ public boolean addOrRefreshModel(String name, final InputStream originalInputStr
154178

155179
@Override
156180
public boolean removeModel(String name) {
181+
return removeModel(name, false);
182+
}
183+
184+
private boolean removeModel(String name, boolean standalone) {
157185
Resource resource = getResource(name);
158186
if (resource != null) {
159187
synchronized (resourceSet) {
160188
// do not physically delete it, but remove it from the resource set
161-
notifyListeners(name, EventType.REMOVED);
189+
if (!standalone) {
190+
notifyListeners(name, EventType.REMOVED);
191+
}
162192
resourceSet.getResources().remove(resource);
163193
return true;
164194
}
@@ -232,6 +262,18 @@ public void removeModelRepositoryChangeListener(ModelRepositoryChangeListener li
232262
listeners.remove(listener);
233263
}
234264

265+
@Override
266+
public @Nullable String addStandaloneModel(String modelType, InputStream inputStream, List<String> errors,
267+
List<String> warnings) {
268+
String name = "tmp_syntax_%d.%s".formatted(++counter, modelType);
269+
return addOrRefreshModel(name, inputStream, errors, warnings, true) ? name : null;
270+
}
271+
272+
@Override
273+
public boolean removeStandaloneModel(String name) {
274+
return removeModel(name, true);
275+
}
276+
235277
@Override
236278
public void generateSyntaxFromModel(OutputStream out, String modelType, EObject modelContent) {
237279
String result = "";
@@ -269,28 +311,28 @@ public void generateSyntaxFromModel(OutputStream out, String modelType, EObject
269311
* Validation will be done on a separate resource, in order to keep the original one intact in case its content
270312
* needs to be removed because of syntactical errors.
271313
*
272-
* @param name
273-
* @param inputStream
274-
* @return error messages as a String if any syntactical error were found, <code>null</code> otherwise
314+
* @param name the model name
315+
* @param inputStream an input stream with the model's content
316+
* @param errors the list to be used to fill the errors
317+
* @param warnings the list to be used to fill the warnings
318+
* @return false if any syntactical error were found, false otherwise
275319
* @throws IOException if there was an error with the given {@link InputStream}, loading the resource from there
276320
*/
277-
private @Nullable String validateModel(String name, InputStream inputStream) throws IOException {
321+
private boolean validateModel(String name, InputStream inputStream, List<String> errors, List<String> warnings)
322+
throws IOException {
278323
// use another resource for validation in order to keep the original one for emergency-removal in case of errors
279324
Resource resource = resourceSet.createResource(URI.createURI("tmp_" + name));
280325
try {
281326
resource.load(inputStream, resourceOptions);
282-
StringBuilder criticalErrors = new StringBuilder();
283-
List<String> warnings = new LinkedList<>();
284327

285328
if (!resource.getContents().isEmpty()) {
286329
// Check for syntactical errors
287330
for (Diagnostic diagnostic : resource.getErrors()) {
288-
criticalErrors
289-
.append(MessageFormat.format("[{0},{1}]: {2}\n", Integer.toString(diagnostic.getLine()),
290-
Integer.toString(diagnostic.getColumn()), diagnostic.getMessage()));
331+
errors.add(MessageFormat.format("[{0},{1}]: {2}", Integer.toString(diagnostic.getLine()),
332+
Integer.toString(diagnostic.getColumn()), diagnostic.getMessage()));
291333
}
292-
if (!criticalErrors.isEmpty()) {
293-
return criticalErrors.toString();
334+
if (!resource.getErrors().isEmpty()) {
335+
return false;
294336
}
295337

296338
// Check for validation errors, but log them only
@@ -300,10 +342,6 @@ public void generateSyntaxFromModel(OutputStream out, String modelType, EObject
300342
for (org.eclipse.emf.common.util.Diagnostic d : diagnostic.getChildren()) {
301343
warnings.add(d.getMessage());
302344
}
303-
if (!warnings.isEmpty()) {
304-
logger.info("Validation issues found in configuration model '{}', using it anyway:\n{}", name,
305-
String.join("\n", warnings));
306-
}
307345
} catch (NullPointerException e) {
308346
// see https://github.com/eclipse/smarthome/issues/3335
309347
logger.debug("Validation of '{}' skipped due to internal errors.", name);
@@ -312,7 +350,7 @@ public void generateSyntaxFromModel(OutputStream out, String modelType, EObject
312350
} finally {
313351
resourceSet.getResources().remove(resource);
314352
}
315-
return null;
353+
return true;
316354
}
317355

318356
private void notifyListeners(String name, EventType type) {

bundles/org.openhab.core.model.item/src/org/openhab/core/model/item/internal/GenericItemProvider.java

+15-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.ArrayList;
1616
import java.util.Arrays;
1717
import java.util.Collection;
18+
import java.util.Collections;
1819
import java.util.HashMap;
1920
import java.util.LinkedHashMap;
2021
import java.util.List;
@@ -67,11 +68,13 @@
6768
*
6869
* @author Kai Kreuzer - Initial contribution
6970
* @author Thomas Eichstaedt-Engelen - Initial contribution
71+
* @author Laurent Garnier - Add interface StandaloneItemProvider
7072
*/
7173
@NonNullByDefault
72-
@Component(service = { ItemProvider.class, StateDescriptionFragmentProvider.class }, immediate = true)
73-
public class GenericItemProvider extends AbstractProvider<Item>
74-
implements ModelRepositoryChangeListener, ItemProvider, StateDescriptionFragmentProvider {
74+
@Component(service = { ItemProvider.class, StandaloneItemProvider.class,
75+
StateDescriptionFragmentProvider.class }, immediate = true)
76+
public class GenericItemProvider extends AbstractProvider<Item> implements ModelRepositoryChangeListener, ItemProvider,
77+
StandaloneItemProvider, StateDescriptionFragmentProvider {
7578

7679
private final Logger logger = LoggerFactory.getLogger(GenericItemProvider.class);
7780

@@ -170,7 +173,7 @@ public Collection<Item> getAll() {
170173
return items;
171174
}
172175

173-
private Collection<Item> getItemsFromModel(String modelName) {
176+
private List<Item> getItemsFromModel(String modelName) {
174177
logger.debug("Read items from model '{}'", modelName);
175178

176179
List<Item> items = new ArrayList<>();
@@ -521,6 +524,14 @@ private Map<String, Item> toItemMap(@Nullable Collection<Item> items) {
521524
return null;
522525
}
523526

527+
@Override
528+
public List<Item> getItemsFromStandaloneModel(String modelName) {
529+
if (modelName.endsWith("items")) {
530+
return getItemsFromModel(modelName);
531+
}
532+
return Collections.emptyList();
533+
}
534+
524535
@Override
525536
public @Nullable StateDescriptionFragment getStateDescriptionFragment(String itemName, @Nullable Locale locale) {
526537
return stateDescriptionFragments.get(itemName);

0 commit comments

Comments
 (0)