EntityModelFactoryImpl.java
package org.dynamoframework.domain.model.impl;
/*-
* #%L
* Dynamo Framework
* %%
* Copyright (C) 2014 - 2024 Open Circle Solutions
* %%
* Licensed 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.
* #L%
*/
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dynamoframework.configuration.DynamoProperties;
import org.dynamoframework.constants.DynamoConstants;
import org.dynamoframework.dao.FetchJoinInformation;
import org.dynamoframework.dao.JoinType;
import org.dynamoframework.domain.AbstractEntity;
import org.dynamoframework.domain.model.*;
import org.dynamoframework.domain.model.annotation.*;
import org.dynamoframework.exception.OCSRuntimeException;
import org.dynamoframework.service.BaseService;
import org.dynamoframework.service.MessageService;
import org.dynamoframework.service.ServiceLocator;
import org.dynamoframework.service.ServiceLocatorFactory;
import org.dynamoframework.utils.ClassUtils;
import org.dynamoframework.utils.DateUtils;
import org.dynamoframework.utils.NumberUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* Implementation of the entity model factory - creates models that hold
* metadata about an entity
*
* @author bas.rutten
*/
@Slf4j
@NoArgsConstructor
public class EntityModelFactoryImpl implements EntityModelFactory {
private static final String CLASS = "class";
private static final String PLURAL_POSTFIX = "s";
private static final String VERSION = "version";
private final ConcurrentMap<String, Class<?>> alreadyProcessed = new ConcurrentHashMap<>();
private final ConcurrentMap<String, EntityModel<?>> cache = new ConcurrentHashMap<>();
private EntityModelFactory[] delegatedModelFactories;
@Autowired(required = false)
private MessageService messageService;
@Autowired
private DynamoProperties dynamoProperties;
private ServiceLocator serviceLocator = ServiceLocatorFactory.getServiceLocator();
/**
* Use this constructor when one needs to delegate creation of models to other
* model factories
*
* @param delegatedModelFactories the delegates
*/
public EntityModelFactoryImpl(EntityModelFactory... delegatedModelFactories) {
this.delegatedModelFactories = delegatedModelFactories;
}
/**
* Actually adds the attribute models to the entity model, in the correct group
*
* @param <T> type parameter
* @param entityClass the entity class
* @param entityModel the entity model
* @param attributeModels the list of attribute models to add
*/
private <T> void addAttributeModels(Class<T> entityClass, EntityModelImpl<T> entityModel,
List<AttributeModel> attributeModels) {
Map<String, String> attributeGroupMap = determineAttributeGroupMapping(entityModel, entityClass);
entityModel.addAttributeGroup(EntityModel.DEFAULT_GROUP);
attributeModels.sort(Comparator.comparing(AttributeModel::getOrder));
for (AttributeModel attributeModel : attributeModels) {
// determine the attribute group name
String group = attributeGroupMap.get(attributeModel.getName());
if (StringUtils.isEmpty(group)) {
group = EntityModel.DEFAULT_GROUP;
}
entityModel.addAttributeModel(group, attributeModel);
}
}
/**
* Adds overrides from annotation to entity model
*
* @param <T> type parameter
* @param entityClass the entity class
* @param builder the entity model builder
*/
private <T> void addEntityModelAnnotationOverrides(Class<?> entityClass,
EntityModelImpl.EntityModelImplBuilder<T> builder) {
Model modelAnnotation = entityClass.getAnnotation(Model.class);
if (modelAnnotation != null) {
if (!StringUtils.isEmpty(modelAnnotation.displayName())) {
builder.defaultDisplayName(modelAnnotation.displayName());
builder.defaultDescription(modelAnnotation.description());
}
if (!StringUtils.isEmpty(modelAnnotation.displayNamePlural())) {
builder.defaultDisplayNamePlural(modelAnnotation.displayNamePlural());
}
if (!StringUtils.isEmpty(modelAnnotation.description())) {
builder.defaultDescription(modelAnnotation.description());
}
if (!StringUtils.isEmpty(modelAnnotation.displayProperty())) {
builder.displayProperty(modelAnnotation.displayProperty());
}
if (modelAnnotation.nestingDepth() > -1) {
builder.nestingDepth(modelAnnotation.nestingDepth());
}
if (!modelAnnotation.listAllowed()) {
builder.listAllowed(false);
}
if (!modelAnnotation.searchAllowed()) {
builder.searchAllowed(false);
}
if (!modelAnnotation.createAllowed()) {
builder.createAllowed(false);
}
if (!modelAnnotation.updateAllowed()) {
builder.updateAllowed(false);
}
if (modelAnnotation.deleteAllowed()) {
builder.deleteAllowed(true);
}
if (!modelAnnotation.exportAllowed()) {
builder.exportAllowed(false);
}
builder.maxSearchResults(modelAnnotation.maxSearchResults());
builder.autofillInstructions(modelAnnotation.autofillInstructions());
}
Roles rolesAnnotation = entityClass.getAnnotation(Roles.class);
if (rolesAnnotation != null) {
builder.readRoles(Arrays.asList(rolesAnnotation.readRoles()));
builder.writeRoles(Arrays.asList(rolesAnnotation.writeRoles()));
builder.deleteRoles(Arrays.asList(rolesAnnotation.deleteRoles()));
}
}
/**
* Adds overrides from message bundle to entity model
*
* @param <T> type parameter
* @param reference the entity model reference
* @param builder the entity model builder
*/
private <T> void addEntityModelMessageBundleOverrides(String reference,
EntityModelImpl.EntityModelImplBuilder<T> builder) {
setStringSetting(getEntityMessage(reference, EntityModel.DISPLAY_NAME),
builder::defaultDisplayName);
setStringSetting(getEntityMessage(reference, EntityModel.DISPLAY_NAME_PLURAL),
builder::defaultDisplayNamePlural);
setStringSetting(getEntityMessage(reference, EntityModel.DESCRIPTION),
builder::defaultDescription);
setStringSetting(getEntityMessage(reference, EntityModel.DISPLAY_PROPERTY),
builder::displayProperty);
setStringSetting(getEntityMessage(reference, EntityModel.AUTO_FILL_INSTRUCTIONS),
builder::autofillInstructions);
setIntSettingIfAbove(getEntityMessage(reference, EntityModel.NESTING_DEPTH), -1, builder::nestingDepth);
setIntSettingIfBelow(getEntityMessage(reference, EntityModel.MAX_SEARCH_RESULTS),
Integer.MAX_VALUE, builder::maxSearchResults);
setBooleanSetting(getEntityMessage(reference, EntityModel.LIST_ALLOWED),
builder::listAllowed);
setBooleanSetting(getEntityMessage(reference, EntityModel.SEARCH_ALLOWED),
builder::searchAllowed);
setBooleanSetting(getEntityMessage(reference, EntityModel.CREATE_ALLOWED),
builder::createAllowed);
setBooleanSetting(getEntityMessage(reference, EntityModel.DELETE_ALLOWED),
builder::deleteAllowed);
setBooleanSetting(getEntityMessage(reference, EntityModel.UPDATE_ALLOWED),
builder::updateAllowed);
setBooleanSetting(getEntityMessage(reference, EntityModel.EXPORT_ALLOWED),
builder::exportAllowed);
setMessageBundleRoleOverrides(reference, EntityModel.READ_ROLES,
builder::readRoles);
setMessageBundleRoleOverrides(reference, EntityModel.WRITE_ROLES,
builder::writeRoles);
setMessageBundleRoleOverrides(reference, EntityModel.DELETE_ROLES,
builder::deleteRoles);
}
/**
* Sets roles based on message bundle overrides
*
* @param reference reference of the entity model
* @param name name of the setting to look up
* @param consumer the consumer that is used to set the values
*/
private void setMessageBundleRoleOverrides(String reference, String name,
Consumer<List<String>> consumer) {
String roleMessage = getEntityMessage(reference, name);
if (roleMessage != null) {
String[] roles = roleMessage.split(",");
setStringListSetting(Arrays.asList(roles), consumer);
}
}
private void addMissingAttributeNames(List<String> explicitAttributeNames, List<AttributeModel> attributeModels,
List<String> additionalNames) {
for (AttributeModel am : attributeModels) {
String name = am.getName();
if (!skipAttribute(name) && !explicitAttributeNames.contains(name)) {
additionalNames.add(name);
}
}
}
/**
* Indicates whether this factory can provide the model for the specified
* combination of reference and entity class
*
* @param reference the reference
* @param entityClass the entity class
*/
@Override
public <T> boolean canProvideModel(String reference, Class<T> entityClass) {
return true;
}
/**
* Collect attribute group data by checking the @AttributeGroup(s) annotations
*
* @param <T> the type parameter
* @param model the entity model
* @param entityClass the entity class
* @return a mapping from attribute name to the associated group
*/
private <T> Map<String, String> collectAttributeGroups(EntityModel<T> model, Class<T> entityClass) {
Map<String, String> result = new HashMap<>();
AttributeGroups groups = entityClass.getAnnotation(AttributeGroups.class);
AttributeGroup[] groupArray = new AttributeGroup[0];
if (groups != null) {
groupArray = groups.value();
} else {
// just a single group
AttributeGroup group = entityClass.getAnnotation(AttributeGroup.class);
if (group != null) {
groupArray = new AttributeGroup[]{group};
}
}
for (AttributeGroup attributeGroup : groupArray) {
model.addAttributeGroup(attributeGroup.messageKey());
for (String attributeName : attributeGroup.attributeNames()) {
result.put(attributeName, attributeGroup.messageKey());
}
}
return result;
}
/**
* Constructs an attribute model for a property
*
* @param descriptor the property descriptor
* @param entityModel the entity model
* @param parentClass the type of the direct parent of the attribute (relevant
* in case of embedded attributes)
* @param nested whether this is a nested attribute
* @param prefix the prefix to apply to the attribute name (in case of
* nested attributes)
* @return the constructed attribute model
*/
protected <T> List<AttributeModel> constructAttributeModel(PropertyDescriptor descriptor,
EntityModel<T> entityModel, Class<?> parentClass, boolean nested,
String prefix) {
List<AttributeModel> result = new ArrayList<>();
// ignore methods annotated with @AssertTrue or @AssertFalse
String fieldName = descriptor.getName();
Class<?> pClass = parentClass != null ? parentClass : entityModel.getEntityClass();
AssertTrue assertTrue = ClassUtils.getAnnotation(pClass, fieldName, AssertTrue.class);
AssertFalse assertFalse = ClassUtils.getAnnotation(pClass, fieldName, AssertFalse.class);
if (assertTrue != null || assertFalse != null) {
return result;
}
AttributeModelImpl model = new AttributeModelImpl(dynamoProperties);
model.setEntityModel(entityModel);
setAttributeModelDefaults(descriptor, entityModel, parentClass, prefix, fieldName, model);
setNestedEntityModel(entityModel, model);
// only basic attributes are shown in the grid by default. nested attributes are hidden
// unless they are IDs
boolean isId = model.getName().equals(DynamoConstants.ID);
boolean displayProperty = model.getName().equals(entityModel.getDisplayProperty());
model.setVisibleInGrid((isId || displayProperty || !nested) && (AttributeType.BASIC.equals(model.getAttributeType())));
model.setVisibleInForm(!isId && (AttributeType.BASIC.equals(model.getAttributeType()) ||
AttributeType.LOB.equals(model.getAttributeType())));
boolean isIdOrNestedId = model.getName().equals(DynamoConstants.ID) ||
model.getName().endsWith(DynamoConstants.ID);
model.setEditableType(isIdOrNestedId ? EditableType.READ_ONLY : EditableType.EDITABLE);
if (getMessageService() != null) {
model.setDefaultTrueRepresentation(dynamoProperties.getDefaults().getTrueRepresentation());
model.setDefaultFalseRepresentation(dynamoProperties.getDefaults().getFalseRepresentation());
}
AttributeSelectMode defaultMode = AttributeType.DETAIL.equals(model.getAttributeType())
? AttributeSelectMode.MULTI_SELECT
: AttributeSelectMode.COMBO;
model.setSelectMode(defaultMode);
model.setTextFieldMode(AttributeTextFieldMode.TEXTFIELD);
model.setSearchSelectMode(defaultMode);
model.setBooleanFieldMode(dynamoProperties.getDefaults().getBooleanFieldMode());
model.setElementCollectionMode(dynamoProperties.getDefaults().getElementCollectionMode());
model.setEnumFieldMode(dynamoProperties.getDefaults().getEnumFieldMode());
model.setShowDetailsPaginator(dynamoProperties.getDefaults().isShowDetailsPaginator());
Email email = ClassUtils.getAnnotation(entityModel.getEntityClass(), fieldName,
Email.class);
if (email != null) {
model.setEmail(true);
}
setAttributeModelAnnotationOverrides(parentClass, model, descriptor, nested);
setAttributeModelMessageBundleOverrides(entityModel, model);
if (!model.isEmbedded()) {
result.add(model);
} else {
processEmbeddedAttributeModel(result, model, entityModel, nested);
}
validateAttributeModel(model);
return result;
}
/**
* Processes an embedded attribute model. This will make sure that the
* properties of the embedded attribute are added to the parent entity model
*
* @param <T> the type of the entity model
* @param result list of attributes so far
* @param model the nested attribute model
* @param entityModel the entity model
* @param nested whether the attribute is nested
*/
private <T> void processEmbeddedAttributeModel(List<AttributeModel> result, AttributeModel model,
EntityModel<T> entityModel, boolean nested) {
// an embedded entity does not get its own entity model, but its properties are
// added
// to the parent entity
if (model.getType().equals(entityModel.getEntityClass())) {
throw new IllegalStateException("Embedding a class in itself is not allowed");
}
PropertyDescriptor[] embeddedDescriptors = BeanUtils.getPropertyDescriptors(model.getType());
for (PropertyDescriptor embeddedDescriptor : embeddedDescriptors) {
String name = embeddedDescriptor.getName();
if (!skipAttribute(name)) {
List<AttributeModel> embeddedModels = constructAttributeModel(embeddedDescriptor, entityModel,
model.getType(), nested, model.getName());
result.addAll(embeddedModels);
}
}
}
/**
* Iterates over the properties of an entity and created attribute models for
* each of them
*
* @param <T> the type parameter
* @param reference reference of the entity model
* @param entityClass the entity class
* @param entityModel the entity model
* @return the constructed attribute models
*/
private <T> List<AttributeModel> constructAttributeModels(String reference, Class<T> entityClass,
EntityModelImpl<T> entityModel) {
boolean nested = reference.indexOf('.') >= 0;
PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(entityClass);
List<AttributeModel> result = new ArrayList<>();
for (PropertyDescriptor descriptor : descriptors) {
if (!skipAttribute(descriptor.getName())) {
List<AttributeModel> attributeModels = constructAttributeModel(descriptor, entityModel,
entityModel.getEntityClass(), nested, null);
result.addAll(attributeModels);
}
}
return result;
}
/**
* Constructs the model for an entity
*
* @param reference unique reference to the entity model
* @param entityClass the class of the entity
* @return the constructed model
*/
protected synchronized <T> EntityModel<T> constructModel(String reference, Class<T> entityClass) {
// Delegate to other factories first
EntityModelImpl<T> entityModel = null;
if (delegatedModelFactories != null) {
for (EntityModelFactory delegate : delegatedModelFactories) {
if (delegate.canProvideModel(reference, entityClass)) {
entityModel = (EntityModelImpl<T>) delegate.getModel(reference, entityClass);
if (entityModel != null) {
break;
}
}
}
}
if (entityModel != null) {
return entityModel;
}
return constructModelInner(entityClass, reference);
}
/**
* Constructs the entity model for a class
*
* @param entityClass the entity class
* @param reference the unique reference for the entity model
* @return the constructed entity model
*/
private <T> EntityModelImpl<T> constructModelInner(Class<T> entityClass, String reference) {
String displayName = org.dynamoframework.utils.StringUtils.propertyIdToHumanFriendly(entityClass.getSimpleName(), dynamoProperties.isCapitalizePropertyNames());
EntityModelImpl.EntityModelImplBuilder<T> builder = EntityModelImpl.builder();
builder.reference(reference).nestingDepth(dynamoProperties.getDefaults().getNestingDepth())
.defaultDescription(displayName).defaultDisplayName(displayName)
.defaultDisplayNamePlural(displayName + PLURAL_POSTFIX).entityClass(entityClass);
builder.listAllowed(true);
builder.createAllowed(true);
builder.searchAllowed(true);
builder.updateAllowed(true);
builder.exportAllowed(true);
builder.maxSearchResults(Integer.MAX_VALUE);
addEntityModelAnnotationOverrides(entityClass, builder);
addEntityModelMessageBundleOverrides(reference, builder);
EntityModelImpl<T> entityModel = builder.build();
alreadyProcessed.put(reference, entityClass);
addEntityModelActions(entityModel);
List<AttributeModel> attributeModels = constructAttributeModels(reference, entityClass, entityModel);
// calculate the various attribute orders
attributeModels.sort((a, b) -> a.getName().compareToIgnoreCase(b.getName()));
determineAttributeOrder(entityClass, reference, attributeModels);
boolean gridOrder = determineGridAttributeOrder(entityClass, reference, attributeModels);
boolean searchOrder = determineSearchAttributeOrder(entityClass, reference, attributeModels);
entityModel.setGridOrderSet(gridOrder);
entityModel.setSearchOrderSet(searchOrder);
addAttributeModels(entityClass, entityModel, attributeModels);
validateGroupTogetherSettings(entityModel);
String sortOrder = null;
Model modelAnnotation = entityClass.getAnnotation(Model.class);
if (modelAnnotation != null && !StringUtils.isEmpty(modelAnnotation.sortOrder())) {
sortOrder = modelAnnotation.sortOrder();
}
String sortOrderMsg = getEntityMessage(reference, EntityModel.SORT_ORDER);
if (!StringUtils.isEmpty(sortOrderMsg)) {
sortOrder = sortOrderMsg;
}
setSortOrder(entityModel, sortOrder);
entityModel.setFetchJoins(new ArrayList<>());
entityModel.setDetailJoins(new ArrayList<>());
processJoinAnnotations(entityClass, entityModel);
processMessageBundleJoinOverrides(entityModel);
cache.put(reference, entityModel);
return entityModel;
}
private <T> void processJoinAnnotations(Class<T> entityClass, EntityModelImpl<T> entityModel) {
FetchJoins joins = entityClass.getAnnotation(FetchJoins.class);
if (joins != null) {
List<FetchJoinInformation> mapped = Arrays.stream(joins.joins())
.map(join -> FetchJoinInformation.of(join.attribute(), join.type()))
.toList();
entityModel.setFetchJoins(mapped);
entityModel.setDetailJoins(mapped);
if (joins.detailJoins() != null && joins.detailJoins().length > 0) {
mapped = Arrays.stream(joins.detailJoins())
.map(join -> FetchJoinInformation.of(join.attribute(), join.type()))
.toList();
entityModel.setDetailJoins(mapped);
}
}
}
private <T> void addEntityModelActions(EntityModelImpl<T> entityModel) {
List<EntityModelAction> modelActions = new ArrayList<>();
BaseService<?, ?> service = serviceLocator.getServiceForEntity(entityModel.getEntityClass());
if (service != null) {
Class<?> clazz = org.springframework.util.ClassUtils.getUserClass(service);
Method[] methods = clazz.getMethods();
for (Method method : methods) {
addEntityModelAction(method, modelActions);
}
}
entityModel.setEntityModelActions(modelActions);
}
/**
* Potentially adds an entity model action for the provided method
*
* @param method the method
* @param modelActions the model actions
*/
private void addEntityModelAction(Method method, List<EntityModelAction> modelActions) {
ModelAction action = ClassUtils.getAnnotationOnMethod(method, ModelAction.class);
if (action != null) {
if (method.getParameters().length == 0) {
throw new OCSRuntimeException("@ModelAction annotation found on method %s without parameters"
.formatted(method.getName()));
}
if (StringUtils.isEmpty(action.id())) {
throw new OCSRuntimeException("@ModelAction annotation found on method %s without an action ID"
.formatted(method.getName()));
}
Class<?> actionClass = method.getParameters()[0].getType();
EntityModelActionImpl actionImpl = new EntityModelActionImpl();
actionImpl.setEntityClass(actionClass);
actionImpl.setReference(action.id());
actionImpl.setId(action.id());
actionImpl.setDefaultDisplayName(action.displayName());
actionImpl.setMethodName(method.getName());
actionImpl.setType(action.type());
actionImpl.setIcon(action.icon());
actionImpl.setRoles(Arrays.asList(action.roles()));
actionImpl.setFormMode(action.formMode());
EntityModel<?> actionModel = constructModel(action.id(), actionClass);
actionImpl.setEntityModel(actionModel);
modelActions.add(actionImpl);
processEntityModelActionMessageBundleOverrides(actionImpl);
}
}
private void processEntityModelActionMessageBundleOverrides(EntityModelActionImpl action) {
setMessageBundleRoleOverrides(action.getReference(), EntityModel.ACTION_ROLES,
action::setRoles);
setStringSetting(getEntityMessage(action.getReference(), EntityModel.ICON),
action::setIcon);
setEnumSetting(getEntityMessage(action.getReference(), EntityModel.FORM_MODE),
ActionFormMode.class, action::setFormMode);
}
/**
* Process message bundle join overrides for an entity
*
* @param entityModel the entity model
* @param <T> type parameter, the type of the entity
*/
private <T> void processMessageBundleJoinOverrides(EntityModelImpl<T> entityModel) {
List<FetchJoinInformation> overrideJoins = processMessageBundleFetchJoinOverrides(
entityModel, EntityModel.JOIN);
if (!overrideJoins.isEmpty()) {
entityModel.setFetchJoins(overrideJoins);
}
List<FetchJoinInformation> overrideDetailJoins = processMessageBundleFetchJoinOverrides(
entityModel, EntityModel.DETAIL_JOIN);
if (!overrideDetailJoins.isEmpty()) {
entityModel.setDetailJoins(overrideDetailJoins);
}
}
/**
* Processes message bundle fetch joins overrides for a specific type
*
* @param model the entity model
* @param joinName the name of the join
* @param <T> type parameter
* @return the list of fetch joins (possibly empty)
*/
protected <T> List<FetchJoinInformation> processMessageBundleFetchJoinOverrides(EntityModel<T> model,
String joinName) {
List<FetchJoinInformation> result = new ArrayList<>();
// look for message bundle overwrites
int i = 1;
if (messageService != null) {
String key = joinName + "." + i + "." + EntityModel.ATTRIBUTE;
String joinAttribute = messageService.getEntityMessage(model.getReference(),
key, getLocale());
while (joinAttribute != null) {
String joinType = messageService.getEntityMessage(model.getReference(),
joinName + "." + i + "." + EntityModel.JOIN_TYPE, getLocale());
if (joinType != null) {
result.add(new FetchJoinInformation(joinAttribute,
JoinType.valueOf(joinType)));
} else {
result.add(new FetchJoinInformation(joinAttribute));
}
i++;
joinAttribute = messageService.getEntityMessage(model.getReference(),
joinName + "." + i + "." + EntityModel.ATTRIBUTE, getLocale());
}
}
return result;
}
/**
* Determines the attribute group mapping - from attribute name to the group it
* belongs to
*
* @param model the entity model
* @param entityClass the entity class
* @return the mapping from attribute name to group
*/
protected <T> Map<String, String> determineAttributeGroupMapping(EntityModel<T> model, Class<T> entityClass) {
Map<String, String> result = collectAttributeGroups(model, entityClass);
int i = 1;
if (messageService != null) {
String groupName = messageService.getEntityMessage(model.getReference(),
EntityModel.ATTRIBUTE_GROUP + "." + i + "." + EntityModel.MESSAGE_KEY, getLocale());
if (groupName != null) {
result.clear();
model.getAttributeGroups().clear();
}
while (groupName != null) {
String attributeNames = messageService.getEntityMessage(model.getReference(),
EntityModel.ATTRIBUTE_GROUP + "." + i + "." + EntityModel.ATTRIBUTE_NAMES, getLocale());
if (attributeNames != null) {
model.addAttributeGroup(groupName);
for (String s : attributeNames.split(",")) {
result.put(s, groupName);
}
}
i++;
groupName = messageService.getEntityMessage(model.getReference(),
EntityModel.ATTRIBUTE_GROUP + "." + i + "." + EntityModel.MESSAGE_KEY, getLocale());
}
}
return result;
}
/**
* Determines the (default) attribute ordering for an entity based on
* the @AttributeOrder annotation
*
* @param <T> the type of the entity class
* @param entityClass the entity class
* @param reference the unique reference of the entity model
* @param attributeModels the list of attribute models to process
*/
protected <T> void determineAttributeOrder(Class<T> entityClass, String reference,
List<AttributeModel> attributeModels) {
List<String> explicitAttributeNames = new ArrayList<>();
AttributeOrder orderAnnotation = entityClass.getAnnotation(AttributeOrder.class);
if (orderAnnotation != null) {
explicitAttributeNames = List.of(orderAnnotation.attributeNames());
}
// set all orders
determineAttributeOrderInner(reference, EntityModel.ATTRIBUTE_ORDER, explicitAttributeNames, attributeModels,
AttributeModelImpl::setOrder);
}
/**
* Determines the order of the attributes - this will first pick up any
* attributes that are mentioned in one of the @AttributeOrder annotations (in
* the order in which they occur) and then add any attributes that are not
* explicitly mentioned
*
* @param reference the unique reference to the entity model
* @param messageBundleKey the key under which to look up the attribute
* overrides in the message bundle
* @param explicitAttributeNames the attribute names explicitly mentioned in the
* annotation
* @param attributeModels the full set of attribute model
* @param consumer the consumer that is called to actually set the
* proper order on the attribute model
* @return whether an explicit ordering is defined
*/
protected boolean determineAttributeOrderInner(String reference, String messageBundleKey,
List<String> explicitAttributeNames, List<AttributeModel> attributeModels,
BiConsumer<AttributeModelImpl, Integer> consumer) {
List<String> additionalNames = new ArrayList<>();
// overwrite by message bundle (if present)
String msg = messageService == null ? null
: messageService.getEntityMessage(reference, messageBundleKey, getLocale());
if (msg != null) {
explicitAttributeNames = List.of(msg.replaceAll("\\s+", "").split(","));
}
boolean explicit = !explicitAttributeNames.isEmpty();
List<String> result = new ArrayList<>(explicitAttributeNames);
addMissingAttributeNames(explicitAttributeNames, attributeModels, additionalNames);
result.addAll(additionalNames);
// loop over the attributes and set the orders
int i = 0;
for (String attributeName : result) {
AttributeModel am = attributeModels.stream().filter(m -> m.getName().equals(attributeName)).findFirst()
.orElse(null);
if (am != null) {
consumer.accept((AttributeModelImpl) am, i);
i++;
} else {
throw new OCSRuntimeException("Attribute %s is not known".formatted(attributeName));
}
}
return explicit;
}
/**
* Determines the attribute type
*
* @param parentClass the parent class on which the attribute is defined
* @param model the model representation of the attribute
* @return the attribute type
*/
protected AttributeType determineAttributeType(Class<?> parentClass, AttributeModelImpl model) {
AttributeType result = null;
String name = model.getName();
int p = name.lastIndexOf('.');
if (p > 0) {
name = name.substring(p + 1);
}
if (!BeanUtils.isSimpleValueType(model.getType()) && !DateUtils.isJava8DateType(model.getType())) {
// No relation type set in view model definition, hence derive
// defaults
Embedded embedded = ClassUtils.getAnnotation(parentClass, name, Embedded.class);
Attribute attribute = ClassUtils.getAnnotation(parentClass, name, Attribute.class);
if (embedded != null) {
result = AttributeType.EMBEDDED;
} else if (Collection.class.isAssignableFrom(model.getType())) {
if (attribute != null && attribute.memberType() != null
&& !attribute.memberType().equals(Object.class)) {
// if a member type is explicitly set, use that type
result = AttributeType.DETAIL;
model.setMemberType(attribute.memberType());
} else if (ClassUtils.getAnnotation(parentClass, name, ManyToMany.class) != null
|| ClassUtils.getAnnotation(parentClass, name, OneToMany.class) != null) {
result = AttributeType.DETAIL;
model.setMemberType(ClassUtils.getResolvedType(parentClass, name, 0));
} else if (ClassUtils.getAnnotation(parentClass, name, ElementCollection.class) != null) {
result = AttributeType.ELEMENT_COLLECTION;
handleElementCollectionSettings(parentClass, model, name);
} else if (AbstractEntity.class.isAssignableFrom(model.getType())) {
// not a collection but a reference to another object
result = AttributeType.MASTER;
}
} else if (model.getType().isArray()) {
Lob lob = ClassUtils.getAnnotation(parentClass, name, Lob.class);
Class<?> componentType = model.getType().getComponentType();
if (lob != null || componentType.equals(byte.class)) {
result = AttributeType.LOB;
}
} else {
// not a collection but a reference to another object
result = AttributeType.MASTER;
}
} else {
// simple attribute type
result = AttributeType.BASIC;
}
return result;
}
/**
* Determines the "dateType" for an attribute
*
* @param modelType the type of the attribute. Can be a java 8 LocalX type
* @return the data type
*/
protected AttributeDateType determineDateType(Class<?> modelType) {
// set the date type
if (LocalDate.class.equals(modelType)) {
return AttributeDateType.DATE;
} else if (LocalDateTime.class.equals(modelType)) {
return AttributeDateType.LOCAL_DATE_TIME;
} else if (LocalTime.class.equals(modelType)) {
return AttributeDateType.TIME;
} else if (Instant.class.equals(modelType)) {
return AttributeDateType.INSTANT;
}
return null;
}
/**
* Determines the default format to use for a date or time property
*
* @param type the type of the property
* @return the default display format
*/
protected String determineDefaultDisplayFormat(Class<?> type) {
String format = null;
if (LocalDate.class.isAssignableFrom(type)) {
format = dynamoProperties.getDefaults().getDateFormat();
} else if (LocalDateTime.class.isAssignableFrom(type)) {
format = dynamoProperties.getDefaults().getDateTimeFormat();
} else if (LocalTime.class.isAssignableFrom(type)) {
format = dynamoProperties.getDefaults().getTimeFormat();
} else if (Instant.class.isAssignableFrom(type)) {
format = dynamoProperties.getDefaults().getDateTimeFormat();
}
return format;
}
/**
* Determines the order in which attributes must appear in a search results grid
* for an entity
*
* @param <T> type parameter
* @param entityClass the class of the entity model
* @param reference unique reference of the entity model
* @param attributeModels the list of attribute models to order
* @return the attribute order
*/
protected <T> boolean determineGridAttributeOrder(Class<T> entityClass, String reference,
List<AttributeModel> attributeModels) {
List<String> explicitAttributeNames = new ArrayList<>();
GridAttributeOrder orderAnnotation = entityClass.getAnnotation(GridAttributeOrder.class);
if (orderAnnotation != null) {
explicitAttributeNames = List.of(orderAnnotation.attributeNames());
}
return determineAttributeOrderInner(reference, EntityModel.GRID_ATTRIBUTE_ORDER, explicitAttributeNames,
attributeModels, AttributeModelImpl::setGridOrder);
}
/**
* Determines the order in which attributes must appear in a search form for an
* entity
*
* @param <T> the type of the class
* @param entityClass the class of the entity model
* @param reference unique reference of the entity model
* @param attributeModels the list of attribute models to order
* @return the attribute order
*/
protected <T> boolean determineSearchAttributeOrder(Class<T> entityClass, String reference,
List<AttributeModel> attributeModels) {
List<String> explicitAttributeNames = new ArrayList<>();
SearchAttributeOrder orderAnnotation = entityClass.getAnnotation(SearchAttributeOrder.class);
if (orderAnnotation != null) {
explicitAttributeNames = List.of(orderAnnotation.attributeNames());
}
return determineAttributeOrderInner(reference, EntityModel.SEARCH_ATTRIBUTE_ORDER, explicitAttributeNames,
attributeModels, AttributeModelImpl::setSearchOrder);
}
/**
* Looks up a possible delegated model factory for an entity model
*
* @param reference the reference of the entity model
* @param entityClass the entity class
* @return the model factory
*/
protected <T> EntityModelFactory findModelFactory(String reference, Class<T> entityClass) {
EntityModelFactory entityModelFactory = this;
if (delegatedModelFactories != null) {
for (EntityModelFactory delegate : delegatedModelFactories) {
if (delegate.canProvideModel(reference, entityClass)) {
entityModelFactory = delegate;
break;
}
}
}
return entityModelFactory;
}
/**
* Retrieves a message relating to an attribute from the message bundle
*
* @param model the entity model
* @param attributeModel the attribute model
* @param propertyName the name of the property
* @return the message
*/
protected <T> String getAttributeMessage(EntityModel<T> model, AttributeModel attributeModel, String propertyName) {
if (messageService != null) {
return messageService.getAttributeMessage(model.getReference(), attributeModel, propertyName, getLocale());
}
return null;
}
/**
* Retrieves a message relating to an entity from the message bundle
*
* @param reference the reference of the entity model
* @param propertyName the name of the property to retrieve the message for
* @return the message
*/
protected String getEntityMessage(String reference, String propertyName) {
if (messageService != null) {
return messageService.getEntityMessage(reference, propertyName, getLocale());
}
return null;
}
protected Locale getLocale() {
return dynamoProperties.getDefaults().getLocale();
}
public MessageService getMessageService() {
return messageService;
}
@Override
public synchronized <T> EntityModel<T> getModel(Class<T> entityClass) {
return getModel(entityClass.getSimpleName(), entityClass);
}
@Override
@SuppressWarnings({"unchecked"})
public synchronized <T> EntityModel<T> getModel(String reference, Class<T> entityClass) {
EntityModel<T> model = null;
if (!StringUtils.isEmpty(reference) && entityClass != null) {
model = (EntityModel<T>) cache.get(reference);
if (model == null) {
log.debug("Creating entity model for {}, ({})", reference, entityClass);
model = constructModel(reference, entityClass);
}
}
return model;
}
/**
* Handles element collection settings for an attribute model
*
* @param parentClass the parent class on which the attribute is defined
* @param model the attribute model
* @param name the name of the attribute model
*/
private void handleElementCollectionSettings(Class<?> parentClass, AttributeModelImpl model, String name) {
model.setMemberType(ClassUtils.getResolvedType(parentClass, name, 0));
model.setCollectionTableName(model.getName());
model.setCollectionTableFieldName(model.getName());
// override table name
CollectionTable table = ClassUtils.getAnnotation(parentClass, name, CollectionTable.class);
if (table != null && table.name() != null) {
model.setCollectionTableName(table.name());
}
// override field name
Column col = ClassUtils.getAnnotation(parentClass, name, Column.class);
if (col != null && col.name() != null) {
model.setCollectionTableFieldName(col.name());
}
}
/**
* Check if a certain entity model has already been processed
*
* @param type the type of the entity
* @param reference the reference to the entity
* @return true if this is the case, false otherwise
*/
protected boolean hasEntityModel(Class<?> type, String reference) {
for (Entry<String, Class<?>> entry : alreadyProcessed.entrySet()) {
if (reference.equals(entry.getKey()) && entry.getValue().equals(type)) {
// only check for starting reference in order to prevent
// recursive looping between
// two-sided relations
return true;
}
}
return false;
}
/**
* Checks whether the model factory already contains a model for the specified
* reference
*
* @param reference the unique reference of the entity model
* @return true if this is the case, false otherwise
*/
public boolean hasModel(String reference) {
return cache.containsKey(reference);
}
/**
* Check whether a message contains a value that marks the attribute as
* "visible". "true" and SHOW are interpreted as positive values, "false" and
* HIDE are negative values
*
* @param msg the message
* @return true if this is the case, false otherwise
*/
private boolean isVisible(String msg) {
try {
VisibilityType other = VisibilityType.valueOf(msg);
return VisibilityType.SHOW.equals(other);
} catch (IllegalArgumentException ex) {
// do nothing, threat as false
}
return Boolean.parseBoolean(msg);
}
/**
* Sets the custom settings for an attribute model based on the annotation
*
* @param attribute the attribute annotation
* @param model the attribute model
*/
private void setAnnotationCustomOverwrites(Attribute attribute, AttributeModel model) {
if (attribute.custom() != null) {
for (CustomSetting s : attribute.custom()) {
if (!StringUtils.isEmpty(s.name())) {
String value = s.value();
if (CustomType.BOOLEAN.equals(s.type())) {
model.setCustomSetting(s.name(), Boolean.valueOf(value));
} else if (CustomType.INT.equals(s.type())) {
model.setCustomSetting(s.name(), Integer.parseInt(value));
} else {
model.setCustomSetting(s.name(), value);
}
}
}
}
}
/**
* Sets visibility settings for an attribute model based on annotation overrides
*
* @param attribute the attribute annotation
* @param model the attribute model
* @param nested whether we are dealing with a nested attribute
*/
private void setAnnotationVisibilityOverrides(Attribute attribute, AttributeModelImpl model, boolean nested) {
// set visibility (hide nested attribute by default; they must be shown using
// the message bundle)
if (attribute.visibleInForm() != null && !VisibilityType.INHERIT.equals(attribute.visibleInForm()) && !nested) {
model.setVisibleInForm(VisibilityType.SHOW.equals(attribute.visibleInForm()));
}
// set grid visibility
if (attribute.visibleInGrid() != null && !VisibilityType.INHERIT.equals(attribute.visibleInGrid()) && !nested) {
model.setVisibleInGrid(VisibilityType.SHOW.equals(attribute.visibleInGrid()));
}
// set paginator visibility
if (attribute.showDetailsPaginator() != null && !VisibilityType.INHERIT.equals(attribute.showDetailsPaginator())) {
model.setShowDetailsPaginator(VisibilityType.SHOW.equals(attribute.showDetailsPaginator()));
}
}
/**
* Overwrite the default settings for an attribute model with the
*
* @param parentClass the entity class in which the attribute is declared
* @param model the attribute model
* @param descriptor the property descriptor for the attribute
* @param nested whether the attribute is nested
*/
private void setAttributeModelAnnotationOverrides(Class<?> parentClass, AttributeModelImpl model,
PropertyDescriptor descriptor, boolean nested) {
Attribute attribute = ClassUtils.getAnnotation(parentClass, descriptor.getName(), Attribute.class);
if (attribute != null) {
if (!StringUtils.isEmpty(attribute.displayName())) {
model.setDefaultDisplayName(attribute.displayName());
model.setDefaultDescription(attribute.displayName());
model.setDefaultPrompt(attribute.displayName());
}
setStringSetting(attribute.description(), model::setDefaultDescription);
setStringSetting(attribute.prompt(), model::setDefaultPrompt);
setStringSetting(attribute.displayFormat(), model::setDefaultDisplayFormat);
setStringSetting(attribute.trueRepresentation(), model::setDefaultTrueRepresentation);
setStringSetting(attribute.falseRepresentation(), model::setDefaultFalseRepresentation);
setStringSetting(attribute.currencyCode(), model::setCurrencyCode);
setStringSetting(attribute.lookupEntityReference(), model::setLookupEntityReference);
setStringSetting(attribute.navigationLink(), model::setNavigationLink);
setStringSetting(attribute.autoFillInstructions(), model::setAutofillInstructions);
boolean isId = model.getName().equals(DynamoConstants.ID) ||
model.getName().endsWith(DynamoConstants.ID);
model.setEditableType(isId ? EditableType.READ_ONLY : attribute.editable());
setAnnotationVisibilityOverrides(attribute, model, nested);
if ((SearchMode.ADVANCED.equals(attribute.searchable()) || SearchMode.ALWAYS.equals(attribute.searchable()))
&& !nested) {
model.setSearchMode(attribute.searchable());
}
if (attribute.requiredForSearching() && !nested) {
model.setRequiredForSearching(true);
}
setBooleanTrueSetting(attribute.image(), model::setImage);
setBooleanTrueSetting(attribute.downloadAllowed(), model::setDownloadAllowed);
setBooleanTrueSetting(attribute.percentage(), model::setPercentage);
setBooleanTrueSetting(attribute.url(), model::setUrl);
setBooleanTrueSetting(attribute.showPassword(), model::setShowPassword);
setBooleanTrueSetting(attribute.quickAddAllowed(), model::setQuickAddAllowed);
setBooleanTrueSetting(attribute.neededInData(), model::setNeededInData);
setBooleanTrueSetting(attribute.nestedDetails(), model::setNestedDetails);
setBooleanFalseSetting(attribute.sortable(), model::setSortable);
if (attribute.allowedExtensions() != null && attribute.allowedExtensions().length > 0) {
Set<String> set = Arrays.stream(attribute.allowedExtensions()).map(String::toLowerCase)
.collect(Collectors.toSet());
model.setAllowedExtensions(set);
}
if (attribute.cascade() != null) {
for (Cascade cascade : attribute.cascade()) {
model.addCascade(cascade.cascadeTo(), cascade.filterPath(), cascade.mode());
}
}
setAnnotationCustomOverwrites(attribute, model);
if (attribute.groupTogetherWith() != null) {
for (String attributeName : attribute.groupTogetherWith()) {
model.addGroupTogetherWith(attributeName);
}
}
if (attribute.dateType() != null && !AttributeDateType.INHERIT.equals(attribute.dateType())) {
model.setDateType(attribute.dateType());
}
if (attribute.selectMode() != null && !AttributeSelectMode.INHERIT.equals(attribute.selectMode())) {
// setting the select mode also sets the search and grid modes
model.setSelectMode(attribute.selectMode());
model.setSearchSelectMode(attribute.selectMode());
}
// multiple search for master object (default to token)
if (attribute.multipleSearch()) {
model.setMultipleSearch(true);
model.setSearchSelectMode(AttributeSelectMode.MULTI_SELECT);
}
if (!AttributeSelectMode.INHERIT.equals(attribute.searchSelectMode())) {
model.setSearchSelectMode(attribute.searchSelectMode());
// for a basic attribute, automatically set multiple search when a token field
// is selected
if (AttributeType.BASIC.equals(model.getAttributeType())
&& AttributeSelectMode.MULTI_SELECT.equals(model.getSearchSelectMode())) {
model.setMultipleSearch(true);
}
}
setEnumValueUnless(attribute.searchCaseSensitive(), BooleanType.INHERIT,
value -> model.setSearchCaseSensitive(value.toBoolean()));
setEnumValueUnless(attribute.searchPrefixOnly(), BooleanType.INHERIT,
value -> model.setSearchPrefixOnly(value.toBoolean()));
setEnumValueUnless(attribute.enumFieldMode(), AttributeEnumFieldMode.INHERIT,
model::setEnumFieldMode);
setEnumValueUnless(attribute.lookupQueryType(), QueryType.INHERIT,
model::setLookupQueryType);
if (attribute.textFieldMode() != null
&& !AttributeTextFieldMode.INHERIT.equals(attribute.textFieldMode())) {
model.setTextFieldMode(attribute.textFieldMode());
}
setIntSetting(attribute.precision(), -1, model::setPrecision);
setIntSetting(attribute.minLength(), -1, model::setMinLength);
setIntSetting(attribute.maxLength(), -1, model::setMaxLength);
setIntSetting(attribute.maxLengthInGrid(), -1, model::setMaxLengthInGrid);
setBigDecimalSetting(attribute.minValue(), Double.MIN_VALUE, true, model::setMinValue);
setBigDecimalSetting(attribute.maxValue(), Double.MAX_VALUE, false, model::setMaxValue);
setStringSetting(attribute.replacementSearchPath(), model::setReplacementSearchPath);
setStringSetting(attribute.replacementSortPath(), model::setReplacementSortPath);
setBooleanTrueSetting(attribute.searchForExactValue(), model::setSearchForExactValue);
setStringSetting(attribute.fileNameProperty(), model::setFileNameProperty);
model.setSearchDateOnly(attribute.searchDateOnly());
setDefaultValue(model, attribute);
setDefaultSearchValue(model, attribute);
setDefaultSearchValueFrom(model, attribute);
setDefaultSearchValueTo(model, attribute);
model.setNavigable(attribute.navigable());
model.setIgnoreInSearchFilter(attribute.ignoreInSearchFilter());
setEnumValueUnless(attribute.trimSpaces(), TrimType.INHERIT,
ts -> model.setTrimSpaces(TrimType.TRIM.equals(ts)));
setEnumValueUnless(attribute.numberFieldMode(), NumberFieldMode.INHERIT, model::setNumberFieldMode);
setEnumValueUnless(attribute.booleanFieldMode(), AttributeBooleanFieldMode.INHERIT,
model::setBooleanFieldMode);
setEnumValueUnless(attribute.elementCollectionMode(), ElementCollectionMode.INHERIT,
model::setElementCollectionMode);
setIntSetting(attribute.numberFieldStep(), 0, model::setNumberFieldStep);
}
}
/**
* Sets the default values for an attribute model
*
* @param <T> the class of the entity model
* @param descriptor the property descriptor to base the attribute model on
* @param entityModel the entity model
* @param parentClass the parent class
* @param prefix the prefix of the attribute path (for nested attributes)
* @param fieldName the name of the field
* @param model the attribute model
*/
private <T> void setAttributeModelDefaults(PropertyDescriptor descriptor, EntityModel<T> entityModel,
Class<?> parentClass, String prefix, String fieldName, AttributeModelImpl model) {
String displayName = org.dynamoframework.utils.StringUtils.propertyIdToHumanFriendly(fieldName, dynamoProperties.isCapitalizePropertyNames());
model.setDefaultDisplayName(displayName);
model.setHasSetterMethod(descriptor.getWriteMethod() != null);
model.setDefaultDescription(displayName);
model.setDefaultPrompt(displayName);
model.setSearchMode(SearchMode.NONE);
model.setName((prefix == null ? "" : (prefix + ".")) + fieldName);
model.setImage(false);
model.setEditableType(descriptor.isHidden() ? EditableType.READ_ONLY : EditableType.EDITABLE);
model.setSortable(true);
model.setPrecision(dynamoProperties.getDefaults().getDecimalPrecision());
model.setSearchCaseSensitive(dynamoProperties.getDefaults().isSearchCaseSensitive());
model.setSearchPrefixOnly(dynamoProperties.getDefaults().isSearchPrefixOnly());
model.setUrl(false);
model.setTrimSpaces(dynamoProperties.getDefaults().isTrimSpaces());
model.setType(descriptor.getPropertyType());
model.setDateType(determineDateType(model.getType()));
model.setDefaultDisplayFormat(determineDefaultDisplayFormat(model.getType()));
model.setNumberFieldMode(dynamoProperties.getDefaults().getNumberFieldMode());
model.setNumberFieldStep(1);
model.setLookupQueryType(QueryType.ID_BASED);
setRequiredAndMinMaxSetting(entityModel, model, parentClass, fieldName);
}
/**
* Overwrite the values of an attribute model with values from a message bundle
*
* @param entityModel the entity model
* @param attributeModel the attribute model implementation
*/
private <T> void setAttributeModelMessageBundleOverrides(EntityModel<T> entityModel,
AttributeModelImpl attributeModel) {
setStringSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.DISPLAY_NAME),
attributeModel::setDefaultDisplayName);
setStringSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.DESCRIPTION),
attributeModel::setDefaultDescription);
setStringSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.DEFAULT_VALUE),
attributeModel::setDefaultValue);
setStringSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.DEFAULT_SEARCH_VALUE),
attributeModel::setDefaultSearchValue);
setStringSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.DEFAULT_SEARCH_VALUE_FROM),
attributeModel::setDefaultSearchValueFrom);
setStringSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.DEFAULT_SEARCH_VALUE_TO),
attributeModel::setDefaultSearchValueTo);
setStringSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.DISPLAY_FORMAT),
attributeModel::setDefaultDisplayFormat);
setStringSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.TRUE_REPRESENTATION),
attributeModel::setDefaultTrueRepresentation);
setStringSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.FALSE_REPRESENTATION),
attributeModel::setDefaultFalseRepresentation);
setStringSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.CURRENCY_CODE),
attributeModel::setCurrencyCode);
setStringSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.LOOKUP_ENTITY_REFERENCE),
attributeModel::setLookupEntityReference);
setStringSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.NAVIGATION_LINK),
attributeModel::setNavigationLink);
setStringSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.AUTO_FILL_INSTRUCTIONS),
attributeModel::setAutofillInstructions);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.REQUIRED_FOR_SEARCHING),
attributeModel::setRequiredForSearching);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.SORTABLE),
attributeModel::setSortable);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.IMAGE),
attributeModel::setImage);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.DOWNLOAD_ALLOWED),
attributeModel::setDownloadAllowed);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.NEEDED_IN_DATA),
attributeModel::setNeededInData);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.SHOW_PASSWORD),
attributeModel::setShowPassword);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.NESTED_DETAILS),
attributeModel::setNestedDetails);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.SEARCH_CASE_SENSITIVE),
attributeModel::setSearchCaseSensitive);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.SEARCH_PREFIX_ONLY),
attributeModel::setSearchPrefixOnly);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.TRIM_SPACES),
attributeModel::setTrimSpaces);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.PERCENTAGE),
attributeModel::setPercentage);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.URL), attributeModel::setUrl);
// check for read only (convenience only, overwritten by "editable")
String msg = getAttributeMessage(entityModel, attributeModel, EntityModel.READ_ONLY);
if (!StringUtils.isEmpty(msg)) {
boolean editable = Boolean.parseBoolean(msg);
if (editable) {
attributeModel.setEditableType(EditableType.READ_ONLY);
} else {
attributeModel.setEditableType(EditableType.EDITABLE);
}
}
setEnumSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.EDITABLE), EditableType.class,
attributeModel::setEditableType);
msg = getAttributeMessage(entityModel, attributeModel, EntityModel.VISIBLE_IN_FORM);
if (!StringUtils.isEmpty(msg)) {
attributeModel.setVisibleInForm(isVisible(msg));
}
msg = getAttributeMessage(entityModel, attributeModel, EntityModel.VISIBLE_IN_GRID);
if (!StringUtils.isEmpty(msg)) {
attributeModel.setVisibleInGrid(isVisible(msg));
}
msg = getAttributeMessage(entityModel, attributeModel, EntityModel.SHOW_DETAILS_PAGINATOR);
if (!StringUtils.isEmpty(msg)) {
attributeModel.setShowDetailsPaginator(isVisible(msg));
}
// "searchable" also supports true/false for legacy reasons
msg = getAttributeMessage(entityModel, attributeModel, EntityModel.SEARCHABLE);
if (!StringUtils.isEmpty(msg)) {
if ("true".equals(msg)) {
attributeModel.setSearchMode(SearchMode.ALWAYS);
} else if ("false".equals(msg)) {
attributeModel.setSearchMode(SearchMode.NONE);
} else {
attributeModel.setSearchMode(SearchMode.valueOf(msg));
}
}
msg = getAttributeMessage(entityModel, attributeModel, EntityModel.ALLOWED_EXTENSIONS);
if (msg != null && !StringUtils.isEmpty(msg)) {
String[] extensions = msg.split(",");
Set<String> hashSet = Set.of(extensions);
attributeModel.setAllowedExtensions(hashSet);
}
msg = getAttributeMessage(entityModel, attributeModel, EntityModel.GROUP_TOGETHER_WITH);
if (msg != null && !StringUtils.isEmpty(msg)) {
String[] extensions = msg.split(",");
for (String s : extensions) {
attributeModel.addGroupTogetherWith(s);
}
}
setIntSettingIfAbove(getAttributeMessage(entityModel, attributeModel, EntityModel.PRECISION), -1,
attributeModel::setPrecision);
// multiple search setting - setting this to true also sets the search select
// mode to TOKEN
msg = getAttributeMessage(entityModel, attributeModel, EntityModel.MULTIPLE_SEARCH);
if (msg != null && !StringUtils.isEmpty(msg)) {
attributeModel.setMultipleSearch(Boolean.parseBoolean(msg));
attributeModel.setSearchSelectMode(AttributeSelectMode.MULTI_SELECT);
}
// set the select mode (also sets the search select mode and grid select mode)
msg = getAttributeMessage(entityModel, attributeModel, EntityModel.SELECT_MODE);
if (!StringUtils.isEmpty(msg)) {
AttributeSelectMode mode = AttributeSelectMode.valueOf(msg);
attributeModel.setSelectMode(mode);
attributeModel.setSearchSelectMode(mode);
}
setEnumSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.SEARCH_SELECT_MODE),
AttributeSelectMode.class, attributeModel::setSearchSelectMode);
setEnumSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.DATE_TYPE), AttributeDateType.class,
attributeModel::setDateType);
setEnumSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.TEXT_FIELD_MODE),
AttributeTextFieldMode.class, attributeModel::setTextFieldMode);
setEnumSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.BOOLEAN_FIELD_MODE),
AttributeBooleanFieldMode.class, attributeModel::setBooleanFieldMode);
setEnumSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.ELEMENT_COLLECTION_MODE),
ElementCollectionMode.class, attributeModel::setElementCollectionMode);
setEnumSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.LOOKUP_QUERY_TYPE),
QueryType.class, attributeModel::setLookupQueryType);
msg = getAttributeMessage(entityModel, attributeModel, EntityModel.MIN_VALUE);
if (!StringUtils.isEmpty(msg)) {
attributeModel.setMinValue(new BigDecimal(msg));
}
setIntSettingIfAbove(getAttributeMessage(entityModel, attributeModel, EntityModel.MIN_LENGTH), -1,
attributeModel::setMinLength);
setIntSettingIfAbove(getAttributeMessage(entityModel, attributeModel, EntityModel.MAX_LENGTH), -1,
attributeModel::setMaxLength);
setIntSettingIfAbove(getAttributeMessage(entityModel, attributeModel, EntityModel.MAX_LENGTH_IN_GRID), -1,
attributeModel::setMaxLengthInGrid);
msg = getAttributeMessage(entityModel, attributeModel, EntityModel.MAX_VALUE);
if (!StringUtils.isEmpty(msg)) {
attributeModel.setMaxValue(new BigDecimal(msg));
}
setStringSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.REPLACEMENT_SEARCH_PATH),
attributeModel::setReplacementSearchPath);
setStringSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.REPLACEMENT_SORT_PATH),
attributeModel::setReplacementSortPath);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.QUICK_ADD_ALLOWED),
attributeModel::setQuickAddAllowed);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.SEARCH_EXACT_VALUE),
attributeModel::setSearchForExactValue);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.NAVIGABLE),
attributeModel::setNavigable);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.SEARCH_DATE_ONLY),
attributeModel::setSearchDateOnly);
setBooleanSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.IGNORE_IN_SEARCH_FILTER),
attributeModel::setIgnoreInSearchFilter);
setEnumSetting(getAttributeMessage(entityModel, attributeModel, EntityModel.NUMBER_FIELD_MODE),
NumberFieldMode.class, attributeModel::setNumberFieldMode);
setIntSettingIfAbove(getAttributeMessage(entityModel, attributeModel, EntityModel.NUMBER_FIELD_STEP), -1,
attributeModel::setNumberFieldStep);
setMessageBundleCascadeOverrides(entityModel, attributeModel);
setMessageBundleCustomOverrides(entityModel, attributeModel);
}
/**
* Sets a value on an attribute model if the provided boolean value is false
*
* @param value the boolean value
* @param receiver the code that is executed to set the value
*/
private void setBooleanFalseSetting(Boolean value, Consumer<Boolean> receiver) {
if (!value) {
receiver.accept(false);
}
}
/**
* Sets a boolean setting if it is non-null
*
* @param value the value
* @param receiver the receiver function
*/
private void setBooleanSetting(String value, Consumer<Boolean> receiver) {
if (value != null) {
receiver.accept(Boolean.valueOf(value));
}
}
/**
* Sets a value on the attribute model if the provided boolean value is true
*
* @param value the boolean value
* @param receiver the code that is executed to set the value
*/
private void setBooleanTrueSetting(Boolean value, Consumer<Boolean> receiver) {
if (value) {
receiver.accept(true);
}
}
/**
* Sets the default value on the attribute model (translates a String to the
* appropriate type)
*
* @param model the attribute model
* @param defaultValue the default value to set
* @param search whether we are dealing with search mode
* @param
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private void setDefaultValue(AttributeModelImpl model, String defaultValue, boolean search, Consumer<Object> consumer) {
if (model.getType().isEnum()) {
Class<? extends Enum> enumType = model.getType().asSubclass(Enum.class);
consumer.accept(Enum.valueOf(enumType, defaultValue));
} else if (DateUtils.isJava8DateType(model.getType())) {
Object object;
if (search && model.isSearchDateOnly()) {
object = DateUtils.createJava8Date(LocalDate.class, defaultValue,
dynamoProperties.getDefaults().getDateFormat());
} else {
object = DateUtils.createJava8Date(model.getType(), defaultValue,
DateUtils.getDefaultDisplayFormat(model.getType()));
}
consumer.accept(object);
} else if (Boolean.class.equals(model.getType()) || boolean.class.equals(model.getType())) {
consumer.accept(Boolean.valueOf(defaultValue));
} else {
consumer.accept(ClassUtils.instantiateClass(model.getType(), defaultValue));
}
}
/**
* Sets the default value of an attribute based on the annotation
*
* @param model the attribute model
* @param attribute the annotation
*/
private void setDefaultValue(AttributeModelImpl model, Attribute attribute) {
if (!StringUtils.isEmpty(attribute.defaultValue())) {
if (!AttributeType.BASIC.equals(model.getAttributeType())) {
throw new OCSRuntimeException("%s: setting a default value is only allowed for BASIC attributes"
.formatted(model.getName()));
}
String defaultValue = attribute.defaultValue();
setDefaultValue(model, defaultValue, false, model::setDefaultValue);
}
}
/**
* Sets the default search value of an attribute based on the annotation
*
* @param model the attribute model
* @param attribute the annotation
*/
private void setDefaultSearchValue(AttributeModelImpl model, Attribute attribute) {
if (!StringUtils.isEmpty(attribute.defaultSearchValue())) {
if (!AttributeType.BASIC.equals(model.getAttributeType())) {
throw new OCSRuntimeException("%s: setting a default search value is only allowed for BASIC attributes"
.formatted(model.getName()));
}
String defaultValue = attribute.defaultSearchValue();
setDefaultValue(model, defaultValue, true, model::setDefaultSearchValue);
}
}
/**
* Sets the default search value of an attribute based on the annotation
*
* @param model the attribute model
* @param attribute the annotation
*/
private void setDefaultSearchValueFrom(AttributeModelImpl model, Attribute attribute) {
if (!StringUtils.isEmpty(attribute.defaultSearchValueFrom())) {
if (!AttributeType.BASIC.equals(model.getAttributeType())) {
throw new OCSRuntimeException("%s: setting a default search from value is only allowed for BASIC attributes"
.formatted(model.getName()));
}
String defaultValue = attribute.defaultSearchValueFrom();
setDefaultValue(model, defaultValue, true, model::setDefaultSearchValueFrom);
}
}
/**
* Sets the default search value of an attribute based on the annotation
*
* @param model the attribute model
* @param attribute the annotation
*/
private void setDefaultSearchValueTo(AttributeModelImpl model, Attribute attribute) {
if (!StringUtils.isEmpty(attribute.defaultSearchValueTo())) {
if (!AttributeType.BASIC.equals(model.getAttributeType())) {
throw new OCSRuntimeException("%s: setting a default search to value is only allowed for BASIC attributes"
.formatted(model.getName()));
}
String defaultValue = attribute.defaultSearchValueTo();
setDefaultValue(model, defaultValue, true, model::setDefaultSearchValueTo);
}
}
/**
* Sets an enum field based on a string value from a message bundle
*
* @param <E> the type of the class
* @param value the string value
* @param enumClass the type of the enum
* @param receiver receiver function
*/
@SneakyThrows
@SuppressWarnings({"rawtypes", "unchecked"})
private <E extends Enum> void setEnumSetting(String value, Class<E> enumClass, Consumer<E> receiver) {
if (!StringUtils.isEmpty(value)) {
E enumValue = (E) Enum.valueOf(enumClass, value);
receiver.accept(enumValue);
}
}
/**
* Sets an enum value on the attribute model, unless the value is the specified
* excluded value
*
* @param <E> enum type parameter
* @param value the value
* @param exclude the value to exclude
* @param consumer consumer to call when the value is not equal to the excluded
* value
*/
private <E extends Enum<?>> void setEnumValueUnless(E value, E exclude, Consumer<E> consumer) {
if (!exclude.equals(value)) {
consumer.accept(value);
}
}
/**
* Sets an integer value on the attribute model if the value is above the
* specified limit
*
* @param value the integer value
* @param limit the lower limit
* @param receiver the receiver function
*/
private void setIntSetting(Integer value, int limit, Consumer<Integer> receiver) {
if (value != null && value > limit) {
receiver.accept(value);
}
}
/**
* Sets an integer value on the attribute model if the value is above the
* specified limit
*
* @param value the integer value
* @param limit the lower limit
* @param receiver the receiver function
*/
private void setIntSettingIfAbove(String value, int limit, Consumer<Integer> receiver) {
if (value == null) {
return;
}
int intValue = Integer.parseInt(value);
if (intValue > limit) {
receiver.accept(intValue);
}
}
/**
* Sets an integer value on the attribute model if the value is above the
* specified limit
*
* @param value the integer value
* @param limit the lower limit
* @param receiver the receiver function
*/
private void setIntSettingIfBelow(String value, int limit, Consumer<Integer> receiver) {
if (value == null) {
return;
}
int intValue = Integer.parseInt(value);
if (intValue < limit) {
receiver.accept(intValue);
}
}
/**
* Sets a long value on the attribute model if it is either above or below the
* specified limit
*
* @param value the value
* @param limit the limit
* @param above whether to check if the value is above the limit
* @param receiver the function to call if the condition is met
*/
private void setBigDecimalSetting(Double value, Double limit, boolean above, Consumer<BigDecimal> receiver) {
if (value != null && (above && value.compareTo(limit) > 0 || value.compareTo(limit) < 0)) {
receiver.accept(BigDecimal.valueOf(value));
}
}
/**
* Reads cascade settings for an attribute from the message bundle
*
* @param entityModel the entity model
* @param model the attribute model
*/
private void setMessageBundleCascadeOverrides(EntityModel<?> entityModel, AttributeModel model) {
String msg = getAttributeMessage(entityModel, model, EntityModel.CASCADE_OFF);
if (msg != null) {
// completely cancel all cascades for this attribute
model.removeCascades();
} else {
int cascadeIndex = 1;
msg = getAttributeMessage(entityModel, model, EntityModel.CASCADE + "." + cascadeIndex);
while (msg != null) {
String filter = getAttributeMessage(entityModel, model,
EntityModel.CASCADE_FILTER_PATH + "." + cascadeIndex);
// optional mode (defaults to BOTH when omitted)
String mode = getAttributeMessage(entityModel, model, EntityModel.CASCADE_MODE + "." + cascadeIndex);
if (filter != null && mode != null) {
model.addCascade(msg, filter, CascadeMode.valueOf(mode));
} else {
throw new OCSRuntimeException("Incomplete cascade definition for " + model.getPath());
}
cascadeIndex++;
msg = getAttributeMessage(entityModel, model, EntityModel.CASCADE + "." + cascadeIndex);
}
}
}
/**
* Adds custom setting overrides. These take the form of "custom.1",
* "customValue.1" and "customType.1"
*
* @param entityModel the entity model that is being processed
* @param model the attribute model
*/
private void setMessageBundleCustomOverrides(EntityModel<?> entityModel, AttributeModel model) {
int customIndex = 1;
String name = getAttributeMessage(entityModel, model, EntityModel.CUSTOM + "." + customIndex);
while (name != null) {
String value = getAttributeMessage(entityModel, model, EntityModel.CUSTOM_VALUE + "." + customIndex);
String type = getAttributeMessage(entityModel, model, EntityModel.CUSTOM_TYPE + "." + customIndex);
CustomType t = CustomType.STRING;
if (type != null) {
t = CustomType.valueOf(type);
}
if (value != null) {
if (CustomType.BOOLEAN.equals(t)) {
model.setCustomSetting(name, Boolean.valueOf(value));
} else if (CustomType.INT.equals(t)) {
model.setCustomSetting(name, Integer.parseInt(value));
} else {
model.setCustomSetting(name, value);
}
customIndex++;
name = getAttributeMessage(entityModel, model, EntityModel.CUSTOM + "." + customIndex);
}
}
}
/**
* Calculates the entity model for a nested property, recursively up until a
* certain depth
*
* @param parentEntityModel the parent entity model
* @param attributeModel the attribute model
*/
protected void setNestedEntityModel(EntityModel<?> parentEntityModel, AttributeModelImpl attributeModel) {
EntityModel<?> em = attributeModel.getEntityModel();
if (StringUtils.countMatches(em.getReference(), ".") < parentEntityModel.getNestingDepth()) {
Class<?> type = null;
// only needed for master and detail attributes
if (AttributeType.MASTER.equals(attributeModel.getAttributeType()) || AttributeType.EMBEDDED.equals(
attributeModel.getAttributeType())) {
type = attributeModel.getType();
} else if (AttributeType.DETAIL.equals(attributeModel.getAttributeType())) {
type = attributeModel.getMemberType();
}
if (type != null) {
String ref;
if (StringUtils.isEmpty(em.getReference())) {
ref = em.getEntityClass() + "." + attributeModel.getName();
} else {
ref = em.getReference() + "." + attributeModel.getName();
}
// if (type.equals(em.getEntityClass()) || !hasEntityModel(type, ref)) {
EntityModel<?> nestedModel = findModelFactory(ref, type).getModel(ref, type);
attributeModel.setNestedEntityModel(nestedModel);
// } else {
// //EntityModel<?> nestedModel = findModelFactory(ref, type).getModel(ref, type);
// //attributeModel.setNestedEntityModel(nestedModel);
// }
}
}
}
/**
* Sets the "required" setting on an attribute based on JPA validation
* annotations
*
* @param <T> the type parameter
* @param entityModel the entity model that the attribute model is part of
* @param model the attribute model
* @param parentClass the parent class
* @param fieldName the name of the field
*/
private <T> void setRequiredAndMinMaxSetting(EntityModel<T> entityModel, AttributeModelImpl model,
Class<?> parentClass, String fieldName) {
// determine if the attribute is required based on the @NotNull
// annotation
NotNull notNull = ClassUtils.getAnnotation(entityModel.getEntityClass(), fieldName, NotNull.class);
model.setRequired(notNull != null);
// also set to required when it is a collection with a size greater than 0
model.setAttributeType(determineAttributeType(parentClass, model));
Size size = ClassUtils.getAnnotation(entityModel.getEntityClass(), fieldName, Size.class);
if (size != null && size.min() > 0 && AttributeType.DETAIL.equals(model.getAttributeType())) {
model.setRequired(true);
}
// minimum and maximum size for collections
if (size != null && AttributeType.DETAIL.equals(model.getAttributeType())) {
model.setMinCollectionSize(size.min());
model.setMaxCollectionSize(size.max());
}
// minimum and maximum length based on the @Size annotation
if (model.getType() == String.class && size != null) {
model.setMinLength(size.min());
model.setMaxLength(size.max());
}
if (model.getAttributeType() == AttributeType.ELEMENT_COLLECTION && size != null) {
model.setMinCollectionSize(size.min());
model.setMaxCollectionSize(size.max());
}
Min min = ClassUtils.getAnnotation(entityModel.getEntityClass(), fieldName, Min.class);
if ((model.isNumerical() || model.getAttributeType() == AttributeType.ELEMENT_COLLECTION) && min != null) {
model.setMinValue(BigDecimal.valueOf(min.value()));
}
Max max = ClassUtils.getAnnotation(entityModel.getEntityClass(), fieldName, Max.class);
if ((model.isNumerical() || model.getAttributeType() == AttributeType.ELEMENT_COLLECTION) && max != null) {
model.setMaxValue(BigDecimal.valueOf(max.value()));
}
}
/**
* Sets the sort order on an entity model
*
* @param model the entity model
* @param sortOrderMsg the sort order from the message bundle
*/
protected <T> void setSortOrder(EntityModel<T> model, String sortOrderMsg) {
if (!StringUtils.isEmpty(sortOrderMsg)) {
String[] tokens = sortOrderMsg.split(",");
for (String token : tokens) {
String[] sd = token.trim().split(" ");
if (sd.length > 0 && !StringUtils.isEmpty(sd[0]) && model.getAttributeModel(sd[0]) != null) {
model.getSortOrder().put(model.getAttributeModel(sd[0]),
sd.length == 1 || (!"DESC".equalsIgnoreCase(sd[1]) && !"DSC".equalsIgnoreCase(sd[1])));
}
}
}
}
/**
* Sets a String field value if the provided argument is not empty
*
* @param value the string value
* @param receiver the code that is executed to set the value
*/
private void setStringSetting(String value, Consumer<String> receiver) {
if (!StringUtils.isEmpty(value)) {
receiver.accept(value);
}
}
/**
* Sets a String field value if the provided argument is not empty
*
* @param value the string value
* @param receiver the code that is executed to set the value
*/
private void setStringListSetting(List<String> value, Consumer<List<String>> receiver) {
if (value != null && !value.isEmpty()) {
receiver.accept(value);
}
}
/**
* Indicates whether to skip an attribute since it does not constitute an actual
* property but rather a generic or technical field that all entities have
*
* @param name the name of the attribute
* @return true if this is case, false otherwise
*/
private boolean skipAttribute(String name) {
return CLASS.equals(name) || VERSION.equals(name);
}
/**
* Validates an attribute model, by checking for illegal combinations of
* settings
*
* @param model the attribute model to check
*/
private void validateAttributeModel(AttributeModel model) {
// multiple select fields not allowed for some attribute types
if (AttributeSelectMode.MULTI_SELECT.equals(model.getSelectMode())
&& (!AttributeType.DETAIL.equals(model.getAttributeType()))) {
throw new OCSRuntimeException("Multi-select field not allowed for attribute %s".formatted(model.getName()));
}
if (AttributeSelectMode.MULTI_SELECT.equals(model.getSearchSelectMode())
&& !(AttributeType.DETAIL.equals(model.getAttributeType()) || isMultiSelectMaster(model))) {
throw new OCSRuntimeException("Multi-select field not allowed for attribute %s".formatted(model.getName()));
}
if (AttributeSelectMode.AUTO_COMPLETE.equals(model.getSelectMode())
&& AttributeType.DETAIL.equals(model.getAttributeType())) {
throw new OCSRuntimeException("Auto-complete field not allowed for attribute %s".formatted(model.getName()));
}
if (AttributeSelectMode.COMBO.equals(model.getSelectMode())
&& AttributeType.DETAIL.equals(model.getAttributeType())) {
throw new OCSRuntimeException("Combo box not allowed for attribute %s".formatted(model.getName()));
}
if (AttributeSelectMode.COMBO.equals(model.getSearchSelectMode())
&& model.isMultipleSearch()) {
throw new OCSRuntimeException("Combo box not allowed for multiple search for attribute %s".formatted(model.getName()));
}
if (AttributeSelectMode.AUTO_COMPLETE.equals(model.getSearchSelectMode())
&& model.isMultipleSearch()) {
throw new OCSRuntimeException("Auto-complete field not allowed for multiple search for attribute %s".formatted(model.getName()));
}
// navigating only allowed in case of a many-to-one relation
if (!AttributeType.MASTER.equals(model.getAttributeType()) && model.isNavigable()) {
throw new OCSRuntimeException("Navigation is not possible for attribute %s".formatted(model.getName()));
}
// searching on a LOB is pointless
if (AttributeType.LOB.equals(model.getAttributeType()) && model.isSearchable()) {
throw new OCSRuntimeException("Searching on a LOB is not allowed for attribute %s".formatted(model.getName()));
}
// "search date only" is only supported for date/time fields
if (model.isSearchDateOnly() && !LocalDateTime.class.equals(model.getType())
&& !Instant.class.equals(model.getType())) {
throw new OCSRuntimeException("SearchDateOnly is not allowed for attribute %s".formatted(model.getName()));
}
// field cannot be percentage and currency at the same time
if (model.isPercentage() && !StringUtils.isEmpty(model.getCurrencyCode())) {
throw new OCSRuntimeException("%s is not allowed to be both a percentage and a currency".formatted(model.getName()));
}
// element collection only supported for strings or integral numbers
if (model.getAttributeType() == AttributeType.ELEMENT_COLLECTION && (!model.getMemberType().equals(String.class)
&& !NumberUtils.isLong(model.getMemberType()) && !NumberUtils.isInteger(model.getMemberType()))) {
throw new OCSRuntimeException("Element collection for %s is not allowed (not a String or an integral number)"
.formatted(model.getName()));
}
}
/**
* Validates the "group together with" settings for all attributes in the
* specified entity model
*
* @param <T> type parameter, type of the class managed by the entity model
* @param entityModel the entity model
*/
private <T> void validateGroupTogetherSettings(EntityModel<T> entityModel) {
Set<String> alreadyUsed = new HashSet<>();
// check if there aren't any illegal "group together" settings
for (AttributeModel am : entityModel.getAttributeModels()) {
alreadyUsed.add(am.getName());
if (!am.getGroupTogetherWith().isEmpty()) {
for (String together : am.getGroupTogetherWith()) {
if (alreadyUsed.contains(together)) {
AttributeModel other = entityModel.getAttributeModel(together);
if (together != null) {
((AttributeModelImpl) other).setAlreadyGrouped(true);
throw new OCSRuntimeException("Incorrect groupTogetherWith found: %s refers to %s".formatted(am.getName(), together));
}
}
}
}
}
}
private boolean isMultiSelectMaster(AttributeModel attributeModel) {
return attributeModel.getAttributeType() == AttributeType.MASTER
&& attributeModel.isMultipleSearch();
}
}