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 */
019package org.nuxeo.runtime.test.runner;
020
021import java.lang.annotation.Annotation;
022import java.lang.reflect.Method;
023import java.net.URL;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.LinkedHashSet;
029import java.util.LinkedList;
030import java.util.List;
031import java.util.Map;
032import java.util.Set;
033
034import javax.inject.Inject;
035
036import org.junit.rules.MethodRule;
037import org.junit.runners.model.FrameworkMethod;
038import org.junit.runners.model.Statement;
039import org.nuxeo.runtime.model.ComponentManager;
040import org.nuxeo.runtime.model.RuntimeContext;
041import org.nuxeo.runtime.osgi.OSGiRuntimeService;
042import org.osgi.framework.Bundle;
043
044import com.google.common.collect.Multimaps;
045import com.google.common.collect.SetMultimap;
046
047/**
048 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
049 */
050public class RuntimeDeployment {
051
052    Set<String> bundles = new HashSet<>();
053
054    Map<String, Set<TargetExtensions>> partialBundles = new HashMap<>();
055
056    Map<String, Collection<String>> mainContribs = new HashMap<>();
057
058    SetMultimap<String, String> mainIndex = Multimaps.newSetMultimap(mainContribs, LinkedHashSet::new);
059
060    /**
061     * @deprecated since 10.1
062     */
063    @Deprecated
064    Map<String, Collection<String>> localContribs = new HashMap<>();
065
066    /**
067     * @deprecated since 10.1
068     */
069    @Deprecated
070    SetMultimap<String, String> localIndex = Multimaps.newSetMultimap(localContribs, LinkedHashSet::new);
071
072    /**
073     * @deprecated since 9.2 we cannot undeploy components while they are started. So we don't need anymore to store the
074     *             contexts
075     */
076    @Deprecated
077    protected LinkedList<RuntimeContext> contexts = new LinkedList<>();
078
079    protected void index(Class<?> clazz) {
080        AnnotationScanner scanner = FeaturesRunner.scanner;
081        scanner.scan(clazz);
082        List<? extends Annotation> annos = scanner.getAnnotations(clazz);
083        if (annos == null) {
084            return;
085        }
086        for (Annotation anno : annos) {
087            if (anno.annotationType() == Deploy.class) {
088                index((Deploy) anno);
089            } else if (anno.annotationType() == Deploys.class) {
090                index((Deploys) anno);
091            } else if (anno.annotationType() == LocalDeploy.class) {
092                index((LocalDeploy) anno);
093            } else if (anno.annotationType() == PartialDeploy.class) {
094                index((PartialDeploy) anno);
095            }
096        }
097    }
098
099    protected void index(RunnerFeature feature) {
100        index(feature.getClass());
101    }
102
103    protected void index(Method method) {
104        index(method.getAnnotation(Deploy.class));
105        index(method.getAnnotation(Deploys.class));
106        index(method.getAnnotation(LocalDeploy.class));
107    }
108
109    protected void index(Deploy config) {
110        if (config == null) {
111            return;
112        }
113        for (String each : config.value()) {
114            index(each, mainIndex);
115        }
116    }
117
118    private void index(Deploys deploys) {
119        if (deploys == null) {
120            return;
121        }
122        for (Deploy value : deploys.value()) {
123            index(value);
124        }
125    }
126
127    /**
128     * @deprecated since 10.1, use {@link #index(Deploy)}
129     */
130    @Deprecated
131    protected void index(LocalDeploy config) {
132        if (config == null) {
133            return;
134        }
135        for (String each : config.value()) {
136            index(each, localIndex);
137        }
138    }
139
140    /**
141     * @since 9.1
142     */
143    protected void index(PartialDeploy config) {
144        if (config == null) {
145            return;
146        }
147
148        Set<TargetExtensions> pairs = partialBundles.computeIfAbsent(config.bundle(), key -> new HashSet<>());
149        Arrays.stream(config.extensions()).map(c -> {
150            try {
151                return c.getDeclaredConstructor().newInstance();
152            } catch (ReflectiveOperationException e) {
153                throw new IllegalStateException(e);
154            }
155        }).forEach(pairs::add);
156    }
157
158    protected void index(Features features) {
159        for (Class<?> each : features.value()) {
160            index(each);
161        }
162    }
163
164    protected void index(String directive, SetMultimap<String, String> contribs) {
165        int sepIndex = directive.indexOf(':');
166        if (sepIndex == -1) {
167            bundles.add(directive);
168        } else {
169            String bundle = directive.substring(0, sepIndex);
170            String resource = directive.substring(sepIndex + 1);
171            contribs.put(bundle, resource);
172        }
173    }
174
175    protected void deploy(FeaturesRunner runner, RuntimeHarness harness) {
176        AssertionError errors = new AssertionError("deployment errors");
177        OSGiRuntimeService runtime = (OSGiRuntimeService) harness.getContext().getRuntime();
178        for (String name : bundles) {
179            Bundle bundle = harness.getOSGiAdapter().getBundle(name);
180            if (bundle == null) {
181                try {
182                    harness.deployBundle(name);
183                    bundle = harness.getOSGiAdapter().getBundle(name);
184                    if (bundle == null) {
185                        throw new UnsupportedOperationException("Should not occur");
186                    }
187                } catch (Exception error) {
188                    errors.addSuppressed(error);
189                    continue;
190                }
191                contexts.add(runtime.getContext(bundle));
192            }
193            try {
194                // deploy bundle contribs
195                for (String resource : mainIndex.removeAll(name)) {
196                    try {
197                        harness.deployContrib(name, resource);
198                    } catch (Exception error) {
199                        errors.addSuppressed(error);
200                    }
201                }
202                // deploy local contribs
203                // this block is dreprecated since 10.1 with @LocalDeploy
204                for (String resource : localIndex.removeAll(name)) {
205                    URL url = runner.getTargetTestResource(resource);
206                    if (url == null) {
207                        url = bundle.getEntry(resource);
208                    }
209                    if (url == null) {
210                        url = runner.getTargetTestClass().getClassLoader().getResource(resource);
211                    }
212                    if (url == null) {
213                        throw new AssertionError("Cannot find " + resource + " in " + name);
214                    }
215                    contexts.add(harness.deployTestContrib(name, url));
216                }
217            } catch (Exception error) {
218                errors.addSuppressed(error);
219            }
220        }
221
222        for (Map.Entry<String, String> resource : mainIndex.entries()) {
223            try {
224                harness.deployContrib(resource.getKey(), resource.getValue());
225            } catch (Exception error) {
226                errors.addSuppressed(error);
227            }
228        }
229        // this block is deprecated since 10.1 with @LocalDeploy
230        for (Map.Entry<String, String> resource : localIndex.entries()) {
231            try {
232                contexts.add(harness.deployTestContrib(resource.getKey(), resource.getValue()));
233            } catch (Exception error) {
234                errors.addSuppressed(error);
235            }
236        }
237
238        for (Map.Entry<String, Set<TargetExtensions>> resource : partialBundles.entrySet()) {
239            try {
240                contexts.add(harness.deployPartial(resource.getKey(), resource.getValue()));
241            } catch (Exception e) {
242                errors.addSuppressed(e);
243            }
244        }
245
246        if (errors.getSuppressed().length > 0) {
247            throw errors;
248        }
249
250    }
251
252    public static RuntimeDeployment onTest(FeaturesRunner runner) {
253        RuntimeDeployment deployment = new RuntimeDeployment();
254        deployment.index(runner.getDescription().getTestClass());
255        for (RunnerFeature each : runner.getFeatures()) {
256            deployment.index(each);
257        }
258        return deployment;
259    }
260
261    public static MethodRule onMethod() {
262        return new OnMethod();
263    }
264
265    protected static class OnMethod implements MethodRule {
266
267        @Inject
268        protected FeaturesRunner runner;
269
270        @Override
271        public Statement apply(Statement base, FrameworkMethod method, Object target) {
272            RuntimeDeployment deployment = new RuntimeDeployment();
273            deployment.index(method.getMethod());
274            return deployment.onStatement(runner, runner.getFeature(RuntimeFeature.class).harness, method, base);
275        }
276
277    }
278
279    protected Statement onStatement(FeaturesRunner runner, RuntimeHarness harness, FrameworkMethod method,
280            Statement base) {
281        return new DeploymentStatement(runner, harness, method, base);
282    }
283
284    protected class DeploymentStatement extends Statement {
285
286        protected final FeaturesRunner runner;
287
288        protected final RuntimeHarness harness;
289
290        // useful for debugging
291        protected final FrameworkMethod method;
292
293        protected final Statement base;
294
295        protected DeploymentStatement(FeaturesRunner runner, RuntimeHarness harness, FrameworkMethod method,
296                Statement base) {
297            this.runner = runner;
298            this.harness = harness;
299            this.method = method;
300            this.base = base;
301        }
302
303        protected void tryDeploy() {
304            // the registry is updated here and not using before or teardown methods.
305            // this approach ensure the components are not stopped between tearDown and the next test
306            // (so that custom feature that relly on the runtime between the two tests are not affected by stopping
307            // components)
308            ComponentManager mgr = harness.getContext().getRuntime().getComponentManager();
309            // the stash may already contains contribs (from @Setup methods)
310            if (mgr.hasChanged()) { // first reset the registry if it was changed by the last test
311                mgr.reset();
312                // the registry is now stopped
313            }
314            // deploy current test contributions if any
315            deploy(runner, harness);
316            mgr.refresh(true);
317            // now the stash is empty
318            mgr.start(); // ensure components are started
319        }
320
321        @Override
322        public void evaluate() throws Throwable {
323            // make sure the clear the stash
324            tryDeploy();
325            try {
326                base.evaluate();
327            } finally {
328                // undeploy cannot be done while the components are started
329                // RuntimeFeature will do a reset if needed
330                // see RuntimeFeature.afterTeardown
331                // undeploy();
332            }
333        }
334
335    }
336
337}