jDoc typo
[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 }
3bfc1d20
NR
185
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 /**
201 * Return the value associated to the given id as an {@link Integer}.
202 *
db31c358 203 * @param id
ec1f3444
NR
204 * the id of the value to get
205 *
206 * @return the associated value
207 */
208 public Integer getInteger(E id) {
3bfc1d20 209 return BundleHelper.parseInteger(getString(id));
ec1f3444
NR
210 }
211
212 /**
db31c358 213 * Return the value associated to the given id as an int.
ec1f3444 214 *
db31c358 215 * @param id
ec1f3444
NR
216 * the id of the value to get
217 * @param def
218 * the default value when it is not present in the config file or
219 * if it is not a int value
220 *
221 * @return the associated value
222 */
223 public int getInteger(E id, int def) {
224 Integer i = getInteger(id);
225 if (i != null)
226 return i;
227
228 return def;
229 }
230
3bfc1d20
NR
231 /**
232 * Set the value associated to the given id as a {@link Integer}.
233 *
234 * @param id
235 * the id of the value to set
236 * @param value
237 * the value
238 *
239 */
240 public void setInteger(E id, int value) {
241 setString(id.name(), BundleHelper.fromInteger(value));
242 }
243
ec1f3444
NR
244 /**
245 * Return the value associated to the given id as a {@link Character}.
246 *
db31c358 247 * @param id
ec1f3444
NR
248 * the id of the value to get
249 *
250 * @return the associated value
251 */
62c9ec78 252 public Character getCharacter(E id) {
3bfc1d20 253 return BundleHelper.parseCharacter(getString(id));
62c9ec78
NR
254 }
255
256 /**
257 * Return the value associated to the given id as a {@link Character}.
258 *
db31c358 259 * @param id
62c9ec78
NR
260 * the id of the value to get
261 * @param def
262 * the default value when it is not present in the config file or
263 * if it is not a char value
264 *
265 * @return the associated value
266 */
267 public char getCharacter(E id, char def) {
3bfc1d20
NR
268 Character car = getCharacter(id);
269 if (car != null)
270 return car;
62c9ec78
NR
271
272 return def;
ec1f3444
NR
273 }
274
b3aad1f9 275 /**
80500544
NR
276 * Return the value associated to the given id as a colour if it is found
277 * and can be parsed.
278 * <p>
279 * The returned value is an ARGB value.
b3aad1f9 280 *
db31c358
NR
281 * @param id
282 * the id of the value to get
b3aad1f9
NR
283 *
284 * @return the associated value
285 */
80500544 286 public Integer getColor(E id) {
3bfc1d20 287 return BundleHelper.parseColor(getString(id));
b3aad1f9
NR
288 }
289
62c9ec78 290 /**
80500544
NR
291 * Set the value associated to the given id as a colour.
292 * <p>
3bfc1d20 293 * The value is a BGRA value.
62c9ec78 294 *
db31c358
NR
295 * @param id
296 * the id of the value to set
297 * @param color
80500544 298 * the new colour
62c9ec78 299 */
80500544 300 public void setColor(E id, Integer color) {
9e834013 301 setString(id, BundleHelper.fromColor(color));
62c9ec78
NR
302 }
303
7c192d3c
NR
304 /**
305 * Return the value associated to the given id as a list of values if it is
306 * found and can be parsed.
307 *
308 * @param id
309 * the id of the value to get
310 *
311 * @return the associated list, empty if the value is empty, NULL if it is
312 * not found or cannot be parsed as a list
313 */
314 public List<String> getList(E id) {
3bfc1d20 315 return BundleHelper.parseList(getString(id));
7c192d3c
NR
316 }
317
318 /**
319 * Set the value associated to the given id as a list of values.
320 *
321 * @param id
322 * the id of the value to set
323 * @param list
324 * the new list of values
325 */
326 public void setList(E id, List<String> list) {
3bfc1d20 327 setString(id, BundleHelper.fromList(list));
7c192d3c
NR
328 }
329
487926f7
NR
330 /**
331 * Create/update the .properties file.
332 * <p>
333 * Will use the most likely candidate as base if the file does not already
334 * exists and this resource is translatable (for instance, "en_US" will use
335 * "en" as a base if the resource is a translation file).
336 * <p>
337 * Will update the files in {@link Bundles#getDirectory()}; it <b>MUST</b>
338 * be set.
339 *
340 * @throws IOException
341 * in case of IO errors
342 */
343 public void updateFile() throws IOException {
344 updateFile(Bundles.getDirectory());
345 }
346
ec1f3444 347 /**
80383c14
NR
348 * Create/update the .properties file.
349 * <p>
350 * Will use the most likely candidate as base if the file does not already
351 * exists and this resource is translatable (for instance, "en_US" will use
352 * "en" as a base if the resource is a translation file).
ec1f3444
NR
353 *
354 * @param path
487926f7
NR
355 * the path where the .properties files are, <b>MUST NOT</b> be
356 * NULL
ec1f3444
NR
357 *
358 * @throws IOException
359 * in case of IO errors
360 */
361 public void updateFile(String path) throws IOException {
362 File file = getUpdateFile(path);
363
364 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
365 new FileOutputStream(file), "UTF-8"));
366
367 writeHeader(writer);
368 writer.write("\n");
369 writer.write("\n");
370
371 for (Field field : type.getDeclaredFields()) {
372 Meta meta = field.getAnnotation(Meta.class);
373 if (meta != null) {
cd0c27d2 374 E id = Enum.valueOf(type, field.getName());
ec1f3444
NR
375 String info = getMetaInfo(meta);
376
377 if (info != null) {
378 writer.write(info);
379 writer.write("\n");
380 }
381
382 writeValue(writer, id);
383 }
384 }
385
386 writer.close();
387 }
388
e8aa5bf9
NR
389 /**
390 * Delete the .properties file.
391 * <p>
392 * Will use the most likely candidate as base if the file does not already
393 * exists and this resource is translatable (for instance, "en_US" will use
394 * "en" as a base if the resource is a translation file).
395 * <p>
396 * Will delete the files in {@link Bundles#getDirectory()}; it <b>MUST</b>
397 * be set.
398 *
399 * @return TRUE if the file was deleted
400 */
401 public boolean deleteFile() {
402 return deleteFile(Bundles.getDirectory());
403 }
404
405 /**
406 * Delete the .properties file.
407 * <p>
408 * Will use the most likely candidate as base if the file does not already
409 * exists and this resource is translatable (for instance, "en_US" will use
410 * "en" as a base if the resource is a translation file).
411 *
412 * @param path
413 * the path where the .properties files are, <b>MUST NOT</b> be
414 * NULL
415 *
416 * @return TRUE if the file was deleted
417 */
418 public boolean deleteFile(String path) {
419 File file = getUpdateFile(path);
420 return file.delete();
421 }
422
db31c358
NR
423 /**
424 * The description {@link TransBundle}, that is, a {@link TransBundle}
425 * dedicated to the description of the values of the given {@link Bundle}
426 * (can be NULL).
427 *
428 * @return the description {@link TransBundle}
429 */
430 public TransBundle<E> getDescriptionBundle() {
431 return descriptionBundle;
432 }
433
80383c14
NR
434 /**
435 * Reload the {@link Bundle} data files.
e9ca6bb8
NR
436 *
437 * @param resetToDefault
438 * reset to the default configuration (do not look into the
439 * possible user configuration files, only take the original
440 * configuration)
80383c14 441 */
e9ca6bb8 442 public void reload(boolean resetToDefault) {
db31c358 443 setBundle(keyType, Locale.getDefault(), resetToDefault);
80383c14
NR
444 }
445
ec1f3444
NR
446 /**
447 * Check if the internal map contains the given key.
448 *
449 * @param key
450 * the key to check for
451 *
452 * @return true if it does
453 */
454 protected boolean containsKey(String key) {
487926f7 455 return changeMap.containsKey(key) || map.containsKey(key);
ec1f3444
NR
456 }
457
458 /**
2cce3dcb
NR
459 * Get the value for the given key if it exists in the internal map, or NULL
460 * if not.
ec1f3444
NR
461 *
462 * @param key
463 * the key to check for
464 *
2cce3dcb 465 * @return the value, or NULL
ec1f3444
NR
466 */
467 protected String getString(String key) {
62c9ec78
NR
468 if (changeMap.containsKey(key)) {
469 return changeMap.get(key);
470 }
471
487926f7
NR
472 if (map.containsKey(key)) {
473 return map.get(key);
ec1f3444
NR
474 }
475
476 return null;
477 }
478
62c9ec78
NR
479 /**
480 * Set the value for this key, in the change map (it is kept in memory, not
481 * yet on disk).
482 *
483 * @param key
484 * the key
485 * @param value
486 * the associated value
487 */
488 protected void setString(String key, String value) {
487926f7 489 changeMap.put(key, value == null ? null : value.trim());
62c9ec78
NR
490 }
491
ec1f3444
NR
492 /**
493 * Return formated, display-able information from the {@link Meta} field
494 * given. Each line will always starts with a "#" character.
495 *
496 * @param meta
497 * the {@link Meta} field
498 *
499 * @return the information to display or NULL if none
500 */
501 protected String getMetaInfo(Meta meta) {
db31c358
NR
502 String desc = meta.description();
503 boolean group = meta.group();
504 Meta.Format format = meta.format();
505 String[] list = meta.list();
506 boolean nullable = meta.nullable();
e8aa5bf9 507 String def = meta.def();
ec1f3444 508 String info = meta.info();
db31c358 509 boolean array = meta.array();
ec1f3444 510
db31c358 511 // Default, empty values -> NULL
e8aa5bf9
NR
512 if (desc.length() + list.length + info.length() + def.length() == 0
513 && !group && nullable && format == Meta.Format.STRING) {
ec1f3444 514 return null;
db31c358 515 }
ec1f3444
NR
516
517 StringBuilder builder = new StringBuilder();
db31c358
NR
518 builder.append("# ").append(desc);
519 if (desc.length() > 20) {
520 builder.append("\n#");
521 }
ec1f3444 522
db31c358
NR
523 if (group) {
524 builder.append("This item is used as a group, its content is not expected to be used.");
525 } else {
526 builder.append(" (FORMAT: ").append(format)
527 .append(nullable ? "" : " (required)");
528 builder.append(") ").append(info);
529
530 if (list.length > 0) {
531 builder.append("\n# ALLOWED VALUES:");
532 for (String value : list) {
533 builder.append(" \"").append(value).append("\"");
534 }
ec1f3444
NR
535 }
536
db31c358
NR
537 if (array) {
538 builder.append("\n# (This item accept a list of comma-separated values)");
ec1f3444
NR
539 }
540 }
541
ec1f3444
NR
542 return builder.toString();
543 }
544
545 /**
546 * The display name used in the <tt>.properties file</tt>.
547 *
548 * @return the name
549 */
550 protected String getBundleDisplayName() {
db31c358 551 return keyType.toString();
ec1f3444
NR
552 }
553
554 /**
555 * Write the header found in the configuration <tt>.properties</tt> file of
556 * this {@link Bundles}.
557 *
558 * @param writer
559 * the {@link Writer} to write the header in
560 *
561 * @throws IOException
562 * in case of IO error
563 */
564 protected void writeHeader(Writer writer) throws IOException {
565 writer.write("# " + getBundleDisplayName() + "\n");
566 writer.write("#\n");
567 }
568
569 /**
570 * Write the given id to the config file, i.e., "MY_ID = my_curent_value"
571 * followed by a new line
572 *
573 * @param writer
574 * the {@link Writer} to write into
575 * @param id
576 * the id to write
577 *
578 * @throws IOException
579 * in case of IO error
580 */
581 protected void writeValue(Writer writer, E id) throws IOException {
582 writeValue(writer, id.name(), getString(id));
583 }
584
585 /**
586 * Write the given data to the config file, i.e., "MY_ID = my_curent_value"
587 * followed by a new line
588 *
589 * @param writer
590 * the {@link Writer} to write into
591 * @param id
592 * the id to write
593 * @param value
594 * the id's value
595 *
596 * @throws IOException
597 * in case of IO error
598 */
599 protected void writeValue(Writer writer, String id, String value)
600 throws IOException {
601 writer.write(id);
602 writer.write(" = ");
603
604 if (value == null) {
605 value = "";
606 }
607
009196a4 608 String[] lines = value.replaceAll("\t", "\\\\\\t").split("\n");
ec1f3444
NR
609 for (int i = 0; i < lines.length; i++) {
610 writer.write(lines[i]);
611 if (i < lines.length - 1) {
612 writer.write("\\n\\");
613 }
614 writer.write("\n");
615 }
616 }
617
618 /**
619 * Return the source file for this {@link Bundles} from the given path.
620 *
621 * @param path
622 * the path where the .properties files are
623 *
624 * @return the source {@link File}
ec1f3444
NR
625 */
626 protected File getUpdateFile(String path) {
db31c358 627 return new File(path, keyType.name() + ".properties");
ec1f3444
NR
628 }
629
630 /**
62c9ec78 631 * Change the currently used bundle, and reset all changes.
ec1f3444
NR
632 *
633 * @param name
634 * the name of the bundle to load
635 * @param locale
636 * the {@link Locale} to use
e9ca6bb8
NR
637 * @param resetToDefault
638 * reset to the default configuration (do not look into the
639 * possible user configuration files, only take the original
640 * configuration)
ec1f3444 641 */
e9ca6bb8 642 protected void setBundle(Enum<?> name, Locale locale, boolean resetToDefault) {
62c9ec78 643 changeMap.clear();
ec1f3444 644 String dir = Bundles.getDirectory();
e8aa5bf9 645 String bname = type.getPackage().getName() + "." + name.name();
ec1f3444 646
487926f7 647 boolean found = false;
e9ca6bb8 648 if (!resetToDefault && dir != null) {
e8aa5bf9 649 // Look into Bundles.getDirectory() for .properties files
ec1f3444
NR
650 try {
651 File file = getPropertyFile(dir, name.name(), locale);
652 if (file != null) {
653 Reader reader = new InputStreamReader(new FileInputStream(
654 file), "UTF8");
487926f7
NR
655 resetMap(new PropertyResourceBundle(reader));
656 found = true;
ec1f3444
NR
657 }
658 } catch (IOException e) {
659 e.printStackTrace();
660 }
661 }
662
487926f7 663 if (!found) {
e8aa5bf9 664 // Look into the package itself for resources
e9ca6bb8 665 try {
487926f7
NR
666 resetMap(ResourceBundle
667 .getBundle(bname, locale, type.getClassLoader(),
668 new FixedResourceBundleControl()));
e8aa5bf9
NR
669 found = true;
670 } catch (MissingResourceException e) {
e9ca6bb8 671 } catch (Exception e) {
e8aa5bf9 672 e.printStackTrace();
487926f7
NR
673 }
674 }
e8aa5bf9
NR
675
676 if (!found) {
677 // We have no bundle for this Bundle
678 System.err.println("No bundle found for: " + bname);
679 resetMap(null);
680 }
487926f7
NR
681 }
682
683 /**
e8aa5bf9
NR
684 * Reset the backing map to the content of the given bundle, or with default
685 * valiues if bundle is NULL.
487926f7
NR
686 *
687 * @param bundle
688 * the bundle to copy
689 */
b771aed5 690 protected void resetMap(ResourceBundle bundle) {
487926f7 691 this.map.clear();
e8aa5bf9
NR
692 for (Field field : type.getDeclaredFields()) {
693 try {
694 Meta meta = field.getAnnotation(Meta.class);
695 if (meta != null) {
696 E id = Enum.valueOf(type, field.getName());
697
698 String value;
699 if (bundle != null) {
700 value = bundle.getString(id.name());
701 } else {
702 value = meta.def();
703 }
704
705 this.map.put(id.name(), value == null ? null : value.trim());
487926f7 706 }
e8aa5bf9 707 } catch (MissingResourceException e) {
e9ca6bb8
NR
708 }
709 }
710 }
cd0c27d2 711
e9ca6bb8
NR
712 /**
713 * Take a snapshot of the changes in memory in this {@link Bundle} made by
714 * the "set" methods ( {@link Bundle#setString(Enum, String)}...) at the
715 * current time.
716 *
487926f7 717 * @return a snapshot to use with {@link Bundle#restoreSnapshot(Object)}
e9ca6bb8 718 */
487926f7 719 public Object takeSnapshot() {
e9ca6bb8
NR
720 return new HashMap<String, String>(changeMap);
721 }
722
723 /**
724 * Restore a snapshot taken with {@link Bundle}, or reset the current
725 * changes if the snapshot is NULL.
726 *
727 * @param snap
728 * the snapshot or NULL
729 */
730 @SuppressWarnings("unchecked")
487926f7 731 public void restoreSnapshot(Object snap) {
e9ca6bb8
NR
732 if (snap == null) {
733 changeMap.clear();
734 } else {
735 if (snap instanceof Map) {
736 changeMap = (Map<String, String>) snap;
737 } else {
d827da2a 738 throw new RuntimeException(
e9ca6bb8
NR
739 "Restoring changes in a Bundle must be done on a changes snapshot, "
740 + "or NULL to discard current changes");
741 }
ec1f3444
NR
742 }
743 }
744
745 /**
746 * Return the resource file that is closer to the {@link Locale}.
747 *
748 * @param dir
db31c358 749 * the directory to look into
ec1f3444 750 * @param name
db31c358 751 * the file base name (without <tt>.properties</tt>)
ec1f3444
NR
752 * @param locale
753 * the {@link Locale}
754 *
755 * @return the closest match or NULL if none
756 */
757 private File getPropertyFile(String dir, String name, Locale locale) {
758 List<String> locales = new ArrayList<String>();
759 if (locale != null) {
760 String country = locale.getCountry() == null ? "" : locale
761 .getCountry();
762 String language = locale.getLanguage() == null ? "" : locale
763 .getLanguage();
764 if (!language.isEmpty() && !country.isEmpty()) {
765 locales.add("_" + language + "-" + country);
766 }
767 if (!language.isEmpty()) {
768 locales.add("_" + language);
769 }
770 }
771
772 locales.add("");
773
774 File file = null;
775 for (String loc : locales) {
776 file = new File(dir, name + loc + ".properties");
777 if (file.exists()) {
778 break;
ec1f3444 779 }
d827da2a 780
cd0c27d2 781 file = null;
ec1f3444
NR
782 }
783
784 return file;
785 }
786}