1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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 }