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