blob: 3119be0d2629a82db609029b37e34ff9dfe9fc0f [file] [log] [blame]
/*
* 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();
}
}
}
}