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 * @ImportModule(UniverseModule) 039 * class UniverseSpec extends Specification { 040 * 041 * @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>@ImportModule</code> indicates which Tapestry module(s) should be started 055 * (and subsequently shut down). The deprecated <code>@SubModule</code> annotation 056 * is still supported for compatibility reasons. 057 * 058 * <p><code>@Inject</code> marks fields which should be injected with a Tapestry service or 059 * symbol. Related Tapestry annotations, such as <code>@Service</code> and <code>@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>@ImportModule</code> or 068 * <code>@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>@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}