Skip to content

Commit 9f9ee70

Browse files
authored
Implement #712: idiomatic protobuf map<K,V> support (read + write) (#719)
1 parent ccff5a6 commit 9f9ee70

13 files changed

Lines changed: 2213 additions & 119 deletions

File tree

protobuf/src/main/java/com/fasterxml/jackson/dataformat/protobuf/ProtobufGenerator.java

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,12 @@ public final void writeFieldName(String name) throws IOException {
281281
if (!_inObject) {
282282
_reportError("Can not write field name: current context not Object but "+_pbContext.typeDesc());
283283
}
284+
// [dataformats-binary#712] Within a `map`, each "field name" is a map key
285+
// that opens a new entry sub-message
286+
if (_pbContext.inMap()) {
287+
_startMapEntry(name);
288+
return;
289+
}
284290
ProtobufField f = _currField;
285291
// important: use current field only if NOT repeated field; repeated
286292
// field means an array until START_OBJECT
@@ -312,8 +318,14 @@ public final void writeFieldName(SerializableString sstr) throws IOException {
312318
if (!_inObject) {
313319
_reportError("Can not write field name: current context not Object but "+_pbContext.typeDesc());
314320
}
315-
ProtobufField f = _currField;
316321
final String name = sstr.getValue();
322+
// [dataformats-binary#712] Within a `map`, each "field name" is a map key
323+
// that opens a new entry sub-message
324+
if (_pbContext.inMap()) {
325+
_startMapEntry(name);
326+
return;
327+
}
328+
ProtobufField f = _currField;
317329
// important: use current field only if NOT repeated field; repeated
318330
// field means an array until START_OBJECT
319331
// NOTE: not ideal -- depends on if it really is sibling field of an array,
@@ -417,6 +429,11 @@ public final void writeStartArray() throws IOException
417429
_reportError("Can not write START_ARRAY without field (message type "+_currMessage.getName()+")");
418430
return; // never gets here but code analyzers can't see that
419431
}
432+
// [dataformats-binary#712] A `map` field is also "repeated" underneath, but
433+
// must be written as an Object, not an Array
434+
if (_currField.isMap) {
435+
_reportError("Can not write START_ARRAY: field '"+_currField.name+"' is a `map`; write START_OBJECT instead");
436+
}
420437
if (!_currField.isArray()) {
421438
_reportError("Can not write START_ARRAY: field '"+_currField.name+"' not declared as 'repeated'");
422439
}
@@ -470,6 +487,17 @@ public final void writeStartObject() throws IOException
470487
}
471488
_currMessage = _schema.getRootType();
472489
// note: no buffering on root
490+
} else if (_currField.isMap) {
491+
// [dataformats-binary#712] a `map<K,V>` field: the Object being opened is
492+
// a sequence of entry sub-messages, not a single one. Push a dedicated map
493+
// context; per-entry buffering happens in writeFieldName. No buffering of
494+
// the map as a whole.
495+
_pbContext = _pbContext.createChildMapContext(_currField);
496+
streamWriteConstraints().validateNestingDepth(_pbContext.getNestingDepth());
497+
_currMessage = _currField.getMessageType(); // the synthetic entry message
498+
_inObject = true;
499+
_writeTag = true;
500+
return;
473501
} else {
474502
// but also, field value must be Message if so
475503
if (!_currField.isObject) {
@@ -504,6 +532,28 @@ public final void writeEndObject() throws IOException
504532
if (!_inObject) {
505533
_reportError("Current context not Object but "+_pbContext.typeDesc());
506534
}
535+
// [dataformats-binary#712] Closing a `map`: finalize the last open entry (if
536+
// any), then pop -- but do NOT finish-buffer the map as a whole (each entry
537+
// was already length-prefixed on its own).
538+
if (_pbContext.inMap()) {
539+
if (_pbContext.isEntryOpen()) {
540+
_pbContext.setEntryOpen(false);
541+
_finishBuffering();
542+
}
543+
_pbContext = _pbContext.getParent();
544+
if (_pbContext.inRoot()) {
545+
if (!_complete) {
546+
_complete();
547+
}
548+
} else {
549+
_currMessage = _pbContext.getMessageType();
550+
}
551+
_currField = _pbContext.getField();
552+
boolean inObj = _pbContext.inObject();
553+
_inObject = inObj;
554+
_writeTag = inObj || !_pbContext.inArray() || !_currField.packed;
555+
return;
556+
}
507557
_pbContext = _pbContext.getParent();
508558
if (_pbContext.inRoot()) {
509559
if (!_complete) {
@@ -1737,6 +1787,141 @@ private final int _writeTag(int ptr)
17371787
return ptr;
17381788
}
17391789

1790+
/*
1791+
/**********************************************************
1792+
/* Internal map (`map<K,V>`) writes [dataformats-binary#712]
1793+
/**********************************************************
1794+
*/
1795+
1796+
/**
1797+
* Opens a new {@code map} entry sub-message for the given key, finalizing the
1798+
* previous entry first if one is still open. Writes the entry's key (tag 1) and
1799+
* leaves {@link #_currField} pointing at the value field (tag 2) so the value
1800+
* write that follows targets it.
1801+
*/
1802+
private void _startMapEntry(String name) throws IOException
1803+
{
1804+
final ProtobufField mapField = _pbContext.getField();
1805+
// Close the previous entry, if any (its length prefix is finalized here)
1806+
if (_pbContext.isEntryOpen()) {
1807+
_pbContext.setEntryOpen(false);
1808+
_finishBuffering();
1809+
}
1810+
// Each entry is a length-delimited sub-message, tagged with the map field's tag
1811+
_startBuffering(mapField.typedTag);
1812+
_pbContext.setEntryOpen(true);
1813+
_currMessage = mapField.getMessageType();
1814+
// Write key (tag 1) ...
1815+
_writeTag = true;
1816+
_writeMapKey(mapField.getKeyField(), name);
1817+
// ... then prime the value field (tag 2) for the value write that follows
1818+
_currField = mapField.getValueField();
1819+
_writeTag = true;
1820+
}
1821+
1822+
/**
1823+
* Writes a map key (always arriving as a {@code String}) using the key field's
1824+
* declared protobuf type. Key types are restricted (and validated during schema
1825+
* resolution) to integral / {@code bool} / {@code string}.
1826+
*/
1827+
private void _writeMapKey(ProtobufField keyField, String name) throws IOException
1828+
{
1829+
_currField = keyField;
1830+
switch (keyField.type) {
1831+
case STRING:
1832+
writeString(name);
1833+
break;
1834+
case BOOLEAN:
1835+
writeBoolean(_parseBooleanMapKey(name, keyField));
1836+
break;
1837+
case VINT32_Z: // `sint32`: signed only
1838+
writeNumber(_parseIntMapKey(name, keyField, false));
1839+
break;
1840+
case VINT32_STD: // `int32` or `uint32`
1841+
case FIXINT32: // `sfixed32` or `fixed32`
1842+
writeNumber(_parseIntMapKey(name, keyField, true));
1843+
break;
1844+
case VINT64_Z: // `sint64`: signed only
1845+
writeNumber(_parseLongMapKey(name, keyField, false));
1846+
break;
1847+
case VINT64_STD: // `int64` or `uint64`
1848+
case FIXINT64: // `sfixed64` or `fixed64`
1849+
writeNumber(_parseLongMapKey(name, keyField, true));
1850+
break;
1851+
default:
1852+
// Should not happen: key types are validated during schema resolution
1853+
_reportError("Unsupported `map` key type "+keyField.type+" for field '"+keyField.name+"'");
1854+
}
1855+
}
1856+
1857+
/**
1858+
* Parses a 32-bit {@code map} key.
1859+
*<p>
1860+
* {@link FieldType} does not distinguish signed from unsigned declarations
1861+
* ({@code int32} and {@code uint32} are both {@code VINT32_STD}), so for those the
1862+
* accepted range is the union of the two: {@code [Integer.MIN_VALUE, 0xFFFFFFFF]}.
1863+
* Narrowing a value above {@code Integer.MAX_VALUE} keeps the same 32 bits, which is
1864+
* exactly the {@code uint32} / {@code fixed32} encoding.
1865+
*
1866+
* @param allowUnsigned Whether the key type has an unsigned variant; {@code false}
1867+
* for {@code sint32}, which is always signed.
1868+
*/
1869+
private int _parseIntMapKey(String name, ProtobufField keyField, boolean allowUnsigned)
1870+
throws IOException
1871+
{
1872+
final long l;
1873+
try {
1874+
l = Long.parseLong(name);
1875+
} catch (NumberFormatException e) {
1876+
_reportError("Invalid `map` key \""+name+"\" for integral key field '"+keyField.name
1877+
+"' (type "+keyField.type+")");
1878+
return 0; // never reached
1879+
}
1880+
final long max = allowUnsigned ? 0xFFFFFFFFL : Integer.MAX_VALUE;
1881+
if ((l < Integer.MIN_VALUE) || (l > max)) {
1882+
_reportError("Invalid `map` key \""+name+"\" for integral key field '"+keyField.name
1883+
+"' (type "+keyField.type+"): out of range ("+Integer.MIN_VALUE+" to "+max+")");
1884+
}
1885+
return (int) l;
1886+
}
1887+
1888+
/**
1889+
* Parses a 64-bit {@code map} key. As with 32-bit keys the signed and unsigned
1890+
* declarations share a {@link FieldType}, so {@code uint64} keys above
1891+
* {@link Long#MAX_VALUE} are accepted as unsigned: the resulting bit pattern is the
1892+
* same 64-bit varint either way.
1893+
*
1894+
* @param allowUnsigned Whether the key type has an unsigned variant; {@code false}
1895+
* for {@code sint64}, which is always signed.
1896+
*/
1897+
private long _parseLongMapKey(String name, ProtobufField keyField, boolean allowUnsigned)
1898+
throws IOException
1899+
{
1900+
try {
1901+
return Long.parseLong(name);
1902+
} catch (NumberFormatException e) {
1903+
if (allowUnsigned) {
1904+
try {
1905+
return Long.parseUnsignedLong(name);
1906+
} catch (NumberFormatException e2) { }
1907+
}
1908+
_reportError("Invalid `map` key \""+name+"\" for integral key field '"+keyField.name
1909+
+"' (type "+keyField.type+")");
1910+
return 0L; // never reached
1911+
}
1912+
}
1913+
1914+
private boolean _parseBooleanMapKey(String name, ProtobufField keyField) throws IOException {
1915+
if ("true".equals(name)) {
1916+
return true;
1917+
}
1918+
if ("false".equals(name)) {
1919+
return false;
1920+
}
1921+
_reportError("Invalid `map` key \""+name+"\" for boolean key field '"+keyField.name+"'");
1922+
return false; // never reached
1923+
}
1924+
17401925
/**
17411926
* Method called when buffering an entry that should be prefixed
17421927
* with a type tag.

0 commit comments

Comments
 (0)