7e52fc23324485cc7cfb583151e7901b6952842f
[marc4j.git] / src / org / marc4j / MarcXmlWriter.java
1 //$Id: MarcXmlWriter.java,v 1.9 2008/10/17 19:11:49 haschart Exp $\r
2 /**\r
3  * Copyright (C) 2004 Bas Peters\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;\r
22 \r
23 import java.io.BufferedWriter;\r
24 import java.io.IOException;\r
25 import java.io.OutputStream;\r
26 import java.io.OutputStreamWriter;\r
27 import java.io.UnsupportedEncodingException;\r
28 import java.io.Writer;\r
29 import java.util.Iterator;\r
30 \r
31 import javax.xml.transform.OutputKeys;\r
32 import javax.xml.transform.Result;\r
33 import javax.xml.transform.Source;\r
34 import javax.xml.transform.TransformerFactory;\r
35 import javax.xml.transform.sax.SAXTransformerFactory;\r
36 import javax.xml.transform.sax.TransformerHandler;\r
37 import javax.xml.transform.stream.StreamResult;\r
38 import javax.xml.transform.stream.StreamSource;\r
39 \r
40 import org.marc4j.converter.CharConverter;\r
41 import org.marc4j.marc.ControlField;\r
42 import org.marc4j.marc.DataField;\r
43 import org.marc4j.marc.Leader;\r
44 import org.marc4j.marc.Record;\r
45 import org.marc4j.marc.Subfield;\r
46 import org.xml.sax.SAXException;\r
47 import org.xml.sax.helpers.AttributesImpl;\r
48 \r
49 import com.ibm.icu.text.Normalizer;\r
50 \r
51 /**\r
52  * Class for writing MARC record objects in MARCXML format. This class outputs a\r
53  * SAX event stream to the given {@link java.io.OutputStream}  or\r
54  * {@link javax.xml.transform.Result} object. It can be used in a SAX\r
55  * pipeline to postprocess the result. By default this class uses a nulll\r
56  * transform. It is strongly recommended to use a dedicated XML serializer.\r
57  * \r
58  * <p>\r
59  * This class requires a JAXP compliant XML parser and XSLT processor. The\r
60  * underlying SAX2 parser should be namespace aware. In addition this class\r
61  * requires <a href="http://icu.sourceforge.net/">ICU4J </a> to perform Unicode\r
62  * normalization. A stripped down version of 2.6 originating from the <a\r
63  * href="http://www.cafeconleche.org/XOM/">XOM </a> project is included in this\r
64  * distribution.\r
65  * </p>\r
66  * <p>\r
67  * The following example reads a file with MARC records and writes MARCXML\r
68  * records in UTF-8 encoding to the console:\r
69  * </p>\r
70  * \r
71  * <pre>\r
72  *  \r
73  *      InputStream input = new FileInputStream(&quot;input.mrc&quot;)\r
74  *      MarcReader reader = new MarcStreamReader(input);\r
75  *              \r
76  *      MarcWriter writer = new MarcXmlWriter(System.out, true);\r
77  *      while (reader.hasNext()) {\r
78  *          Record record = reader.next();\r
79  *          writer.write(record);\r
80  *      }\r
81  *      writer.close();\r
82  *   \r
83  * </pre>\r
84  * \r
85  * <p>\r
86  * To perform a character conversion like MARC-8 to UCS/Unicode register a\r
87  * <code>CharConverter</code>:\r
88  * </p>\r
89  * \r
90  * <pre>\r
91  * writer.setConverter(new AnselToUnicode());\r
92  * </pre>\r
93  * \r
94  * <p>\r
95  * In addition you can perform Unicode normalization. This is for example not\r
96  * done by the MARC-8 to UCS/Unicode converter. With Unicode normalization text\r
97  * is transformed into the canonical composed form. For example &quot;a�bc&quot;\r
98  * is normalized to &quot;�bc&quot;. To perform normalization set Unicode\r
99  * normalization to true:\r
100  * </p>\r
101  * \r
102  * <pre>\r
103  * writer.setUnicodeNormalization(true);\r
104  * </pre>\r
105  * \r
106  * <p>\r
107  * Please note that it's not garanteed to work if you try to convert normalized\r
108  * Unicode back to MARC-8 encoding using\r
109  * {@link org.marc4j.converter.impl.UnicodeToAnsel}.\r
110  * </p>\r
111  * <p>\r
112  * This class provides very basic formatting options. For more advanced options\r
113  * create an instance of this class with a\r
114  * {@link javax.xml.transform.sax.SAXResult}&nbsp;containing a\r
115  * {@link org.xml.sax.ContentHandler}&nbsp;derived from a dedicated XML\r
116  * serializer.\r
117  * </p>\r
118  * \r
119  * <p>\r
120  * The following example uses\r
121  * <code>org.apache.xml.serialize.XMLSerializer</code> to write MARC records\r
122  * to XML using MARC-8 to UCS/Unicode conversion and Unicode normalization:\r
123  * </p>\r
124  * \r
125  * <pre>\r
126  *  \r
127  *      InputStream input = new FileInputStream(&quot;input.mrc&quot;)\r
128  *      MarcReader reader = new MarcStreamReader(input);\r
129  *                \r
130  *      OutputFormat format = new OutputFormat(&quot;xml&quot;,&quot;UTF-8&quot;, true);\r
131  *      OutputStream out = new FileOutputStream(&quot;output.xml&quot;);\r
132  *      XMLSerializer serializer = new XMLSerializer(out, format);\r
133  *      Result result = new SAXResult(serializer.asContentHandler());\r
134  *                \r
135  *      MarcXmlWriter writer = new MarcXmlWriter(result);\r
136  *      writer.setConverter(new AnselToUnicode());\r
137  *      while (reader.hasNext()) {\r
138  *          Record record = reader.next();\r
139  *          writer.write(record);\r
140  *      }\r
141  *      writer.close();\r
142  *   \r
143  * </pre>\r
144  * \r
145  * <p>\r
146  * You can post-process the result using a <code>Source</code> object pointing\r
147  * to a stylesheet resource and a <code>Result</code> object to hold the\r
148  * transformation result tree. The example below converts MARC to MARCXML and\r
149  * transforms the result tree to MODS using the stylesheet provided by The\r
150  * Library of Congress:\r
151  * </p>\r
152  * \r
153  * <pre>\r
154  *  \r
155  *      String stylesheetUrl = &quot;http://www.loc.gov/standards/mods/v3/MARC21slim2MODS3.xsl&quot;;\r
156  *      Source stylesheet = new StreamSource(stylesheetUrl);\r
157  *         \r
158  *      Result result = new StreamResult(System.out);\r
159  *            \r
160  *      InputStream input = new FileInputStream(&quot;input.mrc&quot;)\r
161  *      MarcReader reader = new MarcStreamReader(input);\r
162  *      MarcXmlWriter writer = new MarcXmlWriter(result, stylesheet);\r
163  *      writer.setConverter(new AnselToUnicode());\r
164  *      while (reader.hasNext()) {\r
165  *          Record record = (Record) reader.next();\r
166  *          writer.write(record);\r
167  *      }\r
168  *      writer.close();\r
169  *   \r
170  * </pre>\r
171  * \r
172  * <p>\r
173  * It is also possible to write the result into a DOM Node:\r
174  * </p>\r
175  * \r
176  * <pre>\r
177  *  \r
178  *      InputStream input = new FileInputStream(&quot;input.mrc&quot;)\r
179  *      MarcReader reader = new MarcStreamReader(input);\r
180  *      DOMResult result = new DOMResult();\r
181  *      MarcXmlWriter writer = new MarcXmlWriter(result);\r
182  *      writer.setConverter(new AnselToUnicode());\r
183  *      while (reader.hasNext()) {\r
184  *          Record record = (Record) reader.next();\r
185  *          writer.write(record);\r
186  *      }\r
187  *      writer.close();\r
188  *         \r
189  *      Document doc = (Document) result.getNode();\r
190  *   \r
191  * </pre>\r
192  * \r
193  * @author Bas Peters\r
194  * @version $Revision: 1.9 $\r
195  * \r
196  */\r
197 public class MarcXmlWriter implements MarcWriter {\r
198 \r
199     protected static final String CONTROL_FIELD = "controlfield";\r
200 \r
201     protected static final String DATA_FIELD = "datafield";\r
202 \r
203     protected static final String SUBFIELD = "subfield";\r
204 \r
205     protected static final String COLLECTION = "collection";\r
206 \r
207     protected static final String RECORD = "record";\r
208 \r
209     protected static final String LEADER = "leader";\r
210 \r
211     private boolean indent = false;\r
212 \r
213     private TransformerHandler handler = null;\r
214 \r
215     private Writer writer = null;\r
216     \r
217     \r
218     /**\r
219      * Character encoding. Default is UTF-8.\r
220      */\r
221     //private String encoding = "UTF8";\r
222 \r
223     private CharConverter converter = null;\r
224 \r
225     private boolean normalize = false;\r
226 \r
227     /**\r
228      * Constructs an instance with the specified output stream.\r
229      * \r
230      * The default character encoding for UTF-8 is used.\r
231      *      \r
232      * @throws MarcException\r
233      */\r
234     public MarcXmlWriter(OutputStream out) {\r
235         this(out, false);\r
236     }\r
237 \r
238     /**\r
239      * Constructs an instance with the specified output stream and indentation.\r
240      * \r
241      * The default character encoding for UTF-8 is used.\r
242      * \r
243      * @throws MarcException\r
244      */\r
245     public MarcXmlWriter(OutputStream out, boolean indent) {\r
246         this(out, "UTF8", indent);\r
247     }\r
248 \r
249     /**\r
250      * Constructs an instance with the specified output stream and character\r
251      * encoding.\r
252      * \r
253      * @throws MarcException\r
254      */\r
255     public MarcXmlWriter(OutputStream out, String encoding) {\r
256         this(out, encoding, false);\r
257     }\r
258 \r
259     /**\r
260      * Constructs an instance with the specified output stream, character\r
261      * encoding and indentation.\r
262      * \r
263      * @throws MarcException\r
264      */\r
265     public MarcXmlWriter(OutputStream out, String encoding, boolean indent) {\r
266         if (out == null) {\r
267             throw new NullPointerException("null OutputStream");\r
268         }\r
269         if (encoding == null) {\r
270             throw new NullPointerException("null encoding");\r
271         }\r
272         try {\r
273             setIndent(indent);\r
274             writer = new OutputStreamWriter(out, encoding);\r
275             writer = new BufferedWriter(writer);\r
276             // this.encoding = encoding;\r
277             setHandler(new StreamResult(writer), null);\r
278         } catch (UnsupportedEncodingException e) {\r
279             throw new MarcException(e.getMessage(), e);\r
280         }\r
281         writeStartDocument();\r
282     }\r
283 \r
284     /**\r
285      * Constructs an instance with the specified result.\r
286      * \r
287      * @param result\r
288      * @throws SAXException\r
289      */\r
290     public MarcXmlWriter(Result result) {\r
291         if (result == null)\r
292             throw new NullPointerException("null Result");\r
293         setHandler(result, null);\r
294         writeStartDocument();\r
295     }\r
296 \r
297     /**\r
298      * Constructs an instance with the specified stylesheet location and result.\r
299      * \r
300      * @param result\r
301      * @throws SAXException\r
302      */\r
303     public MarcXmlWriter(Result result, String stylesheetUrl) {\r
304         this(result, new StreamSource(stylesheetUrl));\r
305     }\r
306 \r
307     /**\r
308      * Constructs an instance with the specified stylesheet source and result.\r
309      * \r
310      * @param result\r
311      * @throws SAXException\r
312      */\r
313     public MarcXmlWriter(Result result, Source stylesheet) {\r
314         if (stylesheet == null)\r
315             throw new NullPointerException("null Source");\r
316         if (result == null)\r
317             throw new NullPointerException("null Result");\r
318         setHandler(result, stylesheet);\r
319         writeStartDocument();\r
320     }\r
321 \r
322     public void close() {\r
323         writeEndDocument();\r
324         try {\r
325                 writer.close();\r
326         } catch (IOException e) {\r
327                 throw new MarcException(e.getMessage(), e);\r
328         }\r
329     }\r
330 \r
331     /**\r
332      * Returns the character converter.\r
333      * \r
334      * @return CharConverter the character converter\r
335      */\r
336     public CharConverter getConverter() {\r
337         return converter;\r
338     }\r
339 \r
340     /**\r
341      * Sets the character converter.\r
342      * \r
343      * @param converter\r
344      *            the character converter\r
345      */\r
346     public void setConverter(CharConverter converter) {\r
347         this.converter = converter;\r
348     }\r
349 \r
350     /**\r
351      * If set to true this writer will perform Unicode normalization on data\r
352      * elements using normalization form C (NFC). The default is false.\r
353      * \r
354      * The implementation used is ICU4J 2.6. This version is based on Unicode\r
355      * 4.0.\r
356      * \r
357      * @param normalize\r
358      *            true if this writer performs Unicode normalization, false\r
359      *            otherwise\r
360      */\r
361     public void setUnicodeNormalization(boolean normalize) {\r
362         this.normalize = normalize;\r
363     }\r
364 \r
365     /**\r
366      * Returns true if this writer will perform Unicode normalization, false\r
367      * otherwise.\r
368      * \r
369      * @return boolean - true if this writer performs Unicode normalization,\r
370      *         false otherwise.\r
371      */\r
372     public boolean getUnicodeNormalization() {\r
373         return normalize;\r
374     }\r
375 \r
376     protected void setHandler(Result result, Source stylesheet)\r
377             throws MarcException {\r
378         try {\r
379             TransformerFactory factory = TransformerFactory.newInstance();\r
380             if (!factory.getFeature(SAXTransformerFactory.FEATURE))\r
381                 throw new UnsupportedOperationException(\r
382                         "SAXTransformerFactory is not supported");\r
383 \r
384             SAXTransformerFactory saxFactory = (SAXTransformerFactory) factory;\r
385             if (stylesheet == null)\r
386                 handler = saxFactory.newTransformerHandler();\r
387             else\r
388                 handler = saxFactory.newTransformerHandler(stylesheet);\r
389             handler.getTransformer()\r
390                     .setOutputProperty(OutputKeys.METHOD, "xml");\r
391             handler.setResult(result);\r
392 \r
393         } catch (Exception e) {\r
394             throw new MarcException(e.getMessage(), e);\r
395         }\r
396     }\r
397 \r
398     /**\r
399      * Writes the root start tag to the result.\r
400      * \r
401      * @throws SAXException\r
402      */\r
403     protected void writeStartDocument() {\r
404         try {\r
405             AttributesImpl atts = new AttributesImpl();\r
406             handler.startDocument();\r
407             // The next line duplicates the namespace declaration for Marc XML\r
408             // handler.startPrefixMapping("", Constants.MARCXML_NS_URI);\r
409             // add namespace declaration using attribute - need better solution\r
410             atts.addAttribute(Constants.MARCXML_NS_URI, "xmlns", "xmlns",\r
411                               "CDATA", Constants.MARCXML_NS_URI);            \r
412             handler.startElement(Constants.MARCXML_NS_URI, COLLECTION, COLLECTION, atts);\r
413         } catch (SAXException e) {\r
414             throw new MarcException(\r
415                     "SAX error occured while writing start document", e);\r
416         }\r
417     }\r
418 \r
419     /**\r
420      * Writes the root end tag to the result.\r
421      * \r
422      * @throws SAXException\r
423      */\r
424     protected void writeEndDocument() {\r
425         try {\r
426             if (indent)\r
427                 handler.ignorableWhitespace("\n".toCharArray(), 0, 1);\r
428 \r
429             handler\r
430                     .endElement(Constants.MARCXML_NS_URI, COLLECTION,\r
431                             COLLECTION);\r
432             handler.endPrefixMapping("");\r
433             handler.endDocument();\r
434         } catch (SAXException e) {\r
435             throw new MarcException(\r
436                     "SAX error occured while writing end document", e);\r
437         }\r
438     }\r
439 \r
440     /**\r
441      * Writes a Record object to the result.\r
442      * \r
443      * @param record -\r
444      *            the <code>Record</code> object\r
445      * @throws SAXException\r
446      */\r
447     public void write(Record record) {\r
448         try {\r
449             toXml(record);\r
450         } catch (SAXException e) {\r
451             throw new MarcException("SAX error occured while writing record", e);\r
452         }\r
453     }\r
454 \r
455     /**\r
456      * Returns true if indentation is active, false otherwise.\r
457      * \r
458      * @return boolean\r
459      */\r
460     public boolean hasIndent() {\r
461         return indent;\r
462     }\r
463 \r
464     /**\r
465      * Activates or deactivates indentation. Default value is false.\r
466      * \r
467      * @param indent\r
468      */\r
469     public void setIndent(boolean indent) {\r
470         this.indent = indent;\r
471     }\r
472 \r
473     protected void toXml(Record record) throws SAXException {\r
474         char temp[];\r
475         AttributesImpl atts = new AttributesImpl();\r
476         if (indent)\r
477             handler.ignorableWhitespace("\n  ".toCharArray(), 0, 3);\r
478 \r
479         handler.startElement(Constants.MARCXML_NS_URI, RECORD, RECORD, atts);\r
480 \r
481         if (indent)\r
482             handler.ignorableWhitespace("\n    ".toCharArray(), 0, 5);\r
483 \r
484         handler.startElement(Constants.MARCXML_NS_URI, LEADER, LEADER, atts);\r
485         Leader leader = record.getLeader();\r
486         temp = leader.toString().toCharArray();\r
487         handler.characters(temp, 0, temp.length);\r
488         handler.endElement(Constants.MARCXML_NS_URI, LEADER, LEADER);\r
489 \r
490         Iterator<ControlField> ci = record.getControlFields().iterator();\r
491         while (ci.hasNext()) {\r
492             ControlField field = (ControlField) ci.next();\r
493             atts = new AttributesImpl();\r
494             atts.addAttribute("", "tag", "tag", "CDATA", field.getTag());\r
495 \r
496             if (indent)\r
497                 handler.ignorableWhitespace("\n    ".toCharArray(), 0, 5);\r
498 \r
499             handler.startElement(Constants.MARCXML_NS_URI, CONTROL_FIELD,\r
500                     CONTROL_FIELD, atts);\r
501             temp = getDataElement(field.getData());\r
502             handler.characters(temp, 0, temp.length);\r
503             handler.endElement(Constants.MARCXML_NS_URI, CONTROL_FIELD,\r
504                     CONTROL_FIELD);\r
505         }\r
506 \r
507         Iterator<DataField> di = record.getDataFields().iterator();\r
508         while (di.hasNext()) {\r
509             DataField field = di.next();\r
510             atts = new AttributesImpl();\r
511             atts.addAttribute("", "tag", "tag", "CDATA", field.getTag());\r
512             atts.addAttribute("", "ind1", "ind1", "CDATA", String.valueOf(field\r
513                     .getIndicator1()));\r
514             atts.addAttribute("", "ind2", "ind2", "CDATA", String.valueOf(field\r
515                     .getIndicator2()));\r
516 \r
517             if (indent)\r
518                 handler.ignorableWhitespace("\n    ".toCharArray(), 0, 5);\r
519 \r
520             handler.startElement(Constants.MARCXML_NS_URI, DATA_FIELD,\r
521                     DATA_FIELD, atts);\r
522             Iterator<Subfield> si = field.getSubfields().iterator();\r
523             while (si.hasNext()) {\r
524                 Subfield subfield = (Subfield) si.next();\r
525                 atts = new AttributesImpl();\r
526                 atts.addAttribute("", "code", "code", "CDATA", String\r
527                         .valueOf(subfield.getCode()));\r
528 \r
529                 if (indent)\r
530                     handler.ignorableWhitespace("\n      ".toCharArray(), 0, 7);\r
531 \r
532                 handler.startElement(Constants.MARCXML_NS_URI, SUBFIELD,\r
533                         SUBFIELD, atts);\r
534                 temp = getDataElement(subfield.getData());\r
535                 handler.characters(temp, 0, temp.length);\r
536                 handler\r
537                         .endElement(Constants.MARCXML_NS_URI, SUBFIELD,\r
538                                 SUBFIELD);\r
539             }\r
540 \r
541             if (indent)\r
542                 handler.ignorableWhitespace("\n    ".toCharArray(), 0, 5);\r
543 \r
544             handler\r
545                     .endElement(Constants.MARCXML_NS_URI, DATA_FIELD,\r
546                             DATA_FIELD);\r
547         }\r
548 \r
549         if (indent)\r
550             handler.ignorableWhitespace("\n  ".toCharArray(), 0, 3);\r
551 \r
552         handler.endElement(Constants.MARCXML_NS_URI, RECORD, RECORD);\r
553     }\r
554 \r
555     protected char[] getDataElement(String data) {\r
556         String dataElement = null;\r
557         if (converter == null)\r
558             return data.toCharArray();\r
559         dataElement = converter.convert(data);\r
560         if (normalize)\r
561             dataElement = Normalizer.normalize(dataElement, Normalizer.NFC);\r
562         return dataElement.toCharArray();\r
563     }\r
564 }