1 package org.unicode.cldr.tool; 2 3 import java.io.File; 4 import java.io.FileNotFoundException; 5 import java.io.IOException; 6 import java.io.PrintWriter; 7 import java.io.UncheckedIOException; 8 import java.util.Arrays; 9 import java.util.Collection; 10 import java.util.HashSet; 11 import java.util.LinkedHashSet; 12 import java.util.Map; 13 import java.util.Map.Entry; 14 import java.util.Objects; 15 import java.util.Set; 16 import java.util.TreeSet; 17 import java.util.regex.Matcher; 18 import java.util.regex.Pattern; 19 20 import org.unicode.cldr.tool.Option.Options; 21 import org.unicode.cldr.tool.Option.Params; 22 import org.unicode.cldr.util.*; 23 24 import com.google.common.collect.HashMultimap; 25 import com.google.common.collect.ImmutableSet; 26 import com.google.common.collect.Multimap; 27 import com.google.common.collect.Sets; 28 import com.google.common.collect.TreeMultimap; 29 import com.google.common.io.Files; 30 import com.ibm.icu.util.Output; 31 32 public class GenerateProductionData { 33 static boolean DEBUG = false; 34 static boolean VERBOSE = false; 35 static Matcher FILE_MATCH = null; 36 37 static String SOURCE_COMMON_DIR = null; 38 static String DEST_COMMON_DIR = null; 39 40 static boolean ADD_LOGICAL_GROUPS = false; 41 static boolean ADD_DATETIME = false; 42 static boolean ADD_SIDEWAYS = false; 43 static boolean ADD_ROOT = false; 44 static boolean INCLUDE_COMPREHENSIVE = false; 45 static boolean CONSTRAINED_RESTORATION = false; 46 47 static final Set<String> NON_XML = ImmutableSet.of("dtd", "properties", "testData", "uca"); 48 static final Set<String> COPY_ANYWAY = ImmutableSet.of("casing", "collation"); // don't want to "clean up", makes format difficult to use 49 static final SupplementalDataInfo SDI = CLDRConfig.getInstance().getSupplementalDataInfo(); 50 51 static final Multimap<String, Pair<String, String>> localeToSubdivisionsToMigrate = TreeMultimap.create(); 52 53 enum MyOptions { 54 sourceDirectory(new Params() 55 .setHelp("source common directory") 56 .setDefault(CLDRPaths.COMMON_DIRECTORY) 57 .setMatch(".*")), 58 destinationDirectory(new Params() 59 .setHelp("destination common directory") 60 .setDefault(CLDRPaths.STAGING_DIRECTORY + "production/common") 61 .setMatch(".*")), 62 logicalGroups(new Params() 63 .setHelp("add path/values for logical groups") 64 .setDefault("true") 65 .setMatch("true|false")), 66 time(new Params() 67 .setHelp("add path/values for stock date/time/datetime") 68 .setDefault("true") 69 .setMatch("true|false")), 70 Sideways(new Params() 71 .setHelp("add path/values for sideways inheritance") 72 .setDefault("true") 73 .setMatch("true|false")), 74 root(new Params() 75 .setHelp("add path/values for root and code-fallback") 76 .setDefault("true") 77 .setMatch("true|false")), 78 constrainedRestoration(new Params() 79 .setHelp("only add inherited paths that were in original file") 80 .setDefault("true") 81 .setMatch("true|false")), 82 includeComprehensive(new Params() 83 .setHelp("exclude comprehensive paths — otherwise just to modern level") 84 .setDefault("true") 85 .setMatch("true|false")), 86 verbose(new Params() 87 .setHelp("verbose debugging messages")), 88 Debug(new Params() 89 .setHelp("debug")), 90 fileMatch(new Params() 91 .setHelp("regex to match patterns") 92 .setMatch(".*")), 93 ; 94 95 // BOILERPLATE TO COPY 96 final Option option; 97 MyOptions(Params params)98 private MyOptions(Params params) { 99 option = new Option(this, params); 100 } 101 102 private static Options myOptions = new Options(); 103 static { 104 for (MyOptions option : MyOptions.values()) { myOptions.add(option, option.option)105 myOptions.add(option, option.option); 106 } 107 } 108 parse(String[] args, boolean showArguments)109 private static Set<String> parse(String[] args, boolean showArguments) { 110 return myOptions.parse(MyOptions.values()[0], args, true); 111 } 112 } 113 main(String[] args)114 public static void main(String[] args) { 115 // TODO rbnf and segments don't have modern coverage; fix there. 116 117 MyOptions.parse(args, true); 118 SOURCE_COMMON_DIR = MyOptions.sourceDirectory.option.getValue(); 119 DEST_COMMON_DIR = MyOptions.destinationDirectory.option.getValue(); 120 121 // debugging 122 VERBOSE = MyOptions.verbose.option.doesOccur(); 123 DEBUG = MyOptions.Debug.option.doesOccur(); 124 String fileMatch = MyOptions.fileMatch.option.getValue(); 125 if (fileMatch != null) { 126 FILE_MATCH = Pattern.compile(fileMatch).matcher(""); 127 } 128 129 // controls for minimization 130 ADD_LOGICAL_GROUPS = "true".equalsIgnoreCase(MyOptions.logicalGroups.option.getValue()); 131 ADD_DATETIME = "true".equalsIgnoreCase(MyOptions.time.option.getValue()); 132 ADD_SIDEWAYS = "true".equalsIgnoreCase(MyOptions.Sideways.option.getValue()); 133 ADD_ROOT = "true".equalsIgnoreCase(MyOptions.root.option.getValue()); 134 135 // constraints 136 INCLUDE_COMPREHENSIVE = "true".equalsIgnoreCase(MyOptions.includeComprehensive.option.getValue()); 137 CONSTRAINED_RESTORATION = "true".equalsIgnoreCase(MyOptions.constrainedRestoration.option.getValue()); 138 139 // get directories 140 141 Arrays.asList(DtdType.values()) 142 .parallelStream() 143 .unordered() 144 .forEach(type -> { 145 boolean isLdmlDtdType = type == DtdType.ldml; 146 147 // bit of a hack, using the ldmlICU — otherwise unused! — to get the nonXML files. 148 Set<String> directories = (type == DtdType.ldmlICU) ? NON_XML : type.directories; 149 150 for (String dir : directories) { 151 File sourceDir = new File(SOURCE_COMMON_DIR, dir); 152 File destinationDir = new File(DEST_COMMON_DIR, dir); 153 Stats stats = new Stats(); 154 copyFilesAndReturnIsEmpty(sourceDir, destinationDir, null, isLdmlDtdType, stats); 155 } 156 }); 157 // should be called from the main thread. Synchronizing to document. 158 if (!localeToSubdivisionsToMigrate.isEmpty()) { 159 System.err.println("WARNING: Subdivision files not written, " + localeToSubdivisionsToMigrate.size() + " entries\n" + 160 "For locales: " + localeToSubdivisionsToMigrate.keySet()); 161 for (Entry<String, Pair<String, String>> entry : localeToSubdivisionsToMigrate.entries()) { 162 System.err.println(entry.getKey() + " \t" + entry.getValue()); 163 } 164 } 165 } 166 167 private static class Stats { 168 long files; 169 long removed; 170 long retained; 171 long remaining; clear()172 Stats clear() { 173 files = removed = retained = remaining = 0; 174 return this; 175 } 176 @Override toString()177 public String toString() { 178 return 179 "files=" + files 180 + (removed + retained + remaining == 0 ? "" 181 : "; removed=" + removed 182 + "; retained=" + retained 183 + "; remaining=" + remaining); 184 } showNonZero(String label)185 public void showNonZero(String label) { 186 if (removed + retained + remaining != 0) { 187 System.out.println(label + toString()); 188 } 189 } 190 } 191 192 /** 193 * Copy files in directories, recursively. 194 * @param sourceFile 195 * @param destinationFile 196 * @param factory 197 * @param isLdmlDtdType 198 * @param stats 199 * @param hasChildren 200 * @return true if the file is an ldml file with empty content. 201 */ copyFilesAndReturnIsEmpty(File sourceFile, File destinationFile, Factory factory, boolean isLdmlDtdType, final Stats stats)202 private static boolean copyFilesAndReturnIsEmpty(File sourceFile, File destinationFile, 203 Factory factory, boolean isLdmlDtdType, final Stats stats) { 204 if (sourceFile.isDirectory()) { 205 206 System.out.println(sourceFile + " => " + destinationFile); 207 if (!destinationFile.mkdirs()) { 208 // if created, remove old contents 209 Arrays.stream(destinationFile.listFiles()).forEach(File::delete); 210 } 211 212 Set<String> sorted = new TreeSet<>(); 213 sorted.addAll(Arrays.asList(sourceFile.list())); 214 215 if (COPY_ANYWAY.contains(sourceFile.getName())) { // special cases 216 isLdmlDtdType = false; 217 } 218 // reset factory for directory 219 factory = null; 220 if (isLdmlDtdType) { 221 // if the factory is empty, then we just copy files 222 factory = Factory.make(sourceFile.toString(), ".*"); 223 } 224 boolean isMainDir = factory != null && sourceFile.getName().contentEquals("main"); 225 boolean isRbnfDir = factory != null && sourceFile.getName().contentEquals("rbnf"); 226 boolean isAnnotationsDir = factory != null && sourceFile.getName().startsWith("annotations"); 227 228 Set<String> emptyLocales = new HashSet<>(); 229 final Stats stats2 = new Stats(); 230 final Factory theFactory = factory; 231 final boolean isLdmlDtdType2 = isLdmlDtdType; 232 sorted 233 .parallelStream() 234 .forEach(file -> { 235 File sourceFile2 = new File(sourceFile, file); 236 File destinationFile2 = new File(destinationFile, file); 237 if (VERBOSE) System.out.println("\t" + file); 238 239 // special step to just copy certain files like main/root.xml file 240 Factory currFactory = theFactory; 241 if (isMainDir) { 242 if (file.equals("root.xml")) { 243 currFactory = null; 244 } 245 } else if (isRbnfDir) { 246 currFactory = null; 247 } 248 249 // when the currFactory is null, we just copy files as-is 250 boolean isEmpty = copyFilesAndReturnIsEmpty(sourceFile2, destinationFile2, currFactory, isLdmlDtdType2, stats2); 251 if (isEmpty) { // only happens for ldml 252 emptyLocales.add(file.substring(0,file.length()-4)); // remove .xml for localeId 253 } 254 }); 255 stats2.showNonZero("\tTOTAL:\t"); 256 // if there are empty ldml files, AND we aren't in /main/, 257 // then remove any without children 258 if (!emptyLocales.isEmpty() && !isMainDir) { 259 Set<String> childless = getChildless(emptyLocales, factory.getAvailable(), isAnnotationsDir); 260 if (!childless.isEmpty()) { 261 if (VERBOSE) System.out.println("\t" + destinationFile + "\tRemoving empty locales:" + childless); 262 childless.stream().forEach(locale -> new File(destinationFile, locale + ".xml").delete()); 263 } 264 } 265 return false; 266 } else if (factory != null) { 267 String file = sourceFile.getName(); 268 if (!file.endsWith(".xml")) { 269 return false; 270 } 271 String localeId = file.substring(0, file.length()-4); 272 if (FILE_MATCH != null) { 273 if (!FILE_MATCH.reset(localeId).matches()) { 274 return false; 275 } 276 } 277 boolean isRoot = localeId.equals("root"); 278 String directoryName = sourceFile.getParentFile().getName(); 279 boolean isSubdivisionDirectory = "subdivisions".equals(directoryName); 280 281 CLDRFile cldrFileUnresolved = factory.make(localeId, false); 282 CLDRFile cldrFileResolved = factory.make(localeId, true); 283 boolean gotOne = false; 284 Set<String> toRemove = new TreeSet<>(); // TreeSet just makes debugging easier 285 Set<String> toRetain = new TreeSet<>(); 286 Output<String> pathWhereFound = new Output<>(); 287 Output<String> localeWhereFound = new Output<>(); 288 289 boolean isArabicSpecial = localeId.equals("ar") || localeId.startsWith("ar_"); 290 291 String debugPath = null; // "//ldml/units/unitLength[@type=\"short\"]/unit[@type=\"power-kilowatt\"]/displayName"; 292 String debugLocale = "af"; 293 294 for (String xpath : cldrFileUnresolved) { 295 if (xpath.startsWith("//ldml/identity")) { 296 continue; 297 } 298 if (debugPath != null && localeId.equals(debugLocale) && xpath.equals(debugPath)) { 299 int debug = 0; 300 } 301 302 String value = cldrFileUnresolved.getStringValue(xpath); 303 if (value == null || CldrUtility.INHERITANCE_MARKER.equals(value)) { 304 toRemove.add(xpath); 305 continue; 306 } 307 308 // special-case the root values that are only for Survey Tool use 309 310 if (isRoot) { 311 if (AnnotationUtil.pathIsAnnotation(xpath)) { 312 toRemove.add(xpath); 313 continue; 314 } 315 } 316 317 // special case for Arabic defaultNumberingSystem 318 if (isArabicSpecial && xpath.contains("/defaultNumberingSystem")) { 319 toRetain.add(xpath); 320 } 321 322 // remove items that are the same as their bailey values. This also catches Inheritance Marker 323 324 String bailey = cldrFileResolved.getBaileyValue(xpath, pathWhereFound, localeWhereFound); 325 if (value.equals(bailey) 326 && (!ADD_SIDEWAYS 327 || pathEqualsOrIsAltVariantOf(xpath, pathWhereFound.value)) 328 && (!ADD_ROOT 329 || (!Objects.equals(XMLSource.ROOT_ID, localeWhereFound.value) 330 && !Objects.equals(XMLSource.CODE_FALLBACK_ID, localeWhereFound.value)))) { 331 toRemove.add(xpath); 332 continue; 333 } 334 335 // Move subdivisions elsewhere 336 if (!isSubdivisionDirectory && xpath.startsWith("//ldml/localeDisplayNames/subdivisions/subdivision")) { 337 synchronized(localeToSubdivisionsToMigrate) { 338 localeToSubdivisionsToMigrate.put(localeId, Pair.of(xpath, value)); 339 } 340 toRemove.add(xpath); 341 continue; 342 } 343 // remove level=comprehensive (under setting) 344 345 if (!INCLUDE_COMPREHENSIVE) { 346 Level coverage = SDI.getCoverageLevel(xpath, localeId); 347 if (coverage == Level.COMPREHENSIVE) { 348 toRemove.add(xpath); 349 continue; 350 } 351 } 352 353 // if we got all the way to here, we have a non-empty result 354 355 // check to see if we might need to flesh out logical groups 356 // TODO Should be done in the converter tool!! 357 if (ADD_LOGICAL_GROUPS && !LogicalGrouping.isOptional(cldrFileResolved, xpath)) { 358 Set<String> paths = LogicalGrouping.getPaths(cldrFileResolved, xpath); 359 if (paths != null && paths.size() > 1) { 360 for (String possiblePath : paths) { 361 // Unclear from API whether we need to do this filtering 362 if (!LogicalGrouping.isOptional(cldrFileResolved, possiblePath)) { 363 toRetain.add(possiblePath); 364 } 365 } 366 } 367 } 368 369 // check to see if we might need to flesh out datetime. 370 // TODO Should be done in the converter tool!! 371 if (ADD_DATETIME && isDateTimePath(xpath)) { 372 toRetain.addAll(dateTimePaths(xpath)); 373 } 374 375 // past the gauntlet 376 gotOne = true; 377 } 378 379 // we even add empty files, but can delete them back on the directory level. 380 try (PrintWriter pw = new PrintWriter(destinationFile)) { 381 CLDRFile outCldrFile = cldrFileUnresolved.cloneAsThawed(); 382 if (isSubdivisionDirectory) { 383 synchronized (localeToSubdivisionsToMigrate) { 384 Collection<Pair<String, String>> path_values = localeToSubdivisionsToMigrate.get(localeId); 385 if (path_values != null) { 386 for (Pair<String, String>path_value : path_values) { 387 outCldrFile.add(path_value.getFirst(), path_value.getSecond()); 388 } 389 localeToSubdivisionsToMigrate.removeAll(localeId); 390 } 391 } 392 } 393 394 // Remove paths, but pull out the ones to retain 395 // example: 396 // toRemove == {a b c} // c may have ^^^ value 397 // toRetain == {b c d} // d may have ^^^ value 398 399 if (DEBUG) { 400 showIfNonZero(localeId, "removing", toRemove); 401 showIfNonZero(localeId, "retaining", toRetain); 402 403 } 404 if (CONSTRAINED_RESTORATION) { 405 toRetain.retainAll(toRemove); // only add paths that were there already 406 // toRetain == {b c} 407 if (DEBUG) { 408 showIfNonZero(localeId, "constrained retaining", toRetain); 409 } 410 } 411 412 boolean changed0 = toRemove.removeAll(toRetain); 413 // toRemove == {a} 414 if (DEBUG && changed0) { 415 showIfNonZero(localeId, "final removing", toRemove); 416 } 417 418 boolean changed = toRetain.removeAll(toRemove); 419 // toRetain = {b c d} or if constrained, {b c} 420 if (DEBUG && changed) { 421 showIfNonZero(localeId, "final retaining", toRetain); 422 } 423 424 outCldrFile.removeAll(toRemove, false); 425 if (DEBUG) { 426 for (String xpath : toRemove) { 427 System.out.println(localeId + ": removing: «" 428 + cldrFileUnresolved.getStringValue(xpath) 429 + "», " + xpath); 430 } 431 } 432 433 // now set any null values to bailey values if not present 434 for (String xpath : toRetain) { 435 if (debugPath != null && localeId.equals(debugLocale) && xpath.equals(debugPath)) { 436 int debug = 0; 437 } 438 String value = cldrFileResolved.getStringValue(xpath); 439 if (value == null || value.equals(CldrUtility.INHERITANCE_MARKER)) { 440 throw new IllegalArgumentException(localeId + ": " + value + " in value for " + xpath); 441 } else { 442 if (DEBUG) { 443 String oldValue = cldrFileUnresolved.getStringValue(xpath); 444 System.out.println("Restoring: «" + oldValue + "» ⇒ «" + value 445 + "»\t" + xpath); 446 } 447 outCldrFile.add(xpath, value); 448 } 449 } 450 451 // double-check results 452 int count = 0; 453 for (String xpath : outCldrFile) { 454 if (debugPath != null && localeId.equals(debugLocale) && xpath.equals(debugPath)) { 455 int debug = 0; 456 } 457 String value = outCldrFile.getStringValue(xpath); 458 if (value == null || value.equals(CldrUtility.INHERITANCE_MARKER)) { 459 throw new IllegalArgumentException(localeId + ": " + value + " in value for " + xpath); 460 } 461 } 462 463 outCldrFile.write(pw); 464 ++stats.files; 465 stats.removed += toRemove.size(); 466 stats.retained += toRetain.size(); 467 stats.remaining += count; 468 } catch (FileNotFoundException e) { 469 throw new UncheckedIOException("Can't copy " + sourceFile + " to " + destinationFile + " — ", e); 470 } 471 return !gotOne; 472 } else { 473 if (FILE_MATCH != null) { 474 String file = sourceFile.getName(); 475 int dotPos = file.lastIndexOf('.'); 476 String baseName = dotPos >= 0 ? file.substring(0, file.length()-dotPos) : file; 477 if (!FILE_MATCH.reset(baseName).matches()) { 478 return false; 479 } 480 } 481 // for now, just copy 482 ++stats.files; 483 copyFiles(sourceFile, destinationFile); 484 return false; 485 } 486 } 487 showIfNonZero(String localeId, String title, Set<String> toRemove)488 private static void showIfNonZero(String localeId, String title, Set<String> toRemove) { 489 if (toRemove.size() != 0) { 490 System.out.println(localeId + ": " 491 + title 492 + ": " + toRemove.size()); 493 } 494 } 495 pathEqualsOrIsAltVariantOf(String desiredPath, String foundPath)496 private static boolean pathEqualsOrIsAltVariantOf(String desiredPath, String foundPath) { 497 if (desiredPath.equals(foundPath)) { 498 return true; 499 } 500 if (desiredPath.contains("type=\"en_GB\"") && desiredPath.contains("alt=")) { 501 int debug = 0; 502 } 503 if (foundPath == null || foundPath.equals(GlossonymConstructor.PSEUDO_PATH)) { 504 // We can do this, because the bailey value has already been checked. 505 // Since it isn't null, a null or PSEUDO_PATH indicates a constructed alt value. 506 return true; 507 } 508 XPathParts desiredPathParts = XPathParts.getFrozenInstance(desiredPath); 509 XPathParts foundPathParts = XPathParts.getFrozenInstance(foundPath); 510 if (desiredPathParts.size() != foundPathParts.size()) { 511 return false; 512 } 513 for (int e = 0; e < desiredPathParts.size(); ++e) { 514 String element1 = desiredPathParts.getElement(e); 515 String element2 = foundPathParts.getElement(e); 516 if (!element1.equals(element2)) { 517 return false; 518 } 519 Map<String, String> attr1 = desiredPathParts.getAttributes(e); 520 Map<String, String> attr2 = foundPathParts.getAttributes(e); 521 if (attr1.equals(attr2)) { 522 continue; 523 } 524 Set<String> keys1 = attr1.keySet(); 525 Set<String> keys2 = attr2.keySet(); 526 for (String attr : Sets.union(keys1, keys2)) { 527 if (attr.equals("alt")) { 528 continue; 529 } 530 if (!Objects.equals(attr1.get(attr), attr2.get(attr))) { 531 return false; 532 } 533 } 534 } 535 return true; 536 } 537 isDateTimePath(String xpath)538 private static boolean isDateTimePath(String xpath) { 539 return xpath.startsWith("//ldml/dates/calendars/calendar") 540 && xpath.contains("FormatLength[@type="); 541 } 542 543 /** generate full dateTimePaths from any element 544 //ldml/dates/calendars/calendar[@type="gregorian"]/dateFormats/dateFormatLength[@type=".*"]/dateFormat[@type="standard"]/pattern[@type="standard"] 545 //ldml/dates/calendars/calendar[@type="gregorian"]/timeFormats/timeFormatLength[@type=".*"]/timeFormat[@type="standard"]/pattern[@type="standard"] 546 //ldml/dates/calendars/calendar[@type="gregorian"]/dateTimeFormats/dateTimeFormatLength[@type=".*"]/dateTimeFormat[@type="standard"]/pattern[@type="standard"] 547 */ dateTimePaths(String xpath)548 private static Set<String> dateTimePaths(String xpath) { 549 LinkedHashSet<String> result = new LinkedHashSet<>(); 550 String prefix = xpath.substring(0,xpath.indexOf(']') + 2); // get after ]/ 551 for (String type : Arrays.asList("date", "time", "dateTime")) { 552 String pattern = prefix + "$XFormats/$XFormatLength[@type=\"$Y\"]/$XFormat[@type=\"standard\"]/pattern[@type=\"standard\"]".replace("$X", type); 553 for (String width : Arrays.asList("full", "long", "medium", "short")) { 554 result.add(pattern.replace("$Y", width)); 555 } 556 } 557 return result; 558 } 559 getChildless(Set<String> emptyLocales, Set<String> available, boolean isAnnotationsDir)560 private static Set<String> getChildless(Set<String> emptyLocales, Set<String> available, boolean isAnnotationsDir) { 561 // first build the parent2child map 562 Multimap<String,String> parent2child = HashMultimap.create(); 563 for (String locale : available) { 564 String parent = LocaleIDParser.getParent(locale); 565 if (parent != null) { 566 parent2child.put(parent, locale); 567 } 568 if (isAnnotationsDir) { 569 String simpleParent = LocaleIDParser.getParent(locale, true); 570 if (simpleParent != null && (parent == null || simpleParent != parent)) { 571 parent2child.put(simpleParent, locale); 572 } 573 } 574 } 575 576 // now cycle through the empties 577 Set<String> result = new HashSet<>(); 578 for (String empty : emptyLocales) { 579 if (allChildrenAreEmpty(empty, emptyLocales, parent2child)) { 580 result.add(empty); 581 } 582 } 583 return result; 584 } 585 586 /** 587 * Recursively checks that all children are empty (including that there are no children) 588 * @param name 589 * @param emptyLocales 590 * @param parent2child 591 * @return 592 */ allChildrenAreEmpty( String locale, Set<String> emptyLocales, Multimap<String, String> parent2child)593 private static boolean allChildrenAreEmpty( 594 String locale, 595 Set<String> emptyLocales, 596 Multimap<String, String> parent2child) { 597 598 Collection<String> children = parent2child.get(locale); 599 for (String child : children) { 600 if (!emptyLocales.contains(child)) { 601 return false; 602 } 603 if (!allChildrenAreEmpty(child, emptyLocales, parent2child)) { 604 return false; 605 } 606 } 607 return true; 608 } 609 copyFiles(File sourceFile, File destinationFile)610 private static void copyFiles(File sourceFile, File destinationFile) { 611 try { 612 Files.copy(sourceFile, destinationFile); 613 } catch (IOException e) { 614 System.err.println("Can't copy " + sourceFile + " to " + destinationFile + " — " + e); 615 } 616 } 617 } 618