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.panels;
20  
21  import de.agilecoders.wicket.core.markup.html.bootstrap.components.PopoverBehavior;
22  import de.agilecoders.wicket.core.markup.html.bootstrap.components.PopoverConfig;
23  import de.agilecoders.wicket.core.markup.html.bootstrap.components.TooltipConfig;
24  import io.swagger.v3.oas.annotations.media.Schema;
25  import java.io.Serializable;
26  import java.lang.reflect.Field;
27  import java.lang.reflect.ParameterizedType;
28  import java.time.Duration;
29  import java.time.OffsetDateTime;
30  import java.time.ZonedDateTime;
31  import java.util.ArrayList;
32  import java.util.Date;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Optional;
36  import java.util.stream.Collectors;
37  import org.apache.commons.lang3.time.DateFormatUtils;
38  import org.apache.commons.lang3.tuple.Pair;
39  import org.apache.commons.lang3.tuple.Triple;
40  import org.apache.syncope.client.console.SyncopeConsoleSession;
41  import org.apache.syncope.client.console.SyncopeWebApplication;
42  import org.apache.syncope.client.console.panels.search.AnyObjectSearchPanel;
43  import org.apache.syncope.client.console.panels.search.GroupSearchPanel;
44  import org.apache.syncope.client.console.panels.search.SearchClause;
45  import org.apache.syncope.client.console.panels.search.SearchUtils;
46  import org.apache.syncope.client.console.panels.search.UserSearchPanel;
47  import org.apache.syncope.client.console.rest.SchemaRestClient;
48  import org.apache.syncope.client.console.wicket.markup.html.form.MultiFieldPanel;
49  import org.apache.syncope.client.lib.SyncopeClient;
50  import org.apache.syncope.client.ui.commons.DateOps;
51  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxCheckBoxPanel;
52  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDateTimeFieldPanel;
53  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
54  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxGridFieldPanel;
55  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxPalettePanel;
56  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxSpinnerFieldPanel;
57  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
58  import org.apache.syncope.client.ui.commons.markup.html.form.FieldPanel;
59  import org.apache.syncope.common.lib.report.SearchCondition;
60  import org.apache.syncope.common.lib.search.AbstractFiqlSearchConditionBuilder;
61  import org.apache.syncope.common.lib.to.SchemaTO;
62  import org.apache.syncope.common.lib.types.SchemaType;
63  import org.apache.wicket.PageReference;
64  import org.apache.wicket.core.util.lang.PropertyResolver;
65  import org.apache.wicket.core.util.lang.PropertyResolverConverter;
66  import org.apache.wicket.markup.html.basic.Label;
67  import org.apache.wicket.markup.html.list.ListItem;
68  import org.apache.wicket.markup.html.list.ListView;
69  import org.apache.wicket.markup.html.panel.Fragment;
70  import org.apache.wicket.markup.html.panel.Panel;
71  import org.apache.wicket.model.IModel;
72  import org.apache.wicket.model.LoadableDetachableModel;
73  import org.apache.wicket.model.Model;
74  import org.apache.wicket.model.PropertyModel;
75  import org.apache.wicket.model.ResourceModel;
76  import org.apache.wicket.model.util.ListModel;
77  import org.apache.wicket.spring.injection.annot.SpringBean;
78  import org.slf4j.Logger;
79  import org.slf4j.LoggerFactory;
80  import org.springframework.beans.BeanWrapper;
81  import org.springframework.beans.PropertyAccessorFactory;
82  import org.springframework.util.ClassUtils;
83  import org.springframework.util.ReflectionUtils;
84  
85  public class BeanPanel<T extends Serializable> extends Panel {
86  
87      private static final long serialVersionUID = 3905038169553185171L;
88  
89      protected static final Logger LOG = LoggerFactory.getLogger(BeanPanel.class);
90  
91      @SpringBean
92      protected SchemaRestClient schemaRestClient;
93  
94      protected final List<String> excluded;
95  
96      protected final Map<String, Pair<AbstractFiqlSearchConditionBuilder<?, ?, ?>, List<SearchClause>>> sCondWrapper;
97  
98      public BeanPanel(final String id, final IModel<T> bean, final PageReference pageRef, final String... excluded) {
99          this(id, bean, null, pageRef, excluded);
100     }
101 
102     public BeanPanel(
103             final String id,
104             final IModel<T> bean,
105             final Map<String, Pair<AbstractFiqlSearchConditionBuilder<?, ?, ?>, List<SearchClause>>> sCondWrapper,
106             final PageReference pageRef,
107             final String... excluded) {
108 
109         super(id, bean);
110         setOutputMarkupId(true);
111 
112         this.sCondWrapper = sCondWrapper;
113 
114         this.excluded = new ArrayList<>(List.of(excluded));
115         this.excluded.add("serialVersionUID");
116         this.excluded.add("class");
117 
118         LoadableDetachableModel<List<String>> model = new LoadableDetachableModel<>() {
119 
120             private static final long serialVersionUID = 5275935387613157437L;
121 
122             @Override
123             protected List<String> load() {
124                 List<String> result = new ArrayList<>();
125 
126                 if (BeanPanel.this.getDefaultModelObject() != null) {
127                     ReflectionUtils.doWithFields(BeanPanel.this.getDefaultModelObject().getClass(),
128                             field -> result.add(field.getName()),
129                             field -> !field.isSynthetic() && !BeanPanel.this.excluded.contains(field.getName()));
130                 }
131 
132                 return result;
133             }
134         };
135 
136         add(new ListView<>("propView", model) {
137 
138             private static final long serialVersionUID = 9101744072914090143L;
139 
140             private void setRequired(final ListItem<String> item, final boolean required) {
141                 if (required) {
142                     Fragment fragment = new Fragment("required", "requiredFragment", this);
143                     fragment.add(new Label("requiredLabel", "*"));
144                     item.replace(fragment);
145                 }
146             }
147 
148             private void setDescription(final ListItem<String> item, final String description) {
149                 Fragment fragment = new Fragment("description", "descriptionFragment", this);
150                 fragment.add(new Label("descriptionLabel", Model.of()).add(new PopoverBehavior(
151                         Model.<String>of(),
152                         Model.of(description),
153                         new PopoverConfig().withPlacement(TooltipConfig.Placement.right)) {
154 
155                     private static final long serialVersionUID = -7867802555691605021L;
156 
157                     @Override
158                     protected String createRelAttribute() {
159                         return "description";
160                     }
161                 }).setRenderBodyOnly(false));
162                 item.replace(fragment);
163             }
164 
165             @SuppressWarnings({ "unchecked", "rawtypes" })
166             @Override
167             protected void populateItem(final ListItem<String> item) {
168                 item.add(new Fragment("required", "emptyFragment", this));
169                 item.add(new Fragment("description", "emptyFragment", this));
170 
171                 String fieldName = item.getModelObject();
172 
173                 item.add(new Label("fieldName", new ResourceModel(fieldName, fieldName)));
174 
175                 Field field = ReflectionUtils.findField(bean.getObject().getClass(), fieldName);
176                 if (field == null) {
177                     return;
178                 }
179 
180                 Panel panel;
181 
182                 SearchCondition scondAnnot = field.getAnnotation(SearchCondition.class);
183                 if (scondAnnot != null) {
184                     BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean.getObject());
185                     String fiql = (String) wrapper.getPropertyValue(fieldName);
186 
187                     List<SearchClause> clauses = Optional.ofNullable(fiql).
188                             map(f -> SearchUtils.getSearchClauses(f.replaceAll(
189                             SearchUtils.getTypeConditionPattern(scondAnnot.type()).pattern(), ""))).
190                             orElse(new ArrayList<>());
191 
192                     AbstractFiqlSearchConditionBuilder<?, ?, ?> builder;
193                     switch (scondAnnot.type()) {
194                         case "USER":
195                             panel = new UserSearchPanel.Builder(
196                                     new ListModel<>(clauses), pageRef).required(false).build("value");
197                             builder = SyncopeClient.getUserSearchConditionBuilder();
198                             break;
199 
200                         case "GROUP":
201                             panel = new GroupSearchPanel.Builder(
202                                     new ListModel<>(clauses), pageRef).required(false).build("value");
203                             builder = SyncopeClient.getGroupSearchConditionBuilder();
204                             break;
205 
206                         default:
207                             panel = new AnyObjectSearchPanel.Builder(
208                                     scondAnnot.type(),
209                                     new ListModel<>(clauses), pageRef).required(false).build("value");
210                             builder = SyncopeClient.getAnyObjectSearchConditionBuilder(scondAnnot.type());
211                     }
212 
213                     Optional.ofNullable(BeanPanel.this.sCondWrapper).
214                             ifPresent(scw -> scw.put(fieldName, Pair.of(builder, clauses)));
215                 } else if (List.class.equals(field.getType())) {
216                     Class<?> listItemType = field.getGenericType() instanceof ParameterizedType
217                             ? (Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]
218                             : String.class;
219 
220                     org.apache.syncope.common.lib.Schema schema =
221                             field.getAnnotation(org.apache.syncope.common.lib.Schema.class);
222                     if (listItemType.equals(String.class) && schema != null) {
223                         List<SchemaTO> choices = new ArrayList<>();
224 
225                         for (SchemaType type : schema.type()) {
226                             switch (type) {
227                                 case PLAIN:
228                                     choices.addAll(
229                                             schemaRestClient.getSchemas(SchemaType.PLAIN, schema.anyTypeKind()));
230                                     break;
231 
232                                 case DERIVED:
233                                     choices.addAll(
234                                             schemaRestClient.getSchemas(SchemaType.DERIVED, schema.anyTypeKind()));
235                                     break;
236 
237                                 case VIRTUAL:
238                                     choices.addAll(
239                                             schemaRestClient.getSchemas(SchemaType.VIRTUAL, schema.anyTypeKind()));
240                                     break;
241 
242                                 default:
243                             }
244                         }
245 
246                         panel = new AjaxPalettePanel.Builder<>().setName(fieldName).build(
247                                 "value",
248                                 new PropertyModel<>(bean.getObject(), fieldName),
249                                 new ListModel<>(choices.stream().map(SchemaTO::getKey).collect(Collectors.toList()))).
250                                 hideLabel();
251                     } else if (listItemType.isEnum()) {
252                         panel = new AjaxPalettePanel.Builder<>().setName(fieldName).build(
253                                 "value",
254                                 new PropertyModel<>(bean.getObject(), fieldName),
255                                 new ListModel(List.of(listItemType.getEnumConstants()))).hideLabel();
256                     } else {
257                         Triple<FieldPanel, Boolean, Optional<String>> single =
258                                 buildSinglePanel(bean.getObject(), field.getType(), field.getName(),
259                                         field.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class), "panel");
260 
261                         setRequired(item, single.getMiddle());
262                         single.getRight().ifPresent(description -> setDescription(item, description));
263 
264                         panel = new MultiFieldPanel.Builder<>(
265                                 new PropertyModel<>(bean.getObject(), fieldName)).build(
266                                 "value",
267                                 fieldName,
268                                 single.getLeft()).hideLabel();
269                     }
270                 } else if (Map.class.equals(field.getType())) {
271                     panel = new AjaxGridFieldPanel(
272                             "value", fieldName, new PropertyModel<>(bean, fieldName)).hideLabel();
273                     Optional.ofNullable(field.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class)).
274                             ifPresent(annot -> setDescription(item, annot.description()));
275                 } else {
276                     Triple<FieldPanel, Boolean, Optional<String>> single =
277                             buildSinglePanel(bean.getObject(), field.getType(), field.getName(),
278                                     field.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class), "value");
279 
280                     setRequired(item, single.getMiddle());
281                     single.getRight().ifPresent(description -> setDescription(item, description));
282 
283                     panel = single.getLeft().hideLabel();
284                 }
285 
286                 item.add(panel.setRenderBodyOnly(true));
287             }
288         }.setReuseItems(false));
289     }
290 
291     @SuppressWarnings({ "unchecked", "rawtypes" })
292     private Triple<FieldPanel, Boolean, Optional<String>> buildSinglePanel(
293             final Serializable bean, final Class<?> type, final String fieldName,
294             final io.swagger.v3.oas.annotations.media.Schema schema, final String id) {
295 
296         PropertyModel model = new PropertyModel<>(bean, fieldName);
297 
298         FieldPanel panel;
299         if (ClassUtils.isAssignable(Boolean.class, type)) {
300             panel = new AjaxCheckBoxPanel(id, fieldName, model);
301         } else if (ClassUtils.isAssignable(Number.class, type)) {
302             panel = new AjaxSpinnerFieldPanel.Builder<>().build(
303                     id, fieldName, (Class<Number>) ClassUtils.resolvePrimitiveIfNecessary(type), model);
304         } else if (Date.class.equals(type)) {
305             panel = new AjaxDateTimeFieldPanel(id, fieldName, model,
306                     DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
307         } else if (OffsetDateTime.class.equals(type)) {
308             panel = new AjaxDateTimeFieldPanel(id, fieldName, DateOps.WrappedDateModel.ofOffset(model),
309                     DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
310         } else if (ZonedDateTime.class.equals(type)) {
311             panel = new AjaxDateTimeFieldPanel(id, fieldName, DateOps.WrappedDateModel.ofZoned(model),
312                     DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
313         } else if (type.isEnum()) {
314             panel = new AjaxDropDownChoicePanel(id, fieldName, model).
315                     setChoices(List.of(type.getEnumConstants()));
316         } else if (Duration.class.equals(type)) {
317             panel = new AjaxTextFieldPanel(id, fieldName, new IModel<>() {
318 
319                 private static final long serialVersionUID = 807008909842554829L;
320 
321                 @Override
322                 public String getObject() {
323                     return Optional.ofNullable(PropertyResolver.getValue(fieldName, bean)).
324                             map(Object::toString).orElse(null);
325                 }
326 
327                 @Override
328                 public void setObject(final String object) {
329                     PropertyResolverConverter prc = new PropertyResolverConverter(
330                             SyncopeWebApplication.get().getConverterLocator(),
331                             SyncopeConsoleSession.get().getLocale());
332                     PropertyResolver.setValue(fieldName, bean, Duration.parse(object), prc);
333                 }
334             });
335         } else {
336             // treat as String if nothing matched above
337             panel = new AjaxTextFieldPanel(id, fieldName, model);
338         }
339 
340         boolean required = false;
341         Optional<String> description = Optional.empty();
342 
343         if (schema != null) {
344             panel.setReadOnly(schema.accessMode() == Schema.AccessMode.READ_ONLY);
345 
346             required = schema.requiredMode() == Schema.RequiredMode.REQUIRED;
347             panel.setRequired(required);
348 
349             Optional.ofNullable(schema.example()).ifPresent(panel::setPlaceholder);
350 
351             description = Optional.ofNullable(schema.description());
352 
353             if (panel instanceof AjaxTextFieldPanel
354                     && panel.getModelObject() == null
355                     && schema.defaultValue() != null) {
356 
357                 ((AjaxTextFieldPanel) panel).setModelObject(schema.defaultValue());
358             }
359         }
360 
361         return Triple.of(panel, required, description);
362     }
363 }