1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.timezone.location.data_pipeline.steps;
18 
19 import static java.util.stream.Collectors.toList;
20 
21 import com.android.timezone.location.common.LicenseSupport.License;
22 import com.android.timezone.location.data_pipeline.steps.proto.S2Protos;
23 import com.google.common.geometry.S2CellId;
24 import com.google.common.geometry.S2CellUnion;
25 import com.google.common.geometry.S2Loop;
26 import com.google.common.geometry.S2Point;
27 import com.google.common.geometry.S2Polygon;
28 import com.google.protobuf.Message;
29 import com.google.protobuf.TextFormat;
30 
31 import java.io.File;
32 import java.io.FileFilter;
33 import java.io.FileInputStream;
34 import java.io.FileOutputStream;
35 import java.io.FileReader;
36 import java.io.FileWriter;
37 import java.io.IOException;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.Collections;
41 import java.util.List;
42 import java.util.Objects;
43 
44 /** A set of shared types for use in the reference time zone geolocation data pipeline. */
45 public final class Types {
46 
47     public static final ProtoStorageFormat DEFAULT_PROTO_STORAGE_FORMAT = ProtoStorageFormat.TEXT;
48 
Types()49     private Types() {
50     }
51 
52     /** The two proto storage formats supported. */
53     public enum ProtoStorageFormat {
54         TEXT(".prototxt"),
55         BINARY(".proto");
56 
57         private final String mSuffix;
58 
ProtoStorageFormat(String suffix)59         ProtoStorageFormat(String suffix) {
60             this.mSuffix = suffix;
61         }
62 
63         /**
64          * Stores {@code message} in {@code outputFile} in this format. Also checks for a LICENSE
65          * file and adds a header to the file if possible.
66          */
store(Message message, File outputFile, License license)67         public void store(Message message, File outputFile, License license) throws IOException {
68             // Require the LICENSE file to be present.
69             license.checkLicensePresentInDir(outputFile.getParentFile());
70 
71             if (this == TEXT) {
72                 storeProtoAsText(message, outputFile, license);
73             } else if (this == BINARY) {
74                 storeProtoAsBinary(message, outputFile);
75             } else {
76                 throw new IllegalArgumentException();
77             }
78         }
79 
80         /**
81          * Loads a proto in this format from {@code inputFile} by populating the supplied
82          * {@code builder}.
83          */
load(Message.Builder builder, File inputFile)84         public void load(Message.Builder builder, File inputFile) throws IOException {
85             if (this == TEXT) {
86                 loadProtoAsText(builder, inputFile);
87             } else if (this == BINARY) {
88                 loadProtoAsBinary(builder, inputFile);
89             } else {
90                 throw new IllegalArgumentException();
91             }
92         }
93 
94         /** Returns the type suffix for this storage format. */
getTypeSuffix()95         public String getTypeSuffix() {
96             return mSuffix;
97         }
98 
loadProtoAsText(Message.Builder builder, File inputFile)99         private static void loadProtoAsText(Message.Builder builder, File inputFile)
100                 throws IOException {
101             try (FileReader reader = new FileReader(inputFile)) {
102                 TextFormat.getParser().merge(reader, builder);
103             }
104         }
105 
loadProtoAsBinary(Message.Builder builder, File inputFile)106         private static void loadProtoAsBinary(Message.Builder builder, File inputFile)
107                 throws IOException {
108             try (FileInputStream inputStream = new FileInputStream(inputFile)) {
109                 builder.mergeFrom(inputStream);
110             }
111         }
112 
storeProtoAsText(Message message, File outputFile, License license)113         private static void storeProtoAsText(Message message, File outputFile, License license)
114                 throws IOException {
115             try (FileWriter writer = new FileWriter(outputFile)) {
116                 // Add the license text header.
117                 writer.append(license.getTextProtoHeader());
118 
119                 TextFormat.print(message, writer);
120             }
121         }
122 
storeProtoAsBinary(Message message, File outputFile)123         private static void storeProtoAsBinary(Message message, File outputFile)
124                 throws IOException {
125             try (FileOutputStream fileOutputStream = new FileOutputStream(outputFile)) {
126                 fileOutputStream.write(message.toByteArray());
127             }
128         }
129     }
130 
131     /** A basic pair class. */
132     public static class Pair<A, B> {
133 
134         public final A a;
135 
136         public final B b;
137 
Pair(A a, B b)138         public Pair(A a, B b) {
139             this.a = a;
140             this.b = b;
141         }
142     }
143 
144     /** A combination of a time zone ID and a list of {@link S2Polygon} instances. */
145     public static class TzS2Polygons {
146 
147         private static final String FILE_NAME_CONTENT_IDENTIFIER = "_tzs2polygons";
148 
149         public final String tzId;
150 
151         public final List<S2Polygon> s2PolygonList;
152 
153         /** Creates a new instance. */
TzS2Polygons(String tzId, List<S2Polygon> s2PolygonList)154         public TzS2Polygons(String tzId, List<S2Polygon> s2PolygonList) {
155             this.tzId = Objects.requireNonNull(tzId);
156 
157             // Defensive copy of mutable S2 objects.
158             this.s2PolygonList = new ArrayList<>();
159             s2PolygonList.forEach(x -> this.s2PolygonList.add(new S2Polygon(x)));
160         }
161 
162         @Override
equals(Object o)163         public boolean equals(Object o) {
164             if (this == o) {
165                 return true;
166             }
167             if (o == null || getClass() != o.getClass()) {
168                 return false;
169             }
170             TzS2Polygons that = (TzS2Polygons) o;
171             return tzId.equals(that.tzId)
172                     && s2PolygonsEquals(s2PolygonList, that.s2PolygonList);
173         }
174 
s2PolygonsEquals(List<S2Polygon> one, List<S2Polygon> two)175         private static boolean s2PolygonsEquals(List<S2Polygon> one, List<S2Polygon> two) {
176             if (one.size() != two.size()) {
177                 return false;
178             }
179             for (int polyIndex = 0; polyIndex < one.size(); polyIndex++) {
180                 S2Polygon onePoly = one.get(polyIndex);
181                 S2Polygon twoPoly = two.get(polyIndex);
182                 if (!s2PolygonEquals(onePoly, twoPoly)) {
183                     return false;
184                 }
185             }
186             return true;
187         }
188 
s2PolygonEquals(S2Polygon one, S2Polygon two)189         private static boolean s2PolygonEquals(S2Polygon one, S2Polygon two) {
190             if (one.numLoops() != two.numLoops()) {
191                 return false;
192             }
193 
194             for (int loopIndex = 0; loopIndex < one.numLoops(); loopIndex++) {
195                 S2Loop oneLoop = one.loop(loopIndex);
196                 S2Loop twoLoop = two.loop(loopIndex);
197                 if (!s2LoopEquals(oneLoop, twoLoop)) {
198                     return false;
199                 }
200             }
201             return true;
202         }
203 
s2LoopEquals(S2Loop one, S2Loop two)204         private static boolean s2LoopEquals(S2Loop one, S2Loop two) {
205             if (one.numVertices() != two.numVertices()) {
206                 return false;
207             }
208 
209             for (int vertexIndex = 0; vertexIndex < one.numVertices(); vertexIndex++) {
210                 S2Point onePoint = one.vertex(vertexIndex);
211                 S2Point twoPoint = two.vertex(vertexIndex);
212                 if (!onePoint.equals(twoPoint)) {
213                     return false;
214                 }
215             }
216             return true;
217         }
218 
219         @Override
hashCode()220         public int hashCode() {
221             return Objects.hash(tzId);
222         }
223 
224         @Override
toString()225         public String toString() {
226             return "TzS2Polygons{"
227                     + "tzId='" + tzId + '\''
228                     + ", s2PolygonList=" + s2PolygonList
229                     + '}';
230         }
231 
232         /**
233          * Stores the supplied object in {@code outputFile} in the specified format, adding license
234          * information to the header when possible and checking the target directory contains the
235          * necessary LICENSE file.
236          */
store(TzS2Polygons tzPolygons, File outputFile, ProtoStorageFormat storageFormat, License license)237         public static void store(TzS2Polygons tzPolygons, File outputFile,
238                 ProtoStorageFormat storageFormat, License license) throws IOException {
239             S2Protos.TzS2Polygons.Builder s2PolygonsProtoBuilder =
240                     S2Protos.TzS2Polygons.newBuilder().setTzId(tzPolygons.tzId);
241             for (S2Polygon s2Polygon : tzPolygons.s2PolygonList) {
242                 S2Protos.S2Polygon.Builder s2PolygonProtoBuilder = S2Protos.S2Polygon.newBuilder();
243                 for (int loopIndex = 0; loopIndex < s2Polygon.numLoops(); loopIndex++) {
244                     S2Loop s2Loop = s2Polygon.loop(loopIndex);
245                     S2Protos.S2Loop.Builder s2LoopProtoBuilder = S2Protos.S2Loop.newBuilder();
246                     for (int vIndex = 0; vIndex < s2Loop.numVertices(); vIndex++) {
247                         S2Point s2Point = s2Loop.vertex(vIndex);
248                         S2Protos.S2Point s2PointProto = S2Protos.S2Point.newBuilder()
249                                 .setX(s2Point.get(0))
250                                 .setY(s2Point.get(1))
251                                 .setZ(s2Point.get(2))
252                                 .build();
253                         s2LoopProtoBuilder.addVertices(s2PointProto);
254                     }
255                     s2PolygonProtoBuilder.addLoops(s2LoopProtoBuilder.build());
256                 }
257                 s2PolygonsProtoBuilder.addPolygons(s2PolygonProtoBuilder.build());
258             }
259             S2Protos.TzS2Polygons s2PolygonsProto = s2PolygonsProtoBuilder.build();
260             storageFormat.store(s2PolygonsProto, outputFile, license);
261         }
262 
263         /**
264          * Lists the files that have the file suffix returned by
265          * {@link #getFileSuffix(ProtoStorageFormat)} in the specified directory.
266          */
listFiles(File dir, ProtoStorageFormat storageFormat)267         public static List<File> listFiles(File dir, ProtoStorageFormat storageFormat) {
268             String fileSuffix = getFileSuffix(storageFormat);
269             FileFilter fileFilter = x -> x.getName().endsWith(fileSuffix);
270             return Types.listFiles(dir, fileFilter);
271         }
272 
273         /** Returns a standard suffix for this type in the specified file format. */
getFileSuffix(ProtoStorageFormat storageFormat)274         public static String getFileSuffix(ProtoStorageFormat storageFormat) {
275             return TzS2Polygons.FILE_NAME_CONTENT_IDENTIFIER + storageFormat.getTypeSuffix();
276         }
277 
278         /** Loads the {@code inputFile} in the specified format as a {@link TzS2Polygons}. */
load(File inputFile, ProtoStorageFormat storageFormat)279         public static TzS2Polygons load(File inputFile, ProtoStorageFormat storageFormat)
280                 throws IOException {
281             S2Protos.TzS2Polygons.Builder builder = S2Protos.TzS2Polygons.newBuilder();
282             storageFormat.load(builder, inputFile);
283             S2Protos.TzS2Polygons tzS2PolygonsProto = builder.build();
284 
285             List<S2Polygon> s2Polygons = new ArrayList<>();
286             for (S2Protos.S2Polygon s2PolygonProto : tzS2PolygonsProto.getPolygonsList()) {
287                 List<S2Loop> s2Loops = new ArrayList<>();
288                 for (S2Protos.S2Loop s2LoopProto : s2PolygonProto.getLoopsList()) {
289                     List<S2Point> vertices = new ArrayList<>();
290                     for (S2Protos.S2Point s2PointProto : s2LoopProto.getVerticesList()) {
291                         S2Point s2Point = new S2Point(
292                                 s2PointProto.getX(),
293                                 s2PointProto.getY(),
294                                 s2PointProto.getZ());
295                         vertices.add(s2Point);
296                     }
297                     S2Loop s2Loop = new S2Loop(vertices);
298                     s2Loops.add(s2Loop);
299                 }
300                 S2Polygon s2Polygon = new S2Polygon(s2Loops);
301                 s2Polygons.add(s2Polygon);
302             }
303             return new TzS2Polygons(tzS2PolygonsProto.getTzId(), s2Polygons);
304         }
305     }
306 
307     /** A combination of a time zone ID and a {@link S2CellUnion}. */
308     public static class TzS2CellUnion {
309 
310         private static final String FILE_NAME_CONTENT_IDENTIFIER = "_tzs2cellunion";
311 
312         public final String tzId;
313 
314         public final S2CellUnion s2CellUnion;
315 
316         /** Creates a new instance. */
TzS2CellUnion(String tzId, S2CellUnion s2CellUnion)317         public TzS2CellUnion(String tzId, S2CellUnion s2CellUnion) {
318             this.tzId = Objects.requireNonNull(tzId);
319             this.s2CellUnion = (S2CellUnion) s2CellUnion.clone();
320         }
321 
322         @Override
equals(Object o)323         public boolean equals(Object o) {
324             if (this == o) {
325                 return true;
326             }
327             if (o == null || getClass() != o.getClass()) {
328                 return false;
329             }
330             TzS2CellUnion that = (TzS2CellUnion) o;
331             return tzId.equals(that.tzId)
332                     && s2CellUnionEquals(this.s2CellUnion, that.s2CellUnion);
333         }
334 
s2CellUnionEquals(S2CellUnion one, S2CellUnion two)335         private static boolean s2CellUnionEquals(S2CellUnion one, S2CellUnion two) {
336             return one.cellIds().equals(two.cellIds());
337         }
338 
339         @Override
hashCode()340         public int hashCode() {
341             return Objects.hash(tzId);
342         }
343 
344         @Override
toString()345         public String toString() {
346             return "TzS2CellUnion{"
347                     + "tzId='" + tzId + '\'' +
348                     ", s2CellUnion=" + s2CellUnion
349                     + '}';
350         }
351 
352         /**
353          * Stores the supplied object in {@code outputFile} in the specified format, adding license
354          * information to the header when possible and checking the target directory contains the
355          * necessary LICENSE file.
356          */
store(TzS2CellUnion tzS2CellUnion, File outputFile, ProtoStorageFormat storageFormat, License license)357         public static void store(TzS2CellUnion tzS2CellUnion, File outputFile,
358                 ProtoStorageFormat storageFormat, License license) throws IOException {
359             ArrayList<S2CellId> s2CellIds = tzS2CellUnion.s2CellUnion.cellIds();
360             List<Long> cellIds = s2CellIds.stream().map(S2CellId::id).collect(toList());
361             S2Protos.TzS2CellUnion message = S2Protos.TzS2CellUnion.newBuilder()
362                     .setTzId(tzS2CellUnion.tzId)
363                     .addAllCellIds(cellIds)
364                     .build();
365             storageFormat.store(message, outputFile, license);
366         }
367 
368         /** Loads the {@code inputFile} in the specified format as a {@link TzS2CellUnion}. */
load(File file, ProtoStorageFormat storageFormat)369         public static TzS2CellUnion load(File file, ProtoStorageFormat storageFormat) {
370             S2Protos.TzS2CellUnion.Builder builder = S2Protos.TzS2CellUnion.newBuilder();
371             try {
372                 storageFormat.load(builder, file);
373             } catch (IOException e) {
374                 throw new RuntimeException(e);
375             }
376 
377             S2CellUnion s2CellUnion = new S2CellUnion();
378             List<S2CellId> cellIds =
379                     builder.getCellIdsList().stream().map(S2CellId::new).collect(toList());
380             s2CellUnion.initFromCellIds(new ArrayList<>(cellIds));
381             return new TzS2CellUnion(builder.getTzId(), s2CellUnion);
382         }
383 
384         /**
385          * Lists the files that have the file suffix returned by
386          * {@link #getFileSuffix(ProtoStorageFormat)} in the specified directory.
387          */
listFiles(File dir, ProtoStorageFormat storageFormat)388         public static List<File> listFiles(File dir, ProtoStorageFormat storageFormat) {
389             String fileSuffix = getFileSuffix(storageFormat);
390             FileFilter fileFilter = x -> x.getName().endsWith(fileSuffix);
391             return Types.listFiles(dir, fileFilter);
392         }
393 
394         /** Returns a standard suffix for this type in the specified file format. */
getFileSuffix(ProtoStorageFormat storageFormat)395         public static String getFileSuffix(ProtoStorageFormat storageFormat) {
396             return FILE_NAME_CONTENT_IDENTIFIER + storageFormat.getTypeSuffix();
397         }
398     }
399 
400     /** A <em>mutable</em> container for a list of {@link TzS2Range} instances. */
401     public static class TzS2Ranges {
402 
403         private static final String FILE_NAME_CONTENT_IDENTIFIER = "_tzs2ranges";
404 
405         private final List<TzS2Range> mValues;
406 
TzS2Ranges(List<TzS2Range> values)407         public TzS2Ranges(List<TzS2Range> values) {
408             this.mValues = new ArrayList<>(values);
409         }
410 
411         /**
412          * Replaces the {@link TzS2Range} with the specified index with a new range. Returns the
413          * old range at the index.
414          */
replace(int i, TzS2Range newRange)415         public TzS2Range replace(int i, TzS2Range newRange) {
416             return mValues.set(i, newRange);
417         }
418 
419         /** Returns the {@link TzS2Range} at the specified index. */
get(int i)420         public TzS2Range get(int i) {
421             return mValues.get(i);
422         }
423 
424         /** Returns the number of {@link TzS2Range} instances. */
size()425         public int size() {
426             return mValues.size();
427         }
428 
429         /** Returns (a copy of) the list of {@link TzS2Range} instances held. */
getAll()430         public List<TzS2Range> getAll() {
431             return new ArrayList<>(mValues);
432         }
433 
434         @Override
equals(Object o)435         public boolean equals(Object o) {
436             if (this == o) {
437                 return true;
438             }
439             if (o == null || getClass() != o.getClass()) {
440                 return false;
441             }
442             TzS2Ranges that = (TzS2Ranges) o;
443             return mValues.equals(that.mValues);
444         }
445 
446         @Override
hashCode()447         public int hashCode() {
448             return Objects.hash(mValues);
449         }
450 
451         @Override
toString()452         public String toString() {
453             return "TzS2Ranges{"
454                     + "values=" + mValues
455                     + '}';
456         }
457 
458         /** Loads the {@code inputFile} in the specified format as a {@link TzS2Ranges}. */
load(File inputFile, ProtoStorageFormat storageFormat)459         public static TzS2Ranges load(File inputFile, ProtoStorageFormat storageFormat) {
460             S2Protos.TzS2Ranges.Builder builder = S2Protos.TzS2Ranges.newBuilder();
461             try {
462                 storageFormat.load(builder, inputFile);
463             } catch (IOException e) {
464                 throw new RuntimeException("Failure while reading " + inputFile, e);
465             }
466 
467             List<TzS2Range> values = builder.getRangesList().stream()
468                     .map(x -> new TzS2Range(
469                             x.getValuesList(),
470                             new S2CellId(x.getStartCellId()),
471                             new S2CellId(x.getEndCellId())))
472                     .collect(toList());
473             return new TzS2Ranges(values);
474         }
475 
476         /**
477          * Stores the supplied object in {@code outputFile} in the specified format, adding license
478          * information to the header when possible and checking the target directory contains the
479          * necessary LICENSE file.
480          */
store(TzS2Ranges ranges, File outputFile, ProtoStorageFormat storageFormat, License license)481         public static void store(TzS2Ranges ranges, File outputFile,
482                 ProtoStorageFormat storageFormat, License license) throws IOException {
483             List<S2Protos.TzS2Range> rangeProtos = ranges.mValues.stream()
484                     .map(TzS2Range::createTzS2RangeProto)
485                     .collect(toList());
486             S2Protos.TzS2Ranges message = S2Protos.TzS2Ranges.newBuilder()
487                     .addAllRanges(rangeProtos)
488                     .build();
489             storageFormat.store(message, outputFile, license);
490         }
491 
492         /**
493          * Lists the files that have the file suffix returned by
494          * {@link #getFileSuffix(ProtoStorageFormat)} in the specified directory.
495          */
listFiles(File dir, ProtoStorageFormat storageFormat)496         public static List<File> listFiles(File dir, ProtoStorageFormat storageFormat) {
497             FileFilter fileFilter = x -> x.getName().endsWith(getFileSuffix(storageFormat));
498             return Types.listFiles(dir, fileFilter);
499         }
500 
501         /** Returns a standard suffix for this type in the specified file format. */
getFileSuffix(ProtoStorageFormat storageFormat)502         public static String getFileSuffix(ProtoStorageFormat storageFormat) {
503             return FILE_NAME_CONTENT_IDENTIFIER + storageFormat.getTypeSuffix();
504         }
505     }
506 
507     /**
508      * A range of S2 cell IDs at a fixed S2 level associated with a list of time zone IDs. The range
509      * is expressed as a start cell ID (inclusive) and an end cell ID (exclusive).
510      */
511     public static class TzS2Range {
512 
513         public final List<String> tzIds;
514 
515         public final S2CellId rangeStart;
516 
517         public final S2CellId rangeEnd;
518 
519         /**
520          * Creates an instance. If the range is invalid or the cell IDs are from different levels
521          * this method throws an {@link IllegalArgumentException}.
522          */
TzS2Range(List<String> tzIds, S2CellId rangeStart, S2CellId rangeEnd)523         public TzS2Range(List<String> tzIds, S2CellId rangeStart, S2CellId rangeEnd) {
524             this.tzIds = new ArrayList<>(tzIds);
525             this.rangeStart = Objects.requireNonNull(rangeStart);
526             this.rangeEnd = Objects.requireNonNull(rangeEnd);
527             if (rangeStart.level() != rangeEnd.level()) {
528                 throw new IllegalArgumentException(
529                         "Levels differ: rangeStart=" + rangeStart + ", rangeEnd=" + rangeEnd);
530             }
531             if (rangeStart.greaterOrEquals(rangeEnd)) {
532                 throw new IllegalArgumentException(
533                         "Range start (" + rangeStart + " >= range end (" + rangeEnd + ")");
534             }
535         }
536 
createTzS2RangeProto(TzS2Range x)537         private static S2Protos.TzS2Range createTzS2RangeProto(TzS2Range x) {
538             return S2Protos.TzS2Range.newBuilder()
539                     .setStartCellId(x.rangeStart.id())
540                     .setEndCellId(x.rangeEnd.id())
541                     .addAllValues(x.tzIds)
542                     .build();
543         }
544 
545         /**
546          * Returns the number of cells in this range.
547          */
size()548         public long size() {
549             // Convert into unsigned values.
550             final long rangeStartVal = rangeStart.id() >>> 1;
551             final long rangeEndVal = rangeEnd.id() >>> 1;
552             // Since we shifted the above values right, we need not shift the lowestOnBit left to
553             // obtain the difference between adjacent S2 cells at the given S2 level.
554             final long increment = rangeStart.lowestOnBit();
555             return (rangeEndVal - rangeStartVal) / increment;
556         }
557 
558         @Override
equals(Object o)559         public boolean equals(Object o) {
560             if (this == o) {
561                 return true;
562             }
563             if (o == null || getClass() != o.getClass()) {
564                 return false;
565             }
566             TzS2Range tzS2Range = (TzS2Range) o;
567             return tzIds.equals(tzS2Range.tzIds)
568                     && rangeStart.equals(tzS2Range.rangeStart)
569                     && rangeEnd.equals(tzS2Range.rangeEnd);
570         }
571 
572         @Override
hashCode()573         public int hashCode() {
574             return Objects.hash(tzIds, rangeStart, rangeEnd);
575         }
576 
577         @Override
toString()578         public String toString() {
579             return "TzS2Range{"
580                     + " value=" + tzIds
581                     + ", rangeStart=" + rangeStart
582                     + ", rangeEnd=" + rangeEnd
583                     + '}';
584         }
585     }
586 
587     /** Returns a list of files that match the supplied {@link FileFilter}. */
listFiles(File dir, FileFilter fileFilter)588     private static List<File> listFiles(File dir, FileFilter fileFilter) {
589         File[] files = dir.listFiles(fileFilter);
590         if (files == null) {
591             return Collections.emptyList();
592         }
593 
594         // Sort the results as listFiles() isn't deterministic, which can lead to variations
595         // in output that (for example) cause tests to break occasionally.
596         Arrays.sort(files);
597 
598         return Arrays.asList(files);
599     }
600 }
601