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");
    }
  }