Skip to content

[REST] New API for conversion between file formats #4779

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

Closed
Closed
Show file tree
Hide file tree
Changes from 6 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,28 @@
/*
* 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.thing.dto.ThingDTO;

/**
* This is a data transfer object to serialize the different components that can be contained
* in a file format (items, things, ...).
*
* @author Laurent Garnier - Initial contribution
*/
public class FileFormatDTO {

public List<ThingDTO> things;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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 to serialize the different components that can be contained
* in a file format (items, things, ...) including an optional list of warnings.
*
* @author Laurent Garnier - Initial contribution
*/
public class extendedFileFormatDTO 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 method createTemporaryModel
*/
@NonNullByDefault
public interface ModelRepository {
Expand Down Expand Up @@ -95,6 +97,21 @@ public interface ModelRepository {
*/
void removeModelRepositoryChangeListener(ModelRepositoryChangeListener listener);

/**
* Creates a temporary model in the repository
*
* A temporary model is not attached to a file on disk.
* A temporary model will be loaded without impacting any object registry.
*
* @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 createTemporaryModel(String modelType, InputStream inputStream, List<String> errors, List<String> warnings);

/**
* 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,11 +51,14 @@
* @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 method createTemporaryModel
*/
@Component(immediate = true)
@NonNullByDefault
public class ModelRepositoryImpl implements ModelRepository {

private static final String PREFIX_TMP_MODEL = "tmp_";

private final Logger logger = LoggerFactory.getLogger(ModelRepositoryImpl.class);
private final ResourceSet resourceSet;
private final Map<String, String> resourceOptions = Map.of(XtextResource.OPTION_ENCODING,
Expand Down Expand Up @@ -87,32 +89,52 @@ public ModelRepositoryImpl(final @Reference SafeEMF safeEmf) {
if (!resource.getContents().isEmpty()) {
return resource.getContents().getFirst();
} else {
logger.warn("Configuration model '{}' is either empty or cannot be parsed correctly!", name);
logger.warn("DSL model '{}' is either empty or cannot be parsed correctly!", name);
resourceSet.getResources().remove(resource);
return null;
}
} else {
logger.trace("Configuration model '{}' can not be found", name);
logger.trace("DSL model '{}' can not be found", name);
return null;
}
}
}

@Override
public boolean addOrRefreshModel(String name, final InputStream originalInputStream) {
logger.info("Loading model '{}'", name);
return addOrRefreshModel(name, originalInputStream, null, null);
}

public boolean addOrRefreshModel(String name, final InputStream originalInputStream, @Nullable List<String> errors,
@Nullable List<String> warnings) {
logger.info("Loading DSL 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) {
logger.warn("DSL model '{}' has errors, therefore ignoring it: {}", name, String.join("\n", newErrors));
removeModel(name);
return false;
}
if (!newWarnings.isEmpty()) {
logger.info("Validation issues found in DSL model '{}', using it anyway:\n{}", name,
String.join("\n", newWarnings));
}
} catch (IOException e) {
logger.warn("Configuration model '{}' cannot be parsed correctly!", name, e);
if (errors != null) {
errors.add("Model cannot be parsed correctly: %s".formatted(e.getMessage()));
}
logger.warn("DSL model '{}' cannot be parsed correctly!", name, e);
return false;
}
try (InputStream inputStream = new ByteArrayInputStream(bytes)) {
Expand Down Expand Up @@ -144,7 +166,10 @@ public boolean addOrRefreshModel(String name, final InputStream originalInputStr
}
}
} catch (IOException e) {
logger.warn("Configuration model '{}' cannot be parsed correctly!", name, e);
if (errors != null) {
errors.add("Model cannot be parsed correctly: %s".formatted(e.getMessage()));
}
logger.warn("DSL model '{}' cannot be parsed correctly!", name, e);
if (resource != null) {
resourceSet.getResources().remove(resource);
}
Expand Down Expand Up @@ -176,7 +201,7 @@ public Iterable<String> getAllModelNamesOfType(final String modelType) {
return resourceListCopy.stream()
.filter(input -> input.getURI().lastSegment().contains(".") && input.isLoaded()
&& modelType.equalsIgnoreCase(input.getURI().fileExtension())
&& !input.getURI().lastSegment().startsWith("tmp_"))
&& !isTemporaryModel(input.getURI().lastSegment()))
.map(from -> from.getURI().path()).toList();
}
}
Expand All @@ -189,7 +214,7 @@ public void reloadAllModelsOfType(final String modelType) {
for (Resource resource : resourceListCopy) {
if (resource.getURI().lastSegment().contains(".") && resource.isLoaded()
&& modelType.equalsIgnoreCase(resource.getURI().fileExtension())
&& !resource.getURI().lastSegment().startsWith("tmp_")) {
&& !isTemporaryModel(resource.getURI().lastSegment())) {
XtextResource xtextResource = (XtextResource) resource;
// It's not sufficient to discard the derived state.
// The quick & dirts solution is to reparse the whole resource.
Expand All @@ -211,7 +236,7 @@ public Set<String> removeAllModelsOfType(final String modelType) {
for (Resource resource : resourceListCopy) {
if (resource.getURI().lastSegment().contains(".") && resource.isLoaded()
&& modelType.equalsIgnoreCase(resource.getURI().fileExtension())
&& !resource.getURI().lastSegment().startsWith("tmp_")) {
&& !isTemporaryModel(resource.getURI().lastSegment())) {
logger.debug("Removing resource '{}'", resource.getURI().lastSegment());
ret.add(resource.getURI().lastSegment());
resourceSet.getResources().remove(resource);
Expand All @@ -232,16 +257,27 @@ public void removeModelRepositoryChangeListener(ModelRepositoryChangeListener li
listeners.remove(listener);
}

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

private boolean isTemporaryModel(String modelName) {
return modelName.startsWith(PREFIX_TMP_MODEL);
}

@Override
public void generateSyntaxFromModel(OutputStream out, String modelType, EObject modelContent) {
synchronized (resourceSet) {
String name = "tmp_generated_syntax_%d.%s".formatted(++counter, modelType);
String name = "%sgenerated_syntax_%d.%s".formatted(PREFIX_TMP_MODEL, ++counter, modelType);
Resource resource = resourceSet.createResource(URI.createURI(name));
try {
resource.getContents().add(modelContent);
resource.save(out, Map.of(XtextResource.OPTION_ENCODING, StandardCharsets.UTF_8.name()));
} catch (IOException e) {
logger.warn("Exception when saving the model {}", resource.getURI().lastSegment());
logger.warn("Exception when saving DSL model {}", resource.getURI().lastSegment());
} finally {
resourceSet.getResources().remove(resource);
}
Expand All @@ -268,28 +304,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));
Resource resource = resourceSet.createResource(URI.createURI(PREFIX_TMP_MODEL + 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 @@ -299,10 +335,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 @@ -311,12 +343,14 @@ public void generateSyntaxFromModel(OutputStream out, String modelType, EObject
} finally {
resourceSet.getResources().remove(resource);
}
return null;
return true;
}

private void notifyListeners(String name, EventType type) {
for (ModelRepositoryChangeListener listener : listeners) {
listener.modelChanged(name, type);
if (!isTemporaryModel(name)) {
for (ModelRepositoryChangeListener listener : listeners) {
listener.modelChanged(name, type);
}
}
}
}
Loading