1// © 2016 and later: Unicode, Inc. and others.
2// License & terms of use: http://www.unicode.org/copyright.html#License
3/*
4 ******************************************************************************
5 * Copyright (C) 2003-2013, International Business Machines Corporation and   *
6 * others. All Rights Reserved.                                               *
7 ******************************************************************************
8 */
9
10package com.ibm.icu.dev.tool.localeconverter;
11
12import java.io.BufferedOutputStream;
13import java.io.File;
14import java.io.FileOutputStream;
15import java.io.IOException;
16import java.io.OutputStream;
17import java.text.MessageFormat;
18import java.util.Date;
19
20import javax.xml.XMLConstants;
21import javax.xml.parsers.DocumentBuilder;
22import javax.xml.parsers.DocumentBuilderFactory;
23import javax.xml.validation.Schema;
24import javax.xml.validation.SchemaFactory;
25
26import org.w3c.dom.Document;
27import org.w3c.dom.NamedNodeMap;
28import org.w3c.dom.Node;
29import org.w3c.dom.NodeList;
30import org.xml.sax.ErrorHandler;
31import org.xml.sax.InputSource;
32import org.xml.sax.SAXException;
33import org.xml.sax.SAXParseException;
34
35import com.ibm.icu.dev.tool.UOption;
36
37public final class XLIFF2ICUConverter {
38
39    /**
40     * These must be kept in sync with getOptions().
41     */
42    private static final int HELP1 = 0;
43    private static final int HELP2 = 1;
44    private static final int SOURCEDIR = 2;
45    private static final int DESTDIR = 3;
46    private static final int TARGETONLY = 4;
47    private static final int SOURCEONLY = 5;
48    private static final int MAKE_SOURCE_ROOT = 6;
49    private static final int XLIFF_1_0 = 7;
50
51    private static final UOption[] options = new UOption[] {
52        UOption.HELP_H(),
53        UOption.HELP_QUESTION_MARK(),
54        UOption.SOURCEDIR(),
55        UOption.DESTDIR(),
56        UOption.create("target-only", 't', UOption.OPTIONAL_ARG),
57        UOption.create("source-only", 'c', UOption.OPTIONAL_ARG),
58        UOption.create("make-source-root", 'r', UOption.NO_ARG),
59        UOption.create("xliff-1.0", 'x', UOption.NO_ARG)
60    };
61
62    private static final int ARRAY_RESOURCE     = 0;
63    private static final int ALIAS_RESOURCE     = 1;
64    private static final int BINARY_RESOURCE    = 2;
65    private static final int INTEGER_RESOURCE   = 3;
66    private static final int INTVECTOR_RESOURCE = 4;
67    private static final int TABLE_RESOURCE     = 5;
68
69    private static final String NEW_RESOURCES[] = {
70        "x-icu-array",
71        "x-icu-alias",
72        "x-icu-binary",
73        "x-icu-integer",
74        "x-icu-intvector",
75        "x-icu-table"
76    };
77
78    private static final String OLD_RESOURCES[] = {
79        "array",
80        "alias",
81        "bin",
82        "int",
83        "intvector",
84        "table"
85    };
86
87    private String resources[];
88
89    private static final String ROOT            = "root";
90    private static final String RESTYPE         = "restype";
91    private static final String RESNAME         = "resname";
92    //private static final String YES             = "yes";
93    //private static final String NO              = "no";
94    private static final String TRANSLATE       = "translate";
95    //private static final String BODY            = "body";
96    private static final String GROUPS          = "group";
97    private static final String FILES           = "file";
98    private static final String TRANSUNIT       = "trans-unit";
99    private static final String BINUNIT         = "bin-unit";
100    private static final String BINSOURCE       = "bin-source";
101    //private static final String TS              = "ts";
102    //private static final String ORIGINAL        = "original";
103    private static final String SOURCELANGUAGE  = "source-language";
104    private static final String TARGETLANGUAGE  = "target-language";
105    private static final String TARGET          = "target";
106    private static final String SOURCE          = "source";
107    private static final String NOTE            = "note";
108    private static final String XMLLANG         = "xml:lang";
109    private static final String FILE            = "file";
110    private static final String INTVECTOR       = "intvector";
111    private static final String ARRAYS          = "array";
112    private static final String STRINGS         = "string";
113    private static final String BIN             = "bin";
114    private static final String INTS            = "int";
115    private static final String TABLE           = "table";
116    private static final String IMPORT          = "import";
117    private static final String HREF            = "href";
118    private static final String EXTERNALFILE    = "external-file";
119    private static final String INTERNALFILE    = "internal-file";
120    private static final String ALTTRANS        = "alt-trans";
121    private static final String CRC             = "crc";
122    private static final String ALIAS           = "alias";
123    private static final String LINESEP         = System.getProperty("line.separator");
124    private static final String BOM             = "\uFEFF";
125    private static final String CHARSET         = "UTF-8";
126    private static final String OPENBRACE       = "{";
127    private static final String CLOSEBRACE      = "}";
128    private static final String COLON           = ":";
129    private static final String COMMA           = ",";
130    private static final String QUOTE           = "\"";
131    private static final String COMMENTSTART    = "/**";
132    private static final String COMMENTEND      = " */";
133    private static final String TAG             = " * @";
134    private static final String COMMENTMIDDLE   = " * ";
135    private static final String SPACE           = " ";
136    private static final String INDENT          = "    ";
137    private static final String EMPTY           = "";
138    private static final String ID              = "id";
139
140    public static void main(String[] args) {
141        XLIFF2ICUConverter cnv = new XLIFF2ICUConverter();
142        cnv.processArgs(args);
143    }
144    private String    sourceDir      = null;
145    //private String    fileName       = null;
146    private String    destDir        = null;
147    private boolean   targetOnly     = false;
148    private String    targetFileName = null;
149    private boolean   makeSourceRoot = false;
150    private String    sourceFileName = null;
151    private boolean   sourceOnly     = false;
152    private boolean   xliff10        = false;
153
154    private void processArgs(String[] args) {
155        int remainingArgc = 0;
156        try{
157            remainingArgc = UOption.parseArgs(args, options);
158        }catch (Exception e){
159            System.err.println("ERROR: "+ e.toString());
160            usage();
161        }
162        if(args.length==0 || options[HELP1].doesOccur || options[HELP2].doesOccur) {
163            usage();
164        }
165        if(remainingArgc==0){
166            System.err.println("ERROR: Either the file name to be processed is not "+
167                               "specified or the it is specified after the -t/-c \n"+
168                               "option which has an optional argument. Try rearranging "+
169                               "the options.");
170            usage();
171        }
172        if(options[SOURCEDIR].doesOccur) {
173            sourceDir = options[SOURCEDIR].value;
174        }
175
176        if(options[DESTDIR].doesOccur) {
177            destDir = options[DESTDIR].value;
178        }
179
180        if(options[TARGETONLY].doesOccur){
181            targetOnly = true;
182            targetFileName = options[TARGETONLY].value;
183        }
184
185        if(options[SOURCEONLY].doesOccur){
186            sourceOnly = true;
187            sourceFileName = options[SOURCEONLY].value;
188        }
189
190        if(options[MAKE_SOURCE_ROOT].doesOccur){
191            makeSourceRoot = true;
192        }
193
194        if(options[XLIFF_1_0].doesOccur) {
195            xliff10 = true;
196        }
197
198        if(destDir==null){
199            destDir = ".";
200        }
201
202        if(sourceOnly == true && targetOnly == true){
203            System.err.println("--source-only and --target-only are specified. Please check the arguments and try again.");
204            usage();
205        }
206
207        for (int i = 0; i < remainingArgc; i++) {
208            //int lastIndex = args[i].lastIndexOf(File.separator, args[i].length()) + 1; /* add 1 to skip past the separator */
209            //fileName = args[i].substring(lastIndex, args[i].length());
210            String xmlfileName = getFullPath(false,args[i]);
211            System.out.println("Processing file: "+xmlfileName);
212            createRB(xmlfileName);
213        }
214    }
215
216    private void usage() {
217        System.out.println("\nUsage: XLIFF2ICUConverter [OPTIONS] [FILES]\n\n"+
218            "This program is used to convert XLIFF files to ICU ResourceBundle TXT files.\n"+
219            "Please refer to the following options. Options are not case sensitive.\n"+
220            "Options:\n"+
221            "-s or --sourcedir          source directory for files followed by path, default is current directory.\n" +
222            "-d or --destdir            destination directory, followed by the path, default is current directory.\n" +
223            "-h or -? or --help         this usage text.\n"+
224            "-t or --target-only        only generate the target language txt file, followed by optional output file name.\n" +
225            "                           Cannot be used in conjunction with --source-only.\n"+
226            "-c or --source-only        only generate the source language bundle followed by optional output file name.\n"+
227            "                           Cannot be used in conjunction with --target-only.\n"+
228            "-r or --make-source-root   produce root bundle from source elements.\n" +
229            "-x or --xliff-1.0          source file is XLIFF 1.0" +
230            "example: com.ibm.icu.dev.tool.localeconverter.XLIFF2ICUConverter -t <optional argument> -s xxx -d yyy myResources.xlf");
231        System.exit(-1);
232    }
233
234    private String getFullPath(boolean fileType, String fName){
235        String str;
236        int lastIndex1 = fName.lastIndexOf(File.separator, fName.length()) + 1; /*add 1 to skip past the separator*/
237        int lastIndex2 = fName.lastIndexOf('.', fName.length());
238        if (fileType == true) {
239            if(lastIndex2 == -1){
240                fName = fName.trim() + ".txt";
241            }else{
242                if(!fName.substring(lastIndex2).equalsIgnoreCase(".txt")){
243                    fName =  fName.substring(lastIndex1,lastIndex2) + ".txt";
244                }
245            }
246            if (destDir != null && fName != null) {
247                str = destDir + File.separator + fName.trim();
248            } else {
249                str = System.getProperty("user.dir") + File.separator + fName.trim();
250            }
251        } else {
252            if(lastIndex2 == -1){
253                fName = fName.trim() + ".xlf";
254            }else{
255                if(!fName.substring(lastIndex2).equalsIgnoreCase(".xml") && fName.substring(lastIndex2).equalsIgnoreCase(".xlf")){
256                    fName = fName.substring(lastIndex1,lastIndex2) + ".xlf";
257                }
258            }
259            if(sourceDir != null && fName != null) {
260                str = sourceDir + File.separator + fName;
261            } else if (lastIndex1 > 0) {
262                str = fName;
263            } else {
264                str = System.getProperty("user.dir") + File.separator + fName;
265            }
266        }
267        return str;
268    }
269
270    /*
271     * Utility method to translate a String filename to URL.
272     *
273     * Note: This method is not necessarily proven to get the
274     * correct URL for every possible kind of filename; it should
275     * be improved.  It handles the most common cases that we've
276     * encountered when running Conformance tests on Xalan.
277     * Also note, this method does not handle other non-file:
278     * flavors of URLs at all.
279     *
280     * If the name is null, return null.
281     * If the name starts with a common URI scheme (namely the ones
282     * found in the examples of RFC2396), then simply return the
283     * name as-is (the assumption is that it's already a URL)
284     * Otherwise we attempt (cheaply) to convert to a file:/// URL.
285     */
286    private static String filenameToURL(String filename){
287        // null begets null - something like the commutative property
288        if (null == filename){
289            return null;
290        }
291
292        // Don't translate a string that already looks like a URL
293        if (filename.startsWith("file:")
294            || filename.startsWith("http:")
295            || filename.startsWith("ftp:")
296            || filename.startsWith("gopher:")
297            || filename.startsWith("mailto:")
298            || filename.startsWith("news:")
299            || filename.startsWith("telnet:")
300           ){
301               return filename;
302           }
303
304
305        File f = new File(filename);
306        String tmp = null;
307        try{
308            // This normally gives a better path
309            tmp = f.getCanonicalPath();
310        }catch (IOException ioe){
311            // But this can be used as a backup, for cases
312            //  where the file does not exist, etc.
313            tmp = f.getAbsolutePath();
314        }
315
316        // URLs must explicitly use only forward slashes
317        if (File.separatorChar == '\\') {
318            tmp = tmp.replace('\\', '/');
319        }
320        // Note the presumption that it's a file reference
321        // Ensure we have the correct number of slashes at the
322        //  start: we always want 3 /// if it's absolute
323        //  (which we should have forced above)
324        if (tmp.startsWith("/")){
325            return "file://" + tmp;
326        }
327        else{
328            return "file:///" + tmp;
329        }
330    }
331    private boolean isXmlLang (String lang){
332
333        int suffix;
334        char c;
335
336        if (lang.length () < 2){
337            return false;
338        }
339
340        c = lang.charAt(1);
341        if (c == '-') {
342            c = lang.charAt(0);
343            if (!(c == 'i' || c == 'I' || c == 'x' || c == 'X')){
344                return false;
345            }
346            suffix = 1;
347        } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
348            c = lang.charAt(0);
349            if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))){
350                return false;
351            }
352            suffix = 2;
353        } else{
354            return false;
355        }
356        while (suffix < lang.length ()) {
357            c = lang.charAt(suffix);
358            if (c != '-'){
359                break;
360            }
361            while (++suffix < lang.length ()) {
362                c = lang.charAt(suffix);
363                if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))){
364                    break;
365                }
366            }
367        }
368        return  ((lang.length() == suffix) && (c != '-'));
369    }
370
371    private void createRB(String xmlfileName) {
372
373        String urls = filenameToURL(xmlfileName);
374        DocumentBuilderFactory dfactory = DocumentBuilderFactory.newInstance();
375        dfactory.setNamespaceAware(true);
376        Document doc = null;
377
378        if (xliff10) {
379            dfactory.setValidating(true);
380            resources = OLD_RESOURCES;
381        } else {
382            try {
383                SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
384                Schema schema = schemaFactory.newSchema();
385
386                dfactory.setSchema(schema);
387            } catch (SAXException e) {
388                System.err.println("Can't create the schema...");
389                System.exit(-1);
390            } catch (UnsupportedOperationException e) {
391                System.err.println("ERROR:\tOne of the schema operations is not supported with this JVM.");
392                System.err.println("\tIf you are using GNU Java, you should try using the latest Sun JVM.");
393                System.err.println("\n*Here is the stack trace:");
394                e.printStackTrace();
395                System.exit(-1);
396            }
397
398            resources = NEW_RESOURCES;
399        }
400
401        ErrorHandler nullHandler = new ErrorHandler() {
402            public void warning(SAXParseException e) throws SAXException {
403
404            }
405            public void error(SAXParseException e) throws SAXException {
406                System.err.println("The XLIFF document is invalid, please check it first: ");
407                System.err.println("Line "+e.getLineNumber()+", Column "+e.getColumnNumber());
408                System.err.println("Error: " + e.getMessage());
409                System.exit(-1);
410            }
411            public void fatalError(SAXParseException e) throws SAXException {
412                throw e;
413            }
414        };
415
416        try {
417            DocumentBuilder docBuilder = dfactory.newDocumentBuilder();
418            docBuilder.setErrorHandler(nullHandler);
419            doc = docBuilder.parse(new InputSource(urls));
420
421            NodeList nlist = doc.getElementsByTagName(FILES);
422            if(nlist.getLength()>1){
423                throw new RuntimeException("Multiple <file> elements in the XLIFF file not supported.");
424            }
425
426            // get the value of source-language attribute
427            String sourceLang = getLanguageName(doc, SOURCELANGUAGE);
428            // get the value of target-language attribute
429            String targetLang = getLanguageName(doc, TARGETLANGUAGE);
430
431            // get the list of <source> elements
432            NodeList sourceList = doc.getElementsByTagName(SOURCE);
433            // get the list of target elements
434            NodeList targetList = doc.getElementsByTagName(TARGET);
435
436            // check if the xliff file has source elements in multiple languages
437            // the source-language value should be the same as xml:lang values
438            // of all the source elements.
439            String xmlSrcLang = checkLangAttribute(sourceList, sourceLang);
440
441            // check if the xliff file has target elements in multiple languages
442            // the target-language value should be the same as xml:lang values
443            // of all the target elements.
444            String xmlTargetLang = checkLangAttribute(targetList, targetLang);
445
446            // Create the Resource linked list which will hold the
447            // source and target bundles after parsing
448            Resource[] set = new Resource[2];
449            set[0] = new ResourceTable();
450            set[1] = new ResourceTable();
451
452            // lenient extraction of source language
453            if(makeSourceRoot == true){
454                set[0].name = ROOT;
455            }else if(sourceLang!=null){
456                set[0].name = sourceLang.replace('-','_');
457            }else{
458                if(xmlSrcLang != null){
459                    set[0].name = xmlSrcLang.replace('-','_');
460                }else{
461                    System.err.println("ERROR: Could not figure out the source language of the file. Please check the XLIFF file.");
462                    System.exit(-1);
463                }
464            }
465
466            // lenient extraction of the target language
467            if(targetLang!=null){
468                set[1].name = targetLang.replace('-','_');
469            }else{
470                if(xmlTargetLang!=null){
471                    set[1].name = xmlTargetLang.replace('-','_');
472                }else{
473                    System.err.println("WARNING: Could not figure out the target language of the file. Producing source bundle only.");
474                }
475            }
476
477
478            // check if any <alt-trans> elements are present
479            NodeList altTrans = doc.getElementsByTagName(ALTTRANS);
480            if(altTrans.getLength()>0){
481                System.err.println("WARNING: <alt-trans> elements in found. Ignoring all <alt-trans> elements.");
482            }
483
484            // get all the group elements
485            NodeList list = doc.getElementsByTagName(GROUPS);
486
487            // process the first group element. The first group element is
488            // the base table that must be parsed recursively
489            parseTable(list.item(0), set);
490
491            // write out the bundle
492            writeResource(set, xmlfileName);
493         }
494        catch (Throwable se) {
495            System.err.println("ERROR: " + se.toString());
496            System.exit(1);
497        }
498    }
499
500    private void writeResource(Resource[] set, String xmlfileName){
501        if(targetOnly==false){
502            writeResource(set[0], xmlfileName, sourceFileName);
503        }
504        if(sourceOnly == false){
505            if(targetOnly==true && set[1].name == null){
506                throw new RuntimeException("The "+ xmlfileName +" does not contain translation\n");
507            }
508            if(set[1].name != null){
509                writeResource(set[1], xmlfileName, targetFileName);
510            }
511        }
512    }
513
514    private void writeResource(Resource set, String sourceFilename, String targetFilename){
515        try {
516            String outputFileName = null;
517            if(targetFilename != null){
518                outputFileName = destDir+File.separator+targetFilename+".txt";
519            }else{
520                outputFileName = destDir+File.separator+set.name+".txt";
521            }
522            FileOutputStream file = new FileOutputStream(outputFileName);
523            BufferedOutputStream writer = new BufferedOutputStream(file);
524
525            writeHeader(writer,sourceFilename);
526
527            //Now start writing the resource;
528            Resource current = set;
529            while(current!=null){
530                current.write(writer, 0, false);
531                current = current.next;
532            }
533            writer.flush();
534            writer.close();
535        } catch (Exception ie) {
536            System.err.println("ERROR :" + ie.toString());
537            return;
538        }
539    }
540
541    private String getLanguageName(Document doc, String lang){
542        if(doc!=null){
543            NodeList list = doc.getElementsByTagName(FILE);
544            Node node = list.item(0);
545            NamedNodeMap attr = node.getAttributes();
546            Node orig = attr.getNamedItem(lang);
547
548            if(orig != null){
549                String name = orig.getNodeValue();
550                NodeList groupList = doc.getElementsByTagName(GROUPS);
551                Node group = groupList.item(0);
552                NamedNodeMap groupAtt = group.getAttributes();
553                Node id = groupAtt.getNamedItem(ID);
554                if(id!=null){
555                    String idVal = id.getNodeValue();
556
557                    if(!name.equals(idVal)){
558                        System.out.println("WARNING: The id value != language name. " +
559                                           "Please compare the output with the orignal " +
560                                           "ICU ResourceBundle before proceeding.");
561                    }
562                }
563                if(!isXmlLang(name)){
564                    System.err.println("The attribute "+ lang + "=\""+ name +
565                                       "\" of <file> element is invalid.");
566                    System.exit(-1);
567                }
568                return name;
569            }
570        }
571        return null;
572    }
573
574    // check if the xliff file is translated into multiple languages
575    // The XLIFF specification allows for single <target> element
576    // as the child of <trans-unit> but the attributes of the
577    // <target> element may different across <trans-unit> elements
578    // check for it. Similar is the case with <source> elements
579    private String checkLangAttribute(NodeList list, String origName){
580        String oldLangName=origName;
581        for(int i = 0 ;i<list.getLength(); i++){
582            Node node = list.item(i);
583            NamedNodeMap attr = node.getAttributes();
584            Node lang = attr.getNamedItem(XMLLANG);
585            String langName = null;
586            // the target element should always contain xml:lang attribute
587            if(lang==null ){
588                if(origName==null){
589                    System.err.println("Encountered <target> element without xml:lang attribute. Please fix the below element in the XLIFF file.\n"+ node.toString());
590                    System.exit(-1);
591                }else{
592                    langName = origName;
593                }
594            }else{
595                langName = lang.getNodeValue();
596            }
597
598            if(oldLangName!=null && langName!=null && !langName.equals(oldLangName)){
599                throw new RuntimeException("The <trans-unit> elements must be bilingual, multilingual tranlations not supported. xml:lang = " + oldLangName +
600                                           " and xml:lang = " + langName);
601            }
602            oldLangName = langName;
603        }
604        return oldLangName;
605    }
606
607    private class Resource{
608        String[] note = new String[20];
609        int noteLen = 0;
610        String translate;
611        String comment;
612        String name;
613        Resource next;
614        public String escapeSyntaxChars(String val){
615            // escape the embedded quotes
616            char[] str = val.toCharArray();
617            StringBuffer result = new StringBuffer();
618            for(int i=0; i<str.length; i++){
619                switch (str[i]){
620                    case '\u0022':
621                        result.append('\\'); //append backslash
622                    default:
623                        result.append(str[i]);
624                }
625            }
626            return result.toString();
627        }
628        public void write(OutputStream writer, int numIndent, boolean bare){
629            while(next!=null){
630                next.write(writer, numIndent+1, false);
631            }
632        }
633        public void writeIndent(OutputStream writer, int numIndent){
634            for(int i=0; i< numIndent; i++){
635                write(writer,INDENT);
636            }
637        }
638        public void write(OutputStream writer, String value){
639            try {
640                byte[] bytes = value.getBytes(CHARSET);
641                writer.write(bytes, 0, bytes.length);
642            } catch(Exception e) {
643                System.err.println(e);
644                System.exit(1);
645            }
646        }
647        public void writeComments(OutputStream writer, int numIndent){
648            boolean translateIsDefault = translate == null || translate.equals("yes");
649
650            if(comment!=null || ! translateIsDefault || noteLen > 0){
651                // print the start of the comment
652                writeIndent(writer, numIndent);
653                write(writer, COMMENTSTART+LINESEP);
654
655                // print comment if any
656                if(comment!=null){
657                    writeIndent(writer, numIndent);
658                    write(writer, COMMENTMIDDLE);
659                    write(writer, comment);
660                    write(writer, LINESEP);
661                }
662
663                // print the translate attribute if any
664                if(! translateIsDefault){
665                    writeIndent(writer, numIndent);
666                    write(writer, TAG+TRANSLATE+SPACE);
667                    write(writer, translate);
668                    write(writer, LINESEP);
669                }
670
671                // print note elements if any
672                for(int i=0; i<noteLen; i++){
673                    if(note[i]!=null){
674                        writeIndent(writer, numIndent);
675                        write(writer, TAG+NOTE+SPACE+note[i]);
676                        write(writer, LINESEP);
677                    }
678                }
679
680                // terminate the comment
681                writeIndent(writer, numIndent);
682                write(writer, COMMENTEND+LINESEP);
683            }
684        }
685    }
686
687    private class ResourceString extends Resource{
688        String val;
689        public void write(OutputStream writer, int numIndent, boolean bare){
690            writeComments(writer, numIndent);
691            writeIndent(writer, numIndent);
692            if(bare==true){
693                if(name!=null){
694                    throw new RuntimeException("Bare option is set to true but the resource has a name!");
695                }
696
697                write(writer,QUOTE+escapeSyntaxChars(val)+QUOTE);
698            }else{
699                write(writer, name+COLON+STRINGS+ OPENBRACE + QUOTE + escapeSyntaxChars(val) + QUOTE+ CLOSEBRACE + LINESEP);
700            }
701        }
702    }
703    private class ResourceAlias extends Resource{
704        String val;
705        public void write(OutputStream writer, int numIndent, boolean bare){
706            writeComments(writer, numIndent);
707            writeIndent(writer, numIndent);
708            String line =  ((name==null)? EMPTY: name)+COLON+ALIAS+ OPENBRACE+QUOTE+escapeSyntaxChars(val)+QUOTE+CLOSEBRACE;
709            if(bare==true){
710                if(name!=null){
711                    throw new RuntimeException("Bare option is set to true but the resource has a name!");
712                }
713                write(writer,line);
714            }else{
715                write(writer, line+LINESEP);
716            }
717        }
718    }
719    private class ResourceInt extends Resource{
720        String val;
721        public void write(OutputStream writer, int numIndent, boolean bare){
722            writeComments(writer, numIndent);
723            writeIndent(writer, numIndent);
724            String line =  ((name==null)? EMPTY: name)+COLON+INTS+ OPENBRACE + val +CLOSEBRACE;
725            if(bare==true){
726                if(name!=null){
727                    throw new RuntimeException("Bare option is set to true but the resource has a name!");
728                }
729                write(writer,line);
730            }else{
731                write(writer, line+LINESEP);
732            }
733        }
734    }
735    private class ResourceBinary extends Resource{
736        String internal;
737        String external;
738        public void write(OutputStream writer, int numIndent, boolean bare){
739            writeComments(writer, numIndent);
740            writeIndent(writer, numIndent);
741            if(internal==null){
742                String line = ((name==null) ? EMPTY : name)+COLON+IMPORT+ OPENBRACE+QUOTE+external+QUOTE+CLOSEBRACE + ((bare==true) ?  EMPTY : LINESEP);
743                write(writer, line);
744            }else{
745                String line = ((name==null) ? EMPTY : name)+COLON+BIN+ OPENBRACE+internal+CLOSEBRACE+ ((bare==true) ?  EMPTY : LINESEP);
746                write(writer,line);
747            }
748
749        }
750    }
751    private class ResourceIntVector extends Resource{
752        ResourceInt first;
753        public void write(OutputStream writer, int numIndent, boolean bare){
754            writeComments(writer, numIndent);
755            writeIndent(writer, numIndent);
756            write(writer, name+COLON+INTVECTOR+OPENBRACE+LINESEP);
757            numIndent++;
758            ResourceInt current = (ResourceInt) first;
759            while(current != null){
760                //current.write(writer, numIndent, true);
761                writeIndent(writer, numIndent);
762                write(writer, current.val);
763                write(writer, COMMA+LINESEP);
764                current = (ResourceInt) current.next;
765            }
766            numIndent--;
767            writeIndent(writer, numIndent);
768            write(writer, CLOSEBRACE+LINESEP);
769        }
770    }
771    private class ResourceTable extends Resource{
772        Resource first;
773        public void write(OutputStream writer, int numIndent, boolean bare){
774            writeComments(writer, numIndent);
775            writeIndent(writer, numIndent);
776            write(writer, name+COLON+TABLE+OPENBRACE+LINESEP);
777            numIndent++;
778            Resource current = first;
779            while(current != null){
780                current.write(writer, numIndent, false);
781                current = current.next;
782            }
783            numIndent--;
784            writeIndent(writer, numIndent);
785            write(writer, CLOSEBRACE+LINESEP);
786        }
787    }
788    private class ResourceArray extends Resource{
789        Resource first;
790        public void write(OutputStream writer, int numIndent, boolean bare){
791            writeComments(writer, numIndent);
792            writeIndent(writer, numIndent);
793            write(writer, name+COLON+ARRAYS+OPENBRACE+LINESEP);
794            numIndent++;
795            Resource current = first;
796            while(current != null){
797                current.write(writer, numIndent, true);
798                write(writer, COMMA+LINESEP);
799                current = current.next;
800            }
801            numIndent--;
802            writeIndent(writer, numIndent);
803            write(writer, CLOSEBRACE+LINESEP);
804        }
805    }
806
807    private String getAttributeValue(Node sNode, String attribName){
808        String value=null;
809        Node node = sNode;
810
811        NamedNodeMap attributes = node.getAttributes();
812        Node attr = attributes.getNamedItem(attribName);
813        if(attr!=null){
814            value = attr.getNodeValue();
815        }
816
817        return value;
818    }
819
820    private void parseResourceString(Node node, ResourceString[] set){
821        ResourceString currentSource;
822        ResourceString currentTarget;
823        currentSource =  set[0];
824        currentTarget =  set[1];
825        String resName   = getAttributeValue(node, RESNAME);
826        String translate = getAttributeValue(node, TRANSLATE);
827
828        // loop to pickup <source>, <note> and <target> elements
829        for(Node transUnit = node.getFirstChild(); transUnit!=null; transUnit = transUnit.getNextSibling()){
830            short type = transUnit.getNodeType();
831            String name = transUnit.getNodeName();
832            if(type == Node.COMMENT_NODE){
833                // get the comment
834               currentSource.comment =  currentTarget.comment = transUnit.getNodeValue();
835            }else if( type == Node.ELEMENT_NODE){
836                if(name.equals(SOURCE)){
837                    // save the source and target values
838                    currentSource.name = currentTarget.name = resName;
839                    currentSource.val = currentTarget.val = transUnit.getFirstChild().getNodeValue();
840                    currentSource.translate = currentTarget.translate = translate;
841                }else if(name.equals(NOTE)){
842                    // save the note values
843                    currentSource.note[currentSource.noteLen++] =
844                        currentTarget.note[currentTarget.noteLen++] =
845                        transUnit.getFirstChild().getNodeValue();
846                }else if(name.equals(TARGET)){
847                    // if there is a target element replace it
848                    currentTarget.val = transUnit.getFirstChild().getNodeValue();
849                }
850            }
851
852        }
853    }
854
855    private void parseResourceInt(Node node, ResourceInt[] set){
856        ResourceInt currentSource;
857        ResourceInt currentTarget;
858        currentSource =  set[0];
859        currentTarget =  set[1];
860        String resName   = getAttributeValue(node, RESNAME);
861        String translate = getAttributeValue(node, TRANSLATE);
862        // loop to pickup <source>, <note> and <target> elements
863        for(Node transUnit = node.getFirstChild(); transUnit!=null; transUnit = transUnit.getNextSibling()){
864            short type = transUnit.getNodeType();
865            String name = transUnit.getNodeName();
866            if(type == Node.COMMENT_NODE){
867                // get the comment
868               currentSource.comment =  currentTarget.comment = transUnit.getNodeValue();
869            }else if( type == Node.ELEMENT_NODE){
870                if(name.equals(SOURCE)){
871                    // save the source and target values
872                    currentSource.name = currentTarget.name = resName;
873                    currentSource.translate = currentTarget.translate = translate;
874                    currentSource.val = currentTarget.val = transUnit.getFirstChild().getNodeValue();
875                }else if(name.equals(NOTE)){
876                    // save the note values
877                    currentSource.note[currentSource.noteLen++] =
878                        currentTarget.note[currentTarget.noteLen++] =
879                        transUnit.getFirstChild().getNodeValue();
880                }else if(name.equals(TARGET)){
881                    // if there is a target element replace it
882                    currentTarget.val = transUnit.getFirstChild().getNodeValue();
883                }
884            }
885
886        }
887    }
888
889    private void parseResourceAlias(Node node, ResourceAlias[] set){
890        ResourceAlias currentSource;
891        ResourceAlias currentTarget;
892        currentSource =  set[0];
893        currentTarget =  set[1];
894        String resName   = getAttributeValue(node, RESNAME);
895        String translate = getAttributeValue(node, TRANSLATE);
896        // loop to pickup <source>, <note> and <target> elements
897        for(Node transUnit = node.getFirstChild(); transUnit!=null; transUnit = transUnit.getNextSibling()){
898            short type = transUnit.getNodeType();
899            String name = transUnit.getNodeName();
900            if(type == Node.COMMENT_NODE){
901                // get the comment
902               currentSource.comment =  currentTarget.comment = transUnit.getNodeValue();
903            }else if( type == Node.ELEMENT_NODE){
904                if(name.equals(SOURCE)){
905                    // save the source and target values
906                    currentSource.name = currentTarget.name = resName;
907                    currentSource.translate = currentTarget.translate = translate;
908                    currentSource.val = currentTarget.val = transUnit.getFirstChild().getNodeValue();
909                }else if(name.equals(NOTE)){
910                    // save the note values
911                    currentSource.note[currentSource.noteLen++] =
912                        currentTarget.note[currentTarget.noteLen++] =
913                        transUnit.getFirstChild().getNodeValue();
914                }else if(name.equals(TARGET)){
915                    // if there is a target element replace it
916                    currentTarget.val = transUnit.getFirstChild().getNodeValue();
917                }
918            }
919
920        }
921    }
922    private void parseResourceBinary(Node node, ResourceBinary[] set){
923        ResourceBinary currentSource;
924        ResourceBinary currentTarget;
925        currentSource =  set[0];
926        currentTarget =  set[1];
927
928        // loop to pickup <source>, <note> and <target> elements
929        for(Node transUnit = node.getFirstChild(); transUnit!=null; transUnit = transUnit.getNextSibling()){
930            short type = transUnit.getNodeType();
931            String name = transUnit.getNodeName();
932            if(type == Node.COMMENT_NODE){
933                // get the comment
934               currentSource.comment =  currentTarget.comment = transUnit.getNodeValue();
935            }else if( type == Node.ELEMENT_NODE){
936                if(name.equals(BINSOURCE)){
937                    // loop to pickup internal-file/extenal-file element
938                    continue;
939                }else if(name.equals(NOTE)){
940                    // save the note values
941                    currentSource.note[currentSource.noteLen++] =
942                        currentTarget.note[currentTarget.noteLen++] =
943                        transUnit.getFirstChild().getNodeValue();
944                }else if(name.equals(INTERNALFILE)){
945                    // if there is a target element replace it
946                    String crc = getAttributeValue(transUnit, CRC);
947                    String value = transUnit.getFirstChild().getNodeValue();
948
949                    //verify that the binary value conforms to the CRC
950                    if(Integer.parseInt(crc, 10) != CalculateCRC32.computeCRC32(value)) {
951                        System.err.println("ERROR: CRC value incorrect! Please check.");
952                        System.exit(1);
953                    }
954
955                    currentTarget.internal = currentSource.internal= value;
956
957                }else if(name.equals(EXTERNALFILE)){
958                    currentSource.external = getAttributeValue(transUnit, HREF);
959                    currentTarget.external = currentSource.external;
960                }
961            }
962
963        }
964    }
965    private void parseTransUnit(Node node, Resource[] set){
966
967        String attrType = getAttributeValue(node, RESTYPE);
968        String translate = getAttributeValue(node, TRANSLATE);
969        if(attrType==null || attrType.equals(STRINGS)){
970            ResourceString[] strings = new ResourceString[2];
971            strings[0] = new ResourceString();
972            strings[1] = new ResourceString();
973            parseResourceString(node, strings);
974            strings[0].translate = strings[1].translate = translate;
975            set[0] = strings[0];
976            set[1] = strings[1];
977        }else if(attrType.equals(resources[INTEGER_RESOURCE])){
978            ResourceInt[] ints = new ResourceInt[2];
979            ints[0] = new ResourceInt();
980            ints[1] = new ResourceInt();
981            parseResourceInt(node, ints);
982            ints[0].translate = ints[1].translate = translate;
983            set[0] = ints[0];
984            set[1] = ints[1];
985        }else if(attrType.equals(resources[ALIAS_RESOURCE])){
986            ResourceAlias[] ints = new ResourceAlias[2];
987            ints[0] = new ResourceAlias();
988            ints[1] = new ResourceAlias();
989            parseResourceAlias(node, ints);
990            ints[0].translate = ints[1].translate = translate;
991            set[0] = ints[0];
992            set[1] = ints[1];
993        }
994    }
995
996    private void parseBinUnit(Node node, Resource[] set){
997        if (getAttributeValue(node, RESTYPE).equals(resources[BINARY_RESOURCE])) {
998            ResourceBinary[] bins = new ResourceBinary[2];
999
1000            bins[0] = new ResourceBinary();
1001            bins[1] = new ResourceBinary();
1002
1003            Resource currentSource = bins[0];
1004            Resource currentTarget = bins[1];
1005            String resName   = getAttributeValue(node, RESNAME);
1006            String translate = getAttributeValue(node, TRANSLATE);
1007
1008            currentTarget.name = currentSource.name = resName;
1009            currentSource.translate = currentTarget.translate = translate;
1010
1011            for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()){
1012                short type = child.getNodeType();
1013                String name = child.getNodeName();
1014
1015                if(type == Node.COMMENT_NODE){
1016                    currentSource.comment = currentTarget.comment = child.getNodeValue();
1017                }else if(type == Node.ELEMENT_NODE){
1018                    if(name.equals(BINSOURCE)){
1019                        parseResourceBinary(child, bins);
1020                    }else if(name.equals(NOTE)){
1021                        String note =  child.getFirstChild().getNodeValue();
1022
1023                        currentSource.note[currentSource.noteLen++] = currentTarget.note[currentTarget.noteLen++] = note;
1024                    }
1025                }
1026            }
1027
1028            set[0] = bins[0];
1029            set[1] = bins[1];
1030        }
1031    }
1032
1033    private void parseArray(Node node, Resource[] set){
1034        if(set[0]==null){
1035            set[0] = new ResourceArray();
1036            set[1] = new ResourceArray();
1037        }
1038        Resource currentSource = set[0];
1039        Resource currentTarget = set[1];
1040        String resName = getAttributeValue(node, RESNAME);
1041        currentSource.name = currentTarget.name = resName;
1042        boolean isFirst = true;
1043
1044        for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()){
1045            short type = child.getNodeType();
1046            String name = child.getNodeName();
1047            if(type == Node.COMMENT_NODE){
1048                currentSource.comment = currentTarget.comment = child.getNodeValue();
1049            }else if(type == Node.ELEMENT_NODE){
1050                if(name.equals(TRANSUNIT)){
1051                    Resource[] next = new Resource[2];
1052                    parseTransUnit(child, next);
1053                    if(isFirst==true){
1054                       ((ResourceArray) currentSource).first = next[0];
1055                       ((ResourceArray) currentTarget).first = next[1];
1056                       currentSource = ((ResourceArray) currentSource).first;
1057                       currentTarget = ((ResourceArray) currentTarget).first;
1058                       isFirst = false;
1059                    }else{
1060                        currentSource.next = next[0];
1061                        currentTarget.next = next[1];
1062                        // set the next pointers
1063                        currentSource = currentSource.next;
1064                        currentTarget = currentTarget.next;
1065                    }
1066                }else if(name.equals(NOTE)){
1067                    String note =  child.getFirstChild().getNodeValue();
1068                    currentSource.note[currentSource.noteLen++] = currentTarget.note[currentTarget.noteLen++] = note;
1069                }else if(name.equals(BINUNIT)){
1070                    Resource[] next = new Resource[2];
1071                    parseBinUnit(child, next);
1072                    if(isFirst==true){
1073                       ((ResourceArray) currentSource).first = next[0];
1074                       ((ResourceArray) currentTarget).first = next[1];
1075                       currentSource = ((ResourceArray) currentSource).first.next;
1076                       currentTarget = ((ResourceArray) currentTarget).first.next;
1077                       isFirst = false;
1078                    }else{
1079                        currentSource.next = next[0];
1080                        currentTarget.next = next[1];
1081                        // set the next pointers
1082                        currentSource = currentSource.next;
1083                        currentTarget = currentTarget.next;
1084                    }
1085                }
1086            }
1087        }
1088    }
1089    private void parseIntVector(Node node, Resource[] set){
1090        if(set[0]==null){
1091            set[0] = new ResourceIntVector();
1092            set[1] = new ResourceIntVector();
1093        }
1094        Resource currentSource = set[0];
1095        Resource currentTarget = set[1];
1096        String resName = getAttributeValue(node, RESNAME);
1097        String translate = getAttributeValue(node,TRANSLATE);
1098        currentSource.name = currentTarget.name = resName;
1099        currentSource.translate = translate;
1100        boolean isFirst = true;
1101        for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()){
1102            short type = child.getNodeType();
1103            String name = child.getNodeName();
1104            if(type == Node.COMMENT_NODE){
1105                currentSource.comment = currentTarget.comment = child.getNodeValue();
1106            }else if(type == Node.ELEMENT_NODE){
1107                if(name.equals(TRANSUNIT)){
1108                    Resource[] next = new Resource[2];
1109                    parseTransUnit(child, next);
1110                    if(isFirst==true){
1111                        // the down cast should be safe .. if not something is terribly wrong!!
1112                       ((ResourceIntVector) currentSource).first = (ResourceInt)next[0];
1113                       ((ResourceIntVector) currentTarget).first = (ResourceInt) next[1];
1114                       currentSource = ((ResourceIntVector) currentSource).first;
1115                       currentTarget = ((ResourceIntVector) currentTarget).first;
1116                       isFirst = false;
1117                    }else{
1118                        currentSource.next = next[0];
1119                        currentTarget.next = next[1];
1120                        // set the next pointers
1121                        currentSource = currentSource.next;
1122                        currentTarget = currentTarget.next;
1123                    }
1124                }else if(name.equals(NOTE)){
1125                    String note =  child.getFirstChild().getNodeValue();
1126                    currentSource.note[currentSource.noteLen++] = currentTarget.note[currentTarget.noteLen++] = note;
1127                }
1128            }
1129        }
1130    }
1131    private void parseTable(Node node, Resource[] set){
1132        if(set[0]==null){
1133            set[0] = new ResourceTable();
1134            set[1] = new ResourceTable();
1135        }
1136        Resource currentSource = set[0];
1137        Resource currentTarget = set[1];
1138
1139        String resName = getAttributeValue(node, RESNAME);
1140        String translate = getAttributeValue(node,TRANSLATE);
1141        if(resName!=null && currentSource.name==null && currentTarget.name==null){
1142            currentSource.name = currentTarget.name = resName;
1143        }
1144        currentTarget.translate = currentSource.translate = translate;
1145
1146        boolean isFirst = true;
1147        for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()){
1148            short type = child.getNodeType();
1149            String name = child.getNodeName();
1150            if(type == Node.COMMENT_NODE){
1151                currentSource.comment = currentTarget.comment = child.getNodeValue();
1152            }else if(type == Node.ELEMENT_NODE){
1153                if(name.equals(GROUPS)){
1154                    Resource[] next = new Resource[2];
1155                    parseGroup(child, next);
1156                    if(isFirst==true){
1157                        // the down cast should be safe .. if not something is terribly wrong!!
1158                       ((ResourceTable) currentSource).first = next[0];
1159                       ((ResourceTable) currentTarget).first = next[1];
1160                       currentSource = ((ResourceTable) currentSource).first;
1161                       currentTarget = ((ResourceTable) currentTarget).first;
1162                       isFirst = false;
1163                    }else{
1164                        currentSource.next = next[0];
1165                        currentTarget.next = next[1];
1166                        // set the next pointers
1167                        currentSource = currentSource.next;
1168                        currentTarget = currentTarget.next;
1169                    }
1170                }else if(name.equals(TRANSUNIT)){
1171                    Resource[] next = new Resource[2];
1172                    parseTransUnit(child, next);
1173                    if(isFirst==true){
1174                        // the down cast should be safe .. if not something is terribly wrong!!
1175                       ((ResourceTable) currentSource).first = next[0];
1176                       ((ResourceTable) currentTarget).first = next[1];
1177                       currentSource = ((ResourceTable) currentSource).first;
1178                       currentTarget = ((ResourceTable) currentTarget).first;
1179                       isFirst = false;
1180                    }else{
1181                        currentSource.next = next[0];
1182                        currentTarget.next = next[1];
1183                        // set the next pointers
1184                        currentSource = currentSource.next;
1185                        currentTarget = currentTarget.next;
1186                    }
1187                }else if(name.equals(NOTE)){
1188                    String note =  child.getFirstChild().getNodeValue();
1189                    currentSource.note[currentSource.noteLen++] = currentTarget.note[currentTarget.noteLen++] = note;
1190                }else if(name.equals(BINUNIT)){
1191                    Resource[] next = new Resource[2];
1192                    parseBinUnit(child, next);
1193                    if(isFirst==true){
1194                        // the down cast should be safe .. if not something is terribly wrong!!
1195                       ((ResourceTable) currentSource).first = next[0];
1196                       ((ResourceTable) currentTarget).first = next[1];
1197                       currentSource = ((ResourceTable) currentSource).first;
1198                       currentTarget = ((ResourceTable) currentTarget).first;
1199                       isFirst = false;
1200                    }else{
1201                        currentSource.next = next[0];
1202                        currentTarget.next = next[1];
1203                        // set the next pointers
1204                        currentSource = currentSource.next;
1205                        currentTarget = currentTarget.next;
1206                    }
1207                }
1208            }
1209        }
1210    }
1211
1212    private void parseGroup(Node node, Resource[] set){
1213
1214        // figure out what kind of group this is
1215        String resType = getAttributeValue(node, RESTYPE);
1216        if(resType.equals(resources[ARRAY_RESOURCE])){
1217            parseArray(node, set);
1218        }else if( resType.equals(resources[TABLE_RESOURCE])){
1219            parseTable(node, set);
1220        }else if( resType.equals(resources[INTVECTOR_RESOURCE])){
1221            parseIntVector(node, set);
1222        }
1223    }
1224
1225
1226    private void writeLine(OutputStream writer, String line) {
1227        try {
1228            byte[] bytes = line.getBytes(CHARSET);
1229            writer.write(bytes, 0, bytes.length);
1230        } catch (Exception e) {
1231            System.err.println(e);
1232            System.exit(1);
1233        }
1234    }
1235
1236    private void writeHeader(OutputStream writer, String fileName){
1237        final String header =
1238            "// ***************************************************************************" + LINESEP +
1239            "// *" + LINESEP +
1240            "// * Tool: com.ibm.icu.dev.tool.localeconverter.XLIFF2ICUConverter.java" + LINESEP +
1241            "// * Date & Time: {0,date,MM/dd/yyyy hh:mm:ss a z}"+ LINESEP +
1242            "// * Source File: {1}" + LINESEP +
1243            "// *" + LINESEP +
1244            "// ***************************************************************************" + LINESEP;
1245
1246        writeBOM(writer);
1247        MessageFormat format = new MessageFormat(header);
1248        Object args[] = {new Date(System.currentTimeMillis()), fileName};
1249
1250        writeLine(writer, format.format(args));
1251    }
1252
1253    private  void writeBOM(OutputStream buffer) {
1254        try {
1255            byte[] bytes = BOM.getBytes(CHARSET);
1256            buffer.write(bytes, 0, bytes.length);
1257        } catch(Exception e) {
1258            System.err.println(e);
1259            System.exit(1);
1260        }
1261    }
1262}
1263