All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.croz.nrich.search.parser.SearchDataParser Maven / Gradle / Ivy

/*
 *  Copyright 2020-2023 CROZ d.o.o, the original author or authors.
 *
 *  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
 *
 *      https://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 net.croz.nrich.search.parser;

import lombok.RequiredArgsConstructor;
import net.croz.nrich.search.api.model.operator.DefaultSearchOperator;
import net.croz.nrich.search.api.model.operator.SearchOperator;
import net.croz.nrich.search.api.model.operator.SearchOperatorOverride;
import net.croz.nrich.search.api.model.property.SearchPropertyConfiguration;
import net.croz.nrich.search.api.model.property.SearchPropertyMapping;
import net.croz.nrich.search.bean.MapSupportingDirectFieldAccessFallbackBeanWrapper;
import net.croz.nrich.search.model.AttributeHolder;
import net.croz.nrich.search.model.AttributeHolderWithPath;
import net.croz.nrich.search.model.Restriction;
import net.croz.nrich.search.model.SearchDataParserConfiguration;
import net.croz.nrich.search.support.JpaEntityAttributeResolver;
import net.croz.nrich.search.util.FieldExtractionUtil;
import net.croz.nrich.search.util.PathResolvingUtil;
import net.croz.nrich.search.util.PropertyNameUtil;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import jakarta.persistence.metamodel.Attribute;
import jakarta.persistence.metamodel.ManagedType;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

@RequiredArgsConstructor
public class SearchDataParser {

    private final ManagedType managedType;

    private final Object searchData;

    private final SearchDataParserConfiguration searchConfiguration;

    public Set resolveRestrictionList() {
        return resolveRestrictionList(null);
    }

    public Set resolveRestrictionList(String propertyPrefix) {
        return resolveRestrictionListInternal(new MapSupportingDirectFieldAccessFallbackBeanWrapper(searchData), propertyPrefix, null, managedType, new HashSet<>(), false);
    }

    private Set resolveRestrictionListInternal(MapSupportingDirectFieldAccessFallbackBeanWrapper wrapper, String propertyPrefix, String path,
                                                            ManagedType managedType, Set restrictionList, boolean isPluralAttribute) {
        List fieldNameList = resolveFieldNameList(wrapper);
        JpaEntityAttributeResolver attributeResolver = new JpaEntityAttributeResolver(managedType);

        fieldNameList.forEach(originalFieldName -> {
            String fieldNameWithoutPrefixAndSuffix = fieldNameWithoutSuffixAndPrefix(originalFieldName, propertyPrefix);
            Object value = wrapper.getPropertyValue(originalFieldName);

            if (shouldSkipValue(value)) {
                return;
            }

            AttributeHolder attributeHolder = attributeResolver.resolveAttributeByPath(fieldNameWithoutPrefixAndSuffix);

            if (attributeHolder.isFound()) {
                String currentPath = resolveCurrentPath(path, fieldNameWithoutPrefixAndSuffix);

                if (attributeHolder.getManagedType() != null) {
                    MapSupportingDirectFieldAccessFallbackBeanWrapper currentWrapper = new MapSupportingDirectFieldAccessFallbackBeanWrapper(value);

                    resolveRestrictionListInternal(currentWrapper, propertyPrefix, currentPath, attributeHolder.getManagedType(), restrictionList, attributeHolder.isPlural());
                    return;
                }

                // element collections have null managed type but should be treated as plural attributes
                boolean isCurrentAttributePlural = attributeHolder.isElementCollection() || isPluralAttribute;

                restrictionList.add(createAttributeRestriction(attributeHolder.getAttribute().getJavaType(), originalFieldName, currentPath, value, isCurrentAttributePlural));
            }
            else if (searchUsingPropertyMapping(searchConfiguration)) {
                AttributeHolderWithPath attributeWithPath = resolveAttributeFromSearchConfigurationOrPrefix(attributeResolver, originalFieldName);

                if (attributeWithPath.isFound()) {
                    attributeHolder = attributeWithPath.getAttributeHolder();
                    restrictionList.add(createAttributeRestriction(attributeHolder.getAttribute().getJavaType(), originalFieldName, attributeWithPath.getPath(), value, attributeHolder.isPlural()));
                }
            }
        });

        return restrictionList;
    }

    private List resolveFieldNameList(MapSupportingDirectFieldAccessFallbackBeanWrapper wrapper) {
        List configuredIgnoredFieldList = searchConfiguration.getSearchPropertyConfiguration().getSearchIgnoredPropertyList();
        List ignoredFieldList = configuredIgnoredFieldList == null ? Collections.emptyList() : configuredIgnoredFieldList;

        if (wrapper.getEntityAsMap() != null) {
            return wrapper.getEntityAsMap().keySet().stream()
                .filter(key -> !ignoredFieldList.contains(key))
                .collect(Collectors.toList());
        }

        return FieldExtractionUtil.getAllFields(wrapper.getRootClass()).stream()
            .filter(field -> shouldIncludeField(ignoredFieldList, field))
            .map(Field::getName)
            .collect(Collectors.toList());
    }

    private boolean shouldIncludeField(List ignoredFieldList, Field field) {
        return !(ignoredFieldList.contains(field.getName()) || field.isSynthetic()
            || Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers()));
    }

    private String fieldNameWithoutSuffixAndPrefix(String originalFieldName, String prefix) {
        String fieldName = PropertyNameUtil.propertyNameWithoutSuffix(originalFieldName, searchConfiguration.getSearchPropertyConfiguration());

        if (prefix != null && fieldName.length() > prefix.length()) {
            return StringUtils.uncapitalize(fieldName.substring(prefix.length()));
        }

        return fieldName;
    }

    private boolean shouldSkipValue(Object value) {
        boolean skipValue;
        if (value instanceof Collection) {
            skipValue = CollectionUtils.isEmpty((Collection) value);
        }
        else {
            skipValue = value == null;
        }

        return skipValue;
    }

    private Restriction createAttributeRestriction(Class attributeType, String attributeName, String path, Object value, boolean isPluralAttribute) {
        boolean isRangeSearchSupported = isRangeSearchSupported(attributeType);
        SearchOperator resolvedOperator = resolveFromSearchConfiguration(searchConfiguration, path, attributeType);
        SearchPropertyConfiguration searchPropertyConfiguration = searchConfiguration.getSearchPropertyConfiguration();

        SearchOperator operator = DefaultSearchOperator.EQ;
        if (resolvedOperator != null) {
            operator = resolvedOperator;
        }
        else if (Collection.class.isAssignableFrom(value.getClass())) {
            operator = DefaultSearchOperator.IN;
        }
        else if (String.class.isAssignableFrom(attributeType)) {
            operator = DefaultSearchOperator.ILIKE;
        }
        else if (isRangeSearchSupported) {
            if (attributeName.endsWith(searchPropertyConfiguration.getRangeQueryFromIncludingSuffix())) {
                operator = DefaultSearchOperator.GE;
            }
            else if (attributeName.endsWith(searchPropertyConfiguration.getRangeQueryFromSuffix())) {
                operator = DefaultSearchOperator.GT;
            }
            else if (attributeName.endsWith(searchPropertyConfiguration.getRangeQueryToIncludingSuffix())) {
                operator = DefaultSearchOperator.LE;
            }
            else if (attributeName.endsWith(searchPropertyConfiguration.getRangeQueryToSuffix())) {
                operator = DefaultSearchOperator.LT;
            }
        }

        return new Restriction(path, operator, value, isPluralAttribute);
    }

    private boolean isRangeSearchSupported(Class attributeType) {
        return searchConfiguration.getSearchPropertyConfiguration().getRangeQuerySupportedClassList() != null
            && searchConfiguration.getSearchPropertyConfiguration().getRangeQuerySupportedClassList().stream().anyMatch(type -> type.isAssignableFrom(attributeType));
    }

    private String resolveCurrentPath(String path, String fieldNameWithoutPrefixAndSuffix) {
        return path == null ? fieldNameWithoutPrefixAndSuffix : PathResolvingUtil.joinPath(path, fieldNameWithoutPrefixAndSuffix);
    }

    private AttributeHolderWithPath resolveAttributeFromSearchConfigurationOrPrefix(JpaEntityAttributeResolver attributeResolver, String originalFieldName) {
        String mappedPath = findPathUsingMapping(searchConfiguration.getPropertyMappingList(), originalFieldName);

        if (mappedPath == null) {
            return resolveAttributeByPrefix(attributeResolver, originalFieldName, new ArrayList<>());
        }

        return new AttributeHolderWithPath(mappedPath, attributeResolver.resolveAttributeByPath(mappedPath));
    }

    private AttributeHolderWithPath resolveAttributeByPrefix(JpaEntityAttributeResolver attributeResolver, String path, List previousPathList) {
        String mappedPath = findPathUsingAttributePrefix(path, attributeResolver.getManagedType());

        if (mappedPath == null) {
            return AttributeHolderWithPath.notFound();
        }

        AttributeHolder attributeHolder = attributeResolver.resolveAttributeByPath(mappedPath);

        if (attributeHolder.isFound()) {
            String fullPath = PathResolvingUtil.joinPath(previousPathList, mappedPath);

            return new AttributeHolderWithPath(fullPath, attributeHolder);
        }
        else {
            String[] currentPath = PathResolvingUtil.convertToPathList(mappedPath);
            String currentPrefix = currentPath[0];
            attributeHolder = attributeResolver.resolveAttributeByPath(currentPrefix);

            if (attributeHolder.isFound()) {
                String leftOverPath = PathResolvingUtil.removeFirstPathElement(currentPath);
                previousPathList.add(currentPrefix);

                return resolveAttributeByPrefix(new JpaEntityAttributeResolver(attributeHolder.getManagedType()), leftOverPath, previousPathList);
            }
        }

        return AttributeHolderWithPath.notFound();
    }

    private String findPathUsingMapping(List propertyMappingList, String fieldName) {
        return Optional.ofNullable(propertyMappingList)
            .orElse(Collections.emptyList())
            .stream()
            .filter(mapping -> fieldName.equals(mapping.getName()))
            .map(SearchPropertyMapping::getPath)
            .findAny()
            .orElse(null);
    }

    private String findPathUsingAttributePrefix(String originalFieldName, ManagedType managedType) {
        List attributeNameList = managedType.getAttributes().stream()
            .filter(attribute -> attribute.isAssociation() || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED)
            .map(Attribute::getName)
            .collect(Collectors.toList());

        return attributeNameList.stream()
            .filter(attribute -> isFieldNameValid(originalFieldName, attribute))
            .map(attribute -> PathResolvingUtil.joinPath(attribute, StringUtils.uncapitalize(originalFieldName.substring(attribute.length()))))
            .findFirst()
            .orElse(null);
    }

    private boolean searchUsingPropertyMapping(SearchDataParserConfiguration searchConfiguration) {
        return searchConfiguration.isResolvePropertyMappingUsingPrefix() || searchConfiguration.getPropertyMappingList() != null;
    }

    private SearchOperator resolveFromSearchConfiguration(SearchDataParserConfiguration searchConfiguration, String path, Class attributeType) {
        SearchOperator operator = null;
        SearchOperatorOverride operatorOverride = findOperatorOverride(searchConfiguration.getSearchOperatorOverrideList(), value -> path.equals(value.getPropertyPath()));

        if (operatorOverride == null) {
            Predicate operatorOverridePredicate = value -> value.getPropertyType() != null && attributeType.isAssignableFrom(value.getPropertyType());

            operatorOverride = findOperatorOverride(searchConfiguration.getSearchOperatorOverrideList(), operatorOverridePredicate);
        }

        if (operatorOverride != null) {
            operator = operatorOverride.getSearchOperator();
        }

        return operator;
    }

    private SearchOperatorOverride findOperatorOverride(List searchOperatorOverrideList, Predicate searchOperatorOverridePredicate) {
        return Optional.ofNullable(searchOperatorOverrideList)
            .orElse(Collections.emptyList())
            .stream()
            .filter(searchOperatorOverridePredicate)
            .findFirst()
            .orElse(null);
    }

    private boolean isFieldNameValid(String fieldName, String attribute) {
        return fieldName.startsWith(attribute) && fieldName.length() > attribute.length();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy