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