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.io.Serializable;
22  import java.util.ArrayList;
23  import java.util.Comparator;
24  import java.util.LinkedHashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Optional;
28  import java.util.stream.Collectors;
29  import org.apache.commons.collections4.CollectionUtils;
30  import org.apache.commons.collections4.ListUtils;
31  import org.apache.commons.lang3.StringUtils;
32  import org.apache.commons.lang3.time.DateFormatUtils;
33  import org.apache.commons.lang3.time.FastDateFormat;
34  import org.apache.syncope.client.console.SyncopeConsoleSession;
35  import org.apache.syncope.client.console.rest.AnyTypeClassRestClient;
36  import org.apache.syncope.client.console.rest.SchemaRestClient;
37  import org.apache.syncope.client.console.wicket.markup.html.form.BinaryFieldPanel;
38  import org.apache.syncope.client.console.wicket.markup.html.form.MultiFieldPanel;
39  import org.apache.syncope.client.ui.commons.SchemaUtils;
40  import org.apache.syncope.client.ui.commons.ajax.markup.html.LabelInfo;
41  import org.apache.syncope.client.ui.commons.markup.html.form.AbstractFieldPanel;
42  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxCheckBoxPanel;
43  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDateFieldPanel;
44  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDateTimeFieldPanel;
45  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
46  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxSpinnerFieldPanel;
47  import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
48  import org.apache.syncope.client.ui.commons.markup.html.form.EncryptedFieldPanel;
49  import org.apache.syncope.client.ui.commons.markup.html.form.FieldPanel;
50  import org.apache.syncope.client.ui.commons.wizards.AjaxWizard;
51  import org.apache.syncope.common.lib.Attr;
52  import org.apache.syncope.common.lib.SyncopeConstants;
53  import org.apache.syncope.common.lib.to.AnyTO;
54  import org.apache.syncope.common.lib.to.AnyTypeClassTO;
55  import org.apache.syncope.common.lib.to.PlainSchemaTO;
56  import org.apache.syncope.common.lib.to.SchemaTO;
57  import org.apache.syncope.common.lib.types.AttrSchemaType;
58  import org.apache.syncope.common.lib.types.SchemaType;
59  import org.apache.wicket.PageReference;
60  import org.apache.wicket.extensions.wizard.WizardModel.ICondition;
61  import org.apache.wicket.extensions.wizard.WizardStep;
62  import org.apache.wicket.markup.head.IHeaderResponse;
63  import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
64  import org.apache.wicket.markup.html.form.FormComponent;
65  import org.apache.wicket.markup.html.form.IChoiceRenderer;
66  import org.apache.wicket.markup.html.list.ListItem;
67  import org.apache.wicket.markup.html.panel.Panel;
68  import org.apache.wicket.model.IModel;
69  import org.apache.wicket.model.Model;
70  import org.apache.wicket.model.PropertyModel;
71  import org.apache.wicket.model.util.ListModel;
72  import org.apache.wicket.spring.injection.annot.SpringBean;
73  
74  public abstract class AbstractAttrsWizardStep<S extends SchemaTO> extends WizardStep implements ICondition {
75  
76      private static final long serialVersionUID = 8931397230194043674L;
77  
78      @SpringBean
79      protected AnyTypeClassRestClient anyTypeClassRestClient;
80  
81      @SpringBean
82      protected SchemaRestClient schemaRestClient;
83  
84      protected final Comparator<Attr> attrComparator = new AttrComparator();
85  
86      protected final AnyTO anyTO;
87  
88      protected AnyTO previousObject;
89  
90      protected final List<String> whichAttrs;
91  
92      protected final Map<String, S> schemas = new LinkedHashMap<>();
93  
94      protected final IModel<List<Attr>> attrs;
95  
96      protected final List<String> anyTypeClasses;
97  
98      protected String fileKey = "";
99  
100     protected final AjaxWizard.Mode mode;
101 
102     public AbstractAttrsWizardStep(
103             final AnyTO anyTO,
104             final AjaxWizard.Mode mode,
105             final List<String> anyTypeClasses,
106             final List<String> whichAttrs) {
107 
108         super();
109         this.anyTypeClasses = anyTypeClasses;
110         this.attrs = new ListModel<>(List.of());
111 
112         this.setOutputMarkupId(true);
113 
114         this.mode = mode;
115         this.anyTO = anyTO;
116         this.whichAttrs = whichAttrs;
117     }
118 
119     protected List<Attr> loadAttrs() {
120         List<String> classes = new ArrayList<>(anyTypeClasses);
121         classes.addAll(anyTypeClassRestClient.list(anyTO.getAuxClasses()).stream().
122                 map(AnyTypeClassTO::getKey).collect(Collectors.toList()));
123         setSchemas(classes);
124         setAttrs();
125         return getAttrsFromTO();
126     }
127 
128     protected boolean reoderSchemas() {
129         return !whichAttrs.isEmpty();
130     }
131 
132     protected abstract SchemaType getSchemaType();
133 
134     protected void setSchemas(final List<String> anyTypeClasses) {
135         setSchemas(anyTypeClasses, schemas);
136     }
137 
138     protected void setSchemas(final List<String> anyTypeClasses, final Map<String, S> scs) {
139         List<S> allSchemas = anyTypeClasses.isEmpty()
140                 ? List.of()
141                 : schemaRestClient.getSchemas(getSchemaType(), null, anyTypeClasses.toArray(String[]::new));
142 
143         scs.clear();
144 
145         if (!allSchemas.isEmpty() && reoderSchemas()) {
146             // remove attributes not selected for display
147             allSchemas.removeAll(allSchemas.stream().
148                     filter(schemaTO -> !whichAttrs.contains(schemaTO.getKey())).collect(Collectors.toSet()));
149         }
150 
151         allSchemas.forEach(schemaTO -> scs.put(schemaTO.getKey(), schemaTO));
152     }
153 
154     @Override
155     public void renderHead(final IHeaderResponse response) {
156         super.renderHead(response);
157         if (CollectionUtils.isEmpty(attrs.getObject())) {
158             response.render(OnDomReadyHeaderItem.forScript(
159                     String.format("$('#emptyPlaceholder').append(\"%s\"); $('#attributes').hide();",
160                             getString("attribute.empty.list"))));
161         }
162     }
163 
164     protected abstract void setAttrs();
165 
166     protected abstract List<Attr> getAttrsFromTO();
167 
168     @Override
169     public boolean evaluate() {
170         this.attrs.setObject(loadAttrs());
171         return !attrs.getObject().isEmpty();
172     }
173 
174     public PageReference getPageReference() {
175         // SYNCOPE-1213
176         // default implementation does not require to pass page reference, override this method of want otherwise
177         return null;
178     }
179 
180     @SuppressWarnings({ "rawtypes", "unchecked" })
181     protected FieldPanel getFieldPanel(final PlainSchemaTO plainSchema) {
182         final boolean required;
183         final boolean readOnly;
184         final AttrSchemaType type;
185         final boolean jexlHelp;
186 
187         if (mode == AjaxWizard.Mode.TEMPLATE) {
188             required = false;
189             readOnly = false;
190             type = AttrSchemaType.String;
191             jexlHelp = true;
192         } else {
193             required = plainSchema.getMandatoryCondition().equalsIgnoreCase("true");
194             readOnly = plainSchema.isReadonly();
195             type = plainSchema.getType();
196             jexlHelp = false;
197         }
198 
199         FieldPanel panel;
200         switch (type) {
201             case Boolean:
202                 panel = new AjaxCheckBoxPanel(
203                         "panel",
204                         plainSchema.getLabel(SyncopeConsoleSession.get().getLocale()),
205                         new Model<>(),
206                         true);
207                 panel.setRequired(required);
208                 break;
209 
210             case Date:
211                 String datePattern = plainSchema.getConversionPattern() == null
212                         ? DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.getPattern()
213                         : plainSchema.getConversionPattern();
214 
215                 if (StringUtils.containsIgnoreCase(datePattern, "H")) {
216                     panel = new AjaxDateTimeFieldPanel(
217                             "panel",
218                             plainSchema.getLabel(SyncopeConsoleSession.get().getLocale()),
219                             new Model<>(),
220                             FastDateFormat.getInstance(datePattern));
221                 } else {
222                     panel = new AjaxDateFieldPanel(
223                             "panel",
224                             plainSchema.getLabel(SyncopeConsoleSession.get().getLocale()),
225                             new Model<>(),
226                             FastDateFormat.getInstance(datePattern));
227                 }
228 
229                 if (required) {
230                     panel.addRequiredLabel();
231                 }
232 
233                 break;
234 
235             case Enum:
236                 panel = new AjaxDropDownChoicePanel<>("panel",
237                         plainSchema.getLabel(SyncopeConsoleSession.get().getLocale()), new Model<>(), true);
238                 ((AjaxDropDownChoicePanel<String>) panel).setChoices(SchemaUtils.getEnumeratedValues(plainSchema));
239 
240                 if (StringUtils.isNotBlank(plainSchema.getEnumerationKeys())) {
241                     ((AjaxDropDownChoicePanel) panel).setChoiceRenderer(new IChoiceRenderer<String>() {
242 
243                         private static final long serialVersionUID = -3724971416312135885L;
244 
245                         private final Map<String, String> valueMap = SchemaUtils.getEnumeratedKeyValues(plainSchema);
246 
247                         @Override
248                         public String getDisplayValue(final String value) {
249                             return valueMap.get(value) == null ? value : valueMap.get(value);
250                         }
251 
252                         @Override
253                         public String getIdValue(final String value, final int i) {
254                             return value;
255                         }
256 
257                         @Override
258                         public String getObject(
259                                 final String id, final IModel<? extends List<? extends String>> choices) {
260                             return id;
261                         }
262                     });
263                 }
264 
265                 if (required) {
266                     panel.addRequiredLabel();
267                 }
268                 break;
269 
270             case Long:
271                 panel = new AjaxSpinnerFieldPanel.Builder<Long>().enableOnChange().build(
272                         "panel",
273                         plainSchema.getLabel(SyncopeConsoleSession.get().getLocale()),
274                         Long.class,
275                         new Model<>());
276 
277                 if (required) {
278                     panel.addRequiredLabel();
279                 }
280                 break;
281 
282             case Double:
283                 panel = new AjaxSpinnerFieldPanel.Builder<Double>().enableOnChange().step(0.1).build(
284                         "panel",
285                         plainSchema.getLabel(SyncopeConsoleSession.get().getLocale()),
286                         Double.class,
287                         new Model<>());
288 
289                 if (required) {
290                     panel.addRequiredLabel();
291                 }
292                 break;
293 
294             case Binary:
295                 PageReference pageRef = getPageReference();
296                 panel = new BinaryFieldPanel(
297                         "panel",
298                         plainSchema.getLabel(SyncopeConsoleSession.get().getLocale()),
299                         new Model<>(),
300                         plainSchema.getMimeType(),
301                         fileKey) {
302 
303                     private static final long serialVersionUID = -3268213909514986831L;
304 
305                     @Override
306                     protected PageReference getPageReference() {
307                         return pageRef;
308                     }
309                 };
310                 if (required) {
311                     panel.addRequiredLabel();
312                 }
313                 break;
314 
315             case Encrypted:
316                 panel = SyncopeConstants.ENCRYPTED_DECODE_CONVERSION_PATTERN.equals(plainSchema.getConversionPattern())
317                         ? new AjaxTextFieldPanel("panel",
318                                 plainSchema.getLabel(SyncopeConsoleSession.get().getLocale()), new Model<>(), true)
319                         : new EncryptedFieldPanel("panel",
320                                 plainSchema.getLabel(SyncopeConsoleSession.get().getLocale()), new Model<>(), true);
321 
322                 if (required) {
323                     panel.addRequiredLabel();
324                 }
325                 break;
326 
327             default:
328                 panel = new AjaxTextFieldPanel("panel",
329                         plainSchema.getLabel(SyncopeConsoleSession.get().getLocale()), new Model<>(), true);
330 
331                 if (jexlHelp) {
332                     AjaxTextFieldPanel.class.cast(panel).enableJexlHelp();
333                 }
334 
335                 if (required) {
336                     panel.addRequiredLabel();
337                 }
338         }
339 
340         panel.setReadOnly(readOnly);
341 
342         return panel;
343     }
344 
345     protected FormComponent<?> checkboxToggle(
346             final Attr attr,
347             final AbstractFieldPanel<?> panel,
348             final boolean isMultivalue) {
349 
350         // do nothing
351         return null;
352     }
353 
354     private class AttrComparator implements Comparator<Attr>, Serializable {
355 
356         private static final long serialVersionUID = -5105030477767941060L;
357 
358         @Override
359         public int compare(final Attr left, final Attr right) {
360             if (left == null || StringUtils.isEmpty(left.getSchema())) {
361                 return -1;
362             }
363             if (right == null || StringUtils.isEmpty(right.getSchema())) {
364                 return 1;
365             } else if (AbstractAttrsWizardStep.this.reoderSchemas()) {
366                 int leftIndex = AbstractAttrsWizardStep.this.whichAttrs.indexOf(left.getSchema());
367                 int rightIndex = AbstractAttrsWizardStep.this.whichAttrs.indexOf(right.getSchema());
368 
369                 if (leftIndex > rightIndex) {
370                     return 1;
371                 } else if (leftIndex < rightIndex) {
372                     return -1;
373                 } else {
374                     return 0;
375                 }
376             } else {
377                 return left.getSchema().compareTo(right.getSchema());
378             }
379         }
380     }
381 
382     public static class Schemas extends Panel {
383 
384         private static final long serialVersionUID = -2447602429647965090L;
385 
386         public Schemas(final String id) {
387             super(id);
388         }
389     }
390 
391     protected abstract class PlainSchemas<T> extends Schemas {
392 
393         private static final long serialVersionUID = 8315035592714180404L;
394 
395         public PlainSchemas(final String id) {
396             super(id);
397         }
398 
399         @SuppressWarnings({ "unchecked", "rawtypes" })
400         protected AbstractFieldPanel<?> setPanel(
401                 final Map<String, PlainSchemaTO> schemas,
402                 final ListItem<Attr> item,
403                 final boolean setReadOnly) {
404 
405             Attr attr = item.getModelObject();
406             final boolean isMultivalue = mode != AjaxWizard.Mode.TEMPLATE
407                     && schemas.get(attr.getSchema()).isMultivalue();
408 
409             AbstractFieldPanel<?> panel = getFieldPanel(schemas.get(attr.getSchema()));
410             if (isMultivalue) {
411                 // SYNCOPE-1476 set form as multipart to properly manage membership attributes
412                 panel = new MultiFieldPanel.Builder<>(
413                         new PropertyModel<>(attr, "values")).build(
414                         "panel",
415                         attr.getSchema(),
416                         FieldPanel.class.cast(panel)).setFormAsMultipart(true);
417                 // SYNCOPE-1215 the entire multifield panel must be readonly, not only its field
418                 MultiFieldPanel.class.cast(panel).setReadOnly(schemas.get(attr.getSchema()).isReadonly());
419                 MultiFieldPanel.class.cast(panel).setFormReadOnly(setReadOnly);
420             } else {
421                 FieldPanel.class.cast(panel).setNewModel(attr.getValues()).setReadOnly(setReadOnly);
422             }
423             item.add(panel);
424 
425             setExternalAction(attr, panel);
426 
427             return panel;
428         }
429 
430         protected void setExternalAction(final Attr attr, final AbstractFieldPanel<?> panel) {
431             if (previousObject == null) {
432                 return;
433             }
434 
435             Optional<Attr> prevAttr = previousObject.getPlainAttr(attr.getSchema());
436             if (prevAttr.map(a -> !ListUtils.isEqualList(
437                     a.getValues().stream().filter(StringUtils::isNotBlank).collect(Collectors.toList()),
438                     attr.getValues().stream().filter(StringUtils::isNotBlank).collect(Collectors.toList()))).
439                     orElseGet(() -> attr.getValues().stream().anyMatch(StringUtils::isNotBlank))) {
440 
441                 List<String> oldValues = prevAttr.map(Attr::getValues).orElse(List.of());
442                 panel.showExternAction(new LabelInfo("externalAction", oldValues));
443             }
444         }
445     }
446 }