• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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