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.wizards.any;
20  
21  import java.util.ArrayList;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Optional;
26  import java.util.stream.Collectors;
27  import org.apache.commons.collections4.ListUtils;
28  import org.apache.commons.lang3.StringUtils;
29  import org.apache.syncope.client.console.panels.AnyDirectoryPanel;
30  import org.apache.syncope.client.console.panels.ListViewPanel;
31  import org.apache.syncope.client.console.panels.ListViewPanel.ListViewReload;
32  import org.apache.syncope.client.console.panels.search.AnyObjectSearchPanel;
33  import org.apache.syncope.client.console.panels.search.AnyObjectSelectionDirectoryPanel;
34  import org.apache.syncope.client.console.panels.search.AnySelectionDirectoryPanel;
35  import org.apache.syncope.client.console.panels.search.SearchClausePanel;
36  import org.apache.syncope.client.console.panels.search.SearchUtils;
37  import org.apache.syncope.client.console.rest.AnyObjectRestClient;
38  import org.apache.syncope.client.console.rest.AnyTypeClassRestClient;
39  import org.apache.syncope.client.console.rest.AnyTypeRestClient;
40  import org.apache.syncope.client.console.rest.RelationshipTypeRestClient;
41  import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
42  import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink.ActionType;
43  import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
44  import org.apache.syncope.client.console.wizards.WizardMgtPanel;
45  import org.apache.syncope.client.lib.SyncopeClient;
46  import org.apache.syncope.client.ui.commons.Constants;
47  import org.apache.syncope.client.ui.commons.ajax.form.IndicatorAjaxFormComponentUpdatingBehavior;
48  import org.apache.syncope.client.ui.commons.ajax.markup.html.LabelInfo;
49  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
50  import org.apache.syncope.client.ui.commons.wicket.markup.html.bootstrap.tabs.Accordion;
51  import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
52  import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
53  import org.apache.syncope.common.lib.to.AnyObjectTO;
54  import org.apache.syncope.common.lib.to.AnyTO;
55  import org.apache.syncope.common.lib.to.AnyTypeTO;
56  import org.apache.syncope.common.lib.to.GroupableRelatableTO;
57  import org.apache.syncope.common.lib.to.RelationshipTO;
58  import org.apache.syncope.common.lib.to.RelationshipTypeTO;
59  import org.apache.syncope.common.lib.types.AnyEntitlement;
60  import org.apache.syncope.common.lib.types.AnyTypeKind;
61  import org.apache.wicket.Component;
62  import org.apache.wicket.PageReference;
63  import org.apache.wicket.ajax.AjaxRequestTarget;
64  import org.apache.wicket.event.Broadcast;
65  import org.apache.wicket.event.IEvent;
66  import org.apache.wicket.extensions.markup.html.tabs.AbstractTab;
67  import org.apache.wicket.extensions.wizard.IWizard;
68  import org.apache.wicket.extensions.wizard.WizardModel.ICondition;
69  import org.apache.wicket.extensions.wizard.WizardStep;
70  import org.apache.wicket.markup.head.IHeaderResponse;
71  import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
72  import org.apache.wicket.markup.html.WebMarkupContainer;
73  import org.apache.wicket.markup.html.basic.Label;
74  import org.apache.wicket.markup.html.form.IChoiceRenderer;
75  import org.apache.wicket.markup.html.panel.Fragment;
76  import org.apache.wicket.markup.html.panel.Panel;
77  import org.apache.wicket.model.IModel;
78  import org.apache.wicket.model.PropertyModel;
79  import org.apache.wicket.model.ResourceModel;
80  import org.apache.wicket.model.util.ListModel;
81  import org.apache.wicket.spring.injection.annot.SpringBean;
82  
83  public class Relationships extends WizardStep implements ICondition {
84  
85      private static final long serialVersionUID = 855618618337931784L;
86  
87      @SpringBean
88      protected RelationshipTypeRestClient relationshipTypeRestClient;
89  
90      @SpringBean
91      protected AnyTypeRestClient anyTypeRestClient;
92  
93      @SpringBean
94      protected AnyTypeClassRestClient anyTypeClassRestClient;
95  
96      @SpringBean
97      protected AnyObjectRestClient anyObjectRestClient;
98  
99      protected final AnyTO anyTO;
100 
101     protected final Specification specification;
102 
103     protected final PageReference pageRef;
104 
105     public Relationships(final AnyWrapper<?> modelObject, final PageReference pageRef) {
106         super();
107         add(new Label("title", new ResourceModel("any.relationships")));
108 
109         if (modelObject instanceof UserWrapper
110                 && UserWrapper.class.cast(modelObject).getPreviousUserTO() != null
111                 && !ListUtils.isEqualList(
112                         UserWrapper.class.cast(modelObject).getInnerObject().getRelationships(),
113                         UserWrapper.class.cast(modelObject).getPreviousUserTO().getRelationships())) {
114             add(new LabelInfo("changed", StringUtils.EMPTY));
115         } else {
116             add(new Label("changed", StringUtils.EMPTY));
117         }
118 
119         this.anyTO = modelObject.getInnerObject();
120         this.specification = new Specification();
121         this.pageRef = pageRef;
122 
123         // ------------------------
124         // Existing relationships
125         // ------------------------
126         add(getViewFragment().setRenderBodyOnly(true));
127         // ------------------------ 
128     }
129 
130     @Override
131     public Component getHeader(final String id, final Component parent, final IWizard wizard) {
132         return super.getHeader(id, parent, wizard).setVisible(false);
133     }
134 
135     protected Fragment getViewFragment() {
136         Map<String, List<RelationshipTO>> relationships = new HashMap<>();
137         addRelationship(relationships, getCurrentRelationships().toArray(RelationshipTO[]::new));
138 
139         Fragment viewFragment = new Fragment("relationships", "viewFragment", this);
140         viewFragment.setOutputMarkupId(true);
141 
142         viewFragment.add(new Accordion("relationships", relationships.keySet().stream().
143                 map(relationship -> new AbstractTab(new ResourceModel("relationship", relationship)) {
144 
145             private static final long serialVersionUID = 1037272333056449378L;
146 
147             @Override
148             public Panel getPanel(final String panelId) {
149                 return new ListViewPanel.Builder<>(RelationshipTO.class, pageRef).
150                         setItems(relationships.get(relationship)).
151                         includes("otherEndType", "otherEndKey", "otherEndName").
152                         addAction(new ActionLink<>() {
153 
154                             private static final long serialVersionUID = -6847033126124401556L;
155 
156                             @Override
157                             public void onClick(final AjaxRequestTarget target, final RelationshipTO modelObject) {
158                                 removeRelationships(relationships, modelObject);
159                                 send(Relationships.this, Broadcast.DEPTH, new ListViewReload<>(target));
160                             }
161                         }, ActionType.DELETE, AnyEntitlement.UPDATE.getFor(anyTO.getType()), true).
162                         build(panelId);
163             }
164         }).collect(Collectors.toList())) {
165 
166             private static final long serialVersionUID = 1037272333056449379L;
167 
168             @Override
169             public void renderHead(final IHeaderResponse response) {
170                 super.renderHead(response);
171                 if (relationships.isEmpty()) {
172                     response.render(OnDomReadyHeaderItem.forScript(String.format(
173                             "$('#emptyPlaceholder').append(\"%s\")", getString("relationships.empty.list"))));
174                 }
175             }
176         });
177 
178         ActionsPanel<RelationshipTO> panel = new ActionsPanel<>("actions", null);
179         viewFragment.add(panel);
180 
181         panel.add(new ActionLink<>() {
182 
183             private static final long serialVersionUID = 3257738274365467945L;
184 
185             @Override
186             public void onClick(final AjaxRequestTarget target, final RelationshipTO ignore) {
187                 Fragment addFragment = new Fragment("relationships", "addFragment", Relationships.this);
188                 addOrReplace(addFragment);
189                 addFragment.add(specification.setRenderBodyOnly(true));
190                 target.add(Relationships.this);
191             }
192         }, ActionType.CREATE, AnyEntitlement.UPDATE.getFor(anyTO.getType())).hideLabel();
193 
194         return viewFragment;
195     }
196 
197     protected List<RelationshipTO> getCurrentRelationships() {
198         return anyTO instanceof GroupableRelatableTO
199                 ? GroupableRelatableTO.class.cast(anyTO).getRelationships()
200                 : List.of();
201     }
202 
203     protected void addRelationship(
204             final Map<String, List<RelationshipTO>> relationships,
205             final RelationshipTO... rels) {
206 
207         for (RelationshipTO relationship : rels) {
208             List<RelationshipTO> listrels;
209             if (relationships.containsKey(relationship.getType())) {
210                 listrels = relationships.get(relationship.getType());
211             } else {
212                 listrels = new ArrayList<>();
213                 relationships.put(relationship.getType(), listrels);
214             }
215             listrels.add(relationship);
216         }
217     }
218 
219     protected void addNewRelationships(final RelationshipTO... rels) {
220         getCurrentRelationships().addAll(List.of(rels));
221     }
222 
223     protected void removeRelationships(
224             final Map<String, List<RelationshipTO>> relationships, final RelationshipTO... rels) {
225 
226         List<RelationshipTO> currentRels = getCurrentRelationships();
227         for (RelationshipTO relationship : rels) {
228             currentRels.remove(relationship);
229             if (relationships.containsKey(relationship.getType())) {
230                 List<RelationshipTO> rellist = relationships.get(relationship.getType());
231                 rellist.remove(relationship);
232                 if (rellist.isEmpty()) {
233                     relationships.remove(relationship.getType());
234                 }
235             }
236         }
237     }
238 
239     @Override
240     public boolean evaluate() {
241         // [SYNCOPE-1171] - skip current step when the are no relationships types in Syncope
242         return !relationshipTypeRestClient.list().isEmpty();
243     }
244 
245     public class Specification extends Panel {
246 
247         private static final long serialVersionUID = 6199050589175839467L;
248 
249         protected final RelationshipTO rel;
250 
251         protected final AjaxDropDownChoicePanel<String> type;
252 
253         protected final AjaxDropDownChoicePanel<AnyTypeTO> otherType;
254 
255         protected final WebMarkupContainer container;
256 
257         protected final Fragment emptyFragment;
258 
259         protected final Fragment fragment;
260 
261         protected AnyObjectSearchPanel anyObjectSearchPanel;
262 
263         protected WizardMgtPanel<AnyWrapper<AnyObjectTO>> anyObjectDirectoryPanel;
264 
265         public Specification() {
266             super("specification");
267             rel = new RelationshipTO();
268 
269             List<String> availableRels = relationshipTypeRestClient.list().stream().
270                     map(RelationshipTypeTO::getKey).collect(Collectors.toList());
271 
272             type = new AjaxDropDownChoicePanel<>("type", "type", new PropertyModel<>(rel, "type"));
273             type.setChoices(availableRels);
274             add(type.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true).setRenderBodyOnly(true));
275 
276             List<AnyTypeTO> availableTypes = anyTypeRestClient.listAnyTypes().stream().
277                     filter(anyType -> anyType.getKind() != AnyTypeKind.GROUP
278                     && anyType.getKind() != AnyTypeKind.USER).collect(Collectors.toList());
279 
280             otherType = new AjaxDropDownChoicePanel<>("otherType", "otherType", new PropertyModel<>(rel, "otherType") {
281 
282                 private static final long serialVersionUID = -5861057041758169508L;
283 
284                 @Override
285                 public AnyTypeTO getObject() {
286                     for (AnyTypeTO obj : availableTypes) {
287                         if (obj.getKey().equals(rel.getOtherEndType())) {
288                             return obj;
289                         }
290                     }
291                     return null;
292                 }
293 
294                 @Override
295                 public void setObject(final AnyTypeTO object) {
296                     rel.setOtherEndType(Optional.ofNullable(object).map(AnyTypeTO::getKey).orElse(null));
297                 }
298             }, false);
299             otherType.setChoices(availableTypes);
300             otherType.setChoiceRenderer(new IChoiceRenderer<>() {
301 
302                 private static final long serialVersionUID = -734743540442190178L;
303 
304                 @Override
305                 public Object getDisplayValue(final AnyTypeTO object) {
306                     return object.getKey();
307                 }
308 
309                 @Override
310                 public String getIdValue(final AnyTypeTO object, final int index) {
311                     return object.getKey();
312                 }
313 
314                 @Override
315                 public AnyTypeTO getObject(final String id, final IModel<? extends List<? extends AnyTypeTO>> choices) {
316                     return choices.getObject().stream().
317                             filter(anyTypeTO -> id.equals(anyTypeTO.getKey())).findAny().orElse(null);
318                 }
319             });
320             // enable "otherType" dropdown only if "type" option is selected - SYNCOPE-1140
321             otherType.setEnabled(false);
322             add(otherType.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
323 
324             container = new WebMarkupContainer("searchPanelContainer");
325             add(container.setOutputMarkupId(true));
326 
327             emptyFragment = new Fragment("searchPanel", "emptyFragment", this);
328             container.add(emptyFragment.setRenderBodyOnly(true));
329 
330             fragment = new Fragment("searchPanel", "searchFragment", Specification.this);
331 
332             type.getField().add(new IndicatorAjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
333 
334                 private static final long serialVersionUID = -1107858522700306810L;
335 
336                 @Override
337                 protected void onUpdate(final AjaxRequestTarget target) {
338                     container.addOrReplace(emptyFragment.setRenderBodyOnly(true));
339                     otherType.setModelObject(null);
340                     // enable "otherType" dropdown only if "type" option is selected - SYNCOPE-1140
341                     otherType.setEnabled(type.getModelObject() != null && !type.getModelObject().isEmpty());
342                     target.add(otherType);
343                     target.add(container);
344                 }
345             });
346 
347             otherType.getField().add(new IndicatorAjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
348 
349                 private static final long serialVersionUID = -1107858522700306810L;
350 
351                 @Override
352                 protected void onUpdate(final AjaxRequestTarget target) {
353                     AnyTypeTO anyType = otherType.getModelObject();
354                     if (anyType == null) {
355                         container.addOrReplace(emptyFragment.setRenderBodyOnly(true));
356                     } else {
357                         setupFragment(anyType);
358                         container.addOrReplace(fragment.setRenderBodyOnly(true));
359                     }
360                     target.add(container);
361                 }
362             });
363         }
364 
365         protected void setupFragment(final AnyTypeTO anyType) {
366             anyObjectSearchPanel = new AnyObjectSearchPanel.Builder(
367                     anyType.getKey(),
368                     new ListModel<>(new ArrayList<>()),
369                     pageRef).
370                     enableSearch(Specification.this).
371                     build("searchPanel");
372             fragment.addOrReplace(anyObjectSearchPanel.setRenderBodyOnly(true));
373 
374             anyObjectDirectoryPanel = new AnyObjectSelectionDirectoryPanel.Builder(
375                     anyTypeClassRestClient.list(anyType.getClasses()),
376                     anyObjectRestClient,
377                     anyType.getKey(),
378                     pageRef).
379                     setFiql(SyncopeClient.getAnyObjectSearchConditionBuilder(anyType.getKey()).
380                             is(Constants.KEY_FIELD_NAME).notNullValue().query()).
381                     setWizardInModal(true).build("searchResultPanel");
382             fragment.addOrReplace(anyObjectDirectoryPanel.setRenderBodyOnly(true));
383         }
384 
385         @Override
386         public void onEvent(final IEvent<?> event) {
387             if (event.getPayload() instanceof SearchClausePanel.SearchEvent) {
388                 AjaxRequestTarget target =
389                         SearchClausePanel.SearchEvent.class.cast(event.getPayload()).getTarget();
390                 String fiql = SearchUtils.buildFIQL(anyObjectSearchPanel.getModel().getObject(),
391                         SyncopeClient.getAnyObjectSearchConditionBuilder(anyObjectSearchPanel.getBackObjectType()));
392                 AnyDirectoryPanel.class.cast(Specification.this.anyObjectDirectoryPanel).search(fiql, target);
393             } else if (event.getPayload() instanceof AnySelectionDirectoryPanel.ItemSelection) {
394                 AjaxRequestTarget target =
395                         AnySelectionDirectoryPanel.ItemSelection.class.cast(event.getPayload()).getTarget();
396 
397                 AnyTO right = AnySelectionDirectoryPanel.ItemSelection.class.cast(event.getPayload()).getSelection();
398                 rel.setOtherEndKey(right.getKey());
399 
400                 Relationships.this.addNewRelationships(rel);
401 
402                 Relationships.this.addOrReplace(getViewFragment().setRenderBodyOnly(true));
403                 target.add(Relationships.this);
404             } else {
405                 super.onEvent(event);
406             }
407         }
408     }
409 }