Added i18n feature for profiles (#785)

Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>
pull/808/head
Christoph Weitkamp 2019-05-09 07:50:56 +02:00 committed by Markus Rathgeb
parent 1996f69e6f
commit b5f33d341d
8 changed files with 350 additions and 28 deletions

View File

@ -14,9 +14,13 @@ package org.eclipse.smarthome.core.thing.internal.profiles;
import static org.eclipse.smarthome.core.thing.profiles.SystemProfiles.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -25,6 +29,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.library.CoreItemFactory;
import org.eclipse.smarthome.core.thing.Channel;
import org.eclipse.smarthome.core.thing.DefaultSystemChannelTypeProvider;
import org.eclipse.smarthome.core.thing.UID;
import org.eclipse.smarthome.core.thing.profiles.Profile;
import org.eclipse.smarthome.core.thing.profiles.ProfileAdvisor;
import org.eclipse.smarthome.core.thing.profiles.ProfileCallback;
@ -33,8 +38,12 @@ import org.eclipse.smarthome.core.thing.profiles.ProfileFactory;
import org.eclipse.smarthome.core.thing.profiles.ProfileType;
import org.eclipse.smarthome.core.thing.profiles.ProfileTypeProvider;
import org.eclipse.smarthome.core.thing.profiles.ProfileTypeUID;
import org.eclipse.smarthome.core.thing.profiles.i18n.ProfileTypeI18nLocalizationService;
import org.eclipse.smarthome.core.thing.type.ChannelType;
import org.eclipse.smarthome.core.thing.type.ChannelTypeRegistry;
import org.eclipse.smarthome.core.util.BundleResolver;
import org.osgi.framework.Bundle;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@ -47,12 +56,13 @@ import org.osgi.service.component.annotations.Reference;
* required profile type.
*
* @author Simon Kaufmann - Initial contribution
* @author Christoph Weitkamp - Added translation for profile labels
*/
@NonNullByDefault
@Component(service = { SystemProfileFactory.class, ProfileTypeProvider.class })
public class SystemProfileFactory implements ProfileFactory, ProfileAdvisor, ProfileTypeProvider {
private @NonNullByDefault({}) ChannelTypeRegistry channelTypeRegistry;
private final ChannelTypeRegistry channelTypeRegistry;
private static final Set<ProfileType> SUPPORTED_PROFILE_TYPES = Stream
.of(DEFAULT_TYPE, FOLLOW_TYPE, OFFSET_TYPE, RAWBUTTON_TOGGLE_PLAYER_TYPE, RAWBUTTON_TOGGLE_PLAYER_TYPE,
@ -66,6 +76,20 @@ public class SystemProfileFactory implements ProfileFactory, ProfileAdvisor, Pro
RAWROCKER_NEXT_PREVIOUS, RAWROCKER_ON_OFF, RAWROCKER_PLAY_PAUSE, RAWROCKER_REWIND_FASTFORWARD,
RAWROCKER_STOP_MOVE, RAWROCKER_UP_DOWN, TIMESTAMP_CHANGE, TIMESTAMP_UPDATE).collect(Collectors.toSet());
private final Map<LocalizedProfileTypeKey, @Nullable ProfileType> localizedProfileTypeCache = new ConcurrentHashMap<>();
private final ProfileTypeI18nLocalizationService profileTypeI18nLocalizationService;
private final BundleResolver bundleResolver;
@Activate
public SystemProfileFactory(final @Reference ChannelTypeRegistry channelTypeRegistry,
final @Reference ProfileTypeI18nLocalizationService profileTypeI18nLocalizationService,
final @Reference BundleResolver bundleResolver) {
this.channelTypeRegistry = channelTypeRegistry;
this.profileTypeI18nLocalizationService = profileTypeI18nLocalizationService;
this.bundleResolver = bundleResolver;
}
@Override
public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback,
ProfileContext context) {
@ -159,7 +183,13 @@ public class SystemProfileFactory implements ProfileFactory, ProfileAdvisor, Pro
@Override
public Collection<ProfileType> getProfileTypes(@Nullable Locale locale) {
return SUPPORTED_PROFILE_TYPES;
final List<ProfileType> allProfileTypes = new ArrayList<>();
final Bundle bundle = bundleResolver.resolveBundle(SystemProfileFactory.class);
for (final ProfileType profileType : SUPPORTED_PROFILE_TYPES) {
allProfileTypes.add(createLocalizedProfileType(bundle, profileType, locale));
}
return allProfileTypes;
}
@Override
@ -167,13 +197,79 @@ public class SystemProfileFactory implements ProfileFactory, ProfileAdvisor, Pro
return SUPPORTED_PROFILE_TYPE_UIDS;
}
@Reference
protected void setChannelTypeRegistry(ChannelTypeRegistry channelTypeRegistry) {
this.channelTypeRegistry = channelTypeRegistry;
private ProfileType createLocalizedProfileType(Bundle bundle, ProfileType profileType, @Nullable Locale locale) {
LocalizedProfileTypeKey localizedProfileTypeKey = getLocalizedProfileTypeKey(profileType.getUID(), locale);
ProfileType cachedEntry = localizedProfileTypeCache.get(localizedProfileTypeKey);
if (cachedEntry != null) {
return cachedEntry;
}
ProfileType localizedProfileType = localize(bundle, profileType, locale);
if (localizedProfileType != null) {
localizedProfileTypeCache.put(localizedProfileTypeKey, localizedProfileType);
return localizedProfileType;
} else {
return profileType;
}
}
protected void unsetChannelTypeRegistry(ChannelTypeRegistry channelTypeRegistry) {
this.channelTypeRegistry = null;
private @Nullable ProfileType localize(Bundle bundle, ProfileType profileType, @Nullable Locale locale) {
if (profileTypeI18nLocalizationService == null) {
return null;
}
return profileTypeI18nLocalizationService.createLocalizedProfileType(bundle, profileType, locale);
}
private LocalizedProfileTypeKey getLocalizedProfileTypeKey(UID uid, @Nullable Locale locale) {
return new LocalizedProfileTypeKey(uid, locale != null ? locale.toLanguageTag() : null);
}
private static class LocalizedProfileTypeKey {
public final UID uid;
public final @Nullable String locale;
public LocalizedProfileTypeKey(UID uid, @Nullable String locale) {
this.uid = uid;
this.locale = locale;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
LocalizedProfileTypeKey other = (LocalizedProfileTypeKey) obj;
if (locale == null) {
if (other.locale != null) {
return false;
}
} else if (!locale.equals(other.locale)) {
return false;
}
if (uid == null) {
if (other.uid != null) {
return false;
}
} else if (!uid.equals(other.uid)) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((locale != null) ? locale.hashCode() : 0);
result = prime * result + ((uid == null) ? 0 : uid.hashCode());
return result;
}
}
}

View File

@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2019 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.eclipse.smarthome.core.thing.internal.profiles.i18n;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.i18n.I18nUtil;
import org.eclipse.smarthome.core.i18n.TranslationProvider;
import org.eclipse.smarthome.core.thing.profiles.Profile;
import org.eclipse.smarthome.core.thing.profiles.ProfileType;
import org.eclipse.smarthome.core.thing.profiles.ProfileTypeUID;
import org.eclipse.smarthome.core.thing.profiles.i18n.ProfileTypeI18nLocalizationService;
import org.osgi.framework.Bundle;
/**
* A utility service which localizes {@link Profile}s.
* Falls back to a localized {@link ProfileType} for label and description when not given otherwise.
*
* @see {@link ProfileTypeI18nLocalizationService}
*
* @author Christoph Weitkamp - Initial contribution
*/
@NonNullByDefault
public class ProfileI18nUtil {
private final TranslationProvider i18nProvider;
/**
* Create a new util instance and pass the appropriate dependencies.
*
* @param i18nProvider an instance of {@link TranslationProvider}.
*/
public ProfileI18nUtil(TranslationProvider i18nProvider) {
this.i18nProvider = i18nProvider;
}
public @Nullable String getProfileLabel(Bundle bundle, ProfileTypeUID profileTypeUID, String defaultLabel,
@Nullable Locale locale) {
String key = I18nUtil.stripConstantOr(defaultLabel, () -> inferProfileTypeKey(profileTypeUID, "label"));
return i18nProvider.getText(bundle, key, defaultLabel, locale);
}
private String inferProfileTypeKey(ProfileTypeUID profileTypeUID, String lastSegment) {
return "profile-type." + profileTypeUID.getBindingId() + "." + profileTypeUID.getId() + "." + lastSegment;
}
}

View File

@ -21,12 +21,16 @@ import org.eclipse.jdt.annotation.Nullable;
/**
* A {@link ProfileTypeProvider} is responsible for providing {@link ProfileType}s.
*
* @author Simon Kaufmann - initial contribution and API.
*
* @author Simon Kaufmann - Initial contribution
*/
@NonNullByDefault
public interface ProfileTypeProvider {
/**
* Returns all profile types for the given {@link Locale}.
*
* @param locale (can be null)
* @return all profile types or empty list if no profile type exists
*/
Collection<ProfileType> getProfileTypes(@Nullable Locale locale);
}

View File

@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2019 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.eclipse.smarthome.core.thing.profiles.i18n;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.i18n.TranslationProvider;
import org.eclipse.smarthome.core.thing.internal.profiles.i18n.ProfileI18nUtil;
import org.eclipse.smarthome.core.thing.profiles.ProfileType;
import org.eclipse.smarthome.core.thing.profiles.ProfileTypeBuilder;
import org.eclipse.smarthome.core.thing.profiles.ProfileTypeUID;
import org.eclipse.smarthome.core.thing.profiles.StateProfileType;
import org.eclipse.smarthome.core.thing.profiles.TriggerProfileType;
import org.osgi.framework.Bundle;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* This OSGi service could be used to localize a {@link ProfileType} using the i18n mechanism of the openHAB framework.
*
* @author Christoph Weitkamp - Initial contribution
*/
@Component(service = ProfileTypeI18nLocalizationService.class)
@NonNullByDefault
public class ProfileTypeI18nLocalizationService {
private final ProfileI18nUtil profileI18nUtil;
@Activate
public ProfileTypeI18nLocalizationService(final @Reference TranslationProvider i18nProvider) {
this.profileI18nUtil = new ProfileI18nUtil(i18nProvider);
}
public ProfileType createLocalizedProfileType(Bundle bundle, ProfileType profileType, @Nullable Locale locale) {
ProfileTypeUID profileTypeUID = profileType.getUID();
String defaultLabel = profileType.getLabel();
String label = profileI18nUtil.getProfileLabel(bundle, profileTypeUID, defaultLabel, locale);
if (profileType instanceof StateProfileType) {
return ProfileTypeBuilder.newTrigger(profileTypeUID, label != null ? label : defaultLabel)
.withSupportedItemTypes(profileType.getSupportedItemTypes())
.withSupportedItemTypesOfChannel(((StateProfileType) profileType).getSupportedItemTypesOfChannel())
.build();
} else if (profileType instanceof TriggerProfileType) {
return ProfileTypeBuilder.newTrigger(profileTypeUID, label != null ? label : defaultLabel)
.withSupportedItemTypes(profileType.getSupportedItemTypes())
.withSupportedChannelTypeUIDs(((TriggerProfileType) profileType).getSupportedChannelTypeUIDs())
.build();
} else {
return profileType;
}
}
}

View File

@ -0,0 +1,3 @@
profile-type.system.default.label = Standard
profile-type.system.follow.label = Folgen
profile-type.system.offset.label = Versatz

View File

@ -61,6 +61,7 @@ import org.eclipse.smarthome.core.thing.profiles.ProfileAdvisor;
import org.eclipse.smarthome.core.thing.profiles.ProfileCallback;
import org.eclipse.smarthome.core.thing.profiles.ProfileContext;
import org.eclipse.smarthome.core.thing.profiles.ProfileFactory;
import org.eclipse.smarthome.core.thing.profiles.ProfileTypeProvider;
import org.eclipse.smarthome.core.thing.profiles.ProfileTypeUID;
import org.eclipse.smarthome.core.thing.profiles.StateProfile;
import org.eclipse.smarthome.core.thing.profiles.TriggerProfile;
@ -79,10 +80,9 @@ import org.mockito.Mock;
/**
*
* @author Simon Kaufmann - initial contribution and API.
*
* @author Simon Kaufmann - Initial contribution
*/
public class CommunicationManagerTest extends JavaOSGiTest {
public class CommunicationManagerOSGiTest extends JavaOSGiTest {
private class ItemChannelLinkRegistryAdvanced extends ItemChannelLinkRegistry {
@Override
@ -167,9 +167,12 @@ public class CommunicationManagerTest extends JavaOSGiTest {
safeCaller = getService(SafeCaller.class);
assertNotNull(safeCaller);
SystemProfileFactory profileFactory = getService(ProfileTypeProvider.class, SystemProfileFactory.class);
assertNotNull(profileFactory);
manager = new CommunicationManager();
manager.setEventPublisher(eventPublisher);
manager.setDefaultProfileFactory(new SystemProfileFactory());
manager.setDefaultProfileFactory(profileFactory);
manager.setSafeCaller(safeCaller);
doAnswer(invocation -> {

View File

@ -13,45 +13,53 @@
package org.eclipse.smarthome.core.thing.internal.profiles;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.*;
import java.util.Collection;
import org.eclipse.smarthome.core.library.CoreItemFactory;
import org.eclipse.smarthome.core.thing.Channel;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.binding.builder.ChannelBuilder;
import org.eclipse.smarthome.core.thing.profiles.ProfileType;
import org.eclipse.smarthome.core.thing.profiles.ProfileTypeProvider;
import org.eclipse.smarthome.core.thing.profiles.SystemProfiles;
import org.eclipse.smarthome.core.thing.type.ChannelType;
import org.eclipse.smarthome.core.thing.type.ChannelTypeRegistry;
import org.eclipse.smarthome.test.java.JavaOSGiTest;
import org.junit.Before;
import org.junit.Test;
/**
* Test cases for the {@link SystemProfileFactory} class.
*
* @author Simon Kaufmann - initial contribution and API.
*
* @author Simon Kaufmann - Initial contribution
*/
public class SystemProfileFactoryTest {
public class SystemProfileFactoryOSGiTest extends JavaOSGiTest {
private ChannelTypeRegistry channelTypeRegistry;
private SystemProfileFactory factory;
private SystemProfileFactory profileFactory;
@Before
public void setup() {
channelTypeRegistry = new ChannelTypeRegistry();
public void setUp() {
profileFactory = getService(ProfileTypeProvider.class, SystemProfileFactory.class);
assertNotNull(profileFactory);
}
factory = new SystemProfileFactory();
factory.setChannelTypeRegistry(channelTypeRegistry);
@Test
public void systemProfileTypesShouldBeAvailable() {
Collection<ProfileType> systemProfileTypes = profileFactory.getProfileTypes(null);
assertEquals(14, systemProfileTypes.size());
}
@Test
public void testGetSuggestedProfileTypeUID_nullChannelType1() {
assertThat(factory.getSuggestedProfileTypeUID((ChannelType) null, CoreItemFactory.SWITCH), is(nullValue()));
assertThat(profileFactory.getSuggestedProfileTypeUID((ChannelType) null, CoreItemFactory.SWITCH),
is(nullValue()));
}
@Test
public void testGetSuggestedProfileTypeUID_nullChannelType2() {
Channel channel = ChannelBuilder.create(new ChannelUID("test:test:test:test"), CoreItemFactory.SWITCH).build();
assertThat(factory.getSuggestedProfileTypeUID(channel, CoreItemFactory.SWITCH), is(SystemProfiles.DEFAULT));
assertThat(profileFactory.getSuggestedProfileTypeUID(channel, CoreItemFactory.SWITCH),
is(SystemProfiles.DEFAULT));
}
}

View File

@ -0,0 +1,84 @@
/**
* Copyright (c) 2010-2019 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.eclipse.smarthome.core.thing.profiles.i18n;
import static org.eclipse.smarthome.core.thing.profiles.SystemProfiles.*;
import static org.junit.Assert.*;
import java.util.Collection;
import java.util.Locale;
import java.util.Optional;
import org.eclipse.smarthome.core.thing.internal.profiles.SystemProfileFactory;
import org.eclipse.smarthome.core.thing.profiles.ProfileType;
import org.eclipse.smarthome.core.thing.profiles.ProfileTypeProvider;
import org.eclipse.smarthome.test.java.JavaOSGiTest;
import org.junit.Before;
import org.junit.Test;
/**
* Test cases for i18n of the {@link SystemProfileFactory} class.
*
* @author Christoph Weitkamp - Initial contribution
*/
public class SystemProfileI18nOSGiTest extends JavaOSGiTest {
private ProfileTypeProvider systemProfileTypeProvider;
@Before
public void setUp() {
ProfileTypeProvider provider = getService(ProfileTypeProvider.class, SystemProfileFactory.class);
assertTrue(provider instanceof SystemProfileFactory);
systemProfileTypeProvider = provider;
}
@Test
public void systemProfileTypesShouldHaveOriginalLabel() {
Collection<ProfileType> localizedProfileTypes = systemProfileTypeProvider.getProfileTypes(Locale.ENGLISH);
Optional<ProfileType> defaultProfileType = localizedProfileTypes.stream()
.filter(it -> DEFAULT.equals(it.getUID())).findFirst();
assertTrue(defaultProfileType.isPresent());
assertEquals("Default", defaultProfileType.get().getLabel());
Optional<ProfileType> followProfileType = localizedProfileTypes.stream()
.filter(it -> FOLLOW.equals(it.getUID())).findFirst();
assertTrue(followProfileType.isPresent());
assertEquals("Follow", followProfileType.get().getLabel());
Optional<ProfileType> offsetProfileType = localizedProfileTypes.stream()
.filter(it -> OFFSET.equals(it.getUID())).findFirst();
assertTrue(offsetProfileType.isPresent());
assertEquals("Offset", offsetProfileType.get().getLabel());
}
@Test
public void systemProfileTypesShouldHaveTranslatedLabel() {
Collection<ProfileType> localizedProfileTypes = systemProfileTypeProvider.getProfileTypes(Locale.GERMAN);
Optional<ProfileType> defaultProfileType = localizedProfileTypes.stream()
.filter(it -> DEFAULT.equals(it.getUID())).findFirst();
assertTrue(defaultProfileType.isPresent());
assertEquals("Standard", defaultProfileType.get().getLabel());
Optional<ProfileType> followProfileType = localizedProfileTypes.stream()
.filter(it -> FOLLOW.equals(it.getUID())).findFirst();
assertTrue(followProfileType.isPresent());
assertEquals("Folgen", followProfileType.get().getLabel());
Optional<ProfileType> offsetProfileType = localizedProfileTypes.stream()
.filter(it -> OFFSET.equals(it.getUID())).findFirst();
assertTrue(offsetProfileType.isPresent());
assertEquals("Versatz", offsetProfileType.get().getLabel());
}
}