public DataNode getDataNode() throws BigDBException {
    if (contributions.isEmpty()) return DataNode.NULL;

    // FIXME: There's probably a cleaner way to do this instead of
    // hard-coding the different types of logical data nodes here.
    switch (schemaNode.getNodeType()) {
      case CONTAINER:
        return LogicalContainerDataNode.fromContributions(
            schemaNode, contributions, includeDefaultValues);
      case LIST_ELEMENT:
        return LogicalListElementDataNode.fromContributions(
            schemaNode, contributions, includeDefaultValues);
      case LIST:
        ListSchemaNode listSchemaNode = (ListSchemaNode) schemaNode;
        if (listSchemaNode.isKeyedList()) {
          return LogicalKeyedListDataNode.fromContributions(
              schemaNode, contributions, includeDefaultValues);
        } else {
          Set<String> dataSources = schemaNode.getDataSources();
          assert dataSources.size() == 1;
          String dataSourceName = dataSources.iterator().next();
          Contribution contribution = contributions.get(dataSourceName);
          return LogicalUnkeyedListDataNode.fromContribution(
              schemaNode, contribution, includeDefaultValues);
        }
      default:
        // There currently aren't logical wrappers for the physical
        // leaf or leaf list data nodes since they always come from a single
        // data source and don't need logical aggregation. Also, default
        // value substitution is done in the LogicalDictionaryDataNode.
        // When we support mutation operations it might make sense to have
        // logical wrappers for leaf and leaf list nodes so that we have
        // access to the schema node
        throw new BigDBException("Invalid logical data node type");
    }
  }
  @Override
  protected Iterable<DataNodeWithPath> queryWithPath(
      SchemaNode schemaNode,
      LocationPathExpression queryPath,
      boolean expandTrailingList,
      boolean includeEmptyContainers)
      throws BigDBException {

    // The path must always contain the step for this node, so it's an
    // error if it's called with an empty path.
    if (queryPath.size() == 0) {
      throw new BigDBException("Query path argument cannot be empty");
    }

    ListSchemaNode listSchemaNode = (ListSchemaNode) schemaNode;
    SchemaNode listElementSchemaNode = listSchemaNode.getListElementSchemaNode();
    LocationPathExpression remainingQueryPath = queryPath.subpath(1);

    Step listStep = queryPath.getStep(0);
    String listName = listStep.getName();

    List<DataNodeWithPath> result = new ArrayList<DataNodeWithPath>();

    if (expandTrailingList || (queryPath.size() > 1)) {
      // Currently we only support querying the entire unkeyed list or else
      // a single index list element, so check to see if there's a
      // non-negative integer index predicate.
      int index = listStep.getIndexPredicate();
      Iterable<DataNode> listElementDataNodes;
      boolean singleListElement = (index >= 0);
      if (singleListElement) {
        DataNode elementDataNode = getChild(index);
        listElementDataNodes = Collections.singletonList(elementDataNode);
      } else {
        listElementDataNodes = this;
        index = 0;
      }

      // Iterate over the range of list elements set above
      for (DataNode listElementDataNode : listElementDataNodes) {
        Step listElementStep = DataNodeUtilities.getListElementStep(listName, index++);
        if (singleListElement
            || matchesPredicates(
                listElementSchemaNode, listElementDataNode, listElementStep, listStep)) {
          LocationPathExpression listElementPath = LocationPathExpression.ofStep(listElementStep);
          if (queryPath.size() > 1) {
            // There are more steps in the query path, so we need to
            // call
            // query recursively
            LocationPathExpression listElementQueryPath =
                LocationPathExpression.ofPaths(listElementPath, remainingQueryPath);
            Iterable<DataNodeWithPath> listElementResult =
                listElementDataNode.queryWithPath(
                    listElementSchemaNode, listElementQueryPath, expandTrailingList);
            // Add the results to the results for the overall list
            for (DataNodeWithPath dataNodeWithPath : listElementResult) {
              result.add(dataNodeWithPath);
            }
          } else {
            // No more query path to evaluate, so just make a
            // DataNodeWithPath
            // for the list element data node.
            DataNodeWithPath dataNodeWithPath =
                new DataNodeWithPathImpl(listElementPath, listElementDataNode);
            result.add(dataNodeWithPath);
          }
        }
      }
    } else {
      // Return the list node itself rather than expanding to the
      // matching list elements.
      DataNodeWithPath dataNodeWithPath =
          new DataNodeWithPathImpl(LocationPathExpression.ofStep(Step.of(listName)), this);
      result.add(dataNodeWithPath);
    }
    return result;
  }