/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.isis.core.specsupport.specs;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import org.jmock.Sequence;
import org.jmock.States;
import org.jmock.internal.ExpectationBuilder;
import org.apache.isis.applib.DomainObjectContainer;
import org.apache.isis.applib.services.wrapper.WrapperFactory;
import org.apache.isis.core.specsupport.scenarios.ScenarioExecution;
import org.apache.isis.core.specsupport.scenarios.ScenarioExecutionForUnit;
import org.apache.isis.core.specsupport.scenarios.ScenarioExecutionScope;
import cucumber.api.java.Before;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
/**
* Base class for Cucumber-JVM step definitions.
*
* <p>
* Simply declares that an instance of {@link ScenarioExecution} (or a subclass)
* must be instantiated by the Cucumber-JVM runtime and injected into the step definitions.
*/
public abstract class CukeGlueAbstract {
/**
* Access the {@link ScenarioExecution} as setup through a previous call to {@link #before(ScenarioExecutionScope)}.
*
* <p>
* This corresponds, broadly, to the (Ruby) Cucumber's "World" object.
*/
protected ScenarioExecution scenarioExecution() {
if(ScenarioExecution.current() == null) {
fail();
return null;
}
return ScenarioExecution.current();
}
// //////////////////////////////////////
/**
* Intended to be called at the beginning of any 'when' (after all the 'given's)
* or at the beginning of any 'then' (after all the 'when's)
*
* <p>
* Simply {@link ScenarioExecution#endTran(boolean) ends any existing transaction} and
* then {@link ScenarioExecution#beginTran() starts a new one}.
*/
protected void nextTransaction() {
final ScenarioExecution scenarioExecution = scenarioExecution();
if(scenarioExecution != null) {
scenarioExecution.endTran(true);
scenarioExecution.beginTran();
}
}
// //////////////////////////////////////
/**
* Convenience method
*/
public Object getVar(String type, String id) {
return scenarioExecution().getVar(type, id);
}
/**
* Convenience method
*/
public <X> X getVar(String type, String id, Class<X> cls) {
return scenarioExecution().getVar(type, id ,cls);
}
/**
* Convenience method
*/
public void putVar(String type, String id, Object value) {
scenarioExecution().putVar(type, id, value);
}
/**
* Convenience method
*/
public void removeVar(String type, String id) {
scenarioExecution().removeVar(type, id);
}
/**
* Convenience method
*/
protected <T> T service(Class<T> cls) {
return scenarioExecution().service(cls);
}
/**
* Convenience method
*/
protected DomainObjectContainer container() {
return scenarioExecution().container();
}
/**
* Convenience method
*/
protected WrapperFactory wrapperFactory() {
return scenarioExecution().wrapperFactory();
}
/**
* Convenience method
*/
protected <T> T wrap(T obj) {
return wrapperFactory().wrap(obj);
}
/**
* Convenience method
*/
protected <T> T unwrap(T obj) {
return wrapperFactory().unwrap(obj);
}
/**
* Convenience method
* @return
*/
public boolean supportsMocks() {
return scenarioExecution().supportsMocks();
}
/**
* Convenience method
*/
public void checking(ExpectationBuilder expectations) {
scenarioExecution().checking(expectations);
}
/**
* Convenience method
*/
public void assertMocksSatisfied() {
scenarioExecution().assertIsSatisfied();
}
/**
* Convenience method
*/
public Sequence sequence(String name) {
return scenarioExecution().sequence(name);
}
/**
* Convenience method
*/
public States states(String name) {
return scenarioExecution().states(name);
}
// //////////////////////////////////////
@SuppressWarnings({ "rawtypes", "unchecked" })
public static void assertTableEquals(final List listOfExpecteds, final Iterable iterableOfActuals) {
final List<Object> listOfActuals = Lists.newArrayList(iterableOfActuals);
assertThat(listOfActuals.size(), is(listOfExpecteds.size()));
final StringBuilder buf = new StringBuilder();
for (int i=0; i<listOfActuals.size(); i++) {
final Object actual = listOfActuals.get(i);
final Object expected = listOfExpecteds.get(i);
final Field[] expectedFields = expected.getClass().getDeclaredFields();
for (Field field : expectedFields) {
final String propertyName = field.getName();
final Object actualProp = getProperty(actual, propertyName );
final Object expectedProp = getProperty(expected, propertyName);
if(!Objects.equal(actualProp, expectedProp)) {
buf.append("#" + i + ": " + propertyName + ": " + expectedProp + " vs " + actualProp).append("\n");
}
}
}
if(buf.length() != 0) {
fail("\n" + buf.toString());
}
}
private static Object getProperty(Object obj, String propertyName) {
if(obj == null) {
return null;
}
final Class<? extends Object> cls = obj.getClass();
try {
final String methodName = "get" + capitalize(propertyName);
final Method method = cls.getMethod(methodName, new Class[]{});
if(method != null) {
return method.invoke(obj);
}
} catch (Exception e) {
// continue
}
try {
final String methodName = "is" + capitalize(propertyName);
final Method method = cls.getMethod(methodName, new Class[]{});
if(method != null) {
return method.invoke(obj);
}
} catch (Exception e) {
// continue
}
try {
final Field field = cls.getDeclaredField(propertyName);
if(field != null) {
if(!field.isAccessible()) {
field.setAccessible(true);
}
return field.get(obj);
}
} catch (Exception e) {
// continue
}
return null;
}
private static String capitalize(final String str) {
if (str == null || str.length() == 0) {
return str;
}
if (str.length() == 1) {
return str.toUpperCase();
}
return Character.toUpperCase(str.charAt(0)) + str.substring(1);
}
// //////////////////////////////////////
/**
* Indicate that a scenario is starting, and specify the {@link ScenarioExecutionScope scope}
* at which to run the scenario.
*
* <p>
* This method should be called from a "before" hook (a method annotated with
* Cucumber's {@link Before} annotation, in a step definition subclass. The tag
* should be appropriate for the scope specified. Typically this method should be delegated to
* twice, in two mutually exclusive before hooks.
*
* <p>
* Calling this method makes the {@link ScenarioExecution} available (via {@link #scenarioExecution()}).
* It also delegates to the scenario to {@link ScenarioExecution#beginTran() begin the transaction}.
* (Whether this actually does anything depends in implementation of the {@link ScenarioExecution}).
*
* <p>
* The boilerplate (to copy-n-paste as required) is:
* <pre>
* @cucumber.api.java.Before("@unit")
* public void beforeScenarioUnitScope() {
* before(ScenarioExecutionScope.UNIT);
* }
* @cucumber.api.java.Before("@integration")
* public void beforeScenarioIntegrationScope() {
* before(ScenarioExecutionScope.INTEGRATION);
* }
* </pre>
* The built-in {@link ScenarioExecutionScope#UNIT unit}-level scope will instantiate a
* {@link ScenarioExecutionForUnit}, while the built-in
* {@link ScenarioExecutionScope#INTEGRATION integration}-level scope instantiates
* <tt>ScenarioExecutionForIntegration</tt> (from the <tt>isis-core-integtestsupport</tt> module).
* The former provides access to domain services as mocks, whereas the latter wraps a running
* <tt>IsisSystemForTest</tt>.
*
* <p>
* If need be, it is also possible to define custom scopes, with a different implementation of
* {@link ScenarioExecution}. This might be done when unit testing where a large number of specs
* have similar expectations needing to be set on the mock domain services.
*
* <p>
* Not every class holding step definitions should have these hooks, only those that correspond to the logical
* beginning and end of scenario. As such, this method may only be called once per scenario execution
* (and fails fast if called more than once).
*/
protected void before(ScenarioExecutionScope scope) {
final ScenarioExecution scenarioExecution = scope.instantiate();
scenarioExecution.beginTran();
}
/**
* Indicate that a scenario is ending; the {@link ScenarioExecution} is discarded and no
* longer {@link #scenarioExecution() available}.
*
* <p>
* Before being discarded, the {@link ScenarioExecution} is delegated to
* in order to {@link ScenarioExecution#endTran(boolean) end the transaction}.
* (Whether this actually does anything depends in implementation of the {@link ScenarioExecution}).
*
* <p>
* The boilerplate (to copy-n-paste as required) is:
* <pre>
* @cucumber.api.java.After
* public void afterScenario(cucumber.api.Scenario sc) {
* after(sc);
* }
* </pre>
*
* <p>
* Not every class holding step definitions should have this hook, only those that correspond to the logical
* beginning and end of scenario. As such, this method may only be called once per scenario execution
* (and fails fast if called more than once).
*/
public void after(cucumber.api.Scenario sc) {
ScenarioExecution.current().endTran(!sc.isFailed());
}
}