camera_metadata: Generate java metadata keys source code

Change-Id: Id1d1d4367eb51354e85c4eea38c593a498932e5b
diff --git a/camera/docs/CameraMetadataKeys.mako b/camera/docs/CameraMetadataKeys.mako
new file mode 100644
index 0000000..18af316
--- /dev/null
+++ b/camera/docs/CameraMetadataKeys.mako
@@ -0,0 +1,123 @@
+## -*- coding: utf-8 -*-
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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 android.hardware.photography;
+
+import static android.hardware.photography.CameraMetadata.Key;
+
+/**
+ * ! Do not edit this file directly !
+ *
+ * Generated automatically from ${java_class}Keys.mako
+ *
+ * TODO: Include a hash of the input files here that the build can check.
+ */
+
+<%page args="java_class, xml_kind" />\
+/**
+ * The base class for camera controls and information.
+ *
+ * This class defines the basic key/value map used for querying for camera
+ * characteristics or capture results, and for setting camera request
+ * parameters.
+ *
+ * @see ${java_class}
+ * @see CameraMetadata
+ * @hide
+ **/
+##
+## Function to generate an enum
+<%def name="generate_enum(entry)">
+            public static final class ${entry.get_name_minimal() | pascal_case}Key extends Key<${jtype(entry)}> {
+                public enum Enum {
+                  % for value,last in enumerate_with_last(entry.enum.values):
+                    ${value.name | jidentifier}${"," if not last else ";"}
+                  % endfor
+                }
+
+              % for value in entry.enum.values:
+                public static final Enum ${value.name | jidentifier} = Enum.${value.name | jidentifier};
+              % endfor
+
+                // TODO: remove requirement for constructor by making Key an interface
+                private ${entry.get_name_minimal() | pascal_case}Key(String name) {
+                    super(name, ${jtype(entry)}.class);
+                }
+
+              % if entry.enum.has_values_with_id:
+                static {
+                    CameraMetadata.registerEnumValues(${jenum(entry.enum)}.class, new int[] {
+                      % for (value, last) in enumerate_with_last(entry.enum.values):
+                        ${enum_calculate_value_string(value)}${"," if not last else ""}  // ${value.name | jidentifier}
+                      % endfor
+                    });
+                }
+              % endif
+            }
+</%def>\
+##
+## Generate a list of only Static, Controls, or Dynamic properties.
+<%def name="single_kind_keys(java_name, xml_name)">\
+public final class ${java_name}Keys {
+% for outer_namespace in metadata.outer_namespaces: ## assumes single 'android' namespace
+  % for section in outer_namespace.sections:
+    % if section.find_first(lambda x: isinstance(x, metadata_model.Entry) and x.kind == xml_name):
+    public static final class ${section.name | pascal_case} {
+      % for inner_namespace in get_children_by_filtering_kind(section, xml_name, 'namespaces'):
+## We only support 1 level of inner namespace, i.e. android.a.b and android.a.b.c works, but not android.a.b.c.d
+## If we need to support more, we should use a recursive function here instead.. but the indentation gets trickier.
+        public static final class ${inner_namespace.name| pascal_case} {
+          % for entry in inner_namespace.merged_entries:
+            % if entry.enum:
+${generate_enum(entry)}
+            public static final Key<${jtype(entry)}> ${entry.get_name_minimal() | csym} =
+                    new ${entry.get_name_minimal() | pascal_case}Key("${entry.name}");
+            % else:
+            public static final Key<${jtype(entry)}> ${entry.get_name_minimal() | csym} =
+                    new Key<${jtype(entry)}>("${entry.name}", ${jclass(entry)});
+            % endif
+          % endfor
+        }
+      % endfor
+
+      % for entry in get_children_by_filtering_kind(section, xml_name, 'merged_entries'):
+        % if entry.enum:
+${generate_enum(entry)}
+        public static final Key<${jtype(entry)}> ${entry.get_name_minimal() | csym} =
+                new ${entry.get_name_minimal() | pascal_case}Key("${entry.name}");
+        % else:
+        public static final Key<${jtype(entry)}> ${entry.get_name_minimal() | csym} =
+                new Key<${jtype(entry)}>("${entry.name}", ${jclass(entry)});
+        % endif
+      % endfor
+
+    }
+    % endif
+  % endfor
+% endfor
+}
+</%def>\
+##
+## Static properties only
+##${single_kind_keys('CameraPropertiesKeys', 'static')}
+##
+## Controls properties only
+##${single_kind_keys('CaptureRequestKeys', 'controls')}
+##
+## Dynamic properties only
+##${single_kind_keys('CaptureResultKeys', 'dynamic')}
+${single_kind_keys(java_class, xml_kind)}
diff --git a/camera/docs/CameraPropertiesKeys.mako b/camera/docs/CameraPropertiesKeys.mako
new file mode 100644
index 0000000..ee10f45
--- /dev/null
+++ b/camera/docs/CameraPropertiesKeys.mako
@@ -0,0 +1,17 @@
+## -*- coding: utf-8 -*-
+##
+## Copyright (C) 2013 The Android Open Source Project
+##
+## 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.
+##
+<%include file="CameraMetadataKeys.mako" args="java_class='CameraProperties', xml_kind='static'" />
diff --git a/camera/docs/CaptureRequestKeys.mako b/camera/docs/CaptureRequestKeys.mako
new file mode 100644
index 0000000..bb8910f
--- /dev/null
+++ b/camera/docs/CaptureRequestKeys.mako
@@ -0,0 +1,17 @@
+## -*- coding: utf-8 -*-
+##
+## Copyright (C) 2013 The Android Open Source Project
+##
+## 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.
+##
+<%include file="CameraMetadataKeys.mako" args="java_class='CaptureRequest', xml_kind='controls'" />
diff --git a/camera/docs/CaptureResultKeys.mako b/camera/docs/CaptureResultKeys.mako
new file mode 100644
index 0000000..07bb139
--- /dev/null
+++ b/camera/docs/CaptureResultKeys.mako
@@ -0,0 +1,17 @@
+## -*- coding: utf-8 -*-
+##
+## Copyright (C) 2013 The Android Open Source Project
+##
+## 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.
+##
+<%include file="CameraMetadataKeys.mako" args="java_class='CaptureResult', xml_kind='dynamic'" />
diff --git a/camera/docs/metadata-generate b/camera/docs/metadata-generate
index 6084531..9b3a009 100755
--- a/camera/docs/metadata-generate
+++ b/camera/docs/metadata-generate
@@ -23,7 +23,13 @@
 #   ../src/camera_metadata_tags.h
 #
 
+if [[ -z $ANDROID_BUILD_TOP ]]; then
+    echo "Please source build/envsetup.sh before running script" >& 2
+    exit 1
+fi
+
 thisdir=$(cd "$(dirname "$0")"; pwd)
+fwkdir="$ANDROID_BUILD_TOP/frameworks/base/core/java/android/hardware/photography/"
 out_files=()
 
 function relpath() {
@@ -35,6 +41,14 @@
     local in=$thisdir/$1
     local out=$thisdir/$2
 
+    gen_file_abs "$in" "$out"
+    return $?
+}
+
+function gen_file_abs() {
+    local in="$1"
+    local out="$2"
+
     python $thisdir/metadata_parser_xml.py $thisdir/metadata_properties.xml $in $out
 
     local succ=$?
@@ -67,7 +81,7 @@
             echo "Diff result was $diff_result" >& /dev/null
             echo "Diff result was $diff_result" >& /dev/null
             if [[ $diff_result -eq 0 ]]; then
-                echo "No changes in ${git_path}"  >& /dev/null
+                echo "No changes in ${git_path}" >& /dev/null
             else
                 echo "There are changes in ${git_path}" >& /dev/null
                 git_directories+=("$git_path")
@@ -76,7 +90,7 @@
     done
 
     # print as result the unique list of git directories affected
-    printf %s\\n "${git_directories}" | sort | uniq
+    printf %s\\n "${git_directories[@]}" | sort | uniq
 }
 
 $thisdir/metadata-check-dependencies || exit 1
@@ -85,6 +99,9 @@
 gen_file html.mako docs.html || exit 1
 gen_file camera_metadata_tag_info.mako ../src/camera_metadata_tag_info.c || exit 1
 gen_file camera_metadata_tags.mako ../include/system/camera_metadata_tags.h || exit 1
+gen_file_abs CaptureResultKeys.mako "$fwkdir/CaptureResultKeys.java" || exit 1
+gen_file_abs CaptureRequestKeys.mako "$fwkdir/CaptureRequestKeys.java" || exit 1
+gen_file_abs CameraPropertiesKeys.mako "$fwkdir/CameraPropertiesKeys.java" || exit 1
 
 echo ""
 echo "===================================================="
diff --git a/camera/docs/metadata_helpers.py b/camera/docs/metadata_helpers.py
index fd09d98..7d109e9 100644
--- a/camera/docs/metadata_helpers.py
+++ b/camera/docs/metadata_helpers.py
@@ -19,6 +19,7 @@
 """
 
 import metadata_model
+import re
 from collections import OrderedDict
 
 _context_buf = None
@@ -113,6 +114,66 @@
 
   return ".".join((i.name for i in path))
 
+def has_descendants_with_enums(node):
+  """
+  Determine whether or not the current node is or has any descendants with an
+  Enum node.
+
+  Args:
+    node: a Node instance
+
+  Returns:
+    True if it finds an Enum node in the subtree, False otherwise
+  """
+  return bool(node.find_first(lambda x: isinstance(x, metadata_model.Enum)))
+
+def get_children_by_throwing_away_kind(node, member='entries'):
+  """
+  Get the children of this node by compressing the subtree together by removing
+  the kind and then combining any children nodes with the same name together.
+
+  Args:
+    node: An instance of Section, InnerNamespace, or Kind
+
+  Returns:
+    An iterable over the combined children of the subtree of node,
+    as if the Kinds never existed.
+
+  Remarks:
+    Not recursive. Call this function repeatedly on each child.
+  """
+
+  if isinstance(node, metadata_model.Section):
+    # Note that this makes jump from Section to Kind,
+    # skipping the Kind entirely in the tree.
+    node_to_combine = node.combine_kinds_into_single_node()
+  else:
+    node_to_combine = node
+
+  combined_kind = node_to_combine.combine_children_by_name()
+
+  return (i for i in getattr(combined_kind, member))
+
+def get_children_by_filtering_kind(section, kind_name, member='entries'):
+  """
+  Takes a section and yields the children of the kind under this section.
+
+  Args:
+    section: An instance of Section
+    kind_name: A name of the kind, i.e. 'dynamic' or 'static' or 'controls'
+
+  Returns:
+    An iterable over the children of the specified kind.
+  """
+
+# TODO: test/use this function
+  matched_kind = next((i for i in section.kinds if i.name == kind_name), None)
+
+  if matched_kind:
+    return getattr(matched_kind, member)
+  else:
+    return ()
+
 ##
 ## Filters
 ##
@@ -241,3 +302,233 @@
     code doesn't support enums directly yet.
   """
   return 'TYPE_%s' %(what.upper())
+
+def jtype(entry):
+  """
+  Calculate the Java type from an entry type string, to be used as a generic
+  type argument in Java. The type is guaranteed to inherit from Object.
+
+  Remarks:
+    Since Java generics cannot be instantiated with primitives, this version
+    will use boxed types when absolutely required.
+
+  Returns:
+    The string representing the Java type.
+  """
+
+  if not isinstance(entry, metadata_model.Entry):
+    raise ValueError("Expected entry to be an instance of Entry")
+
+  primitive_type = entry.type
+
+  if entry.enum:
+    name = entry.name
+
+    name_without_ons = entry.get_name_as_list()[1:]
+    base_type = ".".join([pascal_case(i) for i in name_without_ons]) + \
+                 "Key.Enum"
+  else:
+    mapping = {
+      'int32': 'Integer',
+      'int64': 'Long',
+      'float': 'Float',
+      'double': 'Double',
+      'byte': 'Byte',
+      'rational': 'Rational'
+    }
+
+    base_type = mapping[primitive_type]
+
+  if entry.container == 'array':
+    additional = '[]'
+
+    #unbox if it makes sense
+    if primitive_type != 'rational' and not entry.enum:
+      base_type = jtype_primitive(primitive_type)
+  else:
+    additional = ''
+
+  return "%s%s" %(base_type, additional)
+
+def jtype_primitive(what):
+  """
+  Calculate the Java type from an entry type string.
+
+  Remarks:
+    Makes a special exception for Rational, since it's a primitive in terms of
+    the C-library camera_metadata type system.
+
+  Returns:
+    The string representing the primitive type
+  """
+  mapping = {
+    'int32': 'int',
+    'int64': 'long',
+    'float': 'float',
+    'double': 'double',
+    'byte': 'byte',
+    'rational': 'Rational'
+  }
+
+  try:
+    return mapping[what]
+  except KeyError as e:
+    raise ValueError("Can't map '%s' to a primitive, not supported" %what)
+
+def jclass(entry):
+  """
+  Calculate the java Class reference string for an entry.
+
+  Args:
+    entry: an Entry node
+
+  Example:
+    <entry name="some_int" type="int32"/>
+    <entry name="some_int_array" type="int32" container='array'/>
+
+    jclass(some_int) == 'int.class'
+    jclass(some_int_array) == 'int[].class'
+
+  Returns:
+    The ClassName.class string
+  """
+  the_type = entry.type
+  try:
+    class_name = jtype_primitive(the_type)
+  except ValueError as e:
+    class_name = the_type
+
+  if entry.container == 'array':
+    class_name += "[]"
+
+  return "%s.class" %class_name
+
+def jidentifier(what):
+  """
+  Convert the input string into a valid Java identifier.
+
+  Args:
+    what: any identifier string
+
+  Returns:
+    String with added underscores if necessary.
+  """
+  if re.match("\d", what):
+    return "_%s" %what
+  else:
+    return what
+
+def enum_calculate_value_string(enum_value):
+  """
+  Calculate the value of the enum, even if it does not have one explicitly
+  defined.
+
+  This looks back for the first enum value that has a predefined value and then
+  applies addition until we get the right value, using C-enum semantics.
+
+  Args:
+    enum_value: an EnumValue node with a valid Enum parent
+
+  Example:
+    <enum>
+      <value>X</value>
+      <value id="5">Y</value>
+      <value>Z</value>
+    </enum>
+
+    enum_calculate_value_string(X) == '0'
+    enum_calculate_Value_string(Y) == '5'
+    enum_calculate_value_string(Z) == '6'
+
+  Returns:
+    String that represents the enum value as an integer literal.
+  """
+
+  enum_value_siblings = list(enum_value.parent.values)
+  this_index = enum_value_siblings.index(enum_value)
+
+  def is_hex_string(instr):
+    return bool(re.match('0x[a-f0-9]+$', instr, re.IGNORECASE))
+
+  base_value = 0
+  base_offset = 0
+  emit_as_hex = False
+
+  this_id = enum_value_siblings[this_index].id
+  while this_index != 0 and not this_id:
+    this_index -= 1
+    base_offset += 1
+    this_id = enum_value_siblings[this_index].id
+
+  if this_id:
+    base_value = int(this_id, 0)  # guess base
+    emit_as_hex = is_hex_string(this_id)
+
+  if emit_as_hex:
+    return "0x%X" %(base_value + base_offset)
+  else:
+    return "%d" %(base_value + base_offset)
+
+def enumerate_with_last(iterable):
+  """
+  Enumerate a sequence of iterable, while knowing if this element is the last in
+  the sequence or not.
+
+  Args:
+    iterable: an Iterable of some sequence
+
+  Yields:
+    (element, bool) where the bool is True iff the element is last in the seq.
+  """
+  it = (i for i in iterable)
+
+  first = next(it)  # OK: raises exception if it is empty
+
+  second = first  # for when we have only 1 element in iterable
+
+  try:
+    while True:
+      second = next(it)
+      # more elements remaining.
+      yield (first, False)
+      first = second
+  except StopIteration:
+    # last element. no more elements left
+    yield (second, True)
+
+def pascal_case(what):
+  """
+  Convert the first letter of a string to uppercase, to make the identifier
+  conform to PascalCase.
+
+  Args:
+    what: a string representing some identifier
+
+  Returns:
+    String with first letter capitalized
+
+  Example:
+    pascal_case("helloWorld") == "HelloWorld"
+    pascal_case("foo") == "Foo"
+  """
+  return what[0:1].upper() + what[1:]
+
+def jenum(enum):
+  """
+  Calculate the Java symbol referencing an enum value (in Java).
+
+  Args:
+    enum: An Enum node
+
+  Returns:
+    String representing the Java symbol
+  """
+
+  entry = enum.parent
+  name = entry.name
+
+  name_without_ons = entry.get_name_as_list()[1:]
+  jenum_name = ".".join([pascal_case(i) for i in name_without_ons]) + "Key.Enum"
+
+  return jenum_name
+
diff --git a/camera/docs/metadata_helpers_test.py b/camera/docs/metadata_helpers_test.py
new file mode 100644
index 0000000..f4335cc
--- /dev/null
+++ b/camera/docs/metadata_helpers_test.py
@@ -0,0 +1,60 @@
+import unittest
+from unittest import TestCase
+from metadata_model import *
+from metadata_helpers import *
+
+class TestHelpers(TestCase):
+
+  def test_enum_calculate_value_string(self):
+    def compare_values_against_list(expected_list, enum):
+      for (idx, val) in enumerate(expected_list):
+        self.assertEquals(val,
+                          enum_calculate_value_string(list(enum.values)[idx]))
+
+    plain_enum = Enum(parent=None, values=['ON', 'OFF'])
+
+    compare_values_against_list(['0', '1'],
+                                plain_enum)
+
+    ###
+    labeled_enum = Enum(parent=None, values=['A', 'B', 'C'], ids={
+      'A': '12345',
+      'B': '0xC0FFEE',
+      'C': '0xDEADF00D'
+    })
+
+    compare_values_against_list(['12345', '0xC0FFEE', '0xDEADF00D'],
+                                labeled_enum)
+
+    ###
+    mixed_enum = Enum(parent=None,
+                      values=['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'],
+                      ids={
+                        'C': '0xC0FFEE',
+                        'E': '123',
+                        'G': '0xDEADF00D'
+                      })
+
+    expected_values = ['0', '1', '0xC0FFEE', '0xC0FFEF', '123', '124',
+                       '0xDEADF00D',
+                       '0xDEADF00E']
+
+    compare_values_against_list(expected_values, mixed_enum)
+
+  def test_enumerate_with_last(self):
+    empty_list = []
+
+    for (x, y) in enumerate_with_last(empty_list):
+      self.fail("Should not return anything for empty list")
+
+    single_value = [1]
+    for (x, last) in enumerate_with_last(single_value):
+      self.assertEquals(1, x)
+      self.assertEquals(True, last)
+
+    multiple_values = [4, 5, 6]
+    lst = list(enumerate_with_last(multiple_values))
+    self.assertListEqual([(4, False), (5, False), (6, True)], lst)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/camera/docs/metadata_model.py b/camera/docs/metadata_model.py
index fa85a58..daf171c 100644
--- a/camera/docs/metadata_model.py
+++ b/camera/docs/metadata_model.py
@@ -23,12 +23,13 @@
   Node: Base class for most nodes.
   Entry: A node corresponding to <entry> elements.
   Clone: A node corresponding to <clone> elements.
+  MergedEntry: A node corresponding to either <entry> or <clone> elements.
   Kind: A node corresponding to <dynamic>, <static>, <controls> elements.
   InnerNamespace: A node corresponding to a <namespace> nested under a <kind>.
   OuterNamespace: A node corresponding to a <namespace> with <kind> children.
   Section: A node corresponding to a <section> element.
   Enum: A class corresponding an <enum> element within an <entry>
-  Value: A class corresponding to a <value> element within an Enum
+  EnumValue: A class corresponding to a <value> element within an Enum
   Metadata: Root node that also provides tree construction functionality.
   Tag: A node corresponding to a top level <tag> element.
 """
@@ -80,7 +81,6 @@
       for j in i.find_all(pred):
         yield j
 
-
   def find_first(self, pred):
     """
     Find the first descendant that matches the predicate.
@@ -147,7 +147,7 @@
 
   def _children_name_map_matching(self, match=lambda x: True):
     d = {}
-    for i in _get_children():
+    for i in self._get_children():
       if match(i):
         d[i.name] = i
     return d
@@ -283,7 +283,6 @@
       (they will be ignored). Also the target entry need not be inserted
       ahead of the clone entry.
     """
-    entry_name = clone['name']
     # figure out corresponding entry later. allow clone insert, entry insert
     entry = None
     c = Clone(entry, **clone)
@@ -310,7 +309,7 @@
       if p.parent is not None:
         p.parent._entries.remove(p)
       # remove from parents' _leafs list
-      for ancestor in p.find_parents(lambda x: not isinstance(x, MetadataSet)):
+      for ancestor in p.find_parents(lambda x: not isinstance(x, Metadata)):
         ancestor._leafs.remove(p)
 
       # remove from global list
@@ -652,6 +651,37 @@
     for k in new_kinds_lst:
       yield k
 
+  def combine_kinds_into_single_node(self):
+    r"""
+    Combines the section's Kinds into a single node.
+
+    Combines all the children (kinds) of this section into a single
+    virtual Kind node.
+
+    Returns:
+      A new Kind node that collapses all Kind siblings into one, combining
+      all their children together.
+
+      For example, given self.kinds == [ x, y ]
+
+        x  y               z
+      / |  | \    -->   / | | \
+      a b  c d          a b c d
+
+      a new instance z is returned in this example.
+
+    Remarks:
+      The children of the kinds are the same references as before, that is
+      their parents will point to the old parents and not to the new parent.
+    """
+    combined = Kind(name="combined", parent=self)
+
+    for k in self._get_children():
+      combined._namespaces.extend(k.namespaces)
+      combined._entries.extend(k.entries)
+
+    return combined
+
 class Kind(Node):
   """
   A node corresponding to one of: <static>,<dynamic>,<controls> under a
@@ -695,6 +725,63 @@
     for i in self.entries:
       yield i
 
+  def combine_children_by_name(self):
+    r"""
+    Combine multiple children with the same name into a single node.
+
+    Returns:
+      A new Kind where all of the children with the same name were combined.
+
+      For example:
+
+      Given a Kind k:
+
+              k
+            / | \
+            a b c
+            | | |
+            d e f
+
+      a.name == "foo"
+      b.name == "foo"
+      c.name == "bar"
+
+      The returned Kind will look like this:
+
+             k'
+            /  \
+            a' c'
+          / |  |
+          d e  f
+
+    Remarks:
+      This operation is not recursive. To combine the grandchildren and other
+      ancestors, call this method on the ancestor nodes.
+    """
+    return Kind._combine_children_by_name(self, new_type=type(self))
+
+  # new_type is either Kind or InnerNamespace
+  @staticmethod
+  def _combine_children_by_name(self, new_type):
+    new_ins_dict = OrderedDict()
+    new_ent_dict = OrderedDict()
+
+    for ins in self.namespaces:
+      new_ins = new_ins_dict.setdefault(ins.name,
+                                        InnerNamespace(ins.name, parent=self))
+      new_ins._namespaces.extend(ins.namespaces)
+      new_ins._entries.extend(ins.entries)
+
+    for ent in self.entries:
+      new_ent = new_ent_dict.setdefault(ent.name,
+                                        ent.merge())
+
+    kind = new_type(self.name, self.parent)
+    kind._namespaces = new_ins_dict.values()
+    kind._entries = new_ent_dict.values()
+
+    return kind
+
 class InnerNamespace(Node):
   """
   A node corresponding to a <namespace> which is an ancestor of a Kind.
@@ -737,6 +824,42 @@
     for i in self.entries:
       yield i
 
+  def combine_children_by_name(self):
+    r"""
+    Combine multiple children with the same name into a single node.
+
+    Returns:
+      A new InnerNamespace where all of the children with the same name were
+      combined.
+
+      For example:
+
+      Given an InnerNamespace i:
+
+              i
+            / | \
+            a b c
+            | | |
+            d e f
+
+      a.name == "foo"
+      b.name == "foo"
+      c.name == "bar"
+
+      The returned InnerNamespace will look like this:
+
+             i'
+            /  \
+            a' c'
+          / |  |
+          d e  f
+
+    Remarks:
+      This operation is not recursive. To combine the grandchildren and other
+      ancestors, call this method on the ancestor nodes.
+    """
+    return Kind._combine_children_by_name(self, new_type=type(self))
+
 class EnumValue(Node):
   """
   A class corresponding to a <value> element within an <enum> within an <entry>.
@@ -777,6 +900,8 @@
   Attributes (Read-Only):
     parent: An edge to the parent, always an Entry instance.
     values: A sequence of EnumValue children.
+    has_values_with_id: A boolean representing if any of the children have a
+        non-empty id property.
   """
   def __init__(self, parent, values, ids={}, optionals=[], notes={}):
     self._values =                                                             \
@@ -790,6 +915,10 @@
   def values(self):
     return (i for i in self._values)
 
+  @property
+  def has_values_with_id(self):
+    return bool(any(i for i in self.values if i.id))
+
   def _get_children(self):
     return (i for i in self._values)
 
@@ -954,8 +1083,8 @@
     # access these via the 'enum' prop
     enum_values = kwargs.get('enum_values')
     enum_optionals = kwargs.get('enum_optionals')
-    enum_notes = kwargs.get('enum_notes') # { value => notes }
-    enum_ids = kwargs.get('enum_ids') # { value => notes }
+    enum_notes = kwargs.get('enum_notes')  # { value => notes }
+    enum_ids = kwargs.get('enum_ids')  # { value => notes }
     self._tuple_values = kwargs.get('tuple_values')
 
     self._description = kwargs.get('description')
@@ -964,7 +1093,7 @@
     self._notes = kwargs.get('notes')
 
     self._tag_ids = kwargs.get('tag_ids', [])
-    self._tags = None # Filled in by MetadataSet::_construct_tags
+    self._tags = None  # Filled in by MetadataSet::_construct_tags
 
     self._type_notes = kwargs.get('type_notes')
 
@@ -1106,9 +1235,9 @@
       Note that type is not specified since it has to be the same as the
       entry.type.
     """
-    self._entry = entry # Entry object
+    self._entry = entry  # Entry object
     self._target_kind = kwargs['target_kind']
-    self._name = kwargs['name'] # same as entry.name
+    self._name = kwargs['name']  # same as entry.name
     self._kind = kwargs['kind']
 
     # illegal to override the type, it should be the same as the entry
diff --git a/camera/docs/metadata_model_test.py b/camera/docs/metadata_model_test.py
new file mode 100644
index 0000000..eb79c9b
--- /dev/null
+++ b/camera/docs/metadata_model_test.py
@@ -0,0 +1,130 @@
+import unittest
+from unittest import TestCase
+from metadata_model import *
+
+class TestInnerNamespace(TestCase):
+  def test_combine_children_by_name(self):
+    #
+    # Set up
+    #
+    kind = Kind("some_root_kind", parent=None)
+    ins_outer = InnerNamespace("static", parent=kind)
+    kind._namespaces = [ins_outer]
+
+    ins1 = InnerNamespace("ins1", parent=ins_outer)
+    ins1a = InnerNamespace("ins1", parent=ins_outer)  # same name deliberately
+    entry1 = Entry(name="entry1", type="int32", kind="static",
+                   parent=ins1)
+    entry2 = Entry(name="entry2", type="int32", kind="static",
+                   parent=ins1a)
+    entry3 = Entry(name="entry3", type="int32", kind="static",
+                   parent=ins_outer)
+
+    ins_outer._namespaces = [ins1, ins1a]
+    ins_outer._entries = [entry3]
+
+    ins1._entries = [entry1]
+    ins1a._entries = [entry2]
+
+    #
+    # Test
+    #
+    combined_children_namespace = ins_outer.combine_children_by_name()
+
+    self.assertIsInstance(combined_children_namespace, InnerNamespace)
+    combined_ins = [i for i in combined_children_namespace.namespaces]
+    combined_ent = [i for i in combined_children_namespace.entries]
+
+    self.assertEquals(kind, combined_children_namespace.parent)
+    self.assertEquals(1, len(combined_ins))
+    self.assertEquals(1, len(combined_ent))
+
+    self.assertEquals("ins1", combined_ins[0].name)
+    self.assertEquals("entry3", combined_ent[0].name)
+
+    new_ins = combined_ins[0]
+    self.assertIn(entry1, new_ins.entries)
+    self.assertIn(entry2, new_ins.entries)
+
+
+class TestKind(TestCase):
+  def test_combine_kinds_into_single_node(self):
+    #
+    # Set up
+    #
+    section = Section("some_section", parent=None)
+    kind_static = Kind("static", parent=section)
+    kind_dynamic = Kind("dynamic", parent=section)
+    section._kinds = [kind_static, kind_dynamic]
+
+    ins1 = InnerNamespace("ins1", parent=kind_static)
+    ins2 = InnerNamespace("ins2", parent=kind_dynamic)
+    entry1 = Entry(name="entry1", type="int32", kind="static",
+                   parent=kind_static)
+    entry2 = Entry(name="entry2", type="int32", kind="static",
+                   parent=kind_dynamic)
+
+    kind_static._namespaces = [ins1]
+    kind_static._entries = [entry1]
+
+    kind_dynamic._namespaces = [ins2]
+    kind_dynamic._entries = [entry2]
+
+    #
+    # Test
+    #
+    combined_kind = section.combine_kinds_into_single_node()
+
+    self.assertEquals(section, combined_kind.parent)
+
+    self.assertIn(ins1, combined_kind.namespaces)
+    self.assertIn(ins2, combined_kind.namespaces)
+
+    self.assertIn(entry1, combined_kind.entries)
+    self.assertIn(entry2, combined_kind.entries)
+
+  def test_combine_children_by_name(self):
+    #
+    # Set up
+    #
+    section = Section("some_section", parent=None)
+    kind_static = Kind("static", parent=section)
+    section._kinds = [kind_static]
+
+    ins1 = InnerNamespace("ins1", parent=kind_static)
+    ins1a = InnerNamespace("ins1", parent=kind_static)  # same name deliberately
+    entry1 = Entry(name="entry1", type="int32", kind="static",
+                   parent=ins1)
+    entry2 = Entry(name="entry2", type="int32", kind="static",
+                   parent=ins1a)
+    entry3 = Entry(name="entry3", type="int32", kind="static",
+                   parent=kind_static)
+
+    kind_static._namespaces = [ins1, ins1a]
+    kind_static._entries = [entry3]
+
+    ins1._entries = [entry1]
+    ins1a._entries = [entry2]
+
+    #
+    # Test
+    #
+    combined_children_kind = kind_static.combine_children_by_name()
+
+    self.assertIsInstance(combined_children_kind, Kind)
+    combined_ins = [i for i in combined_children_kind.namespaces]
+    combined_ent = [i for i in combined_children_kind.entries]
+
+    self.assertEquals(section, combined_children_kind.parent)
+    self.assertEquals(1, len(combined_ins))
+    self.assertEquals(1, len(combined_ent))
+
+    self.assertEquals("ins1", combined_ins[0].name)
+    self.assertEquals("entry3", combined_ent[0].name)
+
+    new_ins = combined_ins[0]
+    self.assertIn(entry1, new_ins.entries)
+    self.assertIn(entry2, new_ins.entries)
+
+if __name__ == '__main__':
+    unittest.main()