org.eclipse.smarthome.core.transform.AbstractFileTransformationService.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.smarthome.core.transform.AbstractFileTransformationService.java

Source

/**
 * 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 Gal 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;
    }

}