/**
* Copyright (c) 1997, 2015 by ProSyst Software GmbH and others.
* 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.automation.internal.core.provider;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.smarthome.automation.Rule;
import org.eclipse.smarthome.automation.parser.Parser;
import org.eclipse.smarthome.automation.parser.ParsingException;
import org.eclipse.smarthome.automation.template.RuleTemplate;
import org.eclipse.smarthome.automation.template.Template;
import org.eclipse.smarthome.automation.template.TemplateProvider;
import org.eclipse.smarthome.automation.type.ModuleType;
import org.eclipse.smarthome.automation.type.ModuleTypeProvider;
import org.eclipse.smarthome.config.core.ConfigDescriptionParameter;
import org.eclipse.smarthome.config.core.ConfigDescriptionParameterBuilder;
import org.eclipse.smarthome.config.core.ParameterOption;
import org.eclipse.smarthome.config.core.i18n.ConfigDescriptionI18nUtil;
import org.eclipse.smarthome.core.common.registry.Provider;
import org.eclipse.smarthome.core.common.registry.ProviderChangeListener;
import org.eclipse.smarthome.core.i18n.I18nProvider;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.util.tracker.BundleTrackerCustomizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class is base for {@link ModuleTypeProvider}, {@link TemplateProvider} and {@code RuleImporter} which are
* responsible for importing and persisting the {@link ModuleType}s, {@link RuleTemplate}s and {@link Rule}s from
* bundles which provides resource files.
* <p>
* It tracks {@link Parser} services by implementing {@link #addParser(Parser, Map)} and
* {@link #removeParser(Parser, Map)} methods.
* <p>
* The functionality, responsible for tracking the bundles with resources, comes from
* {@link AutomationResourceBundlesTracker} by implementing a {@link BundleTrackerCustomizer} but the functionality for
* processing them, comes from this class.
*
* @author Ana Dimova - Initial Contribution
* @author Kai Kreuzer - refactored (managed) provider and registry implementation
*/
@SuppressWarnings("rawtypes")
public abstract class AbstractResourceBundleProvider<E> {
public AbstractResourceBundleProvider() {
logger = LoggerFactory.getLogger(this.getClass());
providedObjectsHolder = new ConcurrentHashMap<String, E>();
providerPortfolio = new ConcurrentHashMap<Vendor, List<String>>();
queue = new AutomationResourceBundlesEventQueue<E>(this);
parsers = new ConcurrentHashMap<String, Parser<E>>();
waitingProviders = new ConcurrentHashMap<Bundle, List<URL>>();
}
/**
* This static field provides a root directory for automation object resources in the bundle resources.
* It is common for all resources - {@link ModuleType}s, {@link RuleTemplate}s and {@link Rule}s.
*/
protected static String PATH = "ESH-INF/automation";
/**
* This field holds a reference to the service instance for internationalization support within the platform.
*/
protected I18nProvider i18nProvider;
/**
* This field keeps instance of {@link Logger} that is used for logging.
*/
protected Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* A bundle's execution context within the Framework.
*/
protected BundleContext bc;
/**
* This field is initialized in constructors of any particular provider with specific path for the particular
* resources from specific type as {@link ModuleType}s, {@link RuleTemplate}s and {@link Rule}s:
* <li>for
* {@link ModuleType}s it is a "ESH-INF/automation/moduletypes/"
* <li>for {@link RuleTemplate}s it is a
* "ESH-INF/automation/templates/"
* <li>for {@link Rule}s it is a "ESH-INF/automation/rules/"
*/
protected String path;
/**
* This Map collects all binded {@link Parser}s.
*/
protected Map<String, Parser<E>> parsers;
/**
* This Map provides structure for fast access to the provided automation objects. This provides opportunity for
* high performance at runtime of the system, when the Rule Engine asks for any particular object, instead of
* waiting it for parsing every time.
* <p>
* The Map has for keys UIDs of the objects and for values one of {@link ModuleType}s, {@link RuleTemplate}s and
* {@link Rule}s.
*/
protected Map<String, E> providedObjectsHolder;
/**
* This Map provides reference between provider of resources and the loaded objects from these resources.
* <p>
* The Map has for keys - {@link Vendor}s and for values - Lists with UIDs of the objects.
*/
protected Map<Vendor, List<String>> providerPortfolio;
/**
* This Map holds bundles whose {@link Parser} for resources is missing in the moment of processing the bundle.
* Later, if the {@link Parser} appears, they will be added again in the {@link #queue} for processing.
*/
protected Map<Bundle, List<URL>> waitingProviders;
/**
* This field provides an access to the queue for processing bundles.
*/
protected AutomationResourceBundlesEventQueue queue;
protected List<ProviderChangeListener<E>> listeners;
protected void activate(BundleContext bc) {
this.bc = bc;
}
protected void deactivate() {
bc = null;
if (queue != null) {
queue.stop();
}
synchronized (parsers) {
parsers.clear();
}
synchronized (providedObjectsHolder) {
providedObjectsHolder.clear();
}
synchronized (providerPortfolio) {
providerPortfolio.clear();
}
synchronized (waitingProviders) {
waitingProviders.clear();
}
if (listeners != null) {
synchronized (listeners) {
listeners.clear();
}
}
}
/**
* This method is used to initialize field {@link #queue}, when the instance of
* {@link AutomationResourceBundlesEventQueue} is created.
*
* @param queue provides an access to the queue for processing bundles.
*/
protected AutomationResourceBundlesEventQueue getQueue() {
return queue;
}
/**
* This method is called before the {@link Parser} services to be added to the {@code ServiceTracker} and storing
* them in the {@link #parsers} into the memory, for fast access on demand. The returned service object is stored in
* the {@code ServiceTracker} and is available from the {@code getService} and {@code getServices} methods.
* <p>
* Also if there are bundles that were stored in {@link #waitingProviders}, to be processed later, because of
* missing {@link Parser} for particular format,
* <p>
* and then the {@link Parser} service appears, they will be processed.
*
* @param parser {@link Parser} service
* @param properties of the service that has been added.
*/
protected void addParser(Parser<E> parser, Map<String, String> properties) {
String parserType = properties.get(Parser.FORMAT);
parserType = parserType == null ? Parser.FORMAT_JSON : parserType;
parsers.put(parserType, parser);
for (Bundle bundle : waitingProviders.keySet()) {
if (bundle.getState() != Bundle.UNINSTALLED) {
processAutomationProvider(bundle);
}
}
}
/**
* This method is called after a service is no longer being tracked by the {@code ServiceTracker} and removes the
* {@link Parser} service objects from the structure Map "{@link #parsers}".
*
* @param parser The {@link Parser} service object for the specified referenced service.
* @param properties of the service that has been removed.
*/
protected void removeParser(Parser<E> parser, Map<String, String> properties) {
String parserType = properties.get(Parser.FORMAT);
parserType = parserType == null ? Parser.FORMAT_JSON : parserType;
parsers.remove(parserType);
}
protected void setI18nProvider(I18nProvider i18nProvider) {
this.i18nProvider = i18nProvider;
}
protected void removeI18nProvider(I18nProvider i18nProvider) {
this.i18nProvider = null;
}
/**
* This method provides common functionality for {@link ModuleTypeProvider} and {@link TemplateProvider} to process
* the bundles. For {@link RuleResourceBundleImporter} this method is overridden.
* <p>
* Checks for availability of the needed {@link Parser}. If it is not available - the bundle is added into
* {@link #waitingProviders} and the execution of the method ends.
* <p>
* If it is available, the execution of the method continues with checking if the version of the bundle is changed.
* If the version is changed - removes persistence of old variants of the objects, provided by this bundle.
* <p>
* Continues with loading the new version of these objects. If this bundle is added for the very first time, only
* loads the provided objects.
* <p>
* The loading can fail because of {@link IOException}.
*
* @param bundle it is a {@link Bundle} which has to be processed, because it provides resources for automation
* objects.
*/
protected void processAutomationProvider(Bundle bundle) {
Enumeration<URL> urlEnum = null;
try {
if (bundle.getState() != Bundle.UNINSTALLED) {
urlEnum = bundle.findEntries(path, null, true);
}
} catch (IllegalStateException e) {
logger.debug("Can't read from resource of bundle with ID {}. The bundle is uninstalled.",
bundle.getBundleId(), e);
processAutomationProviderUninstalled(bundle);
}
Vendor vendor = new Vendor(bundle.getSymbolicName(), bundle.getVersion().toString());
List<String> previousPortfolio = getPreviousPortfolio(vendor);
List<String> newPortfolio = new LinkedList<String>();
if (urlEnum != null) {
while (urlEnum.hasMoreElements()) {
URL url = urlEnum.nextElement();
if (url.getPath().endsWith(File.separator)) {
continue;
}
String parserType = getParserType(url);
Parser<E> parser = parsers.get(parserType);
updateWaitingProviders(parser, bundle, url);
if (parser != null) {
Set<E> parsedObjects = parseData(parser, url, bundle);
if (parsedObjects != null && !parsedObjects.isEmpty()) {
addNewProvidedObjects(newPortfolio, previousPortfolio, parsedObjects);
}
}
}
putNewPortfolio(vendor, newPortfolio);
}
removeUninstalledObjects(previousPortfolio, newPortfolio);
}
@SuppressWarnings("unchecked")
protected void removeUninstalledObjects(List<String> previousPortfolio, List<String> newPortfolio) {
if (previousPortfolio != null && !previousPortfolio.isEmpty()) {
for (String uid : previousPortfolio) {
if (!newPortfolio.contains(uid)) {
E removedObject = providedObjectsHolder.remove(uid);
if (listeners != null) {
List<ProviderChangeListener<E>> snapshot = null;
synchronized (listeners) {
snapshot = new LinkedList<ProviderChangeListener<E>>(listeners);
}
for (ProviderChangeListener<E> listener : snapshot) {
listener.removed((Provider<E>) this, removedObject);
}
}
}
}
}
}
protected List<String> getPreviousPortfolio(Vendor vendor) {
List<String> portfolio = providerPortfolio.remove(vendor);
if (portfolio == null) {
for (Vendor v : providerPortfolio.keySet()) {
if (v.getVendorSymbolicName().equals(vendor.getVendorSymbolicName())) {
return providerPortfolio.remove(v);
}
}
}
return portfolio;
}
protected void putNewPortfolio(Vendor vendor, List<String> portfolio) {
providerPortfolio.put(vendor, portfolio);
}
/**
* This method is used to determine which parser to be used.
*
* @param url the URL of the source of data for parsing.
* @return the type of the parser.
*/
protected String getParserType(URL url) {
String fileName = url.getPath();
int fileExtesionStartIndex = fileName.lastIndexOf(".") + 1;
if (fileExtesionStartIndex == -1) {
return Parser.FORMAT_JSON;
}
String fileExtesion = fileName.substring(fileExtesionStartIndex);
if (fileExtesion.equals("txt")) {
return Parser.FORMAT_JSON;
}
return fileExtesion;
}
/**
* This method provides common functionality for {@link ModuleTypeProvider} and {@link TemplateProvider} to process
* uninstalling the bundles. For {@link RuleResourceBundleImporter} this method is overridden.
* <p>
* When some of the bundles that provides automation objects is uninstalled, this method will remove it from
* {@link #waitingProviders}, if it is still there or from {@link #providerPortfolio} in the other case.
* <p>
* Will remove the provided objects from {@link #providedObjectsHolder} and will remove their persistence, injected
* in the system from this bundle.
*
* @param bundle the uninstalled {@link Bundle}, provider of automation objects.
*/
@SuppressWarnings("unchecked")
protected void processAutomationProviderUninstalled(Bundle bundle) {
waitingProviders.remove(bundle);
Vendor vendor = new Vendor(bundle.getSymbolicName(), bundle.getVersion().toString());
List<String> portfolio = providerPortfolio.remove(vendor);
if (portfolio != null && !portfolio.isEmpty()) {
for (String uid : portfolio) {
E removedObject = providedObjectsHolder.remove(uid);
if (listeners != null) {
List<ProviderChangeListener<E>> snapshot = null;
synchronized (listeners) {
snapshot = new LinkedList<ProviderChangeListener<E>>(listeners);
}
for (ProviderChangeListener<E> listener : snapshot) {
listener.removed((Provider<E>) this, removedObject);
}
}
}
}
}
/**
* This method is used to get the bundle providing localization resources for {@link ModuleType}s or
* {@link Template}s.
*
* @param uid is the unique identifier of {@link ModuleType} or {@link Template} that must be localized.
* @return the bundle providing localization resources.
*/
protected Bundle getBundle(String uid) {
String symbolicName = null;
for (Entry<Vendor, List<String>> entry : providerPortfolio.entrySet()) {
if (entry.getValue().contains(uid)) {
symbolicName = entry.getKey().getVendorSymbolicName();
break;
}
}
if (symbolicName != null) {
Bundle[] bundles = bc.getBundles();
for (int i = 0; i < bundles.length; i++) {
if (bundles[i].getSymbolicName().equals(symbolicName)) {
return bundles[i];
}
}
}
return null;
}
protected List<ConfigDescriptionParameter> getLocalizedConfigurationDescription(I18nProvider i18nProvider,
List<ConfigDescriptionParameter> config, Bundle bundle, String uid, String prefix, Locale locale) {
List<ConfigDescriptionParameter> configDescriptions = new ArrayList<ConfigDescriptionParameter>();
if (config != null) {
ConfigDescriptionI18nUtil util = new ConfigDescriptionI18nUtil(i18nProvider);
for (ConfigDescriptionParameter parameter : config) {
String parameterName = parameter.getName();
URI uri = null;
try {
uri = new URI(prefix + ":" + uid + ".name");
} catch (URISyntaxException e) {
e.printStackTrace();
}
String llabel = parameter.getLabel();
if (llabel != null) {
llabel = util.getParameterLabel(bundle, uri, parameterName, llabel, locale);
}
String ldescription = parameter.getDescription();
if (ldescription != null) {
ldescription = util.getParameterDescription(bundle, uri, parameterName, ldescription, locale);
}
String lpattern = parameter.getPattern();
if (lpattern != null) {
lpattern = util.getParameterPattern(bundle, uri, parameterName, lpattern, locale);
}
List<ParameterOption> loptions = parameter.getOptions();
if (loptions != null && !loptions.isEmpty()) {
for (ParameterOption option : loptions) {
String label = util.getParameterOptionLabel(bundle, uri, parameterName, option.getValue(),
option.getLabel(), locale);
option = new ParameterOption(option.getValue(), label);
}
}
String lunitLabel = parameter.getUnitLabel();
if (lunitLabel != null) {
lunitLabel = util.getParameterUnitLabel(bundle, uri, parameterName, parameter.getUnit(), lunitLabel,
locale);
}
configDescriptions.add(ConfigDescriptionParameterBuilder.create(parameterName, parameter.getType())
.withMinimum(parameter.getMinimum()).withMaximum(parameter.getMaximum())
.withStepSize(parameter.getStepSize()).withPattern(lpattern)
.withRequired(parameter.isRequired()).withMultiple(parameter.isMultiple())
.withReadOnly(parameter.isReadOnly()).withContext(parameter.getContext())
.withDefault(parameter.getDefault()).withLabel(llabel).withDescription(ldescription)
.withFilterCriteria(parameter.getFilterCriteria()).withGroupName(parameter.getGroupName())
.withAdvanced(parameter.isAdvanced()).withOptions(loptions)
.withLimitToOptions(parameter.getLimitToOptions())
.withMultipleLimit(parameter.getMultipleLimit()).withUnit(parameter.getUnit())
.withUnitLabel(lunitLabel).build());
}
}
return configDescriptions;
}
/**
* This method is called from {@link #processAutomationProvider(Bundle)} to process the loading of the provided
* objects.
*
* @param vendor is a holder of information about the bundle providing data for import.
* @param parser the {@link Parser} which is responsible for parsing of a particular format in which the provided
* objects are presented
* @param url the resource which is used for loading the objects.
* @param bundle is the resource holder
* @return a set of automation objects - the result of loading.
*/
protected Set<E> parseData(Parser<E> parser, URL url, Bundle bundle) {
InputStreamReader reader = null;
InputStream is = null;
try {
is = url.openStream();
reader = new InputStreamReader(is);
return parser.parse(reader);
} catch (ParsingException e) {
logger.error(e.getLocalizedMessage(), e);
} catch (IOException e) {
logger.error("Can't read from resource of bundle with ID {}", bundle.getBundleId(), e);
processAutomationProviderUninstalled(bundle);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ignore) {
}
}
if (is != null) {
try {
is.close();
} catch (IOException ignore) {
}
}
}
return null;
}
@SuppressWarnings("unchecked")
protected void addNewProvidedObjects(List<String> newPortfolio, List<String> previousPortfolio,
Set<E> parsedObjects) {
for (E parsedObject : parsedObjects) {
String uid = getUID(parsedObject);
if (providedObjectsHolder.get(uid) == null) {
if (checkExistence(uid)) {
continue;
}
} else if (previousPortfolio == null || !previousPortfolio.contains(uid)) {
logger.error("{} with UID \"{}\" already exists! Failed to create a second with the same UID!",
parsedObject.getClass().getName(), uid, new IllegalArgumentException());
continue;
}
newPortfolio.add(uid);
E oldelement = providedObjectsHolder.put(uid, parsedObject);
if (listeners != null) {
List<ProviderChangeListener<E>> snapshot = null;
synchronized (listeners) {
snapshot = new LinkedList<ProviderChangeListener<E>>(listeners);
}
for (ProviderChangeListener<E> listener : snapshot) {
if (oldelement == null) {
listener.added((Provider<E>) this, parsedObject);
} else {
listener.updated((Provider<E>) this, oldelement, parsedObject);
}
}
}
}
}
protected void updateWaitingProviders(Parser<E> parser, Bundle bundle, URL url) {
List<URL> urlList = waitingProviders.get(bundle);
if (parser == null) {
if (urlList == null) {
urlList = new ArrayList<URL>();
}
urlList.add(url);
waitingProviders.put(bundle, urlList);
return;
}
if (urlList != null && urlList.remove(url) && urlList.isEmpty()) {
waitingProviders.remove(bundle);
}
}
/**
* @param uid
* @return
*/
protected abstract boolean checkExistence(String uid);
/**
* @param parsedObject
* @return
*/
protected abstract String getUID(E parsedObject);
}