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