package org.apache.isis.viewer.wicket.ui.panels; import java.util.List; import java.util.Objects; import java.util.concurrent.Callable; import com.google.common.base.Throwables; import com.google.common.collect.Lists; import org.apache.wicket.Component; import org.apache.wicket.MarkupContainer; import org.apache.wicket.Page; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.util.visit.IVisit; import org.apache.wicket.util.visit.IVisitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.isis.applib.RecoverableException; import org.apache.isis.applib.services.bookmark.Bookmark; import org.apache.isis.applib.services.command.Command; import org.apache.isis.applib.services.command.CommandContext; import org.apache.isis.applib.services.exceprecog.ExceptionRecognizer; import org.apache.isis.applib.services.exceprecog.ExceptionRecognizerComposite; import org.apache.isis.applib.services.guice.GuiceBeanProvider; import org.apache.isis.applib.services.hint.HintStore; import org.apache.isis.applib.services.message.MessageService; import org.apache.isis.core.commons.authentication.AuthenticationSession; import org.apache.isis.core.commons.authentication.MessageBroker; import org.apache.isis.core.metamodel.adapter.ObjectAdapter; import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager; import org.apache.isis.core.metamodel.adapter.version.ConcurrencyException; import org.apache.isis.core.metamodel.facets.properties.renderunchanged.UnchangingFacet; import org.apache.isis.core.metamodel.services.ServicesInjector; import org.apache.isis.core.metamodel.specloader.SpecificationLoader; 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.IsisSession; import org.apache.isis.core.runtime.system.session.IsisSessionFactory; import org.apache.isis.core.runtime.system.transaction.IsisTransactionManager; import org.apache.isis.viewer.wicket.model.isis.WicketViewerSettings; import org.apache.isis.viewer.wicket.model.mementos.ObjectAdapterMemento; import org.apache.isis.viewer.wicket.model.models.BookmarkableModel; import org.apache.isis.viewer.wicket.model.models.EntityModel; import org.apache.isis.viewer.wicket.model.models.FormExecutor; import org.apache.isis.viewer.wicket.model.models.ParentEntityModelProvider; import org.apache.isis.viewer.wicket.model.models.ScalarModel; import org.apache.isis.viewer.wicket.ui.components.scalars.isisapplib.IsisBlobOrClobPanelAbstract; import org.apache.isis.viewer.wicket.ui.errors.JGrowlUtil; import org.apache.isis.viewer.wicket.ui.pages.entity.EntityPage; public abstract class FormExecutorAbstract<M extends BookmarkableModel<ObjectAdapter> & ParentEntityModelProvider> implements FormExecutor { private static final Logger LOG = LoggerFactory.getLogger(FormExecutorAbstract.class); protected final M model; protected final WicketViewerSettings settings; public FormExecutorAbstract(final M model) { this.model = model; this.settings = getSettings(); } protected WicketViewerSettings getSettings() { final GuiceBeanProvider guiceBeanProvider = getServicesInjector().lookupService(GuiceBeanProvider.class); return guiceBeanProvider.lookup(WicketViewerSettings.class); } @Override public boolean executeAndProcessResults( final Page page, final AjaxRequestTarget targetIfAny, final Form<?> feedbackFormIfAny) { Command command = null; ObjectAdapter targetAdapter = null; final EntityModel targetEntityModel = model.getParentEntityModel(); try { // may immediately throw a concurrency exception if // the Isis Oid held in the underlying EntityModel is stale w.r.t. the the DB. targetAdapter = obtainTargetAdapter(); // no concurrency exception, so continue... // validate the proposed property value/action arguments final String invalidReasonIfAny = getReasonInvalidIfAny(); if (invalidReasonIfAny != null) { raiseWarning(targetIfAny, feedbackFormIfAny, invalidReasonIfAny); return false; } final CommandContext commandContext = getServicesInjector().lookupService(CommandContext.class); if (commandContext != null) { command = commandContext.getCommand(); command.setExecutor(Command.Executor.USER); } // // the following line will (attempt to) invoke the action, and will in turn either: // // 1. return a non-null result from a successful invocation // // 2. return a null result (from a successful action returning void) // // 3. throws a RuntimeException, either: // a) as result of application throwing RecoverableException/ApplicationException (DN xactn still intact) // b) as result of DB exception, eg uniqueness constraint violation (DN xactn marked to abort) // Either way, as a side-effect the Isis transaction will be set to MUST_ABORT (IsisTransactionManager does this) // // (The DB exception might actually be thrown by the flush() that follows. // final ObjectAdapter resultAdapter = obtainResultAdapter(); // flush any queued changes; any concurrency or violation exceptions will actually be thrown here getPersistenceSession().getTransactionManager().flushTransaction(); getPersistenceSession().getPersistenceManager().flush(); // update target, since version updated (concurrency checks) targetEntityModel.resetVersion(); targetAdapter = targetEntityModel.load(); if(!targetAdapter.isDestroyed()) { targetEntityModel.resetPropertyModels(); } // hook to close prompt etc. onExecuteAndProcessResults(targetIfAny); if (resultDiffersOrAlwaysRedirect(targetAdapter, resultAdapter) || hasBlobsOrClobs(page) || targetIfAny == null ) { redirectTo(resultAdapter, targetIfAny); } else { // in this branch the result must be same "logical" object as target, but // the OID might have changed if a view model. if (resultAdapter != null && targetAdapter != resultAdapter) { targetEntityModel.setObject(resultAdapter); targetAdapter = targetEntityModel.load(); } if(!targetAdapter.isDestroyed()) { targetEntityModel.resetPropertyModels(); } // also in this branch we also know that there *is* an ajax target to use addComponentsToRedraw(targetIfAny); final String jGrowlCalls = JGrowlUtil.asJGrowlCalls(getAuthenticationSession().getMessageBroker()); targetIfAny.appendJavaScript(jGrowlCalls); } return true; } catch (ConcurrencyException ex) { // second attempt should succeed, because the Oid would have // been updated in the attempt if (targetAdapter == null) { targetAdapter = obtainTargetAdapter(); } forwardOnConcurrencyException(targetAdapter, ex); final MessageService messageService = getServicesInjector().lookupService(MessageService.class); messageService.warnUser(ex.getMessage()); return false; } catch (RuntimeException ex) { // there's no need to set the abort cause on the transaction, it will have already been done // (in IsisTransactionManager#executeWithinTransaction(...)). // see if is an application-defined exception. If so, convert to an application error, final RecoverableException appEx = RecoverableException.Util.getRecoverableExceptionIfAny(ex); String message = null; if (appEx != null) { message = appEx.getMessage(); } // otherwise, attempt to recognize this exception using the ExceptionRecognizers if(message == null) { message = recognizeException(ex, targetIfAny, feedbackFormIfAny); } // if we did recognize the message, then display to user as a growl pop-up if (message != null) { // ... display as growl pop-up final MessageBroker messageBroker = getAuthenticationSession().getMessageBroker(); messageBroker.setApplicationError(message); } // irrespective, capture error in the Command, and propagate if (command != null) { command.setException(Throwables.getStackTraceAsString(ex)); } // throwing an exception will get caught by WebRequestCycleForIsis#onException(...) // which will redirect to the error page. throw ex; } } private boolean resultDiffersOrAlwaysRedirect( final ObjectAdapter targetAdapter, final ObjectAdapter resultAdapter) { final ObjectAdapterMemento targetOam = ObjectAdapterMemento.createOrNull(targetAdapter); final ObjectAdapterMemento resultOam = ObjectAdapterMemento.createOrNull(resultAdapter); return resultDiffersOrAlwaysRedirect(targetOam, resultOam); } private boolean resultDiffersOrAlwaysRedirect( final ObjectAdapterMemento targetOam, final ObjectAdapterMemento resultOam) { final Bookmark resultBookmark = resultOam != null ? resultOam.asHintingBookmark() : null; final Bookmark targetBookmark = targetOam != null ? targetOam.asHintingBookmark() : null; return resultDiffersOrAlwaysRedirect(targetBookmark, resultBookmark); } private boolean resultDiffersOrAlwaysRedirect( final Bookmark targetBookmark, final Bookmark resultBookmark) { final boolean redirectEvenIfSameObject = getSettings().isRedirectEvenIfSameObject(); if(resultBookmark == null && targetBookmark == null) { return redirectEvenIfSameObject; } if (resultBookmark == null || targetBookmark == null) { return true; } final String resultBookmarkStr = asStr(resultBookmark); final String targetBookmarkStr = asStr(targetBookmark); return !Objects.equals(resultBookmarkStr, targetBookmarkStr) || redirectEvenIfSameObject; } private boolean hasBlobsOrClobs(final Page page) { // this is a bit of a hack... currently the blob/clob panel doesn't correctly redraw itself. // we therefore force a re-forward (unless is declared as unchanging). final Object hasBlobsOrClobs = page.visitChildren(IsisBlobOrClobPanelAbstract.class, new IVisitor<IsisBlobOrClobPanelAbstract, Object>() { @Override public void component(final IsisBlobOrClobPanelAbstract object, final IVisit<Object> visit) { if (!isUnchanging(object)) { visit.stop(true); } } private boolean isUnchanging(final IsisBlobOrClobPanelAbstract object) { final ScalarModel scalarModel = (ScalarModel) object.getModel(); final UnchangingFacet unchangingFacet = scalarModel.getFacet(UnchangingFacet.class); return unchangingFacet != null && unchangingFacet.value(); } }); return hasBlobsOrClobs != null; } private static String asStr(final Bookmark bookmark) { return bookmark instanceof HintStore.BookmarkWithHintId ? ((HintStore.BookmarkWithHintId) bookmark).toStringUsingHintId() : bookmark.toString(); } private void forwardOnConcurrencyException( final ObjectAdapter targetAdapter, final ConcurrencyException ex) { // this will not preserve the URL (because pageParameters are not copied over) // but trying to preserve them seems to cause the 302 redirect to be swallowed somehow final EntityPage entityPage = // disabling concurrency checking after the layout XML (grid) feature // was throwing an exception when rebuild grid after invoking action // not certain why that would be the case, but think it should be // safe to simply disable while recreating the page to re-render back to user. AdapterManager.ConcurrencyChecking.executeWithConcurrencyCheckingDisabled( new Callable<EntityPage>() { @Override public EntityPage call() throws Exception { return new EntityPage(targetAdapter, ex); } } ); // force any changes in state etc to happen now prior to the redirect; // in the case of an object being returned, this should cause our page mementos // (eg EntityModel) to hold the correct state. I hope. getIsisSessionFactory().getCurrentSession().getPersistenceSession().getTransactionManager().flushTransaction(); // "redirect-after-post" final RequestCycle requestCycle = RequestCycle.get(); requestCycle.setResponsePage(entityPage); } private void addComponentsToRedraw(final AjaxRequestTarget target) { final List<Component> componentsToRedraw = Lists.newArrayList(); final List<Component> componentsNotToRedraw = Lists.newArrayList(); final Page page = target.getPage(); page.visitChildren(new IVisitor<Component, Object>() { @Override public void component(final Component component, final IVisit<Object> visit) { if (component.getOutputMarkupId() && !(component instanceof Page)) { List<Component> listToAddTo = shouldRedraw(component) ? componentsToRedraw : componentsNotToRedraw; listToAddTo.add(component); } } private boolean shouldRedraw(final Component component) { // hmm... this doesn't work, because I think that the components // get removed after they've been added to target. // so.. still getting WARN log messages from XmlPartialPageUpdate // final Page page = component.findParent(Page.class); // if(page == null) { // // as per logic in XmlPartialPageUpdate, this has already been // // removed from page so don't attempt to redraw it // return false; // } final Object defaultModel = component.getDefaultModel(); if (!(defaultModel instanceof ScalarModel)) { return true; } final ScalarModel scalarModel = (ScalarModel) defaultModel; final UnchangingFacet unchangingFacet = scalarModel.getFacet(UnchangingFacet.class); return unchangingFacet == null || ! unchangingFacet.value() ; } }); for (Component componentNotToRedraw : componentsNotToRedraw) { MarkupContainer parent = componentNotToRedraw.getParent(); while(parent != null) { parent = parent.getParent(); } componentNotToRedraw.visitParents(MarkupContainer.class, new IVisitor<MarkupContainer, Object>() { @Override public void component(final MarkupContainer parent, final IVisit<Object> visit) { componentsToRedraw.remove(parent); // no-op if not in that list } }); if(componentNotToRedraw instanceof MarkupContainer) { final MarkupContainer containerNotToRedraw = (MarkupContainer) componentNotToRedraw; containerNotToRedraw.visitChildren(new IVisitor<Component, Object>() { @Override public void component(final Component parent, final IVisit<Object> visit) { componentsToRedraw.remove(parent); // no-op if not in that list } }); } } if(LOG.isDebugEnabled()) { debug(componentsToRedraw, componentsNotToRedraw); } for (Component component : componentsToRedraw) { target.add(component); } } private void debug( final List<Component> componentsToRedraw, final List<Component> componentsNotToRedraw) { debug("Not redrawing", componentsNotToRedraw); debug("Redrawing", componentsToRedraw); } private void debug(final String title, final List<Component> list) { LOG.debug(">>> " + title + ":"); for (Component component : list) { LOG.debug( String.format("%30s: %s", component.getClass().getSimpleName(), component.getPath())); } } private String recognizeException(RuntimeException ex, AjaxRequestTarget target, Form<?> feedbackForm) { // REVIEW: similar code also in WebRequestCycleForIsis; combine? // see if the exception is recognized as being a non-serious error // (nb: similar code in WebRequestCycleForIsis, as a fallback) List<ExceptionRecognizer> exceptionRecognizers = getServicesInjector() .lookupServices(ExceptionRecognizer.class); String recognizedErrorIfAny = new ExceptionRecognizerComposite(exceptionRecognizers).recognize(ex); if (recognizedErrorIfAny != null) { // recognized raiseWarning(target, feedbackForm, recognizedErrorIfAny); getTransactionManager().getCurrentTransaction().clearAbortCause(); // there's no need to abort the transaction, it will have already been done // (in IsisTransactionManager#executeWithinTransaction(...)). } return recognizedErrorIfAny; } private void raiseWarning( final AjaxRequestTarget targetIfAny, final Form<?> feedbackFormIfAny, final String error) { if(targetIfAny != null && feedbackFormIfAny != null) { targetIfAny.add(feedbackFormIfAny); feedbackFormIfAny.error(error); } else { final MessageService messageService = getServicesInjector().lookupService(MessageService.class); messageService.warnUser(error); } } // /////////////////////////////////////////////////////////////////// // Dependencies (from IsisContext) // /////////////////////////////////////////////////////////////////// protected IsisSession getCurrentSession() { return getIsisSessionFactory().getCurrentSession(); } protected PersistenceSession getPersistenceSession() { return getCurrentSession().getPersistenceSession(); } protected ServicesInjector getServicesInjector() { return getIsisSessionFactory().getServicesInjector(); } protected SpecificationLoader getSpecificationLoader() { return getIsisSessionFactory().getSpecificationLoader(); } private IsisTransactionManager getTransactionManager() { return getPersistenceSession().getTransactionManager(); } protected IsisSessionFactory getIsisSessionFactory() { return IsisContext.getSessionFactory(); } protected AuthenticationSession getAuthenticationSession() { return getCurrentSession().getAuthenticationSession(); } /////////////////////////////////////////////////////////////////////////////// protected abstract ObjectAdapter obtainTargetAdapter(); protected abstract String getReasonInvalidIfAny(); protected abstract void onExecuteAndProcessResults(final AjaxRequestTarget target); protected abstract ObjectAdapter obtainResultAdapter(); protected abstract void redirectTo( final ObjectAdapter resultAdapter, final AjaxRequestTarget target); }