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.console.widgets;
20  
21  import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.Modal;
22  import de.agilecoders.wicket.core.markup.html.bootstrap.tabs.AjaxBootstrapTabbedPanel;
23  import java.io.Serializable;
24  import java.time.Duration;
25  import java.time.temporal.ChronoUnit;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Optional;
31  import org.apache.commons.lang3.StringUtils;
32  import org.apache.syncope.client.console.SyncopeConsoleSession;
33  import org.apache.syncope.client.console.commons.DirectoryDataProvider;
34  import org.apache.syncope.client.console.commons.SortableDataProviderComparator;
35  import org.apache.syncope.client.console.pages.BasePage;
36  import org.apache.syncope.client.console.panels.DirectoryPanel;
37  import org.apache.syncope.client.console.panels.ExecMessageModal;
38  import org.apache.syncope.client.console.reports.ReportWizardBuilder;
39  import org.apache.syncope.client.console.rest.BaseRestClient;
40  import org.apache.syncope.client.console.rest.ImplementationRestClient;
41  import org.apache.syncope.client.console.rest.NotificationRestClient;
42  import org.apache.syncope.client.console.rest.RealmRestClient;
43  import org.apache.syncope.client.console.rest.ReportRestClient;
44  import org.apache.syncope.client.console.rest.TaskRestClient;
45  import org.apache.syncope.client.console.tasks.SchedTaskWizardBuilder;
46  import org.apache.syncope.client.console.wicket.ajax.IndicatorAjaxTimerBehavior;
47  import org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.BooleanPropertyColumn;
48  import org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.DatePropertyColumn;
49  import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
50  import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
51  import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink.ActionType;
52  import org.apache.syncope.client.console.wicket.markup.html.form.ActionLinksTogglePanel;
53  import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
54  import org.apache.syncope.client.console.wizards.WizardMgtPanel;
55  import org.apache.syncope.client.ui.commons.Constants;
56  import org.apache.syncope.client.ui.commons.MIMETypesLoader;
57  import org.apache.syncope.client.ui.commons.wizards.AjaxWizard;
58  import org.apache.syncope.common.lib.SyncopeClientException;
59  import org.apache.syncope.common.lib.to.ExecTO;
60  import org.apache.syncope.common.lib.to.JobTO;
61  import org.apache.syncope.common.lib.to.ProvisioningTaskTO;
62  import org.apache.syncope.common.lib.to.ReportTO;
63  import org.apache.syncope.common.lib.to.TaskTO;
64  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
65  import org.apache.syncope.common.lib.types.JobAction;
66  import org.apache.syncope.common.lib.types.JobType;
67  import org.apache.syncope.common.lib.types.TaskType;
68  import org.apache.wicket.PageReference;
69  import org.apache.wicket.ajax.AjaxRequestTarget;
70  import org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy;
71  import org.apache.wicket.event.IEvent;
72  import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
73  import org.apache.wicket.extensions.markup.html.repeater.data.sort.SortOrder;
74  import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn;
75  import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
76  import org.apache.wicket.extensions.markup.html.repeater.data.table.PropertyColumn;
77  import org.apache.wicket.extensions.markup.html.tabs.AbstractTab;
78  import org.apache.wicket.extensions.markup.html.tabs.ITab;
79  import org.apache.wicket.markup.html.WebMarkupContainer;
80  import org.apache.wicket.markup.html.WebPage;
81  import org.apache.wicket.markup.html.panel.Panel;
82  import org.apache.wicket.markup.repeater.Item;
83  import org.apache.wicket.model.CompoundPropertyModel;
84  import org.apache.wicket.model.IModel;
85  import org.apache.wicket.model.Model;
86  import org.apache.wicket.model.ResourceModel;
87  import org.apache.wicket.model.StringResourceModel;
88  import org.apache.wicket.spring.injection.annot.SpringBean;
89  
90  public class JobWidget extends BaseWidget {
91  
92      private static final long serialVersionUID = 7667120094526529934L;
93  
94      protected static final int ROWS = 5;
95  
96      @SpringBean
97      protected NotificationRestClient notificationRestClient;
98  
99      @SpringBean
100     protected ReportRestClient reportRestClient;
101 
102     @SpringBean
103     protected TaskRestClient taskRestClient;
104 
105     @SpringBean
106     protected RealmRestClient realmRestClient;
107 
108     @SpringBean
109     protected ImplementationRestClient implementationRestClient;
110 
111     @SpringBean
112     protected MIMETypesLoader mimeTypesLoader;
113 
114     protected final ActionLinksTogglePanel<JobTO> actionTogglePanel;
115 
116     protected final BaseModal<Serializable> modal = new BaseModal<>("modal") {
117 
118         private static final long serialVersionUID = 389935548143327858L;
119 
120         @Override
121         protected void onConfigure() {
122             super.onConfigure();
123             setFooterVisible(false);
124         }
125     };
126 
127     protected final BaseModal<Serializable> detailModal = new BaseModal<>("detailModal") {
128 
129         private static final long serialVersionUID = 389935548143327858L;
130 
131         @Override
132         protected void onConfigure() {
133             super.onConfigure();
134             setFooterVisible(true);
135         }
136     };
137 
138     protected final BaseModal<ReportTO> reportModal = new BaseModal<>("reportModal") {
139 
140         private static final long serialVersionUID = 389935548143327858L;
141 
142         @Override
143         protected void onConfigure() {
144             super.onConfigure();
145             setFooterVisible(false);
146         }
147     };
148 
149     protected final WebMarkupContainer container;
150 
151     protected final List<JobTO> available;
152 
153     protected AvailableJobsPanel availableJobsPanel;
154 
155     protected final List<ExecTO> recent;
156 
157     protected RecentExecPanel recentExecPanel;
158 
159     public JobWidget(final String id, final PageReference pageRef) {
160         super(id);
161         setOutputMarkupId(true);
162         add(modal);
163         modal.setWindowClosedCallback(target -> modal.show(false));
164 
165         add(detailModal);
166         detailModal.setWindowClosedCallback(target -> detailModal.show(false));
167 
168         add(reportModal);
169         reportModal.setWindowClosedCallback(target -> reportModal.show(false));
170 
171         reportModal.size(Modal.Size.Large);
172 
173         available = getUpdatedAvailable();
174         recent = getUpdatedRecent();
175 
176         container = new WebMarkupContainer("jobContainer");
177         container.add(new IndicatorAjaxTimerBehavior(Duration.of(10, ChronoUnit.SECONDS)) {
178 
179             private static final long serialVersionUID = 7298597675929755960L;
180 
181             @Override
182             protected void onTimer(final AjaxRequestTarget target) {
183                 List<JobTO> updatedAvailable = getUpdatedAvailable();
184                 if (!updatedAvailable.equals(available)) {
185                     available.clear();
186                     available.addAll(updatedAvailable);
187                     if (availableJobsPanel != null) {
188                         availableJobsPanel.modelChanged();
189                         target.add(availableJobsPanel);
190                     }
191                 }
192 
193                 List<ExecTO> updatedRecent = getUpdatedRecent();
194                 if (!updatedRecent.equals(recent)) {
195                     recent.clear();
196                     recent.addAll(updatedRecent);
197                     if (recentExecPanel != null) {
198                         recentExecPanel.modelChanged();
199                         target.add(recentExecPanel);
200                     }
201                 }
202             }
203         });
204         add(container);
205 
206         container.add(new AjaxBootstrapTabbedPanel<>("tabbedPanel", buildTabList(pageRef)));
207 
208         actionTogglePanel = new ActionLinksTogglePanel<>("actionTogglePanel", pageRef);
209         add(actionTogglePanel);
210     }
211 
212     protected List<JobTO> getUpdatedAvailable() {
213         List<JobTO> updatedAvailable = new ArrayList<>();
214 
215         if (SyncopeConsoleSession.get().owns(IdRepoEntitlement.NOTIFICATION_LIST)) {
216             JobTO notificationJob = notificationRestClient.getJob();
217             if (notificationJob != null) {
218                 updatedAvailable.add(notificationJob);
219             }
220         }
221         if (SyncopeConsoleSession.get().owns(IdRepoEntitlement.TASK_LIST)) {
222             updatedAvailable.addAll(taskRestClient.listJobs());
223         }
224         if (SyncopeConsoleSession.get().owns(IdRepoEntitlement.REPORT_LIST)) {
225             updatedAvailable.addAll(reportRestClient.listJobs());
226         }
227 
228         return updatedAvailable;
229     }
230 
231     protected List<ExecTO> getUpdatedRecent() {
232         List<ExecTO> updatedRecent = new ArrayList<>();
233 
234         if (SyncopeConsoleSession.get().owns(IdRepoEntitlement.TASK_LIST)) {
235             updatedRecent.addAll(taskRestClient.listRecentExecutions(10));
236         }
237         if (SyncopeConsoleSession.get().owns(IdRepoEntitlement.REPORT_LIST)) {
238             updatedRecent.addAll(reportRestClient.listRecentExecutions(10));
239         }
240 
241         return updatedRecent;
242     }
243 
244     protected List<ITab> buildTabList(final PageReference pageRef) {
245         List<ITab> tabs = new ArrayList<>();
246 
247         tabs.add(new AbstractTab(new ResourceModel("available")) {
248 
249             private static final long serialVersionUID = -6815067322125799251L;
250 
251             @Override
252             public Panel getPanel(final String panelId) {
253                 availableJobsPanel = new AvailableJobsPanel(panelId, pageRef);
254                 availableJobsPanel.setOutputMarkupId(true);
255                 return availableJobsPanel;
256             }
257         });
258 
259         tabs.add(new AbstractTab(new ResourceModel("recent")) {
260 
261             private static final long serialVersionUID = -6815067322125799251L;
262 
263             @Override
264             public Panel getPanel(final String panelId) {
265                 recentExecPanel = new RecentExecPanel(panelId, pageRef);
266                 recentExecPanel.setOutputMarkupId(true);
267                 return recentExecPanel;
268             }
269         });
270 
271         return tabs;
272     }
273 
274     @Override
275     public void onEvent(final IEvent<?> event) {
276         if (event.getPayload() instanceof JobActionPanel.JobActionPayload) {
277             available.clear();
278             available.addAll(getUpdatedAvailable());
279             availableJobsPanel.modelChanged();
280             JobActionPanel.JobActionPayload.class.cast(event.getPayload()).getTarget().add(availableJobsPanel);
281         }
282     }
283 
284     protected class AvailableJobsPanel extends DirectoryPanel<JobTO, JobTO, AvailableJobsProvider, BaseRestClient> {
285 
286         private static final long serialVersionUID = -8214546246301342868L;
287 
288         private final BaseModal<ReportTO> reportModal;
289 
290         private final BaseModal<Serializable> jobModal;
291 
292         AvailableJobsPanel(final String id, final PageReference pageRef) {
293             super(id, new Builder<JobTO, JobTO, BaseRestClient>(null, pageRef) {
294 
295                 private static final long serialVersionUID = 8769126634538601689L;
296 
297                 @Override
298                 protected WizardMgtPanel<JobTO> newInstance(final String id, final boolean wizardInModal) {
299                     throw new UnsupportedOperationException();
300                 }
301             }.disableCheckBoxes().hidePaginator());
302 
303             this.reportModal = JobWidget.this.reportModal;
304             setWindowClosedReloadCallback(reportModal);
305 
306             this.jobModal = JobWidget.this.modal;
307             setWindowClosedReloadCallback(jobModal);
308 
309             rows = ROWS;
310             initResultTable();
311         }
312 
313         @Override
314         protected AvailableJobsProvider dataProvider() {
315             return new AvailableJobsProvider();
316         }
317 
318         @Override
319         protected String paginatorRowsKey() {
320             return StringUtils.EMPTY;
321         }
322 
323         @Override
324         protected Collection<ActionLink.ActionType> getBatches() {
325             return List.of();
326         }
327 
328         @Override
329         protected List<IColumn<JobTO, String>> getColumns() {
330             List<IColumn<JobTO, String>> columns = new ArrayList<>();
331 
332             columns.add(new PropertyColumn<>(new ResourceModel("refDesc"), "refDesc", "refDesc"));
333 
334             columns.add(new BooleanPropertyColumn<>(new ResourceModel("scheduled"), "scheduled", "scheduled"));
335 
336             columns.add(new DatePropertyColumn<>(new ResourceModel("start"), "start", "start"));
337 
338             columns.add(new AbstractColumn<>(new Model<>(""), "running") {
339 
340                 private static final long serialVersionUID = -4008579357070833846L;
341 
342                 @Override
343                 public void populateItem(
344                         final Item<ICellPopulator<JobTO>> cellItem,
345                         final String componentId,
346                         final IModel<JobTO> rowModel) {
347 
348                     JobTO jobTO = rowModel.getObject();
349                     JobActionPanel panel = new JobActionPanel(componentId, jobTO, true, JobWidget.this);
350 
351                     String roles;
352                     switch (jobTO.getType()) {
353                         case TASK:
354                             roles = String.format("%s,%s",
355                                     IdRepoEntitlement.TASK_EXECUTE, IdRepoEntitlement.TASK_UPDATE);
356                             break;
357 
358                         case REPORT:
359                             roles = String.format("%s,%s",
360                                     IdRepoEntitlement.REPORT_EXECUTE, IdRepoEntitlement.REPORT_UPDATE);
361                             break;
362 
363                         case NOTIFICATION:
364                             roles = String.format("%s,%s",
365                                     IdRepoEntitlement.NOTIFICATION_EXECUTE, IdRepoEntitlement.NOTIFICATION_UPDATE);
366                             break;
367 
368                         default:
369                             roles = "NO_ROLES";
370                     }
371                     MetaDataRoleAuthorizationStrategy.authorize(panel, WebPage.ENABLE, roles);
372 
373                     cellItem.add(panel);
374                 }
375 
376                 @Override
377                 public String getCssClass() {
378                     return "running-col";
379                 }
380             });
381 
382             return columns;
383         }
384 
385         @Override
386         protected ActionsPanel<JobTO> getActions(final IModel<JobTO> model) {
387             final ActionsPanel<JobTO> panel = super.getActions(model);
388 
389             final JobTO jobTO = model.getObject();
390 
391             panel.add(new ActionLink<>() {
392 
393                 private static final long serialVersionUID = -7978723352517770644L;
394 
395                 @Override
396                 public void onClick(final AjaxRequestTarget target, final JobTO ignore) {
397                     switch (jobTO.getType()) {
398                         case NOTIFICATION:
399                             break;
400 
401                         case REPORT:
402                             ReportTO reportTO = reportRestClient.read(jobTO.getRefKey());
403 
404                             ReportWizardBuilder rwb = new ReportWizardBuilder(
405                                     reportTO,
406                                     implementationRestClient,
407                                     reportRestClient,
408                                     mimeTypesLoader,
409                                     pageRef);
410                             rwb.setEventSink(AvailableJobsPanel.this);
411 
412                             target.add(jobModal.setContent(rwb.build(BaseModal.CONTENT_ID, AjaxWizard.Mode.EDIT)));
413 
414                             jobModal.header(new StringResourceModel(
415                                     "any.edit",
416                                     AvailableJobsPanel.this,
417                                     new Model<>(reportTO)));
418 
419                             jobModal.show(true);
420                             break;
421 
422                         case TASK:
423                             TaskType taskType = null;
424                             if (jobTO.getRefDesc().startsWith("SCHEDULED")) {
425                                 taskType = TaskType.SCHEDULED;
426                             } else if (jobTO.getRefDesc().startsWith("PULL")) {
427                                 taskType = TaskType.PULL;
428                             } else if (jobTO.getRefDesc().startsWith("PUSH")) {
429                                 taskType = TaskType.PUSH;
430                             } else if (jobTO.getRefDesc().startsWith("MACRO")) {
431                                 taskType = TaskType.MACRO;
432                             }
433                             if (taskType == null) {
434                                 break;
435                             }
436 
437                             TaskTO taskTO = null;
438                             try {
439                                 taskTO = taskRestClient.readTask(taskType, jobTO.getRefKey());
440                             } catch (Exception e) {
441                                 LOG.debug("Failed to read {} as {}", jobTO.getRefKey(), taskType, e);
442                             }
443                             if (taskTO == null) {
444                                 break;
445                             }
446 
447                             if (taskTO instanceof ProvisioningTaskTO) {
448                                 SchedTaskWizardBuilder<ProvisioningTaskTO> swb =
449                                         new SchedTaskWizardBuilder<>(taskType, (ProvisioningTaskTO) taskTO,
450                                                 realmRestClient, taskRestClient, pageRef);
451                                 swb.setEventSink(AvailableJobsPanel.this);
452 
453                                 target.add(jobModal.setContent(swb.build(BaseModal.CONTENT_ID, AjaxWizard.Mode.EDIT)));
454 
455                                 jobModal.header(new StringResourceModel(
456                                         "any.edit",
457                                         AvailableJobsPanel.this,
458                                         new Model<>(taskTO)));
459 
460                                 jobModal.show(true);
461                             } else {
462                                 SyncopeConsoleSession.get().info("Unsupported task type: " + taskType.name());
463                                 ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(target);
464                             }
465                             break;
466 
467                         default:
468                             break;
469                     }
470                 }
471 
472                 @Override
473                 protected boolean statusCondition(final JobTO modelObject) {
474                     return !(null != jobTO.getType() && JobType.NOTIFICATION.equals(jobTO.getType()));
475                 }
476             }, ActionType.EDIT, IdRepoEntitlement.TASK_UPDATE);
477 
478             panel.add(new ActionLink<>() {
479 
480                 private static final long serialVersionUID = -3722207913631435501L;
481 
482                 @Override
483                 public void onClick(final AjaxRequestTarget target, final JobTO ignore) {
484                     try {
485                         if (null != jobTO.getType()) {
486                             switch (jobTO.getType()) {
487 
488                                 case NOTIFICATION:
489                                     break;
490 
491                                 case REPORT:
492                                     reportRestClient.actionJob(jobTO.getRefKey(), JobAction.DELETE);
493                                     break;
494 
495                                 case TASK:
496                                     taskRestClient.actionJob(jobTO.getRefKey(), JobAction.DELETE);
497                                     break;
498 
499                                 default:
500                                     break;
501                             }
502                             SyncopeConsoleSession.get().success(getString(Constants.OPERATION_SUCCEEDED));
503                             target.add(container);
504                         }
505                     } catch (SyncopeClientException e) {
506                         LOG.error("While deleting object {}", jobTO.getRefKey(), e);
507                         SyncopeConsoleSession.get().onException(e);
508                     }
509                     ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(target);
510                 }
511 
512                 @Override
513                 protected boolean statusCondition(final JobTO modelObject) {
514                     return (null != jobTO.getType()
515                             && !JobType.NOTIFICATION.equals(jobTO.getType())
516                             && (jobTO.isScheduled() && !jobTO.isRunning()));
517                 }
518             }, ActionLink.ActionType.DELETE, IdRepoEntitlement.TASK_DELETE, true);
519 
520             return panel;
521         }
522 
523         @Override
524         @SuppressWarnings("unchecked")
525         public void onEvent(final IEvent<?> event) {
526             if (event.getPayload() instanceof AjaxWizard.NewItemCancelEvent
527                     || event.getPayload() instanceof AjaxWizard.NewItemFinishEvent) {
528 
529                 Optional<AjaxRequestTarget> target = ((AjaxWizard.NewItemEvent<?>) event.getPayload()).getTarget();
530                 target.ifPresent(jobModal::close);
531             }
532 
533             super.onEvent(event);
534         }
535     }
536 
537     protected final class AvailableJobsProvider extends DirectoryDataProvider<JobTO> {
538 
539         private static final long serialVersionUID = 3191573490219472572L;
540 
541         private final SortableDataProviderComparator<JobTO> comparator;
542 
543         private AvailableJobsProvider() {
544             super(ROWS);
545             setSort("type", SortOrder.ASCENDING);
546             comparator = new SortableDataProviderComparator<>(this);
547         }
548 
549         @Override
550         public Iterator<JobTO> iterator(final long first, final long count) {
551             available.sort(comparator);
552             return available.subList((int) first, (int) first + (int) count).iterator();
553         }
554 
555         @Override
556         public long size() {
557             return available.size();
558         }
559 
560         @Override
561         public IModel<JobTO> model(final JobTO object) {
562             return new CompoundPropertyModel<>(object);
563         }
564     }
565 
566     private class RecentExecPanel
567             extends DirectoryPanel<ExecTO, ExecTO, RecentExecPanel.RecentExecProvider, BaseRestClient> {
568 
569         private static final long serialVersionUID = -8214546246301342868L;
570 
571         RecentExecPanel(final String id, final PageReference pageRef) {
572             super(id, new Builder<ExecTO, ExecTO, BaseRestClient>(null, pageRef) {
573 
574                 private static final long serialVersionUID = 8769126634538601689L;
575 
576                 @Override
577                 protected WizardMgtPanel<ExecTO> newInstance(final String id, final boolean wizardInModal) {
578                     throw new UnsupportedOperationException();
579                 }
580             }.disableCheckBoxes().hidePaginator());
581 
582             rows = ROWS;
583             initResultTable();
584         }
585 
586         @Override
587         protected RecentExecProvider dataProvider() {
588             return new RecentExecProvider();
589         }
590 
591         @Override
592         protected String paginatorRowsKey() {
593             return StringUtils.EMPTY;
594         }
595 
596         @Override
597         protected Collection<ActionLink.ActionType> getBatches() {
598             return List.of();
599         }
600 
601         @Override
602         protected List<IColumn<ExecTO, String>> getColumns() {
603             List<IColumn<ExecTO, String>> columns = new ArrayList<>();
604 
605             columns.add(new PropertyColumn<>(new ResourceModel("refDesc"), "refDesc", "refDesc"));
606 
607             columns.add(new DatePropertyColumn<>(new ResourceModel("start"), "start", "start"));
608 
609             columns.add(new DatePropertyColumn<>(new ResourceModel("end"), "end", "end"));
610 
611             columns.add(new PropertyColumn<>(new ResourceModel("status"), "status", "status"));
612 
613             return columns;
614         }
615 
616         @Override
617         public ActionsPanel<ExecTO> getActions(final IModel<ExecTO> model) {
618             ActionsPanel<ExecTO> panel = super.getActions(model);
619 
620             panel.add(new ActionLink<>() {
621 
622                 private static final long serialVersionUID = -3722207913631435501L;
623 
624                 @Override
625                 public void onClick(final AjaxRequestTarget target, final ExecTO ignore) {
626                     detailModal.header(new StringResourceModel("execution.view", JobWidget.this, model));
627                     detailModal.setContent(new ExecMessageModal(model.getObject().getMessage()));
628                     detailModal.show(true);
629                     target.add(detailModal);
630                 }
631             }, ActionLink.ActionType.VIEW, IdRepoEntitlement.TASK_READ);
632 
633             return panel;
634         }
635 
636         protected final class RecentExecProvider extends DirectoryDataProvider<ExecTO> {
637 
638             private static final long serialVersionUID = 2835707012690698633L;
639 
640             private final SortableDataProviderComparator<ExecTO> comparator;
641 
642             private RecentExecProvider() {
643                 super(ROWS);
644                 setSort("end", SortOrder.DESCENDING);
645                 comparator = new SortableDataProviderComparator<>(this);
646             }
647 
648             @Override
649             public Iterator<ExecTO> iterator(final long first, final long count) {
650                 recent.sort(comparator);
651                 return recent.subList((int) first, (int) first + (int) count).iterator();
652             }
653 
654             @Override
655             public long size() {
656                 return recent.size();
657             }
658 
659             @Override
660             public IModel<ExecTO> model(final ExecTO object) {
661                 return new CompoundPropertyModel<>(object);
662             }
663         }
664     }
665 }