    001/*
    002 * Copyright 2009 the original author or authors.
    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
    017package org.apache.tapestry5.spock;
    018
    019import org.spockframework.runtime.extension.*;
    020import org.spockframework.runtime.model.SpecInfo;
    021import org.spockframework.util.ReflectionUtil;
    022
    023import java.lang.annotation.Annotation;
    024import java.util.*;
    025
    026import org.apache.tapestry5.ioc.annotations.*;
    027
    028/**
    029 * Facilitates <a href="https://spockframework.org/">Spock</a>-based integration testing
    030 * of Tapestry modules. Spock is a testing and specification framework for Java and Groovy
    031 * applications.
    032 *
    033 * Supports injection of Tapestry services into Spock specifications.
    034 *
    035 * <p><b>Usage example:</b>
    036 *
    037 * <pre>
    038 * &#64;ImportModule(UniverseModule)
    039 * class UniverseSpec extends Specification {
    040 *
    041 *   &#64;Inject
    042 *   UniverseService service
    043 *
    044 *   UniverseService copy = service
    045 *
    046 *   def "service knows the answer to the universe"() {
    047 *     expect:
    048 *     copy == service        // injection occurred before 'copy' was initialized
    049 *     service.answer() == 42 // what else did you expect?!
    050 *   }
    051 * }
    052 * </pre>
    053 *
    054 * <p><code>&#64;ImportModule</code> indicates which Tapestry module(s) should be started
    055 *  (and subsequently shut down). The deprecated <code>&#64;SubModule</code> annotation
    056 *  is still supported for compatibility reasons.
    057 *
    058 * <p><code>&#64;Inject</code> marks fields which should be injected with a Tapestry service or
    059 * symbol. Related Tapestry annotations, such as <code>&#64;Service</code> and <code>&#64;Symbol</code>,
    060 * are also supported.
    061 *
    062 * <p>Note: Only field (and no constructor) injection is supported.
    063 *
    064 * <p>To interact directly with the Tapestry registry, an injection point of type
    065 * <code>ObjectLocator</code> may be defined. However, this should be rarely needed.
    066 *
    067 * <p>For every specification annotated with <code>&#64;ImportModule</code> or
    068 * <code>&#64;SubModule</code>, the Tapestry registry will be started up (and subsequently shut down)
    069 * once. Because fields are injected <em>before</em> field initializers and the <code>setup()</code>/
    070 * <code>setupSpec()</code> methods are run, they can be safely accessed from these places.
    071 *
    072 * <p>Fields marked as <code>&#64;Shared</code> are injected once per specification; regular
    073 * fields once per feature (iteration). However, this does <em>not</em> mean that each
    074 * feature will receive a fresh service instance; rather, it is left to the Tapestry
    075 * registry to control the lifecycle of a service. Most Tapestry services use the default
    076 * "singleton" scope, which results in the same service instance being shared between all
    077 * features.
    078 *
    079 * <p>Features that require their own service instance(s) should be moved into separate
    080 * specifications. To avoid code fragmentation and duplication, you might want to put
    081 * multiple (micro-)specifications into the same source file, and factor out their
    082 * commonalities into a base class.
    083 *
    084 *
    085 * @author Peter Niederwieser
    086 */
    087public class TapestrySpockExtension extends AbstractGlobalExtension
    088{
    089
    090    // since Tapestry 5.4
    091    @SuppressWarnings("unchecked")
    092    private static final Class<? extends Annotation> importModuleAnnotation =
    093        (Class)ReflectionUtil.loadClassIfAvailable("org.apache.tapestry5.ioc.annotations.ImportModule");
    094
    095    // deprecated as of Tapestry 5.4
    096    @SuppressWarnings("unchecked")
    097    private static final Class<? extends Annotation> submoduleAnnotation =
    098        (Class)ReflectionUtil.loadClassIfAvailable("org.apache.tapestry5.ioc.annotations.SubModule");
    099
    100    @Override
    101    public void visitSpec(final SpecInfo spec)
    102    {
    103        Set<Class<?>> modules = collectModules(spec);
    104        if (modules == null) return;
    105
    106        IMethodInterceptor interceptor = new TapestryInterceptor(spec, modules);
    107        spec.addSharedInitializerInterceptor(interceptor);
    108        spec.addInitializerInterceptor(interceptor);
    109        spec.addCleanupSpecInterceptor(interceptor);
    110    }
    111
    112    // Returns null if no ImportModule or SubModule annotation was found.
    113    // Returns an empty list if one or more ImportModule or SubModule annotations were found,
    114    // but they didn't specify any modules. This distinction is important to
    115    // allow activation of the extension w/o specifying any modules.
    116    private Set<Class<?>> collectModules(SpecInfo spec)
    117    {
    118        Set<Class<?>> modules = null;
    119
    120        for (SpecInfo curr : spec.getSpecsTopToBottom())
    121        {
    122            if (importModuleAnnotation != null && spec.isAnnotationPresent(importModuleAnnotation))
    123            {
    124                ImportModule importModule = curr.getAnnotation(ImportModule.class);
    125                if (importModule != null)
    126                {
    127                    if (modules == null) { modules = new HashSet<>(); }
    128                    modules.addAll(Arrays.<Class<?>>asList(importModule.value()));
    129                }
    130            }
    131
    132            // Support for deprecated @SubModule.
    133            if (submoduleAnnotation != null && spec.isAnnotationPresent(submoduleAnnotation))
    134            {
    135                @SuppressWarnings("deprecation")
    136                SubModule subModule = curr.getAnnotation(SubModule.class);
    137                if (subModule != null)
    138                {
    139                    if (modules == null) { modules = new HashSet<>(); }
    140                    modules.addAll(Arrays.<Class<?>>asList(subModule.value()));
    141                }
    142            }
    143        }
    144
    145        return modules;
    146    }
    147
    148}