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