org.n52.geoar.newdata.PluginLoader.java Source code

Java tutorial

Introduction

Here is the source code for org.n52.geoar.newdata.PluginLoader.java

Source

/**
 * Copyright 2012 52North Initiative for Geospatial Open Source Software GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.n52.geoar.newdata;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.n52.geoar.GeoARApplication;
import org.n52.geoar.ar.view.IntroController;
import org.n52.geoar.newdata.CheckList.OnCheckedChangedListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.util.Base64;

import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.PrecisionModel;

public class PluginLoader {

    /**
     * Class to hold information about a plugin, usually obtained from its
     * plugin descriptor file. At least an identifier is required to load a
     * plugin.
     * 
     */
    public static class PluginInfo {
        public PluginInfo(File pluginFile, String name, String description, Long version, String identifier,
                String publisher) {
            this.name = name;
            this.description = description;
            this.version = version;
            this.pluginFile = pluginFile;
            this.identifier = identifier;
            this.publisher = publisher;
        }

        File pluginFile; // Path to the plugin
        String name;
        String description;
        Long version;
        String identifier;
        String publisher;
    }

    private static final FilenameFilter PLUGIN_FILENAME_FILTER = new FilenameFilter() {
        @Override
        public boolean accept(File dir, String fileName) {
            return fileName.endsWith(".apk") || fileName.endsWith(".zip") || fileName.endsWith(".jar");
        }
    };
    private static final String PLUGIN_STATE_PREF = "selected_plugins";
    private static final int PLUGIN_STATE_VERSION = 4;
    private static final File PLUGIN_DIRECTORY_PATH = GeoARApplication.applicationContext.getExternalFilesDir(null);
    // Pattern captures the plugin version string
    private static final Pattern pluginVersionPattern = Pattern.compile("-(\\d+(?:\\.\\d+)*)[.-]");
    // Pattern captures the plugin name, ignoring the optional version and
    // filename ending
    private static final Pattern pluginNamePattern = Pattern.compile("^((?:.(?!-\\d+\\.))+.).*\\.[^.]+$");
    private static CheckList<InstalledPluginHolder> mInstalledPlugins = new CheckList<InstalledPluginHolder>(
            InstalledPluginHolder.class);
    private static List<DataSourceHolder> mDataSources = new ArrayList<DataSourceHolder>();
    private static boolean mReloadingPlugins;
    // Listener to update the list of currently available data sources
    private static OnCheckedChangedListener<InstalledPluginHolder> pluginCheckedChangedListener = new OnCheckedChangedListener<InstalledPluginHolder>() {
        @Override
        public void onCheckedChanged(InstalledPluginHolder item, boolean newState) {
            for (DataSourceHolder dataSource : item.getDataSources()) {
                if (newState == true) {
                    addDataSource(dataSource);
                } else {
                    removeDataSource(dataSource);
                }
            }

            if (newState && !mReloadingPlugins) {
                item.postConstruct();
            }
        }
    };
    private static DefaultHttpClient mHttpClient;
    private static GeometryFactory mGeometryFactory;

    private static final Logger LOG = LoggerFactory.getLogger(PluginLoader.class);

    static {
        mInstalledPlugins.addOnCheckedChangeListener(pluginCheckedChangedListener);
        mReloadingPlugins = true;
        loadPlugins();
        restoreState();
        mReloadingPlugins = false;
    }

    /**
     * Returns a thread safe {@link DefaultHttpClient} instance to be reused
     * among different parts of the application
     * 
     * @return
     */
    public static DefaultHttpClient getSharedHttpClient() {
        if (mHttpClient == null) {
            SchemeRegistry registry = new SchemeRegistry();
            registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
            registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));

            HttpParams httpParameters = new BasicHttpParams();
            HttpConnectionParams.setSoTimeout(httpParameters, 60000);
            HttpConnectionParams.setConnectionTimeout(httpParameters, 20000);
            ClientConnectionManager cm = new ThreadSafeClientConnManager(httpParameters, registry);
            mHttpClient = new DefaultHttpClient(cm, httpParameters);
        }

        return mHttpClient;
    }

    public static GeometryFactory getGeometryFactory() {
        if (mGeometryFactory == null) {
            mGeometryFactory = new GeometryFactory();
        }
        return mGeometryFactory;
    }

    /**
     * Restores the state of plugins from {@link SharedPreferences}. If an error
     * occurs, e.g. if a previously selected plugin got removed, this function
     * will quit silently.
     */
    private static void restoreState() {
        try {
            SharedPreferences preferences = GeoARApplication.applicationContext
                    .getSharedPreferences(GeoARApplication.PREFERENCES_FILE, Context.MODE_PRIVATE);

            byte[] data = Base64.decode(preferences.getString(PLUGIN_STATE_PREF, ""), Base64.DEFAULT);
            PluginStateInputStream objectInputStream = new PluginStateInputStream(new ByteArrayInputStream(data));

            int stateVersion = objectInputStream.readInt();
            if (stateVersion != PLUGIN_STATE_VERSION) {
                // Do not read state if preferences contains old/invalid state
                // information
                return;
            }

            // Restore plugin state
            int count = objectInputStream.readInt();
            for (int i = 0; i < count; i++) {

                InstalledPluginHolder plugin = getPluginByIdentifier(objectInputStream.readUTF());
                if (plugin == null) {
                    return;
                }
                try {
                    plugin.restoreState(objectInputStream);
                    plugin.postConstruct();
                } catch (IOException e) {
                    LOG.warn("Exception while restoring state of plugin " + plugin.getName(), e);
                }
            }
            objectInputStream.close();
        } catch (Exception e) {
            LOG.error("Exception while restoring state ", e);
            // TODO
        }
    }

    /**
     * Saves the state of plugins to {@link SharedPreferences}.
     */
    public static void saveState() {
        try {
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);

            // store plugin state version
            objectOutputStream.writeInt(PLUGIN_STATE_VERSION);

            List<InstalledPluginHolder> checkedPlugins = mInstalledPlugins.getCheckedItems();
            objectOutputStream.writeInt(checkedPlugins.size());

            for (InstalledPluginHolder plugin : checkedPlugins) {
                objectOutputStream.writeUTF(plugin.getIdentifier());
                plugin.saveState(objectOutputStream);
            }

            SharedPreferences preferences = GeoARApplication.applicationContext
                    .getSharedPreferences(GeoARApplication.PREFERENCES_FILE, Context.MODE_PRIVATE);
            Editor editor = preferences.edit();
            objectOutputStream.flush();
            editor.putString(PLUGIN_STATE_PREF, Base64.encodeToString(outputStream.toByteArray(), Base64.DEFAULT));
            editor.commit();
            objectOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
            // TODO
        }
    }

    /**
     * Parses a version string to long. Assumes that each part of a version
     * string is < 100.
     * 
     * @param version
     *            Version string, e.g. "1.2.3"
     * @return long built by multiplying each version component by 100 to the
     *         power of its position from the back, i.e. "0.0.1" -> 1, "0.1.0"
     *         -> 100
     */
    private static long parseVersionNumber(String version) {
        String[] split = version.split("\\.");
        long versionNumber = 0;
        for (int i = 0; i < split.length; i++) {
            int num = Integer.parseInt(split[i]);
            if (num < 0 || num >= 100) {
                throw new NumberFormatException("Unable to parse version number, each part may not exceed 100");
            }
            versionNumber += Math.pow(100, (split.length - 1) - i) * num;
        }
        return versionNumber;
    }

    /**
     * Extracts and parses the geoar-plugin.xml plugin-descriptor to create and
     * fill a {@link PluginInfo} instance.
     * 
     * @param pluginFile
     * @return
     */
    private static PluginInfo readPluginInfoFromPlugin(File pluginFile) {
        try {
            ZipFile zipFile = new ZipFile(pluginFile);
            ZipEntry pluginDescriptorEntry = zipFile.getEntry("geoar-plugin.xml");
            if (pluginDescriptorEntry == null) {
                return null;
            }

            Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder()
                    .parse(zipFile.getInputStream(pluginDescriptorEntry));
            // Find name
            String name = null;
            NodeList nodeList = document.getElementsByTagName("name");
            if (nodeList != null && nodeList.getLength() >= 1) {
                name = nodeList.item(0).getTextContent();
            } else {
                LOG.warn("Plugin Descriptor for " + pluginFile.getName() + " does not specify a name");
            }

            // Find publisher
            String publisher = null;
            nodeList = document.getElementsByTagName("publisher");
            if (nodeList != null && nodeList.getLength() >= 1) {
                publisher = nodeList.item(0).getTextContent();
            } else {
                LOG.warn("Plugin Descriptor for " + pluginFile.getName() + " does not specify a publisher");
            }

            // Find description
            String description = null;
            nodeList = document.getElementsByTagName("description");
            if (nodeList != null && nodeList.getLength() >= 1) {
                description = nodeList.item(0).getTextContent();
            } else {
                LOG.warn("Plugin Descriptor for " + pluginFile.getName() + " does not specify a description");
            }

            // Find identifier
            String identifier = null;
            nodeList = document.getElementsByTagName("identifier");
            if (nodeList != null && nodeList.getLength() >= 1) {
                identifier = nodeList.item(0).getTextContent();
            } else {
                LOG.warn("Plugin Descriptor for " + pluginFile.getName() + " does not specify an identifier");
            }

            // Find version
            Long version = null;
            nodeList = document.getElementsByTagName("version");
            if (nodeList != null && nodeList.getLength() >= 1) {
                String versionString = "-" + nodeList.item(0).getTextContent();

                Matcher matcher = pluginVersionPattern.matcher(versionString);
                if (matcher.find() && matcher.group(1) != null) {
                    try {
                        version = parseVersionNumber(matcher.group(1));
                    } catch (NumberFormatException e) {
                        LOG.error("Plugin filename version invalid: " + matcher.group(1));
                    }
                }
            } else {
                LOG.warn("Plugin Descriptor for " + pluginFile.getName() + " does not specify a version");
            }

            if (identifier == null) {
                identifier = name;
            }

            return new PluginInfo(pluginFile, name, description, version, identifier, publisher);
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (ZipException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * Reads {@link PluginInfo} from the path of a plugin. This will only
     * extract a name and version information
     * 
     * @param pluginFile
     * @return
     */
    private static PluginInfo readPluginInfoFromFilename(File pluginFile) {
        String pluginFileName = pluginFile.getName();

        Matcher matcher = pluginNamePattern.matcher(pluginFileName);
        if (!matcher.matches()) {
            LOG.error("Plugin filename invalid: " + pluginFileName);
            return null;
        }

        String name = matcher.group(1);

        Long version = null;
        matcher = pluginVersionPattern.matcher(pluginFileName);
        if (matcher.find() && matcher.group(1) != null) {
            try {
                version = parseVersionNumber(matcher.group(1));
            } catch (NumberFormatException e) {
                LOG.error("Plugin filename version invalid: " + matcher.group(1));
            }
        }

        return new PluginInfo(pluginFile, name, null, version, name, null);
    }

    /**
     * Loads all plugins from SD card, compares version strings for plugins with
     * same names and loads the most recent plugin.
     * 
     * The plugin name is the filename without its ending and without optional
     * version string. A version string is introduced by a hyphen, following
     * dot-separated numbers, e.g. "-1.2.3"
     */
    private static void loadPlugins() {
        String[] apksInDirectory = PLUGIN_DIRECTORY_PATH.list(PLUGIN_FILENAME_FILTER);

        if (apksInDirectory == null || apksInDirectory.length == 0) {
            IntroController.startIntro(!mInstalledPlugins.isEmpty());
            return;
        }

        // Map to store all plugins with their versions for loading only the
        // newest ones
        HashMap<String, PluginInfo> pluginVersionMap = new HashMap<String, PluginInfo>();

        for (String pluginFileName : apksInDirectory) {

            PluginInfo pluginInfo = readPluginInfoFromPlugin(new File(PLUGIN_DIRECTORY_PATH, pluginFileName));
            if (pluginInfo == null) {
                LOG.info("Plugin " + pluginFileName + " has no plugin descriptor");
                pluginInfo = readPluginInfoFromFilename(new File(PLUGIN_DIRECTORY_PATH, pluginFileName));
            }

            if (pluginInfo.identifier == null) {
                LOG.error("Plugin " + pluginFileName
                        + " has an invalid plugin descriptor and an invalid filename. Plugin excluded from loading.");
                continue;
            }
            if (pluginInfo.version == null) {
                pluginInfo.version = -1L; // Set unknown version to version -1
            }

            PluginInfo pluginInfoMapping = pluginVersionMap.get(pluginInfo.identifier);
            if (pluginInfoMapping == null || pluginInfoMapping.version < pluginInfo.version) {
                // Plugin not yet known or newer
                pluginVersionMap.put(pluginInfo.identifier, pluginInfo);
            }
        }

        for (PluginInfo pluginInfo : pluginVersionMap.values()) {
            InstalledPluginHolder pluginHolder = new InstalledPluginHolder(pluginInfo);
            mInstalledPlugins.add(pluginHolder);
        }
    }

    /**
     * Reloads all plugins. The current state of the plugins are restored
     * afterwards.
     */
    public static void reloadPlugins() {
        mReloadingPlugins = true;
        saveState();
        mInstalledPlugins.clear();
        mDataSources.clear();
        loadPlugins();
        restoreState();
        mReloadingPlugins = false;
    }

    public static CheckList<InstalledPluginHolder> getInstalledPlugins() {
        return mInstalledPlugins;
    }

    private static void addDataSource(DataSourceHolder dataSource) {
        if (!mDataSources.contains(dataSource))
            mDataSources.add(dataSource);
    }

    private static void removeDataSource(DataSourceHolder dataSource) {
        mDataSources.remove(dataSource);
    }

    /**
     * Returns all {@link DataSourceHolder} of currently activated plugins.
     * 
     * @return
     */
    public static List<DataSourceHolder> getDataSources() {
        return mDataSources;
    }

    public static boolean hasDataSources() {
        return mInstalledPlugins == null ? false : mInstalledPlugins.size() > 0;
    }

    public static InstalledPluginHolder getPluginByIdentifier(String identifier) {
        for (InstalledPluginHolder plugin : mInstalledPlugins) {
            if (plugin.getIdentifier().equals(identifier)) {
                return plugin;
            }
        }
        return null;
    }

}