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.time.Duration;
22  import java.time.temporal.ChronoUnit;
23  import java.util.List;
24  import java.util.Objects;
25  import java.util.Optional;
26  import java.util.concurrent.atomic.AtomicReference;
27  import java.util.stream.Collectors;
28  import org.apache.commons.lang3.StringUtils;
29  import org.apache.commons.lang3.tuple.Pair;
30  import org.apache.syncope.client.console.rest.ResourceRestClient;
31  import org.apache.syncope.client.lib.SyncopeClient;
32  import org.apache.syncope.client.ui.commons.ConnIdSpecialName;
33  import org.apache.syncope.client.ui.commons.Constants;
34  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
35  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
36  import org.apache.syncope.common.lib.to.ConnObject;
37  import org.apache.syncope.common.lib.to.LinkedAccountTO;
38  import org.apache.syncope.common.lib.to.ResourceTO;
39  import org.apache.syncope.common.lib.types.AnyTypeKind;
40  import org.apache.syncope.common.rest.api.beans.ConnObjectTOQuery;
41  import org.apache.wicket.Component;
42  import org.apache.wicket.ajax.AjaxEventBehavior;
43  import org.apache.wicket.ajax.AjaxRequestTarget;
44  import org.apache.wicket.ajax.attributes.AjaxCallListener;
45  import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
46  import org.apache.wicket.ajax.attributes.ThrottlingSettings;
47  import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
48  import org.apache.wicket.extensions.markup.html.repeater.util.SortParam;
49  import org.apache.wicket.extensions.wizard.WizardStep;
50  import org.apache.wicket.model.PropertyModel;
51  import org.apache.wicket.spring.injection.annot.SpringBean;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  
55  public class LinkedAccountDetailsPanel extends WizardStep {
56  
57      private static final long serialVersionUID = 1221037007528732347L;
58  
59      @SpringBean
60      protected ResourceRestClient resourceRestClient;
61  
62      protected static final Logger LOG = LoggerFactory.getLogger(LinkedAccountDetailsPanel.class);
63  
64      protected static final int SEARCH_SIZE = 20;
65  
66      protected List<String> connObjectKeyFieldValues;
67  
68      public LinkedAccountDetailsPanel(final LinkedAccountTO linkedAccountTO) {
69          super();
70          setOutputMarkupId(true);
71  
72          AjaxDropDownChoicePanel<String> dropdownResourceField = new AjaxDropDownChoicePanel<>(
73                  "resource",
74                  "resource",
75                  new PropertyModel<>(linkedAccountTO, "resource"),
76                  false);
77  
78          dropdownResourceField.setChoices(resourceRestClient.list().stream().
79                  filter(resource -> resource.getProvision(AnyTypeKind.USER.name()).
80                  flatMap(provision -> Optional.ofNullable(provision.getMapping())).
81                  filter(mapping -> !mapping.getItems().isEmpty()).isPresent()).
82                  map(ResourceTO::getKey).
83                  collect(Collectors.toList()));
84          add(dropdownResourceField.setNullValid(false).addRequiredLabel().setOutputMarkupId(true));
85  
86          String connObjectKeyFieldId = "connObjectKeyValue";
87          AjaxTextFieldPanel connObjectKeyField = new AjaxTextFieldPanel(
88                  "connObjectKeyValue",
89                  "connObjectKeyValue",
90                  new PropertyModel<>(linkedAccountTO, "connObjectKeyValue"),
91                  false);
92          connObjectKeyField.setOutputMarkupId(true);
93          connObjectKeyField.addRequiredLabel();
94          connObjectKeyField.setChoices(List.of());
95          connObjectKeyField.setEnabled(false);
96          add(connObjectKeyField);
97  
98          dropdownResourceField.getField().add(new AjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
99  
100             private static final long serialVersionUID = -1107858522700306810L;
101 
102             @Override
103             protected void onUpdate(final AjaxRequestTarget target) {
104                 boolean enabled = dropdownResourceField.getModelObject() != null
105                         && !dropdownResourceField.getModelObject().isEmpty();
106                 connObjectKeyField.setEnabled(enabled);
107                 if (enabled) {
108                     setConnObjectFieldChoices(connObjectKeyField, dropdownResourceField.getModelObject(), null);
109                 }
110                 target.add(connObjectKeyField);
111             }
112         });
113 
114         connObjectKeyField.getField().setMarkupId(connObjectKeyFieldId);
115         connObjectKeyField.getField().add(new AjaxEventBehavior(Constants.ON_KEYDOWN) {
116 
117             private static final long serialVersionUID = 3533589614190959822L;
118 
119             @Override
120             protected void onEvent(final AjaxRequestTarget target) {
121                 String searchTerm = connObjectKeyField.getField().getInput();
122                 if (StringUtils.isNotBlank(searchTerm) && searchTerm.length() > 1) {
123                     setConnObjectFieldChoices(connObjectKeyField, dropdownResourceField.getModelObject(), searchTerm);
124 
125                     // If elements are found, send an "arrow down" key event to open input autocomplete dropdown
126                     target.appendJavaScript(connObjectKeyFieldValues.isEmpty()
127                             ? "$('#" + connObjectKeyFieldId + "-autocomplete-container').hide();"
128                             : "var simulatedEvent = new KeyboardEvent('keydown', {keyCode: 40, which: 40}); "
129                             + "document.getElementById('" + connObjectKeyFieldId + "').dispatchEvent(simulatedEvent);");
130                 }
131             }
132 
133             @Override
134             protected void updateAjaxAttributes(final AjaxRequestAttributes attributes) {
135                 super.updateAjaxAttributes(attributes);
136 
137                 AjaxCallListener listener = new AjaxCallListener() {
138 
139                     private static final long serialVersionUID = 2208168001920794667L;
140 
141                     @Override
142                     public CharSequence getPrecondition(final Component component) {
143                         // Eevaluates weather an ajax call will be executed or not.
144                         // If the key code is "arrow down" or "arrow up" do NOT trigger the event callback
145                         return "var keycode = Wicket.Event.keyCode(attrs.event); "
146                                 + "if ((keycode == 40) || (keycode == 38)) {return false;} return true;";
147                     }
148                 };
149                 attributes.getAjaxCallListeners().add(listener);
150                 attributes.setThrottlingSettings(
151                         new ThrottlingSettings("id", Duration.of(1, ChronoUnit.SECONDS), true));
152             }
153         });
154     }
155 
156     private void setConnObjectFieldChoices(
157             final AjaxTextFieldPanel ajaxTextFieldPanel,
158             final String resource,
159             final String searchTerm) {
160 
161         AtomicReference<String> resourceRemoteKey = new AtomicReference<>(ConnIdSpecialName.NAME);
162         try {
163             resourceRestClient.read(resource).getProvision(AnyTypeKind.USER.name()).
164                     flatMap(provision -> Optional.ofNullable(provision.getMapping())).
165                     flatMap(mapping -> mapping.getConnObjectKeyItem()).
166                     ifPresent(connObjectKeyItem -> resourceRemoteKey.set(connObjectKeyItem.getExtAttrName()));
167         } catch (Exception ex) {
168             LOG.error("While reading mapping for resource {}", resource, ex);
169         }
170 
171         ConnObjectTOQuery.Builder builder = new ConnObjectTOQuery.Builder().size(SEARCH_SIZE);
172         if (StringUtils.isNotBlank(searchTerm)) {
173             builder.fiql(SyncopeClient.getConnObjectTOFiqlSearchConditionBuilder().
174                     is(resourceRemoteKey.get()).equalTo(searchTerm + "*").query()).build();
175         }
176         Pair<String, List<ConnObject>> items = resourceRestClient.searchConnObjects(
177                 resource,
178                 AnyTypeKind.USER.name(),
179                 builder,
180                 new SortParam<>(resourceRemoteKey.get(), true));
181 
182         connObjectKeyFieldValues = items.getRight().stream().
183                 map(item -> item.getAttr(resourceRemoteKey.get()).map(attr -> attr.getValues().get(0)).orElse(null)).
184                 filter(Objects::nonNull).
185                 collect(Collectors.toList());
186         ajaxTextFieldPanel.setChoices(connObjectKeyFieldValues);
187     }
188 }