/* * 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.integtestsupport; import java.util.List; import java.util.Set; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import org.junit.Before; import org.junit.runner.Description; import org.junit.runners.model.Statement; import org.apache.isis.applib.AppManifest; import org.apache.isis.applib.DomainObjectContainer; import org.apache.isis.applib.fixtures.FixtureClock; import org.apache.isis.applib.fixtures.InstallableFixture; import org.apache.isis.applib.services.command.Command; import org.apache.isis.applib.services.command.CommandContext; import org.apache.isis.core.commons.authentication.AuthenticationSession; import org.apache.isis.core.commons.config.IsisConfiguration; import org.apache.isis.core.commons.config.IsisConfigurationDefault; import org.apache.isis.core.metamodel.deployment.DeploymentCategory; import org.apache.isis.core.metamodel.services.ServicesInjector; import org.apache.isis.core.metamodel.specloader.validator.MetaModelInvalidException; import org.apache.isis.core.runtime.authentication.AuthenticationManager; import org.apache.isis.core.runtime.authentication.AuthenticationRequest; import org.apache.isis.core.runtime.fixtures.FixturesInstallerDelegate; import org.apache.isis.core.runtime.logging.IsisLoggingConfigurer; import org.apache.isis.core.runtime.system.context.IsisContext; import org.apache.isis.core.runtime.system.persistence.PersistenceSession; import org.apache.isis.core.runtime.system.session.IsisSessionFactory; import org.apache.isis.core.runtime.system.session.IsisSessionFactoryBuilder; import org.apache.isis.core.runtime.system.transaction.IsisTransaction; import org.apache.isis.core.runtime.system.transaction.IsisTransaction.State; import org.apache.isis.core.runtime.system.transaction.IsisTransactionManager; import org.apache.isis.core.runtime.systemusinginstallers.IsisComponentProvider; import org.apache.isis.core.security.authentication.AuthenticationRequestNameOnly; import org.apache.isis.core.specsupport.scenarios.DomainServiceProvider; import static org.junit.Assert.fail; /** * Wraps a plain {@link IsisSessionFactoryBuilder}, and provides a number of features to assist with testing. */ public class IsisSystemForTest implements org.junit.rules.TestRule, DomainServiceProvider { //region > Listener, ListenerAdapter public interface Listener { void init(IsisConfiguration configuration) throws Exception; void preOpenSession(boolean firstTime) throws Exception; void postOpenSession(boolean firstTime) throws Exception; void preNextSession() throws Exception; void postNextSession() throws Exception; void preCloseSession() throws Exception; void postCloseSession() throws Exception; } public static abstract class ListenerAdapter implements Listener { private IsisConfiguration configuration; public void init(IsisConfiguration configuration) throws Exception { this.configuration = configuration; } protected IsisConfiguration getConfiguration() { return configuration; } @Override public void preOpenSession(boolean firstTime) throws Exception { } @Override public void postOpenSession(boolean firstTime) throws Exception { } @Override public void preNextSession() throws Exception { } @Override public void postNextSession() throws Exception { } @Override public void preCloseSession() throws Exception { } @Override public void postCloseSession() throws Exception { } } //endregion //region > getElseNull, get, set private static ThreadLocal<IsisSystemForTest> ISFT = new ThreadLocal<>(); public static IsisSystemForTest getElseNull() { return ISFT.get(); } public static IsisSystemForTest get() { final IsisSystemForTest isft = ISFT.get(); if(isft == null) { throw new IllegalStateException("No IsisSystemForTest available on thread; call #set(IsisSystemForTest) first"); } return isft; } public static void set(IsisSystemForTest isft) { ISFT.set(isft); } //endregion //region > Builder public static class Builder { private AuthenticationRequest authenticationRequest = new AuthenticationRequestNameOnly("tester"); private IsisConfigurationDefault configuration = new IsisConfigurationDefault(); private AppManifest appManifestIfAny; private final List<Object> services = Lists.newArrayList(); private final List<InstallableFixture> fixtures = Lists.newArrayList(); private final List <Listener> listeners = Lists.newArrayList(); private org.apache.log4j.Level level; public Builder with(IsisConfiguration configuration) { this.configuration = (IsisConfigurationDefault) configuration; return this; } public Builder with(AuthenticationRequest authenticationRequest) { this.authenticationRequest = authenticationRequest; return this; } public Builder with(AppManifest appManifest) { this.appManifestIfAny = appManifest; return this; } public Builder withLoggingAt(org.apache.log4j.Level level) { this.level = level; return this; } public IsisSystemForTest build() { final IsisSystemForTest isisSystemForTest = new IsisSystemForTest( appManifestIfAny, configuration, authenticationRequest, listeners); if(level != null) { isisSystemForTest.setLevel(level); } Runtime.getRuntime().addShutdownHook(new Thread() { @Override public synchronized void run() { try { isisSystemForTest.closeSession(); } catch (Exception e) { e.printStackTrace(); } try { if(isisSystemForTest.isisSessionFactory != null) { isisSystemForTest.isisSessionFactory.destroyServicesAndShutdown(); } } catch (Exception e) { e.printStackTrace(); } } }); return isisSystemForTest; } public Builder with(Listener listener) { if(listener != null) { listeners.add(listener); } return this; } } public static Builder builder() { return new Builder(); } //endregion //region > constructor, fields // these fields 'xxxForComponentProvider' are used to initialize the IsisComponentProvider, but shouldn't be used thereafter. private final AppManifest appManifestIfAny; private final IsisConfiguration configurationOverride; private final AuthenticationRequest authenticationRequestIfAny; private AuthenticationSession authenticationSession; private IsisSystemForTest( final AppManifest appManifestIfAny, final IsisConfiguration configurationOverride, final AuthenticationRequest authenticationRequestIfAny, final List<Listener> listeners) { this.appManifestIfAny = appManifestIfAny; this.configurationOverride = configurationOverride; this.authenticationRequestIfAny = authenticationRequestIfAny; this.listeners = listeners; } //endregion //region > level private org.apache.log4j.Level level = org.apache.log4j.Level.INFO; /** * The level to use for the root logger if fallback (ie a <tt>logging.properties</tt> file cannot be found). */ public org.apache.log4j.Level getLevel() { return level; } public void setLevel(org.apache.log4j.Level level) { this.level = level; } //endregion //region > setup (also componentProvider) // populated at #setupSystem private IsisComponentProvider componentProvider; /** * Intended to be called from a test's {@link Before} method. */ public IsisSystemForTest setUpSystem() throws RuntimeException { try { initIfRequiredThenOpenSession(FireListeners.FIRE); } catch (Exception e) { throw new RuntimeException(e); } return this; } private void initIfRequiredThenOpenSession(FireListeners fireListeners) throws Exception { // exit as quickly as possible for this case... final MetaModelInvalidException mmie = IsisContext.getMetaModelInvalidExceptionIfAny(); if(mmie != null) { final Set<String> validationErrors = mmie.getValidationErrors(); final String validationMsg = Joiner.on("\n").join(validationErrors); fail(validationMsg); return; } boolean firstTime = isisSessionFactory == null; if(fireListeners.shouldFire()) { fireInitAndPreOpenSession(firstTime); } if(firstTime) { IsisLoggingConfigurer isisLoggingConfigurer = new IsisLoggingConfigurer(getLevel()); isisLoggingConfigurer.configureLogging(".", new String[] {}); componentProvider = new IsisComponentProviderDefault( appManifestIfAny, configurationOverride ); final IsisSessionFactoryBuilder isisSessionFactoryBuilder = new IsisSessionFactoryBuilder(componentProvider, DeploymentCategory.PRODUCTION); // ensures that a FixtureClock is installed as the singleton underpinning the ClockService FixtureClock.initialize(); isisSessionFactory = isisSessionFactoryBuilder.buildSessionFactory(); // REVIEW: does no harm, but is this required? closeSession(); // if the IsisSystem does not initialize properly, then - as a side effect - the resulting // MetaModelInvalidException will be pushed onto the IsisContext (as a static field). final MetaModelInvalidException ex = IsisContext.getMetaModelInvalidExceptionIfAny(); if (ex != null) { // for subsequent tests; the attempt to bootstrap the framework will leave // the IsisContext singleton as set. IsisContext.testReset(); final Set<String> validationErrors = ex.getValidationErrors(); final StringBuilder buf = new StringBuilder(); for (String validationError : validationErrors) { buf.append(validationError).append("\n"); } fail("Metamodel is invalid: \n" + buf.toString()); } } final AuthenticationManager authenticationManager = isisSessionFactory.getAuthenticationManager(); authenticationSession = authenticationManager.authenticate(authenticationRequestIfAny); openSession(); if(fireListeners.shouldFire()) { firePostOpenSession(firstTime); } } public DomainObjectContainer getContainer() { return getService(DomainObjectContainer.class); } //endregion //region > isisSystem (populated during setup) private IsisSessionFactory isisSessionFactory; /** * The {@link IsisSessionFactory} created during {@link #setUpSystem()}. */ public IsisSessionFactory getIsisSessionFactory() { return isisSessionFactory; } /** * The {@link AuthenticationSession} created during {@link #setUpSystem()}. */ public AuthenticationSession getAuthenticationSession() { return authenticationSession; } //endregion //region > teardown private void closeSession(final FireListeners fireListeners) throws Exception { if(fireListeners.shouldFire()) { firePreCloseSession(); } if(isisSessionFactory.inSession()) { isisSessionFactory.closeSession(); } if(fireListeners.shouldFire()) { firePostCloseSession(); } } public void nextSession() throws Exception { firePreNextSession(); closeSession(); openSession(); firePostNextSession(); } //endregion //region > openSession, closeSession public void openSession() throws Exception { openSession(authenticationSession); } public void openSession(AuthenticationSession authenticationSession) throws Exception { isisSessionFactory.openSession(authenticationSession); } public void closeSession() throws Exception { closeSession(FireListeners.FIRE); } //endregion //region > listeners private List <Listener> listeners; private enum FireListeners { FIRE, DONT_FIRE; public boolean shouldFire() { return this == FIRE; } } private void fireInitAndPreOpenSession(boolean firstTime) throws Exception { if(firstTime) { for(Listener listener: listeners) { listener.init(componentProvider.getConfiguration()); } } for(Listener listener: listeners) { listener.preOpenSession(firstTime); } } private void firePostOpenSession(boolean firstTime) throws Exception { for(Listener listener: listeners) { listener.postOpenSession(firstTime); } } private void firePreCloseSession() throws Exception { for(Listener listener: listeners) { listener.preCloseSession(); } } private void firePostCloseSession() throws Exception { for(Listener listener: listeners) { listener.postCloseSession(); } } private void firePreNextSession() throws Exception { for(Listener listener: listeners) { listener.preNextSession(); } } private void firePostNextSession() throws Exception { for(Listener listener: listeners) { listener.postNextSession(); } } //endregion //region > JUnit @Rule integration @Override public Statement apply(final Statement base, final Description description) { return new Statement() { @Override public void evaluate() throws Throwable { setUpSystem(); try { base.evaluate(); closeSession(); } catch(Throwable ex) { try { closeSession(); } catch(Exception ex2) { // ignore, since already one pending } throw ex; } } }; } //endregion //region > beginTran, endTran, commitTran, abortTran /** * @deprecated - ought to be using regular domain services rather than reaching into the framework */ @Deprecated public void beginTran() { final IsisTransactionManager transactionManager = getTransactionManager(); final IsisTransaction transaction = transactionManager.getCurrentTransaction(); if(transaction == null) { startTransactionForUser(transactionManager); return; } final State state = transaction.getState(); switch(state) { case COMMITTED: case ABORTED: startTransactionForUser(transactionManager); break; case IN_PROGRESS: // nothing to do break; case MUST_ABORT: fail("Transaction is in state of '" + state + "'"); break; default: fail("Unknown transaction state '" + state + "'"); } } private void startTransactionForUser(IsisTransactionManager transactionManager) { transactionManager.startTransaction(); // specify that this command (if any) is being executed by a 'USER' final CommandContext commandContext = getService(CommandContext.class); Command command = commandContext.getCommand(); command.setExecutor(Command.Executor.USER); } /** * Either commits or aborts the transaction, depending on the Transaction's {@link org.apache.isis.core.runtime.system.transaction.IsisTransaction#getState()} * * @deprecated - ought to be using regular domain services rather than reaching into the framework */ @Deprecated public void endTran() { final IsisTransactionManager transactionManager = getTransactionManager(); final IsisTransaction transaction = transactionManager.getCurrentTransaction(); if(transaction == null) { fail("No transaction exists"); return; } transactionManager.endTransaction(); final State state = transaction.getState(); switch(state) { case COMMITTED: break; case ABORTED: break; case IN_PROGRESS: fail("Transaction is still in state of '" + state + "'"); break; case MUST_ABORT: fail("Transaction is still in state of '" + state + "'"); break; default: fail("Unknown transaction state '" + state + "'"); } } /** * Commits the transaction. * * @deprecated - ought to be using regular domain services rather than reaching into the framework */ @Deprecated public void commitTran() { final IsisTransactionManager transactionManager = getTransactionManager(); final IsisTransaction transaction = transactionManager.getCurrentTransaction(); if(transaction == null) { fail("No transaction exists"); return; } final State state = transaction.getState(); switch(state) { case COMMITTED: case ABORTED: case MUST_ABORT: fail("Transaction is in state of '" + state + "'"); break; case IN_PROGRESS: transactionManager.endTransaction(); break; default: fail("Unknown transaction state '" + state + "'"); } } /** * Aborts the transaction. * * @deprecated - ought to be using regular domain services rather than reaching into the framework */ @Deprecated public void abortTran() { final IsisTransactionManager transactionManager = getTransactionManager(); final IsisTransaction transaction = transactionManager.getCurrentTransaction(); if(transaction == null) { fail("No transaction exists"); return; } final State state = transaction.getState(); switch(state) { case ABORTED: break; case COMMITTED: fail("Transaction is in state of '" + state + "'"); break; case MUST_ABORT: case IN_PROGRESS: transactionManager.abortTransaction(); break; default: fail("Unknown transaction state '" + state + "'"); } } //endregion //region > getService, replaceService /* (non-Javadoc) * @see org.apache.isis.core.integtestsupport.ServiceProvider#getService(java.lang.Class) */ @Override @SuppressWarnings("unchecked") public <T> T getService(Class<T> serviceClass) { final ServicesInjector servicesInjector = isisSessionFactory.getServicesInjector(); return servicesInjector.lookupServiceElseFail(serviceClass); } @Override public <T> void replaceService(final T originalService, final T replacementService) { final ServicesInjector servicesInjector = isisSessionFactory.getServicesInjector(); servicesInjector.replaceService(originalService, replacementService); } //endregion //region > Fixture management (for each test, rather than at bootstrap) /** * @deprecated - use {@link org.apache.isis.applib.fixturescripts.FixtureScripts} domain service instead. */ @Deprecated public void installFixtures(final InstallableFixture... fixtures) { final FixturesInstallerDelegate fid = new FixturesInstallerDelegate(isisSessionFactory); for (final InstallableFixture fixture : fixtures) { fid.addFixture(fixture); } fid.installFixtures(); // ensure that tests are performed in separate xactn to any fixture setup. final IsisTransactionManager transactionManager = getTransactionManager(); final IsisTransaction transaction = transactionManager.getCurrentTransaction(); final State transactionState = transaction.getState(); if(transactionState.canCommit()) { commitTran(); try { nextSession(); } catch (Exception e) { throw new RuntimeException(e); } beginTran(); } } //endregion //region > Dependencies private IsisTransactionManager getTransactionManager() { return getPersistenceSession().getTransactionManager(); } private PersistenceSession getPersistenceSession() { return isisSessionFactory.getCurrentSession().getPersistenceSession(); } //endregion }