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