/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
//     ailitchev - Bug 244124 in 2.1 - Add AttributeGroup for nesting and LoadGroup support
package org.eclipse.persistence.queries;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.eclipse.persistence.core.queries.CoreAttributeGroup;
import org.eclipse.persistence.descriptors.FetchGroupManager;
import org.eclipse.persistence.internal.localization.ExceptionLocalization;
import org.eclipse.persistence.internal.queries.AttributeItem;
import org.eclipse.persistence.internal.queries.EntityFetchGroup;
import org.eclipse.persistence.sessions.Session;
import org.eclipse.persistence.sessions.UnitOfWork;

/**
 * A FetchGroup is a performance enhancement that allows a group of attributes
 * of an object to be loaded on demand, which means that the data for an
 * attribute might not loaded from the underlying data source until an explicit
 * access call for the attribute first occurs. It avoids loading all data of the
 * object's attributes, in which the user is interested in only a subset of
 * them. A great deal of caution and careful system use case analysis should be
 * use when using the fetch group feature, as the extra round-trip would well
 * offset the gain from the deferred loading in many cases.
 * <p>
 * FetchGroup usage is only possible when an entity class implements the
 * {@link FetchGroupTracker} interface so that the FetchGroup can be stored in
 * the entity. The entity must also use the provided check methods to ensure the
 * attributes are loaded prior to use. In general this support is enabled
 * through weaving of the entity classes. If an entity class does not implement
 * {@link FetchGroupTracker} no FetchGroup functionality will be supported and
 * attempted use of a FetchGroup in a query will not result in the expected
 * behavior.
 * <p>
 * FetchGroups are defined in 3 ways:
 * <ul>
 * <li>A {@link FetchGroupManager#getDefaultFetchGroup()} is created and stored
 * on the {@link FetchGroupManager} during metadata processing if any of the
 * basic ({@link org.eclipse.persistence.mappings.DirectToFieldMapping
 * DirectToFieldMapping}) are configured to be loaded directly.
 * <li>A named FetchGroup can be defined and added to the
 * {@link FetchGroupManager}. For JPA users this can be accomplished using
 * annotation (@FetchGroup) or in an eclipselink-orm.xml. For JPA and native
 * users named groups can be defined in code and added to the
 * {@link FetchGroupManager#addFetchGroup(FetchGroup)}. Adding named groups in
 * code is typically done in a {@link
 * org.eclipse.persistence.config.DescriptorCustomizer}and should be done
 * before the session is initialized at login. To use a named FetchGroup on a
 * query the native {@link ObjectLevelReadQuery#setFetchGroupName(String)} can
 * be used of for JPA users the {@link
 * org.eclipse.persistence.config.QueryHints#FETCH_GROUP_NAME} an be used.
 * <li>A dynamic FetchGroup can be created within the application and used on a
 * query. For native API usage this is done using
 * {@link ObjectLevelReadQuery#setFetchGroup(FetchGroup)} while JPA users
 * generally use the {@link org.eclipse.persistence.config.QueryHints#FETCH_GROUP}.
 * </ul>
 * <p>
 * When a query is executed only one FetchGroup will be used. The order of
 * precedence is:
 * <ol>
 * <li>If a FetchGroup is specified on a query it will be used.
 * <li>If no FetchGroup is specified but a FetchGroup name is specified and the
 * FetchGroupManager has a group by this name it will be used.
 * <li>If neither a FetchGroup nor a FetchGroup name is specified on the query
 * an the FetchGroupManager has a default group then it will be used.
 * <li>If none of these conditions are met then no FetchGroup will be used when
 * executing a query.
 * </ol><br>
 * <i>Note: This includes the execution of queries to populate lazy and eager
 * relationships.</i>
 * <p>
 * <b>Loading:</b> A FetchGroup can optionally specify that it needs its
 * included relationships loaded. This can be done using
 * {@link #setShouldLoad(boolean)} and {@link #setShouldLoadAll(boolean)} as
 * well as the corresponding configurations in the @FetchGroup annotation and
 * the {@literal <fetch-group>} element in the eclipselink-orm.xml. When this
 * is configured the FetchGroup will also function as a {@link LoadGroup}
 * causing all of its specified relationships to be populated prior to returning
 * the results from the query execution.
 *
 * @see FetchGroupManager
 * @see org.eclipse.persistence.config.QueryHints#FETCH_GROUP QueryHints.FETCH_GROUP
 * @see LoadGroup
 *
 * @author King Wang, dclarke, ailitchev
 * @since TopLink 10.1.3
 */
public class FetchGroup extends AttributeGroup {

    /**
     * Indicates whether this group should be also used as a {@link LoadGroup}
     * when processing the query result.
     */
    private boolean shouldLoad;

    /**
     * Caches the EntityFetch group for this FetchGroup
     */
    protected EntityFetchGroup entityFetchGroup;

    /**
     * Stores a reference to the root entity for an Aggregate Object relationship.
     * This ensures that partially loaded aggregates can be triggered.
     */
    protected FetchGroupTracker rootEntity;

    public FetchGroup() {
        super();
    }

    public FetchGroup(String name) {
        super(name);
    }

    /**
     * Return the attribute names on the current FetchGroup. This does not
     * include the attributes on nested FetchGroups
     *
     * @deprecated Use {@link AttributeGroup#getAttributeNames()}
     */
    @Deprecated
    public Set<String> getAttributes() {
        return getAttributeNames();
    }

    /**
     * INTERNAL:
     * Called on attempt to get value of an attribute that hasn't been fetched yet.
     * Returns an error message in case javax.persistence.EntityNotFoundException
     * should be thrown by the calling method,
     * null otherwise.
     * <p>
     * This method is typically only invoked through woven code in the
     * persistence object introduced when {@link FetchGroupTracker} is woven
     * into the entity.
     */
    public String onUnfetchedAttribute(FetchGroupTracker entity, String attributeName) {
        if (rootEntity != null){
            return rootEntity._persistence_getFetchGroup().onUnfetchedAttribute(rootEntity, attributeName);
        }
        ReadObjectQuery query = new ReadObjectQuery(entity);
        query.setShouldUseDefaultFetchGroup(false);
        Session session = entity._persistence_getSession();
        boolean shouldLoadResultIntoSelectionObject = false;
        if (session.isUnitOfWork()) {
            shouldLoadResultIntoSelectionObject = !((UnitOfWork)session).isObjectRegistered(entity);
        } else {
            shouldLoadResultIntoSelectionObject = !session.getIdentityMapAccessor().containsObjectInIdentityMap(entity);
        }
        if (shouldLoadResultIntoSelectionObject) {
            // entity is not in the cache.
            // instead of updating object in the cache update entity directly.
            query.setShouldLoadResultIntoSelectionObject(true);
            // and ignore cache
            query.dontCheckCache();
            query.setShouldMaintainCache(false);
            // To avoid infinite loop clear the fetch group right away.
            entity._persistence_setFetchGroup(null);
            entity._persistence_setSession(null);
        }
        Object result = session.executeQuery(query);
        if (result == null) {
            // the object was not found in the db end exception will be thrown - restore the fetch group back.
            if (shouldLoadResultIntoSelectionObject) {
                entity._persistence_setFetchGroup(this);
                entity._persistence_setSession(session);
            }
            Object[] args = { query.getSelectionId() };
            return ExceptionLocalization.buildMessage("no_entities_retrieved_for_get_reference", args);
        }
        return null;
    }

    /**
     * INTERNAL:
     * Called on attempt to assign value to an attribute that hasn't been fetched yet.
     * Returns an error message in case javax.persistence.EntityNotFoundException
     * should be thrown by the calling method,
     * null otherwise.
     * <p>
     * This method is typically only invoked through woven code in the
     * persistence object introduced when {@link FetchGroupTracker} is woven
     * into the entity.
     */
    public String onUnfetchedAttributeForSet(FetchGroupTracker entity, String attributeName) {
        return onUnfetchedAttribute(entity, attributeName);
    }

    /**
     * INTERNAL:
     * @return the rootEntity
     */
    public FetchGroupTracker getRootEntity() {
        return rootEntity;
    }

    /**
     * INTERNAL:
     * @param rootEntity the rootEntity to set
     */
    public void setRootEntity(FetchGroupTracker rootEntity) {
        this.rootEntity = rootEntity;
    }

    /**
     * Configure this group to also act as a {@link LoadGroup} when set to true
     * and load all of the specified relationships so that the entities returned
     * from the query where this group was used have the requested relationships
     * populated. All subsequent attributes added to this group that create a
     * nested group will have this value applied to them.
     *
     * @see #setShouldLoadAll(boolean) to configure {@link #shouldLoad()} on
     *      nested groups
     */
    public void setShouldLoad(boolean shouldLoad) {
        this.shouldLoad = shouldLoad;
        if (this.superClassGroup != null){
            ((FetchGroup)this.superClassGroup).setShouldLoad(shouldLoad);
        }else{
            setSubclassShouldLoad(shouldLoad);
        }
    }

    /**
     * passes should load to subclasses.
     *
     * @see #setShouldLoadAll(boolean) to configure {@link #shouldLoad()} on
     *      nested groups
     */
    protected void setSubclassShouldLoad(boolean shouldLoad) {
        if (this.subClasses != null){
            for (CoreAttributeGroup group : this.subClasses){
                ((FetchGroup)group).shouldLoad = shouldLoad;
                ((FetchGroup)group).setSubclassShouldLoad(shouldLoad);
            }
        }
    }

    /**
     * Configure this group to also act as a {@link LoadGroup} the same as
     * {@link #setShouldLoad(boolean)}. Additionally this method will apply the
     * provided boolean value to all nested groups already added.
     *
     * @see #setShouldLoad(boolean) to only configure this grup without
     *      effecting existing nested groups.
     */
    public void setShouldLoadAll(boolean shouldLoad) {
        this.setShouldLoad(shouldLoad);
        if(this.hasItems()) {
            Iterator<Map.Entry<String, AttributeItem>> it = getItems().entrySet().iterator();
            while(it.hasNext()) {
                Map.Entry<String, AttributeItem> entry = it.next();
                FetchGroup group = (FetchGroup)entry.getValue().getGroup();
                if(group != null) {
                    group.setShouldLoadAll(shouldLoad);
                }
            }
        }
    }

    /**
     * @return true if this group will be used as a {@link LoadGroup}when
     *         processing the results of a query to force the specified
     *         relationships to be loaded.
     */
    public boolean shouldLoad() {
        return this.shouldLoad;
    }

    @Override
    protected FetchGroup newGroup(String name, CoreAttributeGroup parent) {
        FetchGroup fetchGroup = new FetchGroup(name);
        if(parent != null) {
            fetchGroup.setShouldLoad(((FetchGroup)parent).shouldLoad());
        }
        return fetchGroup;
    }

    @Override
    public boolean isFetchGroup() {
        return true;
    }

    public boolean isEntityFetchGroup() {
        return false;
    }

    /*
     * LoadGroup created with all member groups with shouldLoad set to false dropped.
     */
    public LoadGroup toLoadGroupLoadOnly() {
        return this.toLoadGroup(new HashMap<AttributeGroup, LoadGroup>(), true);
    }

    @Override
    public FetchGroup clone() {
        return (FetchGroup)super.clone();
    }

    public LoadGroup toLoadGroup(Map<AttributeGroup, LoadGroup> cloneMap, boolean loadOnly){
        if (loadOnly && !this.shouldLoad){
            return null;
        }
        return super.toLoadGroup(cloneMap, loadOnly);
    }
    /**
     * INTERNAL:
     * Used to retrieve the EntityFetchGroup for this FetchGroup
     * @return the entityFetchGroup
     */
    public EntityFetchGroup getEntityFetchGroup(FetchGroupManager fetchGroupManager) {
        if (this.entityFetchGroup == null){
            this.entityFetchGroup = fetchGroupManager.getEntityFetchGroup(this.getAttributeNames());
        }
        return entityFetchGroup;
    }

    /**
     * Returns FetchGroup corresponding to the passed (possibly nested) attribute.
     */
    @Override
    public FetchGroup getGroup(String attributeNameOrPath) {
        return (FetchGroup)super.getGroup(attributeNameOrPath);
    }

    @Override
    public void addAttribute(String attributeNameOrPath, CoreAttributeGroup group) {
        this.entityFetchGroup = null;
        super.addAttribute(attributeNameOrPath, (group != null ? ((AttributeGroup)group).toFetchGroup() : null));
    }

    @Override
    public void addAttribute(String attributeNameOrPath, Collection<? extends CoreAttributeGroup> groups) {
        this.entityFetchGroup = null;
        super.addAttribute(attributeNameOrPath, groups);
    }

    @Override
    public void addAttributeKey(String attributeNameOrPath, CoreAttributeGroup group) {
        this.entityFetchGroup = null;
        super.addAttributeKey(attributeNameOrPath, (group != null ? ((AttributeGroup)group).toFetchGroup() : null));
    }


}
