| /* |
| * 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.syncope.client.ui.commons.wizards; |
| |
| import java.io.Serializable; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.Future; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| import org.apache.commons.lang3.tuple.Pair; |
| import org.apache.syncope.client.ui.commons.Constants; |
| import org.apache.wicket.Component; |
| import org.apache.wicket.ajax.AjaxRequestTarget; |
| import org.apache.wicket.event.Broadcast; |
| import org.apache.wicket.event.IEventSink; |
| import org.apache.wicket.extensions.wizard.IWizardModel; |
| import org.apache.wicket.extensions.wizard.IWizardStep; |
| import org.apache.wicket.extensions.wizard.Wizard; |
| import org.apache.wicket.extensions.wizard.WizardModel; |
| import org.apache.wicket.extensions.wizard.WizardStep; |
| import org.apache.wicket.model.CompoundPropertyModel; |
| import org.apache.wicket.request.cycle.RequestCycle; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.apache.syncope.client.ui.commons.pages.BaseWebPage; |
| import org.apache.syncope.client.ui.commons.panels.SubmitableModalPanel; |
| import org.apache.syncope.client.ui.commons.panels.WizardModalPanel; |
| import org.apache.syncope.client.ui.commons.wizards.exception.CaptchaNotMatchingException; |
| import org.apache.wicket.Application; |
| import org.apache.wicket.PageReference; |
| import org.apache.wicket.Session; |
| import org.apache.wicket.ThreadContext; |
| import org.apache.wicket.WicketRuntimeException; |
| import org.apache.wicket.markup.html.list.ListItem; |
| import org.apache.wicket.markup.html.list.ListView; |
| import org.apache.wicket.model.IModel; |
| |
| public abstract class AjaxWizard<T extends Serializable> extends Wizard |
| implements SubmitableModalPanel, WizardModalPanel<T> { |
| |
| private static final long serialVersionUID = -1272120742876833520L; |
| |
| private final List<Component> outerObjects = new ArrayList<>(); |
| |
| public enum Mode { |
| CREATE, |
| EDIT, |
| TEMPLATE, |
| READONLY, |
| EDIT_APPROVAL; |
| |
| } |
| |
| protected static final Logger LOG = LoggerFactory.getLogger(AjaxWizard.class); |
| |
| private T item; |
| |
| private final Mode mode; |
| |
| private IEventSink eventSink; |
| |
| private final PageReference pageRef; |
| |
| /** |
| * Construct. |
| * |
| * @param id The component id |
| * @param item model object |
| * @param model wizard model |
| * @param mode mode |
| * @param pageRef caller page reference. |
| */ |
| public AjaxWizard( |
| final String id, |
| final T item, |
| final WizardModel model, |
| final Mode mode, |
| final PageReference pageRef) { |
| |
| super(id); |
| this.item = item; |
| this.mode = mode; |
| this.pageRef = pageRef; |
| |
| if (mode == Mode.READONLY) { |
| model.setCancelVisible(false); |
| } |
| |
| add(new ListView<>("outerObjectsRepeater", outerObjects) { |
| |
| private static final long serialVersionUID = -9180479401817023838L; |
| |
| @Override |
| protected void populateItem(final ListItem<Component> item) { |
| item.add(item.getModelObject()); |
| } |
| |
| }); |
| |
| setOutputMarkupId(true); |
| setDefaultModel(new CompoundPropertyModel<>(this)); |
| init(model); |
| } |
| |
| /** |
| * Add object outside the main container. |
| * Use this method just to be not influenced by specific inner object css'. |
| * Be sure to provide {@code outer} as id. |
| * |
| * @param childs components to be added. |
| * @return the current panel instance. |
| */ |
| public final AjaxWizard<T> addOuterObject(final List<Component> childs) { |
| outerObjects.addAll(childs); |
| return this; |
| } |
| |
| public AjaxWizard<T> setEventSink(final IEventSink eventSink) { |
| this.eventSink = eventSink; |
| return this; |
| } |
| |
| @Override |
| protected void init(final IWizardModel wizardModel) { |
| super.init(wizardModel); |
| getForm().remove(FEEDBACK_ID); |
| |
| if (mode == Mode.READONLY) { |
| Iterator<IWizardStep> iter = wizardModel.stepIterator(); |
| while (iter.hasNext()) { |
| WizardStep.class.cast(iter.next()).setEnabled(false); |
| } |
| } |
| } |
| |
| @Override |
| protected Component newButtonBar(final String id) { |
| return new AjaxWizardMgtButtonBar<>(id, this, mode); |
| } |
| |
| protected abstract void onCancelInternal(); |
| |
| protected abstract void sendError(Exception exception); |
| |
| protected abstract void sendWarning(String message); |
| |
| protected abstract Future<Pair<Serializable, Serializable>> execute( |
| Callable<Pair<Serializable, Serializable>> future); |
| |
| /** |
| * Apply operation |
| * |
| * @param target request target |
| * @return a pair of payload (maybe null) and resulting object. |
| */ |
| protected abstract Pair<Serializable, Serializable> onApplyInternal(AjaxRequestTarget target); |
| |
| protected abstract long getMaxWaitTimeInSeconds(); |
| |
| @Override |
| public final void onCancel() { |
| AjaxRequestTarget target = RequestCycle.get().find(AjaxRequestTarget.class).orElse(null); |
| try { |
| onCancelInternal(); |
| if (eventSink == null) { |
| send(AjaxWizard.this, Broadcast.BUBBLE, new NewItemCancelEvent<>(item, target)); |
| } else { |
| send(eventSink, Broadcast.EXACT, new NewItemCancelEvent<>(item, target)); |
| } |
| } catch (Exception e) { |
| LOG.warn("Wizard error on cancel", e); |
| sendError(e); |
| ((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(target); |
| } |
| } |
| |
| @Override |
| public final void onFinish() { |
| AjaxRequestTarget target = RequestCycle.get().find(AjaxRequestTarget.class).orElse(null); |
| try { |
| final Serializable res = onApply(target); |
| if (eventSink == null) { |
| send(this, Broadcast.BUBBLE, new NewItemFinishEvent<>(item, target).setResult(res)); |
| } else { |
| send(eventSink, Broadcast.EXACT, new NewItemFinishEvent<>(item, target).setResult(res)); |
| } |
| } catch (TimeoutException te) { |
| LOG.warn("Operation took too long", te); |
| if (eventSink == null) { |
| send(this, Broadcast.BUBBLE, new NewItemCancelEvent<>(item, target)); |
| } else { |
| send(eventSink, Broadcast.EXACT, new NewItemCancelEvent<>(item, target)); |
| } |
| sendWarning(getString("timeout")); |
| ((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(target); |
| } catch (CaptchaNotMatchingException ce) { |
| LOG.error("Wizard error on finish: captcha not matching", ce); |
| sendError(new WicketRuntimeException(getString(Constants.CAPTCHA_ERROR))); |
| ((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(target); |
| } catch (Exception e) { |
| LOG.error("Wizard error on finish", e); |
| sendError(e); |
| ((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(target); |
| } |
| } |
| |
| @Override |
| public T getItem() { |
| return item; |
| } |
| |
| /** |
| * Replaces the default value provided with the constructor. |
| * |
| * @param item new value. |
| * @return the current wizard instance. |
| */ |
| public AjaxWizard<T> setItem(final T item) { |
| this.item = item; |
| return this; |
| } |
| |
| @Override |
| public void onSubmit(final AjaxRequestTarget target) { |
| try { |
| onApply(target); |
| } catch (TimeoutException te) { |
| LOG.warn("Operation took too long", te); |
| send(eventSink, Broadcast.EXACT, new NewItemCancelEvent<>(item, target)); |
| sendWarning(getString("timeout")); |
| ((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(target); |
| } |
| } |
| |
| @Override |
| public void onError(final AjaxRequestTarget target) { |
| ((BaseWebPage) getPage()).getNotificationPanel().refresh(target); |
| } |
| |
| private Serializable onApply(final AjaxRequestTarget target) throws TimeoutException { |
| try { |
| Future<Pair<Serializable, Serializable>> executor = execute(new ApplyFuture(target)); |
| |
| Pair<Serializable, Serializable> res = executor.get(getMaxWaitTimeInSeconds(), TimeUnit.SECONDS); |
| |
| if (res.getLeft() != null) { |
| send(pageRef.getPage(), Broadcast.BUBBLE, res.getLeft()); |
| } |
| |
| return res.getRight(); |
| } catch (InterruptedException | ExecutionException e) { |
| if (e.getCause() instanceof CaptchaNotMatchingException) { |
| throw (CaptchaNotMatchingException) e.getCause(); |
| } |
| throw new WicketRuntimeException(e); |
| } |
| } |
| |
| public abstract static class NewItemEvent<T extends Serializable> { |
| |
| private final T item; |
| |
| private IModel<String> resourceModel; |
| |
| private final AjaxRequestTarget target; |
| |
| private WizardModalPanel<?> modalPanel; |
| |
| public NewItemEvent(final T item, final AjaxRequestTarget target) { |
| this.item = item; |
| this.target = target; |
| } |
| |
| public T getItem() { |
| return item; |
| } |
| |
| public Optional<AjaxRequestTarget> getTarget() { |
| return Optional.ofNullable(target); |
| } |
| |
| public WizardModalPanel<?> getModalPanel() { |
| return modalPanel; |
| } |
| |
| public NewItemEvent<T> forceModalPanel(final WizardModalPanel<?> modalPanel) { |
| this.modalPanel = modalPanel; |
| return this; |
| } |
| |
| public IModel<String> getResourceModel() { |
| return resourceModel; |
| } |
| |
| public NewItemEvent<T> setResourceModel(final IModel<String> resourceModel) { |
| this.resourceModel = resourceModel; |
| return this; |
| } |
| |
| public abstract String getEventDescription(); |
| } |
| |
| public static class NewItemActionEvent<T extends Serializable> extends NewItemEvent<T> { |
| |
| private static final String EVENT_DESCRIPTION = "new"; |
| |
| private int index; |
| |
| public NewItemActionEvent(final T item, final AjaxRequestTarget target) { |
| super(item, target); |
| } |
| |
| public NewItemActionEvent(final T item, final int index, final AjaxRequestTarget target) { |
| super(item, target); |
| this.index = index; |
| } |
| |
| public int getIndex() { |
| return index; |
| } |
| |
| @Override |
| public String getEventDescription() { |
| return NewItemActionEvent.EVENT_DESCRIPTION; |
| } |
| } |
| |
| public static class EditItemActionEvent<T extends Serializable> extends NewItemActionEvent<T> { |
| |
| private static final String EVENT_DESCRIPTION = "edit"; |
| |
| public EditItemActionEvent(final T item, final AjaxRequestTarget target) { |
| super(item, target); |
| } |
| |
| public EditItemActionEvent(final T item, final int index, final AjaxRequestTarget target) { |
| super(item, index, target); |
| } |
| |
| @Override |
| public String getEventDescription() { |
| return EditItemActionEvent.EVENT_DESCRIPTION; |
| } |
| } |
| |
| public static class NewItemCancelEvent<T extends Serializable> extends NewItemEvent<T> { |
| |
| private static final String EVENT_DESCRIPTION = "cancel"; |
| |
| public NewItemCancelEvent(final T item, final AjaxRequestTarget target) { |
| super(item, target); |
| } |
| |
| @Override |
| public String getEventDescription() { |
| return NewItemCancelEvent.EVENT_DESCRIPTION; |
| } |
| } |
| |
| public static class NewItemFinishEvent<T extends Serializable> extends NewItemEvent<T> { |
| |
| private static final String EVENT_DESCRIPTION = "finish"; |
| |
| private Serializable result; |
| |
| public NewItemFinishEvent(final T item, final AjaxRequestTarget target) { |
| super(item, target); |
| } |
| |
| @Override |
| public String getEventDescription() { |
| return NewItemFinishEvent.EVENT_DESCRIPTION; |
| } |
| |
| public NewItemFinishEvent<T> setResult(final Serializable result) { |
| this.result = result; |
| return this; |
| } |
| |
| public Serializable getResult() { |
| return result; |
| } |
| } |
| |
| private class ApplyFuture implements Callable<Pair<Serializable, Serializable>>, Serializable { |
| |
| private static final long serialVersionUID = -4657123322652656848L; |
| |
| private final AjaxRequestTarget target; |
| |
| private final Application application; |
| |
| private final RequestCycle requestCycle; |
| |
| private final Session session; |
| |
| ApplyFuture(final AjaxRequestTarget target) { |
| this.target = target; |
| this.application = Application.get(); |
| this.requestCycle = RequestCycle.get(); |
| this.session = Session.exists() ? Session.get() : null; |
| } |
| |
| @Override |
| public Pair<Serializable, Serializable> call() throws Exception { |
| try { |
| ThreadContext.setApplication(this.application); |
| ThreadContext.setRequestCycle(this.requestCycle); |
| ThreadContext.setSession(this.session); |
| return AjaxWizard.this.onApplyInternal(this.target); |
| } finally { |
| ThreadContext.detach(); |
| } |
| } |
| } |
| } |