Merge branch 'master' into subtree
[nikiroo-utils.git] / Instance.java
1 package be.nikiroo.fanfix;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.util.Date;
6
7 import be.nikiroo.fanfix.bundles.Config;
8 import be.nikiroo.fanfix.bundles.ConfigBundle;
9 import be.nikiroo.fanfix.bundles.StringId;
10 import be.nikiroo.fanfix.bundles.StringIdBundle;
11 import be.nikiroo.fanfix.bundles.StringIdGuiBundle;
12 import be.nikiroo.fanfix.bundles.UiConfig;
13 import be.nikiroo.fanfix.bundles.UiConfigBundle;
14 import be.nikiroo.fanfix.library.BasicLibrary;
15 import be.nikiroo.fanfix.library.CacheLibrary;
16 import be.nikiroo.fanfix.library.LocalLibrary;
17 import be.nikiroo.fanfix.library.RemoteLibrary;
18 import be.nikiroo.fanfix.library.WebLibrary;
19 import be.nikiroo.utils.Cache;
20 import be.nikiroo.utils.IOUtils;
21 import be.nikiroo.utils.Image;
22 import be.nikiroo.utils.Proxy;
23 import be.nikiroo.utils.TempFiles;
24 import be.nikiroo.utils.TraceHandler;
25 import be.nikiroo.utils.resources.Bundles;
26
27 /**
28 * Global state for the program (services and singletons).
29 *
30 * @author niki
31 */
32 public class Instance {
33 static private Instance instance;
34 static private Object instancelock = new Object();
35
36 private ConfigBundle config;
37 private UiConfigBundle uiconfig;
38 private StringIdBundle trans;
39 private DataLoader cache;
40 private StringIdGuiBundle transGui;
41 private BasicLibrary lib;
42 private File coverDir;
43 private File readerTmp;
44 private File remoteDir;
45 private String configDir;
46 private TraceHandler tracer;
47 private TempFiles tempFiles;
48
49 /**
50 * Initialise the instance -- if already initialised, nothing will happen.
51 * <p>
52 * Before calling this method, you may call
53 * {@link Bundles#setDirectory(String)} if wanted.
54 * <p>
55 * Note that this method will honour some environment variables, the 3 most
56 * important ones probably being:
57 * <ul>
58 * <li><tt>DEBUG</tt>: will enable DEBUG output if set to 1 (or Y or TRUE or
59 * ON, case insensitive)</li>
60 * <li><tt>CONFIG_DIR</tt>: will use this directory as configuration
61 * directory (supports $HOME notation, defaults to $HOME/.fanfix</li>
62 * <li><tt>BOOKS_DIR</tt>: will use this directory as library directory
63 * (supports $HOME notation, defaults to $HOME/Books</li>
64 * </ul>
65 */
66 static public void init() {
67 init(false);
68 }
69
70 /**
71 * Initialise the instance -- if already initialised, nothing will happen
72 * unless you pass TRUE to <tt>force</tt>.
73 * <p>
74 * Before calling this method, you may call
75 * {@link Bundles#setDirectory(String)} if wanted.
76 * <p>
77 * Note: forcing the initialisation can be dangerous, so make sure to only
78 * make it under controlled circumstances -- for instance, at the start of
79 * the program, you could call {@link Instance#init()}, change some settings
80 * because you want to force those settings (it will also forbid users to
81 * change them!) and then call {@link Instance#init(boolean)} with
82 * <tt>force</tt> set to TRUE.
83 *
84 * @param force
85 * force the initialisation even if already initialised
86 */
87 static public void init(boolean force) {
88 synchronized (instancelock) {
89 if (instance == null || force) {
90 instance = new Instance();
91 }
92 }
93
94 }
95
96 /**
97 * Force-initialise the {@link Instance} to a known value.
98 * <p>
99 * Usually for DEBUG/Test purposes.
100 *
101 * @param instance
102 * the actual Instance to use
103 */
104 static public void init(Instance instance) {
105 Instance.instance = instance;
106 }
107
108 /**
109 * The (mostly unique) instance of this {@link Instance}.
110 *
111 * @return the (mostly unique) instance
112 */
113 public static Instance getInstance() {
114 return instance;
115 }
116
117 /**
118 * Actually initialise the instance.
119 * <p>
120 * Before calling this method, you may call
121 * {@link Bundles#setDirectory(String)} if wanted.
122 */
123 protected Instance() {
124 // Before we can configure it:
125 Boolean debug = checkEnv("DEBUG");
126 boolean trace = debug != null && debug;
127 tracer = new TraceHandler(true, trace, trace);
128
129 // config dir:
130 configDir = getConfigDir();
131 if (!new File(configDir).exists()) {
132 new File(configDir).mkdirs();
133 }
134
135 // Most of the rest is dependent upon this:
136 createConfigs(configDir, false);
137
138 // Proxy support
139 Proxy.use(config.getString(Config.NETWORK_PROXY));
140
141 // update tracer:
142 if (debug == null) {
143 debug = config.getBoolean(Config.DEBUG_ERR, false);
144 trace = config.getBoolean(Config.DEBUG_TRACE, false);
145 }
146
147 tracer = new TraceHandler(true, debug, trace);
148
149 // default Library
150 remoteDir = new File(configDir, "remote");
151 lib = createDefaultLibrary(remoteDir);
152
153 // create cache and TMP
154 File tmp = getFile(Config.CACHE_DIR, configDir, "tmp");
155 Image.setTemporaryFilesRoot(new File(tmp.getParent(), "tmp.images"));
156
157 String ua = config.getString(Config.NETWORK_USER_AGENT, "");
158 try {
159 int hours = config.getInteger(Config.CACHE_MAX_TIME_CHANGING, 0);
160 int hoursLarge = config.getInteger(Config.CACHE_MAX_TIME_STABLE, 0);
161 cache = new DataLoader(tmp, ua, hours, hoursLarge);
162 } catch (IOException e) {
163 tracer.error(new IOException(
164 "Cannot create cache (will continue without cache)", e));
165 cache = new DataLoader(ua);
166 }
167
168 cache.setTraceHandler(tracer);
169
170 // readerTmp / coverDir
171 readerTmp = getFile(UiConfig.CACHE_DIR_LOCAL_READER, configDir,
172 "tmp-reader");
173 coverDir = getFile(Config.DEFAULT_COVERS_DIR, configDir, "covers");
174 coverDir.mkdirs();
175
176 try {
177 tempFiles = new TempFiles("fanfix");
178 } catch (IOException e) {
179 tracer.error(
180 new IOException("Cannot create temporary directory", e));
181 }
182 }
183
184 /**
185 * The traces handler for this {@link Cache}.
186 * <p>
187 * It is never NULL.
188 *
189 * @return the traces handler (never NULL)
190 */
191 public TraceHandler getTraceHandler() {
192 return tracer;
193 }
194
195 /**
196 * The traces handler for this {@link Cache}.
197 *
198 * @param tracer
199 * the new traces handler or NULL
200 */
201 public void setTraceHandler(TraceHandler tracer) {
202 if (tracer == null) {
203 tracer = new TraceHandler(false, false, false);
204 }
205
206 this.tracer = tracer;
207 cache.setTraceHandler(tracer);
208 }
209
210 /**
211 * Get the (unique) configuration service for the program.
212 *
213 * @return the configuration service
214 */
215 public ConfigBundle getConfig() {
216 return config;
217 }
218
219 /**
220 * Get the (unique) UI configuration service for the program.
221 *
222 * @return the configuration service
223 */
224 public UiConfigBundle getUiConfig() {
225 return uiconfig;
226 }
227
228 /**
229 * Reset the configuration.
230 *
231 * @param resetTrans
232 * also reset the translation files
233 */
234 public void resetConfig(boolean resetTrans) {
235 String dir = Bundles.getDirectory();
236 Bundles.setDirectory(null);
237 try {
238 try {
239 ConfigBundle config = new ConfigBundle();
240 config.updateFile(configDir);
241 } catch (IOException e) {
242 tracer.error(e);
243 }
244 try {
245 UiConfigBundle uiconfig = new UiConfigBundle();
246 uiconfig.updateFile(configDir);
247 } catch (IOException e) {
248 tracer.error(e);
249 }
250
251 if (resetTrans) {
252 try {
253 StringIdBundle trans = new StringIdBundle(null);
254 trans.updateFile(configDir);
255 } catch (IOException e) {
256 tracer.error(e);
257 }
258 }
259 } finally {
260 Bundles.setDirectory(dir);
261 }
262 }
263
264 /**
265 * Get the (unique) {@link DataLoader} for the program.
266 *
267 * @return the {@link DataLoader}
268 */
269 public DataLoader getCache() {
270 return cache;
271 }
272
273 /**
274 * Get the (unique) {link StringIdBundle} for the program.
275 * <p>
276 * This is used for the translations of the core parts of Fanfix.
277 *
278 * @return the {link StringIdBundle}
279 */
280 public StringIdBundle getTrans() {
281 return trans;
282 }
283
284 /**
285 * Get the (unique) {link StringIdGuiBundle} for the program.
286 * <p>
287 * This is used for the translations of the GUI parts of Fanfix.
288 *
289 * @return the {link StringIdGuiBundle}
290 */
291 public StringIdGuiBundle getTransGui() {
292 return transGui;
293 }
294
295 /**
296 * Get the (unique) {@link BasicLibrary} for the program.
297 *
298 * @return the {@link BasicLibrary}
299 */
300 public BasicLibrary getLibrary() {
301 if (lib == null) {
302 throw new NullPointerException("We don't have a library to return");
303 }
304
305 return lib;
306 }
307
308 /**
309 * Change the default {@link BasicLibrary} for this program.
310 * <p>
311 * Be careful.
312 *
313 * @param lib
314 * the new {@link BasicLibrary}
315 */
316 public void setLibrary(BasicLibrary lib) {
317 this.lib = lib;
318 }
319
320 /**
321 * Return the directory where to look for default cover pages.
322 *
323 * @return the default covers directory
324 */
325 public File getCoverDir() {
326 return coverDir;
327 }
328
329 /**
330 * Return the directory where to store temporary files for the local reader.
331 *
332 * @return the directory
333 */
334 public File getReaderDir() {
335 return readerTmp;
336 }
337
338 /**
339 * Return the directory where to store temporary files for the remote
340 * {@link LocalLibrary}.
341 *
342 * @param host
343 * the remote for this host
344 *
345 * @return the directory
346 */
347 public File getRemoteDir(String host) {
348 return getRemoteDir(remoteDir, host);
349 }
350
351 /**
352 * Return the directory where to store temporary files for the remote
353 * {@link LocalLibrary}.
354 *
355 * @param remoteDir
356 * the base remote directory
357 * @param host
358 * the remote for this host
359 *
360 * @return the directory
361 */
362 private File getRemoteDir(File remoteDir, String host) {
363 remoteDir.mkdirs();
364
365 if (host != null) {
366 return new File(remoteDir, host);
367 }
368
369 return remoteDir;
370 }
371
372 /**
373 * Check if we need to check that a new version of Fanfix is available.
374 *
375 * @return TRUE if we need to
376 */
377 public boolean isVersionCheckNeeded() {
378 try {
379 long wait = config.getInteger(Config.NETWORK_UPDATE_INTERVAL, 0)
380 * 24 * 60 * 60 * 1000;
381 if (wait >= 0) {
382 String lastUpString = IOUtils
383 .readSmallFile(new File(configDir, "LAST_UPDATE"));
384 long delay = new Date().getTime()
385 - Long.parseLong(lastUpString);
386 if (delay > wait) {
387 return true;
388 }
389 } else {
390 return false;
391 }
392 } catch (Exception e) {
393 // No file or bad file:
394 return true;
395 }
396
397 return false;
398 }
399
400 /**
401 * Notify that we checked for a new version of Fanfix.
402 */
403 public void setVersionChecked() {
404 try {
405 IOUtils.writeSmallFile(new File(configDir), "LAST_UPDATE",
406 Long.toString(new Date().getTime()));
407 } catch (IOException e) {
408 tracer.error(e);
409 }
410 }
411
412 /**
413 * The facility to use temporary files in this program.
414 * <p>
415 * <b>MUST</b> be closed at end of program.
416 *
417 * @return the facility
418 */
419 public TempFiles getTempFiles() {
420 return tempFiles;
421 }
422
423 /**
424 * The configuration directory (will check, in order of preference, the
425 * system properties, the environment and then defaults to
426 * {@link Instance#getHome()}/.fanfix).
427 *
428 * @return the config directory
429 */
430 private String getConfigDir() {
431 String configDir = System.getProperty("CONFIG_DIR");
432
433 if (configDir == null) {
434 configDir = System.getenv("CONFIG_DIR");
435 }
436
437 if (configDir == null) {
438 configDir = new File(getHome(), ".fanfix").getPath();
439 }
440
441 return configDir;
442 }
443
444 /**
445 * Create the config variables ({@link Instance#config},
446 * {@link Instance#uiconfig}, {@link Instance#trans} and
447 * {@link Instance#transGui}).
448 *
449 * @param configDir
450 * the directory where to find the configuration files
451 * @param refresh
452 * TRUE to reset the configuration files from the default
453 * included ones
454 */
455 private void createConfigs(String configDir, boolean refresh) {
456 if (!refresh) {
457 Bundles.setDirectory(configDir);
458 }
459
460 try {
461 config = new ConfigBundle();
462 config.updateFile(configDir);
463 } catch (IOException e) {
464 tracer.error(e);
465 }
466
467 try {
468 uiconfig = new UiConfigBundle();
469 uiconfig.updateFile(configDir);
470 } catch (IOException e) {
471 tracer.error(e);
472 }
473
474 // No updateFile for this one! (we do not want the user to have custom
475 // translations that won't accept updates from newer versions)
476 trans = new StringIdBundle(getLang());
477 transGui = new StringIdGuiBundle(getLang());
478
479 // Fix an old bug (we used to store custom translation files by
480 // default):
481 if (trans.getString(StringId.INPUT_DESC_CBZ) == null) {
482 trans.deleteFile(configDir);
483 }
484
485 Boolean noutf = checkEnv("NOUTF");
486 if (noutf != null && noutf) {
487 trans.setUnicode(false);
488 transGui.setUnicode(false);
489 }
490
491 Bundles.setDirectory(configDir);
492 }
493
494 /**
495 * Create the default library as specified by the config.
496 *
497 * @param remoteDir
498 * the base remote directory if needed
499 *
500 * @return the default {@link BasicLibrary}
501 */
502 private BasicLibrary createDefaultLibrary(File remoteDir) {
503 BasicLibrary lib = null;
504
505 boolean useRemote = config.getBoolean(Config.REMOTE_LIBRARY_ENABLED,
506 false);
507 if (useRemote) {
508 String host = null;
509 int port = -1;
510 try {
511 host = config.getString(Config.REMOTE_LIBRARY_HOST,
512 "fanfix://localhost");
513 port = config.getInteger(Config.REMOTE_LIBRARY_PORT, -1);
514 String key = config.getString(Config.REMOTE_LIBRARY_KEY);
515
516 if (!host.startsWith("http://") && !host.startsWith("https://")
517 && !host.startsWith("fanfix://")) {
518 host = "fanfix://" + host;
519 }
520
521 tracer.trace("Selecting remote library " + host + ":" + port);
522
523 if (host.startsWith("fanfix://")) {
524 lib = new RemoteLibrary(key, host, port);
525 } else {
526 lib = new WebLibrary(key, host, port);
527 }
528
529 lib = new CacheLibrary(getRemoteDir(remoteDir, host), lib,
530 uiconfig);
531 } catch (Exception e) {
532 tracer.error(
533 new IOException("Cannot create remote library for: "
534 + host + ":" + port, e));
535 }
536 } else {
537 String libDir = System.getenv("BOOKS_DIR");
538 if (libDir == null || libDir.isEmpty()) {
539 libDir = getFile(Config.LIBRARY_DIR, configDir, "$HOME/Books")
540 .getPath();
541 }
542 try {
543 lib = new LocalLibrary(new File(libDir), config);
544 } catch (Exception e) {
545 tracer.error(new IOException(
546 "Cannot create library for directory: " + libDir, e));
547 }
548 }
549
550 return lib;
551 }
552
553 /**
554 * Return a path, but support the special $HOME variable.
555 *
556 * @param id
557 * the key for the path, which may contain "$HOME"
558 * @param configDir
559 * the directory to use as base if not absolute
560 * @param def
561 * the default value if none (will be configDir-rooted if needed)
562 * @return the path, with expanded "$HOME" if needed
563 */
564 protected File getFile(Config id, String configDir, String def) {
565 String path = config.getString(id, def);
566 return getFile(path, configDir);
567 }
568
569 /**
570 * Return a path, but support the special $HOME variable.
571 *
572 * @param id
573 * the key for the path, which may contain "$HOME"
574 * @param configDir
575 * the directory to use as base if not absolute
576 * @param def
577 * the default value if none (will be configDir-rooted if needed)
578 * @return the path, with expanded "$HOME" if needed
579 */
580 protected File getFile(UiConfig id, String configDir, String def) {
581 String path = uiconfig.getString(id, def);
582 return getFile(path, configDir);
583 }
584
585 /**
586 * Return a path, but support the special $HOME variable.
587 *
588 * @param path
589 * the path, which may contain "$HOME"
590 * @param configDir
591 * the directory to use as base if not absolute
592 * @return the path, with expanded "$HOME" if needed
593 */
594 protected File getFile(String path, String configDir) {
595 File file = null;
596 if (path != null && !path.isEmpty()) {
597 path = path.replace('/', File.separatorChar);
598 if (path.contains("$HOME")) {
599 path = path.replace("$HOME", getHome());
600 } else if (!path.startsWith("/")) {
601 path = new File(configDir, path).getPath();
602 }
603
604 file = new File(path);
605 }
606
607 return file;
608 }
609
610 /**
611 * Return the home directory from the environment (FANFIX_DIR) or the system
612 * properties.
613 * <p>
614 * The environment variable is tested first. Then, the custom property
615 * "fanfix.home" is tried, followed by the usual "user.home" then
616 * "java.io.tmp" if nothing else is found.
617 *
618 * @return the home
619 */
620 protected String getHome() {
621 String home = System.getenv("FANFIX_DIR");
622 if (home != null && new File(home).isFile()) {
623 home = null;
624 }
625
626 if (home == null || home.trim().isEmpty()) {
627 home = System.getProperty("fanfix.home");
628 if (home != null && new File(home).isFile()) {
629 home = null;
630 }
631 }
632
633 if (home == null || home.trim().isEmpty()) {
634 home = System.getProperty("user.home");
635 if (!new File(home).isDirectory()) {
636 home = null;
637 }
638 }
639
640 if (home == null || home.trim().isEmpty()) {
641 home = System.getProperty("java.io.tmpdir");
642 if (!new File(home).isDirectory()) {
643 home = null;
644 }
645 }
646
647 if (home == null) {
648 home = "";
649 }
650
651 return home;
652 }
653
654 /**
655 * The language to use for the application (NULL = default system language).
656 *
657 * @return the language
658 */
659 protected String getLang() {
660 String lang = config.getString(Config.LANG);
661
662 if (lang == null || lang.isEmpty()) {
663 if (System.getenv("LANG") != null
664 && !System.getenv("LANG").isEmpty()) {
665 lang = System.getenv("LANG");
666 }
667 }
668
669 if (lang != null && lang.isEmpty()) {
670 lang = null;
671 }
672
673 return lang;
674 }
675
676 /**
677 * Check that the given environment variable is "enabled".
678 *
679 * @param key
680 * the variable to check
681 *
682 * @return TRUE if it is
683 */
684 protected Boolean checkEnv(String key) {
685 String value = System.getenv(key);
686 if (value != null) {
687 value = value.trim().toLowerCase();
688 if ("yes".equals(value) || "true".equals(value)
689 || "on".equals(value) || "1".equals(value)
690 || "y".equals(value)) {
691 return true;
692 }
693
694 return false;
695 }
696
697 return null;
698 }
699 }