Original 2.4
[marc4j.git] / src / org / marc4j / converter / impl / AnselToUnicode.java
1 // $Id: AnselToUnicode.java,v 1.5 2008/10/17 06:47:06 haschart Exp $\r
2 /**\r
3  * Copyright (C) 2002 Bas Peters (mail@bpeters.com)\r
4  *\r
5  * This file is part of MARC4J\r
6  *\r
7  * MARC4J is free software; you can redistribute it and/or\r
8  * modify it under the terms of the GNU Lesser General Public\r
9  * License as published by the Free Software Foundation; either\r
10  * version 2.1 of the License, or (at your option) any later version.\r
11  *\r
12  * MARC4J is distributed in the hope that it will be useful,\r
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\r
15  * Lesser General Public License for more details.\r
16  *\r
17  * You should have received a copy of the GNU Lesser General Public\r
18  * License along with MARC4J; if not, write to the Free Software\r
19  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
20  */\r
21 package org.marc4j.converter.impl;\r
22 \r
23 import java.io.InputStream;\r
24 import java.lang.reflect.Constructor;\r
25 import java.util.Vector;\r
26 \r
27 import org.marc4j.ErrorHandler;\r
28 import org.marc4j.MarcException;\r
29 import org.marc4j.converter.CharConverter;\r
30 \r
31 /**\r
32  * <p>\r
33  * A utility to convert MARC-8 data to non-precomposed UCS/Unicode.\r
34  * </p>\r
35  * \r
36  * <p>\r
37  * The MARC-8 to Unicode mapping used is the version with the March 2005\r
38  * revisions.\r
39  * </p>\r
40  * \r
41  * @author Bas Peters\r
42  * @author Corey Keith\r
43  * @version $Revision: 1.5 $\r
44  */\r
45 public class AnselToUnicode extends CharConverter {\r
46 \r
47     class Queue extends Vector {\r
48 \r
49         /**\r
50          * Puts an item into the queue.\r
51          * \r
52          * @param item\r
53          *            the item to be put into the queue.\r
54          */\r
55         public Object put(Object item) {\r
56             addElement(item);\r
57 \r
58             return item;\r
59         }\r
60 \r
61         /**\r
62          * Gets an item from the front of the queue.\r
63          */\r
64         public Object get() {\r
65             Object obj;\r
66             int len = size();\r
67 \r
68             obj = peek();\r
69             removeElementAt(0);\r
70 \r
71             return obj;\r
72         }\r
73 \r
74         /**\r
75          * Peeks at the front of the queue.\r
76          */\r
77         public Object peek() {\r
78             int len = size();\r
79 \r
80             return elementAt(0);\r
81         }\r
82 \r
83         /**\r
84          * Returns true if the queue is empty.\r
85          */\r
86         public boolean empty() {\r
87             return size() == 0;\r
88         }\r
89     }\r
90 \r
91     class CodeTracker {\r
92         int offset;\r
93 \r
94         int g0;\r
95 \r
96         int g1;\r
97 \r
98         boolean multibyte;\r
99 \r
100         public String toString() {\r
101             return "Offset: " + offset + " G0: " + Integer.toHexString(g0)\r
102                     + " G1: " + Integer.toHexString(g1) + " Multibyte: "\r
103                     + multibyte;\r
104         }\r
105     }\r
106 \r
107     protected CodeTableInterface ct;\r
108 \r
109     protected boolean loadedMultibyte = false;\r
110 \r
111     protected ErrorHandler errorList = null;\r
112     /**\r
113      * Creates a new instance and loads the MARC4J supplied\r
114      * conversion tables based on the official LC tables.\r
115      *  \r
116      */\r
117     public AnselToUnicode() \r
118     {\r
119         ct = loadGeneratedTable(false);\r
120     }\r
121     \r
122     /**\r
123      * Creates a new instance and loads the MARC4J supplied\r
124      * conversion tables based on the official LC tables.\r
125      *  \r
126      */\r
127     public AnselToUnicode(boolean loadMultibyte) \r
128     {\r
129         ct = loadGeneratedTable(loadMultibyte);\r
130     }\r
131     /**\r
132      * Creates a new instance and loads the MARC4J supplied\r
133      * conversion tables based on the official LC tables.\r
134      *  \r
135      */\r
136     public AnselToUnicode(ErrorHandler errorList) \r
137     {\r
138         ct = loadGeneratedTable(false);\r
139         this.errorList = errorList;\r
140     }\r
141 \r
142     /**\r
143      * Creates a new instance and loads the MARC4J supplied\r
144      * conversion tables based on the official LC tables.\r
145      *  \r
146      */\r
147     public AnselToUnicode(ErrorHandler errorList, boolean loadMultibyte) \r
148     {\r
149         ct = loadGeneratedTable(loadMultibyte);\r
150         this.errorList = errorList;\r
151     }\r
152 \r
153 \r
154     private CodeTableInterface loadGeneratedTable(boolean loadMultibyte) \r
155     {\r
156         try\r
157         {\r
158             Class generated = Class.forName("org.marc4j.converter.impl.CodeTableGenerated");\r
159             Constructor cons = generated.getConstructor();\r
160             Object ct = cons.newInstance();\r
161             loadedMultibyte = true;\r
162             return((CodeTableInterface)ct);\r
163         }\r
164         catch (Exception e)\r
165         {\r
166             CodeTableInterface ct;\r
167             if (loadMultibyte)\r
168             {\r
169                 ct = new CodeTable(AnselToUnicode.class.getResourceAsStream("resources/codetables.xml"));                \r
170             }\r
171             else\r
172             {\r
173                 ct = new CodeTable(AnselToUnicode.class.getResourceAsStream("resources/codetablesnocjk.xml"));\r
174             }\r
175             loadedMultibyte = loadMultibyte;\r
176             return(ct);\r
177          }\r
178 \r
179     }\r
180     \r
181     /**\r
182      * Constructs an instance with the specified pathname.\r
183      * \r
184      * Use this constructor to create an instance with a customized code table\r
185      * mapping. The mapping file should follow the structure of LC's XML MARC-8\r
186      * to Unicode mapping (see:\r
187      * http://www.loc.gov/marc/specifications/codetables.xml).\r
188      *  \r
189      */\r
190     public AnselToUnicode(String pathname) {\r
191         ct = new CodeTable(pathname);\r
192         loadedMultibyte = true;\r
193     }\r
194 \r
195     /**\r
196      * Constructs an instance with the specified input stream.\r
197      * \r
198      * Use this constructor to create an instance with a customized code table\r
199      * mapping. The mapping file should follow the structure of LC's XML MARC-8\r
200      * to Unicode mapping (see:\r
201      * http://www.loc.gov/marc/specifications/codetables.xml).\r
202      *  \r
203      */\r
204     public AnselToUnicode(InputStream in) {\r
205         ct = new CodeTable(in);\r
206         loadedMultibyte = true;\r
207     }\r
208 \r
209     /**\r
210      * Loads the entire mapping (including multibyte characters) from the Library\r
211      * of Congress.\r
212      */\r
213     private void loadMultibyte() {\r
214         ct = new CodeTable(getClass().getResourceAsStream(\r
215                 "resources/codetables.xml"));\r
216     }\r
217 \r
218     private void checkMode(char[] data, CodeTracker cdt) {\r
219         int extra = 0;\r
220         int extra2 = 0;\r
221         int extra3 = 0;\r
222         while (cdt.offset + extra + extra2< data.length && isEscape(data[cdt.offset])) {\r
223             switch (data[cdt.offset + 1 + extra]) {\r
224             case 0x28:  // '('\r
225             case 0x2c:  // ','\r
226                 set_cdt(cdt, 0, data, 2 + extra, false); \r
227                 break;\r
228             case 0x29:  // ')'\r
229             case 0x2d:  // '-'\r
230                 set_cdt(cdt, 1, data, 2 + extra, false); \r
231                 break;\r
232             case 0x24:  // '$'\r
233                 if (!loadedMultibyte) {\r
234                     loadMultibyte();\r
235                     loadedMultibyte = true;\r
236                 }\r
237                 switch (data[cdt.offset + 2 + extra + extra2]) {\r
238                 case 0x29:  // ')'\r
239                 case 0x2d:  // '-'\r
240                     set_cdt(cdt, 1, data, 3 + extra + extra2, true); \r
241                     break;\r
242                 case 0x2c:  // ','\r
243                     set_cdt(cdt, 0, data, 3 + extra + extra2, true); \r
244                     break;\r
245                 case 0x31:  // '1'\r
246                     cdt.g0 = data[cdt.offset + 2 + extra + extra2];\r
247                     cdt.offset += 3 + extra + extra2;\r
248                     cdt.multibyte = true;\r
249                     break;\r
250                 case 0x20:  // ' ' \r
251                     // space found in escape code: look ahead and try to proceed\r
252                     extra2++;\r
253                     break;\r
254                 default: \r
255                     // unknown code character found: discard escape sequence and return\r
256                     cdt.offset += 1;\r
257                     if (errorList != null)\r
258                     {\r
259                         errorList.addError(ErrorHandler.MINOR_ERROR, "Unknown character set code found following escape character. Discarding escape character.");\r
260                     }\r
261                     else\r
262                     {\r
263                         throw new MarcException("Unknown character set code found following escape character.");\r
264                     }\r
265                     break;\r
266                 }\r
267                 break;\r
268             case 0x67:  // 'g'\r
269             case 0x62:  // 'b'\r
270             case 0x70:  // 'p'\r
271                 cdt.g0 = data[cdt.offset + 1 + extra];\r
272                 cdt.offset += 2 + extra;\r
273                 cdt.multibyte = false;\r
274                 break;\r
275             case 0x73:  // 's'\r
276                 cdt.g0 = 0x42;\r
277                 cdt.offset += 2 + extra;\r
278                 cdt.multibyte = false;\r
279                 break;\r
280             case 0x20:  // ' ' \r
281                 // space found in escape code: look ahead and try to proceed\r
282                 if (errorList == null)\r
283                 {\r
284                     throw new MarcException("Extraneous space character found within MARC8 character set escape sequence");\r
285                 }\r
286                 extra++;\r
287                 break;\r
288             default: \r
289                 // unknown code character found: discard escape sequence and return\r
290                 cdt.offset += 1;\r
291                 if (errorList != null)\r
292                 {\r
293                     errorList.addError(ErrorHandler.MINOR_ERROR, "Unknown character set code found following escape character. Discarding escape character.");\r
294                 }\r
295                 else\r
296                 {\r
297                     throw new MarcException("Unknown character set code found following escape character.");\r
298                 }\r
299                 break;\r
300             }\r
301         }\r
302         if (errorList != null && ( extra != 0 || extra2 != 0))\r
303         {\r
304             errorList.addError(ErrorHandler.ERROR_TYPO, "" + (extra+extra2) + " extraneous space characters found within MARC8 character set escape sequence");\r
305         }\r
306     }\r
307 \r
308     private void set_cdt(CodeTracker cdt, int g0_or_g1, char[] data, int addnlOffset, boolean multibyte)\r
309     {\r
310         if (data[cdt.offset + addnlOffset] == '!' && data[cdt.offset + addnlOffset + 1] == 'E') \r
311         {\r
312             addnlOffset++;\r
313         }\r
314         else if (data[cdt.offset + addnlOffset] == ' ') \r
315         {\r
316             if (errorList != null)\r
317             {\r
318                 errorList.addError(ErrorHandler.ERROR_TYPO, "Extraneous space character found within MARC8 character set escape sequence. Skipping over space.");\r
319             }           \r
320             else\r
321             {\r
322                 throw new MarcException("Extraneous space character found within MARC8 character set escape sequence");\r
323             }\r
324             addnlOffset++;\r
325         }\r
326         else if ("(,)-$!".indexOf(data[cdt.offset + addnlOffset]) != -1) \r
327         {\r
328             if (errorList != null)\r
329             {\r
330                 errorList.addError(ErrorHandler.MINOR_ERROR, "Extraneaous intermediate character found following escape character. Discarding intermediate character.");\r
331             }           \r
332             else\r
333             {\r
334                 throw new MarcException("Extraneaous intermediate character found following escape character.");\r
335             }\r
336             addnlOffset++;\r
337         }\r
338         if ("34BE1NQS2".indexOf(data[cdt.offset + addnlOffset]) == -1)\r
339         {\r
340             cdt.offset += 1;\r
341             cdt.multibyte = false;\r
342             if (errorList != null)\r
343             {\r
344                 errorList.addError(ErrorHandler.MINOR_ERROR, "Unknown character set code found following escape character. Discarding escape character.");\r
345             }           \r
346             else\r
347             {\r
348                 throw new MarcException("Unknown character set code found following escape character.");\r
349             }\r
350         }\r
351         else  // All is well, proceed normally\r
352         {\r
353             if (g0_or_g1 == 0) cdt.g0 = data[cdt.offset + addnlOffset];\r
354             else               cdt.g1 = data[cdt.offset + addnlOffset];\r
355             cdt.offset += 1 + addnlOffset;\r
356             cdt.multibyte = multibyte;\r
357         }\r
358     }\r
359     /**\r
360      * <p>\r
361      * Converts MARC-8 data to UCS/Unicode.\r
362      * </p>\r
363      * \r
364      * @param data -  the MARC-8 data in an array of char\r
365      * @return String - the UCS/Unicode data\r
366      */\r
367     public String convert(char  data[]) \r
368     {\r
369         StringBuffer sb = new StringBuffer();\r
370         int len = data.length;\r
371 \r
372         CodeTracker cdt = new CodeTracker();\r
373 \r
374         cdt.g0 = 0x42;\r
375         cdt.g1 = 0x45;\r
376         cdt.multibyte = false;\r
377 \r
378         cdt.offset = 0;\r
379 \r
380         checkMode(data, cdt);\r
381 \r
382         Queue diacritics = new Queue();\r
383 \r
384         while (cdt.offset < data.length) \r
385         {\r
386             if (ct.isCombining(data[cdt.offset], cdt.g0, cdt.g1)\r
387                     && hasNext(cdt.offset, len)) \r
388             {\r
389 \r
390                 while (ct.isCombining(data[cdt.offset], cdt.g0, cdt.g1)\r
391                         && hasNext(cdt.offset, len)) \r
392                 {\r
393                     char c = getChar(data[cdt.offset], cdt.g0, cdt.g1);\r
394                     if (c != 0) diacritics.put(new Character(c));\r
395                     cdt.offset++;\r
396                     checkMode(data, cdt);\r
397                 }\r
398 \r
399                 char c2 = getChar(data[cdt.offset], cdt.g0, cdt.g1);\r
400                 cdt.offset++;\r
401                 checkMode(data, cdt);\r
402                 if (c2 != 0) sb.append(c2);\r
403 \r
404                 while (!diacritics.isEmpty()) \r
405                 {\r
406                     char c1 = ((Character) diacritics.get()).charValue();\r
407                     sb.append(c1);\r
408                 }\r
409 \r
410             } \r
411             else if (cdt.multibyte)\r
412             {\r
413                 if (data[cdt.offset]== 0x20)\r
414                 {\r
415                     // if a 0x20 byte occurs amidst a sequence of multibyte characters\r
416                     // skip over it and output a space.\r
417                     // Hmmm.  If the following line is present it seems to output two spaces \r
418                     // when a space occurs in multibytes chars, without it one seems to be output.\r
419                     //    sb.append(getChar(data[cdt.offset], cdt.g0, cdt.g1));\r
420                     cdt.offset += 1;\r
421                 }\r
422                 else if (cdt.offset + 3 <= data.length && (errorList == null || data[cdt.offset+1]!= 0x20 && data[cdt.offset+2]!= 0x20)) \r
423                 {\r
424                     char c = getMBChar(makeMultibyte(data[cdt.offset], data[cdt.offset+1], data[cdt.offset+2]));\r
425                     if (errorList == null  || c != 0)\r
426                     { \r
427                         sb.append(c);\r
428                         cdt.offset += 3;\r
429                     }\r
430                     else if (cdt.offset + 6 <= data.length && data[cdt.offset+4]!= 0x20 && data[cdt.offset+5]!= 0x20 &&\r
431                             getMBChar(makeMultibyte(data[cdt.offset+3], data[cdt.offset+4], data[cdt.offset+5])) != 0)\r
432                     {\r
433                         if (errorList != null)\r
434                         {\r
435                             errorList.addError(ErrorHandler.MINOR_ERROR, "Erroneous MARC8 multibyte character, Discarding bad character and continuing reading Multibyte characters");\r
436                             sb.append("[?]");\r
437                             cdt.offset += 3;\r
438                         }\r
439                     }\r
440                     else if (cdt.offset + 4 <= data.length && data[cdt.offset] > 0x7f && \r
441                             getMBChar(makeMultibyte(data[cdt.offset+1], data[cdt.offset+2], data[cdt.offset+3])) != 0)\r
442                     {\r
443                         if (errorList != null)\r
444                         {\r
445                             errorList.addError(ErrorHandler.MINOR_ERROR, "Erroneous character in MARC8 multibyte character, Copying bad character and continuing reading Multibyte characters");\r
446                             sb.append(getChar(data[cdt.offset], 0x42, 0x45));\r
447                             cdt.offset += 1;\r
448                         }\r
449                     }\r
450                     else\r
451                     {\r
452                         if (errorList != null)\r
453                         {\r
454                             errorList.addError(ErrorHandler.MINOR_ERROR, "Erroneous MARC8 multibyte character, inserting change to default character set");\r
455                         }\r
456                         cdt.multibyte = false;\r
457                         cdt.g0 = 0x42;\r
458                         cdt.g1 = 0x45;\r
459                     }\r
460                 } \r
461                 else if (errorList != null && cdt.offset + 4 <= data.length && ( data[cdt.offset+1] == 0x20 || data[cdt.offset+2]== 0x20)) \r
462                 {\r
463                     int multiByte = makeMultibyte( data[cdt.offset], ((data[cdt.offset+1] != 0x20)? data[cdt.offset+1] : data[cdt.offset+2]),  data[cdt.offset+3]);\r
464                     char c = getMBChar(multiByte);\r
465                     if (c != 0) \r
466                     {\r
467                         if (errorList != null)\r
468                         {\r
469                             errorList.addError(ErrorHandler.ERROR_TYPO, "Extraneous space found within MARC8 multibyte character");\r
470                         }\r
471                         sb.append(c);\r
472                         sb.append(' ');\r
473                         cdt.offset += 4;\r
474                     }\r
475                     else\r
476                     {\r
477                         if (errorList != null)\r
478                         {\r
479                             errorList.addError(ErrorHandler.MINOR_ERROR, "Erroneous MARC8 multibyte character, inserting change to default character set");\r
480                         }\r
481                         cdt.multibyte = false;\r
482                         cdt.g0 = 0x42;\r
483                         cdt.g1 = 0x45;\r
484                     }\r
485                 } \r
486                 else if (cdt.offset + 3 > data.length) \r
487                 {\r
488                     if (errorList != null)\r
489                     {\r
490                         errorList.addError(ErrorHandler.MINOR_ERROR, "Partial MARC8 multibyte character, inserting change to default character set");\r
491                         cdt.multibyte = false;\r
492                         cdt.g0 = 0x42;\r
493                         cdt.g1 = 0x45;\r
494                     }\r
495                     // if a field ends with an incomplete encoding of a multibyte character\r
496                     // simply discard that final partial character.\r
497                     else \r
498                     {\r
499                         cdt.offset += 3;\r
500                     }\r
501                 } \r
502             }\r
503             else \r
504             {\r
505                 char c = getChar(data[cdt.offset], cdt.g0, cdt.g1);\r
506                 if (c != 0) sb.append(c);\r
507                 else \r
508                 {\r
509                     String val = "0000"+Integer.toHexString((int)(data[cdt.offset]));\r
510                     sb.append("<U+"+ (val.substring(val.length()-4, val.length()))+ ">" );\r
511                 }\r
512                 cdt.offset += 1;\r
513             }\r
514             if (hasNext(cdt.offset, len))\r
515             {\r
516                 checkMode(data, cdt);\r
517             }\r
518         }\r
519         return sb.toString();\r
520     }\r
521 \r
522     private int makeMultibyte(char[] data) {\r
523         int[] chars = new int[3];\r
524         chars[0] = data[0] << 16;\r
525         chars[1] = data[1] << 8;\r
526         chars[2] = data[2];\r
527         return chars[0] | chars[1] | chars[2];\r
528     }\r
529     \r
530     public int makeMultibyte(char c1, char c2, char c3) \r
531     {\r
532         int[] chars = new int[3];\r
533         chars[0] = c1 << 16;\r
534         chars[1] = c2 << 8;\r
535         chars[2] = c3;\r
536         return chars[0] | chars[1] | chars[2];\r
537     }\r
538 \r
539     private char getChar(int ch, int g0, int g1) {\r
540         if (ch <= 0x7E)\r
541             return ct.getChar(ch, g0);\r
542         else\r
543             return ct.getChar(ch, g1);\r
544     }\r
545 \r
546     public char getMBChar(int ch) {\r
547         return ct.getChar(ch, 0x31);\r
548     }\r
549 \r
550     private static boolean hasNext(int pos, int len) {\r
551         if (pos < (len - 1))\r
552             return true;\r
553         return false;\r
554     }\r
555 \r
556     private static boolean isEscape(int i) {\r
557         if (i == 0x1B)\r
558             return true;\r
559         return false;\r
560     }\r
561 \r
562 }