This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fury.git
The following commit(s) were added to refs/heads/main by this push:
new 6376e527 feat(java): FuryField annotation hints for struct
serialization (#2036)
6376e527 is described below
commit 6376e5275524a0254772d52e305b3a1556aef2ca
Author: hn <[email protected]>
AuthorDate: Mon Apr 21 23:58:44 2025 +0800
feat(java): FuryField annotation hints for struct serialization (#2036)
<!--
**Thanks for contributing to Fury.**
**If this is your first time opening a PR on fury, you can refer to
[CONTRIBUTING.md](https://github.com/apache/fury/blob/main/CONTRIBUTING.md).**
Contribution Checklist
- The **Apache Fury (incubating)** community has restrictions on the
naming of pr titles. You can also find instructions in
[CONTRIBUTING.md](https://github.com/apache/fury/blob/main/CONTRIBUTING.md).
- Fury has a strong focus on performance. If the PR you submit will have
an impact on performance, please benchmark it first and provide the
benchmark result here.
-->
## What does this PR do?
<!-- Describe the purpose of this PR. -->
## Related issues
#1956
<!--
Is there any related issue? Please attach here.
- #xxxx0
- #xxxx1
- #xxxx2
-->
## Does this PR introduce any user-facing change?
<!--
If any user-facing interface changes, please [open an
issue](https://github.com/apache/fury/issues/new/choose) describing the
need to do so and update the document if necessary.
-->
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
<!--
When the PR has an impact on performance (if you don't know whether the
PR will have an impact on performance, you can submit the PR first, and
if it will have impact on performance, the code reviewer will explain
it), be sure to attach a benchmark data here.
-->
---------
Co-authored-by: hening <[email protected]>
---
.../java/org/apache/fury/annotation/FuryField.java | 33 +++
.../org/apache/fury/resolver/FieldResolver.java | 13 +-
.../fury/serializer/AbstractObjectSerializer.java | 280 +++++++++++++++------
.../fury/serializer/CompatibleSerializer.java | 18 +-
.../fury/serializer/MetaSharedSerializer.java | 8 +-
.../serializer/NonexistentClassSerializers.java | 3 +-
.../apache/fury/serializer/ObjectSerializer.java | 47 ++--
.../fury/serializer/SerializationBinding.java | 79 ++++++
.../main/java/org/apache/fury/type/Descriptor.java | 14 ++
.../fury-core/native-image.properties | 1 +
.../apache/fury/annotation/FuryAnnotationTest.java | 177 +++++++++++++
11 files changed, 568 insertions(+), 105 deletions(-)
diff --git
a/java/fury-core/src/main/java/org/apache/fury/annotation/FuryField.java
b/java/fury-core/src/main/java/org/apache/fury/annotation/FuryField.java
new file mode 100644
index 00000000..c36ed986
--- /dev/null
+++ b/java/fury-core/src/main/java/org/apache/fury/annotation/FuryField.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.fury.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface FuryField {
+
+ /** Whether field is nullable, default false. */
+ boolean nullable() default false;
+}
diff --git
a/java/fury-core/src/main/java/org/apache/fury/resolver/FieldResolver.java
b/java/fury-core/src/main/java/org/apache/fury/resolver/FieldResolver.java
index 17d6b2e8..65110875 100644
--- a/java/fury-core/src/main/java/org/apache/fury/resolver/FieldResolver.java
+++ b/java/fury-core/src/main/java/org/apache/fury/resolver/FieldResolver.java
@@ -43,6 +43,7 @@ import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;
import org.apache.fury.Fury;
+import org.apache.fury.annotation.FuryField;
import org.apache.fury.collection.Tuple2;
import org.apache.fury.exception.ClassNotCompatibleException;
import org.apache.fury.memory.MemoryBuffer;
@@ -748,6 +749,7 @@ public class FieldResolver {
protected final ClassResolver classResolver;
private final FieldAccessor fieldAccessor;
private final ClassInfoHolder classInfoHolder;
+ private boolean nullable;
public FieldInfo(
Fury fury,
@@ -772,6 +774,8 @@ public class FieldResolver {
} else {
fieldAccessor = FieldAccessor.createAccessor(field);
}
+ FuryField furyField = field == null ? null :
field.getAnnotation(FuryField.class);
+ this.nullable = furyField == null || furyField.nullable();
}
public static FieldInfo of(
@@ -838,6 +842,10 @@ public class FieldResolver {
}
}
+ public boolean isNullable() {
+ return nullable;
+ }
+
public String getName() {
return name;
}
@@ -1021,9 +1029,4 @@ public class FieldResolver {
return valueType;
}
}
-
- public static void main(String[] args) {
- System.out.println(computeStringHash("list0") << 2);
- System.out.println(computeStringHash("serializeListLast") << 2);
- }
}
diff --git
a/java/fury-core/src/main/java/org/apache/fury/serializer/AbstractObjectSerializer.java
b/java/fury-core/src/main/java/org/apache/fury/serializer/AbstractObjectSerializer.java
index 8bd5114f..3af22ebe 100644
---
a/java/fury-core/src/main/java/org/apache/fury/serializer/AbstractObjectSerializer.java
+++
b/java/fury-core/src/main/java/org/apache/fury/serializer/AbstractObjectSerializer.java
@@ -89,9 +89,10 @@ public abstract class AbstractObjectSerializer<T> extends
Serializer<T> {
MemoryBuffer buffer) {
Serializer<Object> serializer = fieldInfo.classInfo.getSerializer();
Object fieldValue;
+ boolean nullable = fieldInfo.nullable;
if (isFinal) {
if (!fieldInfo.trackingRef) {
- return binding.readNullable(buffer, serializer);
+ return binding.readNullable(buffer, serializer, nullable);
}
// whether tracking ref is recorded in `fieldInfo.serializer`, so it's
still
// consistent with jit serializer.
@@ -107,13 +108,14 @@ public abstract class AbstractObjectSerializer<T> extends
Serializer<T> {
fieldValue = refResolver.getReadObject();
}
} else {
- byte headFlag = buffer.readByte();
- if (headFlag == Fury.NULL_FLAG) {
- fieldValue = null;
- } else {
- typeResolver.readClassInfo(buffer, fieldInfo.classInfo);
- fieldValue = serializer.read(buffer);
+ if (nullable) {
+ byte headFlag = buffer.readByte();
+ if (headFlag == Fury.NULL_FLAG) {
+ return null;
+ }
}
+ typeResolver.readClassInfo(buffer, fieldInfo.classInfo);
+ fieldValue = serializer.read(buffer);
}
}
return fieldValue;
@@ -122,15 +124,17 @@ public abstract class AbstractObjectSerializer<T> extends
Serializer<T> {
static Object readOtherFieldValue(
SerializationBinding binding, GenericTypeField fieldInfo, MemoryBuffer
buffer) {
Object fieldValue;
+ boolean nullable = fieldInfo.nullable;
if (fieldInfo.trackingRef) {
fieldValue = binding.readRef(buffer, fieldInfo);
} else {
- byte headFlag = buffer.readByte();
- if (headFlag == Fury.NULL_FLAG) {
- fieldValue = null;
- } else {
- fieldValue = binding.readNonRef(buffer, fieldInfo);
+ if (nullable) {
+ byte headFlag = buffer.readByte();
+ if (headFlag == Fury.NULL_FLAG) {
+ return null;
+ }
}
+ fieldValue = binding.readNonRef(buffer, fieldInfo);
}
return fieldValue;
}
@@ -146,14 +150,16 @@ public abstract class AbstractObjectSerializer<T> extends
Serializer<T> {
fieldValue = binding.readContainerFieldValueRef(buffer, fieldInfo);
generics.popGenericType();
} else {
- byte headFlag = buffer.readByte();
- if (headFlag == Fury.NULL_FLAG) {
- fieldValue = null;
- } else {
- generics.pushGenericType(fieldInfo.genericType);
- fieldValue = binding.readContainerFieldValue(buffer, fieldInfo);
- generics.popGenericType();
+ boolean nullable = fieldInfo.nullable;
+ if (nullable) {
+ byte headFlag = buffer.readByte();
+ if (headFlag == Fury.NULL_FLAG) {
+ return null;
+ }
}
+ generics.pushGenericType(fieldInfo.genericType);
+ fieldValue = binding.readContainerFieldValue(buffer, fieldInfo);
+ generics.popGenericType();
}
return fieldValue;
}
@@ -251,7 +257,7 @@ public abstract class AbstractObjectSerializer<T> extends
Serializer<T> {
}
/**
- * Write field value to buffer.
+ * Write field value to buffer. This method handle the situation which all
fields are not null.
*
* @return true if field value isn't written by this function.
*/
@@ -261,6 +267,70 @@ public abstract class AbstractObjectSerializer<T> extends
Serializer<T> {
return true; // let common path handle this.
}
// add time types serialization here.
+ switch (classId) {
+ case ClassResolver.STRING_CLASS_ID: // fastpath for string.
+ String stringValue = (String) (fieldValue);
+ if (fury.getStringSerializer().needToWriteRef()) {
+ fury.writeJavaStringRef(buffer, stringValue);
+ } else {
+ fury.writeString(buffer, stringValue);
+ }
+ return false;
+ case ClassResolver.BOOLEAN_CLASS_ID:
+ {
+ buffer.writeBoolean((Boolean) fieldValue);
+ return false;
+ }
+ case ClassResolver.BYTE_CLASS_ID:
+ {
+ buffer.writeByte((Byte) fieldValue);
+ return false;
+ }
+ case ClassResolver.CHAR_CLASS_ID:
+ {
+ buffer.writeChar((Character) fieldValue);
+ return false;
+ }
+ case ClassResolver.SHORT_CLASS_ID:
+ {
+ buffer.writeInt16((Short) fieldValue);
+ return false;
+ }
+ case ClassResolver.INTEGER_CLASS_ID:
+ {
+ if (fury.compressInt()) {
+ buffer.writeVarInt32((Integer) fieldValue);
+ } else {
+ buffer.writeInt32((Integer) fieldValue);
+ }
+ return false;
+ }
+ case ClassResolver.FLOAT_CLASS_ID:
+ {
+ buffer.writeFloat32((Float) fieldValue);
+ return false;
+ }
+ case ClassResolver.LONG_CLASS_ID:
+ {
+ fury.writeInt64(buffer, (Long) fieldValue);
+ return false;
+ }
+ case ClassResolver.DOUBLE_CLASS_ID:
+ {
+ buffer.writeFloat64((Double) fieldValue);
+ return false;
+ }
+ default:
+ return true;
+ }
+ }
+
+ static boolean writeBasicNullableObjectFieldValueFailed(
+ Fury fury, MemoryBuffer buffer, Object fieldValue, short classId) {
+ if (!fury.isBasicTypesRefIgnored()) {
+ return true; // let common path handle this.
+ }
+ // add time types serialization here.
switch (classId) {
case ClassResolver.STRING_CLASS_ID: // fastpath for string.
fury.writeJavaStringRef(buffer, (String) (fieldValue));
@@ -399,9 +469,6 @@ public abstract class AbstractObjectSerializer<T> extends
Serializer<T> {
case ClassResolver.PRIMITIVE_DOUBLE_CLASS_ID:
fieldAccessor.set(targetObject, buffer.readFloat64());
return false;
- case ClassResolver.STRING_CLASS_ID:
- fieldAccessor.putObject(targetObject, fury.readJavaStringRef(buffer));
- return false;
default:
return true;
}
@@ -438,14 +505,16 @@ public abstract class AbstractObjectSerializer<T> extends
Serializer<T> {
case ClassResolver.PRIMITIVE_DOUBLE_CLASS_ID:
Platform.putDouble(targetObject, fieldOffset, buffer.readFloat64());
return false;
- case ClassResolver.STRING_CLASS_ID:
- Platform.putObject(targetObject, fieldOffset,
fury.readJavaStringRef(buffer));
- return false;
default:
return true;
}
}
+ /**
+ * read field value from buffer. This method handle the situation which all
fields are not null.
+ *
+ * @return true if field value isn't read by this function.
+ */
static boolean readBasicObjectFieldValueFailed(
Fury fury,
MemoryBuffer buffer,
@@ -456,6 +525,73 @@ public abstract class AbstractObjectSerializer<T> extends
Serializer<T> {
return true; // let common path handle this.
}
// add time types serialization here.
+ switch (classId) {
+ case ClassResolver.STRING_CLASS_ID: // fastpath for string.
+ if (fury.getStringSerializer().needToWriteRef()) {
+ fieldAccessor.putObject(targetObject,
fury.readJavaStringRef(buffer));
+ } else {
+ fieldAccessor.putObject(targetObject, fury.readString(buffer));
+ }
+ return false;
+ case ClassResolver.BOOLEAN_CLASS_ID:
+ {
+ fieldAccessor.putObject(targetObject, buffer.readBoolean());
+ return false;
+ }
+ case ClassResolver.BYTE_CLASS_ID:
+ {
+ fieldAccessor.putObject(targetObject, buffer.readByte());
+ return false;
+ }
+ case ClassResolver.CHAR_CLASS_ID:
+ {
+ fieldAccessor.putObject(targetObject, buffer.readChar());
+ return false;
+ }
+ case ClassResolver.SHORT_CLASS_ID:
+ {
+ fieldAccessor.putObject(targetObject, buffer.readInt16());
+ return false;
+ }
+ case ClassResolver.INTEGER_CLASS_ID:
+ {
+ if (fury.compressInt()) {
+ fieldAccessor.putObject(targetObject, buffer.readVarInt32());
+ } else {
+ fieldAccessor.putObject(targetObject, buffer.readInt32());
+ }
+ return false;
+ }
+ case ClassResolver.FLOAT_CLASS_ID:
+ {
+ fieldAccessor.putObject(targetObject, buffer.readFloat32());
+ return false;
+ }
+ case ClassResolver.LONG_CLASS_ID:
+ {
+ fieldAccessor.putObject(targetObject, fury.readInt64(buffer));
+ return false;
+ }
+ case ClassResolver.DOUBLE_CLASS_ID:
+ {
+ fieldAccessor.putObject(targetObject, buffer.readFloat64());
+ return false;
+ }
+ default:
+ return true;
+ }
+ }
+
+ static boolean readBasicNullableObjectFieldValueFailed(
+ Fury fury,
+ MemoryBuffer buffer,
+ Object targetObject,
+ FieldAccessor fieldAccessor,
+ short classId) {
+ if (!fury.isBasicTypesRefIgnored()) {
+ return true; // let common path handle this.
+ }
+ // add time types serialization here.
switch (classId) {
case ClassResolver.STRING_CLASS_ID: // fastpath for string.
fieldAccessor.putObject(targetObject, fury.readJavaStringRef(buffer));
@@ -808,15 +944,15 @@ public abstract class AbstractObjectSerializer<T> extends
Serializer<T> {
new FinalTypeField[primitives.size() + boxed.size() + finals.size()];
int cnt = 0;
for (Descriptor d : primitives) {
- finalFields[cnt++] = buildFinalTypeField(fury, d);
+ finalFields[cnt++] = new FinalTypeField(fury, d);
}
for (Descriptor d : boxed) {
- finalFields[cnt++] = buildFinalTypeField(fury, d);
+ finalFields[cnt++] = new FinalTypeField(fury, d);
}
// TODO(chaokunyang) Support Pojo<T> generics besides Map/Collection
subclass
// when it's supported in BaseObjectCodecBuilder.
for (Descriptor d : finals) {
- finalFields[cnt++] = buildFinalTypeField(fury, d);
+ finalFields[cnt++] = new FinalTypeField(fury, d);
}
boolean[] isFinal = new boolean[finalFields.length];
for (int i = 0; i < isFinal.length; i++) {
@@ -826,14 +962,7 @@ public abstract class AbstractObjectSerializer<T> extends
Serializer<T> {
cnt = 0;
GenericTypeField[] otherFields = new
GenericTypeField[grouper.getOtherDescriptors().size()];
for (Descriptor descriptor : grouper.getOtherDescriptors()) {
- GenericTypeField genericTypeField =
- new GenericTypeField(
- descriptor.getTypeRef(),
- descriptor.getDeclaringClass() + "." + descriptor.getName(),
- descriptor.getField() != null
- ? FieldAccessor.createAccessor(descriptor.getField())
- : null,
- fury);
+ GenericTypeField genericTypeField = new GenericTypeField(fury,
descriptor);
otherFields[cnt++] = genericTypeField;
}
cnt = 0;
@@ -841,54 +970,44 @@ public abstract class AbstractObjectSerializer<T> extends
Serializer<T> {
Collection<Descriptor> maps = grouper.getMapDescriptors();
GenericTypeField[] containerFields = new
GenericTypeField[collections.size() + maps.size()];
for (Descriptor d : collections) {
- containerFields[cnt++] = buildContainerField(fury, d);
+ containerFields[cnt++] = new GenericTypeField(fury, d);
}
for (Descriptor d : maps) {
- containerFields[cnt++] = buildContainerField(fury, d);
+ containerFields[cnt++] = new GenericTypeField(fury, d);
}
return Tuple3.of(Tuple2.of(finalFields, isFinal), otherFields,
containerFields);
}
- private static FinalTypeField buildFinalTypeField(Fury fury, Descriptor d) {
- return new FinalTypeField(
- d.getTypeRef(),
- d.getDeclaringClass() + "." + d.getName(),
- // `d.getField()` will be null when peer class doesn't have this field.
- d.getField() != null ? FieldAccessor.createAccessor(d.getField()) :
null,
- fury);
- }
-
- private static GenericTypeField buildContainerField(Fury fury, Descriptor d)
{
- return new GenericTypeField(
- d.getTypeRef(),
- d.getDeclaringClass() + "." + d.getName(),
- d.getField() != null ? FieldAccessor.createAccessor(d.getField()) :
null,
- fury);
- }
-
public static class InternalFieldInfo {
- private final TypeRef<?> typeRef;
+ protected final TypeRef<?> typeRef;
protected final short classId;
protected final String qualifiedFieldName;
protected final FieldAccessor fieldAccessor;
+ protected boolean nullable;
- private InternalFieldInfo(
- TypeRef<?> typeRef, short classId, String qualifiedFieldName,
FieldAccessor fieldAccessor) {
- this.typeRef = typeRef;
+ private InternalFieldInfo(Descriptor d, short classId) {
+ this.typeRef = d.getTypeRef();
this.classId = classId;
- this.qualifiedFieldName = qualifiedFieldName;
- this.fieldAccessor = fieldAccessor;
+ this.qualifiedFieldName = d.getDeclaringClass() + "." + d.getName();
+ this.fieldAccessor = d.getField() != null ?
FieldAccessor.createAccessor(d.getField()) : null;
+ if (!typeRef.isPrimitive()) {
+ nullable = d.getFuryField() == null || d.getFuryField().nullable();
+ }
}
@Override
public String toString() {
return "InternalFieldInfo{"
- + "classId="
+ + "typeRef="
+ + typeRef
+ + ", classId="
+ classId
- + ", fieldName="
+ + ", qualifiedFieldName='"
+ qualifiedFieldName
- + ", field="
- + (fieldAccessor != null ? fieldAccessor.getField() : null)
+ + ", fieldAccessor="
+ + fieldAccessor
+ + ", nullable="
+ + nullable
+ '}';
}
}
@@ -897,17 +1016,17 @@ public abstract class AbstractObjectSerializer<T>
extends Serializer<T> {
final ClassInfo classInfo;
final boolean trackingRef;
- private FinalTypeField(TypeRef<?> type, String fieldName, FieldAccessor
accessor, Fury fury) {
- super(type, getRegisteredClassId(fury, type.getRawType()), fieldName,
accessor);
+ private FinalTypeField(Fury fury, Descriptor d) {
+ super(d, getRegisteredClassId(fury, d.getTypeRef().getRawType()));
// invoke `copy` to avoid ObjectSerializer construct clear serializer by
`clearSerializer`.
- if (type.getRawType() == FinalObjectTypeStub.class) {
+ if (typeRef.getRawType() == FinalObjectTypeStub.class) {
// `FinalObjectTypeStub` has no fields, using its `classInfo`
// will make deserialization failed.
classInfo = null;
} else {
- classInfo = SerializationUtils.getClassInfo(fury, type.getRawType());
+ classInfo = SerializationUtils.getClassInfo(fury,
typeRef.getRawType());
}
- trackingRef = fury.getClassResolver().needToWriteRef(type);
+ trackingRef = fury.getClassResolver().needToWriteRef(typeRef);
}
}
@@ -918,9 +1037,8 @@ public abstract class AbstractObjectSerializer<T> extends
Serializer<T> {
final boolean isArray;
final ClassInfo containerClassInfo;
- private GenericTypeField(
- TypeRef<?> typeRef, String qualifiedFieldName, FieldAccessor accessor,
Fury fury) {
- super(typeRef, getRegisteredClassId(fury, getRawType(typeRef)),
qualifiedFieldName, accessor);
+ private GenericTypeField(Fury fury, Descriptor d) {
+ super(d, getRegisteredClassId(fury, getRawType(d.getTypeRef())));
// TODO support generics <T> in Pojo<T>, see
ComplexObjectSerializer.getGenericTypes
ClassResolver classResolver = fury.getClassResolver();
GenericType t = classResolver.buildGenericType(typeRef);
@@ -954,12 +1072,20 @@ public abstract class AbstractObjectSerializer<T>
extends Serializer<T> {
return "GenericTypeField{"
+ "genericType="
+ genericType
+ + ", classInfoHolder="
+ + classInfoHolder
+ + ", trackingRef="
+ + trackingRef
+ + ", typeRef="
+ + typeRef
+ ", classId="
+ classId
- + ", qualifiedFieldName="
+ + ", qualifiedFieldName='"
+ qualifiedFieldName
- + ", field="
- + (fieldAccessor != null ? fieldAccessor.getField() : null)
+ + ", fieldAccessor="
+ + fieldAccessor
+ + ", nullable="
+ + nullable
+ '}';
}
}
diff --git
a/java/fury-core/src/main/java/org/apache/fury/serializer/CompatibleSerializer.java
b/java/fury-core/src/main/java/org/apache/fury/serializer/CompatibleSerializer.java
index 0ba59788..2a537e6e 100644
---
a/java/fury-core/src/main/java/org/apache/fury/serializer/CompatibleSerializer.java
+++
b/java/fury-core/src/main/java/org/apache/fury/serializer/CompatibleSerializer.java
@@ -129,13 +129,19 @@ public final class CompatibleSerializer<T> extends
CompatibleSerializerBase<T> {
private void readAndWriteFieldValue(
MemoryBuffer buffer, FieldResolver.FieldInfo fieldInfo, Object
targetObject) {
FieldAccessor fieldAccessor = fieldInfo.getFieldAccessor();
+ boolean nullable = fieldInfo.isNullable();
short classId = fieldInfo.getEmbeddedClassId();
if (AbstractObjectSerializer.writePrimitiveFieldValueFailed(
fury, buffer, targetObject, fieldAccessor, classId)) {
Object fieldValue;
fieldValue = fieldAccessor.getObject(targetObject);
- if (AbstractObjectSerializer.writeBasicObjectFieldValueFailed(
- fury, buffer, fieldValue, classId)) {
+ boolean writeBasicObjectResult =
+ nullable
+ ?
AbstractObjectSerializer.writeBasicNullableObjectFieldValueFailed(
+ fury, buffer, fieldValue, classId)
+ : AbstractObjectSerializer.writeBasicObjectFieldValueFailed(
+ fury, buffer, fieldValue, classId);
+ if (writeBasicObjectResult) {
if (classId == ClassResolver.NO_CLASS_ID) { // SEPARATE_TYPES_HASH
writeSeparateFieldValue(fieldInfo, buffer, fieldValue);
} else {
@@ -559,10 +565,14 @@ public final class CompatibleSerializer<T> extends
CompatibleSerializerBase<T> {
FieldResolver.FieldInfo fieldInfo, MemoryBuffer buffer, Object
targetObject) {
FieldAccessor fieldAccessor = fieldInfo.getFieldAccessor();
short classId = fieldInfo.getEmbeddedClassId();
+ boolean nullable = fieldInfo.isNullable();
if (AbstractObjectSerializer.readPrimitiveFieldValueFailed(
fury, buffer, targetObject, fieldAccessor, classId)
- && AbstractObjectSerializer.readBasicObjectFieldValueFailed(
- fury, buffer, targetObject, fieldAccessor, classId)) {
+ && (nullable
+ ? AbstractObjectSerializer.readBasicNullableObjectFieldValueFailed(
+ fury, buffer, targetObject, fieldAccessor, classId)
+ : AbstractObjectSerializer.readBasicObjectFieldValueFailed(
+ fury, buffer, targetObject, fieldAccessor, classId))) {
if (classId == ClassResolver.NO_CLASS_ID) {
// SEPARATE_TYPES_HASH
Object fieldValue = fieldResolver.readObjectField(buffer, fieldInfo);
diff --git
a/java/fury-core/src/main/java/org/apache/fury/serializer/MetaSharedSerializer.java
b/java/fury-core/src/main/java/org/apache/fury/serializer/MetaSharedSerializer.java
index 41ae4677..7f62d7ec 100644
---
a/java/fury-core/src/main/java/org/apache/fury/serializer/MetaSharedSerializer.java
+++
b/java/fury-core/src/main/java/org/apache/fury/serializer/MetaSharedSerializer.java
@@ -145,12 +145,16 @@ public class MetaSharedSerializer<T> extends
AbstractObjectSerializer<T> {
ObjectSerializer.FinalTypeField fieldInfo = finalFields[i];
boolean isFinal = this.isFinal[i];
FieldAccessor fieldAccessor = fieldInfo.fieldAccessor;
+ boolean nullable = fieldInfo.nullable;
if (fieldAccessor != null) {
short classId = fieldInfo.classId;
if (AbstractObjectSerializer.readPrimitiveFieldValueFailed(
fury, buffer, obj, fieldAccessor, classId)
- && AbstractObjectSerializer.readBasicObjectFieldValueFailed(
- fury, buffer, obj, fieldAccessor, classId)) {
+ && (nullable
+ ?
AbstractObjectSerializer.readBasicNullableObjectFieldValueFailed(
+ fury, buffer, obj, fieldAccessor, classId)
+ : AbstractObjectSerializer.readBasicObjectFieldValueFailed(
+ fury, buffer, obj, fieldAccessor, classId))) {
assert fieldInfo.classInfo != null;
Object fieldValue =
AbstractObjectSerializer.readFinalObjectFieldValue(
diff --git
a/java/fury-core/src/main/java/org/apache/fury/serializer/NonexistentClassSerializers.java
b/java/fury-core/src/main/java/org/apache/fury/serializer/NonexistentClassSerializers.java
index 4d7ccf48..f5e6584a 100644
---
a/java/fury-core/src/main/java/org/apache/fury/serializer/NonexistentClassSerializers.java
+++
b/java/fury-core/src/main/java/org/apache/fury/serializer/NonexistentClassSerializers.java
@@ -138,10 +138,11 @@ public final class NonexistentClassSerializers {
}
for (ObjectSerializer.GenericTypeField fieldInfo :
fieldsInfo.otherFields) {
Object fieldValue = value.get(fieldInfo.qualifiedFieldName);
+ boolean nullable = fieldInfo.nullable;
if (fieldInfo.trackingRef) {
fury.writeRef(buffer, fieldValue, fieldInfo.classInfoHolder);
} else {
- fury.writeNullable(buffer, fieldValue, fieldInfo.classInfoHolder);
+ binding.writeNullable(buffer, fieldValue, fieldInfo.classInfoHolder,
nullable);
}
}
Generics generics = fury.getGenerics();
diff --git
a/java/fury-core/src/main/java/org/apache/fury/serializer/ObjectSerializer.java
b/java/fury-core/src/main/java/org/apache/fury/serializer/ObjectSerializer.java
index b1a996b4..82e4cae7 100644
---
a/java/fury-core/src/main/java/org/apache/fury/serializer/ObjectSerializer.java
+++
b/java/fury-core/src/main/java/org/apache/fury/serializer/ObjectSerializer.java
@@ -131,16 +131,20 @@ public final class ObjectSerializer<T> extends
AbstractObjectSerializer<T> {
}
// write order: primitive,boxed,final,other,collection,map
writeFinalFields(buffer, value, fury, refResolver, typeResolver);
+ writeOtherFields(buffer, value);
+ writeContainerFields(buffer, value, fury, refResolver, typeResolver);
+ }
+
+ private void writeOtherFields(MemoryBuffer buffer, T value) {
for (GenericTypeField fieldInfo : otherFields) {
FieldAccessor fieldAccessor = fieldInfo.fieldAccessor;
Object fieldValue = fieldAccessor.getObject(value);
if (fieldInfo.trackingRef) {
binding.writeRef(buffer, fieldValue, fieldInfo.classInfoHolder);
} else {
- binding.writeNullable(buffer, fieldValue, fieldInfo.classInfoHolder);
+ binding.writeNullable(buffer, fieldValue, fieldInfo.classInfoHolder,
fieldInfo.nullable);
}
}
- writeContainerFields(buffer, value, fury, refResolver, typeResolver);
}
@Override
@@ -155,14 +159,19 @@ public final class ObjectSerializer<T> extends
AbstractObjectSerializer<T> {
for (int i = 0; i < finalFields.length; i++) {
FinalTypeField fieldInfo = finalFields[i];
FieldAccessor fieldAccessor = fieldInfo.fieldAccessor;
+ boolean nullable = fieldInfo.nullable;
short classId = fieldInfo.classId;
if (writePrimitiveFieldValueFailed(fury, buffer, value, fieldAccessor,
classId)) {
Object fieldValue = fieldAccessor.getObject(value);
- if (writeBasicObjectFieldValueFailed(fury, buffer, fieldValue,
classId)) {
+ boolean writeBasicObjectResult =
+ nullable
+ ? writeBasicNullableObjectFieldValueFailed(fury, buffer,
fieldValue, classId)
+ : writeBasicObjectFieldValueFailed(fury, buffer, fieldValue,
classId);
+ if (writeBasicObjectResult) {
Serializer<Object> serializer = fieldInfo.classInfo.getSerializer();
if (!metaShareEnabled || isFinal[i]) {
if (!fieldInfo.trackingRef) {
- binding.writeNullable(buffer, fieldValue, serializer);
+ binding.writeNullable(buffer, fieldValue, serializer, nullable);
} else {
// whether tracking ref is recorded in `fieldInfo.serializer`,
so it's still
// consistent with jit serializer.
@@ -176,7 +185,7 @@ public final class ObjectSerializer<T> extends
AbstractObjectSerializer<T> {
binding.write(buffer, serializer, fieldValue);
}
} else {
- binding.writeNullable(buffer, fieldValue, fieldInfo.classInfo);
+ binding.writeNullable(buffer, fieldValue, serializer, nullable);
}
}
}
@@ -212,17 +221,20 @@ public final class ObjectSerializer<T> extends
AbstractObjectSerializer<T> {
generics.popGenericType();
}
} else {
- if (fieldValue == null) {
- buffer.writeByte(Fury.NULL_FLAG);
- } else {
- buffer.writeByte(Fury.NOT_NULL_VALUE_FLAG);
- generics.pushGenericType(fieldInfo.genericType);
- binding.writeContainerFieldValue(
- buffer,
- fieldValue,
- typeResolver.getClassInfo(fieldValue.getClass(),
fieldInfo.classInfoHolder));
- generics.popGenericType();
+ if (fieldInfo.nullable) {
+ if (fieldValue == null) {
+ buffer.writeByte(Fury.NULL_FLAG);
+ return;
+ } else {
+ buffer.writeByte(Fury.NOT_NULL_VALUE_FLAG);
+ }
}
+ generics.pushGenericType(fieldInfo.genericType);
+ binding.writeContainerFieldValue(
+ buffer,
+ fieldValue,
+ typeResolver.getClassInfo(fieldValue.getClass(),
fieldInfo.classInfoHolder));
+ generics.popGenericType();
}
}
@@ -304,9 +316,12 @@ public final class ObjectSerializer<T> extends
AbstractObjectSerializer<T> {
FinalTypeField fieldInfo = finalFields[i];
boolean isFinal = !metaShareEnabled || this.isFinal[i];
FieldAccessor fieldAccessor = fieldInfo.fieldAccessor;
+ boolean nullable = fieldInfo.nullable;
short classId = fieldInfo.classId;
if (readPrimitiveFieldValueFailed(fury, buffer, obj, fieldAccessor,
classId)
- && readBasicObjectFieldValueFailed(fury, buffer, obj, fieldAccessor,
classId)) {
+ && (nullable
+ ? readBasicNullableObjectFieldValueFailed(fury, buffer, obj,
fieldAccessor, classId)
+ : readBasicObjectFieldValueFailed(fury, buffer, obj,
fieldAccessor, classId))) {
Object fieldValue =
readFinalObjectFieldValue(
binding, refResolver, typeResolver, fieldInfo, isFinal,
buffer);
diff --git
a/java/fury-core/src/main/java/org/apache/fury/serializer/SerializationBinding.java
b/java/fury-core/src/main/java/org/apache/fury/serializer/SerializationBinding.java
index 3db2ef31..c12845d3 100644
---
a/java/fury-core/src/main/java/org/apache/fury/serializer/SerializationBinding.java
+++
b/java/fury-core/src/main/java/org/apache/fury/serializer/SerializationBinding.java
@@ -45,6 +45,8 @@ interface SerializationBinding {
void writeNonRef(MemoryBuffer buffer, Object obj, ClassInfo classInfo);
+ void writeNonRef(MemoryBuffer buffer, Object obj, ClassInfoHolder
classInfoHolder);
+
void writeNullable(MemoryBuffer buffer, Object obj);
void writeNullable(MemoryBuffer buffer, Object obj, Serializer serializer);
@@ -53,6 +55,11 @@ interface SerializationBinding {
void writeNullable(MemoryBuffer buffer, Object obj, ClassInfo classInfo);
+ void writeNullable(
+ MemoryBuffer buffer, Object obj, ClassInfoHolder classInfoHolder,
boolean nullable);
+
+ void writeNullable(MemoryBuffer buffer, Object obj, Serializer serializer,
boolean nullable);
+
void writeContainerFieldValue(MemoryBuffer buffer, Object fieldValue,
ClassInfo classInfo);
void write(MemoryBuffer buffer, Serializer serializer, Object value);
@@ -75,6 +82,8 @@ interface SerializationBinding {
Object readNullable(MemoryBuffer buffer, Serializer<Object> serializer);
+ Object readNullable(MemoryBuffer buffer, Serializer<Object> serializer,
boolean nullable);
+
Object readContainerFieldValue(MemoryBuffer buffer, GenericTypeField field);
Object readContainerFieldValueRef(MemoryBuffer buffer, GenericTypeField
fieldInfo);
@@ -151,6 +160,16 @@ interface SerializationBinding {
return fury.readNullable(buffer, serializer);
}
+ @Override
+ public Object readNullable(
+ MemoryBuffer buffer, Serializer<Object> serializer, boolean nullable) {
+ if (nullable) {
+ return readNullable(buffer, serializer);
+ } else {
+ return read(buffer, serializer);
+ }
+ }
+
@Override
public Object readContainerFieldValue(MemoryBuffer buffer,
GenericTypeField field) {
return fury.readNonRef(buffer, field.classInfoHolder);
@@ -190,6 +209,11 @@ interface SerializationBinding {
fury.writeNonRef(buffer, obj, classInfo);
}
+ @Override
+ public void writeNonRef(MemoryBuffer buffer, Object obj, ClassInfoHolder
classInfoHolder) {
+ fury.writeNonRef(buffer, obj, classResolver.getClassInfo(obj.getClass(),
classInfoHolder));
+ }
+
@Override
public void writeNullable(MemoryBuffer buffer, Object obj) {
if (obj == null) {
@@ -230,6 +254,26 @@ interface SerializationBinding {
}
}
+ @Override
+ public void writeNullable(
+ MemoryBuffer buffer, Object obj, ClassInfoHolder classInfoHolder,
boolean nullable) {
+ if (nullable) {
+ writeNullable(buffer, obj, classInfoHolder);
+ } else {
+ writeNonRef(buffer, obj, classInfoHolder);
+ }
+ }
+
+ @Override
+ public void writeNullable(
+ MemoryBuffer buffer, Object obj, Serializer serializer, boolean
nullable) {
+ if (nullable) {
+ writeNullable(buffer, obj, serializer);
+ } else {
+ write(buffer, serializer, obj);
+ }
+ }
+
@Override
public void writeContainerFieldValue(
MemoryBuffer buffer, Object fieldValue, ClassInfo classInfo) {
@@ -318,6 +362,16 @@ interface SerializationBinding {
return fury.xreadNullable(buffer, serializer);
}
+ @Override
+ public Object readNullable(
+ MemoryBuffer buffer, Serializer<Object> serializer, boolean nullable) {
+ if (nullable) {
+ return readNullable(buffer, serializer);
+ } else {
+ return read(buffer, serializer);
+ }
+ }
+
@Override
public Object readContainerFieldValue(MemoryBuffer buffer,
GenericTypeField field) {
return fury.xreadNonRef(buffer, field.containerClassInfo);
@@ -355,6 +409,11 @@ interface SerializationBinding {
fury.xwriteNonRef(buffer, obj, classInfo);
}
+ @Override
+ public void writeNonRef(MemoryBuffer buffer, Object obj, ClassInfoHolder
classInfoHolder) {
+ fury.xwriteNonRef(buffer, obj,
xtypeResolver.getClassInfo(obj.getClass(), classInfoHolder));
+ }
+
@Override
public void writeNullable(MemoryBuffer buffer, Object obj) {
if (obj == null) {
@@ -395,6 +454,26 @@ interface SerializationBinding {
}
}
+ @Override
+ public void writeNullable(
+ MemoryBuffer buffer, Object obj, ClassInfoHolder classInfoHolder,
boolean nullable) {
+ if (nullable) {
+ writeNullable(buffer, obj, classInfoHolder);
+ } else {
+ writeNonRef(buffer, obj, classInfoHolder);
+ }
+ }
+
+ @Override
+ public void writeNullable(
+ MemoryBuffer buffer, Object obj, Serializer serializer, boolean
nullable) {
+ if (nullable) {
+ writeNullable(buffer, obj, serializer);
+ } else {
+ write(buffer, serializer, obj);
+ }
+ }
+
@Override
public void writeContainerFieldValue(
MemoryBuffer buffer, Object fieldValue, ClassInfo classInfo) {
diff --git a/java/fury-core/src/main/java/org/apache/fury/type/Descriptor.java
b/java/fury-core/src/main/java/org/apache/fury/type/Descriptor.java
index 5ff776f2..00974c4d 100644
--- a/java/fury-core/src/main/java/org/apache/fury/type/Descriptor.java
+++ b/java/fury-core/src/main/java/org/apache/fury/type/Descriptor.java
@@ -43,6 +43,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.fury.annotation.Expose;
+import org.apache.fury.annotation.FuryField;
import org.apache.fury.annotation.Ignore;
import org.apache.fury.annotation.Internal;
import org.apache.fury.collection.Tuple2;
@@ -84,6 +85,7 @@ public class Descriptor {
private final Field field;
private final Method readMethod;
private final Method writeMethod;
+ private final FuryField furyField;
public Descriptor(Field field, TypeRef<?> typeRef, Method readMethod, Method
writeMethod) {
this.field = field;
@@ -94,6 +96,7 @@ public class Descriptor {
this.readMethod = readMethod;
this.writeMethod = writeMethod;
this.typeRef = typeRef;
+ this.furyField = this.field.getAnnotation(FuryField.class);
}
public Descriptor(TypeRef<?> typeRef, String name, int modifier, String
declaringClass) {
@@ -105,6 +108,7 @@ public class Descriptor {
this.typeRef = typeRef;
this.readMethod = null;
this.writeMethod = null;
+ this.furyField = null;
}
private Descriptor(Field field, Method readMethod) {
@@ -116,6 +120,7 @@ public class Descriptor {
this.readMethod = readMethod;
this.writeMethod = null;
this.typeRef = null;
+ this.furyField = this.field.getAnnotation(FuryField.class);
}
private Descriptor(
@@ -135,6 +140,7 @@ public class Descriptor {
this.field = field;
this.readMethod = readMethod;
this.writeMethod = writeMethod;
+ this.furyField = this.field == null ? null :
this.field.getAnnotation(FuryField.class);
}
public Descriptor copy(Method readMethod, Method writeMethod) {
@@ -186,6 +192,10 @@ public class Descriptor {
return typeName;
}
+ public FuryField getFuryField() {
+ return furyField;
+ }
+
/** Try not use {@link TypeRef#getRawType()} since it's expensive. */
public Class<?> getRawType() {
Class<?> type = this.type;
@@ -222,6 +232,10 @@ public class Descriptor {
if (writeMethod != null) {
sb.append(", writeMethod=").append(writeMethod);
}
+ if (typeRef != null) {
+ sb.append(", typeRef=").append(typeRef);
+ }
+ sb.append(", furyField=").append(furyField);
sb.append('}');
return sb.toString();
}
diff --git
a/java/fury-core/src/main/resources/META-INF/native-image/org.apache.fury/fury-core/native-image.properties
b/java/fury-core/src/main/resources/META-INF/native-image/org.apache.fury/fury-core/native-image.properties
index e0f1d9c4..01170fe8 100644
---
a/java/fury-core/src/main/resources/META-INF/native-image/org.apache.fury/fury-core/native-image.properties
+++
b/java/fury-core/src/main/resources/META-INF/native-image/org.apache.fury/fury-core/native-image.properties
@@ -423,6 +423,7 @@
Args=--initialize-at-build-time=org.apache.fury.memory.MemoryBuffer,\
org.apache.fury.serializer.collection.UnmodifiableSerializers$UnmodifiableCollectionSerializer,\
org.apache.fury.serializer.collection.UnmodifiableSerializers$UnmodifiableMapSerializer,\
org.apache.fury.serializer.collection.UnmodifiableSerializers,\
+ org.apache.fury.serializer.AbstractObjectSerializer$FuryFieldInfo,\
org.apache.fury.serializer.LazySerializer,\
org.apache.fury.serializer.LazySerializer$LazyObjectSerializer,\
org.apache.fury.serializer.shim.ShimDispatcher,\
diff --git
a/java/fury-core/src/test/java/org/apache/fury/annotation/FuryAnnotationTest.java
b/java/fury-core/src/test/java/org/apache/fury/annotation/FuryAnnotationTest.java
new file mode 100644
index 00000000..2d3bdd71
--- /dev/null
+++
b/java/fury-core/src/test/java/org/apache/fury/annotation/FuryAnnotationTest.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.fury.annotation;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
+
+import com.google.common.collect.Lists;
+import java.util.List;
+import lombok.Data;
+import org.apache.fury.Fury;
+import org.apache.fury.FuryTestBase;
+import org.apache.fury.config.CompatibleMode;
+import org.apache.fury.config.Language;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class FuryAnnotationTest extends FuryTestBase {
+
+ @Data
+ public static class BeanM {
+ @FuryField(nullable = false)
+ public Long f1;
+
+ @FuryField(nullable = false)
+ private Long f2;
+
+ String s = "str";
+ Short shortValue = Short.valueOf((short) 2);
+ Byte byteValue = Byte.valueOf((byte) 3);
+ Long longValue = Long.valueOf(4L);
+ Boolean booleanValue = Boolean.TRUE;
+ Float floatValue = Float.valueOf(5.0f);
+ Double doubleValue = Double.valueOf(6.0);
+ Character character = Character.valueOf('c');
+
+ int i = 10;
+
+ int i2;
+
+ long l1;
+
+ double d1;
+
+ char c1;
+
+ boolean b1;
+
+ byte byte1;
+
+ @FuryField int i3 = 10;
+
+ @FuryField List<Integer> integerList = Lists.newArrayList(1);
+
+ @FuryField String s1 = "str";
+
+ @FuryField(nullable = false)
+ Short shortValue1 = Short.valueOf((short) 2);
+
+ @FuryField(nullable = false)
+ Byte byteValue1 = Byte.valueOf((byte) 3);
+
+ @FuryField(nullable = false)
+ Long longValue1 = Long.valueOf(4L);
+
+ @FuryField(nullable = false)
+ Boolean booleanValue1 = Boolean.TRUE;
+
+ @FuryField(nullable = false)
+ Float floatValue1 = Float.valueOf(5.0f);
+
+ @FuryField(nullable = false)
+ Double doubleValue1 = Double.valueOf(6.0);
+
+ @FuryField(nullable = false)
+ Character character1 = Character.valueOf('c');
+
+ @FuryField(nullable = true)
+ List<Integer> integerList1 = Lists.newArrayList(1);
+
+ @FuryField(nullable = true)
+ String s2 = "str";
+
+ @FuryField(nullable = true)
+ Short shortValue2 = Short.valueOf((short) 2);
+
+ @FuryField(nullable = true)
+ Byte byteValue2 = Byte.valueOf((byte) 3);
+
+ @FuryField(nullable = true)
+ Long longValue2 = Long.valueOf(4L);
+
+ @FuryField(nullable = true)
+ Boolean booleanValue2 = Boolean.TRUE;
+
+ @FuryField(nullable = true)
+ Float floatValue2 = Float.valueOf(5.0f);
+
+ @FuryField(nullable = true)
+ Double doubleValue2 = Double.valueOf(6.0);
+
+ @FuryField(nullable = true)
+ Character character2 = Character.valueOf('c');
+
+ public BeanM() {
+ this.f1 = 1L;
+ this.f2 = 1L;
+ }
+ }
+
+ @Data
+ public static class BeanN {
+ public long f1;
+ private long f2;
+ }
+
+ @Data
+ public static class BeanM1 {
+
+ @FuryField private BeanN beanN;
+ }
+
+ @Test(dataProvider = "basicMultiConfigFury")
+ public void testFuryFieldAnnotation(
+ boolean trackingRef,
+ boolean codeGen,
+ boolean scopedMetaShare,
+ CompatibleMode compatibleMode) {
+ Fury fury =
+ Fury.builder()
+ .withLanguage(Language.JAVA)
+ .withRefTracking(trackingRef)
+ .requireClassRegistration(false)
+ .withCodegen(codeGen)
+ .withCompatibleMode(compatibleMode)
+ .withScopedMetaShare(scopedMetaShare)
+ .build();
+ BeanM o = new BeanM();
+ byte[] bytes = fury.serialize(o);
+ final Object deserialize = fury.deserialize(bytes);
+ Assert.assertEquals(o, deserialize);
+ }
+
+ @Test(dataProvider = "referenceTrackingConfig")
+ public void testFuryFieldAnnotationException(boolean referenceTracking) {
+ Fury fury =
+ Fury.builder()
+ .withLanguage(Language.JAVA)
+ .withRefTracking(referenceTracking)
+ .requireClassRegistration(false)
+ .withCodegen(false)
+ .build();
+ BeanM1 o1 = new BeanM1();
+ if (referenceTracking) {
+ assertEquals(serDe(fury, o1), o1);
+ } else {
+ assertThrows(NullPointerException.class, () -> fury.serialize(o1));
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]