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.PullTaskTO;
63  import org.apache.syncope.common.lib.to.ReportTO;
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                             ProvisioningTaskTO schedTaskTO;
424                             try {
425                                 schedTaskTO = taskRestClient.readTask(TaskType.PULL, jobTO.getRefKey());
426                             } catch (Exception e) {
427                                 LOG.debug("Failed to read {} as {}, attempting {}",
428                                         jobTO.getRefKey(), TaskType.PULL, TaskType.PUSH, e);
429                                 schedTaskTO = taskRestClient.readTask(TaskType.PUSH, jobTO.getRefKey());
430                             }
431 
432                             SchedTaskWizardBuilder<ProvisioningTaskTO> swb =
433                                     new SchedTaskWizardBuilder<>(schedTaskTO instanceof PullTaskTO
434                                             ? TaskType.PULL : TaskType.PUSH, schedTaskTO,
435                                             realmRestClient, taskRestClient, pageRef);
436                             swb.setEventSink(AvailableJobsPanel.this);
437 
438                             target.add(jobModal.setContent(swb.build(BaseModal.CONTENT_ID, AjaxWizard.Mode.EDIT)));
439 
440                             jobModal.header(new StringResourceModel(
441                                     "any.edit",
442                                     AvailableJobsPanel.this,
443                                     new Model<>(schedTaskTO)));
444 
445                             jobModal.show(true);
446                             break;
447 
448                         default:
449                             break;
450                     }
451                 }
452 
453                 @Override
454                 protected boolean statusCondition(final JobTO modelObject) {
455                     return !(null != jobTO.getType() && JobType.NOTIFICATION.equals(jobTO.getType()));
456                 }
457             }, ActionType.EDIT, IdRepoEntitlement.TASK_UPDATE);
458 
459             panel.add(new ActionLink<>() {
460 
461                 private static final long serialVersionUID = -3722207913631435501L;
462 
463                 @Override
464                 public void onClick(final AjaxRequestTarget target, final JobTO ignore) {
465                     try {
466                         if (null != jobTO.getType()) {
467                             switch (jobTO.getType()) {
468 
469                                 case NOTIFICATION:
470                                     break;
471 
472                                 case REPORT:
473                                     reportRestClient.actionJob(jobTO.getRefKey(), JobAction.DELETE);
474                                     break;
475 
476                                 case TASK:
477                                     taskRestClient.actionJob(jobTO.getRefKey(), JobAction.DELETE);
478                                     break;
479 
480                                 default:
481                                     break;
482                             }
483                             SyncopeConsoleSession.get().success(getString(Constants.OPERATION_SUCCEEDED));
484                             target.add(container);
485                         }
486                     } catch (SyncopeClientException e) {
487                         LOG.error("While deleting object {}", jobTO.getRefKey(), e);
488                         SyncopeConsoleSession.get().onException(e);
489                     }
490                     ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(target);
491                 }
492 
493                 @Override
494                 protected boolean statusCondition(final JobTO modelObject) {
495                     return (null != jobTO.getType()
496                             && !JobType.NOTIFICATION.equals(jobTO.getType())
497                             && (jobTO.isScheduled() && !jobTO.isRunning()));
498                 }
499             }, ActionLink.ActionType.DELETE, IdRepoEntitlement.TASK_DELETE, true);
500 
501             return panel;
502         }
503 
504         @Override
505         @SuppressWarnings("unchecked")
506         public void onEvent(final IEvent<?> event) {
507             if (event.getPayload() instanceof AjaxWizard.NewItemCancelEvent
508                     || event.getPayload() instanceof AjaxWizard.NewItemFinishEvent) {
509 
510                 Optional<AjaxRequestTarget> target = ((AjaxWizard.NewItemEvent<?>) event.getPayload()).getTarget();
511                 target.ifPresent(jobModal::close);
512             }
513 
514             super.onEvent(event);
515         }
516     }
517 
518     protected final class AvailableJobsProvider extends DirectoryDataProvider<JobTO> {
519 
520         private static final long serialVersionUID = 3191573490219472572L;
521 
522         private final SortableDataProviderComparator<JobTO> comparator;
523 
524         private AvailableJobsProvider() {
525             super(ROWS);
526             setSort("type", SortOrder.ASCENDING);
527             comparator = new SortableDataProviderComparator<>(this);
528         }
529 
530         @Override
531         public Iterator<JobTO> iterator(final long first, final long count) {
532             available.sort(comparator);
533             return available.subList((int) first, (int) first + (int) count).iterator();
534         }
535 
536         @Override
537         public long size() {
538             return available.size();
539         }
540 
541         @Override
542         public IModel<JobTO> model(final JobTO object) {
543             return new CompoundPropertyModel<>(object);
544         }
545     }
546 
547     private class RecentExecPanel
548             extends DirectoryPanel<ExecTO, ExecTO, RecentExecPanel.RecentExecProvider, BaseRestClient> {
549 
550         private static final long serialVersionUID = -8214546246301342868L;
551 
552         RecentExecPanel(final String id, final PageReference pageRef) {
553             super(id, new Builder<ExecTO, ExecTO, BaseRestClient>(null, pageRef) {
554 
555                 private static final long serialVersionUID = 8769126634538601689L;
556 
557                 @Override
558                 protected WizardMgtPanel<ExecTO> newInstance(final String id, final boolean wizardInModal) {
559                     throw new UnsupportedOperationException();
560                 }
561             }.disableCheckBoxes().hidePaginator());
562 
563             rows = ROWS;
564             initResultTable();
565         }
566 
567         @Override
568         protected RecentExecProvider dataProvider() {
569             return new RecentExecProvider();
570         }
571 
572         @Override
573         protected String paginatorRowsKey() {
574             return StringUtils.EMPTY;
575         }
576 
577         @Override
578         protected Collection<ActionLink.ActionType> getBatches() {
579             return List.of();
580         }
581 
582         @Override
583         protected List<IColumn<ExecTO, String>> getColumns() {
584             List<IColumn<ExecTO, String>> columns = new ArrayList<>();
585 
586             columns.add(new PropertyColumn<>(new ResourceModel("refDesc"), "refDesc", "refDesc"));
587 
588             columns.add(new DatePropertyColumn<>(new ResourceModel("start"), "start", "start"));
589 
590             columns.add(new DatePropertyColumn<>(new ResourceModel("end"), "end", "end"));
591 
592             columns.add(new PropertyColumn<>(new ResourceModel("status"), "status", "status"));
593 
594             return columns;
595         }
596 
597         @Override
598         public ActionsPanel<ExecTO> getActions(final IModel<ExecTO> model) {
599             ActionsPanel<ExecTO> panel = super.getActions(model);
600 
601             panel.add(new ActionLink<>() {
602 
603                 private static final long serialVersionUID = -3722207913631435501L;
604 
605                 @Override
606                 public void onClick(final AjaxRequestTarget target, final ExecTO ignore) {
607                     detailModal.header(new StringResourceModel("execution.view", JobWidget.this, model));
608                     detailModal.setContent(new ExecMessageModal(model.getObject().getMessage()));
609                     detailModal.show(true);
610                     target.add(detailModal);
611                 }
612             }, ActionLink.ActionType.VIEW, IdRepoEntitlement.TASK_READ);
613 
614             return panel;
615         }
616 
617         protected final class RecentExecProvider extends DirectoryDataProvider<ExecTO> {
618 
619             private static final long serialVersionUID = 2835707012690698633L;
620 
621             private final SortableDataProviderComparator<ExecTO> comparator;
622 
623             private RecentExecProvider() {
624                 super(ROWS);
625                 setSort("end", SortOrder.DESCENDING);
626                 comparator = new SortableDataProviderComparator<>(this);
627             }
628 
629             @Override
630             public Iterator<ExecTO> iterator(final long first, final long count) {
631                 recent.sort(comparator);
632                 return recent.subList((int) first, (int) first + (int) count).iterator();
633             }
634 
635             @Override
636             public long size() {
637                 return recent.size();
638             }
639 
640             @Override
641             public IModel<ExecTO> model(final ExecTO object) {
642                 return new CompoundPropertyModel<>(object);
643             }
644         }
645     }
646 }