| /* |
| * 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.Generic; |
| using System.Globalization; |
| using System.IO; |
| using System.Linq; |
| using Avro.Generic; |
| using Avro.IO; |
| using Avro.Specific; |
| |
| namespace Avro.File |
| { |
| /// <summary> |
| /// Provides access to Avro data written using the <see cref="DataFileWriter{T}" />. |
| /// </summary> |
| /// <typeparam name="T">Type to deserialze data objects to.</typeparam> |
| /// <seealso cref="IFileReader<T>" /> |
| public class DataFileReader<T> : IFileReader<T> |
| { |
| /// <summary> |
| /// Defines the signature for a function that returns a new <see cref="DatumReader{T}" /> |
| /// given a writer and reader schema. |
| /// </summary> |
| /// <param name="writerSchema">Schema used to write the datum.</param> |
| /// <param name="readerSchema">Schema used to read the datum.</param> |
| /// <returns> |
| /// A datum reader. |
| /// </returns> |
| public delegate DatumReader<T> CreateDatumReader(Schema writerSchema, Schema readerSchema); |
| |
| private DatumReader<T> _reader; |
| private Decoder _decoder, _datumDecoder; |
| private Header _header; |
| private Codec _codec; |
| private DataBlock _currentBlock; |
| private long _blockRemaining; |
| private long _blockSize; |
| private bool _availableBlock; |
| private byte[] _syncBuffer; |
| private long _blockStart; |
| private Stream _stream; |
| private readonly bool _leaveOpen; |
| private readonly Schema _readerSchema; |
| private readonly CreateDatumReader _datumReaderFactory; |
| |
| /// <summary> |
| /// Open a reader for a file using path. |
| /// </summary> |
| /// <param name="path">The path.</param> |
| /// <returns> |
| /// File Reader. |
| /// </returns> |
| public static IFileReader<T> OpenReader(string path) |
| { |
| return OpenReader(new FileStream(path, FileMode.Open), null); |
| } |
| |
| /// <summary> |
| /// Open a reader for a file using path and the reader's schema. |
| /// </summary> |
| /// <param name="path">Path to the file.</param> |
| /// <param name="readerSchema">Schema used to read data from the file.</param> |
| /// <returns> |
| /// A new file reader. |
| /// </returns> |
| public static IFileReader<T> OpenReader(string path, Schema readerSchema) |
| { |
| return OpenReader(new FileStream(path, FileMode.Open), readerSchema); |
| } |
| |
| /// <summary> |
| /// Open a reader for a stream. |
| /// </summary> |
| /// <param name="inStream">The in stream.</param> |
| /// <returns> |
| /// File Reader. |
| /// </returns> |
| public static IFileReader<T> OpenReader(Stream inStream) |
| { |
| return OpenReader(inStream, null); |
| } |
| |
| /// <summary> |
| /// Open a reader for a stream. |
| /// </summary> |
| /// <param name="inStream">The in stream.</param> |
| /// <param name="leaveOpen">Leave the stream open after disposing the object.</param> |
| /// <returns> |
| /// File Reader. |
| /// </returns> |
| public static IFileReader<T> OpenReader(Stream inStream, bool leaveOpen) |
| { |
| return OpenReader(inStream, null, leaveOpen); |
| } |
| |
| /// <summary> |
| /// Open a reader for a stream using the reader's schema. |
| /// </summary> |
| /// <param name="inStream">Stream containing the file contents.</param> |
| /// <param name="readerSchema">Schema used to read the file.</param> |
| /// <returns> |
| /// A new file reader. |
| /// </returns> |
| public static IFileReader<T> OpenReader(Stream inStream, Schema readerSchema) |
| { |
| return OpenReader(inStream, readerSchema, CreateDefaultReader); |
| } |
| |
| /// <summary> |
| /// Open a reader for a stream using the reader's schema. |
| /// </summary> |
| /// <param name="inStream">Stream containing the file contents.</param> |
| /// <param name="readerSchema">Schema used to read the file.</param> |
| /// <param name="leaveOpen">Leave the stream open after disposing the object.</param> |
| /// <returns> |
| /// A new file reader. |
| /// </returns> |
| public static IFileReader<T> OpenReader(Stream inStream, Schema readerSchema, bool leaveOpen) |
| { |
| return OpenReader(inStream, readerSchema, CreateDefaultReader, leaveOpen); |
| } |
| |
| /// <summary> |
| /// Open a reader for a stream using the reader's schema and a custom DatumReader. |
| /// </summary> |
| /// <param name="inStream">Stream of file contents.</param> |
| /// <param name="readerSchema">Schema used to read the file.</param> |
| /// <param name="datumReaderFactory">Factory to create datum readers given a reader an writer schema.</param> |
| /// <returns> |
| /// A new file reader. |
| /// </returns> |
| public static IFileReader<T> OpenReader(Stream inStream, Schema readerSchema, CreateDatumReader datumReaderFactory) |
| { |
| return new DataFileReader<T>(inStream, readerSchema, datumReaderFactory, false); // (not supporting 1.2 or below, format) |
| } |
| |
| /// <summary> |
| /// Open a reader for a stream using the reader's schema and a custom DatumReader. |
| /// </summary> |
| /// <param name="inStream">Stream of file contents.</param> |
| /// <param name="readerSchema">Schema used to read the file.</param> |
| /// <param name="datumReaderFactory">Factory to create datum readers given a reader an writer schema.</param> |
| /// <param name="leaveOpen">Leave the stream open after disposing the object.</param> |
| /// <returns> |
| /// A new file reader. |
| /// </returns> |
| public static IFileReader<T> OpenReader(Stream inStream, Schema readerSchema, CreateDatumReader datumReaderFactory, bool leaveOpen) |
| { |
| return new DataFileReader<T>(inStream, readerSchema, datumReaderFactory, leaveOpen); // (not supporting 1.2 or below, format) |
| } |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="DataFileReader{T}"/> class. |
| /// </summary> |
| /// <param name="stream">The stream.</param> |
| /// <param name="readerSchema">The reader schema.</param> |
| /// <param name="datumReaderFactory">The datum reader factory.</param> |
| /// <param name="leaveOpen">if set to <c>true</c> [leave open].</param> |
| private DataFileReader(Stream stream, Schema readerSchema, CreateDatumReader datumReaderFactory, bool leaveOpen) |
| { |
| _readerSchema = readerSchema; |
| _datumReaderFactory = datumReaderFactory; |
| _leaveOpen = leaveOpen; |
| Init(stream); |
| BlockFinished(); |
| } |
| |
| /// <inheritdoc/> |
| public Header GetHeader() |
| { |
| return _header; |
| } |
| |
| /// <inheritdoc/> |
| public Schema GetSchema() |
| { |
| return _header.Schema; |
| } |
| |
| /// <inheritdoc/> |
| public ICollection<string> GetMetaKeys() |
| { |
| return _header.MetaData.Keys; |
| } |
| |
| /// <inheritdoc/> |
| public byte[] GetMeta(string key) |
| { |
| try |
| { |
| return _header.MetaData[key]; |
| } |
| catch (KeyNotFoundException) |
| { |
| return null; |
| } |
| } |
| |
| /// <inheritdoc/> |
| public long GetMetaLong(string key) |
| { |
| return long.Parse(GetMetaString(key), CultureInfo.InvariantCulture); |
| } |
| |
| /// <inheritdoc/> |
| public string GetMetaString(string key) |
| { |
| byte[] value = GetMeta(key); |
| if (value == null) |
| { |
| return null; |
| } |
| try |
| { |
| return System.Text.Encoding.UTF8.GetString(value); |
| } |
| catch (Exception e) |
| { |
| throw new AvroRuntimeException(string.Format(CultureInfo.InvariantCulture, |
| "Error fetching meta data for key: {0}", key), e); |
| } |
| } |
| |
| /// <inheritdoc/> |
| public void Seek(long position) |
| { |
| if (!_stream.CanSeek) |
| throw new AvroRuntimeException("Not a valid input stream - must be seekable!"); |
| |
| _stream.Position = position; |
| _decoder = new BinaryDecoder(_stream); |
| _datumDecoder = null; |
| _blockRemaining = 0; |
| _blockStart = position; |
| } |
| |
| /// <inheritdoc/> |
| public void Sync(long position) |
| { |
| Seek(position); |
| |
| // work around an issue where 1.5.4 C stored sync in metadata |
| if ((position == 0) && (GetMeta(DataFileConstants.MetaDataSync) != null)) |
| { |
| Init(_stream); // re-init to skip header |
| return; |
| } |
| |
| try |
| { |
| bool done = false; |
| |
| // read until sync mark matched |
| do |
| { |
| _decoder.ReadFixed(_syncBuffer); |
| if (Enumerable.SequenceEqual(_syncBuffer, _header.SyncData)) |
| { |
| done = true; |
| } |
| else |
| { |
| _stream.Position -= DataFileConstants.SyncSize - 1; |
| } |
| } |
| while (!done); |
| } |
| catch |
| { |
| // could not find .. default to EOF |
| } |
| |
| _blockStart = _stream.Position; |
| } |
| |
| /// <inheritdoc/> |
| public bool PastSync(long position) |
| { |
| return (_blockStart >= position + DataFileConstants.SyncSize) || (_blockStart >= _stream.Length); |
| } |
| |
| /// <inheritdoc/> |
| public long PreviousSync() |
| { |
| if (!_stream.CanSeek) |
| throw new AvroRuntimeException("Not a valid input stream - must be seekable!"); |
| return _blockStart; |
| } |
| |
| /// <inheritdoc/> |
| public long Tell() |
| { |
| return _stream.Position; |
| } |
| |
| /// <inheritdoc/> |
| public IEnumerable<T> NextEntries |
| { |
| get |
| { |
| while (HasNext()) |
| { |
| yield return Next(); |
| } |
| } |
| } |
| |
| /// <inheritdoc/> |
| public bool HasNext() |
| { |
| try |
| { |
| if (_blockRemaining == 0) |
| { |
| // TODO: Check that the (block) stream is not partially read |
| /*if (_datumDecoder != null) |
| { }*/ |
| if (HasNextBlock()) |
| { |
| _currentBlock = NextRawBlock(_currentBlock); |
| _currentBlock.Data = _codec.Decompress(_currentBlock.Data, (int)_blockSize); |
| _datumDecoder = new BinaryDecoder(_currentBlock.GetDataAsStream()); |
| } |
| } |
| return _blockRemaining != 0; |
| } |
| catch (Exception e) |
| { |
| throw new AvroRuntimeException(string.Format(CultureInfo.InvariantCulture, |
| "Error fetching next object from block: {0}", e)); |
| } |
| } |
| |
| /// <summary> |
| /// Resets this reader. |
| /// </summary> |
| public void Reset() |
| { |
| Init(_stream); |
| } |
| |
| /// <inheritdoc/> |
| public void Dispose() |
| { |
| Dispose(true); |
| GC.SuppressFinalize(this); |
| } |
| |
| /// <summary> |
| /// Releases resources associated with this <see cref="DataFileReader{T}"/>. |
| /// </summary> |
| /// <param name="disposing"> |
| /// True if called from <see cref="Dispose()"/>; false otherwise. |
| /// </param> |
| protected virtual void Dispose(bool disposing) |
| { |
| if (!_leaveOpen) |
| _stream.Close(); |
| |
| if (disposing && !_leaveOpen) |
| _stream.Dispose(); |
| } |
| |
| /// <summary> |
| /// Initializes the specified stream. |
| /// </summary> |
| /// <param name="stream">The stream.</param> |
| /// <exception cref="AvroRuntimeException"> |
| /// Not a valid data file! |
| /// or |
| /// Not a valid data file!. |
| /// </exception> |
| private void Init(Stream stream) |
| { |
| _stream = stream; |
| _header = new Header(); |
| _decoder = new BinaryDecoder(stream); |
| _syncBuffer = new byte[DataFileConstants.SyncSize]; |
| |
| // read magic |
| byte[] firstBytes = new byte[DataFileConstants.Magic.Length]; |
| try |
| { |
| _decoder.ReadFixed(firstBytes); |
| } |
| catch (Exception e) |
| { |
| throw new AvroRuntimeException("Not a valid data file!", e); |
| } |
| if (!firstBytes.SequenceEqual(DataFileConstants.Magic)) |
| throw new AvroRuntimeException("Not a valid data file!"); |
| |
| // read meta data |
| long len = _decoder.ReadMapStart(); |
| if (len > 0) |
| { |
| do |
| { |
| for (long i = 0; i < len; i++) |
| { |
| string key = _decoder.ReadString(); |
| byte[] val = _decoder.ReadBytes(); |
| _header.MetaData.Add(key, val); |
| } |
| } while ((len = _decoder.ReadMapNext()) != 0); |
| } |
| |
| // read in sync data |
| _decoder.ReadFixed(_header.SyncData); |
| |
| // parse schema and set codec |
| _header.Schema = Schema.Parse(GetMetaString(DataFileConstants.MetaDataSchema)); |
| _reader = _datumReaderFactory(_header.Schema, _readerSchema ?? _header.Schema); |
| _codec = ResolveCodec(); |
| } |
| |
| /// <summary> |
| /// Creates the default reader. |
| /// </summary> |
| /// <param name="writerSchema">The writer schema.</param> |
| /// <param name="readerSchema">The reader schema.</param> |
| /// <returns> |
| /// Datum Reader. |
| /// </returns> |
| private static DatumReader<T> CreateDefaultReader(Schema writerSchema, Schema readerSchema) |
| { |
| DatumReader<T> reader = null; |
| Type type = typeof(T); |
| |
| if (typeof(ISpecificRecord).IsAssignableFrom(type)) |
| { |
| reader = new SpecificReader<T>(writerSchema, readerSchema); |
| } |
| else // generic |
| { |
| reader = new GenericReader<T>(writerSchema, readerSchema); |
| } |
| return reader; |
| } |
| |
| /// <summary> |
| /// Resolves the codec. |
| /// </summary> |
| /// <returns> |
| /// Resolved codec. |
| /// </returns> |
| private Codec ResolveCodec() |
| { |
| string codec = GetMetaString(DataFileConstants.MetaDataCodec); |
| |
| // If codec is absent, it is assumed to be "null" |
| if (codec == null) |
| return Codec.CreateCodec(Codec.Type.Null); |
| |
| return Codec.CreateCodecFromString(codec); |
| } |
| |
| /// <inheritdoc/> |
| public T Next() |
| { |
| return Next(default(T)); |
| } |
| |
| /// <summary> |
| /// Reads the next datum from the file. |
| /// </summary> |
| /// <param name="reuse">The reuse.</param> |
| /// <returns>Next deserialized data entry.</returns> |
| /// <exception cref="AvroRuntimeException">No more datum objects remaining in block! |
| /// or |
| /// Error fetching next object from block: {0}. |
| /// </exception> |
| private T Next(T reuse) |
| { |
| try |
| { |
| if (!HasNext()) |
| throw new AvroRuntimeException("No more datum objects remaining in block!"); |
| |
| T result = _reader.Read(reuse, _datumDecoder); |
| if (--_blockRemaining == 0) |
| { |
| BlockFinished(); |
| } |
| return result; |
| } |
| catch (Exception e) |
| { |
| throw new AvroRuntimeException(string.Format(CultureInfo.InvariantCulture, |
| "Error fetching next object from block: {0}", e)); |
| } |
| } |
| |
| /// <summary> |
| /// Ends the stream for the block. |
| /// </summary> |
| private void BlockFinished() |
| { |
| if (_stream.CanSeek) |
| _blockStart = _stream.Position; |
| } |
| |
| /// <summary> |
| /// Reads the Next block from the file. |
| /// </summary> |
| /// <param name="reuse">The reuse.</param> |
| /// <returns>Data Block.</returns> |
| /// <exception cref="AvroRuntimeException"> |
| /// No data remaining in block! |
| /// or |
| /// Invalid sync!. |
| /// </exception> |
| private DataBlock NextRawBlock(DataBlock reuse) |
| { |
| if (!HasNextBlock()) |
| throw new AvroRuntimeException("No data remaining in block!"); |
| |
| if (reuse == null || reuse.Data.Length < _blockSize) |
| { |
| reuse = new DataBlock(_blockRemaining, _blockSize); |
| } |
| else |
| { |
| reuse.NumberOfEntries = _blockRemaining; |
| reuse.BlockSize = _blockSize; |
| } |
| |
| _decoder.ReadFixed(reuse.Data, 0, (int)reuse.BlockSize); |
| _decoder.ReadFixed(_syncBuffer); |
| |
| if (!Enumerable.SequenceEqual(_syncBuffer, _header.SyncData)) |
| throw new AvroRuntimeException("Invalid sync!"); |
| |
| _availableBlock = false; |
| return reuse; |
| } |
| |
| /// <summary> |
| /// Evaluates if there is data left in the stream. |
| /// </summary> |
| /// <returns>True if there is data left in the stream, otherwise false.</returns> |
| private bool DataLeft() |
| { |
| long currentPosition = _stream.Position; |
| if (_stream.ReadByte() != -1) |
| _stream.Position = currentPosition; |
| else |
| return false; |
| |
| return true; |
| } |
| |
| /// <summary> |
| /// Determines whether [has next block]. |
| /// </summary> |
| /// <returns> |
| /// <c>true</c> if [has next block]; otherwise, <c>false</c>. |
| /// </returns> |
| /// <exception cref="AvroRuntimeException"> |
| /// Block size invalid or too large for this implementation: " + _blockSize |
| /// or |
| /// Error ascertaining if data has next block: {0}. |
| /// </exception> |
| private bool HasNextBlock() |
| { |
| try |
| { |
| // block currently being read |
| if (_availableBlock) |
| return true; |
| |
| // check to ensure still data to read |
| if (_stream.CanSeek) |
| { |
| if (!DataLeft()) |
| return false; |
| |
| _blockRemaining = _decoder.ReadLong(); // read block count |
| } |
| else |
| { |
| // when the stream is not seekable, the only way to know if there is still |
| // some data to read is to reach the end and raise an AvroException here. |
| try |
| { |
| _blockRemaining = _decoder.ReadLong(); // read block count |
| } |
| catch (AvroException) |
| { |
| return false; |
| } |
| } |
| |
| _blockSize = _decoder.ReadLong(); // read block size |
| if (_blockSize > int.MaxValue || _blockSize < 0) |
| { |
| throw new AvroRuntimeException("Block size invalid or too large for this " + |
| "implementation: " + _blockSize); |
| } |
| _availableBlock = true; |
| return true; |
| } |
| catch (Exception e) |
| { |
| throw new AvroRuntimeException(string.Format(CultureInfo.InvariantCulture, |
| "Error ascertaining if data has next block: {0}", e), e); |
| } |
| } |
| |
| /// <summary> |
| /// Encapsulates a block of data read by the <see cref="DataFileReader{T}" />. |
| /// </summary> |
| /// <seealso cref="IFileReader<T>" /> |
| private class DataBlock |
| { |
| /// <summary> |
| /// Gets or sets raw bytes within this block. |
| /// </summary> |
| /// <value> |
| /// The data. |
| /// </value> |
| public byte[] Data { get; set; } |
| |
| /// <summary> |
| /// Gets or sets number of entries in this block. |
| /// </summary> |
| /// <value> |
| /// The number of entries. |
| /// </value> |
| public long NumberOfEntries { get; set; } |
| |
| /// <summary> |
| /// Gets or sets size of this block in bytes. |
| /// </summary> |
| /// <value> |
| /// The size of the block. |
| /// </value> |
| public long BlockSize { get; set; } |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="DataBlock" /> class. |
| /// </summary> |
| /// <param name="numberOfEntries">Number of entries in this block.</param> |
| /// <param name="blockSize">Size of this block in bytes.</param> |
| public DataBlock(long numberOfEntries, long blockSize) |
| { |
| NumberOfEntries = numberOfEntries; |
| BlockSize = blockSize; |
| Data = new byte[blockSize]; |
| } |
| |
| /// <summary> |
| /// Gets the data as stream. |
| /// </summary> |
| /// <returns>A stream.</returns> |
| internal Stream GetDataAsStream() |
| { |
| return new MemoryStream(Data); |
| } |
| } |
| } |
| } |