EntityModelImpl.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 lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.apache.commons.lang3.StringUtils;
import org.dynamoframework.dao.FetchJoinInformation;
import org.dynamoframework.domain.model.*;
import org.dynamoframework.utils.ClassUtils;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.stream.Stream;

/**
 * An implementation of an entity model - holds metadata about an entity
 *
 * @param <T> the class of the entity
 * @author bas.rutten
 */
@Data
@EqualsAndHashCode(callSuper = false, of = {"reference", "entityClass"})
@Builder(toBuilder = true)
@ToString
public class EntityModelImpl<T> implements EntityModel<T> {

	private String autofillInstructions;

	@Builder.Default
	@ToString.Exclude
	private final Map<String, List<AttributeModel>> attributeModels = new LinkedHashMap<>();

	private String defaultDescription;

	private String defaultDisplayName;

	private String defaultDisplayNamePlural;

	@Builder.Default
	private Map<String, Optional<String>> descriptions = new ConcurrentHashMap<>();

	@Builder.Default
	private Map<String, Optional<String>> displayNames = new ConcurrentHashMap<>();

	@Builder.Default
	private Map<String, Optional<String>> displayNamesPlural = new ConcurrentHashMap<>();

	private String displayProperty;

	private Class<T> entityClass;

	private boolean gridOrderSet;

	private int nestingDepth;

	private String reference;

	private boolean searchOrderSet;

	private boolean listAllowed;

	private boolean exportAllowed;

	private boolean updateAllowed;

	private boolean deleteAllowed;

	private boolean createAllowed;

	private boolean searchAllowed;

	private String fileUploadEntityName;

	private String fileUploadAttributeName;

	private String fileUploadEntityIdPath;

	private List<FetchJoinInformation> fetchJoins;

	private List<FetchJoinInformation> detailJoins;

	private int maxSearchResults;

	private List<EntityModelAction> entityModelActions;

	@Builder.Default
	private Map<AttributeModel, Boolean> sortOrder = new LinkedHashMap<>();

	@Builder.Default
	private List<String> readRoles = new ArrayList<>();

	@Builder.Default
	private List<String> writeRoles = new ArrayList<>();

	@Builder.Default
	private List<String> deleteRoles = new ArrayList<>();

	@Override
	public void addAttributeGroup(String attributeGroup) {
		if (!attributeModels.containsKey(attributeGroup)) {
			attributeModels.put(attributeGroup, new ArrayList<>());
		}
	}

	public void addAttributeModel(String attributeGroup, AttributeModel model) {
		attributeModels.get(attributeGroup).add(model);
	}

	@Override
	public void addAttributeModel(String attributeGroup, AttributeModel model, AttributeModel existingModel) {
		List<AttributeModel> group = attributeModels.get(attributeGroup);
		if (group.contains(existingModel)) {
			group.add(group.indexOf(existingModel), model);
		} else {
			group.add(model);
		}
	}

	private Stream<AttributeModel> constructAttributeModelStream(Comparator<AttributeModel> comp) {
		return attributeModels.values().stream().flatMap(List::stream).sorted(comp);
	}

	private List<AttributeModel> filterAttributeModels(Predicate<AttributeModel> p) {
		return Collections
			.unmodifiableList(constructAttributeModelStream(Comparator.comparing(AttributeModel::getOrder))
				.filter(p).toList());
	}

	private AttributeModel findAttributeModel(Predicate<AttributeModel> p) {
		return constructAttributeModelStream(Comparator.comparing(AttributeModel::getOrder)).filter(p).findFirst()
			.orElse(null);
	}

	@Override
	public List<String> getAttributeGroups() {
		return new ArrayList<>(attributeModels.keySet());
	}

	@Override
	public AttributeModel getAttributeModel(String attributeName) {
		if (!StringUtils.isEmpty(attributeName)) {

			AttributeModel attributeModel = findAttributeModel(am -> am.getName().equals(attributeName));
			if (attributeModel != null) {
				return attributeModel;
			}

			// check for nested property
			String[] names = attributeName.split("\\.");
			if (names.length > 1) {
				// Find Attribute attributeModel
				AttributeModel am = getAttributeModel(names[0]);
				if (am != null) {
					// Find nested entity attributeModel
					EntityModel<?> nem = am.getNestedEntityModel();
					if (nem != null) {
						return nem.getAttributeModel(attributeName.substring(names[0].length() + 1));
					}
				}
			}
		}
		return null;
	}

	@Override
	public AttributeModel getAttributeModelByActualSortPath(String actualSortPath) {
		return findAttributeModel(m -> m.getActualSortPath().equals(actualSortPath));
	}

	@Override
	public List<AttributeModel> getAttributeModels() {
		List<AttributeModel> list = constructAttributeModelStream(Comparator.comparing(AttributeModel::getOrder))
			.toList();
		return Collections.unmodifiableList(list);
	}

	@Override
	public List<AttributeModel> getAttributeModelsForGroup(String group) {
		return Collections.unmodifiableList(attributeModels.get(group));
	}

	@Override
	public List<AttributeModel> getAttributeModelsForType(AttributeType attributeType, Class<?> type) {
		return filterAttributeModels(model -> {
			Class<?> rt = ClassUtils.getResolvedType(getEntityClass(), model.getName(), 0);
			return (attributeType == null || attributeType.equals(model.getAttributeType())) && (type == null
				|| type.isAssignableFrom(model.getType()) || (rt != null && type.isAssignableFrom(rt)));
		});
	}

	@Override
	public List<AttributeModel> getAttributeModelsSortedForGrid() {
		if (!gridOrderSet) {
			return getAttributeModels();
		}
		List<AttributeModel> list = constructAttributeModelStream(Comparator.comparing(AttributeModel::getGridOrder))
			.toList();
		return Collections.unmodifiableList(list);
	}

	@Override
	public List<AttributeModel> getAttributeModelsSortedForSearch() {
		if (!searchOrderSet) {
			return getAttributeModels();
		}
		List<AttributeModel> list = constructAttributeModelStream(Comparator.comparing(AttributeModel::getSearchOrder))
			.toList();
		return Collections.unmodifiableList(list);
	}

	@Override
	public List<AttributeModel> getCascadeAttributeModels() {
		List<AttributeModel> result = new ArrayList<>();
		for (AttributeModel model : getAttributeModels()) {
			if (!model.getCascadeAttributes().isEmpty()) {
				result.add(model);
			}

			// add nested models
			if (model.getNestedEntityModel() != null) {
				List<AttributeModel> nested = model.getNestedEntityModel().getCascadeAttributeModels();
				result.addAll(nested);
			}
		}
		return Collections.unmodifiableList(result);
	}

	@Override
	public String getDescription(Locale locale) {
		return lookup(descriptions, locale, EntityModel.DESCRIPTION, defaultDescription);
	}

	@Override
	public String getDisplayName(Locale locale) {
		return lookup(displayNames, locale, EntityModel.DISPLAY_NAME, defaultDisplayName);
	}

	@Override
	public String getDisplayNamePlural(Locale locale) {
		return lookup(displayNamesPlural, locale, EntityModel.DISPLAY_NAME_PLURAL, defaultDisplayNamePlural);
	}

	@Override
	public List<AttributeModel> getRequiredForSearchingAttributeModels() {
		List<AttributeModel> result = constructAttributeModelStream(Comparator.comparing(AttributeModel::getOrder))
			.map(m -> {
				List<AttributeModel> list = new ArrayList<>();
				if (m.isSearchable() && m.isRequiredForSearching()) {
					list.add(m);
				}
				// add nested models
				if (m.getNestedEntityModel() != null) {
					List<AttributeModel> nested = m.getNestedEntityModel().getRequiredForSearchingAttributeModels();
					list.addAll(nested);
				}
				return list;
			}).flatMap(List::stream).toList();
		return Collections.unmodifiableList(result);
	}

	@Override
	public boolean isAttributeGroupVisible(String group, boolean readOnly) {
		return attributeModels.get(group).stream()
			.anyMatch(am -> am.isVisibleInForm() && (readOnly || !am.getEditableType().equals(EditableType.READ_ONLY)));
	}

	/**
	 * Looks up a text message from a resource bundle
	 *
	 * @param source   the message cache
	 * @param locale   the desired locale
	 * @param key      the message key
	 * @param fallBack value to fall back to if no match is found
	 * @return the translation of the key
	 */
	private String lookup(Map<String, Optional<String>> source, Locale locale, String key, String fallBack) {
		// look up in message bundle and add to cache
		if (!source.containsKey(locale.toString())) {
			try {
				ResourceBundle rb = ResourceBundle.getBundle("META-INF/entitymodel", locale);
				String str = rb.getString(reference + "." + key);
				source.put(locale.toString(), Optional.of(str));
			} catch (MissingResourceException ex) {
				source.put(locale.toString(), Optional.empty());
			}
		}

		// look up or return fallback value
		Optional<String> optional = source.get(locale.toString());
		return optional.orElse(fallBack);
	}

	@Override
	public boolean usesDefaultGroupOnly() {
		return attributeModels.keySet().size() == 1
			&& attributeModels.keySet().iterator().next().equals(EntityModel.DEFAULT_GROUP);
	}

	@Override
	public Stream<AttributeModel> getAttributeModels(Predicate<AttributeModel> predicate) {
		return getAttributeModels().stream().filter(predicate::test);
	}

	@Override
	public EntityModelAction findAction(String actionId) {
		return entityModelActions.stream().filter(action -> action.getId().equals(actionId))
			.findFirst().orElse(null);
	}

	@Override
	public boolean hasEmbeddedAttributeModel(String property) {
		return !filterAttributeModels(am -> am.getName().startsWith(property + ".")).isEmpty();
	}

}