/* * 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 Newtonsoft.Json.Linq; using Newtonsoft.Json; namespace Avro { /// /// Base class for all schema types /// public abstract class Schema { /// /// Enum for schema types /// public enum Type { /// /// No value. /// Null, /// /// A binary value. /// Boolean, /// /// A 32-bit signed integer. /// Int, /// /// A 64-bit signed integer. /// Long, /// /// A single precision (32-bit) IEEE 754 floating-point number. /// Float, /// /// A double precision (64-bit) IEEE 754 floating-point number. /// Double, /// /// A sequence of 8-bit unsigned bytes. /// Bytes, /// /// An unicode character sequence. /// String, /// /// A logical collection of fields. /// Record, /// /// An enumeration. /// Enumeration, /// /// An array of values. /// Array, /// /// A map of values with string keys. /// Map, /// /// A union. /// Union, /// /// A fixed-length byte string. /// Fixed, /// /// A protocol error. /// Error, /// /// A logical type. /// Logical } /// /// Schema type property /// public Type Tag { get; private set; } /// /// Additional JSON attributes apart from those defined in the AVRO spec /// internal PropertyMap Props { get; private set; } /// /// Constructor for schema class /// /// /// dictionary that provides access to custom properties protected Schema(Type type, PropertyMap props) { this.Tag = type; this.Props = props; } /// /// If this is a record, enum or fixed, returns its name, otherwise the name the primitive type. /// public abstract string Name { get; } /// /// The name of this schema. If this is a named schema such as an enum, it returns the fully qualified /// name for the schema. For other schemas, it returns the type of the schema. /// public virtual string Fullname { get { return Name; } } /// /// Static class to return new instance of schema object /// /// JSON object /// list of named schemas already read /// enclosing namespace of the schema /// new Schema object internal static Schema ParseJson(JToken jtok, SchemaNames names, string encspace) { if (null == jtok) throw new ArgumentNullException("j", "j cannot be null."); if (jtok.Type == JTokenType.String) // primitive schema with no 'type' property or primitive or named type of a record field { string value = (string)jtok; PrimitiveSchema ps = PrimitiveSchema.NewInstance(value); if (null != ps) return ps; NamedSchema schema = null; if (names.TryGetValue(value, null, encspace, null, out schema)) return schema; throw new SchemaParseException($"Undefined name: {value} at '{jtok.Path}'"); } if (jtok is JArray) // union schema with no 'type' property or union type for a record field return UnionSchema.NewInstance(jtok as JArray, null, names, encspace); if (jtok is JObject) // JSON object with open/close parenthesis, it must have a 'type' property { JObject jo = jtok as JObject; JToken jtype = jo["type"]; if (null == jtype) throw new SchemaParseException($"Property type is required at '{jtok.Path}'"); var props = Schema.GetProperties(jtok); if (jtype.Type == JTokenType.String) { string type = (string)jtype; if (type.Equals("array", StringComparison.Ordinal)) return ArraySchema.NewInstance(jtok, props, names, encspace); if (type.Equals("map", StringComparison.Ordinal)) return MapSchema.NewInstance(jtok, props, names, encspace); if (null != jo["logicalType"]) // logical type based on a primitive return LogicalSchema.NewInstance(jtok, props, names, encspace); Schema schema = PrimitiveSchema.NewInstance((string)type, props); if (null != schema) return schema; return NamedSchema.NewInstance(jo, props, names, encspace); } else if (jtype.Type == JTokenType.Array) return UnionSchema.NewInstance(jtype as JArray, props, names, encspace); else if (jtype.Type == JTokenType.Object && null != jo["logicalType"]) // logical type based on a complex type return LogicalSchema.NewInstance(jtok, props, names, encspace); } throw new AvroTypeException($"Invalid JSON for schema: {jtok} at '{jtok.Path}'"); } /// /// Parses a given JSON string to create a new schema object /// /// JSON string /// new Schema object public static Schema Parse(string json) { if (string.IsNullOrEmpty(json)) throw new ArgumentNullException(nameof(json), "json cannot be null."); return Parse(json.Trim(), new SchemaNames(), null); // standalone schema, so no enclosing namespace } /// /// Parses a JSON string to create a new schema object /// /// JSON string /// list of named schemas already read /// enclosing namespace of the schema /// new Schema object internal static Schema Parse(string json, SchemaNames names, string encspace) { Schema sc = PrimitiveSchema.NewInstance(json); if (null != sc) return sc; try { bool IsArray = json.StartsWith("[", StringComparison.Ordinal) && json.EndsWith("]", StringComparison.Ordinal); JContainer j = IsArray ? (JContainer)JArray.Parse(json) : (JContainer)JObject.Parse(json); return ParseJson(j, names, encspace); } catch (Newtonsoft.Json.JsonSerializationException ex) { throw new SchemaParseException("Could not parse. " + ex.Message + Environment.NewLine + json); } } /// /// Static function to parse custom properties (not defined in the Avro spec) from the given JSON object /// /// JSON object to parse /// Property map if custom properties were found, null if no custom properties found internal static PropertyMap GetProperties(JToken jtok) { var props = new PropertyMap(); props.Parse(jtok); if (props.Count > 0) return props; else return null; } /// /// Returns the canonical JSON representation of this schema. /// /// The canonical JSON representation of this schema. public override string ToString() { using (System.IO.StringWriter sw = new System.IO.StringWriter()) using (Newtonsoft.Json.JsonTextWriter writer = new Newtonsoft.Json.JsonTextWriter(sw)) { WriteJson(writer, new SchemaNames(), null); // stand alone schema, so no enclosing name space return sw.ToString(); } } /// /// Writes opening { and 'type' property /// /// JSON writer private void writeStartObject(JsonTextWriter writer) { writer.WriteStartObject(); writer.WritePropertyName("type"); writer.WriteValue(GetTypeString(this.Tag)); } /// /// Returns symbol name for the given schema type /// /// schema type /// symbol name public static string GetTypeString(Type type) { return type != Type.Enumeration ? type.ToString().ToLowerInvariant() : "enum"; } /// /// Default implementation for writing schema properties in JSON format /// /// JSON writer /// list of named schemas already written /// enclosing namespace of the schema protected internal virtual void WriteJsonFields(JsonTextWriter writer, SchemaNames names, string encspace) { } /// /// Writes schema object in JSON format /// /// JSON writer /// list of named schemas already written /// enclosing namespace of the schema protected internal virtual void WriteJson(JsonTextWriter writer, SchemaNames names, string encspace) { writeStartObject(writer); WriteJsonFields(writer, names, encspace); if (null != this.Props) Props.WriteJson(writer); writer.WriteEndObject(); } /// /// Returns the schema's custom property value given the property name /// /// custom property name /// custom property value public string GetProperty(string key) { if (null == this.Props) return null; string v; return this.Props.TryGetValue(key, out v) ? v : null; } /// /// Hash code function /// /// public override int GetHashCode() { return Tag.GetHashCode() + getHashCode(Props); } /// /// Returns true if and only if data written using writerSchema can be read using the current schema /// according to the Avro resolution rules. /// /// The writer's schema to match against. /// True if and only if the current schema matches the writer's. public virtual bool CanRead(Schema writerSchema) { return Tag == writerSchema.Tag; } /// /// Compares two objects, null is equal to null /// /// first object /// second object /// true if two objects are equal, false otherwise protected static bool areEqual(object o1, object o2) { return o1 == null ? o2 == null : o1.Equals(o2); } /// /// Hash code helper function /// /// /// protected static int getHashCode(object obj) { return obj == null ? 0 : obj.GetHashCode(); } } }