/* * 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.applib.fixturescripts; import java.lang.reflect.Constructor; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; import javax.annotation.PostConstruct; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.Lists; import org.apache.isis.applib.AbstractService; import org.apache.isis.applib.ViewModel; import org.apache.isis.applib.annotation.Action; import org.apache.isis.applib.annotation.MemberOrder; import org.apache.isis.applib.annotation.MinLength; import org.apache.isis.applib.annotation.Optionality; import org.apache.isis.applib.annotation.Parameter; import org.apache.isis.applib.annotation.ParameterLayout; import org.apache.isis.applib.annotation.Programmatic; import org.apache.isis.applib.annotation.RestrictTo; import org.apache.isis.applib.services.bookmark.BookmarkService; import org.apache.isis.applib.services.classdiscovery.ClassDiscoveryService; import org.apache.isis.applib.services.classdiscovery.ClassDiscoveryService2; import org.apache.isis.applib.services.fixturespec.FixtureScriptsDefault; import org.apache.isis.applib.services.fixturespec.FixtureScriptsSpecification; import org.apache.isis.applib.services.memento.MementoService; import org.apache.isis.applib.services.memento.MementoService.Memento; import org.apache.isis.applib.util.ObjectContracts; /** * Rather than subclassing, instead implement * {@link org.apache.isis.applib.services.fixturespec.FixtureScriptsSpecificationProvider}. The framework will * automatically provide a default implementation configured using that provider service. */ public abstract class FixtureScripts extends AbstractService { //region > Specification, nonPersistedObjectsStrategy, multipleExecutionStrategy enums /** * How to handle objects that are to be * {@link FixtureScripts#newFixtureResult(FixtureScript, String, Object, boolean) added} * into a {@link org.apache.isis.applib.fixturescripts.FixtureResult} but which are not yet persisted. */ public enum NonPersistedObjectsStrategy { PERSIST, IGNORE } /** * How to handle fixture scripts that are submitted to be executed more than once. * * <p> * Note that this is a {@link FixtureScripts#getMultipleExecutionStrategy() global setting} of the * {@link FixtureScripts} service; there isn't (currently) any way to mix-and-match fixture scripts that are * written with differing semantics in mind. Ideally it should be the responsibility of the fixture script * itself to determine whether it should be run. As a partial solution to this, the * * </p> */ public enum MultipleExecutionStrategy { /** * @deprecated - renamed to {@link #EXECUTE_ONCE_BY_CLASS}. */ @Deprecated IGNORE, /** * Any given fixture script (or more precisely, any fixture script instance for a particular fixture script * class) can only be run once. * * <p> * This strategy represents the original design of fixture scripts service. Specifically, it allows an * arbitrary graph of fixture scripts (eg A -> B -> C, A -> B -> D, A -> C -> D) to be created each * specifying its dependencies, and without having to worry or co-ordinate whether those prerequisite * fixture scripts have already been run. * </p> * <p> * The most obvious example is a global teardown script; every fixture script can require this to be * called, but it will only be run once. * </p> * <p> * Note that this strategy treats fixture scripts as combining both the 'how' (which business action(s) to * call) and the also the 'what' (what the arguments are to those actions). * </p> */ EXECUTE_ONCE_BY_CLASS, /** * Any given fixture script can only be run once, where the check to determine if a fixture script has already * been run is performed using value semantics. * * <p> * This strategy is a half-way house between the {@link #EXECUTE_ONCE_BY_VALUE} and {@link #EXECUTE} * strategies, where we want to prevent a fixture from running more than once, where by "fixture" we mean * the 'what' - the data to be loaded up; the 'how' is unimportant. * </p> * * <p> * This strategy was introduced in order to better support the <tt>ExcelFixture</tt> fixture script * (provided by the (non-ASF) Isis Addons' * <a href="https://github.com/isisaddons/isis-module-excel">Excel module</a>. The <tt>ExcelFixture</tt> * takes an Excel spreadsheet as the 'what' and loads up each row. So the 'how' is re-usable (therefore * the {@link #EXECUTE_ONCE_BY_CLASS} doesn't apply) on the other hand we don't want the 'what' to be * loaded more than once (so the {@link #EXECUTE} strategy doesn't apply either). The solution is for * <tt>ExcelFixture</tt> to have value semantics (a digest of the spreadsheet argument). * </p> * * @see #IGNORE */ EXECUTE_ONCE_BY_VALUE, /** * Allow fixture scripts to run as requested. * * <p> * This strategy is conceptually the simplest; all fixtures are run as requested. However, it is then * the responsibility of the programmer to ensure that fixtures do not interfere with each other. For * example, if fixture A calls fixture B which calls teardown, and fixture A also calls fixture C that * itself calls teardown, then fixture B's setup will get removed. * </p> * <p> * The workaround to the teardown issue is of course to call the teardown fixture only once in the test * itself; however even then this strategy cannot cope with arbitrary graphs of fixtures. The solution * is for the fixture list to be flat, one level high. * </p> */ EXECUTE; /** * @deprecated - use {@link #isExecuteOnceByClass()}. * @return */ @Deprecated public boolean isIgnore() { return this == IGNORE; } public boolean isExecuteOnceByClass() { return this == EXECUTE_ONCE_BY_CLASS; } public boolean isExecuteOnceByValue() { return this == EXECUTE_ONCE_BY_VALUE; } public boolean isExecute() { return this == EXECUTE; } } //endregion //region > constructors /** * Defaults to {@link FixtureScripts.NonPersistedObjectsStrategy#PERSIST persist} * strategy (if non-persisted objects are {@link FixtureScripts#newFixtureResult(FixtureScript, String, Object, boolean) added} to a {@link FixtureResultList}), * defaults {@link #getMultipleExecutionStrategy()} to {@link FixtureScripts.MultipleExecutionStrategy#IGNORE ignore} * if multiple instances of the same fixture script class are encountered. * * @param packagePrefix - to search for fixture script implementations, eg "com.mycompany". Note that this is ignored if an {@link org.apache.isis.applib.AppManifest} is in use. * * @deprecated - use {@link #FixtureScripts(FixtureScriptsSpecification)} instead. */ @Deprecated public FixtureScripts(final String packagePrefix) { this(FixtureScriptsSpecification.builder(packagePrefix) .build()); } /** * Defaults to {@link FixtureScripts.NonPersistedObjectsStrategy#PERSIST persist} * strategy (if non-persisted objects are {@link FixtureScripts#newFixtureResult(FixtureScript, String, Object, boolean) added} to a {@link FixtureResultList}). * * @param packagePrefix - to search for fixture script implementations, eg "com.mycompany". Note that this is ignored if an {@link org.apache.isis.applib.AppManifest} is in use. * @param multipleExecutionStrategy - whether more than one instance of the same fixture script class can be run multiple times. See {@link MultipleExecutionStrategy} for more details. * * @deprecated - use {@link #FixtureScripts(FixtureScriptsSpecification)} instead. */ @Deprecated public FixtureScripts( final String packagePrefix, final MultipleExecutionStrategy multipleExecutionStrategy) { this(FixtureScriptsSpecification.builder(packagePrefix) .with(multipleExecutionStrategy) .build()); } /** * Defaults {@link #getMultipleExecutionStrategy()} to {@link FixtureScripts.MultipleExecutionStrategy#IGNORE ignore} * if multiple instances of the same fixture script class are encountered. * * @param packagePrefix - to search for fixture script implementations, eg "com.mycompany". Note that this is ignored if an {@link org.apache.isis.applib.AppManifest} is in use. * @param nonPersistedObjectsStrategy - how to handle any non-persisted objects that are {@link #newFixtureResult(FixtureScript, String, Object, boolean) added} to a {@link org.apache.isis.applib.fixturescripts.FixtureResultList}. * * @deprecated - use {@link #FixtureScripts(FixtureScriptsSpecification)} instead. */ @Deprecated public FixtureScripts( final String packagePrefix, final NonPersistedObjectsStrategy nonPersistedObjectsStrategy) { this(FixtureScriptsSpecification.builder(packagePrefix) .with(nonPersistedObjectsStrategy) .build()); } /** * @param packagePrefix - to search for fixture script implementations, eg "com.mycompany". Note that this is ignored if an {@link org.apache.isis.applib.AppManifest} is in use. * @param nonPersistedObjectsStrategy - how to handle any non-persisted objects that are {@link #newFixtureResult(FixtureScript, String, Object, boolean) added} to a {@link org.apache.isis.applib.fixturescripts.FixtureResultList}. * @param multipleExecutionStrategy - whether more than one instance of the same fixture script class can be run multiple times * * @deprecated - use {@link #FixtureScripts(FixtureScriptsSpecification)} instead. */ @Deprecated public FixtureScripts( final String packagePrefix, final NonPersistedObjectsStrategy nonPersistedObjectsStrategy, final MultipleExecutionStrategy multipleExecutionStrategy) { this(FixtureScriptsSpecification.builder(packagePrefix) .with(nonPersistedObjectsStrategy) .with(multipleExecutionStrategy) .build()); } /** * @param specification - specifies how the service will find instances and execute them. */ public FixtureScripts(final FixtureScriptsSpecification specification) { this.specification = specification; } //endregion //region > packagePrefix, nonPersistedObjectsStrategy, multipleExecutionStrategy private FixtureScriptsSpecification specification; @Programmatic public FixtureScriptsSpecification getSpecification() { return specification; } /** * Allows the specification to be overridden if required. * * <p> * This is used by {@link FixtureScriptsDefault}. * </p> * * @param specification */ protected void setSpecification(final FixtureScriptsSpecification specification) { this.specification = specification; } @Programmatic public String getPackagePrefix() { return specification.getPackagePrefix(); } @Programmatic public NonPersistedObjectsStrategy getNonPersistedObjectsStrategy() { return specification.getNonPersistedObjectsStrategy(); } /** * Global setting as to how to handle fixture scripts that are executed more than once. See * {@link MultipleExecutionStrategy} for more details. */ @Programmatic public MultipleExecutionStrategy getMultipleExecutionStrategy() { return specification.getMultipleExecutionStrategy(); } //endregion //region > init @Programmatic @PostConstruct public void init() { } //endregion //region > fixtureScriptList (lazily built) private List<FixtureScript> fixtureScriptList; @Programmatic public List<FixtureScript> getFixtureScriptList() { if(fixtureScriptList == null) { fixtureScriptList = findAndInstantiateFixtureScripts(); } return fixtureScriptList; } private List<FixtureScript> findAndInstantiateFixtureScripts() { final List<FixtureScript> fixtureScripts = Lists.newArrayList(); final Set<Class<? extends FixtureScript>> fixtureScriptSubtypes = findFixtureScriptSubTypesInPackage(); for (final Class<? extends FixtureScript> fixtureScriptCls : fixtureScriptSubtypes) { final String packageName = fixtureScriptCls.getPackage().getName(); if(!packageName.startsWith(getPackagePrefix())) { // redundant check if ClassDiscoveryService2 in use because already filtered out continue; } final FixtureScript fs = newFixtureScript(fixtureScriptCls); if(fs != null) { fixtureScripts.add(fs); } } Collections.sort(fixtureScripts, new Comparator<FixtureScript>() { @Override public int compare(final FixtureScript o1, final FixtureScript o2) { return ObjectContracts.compare(o1, o2, "friendlyName","qualifiedName"); } }); return fixtureScripts; } private Set<Class<? extends FixtureScript>> findFixtureScriptSubTypesInPackage() { return findSubTypesOfClasses(FixtureScript.class, getPackagePrefix()); } private <T> Set<Class<? extends T>> findSubTypesOfClasses(Class<T> cls, final String packagePrefix) { if(classDiscoveryService instanceof ClassDiscoveryService2) { final ClassDiscoveryService2 classDiscoveryService2 = (ClassDiscoveryService2) classDiscoveryService; return classDiscoveryService2.findSubTypesOfClasses(cls, packagePrefix); } else { return classDiscoveryService.findSubTypesOfClasses(cls); } } private FixtureScript newFixtureScript(final Class<? extends FixtureScript> fixtureScriptCls) { try { final Constructor<? extends FixtureScript> constructor = fixtureScriptCls.getConstructor(); final FixtureScript template = constructor.newInstance(); if(!template.isDiscoverable()) { return null; } return getContainer().newViewModelInstance(fixtureScriptCls, mementoFor(template)); } catch(final Exception ex) { // ignore if does not have a no-arg constructor or cannot be instantiated return null; } } //endregion //region > runFixtureScript (prototype action) /** * To make this action usable in the UI, override either {@link #choices0RunFixtureScript()} or * {@link #autoComplete0RunFixtureScript(String)} with <tt>public</tt> visibility</tt>. */ @Action( restrictTo = RestrictTo.PROTOTYPING ) @MemberOrder(sequence="10") public List<FixtureResult> runFixtureScript( final FixtureScript fixtureScript, @ParameterLayout( named="Parameters", describedAs="Script-specific parameters (if any). The format depends on the script implementation (eg key=value, CSV, JSON, XML etc)", multiLine = 10 ) @Parameter(optionality = Optionality.OPTIONAL) final String parameters) { // if this method is called programmatically, the caller may have simply new'd up the fixture script // (rather than use container.newTransientInstance(...). To allow this use case, we need to ensure that // domain services are injected into the fixture script. getContainer().injectServicesInto(fixtureScript); return fixtureScript.run(parameters); } public FixtureScript default0RunFixtureScript() { return getFixtureScriptList().isEmpty() ? null: getFixtureScriptList().get(0); } protected List<FixtureScript> choices0RunFixtureScript() { return getFixtureScriptList(); } protected List<FixtureScript> autoComplete0RunFixtureScript(final @MinLength(1) String arg) { return Lists.newArrayList( Collections2.filter(getFixtureScriptList(), new Predicate<FixtureScript>() { @Override public boolean apply(final FixtureScript input) { return contains(input.getFriendlyName()) || contains(input.getLocalName()); } private boolean contains(final String str) { return str != null && str.contains(arg); } })); } public String disableRunFixtureScript() { return getFixtureScriptList().isEmpty()? "No fixture scripts found under package '" + getPackagePrefix() + "'": null; } public String validateRunFixtureScript(final FixtureScript fixtureScript, final String parameters) { return fixtureScript.validateRun(parameters); } //endregion //region > programmatic API @Programmatic public FixtureScript findFixtureScriptFor(final Class<? extends FixtureScript> fixtureScriptClass) { final List<FixtureScript> fixtureScripts = getFixtureScriptList(); for (final FixtureScript fs : fixtureScripts) { if(fixtureScriptClass.isAssignableFrom(fs.getClass())) { return fs; } } return null; } @Programmatic public FixtureScript.ExecutionContext newExecutionContext(final String parameters) { final ExecutionParameters executionParameters = executionParametersService != null ? executionParametersService.newExecutionParameters(parameters) : new ExecutionParameters(parameters); return FixtureScript.ExecutionContext.create(executionParameters, this); } //endregion //region > hooks /** * Optional hook. */ protected FixtureScript findFixtureScriptFor(final String qualifiedName) { final List<FixtureScript> fixtureScripts = getFixtureScriptList(); for (final FixtureScript fs : fixtureScripts) { if(fs.getQualifiedName().contains(qualifiedName)) { return fs; } } return null; } //endregion //region > memento support for FixtureScript String mementoFor(final FixtureScript fs) { return mementoService.create() .set("path", fs.getParentPath()) .asString(); } void initOf(final String mementoStr, final FixtureScript fs) { final Memento memento = mementoService.parse(mementoStr); fs.setParentPath(memento.get("path", String.class)); } //endregion //region > helpers (package level) @Programmatic FixtureResult newFixtureResult(final FixtureScript script, final String subkey, final Object object, final boolean firstTime) { if(object == null) { return null; } if (object instanceof ViewModel || getContainer().isPersistent(object)) { // continue } else { switch(getNonPersistedObjectsStrategy()) { case PERSIST: getContainer().flush(); break; case IGNORE: return null; } } final FixtureResult fixtureResult = new FixtureResult(); fixtureResult.setFixtureScriptClassName(firstTime ? script.getClass().getName() : null); fixtureResult.setFixtureScriptQualifiedName(script.getQualifiedName()); fixtureResult.setKey(script.pathWith(subkey)); fixtureResult.setObject(object); return fixtureResult; } @Programmatic String titleOf(final FixtureResult fixtureResult) { final Object object = fixtureResult.getObject(); return object != null? getContainer().titleOf(object): "(null)"; } //endregion //region > injected services @javax.inject.Inject private MementoService mementoService; @javax.inject.Inject private BookmarkService bookmarkService; @javax.inject.Inject private ClassDiscoveryService classDiscoveryService; @javax.inject.Inject private ExecutionParametersService executionParametersService; //endregion }