SEBA-271 Fix xproto field numbers not passed through

Change-Id: Ib5abee510e1a02f025d3011699d9f34d59e201c1
diff --git a/lib/xos-genx/xos-genx-tests/helpers.py b/lib/xos-genx/xos-genx-tests/helpers.py
index 232cae1..ca73402 100644
--- a/lib/xos-genx/xos-genx-tests/helpers.py
+++ b/lib/xos-genx/xos-genx-tests/helpers.py
@@ -23,6 +23,7 @@
 
 # Store in this class the args to pass at the generator
 class FakeArgs:
+    verbosity=0
     pass
 
 class XProtoTestHelpers:
diff --git a/lib/xos-genx/xosgenx/generator.py b/lib/xos-genx/xosgenx/generator.py
index cc00742..dc1ff9e 100644
--- a/lib/xos-genx/xosgenx/generator.py
+++ b/lib/xos-genx/xosgenx/generator.py
@@ -42,7 +42,7 @@
             v = Proto2XProto()
             ast.accept(v)
 
-        v = XOS2Jinja()
+        v = XOS2Jinja(args)
         ast.accept(v)
         return v
 
diff --git a/lib/xos-genx/xosgenx/jinja2_extensions/base.py b/lib/xos-genx/xosgenx/jinja2_extensions/base.py
index 1293863..561aeea 100644
--- a/lib/xos-genx/xosgenx/jinja2_extensions/base.py
+++ b/lib/xos-genx/xosgenx/jinja2_extensions/base.py
@@ -120,7 +120,10 @@
         if accessor:
             base_fields = xproto_base_fields(table[accessor], table)
 
-            model_fields = table[accessor]['fields']
+            model_fields = [x.copy() for x in table[accessor]['fields']]
+            for field in model_fields:
+                field["accessor"] = accessor
+
             fields.extend(base_fields)
             fields.extend(model_fields)
 
@@ -132,6 +135,53 @@
 
     return fields
 
+def xproto_fields(m, table):
+    """ Generate the full list of models for the xproto message `m` including fields from the classes it inherits.
+
+        Inserts the special field "id" at the very beginning.
+
+        Each time we descend a new level of inheritance, increment the offset field numbers by 100. The base
+        class's fields will be numbered from 1-99, the first descendant will be number 100-199, the second
+        descdendant numbered from 200-299, and so on. This assumes any particular model as at most 100
+        fields.
+    """
+
+    model_fields = [x.copy() for x in m["fields"]]
+    for field in model_fields:
+        field["accessor"] = m["fqn"]
+
+    fields = xproto_base_fields(m, table) + model_fields
+
+    # The "id" field is a special field. Every model has one. Put it up front and pretend it's part of the
+
+    id_field = {'type': 'int32', 'name': 'id', 'options': {}, "id": "1", "accessor": fields[0]["accessor"]}
+
+    fields = [id_field] + fields
+
+    # Walk through the list of fields. They will be in depth-first search order from the base model forward. Each time
+    # the model changes, offset the protobuf field numbers by 100.
+    offset = 0
+    last_accessor = fields[0]["accessor"]
+    for field in fields:
+        if (field["accessor"] != last_accessor):
+            last_accessor = field["accessor"]
+            offset += 100
+        field_id = int(field["id"])
+        if (field_id < 1) or (field_id >= 100):
+            raise Exception("Only field numbers from 1 to 99 are permitted, field %s in model %s" % (field["name"], field["accessor"]))
+        field["id"] = int(field["id"]) + offset
+
+    # Check for duplicates
+    fields_by_number = {}
+    for field in fields:
+        id = field["id"]
+        dup = fields_by_number.get(id)
+        if dup:
+            raise Exception("Field %s has duplicate number %d with field %s in model %s" % (field["name"], id, dup["name"], field["accessor"]))
+        fields_by_number[id] = field
+
+    return fields
+
 def xproto_base_rlinks(m, table):
     links = []
 
@@ -146,6 +196,38 @@
 
     return links
 
+def xproto_rlinks(m, table):
+    """ Return the reverse links for the xproto message `m`.
+
+        If the link includes a reverse_id, then it will be used for the protobuf field id. If there is no
+        reverse_id, then one will automatically be allocated started at id 1900. It is incouraged that all links
+        include reverse_ids, so that field identifiers are deterministic across all protobuf messages.
+    """
+
+    index = 1900
+    links = xproto_base_rlinks(m, table) + m["rlinks"]
+
+    links = [x for x in links if ("+" not in x["src_port"]) and ("+" not in x["dst_port"])]
+
+    for link in links:
+        if link["reverse_id"]:
+            link["id"] = int(link["reverse_id"])
+        else:
+            link["id"] = index
+            index += 1
+
+    # check for duplicates
+    links_by_number={}
+    for link in links:
+        id = link["id"]
+        dup=links_by_number.get(id)
+        if dup:
+            raise Exception("Field %s has duplicate number %d with field %s in model %s" % (link["src_port"], id, link["src_port"], m["name"]))
+        links_by_number[id] = link
+
+    return links
+
+
 def xproto_base_links(m, table):
     links = []
 
diff --git a/lib/xos-genx/xosgenx/targets/fieldlist.xtarget b/lib/xos-genx/xosgenx/targets/fieldlist.xtarget
new file mode 100644
index 0000000..3c0dea4
--- /dev/null
+++ b/lib/xos-genx/xosgenx/targets/fieldlist.xtarget
@@ -0,0 +1,11 @@
+
+{% for object in proto.messages|sort(attribute='name') %}
+{{ object.name }}
+{%- for field in xproto_fields(object, proto.message_table) | sort(attribute='id') %}
+  {{ field.name }}, {{ field.id }}, {{ field.accessor }}
+{%- endfor %}
+{%- for field in xproto_rlinks(object, proto.message_table) | sort(attribute='id') %}
+  {{ field.src_port }}_ids, {{ field.id }}, {{ field.accessor }}
+{%- endfor %}
+
+{% endfor %}
diff --git a/lib/xos-genx/xosgenx/targets/protoapi.xtarget b/lib/xos-genx/xosgenx/targets/protoapi.xtarget
index 183bacd..2311b98 100644
--- a/lib/xos-genx/xosgenx/targets/protoapi.xtarget
+++ b/lib/xos-genx/xosgenx/targets/protoapi.xtarget
@@ -20,23 +20,23 @@
     option (contentTypeId) = "{{ xproto_unquote(xproto_first_non_empty([object.options.name, object.options.app_label, options.name, context.app_label])) }}.{{ object.name | lower }}";
     {%- endif %}
     {%- set id_field = {'type':'int32', 'name':'id', 'options':{}} -%}
-  {%- for field in (xproto_base_fields(object, proto.message_table) + object.fields + [id_field]) | sort(attribute='name')%}
+  {%- for field in xproto_fields(object, proto.message_table) | sort(attribute='id')%}
   {%- if field.options.type == "link" and field.options.link_type == "manytomany" %}
-    repeated int32 {{ field.name }}_ids = {{ loop.index }} [(manyToManyForeignKey).modelName = "{{ field.options.model }}"];
+    repeated int32 {{ field.name }}_ids = {{ field.id }} [(manyToManyForeignKey).modelName = "{{ field.options.model }}"];
   {%- else %}
     oneof {{ field.name }}_present {
-      {{ xproto_api_type(field) }} {{ field.name }}{% if field.link -%}_id{% endif %} = {{ loop.index }}{{ xproto_api_opts(field) }};
+      {{ xproto_api_type(field) }} {{ field.name }}{% if field.link -%}_id{% endif %} = {{ field.id }} {{ xproto_api_opts(field) }};
     }
   {%- endif -%}
   {%- endfor -%}
 
-  {%- for ref in xproto_base_rlinks(object, proto.message_table) + object.rlinks | sort(attribute='src_port') %}
+  {%- for ref in xproto_rlinks(object, proto.message_table) | sort(attribute='id') %}
   {%- if '+' not in ref.src_port and '+' not in ref.dst_port %}
-    repeated int32 {{ ref.src_port }}_ids  = {{ loop.index + 100 }} [(reverseForeignKey).modelName = "{{ ref.peer.name }}"];
+    repeated int32 {{ ref.src_port }}_ids  = {{ ref["id"] }} [(reverseForeignKey).modelName = "{{ ref.peer.name }}"];
   {%- endif -%}
   {%- endfor %}
-  string class_names = 201;
-  string self_content_type_id = 202;
+  string class_names = 2046;
+  string self_content_type_id = 2047;
 }
 
 message {{ xproto_pluralize(object) }} {
diff --git a/lib/xos-genx/xosgenx/xos2jinja.py b/lib/xos-genx/xosgenx/xos2jinja.py
index 42e83c3..aca2468 100644
--- a/lib/xos-genx/xosgenx/xos2jinja.py
+++ b/lib/xos-genx/xosgenx/xos2jinja.py
@@ -72,41 +72,6 @@
             count += 1
     return count
 
-def compute_rlinks(messages, message_dict):
-    rev_links = {}
-
-    link_opposite = {
-        'manytomany': 'manytomany',
-        'manytoone': 'onetomany',
-        'onetoone': 'onetoone',
-        'onetomany': 'manytoone'
-    }
-
-    for m in messages:
-        for l in m['links']:
-            rlink = copy.deepcopy(l)
-
-            rlink['_type'] = 'rlink'  # An implicit link, not declared in the model
-            rlink['src_port'] = l['dst_port']
-            rlink['dst_port'] = l['src_port']
-            rlink['peer'] = {'name': m['name'], 'package': m['package'], 'fqn': m['fqn']}
-            rlink['link_type'] = link_opposite[l['link_type']]
-
-            try:
-                try:
-                    rev_links[l['peer']['fqn']].append(rlink)
-                except TypeError:
-                    pass
-            except KeyError:
-                rev_links[l['peer']['fqn']] = [rlink]
-
-    for m in messages:
-        try:
-            m['rlinks'] = rev_links[m['name']]
-            message_dict[m['name']]['rlinks'] = m['rlinks']
-        except KeyError:
-            pass
-
 
 def name_to_value(obj):
     try:
@@ -129,7 +94,7 @@
     in addition to traversing it '''
 
 class XOS2Jinja(Visitor):
-    def __init__(self):
+    def __init__(self, args):
         super(XOS2Jinja, self).__init__()
 
         self.stack = Stack()
@@ -145,6 +110,7 @@
         self.verbose = 0
         self.first_field = True
         self.first_method = True
+        self.args = args
 
     def visit_PolicyDefinition(self, obj):
         if self.package:
@@ -241,6 +207,11 @@
             except AttributeError:
                 s['peer'] = obj.name
 
+        try:
+            s['reverse_id'] = obj.reverse_id.pval
+        except AttributeError:
+            s['reverse_id'] = obj.reverse_id
+
         s['_type'] = 'link'
         s['options'] = {'modifier': 'optional'}
 
@@ -397,7 +368,7 @@
 
             messages.insert(0, m)
 
-        compute_rlinks(messages, self.models)
+        self.compute_rlinks(messages, self.models)
 
         self.messages = messages
         return True
@@ -406,3 +377,45 @@
         count = self.count_stack.pop()
         self.count_stack.push(count + 1)
         return True
+
+    def compute_rlinks(self, messages, message_dict):
+        rev_links = {}
+
+        link_opposite = {
+            'manytomany': 'manytomany',
+            'manytoone': 'onetomany',
+            'onetoone': 'onetoone',
+            'onetomany': 'manytoone'
+        }
+
+        for m in messages:
+            for l in m['links']:
+                rlink = copy.deepcopy(l)
+
+                rlink['_type'] = 'rlink'  # An implicit link, not declared in the model
+                rlink['src_port'] = l['dst_port']
+                rlink['dst_port'] = l['src_port']
+                rlink['peer'] = {'name': m['name'], 'package': m['package'], 'fqn': m['fqn']}
+                rlink['link_type'] = link_opposite[l['link_type']]
+                rlink["reverse_id"] = l['reverse_id']
+
+                if (not l['reverse_id']) and (self.args.verbosity >= 1):
+                    print >> sys.stderr, "WARNING: Field %s in model %s has no reverse_id" % (l["src_port"], m["name"])
+
+                if l["reverse_id"] and ((int(l["reverse_id"]) < 1000) or (int(l["reverse_id"]) >= 1900)):
+                    raise Exception("reverse id for field %s in model %s should be between 1000 and 1899" % (l["src_port"], m["name"]))
+
+                try:
+                    try:
+                        rev_links[l['peer']['fqn']].append(rlink)
+                    except TypeError:
+                        pass
+                except KeyError:
+                    rev_links[l['peer']['fqn']] = [rlink]
+
+        for m in messages:
+            try:
+                m['rlinks'] = rev_links[m['name']]
+                message_dict[m['name']]['rlinks'] = m['rlinks']
+            except KeyError:
+                pass
diff --git a/lib/xos-genx/xosgenx/xosgen.py b/lib/xos-genx/xosgenx/xosgen.py
index ec1ba68..8259d58 100755
--- a/lib/xos-genx/xosgenx/xosgen.py
+++ b/lib/xos-genx/xosgenx/xosgen.py
@@ -27,6 +27,7 @@
 parse.add_argument('--kvpairs', dest='kv', action='store',default=None, help='Key value pairs to make available to the target')
 parse.add_argument('--write-to-file', dest='write_to_file', choices = ['single', 'model', 'target'], action='store',default=None, help='Single output file (single) or output file per model (model) or let target decide (target)')
 parse.add_argument('--version', action='version', version=__version__)
+parse.add_argument("-v", "--verbosity", action="count", default=0, help="increase output verbosity")
 
 group = parse.add_mutually_exclusive_group()
 group.add_argument('--dest-file', dest='dest_file', action='store',default=None, help='Output file name (if write-to-file is set to single)')