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