From 8a6e7432cd5a85385c03891ab40880d723784688 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Wed, 22 Nov 2023 21:35:35 +0100 Subject: [PATCH 001/135] Add log websocket (#3859) This adds a websocket connection for receiving logs. Signed-off-by: Jan N. Klug --- .../io/websocket/CommonWebSocketServlet.java | 1 + .../io/websocket/{ => event}/EventDTO.java | 2 +- .../{ => event}/EventProcessingException.java | 2 +- .../websocket/{ => event}/EventWebSocket.java | 2 +- .../{ => event}/EventWebSocketAdapter.java | 3 +- .../{ => event}/ItemEventUtility.java | 2 +- .../openhab/core/io/websocket/log/LogDTO.java | 34 ++++ .../core/io/websocket/log/LogWebSocket.java | 148 ++++++++++++++++++ .../io/websocket/log/LogWebSocketAdapter.java | 73 +++++++++ .../websocket/CommonWebSocketServletTest.java | 1 + .../core/io/websocket/EventWebSocketTest.java | 8 +- .../io/websocket/ItemEventUtilityTest.java | 3 + 12 files changed, 272 insertions(+), 7 deletions(-) rename bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/{ => event}/EventDTO.java (98%) rename bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/{ => event}/EventProcessingException.java (94%) rename bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/{ => event}/EventWebSocket.java (99%) rename bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/{ => event}/EventWebSocketAdapter.java (96%) rename bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/{ => event}/ItemEventUtility.java (99%) create mode 100644 bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/log/LogDTO.java create mode 100644 bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/log/LogWebSocket.java create mode 100644 bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/log/LogWebSocketAdapter.java diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/CommonWebSocketServlet.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/CommonWebSocketServlet.java index 7937ef0e39b..d1eb7e95523 100644 --- a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/CommonWebSocketServlet.java +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/CommonWebSocketServlet.java @@ -30,6 +30,7 @@ import org.openhab.core.auth.AuthenticationException; import org.openhab.core.auth.Role; import org.openhab.core.io.rest.auth.AuthFilter; +import org.openhab.core.io.websocket.event.EventWebSocketAdapter; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventDTO.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventDTO.java similarity index 98% rename from bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventDTO.java rename to bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventDTO.java index 6038e41c929..87f0138aecc 100644 --- a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventDTO.java +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventDTO.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.io.websocket; +package org.openhab.core.io.websocket.event; import java.util.Objects; diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventProcessingException.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventProcessingException.java similarity index 94% rename from bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventProcessingException.java rename to bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventProcessingException.java index b2a504ea61e..a83ab244214 100644 --- a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventProcessingException.java +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventProcessingException.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.io.websocket; +package org.openhab.core.io.websocket.event; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocket.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventWebSocket.java similarity index 99% rename from bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocket.java rename to bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventWebSocket.java index f37bb4cc8e2..f0136dc3532 100644 --- a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocket.java +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventWebSocket.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.io.websocket; +package org.openhab.core.io.websocket.event; import java.io.IOException; import java.lang.reflect.Type; diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocketAdapter.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventWebSocketAdapter.java similarity index 96% rename from bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocketAdapter.java rename to bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventWebSocketAdapter.java index 6ef61b804cb..df905c7e3ee 100644 --- a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/EventWebSocketAdapter.java +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventWebSocketAdapter.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.io.websocket; +package org.openhab.core.io.websocket.event; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; @@ -21,6 +21,7 @@ import org.openhab.core.events.Event; import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventSubscriber; +import org.openhab.core.io.websocket.WebSocketAdapter; import org.openhab.core.items.ItemRegistry; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/ItemEventUtility.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/ItemEventUtility.java similarity index 99% rename from bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/ItemEventUtility.java rename to bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/ItemEventUtility.java index e8f1d2b9ec9..547e161d0a0 100644 --- a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/ItemEventUtility.java +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/ItemEventUtility.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.core.io.websocket; +package org.openhab.core.io.websocket.event; import java.util.List; import java.util.regex.Matcher; diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/log/LogDTO.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/log/LogDTO.java new file mode 100644 index 00000000000..ff4c0587372 --- /dev/null +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/log/LogDTO.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2023 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.websocket.log; + +import java.util.Date; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.io.websocket.event.EventDTO; +import org.osgi.service.log.LogLevel; + +/** + * The {@link EventDTO} is used for serialization and deserialization of events + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class LogDTO { + public @Nullable String loggerName; + public @Nullable LogLevel level; + public @Nullable Date timestamp; + public long unixtime; + public @Nullable String message; +} diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/log/LogWebSocket.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/log/LogWebSocket.java new file mode 100644 index 00000000000..e08cd24536e --- /dev/null +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/log/LogWebSocket.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2010-2023 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.websocket.log; + +import java.io.IOException; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.osgi.service.log.LogEntry; +import org.osgi.service.log.LogListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; + +/** + * The {@link LogWebSocket} is the WebSocket implementation for logs + * + * @author Jan N. Klug - Initial contribution + */ +@WebSocket +@NonNullByDefault +@SuppressWarnings("unused") +public class LogWebSocket implements LogListener { + @SuppressWarnings("unchecked") + private static final TypeToken> STRING_LIST_TYPE = (TypeToken>) TypeToken + .getParameterized(List.class, String.class); + + private final Logger logger = LoggerFactory.getLogger(LogWebSocket.class); + + private final LogWebSocketAdapter wsAdapter; + private final Gson gson; + + private @Nullable Session session; + private @Nullable RemoteEndpoint remoteEndpoint; + private String remoteIdentifier = ""; + + private List loggerPatterns = List.of(); + + public LogWebSocket(Gson gson, LogWebSocketAdapter wsAdapter) { + this.wsAdapter = wsAdapter; + this.gson = gson; + } + + @OnWebSocketClose + public void onClose(int statusCode, String reason) { + this.wsAdapter.unregisterListener(this); + remoteIdentifier = ""; + this.session = null; + this.remoteEndpoint = null; + } + + @OnWebSocketConnect + public void onConnect(Session session) { + this.session = session; + RemoteEndpoint remoteEndpoint = session.getRemote(); + this.remoteEndpoint = remoteEndpoint; + this.remoteIdentifier = remoteEndpoint.getInetSocketAddress().toString(); + this.wsAdapter.registerListener(this); + } + + @OnWebSocketMessage + public void onText(String message) { + RemoteEndpoint remoteEndpoint = this.remoteEndpoint; + if (session == null || remoteEndpoint == null) { + // no connection or no remote endpoint , do nothing this is possible due to async behavior + return; + } + + try { + loggerPatterns = gson.fromJson(message, STRING_LIST_TYPE).stream().map(Pattern::compile).toList(); + } catch (JsonParseException e) { + logger.warn("Failed to parse '{}' to a list of subscribed loggers", message); + } + } + + @OnWebSocketError + public void onError(Session session, @Nullable Throwable error) { + if (session != null) { + session.close(); + } + + String message = error == null ? "" : Objects.requireNonNullElse(error.getMessage(), ""); + logger.info("WebSocket error: {}", message); + onClose(StatusCode.NO_CODE, message); + } + + private synchronized void sendMessage(String message) throws IOException { + RemoteEndpoint remoteEndpoint = this.remoteEndpoint; + if (remoteEndpoint == null) { + logger.warn("Could not determine remote endpoint, failed to send '{}'.", message); + return; + } + remoteEndpoint.sendString(message); + } + + @Override + public void logged(@NonNullByDefault({}) LogEntry logEntry) { + if (!loggerPatterns.isEmpty() && loggerPatterns.stream().noneMatch(logMatch(logEntry))) { + return; + } + try { + LogDTO logDTO = map(logEntry); + sendMessage(gson.toJson(logDTO)); + } catch (IOException e) { + logger.debug("Failed to send log {} to {}: {}", logEntry, remoteIdentifier, e.getMessage()); + } + } + + private static Predicate logMatch(LogEntry logEntry) { + return pattern -> pattern.matcher(logEntry.getLoggerName()).matches(); + } + + private static LogDTO map(LogEntry logEntry) { + LogDTO logDTO = new LogDTO(); + logDTO.loggerName = logEntry.getLoggerName(); + logDTO.level = logEntry.getLogLevel(); + logDTO.unixtime = logEntry.getTime(); + logDTO.timestamp = new Date(logEntry.getTime()); + logDTO.message = logEntry.getMessage(); + return logDTO; + } +} diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/log/LogWebSocketAdapter.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/log/LogWebSocketAdapter.java new file mode 100644 index 00000000000..adbdeac1e9b --- /dev/null +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/log/LogWebSocketAdapter.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2023 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.websocket.log; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.openhab.core.io.websocket.WebSocketAdapter; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.log.LogReaderService; + +import com.google.gson.Gson; + +/** + * The {@link LogWebSocketAdapter} allows subscription to log events over WebSocket + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = { WebSocketAdapter.class }) +public class LogWebSocketAdapter implements WebSocketAdapter { + public static final String ADAPTER_ID = "logs"; + private final Gson gson = new Gson(); + private final Set webSockets = new CopyOnWriteArraySet<>(); + private final LogReaderService logReaderService; + + @Activate + public LogWebSocketAdapter(@Reference LogReaderService logReaderService) { + this.logReaderService = logReaderService; + } + + @Deactivate + public void deactivate() { + webSockets.forEach(logReaderService::removeLogListener); + } + + public void registerListener(LogWebSocket eventWebSocket) { + webSockets.add(eventWebSocket); + logReaderService.addLogListener(eventWebSocket); + } + + public void unregisterListener(LogWebSocket eventWebSocket) { + logReaderService.removeLogListener(eventWebSocket); + webSockets.remove(eventWebSocket); + } + + @Override + public String getId() { + return ADAPTER_ID; + } + + @Override + public Object createWebSocket(ServletUpgradeRequest servletUpgradeRequest, + ServletUpgradeResponse servletUpgradeResponse) { + return new LogWebSocket(gson, LogWebSocketAdapter.this); + } +} diff --git a/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/CommonWebSocketServletTest.java b/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/CommonWebSocketServletTest.java index c7aafb587dc..93df3e456b1 100644 --- a/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/CommonWebSocketServletTest.java +++ b/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/CommonWebSocketServletTest.java @@ -43,6 +43,7 @@ import org.openhab.core.auth.AuthenticationException; import org.openhab.core.io.rest.auth.AnonymousUserSecurityContext; import org.openhab.core.io.rest.auth.AuthFilter; +import org.openhab.core.io.websocket.event.EventWebSocket; import org.osgi.service.http.NamespaceException; /** diff --git a/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/EventWebSocketTest.java b/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/EventWebSocketTest.java index 49eb37596de..01a531e4e94 100644 --- a/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/EventWebSocketTest.java +++ b/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/EventWebSocketTest.java @@ -18,8 +18,8 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.openhab.core.io.websocket.EventWebSocket.WEBSOCKET_EVENT_TYPE; -import static org.openhab.core.io.websocket.EventWebSocket.WEBSOCKET_TOPIC_PREFIX; +import static org.openhab.core.io.websocket.event.EventWebSocket.WEBSOCKET_EVENT_TYPE; +import static org.openhab.core.io.websocket.event.EventWebSocket.WEBSOCKET_TOPIC_PREFIX; import java.io.IOException; import java.net.InetSocketAddress; @@ -39,6 +39,10 @@ import org.mockito.quality.Strictness; import org.openhab.core.events.Event; import org.openhab.core.events.EventPublisher; +import org.openhab.core.io.websocket.event.EventDTO; +import org.openhab.core.io.websocket.event.EventWebSocket; +import org.openhab.core.io.websocket.event.EventWebSocketAdapter; +import org.openhab.core.io.websocket.event.ItemEventUtility; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.events.ItemEventFactory; diff --git a/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/ItemEventUtilityTest.java b/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/ItemEventUtilityTest.java index 081c31146cf..63ac9bc63bb 100644 --- a/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/ItemEventUtilityTest.java +++ b/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/ItemEventUtilityTest.java @@ -27,6 +27,9 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.openhab.core.events.Event; +import org.openhab.core.io.websocket.event.EventDTO; +import org.openhab.core.io.websocket.event.EventProcessingException; +import org.openhab.core.io.websocket.event.ItemEventUtility; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.events.ItemEvent; From 3e3afd8cdcdb162ebafc73b36fb1a0e7d30c6137 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Wed, 22 Nov 2023 21:36:39 +0100 Subject: [PATCH 002/135] [DSL] Expose ColorUtil methods to DSL rules (#3879) New functions for RGBW introduced in #3849. Signed-off-by: Holger Friedrich --- .../org/openhab/core/model/script/actions/CoreUtil.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/CoreUtil.java b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/CoreUtil.java index 5b35a55387c..084c286ef85 100644 --- a/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/CoreUtil.java +++ b/bundles/org.openhab.core.model.script/src/org/openhab/core/model/script/actions/CoreUtil.java @@ -36,6 +36,14 @@ public static int hsbTosRgb(HSBType hsb) { return ColorUtil.hsbTosRgb(hsb); } + public static int[] hsbToRgbw(HSBType hsb) { + return ColorUtil.hsbToRgbw(hsb); + } + + public static PercentType[] hsbToRgbwPercent(HSBType hsb) { + return ColorUtil.hsbToRgbwPercent(hsb); + } + public static double[] hsbToXY(HSBType hsb) { return ColorUtil.hsbToXY(hsb); } From bdb1e55b5869b5118e3089576bb9527c44f28d02 Mon Sep 17 00:00:00 2001 From: joerg1985 <16140691+joerg1985@users.noreply.github.com> Date: Wed, 22 Nov 2023 22:43:44 +0100 Subject: [PATCH 003/135] Use a scheduled thread pool in JsonStorage + Bugfixes (#3874) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use a scheduled thread pool in JsonStorage to avoid one thread per instance * Removed an incorrect conversion between millis and nanos Signed-off-by: Jörg Sautter --- .../storage/json/internal/JsonStorage.java | 55 ++++++++----------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/bundles/org.openhab.core.storage.json/src/main/java/org/openhab/core/storage/json/internal/JsonStorage.java b/bundles/org.openhab.core.storage.json/src/main/java/org/openhab/core/storage/json/internal/JsonStorage.java index 1d127974ad6..d817999ebfa 100644 --- a/bundles/org.openhab.core.storage.json/src/main/java/org/openhab/core/storage/json/internal/JsonStorage.java +++ b/bundles/org.openhab.core.storage.json/src/main/java/org/openhab/core/storage/json/internal/JsonStorage.java @@ -23,13 +23,15 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.ConfigurationDeserializer; import org.openhab.core.config.core.OrderingMapSerializer; @@ -60,6 +62,7 @@ * de-/serialization, keep json structures in map * @author Sami Salonen - ordered inner and outer serialization of Maps, * Sets and properties of Configuration + * @author Jörg Sautter - use a scheduled thread pool */ @NonNullByDefault public class JsonStorage implements Storage { @@ -75,8 +78,8 @@ public class JsonStorage implements Storage { private static final String BACKUP_EXTENSION = "backup"; private static final String SEPARATOR = "--"; - private final Timer commitTimer; - private @Nullable TimerTask commitTimerTask; + private final ScheduledExecutorService scheduledExecutorService; + private @Nullable ScheduledFuture commitScheduledFuture; private long deferredSince = 0; @@ -112,7 +115,7 @@ public JsonStorage(File file, @Nullable ClassLoader classLoader, int maxBackupFi .setPrettyPrinting() // .create(); - commitTimer = new Timer(); + scheduledExecutorService = ThreadPoolManager.getScheduledPool("JsonStorage"); Map inputMap = null; if (file.exists()) { @@ -333,11 +336,11 @@ private void writeDatabaseFile(File dataFile, String data) throws IOException { * require a read and write, and is thus slower). */ public synchronized void flush() { - // Stop any existing timer - TimerTask commitTimerTask = this.commitTimerTask; - if (commitTimerTask != null) { - commitTimerTask.cancel(); - this.commitTimerTask = null; + // Stop any existing scheduled commit + ScheduledFuture commitScheduledFuture = this.commitScheduledFuture; + if (commitScheduledFuture != null) { + commitScheduledFuture.cancel(false); + this.commitScheduledFuture = null; } if (dirty) { @@ -376,41 +379,29 @@ private void cleanupBackups() { } } - private class CommitTimerTask extends TimerTask { - @Override - public void run() { - // Save the database - flush(); - } - } - public synchronized void deferredCommit() { dirty = true; - // Stop any existing timer - TimerTask commitTimerTask = this.commitTimerTask; - if (commitTimerTask != null) { - commitTimerTask.cancel(); - this.commitTimerTask = null; + // Stop any existing scheduled commit + ScheduledFuture commitScheduledFuture = this.commitScheduledFuture; + if (commitScheduledFuture != null) { + commitScheduledFuture.cancel(false); + this.commitScheduledFuture = null; } // Handle a maximum time for deferring the commit. // This stops a pathological loop preventing saving - if (deferredSince != 0 && deferredSince < System.nanoTime() - (maxDeferredPeriod * 1000L)) { + if (deferredSince != 0 && deferredSince < System.currentTimeMillis() - maxDeferredPeriod) { flush(); - // as we committed the database now, there is no need to start a new commit - // timer + // as we committed the database now, there is no need to schedule a new commit return; } if (deferredSince == 0) { - deferredSince = System.nanoTime(); + deferredSince = System.currentTimeMillis(); } - // Create the timer task - commitTimerTask = new CommitTimerTask(); - - // Start the timer - commitTimer.schedule(commitTimerTask, writeDelay); + // Schedule the commit + this.commitScheduledFuture = scheduledExecutorService.schedule(this::flush, writeDelay, TimeUnit.MILLISECONDS); } } From ae117f631778457b931644f9f87035d3b2727167 Mon Sep 17 00:00:00 2001 From: joerg1985 <16140691+joerg1985@users.noreply.github.com> Date: Fri, 24 Nov 2023 16:52:07 +0100 Subject: [PATCH 004/135] Use a single thread to watch all event executors (#3884) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörg Sautter --- .../openhab/core/internal/events/EventHandler.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/events/EventHandler.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/events/EventHandler.java index e2ca6ba6da4..3560b1fb086 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/events/EventHandler.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/events/EventHandler.java @@ -53,6 +53,7 @@ public class EventHandler implements AutoCloseable { private final Map typedEventFactories; private final Map, ExecutorRecord> executors = new HashMap<>(); + private final ScheduledExecutorService watcher; /** * Create a new event handler. @@ -64,12 +65,12 @@ public EventHandler(final Map> typedEventSubscriber final Map typedEventFactories) { this.typedEventSubscribers = typedEventSubscribers; this.typedEventFactories = typedEventFactories; + watcher = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("eventwatcher")); } private synchronized ExecutorRecord createExecutorRecord(Class subscriber) { return new ExecutorRecord( Executors.newSingleThreadExecutor(new NamedThreadFactory("eventexecutor-" + executors.size())), - Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("eventwatcher-" + executors.size())), new AtomicInteger()); } @@ -77,8 +78,8 @@ private synchronized ExecutorRecord createExecutorRecord(Class { r.executor.shutdownNow(); - r.watcher.shutdownNow(); }); + watcher.shutdownNow(); } public void handleEvent(org.osgi.service.event.Event osgiEvent) { @@ -154,13 +155,13 @@ private synchronized void dispatchEvent(final Set eventSubscrib logger.trace("Delegate event to subscriber ({}).", eventSubscriber.getClass()); ExecutorRecord executorRecord = Objects.requireNonNull( executors.computeIfAbsent(eventSubscriber.getClass(), this::createExecutorRecord)); - int queueSize = executorRecord.count().incrementAndGet(); + int queueSize = executorRecord.count.incrementAndGet(); if (queueSize > EVENT_QUEUE_WARN_LIMIT) { logger.warn("The queue for a subscriber of type '{}' exceeds {} elements. System may be unstable.", eventSubscriber.getClass(), EVENT_QUEUE_WARN_LIMIT); } CompletableFuture.runAsync(() -> { - ScheduledFuture logTimeout = executorRecord.watcher().schedule( + ScheduledFuture logTimeout = watcher.schedule( () -> logger.warn("Dispatching event to subscriber '{}' takes more than {}ms.", eventSubscriber, EVENTSUBSCRIBER_EVENTHANDLING_MAX_MS), EVENTSUBSCRIBER_EVENTHANDLING_MAX_MS, TimeUnit.MILLISECONDS); @@ -171,13 +172,13 @@ private synchronized void dispatchEvent(final Set eventSubscrib EventSubscriber.class.getName(), ex.getMessage(), ex); } logTimeout.cancel(false); - }, executorRecord.executor()).thenRun(executorRecord.count::decrementAndGet); + }, executorRecord.executor).thenRun(executorRecord.count::decrementAndGet); } else { logger.trace("Skip event subscriber ({}) because of its filter.", eventSubscriber.getClass()); } } } - private record ExecutorRecord(ExecutorService executor, ScheduledExecutorService watcher, AtomicInteger count) { + private record ExecutorRecord(ExecutorService executor, AtomicInteger count) { } } From f71ebfb83c5f27bfd7d2679c8c4e3534dec5b72e Mon Sep 17 00:00:00 2001 From: lolodomo Date: Fri, 24 Nov 2023 16:53:10 +0100 Subject: [PATCH 005/135] Extend support to ISO8601 format for sitemap chart period parameter (#3863) * Extend support to ISO8601 format for sitemap chart period parameter Signed-off-by: Laurent Garnier --- .../openhab/core/io/rest/RESTConstants.java | 12 +- .../core/ui/internal/chart/ChartServlet.java | 50 +++++--- .../chart/ChartServletPeriodParamTest.java | 107 ++++++++++++++++++ 3 files changed, 155 insertions(+), 14 deletions(-) create mode 100644 bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/chart/ChartServletPeriodParamTest.java diff --git a/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/RESTConstants.java b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/RESTConstants.java index 7f3c5b5affb..73a7c68474a 100644 --- a/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/RESTConstants.java +++ b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/RESTConstants.java @@ -27,5 +27,15 @@ public class RESTConstants { public static final String JAX_RS_NAME = "openhab"; - public static final String API_VERSION = "5"; + /** + * Version of the openHAB API + * + * Version 1: initial version + * Version 2: include invisible widgets into sitemap response (#499) + * Version 3: Addition of anyFormat icon parameter (#978) + * Version 4: OH3, refactored extensions to addons (#1560) + * Version 5: transparent charts (#2502) + * Version 6: extended chart period parameter format (#3863) + */ + public static final String API_VERSION = "6"; } diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/ChartServlet.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/ChartServlet.java index 9464e1c4195..82091f3f92f 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/ChartServlet.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/ChartServlet.java @@ -12,15 +12,15 @@ */ package org.openhab.core.ui.internal.chart; -import static java.util.Map.entry; - import java.awt.image.BufferedImage; import java.io.IOException; import java.time.Duration; import java.time.LocalDateTime; +import java.time.Period; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAmount; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; @@ -69,6 +69,7 @@ * * @author Chris Jackson - Initial contribution * @author Holger Reichert - Support for themes, DPI, legend hiding + * @author Laurent Garnier - Extend support to ISO8601 format for chart period parameter */ @Component(immediate = true, service = { ChartServlet.class, Servlet.class }, configurationPid = "org.openhab.chart", // property = Constants.SERVICE_PID + "=org.openhab.chart") @@ -101,16 +102,6 @@ public class ChartServlet extends HttpServlet { private static final Duration DEFAULT_PERIOD = Duration.ofDays(1); - private static final Map PERIODS = Map.ofEntries( // - entry("h", Duration.ofHours(1)), entry("4h", Duration.ofHours(4)), // - entry("8h", Duration.ofHours(8)), entry("12h", Duration.ofHours(12)), // - entry("D", Duration.ofDays(1)), entry("2D", Duration.ofDays(2)), // - entry("3D", Duration.ofDays(3)), entry("W", Duration.ofDays(7)), // - entry("2W", Duration.ofDays(14)), entry("M", Duration.ofDays(30)), // - entry("2M", Duration.ofDays(60)), entry("4M", Duration.ofDays(120)), // - entry("Y", Duration.ofDays(365))// - ); - protected static final Map CHART_PROVIDERS = new ConcurrentHashMap<>(); @Activate @@ -233,7 +224,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse res) throws Ser } // Read out the parameter period, begin and end and save them. - Duration period = periodParam == null ? DEFAULT_PERIOD : PERIODS.getOrDefault(periodParam, DEFAULT_PERIOD); + TemporalAmount period = convertToTemporalAmount(periodParam, DEFAULT_PERIOD); ZonedDateTime timeBegin = null; ZonedDateTime timeEnd = null; @@ -359,4 +350,37 @@ public void init(@Nullable ServletConfig config) throws ServletException { @Override public void destroy() { } + + public static TemporalAmount convertToTemporalAmount(@Nullable String periodParam, TemporalAmount defaultPeriod) { + TemporalAmount period = defaultPeriod; + String convertedPeriod = convertPeriodToISO8601(periodParam); + if (convertedPeriod != null) { + boolean failed = false; + try { + period = Period.parse(convertedPeriod); + } catch (DateTimeParseException e) { + failed = true; + } + if (failed) { + try { + period = Duration.parse(convertedPeriod); + } catch (DateTimeParseException e) { + // Ignored + } + } + } + return period; + } + + private static @Nullable String convertPeriodToISO8601(@Nullable String period) { + if (period == null || period.startsWith("P") || !(period.endsWith("h") || period.endsWith("D") + || period.endsWith("W") || period.endsWith("M") || period.endsWith("Y"))) { + return period; + } + String newPeriod = period.length() == 1 ? "1" + period : period; + if (newPeriod.endsWith("h")) { + newPeriod = "T" + newPeriod.replace("h", "H"); + } + return "P" + newPeriod; + } } diff --git a/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/chart/ChartServletPeriodParamTest.java b/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/chart/ChartServletPeriodParamTest.java new file mode 100644 index 00000000000..31e0d642d2c --- /dev/null +++ b/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/chart/ChartServletPeriodParamTest.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2010-2023 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.ui.internal.chart; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAmount; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class ChartServletPeriodParamTest { + + @Test + public void convertToTemporalAmountFromNull() { + TemporalAmount period = ChartServlet.convertToTemporalAmount(null, Duration.ZERO); + assertEquals(0, period.get(ChronoUnit.SECONDS)); + } + + @Test + public void convertToTemporalAmountFromHours() { + TemporalAmount period = ChartServlet.convertToTemporalAmount("h", Duration.ZERO); + assertEquals(1 * 60 * 60, period.get(ChronoUnit.SECONDS)); + + period = ChartServlet.convertToTemporalAmount("12h", Duration.ZERO); + assertEquals(12 * 60 * 60, period.get(ChronoUnit.SECONDS)); + } + + @Test + public void convertToTemporalAmountFromDays() { + TemporalAmount period = ChartServlet.convertToTemporalAmount("D", Duration.ZERO); + assertEquals(1, period.get(ChronoUnit.DAYS)); + assertEquals(0, period.get(ChronoUnit.MONTHS)); + assertEquals(0, period.get(ChronoUnit.YEARS)); + + period = ChartServlet.convertToTemporalAmount("4D", Duration.ZERO); + assertEquals(4, period.get(ChronoUnit.DAYS)); + assertEquals(0, period.get(ChronoUnit.MONTHS)); + assertEquals(0, period.get(ChronoUnit.YEARS)); + } + + @Test + public void convertToTemporalAmountFromWeeks() { + TemporalAmount period = ChartServlet.convertToTemporalAmount("W", Duration.ZERO); + assertEquals(7, period.get(ChronoUnit.DAYS)); + assertEquals(0, period.get(ChronoUnit.MONTHS)); + assertEquals(0, period.get(ChronoUnit.YEARS)); + + period = ChartServlet.convertToTemporalAmount("2W", Duration.ZERO); + assertEquals(14, period.get(ChronoUnit.DAYS)); + assertEquals(0, period.get(ChronoUnit.MONTHS)); + assertEquals(0, period.get(ChronoUnit.YEARS)); + } + + @Test + public void convertToTemporalAmountFromMonths() { + TemporalAmount period = ChartServlet.convertToTemporalAmount("M", Duration.ZERO); + assertEquals(0, period.get(ChronoUnit.DAYS)); + assertEquals(1, period.get(ChronoUnit.MONTHS)); + assertEquals(0, period.get(ChronoUnit.YEARS)); + + period = ChartServlet.convertToTemporalAmount("3M", Duration.ZERO); + assertEquals(0, period.get(ChronoUnit.DAYS)); + assertEquals(3, period.get(ChronoUnit.MONTHS)); + assertEquals(0, period.get(ChronoUnit.YEARS)); + } + + @Test + public void convertToTemporalAmountFromYears() { + TemporalAmount period = ChartServlet.convertToTemporalAmount("Y", Duration.ZERO); + assertEquals(0, period.get(ChronoUnit.DAYS)); + assertEquals(0, period.get(ChronoUnit.MONTHS)); + assertEquals(1, period.get(ChronoUnit.YEARS)); + + period = ChartServlet.convertToTemporalAmount("2Y", Duration.ZERO); + assertEquals(0, period.get(ChronoUnit.DAYS)); + assertEquals(0, period.get(ChronoUnit.MONTHS)); + assertEquals(2, period.get(ChronoUnit.YEARS)); + } + + @Test + public void convertToTemporalAmountFromISO8601() { + TemporalAmount period = ChartServlet.convertToTemporalAmount("P2Y3M4D", Duration.ZERO); + assertEquals(4, period.get(ChronoUnit.DAYS)); + assertEquals(3, period.get(ChronoUnit.MONTHS)); + assertEquals(2, period.get(ChronoUnit.YEARS)); + + period = ChartServlet.convertToTemporalAmount("P1DT12H30M15S", Duration.ZERO); + assertEquals(36 * 60 * 60 + 30 * 60 + 15, period.get(ChronoUnit.SECONDS)); + } +} From 32237a9bdc861e94554fc3c56ec8be02490744eb Mon Sep 17 00:00:00 2001 From: joerg1985 <16140691+joerg1985@users.noreply.github.com> Date: Fri, 24 Nov 2023 22:24:37 +0100 Subject: [PATCH 006/135] Do not leak running pools from the internal collection (#3885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörg Sautter --- .../core/common/ThreadPoolManager.java | 59 ++++++++----------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/common/ThreadPoolManager.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/common/ThreadPoolManager.java index 42e3076f6a1..69c51d2989d 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/common/ThreadPoolManager.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/common/ThreadPoolManager.java @@ -18,7 +18,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import java.util.WeakHashMap; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -74,7 +73,7 @@ public class ThreadPoolManager { protected static final long THREAD_TIMEOUT = 65L; protected static final long THREAD_MONITOR_SLEEP = 60000; - protected static Map pools = new WeakHashMap<>(); + protected static Map pools = new ConcurrentHashMap<>(); private static Map configs = new ConcurrentHashMap<>(); @@ -124,23 +123,17 @@ protected void modified(Map properties) { * @return an instance to use */ public static ScheduledExecutorService getScheduledPool(String poolName) { - ExecutorService pool = pools.get(poolName); - if (pool == null) { - synchronized (pools) { - // do a double check if it is still null or if another thread might have created it meanwhile - pool = pools.get(poolName); - if (pool == null) { - int cfg = getConfig(poolName); - pool = new WrappedScheduledExecutorService(cfg, - new NamedThreadFactory(poolName, true, Thread.NORM_PRIORITY)); - ((ThreadPoolExecutor) pool).setKeepAliveTime(THREAD_TIMEOUT, TimeUnit.SECONDS); - ((ThreadPoolExecutor) pool).allowCoreThreadTimeOut(true); - ((ScheduledThreadPoolExecutor) pool).setRemoveOnCancelPolicy(true); - pools.put(poolName, pool); - LOGGER.debug("Created scheduled thread pool '{}' of size {}", poolName, cfg); - } - } - } + ExecutorService pool = pools.computeIfAbsent(poolName, (name) -> { + int cfg = getConfig(name); + ScheduledThreadPoolExecutor executor = new WrappedScheduledExecutorService(cfg, + new NamedThreadFactory(name, true, Thread.NORM_PRIORITY)); + executor.setKeepAliveTime(THREAD_TIMEOUT, TimeUnit.SECONDS); + executor.allowCoreThreadTimeOut(true); + executor.setRemoveOnCancelPolicy(true); + LOGGER.debug("Created scheduled thread pool '{}' of size {}", name, cfg); + return executor; + }); + if (pool instanceof ScheduledExecutorService service) { return new UnstoppableScheduledExecutorService(poolName, service); } else { @@ -156,21 +149,15 @@ public static ScheduledExecutorService getScheduledPool(String poolName) { * @return an instance to use */ public static ExecutorService getPool(String poolName) { - ExecutorService pool = pools.get(poolName); - if (pool == null) { - synchronized (pools) { - // do a double check if it is still null or if another thread might have created it meanwhile - pool = pools.get(poolName); - if (pool == null) { - int cfg = getConfig(poolName); - pool = QueueingThreadPoolExecutor.createInstance(poolName, cfg); - ((ThreadPoolExecutor) pool).setKeepAliveTime(THREAD_TIMEOUT, TimeUnit.SECONDS); - ((ThreadPoolExecutor) pool).allowCoreThreadTimeOut(true); - pools.put(poolName, pool); - LOGGER.debug("Created thread pool '{}' with size {}", poolName, cfg); - } - } - } + ExecutorService pool = pools.computeIfAbsent(poolName, (name) -> { + int cfg = getConfig(name); + ThreadPoolExecutor executor = QueueingThreadPoolExecutor.createInstance(name, cfg); + executor.setKeepAliveTime(THREAD_TIMEOUT, TimeUnit.SECONDS); + executor.allowCoreThreadTimeOut(true); + LOGGER.debug("Created thread pool '{}' with size {}", name, cfg); + return executor; + }); + return new UnstoppableExecutorService<>(poolName, pool); } @@ -179,9 +166,9 @@ static ThreadPoolExecutor getPoolUnwrapped(String poolName) { return (ThreadPoolExecutor) ret.getDelegate(); } - static ThreadPoolExecutor getScheduledPoolUnwrapped(String poolName) { + static ScheduledThreadPoolExecutor getScheduledPoolUnwrapped(String poolName) { UnstoppableExecutorService ret = (UnstoppableScheduledExecutorService) getScheduledPool(poolName); - return (ThreadPoolExecutor) ret.getDelegate(); + return (ScheduledThreadPoolExecutor) ret.getDelegate(); } protected static int getConfig(String poolName) { From 3624682c1e2574d395b9fd0e81592404365722b8 Mon Sep 17 00:00:00 2001 From: Wouter Born Date: Sun, 26 Nov 2023 21:26:19 +0100 Subject: [PATCH 007/135] Add JavaDoc build badge and reconfigure plugin to indicate JavaDoc warnings/errors (#3886) * Add JavaDoc build badge * Add dependency to upgradetool to fix remaining JavaDoc error * Fail JavaDoc build on JavaDoc warnings/errors * Upgrade maven-javadoc-plugin to fix it failures even if there are no warnings/errors * Enable legacyMode to workaround JPMS issues in newer maven-javadoc-plugin versions Signed-off-by: Wouter Born --- README.md | 1 + pom.xml | 6 ++++-- tools/upgradetool/pom.xml | 5 +++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 06b306d29ac..1ce5ff03b59 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![GitHub Actions Build Status](https://github.com/openhab/openhab-core/actions/workflows/ci-build.yml/badge.svg?branch=main)](https://github.com/openhab/openhab-core/actions/workflows/ci-build.yml) [![Jenkins Build Status](https://ci.openhab.org/job/openHAB-Core/badge/icon)](https://ci.openhab.org/job/openHAB-Core/) +[![JavaDoc Build Status](https://ci.openhab.org/view/Documentation/job/openHAB-JavaDoc/badge/icon?subject=javadoc)](https://ci.openhab.org/view/Documentation/job/openHAB-JavaDoc/) [![EPL-2.0](https://img.shields.io/badge/license-EPL%202-green.svg)](https://opensource.org/licenses/EPL-2.0) [![Crowdin](https://badges.crowdin.net/openhab-core/localized.svg)](https://crowdin.com/project/openhab-core) diff --git a/pom.xml b/pom.xml index 51128460317..dde28aa8d3a 100644 --- a/pom.xml +++ b/pom.xml @@ -326,10 +326,12 @@ Import-Package: \\ org.apache.maven.plugins maven-javadoc-plugin - 3.2.0 + 3.6.2 - !${quality.skip} + true + true none + true *.internal,*.internal.* diff --git a/tools/upgradetool/pom.xml b/tools/upgradetool/pom.xml index 890c02c14b2..a0c99143202 100644 --- a/tools/upgradetool/pom.xml +++ b/tools/upgradetool/pom.xml @@ -38,6 +38,11 @@ commons-cli 1.5.0 + + org.osgi + org.osgi.service.component.annotations + 1.5.0 + org.slf4j slf4j-simple From 7186bf85ff2eba3a1a0fb06de53b5ed249736d8f Mon Sep 17 00:00:00 2001 From: J-N-K Date: Sun, 3 Dec 2023 20:11:56 +0100 Subject: [PATCH 008/135] Add time-series support for websockets (#3889) Signed-off-by: Jan N. Klug --- .../io/websocket/event/EventWebSocket.java | 6 ++ .../io/websocket/event/ItemEventUtility.java | 65 +++++++++++++++++-- .../io/websocket/ItemEventUtilityTest.java | 18 +++++ 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventWebSocket.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventWebSocket.java index f0136dc3532..c104233dfbb 100644 --- a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventWebSocket.java +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/EventWebSocket.java @@ -123,6 +123,12 @@ public void onText(String message) { responseEvent = new EventDTO(WEBSOCKET_EVENT_TYPE, WEBSOCKET_TOPIC_PREFIX + "response/success", "", null, eventDTO.eventId); break; + case "ItemTimeSeriesEvent": + Event itemTimeseriesEvent = itemEventUtility.createTimeSeriesEvent(eventDTO); + eventPublisher.post(itemTimeseriesEvent); + responseEvent = new EventDTO(WEBSOCKET_EVENT_TYPE, WEBSOCKET_TOPIC_PREFIX + "response/success", + "", null, eventDTO.eventId); + break; case WEBSOCKET_EVENT_TYPE: if ((WEBSOCKET_TOPIC_PREFIX + "heartbeat").equals(eventDTO.topic) && "PING".equals(eventDTO.payload)) { diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/ItemEventUtility.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/ItemEventUtility.java index 547e161d0a0..14d2e8825b9 100644 --- a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/ItemEventUtility.java +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/event/ItemEventUtility.java @@ -12,6 +12,8 @@ */ package org.openhab.core.io.websocket.event; +import java.time.Instant; +import java.time.format.DateTimeParseException; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,6 +28,7 @@ import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; +import org.openhab.core.types.TimeSeries; import org.openhab.core.types.Type; import org.openhab.core.types.TypeParser; import org.openhab.core.types.UnDefType; @@ -78,6 +81,13 @@ public Event createStateEvent(EventDTO eventDTO) throws EventProcessingException throw new EventProcessingException("Incompatible datatype, rejected."); } + public Event createTimeSeriesEvent(EventDTO eventDTO) throws EventProcessingException { + Matcher matcher = getTopicMatcher(eventDTO.topic, "timeseries"); + Item item = getItem(matcher.group("entity")); + TimeSeries timeSeries = parseTimeSeries(eventDTO.payload); + return ItemEventFactory.createTimeSeriesEvent(item.getName(), timeSeries, eventDTO.source); + } + private Matcher getTopicMatcher(@Nullable String topic, String action) throws EventProcessingException { if (topic == null) { throw new EventProcessingException("Topic must not be null"); @@ -102,6 +112,36 @@ private Item getItem(String itemName) throws EventProcessingException { } } + private TimeSeries parseTimeSeries(@Nullable String payload) throws EventProcessingException { + ItemTimeSeriesEventPayloadBean bean = null; + try { + bean = gson.fromJson(payload, ItemTimeSeriesEventPayloadBean.class); + } catch (JsonParseException ignored) { + } + if (bean == null) { + throw new EventProcessingException("Failed to deserialize payload '" + payload + "'."); + } + + TimeSeries timeSeries = new TimeSeries(TimeSeries.Policy.valueOf(bean.policy)); + + for (ItemTimeSeriesEventPayloadBean.TimeSeriesPayload timeSeriesPayload : bean.timeSeries) { + Type value = parseType(timeSeriesPayload.type, timeSeriesPayload.value); + if (value instanceof State state) { + try { + Instant timestamp = Instant.parse(timeSeriesPayload.timestamp); + timeSeries.add(timestamp, state); + } catch (DateTimeParseException e) { + throw new EventProcessingException( + "Could not parse '" + timeSeriesPayload.timestamp + "' to an instant."); + } + } else { + throw new EventProcessingException("Only states are allowed in timeseries events."); + } + } + + return timeSeries; + } + private Type parseType(@Nullable String payload) throws EventProcessingException { ItemEventPayloadBean bean = null; try { @@ -112,20 +152,24 @@ private Type parseType(@Nullable String payload) throws EventProcessingException throw new EventProcessingException("Failed to deserialize payload '" + payload + "'."); } - String simpleClassName = bean.type + TYPE_POSTFIX; + return parseType(bean.type, bean.value); + } + + private Type parseType(String type, String value) throws EventProcessingException { + String simpleClassName = type + TYPE_POSTFIX; Type returnType; if (simpleClassName.equals(UnDefType.class.getSimpleName())) { - returnType = UnDefType.valueOf(bean.value); + returnType = UnDefType.valueOf(value); } else if (simpleClassName.equals(RefreshType.class.getSimpleName())) { - returnType = RefreshType.valueOf(bean.value); + returnType = RefreshType.valueOf(value); } else { - returnType = TypeParser.parseType(simpleClassName, bean.value); + returnType = TypeParser.parseType(simpleClassName, value); } if (returnType == null) { throw new EventProcessingException( - "Error parsing simpleClassName '" + simpleClassName + "' with value '" + bean.value + "'."); + "Error parsing simpleClassName '" + simpleClassName + "' with value '" + value + "'."); } return returnType; @@ -135,4 +179,15 @@ private static class ItemEventPayloadBean { public @NonNullByDefault({}) String type; public @NonNullByDefault({}) String value; } + + private static class ItemTimeSeriesEventPayloadBean { + private @NonNullByDefault({}) List timeSeries; + private @NonNullByDefault({}) String policy; + + private static class TimeSeriesPayload { + private @NonNullByDefault({}) String type; + private @NonNullByDefault({}) String value; + private @NonNullByDefault({}) String timestamp; + } + } } diff --git a/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/ItemEventUtilityTest.java b/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/ItemEventUtilityTest.java index 63ac9bc63bb..81932a97828 100644 --- a/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/ItemEventUtilityTest.java +++ b/bundles/org.openhab.core.io.websocket/src/test/java/org/openhab/core/io/websocket/ItemEventUtilityTest.java @@ -18,6 +18,8 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; +import java.time.Instant; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,10 +36,13 @@ import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.events.ItemEvent; import org.openhab.core.items.events.ItemEventFactory; +import org.openhab.core.items.events.ItemTimeSeriesEvent; import org.openhab.core.library.items.StringItem; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.StringType; +import org.openhab.core.types.TimeSeries; import com.google.gson.Gson; @@ -171,4 +176,17 @@ public void invalidCommandEventPayload() { () -> itemEventUtility.createCommandEvent(eventDTO)); assertThat(e.getMessage(), is("Failed to deserialize payload 'invalidNoJson'.")); } + + @Test + public void validTimeSeriesEvent() throws EventProcessingException { + TimeSeries timeSeries = new TimeSeries(TimeSeries.Policy.REPLACE); + timeSeries.add(Instant.now(), OnOffType.ON); + timeSeries.add(Instant.now().plusSeconds(5), OnOffType.OFF); + ItemTimeSeriesEvent event = ItemEventFactory.createTimeSeriesEvent(EXISTING_ITEM_NAME, timeSeries, null); + EventDTO eventDTO = new EventDTO(event); + + Event itemEvent = itemEventUtility.createTimeSeriesEvent(eventDTO); + + assertThat(itemEvent, is(event)); + } } From 9a9726b8fe34573fb610f1457a9264d4460ee2e9 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Sun, 3 Dec 2023 20:14:35 +0100 Subject: [PATCH 009/135] [ColorUtil] Extend rgbToHsb(PercentType[]) for RGBW (#3882) * [ColorUtil] Extend rgbToHsb(PercentType[]) for RGBW rgbToHsb(PercentType) supports arrays of size 4 (RGBW) in addition to and arrays of size 3 (RGB). Signed-off-by: Holger Friedrich --- .../java/org/openhab/core/util/ColorUtil.java | 36 +++++++++++-------- .../org/openhab/core/util/ColorUtilTest.java | 20 +++++++++++ 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/util/ColorUtil.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/util/ColorUtil.java index 4652cf50906..f881406ccba 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/util/ColorUtil.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/util/ColorUtil.java @@ -286,40 +286,43 @@ public static double[] hsbToXY(HSBType hsb, Gamut gamut) { */ public static HSBType rgbToHsb(int[] rgbw) throws IllegalArgumentException { if (rgbw.length == 4) { - return rgbwToHsb(rgbw); + if (!inByteRange(rgbw[0]) || !inByteRange(rgbw[1]) || !inByteRange(rgbw[2]) || !inByteRange(rgbw[3])) { + throw new IllegalArgumentException("rgbToHsb requires 3 or 4 values between 0 and 255"); + } + return rgbwToHsb(new PercentType[] { convertByteToColorPercent(rgbw[0]), convertByteToColorPercent(rgbw[1]), + convertByteToColorPercent(rgbw[2]), convertByteToColorPercent(rgbw[3]) }); } if (rgbw.length != 3 || !inByteRange(rgbw[0]) || !inByteRange(rgbw[1]) || !inByteRange(rgbw[2])) { - throw new IllegalArgumentException("RGB array only allows values between 0 and 255"); + throw new IllegalArgumentException("rgbToHsb requires 3 or 4 values between 0 and 255"); } return rgbToHsb(new PercentType[] { convertByteToColorPercent(rgbw[0]), convertByteToColorPercent(rgbw[1]), convertByteToColorPercent(rgbw[2]) }); } /** - * Transform HSV based {@link HSBType} to RGBW. + * Transform RGBW to HSV based {@link HSBType}. * * See Converting RGB to RGBW. * - * This function does not round the components. For conversion to integer values in the range 0 to 255 use - * {@link #hsbToRgb(HSBType)}. - * * See also: {@link #hsbToRgb(HSBType)}, {@link #hsbTosRgb(HSBType)}, {@link #hsbToRgbPercent(HSBType)} * * @param rgbw array of four int with the RGBW values in the range 0 to 255. * @return hsb an {@link HSBType} value. * */ - private static HSBType rgbwToHsb(int[] rgbw) { - if (rgbw.length != 4 || !inByteRange(rgbw[0]) || !inByteRange(rgbw[1]) || !inByteRange(rgbw[2]) - || !inByteRange(rgbw[3])) { - throw new IllegalArgumentException("RGBW array only allows values between 0 and 255 with 4 values"); + private static HSBType rgbwToHsb(PercentType[] rgbw) { + if (rgbw.length != 4) { + throw new IllegalArgumentException("RGBW requires 4 values"); } - BigDecimal luminance = BigDecimal.valueOf(rgbw[3]); - BigDecimal inRed = BigDecimal.valueOf(rgbw[0]).add(luminance); - BigDecimal inGreen = BigDecimal.valueOf(rgbw[1]).add(luminance); - BigDecimal inBlue = BigDecimal.valueOf(rgbw[2]).add(luminance); + BigDecimal luminance = BigDecimal.valueOf(rgbw[3].doubleValue() / PercentType.HUNDRED.doubleValue() * 255.0); + BigDecimal inRed = BigDecimal.valueOf(rgbw[0].doubleValue() / PercentType.HUNDRED.doubleValue() * 255.0) + .add(luminance); + BigDecimal inGreen = BigDecimal.valueOf(rgbw[1].doubleValue() / PercentType.HUNDRED.doubleValue() * 255.0) + .add(luminance); + BigDecimal inBlue = BigDecimal.valueOf(rgbw[2].doubleValue() / PercentType.HUNDRED.doubleValue() * 255.0) + .add(luminance); // Get the maximum between R, G, and B final BigDecimal maxColor = BIG_DECIMAL_255.min(inRed.max(inGreen.max(inBlue)).max(BigDecimal.ZERO)); @@ -342,11 +345,14 @@ private static HSBType rgbwToHsb(int[] rgbw) { * Transform sRGB color format to * HSV based {@link HSBType}. * - * @param rgb array of three {@link PercentType} with the RGB values in the range 0 to 100 percent. + * @param rgb array of three or four {@link PercentType} with the RGB(W) values in the range 0 to 100 percent. * @return the corresponding {@link HSBType}. * @throws IllegalArgumentException when input array has wrong size or exceeds allowed value range. */ public static HSBType rgbToHsb(PercentType[] rgb) throws IllegalArgumentException { + if (rgb.length == 4) { + return rgbwToHsb(rgb); + } if (rgb.length != 3) { throw new IllegalArgumentException("RGB array needs exactly three values!"); } diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/util/ColorUtilTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/util/ColorUtilTest.java index 1f02ffa9aa7..aac0a5d5afe 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/util/ColorUtilTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/util/ColorUtilTest.java @@ -103,26 +103,46 @@ public void rgbwToHsbTest() { HSBType hsb = ColorUtil.rgbToHsb(new int[] { 255, 0, 0, 0 }); int[] convertedRgb = ColorUtil.hsbToRgb(hsb); assertRgbEquals(new int[] { 255, 0, 0 }, convertedRgb); + hsb = ColorUtil.rgbToHsb( + new PercentType[] { new PercentType(100), new PercentType(0), new PercentType(0), new PercentType(0) }); + convertedRgb = ColorUtil.hsbToRgb(hsb); + assertRgbEquals(new int[] { 255, 0, 0 }, convertedRgb); // Test Green hsb = ColorUtil.rgbToHsb(new int[] { 0, 255, 0, 0 }); convertedRgb = ColorUtil.hsbToRgb(hsb); assertRgbEquals(new int[] { 0, 255, 0 }, convertedRgb); + hsb = ColorUtil.rgbToHsb( + new PercentType[] { new PercentType(0), new PercentType(100), new PercentType(0), new PercentType(0) }); + convertedRgb = ColorUtil.hsbToRgb(hsb); + assertRgbEquals(new int[] { 0, 255, 0 }, convertedRgb); // Test Blue hsb = ColorUtil.rgbToHsb(new int[] { 0, 0, 255, 0 }); convertedRgb = ColorUtil.hsbToRgb(hsb); assertRgbEquals(new int[] { 0, 0, 255 }, convertedRgb); + hsb = ColorUtil.rgbToHsb( + new PercentType[] { new PercentType(0), new PercentType(0), new PercentType(100), new PercentType(0) }); + convertedRgb = ColorUtil.hsbToRgb(hsb); + assertRgbEquals(new int[] { 0, 0, 255 }, convertedRgb); // Test White hsb = ColorUtil.rgbToHsb(new int[] { 0, 0, 0, 255 }); convertedRgb = ColorUtil.hsbToRgb(hsb); assertRgbEquals(new int[] { 255, 255, 255 }, convertedRgb); + hsb = ColorUtil.rgbToHsb( + new PercentType[] { new PercentType(0), new PercentType(0), new PercentType(0), new PercentType(100) }); + convertedRgb = ColorUtil.hsbToRgb(hsb); + assertRgbEquals(new int[] { 255, 255, 255 }, convertedRgb); // Test Black hsb = ColorUtil.rgbToHsb(new int[] { 0, 0, 0, 0 }); convertedRgb = ColorUtil.hsbToRgb(hsb); assertRgbEquals(new int[] { 0, 0, 0 }, convertedRgb); + hsb = ColorUtil.rgbToHsb( + new PercentType[] { new PercentType(0), new PercentType(0), new PercentType(0), new PercentType(0) }); + convertedRgb = ColorUtil.hsbToRgb(hsb); + assertRgbEquals(new int[] { 0, 0, 0 }, convertedRgb); } @ParameterizedTest From aa305d90d04c14f09c2a19d38579bc50bfe29bc5 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Mon, 4 Dec 2023 10:56:33 +0100 Subject: [PATCH 010/135] Fix AutoUpdatePolicy for channel (#3888) * Fix AutoUpdatePolicy for channel Fixes #3887 Signed-off-by: Jacob Laursen * Simplify setting of auto update policy Signed-off-by: Jacob Laursen --------- Signed-off-by: Jacob Laursen --- .../core/thing/binding/builder/ChannelBuilder.java | 2 +- .../openhab/core/thing/internal/ThingFactoryHelper.java | 8 +++++++- .../core/thing/binding/builder/ChannelBuilderTest.java | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/builder/ChannelBuilder.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/builder/ChannelBuilder.java index b7703a19ba6..94cbbf40b7c 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/builder/ChannelBuilder.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/builder/ChannelBuilder.java @@ -96,7 +96,7 @@ public static ChannelBuilder create(Channel channel) { ChannelBuilder channelBuilder = create(channel.getUID(), channel.getAcceptedItemType()) .withConfiguration(channel.getConfiguration()).withDefaultTags(channel.getDefaultTags()) .withKind(channel.getKind()).withProperties(channel.getProperties()) - .withType(channel.getChannelTypeUID()); + .withType(channel.getChannelTypeUID()).withAutoUpdatePolicy(channel.getAutoUpdatePolicy()); String label = channel.getLabel(); if (label != null) { channelBuilder.withLabel(label); diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingFactoryHelper.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingFactoryHelper.java index 3322f845f0b..b0300ca2209 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingFactoryHelper.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingFactoryHelper.java @@ -26,6 +26,7 @@ import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.ThingFactory; import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.type.AutoUpdatePolicy; import org.openhab.core.thing.type.ChannelDefinition; import org.openhab.core.thing.type.ChannelGroupDefinition; import org.openhab.core.thing.type.ChannelGroupType; @@ -184,12 +185,17 @@ public static ChannelBuilder createChannelBuilder(ChannelUID channelUID, Channel label = channelType.getLabel(); } + AutoUpdatePolicy autoUpdatePolicy = channelDefinition.getAutoUpdatePolicy(); + if (autoUpdatePolicy == null) { + autoUpdatePolicy = channelType.getAutoUpdatePolicy(); + } + final ChannelBuilder channelBuilder = ChannelBuilder.create(channelUID, channelType.getItemType()) // .withType(channelType.getUID()) // .withDefaultTags(channelType.getTags()) // .withKind(channelType.getKind()) // .withLabel(label) // - .withAutoUpdatePolicy(channelType.getAutoUpdatePolicy()); + .withAutoUpdatePolicy(autoUpdatePolicy); String description = channelDefinition.getDescription(); if (description == null) { diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/builder/ChannelBuilderTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/builder/ChannelBuilderTest.java index efa439da8e1..5d8bc561faa 100644 --- a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/builder/ChannelBuilderTest.java +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/binding/builder/ChannelBuilderTest.java @@ -84,6 +84,7 @@ public void testChannelBuilderFromChannel() { assertThat(otherChannel.getDescription(), is(channel.getDescription())); assertThat(otherChannel.getKind(), is(channel.getKind())); assertThat(otherChannel.getLabel(), is(channel.getLabel())); + assertThat(otherChannel.getAutoUpdatePolicy(), is(channel.getAutoUpdatePolicy())); assertThat(otherChannel.getProperties().size(), is(channel.getProperties().size())); assertThat(otherChannel.getProperties().get(KEY1), is(channel.getProperties().get(KEY1))); assertThat(otherChannel.getProperties().get(KEY2), is(channel.getProperties().get(KEY2))); From cc9b70516ae4c785b3df815325a61fd83bacacf5 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Tue, 5 Dec 2023 15:49:31 +0000 Subject: [PATCH 011/135] AddonInfo extensions (#3865) Signed-off-by: Andrew Fiddian-Green --- .../schema/addon-1.0.0.xsd | 82 +++++--- .../schema/addon-info-list-1.0.0.xsd | 23 ++ .../core/addon/AddonDiscoveryMethod.java | 81 +++++++ .../org/openhab/core/addon/AddonInfo.java | 36 +++- .../org/openhab/core/addon/AddonInfoList.java | 38 ++++ .../openhab/core/addon/AddonInfoProvider.java | 6 +- .../openhab/core/addon/AddonInfoRegistry.java | 79 ++++++- .../core/addon/AddonMatchProperty.java | 73 +++++++ .../xml/AddonDiscoveryMethodConverter.java | 61 ++++++ .../xml/AddonInfoAddonsXmlProvider.java | 128 +++++++++++ .../internal/xml/AddonInfoConverter.java | 7 + .../internal/xml/AddonInfoListConverter.java | 57 +++++ .../internal/xml/AddonInfoListReader.java | 102 +++++++++ .../addon/internal/xml/AddonInfoReader.java | 15 +- .../xml/AddonMatchPropertyConverter.java | 52 +++++ .../core/addon/AddonInfoListReaderTest.java | 117 +++++++++++ .../addon/AddonInfoRegistryMergeTest.java | 198 ++++++++++++++++++ .../core/xml/util/XmlDocumentReader.java | 15 ++ .../core/addon/xml/test/AddonInfoTest.java | 33 ++- .../OH-INF/addon/addon.xml | 17 ++ 20 files changed, 1172 insertions(+), 48 deletions(-) create mode 100644 bundles/org.openhab.core.addon/schema/addon-info-list-1.0.0.xsd create mode 100644 bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonDiscoveryMethod.java create mode 100644 bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoList.java create mode 100644 bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonMatchProperty.java create mode 100644 bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonDiscoveryMethodConverter.java create mode 100644 bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoAddonsXmlProvider.java create mode 100644 bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoListConverter.java create mode 100644 bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoListReader.java create mode 100644 bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonMatchPropertyConverter.java create mode 100644 bundles/org.openhab.core.addon/src/test/java/org/openhab/core/addon/AddonInfoListReaderTest.java create mode 100644 bundles/org.openhab.core.addon/src/test/java/org/openhab/core/addon/AddonInfoRegistryMergeTest.java diff --git a/bundles/org.openhab.core.addon/schema/addon-1.0.0.xsd b/bundles/org.openhab.core.addon/schema/addon-1.0.0.xsd index 21142133d17..c32e01a70e4 100644 --- a/bundles/org.openhab.core.addon/schema/addon-1.0.0.xsd +++ b/bundles/org.openhab.core.addon/schema/addon-1.0.0.xsd @@ -7,35 +7,36 @@ - - - - - - - - - - Comma-separated list of two-letter ISO country codes. - - - - - The ID (service.pid or component.name) of the main add-on service, which can be configured through OSGi configuration admin service. Should only be used in combination with a config description definition. The default value is <type>.<name> - - - - - - - - + + + + + + + + + + + Comma-separated list of two-letter ISO country codes. + + + - The id is used to construct the UID of this add-on to <type>-<name> + The ID (service.pid or component.name) of the main add-on service, which can be configured through OSGi configuration admin service. Should only be used in combination with a config description definition. The default value is <type>.<name> - - - + + + + + + + + + + The id is used to construct the UID of this add-on to <type>-<name> + + + @@ -80,4 +81,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.addon/schema/addon-info-list-1.0.0.xsd b/bundles/org.openhab.core.addon/schema/addon-info-list-1.0.0.xsd new file mode 100644 index 00000000000..4f720dc9815 --- /dev/null +++ b/bundles/org.openhab.core.addon/schema/addon-info-list-1.0.0.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonDiscoveryMethod.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonDiscoveryMethod.java new file mode 100644 index 00000000000..88eb8669819 --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonDiscoveryMethod.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2023 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.addon; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * DTO for serialization of a suggested addon discovery method. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class AddonDiscoveryMethod { + private @NonNullByDefault({}) String serviceType; + private @Nullable String mdnsServiceType; + private @Nullable List matchProperties; + + public String getServiceType() { + return serviceType.toLowerCase(); + } + + public String getMdnsServiceType() { + String mdnsServiceType = this.mdnsServiceType; + return mdnsServiceType != null ? mdnsServiceType : ""; + } + + public List getMatchProperties() { + List matchProperties = this.matchProperties; + return matchProperties != null ? matchProperties : List.of(); + } + + public AddonDiscoveryMethod setServiceType(String serviceType) { + this.serviceType = serviceType.toLowerCase(); + return this; + } + + public AddonDiscoveryMethod setMdnsServiceType(@Nullable String mdnsServiceType) { + this.mdnsServiceType = mdnsServiceType; + return this; + } + + public AddonDiscoveryMethod setMatchProperties(@Nullable List matchProperties) { + this.matchProperties = matchProperties; + return this; + } + + @Override + public int hashCode() { + return Objects.hash(serviceType, mdnsServiceType, matchProperties); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AddonDiscoveryMethod other = (AddonDiscoveryMethod) obj; + return Objects.equals(serviceType, other.serviceType) && Objects.equals(mdnsServiceType, other.mdnsServiceType) + && Objects.equals(matchProperties, other.matchProperties); + } +} diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfo.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfo.java index 8de5d828f70..8e45c47c918 100644 --- a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfo.java +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfo.java @@ -37,6 +37,7 @@ public class AddonInfo implements Identifiable { private final String id; private final String type; + private final String uid; private final String name; private final String description; private final @Nullable String connection; @@ -44,10 +45,12 @@ public class AddonInfo implements Identifiable { private final @Nullable String configDescriptionURI; private final String serviceId; private @Nullable String sourceBundle; + private @Nullable List discoveryMethods; - private AddonInfo(String id, String type, String name, String description, @Nullable String connection, - List countries, @Nullable String configDescriptionURI, @Nullable String serviceId, - @Nullable String sourceBundle) throws IllegalArgumentException { + private AddonInfo(String id, String type, @Nullable String uid, String name, String description, + @Nullable String connection, List countries, @Nullable String configDescriptionURI, + @Nullable String serviceId, @Nullable String sourceBundle, + @Nullable List discoveryMethods) throws IllegalArgumentException { // mandatory fields if (id.isBlank()) { throw new IllegalArgumentException("The ID must neither be null nor empty!"); @@ -64,6 +67,7 @@ private AddonInfo(String id, String type, String name, String description, @Null } this.id = id; this.type = type; + this.uid = uid != null ? uid : type + Addon.ADDON_SEPARATOR + id; this.name = name; this.description = description; @@ -73,6 +77,7 @@ private AddonInfo(String id, String type, String name, String description, @Null this.configDescriptionURI = configDescriptionURI; this.serviceId = Objects.requireNonNullElse(serviceId, type + "." + id); this.sourceBundle = sourceBundle; + this.discoveryMethods = discoveryMethods; } /** @@ -82,7 +87,7 @@ private AddonInfo(String id, String type, String name, String description, @Null */ @Override public String getUID() { - return type + Addon.ADDON_SEPARATOR + id; + return uid; } /** @@ -142,6 +147,11 @@ public List getCountries() { return countries; } + public List getDiscoveryMethods() { + List discoveryMethods = this.discoveryMethods; + return discoveryMethods != null ? discoveryMethods : List.of(); + } + public static Builder builder(String id, String type) { return new Builder(id, type); } @@ -154,6 +164,7 @@ public static class Builder { private final String id; private final String type; + private @Nullable String uid; private String name = ""; private String description = ""; private @Nullable String connection; @@ -161,6 +172,7 @@ public static class Builder { private @Nullable String configDescriptionURI = ""; private @Nullable String serviceId; private @Nullable String sourceBundle; + private @Nullable List discoveryMethods; private Builder(String id, String type) { this.id = id; @@ -170,6 +182,7 @@ private Builder(String id, String type) { private Builder(AddonInfo addonInfo) { this.id = addonInfo.id; this.type = addonInfo.type; + this.uid = addonInfo.uid; this.name = addonInfo.name; this.description = addonInfo.description; this.connection = addonInfo.connection; @@ -177,6 +190,12 @@ private Builder(AddonInfo addonInfo) { this.configDescriptionURI = addonInfo.configDescriptionURI; this.serviceId = addonInfo.serviceId; this.sourceBundle = addonInfo.sourceBundle; + this.discoveryMethods = addonInfo.discoveryMethods; + } + + public Builder withUID(@Nullable String uid) { + this.uid = uid; + return this; } public Builder withName(String name) { @@ -219,6 +238,11 @@ public Builder withSourceBundle(@Nullable String sourceBundle) { return this; } + public Builder withDiscoveryMethods(@Nullable List discoveryMethods) { + this.discoveryMethods = discoveryMethods; + return this; + } + /** * Build an {@link AddonInfo} from this builder * @@ -226,8 +250,8 @@ public Builder withSourceBundle(@Nullable String sourceBundle) { * @throws IllegalArgumentException if any of the information in this builder is invalid */ public AddonInfo build() throws IllegalArgumentException { - return new AddonInfo(id, type, name, description, connection, countries, configDescriptionURI, serviceId, - sourceBundle); + return new AddonInfo(id, type, uid, name, description, connection, countries, configDescriptionURI, + serviceId, sourceBundle, discoveryMethods); } } } diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoList.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoList.java new file mode 100644 index 00000000000..6638d9dec49 --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoList.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2023 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.addon; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * DTO containing a list of {@code AddonInfo} + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class AddonInfoList { + protected @Nullable List addons; + + public List getAddons() { + List addons = this.addons; + return addons != null ? addons : List.of(); + } + + public AddonInfoList setAddons(@Nullable List addons) { + this.addons = addons; + return this; + } +} diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoProvider.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoProvider.java index edd8b095883..b444451afb4 100644 --- a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoProvider.java +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoProvider.java @@ -31,15 +31,15 @@ public interface AddonInfoProvider { /** - * Returns the binding information for the specified binding ID and locale (language), + * Returns the binding information for the specified binding UID and locale (language), * or {@code null} if no binding information could be found. * - * @param id the ID to be looked for (could be null or empty) + * @param uid the UID to be looked for (could be null or empty) * @param locale the locale to be used for the binding information (could be null) * @return a localized binding information object (could be null) */ @Nullable - AddonInfo getAddonInfo(@Nullable String id, @Nullable Locale locale); + AddonInfo getAddonInfo(@Nullable String uid, @Nullable Locale locale); /** * Returns all binding information in the specified locale (language) this provider contains. diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoRegistry.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoRegistry.java index 678c329ca2d..338a3cbfad6 100644 --- a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoRegistry.java +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonInfoRegistry.java @@ -13,10 +13,13 @@ package org.openhab.core.addon; import java.util.Collection; +import java.util.HashSet; import java.util.Locale; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BinaryOperator; import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -44,34 +47,90 @@ protected void addAddonInfoProvider(AddonInfoProvider addonInfoProvider) { addonInfoProviders.add(addonInfoProvider); } - protected void removeAddonInfoProvider(AddonInfoProvider addonInfoProvider) { + public void removeAddonInfoProvider(AddonInfoProvider addonInfoProvider) { addonInfoProviders.remove(addonInfoProvider); } /** - * Returns the add-on information for the specified add-on ID, or {@code null} if no add-on information could be + * Returns the add-on information for the specified add-on UID, or {@code null} if no add-on information could be * found. * - * @param id the ID to be looked + * @param uid the UID to be looked * @return a add-on information object (could be null) */ - public @Nullable AddonInfo getAddonInfo(String id) { - return getAddonInfo(id, null); + public @Nullable AddonInfo getAddonInfo(String uid) { + return getAddonInfo(uid, null); } /** - * Returns the add-on information for the specified add-on ID and locale (language), + * Returns the add-on information for the specified add-on UID and locale (language), * or {@code null} if no add-on information could be found. + *

+ * If more than one provider provides information for the specified add-on UID and locale, + * it returns a new {@link AddonInfo} containing merged information from all such providers. * - * @param id the ID to be looked for + * @param uid the UID to be looked for * @param locale the locale to be used for the add-on information (could be null) * @return a localized add-on information object (could be null) */ - public @Nullable AddonInfo getAddonInfo(String id, @Nullable Locale locale) { - return addonInfoProviders.stream().map(p -> p.getAddonInfo(id, locale)).filter(Objects::nonNull).findAny() - .orElse(null); + public @Nullable AddonInfo getAddonInfo(String uid, @Nullable Locale locale) { + return addonInfoProviders.stream().map(p -> p.getAddonInfo(uid, locale)).filter(Objects::nonNull) + .collect(Collectors.groupingBy(a -> a == null ? "" : a.getUID(), + Collectors.collectingAndThen(Collectors.reducing(mergeAddonInfos), Optional::get))) + .get(uid); } + /** + * A {@link BinaryOperator} to merge the field values from two {@link AddonInfo} objects into a third such object. + *

+ * If the first object has a non-null field value the result object takes the first value, or if the second object + * has a non-null field value the result object takes the second value. Otherwise the field remains null. + * + * @param a the first {@link AddonInfo} (could be null) + * @param b the second {@link AddonInfo} (could be null) + * @return a new {@link AddonInfo} containing the combined field values (could be null) + */ + private static BinaryOperator<@Nullable AddonInfo> mergeAddonInfos = (a, b) -> { + if (a == null) { + return b; + } else if (b == null) { + return a; + } + AddonInfo.Builder builder = AddonInfo.builder(a); + if (a.getDescription().isEmpty()) { + builder.withDescription(b.getDescription()); + } + if (a.getConnection() == null && b.getConnection() != null) { + builder.withConnection(b.getConnection()); + } + Set countries = new HashSet<>(a.getCountries()); + countries.addAll(b.getCountries()); + if (!countries.isEmpty()) { + builder.withCountries(countries.stream().toList()); + } + String aConfigDescriptionURI = a.getConfigDescriptionURI(); + if (aConfigDescriptionURI == null || aConfigDescriptionURI.isEmpty() && b.getConfigDescriptionURI() != null) { + builder.withConfigDescriptionURI(b.getConfigDescriptionURI()); + } + if (a.getSourceBundle() == null && b.getSourceBundle() != null) { + builder.withSourceBundle(b.getSourceBundle()); + } + String defaultServiceId = a.getType() + "." + a.getId(); + if (defaultServiceId.equals(a.getServiceId()) && !defaultServiceId.equals(b.getServiceId())) { + builder.withServiceId(b.getServiceId()); + } + String defaultUID = a.getType() + Addon.ADDON_SEPARATOR + a.getId(); + if (defaultUID.equals(a.getUID()) && !defaultUID.equals(b.getUID())) { + builder.withUID(b.getUID()); + } + Set discoveryMethods = new HashSet<>(a.getDiscoveryMethods()); + discoveryMethods.addAll(b.getDiscoveryMethods()); + if (!discoveryMethods.isEmpty()) { + builder.withDiscoveryMethods(discoveryMethods.stream().toList()); + } + return builder.build(); + }; + /** * Returns all add-on information this registry contains. * diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonMatchProperty.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonMatchProperty.java new file mode 100644 index 00000000000..ac5ccebe9b3 --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/AddonMatchProperty.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2023 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.addon; + +import java.util.Objects; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * DTO for serialization of a property match regular expression. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class AddonMatchProperty { + private @NonNullByDefault({}) String name; + private @NonNullByDefault({}) String regex; + private transient @NonNullByDefault({}) Pattern pattern; + + public AddonMatchProperty(String name, String regex) { + this.name = name; + this.regex = regex; + this.pattern = null; + } + + public String getName() { + return name; + } + + public Pattern getPattern() { + Pattern pattern = this.pattern; + if (pattern == null) { + this.pattern = Pattern.compile(regex); + } + return this.pattern; + } + + public String getRegex() { + return regex; + } + + @Override + public int hashCode() { + return Objects.hash(name, regex); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AddonMatchProperty other = (AddonMatchProperty) obj; + return Objects.equals(name, other.name) && Objects.equals(regex, other.regex); + } +} diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonDiscoveryMethodConverter.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonDiscoveryMethodConverter.java new file mode 100644 index 00000000000..39110721aec --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonDiscoveryMethodConverter.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2023 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.addon.internal.xml; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.addon.AddonDiscoveryMethod; +import org.openhab.core.addon.AddonMatchProperty; +import org.openhab.core.config.core.xml.util.GenericUnmarshaller; +import org.openhab.core.config.core.xml.util.NodeIterator; + +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; + +/** + * The {@link AddonDiscoveryMethodConverter} is a concrete implementation of the {@code XStream} {@link Converter} + * interface used to convert add-on discovery method information within an XML document into a + * {@link AddonDiscoveryMethod} object. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class AddonDiscoveryMethodConverter extends GenericUnmarshaller { + + public AddonDiscoveryMethodConverter() { + super(AddonDiscoveryMethod.class); + } + + @Override + public @Nullable Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + List nodes = (List) context.convertAnother(context, List.class); + NodeIterator nodeIterator = new NodeIterator(nodes); + + String serviceType = requireNonEmpty((String) nodeIterator.nextValue("service-type", true), + "Service type is null or empty"); + + String mdnsServiceType = (String) nodeIterator.nextValue("mdns-service-type", false); + + Object object = nodeIterator.nextList("match-properties", false); + List matchProperties = !(object instanceof List list) ? null + : list.stream().filter(e -> (e instanceof AddonMatchProperty)).map(e -> ((AddonMatchProperty) e)) + .toList(); + + nodeIterator.assertEndOfType(); + + return new AddonDiscoveryMethod().setServiceType(serviceType).setMdnsServiceType(mdnsServiceType) + .setMatchProperties(matchProperties); + } +} diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoAddonsXmlProvider.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoAddonsXmlProvider.java new file mode 100644 index 00000000000..aeae88583bf --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoAddonsXmlProvider.java @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2010-2023 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.addon.internal.xml; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.OpenHAB; +import org.openhab.core.addon.AddonDiscoveryMethod; +import org.openhab.core.addon.AddonInfo; +import org.openhab.core.addon.AddonInfoProvider; +import org.openhab.core.addon.AddonMatchProperty; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.thoughtworks.xstream.XStreamException; +import com.thoughtworks.xstream.converters.ConversionException; + +/** + * The {@link AddonInfoAddonsXmlProvider} reads all {@code userdata/addons/*.xml} files, each of which + * should contain a list of {@code addon} elements, and convert their combined contents into a list + * of {@link AddonInfo} objects can be accessed via the {@link AddonInfoProvider} interface. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +@Component(service = AddonInfoProvider.class, name = AddonInfoAddonsXmlProvider.SERVICE_NAME) +public class AddonInfoAddonsXmlProvider implements AddonInfoProvider { + + public static final String SERVICE_NAME = "addons-info-provider"; + + private final Logger logger = LoggerFactory.getLogger(AddonInfoAddonsXmlProvider.class); + private final String folder = OpenHAB.getUserDataFolder() + File.separator + "addons"; + private final Set addonInfos = new HashSet<>(); + + @Activate + public AddonInfoAddonsXmlProvider() { + initialize(); + testAddonDeveloperRegexSyntax(); + } + + @Deactivate + public void deactivate() { + addonInfos.clear(); + } + + @Override + public @Nullable AddonInfo getAddonInfo(@Nullable String uid, @Nullable Locale locale) { + return addonInfos.stream().filter(a -> a.getUID().equals(uid)).findFirst().orElse(null); + } + + @Override + public Set getAddonInfos(@Nullable Locale locale) { + return addonInfos; + } + + private void initialize() { + AddonInfoListReader reader = new AddonInfoListReader(); + Stream.of(new File(folder).listFiles()).filter(f -> f.isFile() && f.getName().endsWith(".xml")).forEach(f -> { + try { + String xml = Files.readString(f.toPath()); + if (xml != null && !xml.isBlank()) { + addonInfos.addAll(reader.readFromXML(xml).getAddons().stream().collect(Collectors.toSet())); + } else { + logger.warn("File '{}' contents are null or empty", f.getName()); + } + } catch (IOException e) { + logger.warn("File '{}' could not be read", f.getName()); + } catch (ConversionException e) { + logger.warn("File '{}' has invalid content", f.getName()); + } catch (XStreamException e) { + logger.warn("File '{}' could not be deserialized", f.getName()); + } + }); + } + + /* + * The openhab-addons Maven build process checks individual developer addon.xml contributions + * against the 'addon-1.0.0.xsd' schema, but it can't check the discovery-method match-property + * regex syntax. Invalid regexes do throw exceptions at run-time, but the log can't identify the + * culprit addon. Ideally we need to add syntax checks to the Maven build; and this test is an + * interim solution. + */ + private void testAddonDeveloperRegexSyntax() { + List patternErrors = new ArrayList<>(); + for (AddonInfo addonInfo : addonInfos) { + for (AddonDiscoveryMethod discoveryMethod : addonInfo.getDiscoveryMethods()) { + for (AddonMatchProperty matchProperty : discoveryMethod.getMatchProperties()) { + try { + matchProperty.getPattern(); + } catch (PatternSyntaxException e) { + patternErrors.add(String.format( + "Regex syntax error in org.openhab.%s.%s addon.xml => %s in \"%s\" position %d", + addonInfo.getType(), addonInfo.getId(), e.getDescription(), e.getPattern(), + e.getIndex())); + } + } + } + } + if (!patternErrors.isEmpty()) { + logger.warn("The following errors were found:\n\t{}", String.join("\n\t", patternErrors)); + } + } +} diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoConverter.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoConverter.java index 257dfff682f..2e1db84409a 100644 --- a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoConverter.java +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoConverter.java @@ -18,6 +18,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.addon.AddonDiscoveryMethod; import org.openhab.core.addon.AddonInfo; import org.openhab.core.config.core.ConfigDescription; import org.openhab.core.config.core.ConfigDescriptionBuilder; @@ -37,6 +38,7 @@ * @author Michael Grammling - Initial contribution * @author Andre Fuechsel - Made author tag optional * @author Jan N. Klug - Refactored to cover all add-ons + * @author Andrew Fiddian-Green - Added discovery methods */ @NonNullByDefault public class AddonInfoConverter extends GenericUnmarshaller { @@ -107,6 +109,11 @@ public AddonInfoConverter() { addonInfo.withConfigDescriptionURI(configDescriptionURI); + Object object = nodeIterator.nextList("discovery-methods", false); + addonInfo.withDiscoveryMethods(!(object instanceof List list) ? null + : list.stream().filter(e -> (e instanceof AddonDiscoveryMethod)).map(e -> ((AddonDiscoveryMethod) e)) + .toList()); + nodeIterator.assertEndOfType(); // create object diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoListConverter.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoListConverter.java new file mode 100644 index 00000000000..60a93ad0027 --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoListConverter.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2023 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.addon.internal.xml; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.addon.AddonInfo; +import org.openhab.core.addon.AddonInfoList; +import org.openhab.core.config.core.xml.util.GenericUnmarshaller; +import org.openhab.core.config.core.xml.util.NodeIterator; + +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; + +/** + * The {@link AddonInfoListConverter} is a concrete implementation of the {@code XStream} {@link Converter} + * interface used to convert a list of add-on information within an XML document into a list of {@link AddonInfo} + * objects. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class AddonInfoListConverter extends GenericUnmarshaller { + + public AddonInfoListConverter() { + super(AddonInfoList.class); + } + + @Override + public @Nullable Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + List nodes = (List) context.convertAnother(context, List.class); + NodeIterator nodeIterator = new NodeIterator(nodes); + + Object object = nodeIterator.nextList("addons", false); + List addons = (object instanceof List list) + ? list.stream().filter(e -> e != null).filter(e -> (e instanceof AddonInfoXmlResult)) + .map(e -> (AddonInfoXmlResult) e).map(r -> r.addonInfo()).toList() + : null; + + nodeIterator.assertEndOfType(); + + return new AddonInfoList().setAddons(addons); + } +} diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoListReader.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoListReader.java new file mode 100644 index 00000000000..a733e169492 --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoListReader.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2010-2023 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.addon.internal.xml; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.addon.AddonDiscoveryMethod; +import org.openhab.core.addon.AddonInfoList; +import org.openhab.core.addon.AddonMatchProperty; +import org.openhab.core.config.core.ConfigDescription; +import org.openhab.core.config.core.ConfigDescriptionParameter; +import org.openhab.core.config.core.ConfigDescriptionParameterGroup; +import org.openhab.core.config.core.FilterCriteria; +import org.openhab.core.config.core.xml.ConfigDescriptionConverter; +import org.openhab.core.config.core.xml.ConfigDescriptionParameterConverter; +import org.openhab.core.config.core.xml.ConfigDescriptionParameterGroupConverter; +import org.openhab.core.config.core.xml.FilterCriteriaConverter; +import org.openhab.core.config.core.xml.util.NodeAttributes; +import org.openhab.core.config.core.xml.util.NodeAttributesConverter; +import org.openhab.core.config.core.xml.util.NodeList; +import org.openhab.core.config.core.xml.util.NodeListConverter; +import org.openhab.core.config.core.xml.util.NodeValue; +import org.openhab.core.config.core.xml.util.NodeValueConverter; +import org.openhab.core.config.core.xml.util.XmlDocumentReader; + +import com.thoughtworks.xstream.XStream; + +/** + * The {@link AddonInfoListReader} reads XML documents, which contain the {@code addon} XML tag, and converts them to + * a List of {@link AddonInfoXmlResult} objects. + *

+ * This reader uses {@code XStream} and {@code StAX} to parse and convert the XML document. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class AddonInfoListReader extends XmlDocumentReader { + + /** + * The default constructor of this class. + */ + public AddonInfoListReader() { + ClassLoader classLoader = AddonInfoListReader.class.getClassLoader(); + if (classLoader != null) { + super.setClassLoader(classLoader); + } + } + + @Override + protected void registerConverters(XStream xstream) { + xstream.registerConverter(new NodeAttributesConverter()); + xstream.registerConverter(new NodeListConverter()); + xstream.registerConverter(new NodeValueConverter()); + xstream.registerConverter(new AddonInfoListConverter()); + xstream.registerConverter(new AddonInfoConverter()); + xstream.registerConverter(new ConfigDescriptionConverter()); + xstream.registerConverter(new ConfigDescriptionParameterConverter()); + xstream.registerConverter(new ConfigDescriptionParameterGroupConverter()); + xstream.registerConverter(new FilterCriteriaConverter()); + xstream.registerConverter(new AddonDiscoveryMethodConverter()); + xstream.registerConverter(new AddonMatchPropertyConverter()); + } + + @Override + protected void registerAliases(XStream xstream) { + xstream.alias("addon-info-list", AddonInfoList.class); + xstream.alias("addons", NodeList.class); + xstream.alias("addon", AddonInfoXmlResult.class); + xstream.alias("name", NodeValue.class); + xstream.alias("description", NodeValue.class); + xstream.alias("type", NodeValue.class); + xstream.alias("connection", NodeValue.class); + xstream.alias("countries", NodeValue.class); + xstream.alias("config-description", ConfigDescription.class); + xstream.alias("config-description-ref", NodeAttributes.class); + xstream.alias("parameter", ConfigDescriptionParameter.class); + xstream.alias("parameter-group", ConfigDescriptionParameterGroup.class); + xstream.alias("options", NodeList.class); + xstream.alias("option", NodeValue.class); + xstream.alias("filter", List.class); + xstream.alias("criteria", FilterCriteria.class); + xstream.alias("service-id", NodeValue.class); + xstream.alias("discovery-methods", NodeList.class); + xstream.alias("discovery-method", AddonDiscoveryMethod.class); + xstream.alias("service-type", NodeValue.class); + xstream.alias("mdns-service-type", NodeValue.class); + xstream.alias("match-properties", NodeList.class); + xstream.alias("match-property", AddonMatchProperty.class); + xstream.alias("regex", NodeValue.class); + } +} diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoReader.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoReader.java index a33cdd09cd8..47cad85e943 100644 --- a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoReader.java +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonInfoReader.java @@ -15,6 +15,8 @@ import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.addon.AddonDiscoveryMethod; +import org.openhab.core.addon.AddonMatchProperty; import org.openhab.core.config.core.ConfigDescription; import org.openhab.core.config.core.ConfigDescriptionParameter; import org.openhab.core.config.core.ConfigDescriptionParameterGroup; @@ -26,6 +28,7 @@ import org.openhab.core.config.core.xml.util.NodeAttributes; import org.openhab.core.config.core.xml.util.NodeAttributesConverter; import org.openhab.core.config.core.xml.util.NodeList; +import org.openhab.core.config.core.xml.util.NodeListConverter; import org.openhab.core.config.core.xml.util.NodeValue; import org.openhab.core.config.core.xml.util.NodeValueConverter; import org.openhab.core.config.core.xml.util.XmlDocumentReader; @@ -33,7 +36,7 @@ import com.thoughtworks.xstream.XStream; /** - * The {@link AddonInfoReader} reads XML documents, which contain the {@code binding} XML tag, + * The {@link AddonInfoReader} reads XML documents, which contain the {@code addon} XML tag, * and converts them to {@link AddonInfoXmlResult} objects. *

* This reader uses {@code XStream} and {@code StAX} to parse and convert the XML document. @@ -59,12 +62,15 @@ public AddonInfoReader() { @Override protected void registerConverters(XStream xstream) { xstream.registerConverter(new NodeAttributesConverter()); + xstream.registerConverter(new NodeListConverter()); xstream.registerConverter(new NodeValueConverter()); xstream.registerConverter(new AddonInfoConverter()); xstream.registerConverter(new ConfigDescriptionConverter()); xstream.registerConverter(new ConfigDescriptionParameterConverter()); xstream.registerConverter(new ConfigDescriptionParameterGroupConverter()); xstream.registerConverter(new FilterCriteriaConverter()); + xstream.registerConverter(new AddonDiscoveryMethodConverter()); + xstream.registerConverter(new AddonMatchPropertyConverter()); } @Override @@ -84,5 +90,12 @@ protected void registerAliases(XStream xstream) { xstream.alias("filter", List.class); xstream.alias("criteria", FilterCriteria.class); xstream.alias("service-id", NodeValue.class); + xstream.alias("discovery-methods", NodeList.class); + xstream.alias("discovery-method", AddonDiscoveryMethod.class); + xstream.alias("service-type", NodeValue.class); + xstream.alias("mdns-service-type", NodeValue.class); + xstream.alias("match-properties", NodeList.class); + xstream.alias("match-property", AddonMatchProperty.class); + xstream.alias("regex", NodeValue.class); } } diff --git a/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonMatchPropertyConverter.java b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonMatchPropertyConverter.java new file mode 100644 index 00000000000..f8f5321d9de --- /dev/null +++ b/bundles/org.openhab.core.addon/src/main/java/org/openhab/core/addon/internal/xml/AddonMatchPropertyConverter.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2023 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.addon.internal.xml; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.addon.AddonMatchProperty; +import org.openhab.core.config.core.xml.util.GenericUnmarshaller; +import org.openhab.core.config.core.xml.util.NodeIterator; + +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; + +/** + * The {@link AddonMatchPropertyConverter} is a concrete implementation of the {@code XStream} {@link Converter} + * interface used to convert add-on discovery method match property information within an XML document into a + * {@link AddonMatchProperty} object. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class AddonMatchPropertyConverter extends GenericUnmarshaller { + + public AddonMatchPropertyConverter() { + super(AddonMatchProperty.class); + } + + @Override + public @Nullable Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + List nodes = (List) context.convertAnother(context, List.class); + NodeIterator nodeIterator = new NodeIterator(nodes); + + String name = requireNonEmpty((String) nodeIterator.nextValue("name", true), "Name is null or empty"); + String regex = requireNonEmpty((String) nodeIterator.nextValue("regex", true), "Regex is null or empty"); + + nodeIterator.assertEndOfType(); + + return new AddonMatchProperty(name, regex); + } +} diff --git a/bundles/org.openhab.core.addon/src/test/java/org/openhab/core/addon/AddonInfoListReaderTest.java b/bundles/org.openhab.core.addon/src/test/java/org/openhab/core/addon/AddonInfoListReaderTest.java new file mode 100644 index 00000000000..66c8485230f --- /dev/null +++ b/bundles/org.openhab.core.addon/src/test/java/org/openhab/core/addon/AddonInfoListReaderTest.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2010-2023 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.addon; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.core.addon.internal.xml.AddonInfoListReader; + +/** + * JUnit tests for {@link AddonInfoListReader}. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +class AddonInfoListReaderTest { + + // @formatter:off + private final String testXml = + "" + + " " + + " automation" + + " Groovy Scripting" + + " This adds a Groovy script engine." + + " none" + + " " + + " " + + " mdns" + + " _printer._tcp.local." + + " " + + " " + + " rp" + + " .*" + + " " + + " " + + " ty" + + " hp (.*)" + + " " + + " " + + " " + + " " + + " upnp" + + " " + + " " + + " modelName" + + " Philips hue bridge" + + " " + + " " + + " " + + " " + + " " + + ""; + // @formatter:on + + @Test + void testAddonInfoListReader() { + AddonInfoList addons = null; + try { + AddonInfoListReader reader = new AddonInfoListReader(); + addons = reader.readFromXML(testXml); + } catch (Exception e) { + fail(e); + } + assertNotNull(addons); + List addonsInfos = addons.getAddons(); + assertEquals(1, addonsInfos.size()); + AddonInfo addon = addonsInfos.get(0); + assertNotNull(addon); + List discoveryMethods = addon.getDiscoveryMethods(); + assertNotNull(discoveryMethods); + assertEquals(2, discoveryMethods.size()); + + AddonDiscoveryMethod method = discoveryMethods.get(0); + assertNotNull(method); + assertEquals("mdns", method.getServiceType()); + assertEquals("_printer._tcp.local.", method.getMdnsServiceType()); + List matchProperties = method.getMatchProperties(); + assertNotNull(matchProperties); + assertEquals(2, matchProperties.size()); + AddonMatchProperty property = matchProperties.get(0); + assertNotNull(property); + assertEquals("rp", property.getName()); + assertEquals(".*", property.getRegex()); + assertTrue(property.getPattern().matcher("the cat sat on the mat").matches()); + + method = discoveryMethods.get(1); + assertNotNull(method); + assertEquals("upnp", method.getServiceType()); + assertEquals("", method.getMdnsServiceType()); + matchProperties = method.getMatchProperties(); + assertNotNull(matchProperties); + assertEquals(1, matchProperties.size()); + property = matchProperties.get(0); + assertNotNull(property); + assertEquals("modelName", property.getName()); + assertEquals("Philips hue bridge", property.getRegex()); + assertTrue(property.getPattern().matcher("Philips hue bridge").matches()); + } +} diff --git a/bundles/org.openhab.core.addon/src/test/java/org/openhab/core/addon/AddonInfoRegistryMergeTest.java b/bundles/org.openhab.core.addon/src/test/java/org/openhab/core/addon/AddonInfoRegistryMergeTest.java new file mode 100644 index 00000000000..335dc1012ec --- /dev/null +++ b/bundles/org.openhab.core.addon/src/test/java/org/openhab/core/addon/AddonInfoRegistryMergeTest.java @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2010-2023 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.addon; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; + +/** + * JUnit test for the {@link AddonInfoRegistry} merge function. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +@TestInstance(Lifecycle.PER_CLASS) +class AddonInfoRegistryMergeTest { + + private @Nullable AddonInfoProvider addonInfoProvider0; + private @Nullable AddonInfoProvider addonInfoProvider1; + private @Nullable AddonInfoProvider addonInfoProvider2; + + @BeforeAll + void beforeAll() { + addonInfoProvider0 = createAddonInfoProvider0(); + addonInfoProvider1 = createAddonInfoProvider1(); + addonInfoProvider2 = createAddonInfoProvider2(); + } + + private AddonInfoProvider createAddonInfoProvider0() { + AddonInfo addonInfo = AddonInfo.builder("hue", "binding").withName("name-zero") + .withDescription("description-zero").build(); + AddonInfoProvider provider = mock(AddonInfoProvider.class); + when(provider.getAddonInfo(anyString(), any(Locale.class))).thenReturn(null); + when(provider.getAddonInfo(anyString(), eq(null))).thenReturn(null); + when(provider.getAddonInfo(eq("binding-hue"), any(Locale.class))).thenReturn(addonInfo); + when(provider.getAddonInfo(eq("binding-hue"), eq(null))).thenReturn(null); + return provider; + } + + private AddonInfoProvider createAddonInfoProvider1() { + AddonDiscoveryMethod discoveryMethod = new AddonDiscoveryMethod().setServiceType("mdns") + .setMdnsServiceType("_hue._tcp.local."); + AddonInfo addonInfo = AddonInfo.builder("hue", "binding").withName("name-one") + .withDescription("description-one").withCountries("GB,NL").withConnection("local") + .withDiscoveryMethods(List.of(discoveryMethod)).build(); + AddonInfoProvider provider = mock(AddonInfoProvider.class); + when(provider.getAddonInfo(anyString(), any(Locale.class))).thenReturn(null); + when(provider.getAddonInfo(anyString(), eq(null))).thenReturn(null); + when(provider.getAddonInfo(eq("binding-hue"), any(Locale.class))).thenReturn(addonInfo); + when(provider.getAddonInfo(eq("binding-hue"), eq(null))).thenReturn(null); + return provider; + } + + private AddonInfoProvider createAddonInfoProvider2() { + AddonDiscoveryMethod discoveryMethod = new AddonDiscoveryMethod().setServiceType("upnp") + .setMatchProperties(List.of(new AddonMatchProperty("modelName", "Philips hue bridge"))); + AddonInfo addonInfo = AddonInfo.builder("hue", "binding").withName("name-two") + .withDescription("description-two").withCountries("DE,FR").withSourceBundle("source-bundle") + .withServiceId("service-id").withConfigDescriptionURI("http://www.openhab.org") + .withDiscoveryMethods(List.of(discoveryMethod)).build(); + AddonInfoProvider provider = mock(AddonInfoProvider.class); + when(provider.getAddonInfo(anyString(), any(Locale.class))).thenReturn(null); + when(provider.getAddonInfo(anyString(), eq(null))).thenReturn(null); + when(provider.getAddonInfo(eq("binding-hue"), any(Locale.class))).thenReturn(addonInfo); + when(provider.getAddonInfo(eq("binding-hue"), eq(null))).thenReturn(null); + return provider; + } + + /** + * Test fetching a single addon-info from the registry with no merging. + */ + @Test + void testGetOneAddonInfo() { + AddonInfoRegistry registry = new AddonInfoRegistry(); + assertNotNull(addonInfoProvider0); + registry.addAddonInfoProvider(Objects.requireNonNull(addonInfoProvider0)); + + AddonInfo addonInfo; + addonInfo = registry.getAddonInfo("aardvark", Locale.US); + assertNull(addonInfo); + addonInfo = registry.getAddonInfo("aardvark", null); + assertNull(addonInfo); + addonInfo = registry.getAddonInfo("binding-hue", null); + assertNull(addonInfo); + addonInfo = registry.getAddonInfo("binding-hue", Locale.US); + assertNotNull(addonInfo); + + assertEquals("hue", addonInfo.getId()); + assertEquals("binding", addonInfo.getType()); + assertEquals("binding-hue", addonInfo.getUID()); + assertTrue(addonInfo.getName().startsWith("name-")); + assertTrue(addonInfo.getDescription().startsWith("description-")); + assertNull(addonInfo.getSourceBundle()); + assertNotEquals("local", addonInfo.getConnection()); + assertEquals(0, addonInfo.getCountries().size()); + assertNotEquals("http://www.openhab.org", addonInfo.getConfigDescriptionURI()); + assertEquals("binding.hue", addonInfo.getServiceId()); + assertEquals(0, addonInfo.getDiscoveryMethods().size()); + } + + /** + * Test fetching two addon-info's from the registry with merging. + */ + @Test + void testMergeAddonInfos2() { + AddonInfoRegistry registry = new AddonInfoRegistry(); + assertNotNull(addonInfoProvider0); + registry.addAddonInfoProvider(Objects.requireNonNull(addonInfoProvider0)); + assertNotNull(addonInfoProvider1); + registry.addAddonInfoProvider(Objects.requireNonNull(addonInfoProvider1)); + + AddonInfo addonInfo; + addonInfo = registry.getAddonInfo("aardvark", Locale.US); + assertNull(addonInfo); + addonInfo = registry.getAddonInfo("aardvark", null); + assertNull(addonInfo); + addonInfo = registry.getAddonInfo("binding-hue", null); + assertNull(addonInfo); + addonInfo = registry.getAddonInfo("binding-hue", Locale.US); + assertNotNull(addonInfo); + + assertEquals("hue", addonInfo.getId()); + assertEquals("binding", addonInfo.getType()); + assertEquals("binding-hue", addonInfo.getUID()); + assertTrue(addonInfo.getName().startsWith("name-")); + assertTrue(addonInfo.getDescription().startsWith("description-")); + assertNull(addonInfo.getSourceBundle()); + assertEquals("local", addonInfo.getConnection()); + assertEquals(2, addonInfo.getCountries().size()); + assertNotEquals("http://www.openhab.org", addonInfo.getConfigDescriptionURI()); + assertEquals("binding.hue", addonInfo.getServiceId()); + assertEquals(1, addonInfo.getDiscoveryMethods().size()); + } + + /** + * Test fetching three addon-info's from the registry with full merging. + */ + @Test + void testMergeAddonInfos3() { + AddonInfoRegistry registry = new AddonInfoRegistry(); + assertNotNull(addonInfoProvider0); + registry.addAddonInfoProvider(Objects.requireNonNull(addonInfoProvider0)); + assertNotNull(addonInfoProvider1); + registry.addAddonInfoProvider(Objects.requireNonNull(addonInfoProvider1)); + assertNotNull(addonInfoProvider2); + registry.addAddonInfoProvider(Objects.requireNonNull(addonInfoProvider2)); + + AddonInfo addonInfo; + addonInfo = registry.getAddonInfo("aardvark", Locale.US); + assertNull(addonInfo); + addonInfo = registry.getAddonInfo("aardvark", null); + assertNull(addonInfo); + addonInfo = registry.getAddonInfo("binding-hue", null); + assertNull(addonInfo); + addonInfo = registry.getAddonInfo("binding-hue", Locale.US); + assertNotNull(addonInfo); + + assertEquals("hue", addonInfo.getId()); + assertEquals("binding", addonInfo.getType()); + assertEquals("binding-hue", addonInfo.getUID()); + assertTrue(addonInfo.getName().startsWith("name-")); + assertTrue(addonInfo.getDescription().startsWith("description-")); + assertEquals("source-bundle", addonInfo.getSourceBundle()); + assertEquals("local", addonInfo.getConnection()); + assertEquals(4, addonInfo.getCountries().size()); + assertEquals("http://www.openhab.org", addonInfo.getConfigDescriptionURI()); + assertEquals("service-id", addonInfo.getServiceId()); + assertEquals(2, addonInfo.getDiscoveryMethods().size()); + } +} diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/XmlDocumentReader.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/XmlDocumentReader.java index caa8bfd343a..5c2754c6f6f 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/XmlDocumentReader.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/xml/util/XmlDocumentReader.java @@ -19,6 +19,7 @@ import org.eclipse.jdt.annotation.Nullable; import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.XStreamException; import com.thoughtworks.xstream.converters.ConversionException; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.io.xml.StaxDriver; @@ -104,4 +105,18 @@ protected void configureSecurity(XStream xstream) { public @Nullable T readFromXML(URL xmlURL) throws ConversionException { return (@Nullable T) xstream.fromXML(xmlURL); } + + /** + * Reads the XML document containing a specific XML tag from the specified xml string and converts it to the + * according object. + * + * @param xml a string containing the XML document to be read. + * @return the conversion result object (could be null). + * @throws XStreamException if the object cannot be deserialized. + * @throws ConversionException if the specified document contains invalid content + */ + @SuppressWarnings("unchecked") + public @Nullable T readFromXML(String xml) throws ConversionException { + return (@Nullable T) xstream.fromXML(xml); + } } diff --git a/itests/org.openhab.core.addon.tests/src/main/java/org/openhab/core/addon/xml/test/AddonInfoTest.java b/itests/org.openhab.core.addon.tests/src/main/java/org/openhab/core/addon/xml/test/AddonInfoTest.java index 1531c2ab4b8..3e109b7a5a9 100644 --- a/itests/org.openhab.core.addon.tests/src/main/java/org/openhab/core/addon/xml/test/AddonInfoTest.java +++ b/itests/org.openhab.core.addon.tests/src/main/java/org/openhab/core/addon/xml/test/AddonInfoTest.java @@ -12,8 +12,12 @@ */ package org.openhab.core.addon.xml.test; -import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.URI; import java.util.List; @@ -24,8 +28,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.openhab.core.addon.AddonDiscoveryMethod; import org.openhab.core.addon.AddonInfo; import org.openhab.core.addon.AddonInfoRegistry; +import org.openhab.core.addon.AddonMatchProperty; import org.openhab.core.config.core.ConfigDescription; import org.openhab.core.config.core.ConfigDescriptionParameter; import org.openhab.core.config.core.ConfigDescriptionRegistry; @@ -66,6 +72,31 @@ public void assertThatAddonInfoIsReadProperly() throws Exception { assertThat(addonInfo.getDescription(), is("The hue Binding integrates the Philips hue system. It allows to control hue lights.")); assertThat(addonInfo.getName(), is("hue Binding")); + + List discoveryMethods = addonInfo.getDiscoveryMethods(); + assertNotNull(discoveryMethods); + assertEquals(2, discoveryMethods.size()); + + AddonDiscoveryMethod discoveryMethod = discoveryMethods.get(0); + assertNotNull(discoveryMethod); + assertEquals("mdns", discoveryMethod.getServiceType()); + assertEquals("_hue._tcp.local.", discoveryMethod.getMdnsServiceType()); + List properties = discoveryMethod.getMatchProperties(); + assertNotNull(properties); + assertEquals(0, properties.size()); + + discoveryMethod = discoveryMethods.get(1); + assertNotNull(discoveryMethod); + assertEquals("upnp", discoveryMethod.getServiceType()); + assertEquals("", discoveryMethod.getMdnsServiceType()); + properties = discoveryMethod.getMatchProperties(); + assertNotNull(properties); + assertEquals(1, properties.size()); + AddonMatchProperty property = properties.get(0); + assertNotNull(property); + assertEquals("modelName", property.getName()); + assertEquals("Philips hue bridge", property.getRegex()); + assertTrue(property.getPattern().matcher("Philips hue bridge").matches()); }); } diff --git a/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/BundleInfoTest.bundle/OH-INF/addon/addon.xml b/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/BundleInfoTest.bundle/OH-INF/addon/addon.xml index 4d351403227..9e5db0944bd 100644 --- a/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/BundleInfoTest.bundle/OH-INF/addon/addon.xml +++ b/itests/org.openhab.core.addon.tests/src/main/resources/test-bundle-pool/BundleInfoTest.bundle/OH-INF/addon/addon.xml @@ -30,4 +30,21 @@ + + + + mdns + _hue._tcp.local. + + + upnp + + + modelName + Philips hue bridge + + + + + From 76b10ac1c1c928aaa473843448c69b758291c5ee Mon Sep 17 00:00:00 2001 From: lolodomo Date: Tue, 5 Dec 2023 21:50:58 +0100 Subject: [PATCH 012/135] [Sitemap] Change syntax for Buttongrid sitemap element (#3898) Follow-up #3810 Location in the grid is now defined by a row number and a column number. Signed-off-by: Laurent Garnier --- .../io/rest/sitemap/internal/MappingDTO.java | 4 +++- .../rest/sitemap/internal/SitemapResource.java | 4 ++-- .../core/io/rest/sitemap/internal/WidgetDTO.java | 2 +- .../org/openhab/core/model/sitemap/Sitemap.xtext | 3 +-- .../components/UIComponentSitemapProvider.java | 16 +++++++++------- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/MappingDTO.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/MappingDTO.java index 585f2c5ce24..f5ffeb2a28d 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/MappingDTO.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/MappingDTO.java @@ -17,10 +17,12 @@ * * @author Kai Kreuzer - Initial contribution * @author Laurent Garnier - New fields position and icon + * @author Laurent Garnier - Replace field position by fields row and column */ public class MappingDTO { - public Integer position; + public Integer row; + public Integer column; public String command; public String label; public String icon; diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java index 0a6310aca47..55a46ea95e5 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java @@ -625,10 +625,10 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null bean.step = setpointWidget.getStep(); } if (widget instanceof Buttongrid buttonGridWidget) { - bean.columns = buttonGridWidget.getColumns(); for (Button button : buttonGridWidget.getButtons()) { MappingDTO mappingBean = new MappingDTO(); - mappingBean.position = button.getPosition(); + mappingBean.row = button.getRow(); + mappingBean.column = button.getColumn(); mappingBean.command = button.getCmd(); mappingBean.label = button.getLabel(); mappingBean.icon = button.getIcon(); diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java index ae475be9685..8f792578ff6 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetDTO.java @@ -27,6 +27,7 @@ * @author Mark herwege - New fields pattern, unit * @author Laurent Garnier - New field columns * @author Danny Baumann - New field labelSource + * @author Laurent Garnier - Remove field columns */ public class WidgetDTO { @@ -68,7 +69,6 @@ public class WidgetDTO { public String yAxisDecimalPattern; public Boolean legend; public Boolean forceAsItem; - public Integer columns; public String state; public EnrichedItemDTO item; diff --git a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext index 4b443dd23d6..b0a6660f384 100644 --- a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext +++ b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext @@ -182,7 +182,6 @@ Buttongrid: (('icon=' icon=Icon) | ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | ('staticIcon=' staticIcon=Icon))? & - ('columns=' columns=INT) & ('buttons=[' buttons+=Button (',' buttons+=Button)* ']') & ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & @@ -201,7 +200,7 @@ Default: ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); Button: - position=INT ':' cmd=Command '=' label=(ID | STRING) ('=' icon=Icon)?; + row=INT ':' column=INT ':' cmd=Command '=' label=(ID | STRING) ('=' icon=Icon)?; Mapping: cmd=Command '=' label=(ID | STRING) ('=' icon=Icon)?; diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java index 31036ae544f..832e957ac52 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java @@ -269,7 +269,6 @@ protected Sitemap buildSitemap(RootUIComponent rootComponent) { ButtongridImpl buttongridWidget = (ButtongridImpl) SitemapFactory.eINSTANCE.createButtongrid(); addWidgetButtons(buttongridWidget.getButtons(), component); widget = buttongridWidget; - setWidgetPropertyFromComponentConfig(widget, component, "columns", SitemapPackage.BUTTONGRID__COLUMNS); break; case "Default": DefaultImpl defaultWidget = (DefaultImpl) SitemapFactory.eINSTANCE.createDefault(); @@ -378,13 +377,16 @@ private void addWidgetButtons(EList