001/*
002 * (C) Copyright 2006-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     bstefanescu
018 *     Kevin Leturc <kleturc@nuxeo.com>
019 */
020package org.nuxeo.runtime.reload;
021
022import java.io.File;
023import java.io.IOException;
024import java.io.InputStream;
025import java.lang.reflect.Field;
026import java.net.MalformedURLException;
027import java.net.URL;
028import java.nio.file.Files;
029import java.nio.file.Path;
030import java.nio.file.StandardCopyOption;
031import java.util.ArrayList;
032import java.util.LinkedHashMap;
033import java.util.List;
034import java.util.Map;
035import java.util.Map.Entry;
036import java.util.Optional;
037import java.util.concurrent.TimeUnit;
038import java.util.jar.JarFile;
039import java.util.jar.Manifest;
040import java.util.stream.Collectors;
041import java.util.stream.Stream;
042
043import javax.transaction.Transaction;
044
045import org.apache.commons.io.FileUtils;
046import org.apache.logging.log4j.LogManager;
047import org.apache.logging.log4j.Logger;
048import org.nuxeo.common.Environment;
049import org.nuxeo.common.utils.JarUtils;
050import org.nuxeo.common.utils.ZipUtils;
051import org.nuxeo.osgi.application.DevMutableClassLoader;
052import org.nuxeo.runtime.RuntimeServiceException;
053import org.nuxeo.runtime.api.Framework;
054import org.nuxeo.runtime.deployment.preprocessor.DeploymentPreprocessor;
055import org.nuxeo.runtime.model.ComponentContext;
056import org.nuxeo.runtime.model.ComponentManager;
057import org.nuxeo.runtime.model.DefaultComponent;
058import org.nuxeo.runtime.services.event.Event;
059import org.nuxeo.runtime.services.event.EventService;
060import org.nuxeo.runtime.transaction.TransactionHelper;
061import org.nuxeo.runtime.util.Watch;
062import org.osgi.framework.Bundle;
063import org.osgi.framework.BundleContext;
064import org.osgi.framework.BundleException;
065import org.osgi.framework.ServiceReference;
066import org.osgi.service.packageadmin.PackageAdmin;
067
068/**
069 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
070 */
071public class ReloadComponent extends DefaultComponent implements ReloadService {
072
073    /**
074     * The reload strategy to adopt for hot reload. Default value is {@link #RELOAD_STRATEGY_VALUE_DEFAULT}.
075     *
076     * @since 9.3
077     */
078    public static final String RELOAD_STRATEGY_PARAMETER = "org.nuxeo.runtime.reload_strategy";
079
080    public static final String RELOAD_STRATEGY_VALUE_UNSTASH = "unstash";
081
082    public static final String RELOAD_STRATEGY_VALUE_STANDBY = "standby";
083
084    public static final String RELOAD_STRATEGY_VALUE_RESTART = "restart";
085
086    public static final String RELOAD_STRATEGY_VALUE_DEFAULT = RELOAD_STRATEGY_VALUE_STANDBY;
087
088    private static final Logger log = LogManager.getLogger(ReloadComponent.class);
089
090    protected static Bundle bundle;
091
092    protected Long lastFlushed;
093
094    public static BundleContext getBundleContext() {
095        return bundle.getBundleContext();
096    }
097
098    public static Bundle getBundle() {
099        return bundle;
100    }
101
102    @Override
103    public void activate(ComponentContext context) {
104        super.activate(context);
105        bundle = context.getRuntimeContext().getBundle();
106    }
107
108    @Override
109    public void deactivate(ComponentContext context) {
110        super.deactivate(context);
111        bundle = null;
112    }
113
114    /**
115     * @deprecated since 9.3, this method is only used in deployBundles and undeployBundles which are deprecated. Keep
116     *             it for backward compatibility.
117     */
118    @Deprecated
119    protected void refreshComponents() {
120        String reloadStrategy = Framework.getProperty(RELOAD_STRATEGY_PARAMETER, RELOAD_STRATEGY_VALUE_DEFAULT);
121        log.info("Refresh components. Strategy={}", reloadStrategy);
122        // reload components / contributions
123        ComponentManager mgr = Framework.getRuntime().getComponentManager();
124        switch (reloadStrategy) {
125        case RELOAD_STRATEGY_VALUE_UNSTASH:
126            // compat mode
127            mgr.unstash();
128            break;
129        case RELOAD_STRATEGY_VALUE_STANDBY:
130            // standby / resume
131            mgr.standby();
132            mgr.unstash();
133            mgr.resume();
134            break;
135        case RELOAD_STRATEGY_VALUE_RESTART:
136        default:
137            // restart mode
138            mgr.refresh(false);
139            break;
140        }
141    }
142
143    @Override
144    public void reload() throws InterruptedException {
145        log.debug("Starting reload");
146
147        try {
148            reloadProperties();
149        } catch (IOException e) {
150            throw new RuntimeServiceException(e);
151        }
152
153        triggerReloadWithNewTransaction(RELOAD_EVENT_ID);
154    }
155
156    @Override
157    public void reloadProperties() throws IOException {
158        log.info("Before reload runtime properties");
159        Framework.getRuntime().reloadProperties();
160        log.info("After reload runtime properties");
161    }
162
163    @Override
164    public void reloadSeamComponents() {
165        log.info("Reload Seam components");
166        Framework.getService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, RELOAD_SEAM_EVENT_ID, this, null));
167    }
168
169    @Override
170    public void flush() {
171        log.info("Before flush caches");
172        Framework.getService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, FLUSH_EVENT_ID, this, null));
173        flushJaasCache();
174        setFlushedNow();
175        log.info("After flush caches");
176    }
177
178    @Override
179    public void flushJaasCache() {
180        log.info("Before flush the JAAS cache");
181        Framework.getService(EventService.class).sendEvent(new Event("usermanager", "user_changed", this, "Deployer"));
182        setFlushedNow();
183        log.info("After flush the JAAS cache");
184    }
185
186    @Override
187    public void flushSeamComponents() {
188        log.info("Flush Seam components");
189        Framework.getService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, FLUSH_SEAM_EVENT_ID, this, null));
190        setFlushedNow();
191    }
192
193    /**
194     * @deprecated since 9.3 use {@link #reloadBundles(ReloadContext)} instead.
195     */
196    @Override
197    @Deprecated
198    public void deployBundles(List<File> files, boolean reloadResources) throws BundleException {
199        long begin = System.currentTimeMillis();
200        List<String> missingNames = files.stream()
201                                         .filter(file -> getOSGIBundleName(file) == null)
202                                         .map(File::getAbsolutePath)
203                                         .collect(Collectors.toList());
204        if (!missingNames.isEmpty()) {
205            missingNames.forEach(name -> log.error("No Bundle-SymbolicName found in MANIFEST for jar at '{}'", name));
206            // TODO investigate why we need to exit here, getBundleContext().installBundle(path) will throw an exception
207            // unless, maybe tests ?
208            return;
209        }
210
211        log.info(() -> {
212            StringBuilder builder = new StringBuilder("Before deploy bundles\n");
213            Framework.getRuntime().getStatusMessage(builder);
214            return builder.toString();
215        });
216
217        // Reload resources
218        if (reloadResources) {
219            List<URL> urls = files.stream().map(this::toURL).collect(Collectors.toList());
220            Framework.reloadResourceLoader(urls, null);
221        }
222
223        // Deploy bundles
224        Transaction tx = TransactionHelper.suspendTransaction();
225        try {
226            _deployBundles(files);
227            refreshComponents();
228        } finally {
229            TransactionHelper.resumeTransaction(tx);
230        }
231
232        log.info(() -> {
233            StringBuilder builder = new StringBuilder("After deploy bundles.\n");
234            Framework.getRuntime().getStatusMessage(builder);
235            return builder.toString();
236        });
237        log.info("Hot deploy was done in {} ms.", System.currentTimeMillis() - begin);
238    }
239
240    /**
241     * @deprecated since 9.3 use {@link #reloadBundles(ReloadContext)} instead.
242     */
243    @Override
244    @Deprecated
245    public void undeployBundles(List<String> bundleNames, boolean reloadResources) throws BundleException {
246        long begin = System.currentTimeMillis();
247        log.info(() -> {
248            StringBuilder builder = new StringBuilder("Before undeploy bundles\n");
249            Framework.getRuntime().getStatusMessage(builder);
250            return builder.toString();
251        });
252
253        // Undeploy bundles
254        Transaction tx = TransactionHelper.suspendTransaction();
255        ReloadResult result = new ReloadResult();
256        try {
257            result.merge(_undeployBundles(bundleNames));
258            refreshComponents();
259        } finally {
260            TransactionHelper.resumeTransaction(tx);
261        }
262
263        // Reload resources
264        if (reloadResources) {
265            List<URL> undeployedBundleURLs = result.undeployedBundles.stream().map(this::toURL).collect(
266                    Collectors.toList());
267            Framework.reloadResourceLoader(null, undeployedBundleURLs);
268        }
269
270        log.info(() -> {
271            StringBuilder builder = new StringBuilder("After undeploy bundles.\n");
272            Framework.getRuntime().getStatusMessage(builder);
273            return builder.toString();
274        });
275        log.info("Hot undeploy was done in {} ms.", System.currentTimeMillis() - begin);
276    }
277
278    @Override
279    public synchronized ReloadResult reloadBundles(ReloadContext context) throws BundleException {
280        ReloadResult result = new ReloadResult();
281        List<String> bundlesNamesToUndeploy = context.bundlesNamesToUndeploy;
282
283        Watch watch = new Watch(new LinkedHashMap<>()).start();
284        log.info(() -> {
285            StringBuilder builder = new StringBuilder("Before updating Nuxeo server\n");
286            Framework.getRuntime().getStatusMessage(builder);
287            return builder.toString();
288        });
289        // get class loader
290        Optional<DevMutableClassLoader> classLoader = Optional.of(getClass().getClassLoader())
291                                                              .filter(DevMutableClassLoader.class::isInstance)
292                                                              .map(DevMutableClassLoader.class::cast);
293
294        watch.start("flush");
295        flush();
296        watch.stop("flush");
297
298        // Suspend current transaction
299        Transaction tx = TransactionHelper.suspendTransaction();
300
301        try {
302            // Stop or Standby the component manager
303            ComponentManager componentManager = Framework.getRuntime().getComponentManager();
304            String reloadStrategy = Framework.getProperty(RELOAD_STRATEGY_PARAMETER, RELOAD_STRATEGY_VALUE_DEFAULT);
305            log.info("Component reload strategy={}", reloadStrategy);
306
307            watch.start("stop/standby");
308            log.info("Before stop/standby component manager");
309            if (RELOAD_STRATEGY_VALUE_RESTART.equals(reloadStrategy)) {
310                componentManager.stop();
311            } else {
312                // standby strategy by default
313                componentManager.standby();
314            }
315            log.info("After stop/standby component manager");
316            watch.stop("stop/standby");
317
318            // Undeploy bundles
319            if (!bundlesNamesToUndeploy.isEmpty()) {
320                watch.start("undeploy-bundles");
321                log.info("Before undeploy bundles");
322                logComponentManagerStatus();
323
324                result.merge(_undeployBundles(bundlesNamesToUndeploy));
325                clearJarFileFactoryCache(result);
326                componentManager.unstash();
327
328                // Clear the class loader
329                classLoader.ifPresent(DevMutableClassLoader::clearPreviousClassLoader);
330                // TODO shall we do a GC here ? see DevFrameworkBootstrap#clearClassLoader
331
332                log.info("After undeploy bundles");
333                logComponentManagerStatus();
334                watch.stop("undeploy-bundles");
335            }
336
337            watch.start("delete-copy");
338            // Delete old bundles
339            log.info("Before delete-copy");
340            List<URL> urlsToRemove = result.undeployedBundles.stream()
341                                                             .map(Bundle::getLocation)
342                                                             .map(File::new)
343                                                             .peek(File::delete)
344                                                             .map(this::toURL)
345                                                             .collect(Collectors.toList());
346            // Then copy new ones
347            List<File> bundlesToDeploy = copyBundlesToDeploy(context);
348            List<URL> urlsToAdd = bundlesToDeploy.stream().map(this::toURL).collect(Collectors.toList());
349            log.info("After delete-copy");
350            watch.stop("delete-copy");
351
352            // Reload resources
353            watch.start("reload-resources");
354            Framework.reloadResourceLoader(urlsToAdd, urlsToRemove);
355            watch.stop("reload-resources");
356
357            // Deploy bundles
358            if (!bundlesToDeploy.isEmpty()) {
359                watch.start("deploy-bundles");
360                log.info("Before deploy bundles");
361                logComponentManagerStatus();
362
363                // Fill the class loader
364                classLoader.ifPresent(cl -> cl.addClassLoader(urlsToAdd.toArray(new URL[0])));
365
366                result.merge(_deployBundles(bundlesToDeploy));
367                componentManager.unstash();
368
369                log.info("After deploy bundles");
370                logComponentManagerStatus();
371                watch.stop("deploy-bundles");
372            }
373
374            // Start or Resume the component manager
375            watch.start("start/resume");
376            log.info("Before start/resume component manager");
377            if (RELOAD_STRATEGY_VALUE_RESTART.equals(reloadStrategy)) {
378                componentManager.start();
379            } else {
380                // standby strategy by default
381                componentManager.resume();
382            }
383            log.info("After start/resume component manager");
384            watch.stop("start/resume");
385
386            try {
387                // run deployment preprocessor
388                watch.start("deployment-preprocessor");
389                runDeploymentPreprocessor();
390                watch.stop("deployment-preprocessor");
391            } catch (IOException e) {
392                throw new BundleException("Unable to run deployment preprocessor", e);
393            }
394
395            try {
396                // reload
397                watch.start("reload-properties");
398                reloadProperties();
399                watch.stop("reload-properties");
400            } catch (IOException e) {
401                throw new BundleException("Unable to reload properties", e);
402            }
403        } finally {
404            TransactionHelper.resumeTransaction(tx);
405        }
406
407        log.info(() -> {
408            StringBuilder builder = new StringBuilder("After updating Nuxeo server\n");
409            Framework.getRuntime().getStatusMessage(builder);
410            return builder.toString();
411        });
412
413        watch.stop();
414        log.info("Hot reload was done in {} ms, detailed steps:\n{}",
415                () -> watch.getTotal().elapsed(TimeUnit.MILLISECONDS),
416                () -> Stream.of(watch.getIntervals())
417                            .map(i -> "- " + i.getName() + ": " + i.elapsed(TimeUnit.MILLISECONDS) + " ms")
418                            .collect(Collectors.joining("\n")));
419        return result;
420    }
421
422    protected List<File> copyBundlesToDeploy(ReloadContext context) throws BundleException {
423        List<File> bundlesToDeploy = new ArrayList<>();
424        Path homePath = Framework.getRuntime().getHome().toPath();
425        Path destinationPath = homePath.resolve(context.bundlesDestination);
426        try {
427            Files.createDirectories(destinationPath);
428            for (File bundle : context.bundlesToDeploy) {
429                Path bundlePath = bundle.toPath();
430                // check if the bundle is located under the desired destination
431                // if not copy it to the desired destination
432                if (!bundlePath.startsWith(destinationPath)) {
433                    if (Files.isDirectory(bundlePath)) {
434                        // If it's a directory, assume that it's an exploded jar
435                        bundlePath = JarUtils.zipDirectory(bundlePath,
436                                destinationPath.resolve("hotreload-bundle-" + System.currentTimeMillis() + ".jar"),
437                                StandardCopyOption.REPLACE_EXISTING);
438                    } else {
439                        bundlePath = destinationPath.resolve(bundle.getName());
440                        // JDK nio Files will replace the existing file (if destination already exists) which is an
441                        // an issue on Windows cause you can't replace a file used by the JVM
442                        // so use commons-io instead because it will override the content by using a FileInputStream
443                        // instead of replacing the file
444                        FileUtils.copyFile(bundle, bundlePath.toFile(), false);
445                    }
446                }
447                bundlesToDeploy.add(bundlePath.toFile());
448            }
449            return bundlesToDeploy;
450        } catch (IOException e) {
451            throw new BundleException("Unable to copy bundles to " + destinationPath, e);
452        }
453    }
454
455    /*
456     * TODO Change this method name when deployBundles will be removed.
457     */
458    protected ReloadResult _deployBundles(List<File> bundlesToDeploy) throws BundleException {
459        ReloadResult result = new ReloadResult();
460        BundleContext bundleContext = getBundleContext();
461        for (File file : bundlesToDeploy) {
462            String path = file.getAbsolutePath();
463            log.info("Before deploy bundle for file at '{}'", path);
464            Bundle bundle = bundleContext.installBundle(path);
465            if (bundle == null) {
466                // TODO check why this is necessary, our implementation always return sth
467                throw new IllegalArgumentException("Could not find a valid bundle at path: " + path);
468            }
469            bundle.start();
470            result.deployedBundles.add(bundle);
471            log.info("Deploy done for bundle with name '{}'", bundle.getSymbolicName());
472        }
473        return result;
474    }
475
476    /*
477     * TODO Change this method name when undeployBundles will be removed.
478     */
479    protected ReloadResult _undeployBundles(List<String> bundleNames) throws BundleException {
480        ReloadResult result = new ReloadResult();
481        BundleContext ctx = getBundleContext();
482        ServiceReference ref = ctx.getServiceReference(PackageAdmin.class.getName());
483        PackageAdmin srv = (PackageAdmin) ctx.getService(ref);
484        try {
485            for (String bundleName : bundleNames) {
486                for (Bundle bundle : srv.getBundles(bundleName, null)) {
487                    if (bundle != null && bundle.getState() == Bundle.ACTIVE) {
488                        log.info("Before undeploy bundle with name '{}'.", bundleName);
489                        bundle.stop();
490                        bundle.uninstall();
491                        result.undeployedBundles.add(bundle);
492                        log.info("After undeploy bundle with name '{}'.", bundleName);
493                    }
494                }
495            }
496        } finally {
497            ctx.ungetService(ref);
498        }
499        return result;
500    }
501
502    /**
503     * Gets the un-deployed bundle from given {@link ReloadResult result} and try to remove them from
504     * {@link sun.net.www.protocol.jar.JarFileFactory} otherwise we'll have resource conflict when opening
505     * {@link InputStream stream} from {@link URL url}.
506     */
507    @SuppressWarnings({ "unchecked", "SynchronizationOnLocalVariableOrMethodParameter" })
508    protected void clearJarFileFactoryCache(ReloadResult result) {
509        try {
510            List<String> jarLocations = result.undeployedBundlesAsStream().map(Bundle::getLocation).collect(
511                    Collectors.toList());
512            log.debug("Clear JarFileFactory caches for jars={}", jarLocations);
513            Class jarFileFactory = Class.forName("sun.net.www.protocol.jar.JarFileFactory");
514
515            Field factoryInstanceField = jarFileFactory.getDeclaredField("instance");
516            factoryInstanceField.setAccessible(true);
517            Object factoryInstance = factoryInstanceField.get(null);
518
519            Field fileCacheField = jarFileFactory.getDeclaredField("fileCache");
520            fileCacheField.setAccessible(true);
521            Map<String, JarFile> fileCache = (Map<String, JarFile>) fileCacheField.get(null);
522
523            Field urlCacheField = jarFileFactory.getDeclaredField("urlCache");
524            urlCacheField.setAccessible(true);
525            Map<JarFile, URL> urlCache = (Map<JarFile, URL>) urlCacheField.get(null);
526
527            synchronized (factoryInstance) {
528                // collect keys of cache
529                List<JarFile> urlCacheRemoveKeys = new ArrayList<>();
530                for (Entry<JarFile, URL> entry : urlCache.entrySet()) {
531                    JarFile jarFile = entry.getKey();
532                    if (jarLocations.stream().anyMatch(jar -> jar.startsWith(jarFile.getName()))) {
533                        urlCacheRemoveKeys.add(jarFile);
534                    }
535                }
536
537                List<String> fileCacheRemoveKeys = new ArrayList<>();
538                for (Entry<String, JarFile> entry : fileCache.entrySet()) {
539                    if (urlCacheRemoveKeys.contains(entry.getValue())) {
540                        fileCacheRemoveKeys.add(entry.getKey());
541                    }
542                }
543
544                // now remove from factory
545                for (String fileCacheRemoveKey : fileCacheRemoveKeys) {
546                    JarFile remove = fileCache.remove(fileCacheRemoveKey);
547                    if (remove != null) {
548                        log.trace("Removed item from fileCache={}", remove);
549                    }
550                }
551
552                for (JarFile urlCacheRemoveKey : urlCacheRemoveKeys) {
553                    URL remove = urlCache.remove(urlCacheRemoveKey);
554                    try {
555                        urlCacheRemoveKey.close();
556                    } catch (IOException e) {
557                        log.info("Unable to close JarFile={}", urlCacheRemoveKey, e);
558                    }
559                    if (remove != null) {
560                        log.trace("Removed item from urlCache={}",  remove);
561                    }
562                }
563            }
564        } catch (ReflectiveOperationException | ClassCastException e) {
565            log.error("Unable to clear JarFileFactory, you might need to restart Nuxeo", e);
566        }
567    }
568
569    /**
570     * This method needs to be called before bundle uninstallation, otherwise {@link Bundle#getLocation()} throw a NPE.
571     */
572    protected URL toURL(Bundle bundle) {
573        String location = bundle.getLocation();
574        File file = new File(location);
575        return toURL(file);
576    }
577
578    protected URL toURL(File file) {
579        try {
580            return file.toURI().toURL();
581        } catch (MalformedURLException e) {
582            throw new RuntimeServiceException(e);
583        }
584    }
585
586    /**
587     * Logs the {@link ComponentManager} status.
588     */
589    protected void logComponentManagerStatus() {
590        log.debug(() -> {
591            StringBuilder builder = new StringBuilder("ComponentManager status:\n");
592            Framework.getRuntime().getStatusMessage(builder);
593            return builder.toString();
594        });
595    }
596
597    @Override
598    public Long lastFlushed() {
599        return lastFlushed;
600    }
601
602    /**
603     * Sets the last date date to current date timestamp
604     *
605     * @since 5.6
606     */
607    protected void setFlushedNow() {
608        lastFlushed = Long.valueOf(System.currentTimeMillis());
609    }
610
611    /**
612     * @deprecated since 5.6, use {@link #runDeploymentPreprocessor()} instead. Keep it as compatibility code until
613     *             NXP-9642 is done.
614     */
615    @Override
616    @Deprecated
617    public void installWebResources(File file) throws IOException {
618        log.info("Install web resources");
619        if (file.isDirectory()) {
620            File war = new File(file, "web");
621            war = new File(war, "nuxeo.war");
622            if (war.isDirectory()) {
623                org.nuxeo.common.utils.FileUtils.copyTree(war, getAppDir());
624            } else {
625                // compatibility mode with studio 1.5 - see NXP-6186
626                war = new File(file, "nuxeo.war");
627                if (war.isDirectory()) {
628                    org.nuxeo.common.utils.FileUtils.copyTree(war, getAppDir());
629                }
630            }
631        } else if (file.isFile()) { // a jar
632            File war = getWarDir();
633            ZipUtils.unzip("web/nuxeo.war", file, war);
634            // compatibility mode with studio 1.5 - see NXP-6186
635            ZipUtils.unzip("nuxeo.war", file, war);
636        }
637    }
638
639    @Override
640    public void runDeploymentPreprocessor() throws IOException {
641        log.info("Start running deployment preprocessor");
642        String rootPath = Environment.getDefault().getRuntimeHome().getAbsolutePath();
643        File root = new File(rootPath);
644        DeploymentPreprocessor processor = new DeploymentPreprocessor(root);
645        // initialize
646        processor.init();
647        // and predeploy
648        processor.predeploy();
649        log.info("Deployment preprocessing done");
650    }
651
652    protected static File getAppDir() {
653        return Environment.getDefault().getConfig().getParentFile();
654    }
655
656    protected static File getWarDir() {
657        return new File(getAppDir(), "nuxeo.war");
658    }
659
660    @Override
661    public String getOSGIBundleName(File file) {
662        Manifest mf = JarUtils.getManifest(file);
663        if (mf == null) {
664            return null;
665        }
666        String bundleName = mf.getMainAttributes().getValue("Bundle-SymbolicName");
667        if (bundleName == null) {
668            return null;
669        }
670        int index = bundleName.indexOf(';');
671        if (index > -1) {
672            bundleName = bundleName.substring(0, index);
673        }
674        return bundleName;
675    }
676
677    /**
678     * @deprecated since 9.3 should not be needed anymore
679     */
680    @Deprecated
681    protected void triggerReloadWithNewTransaction(String eventId) {
682        if (TransactionHelper.isTransactionMarkedRollback()) {
683            throw new AssertionError("The calling transaction is marked rollback");
684        }
685        // we need to commit or rollback transaction because suspending it leads to a lock/errors when acquiring a new
686        // connection during the datasource reload
687        boolean hasTransaction = TransactionHelper.isTransactionActiveOrMarkedRollback();
688        if (hasTransaction) {
689            TransactionHelper.commitOrRollbackTransaction();
690        }
691        try {
692            TransactionHelper.runInTransaction(() -> triggerReload(eventId));
693        } finally {
694            // start a new transaction only if one already existed
695            // this is because there's no user transaction when coming from SDK
696            if (hasTransaction) {
697                TransactionHelper.startTransaction();
698            }
699        }
700    }
701
702    /**
703     * @deprecated since 9.3 should not be needed anymore
704     */
705    @Deprecated
706    protected void triggerReload(String eventId) {
707        log.info("About to send reload event for id: {}", eventId);
708        Framework.getService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, BEFORE_RELOAD_EVENT_ID, this, null));
709        try {
710            Framework.getService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, eventId, this, null));
711        } finally {
712            Framework.getService(EventService.class)
713                     .sendEvent(new Event(RELOAD_TOPIC, AFTER_RELOAD_EVENT_ID, this, null));
714            log.info("Returning from reload for event id: {}", eventId);
715        }
716    }
717}