blob: 9960f1f64c7568fb27b1fd9d286ff421369e0609 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.syncope.client.console.panels;
import de.agilecoders.wicket.core.markup.html.bootstrap.components.PopoverBehavior;
import de.agilecoders.wicket.core.markup.html.bootstrap.components.PopoverConfig;
import de.agilecoders.wicket.core.markup.html.bootstrap.components.TooltipConfig;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.syncope.client.console.SyncopeConsoleSession;
import org.apache.syncope.client.console.SyncopeWebApplication;
import org.apache.syncope.client.console.panels.search.AnyObjectSearchPanel;
import org.apache.syncope.client.console.panels.search.GroupSearchPanel;
import org.apache.syncope.client.console.panels.search.SearchClause;
import org.apache.syncope.client.console.panels.search.SearchUtils;
import org.apache.syncope.client.console.panels.search.UserSearchPanel;
import org.apache.syncope.client.console.rest.SchemaRestClient;
import org.apache.syncope.client.console.wicket.markup.html.form.MultiFieldPanel;
import org.apache.syncope.client.lib.SyncopeClient;
import org.apache.syncope.client.ui.commons.DateOps;
import org.apache.syncope.client.ui.commons.markup.html.form.AjaxCheckBoxPanel;
import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDateTimeFieldPanel;
import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
import org.apache.syncope.client.ui.commons.markup.html.form.AjaxGridFieldPanel;
import org.apache.syncope.client.ui.commons.markup.html.form.AjaxPalettePanel;
import org.apache.syncope.client.ui.commons.markup.html.form.AjaxSpinnerFieldPanel;
import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
import org.apache.syncope.client.ui.commons.markup.html.form.FieldPanel;
import org.apache.syncope.common.lib.report.SearchCondition;
import org.apache.syncope.common.lib.search.AbstractFiqlSearchConditionBuilder;
import org.apache.syncope.common.lib.to.SchemaTO;
import org.apache.syncope.common.lib.types.SchemaType;
import org.apache.wicket.PageReference;
import org.apache.wicket.core.util.lang.PropertyResolver;
import org.apache.wicket.core.util.lang.PropertyResolverConverter;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.model.ResourceModel;
import org.apache.wicket.model.util.ListModel;
import org.apache.wicket.spring.injection.annot.SpringBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
public class BeanPanel<T extends Serializable> extends Panel {
private static final long serialVersionUID = 3905038169553185171L;
protected static final Logger LOG = LoggerFactory.getLogger(BeanPanel.class);
@SpringBean
protected SchemaRestClient schemaRestClient;
protected final List<String> excluded;
protected final Map<String, Pair<AbstractFiqlSearchConditionBuilder<?, ?, ?>, List<SearchClause>>> sCondWrapper;
public BeanPanel(final String id, final IModel<T> bean, final PageReference pageRef, final String... excluded) {
this(id, bean, null, pageRef, excluded);
}
public BeanPanel(
final String id,
final IModel<T> bean,
final Map<String, Pair<AbstractFiqlSearchConditionBuilder<?, ?, ?>, List<SearchClause>>> sCondWrapper,
final PageReference pageRef,
final String... excluded) {
super(id, bean);
setOutputMarkupId(true);
this.sCondWrapper = sCondWrapper;
this.excluded = new ArrayList<>(List.of(excluded));
this.excluded.add("serialVersionUID");
this.excluded.add("class");
LoadableDetachableModel<List<Field>> model = new LoadableDetachableModel<>() {
private static final long serialVersionUID = 5275935387613157437L;
@Override
protected List<Field> load() {
List<Field> result = new ArrayList<>();
if (BeanPanel.this.getDefaultModelObject() != null) {
ReflectionUtils.doWithFields(
BeanPanel.this.getDefaultModelObject().getClass(),
result::add,
field -> !field.isSynthetic() && !BeanPanel.this.excluded.contains(field.getName()));
}
return result;
}
};
add(new ListView<>("propView", model) {
private static final long serialVersionUID = 9101744072914090143L;
private void setRequired(final ListItem<Field> item, final boolean required) {
if (required) {
Fragment fragment = new Fragment("required", "requiredFragment", this);
fragment.add(new Label("requiredLabel", "*"));
item.replace(fragment);
}
}
private void setDescription(final ListItem<Field> item, final String description) {
Fragment fragment = new Fragment("description", "descriptionFragment", this);
fragment.add(new Label("descriptionLabel", Model.of()).add(new PopoverBehavior(
Model.<String>of(),
Model.of(description),
new PopoverConfig().withPlacement(TooltipConfig.Placement.right)) {
private static final long serialVersionUID = -7867802555691605021L;
@Override
protected String createRelAttribute() {
return "description";
}
}).setRenderBodyOnly(false));
item.replace(fragment);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
protected void populateItem(final ListItem<Field> item) {
item.add(new Fragment("required", "emptyFragment", this));
item.add(new Fragment("description", "emptyFragment", this));
Field field = item.getModelObject();
item.add(new Label("fieldName", new ResourceModel(field.getName(), field.getName())));
Panel panel;
SearchCondition scondAnnot = field.getAnnotation(SearchCondition.class);
if (scondAnnot != null) {
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean.getObject());
String fiql = (String) wrapper.getPropertyValue(field.getName());
List<SearchClause> clauses = Optional.ofNullable(fiql).
map(f -> SearchUtils.getSearchClauses(f.replaceAll(
SearchUtils.getTypeConditionPattern(scondAnnot.type()).pattern(), ""))).
orElse(new ArrayList<>());
AbstractFiqlSearchConditionBuilder<?, ?, ?> builder;
switch (scondAnnot.type()) {
case "USER":
panel = new UserSearchPanel.Builder(
new ListModel<>(clauses), pageRef).required(false).build("value");
builder = SyncopeClient.getUserSearchConditionBuilder();
break;
case "GROUP":
panel = new GroupSearchPanel.Builder(
new ListModel<>(clauses), pageRef).required(false).build("value");
builder = SyncopeClient.getGroupSearchConditionBuilder();
break;
default:
panel = new AnyObjectSearchPanel.Builder(
scondAnnot.type(),
new ListModel<>(clauses), pageRef).required(false).build("value");
builder = SyncopeClient.getAnyObjectSearchConditionBuilder(scondAnnot.type());
}
Optional.ofNullable(BeanPanel.this.sCondWrapper).
ifPresent(scw -> scw.put(field.getName(), Pair.of(builder, clauses)));
} else if (List.class.equals(field.getType())) {
Class<?> listItemType = field.getGenericType() instanceof ParameterizedType
? (Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]
: String.class;
org.apache.syncope.common.lib.Schema schema =
field.getAnnotation(org.apache.syncope.common.lib.Schema.class);
if (listItemType.equals(String.class) && schema != null) {
List<SchemaTO> choices = new ArrayList<>();
for (SchemaType type : schema.type()) {
switch (type) {
case PLAIN:
choices.addAll(
schemaRestClient.getSchemas(SchemaType.PLAIN, schema.anyTypeKind()));
break;
case DERIVED:
choices.addAll(
schemaRestClient.getSchemas(SchemaType.DERIVED, schema.anyTypeKind()));
break;
case VIRTUAL:
choices.addAll(
schemaRestClient.getSchemas(SchemaType.VIRTUAL, schema.anyTypeKind()));
break;
default:
}
}
panel = new AjaxPalettePanel.Builder<>().setName(field.getName()).build(
"value",
new PropertyModel<>(bean.getObject(), field.getName()),
new ListModel<>(choices.stream().map(SchemaTO::getKey).collect(Collectors.toList()))).
hideLabel();
} else if (listItemType.isEnum()) {
panel = new AjaxPalettePanel.Builder<>().setName(field.getName()).build(
"value",
new PropertyModel<>(bean.getObject(), field.getName()),
new ListModel(List.of(listItemType.getEnumConstants()))).hideLabel();
} else {
Triple<FieldPanel, Boolean, Optional<String>> single =
buildSinglePanel(bean.getObject(), field.getType(), field.getName(),
field.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class), "panel");
setRequired(item, single.getMiddle());
single.getRight().ifPresent(description -> setDescription(item, description));
panel = new MultiFieldPanel.Builder<>(
new PropertyModel<>(bean.getObject(), field.getName())).build(
"value",
field.getName(),
single.getLeft()).hideLabel();
}
} else if (Map.class.equals(field.getType())) {
panel = new AjaxGridFieldPanel(
"value", field.getName(), new PropertyModel<>(bean, field.getName())).hideLabel();
Optional.ofNullable(field.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class)).
ifPresent(annot -> setDescription(item, annot.description()));
} else {
Triple<FieldPanel, Boolean, Optional<String>> single =
buildSinglePanel(bean.getObject(), field.getType(), field.getName(),
field.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class), "value");
setRequired(item, single.getMiddle());
single.getRight().ifPresent(description -> setDescription(item, description));
panel = single.getLeft().hideLabel();
}
item.add(panel.setRenderBodyOnly(true));
}
}.setReuseItems(false));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private Triple<FieldPanel, Boolean, Optional<String>> buildSinglePanel(
final Serializable bean, final Class<?> type, final String fieldName,
final io.swagger.v3.oas.annotations.media.Schema schema, final String id) {
PropertyModel model = new PropertyModel<>(bean, fieldName);
FieldPanel panel;
if (ClassUtils.isAssignable(Boolean.class, type)) {
panel = new AjaxCheckBoxPanel(id, fieldName, model);
} else if (ClassUtils.isAssignable(Number.class, type)) {
panel = new AjaxSpinnerFieldPanel.Builder<>().build(
id, fieldName, (Class<Number>) ClassUtils.resolvePrimitiveIfNecessary(type), model);
} else if (Date.class.equals(type)) {
panel = new AjaxDateTimeFieldPanel(id, fieldName, model,
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
} else if (OffsetDateTime.class.equals(type)) {
panel = new AjaxDateTimeFieldPanel(id, fieldName, DateOps.WrappedDateModel.ofOffset(model),
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
} else if (ZonedDateTime.class.equals(type)) {
panel = new AjaxDateTimeFieldPanel(id, fieldName, DateOps.WrappedDateModel.ofZoned(model),
DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
} else if (type.isEnum()) {
panel = new AjaxDropDownChoicePanel(id, fieldName, model).
setChoices(List.of(type.getEnumConstants()));
} else if (Duration.class.equals(type)) {
panel = new AjaxTextFieldPanel(id, fieldName, new IModel<>() {
private static final long serialVersionUID = 807008909842554829L;
@Override
public String getObject() {
return Optional.ofNullable(PropertyResolver.getValue(fieldName, bean)).
map(Object::toString).orElse(null);
}
@Override
public void setObject(final String object) {
PropertyResolverConverter prc = new PropertyResolverConverter(
SyncopeWebApplication.get().getConverterLocator(),
SyncopeConsoleSession.get().getLocale());
PropertyResolver.setValue(fieldName, bean, Duration.parse(object), prc);
}
});
} else {
// treat as String if nothing matched above
panel = new AjaxTextFieldPanel(id, fieldName, model);
}
boolean required = false;
Optional<String> description = Optional.empty();
if (schema != null) {
panel.setReadOnly(schema.accessMode() == Schema.AccessMode.READ_ONLY);
required = schema.requiredMode() == Schema.RequiredMode.REQUIRED;
panel.setRequired(required);
Optional.ofNullable(schema.example()).ifPresent(panel::setPlaceholder);
description = Optional.ofNullable(schema.description());
if (panel instanceof AjaxTextFieldPanel
&& panel.getModelObject() == null
&& schema.defaultValue() != null) {
((AjaxTextFieldPanel) panel).setModelObject(schema.defaultValue());
}
}
return Triple.of(panel, required, description);
}
}