Skip to content

Commit 41f606c

Browse files
committed
[REST] New API to create and parse DSL file formats
Related to #4585 POST /file-format/items/parse to parse items from file format POST /file-format/items/create to create items in file format POST /file-format/things/parse to parse things from file format POST /file-format/things/create to create things in file format Signed-off-by: Laurent Garnier <[email protected]>
1 parent 67303fa commit 41f606c

File tree

11 files changed

+767
-58
lines changed

11 files changed

+767
-58
lines changed

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

+402-5
Large diffs are not rendered by default.

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

+22-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
*
2929
* @author Kai Kreuzer - Initial contribution
3030
* @author Laurent Garnier - Added method generateSyntaxFromModel
31+
* @author Laurent Garnier - Added methods addStandaloneModel and removeStandaloneModel
3132
*/
3233
@NonNullByDefault
3334
public interface ModelRepository {
@@ -95,13 +96,33 @@ public interface ModelRepository {
9596
*/
9697
void removeModelRepositoryChangeListener(ModelRepositoryChangeListener listener);
9798

99+
/**
100+
* Adds a standalone model to the repository
101+
* A standalone model will be loaded without triggering any listener.
102+
*
103+
* @param modelType the model type
104+
* @param inputStream an input stream with the model's content
105+
* @param errors the string builder used to fill the errors
106+
* @param warnings the string builder used to fill the warnings
107+
* @return the created model name if it was successfully processed, null otherwise
108+
*/
109+
@Nullable
110+
String addStandaloneModel(String modelType, InputStream inputStream, StringBuilder errors, StringBuilder warnings);
111+
112+
/**
113+
* Removes a standalone model from the repository
114+
*
115+
* @param name the name of the model to remove
116+
* @return true, if model was removed, false, if it did not exist
117+
*/
118+
boolean removeStandaloneModel(String name);
119+
98120
/**
99121
* Generate the syntax from a provided model content.
100122
*
101123
* @param out the output stream to write the generated syntax to
102124
* @param modelType the model type
103125
* @param modelContent the content of the model
104-
* @return the corresponding syntax
105126
*/
106127
void generateSyntaxFromModel(OutputStream out, String modelType, EObject modelContent);
107128
}

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

+62-24
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 StringBuilder errors,
107+
@Nullable StringBuilder 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+
StringBuilder newErrors = new StringBuilder();
114+
StringBuilder newWarnings = new StringBuilder();
115+
boolean valid = validateModel(name, new ByteArrayInputStream(bytes), newErrors, newWarnings);
116+
if (errors != null) {
117+
errors.append(newErrors);
118+
}
119+
if (warnings != null) {
120+
warnings.append(newWarnings);
121+
}
122+
if (!valid) {
123+
if (!standalone) {
124+
logger.warn("Configuration model '{}' has errors, therefore ignoring it: {}", name,
125+
newErrors.toString());
126+
}
111127
removeModel(name);
112128
return false;
113129
}
130+
String message = newWarnings.toString();
131+
if (!standalone && !message.isEmpty()) {
132+
logger.info("Validation issues found in configuration model '{}', using it anyway:\n{}", name, message);
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, StringBuilder errors,
267+
StringBuilder 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,40 +311,36 @@ 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 string builder used to fill the errors
317+
* @param warnings the string builder 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, StringBuilder errors, StringBuilder 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.append(MessageFormat.format("[{0},{1}]: {2}\n", 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
297339
try {
298340
final org.eclipse.emf.common.util.Diagnostic diagnostic = safeEmf
299341
.call(() -> Diagnostician.INSTANCE.validate(resource.getContents().getFirst()));
300342
for (org.eclipse.emf.common.util.Diagnostic d : diagnostic.getChildren()) {
301-
warnings.add(d.getMessage());
302-
}
303-
if (!warnings.isEmpty()) {
304-
logger.info("Validation issues found in configuration model '{}', using it anyway:\n{}", name,
305-
String.join("\n", warnings));
343+
warnings.append(d.getMessage() + "\n");
306344
}
307345
} catch (NullPointerException e) {
308346
// see https://github.com/eclipse/smarthome/issues/3335
@@ -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

+14-3
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

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

527+
@Override
528+
public Collection<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);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.model.item.internal;
14+
15+
import java.util.Collection;
16+
17+
import org.eclipse.jdt.annotation.NonNullByDefault;
18+
import org.openhab.core.items.Item;
19+
20+
/**
21+
* {@link StandaloneItemProvider} is the interface to implement by an {@link Item} provider that is able
22+
* to create a list of items from a model without impacting the item registry.
23+
*
24+
* @author Laurent Garnier - Initial contribution
25+
*/
26+
@NonNullByDefault
27+
public interface StandaloneItemProvider {
28+
29+
/**
30+
* Parse the provided syntax and return the corresponding {@link Item} objects without impacting
31+
* the item registry.
32+
*
33+
* @param modelName the model name
34+
* @return the collection of corresponding {@link Item}
35+
*/
36+
Collection<Item> getItemsFromStandaloneModel(String modelName);
37+
}

bundles/org.openhab.core.model.item/src/org/openhab/core/model/item/internal/fileconverter/DslItemFileConverter.java

+28-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*/
1313
package org.openhab.core.model.item.internal.fileconverter;
1414

15+
import java.io.ByteArrayInputStream;
1516
import java.io.OutputStream;
1617
import java.net.URI;
1718
import java.net.URISyntaxException;
@@ -35,7 +36,9 @@
3536
import org.openhab.core.items.Metadata;
3637
import org.openhab.core.items.fileconverter.AbstractItemFileGenerator;
3738
import org.openhab.core.items.fileconverter.ItemFileGenerator;
39+
import org.openhab.core.items.fileconverter.ItemFileParser;
3840
import org.openhab.core.model.core.ModelRepository;
41+
import org.openhab.core.model.item.internal.StandaloneItemProvider;
3942
import org.openhab.core.model.items.ItemModel;
4043
import org.openhab.core.model.items.ItemsFactory;
4144
import org.openhab.core.model.items.ModelBinding;
@@ -58,18 +61,21 @@
5861
* @author Laurent Garnier - Initial contribution
5962
*/
6063
@NonNullByDefault
61-
@Component(immediate = true, service = ItemFileGenerator.class)
62-
public class DslItemFileConverter extends AbstractItemFileGenerator {
64+
@Component(immediate = true, service = { ItemFileGenerator.class, ItemFileParser.class })
65+
public class DslItemFileConverter extends AbstractItemFileGenerator implements ItemFileParser {
6366

6467
private final Logger logger = LoggerFactory.getLogger(DslItemFileConverter.class);
6568

6669
private final ModelRepository modelRepository;
70+
private final StandaloneItemProvider standaloneItemProvider;
6771
private final ConfigDescriptionRegistry configDescriptionRegistry;
6872

6973
@Activate
7074
public DslItemFileConverter(final @Reference ModelRepository modelRepository,
75+
final @Reference StandaloneItemProvider standaloneItemProvider,
7176
final @Reference ConfigDescriptionRegistry configDescriptionRegistry) {
7277
this.modelRepository = modelRepository;
78+
this.standaloneItemProvider = standaloneItemProvider;
7379
this.configDescriptionRegistry = configDescriptionRegistry;
7480
}
7581

@@ -247,4 +253,24 @@ private List<ConfigParameter> getConfigurationParameters(Metadata metadata, bool
247253
}
248254
return parameters;
249255
}
256+
257+
@Override
258+
public String getFileFormatParser() {
259+
return "DSL";
260+
}
261+
262+
@Override
263+
public boolean parseFileFormat(String syntax, Collection<Item> items, Collection<Metadata> metadata,
264+
StringBuilder errors, StringBuilder warnings) {
265+
ByteArrayInputStream inputStream = new ByteArrayInputStream(syntax.getBytes());
266+
String modelName = modelRepository.addStandaloneModel("items", inputStream, errors, warnings);
267+
if (modelName != null) {
268+
Collection<Item> newItems = standaloneItemProvider.getItemsFromStandaloneModel(modelName);
269+
items.addAll(newItems);
270+
// TODO retrieve metadata
271+
modelRepository.removeStandaloneModel(modelName);
272+
return true;
273+
}
274+
return false;
275+
}
250276
}

0 commit comments

Comments
 (0)