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 *     Nuxeo - initial API and implementation
018 */
019package org.nuxeo.runtime.test;
020
021import static org.junit.Assert.assertEquals;
022import static org.junit.Assert.assertNotNull;
023import static org.junit.Assert.fail;
024
025import java.io.File;
026import java.io.IOException;
027import java.net.MalformedURLException;
028import java.net.URI;
029import java.net.URISyntaxException;
030import java.net.URL;
031import java.util.ArrayList;
032import java.util.Arrays;
033import java.util.Enumeration;
034import java.util.HashMap;
035import java.util.HashSet;
036import java.util.List;
037import java.util.Map;
038import java.util.Objects;
039import java.util.Properties;
040import java.util.Set;
041import java.util.jar.Attributes;
042import java.util.jar.Manifest;
043import java.util.stream.Collectors;
044import java.util.stream.Stream;
045
046import org.apache.commons.io.FileUtils;
047import org.apache.commons.logging.Log;
048import org.apache.commons.logging.LogFactory;
049import org.jmock.Mockery;
050import org.jmock.integration.junit4.JUnit4Mockery;
051import org.junit.After;
052import org.junit.Before;
053import org.junit.Ignore;
054import org.junit.runner.RunWith;
055import org.nuxeo.common.Environment;
056import org.nuxeo.osgi.BundleFile;
057import org.nuxeo.osgi.BundleImpl;
058import org.nuxeo.osgi.DirectoryBundleFile;
059import org.nuxeo.osgi.JarBundleFile;
060import org.nuxeo.osgi.OSGiAdapter;
061import org.nuxeo.osgi.SystemBundle;
062import org.nuxeo.osgi.SystemBundleFile;
063import org.nuxeo.osgi.application.StandaloneBundleLoader;
064import org.nuxeo.runtime.AbstractRuntimeService;
065import org.nuxeo.runtime.RuntimeServiceException;
066import org.nuxeo.runtime.api.Framework;
067import org.nuxeo.runtime.model.Extension;
068import org.nuxeo.runtime.model.RegistrationInfo;
069import org.nuxeo.runtime.model.RuntimeContext;
070import org.nuxeo.runtime.model.StreamRef;
071import org.nuxeo.runtime.model.URLStreamRef;
072import org.nuxeo.runtime.model.impl.DefaultRuntimeContext;
073import org.nuxeo.runtime.osgi.OSGiRuntimeContext;
074import org.nuxeo.runtime.osgi.OSGiRuntimeService;
075import org.nuxeo.runtime.test.protocols.inline.InlineURLFactory;
076import org.nuxeo.runtime.test.runner.ConditionalIgnoreRule;
077import org.nuxeo.runtime.test.runner.Features;
078import org.nuxeo.runtime.test.runner.FeaturesRunner;
079import org.nuxeo.runtime.test.runner.MDCFeature;
080import org.nuxeo.runtime.test.runner.RandomBug;
081import org.nuxeo.runtime.test.runner.RuntimeHarness;
082import org.nuxeo.runtime.test.runner.TargetExtensions;
083import org.nuxeo.runtime.transaction.TransactionHelper;
084import org.osgi.framework.Bundle;
085import org.osgi.framework.FrameworkEvent;
086
087import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;
088
089/**
090 * Abstract base class for test cases that require a test runtime service.
091 * <p>
092 * The runtime service itself is conveniently available as the <code>runtime</code> instance variable in derived
093 * classes.
094 * <p>
095 * <b>Warning:</b> NXRuntimeTestCase subclasses <b>must</b>
096 * <ul>
097 * <li>not declare they own @Before and @After.
098 * <li>override doSetUp and doTearDown (and postSetUp if needed) instead of setUp and tearDown.
099 * <li>never call deployXXX methods outside the doSetUp method.
100 *
101 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
102 */
103// Make sure this class is kept in sync with with RuntimeHarness
104@RunWith(FeaturesRunner.class)
105@Features({ MDCFeature.class, ConditionalIgnoreRule.Feature.class, RandomBug.Feature.class })
106@Ignore
107public class NXRuntimeTestCase implements RuntimeHarness {
108
109    protected Mockery jmcontext = new JUnit4Mockery();
110
111    static {
112        // jul to jcl redirection may pose problems (infinite loops) in some
113        // environment
114        // where slf4j to jul, and jcl over slf4j is deployed
115        System.setProperty(AbstractRuntimeService.REDIRECT_JUL, "false");
116    }
117
118    private static final Log log = LogFactory.getLog(NXRuntimeTestCase.class);
119
120    protected OSGiRuntimeService runtime;
121
122    protected URL[] urls; // classpath urls, used for bundles lookup
123
124    protected File workingDir;
125
126    protected StandaloneBundleLoader bundleLoader;
127
128    private Set<URI> readUris;
129
130    protected Map<String, BundleFile> bundles;
131
132    protected boolean restart = false;
133
134    protected List<String[]> deploymentStack = new ArrayList<>();
135
136    /**
137     * Whether or not the runtime components were started. This is useful to ensure the runtime is started once.
138     */
139    protected boolean frameworkStarted = false;
140
141    @Override
142    public boolean isRestart() {
143        return restart;
144    }
145
146    protected OSGiAdapter osgi;
147
148    protected Bundle runtimeBundle;
149
150    protected final List<WorkingDirectoryConfigurator> wdConfigs = new ArrayList<>();
151
152    protected final TargetResourceLocator targetResourceLocator;
153
154    /**
155     * Set to true when the instance of this class is a JUnit test case. Set to false when the instance of this class is
156     * instantiated by the FeaturesRunner to manage the framework If the class is a JUnit test case then the runtime
157     * components will be started at the end of the setUp method
158     */
159    protected final boolean isTestUnit;
160
161    /**
162     * Used when subclassing to create standalone test cases
163     */
164    public NXRuntimeTestCase() {
165        targetResourceLocator = new TargetResourceLocator(this.getClass());
166        isTestUnit = true;
167    }
168
169    /**
170     * Used by the features runner to manage the Nuxeo framework
171     */
172    public NXRuntimeTestCase(Class<?> clazz) {
173        targetResourceLocator = new TargetResourceLocator(clazz);
174        isTestUnit = false;
175    }
176
177    @Override
178    public void addWorkingDirectoryConfigurator(WorkingDirectoryConfigurator config) {
179        wdConfigs.add(config);
180    }
181
182    @Override
183    public File getWorkingDir() {
184        return workingDir;
185    }
186
187    /**
188     * Restarts the runtime and preserve homes directory.
189     */
190    @Override
191    public void restart() throws Exception {
192        restart = true;
193        try {
194            tearDown();
195            setUp();
196        } finally {
197            restart = false;
198        }
199    }
200
201    @Override
202    public void start() throws Exception {
203        startRuntime();
204    }
205
206    @Before
207    public void startRuntime() throws Exception {
208        System.setProperty("org.nuxeo.runtime.testing", "true");
209        // super.setUp();
210        wipeRuntime();
211        initUrls();
212        if (urls == null) {
213            throw new UnsupportedOperationException("no bundles available");
214        }
215        initOsgiRuntime();
216        setUp(); // let a chance to the subclasses to contribute bundles and/or components
217        if (isTestUnit) { // if this class is running as a test case start the runtime components
218            fireFrameworkStarted();
219        }
220        postSetUp();
221    }
222
223    /**
224     * Implementors should override this method to setup tests and not the {@link #startRuntime()} method. This method
225     * should contain all the bundle or component deployments needed by the tests. At the time this method is called the
226     * components are not yet started. If you need to perform component/service lookups use instead the
227     * {@link #postSetUp()} method
228     */
229    protected void setUp() throws Exception {
230    }
231
232    /**
233     * Implementors should override this method to implement any specific test tear down and not the
234     * {@link #stopRuntime()} method
235     *
236     * @throws Exception
237     */
238    protected void tearDown() throws Exception {
239        deploymentStack = new ArrayList<>();
240    }
241
242    /**
243     * Called after framework was started (at the end of setUp). Implementors may use this to use deployed services to
244     * initialize fields etc.
245     */
246    protected void postSetUp() throws Exception {
247    }
248
249    /**
250     * Fire the event {@code FrameworkEvent.STARTED}. This will start all the resolved Nuxeo components
251     *
252     * @see OSGiRuntimeService#frameworkEvent(FrameworkEvent)
253     */
254    @Override
255    public void fireFrameworkStarted() throws Exception {
256        if (frameworkStarted) {
257            // avoid starting twice the runtime (fix situations where tests are starting themselves the runtime)
258            // If this happens the faulty test should be fixed
259            // TODO NXP-22534 - throw an exception?
260            return;
261        }
262        frameworkStarted = true;
263        boolean txStarted = !TransactionHelper.isTransactionActiveOrMarkedRollback()
264                && TransactionHelper.startTransaction();
265        boolean txFinished = false;
266        try {
267            osgi.fireFrameworkEvent(new FrameworkEvent(FrameworkEvent.STARTED, runtimeBundle, null));
268            txFinished = true;
269        } finally {
270            if (!txFinished) {
271                TransactionHelper.setTransactionRollbackOnly();
272            }
273            if (txStarted) {
274                TransactionHelper.commitOrRollbackTransaction();
275            }
276        }
277    }
278
279    @After
280    public void stopRuntime() throws Exception {
281        tearDown();
282        wipeRuntime();
283        if (workingDir != null) {
284            if (!restart) {
285                if (workingDir.exists() && !FileUtils.deleteQuietly(workingDir)) {
286                    log.warn("Cannot delete " + workingDir);
287                }
288                workingDir = null;
289            }
290        }
291        readUris = null;
292        bundles = null;
293    }
294
295    @Override
296    public void stop() throws Exception {
297        stopRuntime();
298    }
299
300    @Override
301    public boolean isStarted() {
302        return runtime != null;
303    }
304
305    protected void initOsgiRuntime() throws Exception {
306        try {
307            if (!restart) {
308                Environment.setDefault(null);
309                if (System.getProperties().remove("nuxeo.home") != null) {
310                    log.warn("Removed System property nuxeo.home.");
311                }
312                workingDir = File.createTempFile("nxruntime-" + Thread.currentThread().getName() + "-", null,
313                        new File("target"));
314                workingDir.delete();
315            }
316        } catch (IOException e) {
317            log.error("Could not init working directory", e);
318            throw e;
319        }
320        osgi = new OSGiAdapter(workingDir);
321        BundleFile bf = new SystemBundleFile(workingDir);
322        bundleLoader = new StandaloneBundleLoader(osgi, NXRuntimeTestCase.class.getClassLoader());
323        SystemBundle systemBundle = new SystemBundle(osgi, bf, bundleLoader.getSharedClassLoader().getLoader());
324        osgi.setSystemBundle(systemBundle);
325        Thread.currentThread().setContextClassLoader(bundleLoader.getSharedClassLoader().getLoader());
326
327        for (WorkingDirectoryConfigurator cfg : wdConfigs) {
328            cfg.configure(this, workingDir);
329        }
330
331        bundleLoader.setScanForNestedJARs(false); // for now
332        bundleLoader.setExtractNestedJARs(false);
333
334        BundleFile bundleFile = lookupBundle("org.nuxeo.runtime");
335        runtimeBundle = new RootRuntimeBundle(osgi, bundleFile, bundleLoader.getClass().getClassLoader(), true);
336        runtimeBundle.start();
337
338        runtime = handleNewRuntime((OSGiRuntimeService) Framework.getRuntime());
339
340        assertNotNull(runtime);
341    }
342
343    protected OSGiRuntimeService handleNewRuntime(OSGiRuntimeService aRuntime) {
344        return aRuntime;
345    }
346
347    public static URL[] introspectClasspath(ClassLoader loader) {
348        return new FastClasspathScanner().getUniqueClasspathElements().stream().map(file -> {
349            try {
350                return file.toURI().toURL();
351            } catch (MalformedURLException cause) {
352                throw new Error("Could not get URL from " + file, cause);
353            }
354        }).toArray(URL[]::new);
355    }
356
357    protected void initUrls() throws Exception {
358        ClassLoader classLoader = NXRuntimeTestCase.class.getClassLoader();
359        urls = introspectClasspath(classLoader);
360        if (log.isDebugEnabled()) {
361            StringBuilder sb = new StringBuilder();
362            sb.append("URLs on the classpath: ");
363            for (URL url : urls) {
364                sb.append(url.toString());
365                sb.append('\n');
366            }
367            log.debug(sb.toString());
368        }
369        readUris = new HashSet<>();
370        bundles = new HashMap<>();
371    }
372
373    /**
374     * Makes sure there is no previous runtime hanging around.
375     * <p>
376     * This happens for instance if a previous test had errors in its <code>setUp()</code>, because
377     * <code>tearDown()</code> has not been called.
378     */
379    protected void wipeRuntime() throws Exception {
380        // Make sure there is no active runtime (this might happen if an
381        // exception is raised during a previous setUp -> tearDown is not called
382        // afterwards).
383        runtime = null;
384        frameworkStarted = false;
385        if (Framework.getRuntime() != null) {
386            Framework.shutdown();
387        }
388    }
389
390    public static URL getResource(String name) {
391        final ClassLoader loader = Thread.currentThread().getContextClassLoader();
392        String callerName = Thread.currentThread().getStackTrace()[2].getClassName();
393        final String relativePath = callerName.replace('.', '/').concat(".class");
394        final String fullPath = loader.getResource(relativePath).getPath();
395        final String basePath = fullPath.substring(0, fullPath.indexOf(relativePath));
396        Enumeration<URL> resources;
397        try {
398            resources = loader.getResources(name);
399            while (resources.hasMoreElements()) {
400                URL resource = resources.nextElement();
401                if (resource.getPath().startsWith(basePath)) {
402                    return resource;
403                }
404            }
405        } catch (IOException e) {
406            return null;
407        }
408        return loader.getResource(name);
409    }
410
411    protected void deployContrib(URL url) {
412        assertEquals(runtime, Framework.getRuntime());
413        log.info("Deploying contribution from " + url.toString());
414        try {
415            runtime.getContext().deploy(url);
416        } catch (Exception e) {
417            fail("Failed to deploy contrib " + url.toString());
418        }
419    }
420
421    /**
422     * Deploys a contribution from a given bundle.
423     * <p>
424     * The path will be relative to the bundle root. Example: <code>
425     * deployContrib("org.nuxeo.ecm.core", "OSGI-INF/CoreExtensions.xml")
426     * </code>
427     * <p>
428     * For compatibility reasons the name of the bundle may be a jar name, but this use is discouraged and deprecated.
429     *
430     * @param name the name of the bundle to peek the contrib in
431     * @param contrib the path to contrib in the bundle.
432     */
433    @Override
434    public void deployContrib(String name, String contrib) throws Exception {
435        RuntimeContext context = runtime.getContext(name);
436        if (context == null) {
437            context = runtime.getContext();
438            BundleFile file = lookupBundle(name);
439            URL location = file.getEntry(contrib);
440            if (location == null) {
441                throw new AssertionError("Cannot locate " + contrib + " in " + name);
442            }
443            context.deploy(location);
444            return;
445        }
446        context.deploy(contrib);
447    }
448
449    /**
450     * Deploy a contribution specified as a "bundleName:path" uri
451     */
452    public void deployContrib(String uri) throws Exception {
453        int i = uri.indexOf(':');
454        if (i == -1) {
455            throw new IllegalArgumentException(
456                    "Invalid deployment URI: " + uri + ". Must be of the form bundleSymbolicName:pathInBundleJar");
457        }
458        deployContrib(uri.substring(0, i), uri.substring(i + 1));
459    }
460
461    /**
462     * Deploy an XML contribution from outside a bundle.
463     * <p>
464     * This should be used by tests wiling to deploy test contribution as part of a real bundle.
465     * <p>
466     * The bundle owner is important since the contribution may depend on resources deployed in that bundle.
467     * <p>
468     * Note that the owner bundle MUST be an already deployed bundle.
469     *
470     * @param bundle the bundle that becomes the contribution owner
471     * @param contrib the contribution to deploy as part of the given bundle
472     * @deprecated since 10.1, use {@link #deployContrib(String, String)}
473     */
474    @Override
475    @Deprecated
476    public RuntimeContext deployTestContrib(String bundle, String contrib) throws Exception {
477        URL url = targetResourceLocator.getTargetTestResource(contrib);
478        return deployTestContrib(bundle, url);
479    }
480
481    /**
482     * @deprecated since 10.1, use {@link #deployContrib(String, String)}
483     */
484    @Override
485    @Deprecated
486    public RuntimeContext deployTestContrib(String bundle, URL contrib) throws Exception {
487        Bundle b = bundleLoader.getOSGi().getRegistry().getBundle(bundle);
488        if (b == null) {
489            b = osgi.getSystemBundle();
490        }
491        OSGiRuntimeContext ctx = new OSGiRuntimeContext(runtime, b);
492        ctx.deploy(contrib);
493        return ctx;
494    }
495
496    @Override
497    public RuntimeContext deployPartial(String name, Set<TargetExtensions> targetExtensions) throws Exception {
498        // Do not install bundle; we only need the Object to list his components
499        Bundle bundle = new BundleImpl(osgi, lookupBundle(name), null);
500
501        RuntimeContext ctx = new OSGiRuntimeContext(runtime, bundle);
502        listBundleComponents(bundle).map(URLStreamRef::new).forEach(component -> {
503            try {
504                deployPartialComponent(ctx, targetExtensions, component);
505            } catch (IOException e) {
506                log.error("PartialBundle: " + name + " failed to load: " + component, e);
507            }
508        });
509        return ctx;
510    }
511
512    /**
513     * Read a component from his StreamRef and create a new component (suffixed with `-partial`, and the base component
514     * name aliased) with only matching contributions of the extensionPoints parameter.
515     *
516     * @param ctx RuntimeContext in which the new component will be deployed
517     * @param extensionPoints Set of white listed TargetExtensions
518     * @param component Reference to the original component
519     */
520    protected void deployPartialComponent(RuntimeContext ctx, Set<TargetExtensions> extensionPoints,
521            StreamRef component) throws IOException {
522        RegistrationInfo ri = ((DefaultRuntimeContext) ctx).createRegistrationInfo(component);
523        String name = ri.getName().getName() + "-partial";
524
525        // Flatten Target Extension Points
526        Set<String> targets = extensionPoints.stream()
527                                             .map(TargetExtensions::getTargetExtensions)
528                                             .flatMap(Set::stream)
529                                             .collect(Collectors.toSet());
530
531        String ext = Arrays.stream(ri.getExtensions())
532                           .filter(e -> targets.contains(TargetExtensions.newTargetExtension(
533                                   e.getTargetComponent().getName(), e.getExtensionPoint())))
534                           .map(Extension::toXML)
535                           .collect(Collectors.joining());
536
537        InlineURLFactory.install();
538        ctx.deploy(new InlineRef(name, String.format("<component name=\"%s\">%s</component>", name, ext)));
539    }
540
541    /**
542     * Listing component's urls of a bundle. Inspired from org.nuxeo.runtime.osgi.OSGiRuntimeService#loadComponents but
543     * without deploying anything.
544     *
545     * @param bundle Bundle to be read
546     */
547    protected Stream<URL> listBundleComponents(Bundle bundle) {
548        String list = OSGiRuntimeService.getComponentsList(bundle);
549        String name = bundle.getSymbolicName();
550        log.debug("PartialBundle: " + name + " components: " + list);
551        if (list == null) {
552            return null;
553        }
554
555        return Arrays.stream(list.split("[, \t\n\r\f]")).map(bundle::getEntry).filter(Objects::nonNull);
556    }
557
558    /**
559     * Undeploys a contribution from a given bundle.
560     * <p>
561     * The path will be relative to the bundle root. Example: <code>
562     * undeployContrib("org.nuxeo.ecm.core", "OSGI-INF/CoreExtensions.xml")
563     * </code>
564     *
565     * @param name the bundle
566     * @param contrib the contribution
567     */
568    @Override
569    public void undeployContrib(String name, String contrib) throws Exception {
570        RuntimeContext context = runtime.getContext(name);
571        if (context == null) {
572            context = runtime.getContext();
573        }
574        context.undeploy(contrib);
575    }
576
577    public void undeployContrib(String uri) throws Exception {
578        int i = uri.indexOf(':');
579        if (i == -1) {
580            throw new IllegalArgumentException(
581                    "Invalid deployment URI: " + uri + ". Must be of the form bundleSymbolicName:pathInBundleJar");
582        }
583        undeployContrib(uri.substring(0, i), uri.substring(i + 1));
584    }
585
586    protected static boolean isVersionSuffix(String s) {
587        if (s.length() == 0) {
588            return true;
589        }
590        return s.matches("-(\\d+\\.?)+(-SNAPSHOT)?(\\.\\w+)?");
591    }
592
593    /**
594     * Resolves an URL for bundle deployment code.
595     * <p>
596     * TODO: Implementation could be finer...
597     *
598     * @return the resolved url
599     */
600    protected URL lookupBundleUrl(String bundle) {
601        for (URL url : urls) {
602            String[] pathElts = url.getPath().split("/");
603            for (int i = 0; i < pathElts.length; i++) {
604                if (pathElts[i].startsWith(bundle) && isVersionSuffix(pathElts[i].substring(bundle.length()))) {
605                    // we want the main version of the bundle
606                    boolean isTestVersion = false;
607                    for (int j = i + 1; j < pathElts.length; j++) {
608                        // ok for Eclipse (/test) and Maven (/test-classes)
609                        if (pathElts[j].startsWith("test")) {
610                            isTestVersion = true;
611                            break;
612                        }
613                    }
614                    if (!isTestVersion) {
615                        log.info("Resolved " + bundle + " as " + url.toString());
616                        return url;
617                    }
618                }
619            }
620        }
621        throw new RuntimeException("Could not resolve bundle " + bundle);
622    }
623
624    /**
625     * Deploys a whole OSGI bundle.
626     * <p>
627     * The lookup is first done on symbolic name, as set in <code>MANIFEST.MF</code> and then falls back to the bundle
628     * url (e.g., <code>nuxeo-platform-search-api</code>) for backwards compatibility.
629     *
630     * @param name the symbolic name
631     */
632    @Override
633    public void deployBundle(String name) throws Exception {
634        // install only if not yet installed
635        Bundle bundle = bundleLoader.getOSGi().getRegistry().getBundle(name);
636        if (bundle == null) {
637            BundleFile bundleFile = lookupBundle(name);
638            bundleLoader.loadBundle(bundleFile);
639            bundleLoader.installBundle(bundleFile);
640            bundle = bundleLoader.getOSGi().getRegistry().getBundle(name);
641        }
642        if (runtime.getContext(bundle) == null) {
643            runtime.createContext(bundle);
644        }
645    }
646
647    protected String readSymbolicName(BundleFile bf) {
648        Manifest manifest = bf.getManifest();
649        if (manifest == null) {
650            return null;
651        }
652        Attributes attrs = manifest.getMainAttributes();
653        String name = attrs.getValue("Bundle-SymbolicName");
654        if (name == null) {
655            return null;
656        }
657        String[] sp = name.split(";", 2);
658        return sp[0];
659    }
660
661    public BundleFile lookupBundle(String bundleName) throws Exception {
662        BundleFile bundleFile = bundles.get(bundleName);
663        if (bundleFile != null) {
664            return bundleFile;
665        }
666        for (URL url : urls) {
667            URI uri = url.toURI();
668            if (readUris.contains(uri)) {
669                continue;
670            }
671            File file = new File(uri);
672            readUris.add(uri);
673            try {
674                if (file.isDirectory()) {
675                    bundleFile = new DirectoryBundleFile(file);
676                } else {
677                    bundleFile = new JarBundleFile(file);
678                }
679            } catch (IOException e) {
680                // no manifest => not a bundle
681                continue;
682            }
683            String symbolicName = readSymbolicName(bundleFile);
684            if (symbolicName != null) {
685                log.info(String.format("Bundle '%s' has URL %s", symbolicName, url));
686                bundles.put(symbolicName, bundleFile);
687            }
688            if (bundleName.equals(symbolicName)) {
689                return bundleFile;
690            }
691        }
692        throw new RuntimeServiceException(String.format("No bundle with symbolic name '%s';", bundleName));
693    }
694
695    @Override
696    public void deployFolder(File folder, ClassLoader loader) throws Exception {
697        DirectoryBundleFile bf = new DirectoryBundleFile(folder);
698        BundleImpl bundle = new BundleImpl(osgi, bf, loader);
699        osgi.install(bundle);
700    }
701
702    @Override
703    public Properties getProperties() {
704        return runtime.getProperties();
705    }
706
707    @Override
708    public RuntimeContext getContext() {
709        return runtime.getContext();
710    }
711
712    @Override
713    public OSGiAdapter getOSGiAdapter() {
714        return osgi;
715    }
716
717    /*
718     * (non-Javadoc)
719     * @see org.nuxeo.runtime.test.runner.RuntimeHarness#getClassLoaderFiles()
720     */
721    @Override
722    public List<String> getClassLoaderFiles() throws URISyntaxException {
723        List<String> files = new ArrayList<>(urls.length);
724        for (URL url : urls) {
725            files.add(url.toURI().getPath());
726        }
727        return files;
728    }
729
730    /**
731     * Should be called by subclasses after one or more inline deployments are made inside a test method. Without
732     * calling this the inline deployment(s) will not have any effects.
733     * <p />
734     * <b>Be Warned</b> that if you reference runtime services or components you should lookup them again after calling
735     * this method!
736     * <p />
737     * This method also calls {@link #postSetUp()} for convenience.
738     */
739    protected void applyInlineDeployments() throws Exception {
740        runtime.getComponentManager().refresh(false);
741        runtime.getComponentManager().start(); // make sure components are started
742        postSetUp();
743    }
744
745    /**
746     * Should be called by subclasses to remove any inline deployments made in the current test method.
747     * <p />
748     * <b>Be Warned</b> that if you reference runtime services or components you should lookup them again after calling
749     * this method!
750     * <p />
751     * This method also calls {@link #postSetUp()} for convenience.
752     */
753    protected void removeInlineDeployments() throws Exception {
754        runtime.getComponentManager().reset();
755        runtime.getComponentManager().start();
756        postSetUp();
757    }
758
759    /**
760     * Hot deploy the given components (identified by an URI). All the started components are stopped, the new ones are
761     * registered and then all components are started. You can undeploy these components by calling
762     * {@link #popInlineDeployments()}
763     * <p>
764     * A component URI is of the form: bundleSymbolicName:pathToComponentXmlInBundle
765     */
766    public void pushInlineDeployments(String... deploymentUris) throws Exception {
767        deploymentStack.add(deploymentUris);
768        for (String uri : deploymentUris) {
769            deployContrib(uri);
770        }
771        applyInlineDeployments();
772    }
773
774    /**
775     * Remove the latest deployed components using {@link #pushInlineDeployments(String...)}.
776     */
777    public void popInlineDeployments() throws Exception {
778        if (deploymentStack.isEmpty()) {
779            throw new IllegalStateException("deployment stack is empty");
780        }
781        popInlineDeployments(deploymentStack.size() - 1);
782    }
783
784    public void popInlineDeployments(int index) throws Exception {
785        if (index < 0 || index > deploymentStack.size() - 1) {
786            throw new IllegalStateException("deployment stack index is invalid: " + index);
787        }
788        deploymentStack.remove(index);
789
790        runtime.getComponentManager().reset();
791        for (String[] ar : deploymentStack) {
792            for (String element : ar) {
793                deployContrib(element);
794            }
795        }
796        applyInlineDeployments();
797    }
798
799}