/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.core.thing.xml.internal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.smarthome.config.core.ConfigDescriptionProvider;
import org.eclipse.smarthome.config.xml.AbstractXmlConfigDescriptionProvider;
import org.eclipse.smarthome.config.xml.osgi.XmlDocumentBundleTracker;
import org.eclipse.smarthome.config.xml.osgi.XmlDocumentProviderFactory;
import org.eclipse.smarthome.config.xml.util.XmlDocumentReader;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.binding.ThingTypeProvider;
import org.eclipse.smarthome.core.thing.i18n.ThingTypeI18nLocalizationService;
import org.eclipse.smarthome.core.thing.type.ChannelTypeProvider;
import org.eclipse.smarthome.core.thing.type.ThingType;
import org.osgi.framework.Bundle;
import org.osgi.service.component.ComponentContext;
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;
/**
* The {@link XmlThingTypeProvider} is a concrete implementation of the {@link ThingTypeProvider} service interface.
* <p>
* This implementation manages any {@link ThingType} objects associated to specific modules. If a specific module
* disappears, any registered {@link ThingType} objects associated with that module are released.
*
* @author Michael Grammling - Initial Contribution
* @author Dennis Nobel - Added locale support, Added cache for localized thing types
* @author Ivan Iliev - Added support for system wide channel types
* @author Kai Kreuzer - fixed concurrency issues
*/
@Component(immediate = true, property = { "esh.scope=core.xml" })
public class XmlThingTypeProvider implements ThingTypeProvider {
private class LocalizedThingTypeKey {
public ThingTypeUID uid;
public String locale;
public LocalizedThingTypeKey(ThingTypeUID uid, String locale) {
this.uid = uid;
this.locale = locale;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getOuterType().hashCode();
result = prime * result + ((locale == null) ? 0 : locale.hashCode());
result = prime * result + ((uid == null) ? 0 : uid.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
LocalizedThingTypeKey other = (LocalizedThingTypeKey) obj;
if (!getOuterType().equals(other.getOuterType())) {
return false;
}
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;
}
private XmlThingTypeProvider getOuterType() {
return XmlThingTypeProvider.this;
}
}
private static final String XML_DIRECTORY = "/ESH-INF/thing/";
private Map<LocalizedThingTypeKey, ThingType> localizedThingTypeCache = new ConcurrentHashMap<>();
private Map<Bundle, List<ThingType>> bundleThingTypesMap = new ConcurrentHashMap<>(10);
private ThingTypeI18nLocalizationService thingTypeI18nLocalizationService;
private AbstractXmlConfigDescriptionProvider configDescriptionProvider;
private XmlChannelTypeProvider channelTypeProvider;
private XmlDocumentBundleTracker<List<?>> thingTypeTracker;
@Activate
protected void activate(ComponentContext context) {
XmlDocumentReader<List<?>> thingTypeReader = new ThingDescriptionReader();
XmlDocumentProviderFactory<List<?>> thingTypeProviderFactory = new ThingTypeXmlProviderFactory(
configDescriptionProvider, this, channelTypeProvider);
thingTypeTracker = new XmlDocumentBundleTracker<List<?>>(context.getBundleContext(), XML_DIRECTORY,
thingTypeReader, thingTypeProviderFactory);
thingTypeTracker.open();
}
@Deactivate
protected void deactivate(ComponentContext context) {
thingTypeTracker.close();
thingTypeTracker = null;
}
private List<ThingType> acquireThingTypes(Bundle bundle) {
if (bundle != null) {
List<ThingType> thingTypes = this.bundleThingTypesMap.get(bundle);
if (thingTypes == null) {
thingTypes = new CopyOnWriteArrayList<ThingType>();
this.bundleThingTypesMap.put(bundle, thingTypes);
}
return thingTypes;
}
return null;
}
/**
* Adds a {@link ThingType} object to the internal list associated with the
* specified module.
* <p>
* This method returns silently, if any of the parameters is {@code null}.
*
* @param bundle
* the module to which the Thing type to be added
* @param thingType
* the Thing type to be added
*/
public synchronized void addThingType(Bundle bundle, ThingType thingType) {
if (thingType != null) {
List<ThingType> thingTypes = acquireThingTypes(bundle);
if (thingTypes != null) {
thingTypes.add(thingType);
// just make sure no old entry remains in the cache
removeCachedEntries(thingType);
}
}
}
private ThingType createLocalizedThingType(Bundle bundle, ThingType thingType, Locale locale) {
// Create a localized thing type key (used for caching localized thing types).
final LocalizedThingTypeKey localizedThingTypeKey = getLocalizedThingTypeKey(thingType, locale);
// Check if there is already an entry in our cache.
final ThingType cacheEntry = localizedThingTypeCache.get(localizedThingTypeKey);
if (cacheEntry != null) {
return cacheEntry;
}
// Check if there is a localization service available.
if (thingTypeI18nLocalizationService != null) {
// Fetch the localized thing type.
final ThingType localizedThingType = thingTypeI18nLocalizationService.createLocalizedThingType(bundle,
thingType, locale);
// Put the localized thing type in our cache, so we could reuse it.
localizedThingTypeCache.put(localizedThingTypeKey, localizedThingType);
return localizedThingType;
} else {
// There is no localization service available, return the non-localized one.
return thingType;
}
}
private LocalizedThingTypeKey getLocalizedThingTypeKey(ThingType thingType, Locale locale) {
String localeString = locale != null ? locale.toLanguageTag() : null;
LocalizedThingTypeKey localizedThingTypeKey = new LocalizedThingTypeKey(thingType.getUID(),
locale != null ? localeString : null);
return localizedThingTypeKey;
}
private void removeCachedEntries(List<ThingType> thingTypes) {
for (ThingType thingType : thingTypes) {
removeCachedEntries(thingType);
}
}
private void removeCachedEntries(ThingType thingType) {
for (Iterator<Entry<LocalizedThingTypeKey, ThingType>> iterator = this.localizedThingTypeCache.entrySet()
.iterator(); iterator.hasNext();) {
Entry<LocalizedThingTypeKey, ThingType> entry = iterator.next();
if (entry.getKey().uid.equals(thingType.getUID())) {
iterator.remove();
}
}
}
@Override
public ThingType getThingType(ThingTypeUID thingTypeUID, Locale locale) {
Collection<Entry<Bundle, List<ThingType>>> thingTypesList = this.bundleThingTypesMap.entrySet();
if (thingTypesList != null) {
for (Entry<Bundle, List<ThingType>> thingTypes : thingTypesList) {
for (ThingType thingType : thingTypes.getValue()) {
if (thingType.getUID().equals(thingTypeUID)) {
return createLocalizedThingType(thingTypes.getKey(), thingType, locale);
}
}
}
}
return null;
}
@Override
public synchronized Collection<ThingType> getThingTypes(Locale locale) {
List<ThingType> allThingTypes = new ArrayList<>(10);
Collection<Entry<Bundle, List<ThingType>>> thingTypesList = this.bundleThingTypesMap.entrySet();
if (thingTypesList != null) {
for (Entry<Bundle, List<ThingType>> thingTypes : thingTypesList) {
for (ThingType thingType : thingTypes.getValue()) {
ThingType localizedThingType = createLocalizedThingType(thingTypes.getKey(), thingType, locale);
allThingTypes.add(localizedThingType);
}
}
}
return allThingTypes;
}
/**
* Removes all {@link ThingType} objects from the internal list associated
* with the specified module.
* <p>
* This method returns silently if the module is {@code null}.
*
* @param bundle
* the module for which all associated Thing types to be removed
*/
public synchronized void removeAllThingTypes(Bundle bundle) {
if (bundle != null) {
List<ThingType> thingTypes = this.bundleThingTypesMap.get(bundle);
if (thingTypes != null) {
this.bundleThingTypesMap.remove(bundle);
removeCachedEntries(thingTypes);
}
}
}
@Reference
public void setThingTypeI18nLocalizationService(
final ThingTypeI18nLocalizationService thingTypeI18nLocalizationService) {
this.thingTypeI18nLocalizationService = thingTypeI18nLocalizationService;
}
public void unsetThingTypeI18nLocalizationService(
final ThingTypeI18nLocalizationService thingTypeI18nLocalizationService) {
this.thingTypeI18nLocalizationService = null;
}
@Reference(target = "(esh.scope=core.xml.thing)")
public void setConfigDescriptionProvider(ConfigDescriptionProvider configDescriptionProvider) {
this.configDescriptionProvider = (AbstractXmlConfigDescriptionProvider) configDescriptionProvider;
}
public void unsetConfigDescriptionProvider(ConfigDescriptionProvider configDescriptionProvider) {
this.configDescriptionProvider = null;
}
@Reference(target = "(esh.scope=core.xml)")
public void setChannelTypeProvider(ChannelTypeProvider channelTypeProvider) {
this.channelTypeProvider = (XmlChannelTypeProvider) channelTypeProvider;
}
public void unsetChannelTypeProvider(ChannelTypeProvider configDescriptionProvider) {
this.channelTypeProvider = null;
}
}