protected function processPropertyList($properties, $include_internal) {
    $return = [];

    // Track the primary property so we can check there is not more than one.
    $primary = NULL;

    foreach ($properties as $property_name => $property_info) {
      // Check there is no more than one property set as primary.
      if (!empty($property_info['primary'])) {
        assert(is_null($primary), "Property {$primary} already set as primary, but so is {$property_name}.");

        $primary = $property_name;
      }

      // Skip computed and internal if not requested.
      if (!$include_internal) {
        if (!empty($property_info['computed']) ||
          !empty($property_info['internal']) ||
          !empty($property_info['acquired'])
        ) {
          continue;
        }
      }

      // Add defaults.
      // We add these for internal properties as well, even though UIs won't
      // see them, for our own convenience of not having to check whether
      // keys such as 'format' are set.
      $this->componentDataInfoAddDefaults($property_info);

      // Expand compound properties..
      if ($property_info['format'] == 'compound') {
        if (isset($property_info['component_type'])) {
          // Properties that use a generator.
          $component_type = $property_info['component_type'];

          // Recurse to get the child properties.
          $child_properties = $this->getComponentDataInfo($component_type, $include_internal);

          $property_info['properties'] = $child_properties;
        }
        else {
          // Recurse into the child properties list.
          $property_info['properties'] = $this->processPropertyList($property_info['properties'], $include_internal);
        }
      }

      $return[$property_name] = $property_info;
    }

    return $return;
  }