/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.ambari.server.api.query;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.apache.ambari.server.api.query.render.DefaultRenderer;
import org.apache.ambari.server.api.query.render.Renderer;
import org.apache.ambari.server.api.resources.ResourceDefinition;
import org.apache.ambari.server.api.resources.ResourceInstance;
import org.apache.ambari.server.api.resources.ResourceInstanceFactoryImpl;
import org.apache.ambari.server.api.resources.SubResourceDefinition;
import org.apache.ambari.server.api.services.BaseRequest;
import org.apache.ambari.server.api.services.Result;
import org.apache.ambari.server.api.services.ResultImpl;
import org.apache.ambari.server.api.util.TreeNode;
import org.apache.ambari.server.api.util.TreeNodeImpl;
import org.apache.ambari.server.controller.internal.QueryResponseImpl;
import org.apache.ambari.server.controller.predicate.AndPredicate;
import org.apache.ambari.server.controller.predicate.EqualsPredicate;
import org.apache.ambari.server.controller.spi.ClusterController;
import org.apache.ambari.server.controller.spi.NoSuchParentResourceException;
import org.apache.ambari.server.controller.spi.NoSuchResourceException;
import org.apache.ambari.server.controller.spi.PageRequest;
import org.apache.ambari.server.controller.spi.PageRequest.StartingPoint;
import org.apache.ambari.server.controller.spi.PageResponse;
import org.apache.ambari.server.controller.spi.Predicate;
import org.apache.ambari.server.controller.spi.QueryResponse;
import org.apache.ambari.server.controller.spi.Request;
import org.apache.ambari.server.controller.spi.Resource;
import org.apache.ambari.server.controller.spi.ResourceProvider;
import org.apache.ambari.server.controller.spi.Schema;
import org.apache.ambari.server.controller.spi.SortRequest;
import org.apache.ambari.server.controller.spi.SystemException;
import org.apache.ambari.server.controller.spi.TemporalInfo;
import org.apache.ambari.server.controller.spi.UnsupportedPropertyException;
import org.apache.ambari.server.controller.utilities.PredicateHelper;
import org.apache.ambari.server.controller.utilities.PropertyHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Default read query.
 */
public class QueryImpl implements Query, ResourceInstance {

  /**
   * Definition for the resource type.  The definition contains all information specific to the
   * resource type.
   */
  private final ResourceDefinition resourceDefinition;

  /**
   * The cluster controller.
   */
  private final ClusterController clusterController;

  /**
   * Properties of the query which make up the select portion of the query.
   */
  private final Set<String> requestedProperties = new HashSet<String>();

  /**
   * Map that associates categories with temporal data.
   */
  private final Map<String, TemporalInfo> temporalInfoMap = new HashMap<String, TemporalInfo>();

  /**
   * Map of primary and foreign key values.
   */
  private final Map<Resource.Type, String> keyValueMap = new HashMap<Resource.Type, String>();

  /**
   * Set of query results.
   */
  Map<Resource, QueryResult> queryResults = new LinkedHashMap<Resource, QueryResult>();

  /**
   * Set of populated query results
   */
  Map<Resource, QueryResult> populatedQueryResults = new LinkedHashMap<Resource, QueryResult>();

  /**
   * Sub-resources of the resource which is being operated on.
   * Should only be added via {@link #addSubResource(String, QueryImpl)}
   */
  private final Map<String, QueryImpl> requestedSubResources = new HashMap<String, QueryImpl>();

  /**
   * Sub-resource instances of this resource.
   * Map of resource name to resource instance.
   */
  private Map<String, QueryImpl> availableSubResources;

  /**
   * Indicates that the query should include all available properties.
   */
  private boolean allProperties = false;

  /**
   * The user supplied predicate.
   */
  private Predicate userPredicate;

  /**
   * The user supplied page request information.
   */
  private PageRequest pageRequest;

  /**
   * The user supplied sorting information.
   */
  private SortRequest sortRequest;

  /**
   * The sub resource properties referenced in the user predicate.
   */
  private final Set<String> subResourcePredicateProperties = new HashSet<String>();

  /**
   * Associated renderer. The default renderer is used unless
   * an alternate renderer is specified for the request. The renderer
   * is responsible for determining which properties are selected
   * as well as the overall structure of the result.
   */
  private Renderer renderer;

  /**
   * Sub-resource predicate.
   */
  private Predicate subResourcePredicate;

  /**
   * Processed predicate.
   */
  private Predicate processedPredicate;

  /**
   * The logger.
   */
  private final static Logger LOG =
      LoggerFactory.getLogger(QueryImpl.class);


  // ----- Constructor -------------------------------------------------------

  /**
   * Constructor
   *
   * @param keyValueMap         the map of key values
   * @param resourceDefinition  the resource definition
   * @param clusterController   the cluster controller
   */
  public QueryImpl(Map<Resource.Type, String> keyValueMap,
                   ResourceDefinition resourceDefinition,
                   ClusterController clusterController) {
    this.resourceDefinition = resourceDefinition;
    this.clusterController  = clusterController;
    setKeyValueMap(keyValueMap);
  }


  // ----- Query -------------------------------------------------------------

  @Override
  public void addProperty(String propertyId, TemporalInfo temporalInfo) {
    if (propertyId.equals("*")) {
      // wildcard
      addAllProperties(temporalInfo);
    } else{
      if (! addPropertyToSubResource(propertyId, temporalInfo)) {
        if (propertyId.endsWith("/*")) {
          propertyId = propertyId.substring(0, propertyId.length() - 2);
        }
        addLocalProperty(propertyId);
        if (temporalInfo != null) {
          temporalInfoMap.put(propertyId, temporalInfo);
        }
      }
    }
  }

  @Override
  public void addLocalProperty(String property) {
    requestedProperties.add(property);
  }

  @Override
  public Result execute()
      throws UnsupportedPropertyException,
             SystemException,
             NoSuchResourceException,
             NoSuchParentResourceException {

    queryForResources();
    return getResult(null);
  }

  @Override
  public Predicate getPredicate() {
    return createPredicate();
  }

  @Override
  public Set<String> getProperties() {
    return Collections.unmodifiableSet(requestedProperties);
  }

  @Override
  public void setUserPredicate(Predicate predicate) {
    userPredicate = predicate;
  }

  @Override
  public void setPageRequest(PageRequest pageRequest) {
    this.pageRequest = pageRequest;
  }

  @Override
  public void setSortRequest(SortRequest sortRequest) {
    this.sortRequest = sortRequest;
  }

  @Override
  public void setRenderer(Renderer renderer) {
    this.renderer = renderer;
    renderer.init(clusterController);
  }


// ----- ResourceInstance --------------------------------------------------

  @Override
  public void setKeyValueMap(Map<Resource.Type, String> keyValueMap) {
    this.keyValueMap.putAll(keyValueMap);
  }

  @Override
  public Map<Resource.Type, String> getKeyValueMap() {
    return new HashMap<Resource.Type, String>((keyValueMap));
  }

  @Override
  public Query getQuery() {
    return this;
  }

  @Override
  public ResourceDefinition getResourceDefinition() {
    return resourceDefinition;
  }

  @Override
  public boolean isCollectionResource() {
    return getKeyValueMap().get(getResourceDefinition().getType()) == null;
  }

  @Override
  public Map<String, ResourceInstance> getSubResources() {
    return new HashMap<String, ResourceInstance>(ensureSubResources());
  }


  // ----- Object overrides --------------------------------------------------

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

    QueryImpl query = (QueryImpl) o;

    return clusterController.equals(query.clusterController) && !(pageRequest != null ?
        !pageRequest.equals(query.pageRequest) :
        query.pageRequest != null) && requestedProperties.equals(query.requestedProperties) &&
        resourceDefinition.equals(query.resourceDefinition) &&
        keyValueMap.equals(query.keyValueMap) && !(userPredicate != null ?
        !userPredicate.equals(query.userPredicate) :
        query.userPredicate != null);
  }

  @Override
  public int hashCode() {
    int result = resourceDefinition.hashCode();
    result = 31 * result + clusterController.hashCode();
    result = 31 * result + requestedProperties.hashCode();
    result = 31 * result + keyValueMap.hashCode();
    result = 31 * result + (userPredicate != null ? userPredicate.hashCode() : 0);
    result = 31 * result + (pageRequest != null ? pageRequest.hashCode() : 0);
    return result;
  }


  // ----- helper methods ----------------------------------------------------

  /**
   * Get the map of sub-resources.  Lazily create the map if required.
   */
  protected Map<String, QueryImpl> ensureSubResources() {
    if (availableSubResources == null) {
      availableSubResources = new HashMap<String, QueryImpl>();
      Set<SubResourceDefinition> setSubResourceDefs =
          getResourceDefinition().getSubResourceDefinitions();

      ClusterController controller = clusterController;

      for (SubResourceDefinition subResDef : setSubResourceDefs) {
        Resource.Type type = subResDef.getType();
        Map<Resource.Type, String> valueMap = getKeyValueMap();
        QueryImpl resource =  new QueryImpl(valueMap,
            ResourceInstanceFactoryImpl.getResourceDefinition(type, valueMap),
            controller);

        String subResourceName = getSubResourceName(resource.getResourceDefinition(), subResDef);
        availableSubResources.put(subResourceName, resource);
      }
    }
    return availableSubResources;
  }

  /**
   * @param type the resource type
   * @return whether sub-resources is in processed predicate
   */
  private boolean populateResourceRequired(Resource.Type type) {
    ResourceProvider resourceProvider = clusterController.ensureResourceProvider(type);
    Set<String> unsupportedProperties =
      resourceProvider.checkPropertyIds(PredicateHelper.getPropertyIds(processedPredicate));
    return !unsupportedProperties.isEmpty() || hasSubResourcePredicate();
  }

  /**
   * Query the cluster controller for the top level resources.
   */
  private void queryForResources()
      throws UnsupportedPropertyException,
      SystemException,
      NoSuchResourceException,
      NoSuchParentResourceException {

    Resource.Type resourceType    = getResourceDefinition().getType();
    Predicate     queryPredicate  = createPredicate(getKeyValueMap(), processUserPredicate(userPredicate));

    // must occur after processing user predicate and prior to creating request
    finalizeProperties();

    Request request = createRequest();

    // use linked hash sets so that we maintain insertion and traversal order
    // in the event that the resource provider already gave us a sorted set
    // back
    Set<Resource> resourceSet = new LinkedHashSet<Resource>();
    Set<Resource> providerResourceSet = new LinkedHashSet<Resource>();

    QueryResponse queryResponse = doQuery(resourceType, request, queryPredicate, true);

    // If there is a page request and the predicate does not contain properties
    // that need to be set
    if ((pageRequest != null || sortRequest != null ) &&
      !populateResourceRequired(resourceType)) {
      PageResponse pageResponse = clusterController.getPage(resourceType,
          queryResponse, request, queryPredicate, pageRequest, sortRequest);

      // build a new set
      for (Resource r : pageResponse.getIterable()) {
        resourceSet.add(r);
        providerResourceSet.add(r);
      }
    } else {
      resourceSet.addAll(queryResponse.getResources());
      providerResourceSet.addAll(queryResponse.getResources());
    }

    populatedQueryResults.put(null, new QueryResult(
      request, queryPredicate, userPredicate, getKeyValueMap(), new QueryResponseImpl(resourceSet)));

    queryResults.put(null, new QueryResult(
      request, queryPredicate, userPredicate, getKeyValueMap(), queryResponse));

    clusterController.populateResources(resourceType, providerResourceSet, request, queryPredicate);
    queryForSubResources();
  }

  /**
   * Query the cluster controller for the sub-resources associated with
   * this query object.
   */
  private void queryForSubResources()
      throws UnsupportedPropertyException,
      SystemException,
      NoSuchResourceException,
      NoSuchParentResourceException {

    for (Map.Entry<String, QueryImpl> entry : requestedSubResources.entrySet()) {
      QueryImpl     subResource         = entry.getValue();
      Resource.Type resourceType        = subResource.getResourceDefinition().getType();
      Request       request             = subResource.createRequest();
      Set<Resource> providerResourceSet = new HashSet<Resource>();

      for (QueryResult queryResult : populatedQueryResults.values()) {
        for (Resource resource : queryResult.getQueryResponse().getResources()) {
          Map<Resource.Type, String> map = getKeyValueMap(resource, queryResult.getKeyValueMap());

          Predicate     queryPredicate = subResource.createPredicate(map, subResource.processedPredicate);
          Set<Resource> resourceSet    = new LinkedHashSet<Resource>();

          try {
            Set<Resource> queryResources =
                subResource.doQuery(resourceType, request, queryPredicate, false).getResources();

            providerResourceSet.addAll(queryResources);
            resourceSet.addAll(queryResources);
          } catch (NoSuchResourceException e) {
            // do nothing ...
          }
          subResource.queryResults.put(resource,
              new QueryResult(request, queryPredicate, subResourcePredicate, map, new QueryResponseImpl(resourceSet)));
          subResource.populatedQueryResults.put(resource,
            new QueryResult(request, queryPredicate, subResourcePredicate, map, new QueryResponseImpl(resourceSet)));
        }
      }
      clusterController.populateResources(resourceType, providerResourceSet, request, null);
      subResource.queryForSubResources();
    }
  }

  /**
   * Query the cluster controller for the resources.
   *
   * @param type                the resource type
   * @param request             the request information
   * @param predicate           the predicate
   * @param checkEmptyResponse  true if an empty query response can trigger a NoSuchResourceException
   *
   * @return the result of the cluster controller query
   *
   * @throws NoSuchResourceException if a specific resource was asked for and not found and checkEmptyResponse == true
   */
  private QueryResponse doQuery(Resource.Type type, Request request, Predicate predicate, boolean checkEmptyResponse)
      throws UnsupportedPropertyException,
      SystemException,
      NoSuchResourceException,
      NoSuchParentResourceException {

    if (LOG.isDebugEnabled()) {
      LOG.debug("Executing resource query: " + request + " where " + predicate);
    }

    QueryResponse queryResponse = clusterController.getResources(type, request, predicate);

    if (checkEmptyResponse && queryResponse.getResources().isEmpty()) {

      // If this is not a collection request then we must throw
      // NoSuchResourceException (404 response) for an empty query result
      if(!isCollectionResource()) {
        throw new NoSuchResourceException(
            "The requested resource doesn't exist: " + type.toString() + " not found where " + predicate + ".");
      }
    }
    return queryResponse;
  }

  /**
   * Get a map of property sets keyed by the resources associated with this
   * query. The property sets should contain the joined sets of all of the
   * requested properties from each resource's sub-resources.
   *
   * For example, if this query is associated with the resources AResource1,
   * AResource1 and AResource3 as follows ...
   *
   * <pre>
   * a_resources
   * │
   * └──AResource1 ─────────────AResource1 ─────────────AResource3
   *      │                       │                       │
   *      ├── b_resources         ├── b_resources         ├── BResources
   *      │   ├── BResource1      │   ├── BResource3      │    └── BResource5
   *      │   │     p1:1          │   │     p1:3          │          p1:5
   *      │   │     p2:5          │   │     p2:5          │          p2:5
   *      │   │                   │   │                   │
   *      │   └── BResource2      │   └── BResource4      └── c_resources
   *      │         p1:2          │         p1:4              └── CResource4
   *      │         p2:0          │         p2:0                    p3:4
   *      │                       │
   *      └── c_resources         └── c_resources
   *          ├── CResource1          └── CResource3
   *          │     p3:1                    p3:3
   *          │
   *          └── CResource2
   *                p3:2
   * 
   * Given the following query ...
   * 
   *     api/v1/a_resources?b_resources/p1>3&b_resources/p2=5&c_resources/p3=1
   * 
   * The caller should pass the following property ids ...
   * 
   *     b_resources/p1
   *     b_resources/p2
   *     c_resources/p3
   * 
   * getJoinedResourceProperties should produce the following map of property sets
   * by making recursive calls on the sub-resources of each of this query's resources,
   * joining the resulting property sets, and adding them to the map keyed by the
   * resource ...
   * 
   *  {
   *    AResource1=[{b_resources/p1=1, b_resources/p2=5, c_resources/p3=1},
   *                {b_resources/p1=2, b_resources/p2=0, c_resources/p3=1},
   *                {b_resources/p1=1, b_resources/p2=5, c_resources/p3=2},
   *                {b_resources/p1=2, b_resources/p2=0, c_resources/p3=2}],
   *    AResource2=[{b_resources/p1=3, b_resources/p2=5, c_resources/p3=3},
   *                {b_resources/p1=4, b_resources/p2=0, c_resources/p3=3}],
   *    AResource3=[{b_resources/p1=5, b_resources/p2=5, c_resources/p3=4}],
   *  }
   * </pre>
   *
   * @param propertyIds
   *          the requested properties
   * @param parentResource
   *          the parent resource; may be null
   * @param category
   *          the sub-resource category; may be null
   *
   * @return a map of property sets keyed by the resources associated with this
   *         query
   */
  protected Map<Resource, Set<Map<String, Object>>> getJoinedResourceProperties(Set<String> propertyIds,
                                                                                Resource parentResource,
                                                                                String category)
      throws SystemException, UnsupportedPropertyException, NoSuchParentResourceException, NoSuchResourceException {

    Map<Resource, Set<Map<String, Object>>> resourcePropertyMaps =
        new HashMap<Resource, Set<Map<String, Object>>>();

    Map<String, String> categoryPropertyIdMap =
        getPropertyIdsForCategory(propertyIds, category);

    for (Map.Entry<Resource, QueryResult> queryResultEntry : populatedQueryResults.entrySet()) {
      QueryResult queryResult         = queryResultEntry.getValue();
      Resource    queryParentResource = queryResultEntry.getKey();

      // for each resource for the given parent ...
      if (queryParentResource == parentResource) {

        Iterable<Resource> iterResource = clusterController.getIterable(
            resourceDefinition.getType(), queryResult.getQueryResponse(),
            queryResult.getRequest(), queryResult.getPredicate(), null, null);

        for (Resource resource : iterResource) {
          // get the resource properties
          Map<String, Object> resourcePropertyMap = new HashMap<String, Object>();
          for (Map.Entry<String, String> categoryPropertyIdEntry : categoryPropertyIdMap.entrySet()) {
            Object value = resource.getPropertyValue(categoryPropertyIdEntry.getValue());
            if (value != null) {
              resourcePropertyMap.put(categoryPropertyIdEntry.getKey(), value);
            }
          }

          Set<Map<String, Object>> propertyMaps = new HashSet<Map<String, Object>>();

          // For each sub category get the property maps for the sub resources
          for (Map.Entry<String, QueryImpl> entry : requestedSubResources.entrySet()) {
            String subResourceCategory = category == null ? entry.getKey() : category + "/" + entry.getKey();

            QueryImpl subResource = entry.getValue();

            Map<Resource, Set<Map<String, Object>>> subResourcePropertyMaps =
                subResource.getJoinedResourceProperties(propertyIds, resource, subResourceCategory);

            Set<Map<String, Object>> combinedSubResourcePropertyMaps = new HashSet<Map<String, Object>>();
            for (Set<Map<String, Object>> maps : subResourcePropertyMaps.values()) {
              combinedSubResourcePropertyMaps.addAll(maps);
            }
            propertyMaps = joinPropertyMaps(propertyMaps, combinedSubResourcePropertyMaps);
          }
          // add parent resource properties to joinedResources
          if (!resourcePropertyMap.isEmpty()) {
            if (propertyMaps.isEmpty()) {
              propertyMaps.add(resourcePropertyMap);
            } else {
              for (Map<String, Object> propertyMap : propertyMaps) {
                propertyMap.putAll(resourcePropertyMap);
              }
            }
          }
          resourcePropertyMaps.put(resource, propertyMaps);
        }
      }
    }
    return resourcePropertyMaps;
  }

  /**
   * Finalize properties for entire query tree before executing query.
   */
  private void finalizeProperties() {
    ResourceDefinition rootDefinition = resourceDefinition;

    QueryInfo rootQueryInfo = new QueryInfo(rootDefinition, requestedProperties);
    TreeNode<QueryInfo> rootNode = new TreeNodeImpl<QueryInfo>(
        null, rootQueryInfo, rootDefinition.getType().name());

    TreeNode<QueryInfo> requestedPropertyTree = buildQueryPropertyTree(this, rootNode);

    mergeFinalizedProperties(renderer.finalizeProperties(
        requestedPropertyTree, isCollectionResource()), this);
  }

  /**
   * Recursively build a tree of query information.
   *
   * @param query  query to process
   * @param node   tree node associated with the query
   *
   * @return query info tree
   */
  private TreeNode<QueryInfo> buildQueryPropertyTree(QueryImpl query, TreeNode<QueryInfo> node) {
    for (QueryImpl subQuery : query.requestedSubResources.values()) {
      ResourceDefinition childResource = subQuery.resourceDefinition;

      QueryInfo queryInfo = new QueryInfo(childResource, subQuery.requestedProperties);
      TreeNode<QueryInfo> childNode = node.addChild(queryInfo, childResource.getType().name());
      buildQueryPropertyTree(subQuery, childNode);
    }
    return node;
  }

  /**
   * Merge the tree of query properties returned by the renderer with properties in
   * the query tree.
   *
   * @param node   property tree node
   * @param query  query associated with the property tree node
   */
  private void mergeFinalizedProperties(TreeNode<Set<String>> node, QueryImpl query) {

    Set<String> finalizedProperties = node.getObject();
    query.requestedProperties.clear();
    // currently not exposing temporal information to renderer
    query.requestedProperties.addAll(finalizedProperties);

    for (TreeNode<Set<String>> child : node.getChildren()) {
      Resource.Type childType = Resource.Type.valueOf(child.getName());
      ResourceDefinition parentResource = query.resourceDefinition;
      Set<SubResourceDefinition> subResources = parentResource.getSubResourceDefinitions();
      String subResourceName = null;
      for (SubResourceDefinition subResource : subResources) {
        if (subResource.getType() == childType) {
          ResourceDefinition resource = ResourceInstanceFactoryImpl.getResourceDefinition(
              subResource.getType(), query.keyValueMap);
          subResourceName = getSubResourceName(resource, subResource);
          break;
        }
      }
      QueryImpl subQuery = query.requestedSubResources.get(subResourceName);
      if (subQuery == null) {
        query.addProperty(subResourceName, null);
        subQuery = query.requestedSubResources.get(subResourceName);
      }
      mergeFinalizedProperties(child, subQuery);
    }
  }

  // Map the given set of property ids to corresponding property ids in the
  // given sub-resource category.
  private Map<String, String> getPropertyIdsForCategory(Set<String> propertyIds, String category) {
    Map<String, String> map = new HashMap<String, String>();

    for (String propertyId : propertyIds) {
      if (category == null || propertyId.startsWith(category)) {
        map.put(propertyId, category==null ? propertyId : propertyId.substring(category.length() + 1));
      }
    }
    return map;
  }

  // Join two sets of property maps into one.
  private static Set<Map<String, Object>> joinPropertyMaps(Set<Map<String, Object>> propertyMaps1,
                                                           Set<Map<String, Object>> propertyMaps2) {
    Set<Map<String, Object>> propertyMaps = new HashSet<Map<String, Object>>();

    if (propertyMaps1.isEmpty()) {
      return propertyMaps2;
    }
    if (propertyMaps2.isEmpty()) {
      return propertyMaps1;
    }

    for (Map<String, Object> map1 : propertyMaps1) {
      for (Map<String, Object> map2 : propertyMaps2) {
        Map<String, Object> joinedMap = new HashMap<String, Object>(map1);
        joinedMap.putAll(map2);
        propertyMaps.add(joinedMap);
      }
    }
    return propertyMaps;
  }

   // Get a result from this query.
  private Result getResult(Resource parentResource)
      throws UnsupportedPropertyException, SystemException, NoSuchResourceException, NoSuchParentResourceException {

    Result result = new ResultImpl(true);
    Resource.Type resourceType = getResourceDefinition().getType();
    TreeNode<Resource> tree = result.getResultTree();

    if (isCollectionResource()) {
      tree.setProperty("isCollection", "true");
    }

    QueryResult queryResult = queryResults.get(parentResource);

    if (queryResult != null) {
      Predicate queryPredicate     = queryResult.getPredicate();
      Predicate queryUserPredicate = queryResult.getUserPredicate();
      Request   queryRequest       = queryResult.getRequest();

      QueryResponse queryResponse = queryResult.getQueryResponse();

      if (hasSubResourcePredicate() && queryUserPredicate != null) {
        queryPredicate = getExtendedPredicate(parentResource, queryUserPredicate);
      }

      Iterable<Resource> iterResource;

      if (pageRequest == null) {
        iterResource = clusterController.getIterable(
          resourceType, queryResponse, queryRequest, queryPredicate,
          null, sortRequest
        );
      } else {
        PageResponse pageResponse = clusterController.getPage(
          resourceType, queryResponse, queryRequest, queryPredicate,
          pageRequest, sortRequest
        );
        iterResource = pageResponse.getIterable();
        tree.setProperty("count", pageResponse.getTotalResourceCount().toString());
      }

      int count = 1;
      for (Resource resource : iterResource) {

        // add a child node for the resource and provide a unique name.  The name is never used.
        TreeNode<Resource> node = tree.addChild(
            resource, resource.getType() + ":" + count++);

        for (Map.Entry<String, QueryImpl> entry : requestedSubResources.entrySet()) {
          String    subResCategory = entry.getKey();
          QueryImpl subResource    = entry.getValue();

          TreeNode<Resource> childResult = subResource.getResult(resource).getResultTree();
          childResult.setName(subResCategory);
          childResult.setProperty("isCollection", "false");
          node.addChild(childResult);
        }
      }
    }
    return renderer.finalizeResult(result);
  }

  // Indicates whether or not this query has sub-resource elements
  // in its predicate.
  private boolean hasSubResourcePredicate() {
    return !subResourcePredicateProperties.isEmpty();
  }

  // Alter the given predicate so that the resources referenced by
  // the predicate will be extended to include the joined properties
  // of their sub-resources.
  private Predicate getExtendedPredicate(Resource parentResource,
                                         Predicate predicate)
      throws SystemException,
             UnsupportedPropertyException,
             NoSuchParentResourceException,
             NoSuchResourceException {

    Map<Resource, Set<Map<String, Object>>> joinedResources =
        getJoinedResourceProperties(subResourcePredicateProperties, parentResource, null);

    ExtendedResourcePredicateVisitor visitor =
        new ExtendedResourcePredicateVisitor(joinedResources);

    PredicateHelper.visit(predicate, visitor);
    return visitor.getExtendedPredicate();
  }

  private void addAllProperties(TemporalInfo temporalInfo) {
    allProperties = true;
    if (temporalInfo != null) {
      temporalInfoMap.put(null, temporalInfo);
    }

    for (Map.Entry<String, QueryImpl> entry : ensureSubResources().entrySet()) {
      String name = entry.getKey();
      if (! requestedSubResources.containsKey(name)) {
        addSubResource(name, entry.getValue());
      }
    }
  }

  private boolean addPropertyToSubResource(String propertyId, TemporalInfo temporalInfo) {
    int    index    = propertyId.indexOf("/");
    String category = index == -1 ? propertyId : propertyId.substring(0, index);

    Map<String, QueryImpl> subResources = ensureSubResources();

    QueryImpl subResource = subResources.get(category);
    if (subResource != null) {
      addSubResource(category, subResource);

      //only add if a sub property is set or if a sub category is specified
      if (index != -1) {
        subResource.addProperty(propertyId.substring(index + 1), temporalInfo);
      }
      return true;
    }
    return false;
  }

  private Predicate createInternalPredicate(Map<Resource.Type, String> mapResourceIds) {
    Resource.Type resourceType = getResourceDefinition().getType();
    Schema schema = clusterController.getSchema(resourceType);

    Set<Predicate> setPredicates = new HashSet<Predicate>();
    for (Map.Entry<Resource.Type, String> entry : mapResourceIds.entrySet()) {
      if (entry.getValue() != null) {
        String keyPropertyId = schema.getKeyPropertyId(entry.getKey());
        if (keyPropertyId != null) {
          setPredicates.add(new EqualsPredicate<String>(keyPropertyId, entry.getValue()));
        }
      }
    }

    if (setPredicates.size() == 1) {
      return setPredicates.iterator().next();
    } else if (setPredicates.size() > 1) {
      return new AndPredicate(setPredicates.toArray(new Predicate[setPredicates.size()]));
    } else {
      return null;
    }
  }

  private Predicate createPredicate() {
    return createPredicate(getKeyValueMap(), userPredicate);
  }

  private Predicate createPredicate(Map<Resource.Type, String> keyValueMap, Predicate predicate) {
    Predicate internalPredicate = createInternalPredicate(keyValueMap);

    if (internalPredicate == null) {
        return predicate;
    }
    return (predicate == null ? internalPredicate :
          new AndPredicate(predicate, internalPredicate));
  }

  // Get a sub-resource predicate from the given predicate.
  private Predicate getSubResourcePredicate(Predicate predicate, String category) {
    if (predicate == null) {
      return null;
    }

    SubResourcePredicateVisitor visitor = new SubResourcePredicateVisitor(category);
    PredicateHelper.visit(predicate, visitor);
    return visitor.getSubResourcePredicate();
  }

  // Process the given predicate to remove sub-resource elements.
  private Predicate processUserPredicate(Predicate predicate) {
    if (predicate == null) {
      return null;
    }
    ProcessingPredicateVisitor visitor = new ProcessingPredicateVisitor(this);
    PredicateHelper.visit(predicate, visitor);

    // add the sub-resource to the request
    Set<String> categories = visitor.getSubResourceCategories();
    for (String category : categories) {
      addPropertyToSubResource(category, null);
    }
    // record the sub-resource properties on this query
    subResourcePredicateProperties.addAll(visitor.getSubResourceProperties());

    if (hasSubResourcePredicate()) {
      for (Map.Entry<String, QueryImpl> entry : requestedSubResources.entrySet()) {
        subResourcePredicate = getSubResourcePredicate(predicate, entry.getKey());
        entry.getValue().processUserPredicate(subResourcePredicate);
      }
    }

    processedPredicate = visitor.getProcessedPredicate();
    return processedPredicate;
  }

  private Request createRequest() {
    Map<String, String> requestInfoProperties = new HashMap<String, String>();

    if (pageRequest != null) {
      requestInfoProperties.put(BaseRequest.PAGE_SIZE_PROPERTY_KEY,
          Integer.toString(pageRequest.getPageSize() + pageRequest.getOffset()));

      requestInfoProperties.put(
        BaseRequest.ASC_ORDER_PROPERTY_KEY,
          Boolean.toString(pageRequest.getStartingPoint() == StartingPoint.Beginning
              || pageRequest.getStartingPoint() == StartingPoint.OffsetStart));
    }

    if (allProperties) {
      return PropertyHelper.getReadRequest(Collections.<String> emptySet(),
          requestInfoProperties, null, pageRequest, sortRequest);
    }

    Map<String, TemporalInfo> mapTemporalInfo    = new HashMap<String, TemporalInfo>();
    TemporalInfo              globalTemporalInfo = temporalInfoMap.get(null);

    Set<String> setProperties = new HashSet<String>();
    setProperties.addAll(requestedProperties);
    for (String propertyId : setProperties) {
      TemporalInfo temporalInfo = temporalInfoMap.get(propertyId);
      if (temporalInfo != null) {
        mapTemporalInfo.put(propertyId, temporalInfo);
      } else if (globalTemporalInfo != null) {
        mapTemporalInfo.put(propertyId, globalTemporalInfo);
      }
    }

    return PropertyHelper.getReadRequest(setProperties, requestInfoProperties,
        mapTemporalInfo, pageRequest, sortRequest);
  }


  // Get a key value map based on the given resource and an existing key value map
  private Map<Resource.Type, String> getKeyValueMap(Resource resource,
                                                    Map<Resource.Type, String> keyValueMap) {
    Map<Resource.Type, String> resourceKeyValueMap = new HashMap<Resource.Type, String>(keyValueMap.size());
    for (Map.Entry<Resource.Type, String> resourceIdEntry : keyValueMap.entrySet()) {
      Resource.Type type = resourceIdEntry.getKey();
      String value = resourceIdEntry.getValue();

      if (value == null) {
        Object o = resource.getPropertyValue(clusterController.getSchema(type).getKeyPropertyId(type));
        value = o == null ? null : o.toString();
      }
      if (value != null) {
        resourceKeyValueMap.put(type, value);
      }
    }
    Schema schema = clusterController.getSchema(resource.getType());
    Set<Resource.Type> types = schema.getKeyTypes();

    for (Resource.Type type : types) {
      String resourceKeyProp = schema.getKeyPropertyId(type);
      Object resourceValue = resource.getPropertyValue(resourceKeyProp);
      if (null != resourceValue) {
        resourceKeyValueMap.put(type, resourceValue.toString());
      }
    }
    return resourceKeyValueMap;
  }

  /**
   * Add a sub query with the renderer set.
   *
   * @param name   name of sub resource
   * @param query  sub resource
   */
  private void addSubResource(String name, QueryImpl query) {
    // renderer specified for request only applies to top level query
    query.setRenderer(new DefaultRenderer());
    requestedSubResources.put(name, query);
  }

  /**
   * Obtain the name of a sub-resource.
   *
   * @param resource     parent resource
   * @param subResource  sub-resource
   *
   * @return either the plural or singular sub-resource name based on whether the sub-resource is
   *         included as a collection
   */
  private String getSubResourceName(ResourceDefinition resource, SubResourceDefinition subResource) {
    return subResource.isCollection() ?
        resource.getPluralName() :
        resource.getSingularName();
  }

  // ----- inner class : QueryResult -----------------------------------------

  /**
   * Maintain information about an individual query and its result.
   */
  private static class QueryResult {
    private final Request request;
    private final Predicate predicate;
    private final Predicate userPredicate;
    private final Map<Resource.Type, String> keyValueMap;
    private final QueryResponse queryResponse;

    // ----- Constructor -----------------------------------------------------

    private  QueryResult(Request request, Predicate predicate, Predicate userPredicate,
                         Map<Resource.Type, String> keyValueMap,
                         QueryResponse queryResponse) {
      this.request        = request;
      this.predicate      = predicate;
      this.userPredicate  = userPredicate;
      this.keyValueMap    = keyValueMap;
      this.queryResponse  = queryResponse;
    }

    // ----- accessors -------------------------------------------------------

    public Request getRequest() {
      return request;
    }

    public Predicate getPredicate() {
      return predicate;
    }

    public Predicate getUserPredicate() {
      return userPredicate;
    }

    public Map<Resource.Type, String> getKeyValueMap() {
      return keyValueMap;
    }

    public QueryResponse getQueryResponse() {
      return queryResponse;
    }
  }
}
