/**
* 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.transform;
import static java.nio.file.StandardWatchEventKinds.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.io.FilenameUtils;
import org.eclipse.smarthome.config.core.ConfigConstants;
import org.eclipse.smarthome.core.i18n.LocaleProvider;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for cacheable and localizable file based transformation
* {@link TransformationService}.
* It expects the transformation to be applied to be read from a file stored
* under the 'transform' folder within the configuration path. To organize the various
* transformations one might use subfolders.
*
* @author Gaƫl L'hopital - Initial contribution
* @author Kai Kreuzer - File caching mechanism
* @author Markus Rathgeb - Add locale provider support
*/
public abstract class AbstractFileTransformationService<T> implements TransformationService {
private WatchService watchService = null;
protected final Map<String, T> cachedFiles = new ConcurrentHashMap<>();
protected final List<String> watchedDirectories = new ArrayList<String>();
private final Logger logger = LoggerFactory.getLogger(AbstractFileTransformationService.class);
private LocaleProvider localeProvider;
private ServiceTracker<LocaleProvider, LocaleProvider> localeProviderTracker;
private class LocaleProviderServiceTrackerCustomizer
implements ServiceTrackerCustomizer<LocaleProvider, LocaleProvider> {
private final BundleContext context;
public LocaleProviderServiceTrackerCustomizer(final BundleContext context) {
this.context = context;
}
@Override
public LocaleProvider addingService(ServiceReference<LocaleProvider> reference) {
localeProvider = context.getService(reference);
return localeProvider;
}
@Override
public void modifiedService(ServiceReference<LocaleProvider> reference, LocaleProvider service) {
}
@Override
public void removedService(ServiceReference<LocaleProvider> reference, LocaleProvider service) {
localeProvider = null;
}
}
protected void activate(final BundleContext context) {
localeProviderTracker = new ServiceTracker<>(context, LocaleProvider.class,
new LocaleProviderServiceTrackerCustomizer(context));
localeProviderTracker.open();
}
protected void deactivate() {
localeProviderTracker.close();
}
protected Locale getLocale() {
return localeProvider.getLocale();
}
/**
* <p>
* Transforms the input <code>source</code> by the according method defined in subclass to another string.
* It expects the transformation to be read from a file which is stored
* under the 'conf/transform'
* </p>
*
* @param filename
* the name of the file which contains the transformation definition.
* The name may contain subfoldernames
* as well
* @param source
* the input to transform
* @throws TransformationException
*
* @{inheritDoc
*
*/
@Override
public String transform(String filename, String source) throws TransformationException {
if (filename == null || source == null) {
throw new TransformationException("the given parameters 'filename' and 'source' must not be null");
}
if (watchService == null) {
initializeWatchService();
} else {
processFolderEvents();
}
String transformFile = getLocalizedProposedFilename(filename);
T transform = cachedFiles.get(transformFile);
if (transform == null) {
transform = internalLoadTransform(transformFile);
cachedFiles.put(transformFile, transform);
}
try {
return internalTransform(transform, source);
} catch (TransformationException e) {
logger.warn("Could not transform '{}' with the file '{}' : {}", source, filename, e.getMessage());
return "";
}
}
/**
* <p>
* Abstract method defined by subclasses to effectively operate the
* transformation according to its rules
* </p>
*
* @param transform
* transformation held by the file provided to <code>transform</code> method
*
* @param source
* the input to transform
*
* @return the transformed result or null if the
* transformation couldn't be completed for any reason.
*
*/
protected abstract String internalTransform(T transform, String source) throws TransformationException;
/**
* <p>
* Abstract method defined by subclasses to effectively read the transformation
* source file according to their own needs.
* </p>
*
* @param filename
* Name of the file to be read. This filename may have been transposed
* to a localized one
*
* @return
* An object containing the source file
*
* @throws TransformationException
* file couldn't be read for any reason
*/
protected abstract T internalLoadTransform(String filename) throws TransformationException;
private void initializeWatchService() {
try {
watchService = FileSystems.getDefault().newWatchService();
watchSubDirectory("");
} catch (IOException e) {
logger.error("Unable to start transformation directory monitoring");
}
}
private void watchSubDirectory(String subDirectory) {
if (watchedDirectories.indexOf(subDirectory) == -1) {
String watchedDirectory = getSourcePath() + subDirectory;
Path transformFilePath = Paths.get(watchedDirectory);
try {
transformFilePath.register(watchService, ENTRY_DELETE, ENTRY_MODIFY);
logger.debug("Watching directory {}", transformFilePath);
watchedDirectories.add(subDirectory);
} catch (IOException e) {
logger.warn("Unable to watch transformation directory : {}", watchedDirectory);
cachedFiles.clear();
}
}
}
/**
* Ensures that a modified or deleted cached files does not stay in the cache
*/
private void processFolderEvents() {
WatchKey key = watchService.poll();
if (key != null) {
for (WatchEvent<?> e : key.pollEvents()) {
if (e.kind() == OVERFLOW) {
continue;
}
// Context for directory entry event is the file name of entry
@SuppressWarnings("unchecked")
WatchEvent<Path> ev = (WatchEvent<Path>) e;
Path path = ev.context();
logger.debug("Refreshing transformation file '{}'", path);
for (String fileEntry : cachedFiles.keySet()) {
if (fileEntry.endsWith(path.toString())) {
cachedFiles.remove(fileEntry);
}
}
}
key.reset();
}
}
/**
* Returns the name of the localized transformation file
* if it actually exists, keeps the original in the other case
*
* @param filename name of the requested transformation file
* @return original or localized transformation file to use
*/
protected String getLocalizedProposedFilename(String filename) {
String extension = FilenameUtils.getExtension(filename);
String prefix = FilenameUtils.getPath(filename);
String result = filename;
if (!prefix.isEmpty()) {
watchSubDirectory(prefix);
}
// the filename may already contain locale information
if (!filename.matches(".*_[a-z]{2}." + extension + "$")) {
String basename = FilenameUtils.getBaseName(filename);
String alternateName = prefix + basename + "_" + getLocale().getLanguage() + "." + extension;
String alternatePath = getSourcePath() + alternateName;
File f = new File(alternatePath);
if (f.exists()) {
result = alternateName;
}
}
result = getSourcePath() + result;
return result;
}
/**
* Returns the path to the root of the transformation folder
*/
protected String getSourcePath() {
return ConfigConstants.getConfigFolder() + File.separator + TransformationService.TRANSFORM_FOLDER_NAME
+ File.separator;
}
}