/**
* (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, immediate=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);
}
}
}