/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.karaf.bundle.core.internal; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.apache.karaf.bundle.core.BundleService; import org.apache.karaf.bundle.core.BundleWatcher; import org.apache.karaf.util.bundles.BundleUtils; import org.apache.karaf.util.maven.Parser; import org.osgi.framework.*; import org.osgi.framework.wiring.FrameworkWiring; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A Runnable singleton which watches at the defined location for bundle * updates. */ public class BundleWatcherImpl implements Runnable, BundleListener, BundleWatcher { private final Logger logger = LoggerFactory.getLogger(BundleWatcherImpl.class); private BundleContext bundleContext; private final BundleService bundleService; private final MavenConfigService localRepoDetector; private AtomicBoolean running = new AtomicBoolean(false); private long interval = 1000L; private List<String> watchURLs = new CopyOnWriteArrayList<String>(); private AtomicInteger counter = new AtomicInteger(0); public BundleWatcherImpl(BundleContext bundleContext, MavenConfigService mavenConfigService, BundleService bundleService) { this.bundleContext = bundleContext; this.localRepoDetector = mavenConfigService; this.bundleService = bundleService; } /* (non-Javadoc) * @see org.apache.karaf.dev.core.internal.BundleWatcher#bundleChanged(org.osgi.framework.BundleEvent) */ @Override public void bundleChanged(BundleEvent event) { if (event.getType() == BundleEvent.INSTALLED || event.getType() == BundleEvent.UNINSTALLED) { counter.incrementAndGet(); } } public void run() { logger.debug("Bundle watcher thread started"); int oldCounter = -1; Set<Bundle> watchedBundles = new HashSet<Bundle>(); while (running.get() && watchURLs.size() > 0) { if (oldCounter != counter.get()) { oldCounter = counter.get(); watchedBundles.clear(); for (String bundleURL : watchURLs) { // Transform into regexp bundleURL = bundleURL.replaceAll("\\*", ".*"); for (Bundle bundle : bundleService.selectBundles(Collections.singletonList(bundleURL), false)) { if (isMavenSnapshotUrl(getLocation(bundle))) { watchedBundles.add(bundle); } } } } if (watchedBundles.size() > 0) { // Get the wiring before any in case of a refresh of a dependency FrameworkWiring wiring = bundleContext.getBundle(0).adapt(FrameworkWiring.class); File localRepository = this.localRepoDetector.getLocalRepository(); List<Bundle> updated = new ArrayList<Bundle>(); for (Bundle bundle : watchedBundles) { try { updateBundleIfNecessary(localRepository, updated, bundle); } catch (IOException ex) { logger.error("Error watching bundle.", ex); } catch (BundleException ex) { logger.error("Error updating bundle.", ex); } } if (!updated.isEmpty()) { try { final CountDownLatch latch = new CountDownLatch(1); wiring.refreshBundles(updated, new FrameworkListener() { public void frameworkEvent(FrameworkEvent event) { latch.countDown(); } }); latch.await(); } catch (InterruptedException e) { running.set(false); } for (Bundle bundle : updated) { try { if (bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null) { logger.info("[Watch] Bundle {} is a fragment, so it's not started", bundle.getSymbolicName()); } else { bundle.start(Bundle.START_TRANSIENT); } } catch (BundleException ex) { logger.warn("[Watch] Error starting bundle", ex); } } } } try { Thread.sleep(interval); } catch (InterruptedException ex) { running.set(false); } } if (logger.isDebugEnabled()) { logger.debug("Bundle watcher thread stopped"); } } private String getLocation(Bundle bundle) { String location = bundle.getHeaders().get(Constants.BUNDLE_UPDATELOCATION); return location != null ? location : bundle.getLocation(); } private boolean isMavenSnapshotUrl(String url) { return url.startsWith("mvn:") && url.contains("SNAPSHOT"); } private void updateBundleIfNecessary(File localRepository, List<Bundle> updated, Bundle bundle) throws BundleException, IOException { File location = getBundleExternalLocation(localRepository, bundle); if (location != null && location.exists() && location.lastModified() > bundle.getLastModified()) { InputStream is = new FileInputStream(location); try { logger.info("[Watch] Updating watched bundle: {} ({})", bundle.getSymbolicName(), bundle.getVersion()); if (bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null) { logger.info("[Watch] Bundle {} is a fragment, so it's not stopped", bundle.getSymbolicName()); } else { bundle.stop(Bundle.STOP_TRANSIENT); } // We don't really want to loose the update-location String updateLocation = getLocation(bundle); if (!updateLocation.equals(bundle.getLocation())) { File file = BundleUtils.fixBundleWithUpdateLocation(is, updateLocation); try (FileInputStream fis = new FileInputStream(file)) { bundle.update(fis); } file.delete(); } else { bundle.update(is); } updated.add(bundle); } finally { is.close(); } } } /* (non-Javadoc) * @see org.apache.karaf.dev.core.internal.BundleWatcher#add(java.lang.String) */ @Override public void add(String url) { boolean shouldStart = running.get() && (watchURLs.size() == 0); if (!watchURLs.contains(url)) { watchURLs.add(url); counter.incrementAndGet(); } if (shouldStart) { Thread thread = new Thread(this); thread.start(); } } /* (non-Javadoc) * @see org.apache.karaf.dev.core.internal.BundleWatcher#remove(java.lang.String) */ @Override public void remove(String url) { watchURLs.remove(url); counter.incrementAndGet(); } /** * Return the location of the Bundle inside the local maven repository. * * @param localRepository the repository where to look for bundle update. * @param bundle the bundle to check update. * @return the updated file. */ private File getBundleExternalLocation(File localRepository, Bundle bundle) { try { Parser p = new Parser(getLocation(bundle).substring(4)); return new File(localRepository.getPath() + File.separator + p.getArtifactPath()); } catch (MalformedURLException e) { logger.error("Could not parse artifact path for bundle" + bundle.getSymbolicName(), e); } return null; } public void start() { bundleContext.addBundleListener(this); // start the watch thread if (running.compareAndSet(false, true)) { if (watchURLs.size() > 0) { Thread thread = new Thread(this); thread.start(); } } } /** * Stop the execution of the thread and releases the singleton instance. */ public void stop() { running.set(false); bundleContext.removeBundleListener(this); } /* (non-Javadoc) * @see org.apache.karaf.dev.core.internal.BundleWatcher#getWatchURLs() */ @Override public List<String> getWatchURLs() { return watchURLs; } /* (non-Javadoc) * @see org.apache.karaf.dev.core.internal.BundleWatcher#setWatchURLs(java.util.List) */ @Override public void setWatchURLs(List<String> watchURLs) { this.watchURLs = watchURLs; } public long getInterval() { return interval; } public void setInterval(long interval) { this.interval = interval; } public boolean isRunning() { return running.get(); } @Override public List<Bundle> getBundlesByURL(String urlFilter) { urlFilter = urlFilter.replaceAll("\\*", ".*"); return bundleService.selectBundles(Collections.singletonList(urlFilter), false); } }