code cleanup
[nikiroo-utils.git] / Main.java
1 package be.nikiroo.fanfix;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.net.MalformedURLException;
6 import java.net.URL;
7 import java.util.ArrayList;
8 import java.util.List;
9
10 import javax.net.ssl.SSLException;
11
12 import be.nikiroo.fanfix.bundles.Config;
13 import be.nikiroo.fanfix.bundles.StringId;
14 import be.nikiroo.fanfix.data.Chapter;
15 import be.nikiroo.fanfix.data.MetaData;
16 import be.nikiroo.fanfix.data.Story;
17 import be.nikiroo.fanfix.library.BasicLibrary;
18 import be.nikiroo.fanfix.library.CacheLibrary;
19 import be.nikiroo.fanfix.library.LocalLibrary;
20 import be.nikiroo.fanfix.library.RemoteLibrary;
21 import be.nikiroo.fanfix.library.RemoteLibraryServer;
22 import be.nikiroo.fanfix.output.BasicOutput;
23 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
24 import be.nikiroo.fanfix.reader.BasicReader;
25 import be.nikiroo.fanfix.reader.CliReader;
26 import be.nikiroo.fanfix.searchable.BasicSearchable;
27 import be.nikiroo.fanfix.supported.BasicSupport;
28 import be.nikiroo.fanfix.supported.SupportType;
29 import be.nikiroo.utils.Progress;
30 import be.nikiroo.utils.Version;
31 import be.nikiroo.utils.serial.server.ServerObject;
32
33 /**
34 * Main program entry point.
35 *
36 * @author niki
37 */
38 public class Main {
39 private enum MainAction {
40 IMPORT, EXPORT, CONVERT, READ, READ_URL, LIST, HELP, START, VERSION, SERVER, STOP_SERVER, REMOTE, SET_SOURCE, SET_TITLE, SET_AUTHOR, SEARCH, SEARCH_TAG
41 }
42
43 /**
44 * Main program entry point.
45 * <p>
46 * Known environment variables:
47 * <ul>
48 * <li>NOUTF: if set to 1 or 'true', the program will prefer non-unicode
49 * {@link String}s when possible</li>
50 * <li>CONFIG_DIR: a path where to look for the <tt>.properties</tt> files
51 * before taking the usual ones; they will also be saved/updated into this
52 * path when the program starts</li>
53 * <li>DEBUG: if set to 1 or 'true', the program will override the DEBUG_ERR
54 * configuration value with 'true'</li>
55 * </ul>
56 * <p>
57 * <ul>
58 * <li>--import [URL]: import into library</li>
59 * <li>--export [id] [output_type] [target]: export story to target</li>
60 * <li>--convert [URL] [output_type] [target] (+info): convert URL into
61 * target</li>
62 * <li>--read [id] ([chapter number]): read the given story from the library
63 * </li>
64 * <li>--read-url [URL] ([chapter number]): convert on the fly and read the
65 * story, without saving it</li>
66 * <li>--search: list the supported websites (where)</li>
67 * <li>--search [where] [keywords] (page [page]) (item [item]): search on
68 * the supported website and display the given results page of stories it
69 * found, or the story details if asked</li>
70 * <li>--search-tag [where]: list all the tags supported by this website</li>
71 * <li>--search-tag [index 1]... (page [page]) (item [item]): search for the
72 * given stories or subtags, tag by tag, and display information about a
73 * specific page of results or about a specific item if requested</li>
74 * <li>--list ([type]): list the stories present in the library</li>
75 * <li>--set-source [id] [new source]: change the source of the given story</li>
76 * <li>--set-title [id] [new title]: change the title of the given story</li>
77 * <li>--set-author [id] [new author]: change the author of the given story</li>
78 * <li>--version: get the version of the program</li>
79 * <li>--server: start the server mode (see config file for parameters)</li>
80 * <li>--stop-server: stop the running server on this port if any</li>
81 * <li>--remote [key] [host] [port]: use a the given remote library</li>
82 * </ul>
83 *
84 * @param args
85 * see method description
86 */
87 public static void main(String[] args) {
88 new Main().start(args);
89 }
90
91 /**
92 * Start the default handling for the application.
93 * <p>
94 * If specific actions were asked (with correct parameters), they will be
95 * forwarded to the different protected methods that you can override.
96 * <p>
97 * At the end of the method, {@link Main#exit(int)} will be called; by
98 * default, it calls {@link System#exit(int)} if the status is not 0.
99 *
100 * @param args
101 * the arguments received from the system
102 */
103 public void start(String [] args) {
104 // Only one line, but very important:
105 Instance.init();
106
107 String urlString = null;
108 String luid = null;
109 String sourceString = null;
110 String titleString = null;
111 String authorString = null;
112 String chapString = null;
113 String target = null;
114 String key = null;
115 MainAction action = MainAction.START;
116 Boolean plusInfo = null;
117 String host = null;
118 Integer port = null;
119 SupportType searchOn = null;
120 String search = null;
121 List<Integer> tags = new ArrayList<Integer>();
122 Integer page = null;
123 Integer item = null;
124
125 boolean noMoreActions = false;
126
127 int exitCode = 0;
128 for (int i = 0; exitCode == 0 && i < args.length; i++) {
129 if (args[i] == null)
130 continue;
131
132 // Action (--) handling:
133 if (!noMoreActions && args[i].startsWith("--")) {
134 if (args[i].equals("--")) {
135 noMoreActions = true;
136 } else {
137 try {
138 action = MainAction.valueOf(args[i].substring(2)
139 .toUpperCase().replace("-", "_"));
140 } catch (Exception e) {
141 Instance.getInstance().getTraceHandler()
142 .error(new IllegalArgumentException("Unknown action: " + args[i], e));
143 exitCode = 255;
144 }
145 }
146
147 continue;
148 }
149
150 switch (action) {
151 case IMPORT:
152 if (urlString == null) {
153 urlString = args[i];
154 } else {
155 exitCode = 255;
156 }
157 break;
158 case EXPORT:
159 if (luid == null) {
160 luid = args[i];
161 } else if (sourceString == null) {
162 sourceString = args[i];
163 } else if (target == null) {
164 target = args[i];
165 } else {
166 exitCode = 255;
167 }
168 break;
169 case CONVERT:
170 if (urlString == null) {
171 urlString = args[i];
172 } else if (sourceString == null) {
173 sourceString = args[i];
174 } else if (target == null) {
175 target = args[i];
176 } else if (plusInfo == null) {
177 if ("+info".equals(args[i])) {
178 plusInfo = true;
179 } else {
180 exitCode = 255;
181 }
182 } else {
183 exitCode = 255;
184 }
185 break;
186 case LIST:
187 if (sourceString == null) {
188 sourceString = args[i];
189 } else {
190 exitCode = 255;
191 }
192 break;
193 case SET_SOURCE:
194 if (luid == null) {
195 luid = args[i];
196 } else if (sourceString == null) {
197 sourceString = args[i];
198 } else {
199 exitCode = 255;
200 }
201 break;
202 case SET_TITLE:
203 if (luid == null) {
204 luid = args[i];
205 } else if (sourceString == null) {
206 titleString = args[i];
207 } else {
208 exitCode = 255;
209 }
210 break;
211 case SET_AUTHOR:
212 if (luid == null) {
213 luid = args[i];
214 } else if (sourceString == null) {
215 authorString = args[i];
216 } else {
217 exitCode = 255;
218 }
219 break;
220 case READ:
221 if (luid == null) {
222 luid = args[i];
223 } else if (chapString == null) {
224 chapString = args[i];
225 } else {
226 exitCode = 255;
227 }
228 break;
229 case READ_URL:
230 if (urlString == null) {
231 urlString = args[i];
232 } else if (chapString == null) {
233 chapString = args[i];
234 } else {
235 exitCode = 255;
236 }
237 break;
238 case SEARCH:
239 if (searchOn == null) {
240 searchOn = SupportType.valueOfAllOkUC(args[i]);
241
242 if (searchOn == null) {
243 Instance.getInstance().getTraceHandler().error("Website not known: <" + args[i] + ">");
244 exitCode = 41;
245 break;
246 }
247
248 if (BasicSearchable.getSearchable(searchOn) == null) {
249 Instance.getInstance().getTraceHandler().error("Website not supported: " + searchOn);
250 exitCode = 42;
251 break;
252 }
253 } else if (search == null) {
254 search = args[i];
255 } else if (page != null && page == -1) {
256 try {
257 page = Integer.parseInt(args[i]);
258 } catch (Exception e) {
259 page = -2;
260 }
261 } else if (item != null && item == -1) {
262 try {
263 item = Integer.parseInt(args[i]);
264 } catch (Exception e) {
265 item = -2;
266 }
267 } else if (page == null || item == null) {
268 if (page == null && "page".equals(args[i])) {
269 page = -1;
270 } else if (item == null && "item".equals(args[i])) {
271 item = -1;
272 } else {
273 exitCode = 255;
274 }
275 } else {
276 exitCode = 255;
277 }
278 break;
279 case SEARCH_TAG:
280 if (searchOn == null) {
281 searchOn = SupportType.valueOfAllOkUC(args[i]);
282
283 if (searchOn == null) {
284 Instance.getInstance().getTraceHandler().error("Website not known: <" + args[i] + ">");
285 exitCode = 255;
286 }
287
288 if (BasicSearchable.getSearchable(searchOn) == null) {
289 Instance.getInstance().getTraceHandler().error("Website not supported: " + searchOn);
290 exitCode = 255;
291 }
292 } else if (page == null && item == null) {
293 if ("page".equals(args[i])) {
294 page = -1;
295 } else if ("item".equals(args[i])) {
296 item = -1;
297 } else {
298 try {
299 int index = Integer.parseInt(args[i]);
300 tags.add(index);
301 } catch (NumberFormatException e) {
302 Instance.getInstance().getTraceHandler().error("Invalid tag index: " + args[i]);
303 exitCode = 255;
304 }
305 }
306 } else if (page != null && page == -1) {
307 try {
308 page = Integer.parseInt(args[i]);
309 } catch (Exception e) {
310 page = -2;
311 }
312 } else if (item != null && item == -1) {
313 try {
314 item = Integer.parseInt(args[i]);
315 } catch (Exception e) {
316 item = -2;
317 }
318 } else if (page == null || item == null) {
319 if (page == null && "page".equals(args[i])) {
320 page = -1;
321 } else if (item == null && "item".equals(args[i])) {
322 item = -1;
323 } else {
324 exitCode = 255;
325 }
326 } else {
327 exitCode = 255;
328 }
329 break;
330 case HELP:
331 exitCode = 255;
332 break;
333 case START:
334 exitCode = 255; // not supposed to be selected by user
335 break;
336 case VERSION:
337 exitCode = 255; // no arguments for this option
338 break;
339 case SERVER:
340 exitCode = 255; // no arguments for this option
341 break;
342 case STOP_SERVER:
343 exitCode = 255; // no arguments for this option
344 break;
345 case REMOTE:
346 if (key == null) {
347 key = args[i];
348 } else if (host == null) {
349 host = args[i];
350 } else if (port == null) {
351 port = Integer.parseInt(args[i]);
352
353 BasicLibrary lib = new RemoteLibrary(key, host, port);
354 lib = new CacheLibrary(
355 Instance.getInstance().getRemoteDir(host), lib,
356 Instance.getInstance().getUiConfig());
357
358 Instance.getInstance().setLibrary(lib);
359
360 action = MainAction.START;
361 } else {
362 exitCode = 255;
363 }
364 break;
365 }
366 }
367
368 final Progress mainProgress = new Progress(0, 80);
369 mainProgress.addProgressListener(new Progress.ProgressListener() {
370 private int current = mainProgress.getMin();
371
372 @Override
373 public void progress(Progress progress, String name) {
374 int diff = progress.getProgress() - current;
375 current += diff;
376
377 if (diff <= 0)
378 return;
379
380 StringBuilder builder = new StringBuilder();
381 for (int i = 0; i < diff; i++) {
382 builder.append('.');
383 }
384
385 System.err.print(builder.toString());
386
387 if (progress.isDone()) {
388 System.err.println("");
389 }
390 }
391 });
392 Progress pg = new Progress();
393 mainProgress.addProgress(pg, mainProgress.getMax());
394
395 VersionCheck updates = checkUpdates();
396
397 if (exitCode == 0) {
398 switch (action) {
399 case IMPORT:
400 if (updates != null)
401 updates.ok(); // we consider it read
402
403 try {
404 exitCode = imprt(BasicReader.getUrl(urlString), pg);
405 } catch (MalformedURLException e) {
406 Instance.getInstance().getTraceHandler().error(e);
407 exitCode = 1;
408 }
409
410 break;
411 case EXPORT:
412 if (updates != null)
413 updates.ok(); // we consider it read
414
415 OutputType exportType = OutputType.valueOfNullOkUC(sourceString, null);
416 if (exportType == null) {
417 Instance.getInstance().getTraceHandler().error(new Exception(trans(StringId.OUTPUT_DESC, sourceString)));
418 exitCode = 1;
419 break;
420 }
421
422 exitCode = export(luid, exportType, target, pg);
423
424 break;
425 case CONVERT:
426 if (updates != null)
427 updates.ok(); // we consider it read
428
429 OutputType convertType = OutputType.valueOfAllOkUC(sourceString, null);
430 if (convertType == null) {
431 Instance.getInstance().getTraceHandler()
432 .error(new IOException(trans(StringId.ERR_BAD_OUTPUT_TYPE, sourceString)));
433
434 exitCode = 2;
435 break;
436 }
437
438 exitCode = convert(urlString, convertType, target,
439 plusInfo == null ? false : plusInfo, pg);
440
441 break;
442 case LIST:
443 exitCode = list(sourceString);
444 break;
445 case SET_SOURCE:
446 try {
447 Instance.getInstance().getLibrary().changeSource(luid, sourceString, pg);
448 } catch (IOException e1) {
449 Instance.getInstance().getTraceHandler().error(e1);
450 exitCode = 21;
451 }
452 break;
453 case SET_TITLE:
454 try {
455 Instance.getInstance().getLibrary().changeTitle(luid, titleString, pg);
456 } catch (IOException e1) {
457 Instance.getInstance().getTraceHandler().error(e1);
458 exitCode = 22;
459 }
460 break;
461 case SET_AUTHOR:
462 try {
463 Instance.getInstance().getLibrary().changeAuthor(luid, authorString, pg);
464 } catch (IOException e1) {
465 Instance.getInstance().getTraceHandler().error(e1);
466 exitCode = 23;
467 }
468 break;
469 case READ:
470 if (luid == null || luid.isEmpty()) {
471 syntax(false);
472 exitCode = 255;
473 break;
474 }
475
476 try {
477 Integer chap = null;
478 if (chapString != null) {
479 try {
480 chap = Integer.parseInt(chapString);
481 } catch (NumberFormatException e) {
482 Instance.getInstance().getTraceHandler().error(new IOException(
483 "Chapter number cannot be parsed: " + chapString, e));
484 exitCode = 2;
485 break;
486 }
487 }
488
489 BasicLibrary lib = Instance.getInstance().getLibrary();
490 exitCode = read(lib.getStory(luid, null), chap);
491 } catch (IOException e) {
492 Instance.getInstance().getTraceHandler()
493 .error(new IOException("Failed to read book", e));
494 exitCode = 2;
495 }
496
497 break;
498 case READ_URL:
499 if (urlString == null || urlString.isEmpty()) {
500 syntax(false);
501 exitCode = 255;
502 break;
503 }
504
505 try {
506 Integer chap = null;
507 if (chapString != null) {
508 try {
509 chap = Integer.parseInt(chapString);
510 } catch (NumberFormatException e) {
511 Instance.getInstance().getTraceHandler().error(new IOException(
512 "Chapter number cannot be parsed: " + chapString, e));
513 exitCode = 2;
514 break;
515 }
516 }
517
518 BasicSupport support = BasicSupport
519 .getSupport(BasicReader.getUrl(urlString));
520 if (support == null) {
521 Instance.getInstance().getTraceHandler()
522 .error("URL not supported: " + urlString);
523 exitCode = 2;
524 break;
525 }
526
527 exitCode = read(support.process(null), chap);
528 } catch (IOException e) {
529 Instance.getInstance().getTraceHandler()
530 .error(new IOException("Failed to read book", e));
531 exitCode = 2;
532 }
533
534 break;
535 case SEARCH:
536 page = page == null ? 1 : page;
537 if (page < 0) {
538 Instance.getInstance().getTraceHandler().error("Incorrect page number");
539 exitCode = 255;
540 break;
541 }
542
543 item = item == null ? 0 : item;
544 if (item < 0) {
545 Instance.getInstance().getTraceHandler().error("Incorrect item number");
546 exitCode = 255;
547 break;
548 }
549
550 if (searchOn == null) {
551 try {
552 search();
553 } catch (IOException e) {
554 Instance.getInstance().getTraceHandler().error(e);
555 exitCode = 1;
556 }
557 } else if (search != null) {
558 try {
559 searchKeywords(searchOn, search, page, item);
560 } catch (IOException e) {
561 Instance.getInstance().getTraceHandler().error(e);
562 exitCode = 20;
563 }
564 } else {
565 exitCode = 255;
566 }
567
568 break;
569 case SEARCH_TAG:
570 if (searchOn == null) {
571 exitCode = 255;
572 break;
573 }
574
575 page = page == null ? 1 : page;
576 if (page < 0) {
577 Instance.getInstance().getTraceHandler().error("Incorrect page number");
578 exitCode = 255;
579 break;
580 }
581
582 item = item == null ? 0 : item;
583 if (item < 0) {
584 Instance.getInstance().getTraceHandler().error("Incorrect item number");
585 exitCode = 255;
586 break;
587 }
588
589 try {
590 searchTags(searchOn, page, item,
591 tags.toArray(new Integer[] {}));
592 } catch (IOException e) {
593 Instance.getInstance().getTraceHandler().error(e);
594 }
595
596 break;
597 case HELP:
598 syntax(true);
599 exitCode = 0;
600 break;
601 case VERSION:
602 if (updates != null)
603 updates.ok(); // we consider it read
604
605 System.out
606 .println(String.format("Fanfix version %s"
607 + "%nhttps://github.com/nikiroo/fanfix/"
608 + "%n\tWritten by Nikiroo",
609 Version.getCurrentVersion()));
610 break;
611 case START:
612 try {
613 start();
614 } catch (IOException e) {
615 Instance.getInstance().getTraceHandler().error(e);
616 exitCode = 66;
617 }
618 break;
619 case SERVER:
620 key = Instance.getInstance().getConfig().getString(Config.SERVER_KEY);
621 port = Instance.getInstance().getConfig().getInteger(Config.SERVER_PORT);
622 if (port == null) {
623 System.err.println("No port configured in the config file");
624 exitCode = 15;
625 break;
626 }
627 try {
628 startServer(key, port);
629 } catch (IOException e) {
630 Instance.getInstance().getTraceHandler().error(e);
631 }
632
633 break;
634 case STOP_SERVER:
635 // Can be given via "--remote XX XX XX"
636 if (key == null)
637 key = Instance.getInstance().getConfig().getString(Config.SERVER_KEY);
638 if (port == null)
639 port = Instance.getInstance().getConfig().getInteger(Config.SERVER_PORT);
640
641 if (port == null) {
642 System.err.println("No port given nor configured in the config file");
643 exitCode = 15;
644 break;
645 }
646 try {
647 stopServer(key, host, port);
648 } catch (SSLException e) {
649 Instance.getInstance().getTraceHandler().error(
650 "Bad access key for remote library");
651 exitCode = 43;
652 } catch (IOException e) {
653 Instance.getInstance().getTraceHandler().error(e);
654 exitCode = 44;
655 }
656
657 break;
658 case REMOTE:
659 exitCode = 255; // should not be reachable (REMOTE -> START)
660 break;
661 }
662 }
663
664 try {
665 Instance.getInstance().getTempFiles().close();
666 } catch (IOException e) {
667 Instance.getInstance().getTraceHandler().error(new IOException(
668 "Cannot dispose of the temporary files", e));
669 }
670
671 if (exitCode == 255) {
672 syntax(false);
673 }
674
675 exit(exitCode);
676 }
677
678 /**
679 * A normal invocation of the program (without parameters or at least
680 * without "action" parameters).
681 * <p>
682 * You will probably want to override that one if you offer a user
683 * interface.
684 *
685 * @throws IOException
686 * in case of I/O error
687 */
688 protected void start() throws IOException {
689 new CliReader().listBooks(null);
690 }
691
692 /**
693 * Will check if updates are available, synchronously.
694 * <p>
695 * For this, it will simply forward the call to
696 * {@link Main#checkUpdates(String)} with a value of "nikiroo/fanfix".
697 * <p>
698 * You may want to override it so you call the forward method with the right
699 * parameters (or also if you want it to be asynchronous).
700 *
701 * @return the newer version information or NULL if nothing new
702 */
703 protected VersionCheck checkUpdates() {
704 return checkUpdates("nikiroo/fanfix");
705 }
706
707 /**
708 * Will check if updates are available on a specific GitHub project.
709 * <p>
710 * Will be called by {@link Main#checkUpdates()}, but if you override that
711 * one you mall call it with another project.
712 *
713 * @param githubProject
714 * the GitHub project, for instance "nikiroo/fanfix"
715 *
716 * @return the newer version information or NULL if nothing new
717 */
718 protected VersionCheck checkUpdates(String githubProject) {
719 VersionCheck updates = VersionCheck.check(githubProject);
720 if (updates.isNewVersionAvailable()) {
721 notifyUpdates(updates);
722 return updates;
723 }
724
725 return null;
726 }
727
728 /**
729 * Notify the user about available updates.
730 * <p>
731 * Will only be called when a version is available.
732 * <p>
733 * Note that you can call {@link VersionCheck#ok()} on it if the user has
734 * read the information (by default, it is marked read only on certain other
735 * actions).
736 *
737 * @param updates
738 * the new version information
739 */
740 protected void notifyUpdates(VersionCheck updates) {
741 // Sent to syserr so not to cause problem if one tries to capture a
742 // story content in text mode
743 System.err.println(
744 "A new version of the program is available at https://github.com/nikiroo/fanfix/releases");
745 System.err.println("");
746 for (Version v : updates.getNewer()) {
747 System.err.println("\tVersion " + v);
748 System.err.println("\t-------------");
749 System.err.println("");
750 for (String it : updates.getChanges().get(v)) {
751 System.err.println("\t- " + it);
752 }
753 System.err.println("");
754 }
755 }
756
757 /**
758 * Import the given resource into the {@link LocalLibrary}.
759 *
760 * @param url
761 * the resource to import
762 * @param pg
763 * the optional progress reporter
764 *
765 * @return the exit return code (0 = success)
766 */
767 protected static int imprt(URL url, Progress pg) {
768 try {
769 MetaData meta = Instance.getInstance().getLibrary().imprt(url, pg);
770 System.out.println(meta.getLuid() + ": \"" + meta.getTitle() + "\" imported.");
771 } catch (IOException e) {
772 Instance.getInstance().getTraceHandler().error(e);
773 return 1;
774 }
775
776 return 0;
777 }
778
779 /**
780 * Export the {@link Story} from the {@link LocalLibrary} to the given
781 * target.
782 *
783 * @param luid
784 * the story LUID
785 * @param type
786 * the {@link OutputType} to use
787 * @param target
788 * the target
789 * @param pg
790 * the optional progress reporter
791 *
792 * @return the exit return code (0 = success)
793 */
794 protected static int export(String luid, OutputType type, String target,
795 Progress pg) {
796 try {
797 Instance.getInstance().getLibrary().export(luid, type, target, pg);
798 } catch (IOException e) {
799 Instance.getInstance().getTraceHandler().error(e);
800 return 4;
801 }
802
803 return 0;
804 }
805
806 /**
807 * List the stories of the given source from the {@link LocalLibrary}
808 * (unless NULL is passed, in which case all stories will be listed).
809 *
810 * @param source
811 * the source to list the known stories of, or NULL to list all
812 * stories
813 *
814 * @return the exit return code (0 = success)
815 */
816 protected int list(String source) {
817 try {
818 new CliReader().listBooks(source);
819 } catch (IOException e) {
820 Instance.getInstance().getTraceHandler().error(e);
821 return 66;
822 }
823
824 return 0;
825 }
826
827 /**
828 * Start the current reader for this {@link Story}.
829 *
830 * @param story
831 * the story to read
832 * @param chap
833 * which {@link Chapter} to read (starting at 1), or NULL to get
834 * the {@link Story} description
835 *
836 * @return the exit return code (0 = success)
837 */
838 protected int read(Story story, Integer chap) {
839 if (story != null) {
840 try {
841 if (chap == null) {
842 new CliReader().listChapters(story);
843 } else {
844 new CliReader().printChapter(story, chap);
845 }
846 } catch (IOException e) {
847 Instance.getInstance().getTraceHandler()
848 .error(new IOException("Failed to read book", e));
849 return 2;
850 }
851 } else {
852 Instance.getInstance().getTraceHandler()
853 .error("Cannot find book: " + story);
854 return 2;
855 }
856
857 return 0;
858 }
859
860 /**
861 * Convert the {@link Story} into another format.
862 *
863 * @param urlString
864 * the source {@link Story} to convert
865 * @param type
866 * the {@link OutputType} to convert to
867 * @param target
868 * the target file
869 * @param infoCover
870 * TRUE to also export the cover and info file, even if the given
871 * {@link OutputType} does not usually save them
872 * @param pg
873 * the optional progress reporter
874 *
875 * @return the exit return code (0 = success)
876 */
877 protected int convert(String urlString, OutputType type,
878 String target, boolean infoCover, Progress pg) {
879 int exitCode = 0;
880
881 Instance.getInstance().getTraceHandler().trace("Convert: " + urlString);
882 String sourceName = urlString;
883 try {
884 URL source = BasicReader.getUrl(urlString);
885 sourceName = source.toString();
886 if (sourceName.startsWith("file://")) {
887 sourceName = sourceName.substring("file://".length());
888 }
889
890 try {
891 BasicSupport support = BasicSupport.getSupport(source);
892
893 if (support != null) {
894 Instance.getInstance().getTraceHandler()
895 .trace("Support found: " + support.getClass());
896 Progress pgIn = new Progress();
897 Progress pgOut = new Progress();
898 if (pg != null) {
899 pg.setMax(2);
900 pg.addProgress(pgIn, 1);
901 pg.addProgress(pgOut, 1);
902 }
903
904 Story story = support.process(pgIn);
905 try {
906 target = new File(target).getAbsolutePath();
907 BasicOutput.getOutput(type, infoCover, infoCover)
908 .process(story, target, pgOut);
909 } catch (IOException e) {
910 Instance.getInstance().getTraceHandler()
911 .error(new IOException(
912 trans(StringId.ERR_SAVING, target), e));
913 exitCode = 5;
914 }
915 } else {
916 Instance.getInstance().getTraceHandler()
917 .error(new IOException(
918 trans(StringId.ERR_NOT_SUPPORTED, source)));
919
920 exitCode = 4;
921 }
922 } catch (IOException e) {
923 Instance.getInstance().getTraceHandler().error(new IOException(
924 trans(StringId.ERR_LOADING, sourceName), e));
925 exitCode = 3;
926 }
927 } catch (MalformedURLException e) {
928 Instance.getInstance().getTraceHandler().error(new IOException(trans(StringId.ERR_BAD_URL, sourceName), e));
929 exitCode = 1;
930 }
931
932 return exitCode;
933 }
934
935 /**
936 * Display the correct syntax of the program to the user to stdout, or an
937 * error message if the syntax used was wrong on stderr.
938 *
939 * @param showHelp
940 * TRUE to show the syntax help, FALSE to show "syntax error"
941 */
942 protected void syntax(boolean showHelp) {
943 if (showHelp) {
944 StringBuilder builder = new StringBuilder();
945 for (SupportType type : SupportType.values()) {
946 builder.append(trans(StringId.ERR_SYNTAX_TYPE, type.toString(),
947 type.getDesc()));
948 builder.append('\n');
949 }
950
951 String typesIn = builder.toString();
952 builder.setLength(0);
953
954 for (OutputType type : OutputType.values()) {
955 builder.append(trans(StringId.ERR_SYNTAX_TYPE, type.toString(),
956 type.getDesc(true)));
957 builder.append('\n');
958 }
959
960 String typesOut = builder.toString();
961
962 System.out.println(trans(StringId.HELP_SYNTAX, typesIn, typesOut));
963 } else {
964 System.err.println(trans(StringId.ERR_SYNTAX));
965 }
966 }
967
968 /**
969 * Starts a search operation (i.e., list the available web sites we can
970 * search on).
971 *
972 * @throws IOException
973 * in case of I/O errors
974 */
975 protected void search() throws IOException {
976 new CliReader().listSearchables();
977 }
978
979 /**
980 * Search for books by keywords on the given supported web site.
981 *
982 * @param searchOn
983 * the web site to search on
984 * @param search
985 * the keyword to look for
986 * @param page
987 * the page of results to get, or 0 to inquire about the number
988 * of pages
989 * @param item
990 * the index of the book we are interested by, or 0 to query
991 * about how many books are in that page of results
992 *
993 * @throws IOException
994 * in case of I/O error
995 */
996 protected void searchKeywords(SupportType searchOn, String search,
997 int page, Integer item) throws IOException {
998 new CliReader().searchBooksByKeyword(searchOn, search, page, item);
999 }
1000
1001 /**
1002 * Search for books by tags on the given supported web site.
1003 *
1004 * @param searchOn
1005 * the web site to search on
1006 * @param page
1007 * the page of results to get, or 0 to inquire about the number
1008 * of pages
1009 * @param item
1010 * the index of the book we are interested by, or 0 to query
1011 * about how many books are in that page of results
1012 * @param tags
1013 * the tags to look for
1014 *
1015 * @throws IOException
1016 * in case of I/O error
1017 */
1018 protected void searchTags(SupportType searchOn, Integer page, Integer item,
1019 Integer[] tags) throws IOException {
1020 new CliReader().searchBooksByTag(searchOn, page, item, tags);
1021 }
1022
1023 /**
1024 * Start a Fanfix server.
1025 *
1026 * @param key
1027 * the key taht will be needed to contact the Fanfix server
1028 * @param port
1029 * the port on which to run
1030 *
1031 * @throws IOException
1032 * in case of I/O errors
1033 * @throws SSLException
1034 * when the key was not accepted
1035 */
1036 private void startServer(String key, int port) throws IOException {
1037 ServerObject server = new RemoteLibraryServer(key, port);
1038 server.setTraceHandler(Instance.getInstance().getTraceHandler());
1039 server.run();
1040 }
1041
1042 /**
1043 * Stop a running Fanfix server.
1044 *
1045 * @param key
1046 * the key to contact the Fanfix server
1047 * @param host
1048 * the host on which it runs (NULL means localhost)
1049 * @param port
1050 * the port on which it runs
1051 *
1052 * @throws IOException
1053 * in case of I/O errors
1054 * @throws SSLException
1055 * when the key was not accepted
1056 */
1057 private void stopServer(
1058 String key, String host, Integer port)
1059 throws IOException, SSLException {
1060 new RemoteLibrary(key, host, port).exit();
1061 }
1062
1063 /**
1064 * We are done and ready to exit.
1065 * <p>
1066 * By default, it will call {@link System#exit(int)} if the status is not 0.
1067 *
1068 * @param status
1069 * the exit status
1070 */
1071 protected void exit(int status) {
1072 if (status != 0) {
1073 System.exit(status);
1074 }
1075 }
1076
1077 /**
1078 * Simple shortcut method to call {link Instance#getTrans()#getString()}.
1079 *
1080 * @param id
1081 * the ID to translate
1082 *
1083 * @return the translated result
1084 */
1085 static private String trans(StringId id, Object... params) {
1086 return Instance.getInstance().getTrans().getString(id, params);
1087 }
1088 }