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