Dawno, dawno temu ;), przeczytałem na blogu Pawła Potasińskiego o sposobach konwersji polskich liter zakodowanych w standardzie Mazovia w środowisku SQL Serwera. Paweł kontynuował potem ten temat, próbując zrealizować konwersję za pomocą funkcji rozszerzonej, napisanej w .NET. Niestety, nie mógł skorzystać ze standardowych mechanizmów .NET do konwersji łańcuchów (klasa Encoding) i zrealizował ją poprzez zwykłą podmianę znaków (metodą Replace). Dlaczego klasa Encoding nie podołała temu zadaniu? Otóż w zestawie dostępnych stron kodowych tejże klasy, nie istniała taka, która pozwaliłaby na konwersję z systemu Mazovii. Czytając wówczas ten wpis pomyślałem, że warto byłoby w takim razie napisać stosowną klasę dziedziczącą po Encoding, która realizowałaby takie zadanie i wyeliminować ów – dość niebezpieczny (w szczególnych przypadkach) – sposób (dlaczego – opisałem w komentarzach do wspomnianego wpisu Pawła).

Minęły prawie dwa lata, zanim byłem w stanie zrealizować swój zamiar. Niemniej – lepiej późno niż wcale ;).

Aby zaimplementować pochodną klasy Encoding, konieczne jest de facto zaimplementowanie dwóch odrębnych klas, do których będzie ona delegowała swoje zadania. Będą to: klasa odpowiedzialna za zakodowanie treści z systemu Unicode do Mazovii oraz klasa wykonująca czynność odwrotną czyli przetworzenie treści w Mazovii na treść w Unicode. Na potrzeby Pawła wystarczyłaby właściwie jedynie druga klasa, ale skoro założyłem implementację pochodnej klasy Encoding – należy konsekwentnie zrealizować ją w całości, a nie po łebkach.

Na początek omówię klasę kodującą do systemu Mazovii. Będzie ona dziedziczyć po klasie Encoder – abstrakcji przeznaczonej do transformacji (kodowania) tekstu w Unicode do dowolnej strony kodowej. W tworzonej klasie potomnej (MazoviaEncoder) konieczne będzie zaimplementowanie dwóch metod: ustalającej długość ciągu po zakodowaniu GetByteCount oraz odpowiedzialnej za samo kodowanie – metody GetBytes.

public sealed class MazoviaEncoder : Encoder
{
	private Dictionary<char, byte> Translator = new Dictionary<char, byte>();

	internal MazoviaEncoder()
	{
		for (byte i = 0x00; i < 0x80; i++)
		{
			char c = (char)i;
			Translator.Add(c, i); // znaki poniżej 128 to standardowe kody ASCII
		}
		for (byte i = 0x00; i < 0x80; i++)
		{
			char c = (char)MazoviaAsUnicode.Content[i];
			Translator.Add(c, (byte)(i + 0x80)); // znaki powyżej 127 to kody zgodne z Mazovią - trzeba użyć słownika translacji
		}
	}

	public override int GetByteCount(char[] chars, int index, int count, bool flush)
	{
		// Mazovia jest jednobajtową stroną kodową, więc ilość bajtów dla podanej długości tekstu "count",
		// jest równa tej długości (jeden unikodowy, dwubajtowy znak równa się jednemu bajtowi Mazovii)
		return count; 
	}

	public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex, bool flush)
	{
		byte b;
		for (int i = 0; i < charCount; i++)
		{
			if (!Translator.TryGetValue(chars[charIndex + i], out b))
				b = 0x3F;
			bytes[byteIndex + i] = b;
		}
		return charCount;
	}

}

W powyższym kodzie używana jest klasa statyczna MazoviaAsUnicode. Klasa ta jest wyodrębniona, ponieważ korzystać z niej będzie także klasa dekodująca Mazovię do Unicode. Poniżej jej implementacja.

internal static class MazoviaAsUnicode
{
	public static readonly short[] Content = {
		0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x0105, 0x00E7,
		0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x0107, 0x00C4, 0x0104,
		0x0118, 0x0119, 0x0142, 0x00F4, 0x00F6, 0x0106, 0x00FB, 0x00F9,
		0x015A, 0x00D6, 0x00DC, 0x00A2, 0x0141, 0x00A5, 0x015B, 0x0192,
		0x0179, 0x017B, 0x00F3, 0x00D3, 0x0144, 0x0143, 0x017A, 0x017C,
		0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB,
		0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556,
		0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510,
		0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F,
		0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567,
		0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B,
		0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580,
		0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4,
		0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229,
		0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248,
		0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0};
}

Pole statyczne musi być opatrzone klauzulą readonly, ponieważ tego wymaga od pól statycznych SQL Serwer (inaczej zestawu nie dałoby się zarejestrować w bazie danych).

Pozostaje przedstawić klasę najistotniejszą z punktu widzenia inspirującego wpisu, czyli klasę odpowiedzialną za dekodowanie Mazovii. W tym przypadku dziedziczy ona po abstrakcyjnej klasie Decoder, którą zaprojektowano do realizacji transformacji z dowolnej strony kodowej do Unicode. Także w przypadku tej klasy konieczne będzie zaimplementowanie dwóch metod: ustalającej długość ciągu po odkodowaniu GetCharCount oraz odpowiedzialnej za samo dekodowanie – metody GetChars.

public sealed class MazoviaDecoder : Decoder
{
	private char[] Translator = new char[256];

	internal MazoviaDecoder()
	{
		for (byte i = 0x00; i < 0x80; i++)
		{
			char c = (char)i;
			Translator[i] = c; // znaki poniżej 128 to standardowe kody ASCII
		}
		for (byte i = 0x00; i < 0x80; i++)
		{
			char c = (char)MazoviaAsUnicode.Content[i];
			Translator[i + 0x80] = c; // znaki powyżej 127 to kody zgodne z Mazovią - trzeba użyć słownika translacji
		}
	}

	public override int GetCharCount(byte[] bytes, int index, int count)
	{
		// Mazovia jest jednobajtową stroną kodową, więc ilość znaków dla podanej ilości bajtów "count",
		// jest równa tej właśnie ilości (jeden bajt Mazovii równa się dwubajtowemu znakowi)
		return count;
	}

	public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
	{
		for (int i = 0; i < byteCount; i++)
			chars[charIndex + i] = Translator[bytes[byteIndex + i]];
		return byteCount;
	}

}

Jak już wspomniałem – także klasa dekodująca używa klasy pomocniczej MazoviaAsUnicode, zawierającej dwukierunkowy słownik translacji pomiędzy oboma systemami kodowania znaków.

Na koniec pozostaje przedstawić klasę dziedziczącą po Encoding i realizującą translację z jednego systemu na drugi i na odwrót. Właściwie korzysta ona wyłącznie z zaimplementowanych powyżej klas delegując do nich stosowne działania i będą de facto ich fabryką.

public class Mazovia : System.Text.Encoding
{
	public static MazoviaEncoder GetMazoviaEncoder()
	{
		return new MazoviaEncoder();
	}

	public static MazoviaDecoder GetMazoviaDecoder()
	{
		return new MazoviaDecoder();
	}

	public override int GetByteCount(char[] chars, int index, int count)
	{
		return GetMazoviaEncoder().GetByteCount(chars, index, count, false);
	}

	public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
	{
		return GetMazoviaEncoder().GetBytes(chars, charIndex, charCount, bytes, byteIndex, false);
	}

	public override int GetCharCount(byte[] bytes, int index, int count)
	{
		return GetMazoviaDecoder().GetCharCount(bytes, index, count);
	}

	public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
	{
		return GetMazoviaDecoder().GetChars(bytes, byteIndex, byteCount, chars, charIndex);
	}

	public override int GetMaxByteCount(int charCount)
	{
		return charCount;
	}

	public override int GetMaxCharCount(int byteCount)
	{
		return byteCount;
	}
}

Uzbrojeni w powyższy kod, możemy rozpocząć realizację właściwej funkcji dekodującej:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

namespace Mazovia
{
	public class External
	{
		[Microsoft.SqlServer.Server.SqlFunction()]
		public static SqlString AsMazovia(SqlString text)
		{
			if (text.IsNull)
				return SqlString.Null;
			byte[] b = text.GetNonUnicodeBytes();
			char[] c = new char[b.Length];
			Mazovia Mazovia = new Mazovia();
			Mazovia.GetChars(b, 0, b.Length, c, 0);
			StringBuilder sb = new StringBuilder(b.Length);
			foreach (var i in c)
				sb.Append(i);
			SqlString result = new SqlString(sb.ToString());
			return result.Value;
		}
	}
}

Warto zwrócić uwagę na użycie metody GetNonUnicodeBytes, która zwraca tekst w postaci ANSI, a więc jednobajtowych znaków (Unicode to dwubatjowe znaki). Dzięki temu nie jest konieczna gimnastyka ze zmianą treści słownika translacji, jaką musiał zastosować Paweł (dekodował de facto nie z Mazovii, ale jej postaci przekodowanej do Unicode).

Do utworzenia projektu należy wybrać szablon biblioteki klas, jako framework docelowy ustalić wersję 3.5 (wyższej SQL Serwer nie zaakceptuje). Po kompilacji należy zapamiętać folder, w którym została umieszczona biblioteka DLL – będzie go trzeba podać w następnym fragmencie kodu.

Ostatnie czynności to zarejestrowanie zestawu .NET oraz jego elementów w SQL Serwer (UWAGA! testując u siebie należy zmienić poniższy folder na ten zapamiętany, o co prosiłem przed chwilą).

-- koniecznie - umożliwienie używania oprogramowania napisanego w kodzie zarządzanym
exec sp_configure 'clr enabled', 1;
reconfigure with override;
GO-- rejestracja zestawu .NET (koniecznie zmienić folder na swój)
create assembly Mazovia from 'C:\Users\PaSkol\Documents\Visual Studio 2010\Projects\Mazovia\Mazovia\bin\Debug\Mazovia.dll' with permission_set = safe;
GO
-- rejestracja funkcji skalarnej SQL jako metody wybranej klasy zarejestrowanego wcześniej zestawu
create function AsMazovia(@content nvarchar(max)) returns nvarchar(max) AS EXTERNAL NAME Mazovia.[Mazovia.External].AsMazovia;
GO

Wyposażeni we właśnie zainstalowane narzędzia możemy zacząć je wykorzystywać. Do celów testowych proponuję pobrać stosowny plik tekstowy zawierający dość nietypowy wierszyk z książki „Alicja po drugiej stronie lustra” oraz poprzedzające go dwie kontrolne linie i wykonać poniższy kod (oczywiście plik należy albo umieścić w folderze publicznym C:\Users\Public, albo w takim, do którego ma dostęp SQL Server – wówczas trzeba będzie zmienić ścieżkę w poniższym kodzie).

if object_id('tempdb..#Mazovia') is not null
	drop table #Mazovia;
create table #Mazovia (tekst varchar(100));
bulk insert #Mazovia 
from 'C:\Users\Public\mazovia.txt'
with
(
  CodePage = 'RAW',
  FieldTerminator = '|',
  RowTerminator = '\n'
);
select dbo.AsMazovia(tekst), tekst from #Mazovia;

W powyższych rozważaniach podałem ręczny sposób rejestracji zestawu .NET w serwerze SQL. Można to także zrobić za pomocą VS, ale nie każdej wersji, więc to rozwiązanie – choć wymaga więcej pracy – zadziała zawsze.