ConfigItem upgrade
[nikiroo-utils.git] / src / be / nikiroo / utils / resources / Bundle.java
CommitLineData
ec1f3444
NR
1package be.nikiroo.utils.resources;
2
3import java.io.BufferedWriter;
4import java.io.File;
5import java.io.FileInputStream;
6import java.io.FileOutputStream;
7import java.io.IOException;
8import java.io.InputStreamReader;
9import java.io.OutputStreamWriter;
10import java.io.Reader;
ec1f3444
NR
11import java.io.Writer;
12import java.lang.reflect.Field;
13import java.util.ArrayList;
62c9ec78 14import java.util.HashMap;
ec1f3444
NR
15import java.util.List;
16import java.util.Locale;
62c9ec78 17import java.util.Map;
ec1f3444
NR
18import java.util.MissingResourceException;
19import java.util.PropertyResourceBundle;
20import java.util.ResourceBundle;
21
ec1f3444 22/**
62c9ec78 23 * This class encapsulate a {@link ResourceBundle} in UTF-8. It allows to
ec1f3444
NR
24 * retrieve values associated to an enumeration, and allows some additional
25 * methods.
62c9ec78
NR
26 * <p>
27 * It also sports a writable change map, and you can save back the
28 * {@link Bundle} to file with {@link Bundle#updateFile(String)}.
ec1f3444 29 *
ec1f3444
NR
30 * @param <E>
31 * the enum to use to get values out of this class
db31c358
NR
32 *
33 * @author niki
ec1f3444 34 */
db31c358 35
ec1f3444 36public class Bundle<E extends Enum<E>> {
db31c358 37 /** The type of E. */
ec1f3444 38 protected Class<E> type;
db31c358
NR
39 /**
40 * The {@link Enum} associated to this {@link Bundle} (all the keys used in
41 * this {@link Bundle} will be of this type).
42 */
43 protected Enum<?> keyType;
44
45 private TransBundle<E> descriptionBundle;
46
47 /** R/O map */
48 private Map<String, String> map;
49 /** R/W map */
50 private Map<String, String> changeMap;
ec1f3444
NR
51
52 /**
53 * Create a new {@link Bundles} of the given name.
54 *
55 * @param type
56 * a runtime instance of the class of E
ec1f3444
NR
57 * @param name
58 * the name of the {@link Bundles}
db31c358
NR
59 * @param descriptionBundle
60 * the description {@link TransBundle}, that is, a
61 * {@link TransBundle} dedicated to the description of the values
62 * of the given {@link Bundle} (can be NULL)
ec1f3444 63 */
db31c358
NR
64 protected Bundle(Class<E> type, Enum<?> name,
65 TransBundle<E> descriptionBundle) {
ec1f3444 66 this.type = type;
db31c358
NR
67 this.keyType = name;
68 this.descriptionBundle = descriptionBundle;
69
487926f7 70 this.map = new HashMap<String, String>();
62c9ec78 71 this.changeMap = new HashMap<String, String>();
e9ca6bb8 72 setBundle(name, Locale.getDefault(), false);
ec1f3444
NR
73 }
74
75 /**
76 * Return the value associated to the given id as a {@link String}.
77 *
db31c358 78 * @param id
ec1f3444
NR
79 * the id of the value to get
80 *
81 * @return the associated value, or NULL if not found (not present in the
82 * resource file)
83 */
84 public String getString(E id) {
487926f7 85 return getString(id.name());
ec1f3444
NR
86 }
87
62c9ec78
NR
88 /**
89 * Set the value associated to the given id as a {@link String}.
90 *
db31c358 91 * @param id
3bfc1d20 92 * the id of the value to set
62c9ec78
NR
93 * @param value
94 * the value
95 *
96 */
97 public void setString(E id, String value) {
487926f7 98 setString(id.name(), value);
62c9ec78
NR
99 }
100
ec1f3444
NR
101 /**
102 * Return the value associated to the given id as a {@link String} suffixed
103 * with the runtime value "_suffix" (that is, "_" and suffix).
487926f7
NR
104 * <p>
105 * Will only accept suffixes that form an existing id.
ec1f3444 106 *
db31c358 107 * @param id
ec1f3444
NR
108 * the id of the value to get
109 * @param suffix
110 * the runtime suffix
111 *
112 * @return the associated value, or NULL if not found (not present in the
113 * resource file)
114 */
115 public String getStringX(E id, String suffix) {
116 String key = id.name()
80383c14 117 + (suffix == null ? "" : "_" + suffix.toUpperCase());
ec1f3444 118
487926f7
NR
119 try {
120 id = Enum.valueOf(type, key);
121 return getString(id);
122 } catch (IllegalArgumentException e) {
123
ec1f3444
NR
124 }
125
126 return null;
127 }
128
62c9ec78
NR
129 /**
130 * Set the value associated to the given id as a {@link String} suffixed
131 * with the runtime value "_suffix" (that is, "_" and suffix).
487926f7
NR
132 * <p>
133 * Will only accept suffixes that form an existing id.
62c9ec78 134 *
db31c358 135 * @param id
3bfc1d20 136 * the id of the value to set
62c9ec78
NR
137 * @param suffix
138 * the runtime suffix
139 * @param value
140 * the value
141 */
142 public void setStringX(E id, String suffix, String value) {
143 String key = id.name()
144 + (suffix == null ? "" : "_" + suffix.toUpperCase());
145
487926f7
NR
146 try {
147 id = Enum.valueOf(type, key);
148 setString(id, value);
149 } catch (IllegalArgumentException e) {
150
151 }
62c9ec78
NR
152 }
153
ec1f3444
NR
154 /**
155 * Return the value associated to the given id as a {@link Boolean}.
156 *
db31c358 157 * @param id
ec1f3444
NR
158 * the id of the value to get
159 *
160 * @return the associated value
161 */
162 public Boolean getBoolean(E id) {
163 String str = getString(id);
3bfc1d20 164 return BundleHelper.parseBoolean(str);
ec1f3444
NR
165 }
166
167 /**
62c9ec78 168 * Return the value associated to the given id as a {@link Boolean}.
ec1f3444 169 *
db31c358 170 * @param id
ec1f3444
NR
171 * the id of the value to get
172 * @param def
173 * the default value when it is not present in the config file or
174 * if it is not a boolean value
175 *
176 * @return the associated value
177 */
178 public boolean getBoolean(E id, boolean def) {
179 Boolean b = getBoolean(id);
180 if (b != null)
181 return b;
182
183 return def;
184 }
d18e136e 185
3bfc1d20
NR
186 /**
187 * Set the value associated to the given id as a {@link Boolean}.
188 *
189 * @param id
190 * the id of the value to set
191 * @param value
192 * the value
193 *
194 */
195 public void setBoolean(E id, boolean value) {
196 setString(id.name(), BundleHelper.fromBoolean(value));
197 }
198
ec1f3444
NR
199 /**
200 * Return the value associated to the given id as an {@link Integer}.
201 *
db31c358 202 * @param id
ec1f3444
NR
203 * the id of the value to get
204 *
205 * @return the associated value
206 */
207 public Integer getInteger(E id) {
3bfc1d20 208 return BundleHelper.parseInteger(getString(id));
ec1f3444
NR
209 }
210
211 /**
db31c358 212 * Return the value associated to the given id as an int.
ec1f3444 213 *
db31c358 214 * @param id
ec1f3444
NR
215 * the id of the value to get
216 * @param def
217 * the default value when it is not present in the config file or
218 * if it is not a int value
219 *
220 * @return the associated value
221 */
222 public int getInteger(E id, int def) {
223 Integer i = getInteger(id);
224 if (i != null)
225 return i;
226
227 return def;
228 }
229
3bfc1d20
NR
230 /**
231 * Set the value associated to the given id as a {@link Integer}.
232 *
233 * @param id
234 * the id of the value to set
235 * @param value
236 * the value
237 *
238 */
239 public void setInteger(E id, int value) {
240 setString(id.name(), BundleHelper.fromInteger(value));
241 }
d18e136e 242
ec1f3444
NR
243 /**
244 * Return the value associated to the given id as a {@link Character}.
245 *
db31c358 246 * @param id
ec1f3444
NR
247 * the id of the value to get
248 *
249 * @return the associated value
250 */
62c9ec78 251 public Character getCharacter(E id) {
3bfc1d20 252 return BundleHelper.parseCharacter(getString(id));
62c9ec78
NR
253 }
254
255 /**
256 * Return the value associated to the given id as a {@link Character}.
257 *
db31c358 258 * @param id
62c9ec78
NR
259 * the id of the value to get
260 * @param def
261 * the default value when it is not present in the config file or
262 * if it is not a char value
263 *
264 * @return the associated value
265 */
266 public char getCharacter(E id, char def) {
3bfc1d20
NR
267 Character car = getCharacter(id);
268 if (car != null)
269 return car;
62c9ec78
NR
270
271 return def;
ec1f3444
NR
272 }
273
b3aad1f9 274 /**
80500544
NR
275 * Return the value associated to the given id as a colour if it is found
276 * and can be parsed.
277 * <p>
278 * The returned value is an ARGB value.
b3aad1f9 279 *
db31c358
NR
280 * @param id
281 * the id of the value to get
b3aad1f9
NR
282 *
283 * @return the associated value
284 */
80500544 285 public Integer getColor(E id) {
3bfc1d20 286 return BundleHelper.parseColor(getString(id));
b3aad1f9
NR
287 }
288
62c9ec78 289 /**
80500544
NR
290 * Set the value associated to the given id as a colour.
291 * <p>
3bfc1d20 292 * The value is a BGRA value.
62c9ec78 293 *
db31c358
NR
294 * @param id
295 * the id of the value to set
296 * @param color
80500544 297 * the new colour
62c9ec78 298 */
80500544 299 public void setColor(E id, Integer color) {
9e834013 300 setString(id, BundleHelper.fromColor(color));
62c9ec78
NR
301 }
302
7c192d3c
NR
303 /**
304 * Return the value associated to the given id as a list of values if it is
305 * found and can be parsed.
306 *
307 * @param id
308 * the id of the value to get
309 *
310 * @return the associated list, empty if the value is empty, NULL if it is
311 * not found or cannot be parsed as a list
312 */
313 public List<String> getList(E id) {
3bfc1d20 314 return BundleHelper.parseList(getString(id));
7c192d3c
NR
315 }
316
317 /**
318 * Set the value associated to the given id as a list of values.
319 *
320 * @param id
321 * the id of the value to set
322 * @param list
323 * the new list of values
324 */
325 public void setList(E id, List<String> list) {
3bfc1d20 326 setString(id, BundleHelper.fromList(list));
7c192d3c
NR
327 }
328
487926f7
NR
329 /**
330 * Create/update the .properties file.
331 * <p>
332 * Will use the most likely candidate as base if the file does not already
333 * exists and this resource is translatable (for instance, "en_US" will use
334 * "en" as a base if the resource is a translation file).
335 * <p>
336 * Will update the files in {@link Bundles#getDirectory()}; it <b>MUST</b>
337 * be set.
338 *
339 * @throws IOException
340 * in case of IO errors
341 */
342 public void updateFile() throws IOException {
343 updateFile(Bundles.getDirectory());
344 }
345
ec1f3444 346 /**
80383c14
NR
347 * Create/update the .properties file.
348 * <p>
349 * Will use the most likely candidate as base if the file does not already
350 * exists and this resource is translatable (for instance, "en_US" will use
351 * "en" as a base if the resource is a translation file).
ec1f3444
NR
352 *
353 * @param path
487926f7
NR
354 * the path where the .properties files are, <b>MUST NOT</b> be
355 * NULL
ec1f3444
NR
356 *
357 * @throws IOException
358 * in case of IO errors
359 */
360 public void updateFile(String path) throws IOException {
361 File file = getUpdateFile(path);
362
363 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
364 new FileOutputStream(file), "UTF-8"));
365
366 writeHeader(writer);
367 writer.write("\n");
368 writer.write("\n");
369
370 for (Field field : type.getDeclaredFields()) {
371 Meta meta = field.getAnnotation(Meta.class);
372 if (meta != null) {
cd0c27d2 373 E id = Enum.valueOf(type, field.getName());
ec1f3444
NR
374 String info = getMetaInfo(meta);
375
376 if (info != null) {
377 writer.write(info);
378 writer.write("\n");
379 }
380
381 writeValue(writer, id);
382 }
383 }
384
385 writer.close();
386 }
387
e8aa5bf9
NR
388 /**
389 * Delete the .properties file.
390 * <p>
391 * Will use the most likely candidate as base if the file does not already
392 * exists and this resource is translatable (for instance, "en_US" will use
393 * "en" as a base if the resource is a translation file).
394 * <p>
395 * Will delete the files in {@link Bundles#getDirectory()}; it <b>MUST</b>
396 * be set.
397 *
398 * @return TRUE if the file was deleted
399 */
400 public boolean deleteFile() {
401 return deleteFile(Bundles.getDirectory());
402 }
403
404 /**
405 * Delete the .properties file.
406 * <p>
407 * Will use the most likely candidate as base if the file does not already
408 * exists and this resource is translatable (for instance, "en_US" will use
409 * "en" as a base if the resource is a translation file).
410 *
411 * @param path
412 * the path where the .properties files are, <b>MUST NOT</b> be
413 * NULL
414 *
415 * @return TRUE if the file was deleted
416 */
417 public boolean deleteFile(String path) {
418 File file = getUpdateFile(path);
419 return file.delete();
420 }
421
db31c358
NR
422 /**
423 * The description {@link TransBundle}, that is, a {@link TransBundle}
424 * dedicated to the description of the values of the given {@link Bundle}
425 * (can be NULL).
426 *
427 * @return the description {@link TransBundle}
428 */
429 public TransBundle<E> getDescriptionBundle() {
430 return descriptionBundle;
431 }
432
80383c14
NR
433 /**
434 * Reload the {@link Bundle} data files.
e9ca6bb8
NR
435 *
436 * @param resetToDefault
437 * reset to the default configuration (do not look into the
438 * possible user configuration files, only take the original
439 * configuration)
80383c14 440 */
e9ca6bb8 441 public void reload(boolean resetToDefault) {
db31c358 442 setBundle(keyType, Locale.getDefault(), resetToDefault);
80383c14
NR
443 }
444
ec1f3444
NR
445 /**
446 * Check if the internal map contains the given key.
447 *
448 * @param key
449 * the key to check for
450 *
451 * @return true if it does
452 */
453 protected boolean containsKey(String key) {
487926f7 454 return changeMap.containsKey(key) || map.containsKey(key);
ec1f3444
NR
455 }
456
457 /**
2cce3dcb
NR
458 * Get the value for the given key if it exists in the internal map, or NULL
459 * if not.
ec1f3444
NR
460 *
461 * @param key
462 * the key to check for
463 *
2cce3dcb 464 * @return the value, or NULL
ec1f3444
NR
465 */
466 protected String getString(String key) {
62c9ec78
NR
467 if (changeMap.containsKey(key)) {
468 return changeMap.get(key);
469 }
470
487926f7
NR
471 if (map.containsKey(key)) {
472 return map.get(key);
ec1f3444
NR
473 }
474
475 return null;
476 }
477
62c9ec78
NR
478 /**
479 * Set the value for this key, in the change map (it is kept in memory, not
480 * yet on disk).
481 *
482 * @param key
483 * the key
484 * @param value
485 * the associated value
486 */
487 protected void setString(String key, String value) {
487926f7 488 changeMap.put(key, value == null ? null : value.trim());
62c9ec78
NR
489 }
490
ec1f3444
NR
491 /**
492 * Return formated, display-able information from the {@link Meta} field
493 * given. Each line will always starts with a "#" character.
494 *
495 * @param meta
496 * the {@link Meta} field
497 *
498 * @return the information to display or NULL if none
499 */
500 protected String getMetaInfo(Meta meta) {
db31c358
NR
501 String desc = meta.description();
502 boolean group = meta.group();
503 Meta.Format format = meta.format();
504 String[] list = meta.list();
505 boolean nullable = meta.nullable();
e8aa5bf9 506 String def = meta.def();
db31c358 507 boolean array = meta.array();
ec1f3444 508
db31c358 509 // Default, empty values -> NULL
d18e136e
NR
510 if (desc.length() + list.length + def.length() == 0 && !group
511 && nullable && format == Meta.Format.STRING) {
ec1f3444 512 return null;
db31c358 513 }
ec1f3444
NR
514
515 StringBuilder builder = new StringBuilder();
db31c358
NR
516 builder.append("# ").append(desc);
517 if (desc.length() > 20) {
518 builder.append("\n#");
519 }
ec1f3444 520
db31c358
NR
521 if (group) {
522 builder.append("This item is used as a group, its content is not expected to be used.");
523 } else {
524 builder.append(" (FORMAT: ").append(format)
525 .append(nullable ? "" : " (required)");
d18e136e 526 builder.append(") ");
db31c358
NR
527
528 if (list.length > 0) {
529 builder.append("\n# ALLOWED VALUES:");
530 for (String value : list) {
531 builder.append(" \"").append(value).append("\"");
532 }
ec1f3444
NR
533 }
534
db31c358
NR
535 if (array) {
536 builder.append("\n# (This item accept a list of comma-separated values)");
ec1f3444
NR
537 }
538 }
539
ec1f3444
NR
540 return builder.toString();
541 }
542
543 /**
544 * The display name used in the <tt>.properties file</tt>.
545 *
546 * @return the name
547 */
548 protected String getBundleDisplayName() {
db31c358 549 return keyType.toString();
ec1f3444
NR
550 }
551
552 /**
553 * Write the header found in the configuration <tt>.properties</tt> file of
554 * this {@link Bundles}.
555 *
556 * @param writer
557 * the {@link Writer} to write the header in
558 *
559 * @throws IOException
560 * in case of IO error
561 */
562 protected void writeHeader(Writer writer) throws IOException {
563 writer.write("# " + getBundleDisplayName() + "\n");
564 writer.write("#\n");
565 }
566
567 /**
568 * Write the given id to the config file, i.e., "MY_ID = my_curent_value"
569 * followed by a new line
570 *
571 * @param writer
572 * the {@link Writer} to write into
573 * @param id
574 * the id to write
575 *
576 * @throws IOException
577 * in case of IO error
578 */
579 protected void writeValue(Writer writer, E id) throws IOException {
580 writeValue(writer, id.name(), getString(id));
581 }
582
583 /**
584 * Write the given data to the config file, i.e., "MY_ID = my_curent_value"
585 * followed by a new line
586 *
587 * @param writer
588 * the {@link Writer} to write into
589 * @param id
590 * the id to write
591 * @param value
592 * the id's value
593 *
594 * @throws IOException
595 * in case of IO error
596 */
597 protected void writeValue(Writer writer, String id, String value)
598 throws IOException {
599 writer.write(id);
600 writer.write(" = ");
601
602 if (value == null) {
603 value = "";
604 }
605
009196a4 606 String[] lines = value.replaceAll("\t", "\\\\\\t").split("\n");
ec1f3444
NR
607 for (int i = 0; i < lines.length; i++) {
608 writer.write(lines[i]);
609 if (i < lines.length - 1) {
610 writer.write("\\n\\");
611 }
612 writer.write("\n");
613 }
614 }
615
616 /**
617 * Return the source file for this {@link Bundles} from the given path.
618 *
619 * @param path
620 * the path where the .properties files are
621 *
622 * @return the source {@link File}
ec1f3444
NR
623 */
624 protected File getUpdateFile(String path) {
db31c358 625 return new File(path, keyType.name() + ".properties");
ec1f3444
NR
626 }
627
628 /**
62c9ec78 629 * Change the currently used bundle, and reset all changes.
ec1f3444
NR
630 *
631 * @param name
632 * the name of the bundle to load
633 * @param locale
634 * the {@link Locale} to use
e9ca6bb8
NR
635 * @param resetToDefault
636 * reset to the default configuration (do not look into the
637 * possible user configuration files, only take the original
638 * configuration)
ec1f3444 639 */
e9ca6bb8 640 protected void setBundle(Enum<?> name, Locale locale, boolean resetToDefault) {
62c9ec78 641 changeMap.clear();
ec1f3444 642 String dir = Bundles.getDirectory();
e8aa5bf9 643 String bname = type.getPackage().getName() + "." + name.name();
ec1f3444 644
487926f7 645 boolean found = false;
e9ca6bb8 646 if (!resetToDefault && dir != null) {
e8aa5bf9 647 // Look into Bundles.getDirectory() for .properties files
ec1f3444
NR
648 try {
649 File file = getPropertyFile(dir, name.name(), locale);
650 if (file != null) {
651 Reader reader = new InputStreamReader(new FileInputStream(
652 file), "UTF8");
487926f7
NR
653 resetMap(new PropertyResourceBundle(reader));
654 found = true;
ec1f3444
NR
655 }
656 } catch (IOException e) {
657 e.printStackTrace();
658 }
659 }
660
487926f7 661 if (!found) {
e8aa5bf9 662 // Look into the package itself for resources
e9ca6bb8 663 try {
487926f7
NR
664 resetMap(ResourceBundle
665 .getBundle(bname, locale, type.getClassLoader(),
666 new FixedResourceBundleControl()));
e8aa5bf9
NR
667 found = true;
668 } catch (MissingResourceException e) {
e9ca6bb8 669 } catch (Exception e) {
e8aa5bf9 670 e.printStackTrace();
487926f7
NR
671 }
672 }
e8aa5bf9
NR
673
674 if (!found) {
675 // We have no bundle for this Bundle
676 System.err.println("No bundle found for: " + bname);
677 resetMap(null);
678 }
487926f7
NR
679 }
680
681 /**
e8aa5bf9
NR
682 * Reset the backing map to the content of the given bundle, or with default
683 * valiues if bundle is NULL.
487926f7
NR
684 *
685 * @param bundle
686 * the bundle to copy
687 */
b771aed5 688 protected void resetMap(ResourceBundle bundle) {
487926f7 689 this.map.clear();
e8aa5bf9
NR
690 for (Field field : type.getDeclaredFields()) {
691 try {
692 Meta meta = field.getAnnotation(Meta.class);
693 if (meta != null) {
694 E id = Enum.valueOf(type, field.getName());
695
696 String value;
697 if (bundle != null) {
698 value = bundle.getString(id.name());
699 } else {
700 value = meta.def();
701 }
702
703 this.map.put(id.name(), value == null ? null : value.trim());
487926f7 704 }
e8aa5bf9 705 } catch (MissingResourceException e) {
e9ca6bb8
NR
706 }
707 }
708 }
cd0c27d2 709
e9ca6bb8
NR
710 /**
711 * Take a snapshot of the changes in memory in this {@link Bundle} made by
712 * the "set" methods ( {@link Bundle#setString(Enum, String)}...) at the
713 * current time.
714 *
487926f7 715 * @return a snapshot to use with {@link Bundle#restoreSnapshot(Object)}
e9ca6bb8 716 */
487926f7 717 public Object takeSnapshot() {
e9ca6bb8
NR
718 return new HashMap<String, String>(changeMap);
719 }
720
721 /**
722 * Restore a snapshot taken with {@link Bundle}, or reset the current
723 * changes if the snapshot is NULL.
724 *
725 * @param snap
726 * the snapshot or NULL
727 */
728 @SuppressWarnings("unchecked")
487926f7 729 public void restoreSnapshot(Object snap) {
e9ca6bb8
NR
730 if (snap == null) {
731 changeMap.clear();
732 } else {
733 if (snap instanceof Map) {
734 changeMap = (Map<String, String>) snap;
735 } else {
d827da2a 736 throw new RuntimeException(
e9ca6bb8
NR
737 "Restoring changes in a Bundle must be done on a changes snapshot, "
738 + "or NULL to discard current changes");
739 }
ec1f3444
NR
740 }
741 }
742
743 /**
744 * Return the resource file that is closer to the {@link Locale}.
745 *
746 * @param dir
db31c358 747 * the directory to look into
ec1f3444 748 * @param name
db31c358 749 * the file base name (without <tt>.properties</tt>)
ec1f3444
NR
750 * @param locale
751 * the {@link Locale}
752 *
753 * @return the closest match or NULL if none
754 */
755 private File getPropertyFile(String dir, String name, Locale locale) {
756 List<String> locales = new ArrayList<String>();
757 if (locale != null) {
758 String country = locale.getCountry() == null ? "" : locale
759 .getCountry();
760 String language = locale.getLanguage() == null ? "" : locale
761 .getLanguage();
762 if (!language.isEmpty() && !country.isEmpty()) {
763 locales.add("_" + language + "-" + country);
764 }
765 if (!language.isEmpty()) {
766 locales.add("_" + language);
767 }
768 }
769
770 locales.add("");
771
772 File file = null;
773 for (String loc : locales) {
774 file = new File(dir, name + loc + ".properties");
775 if (file.exists()) {
776 break;
ec1f3444 777 }
d827da2a 778
cd0c27d2 779 file = null;
ec1f3444
NR
780 }
781
782 return file;
783 }
784}