com.haulmont.cuba.core.sys.FetchGroupManager.java Source code

Java tutorial

Introduction

Here is the source code for com.haulmont.cuba.core.sys.FetchGroupManager.java

Source

/*
 * Copyright (c) 2008-2016 Haulmont.
 *
 * 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.
 *
 */

package com.haulmont.cuba.core.sys;

import com.haulmont.bali.util.Preconditions;
import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.chile.core.model.MetaProperty;
import com.haulmont.chile.core.model.MetaPropertyPath;
import com.haulmont.chile.core.model.Range;
import com.haulmont.cuba.core.entity.BaseUuidEntity;
import com.haulmont.cuba.core.entity.EmbeddableEntity;
import com.haulmont.cuba.core.entity.Entity;
import com.haulmont.cuba.core.entity.SoftDelete;
import com.haulmont.cuba.core.global.*;
import org.apache.commons.lang.StringUtils;
import org.eclipse.persistence.config.QueryHints;
import org.eclipse.persistence.jpa.JpaQuery;
import org.eclipse.persistence.queries.AttributeGroup;
import org.eclipse.persistence.queries.FetchGroup;
import org.eclipse.persistence.queries.LoadGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;

import javax.annotation.Nullable;
import javax.inject.Inject;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;

@Component(FetchGroupManager.NAME)
public class FetchGroupManager {

    public static final String NAME = "cuba_FetchGroupManager";

    private final Logger log = LoggerFactory.getLogger(FetchGroupManager.class);

    @Inject
    private Metadata metadata;

    @Inject
    private MetadataTools metadataTools;

    @Inject
    private ViewRepository viewRepository;

    public void setView(JpaQuery query, String queryString, @Nullable View view, boolean singleResultExpected) {
        Preconditions.checkNotNullArgument(query, "query is null");
        if (view != null) {
            AttributeGroup ag = view.loadPartialEntities() ? new FetchGroup() : new LoadGroup();
            applyView(query, queryString, ag, view, singleResultExpected);
        } else {
            query.setHint(QueryHints.FETCH_GROUP, null);
        }
    }

    public void addView(JpaQuery query, String queryString, View view, boolean singleResultExpected) {
        Preconditions.checkNotNullArgument(query, "query is null");
        Preconditions.checkNotNullArgument(view, "view is null");

        Map<String, Object> hints = query.getHints();
        AttributeGroup ag = null;
        if (view.loadPartialEntities()) {
            if (hints != null)
                ag = (FetchGroup) hints.get(QueryHints.FETCH_GROUP);
            if (ag == null)
                ag = new FetchGroup();
        } else {
            if (hints != null)
                ag = (LoadGroup) hints.get(QueryHints.LOAD_GROUP);
            if (ag == null)
                ag = new LoadGroup();
        }

        applyView(query, queryString, ag, view, singleResultExpected);
    }

    private void applyView(JpaQuery query, String queryString, AttributeGroup attrGroup, View view,
            boolean singleResultExpected) {

        boolean useFetchGroup = attrGroup instanceof FetchGroup;

        Set<FetchGroupField> fetchGroupFields = new LinkedHashSet<>();
        processView(view, null, fetchGroupFields, useFetchGroup);

        Set<String> fetchGroupAttributes = new TreeSet<>();
        Map<String, String> fetchHints = new TreeMap<>(); // sort hints by attribute path

        for (FetchGroupField field : fetchGroupFields) {
            fetchGroupAttributes.add(field.path());
        }
        if (attrGroup instanceof FetchGroup)
            ((FetchGroup) attrGroup).setShouldLoadAll(true);

        List<FetchGroupField> refFields = new ArrayList<>();
        for (FetchGroupField field : fetchGroupFields) {
            if (field.metaProperty.getRange().isClass() && !metadataTools.isEmbedded(field.metaProperty))
                refFields.add(field);
        }

        boolean hasBatches = false;

        MetaClass metaClass = metadata.getClassNN(view.getEntityClass());
        if (!refFields.isEmpty()) {
            String alias = QueryTransformerFactory.createParser(queryString).getEntityAlias();

            List<FetchGroupField> batchFields = new ArrayList<>();
            List<FetchGroupField> joinFields = new ArrayList<>();

            for (FetchGroupField refField : refFields) {
                if (refField.fetchMode == FetchMode.UNDEFINED) {
                    if (refField.metaProperty.getRange().getCardinality().isMany()) {
                        List<String> masterAttributes = getMasterEntityAttributes(fetchGroupFields, refField,
                                useFetchGroup);
                        fetchGroupAttributes.addAll(masterAttributes);
                    }
                    continue;
                }

                boolean selfRef = false;
                for (MetaProperty mp : refField.metaPropertyPath.getMetaProperties()) {
                    if (!mp.getRange().getCardinality().isMany()) {
                        MetaClass mpClass = mp.getRange().asClass();
                        if (metadataTools.isAssignableFrom(mpClass, metaClass)
                                || metadataTools.isAssignableFrom(metaClass, mpClass)) {
                            batchFields.add(refField);
                            selfRef = true;
                            break;
                        }
                    }
                }

                if (!selfRef) {
                    if (refField.metaProperty.getRange().getCardinality().isMany()) {
                        List<String> masterAttributes = getMasterEntityAttributes(fetchGroupFields, refField,
                                useFetchGroup);
                        fetchGroupAttributes.addAll(masterAttributes);

                        if (refField.fetchMode == FetchMode.JOIN) {
                            joinFields.add(refField);
                        } else {
                            batchFields.add(refField);
                        }
                    } else {
                        if (refField.fetchMode == FetchMode.BATCH) {
                            batchFields.add(refField);
                        } else {
                            joinFields.add(refField);
                        }
                    }
                }
            }

            for (FetchGroupField joinField : new ArrayList<>(joinFields)) {
                // adjust fetch mode according to parent attributes
                if (joinField.fetchMode == FetchMode.AUTO) {
                    Optional<FetchMode> parentMode = refFields.stream()
                            .filter(f -> joinField.metaPropertyPath.startsWith(f.metaPropertyPath)
                                    && joinField.fetchMode != FetchMode.JOIN)
                            .sorted((f1, f2) -> f1.metaPropertyPath.getPath().length
                                    - f2.metaPropertyPath.getPath().length)
                            .findFirst().map(f -> f.fetchMode);
                    if (parentMode.isPresent() && parentMode.get() == FetchMode.UNDEFINED) {
                        joinFields.remove(joinField);
                    } else {
                        for (FetchGroupField batchField : new ArrayList<>(batchFields)) {
                            if (joinField.metaPropertyPath.startsWith(batchField.metaPropertyPath)) {
                                joinFields.remove(joinField);
                                batchFields.add(joinField);
                            }
                        }
                    }
                }
            }

            QueryParser parser = QueryTransformerFactory.createParser(queryString);

            List<FetchGroupField> isNullFields = joinFields.stream()
                    .filter(f -> f.fetchMode == FetchMode.AUTO && parser.hasIsNullCondition(f.path()))
                    .collect(Collectors.toList());
            if (!isNullFields.isEmpty()) {
                for (Iterator<FetchGroupField> fieldIt = joinFields.iterator(); fieldIt.hasNext();) {
                    FetchGroupField joinField = fieldIt.next();
                    boolean isNullField = isNullFields.stream()
                            .anyMatch(f -> joinField == f || f.fetchMode == FetchMode.AUTO
                                    && joinField.metaPropertyPath.startsWith(f.metaPropertyPath));
                    if (isNullField) {
                        fieldIt.remove();
                        fetchGroupAttributes.removeIf(attr -> attr.startsWith(joinField.path() + "."));
                    }
                }
            }

            long toManyCount = refFields.stream().filter(f -> f.metaProperty.getRange().getCardinality().isMany())
                    .count();

            // For query by ID, remove BATCH mode for to-many attributes that have no nested attributes
            if (singleResultExpected && toManyCount <= 1) {
                for (FetchGroupField batchField : new ArrayList<>(batchFields)) {
                    if (batchField.metaProperty.getRange().getCardinality().isMany()) {
                        boolean hasNested = refFields.stream().anyMatch(
                                f -> f != batchField && f.metaPropertyPath.startsWith(batchField.metaPropertyPath));
                        if (!hasNested && batchField.fetchMode != FetchMode.BATCH) {
                            batchFields.remove(batchField);
                        }
                    }
                }
            }

            //Find many-to-many fields with cycle loading same: {E}.b.a.b, where b of type {E}.
            //If {E}.b BATCH, {E}.b.a BATCH and {E}.b.a.b BATCH then same query used simultaneously
            //while loading {E}.b and {E}.b.a.b, so result of batch query is incorrect.
            //Remove this fields from BATCH processing
            for (FetchGroupField refField : refFields) {
                if (refField.fetchMode == FetchMode.AUTO
                        && refField.metaProperty.getRange().getCardinality() == Range.Cardinality.MANY_TO_MANY) {
                    //find property {E}.a.b for {E}.a where b of type {E}
                    List<FetchGroupField> selfRefs = refFields.stream()
                            .filter(f -> isTransitiveSelfReference(refField, f)).collect(Collectors.toList());
                    for (FetchGroupField selfRef : selfRefs) {
                        List<FetchGroupField> secondLevelSelfRefs = refFields.stream()
                                .filter(f -> isTransitiveSelfReference(selfRef, f)).collect(Collectors.toList());
                        for (FetchGroupField f : secondLevelSelfRefs) {
                            batchFields.remove(f);
                            batchFields.remove(selfRef);
                            batchFields.remove(refField);
                        }
                    }
                }
            }

            for (FetchGroupField joinField : joinFields) {
                String attr = alias + "." + joinField.path();
                fetchHints.put(attr, QueryHints.LEFT_FETCH);
            }

            for (FetchGroupField batchField : batchFields) {
                if (batchField.fetchMode == FetchMode.BATCH || !singleResultExpected || batchFields.size() > 1) {
                    String attr = alias + "." + batchField.path();
                    fetchHints.put(attr, QueryHints.BATCH);
                    hasBatches = true;
                }
            }
        }

        if (log.isTraceEnabled())
            log.trace((useFetchGroup ? "Fetch" : "Load") + " group for " + view + ":\n"
                    + fetchGroupAttributes.stream().collect(Collectors.joining("\n")));
        for (String attribute : fetchGroupAttributes) {
            attrGroup.addAttribute(attribute);
        }

        if (!metadataTools.isCacheable(metaClass)) {
            query.setHint(useFetchGroup ? QueryHints.FETCH_GROUP : QueryHints.LOAD_GROUP, attrGroup);
        }

        if (log.isDebugEnabled()) {
            String fetchModes = fetchHints.entrySet().stream()
                    .map(e -> e.getKey() + "=" + (e.getValue().equals(QueryHints.LEFT_FETCH) ? "JOIN" : "BATCH"))
                    .collect(Collectors.joining(", "));
            log.debug("Fetch modes for " + view + ": " + (fetchModes.equals("") ? "<none>" : fetchModes));
        }
        for (Map.Entry<String, String> entry : fetchHints.entrySet()) {
            query.setHint(entry.getValue(), entry.getKey());
        }

        if (hasBatches) {
            query.setHint(QueryHints.BATCH_TYPE, "IN");
        }
    }

    private boolean isTransitiveSelfReference(FetchGroupField root, FetchGroupField current) {
        return root != current && current.fetchMode == FetchMode.AUTO
                && current.metaPropertyPath.startsWith(root.metaPropertyPath)
                && current.metaProperty.getRange().isClass()
                && current.metaProperty.getRange().getCardinality() == Range.Cardinality.MANY_TO_MANY
                && Objects.equals(current.metaProperty.getRange().asClass(), root.metaClass);
    }

    private List<String> getMasterEntityAttributes(Set<FetchGroupField> fetchGroupFields,
            FetchGroupField toManyField, boolean useFetchGroup) {
        List<String> result = new ArrayList<>();

        MetaClass propMetaClass = toManyField.metaProperty.getRange().asClass();
        propMetaClass.getProperties().stream()
                .filter(mp -> mp.getRange().isClass() && toManyField.metaProperty.getInverse() == mp).findFirst()
                .ifPresent(inverseProp -> {
                    if (useFetchGroup) {
                        for (FetchGroupField fetchGroupField : fetchGroupFields) {
                            // compare with original class, because in case of entity extension properties are remapped to extended entities
                            MetaClass inversePropRangeClass = metadata.getExtendedEntities()
                                    .getOriginalOrThisMetaClass(inverseProp.getRange().asClass());
                            if (fetchGroupField.metaClass.equals(toManyField.metaClass)
                                    // add only local properties
                                    && !fetchGroupField.metaProperty.getRange().isClass()
                            // do not add properties from subclasses
                                    && fetchGroupField.metaProperty.getDomain().equals(inversePropRangeClass)) {
                                String attribute = toManyField.path() + "." + inverseProp.getName() + "."
                                        + fetchGroupField.metaProperty.getName();
                                result.add(attribute);
                            }
                        }
                        if (result.isEmpty()) {
                            result.add(toManyField.path() + "." + inverseProp.getName() + ".id");
                        }
                    } else {
                        result.add(toManyField.path() + "." + inverseProp.getName());
                    }
                });

        return result;
    }

    private void processView(View view, FetchGroupField parentField, Set<FetchGroupField> fetchGroupFields,
            boolean useFetchGroup) {
        Class<? extends Entity> entityClass = view.getEntityClass();

        if (useFetchGroup) {
            // Always add SoftDelete properties to support EntityManager contract
            if (SoftDelete.class.isAssignableFrom(entityClass)) {
                for (String property : getInterfaceProperties(SoftDelete.class)) {
                    fetchGroupFields.add(createFetchGroupField(entityClass, parentField, property));
                }
            }

            // Always add uuid property if the entity has primary key not of type UUID
            if (!BaseUuidEntity.class.isAssignableFrom(entityClass)
                    && !EmbeddableEntity.class.isAssignableFrom(entityClass)) {
                MetaProperty uuidProp = metadata.getClassNN(entityClass).getProperty("uuid");
                if (uuidProp != null && metadataTools.isPersistent(uuidProp)) {
                    fetchGroupFields.add(createFetchGroupField(entityClass, parentField, "uuid"));
                }
            }
        }

        for (ViewProperty property : view.getProperties()) {
            String propertyName = property.getName();
            MetaClass metaClass = metadata.getClassNN(entityClass);
            MetaProperty metaProperty = metaClass.getPropertyNN(propertyName);

            if (metadataTools.isPersistent(metaProperty) && (metaProperty.getRange().isClass() || useFetchGroup)) {
                FetchGroupField field = createFetchGroupField(entityClass, parentField, propertyName,
                        property.getFetchMode());
                fetchGroupFields.add(field);
                if (property.getView() != null) {
                    if (ClassUtils.isPrimitiveOrWrapper(metaProperty.getJavaType())
                            || String.class.isAssignableFrom(metaProperty.getJavaType())) {
                        String message = "Wrong Views mechanism usage found. View%s is set for property \"%s\" of "
                                + "class \"%s\", but this property does not point to an Entity";

                        String propertyViewName = property.getView().getName();
                        propertyViewName = propertyViewName != null && !propertyViewName.isEmpty()
                                ? " \"" + propertyViewName + "\""
                                : "";

                        message = String.format(message, propertyViewName, property.getName(), metaClass.getName());
                        throw new DevelopmentException(message);
                    }

                    processView(property.getView(), field, fetchGroupFields, useFetchGroup);
                }
            }

            List<String> relatedProperties = metadataTools.getRelatedProperties(entityClass, propertyName);
            for (String relatedProperty : relatedProperties) {
                MetaProperty relatedMetaProp = metaClass.getPropertyNN(relatedProperty);
                if (!view.containsProperty(relatedProperty)
                        && (relatedMetaProp.getRange().isClass() || useFetchGroup)) {
                    FetchGroupField field = createFetchGroupField(entityClass, parentField, relatedProperty);
                    fetchGroupFields.add(field);
                    if (relatedMetaProp.getRange().isClass()) {
                        View relatedView = viewRepository.getView(relatedMetaProp.getRange().asClass(),
                                View.MINIMAL);
                        processView(relatedView, field, fetchGroupFields, useFetchGroup);
                    }
                }
            }
        }
    }

    private List<String> getInterfaceProperties(Class<?> intf) {
        List<String> result = new ArrayList<>();
        for (Method method : intf.getDeclaredMethods()) {
            if (method.getName().startsWith("get") && method.getParameterTypes().length == 0) {
                result.add(StringUtils.uncapitalize(method.getName().substring(3)));
            }
        }
        return result;
    }

    private FetchGroupField createFetchGroupField(Class<? extends Entity> entityClass, FetchGroupField parentField,
            String property) {
        return createFetchGroupField(entityClass, parentField, property, FetchMode.AUTO);
    }

    private FetchGroupField createFetchGroupField(Class<? extends Entity> entityClass, FetchGroupField parentField,
            String property, FetchMode fetchMode) {
        MetaClass metaClass = getRealClass(entityClass, property);
        return new FetchGroupField(metaClass, parentField, property, getFetchMode(metaClass, fetchMode));
    }

    private FetchMode getFetchMode(MetaClass metaClass, FetchMode fetchMode) {
        return metadataTools.isCacheable(metaClass) ? FetchMode.UNDEFINED : fetchMode;
    }

    private MetaClass getRealClass(Class<? extends Entity> entityClass, String property) {
        // todo ?
        return metadata.getClassNN(entityClass);
    }

    protected static class FetchGroupField {
        private final MetaClass metaClass;
        private FetchMode fetchMode;
        private final MetaProperty metaProperty;
        private final MetaPropertyPath metaPropertyPath;

        public FetchGroupField(MetaClass metaClass, FetchGroupField parentField, String property,
                FetchMode fetchMode) {
            this.metaClass = metaClass;
            this.fetchMode = fetchMode;
            this.metaProperty = metaClass.getPropertyNN(property);
            this.metaPropertyPath = parentField == null ? new MetaPropertyPath(metaClass, metaProperty)
                    : new MetaPropertyPath(parentField.metaPropertyPath, metaProperty);
        }

        public String path() {
            return metaPropertyPath.toString();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            FetchGroupField that = (FetchGroupField) o;

            if (!metaClass.equals(that.metaClass))
                return false;
            if (!metaPropertyPath.equals(that.metaPropertyPath))
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = metaClass.hashCode();
            result = 31 * result + metaPropertyPath.hashCode();
            return result;
        }

        @Override
        public String toString() {
            return path();
        }
    }
}