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