/* * Copyright 2009 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.spockframework.tapestry; import java.util.*; import org.apache.tapestry5.ioc.annotations.SubModule; import org.spockframework.runtime.extension.AbstractGlobalExtension; import org.spockframework.runtime.extension.IMethodInterceptor; import org.spockframework.runtime.model.SpecInfo; /** * Facilitates the creation of integration-level specifications for applications based * on the Tapestry 5 inversion-of-control container. The main focus is on injecting * specifications with Tapestry-provided objects. This works just like for * regular Tapestry services, except that only field (and no constructor) * injection is supported. * * <p>Instead of inventing its own set of annotations, this extension reuses * Tapestry's own annotations. In particular, * * <ul> * <li><tt>@SubModule</tt> indicates which Tapestry module(s) should be started * (and subsequently shut down)</li> * <li><tt>@Inject</tt> marks fields which should be injected with a Tapestry service or * symbol</li> * </ul> * * Related Tapestry annotations, such as <tt>@Service</tt> and <tt>@Symbol</tt>, * are also supported. For information on their use, see the * <a href="http://tapestry.apache.org/tapestry5/tapestry-ioc/">Tapestry IoC documentation</a>. * To interact directly with the Tapestry registry, an injection point of type * <tt>ObjectLocator</tt> may be defined. However, this should be rarely needed. * * <p>For every specification annotated with <tt>@SubModule</tt>, the Tapestry * registry will be started up (and subsequently shut down) once. Because fields are injected * <em>before</em> field initializers and the <tt>setup()</tt>/<tt>setupSpec()</tt> * methods are run, they can be safely accessed from these places. * * <p>Fields marked as <tt>@Shared</tt> are injected once per specification; regular * fields once per feature (iteration). However, this does <em>not</em> mean that each * feature will receive a fresh service instance; rather, it is left to the Tapestry * registry to control the lifecycle of a service. Most Tapestry services use the default * "singleton" scope, which results in the same service instance being shared between all * features. * * <p>Features that require their own service instance(s) should be moved into separate * specifications. To avoid code fragmentation and duplication, you might want to put * multiple (micro-)specifications into the same source file, and factor out their * commonalities into a base class. * * <p><b>Usage example:</b> * * <pre> * @SubModule(UniverseModule) * class UniverseSpec extends Specification { * @Inject * UniverseService service * * UniverseService copy = service * * def "service knows the answer to the universe"() { * expect: * copy == service // injection occurred before 'copy' was initialized * service.answer() == 42 // what else did you expect?! * } * } * </pre> * * @author Peter Niederwieser */ public class TapestryExtension extends AbstractGlobalExtension { public void visitSpec(SpecInfo spec) { Set<Class<?>> modules = collectModules(spec); if (modules == null) return; IMethodInterceptor interceptor = new TapestryInterceptor(spec, modules); spec.addSharedInitializerInterceptor(interceptor); spec.addInitializerInterceptor(interceptor); spec.addCleanupSpecInterceptor(interceptor); } // Returns null if no SubModule annotation was found. // Returns an empty list if one or more SubModule annotations were found, // but they didn't specify any modules. This distinction is important to // allow activation of the extension w/o specifying any modules. private Set<Class<?>> collectModules(SpecInfo spec) { Set<Class<?>> modules = null; for (SpecInfo curr : spec.getSpecsTopToBottom()) { SubModule subModule = curr.getAnnotation(SubModule.class); if (subModule == null) continue; if (modules == null) modules = new HashSet<Class<?>>(); modules.addAll(Arrays.<Class<?>>asList(subModule.value())); } return modules; } }