org.jabylon.updatecenter.repository.impl.OBRRepositoryConnectorImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.jabylon.updatecenter.repository.impl.OBRRepositoryConnectorImpl.java

Source

/**
 * (C) Copyright 2013 Jabylon (http://www.jabylon.org) 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.jabylon.updatecenter.repository.impl;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.text.Collator;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.felix.bundlerepository.Repository;
import org.apache.felix.bundlerepository.RepositoryAdmin;
import org.apache.felix.bundlerepository.Resource;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.jabylon.cdo.server.ServerConstants;
import org.jabylon.common.util.PreferencesUtil;
import org.jabylon.common.util.config.DynamicConfigUtil;
import org.jabylon.updatecenter.repository.OBRException;
import org.jabylon.updatecenter.repository.OBRRepositoryService;
import org.jabylon.updatecenter.repository.ResourceFilter;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.Version;
import org.osgi.framework.wiring.FrameworkWiring;
import org.osgi.service.prefs.Preferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Multimap;
import com.google.common.collect.SortedSetMultimap;
import com.google.common.collect.TreeMultimap;

/**
 * @author jutzig.dev@googlemail.com
 *
 */
@Component(enabled = true)
@Service
public class OBRRepositoryConnectorImpl implements OBRRepositoryService {

    private static final String DEFAULT_REPOSITORY = "http://jabylon.org/maven/repository.xml";
    @Reference
    RepositoryAdmin admin;

    private static final Logger logger = LoggerFactory.getLogger(OBRRepositoryConnectorImpl.class);

    private static final Pattern BUNDLE_PATTERN = Pattern.compile("(.*?)_(.*?)\\.jar");

    private static final Comparator<Version> COMPARATOR = new OSGiVersionComparator();

    /**
     * where we download plugins
     */
    private File pluginDir;

    private BundleContext context;

    @Activate
    public void activate() {
        Bundle bundle = FrameworkUtil.getBundle(getClass());
        context = bundle.getBundleContext();
        Thread t = new Thread(new Runnable() {
            //this is expensive so we should do it in a thread instead
            @Override
            public void run() {
                pluginDir = new File(new File(ServerConstants.WORKING_DIR), "addons");
                deployAddons(pluginDir);

            }
        }, "Addon Deployment");
        t.start();
    }

    /*
     * (non-Javadoc)
     *
     * @see org.jabylon.updatecenter.repository.impl.OBRRepositoryService#
     * listInstalledBundles()
     */
    @Override
    public List<Bundle> listInstalledBundles() {

        List<Bundle> resources = Arrays.asList(context.getBundles());
        return resources;
    }

    private void deployAddons(File pluginDir) {
        File[] addons = pluginDir.listFiles();
        if (addons == null)
            return;
        List<String> bundleFiles = getHighestBundleVersions(pluginDir.list());

        List<Bundle> bundles = new ArrayList<Bundle>();
        for (String addonName : bundleFiles) {
            try {
                File addon = new File(pluginDir, addonName);
                String uri = addon.toURI().toString();
                Bundle bundle = context.getBundle(uri);
                if (bundle == null) {
                    logger.info("Installing Addon {}", addonName);
                    bundle = context.installBundle(uri);
                }
                bundles.add(bundle);
            } catch (BundleException e) {
                logger.error("Failed to deploy addon " + addonName);
            }
        }

        for (Bundle bundle : bundles) {
            try {
                bundle.start();
            } catch (BundleException e) {
                logger.error("Failed to start addon " + bundle.getSymbolicName());
            }
        }

    }

    protected List<String> getHighestBundleVersions(String... filenames) {
        if (filenames == null)
            return Collections.emptyList();
        SortedSetMultimap<String, String> map = TreeMultimap.create(Collator.getInstance(),
                new VersionComparator());
        for (String string : filenames) {
            Matcher matcher = BUNDLE_PATTERN.matcher(string);
            if (matcher.matches()) {
                String name = matcher.group(1);
                String version = matcher.group(2);
                map.put(name, version);
            } else {
                logger.warn("{} does not match the pattern {}. Skipping", string, BUNDLE_PATTERN);
            }
        }
        Set<Entry<String, Collection<String>>> entrySet = map.asMap().entrySet();
        List<String> result = new ArrayList<String>(entrySet.size());
        for (Entry<String, Collection<String>> entry : entrySet) {
            result.add(entry.getKey() + "_" + entry.getValue().iterator().next() + ".jar");
        }
        return result;
    }

    protected String getHighestVersion(List<String> versions) {
        String versionName = versions.get(0);
        Version highest = Version.parseVersion(versionName);
        for (String currentName : versions) {
            Version current = Version.parseVersion(currentName);
            if (current.compareTo(highest) > 0)
                highest = current;
        }
        return highest.toString();
    }

    @Override
    public List<Resource> getAvailableResources(ResourceFilter filter) {
        List<Resource> filteredResources = new ArrayList<Resource>();
        List<Bundle> bundles = listInstalledBundles();
        Multimap<String, Bundle> map = buildMap(bundles);
        try {

            Repository repository = getRepository();
            Resource[] resources = repository.getResources();
            for (Resource resource : resources) {
                if (applies(filter, map, resource))
                    filteredResources.add(resource);
            }

            if (filter == ResourceFilter.PLUGIN) {
                //for update we only want to see the latest version of everything updateable
                filteredResources = removeOldVersions(filteredResources);
            }

        } catch (Exception e) {
            logger.error("Failed to discover resources with filter " + filter, e);
        }
        return filteredResources;
    }

    private List<Resource> removeOldVersions(List<Resource> resources) {
        SortedSetMultimap<String, Resource> map = TreeMultimap.create(Collator.getInstance(),
                new ResourceComparator());
        for (Resource bundle : resources) {
            map.put(bundle.getSymbolicName(), bundle);
        }
        resources.clear();
        Set<Entry<String, Collection<Resource>>> entries = map.asMap().entrySet();
        for (Entry<String, Collection<Resource>> entry : entries) {
            //add the highest version
            resources.add(entry.getValue().iterator().next());
        }
        return resources;
    }

    private Multimap<String, Bundle> buildMap(List<Bundle> bundles) {
        SortedSetMultimap<String, Bundle> result = TreeMultimap.create(Collator.getInstance(),
                new BundleVersionComparator());
        for (Bundle bundle : bundles) {
            result.put(bundle.getSymbolicName(), bundle);
        }
        return result;
    }

    private Repository getRepository() {
        Preferences node = PreferencesUtil.workspaceScope().node("update");
        String url = node.get("update.url", DEFAULT_REPOSITORY);
        try {
            return admin.addRepository(url);
        } catch (Exception e) {
            logger.error("Failed to add repository " + url, e);
        }
        return null;
    }

    private boolean applies(ResourceFilter filter, Multimap<String, Bundle> bundles, Resource resource) {
        switch (filter) {
        case ALL:
            return true;
        case PLUGIN:
            String[] categories = resource.getCategories();
            if (categories == null)
                return false;
            for (String string : categories) {
                if ("Jabylon-Plugin".equals(string))
                    return !bundles.containsKey(resource.getSymbolicName());
            }
            return false;
        case INSTALLABLE: {
            // can install anything that isn't installed yet
            return !bundles.containsKey(resource.getSymbolicName());
        }
        case UPDATEABLE:
            Collection<Bundle> installed = bundles.get(resource.getSymbolicName());
            if (installed.isEmpty())
                return false;
            // updateable if the latest installed version is less than
            // resource.getVersion
            return COMPARATOR.compare(installed.iterator().next().getVersion(), resource.getVersion()) > 0;
        case INSTALLED:
            Collection<Bundle> available = bundles.get(resource.getSymbolicName());
            for (Bundle bundle : available) {
                if (bundle.getVersion().equals(resource.getVersion()))
                    return true;
            }
            return false;
        default:
            break;
        }
        return true;
    }

    @Override
    public void install(String resourceId) throws OBRException {
        Resource[] resources;

        String filter = MessageFormat.format("({0}={1})", Resource.ID, resourceId);
        try {
            resources = admin.discoverResources(filter);
            if (resources.length > 0) {
                install(resources[0]);

            }
        } catch (InvalidSyntaxException e) {
            logger.error("Invalid OSGi filter " + filter, e);
        }

    }

    @Override
    public void install(Resource... resources) throws OBRException {
        List<Bundle> bundles = new ArrayList<Bundle>();
        for (Resource resource : resources) {
            try {

                Bundle bundle = context.installBundle(downloadBundle(resource));
                bundles.add(bundle);
            } catch (BundleException e) {
                OBRException exception = new OBRException("Failed to start bundle " + resource, e);
                logger.error("Plugin Installation failed", e);
                throw exception;
            } catch (MalformedURLException e) {
                OBRException exception = new OBRException("Incorrect URI for bundle " + resource, e);
                logger.error("Plugin Installation failed", e);
                throw exception;
            } catch (IOException e) {
                OBRException exception = new OBRException("Failed to download bundle " + resource, e);
                logger.error("Plugin Installation failed", e);
                throw exception;
            }
        }
        boolean needRefresh = false;
        for (Bundle bundle : bundles) {
            logger.info("Starting Bundle " + bundle);
            try {
                needRefresh |= checkIfUpdate(bundle);
                bundle.start();
            } catch (BundleException e) {
                logger.error("Failed to start bundle " + bundle.getSymbolicName(), e);
                throw new OBRException("Failed to start bundle " + bundle.getSymbolicName(), e);
            }
        }
        if (!bundles.isEmpty()) {
            DynamicConfigUtil.refresh();
        }
        if (needRefresh) {
            Bundle systemBundle = context.getBundle(0);
            FrameworkWiring wiring = systemBundle.adapt(FrameworkWiring.class);
            if (wiring != null) {
                logger.info("Executing package refresh after update");
                wiring.refreshBundles(null);
            }

        }

        // for some reason this doesn't work in jetty
        //        for (Resource resource : resources) {
        //           resolver.add(resource);
        //        }
        //
        //        if (resolver.resolve(Resolver.NO_OPTIONAL_RESOURCES)) {
        //            resolver.deploy(Resolver.START);
        //        }
        //        else {
        //           Reason[] reasons = resolver.getUnsatisfiedRequirements();
        //           for (Reason reason : reasons) {
        //              logger.error("Failed to install "+reason.getResource() + " missing : " +reason.getRequirement());
        //         }
        //           throw new OBRException("Installation Failed", reasons);
        //        }
    }

    /**
     * checks if the bundle updates an existing one, and if so, deactivates any others with the same name
     * @param bundle
     * @param <code>true</code> if one or more bundles had to be stopped/uninstalled because they are updated
     */
    private boolean checkIfUpdate(Bundle bundle) {
        Bundle[] bundles = context.getBundles();
        boolean updated = false;
        for (Bundle other : bundles) {
            if (other == bundle)
                continue;
            if (other.getSymbolicName().equals(bundle.getSymbolicName())) {
                try {
                    if (other.getState() == Bundle.UNINSTALLED)
                        break;
                    updated = true;
                    if (isSingleton(other)) {
                        logger.info("{}:{} is a singleton. Uninstalling due to an update", other.getSymbolicName(),
                                other.getVersion());
                        other.uninstall();
                    } else {
                        logger.info("Stopping {}:{} due to an update", other.getSymbolicName(), other.getVersion());
                        other.stop();
                    }
                } catch (BundleException e) {
                    logger.error("Failed to stop " + other, e);
                }

            }
        }
        return updated;
    }

    private boolean isSingleton(Bundle other) {
        String string = other.getHeaders().get("Bundle-SymbolicName");
        return string != null && string.contains("singleton:=true");
    }

    private String downloadBundle(Resource resource) throws MalformedURLException, IOException {
        String uriString = resource.getURI();
        URI uri = URI.create(uriString);
        InputStream stream = null;
        OutputStream out = null;
        File destination;
        try {
            stream = uri.toURL().openStream();
            pluginDir.mkdirs();
            destination = new File(pluginDir, resource.getSymbolicName() + "_" + resource.getVersion() + ".jar");
            out = new FileOutputStream(destination);
            byte[] buffer = new byte[4096];
            int read = 0;
            while ((read = stream.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
        } finally {
            if (out != null)
                out.close();
            if (stream != null)
                stream.close();
        }
        return destination.toURI().toString();
    }

    @Override
    public Resource[] findResources(String id) {
        // String filter = "(&(SYMBOLIC_NAME={0})(VERSION={1}))";
        String filter = "({0}={1})";
        filter = MessageFormat.format(filter, Resource.ID, id);
        try {
            return admin.discoverResources(filter);
        } catch (InvalidSyntaxException e) {
            logger.error("Invalid OSGi filter " + filter, e);
        }
        return new Resource[0];
    }

    private static class BundleVersionComparator implements Comparator<Bundle> {

        @Override
        public int compare(Bundle o1, Bundle o2) {
            Version v1 = o1.getVersion();
            Version v2 = o2.getVersion();
            return COMPARATOR.compare(v1, v2);
        }

    }

    private static class OSGiVersionComparator implements Comparator<Version> {

        @Override
        public int compare(Version v1, Version v2) {

            if (!("SNAPSHOT".equals(v1.getQualifier()) && "SNAPSHOT".equals(v2.getQualifier()))) {
                if ("SNAPSHOT".equals(v1.getQualifier())) {
                    if (v1.getMajor() == v2.getMajor() && v1.getMicro() == v2.getMicro()
                            && v1.getMinor() == v2.getMinor()) {
                        //we consider SNAPSHOT < release
                        return 1;
                    }
                } else if ("SNAPSHOT".equals(v2.getQualifier())) {
                    if (v1.getMajor() == v2.getMajor() && v1.getMicro() == v2.getMicro()
                            && v1.getMinor() == v2.getMinor()) {
                        //we consider SNAPSHOT < release
                        return -1;
                    }
                }
            }
            // to have the highest version at the beginning
            return -(v1.compareTo(v2));
        }

    }

    private static class VersionComparator implements Comparator<String> {

        @Override
        public int compare(String o1, String o2) {

            Version v1 = Version.parseVersion(o1);
            Version v2 = Version.parseVersion(o2);
            return COMPARATOR.compare(v1, v2);
        }
    }

    private static class ResourceComparator implements Comparator<Resource> {

        @Override
        public int compare(Resource o1, Resource o2) {

            Version v1 = o1.getVersion();
            Version v2 = o2.getVersion();
            return COMPARATOR.compare(v1, v2);
        }
    }

}