// Npgsql.NpgsqlCopySerializer.cs
//
// Author:
// Kalle Hallivuori
//
// Copyright (C) 2007 The Npgsql Development Team
// npgsql-general@gborg.postgresql.org
// http://gborg.postgresql.org/project/npgsql/projdisplay.php
//
// Permission to use, copy, modify, and distribute this software and its
// documentation for any purpose, without fee, and without a written
// agreement is hereby granted, provided that the above copyright notice
// and this paragraph and the following two paragraphs appear in all copies.
//
// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
//
// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS
// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
using System;
using System.IO;
using System.Text;
using System.Globalization;
namespace Npgsql
{
///
/// Writes given objects into a stream for PostgreSQL COPY in default copy format (not CSV or BINARY).
///
public class NpgsqlCopySerializer
{
///
/// Default delimiter.
///
public const String DEFAULT_DELIMITER = "\t";
///
/// Default separator.
///
public const String DEFAULT_SEPARATOR = "\n";
///
/// Default null.
///
public const String DEFAULT_NULL = "\\N";
///
/// Default escape.
///
public const String DEFAULT_ESCAPE = "\\";
///
/// Default quote.
///
public const String DEFAULT_QUOTE = "\"";
///
/// Default buffer size.
///
public const int DEFAULT_BUFFER_SIZE = 8192;
private static readonly CultureInfo _cultureInfo = CultureInfo.InvariantCulture; // PostgreSQL currently only supports SQL notation for decimal point (which is the same as InvariantCulture)
private readonly NpgsqlConnector _context;
private Stream _toStream;
private String _delimiter = DEFAULT_DELIMITER,
_escape = DEFAULT_ESCAPE,
_separator = DEFAULT_SEPARATOR,
_null = DEFAULT_NULL;
private byte[] _delimiterBytes = null, _escapeBytes = null, _separatorBytes = null, _nullBytes = null;
private byte[][] _escapeSequenceBytes = null;
private String[] _stringsToEscape = null;
private byte[] _sendBuffer = null;
private int _sendBufferAt = 0, _lastFieldEndAt = 0, _lastRowEndAt = 0, _atField = 0;
///
/// Constructor.
///
///
public NpgsqlCopySerializer(NpgsqlConnection conn)
{
_context = conn.Connector;
}
///
/// Report whether the serializer is active.
///
public bool IsActive
{
get { return _toStream != null && _context.Mediator.CopyStream == _toStream && _context.CurrentState is NpgsqlCopyInState; }
}
///
/// To Stream.
///
public Stream ToStream
{
get
{
if (_toStream == null)
{
_toStream = _context.Mediator.CopyStream;
}
return _toStream;
}
set
{
if (IsActive)
{
throw new NpgsqlException("Do not change stream of an active " + this);
}
_toStream = value;
}
}
///
/// Delimiter.
///
public String Delimiter
{
get { return _delimiter; }
set
{
if (IsActive)
{
throw new NpgsqlException("Do not change delimiter of an active " + this);
}
_delimiter = value ?? DEFAULT_DELIMITER;
_delimiterBytes = null;
_stringsToEscape = null;
_escapeSequenceBytes = null;
}
}
private byte[] DelimiterBytes
{
get
{
if (_delimiterBytes == null)
{
_delimiterBytes = BackendEncoding.UTF8Encoding.GetBytes(_delimiter);
}
return _delimiterBytes;
}
}
///
/// Separator.
///
public String Separator
{
get { return _separator; }
set
{
if (IsActive)
{
throw new NpgsqlException("Do not change separator of an active " + this);
}
_separator = value ?? DEFAULT_SEPARATOR;
_separatorBytes = null;
_stringsToEscape = null;
_escapeSequenceBytes = null;
}
}
private byte[] SeparatorBytes
{
get
{
if (_separatorBytes == null)
{
_separatorBytes = BackendEncoding.UTF8Encoding.GetBytes(_separator);
}
return _separatorBytes;
}
}
///
/// Escape.
///
public String Escape
{
get { return _escape; }
set
{
if (IsActive)
{
throw new NpgsqlException("Do not change escape symbol of an active " + this);
}
_escape = value ?? DEFAULT_ESCAPE;
_escapeBytes = null;
_stringsToEscape = null;
_escapeSequenceBytes = null;
}
}
private byte[] EscapeBytes
{
get
{
if (_escapeBytes == null)
{
_escapeBytes = BackendEncoding.UTF8Encoding.GetBytes(_escape);
}
return _escapeBytes;
}
}
///
/// Null.
///
public String Null
{
get { return _null; }
set
{
if (IsActive)
{
throw new NpgsqlException("Do not change null symbol of an active " + this);
}
_null = value ?? DEFAULT_NULL;
_nullBytes = null;
_stringsToEscape = null;
_escapeSequenceBytes = null;
}
}
private byte[] NullBytes
{
get
{
if (_nullBytes == null)
{
_nullBytes = BackendEncoding.UTF8Encoding.GetBytes(_null);
}
return _nullBytes;
}
}
///
/// Buffer size.
///
public Int32 BufferSize
{
get { return _sendBuffer != null ? _sendBuffer.Length : DEFAULT_BUFFER_SIZE; }
set
{
byte[] _newBuffer = new byte[value];
if (_sendBuffer != null)
{
for (int i = 0; i < _sendBufferAt; i++)
{
_newBuffer[i] = _sendBuffer[i];
}
}
_sendBuffer = _newBuffer;
}
}
///
/// Flush buffers.
///
public void Flush()
{
if (_sendBufferAt > 0)
{
ToStream.Write(_sendBuffer, 0, _sendBufferAt);
ToStream.Flush();
}
_sendBufferAt = 0;
_lastRowEndAt = 0;
_lastFieldEndAt = 0;
}
///
/// Flush rows.
///
public void FlushRows()
{
if (_lastRowEndAt > 0)
{
ToStream.Write(_sendBuffer, 0, _lastRowEndAt);
ToStream.Flush();
int len = _sendBufferAt - _lastRowEndAt;
for (int i = 0; i < len; i++)
{
_sendBuffer[i] = _sendBuffer[_lastRowEndAt + i];
}
_lastFieldEndAt -= _lastRowEndAt;
_sendBufferAt -= _lastRowEndAt;
_lastRowEndAt = 0;
}
}
///
/// Flush fields.
///
public void FlushFields()
{
if (_lastFieldEndAt > 0)
{
ToStream.Write(_sendBuffer, 0, _lastFieldEndAt);
ToStream.Flush();
int len = _sendBufferAt - _lastFieldEndAt;
for (int i = 0; i < len; i++)
{
_sendBuffer[i] = _sendBuffer[_lastFieldEndAt + i];
}
_lastRowEndAt -= _lastFieldEndAt;
_sendBufferAt -= _lastFieldEndAt;
_lastFieldEndAt = 0;
}
}
///
/// Close the serializer.
///
public void Close()
{
if (_atField > 0)
{
EndRow();
}
Flush();
ToStream.Close();
}
///
/// Report whether space remains in the buffer.
///
protected int SpaceInBuffer
{
get { return BufferSize - _sendBufferAt; }
}
///
/// Strings to escape.
///
protected String[] StringsToEscape
{
get
{
if (_stringsToEscape == null)
{
_stringsToEscape = new String[] {Delimiter, Separator, Escape, "\r", "\n"};
}
return _stringsToEscape;
}
}
///
/// Escape sequence bytes.
///
protected byte[][] EscapeSequenceBytes
{
get
{
if (_escapeSequenceBytes == null)
{
_escapeSequenceBytes = new byte[StringsToEscape.Length][];
for (int i = 0; i < StringsToEscape.Length; i++)
{
_escapeSequenceBytes[i] = EscapeSequenceFor(StringsToEscape[i].ToCharArray(0, 1)[0]);
}
}
return _escapeSequenceBytes;
}
}
private static readonly byte[] esc_t = new byte[] { (byte)ASCIIBytes.t };
private static readonly byte[] esc_n = new byte[] { (byte)ASCIIBytes.n };
private static readonly byte[] esc_r = new byte[] { (byte)ASCIIBytes.r };
private static readonly byte[] esc_b = new byte[] { (byte)ASCIIBytes.b };
private static readonly byte[] esc_f = new byte[] { (byte)ASCIIBytes.f };
private static readonly byte[] esc_v = new byte[] { (byte)ASCIIBytes.v };
///
/// Escape sequence for the given character.
///
///
///
protected static byte[] EscapeSequenceFor(char c)
{
switch (c)
{
case '\t' :
return esc_t;
case '\n' :
return esc_n;
case '\r' :
return esc_r;
case '\b' :
return esc_b;
case '\f' :
return esc_f;
case '\v' :
return esc_v;
default :
if (c < 32 || c > 127)
{
return new byte[] {(byte) ('0' + ((c/64) & 7)), (byte) ('0' + ((c/8) & 7)), (byte) ('0' + (c & 7))};
}
else
{
return new byte[] {(byte) c};
}
}
}
///
/// Make room for bytes.
///
///
protected void MakeRoomForBytes(int len)
{
if (_sendBuffer == null)
{
_sendBuffer = new byte[BufferSize];
}
if (len >= SpaceInBuffer)
{
FlushRows();
if (len >= SpaceInBuffer)
{
FlushFields();
if (len >= SpaceInBuffer)
{
BufferSize = len;
}
}
}
}
///
/// Add bytes.
///
///
protected void AddBytes(byte[] bytes)
{
MakeRoomForBytes(bytes.Length);
for (int i = 0; i < bytes.Length; i++)
{
_sendBuffer[_sendBufferAt++] = bytes[i];
}
}
///
/// End row.
///
public void EndRow()
{
if (_context != null)
{
while (_atField < _context.CurrentState.CopyFormat.FieldCount)
{
AddNull();
}
}
if (_context == null || ! _context.CurrentState.CopyFormat.IsBinary)
{
AddBytes(SeparatorBytes);
}
_lastRowEndAt = _sendBufferAt;
_atField = 0;
}
///
/// Prefix field.
///
protected void PrefixField()
{
if (_atField > 0)
{
if (_atField >= _context.CurrentState.CopyFormat.FieldCount)
{
throw new NpgsqlException("Tried to add too many fields to a copy record with " + _atField + " fields");
}
AddBytes(DelimiterBytes);
}
}
///
/// Field added.
///
protected void FieldAdded()
{
_lastFieldEndAt = _sendBufferAt;
_atField++;
}
///
/// Add null.
///
public void AddNull()
{
PrefixField();
AddBytes(NullBytes);
FieldAdded();
}
///
/// Add string.
///
///
public void AddString(String fieldValue)
{
PrefixField();
int bufferedUpto = 0;
while (bufferedUpto < fieldValue.Length)
{
int escapeAt = fieldValue.Length;
byte[] escapeSequence = null;
// choose closest instance of strings to escape in fieldValue
for (int eachEscapeable = 0; eachEscapeable < StringsToEscape.Length; eachEscapeable++)
{
int i = fieldValue.IndexOf(StringsToEscape[eachEscapeable], bufferedUpto);
if (i > -1 && i < escapeAt)
{
escapeAt = i;
escapeSequence = EscapeSequenceBytes[eachEscapeable];
}
}
// some, possibly all of fieldValue string does not require escaping and can be buffered for output
if (escapeAt > bufferedUpto)
{
// int encodedLength = BackendEncoding.UTF8Encoding.GetByteCount(fieldValue.ToCharArray(bufferedUpto, escapeAt));
// MakeRoomForBytes(encodedLength);
// _sendBufferAt += BackendEncoding.UTF8Encoding.GetBytes(fieldValue, bufferedUpto, escapeAt, _sendBuffer, _sendBufferAt);
// bufferedUpto = escapeAt;
int encodedLength = BackendEncoding.UTF8Encoding.GetByteCount(fieldValue.ToCharArray(bufferedUpto, escapeAt - bufferedUpto));
MakeRoomForBytes(encodedLength);
_sendBufferAt += BackendEncoding.UTF8Encoding.GetBytes(fieldValue, bufferedUpto, escapeAt - bufferedUpto, _sendBuffer, _sendBufferAt);
bufferedUpto = escapeAt;
}
// now buffer the escape sequence for output
if (escapeSequence != null)
{
AddBytes(EscapeBytes);
AddBytes(escapeSequence);
bufferedUpto++;
}
}
FieldAdded();
}
///
/// add Int32.
///
///
public void AddInt32(Int32 fieldValue)
{
AddString(string.Format(_cultureInfo, "{0}", fieldValue));
}
///
/// Add Int64.
///
///
public void AddInt64(Int64 fieldValue)
{
AddString(string.Format(_cultureInfo, "{0}", fieldValue));
}
///
/// Add number.
///
///
public void AddNumber(double fieldValue)
{
AddString(string.Format(_cultureInfo, "{0}", fieldValue));
}
///
/// Add bool
///
///
public void AddBool(bool fieldValue)
{
AddString(fieldValue ? "TRUE" : "FALSE");
}
///
/// Add DateTime.
///
///
public void AddDateTime(DateTime fieldValue)
{
AddString(string.Format("{0}-{1}-{2} {3}:{4}:{5}.{6}", fieldValue.Year, fieldValue.Month, fieldValue.Day, fieldValue.Hour, fieldValue.Minute, fieldValue.Second, fieldValue.Millisecond));
}
}
}