diff --git a/bundles/org.opensmarthouse.core.i18n.core/src/test/java/org/openhab/core/internal/i18n/I18nExceptionTest.java b/bundles/org.opensmarthouse.core.i18n.core/src/test/java/org/openhab/core/internal/i18n/I18nExceptionTest.java new file mode 100644 index 00000000000..57435ee23d6 --- /dev/null +++ b/bundles/org.opensmarthouse.core.i18n.core/src/test/java/org/openhab/core/internal/i18n/I18nExceptionTest.java @@ -0,0 +1,203 @@ +/** + * Copyright (c) 2010-2021 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.internal.i18n; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.i18n.AbstractI18nException; +import org.openhab.core.i18n.CommunicationException; +import org.openhab.core.i18n.TranslationProvider; +import org.osgi.framework.Bundle; + +/** + * The {@link I18nExceptionTest} tests all the functionalities of the {@link AbstractI18nException} class. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +public class I18nExceptionTest { + + private static final String PARAM1 = "ABC"; + private static final int PARAM2 = 50; + + private static final String MSG = "hardcoded message"; + + private static final String KEY1 = "key1"; + private static final String MSG_KEY1 = "@text/" + KEY1; + private static final String RAW_MSG_KEY1 = MSG_KEY1; + private static final String MSG_KEY1_EN = "This is an exception."; + private static final String MSG_KEY1_FR = "Ceci est une exception."; + + private static final String KEY2 = "key2"; + private static final String MSG_KEY2 = "@text/" + KEY2; + private static final String RAW_MSG_KEY2 = String.format("@text/%s [ \"%s\", \"%d\" ]", KEY2, PARAM1, PARAM2); + private static final String MSG_KEY2_EN = String.format("%s: value %d.", PARAM1, PARAM2); + private static final String MSG_KEY2_FR = String.format("%s: valeur %d.", PARAM1, PARAM2); + + private static final String KEY3 = "key3"; + private static final String MSG_KEY3 = "@text/" + KEY3; + private static final String RAW_MSG_KEY3 = String.format("@text/%s [ \"%d\" ]", KEY3, PARAM2); + private static final String MSG_KEY3_EN = String.format("Value %d.", PARAM2); + private static final String MSG_KEY3_FR = String.format("Valeur %d.", PARAM2); + + private static final String CAUSE = "Here is the root cause."; + + private @Nullable @Mock Bundle bundle; + + TranslationProvider i18nProvider = new TranslationProvider() { + @Override + public @Nullable String getText(@Nullable Bundle bundle, @Nullable String key, @Nullable String defaultText, + @Nullable Locale locale, @Nullable Object @Nullable... arguments) { + if (bundle != null) { + if (KEY1.equals(key)) { + return Locale.FRENCH.equals(locale) ? MSG_KEY1_FR : MSG_KEY1_EN; + } else if (KEY2.equals(key)) { + return Locale.FRENCH.equals(locale) ? MSG_KEY2_FR : MSG_KEY2_EN; + } else if (KEY3.equals(key)) { + return Locale.FRENCH.equals(locale) ? MSG_KEY3_FR : MSG_KEY3_EN; + } + } + return null; + } + + @Override + public @Nullable String getText(@Nullable Bundle bundle, @Nullable String key, @Nullable String defaultText, + @Nullable Locale locale) { + return null; + } + }; + + @Test + public void testMessageWithoutKey() { + CommunicationException exception = new CommunicationException(MSG); + + assertThat(exception.getMessage(), is(MSG)); + assertThat(exception.getLocalizedMessage(), is(MSG)); + assertThat(exception.getMessage(bundle, i18nProvider), is(MSG)); + assertThat(exception.getLocalizedMessage(bundle, i18nProvider, null), is(MSG)); + assertThat(exception.getLocalizedMessage(bundle, i18nProvider, Locale.FRENCH), is(MSG)); + assertThat(exception.getRawMessage(), is(MSG)); + assertNull(exception.getCause()); + } + + @Test + public void testMessageWithoutKeyAndWithCause() { + Exception exception0 = new Exception(CAUSE); + CommunicationException exception = new CommunicationException(MSG, exception0); + + assertThat(exception.getMessage(), is(MSG)); + assertThat(exception.getLocalizedMessage(), is(MSG)); + assertThat(exception.getMessage(bundle, i18nProvider), is(MSG)); + assertThat(exception.getLocalizedMessage(bundle, i18nProvider, null), is(MSG)); + assertThat(exception.getLocalizedMessage(bundle, i18nProvider, Locale.FRENCH), is(MSG)); + assertThat(exception.getRawMessage(), is(MSG)); + assertNotNull(exception.getCause()); + assertThat(exception.getCause().getMessage(), is(CAUSE)); + } + + @Test + public void testMessageWithKeyButMissingParams() { + CommunicationException exception = new CommunicationException(MSG_KEY1); + + assertNull(exception.getMessage()); + assertNull(exception.getLocalizedMessage()); + assertNull(exception.getMessage(bundle, null)); + assertNull(exception.getMessage(null, i18nProvider)); + assertNull(exception.getLocalizedMessage(bundle, null, Locale.FRENCH)); + assertNull(exception.getLocalizedMessage(null, i18nProvider, Locale.FRENCH)); + assertThat(exception.getRawMessage(), is(RAW_MSG_KEY1)); + assertNull(exception.getCause()); + } + + @Test + public void testMessageWithKeyNoParam() { + CommunicationException exception = new CommunicationException(MSG_KEY1); + + assertNull(exception.getMessage()); + assertNull(exception.getLocalizedMessage()); + assertThat(exception.getMessage(bundle, i18nProvider), is(MSG_KEY1_EN)); + assertThat(exception.getLocalizedMessage(bundle, i18nProvider, null), is(MSG_KEY1_EN)); + assertThat(exception.getLocalizedMessage(bundle, i18nProvider, Locale.FRENCH), is(MSG_KEY1_FR)); + assertThat(exception.getRawMessage(), is(RAW_MSG_KEY1)); + assertNull(exception.getCause()); + } + + @Test + public void testMessageWithKeyTwoParams() { + CommunicationException exception = new CommunicationException(MSG_KEY2, PARAM1, PARAM2); + + assertNull(exception.getMessage()); + assertNull(exception.getLocalizedMessage()); + assertThat(exception.getMessage(bundle, i18nProvider), is(MSG_KEY2_EN)); + assertThat(exception.getLocalizedMessage(bundle, i18nProvider, null), is(MSG_KEY2_EN)); + assertThat(exception.getLocalizedMessage(bundle, i18nProvider, Locale.FRENCH), is(MSG_KEY2_FR)); + assertThat(exception.getRawMessage(), is(RAW_MSG_KEY2)); + assertNull(exception.getCause()); + } + + @Test + public void testMessageWithKeyOneParam() { + CommunicationException exception = new CommunicationException(MSG_KEY3, PARAM2); + + assertNull(exception.getMessage()); + assertNull(exception.getLocalizedMessage()); + assertThat(exception.getMessage(bundle, i18nProvider), is(MSG_KEY3_EN)); + assertThat(exception.getLocalizedMessage(bundle, i18nProvider, null), is(MSG_KEY3_EN)); + assertThat(exception.getLocalizedMessage(bundle, i18nProvider, Locale.FRENCH), is(MSG_KEY3_FR)); + assertThat(exception.getRawMessage(), is(RAW_MSG_KEY3)); + assertNull(exception.getCause()); + } + + @Test + public void testMessageWithKeyAndWithCause() { + Exception exception0 = new Exception(CAUSE); + CommunicationException exception = new CommunicationException(MSG_KEY1, exception0); + + assertNull(exception.getMessage()); + assertNull(exception.getLocalizedMessage()); + assertThat(exception.getMessage(bundle, i18nProvider), is(MSG_KEY1_EN)); + assertThat(exception.getLocalizedMessage(bundle, i18nProvider, null), is(MSG_KEY1_EN)); + assertThat(exception.getLocalizedMessage(bundle, i18nProvider, Locale.FRENCH), is(MSG_KEY1_FR)); + assertThat(exception.getRawMessage(), is(RAW_MSG_KEY1)); + assertNotNull(exception.getCause()); + assertThat(exception.getCause().getMessage(), is(CAUSE)); + } + + @Test + public void testCauseOnly() { + Exception exception0 = new Exception(CAUSE); + CommunicationException exception = new CommunicationException(exception0); + + String expectedMsg = String.format("%s: %s", exception0.getClass().getName(), CAUSE); + + assertThat(exception.getMessage(), is(expectedMsg)); + assertThat(exception.getLocalizedMessage(), is(expectedMsg)); + assertThat(exception.getMessage(bundle, i18nProvider), is(expectedMsg)); + assertThat(exception.getLocalizedMessage(bundle, i18nProvider, null), is(expectedMsg)); + assertThat(exception.getLocalizedMessage(bundle, i18nProvider, Locale.FRENCH), is(expectedMsg)); + assertThat(exception.getRawMessage(), is(expectedMsg)); + assertNotNull(exception.getCause()); + assertThat(exception.getCause().getMessage(), is(CAUSE)); + } +} diff --git a/bundles/org.opensmarthouse.core.i18n/src/main/java/org/openhab/core/i18n/AbstractI18nException.java b/bundles/org.opensmarthouse.core.i18n/src/main/java/org/openhab/core/i18n/AbstractI18nException.java new file mode 100644 index 00000000000..cad5af01d18 --- /dev/null +++ b/bundles/org.opensmarthouse.core.i18n/src/main/java/org/openhab/core/i18n/AbstractI18nException.java @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2010-2021 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.i18n; + +import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.osgi.framework.Bundle; + +/** + * Provides an exception class for openHAB that incorporates support for internationalization + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractI18nException extends RuntimeException { + + private String msgKey; + private @Nullable Object @Nullable [] msgParams; + + /** + * + * @param message the exception message; use "@text/key" to reference "key" entry in the properties file + * @param msgParams the optional arguments of the message defined in the properties file + */ + public AbstractI18nException(String message, @Nullable Object @Nullable... msgParams) { + this(message, null, msgParams); + } + + /** + * + * @param message the exception message; use "@text/key" to reference "key" entry in the properties file + * @param cause the cause (which is saved for later retrieval by the getCause() method). A null value is permitted, + * and indicates that the cause is nonexistent or unknown. + * @param msgParams the optional arguments of the message defined in the properties file + */ + public AbstractI18nException(String message, @Nullable Throwable cause, @Nullable Object @Nullable... msgParams) { + super(I18nUtil.isConstant(message) ? null : message, cause); + if (I18nUtil.isConstant(message)) { + this.msgKey = I18nUtil.stripConstant(message); + this.msgParams = msgParams; + } else { + this.msgKey = ""; + } + } + + /** + * + * @param cause the cause (which is saved for later retrieval by the getCause() method). + */ + public AbstractI18nException(Throwable cause) { + super(cause); + this.msgKey = ""; + } + + /** + * Returns the detail message string of this exception. + * + * In case the message starts with "@text/" and the parameters bundle and i18nProvider are not null, the translation + * provider is used to look for the message key in the English properties file of the provided bundle. + * + * @param bundle the bundle containing the i18n properties + * @param i18nProvider the translation provider + * @return the detail message string of this exception instance (which may be null) + */ + public @Nullable String getMessage(@Nullable Bundle bundle, @Nullable TranslationProvider i18nProvider) { + return getLocalizedMessage(bundle, i18nProvider, Locale.ENGLISH); + } + + /** + * Returns a localized description of this exception. + * + * In case the message starts with "@text/" and the parameters bundle and i18nProvider are not null, the translation + * provider is used to look for the message key in the properties file of the provided bundle containing strings for + * the requested language. + * English language is considered if no language is provided. + * + * @param bundle the bundle containing the i18n properties + * @param i18nProvider the translation provider + * @param locale the language to use for localizing the message + * @return the localized description of this exception instance (which may be null) + */ + public @Nullable String getLocalizedMessage(@Nullable Bundle bundle, @Nullable TranslationProvider i18nProvider, + @Nullable Locale locale) { + if (msgKey.isBlank() || bundle == null || i18nProvider == null) { + return super.getMessage(); + } else { + return i18nProvider.getText(bundle, msgKey, null, locale != null ? locale : Locale.ENGLISH, msgParams); + } + } + + /** + * Provides the raw message + * + * If the message does not start with "@text/", it returns the same as the getMessage() method. + * If the message starts with "@text/" and no optional arguments are set, it returns a string of this + * kind: @text/key + * If the message starts with "@text/" and optional arguments are set, it returns a string of this kind: @text/key [ + * "param1", "param2" ] + * + * @return the raw message or null if the message is undefined + */ + public @Nullable String getRawMessage() { + if (msgKey.isBlank()) { + return super.getMessage(); + } + String result = "@text/" + msgKey; + Object @Nullable [] params = msgParams; + if (params != null && params.length > 0) { + result += Stream.of(params).map(param -> String.format("\"%s\"", param == null ? "" : param.toString())) + .collect(Collectors.joining(", ", " [ ", " ]")); + } + return result; + } +} diff --git a/bundles/org.opensmarthouse.core.i18n/src/main/java/org/openhab/core/i18n/CommunicationException.java b/bundles/org.opensmarthouse.core.i18n/src/main/java/org/openhab/core/i18n/CommunicationException.java new file mode 100644 index 00000000000..176c8211bac --- /dev/null +++ b/bundles/org.opensmarthouse.core.i18n/src/main/java/org/openhab/core/i18n/CommunicationException.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2021 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.i18n; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Provides an exception class for openHAB to be used in case of communication issues with a device + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class CommunicationException extends AbstractI18nException { + + private static final long serialVersionUID = 1L; + + public CommunicationException(String message, @Nullable Object @Nullable... msgParams) { + super(message, msgParams); + } + + public CommunicationException(String message, @Nullable Throwable cause, @Nullable Object @Nullable... msgParams) { + super(message, cause, msgParams); + } + + public CommunicationException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.opensmarthouse.core.i18n/src/main/java/org/openhab/core/i18n/ConfigurationException.java b/bundles/org.opensmarthouse.core.i18n/src/main/java/org/openhab/core/i18n/ConfigurationException.java new file mode 100644 index 00000000000..63ed15dbe19 --- /dev/null +++ b/bundles/org.opensmarthouse.core.i18n/src/main/java/org/openhab/core/i18n/ConfigurationException.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2021 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.i18n; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Provides an exception class for openHAB to be used in case of configuration issues + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class ConfigurationException extends AbstractI18nException { + + private static final long serialVersionUID = 1L; + + public ConfigurationException(String message, @Nullable Object @Nullable... msgParams) { + super(message, msgParams); + } + + public ConfigurationException(String message, @Nullable Throwable cause, @Nullable Object @Nullable... msgParams) { + super(message, cause, msgParams); + } + + public ConfigurationException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.opensmarthouse.core.i18n/src/main/java/org/openhab/core/i18n/ConnectionException.java b/bundles/org.opensmarthouse.core.i18n/src/main/java/org/openhab/core/i18n/ConnectionException.java new file mode 100644 index 00000000000..f5280dadc99 --- /dev/null +++ b/bundles/org.opensmarthouse.core.i18n/src/main/java/org/openhab/core/i18n/ConnectionException.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2021 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.i18n; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Provides an exception class for openHAB to be used in case of connection issues with a device + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class ConnectionException extends CommunicationException { + + private static final long serialVersionUID = 1L; + + public ConnectionException(String message, @Nullable Object @Nullable... msgParams) { + super(message, msgParams); + } + + public ConnectionException(String message, @Nullable Throwable cause, @Nullable Object @Nullable... msgParams) { + super(message, cause, msgParams); + } + + public ConnectionException(Throwable cause) { + super(cause); + } +}