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