/* * 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 * * https://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. */ using System; using System.Collections; using System.Collections.Concurrent; namespace Avro.Reflect { /// /// Class holds a cache of C# classes and their properties. The key for the cache is the schema full name. /// public class ClassCache { private static ConcurrentBag _defaultConverters = new ConcurrentBag(); private ConcurrentDictionary _nameClassMap = new ConcurrentDictionary(); private ConcurrentDictionary _nameArrayMap = new ConcurrentDictionary(); private ConcurrentDictionary _previousFields = new ConcurrentDictionary(); private void AddClassNameMapItem(RecordSchema schema, Type dotnetClass) { if (schema != null && GetClass(schema) != null) { return; } if (!dotnetClass.IsClass) { throw new AvroException($"Type {dotnetClass.Name} is not a class"); } _nameClassMap.TryAdd(schema.Fullname, new DotnetClass(dotnetClass, schema, this)); } /// /// Add a default field converter /// /// public static void AddDefaultConverter(IAvroFieldConverter converter) { _defaultConverters.Add(converter); } /// /// Add a converter defined using Func<>. The converter will be used whenever the source and target types /// match and a specific attribute is not defined. /// /// /// /// /// public static void AddDefaultConverter(Func from, Func to) { _defaultConverters.Add(new FuncFieldConverter(from, to)); } /// /// Find a default converter /// /// /// /// The first matching converter - null if there isnt one public IAvroFieldConverter GetDefaultConverter(Avro.Schema.Type tag, Type propType) { Type avroType; switch (tag) { case Avro.Schema.Type.Null: return null; case Avro.Schema.Type.Boolean: avroType = typeof(bool); break; case Avro.Schema.Type.Int: avroType = typeof(int); break; case Avro.Schema.Type.Long: avroType = typeof(long); break; case Avro.Schema.Type.Float: avroType = typeof(float); break; case Avro.Schema.Type.Double: avroType = typeof(double); break; case Avro.Schema.Type.Bytes: avroType = typeof(byte[]); break; case Avro.Schema.Type.String: avroType = typeof(string); break; case Avro.Schema.Type.Record: return null; case Avro.Schema.Type.Enumeration: return null; case Avro.Schema.Type.Array: return null; case Avro.Schema.Type.Map: return null; case Avro.Schema.Type.Union: return null; case Avro.Schema.Type.Fixed: avroType = typeof(byte[]); break; case Avro.Schema.Type.Error: return null; default: return null; } foreach (var c in _defaultConverters) { if (c.GetAvroType() == avroType && c.GetPropertyType() == propType) { return c; } } return null; } /// /// Add an array helper. Array helpers are used for collections that are not generic lists. /// /// Name of the helper. Corresponds to metadata "helper" field in the schema. /// Type of helper. Inherited from ArrayHelper public void AddArrayHelper(string name, Type helperType) { if (!typeof(ArrayHelper).IsAssignableFrom(helperType)) { throw new AvroException($"{helperType.Name} is not an ArrayHelper"); } _nameArrayMap.TryAdd(name, helperType); } /// /// Find an array helper for an array schema node. /// /// Schema /// The array object. If it is null then Add(), Count() and Clear methods will throw exceptions. /// public ArrayHelper GetArrayHelper(ArraySchema schema, IEnumerable enumerable) { Type h; // note ArraySchema is unamed and doesnt have a FulllName, use "helper" metadata // metadata is json string, strip quotes string s = null; s = schema.GetHelper(); if (s != null && _nameArrayMap.TryGetValue(s, out h)) { return (ArrayHelper)Activator.CreateInstance(h, enumerable); } return (ArrayHelper)Activator.CreateInstance(typeof(ArrayHelper), enumerable); } /// /// Find a class that matches the schema full name. /// /// /// public DotnetClass GetClass(RecordSchema schema) { DotnetClass c; if (!_nameClassMap.TryGetValue(schema.Fullname, out c)) { return null; } return c; } /// /// Add an entry to the class cache. /// /// Type of the C# class /// Schema public void LoadClassCache(Type objType, Schema s) { switch (s) { case RecordSchema rs: if (!objType.IsClass) { throw new AvroException($"Cant map scalar type {objType.Name} to record {rs.Fullname}"); } if (typeof(byte[]).IsAssignableFrom(objType) || typeof(string).IsAssignableFrom(objType) || typeof(IEnumerable).IsAssignableFrom(objType) || typeof(IDictionary).IsAssignableFrom(objType)) { throw new AvroException($"Cant map type {objType.Name} to record {rs.Fullname}"); } AddClassNameMapItem(rs, objType); var c = GetClass(rs); foreach (var f in rs.Fields) { /* //.StackOverflowException var t = c.GetPropertyType(f); LoadClassCache(t, f.Schema); */ if (_previousFields.TryAdd(f.Name, f.Schema)) { var t = c.GetPropertyType(f); LoadClassCache(t, f.Schema); } } break; case ArraySchema ars: if (!typeof(IEnumerable).IsAssignableFrom(objType)) { throw new AvroException($"Cant map type {objType.Name} to array {ars.Name}"); } if (!objType.IsGenericType) { throw new AvroException($"{objType.Name} needs to be a generic type"); } LoadClassCache(objType.GenericTypeArguments[0], ars.ItemSchema); break; case MapSchema ms: if (!typeof(IDictionary).IsAssignableFrom(objType)) { throw new AvroException($"Cant map type {objType.Name} to map {ms.Name}"); } if (!objType.IsGenericType) { throw new AvroException($"Cant map non-generic type {objType.Name} to map {ms.Name}"); } if (!typeof(string).IsAssignableFrom(objType.GenericTypeArguments[0])) { throw new AvroException($"First type parameter of {objType.Name} must be assignable to string"); } LoadClassCache(objType.GenericTypeArguments[1], ms.ValueSchema); break; case NamedSchema ns: EnumCache.AddEnumNameMapItem(ns, objType); break; case UnionSchema us: if (us.Schemas.Count == 2 && (us.Schemas[0].Tag == Schema.Type.Null || us.Schemas[1].Tag == Schema.Type.Null) && objType.IsClass) { // in this case objType will match the non null type in the union foreach (var o in us.Schemas) { if (o.Tag != Schema.Type.Null) { LoadClassCache(objType, o); } } } else { // check the schema types are registered foreach (var o in us.Schemas) { if (o.Tag == Schema.Type.Record && GetClass(o as RecordSchema) == null) { throw new AvroException($"Class for union record type {o.Fullname} is not registered. Create a ClassCache object and call LoadClassCache"); } } } break; } } } }