This repository has been archived by the owner on Nov 14, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ProtoWrapper class to represent Protobuf messages as Java Map/List
This commit introduces the `ProtoWrapper` class to the `twister-proto` module. `ProtoWrapper` provides a convenient way to interact with Protobuf messages using standard Java Map and List interfaces. It supports various types of fields, including repeated fields, nested messages, and enums. Main changes include: - Introduced `ProtoWrapper` class to wrap a Protobuf message into a Java Map. The map keys are the names of the Protobuf fields, and the values are field values converted to more Java-friendly types when necessary. - Added `Facade` and `FacadeList` classes as private inner classes of `ProtoWrapper` to handle Protobuf message representation. `Facade` presents Protobuf message as a Java Map while `FacadeList` presents repeated Protobuf fields as a Java List. - Implemented the `convertValue` method in `ProtoWrapper` to convert Protobuf field values to Java-friendly types. - Integrated support for Protobuf's 'oneof' fields within the `Facade` class. Only the currently set field in the 'oneof' is included in the Map. - Added `ProtoWrapperTest` in the test module to verify the functionality of `ProtoWrapper`. The above changes provide a more Java-friendly way to work with Protobuf messages, and also support the exploration of the entire structure of a Protobuf message using standard Java interfaces.
- Loading branch information
1 parent
1bd58c8
commit d35200e
Showing
2 changed files
with
477 additions
and
0 deletions.
There are no files selected for viewing
232 changes: 232 additions & 0 deletions
232
twister-proto/src/main/java/dev/twister/proto/ProtoWrapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,232 @@ | ||
package dev.twister.proto; | ||
|
||
import com.google.protobuf.ByteString; | ||
import com.google.protobuf.Descriptors; | ||
import com.google.protobuf.Message; | ||
|
||
import java.math.BigInteger; | ||
import java.util.AbstractList; | ||
import java.util.AbstractMap; | ||
import java.util.AbstractSet; | ||
import java.util.Iterator; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.NoSuchElementException; | ||
import java.util.Set; | ||
|
||
/** | ||
* Wraps a Protobuf message into a Java map, providing a convenient way to access the fields of | ||
* the message using standard Java methods. This can be useful when you need to work with Protobuf | ||
* messages but don't have access to the generated Java classes for them. | ||
* <p> | ||
* The keys of the map are the names of the Protobuf fields. The values are the field values, | ||
* converted to more Java-friendly types when necessary (for example, byte fields are converted to | ||
* ByteBuffer instances, and enum fields are converted to the names of the enum values). | ||
* <p> | ||
* Nested messages and repeated fields are also supported. Nested messages are themselves wrapped | ||
* into Facade instances, and repeated fields are wrapped into FacadeList instances. This means that | ||
* you can navigate the entire structure of a Protobuf message using just the standard Map and List | ||
* interfaces. | ||
*/ | ||
public class ProtoWrapper { | ||
public Map<String, Object> wrap(Message message) { | ||
return new Facade(message); | ||
} | ||
|
||
/** | ||
* Converts a Protobuf field value to a more Java-friendly form. | ||
* | ||
* <p>The method supports various types of fields, including repeated fields and nested messages. | ||
* Scalar types (like integers, strings, booleans, etc.) are converted as-is. Some other types | ||
* (like enum values or bytes) are converted to more convenient or idiomatic Java types. Repeated | ||
* fields are wrapped into a {@link FacadeList} instance, and nested messages are wrapped into a | ||
* {@link Facade} instance. | ||
* | ||
* <p>The treatment of repeated fields is special. When a repeated field is first encountered, | ||
* it's wrapped into a {@link FacadeList}. The {@code isRepeated} argument should be true in this | ||
* case. The {@link FacadeList} will later call this method for individual elements of the list, | ||
* and in this case {@code isRepeated} should be false, because those individual elements are not | ||
* repeated fields themselves. | ||
* | ||
* @param field the field descriptor | ||
* @param value the field value | ||
* @param isRepeated whether the field is a repeated field | ||
* @return the converted value | ||
* @throws IllegalArgumentException if the field type is unsupported | ||
*/ | ||
private Object convertValue(Descriptors.FieldDescriptor field, Object value, boolean isRepeated) { | ||
if (isRepeated) { | ||
return new FacadeList(field, (List<?>) value); | ||
} else if (field.getType() == Descriptors.FieldDescriptor.Type.MESSAGE) { | ||
return new Facade((Message) value); | ||
} else { | ||
switch (field.getType()) { | ||
case INT32: | ||
case SINT32: | ||
case SFIXED32: | ||
case INT64: | ||
case SINT64: | ||
case SFIXED64: | ||
case BOOL: | ||
case STRING: | ||
case DOUBLE: | ||
case FLOAT: | ||
return value; | ||
case UINT32: | ||
case FIXED32: | ||
return ((Integer) value).longValue() & 0xFFFFFFFFL; | ||
case UINT64: | ||
case FIXED64: | ||
return BigInteger.valueOf((Long) value).and(BigInteger.valueOf(Long.MAX_VALUE)).setBit(63); | ||
case ENUM: | ||
return ((Descriptors.EnumValueDescriptor) value).getName(); | ||
case BYTES: | ||
return ((ByteString) value).asReadOnlyByteBuffer(); | ||
default: | ||
throw new IllegalArgumentException("Unsupported type: " + field.getType()); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Provides a view of a Protobuf message as a Java Map. The map keys are the names of the fields | ||
* in the Protobuf message, and the map values are the corresponding field values, converted to | ||
* Java-friendly types where necessary. | ||
* <p> | ||
* This class supports all Protobuf field types, including nested messages and repeated fields. | ||
* Nested messages are wrapped into Facade instances, and repeated fields are wrapped into | ||
* FacadeList instances. This means that you can navigate the entire structure of a Protobuf | ||
* message using just the standard Map and List interfaces. | ||
* <p> | ||
* This class also takes into account 'oneof' fields. Only the field that is currently set in | ||
* the 'oneof' is included in the map. | ||
*/ | ||
private class Facade extends AbstractMap<String, Object> { | ||
|
||
private final Message message; | ||
private final List<Descriptors.FieldDescriptor> allFields; | ||
private int size; | ||
|
||
/** | ||
* Constructor to create Facade wrapping the protobuf message. | ||
* | ||
* @param message the protobuf message | ||
*/ | ||
Facade(Message message) { | ||
this.message = message; | ||
this.allFields = message.getDescriptorForType().getFields(); | ||
this.size = calculateSize(); | ||
} | ||
|
||
/** | ||
* Calculates the size (the number of set fields), taking 'oneof' fields into account. | ||
* | ||
* @return the size | ||
*/ | ||
private int calculateSize() { | ||
int size = 0; | ||
for (Descriptors.FieldDescriptor field : allFields) { | ||
if (field.getContainingOneof() != null) { | ||
if (message.getOneofFieldDescriptor(field.getContainingOneof()) == field) { | ||
size++; // Count the field if it's the one currently set in the 'oneof'. | ||
} | ||
} else { | ||
size++; // Regular field, always count it. | ||
} | ||
} | ||
return size; | ||
} | ||
|
||
@Override | ||
public Set<Entry<String, Object>> entrySet() { | ||
return new AbstractSet<>() { | ||
/** | ||
* This iterator iterates over both regular and 'oneof' fields of the Protobuf message, maintaining the | ||
* order of fields as they are defined in the proto file. If a 'oneof' field is currently set, it is | ||
* returned when its turn comes according to its order in the proto file. | ||
*/ | ||
@Override | ||
public Iterator<Entry<String, Object>> iterator() { | ||
return new Iterator<>() { | ||
private int index = 0; // Current field index. | ||
|
||
/** | ||
* Checks if there is a next field. This method also prepares the next field to be returned, | ||
* taking 'oneof' fields into account. | ||
*/ | ||
@Override | ||
public boolean hasNext() { | ||
return index < allFields.size(); | ||
} | ||
|
||
/** | ||
* Returns the next field. If a 'oneof' field is set and its turn comes, it is returned. | ||
*/ | ||
@Override | ||
public Entry<String, Object> next() { | ||
if (!hasNext()) { | ||
throw new NoSuchElementException(); | ||
} | ||
|
||
Descriptors.FieldDescriptor field = allFields.get(index); | ||
|
||
// If the field is part of a `oneof`, and it is not the one currently set in the `oneof`, | ||
// continue to the next field. | ||
while (field.getContainingOneof() != null | ||
&& field != message.getOneofFieldDescriptor(field.getContainingOneof())) { | ||
if (++index >= allFields.size()) { | ||
throw new NoSuchElementException(); | ||
} | ||
field = allFields.get(index); | ||
} | ||
|
||
Object value = message.getField(field); | ||
index++; | ||
return new SimpleImmutableEntry<>(field.getName(), | ||
convertValue(field, value, field.isRepeated())); | ||
} | ||
}; | ||
} | ||
|
||
/** | ||
* Returns the number of set fields in the Protobuf message. This includes the set field of any 'oneof' | ||
* field group, if any. | ||
*/ | ||
@Override | ||
public int size() { | ||
return size; | ||
} | ||
}; | ||
} | ||
} | ||
|
||
/** | ||
* Provides a view of a Protobuf repeated field as a Java List. The list elements are the | ||
* values of the repeated field, converted to Java-friendly types where necessary. | ||
* <p> | ||
* This class supports all Protobuf field types, including nested messages. Nested messages are | ||
* wrapped into Facade instances. This means that you can navigate the entire structure of a | ||
* Protobuf message using just the standard Map and List interfaces. | ||
*/ | ||
private class FacadeList extends AbstractList<Object> { | ||
|
||
private final Descriptors.FieldDescriptor field; | ||
private final List<?> list; | ||
|
||
FacadeList(Descriptors.FieldDescriptor field, List<?> list) { | ||
this.field = field; | ||
this.list = list; | ||
} | ||
|
||
@Override | ||
public Object get(int index) { | ||
// Pass `false` for the `isRepeated` parameter to handle individual elements correctly | ||
return convertValue(field, list.get(index), false); | ||
} | ||
|
||
@Override | ||
public int size() { | ||
return list.size(); | ||
} | ||
} | ||
} |
Oops, something went wrong.