Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP][REST] New API to transform to/from DSL file format (items & things) #4630

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.rest.core.fileformat;

import java.util.List;

import org.openhab.core.items.dto.ItemDTO;
import org.openhab.core.thing.dto.ThingDTO;
import org.openhab.core.thing.link.dto.ItemChannelLinkDTO;

/**
* This is a data transfer object that is used to serialize different components that can be embedded
* inside a file format, like items, metadata, channel links, things, ...
*
* @author Laurent Garnier - Initial contribution
*/
public class FileFormatDTO {

public List<ItemDTO> items;
public List<MetadataDTO> metadata;
public List<ItemChannelLinkDTO> channelLinks;
public List<ThingDTO> things;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.rest.core.fileformat;

import java.util.Map;

/**
* This is a data transfer object that is used to serialize a metadata.
*
* @author Laurent Garnier - Initial contribution
*/
public class MetadataDTO {

public String itemName;
public String namespace;
public String value;
public Map<String, Object> configuration;

public MetadataDTO(String itemName, String namespace, String value, Map<String, Object> configuration) {
this.itemName = itemName;
this.namespace = namespace;
this.value = value;
this.configuration = configuration;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.rest.core.fileformat;

import java.util.List;

/**
* This is a data transfer object that is used to serialize the result of file format parsing.
*
* @author Laurent Garnier - Initial contribution
*/
public class ParsedFileFormatDTO extends FileFormatDTO {

public List<String> warnings;
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Set;

import org.eclipse.emf.ecore.EObject;
Expand All @@ -28,6 +29,7 @@
*
* @author Kai Kreuzer - Initial contribution
* @author Laurent Garnier - Added method generateSyntaxFromModel
* @author Laurent Garnier - Added methods addStandaloneModel and removeStandaloneModel
*/
@NonNullByDefault
public interface ModelRepository {
Expand Down Expand Up @@ -95,6 +97,27 @@ public interface ModelRepository {
*/
void removeModelRepositoryChangeListener(ModelRepositoryChangeListener listener);

/**
* Adds a standalone model to the repository
* A standalone model will be loaded without triggering any listener.
*
* @param modelType the model type
* @param inputStream an input stream with the model's content
* @param errors the list to be used to fill the errors
* @param warnings the list to be used to fill the warnings
* @return the created model name if it was successfully processed, null otherwise
*/
@Nullable
String addStandaloneModel(String modelType, InputStream inputStream, List<String> errors, List<String> warnings);

/**
* Removes a standalone model from the repository
*
* @param name the name of the model to remove
* @return true, if model was removed, false, if it did not exist
*/
boolean removeStandaloneModel(String name);

/**
* Generate the syntax from a provided model content.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -52,6 +51,7 @@
* @author Oliver Libutzki - Added reloadAllModelsOfType method
* @author Simon Kaufmann - added validation of models before loading them
* @author Laurent Garnier - Added method generateSyntaxFromModel
* @author Laurent Garnier - Added methods addStandaloneModel and removeStandaloneModel
*/
@Component(immediate = true)
@NonNullByDefault
Expand Down Expand Up @@ -100,17 +100,37 @@ public ModelRepositoryImpl(final @Reference SafeEMF safeEmf) {

@Override
public boolean addOrRefreshModel(String name, final InputStream originalInputStream) {
return addOrRefreshModel(name, originalInputStream, null, null, false);
}

public boolean addOrRefreshModel(String name, final InputStream originalInputStream, @Nullable List<String> errors,
@Nullable List<String> warnings, boolean standalone) {
logger.info("Loading model '{}'", name);
Resource resource = null;
byte[] bytes;
try (InputStream inputStream = originalInputStream) {
bytes = inputStream.readAllBytes();
String validationResult = validateModel(name, new ByteArrayInputStream(bytes));
if (validationResult != null) {
logger.warn("Configuration model '{}' has errors, therefore ignoring it: {}", name, validationResult);
List<String> newErrors = new ArrayList<>();
List<String> newWarnings = new ArrayList<>();
boolean valid = validateModel(name, new ByteArrayInputStream(bytes), newErrors, newWarnings);
if (errors != null) {
errors.addAll(newErrors);
}
if (warnings != null) {
warnings.addAll(newWarnings);
}
if (!valid) {
if (!standalone) {
logger.warn("Configuration model '{}' has errors, therefore ignoring it: {}", name,
String.join("\n", newErrors));
}
removeModel(name);
return false;
}
if (!standalone && !newWarnings.isEmpty()) {
logger.info("Validation issues found in configuration model '{}', using it anyway:\n{}", name,
String.join("\n", newWarnings));
}
} catch (IOException e) {
logger.warn("Configuration model '{}' cannot be parsed correctly!", name, e);
return false;
Expand All @@ -128,7 +148,9 @@ public boolean addOrRefreshModel(String name, final InputStream originalInputStr
resource = resourceSet.createResource(URI.createURI(name));
if (resource != null) {
resource.load(inputStream, resourceOptions);
notifyListeners(name, EventType.ADDED);
if (!standalone) {
notifyListeners(name, EventType.ADDED);
}
return true;
} else {
logger.warn("Ignoring file '{}' as we do not have a parser for it.", name);
Expand All @@ -139,7 +161,9 @@ public boolean addOrRefreshModel(String name, final InputStream originalInputStr
synchronized (resourceSet) {
resource.unload();
resource.load(inputStream, resourceOptions);
notifyListeners(name, EventType.MODIFIED);
if (!standalone) {
notifyListeners(name, EventType.MODIFIED);
}
return true;
}
}
Expand All @@ -154,11 +178,17 @@ public boolean addOrRefreshModel(String name, final InputStream originalInputStr

@Override
public boolean removeModel(String name) {
return removeModel(name, false);
}

private boolean removeModel(String name, boolean standalone) {
Resource resource = getResource(name);
if (resource != null) {
synchronized (resourceSet) {
// do not physically delete it, but remove it from the resource set
notifyListeners(name, EventType.REMOVED);
if (!standalone) {
notifyListeners(name, EventType.REMOVED);
}
resourceSet.getResources().remove(resource);
return true;
}
Expand Down Expand Up @@ -232,6 +262,18 @@ public void removeModelRepositoryChangeListener(ModelRepositoryChangeListener li
listeners.remove(listener);
}

@Override
public @Nullable String addStandaloneModel(String modelType, InputStream inputStream, List<String> errors,
List<String> warnings) {
String name = "tmp_syntax_%d.%s".formatted(++counter, modelType);
return addOrRefreshModel(name, inputStream, errors, warnings, true) ? name : null;
}

@Override
public boolean removeStandaloneModel(String name) {
return removeModel(name, true);
}

@Override
public void generateSyntaxFromModel(OutputStream out, String modelType, EObject modelContent) {
String result = "";
Expand Down Expand Up @@ -269,28 +311,28 @@ public void generateSyntaxFromModel(OutputStream out, String modelType, EObject
* Validation will be done on a separate resource, in order to keep the original one intact in case its content
* needs to be removed because of syntactical errors.
*
* @param name
* @param inputStream
* @return error messages as a String if any syntactical error were found, <code>null</code> otherwise
* @param name the model name
* @param inputStream an input stream with the model's content
* @param errors the list to be used to fill the errors
* @param warnings the list to be used to fill the warnings
* @return false if any syntactical error were found, false otherwise
* @throws IOException if there was an error with the given {@link InputStream}, loading the resource from there
*/
private @Nullable String validateModel(String name, InputStream inputStream) throws IOException {
private boolean validateModel(String name, InputStream inputStream, List<String> errors, List<String> warnings)
throws IOException {
// use another resource for validation in order to keep the original one for emergency-removal in case of errors
Resource resource = resourceSet.createResource(URI.createURI("tmp_" + name));
try {
resource.load(inputStream, resourceOptions);
StringBuilder criticalErrors = new StringBuilder();
List<String> warnings = new LinkedList<>();

if (!resource.getContents().isEmpty()) {
// Check for syntactical errors
for (Diagnostic diagnostic : resource.getErrors()) {
criticalErrors
.append(MessageFormat.format("[{0},{1}]: {2}\n", Integer.toString(diagnostic.getLine()),
Integer.toString(diagnostic.getColumn()), diagnostic.getMessage()));
errors.add(MessageFormat.format("[{0},{1}]: {2}", Integer.toString(diagnostic.getLine()),
Integer.toString(diagnostic.getColumn()), diagnostic.getMessage()));
}
if (!criticalErrors.isEmpty()) {
return criticalErrors.toString();
if (!resource.getErrors().isEmpty()) {
return false;
}

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

private void notifyListeners(String name, EventType type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
Expand Down Expand Up @@ -67,11 +68,13 @@
*
* @author Kai Kreuzer - Initial contribution
* @author Thomas Eichstaedt-Engelen - Initial contribution
* @author Laurent Garnier - Add interface StandaloneItemProvider
*/
@NonNullByDefault
@Component(service = { ItemProvider.class, StateDescriptionFragmentProvider.class }, immediate = true)
public class GenericItemProvider extends AbstractProvider<Item>
implements ModelRepositoryChangeListener, ItemProvider, StateDescriptionFragmentProvider {
@Component(service = { ItemProvider.class, StandaloneItemProvider.class,
StateDescriptionFragmentProvider.class }, immediate = true)
public class GenericItemProvider extends AbstractProvider<Item> implements ModelRepositoryChangeListener, ItemProvider,
StandaloneItemProvider, StateDescriptionFragmentProvider {

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

Expand Down Expand Up @@ -170,7 +173,7 @@ public Collection<Item> getAll() {
return items;
}

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

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

@Override
public List<Item> getItemsFromStandaloneModel(String modelName) {
if (modelName.endsWith("items")) {
return getItemsFromModel(modelName);
}
return Collections.emptyList();
}

@Override
public @Nullable StateDescriptionFragment getStateDescriptionFragment(String itemName, @Nullable Locale locale) {
return stateDescriptionFragments.get(itemName);
Expand Down
Loading