Drogie Panie i Panowie, oto sposób na Mazowię.
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.