Skip to content

Commit

Permalink
[core] Added basic exception classes which supports internationalizat…
Browse files Browse the repository at this point in the history
…ion (openhab#2549)

* New exception class that incorporates support for internationalization
* Add ConnectionException, CommunicationException and ConfigurationException

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
GitOrigin-RevId: 998ce26
  • Loading branch information
lolodomo authored and splatch committed Jul 12, 2023
1 parent ef6c66a commit 0ecd75d
Show file tree
Hide file tree
Showing 5 changed files with 448 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 0ecd75d

Please sign in to comment.