|
1 | 1 | package com.fasterxml.jackson.dataformat.avro.ser;
|
2 | 2 |
|
3 | 3 | import java.io.IOException;
|
| 4 | +import java.math.BigDecimal; |
4 | 5 |
|
5 | 6 | import org.apache.avro.Schema;
|
6 | 7 | import org.apache.avro.Schema.Type;
|
7 |
| -import org.apache.avro.generic.*; |
| 8 | +import org.apache.avro.UnresolvedUnionException; |
| 9 | +import org.apache.avro.generic.GenericArray; |
| 10 | +import org.apache.avro.generic.GenericData; |
| 11 | +import org.apache.avro.generic.GenericRecord; |
8 | 12 | import org.apache.avro.io.BinaryEncoder;
|
| 13 | +import org.apache.avro.reflect.ReflectData; |
9 | 14 |
|
10 | 15 | import com.fasterxml.jackson.core.JsonStreamContext;
|
11 | 16 | import com.fasterxml.jackson.databind.JsonMappingException;
|
12 | 17 | import com.fasterxml.jackson.dataformat.avro.AvroGenerator;
|
| 18 | +import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper; |
13 | 19 |
|
14 | 20 | public abstract class AvroWriteContext
|
15 | 21 | extends JsonStreamContext
|
@@ -58,8 +64,9 @@ public void complete() throws IOException {
|
58 | 64 | throw new IllegalStateException("Can not be called on "+getClass().getName());
|
59 | 65 | }
|
60 | 66 |
|
61 |
| - /* |
62 |
| - /********************************************************** |
| 67 | + |
| 68 | + public AvroWriteContext createChildObjectContext(Object object) throws JsonMappingException { return createChildObjectContext(); } |
| 69 | + |
63 | 70 | /* Accessors
|
64 | 71 | /**********************************************************
|
65 | 72 | */
|
@@ -140,33 +147,36 @@ protected GenericRecord _createRecord(Schema schema) throws JsonMappingException
|
140 | 147 | throw new JsonMappingException(null, "Failed to create Record type from "+type, e);
|
141 | 148 | }
|
142 | 149 | }
|
143 |
| - |
| 150 | + |
144 | 151 | protected GenericArray<Object> _createArray(Schema schema)
|
145 | 152 | {
|
146 | 153 | if (schema.getType() == Schema.Type.UNION) {
|
147 |
| - Schema match = null; |
148 |
| - for (Schema s : schema.getTypes()) { |
149 |
| - if (s.getType() == Schema.Type.ARRAY) { |
150 |
| - if (match != null) { |
151 |
| - throw new IllegalStateException("Multiple Array types, can not figure out which to use for: " |
152 |
| - +schema); |
153 |
| - } |
154 |
| - match = s; |
155 |
| - } |
156 |
| - } |
157 |
| - if (match == null) { |
| 154 | + int arraySchemaIndex = schema.getIndexNamed(Type.ARRAY.getName()); |
| 155 | + if (arraySchemaIndex < 0) { |
158 | 156 | throw new IllegalStateException("No Array type found in union type: "+schema);
|
159 | 157 | }
|
160 |
| - schema = match; |
| 158 | + schema = schema.getTypes().get(arraySchemaIndex); |
161 | 159 | }
|
162 | 160 | return new GenericData.Array<Object>(8, schema);
|
163 | 161 | }
|
164 | 162 |
|
165 |
| - protected AvroWriteContext _createObjectContext(Schema schema) throws JsonMappingException |
| 163 | + protected AvroWriteContext _createObjectContext(Schema schema) throws JsonMappingException { |
| 164 | + if (schema.getType() == Type.UNION) { |
| 165 | + schema = _recordOrMapFromUnion(schema); |
| 166 | + } |
| 167 | + return _createObjectContext(schema, null); // Object doesn't matter as long as schema isn't a union |
| 168 | + } |
| 169 | + |
| 170 | + protected AvroWriteContext _createObjectContext(Schema schema, Object object) throws JsonMappingException |
166 | 171 | {
|
167 | 172 | Type type = schema.getType();
|
168 | 173 | if (type == Schema.Type.UNION) {
|
169 |
| - schema = _recordOrMapFromUnion(schema); |
| 174 | + try { |
| 175 | + schema = resolveUnionSchema(schema, object); |
| 176 | + } catch (UnresolvedUnionException e) { |
| 177 | + // couldn't find an exact match |
| 178 | + schema = _recordOrMapFromUnion(schema); |
| 179 | + } |
170 | 180 | type = schema.getType();
|
171 | 181 | }
|
172 | 182 | if (type == Schema.Type.MAP) {
|
@@ -194,6 +204,69 @@ protected Schema _recordOrMapFromUnion(Schema unionSchema)
|
194 | 204 | return match;
|
195 | 205 | }
|
196 | 206 |
|
| 207 | + /** |
| 208 | + * Resolves the sub-schema from a union that should correspond to the {@code datum}. |
| 209 | + * |
| 210 | + * @param unionSchema Union of schemas from which to choose |
| 211 | + * @param datum Object that needs to map to one of the schemas in {@code unionSchema} |
| 212 | + * @return Index into {@link Schema#getTypes() unionSchema.getTypes()} that matches {@code datum} |
| 213 | + * @see #resolveUnionSchema(Schema, Object) |
| 214 | + * @throws org.apache.avro.UnresolvedUnionException if {@code unionSchema} does not have a schema that can encode {@code datum} |
| 215 | + */ |
| 216 | + public static int resolveUnionIndex(Schema unionSchema, Object datum) { |
| 217 | + if (datum != null) { |
| 218 | + int subOptimal = -1; |
| 219 | + for(int i = 0, size = unionSchema.getTypes().size(); i < size; i++) { |
| 220 | + Schema schema = unionSchema.getTypes().get(i); |
| 221 | + if (datum instanceof BigDecimal) { |
| 222 | + // BigDecimals can be shoved into a double, but optimally would be a String or byte[] with logical type information |
| 223 | + if (schema.getType() == Type.DOUBLE) { |
| 224 | + subOptimal = i; |
| 225 | + continue; |
| 226 | + } |
| 227 | + } |
| 228 | + if (datum instanceof String) { |
| 229 | + // Jackson serializes enums as strings, so try and find a matching schema |
| 230 | + if (schema.getType() == Type.ENUM && schema.hasEnumSymbol((String) datum)) { |
| 231 | + return i; |
| 232 | + } |
| 233 | + // Jackson serializes char/Character as a string, so try and find a matching schema |
| 234 | + if (schema.getType() == Type.INT |
| 235 | + && ((String) datum).length() == 1 |
| 236 | + && AvroSchemaHelper.getTypeId(Character.class).equals(schema.getProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_CLASS)) |
| 237 | + ) { |
| 238 | + return i; |
| 239 | + } |
| 240 | + // Jackson serializes char[]/Character[] as a string, so try and find a matching schema |
| 241 | + if (schema.getType() == Type.ARRAY |
| 242 | + && schema.getElementType().getType() == Type.INT |
| 243 | + && AvroSchemaHelper.getTypeId(Character.class).equals(schema.getElementType().getProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_CLASS)) |
| 244 | + ) { |
| 245 | + return i; |
| 246 | + } |
| 247 | + } |
| 248 | + } |
| 249 | + // Did we find a sub-optimal match? |
| 250 | + if (subOptimal != -1) { |
| 251 | + return subOptimal; |
| 252 | + } |
| 253 | + } |
| 254 | + return ReflectData.get().resolveUnion(unionSchema, datum); |
| 255 | + } |
| 256 | + |
| 257 | + /** |
| 258 | + * Resolves the sub-schema from a union that should correspond to the {@code datum}. |
| 259 | + * |
| 260 | + * @param unionSchema Union of schemas from which to choose |
| 261 | + * @param datum Object that needs to map to one of the schemas in {@code unionSchema} |
| 262 | + * @return Schema that matches {@code datum} |
| 263 | + * @see #resolveUnionIndex(Schema, Object) |
| 264 | + * @throws org.apache.avro.UnresolvedUnionException if {@code unionSchema} does not have a schema that can encode {@code datum} |
| 265 | + */ |
| 266 | + public static Schema resolveUnionSchema(Schema unionSchema, Object datum) { |
| 267 | + return unionSchema.getTypes().get(resolveUnionIndex(unionSchema, datum)); |
| 268 | + } |
| 269 | + |
197 | 270 | /*
|
198 | 271 | /**********************************************************
|
199 | 272 | /* Implementations
|
|
0 commit comments