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