View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.client.ui.commons.wizards;
20  
21  import java.io.Serializable;
22  import java.util.ArrayList;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Optional;
26  import java.util.concurrent.Callable;
27  import java.util.concurrent.ExecutionException;
28  import java.util.concurrent.Future;
29  import java.util.concurrent.TimeUnit;
30  import java.util.concurrent.TimeoutException;
31  import org.apache.commons.lang3.tuple.Pair;
32  import org.apache.syncope.client.ui.commons.Constants;
33  import org.apache.syncope.client.ui.commons.pages.BaseWebPage;
34  import org.apache.syncope.client.ui.commons.panels.SubmitableModalPanel;
35  import org.apache.syncope.client.ui.commons.panels.WizardModalPanel;
36  import org.apache.syncope.client.ui.commons.wizards.exception.CaptchaNotMatchingException;
37  import org.apache.wicket.Application;
38  import org.apache.wicket.Component;
39  import org.apache.wicket.PageReference;
40  import org.apache.wicket.Session;
41  import org.apache.wicket.ThreadContext;
42  import org.apache.wicket.WicketRuntimeException;
43  import org.apache.wicket.ajax.AjaxRequestTarget;
44  import org.apache.wicket.event.Broadcast;
45  import org.apache.wicket.event.IEventSink;
46  import org.apache.wicket.extensions.wizard.IWizardModel;
47  import org.apache.wicket.extensions.wizard.IWizardStep;
48  import org.apache.wicket.extensions.wizard.Wizard;
49  import org.apache.wicket.extensions.wizard.WizardModel;
50  import org.apache.wicket.extensions.wizard.WizardStep;
51  import org.apache.wicket.markup.html.list.ListItem;
52  import org.apache.wicket.markup.html.list.ListView;
53  import org.apache.wicket.model.CompoundPropertyModel;
54  import org.apache.wicket.model.IModel;
55  import org.apache.wicket.request.cycle.RequestCycle;
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  public abstract class AjaxWizard<T extends Serializable> extends Wizard
60          implements SubmitableModalPanel, WizardModalPanel<T> {
61  
62      private static final long serialVersionUID = -1272120742876833520L;
63  
64      private final List<Component> outerObjects = new ArrayList<>();
65  
66      public enum Mode {
67          CREATE,
68          EDIT,
69          TEMPLATE,
70          READONLY,
71          EDIT_APPROVAL;
72  
73      }
74  
75      protected static final Logger LOG = LoggerFactory.getLogger(AjaxWizard.class);
76  
77      private T item;
78  
79      private final Mode mode;
80  
81      private IEventSink eventSink;
82  
83      private final PageReference pageRef;
84  
85      /**
86       * Construct.
87       *
88       * @param id The component id
89       * @param item model object
90       * @param model wizard model
91       * @param mode mode
92       * @param pageRef caller page reference.
93       */
94      public AjaxWizard(
95              final String id,
96              final T item,
97              final WizardModel model,
98              final Mode mode,
99              final PageReference pageRef) {
100 
101         super(id);
102         this.item = item;
103         this.mode = mode;
104         this.pageRef = pageRef;
105 
106         if (mode == Mode.READONLY) {
107             model.setCancelVisible(false);
108         }
109 
110         add(new ListView<>("outerObjectsRepeater", outerObjects) {
111 
112             private static final long serialVersionUID = -9180479401817023838L;
113 
114             @Override
115             protected void populateItem(final ListItem<Component> item) {
116                 item.add(item.getModelObject());
117             }
118 
119         });
120 
121         setOutputMarkupId(true);
122         setDefaultModel(new CompoundPropertyModel<>(this));
123         init(model);
124     }
125 
126     /**
127      * Add object outside the main container.
128      * Use this method just to be not influenced by specific inner object css'.
129      * Be sure to provide {@code outer} as id.
130      *
131      * @param childs components to be added.
132      * @return the current panel instance.
133      */
134     public final AjaxWizard<T> addOuterObject(final List<Component> childs) {
135         outerObjects.addAll(childs);
136         return this;
137     }
138 
139     public AjaxWizard<T> setEventSink(final IEventSink eventSink) {
140         this.eventSink = eventSink;
141         return this;
142     }
143 
144     @Override
145     protected void init(final IWizardModel wizardModel) {
146         super.init(wizardModel);
147         getForm().remove(FEEDBACK_ID);
148 
149         if (mode == Mode.READONLY) {
150             Iterator<IWizardStep> iter = wizardModel.stepIterator();
151             while (iter.hasNext()) {
152                 WizardStep.class.cast(iter.next()).setEnabled(false);
153             }
154         }
155     }
156 
157     @Override
158     protected Component newButtonBar(final String id) {
159         return new AjaxWizardMgtButtonBar<>(id, this, mode);
160     }
161 
162     protected abstract void onCancelInternal();
163 
164     protected abstract void sendError(Exception exception);
165 
166     protected abstract void sendWarning(String message);
167 
168     protected abstract Future<Pair<Serializable, Serializable>> execute(
169             Callable<Pair<Serializable, Serializable>> future);
170 
171     /**
172      * Apply operation
173      *
174      * @param target request target
175      * @return a pair of payload (maybe null) and resulting object.
176      */
177     protected abstract Pair<Serializable, Serializable> onApplyInternal(AjaxRequestTarget target);
178 
179     protected abstract long getMaxWaitTimeInSeconds();
180 
181     @Override
182     public final void onCancel() {
183         AjaxRequestTarget target = RequestCycle.get().find(AjaxRequestTarget.class).orElse(null);
184         try {
185             onCancelInternal();
186             if (eventSink == null) {
187                 send(AjaxWizard.this, Broadcast.BUBBLE, new NewItemCancelEvent<>(item, target));
188             } else {
189                 send(eventSink, Broadcast.EXACT, new NewItemCancelEvent<>(item, target));
190             }
191         } catch (Exception e) {
192             LOG.warn("Wizard error on cancel", e);
193             sendError(e);
194             ((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(target);
195         }
196     }
197 
198     @Override
199     public final void onFinish() {
200         AjaxRequestTarget target = RequestCycle.get().find(AjaxRequestTarget.class).orElse(null);
201         try {
202             final Serializable res = onApply(target);
203             if (eventSink == null) {
204                 send(this, Broadcast.BUBBLE, new NewItemFinishEvent<>(item, target).setResult(res));
205             } else {
206                 send(eventSink, Broadcast.EXACT, new NewItemFinishEvent<>(item, target).setResult(res));
207             }
208         } catch (TimeoutException te) {
209             LOG.warn("Operation took too long", te);
210             if (eventSink == null) {
211                 send(this, Broadcast.BUBBLE, new NewItemCancelEvent<>(item, target));
212             } else {
213                 send(eventSink, Broadcast.EXACT, new NewItemCancelEvent<>(item, target));
214             }
215             sendWarning(getString("timeout"));
216             ((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(target);
217         } catch (CaptchaNotMatchingException ce) {
218             LOG.error("Wizard error on finish: captcha not matching", ce);
219             sendError(new WicketRuntimeException(getString(Constants.CAPTCHA_ERROR)));
220             ((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(target);
221         } catch (Exception e) {
222             LOG.error("Wizard error on finish", e);
223             sendError(e);
224             ((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(target);
225         }
226     }
227 
228     @Override
229     public T getItem() {
230         return item;
231     }
232 
233     /**
234      * Replaces the default value provided with the constructor.
235      *
236      * @param item new value.
237      * @return the current wizard instance.
238      */
239     public AjaxWizard<T> setItem(final T item) {
240         this.item = item;
241         return this;
242     }
243 
244     @Override
245     public void onSubmit(final AjaxRequestTarget target) {
246         try {
247             onApply(target);
248         } catch (TimeoutException te) {
249             LOG.warn("Operation took too long", te);
250             send(eventSink, Broadcast.EXACT, new NewItemCancelEvent<>(item, target));
251             sendWarning(getString("timeout"));
252             ((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(target);
253         }
254     }
255 
256     @Override
257     public void onError(final AjaxRequestTarget target) {
258         ((BaseWebPage) getPage()).getNotificationPanel().refresh(target);
259     }
260 
261     private Serializable onApply(final AjaxRequestTarget target) throws TimeoutException {
262         try {
263             Future<Pair<Serializable, Serializable>> executor = execute(new ApplyFuture(target));
264 
265             Pair<Serializable, Serializable> res = executor.get(getMaxWaitTimeInSeconds(), TimeUnit.SECONDS);
266 
267             if (res.getLeft() != null) {
268                 send(pageRef.getPage(), Broadcast.BUBBLE, res.getLeft());
269             }
270 
271             return res.getRight();
272         } catch (InterruptedException | ExecutionException e) {
273             if (e.getCause() instanceof CaptchaNotMatchingException) {
274                 throw (CaptchaNotMatchingException) e.getCause();
275             }
276             throw new WicketRuntimeException(e);
277         }
278     }
279 
280     public abstract static class NewItemEvent<T extends Serializable> {
281 
282         private final T item;
283 
284         private IModel<String> titleModel;
285 
286         private final AjaxRequestTarget target;
287 
288         private WizardModalPanel<?> modalPanel;
289 
290         public NewItemEvent(final T item, final AjaxRequestTarget target) {
291             this.item = item;
292             this.target = target;
293         }
294 
295         public T getItem() {
296             return item;
297         }
298 
299         public Optional<AjaxRequestTarget> getTarget() {
300             return Optional.ofNullable(target);
301         }
302 
303         public WizardModalPanel<?> getModalPanel() {
304             return modalPanel;
305         }
306 
307         public NewItemEvent<T> forceModalPanel(final WizardModalPanel<?> modalPanel) {
308             this.modalPanel = modalPanel;
309             return this;
310         }
311 
312         public IModel<String> getTitleModel() {
313             return titleModel;
314         }
315 
316         public NewItemEvent<T> setTitleModel(final IModel<String> titleModel) {
317             this.titleModel = titleModel;
318             return this;
319         }
320 
321         public abstract String getEventDescription();
322     }
323 
324     public static class NewItemActionEvent<T extends Serializable> extends NewItemEvent<T> {
325 
326         private static final String EVENT_DESCRIPTION = "new";
327 
328         private int index;
329 
330         public NewItemActionEvent(final T item, final AjaxRequestTarget target) {
331             super(item, target);
332         }
333 
334         public NewItemActionEvent(final T item, final int index, final AjaxRequestTarget target) {
335             super(item, target);
336             this.index = index;
337         }
338 
339         public int getIndex() {
340             return index;
341         }
342 
343         @Override
344         public String getEventDescription() {
345             return NewItemActionEvent.EVENT_DESCRIPTION;
346         }
347     }
348 
349     public static class EditItemActionEvent<T extends Serializable> extends NewItemActionEvent<T> {
350 
351         private static final String EVENT_DESCRIPTION = "edit";
352 
353         public EditItemActionEvent(final T item, final AjaxRequestTarget target) {
354             super(item, target);
355         }
356 
357         public EditItemActionEvent(final T item, final int index, final AjaxRequestTarget target) {
358             super(item, index, target);
359         }
360 
361         @Override
362         public String getEventDescription() {
363             return EditItemActionEvent.EVENT_DESCRIPTION;
364         }
365     }
366 
367     public static class NewItemCancelEvent<T extends Serializable> extends NewItemEvent<T> {
368 
369         private static final String EVENT_DESCRIPTION = "cancel";
370 
371         public NewItemCancelEvent(final T item, final AjaxRequestTarget target) {
372             super(item, target);
373         }
374 
375         @Override
376         public String getEventDescription() {
377             return NewItemCancelEvent.EVENT_DESCRIPTION;
378         }
379     }
380 
381     public static class NewItemFinishEvent<T extends Serializable> extends NewItemEvent<T> {
382 
383         private static final String EVENT_DESCRIPTION = "finish";
384 
385         private Serializable result;
386 
387         public NewItemFinishEvent(final T item, final AjaxRequestTarget target) {
388             super(item, target);
389         }
390 
391         @Override
392         public String getEventDescription() {
393             return NewItemFinishEvent.EVENT_DESCRIPTION;
394         }
395 
396         public NewItemFinishEvent<T> setResult(final Serializable result) {
397             this.result = result;
398             return this;
399         }
400 
401         public Serializable getResult() {
402             return result;
403         }
404     }
405 
406     private class ApplyFuture implements Callable<Pair<Serializable, Serializable>>, Serializable {
407 
408         private static final long serialVersionUID = -4657123322652656848L;
409 
410         private final AjaxRequestTarget target;
411 
412         private final Application application;
413 
414         private final RequestCycle requestCycle;
415 
416         private final Session session;
417 
418         ApplyFuture(final AjaxRequestTarget target) {
419             this.target = target;
420             this.application = Application.get();
421             this.requestCycle = RequestCycle.get();
422             this.session = Session.exists() ? Session.get() : null;
423         }
424 
425         @Override
426         public Pair<Serializable, Serializable> call() throws Exception {
427             try {
428                 ThreadContext.setApplication(this.application);
429                 ThreadContext.setRequestCycle(this.requestCycle);
430                 ThreadContext.setSession(this.session);
431                 return AjaxWizard.this.onApplyInternal(this.target);
432             } finally {
433                 ThreadContext.detach();
434             }
435         }
436     }
437 }