d893e101e710ede1013025beb47f424eb879becd
[jvcard.git] / src / be / nikiroo / jvcard / launcher / Main.java
1 package be.nikiroo.jvcard.launcher;
2
3 import java.io.File;
4 import java.io.FileInputStream;
5 import java.io.IOException;
6 import java.io.InputStream;
7 import java.lang.reflect.Field;
8 import java.net.Socket;
9 import java.nio.charset.Charset;
10 import java.util.LinkedList;
11 import java.util.List;
12
13 import javax.imageio.ImageIO;
14
15 import be.nikiroo.jvcard.Card;
16 import be.nikiroo.jvcard.Contact;
17 import be.nikiroo.jvcard.Data;
18 import be.nikiroo.jvcard.TypeInfo;
19 import be.nikiroo.jvcard.launcher.CardResult.MergeCallback;
20 import be.nikiroo.jvcard.launcher.Optional.NotSupportedException;
21 import be.nikiroo.jvcard.parsers.Format;
22 import be.nikiroo.jvcard.remote.Command;
23 import be.nikiroo.jvcard.remote.SimpleSocket;
24 import be.nikiroo.jvcard.resources.DisplayBundle;
25 import be.nikiroo.jvcard.resources.DisplayOption;
26 import be.nikiroo.jvcard.resources.RemoteBundle;
27 import be.nikiroo.jvcard.resources.StringId;
28 import be.nikiroo.jvcard.resources.TransBundle;
29 import be.nikiroo.utils.ImageUtils;
30 import be.nikiroo.utils.StringUtils;
31 import be.nikiroo.utils.Version;
32 import be.nikiroo.utils.resources.Bundles;
33
34 /**
35 * This class contains the runnable Main method. It will parse the user supplied
36 * parameters and take action based upon those. Most of the time, it will start
37 * a MainWindow.
38 *
39 * @author niki
40 *
41 */
42 public class Main {
43 static public final String APPLICATION_TITLE = "jVcard";
44
45 static private final int ERR_NO_FILE = 1;
46 static private final int ERR_SYNTAX = 2;
47 static private final int ERR_INTERNAL = 3;
48 static private TransBundle transService;
49
50 static private String defaultFn;
51 static private boolean forceComputedFn;
52
53 enum Mode {
54 CONTACT_MANAGER, I18N, SERVER, LOAD_PHOTO, SAVE_PHOTO, SAVE_CONFIG, HELP
55 }
56
57 /**
58 * Translate the given {@link StringId} into user text.
59 *
60 * @param stringId
61 * the ID to translate
62 * @param values
63 * the values to insert instead of the place holders in the
64 * translation
65 *
66 * @return the translated text with the given value where required
67 */
68 static public String trans(StringId id, Object... values) {
69 return transService.getString(id, values);
70 }
71
72 /**
73 * Check if unicode characters should be used.
74 *
75 * @return TRUE to allow unicode
76 */
77 static public boolean isUnicode() {
78 return transService.isUnicode();
79 }
80
81 /**
82 * Start the application.
83 *
84 * <p>
85 * The returned exit codes are:
86 * <ul>
87 * <li>1: no files to open</li>
88 * <li>2: invalid syntax</li>
89 * <li>3: internal error</li>
90 * </ul>
91 * </p>
92 *
93 * @param args
94 * the parameters (see <tt>--help</tt> to know which are
95 * supported)
96 */
97 public static void main(String[] args) {
98 Boolean textMode = null;
99 boolean noMoreParams = false;
100 boolean filesTried = false;
101
102 // get the "system default" language to help translate the --help
103 // message if needed
104 String language = null;
105 transService = new TransBundle(language);
106
107 boolean unicode = transService.isUnicode();
108 String dir = null;
109 List<String> files = new LinkedList<String>();
110 int port = -1;
111 Mode mode = Mode.CONTACT_MANAGER;
112 String format = null;
113 for (int index = 0; index < args.length; index++) {
114 String arg = args[index];
115 if (!noMoreParams && arg.equals("--")) {
116 noMoreParams = true;
117 } else if (!noMoreParams && arg.equals("--help")) {
118 if (mode != Mode.CONTACT_MANAGER) {
119 SERR(StringId.CLI_SERR_MODES);
120 return;
121 }
122 mode = Mode.HELP;
123 } else if (!noMoreParams && arg.equals("--tui")) {
124 textMode = true;
125 } else if (!noMoreParams && arg.equals("--gui")) {
126 textMode = false;
127 } else if (!noMoreParams && arg.equals("--noutf")) {
128 unicode = false;
129 transService.setUnicode(unicode);
130 } else if (!noMoreParams && arg.equals("--lang")) {
131 index++;
132 if (index >= args.length) {
133 SERR(StringId.CLI_SERR_NOLANG);
134 return;
135 }
136
137 language = args[index];
138 transService = new TransBundle(language);
139 transService.setUnicode(unicode);
140 } else if (!noMoreParams && arg.equals("--config")) {
141 index++;
142 if (index >= args.length) {
143 SERR(StringId.CLI_SERR_NODIR);
144 return;
145 }
146
147 Bundles.setDirectory(args[index]);
148 transService = new TransBundle(language);
149 transService.setUnicode(unicode);
150 } else if (!noMoreParams && arg.equals("--save-config")) {
151 index++;
152 if (index >= args.length) {
153 SERR(StringId.CLI_SERR_NODIR);
154 return;
155 }
156 dir = args[index];
157
158 if (mode != Mode.CONTACT_MANAGER) {
159 SERR(StringId.CLI_SERR_MODES);
160 return;
161 }
162 mode = Mode.SAVE_CONFIG;
163 } else if (!noMoreParams && arg.equals("--server")) {
164 if (mode != Mode.CONTACT_MANAGER) {
165 SERR(StringId.CLI_SERR_MODES);
166 return;
167 }
168 mode = Mode.SERVER;
169
170 index++;
171 if (index >= args.length) {
172 SERR(StringId.CLI_SERR_NOPORT);
173 return;
174 }
175
176 try {
177 port = Integer.parseInt(args[index]);
178 } catch (NumberFormatException e) {
179 SERR(StringId.CLI_SERR_BADPORT, "" + args[index]);
180 return;
181 }
182 } else if (!noMoreParams && arg.equals("--i18n")) {
183 if (mode != Mode.CONTACT_MANAGER) {
184 SERR(StringId.CLI_SERR_MODES);
185 return;
186 }
187 mode = Mode.I18N;
188
189 index++;
190 if (index >= args.length) {
191 SERR(StringId.CLI_SERR_NODIR);
192 return;
193 }
194
195 dir = args[index];
196 } else if (!noMoreParams
197 && (arg.equals("--load-photo")
198 || arg.equals("--save-photo") || arg
199 .equals("--only-photo"))) {
200 if (mode != Mode.CONTACT_MANAGER) {
201 SERR(StringId.CLI_SERR_MODES);
202 return;
203 }
204
205 if (arg.equals("--load-photo")) {
206 mode = Mode.LOAD_PHOTO;
207 } else if (arg.equals("--save-photo")) {
208 mode = Mode.SAVE_PHOTO;
209 }
210
211 index++;
212 if (index >= args.length) {
213 SERR(StringId.CLI_SERR_NODIR);
214 return;
215 }
216
217 dir = args[index];
218
219 index++;
220 if (index >= args.length) {
221 SERR(StringId.CLI_SERR_NOFORMAT);
222 return;
223 }
224
225 format = args[index];
226 } else {
227 filesTried = true;
228 files.addAll(open(arg));
229 }
230 }
231
232 // Force headless mode if we run in forced-text mode
233 if (mode != Mode.CONTACT_MANAGER || (textMode != null && textMode)) {
234 // same as -Djava.awt.headless=true
235 System.setProperty("java.awt.headless", "true");
236 }
237
238 if (unicode) {
239 utf8();
240 }
241
242 // N/FN fix information:
243 readNFN();
244
245 // Error management:
246 if (mode == Mode.SERVER && files.size() > 0) {
247 SERR(StringId.CLI_SERR_NOLANG, "--server");
248 return;
249 } else if (mode == Mode.I18N && files.size() > 0) {
250 SERR(StringId.CLI_SERR_NOLANG, "--i18n");
251 return;
252 } else if (mode == Mode.I18N && language == null) {
253 SERR(StringId.CLI_SERR_NOLANG);
254 } else if ((mode == Mode.CONTACT_MANAGER || mode == Mode.SAVE_PHOTO || mode == Mode.LOAD_PHOTO)
255 && files.size() == 0) {
256 if (files.size() == 0 && !filesTried) {
257 files.addAll(open("."));
258 }
259
260 if (files.size() == 0) {
261 ERR(StringId.CLI_ERR, StringId.CLI_ERR_NOFILES, ERR_NO_FILE);
262 return;
263 }
264 }
265 //
266
267 switch (mode) {
268 case SAVE_CONFIG: {
269 try {
270 if (!new File(dir).isDirectory()) {
271 if (!new File(dir).mkdir()) {
272 System.err.println(trans(
273 StringId.CLI_ERR_CANNOT_CREATE_CONFDIR, dir));
274 }
275 }
276
277 new TransBundle().updateFile(dir); // default locale
278 for (String lang : new TransBundle().getKnownLanguages()) {
279 new TransBundle(lang).updateFile(dir);
280 }
281
282 // new UIColors().updateFile(dir);
283 new DisplayBundle().updateFile(dir);
284 new RemoteBundle().updateFile(dir);
285 } catch (IOException e) {
286 e.printStackTrace();
287 System.err.flush();
288 System.exit(ERR_INTERNAL);
289 }
290 break;
291 }
292 case SERVER: {
293 try {
294 Optional.runServer(port);
295 } catch (IOException e) {
296 ERR(StringId.CLI_ERR, StringId.CLI_ERR_CANNOT_START,
297 ERR_INTERNAL);
298 return;
299 } catch (NotSupportedException e) {
300 if (!e.isCompiledIn()) {
301 ERR(StringId.CLI_ERR, StringId.CLI_ERR_NO_REMOTING,
302 ERR_INTERNAL);
303 return;
304 } else {
305 e.printStackTrace();
306 ERR(StringId.CLI_ERR, StringId.CLI_ERR, ERR_INTERNAL);
307 return;
308 }
309 }
310 break;
311 }
312 case I18N: {
313 try {
314 transService.updateFile(dir);
315 } catch (IOException e) {
316 ERR(StringId.CLI_ERR, StringId.CLI_ERR_CANNOT_CREATE_LANG,
317 ERR_INTERNAL);
318 return;
319 }
320 break;
321 }
322 case LOAD_PHOTO: {
323 for (String file : files) {
324 try {
325 Card card = getCard(file, null).getCard();
326 for (Contact contact : card) {
327 String filename = contact.toString(format, "");
328 File f = new File(dir, filename);
329
330 if (f.exists()) {
331 System.out.println("Loading " + f);
332 try {
333 String type = "jpeg";
334 int dotIndex = filename.indexOf('.');
335 if (dotIndex >= 0
336 && (dotIndex + 1) < filename.length()) {
337 type = filename.substring(dotIndex + 1)
338 .toLowerCase();
339 }
340
341 String b64;
342 InputStream in = null;
343 try {
344 in = new FileInputStream(f);
345 b64 = ImageUtils.toBase64(in);
346 } finally {
347 if (in != null) {
348 in.close();
349 }
350 }
351
352 // remove previous photos:
353 for (Data photo = contact
354 .getPreferredData("PHOTO"); photo != null; photo = contact
355 .getPreferredData("PHOTO")) {
356 photo.delete();
357 }
358 //
359
360 List<TypeInfo> types = new LinkedList<TypeInfo>();
361 types.add(new TypeInfo("ENCODING", "b"));
362 types.add(new TypeInfo("TYPE", type));
363 Data photo = new Data(types, "PHOTO", b64, null);
364 contact.add(photo);
365 } catch (IOException e) {
366 System.err.println("Cannot read photo: "
367 + filename);
368 }
369 }
370 }
371 card.save();
372 } catch (IOException e) {
373 System.err
374 .println(trans(StringId.CLI_ERR_CANNOT_OPEN, file));
375 }
376 }
377 break;
378 }
379 case SAVE_PHOTO: {
380 for (String file : files) {
381 try {
382 Card card = getCard(file, null).getCard();
383 for (Contact contact : card) {
384 Data photo = contact.getPreferredData("PHOTO");
385 if (photo != null) {
386 String filename = contact.toString(format, "");
387 File f = new File(dir, filename + ".png");
388 System.out.println("Saving " + f);
389 try {
390 ImageIO.write(
391 ImageUtils.fromBase64(photo.getValue()),
392 "png", f);
393 } catch (IOException e) {
394 System.err.println(trans(
395 StringId.CLI_ERR_CANNOT_SAVE_PHOTO,
396 contact.getPreferredDataValue("FN")));
397 }
398 }
399 }
400 } catch (IOException e) {
401 System.err
402 .println(trans(StringId.CLI_ERR_CANNOT_OPEN, file));
403 }
404 }
405 break;
406 }
407 case CONTACT_MANAGER: {
408 try {
409 Optional.startTui(textMode, files);
410 } catch (IOException e) {
411 ERR(StringId.CLI_ERR, StringId.CLI_ERR_CANNOT_START,
412 ERR_NO_FILE);
413 return;
414 } catch (NotSupportedException e) {
415 if (!e.isCompiledIn()) {
416 ERR(StringId.CLI_ERR, StringId.CLI_ERR_NO_TUI, ERR_INTERNAL);
417 return;
418 } else {
419 e.printStackTrace();
420 ERR(StringId.CLI_ERR, StringId.CLI_ERR, ERR_INTERNAL);
421 return;
422 }
423 }
424 break;
425 }
426 case HELP: {
427 System.out.println(APPLICATION_TITLE + " "
428 + Version.getCurrentVersion());
429 System.out.println();
430
431 System.out.println(trans(StringId.CLI_HELP));
432 System.out.println();
433
434 System.out.println(trans(StringId.CLI_HELP_MODES));
435 System.out.println("\t--help : "
436 + trans(StringId.CLI_HELP_MODE_HELP));
437 System.out.println("\t(--tui|--gui) (--noutf) ... : "
438 + trans(StringId.CLI_HELP_MODE_CONTACT_MANAGER));
439 System.out.println("\t--server PORT ... : "
440 + trans(StringId.CLI_HELP_MODE_SERVER));
441 System.out.println("\t--save-config DIR : "
442 + trans(StringId.CLI_HELP_MODE_SAVE_CONFIG));
443 System.out.println("\t--i18n DIR ---lang LANG : "
444 + trans(StringId.CLI_HELP_MODE_I18N));
445 System.out.println("\t--load-photo DIR FORMAT ... : "
446 + trans(StringId.CLI_HELP_MODE_LOAD_PHOTO));
447 System.out.println("\t--save-photo DIR FORMAT ... : "
448 + trans(StringId.CLI_HELP_MODE_SAVE_PHOTO));
449 System.out.println();
450
451 System.out.println(trans(StringId.CLI_HELP_OPTIONS));
452 System.out.println("\t-- : " + trans(StringId.CLI_HELP_DD));
453 System.out.println("\t--lang LANG : "
454 + trans(StringId.CLI_HELP_LANG));
455 System.out.println("\t--tui : " + trans(StringId.CLI_HELP_TUI));
456 System.out.println("\t--gui : " + trans(StringId.CLI_HELP_GUI));
457 System.out.println("\t--noutf : "
458 + trans(StringId.CLI_HELP_NOUTF_OPTION));
459 System.out.println("\t--config : "
460 + trans(StringId.CLI_HELP_CONFIG));
461 System.out.println();
462
463 System.out.println(trans(StringId.CLI_HELP_FOOTER));
464 System.out.println();
465
466 }
467 }
468 }
469
470 /**
471 * Return the {@link Card} corresponding to the given resource name -- a
472 * file or a remote jvcard URL.
473 *
474 * <p>
475 * Will also fix the FN if required (see display.properties).
476 * </p>
477 *
478 * @param input
479 * a filename or a remote jvcard url with named resource (e.g.:
480 * <tt>jvcard://localhost:4444/coworkers.vcf</tt>)
481 * @param callback
482 * the {@link MergeCallback} to call in case of conflict, or NULL
483 * to disallow conflict management (the {@link Card} will not be
484 * allowed to synchronise in case of conflicts)
485 *
486 * @return the {@link Card}
487 *
488 * @throws IOException
489 * in case of IO error or remoting not available
490 */
491 static public CardResult getCard(String input, MergeCallback callback)
492 throws IOException {
493 boolean remote = false;
494 Format format = Format.Abook;
495 String ext = input;
496 if (ext.contains(".")) {
497 String tab[] = ext.split("\\.");
498 if (tab.length > 1 && tab[tab.length - 1].equalsIgnoreCase("vcf")) {
499 format = Format.VCard21;
500 }
501 }
502
503 if (input.contains("://")) {
504 format = Format.VCard21;
505 remote = true;
506 }
507
508 CardResult card = null;
509 try {
510 if (remote) {
511 card = Optional.syncCard(input, callback);
512 } else {
513 card = new CardResult(new Card(new File(input), format), false,
514 false, false);
515 }
516 } catch (IOException ioe) {
517 throw ioe;
518 } catch (NotSupportedException e) {
519 throw new IOException("Remoting support not available", e);
520 }
521
522 // Fix the FN value
523 if (defaultFn != null) {
524 try {
525 for (Contact contact : card.getCard()) {
526 Data name = contact.getPreferredData("FN");
527 if (name == null || name.getValue().length() == 0
528 || forceComputedFn) {
529 name.setValue(contact.toString(defaultFn, "").trim());
530 }
531 }
532 } catch (Exception e) {
533 // sync failed -> getCard() throws.
534 // do not update.
535 }
536 }
537
538 return card;
539 }
540
541 /**
542 * Open the given path and add all its files if it is a directory or just
543 * this one if not to the returned list.
544 *
545 * @param path
546 * the path to open
547 *
548 * @return the list of opened files
549 */
550 static private List<String> open(String path) {
551 List<String> files = new LinkedList<String>();
552
553 if (path != null && path.startsWith("jvcard://")) {
554 if (path.endsWith("/")) {
555 files.addAll(list(path));
556 } else {
557 files.add(path);
558 }
559 } else {
560 File file = new File(path);
561 if (file.exists()) {
562 if (file.isDirectory()) {
563 for (File subfile : file.listFiles()) {
564 if (!subfile.isDirectory())
565 files.add(subfile.getAbsolutePath());
566 }
567 } else {
568 files.add(file.getAbsolutePath());
569 }
570 } else {
571 System.err.println("File or directory not found: \"" + path
572 + "\"");
573 }
574 }
575
576 return files;
577 }
578
579 /**
580 * List all the available {@link Card}s on the given network location (which
581 * is expected to be a jVCard remote server, obviously).
582 *
583 * @param path
584 * the jVCard remote server path (e.g.:
585 * <tt>jvcard://localhost:4444/</tt>)
586 *
587 * @return the list of {@link Card}s
588 */
589 static private List<String> list(String path) {
590 List<String> files = new LinkedList<String>();
591
592 try {
593 String host = path.split("\\:")[1].substring(2);
594 int port = Integer.parseInt(path.split("\\:")[2].replaceAll("/$",
595 ""));
596 SimpleSocket s = new SimpleSocket(new Socket(host, port),
597 "sync client");
598 s.open(true);
599
600 s.sendCommand(Command.LIST_CARD);
601 for (String p : s.receiveBlock()) {
602 files.add(path
603 + p.substring(StringUtils.fromTime(0).length() + 1));
604 }
605 s.close();
606 } catch (Exception e) {
607 e.printStackTrace();
608 }
609
610 return files;
611 }
612
613 /**
614 * Really, really ask for UTF-8 encoding.
615 */
616 static private void utf8() {
617 try {
618 System.setProperty("file.encoding", "UTF-8");
619 Field charset = Charset.class.getDeclaredField("defaultCharset");
620 charset.setAccessible(true);
621 charset.set(null, null);
622 } catch (SecurityException e) {
623 } catch (NoSuchFieldException e) {
624 } catch (IllegalArgumentException e) {
625 } catch (IllegalAccessException e) {
626 }
627 }
628
629 /**
630 * Read display.properties to know if we should fix the FN field when empty,
631 * or always, or never.
632 */
633 static private void readNFN() {
634 DisplayBundle map = new DisplayBundle();
635
636 defaultFn = map.getString(DisplayOption.CONTACT_DETAILS_DEFAULT_FN);
637
638 forceComputedFn = map.getBoolean(
639 DisplayOption.CONTACT_DETAILS_SHOW_COMPUTED_FN, false);
640 }
641
642 /**
643 * Syntax error detected, closing the application with an error message.
644 *
645 * @param err
646 * the syntax error case
647 */
648 static private void SERR(StringId err, Object... values) {
649 ERR(StringId.CLI_SERR, err, ERR_SYNTAX, values);
650 }
651
652 /**
653 * Error detected, closing the application with an error message.
654 *
655 * @param err
656 * the error case
657 * @param suberr
658 * the suberror or NULL if none
659 * @param CODE
660 * the error code as declared above
661 */
662 static private void ERR(StringId err, StringId suberr, int CODE,
663 Object... subvalues) {
664 if (suberr == null)
665 System.err.println(trans(err));
666 else
667 System.err.println(trans(err, trans(suberr, subvalues)));
668
669 System.err.flush();
670 System.exit(CODE);
671 }
672 }