Resources system rewrite + new "--save-config DIR" option
[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-beta3-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, ONLY_PHOTO, SAVE_CONFIG
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 System.out
116 .println("TODO: implement some help text.\n"
117 + "Usable switches:\n"
118 + "\t--: stop looking for switches\n"
119 + "\t--help: this here thingy\n"
120 + "\t--lang LANGUAGE: choose the language, for instance en_GB\n"
121 + "\t--tui: force pure text mode even if swing treminal is available\n"
122 + "\t--gui: force swing terminal mode\n"
123 + "\t--noutf: force non-utf8 mode if you need it\n"
124 + "\t--config DIRECTORY: use the given directory as a CONFIG_DIR\n"
125 + "\t--save-config DIRECTORY: save the current config to DIRECTORY (lang: only current)\n"
126 + "\t--server PORT: start a remoting server instead of a client\n"
127 + "\t--i18n DIR: generate the translation file for the given language (can be \"\") to/from the .properties given dir\n"
128 + "\t--save-photo DIR FORMAT: save the contacts' photos to DIR, named after FORMAT\n"
129 + "\t--load-photo DIR FORMAT: load the contacts' photos from DIR, named after FORMAT\n"
130 + "\t--only-photo DIR FORMAT: load the contacts' photos from DIR, named after FORMAT, overwrite all other photos of selected contacts\n"
131 + "everyhing else is either a file to open or a directory to open\n"
132 + "(we will only open 1st level files in given directories)\n"
133 + "('jvcard://hostname:8888/file' links -- or without 'file' -- are also ok)\n");
134 return;
135 } else if (!noMoreParams && arg.equals("--tui")) {
136 textMode = true;
137 } else if (!noMoreParams && arg.equals("--gui")) {
138 textMode = false;
139 } else if (!noMoreParams && arg.equals("--noutf")) {
140 unicode = false;
141 transService.setUnicode(unicode);
142 } else if (!noMoreParams && arg.equals("--lang")) {
143 index++;
144 if (index >= args.length) {
145 System.err.println("Syntax error: no language given");
146 System.exit(ERR_SYNTAX);
147 return;
148 }
149
150 language = args[index];
151 transService = new TransBundle(language);
152 transService.setUnicode(unicode);
153 } else if (!noMoreParams && arg.equals("--config")) {
154 index++;
155 if (index >= args.length) {
156 System.err
157 .println("Syntax error: no config directory given");
158 System.exit(ERR_SYNTAX);
159 return;
160 }
161
162 Bundles.setDirectory(args[index]);
163 transService = new TransBundle(language);
164 transService.setUnicode(unicode);
165 } else if (!noMoreParams && arg.equals("--save-config")) {
166 index++;
167 if (index >= args.length) {
168 System.err
169 .println("Syntax error: no config directory given");
170 System.exit(ERR_SYNTAX);
171 return;
172 }
173 dir = args[index];
174
175 if (mode != Mode.CONTACT_MANAGER) {
176 System.err
177 .println("Syntax error: you can only use one of: \n"
178 + "--server\n"
179 + "--save-config\n"
180 + "--i18n\n"
181 + "--load-photo\n"
182 + "--save-photo\n" + "--only-photo\n");
183 System.exit(ERR_SYNTAX);
184 return;
185 }
186 mode = Mode.SAVE_CONFIG;
187 } else if (!noMoreParams && arg.equals("--server")) {
188 if (mode != Mode.CONTACT_MANAGER) {
189 System.err
190 .println("Syntax error: you can only use one of: \n"
191 + "--server\n"
192 + "--save-config\n"
193 + "--i18n\n"
194 + "--load-photo\n"
195 + "--save-photo\n" + "--only-photo\n");
196 System.exit(ERR_SYNTAX);
197 return;
198 }
199 mode = Mode.SERVER;
200
201 index++;
202 if (index >= args.length) {
203 System.err.println("Syntax error: no port given");
204 System.exit(ERR_SYNTAX);
205 return;
206 }
207
208 try {
209 port = Integer.parseInt(args[index]);
210 } catch (NumberFormatException e) {
211 System.err.println("Invalid port number: " + args[index]);
212 System.exit(ERR_SYNTAX);
213 return;
214 }
215 } else if (!noMoreParams && arg.equals("--i18n")) {
216 if (mode != Mode.CONTACT_MANAGER) {
217 System.err
218 .println("Syntax error: you can only use one of: \n"
219 + "--server\n"
220 + "--save-config\n"
221 + "--i18n\n"
222 + "--load-photo\n"
223 + "--save-photo\n" + "--only-photo\n");
224 System.exit(ERR_SYNTAX);
225 return;
226 }
227 mode = Mode.I18N;
228
229 index++;
230 if (index >= args.length) {
231 System.err
232 .println("Syntax error: no .properties directory given");
233 System.exit(ERR_SYNTAX);
234 return;
235 }
236
237 dir = args[index];
238 } else if (!noMoreParams
239 && (arg.equals("--load-photo")
240 || arg.equals("--save-photo") || arg
241 .equals("--only-photo"))) {
242 if (mode != Mode.CONTACT_MANAGER) {
243 System.err
244 .println("Syntax error: you can only use one of: \n"
245 + "--server\n"
246 + "--save-config\n"
247 + "--i18n\n"
248 + "--load-photo\n"
249 + "--save-photo\n" + "--only-photo\n");
250 System.exit(ERR_SYNTAX);
251 return;
252 }
253
254 if (arg.equals("--load-photo")) {
255 mode = Mode.LOAD_PHOTO;
256 } else if (arg.equals("--save-photo")) {
257 mode = Mode.SAVE_PHOTO;
258 } else {
259 mode = Mode.ONLY_PHOTO;
260 }
261
262 index++;
263 if (index >= args.length) {
264 System.err.println("Syntax error: photo directory given");
265 System.exit(ERR_SYNTAX);
266 return;
267 }
268
269 dir = args[index];
270
271 index++;
272 if (index >= args.length) {
273 System.err.println("Syntax error: photo format given");
274 System.exit(ERR_SYNTAX);
275 return;
276 }
277
278 format = args[index];
279 } else {
280 filesTried = true;
281 files.addAll(open(arg));
282 }
283 }
284
285 // Force headless mode if we run in forced-text mode
286 if (mode != Mode.CONTACT_MANAGER || (textMode != null && textMode)) {
287 // same as -Djava.awt.headless=true
288 System.setProperty("java.awt.headless", "true");
289 }
290
291 if (unicode) {
292 utf8();
293 }
294
295 // N/FN fix information:
296 readNFN();
297
298 // Error management:
299 if (mode == Mode.SERVER && files.size() > 0) {
300 System.err
301 .println("Invalid syntax: you cannot both use --server and provide card files");
302 System.exit(ERR_SYNTAX);
303 } else if (mode == Mode.I18N && files.size() > 0) {
304 System.err
305 .println("Invalid syntax: you cannot both use --i18n and provide card files");
306 System.exit(ERR_SYNTAX);
307 } else if (mode == Mode.I18N && language == null) {
308 System.err
309 .println("Invalid syntax: you cannot use --i18n without --lang");
310 System.exit(ERR_SYNTAX);
311 } else if ((mode == Mode.CONTACT_MANAGER || mode == Mode.SAVE_PHOTO || mode == Mode.LOAD_PHOTO)
312 && files.size() == 0) {
313 if (files.size() == 0 && !filesTried) {
314 files.addAll(open("."));
315 }
316
317 if (files.size() == 0) {
318 System.err.println("No files to open");
319 System.exit(ERR_NO_FILE);
320 return;
321 }
322 }
323 //
324
325 switch (mode) {
326 case SAVE_CONFIG: {
327 try {
328 if (!new File(dir).isDirectory()) {
329 if (!new File(dir).mkdir()) {
330 System.err
331 .println("Cannot create configuration directory: "
332 + dir);
333 }
334 }
335
336 transService.updateFile(dir); // current lang TransBundle
337 new TransBundle().updateFile(dir);
338 new ColorBundle().updateFile(dir);
339 new DisplayBundle().updateFile(dir);
340 new RemoteBundle().updateFile(dir);
341 } catch (IOException e) {
342 e.printStackTrace();
343 System.exit(ERR_INTERNAL);
344 }
345 break;
346 }
347 case SERVER: {
348 try {
349 Optional.runServer(port);
350 } catch (Exception e) {
351 if (e instanceof IOException) {
352 System.err
353 .println("I/O Exception: Cannot start the server");
354 } else {
355 System.err.println("Remoting support not available");
356 System.exit(ERR_INTERNAL);
357 }
358 }
359 break;
360 }
361 case I18N: {
362 try {
363 transService.updateFile(dir);
364 } catch (IOException e) {
365 System.err
366 .println("I/O Exception: Cannot create/update a language in directory: "
367 + dir);
368 e.printStackTrace();
369 }
370 break;
371 }
372 case ONLY_PHOTO:
373 case LOAD_PHOTO: {
374 for (String file : files) {
375 try {
376 Card card = getCard(file, null).getCard();
377 for (Contact contact : card) {
378 String filename = contact.toString(format, "");
379 File f = new File(dir, filename);
380
381 if (f.exists()) {
382 try {
383 String b64 = StringUtils.fromImage(ImageIO
384 .read(f));
385
386 if (mode == Mode.ONLY_PHOTO) {
387 for (Data photo = contact
388 .getPreferredData("PHOTO"); photo != null; photo = contact
389 .getPreferredData("PHOTO")) {
390 photo.delete();
391 }
392 }
393
394 List<TypeInfo> types = new LinkedList<TypeInfo>();
395 types.add(new TypeInfo("ENCODING", "b"));
396 types.add(new TypeInfo("TYPE", "png"));
397 Data photo = new Data(types, "PHOTO", b64, null);
398 contact.add(photo);
399 } catch (IOException e) {
400 System.err.println("Cannot read photo: "
401 + filename);
402 }
403 }
404 }
405 card.save();
406 } catch (IOException e) {
407 System.err.println("Card cannot be opened: " + file);
408 }
409 }
410 break;
411 }
412 case SAVE_PHOTO: {
413 for (String file : files) {
414 try {
415 Card card = getCard(file, null).getCard();
416 for (Contact contact : card) {
417 Data photo = contact.getPreferredData("PHOTO");
418 if (photo != null) {
419 String filename = contact.toString(format, "");
420 File f = new File(dir, filename + ".png");
421 try {
422 ImageIO.write(
423 StringUtils.toImage(photo.getValue()),
424 "png", f);
425 } catch (IOException e) {
426 System.err
427 .println("Cannot save photo of contact: "
428 + contact
429 .getPreferredDataValue("FN"));
430 }
431 }
432 }
433 } catch (IOException e) {
434 System.err.println("Card cannot be opened: " + file);
435 }
436 }
437 break;
438 }
439 case CONTACT_MANAGER: {
440 try {
441 Optional.startTui(textMode, files);
442 } catch (Exception e) {
443 if (e instanceof IOException) {
444 System.err
445 .println("I/O Exception: Cannot start the program with the given cards");
446 } else {
447 System.err.println("TUI support not available");
448 System.exit(ERR_INTERNAL);
449 }
450 }
451 break;
452 }
453 }
454 }
455
456 /**
457 * Return the {@link Card} corresponding to the given resource name -- a
458 * file or a remote jvcard URL.
459 *
460 * <p>
461 * Will also fix the FN if required (see display.properties).
462 * </p>
463 *
464 * @param input
465 * a filename or a remote jvcard url with named resource (e.g.:
466 * <tt>jvcard://localhost:4444/coworkers.vcf</tt>)
467 * @param callback
468 * the {@link MergeCallback} to call in case of conflict, or NULL
469 * to disallow conflict management (the {@link Card} will not be
470 * allowed to synchronise in case of conflicts)
471 *
472 * @return the {@link Card}
473 *
474 * @throws IOException
475 * in case of IO error or remoting not available
476 */
477 static public CardResult getCard(String input, MergeCallback callback)
478 throws IOException {
479 boolean remote = false;
480 Format format = Format.Abook;
481 String ext = input;
482 if (ext.contains(".")) {
483 String tab[] = ext.split("\\.");
484 if (tab.length > 1 && tab[tab.length - 1].equalsIgnoreCase("vcf")) {
485 format = Format.VCard21;
486 }
487 }
488
489 if (input.contains("://")) {
490 format = Format.VCard21;
491 remote = true;
492 }
493
494 CardResult card = null;
495 try {
496 if (remote) {
497 card = Optional.syncCard(input, callback);
498 } else {
499 card = new CardResult(new Card(new File(input), format), false,
500 false, false);
501 }
502 } catch (IOException ioe) {
503 throw ioe;
504 } catch (Exception e) {
505 throw new IOException("Remoting support not available", e);
506 }
507
508 // Fix the FN value
509 if (defaultFn != null) {
510 try {
511 for (Contact contact : card.getCard()) {
512 Data name = contact.getPreferredData("FN");
513 if (name == null || name.getValue().length() == 0
514 || forceComputedFn) {
515 name.setValue(contact.toString(defaultFn, ""));
516 }
517 }
518 } catch (Exception e) {
519 // sync failed -> getCard() throws.
520 // do not update.
521 }
522 }
523
524 return card;
525 }
526
527 /**
528 * Open the given path and add all its files if it is a directory or just
529 * this one if not to the returned list.
530 *
531 * @param path
532 * the path to open
533 *
534 * @return the list of opened files
535 */
536 static private List<String> open(String path) {
537 List<String> files = new LinkedList<String>();
538
539 if (path != null && path.startsWith("jvcard://")) {
540 if (path.endsWith("/")) {
541 files.addAll(list(path));
542 } else {
543 files.add(path);
544 }
545 } else {
546 File file = new File(path);
547 if (file.exists()) {
548 if (file.isDirectory()) {
549 for (File subfile : file.listFiles()) {
550 if (!subfile.isDirectory())
551 files.add(subfile.getAbsolutePath());
552 }
553 } else {
554 files.add(file.getAbsolutePath());
555 }
556 } else {
557 System.err.println("File or directory not found: \"" + path
558 + "\"");
559 }
560 }
561
562 return files;
563 }
564
565 /**
566 * List all the available {@link Card}s on the given network location (which
567 * is expected to be a jVCard remote server, obviously).
568 *
569 * @param path
570 * the jVCard remote server path (e.g.:
571 * <tt>jvcard://localhost:4444/</tt>)
572 *
573 * @return the list of {@link Card}s
574 */
575 static private List<String> list(String path) {
576 List<String> files = new LinkedList<String>();
577
578 try {
579 String host = path.split("\\:")[1].substring(2);
580 int port = Integer.parseInt(path.split("\\:")[2].replaceAll("/$",
581 ""));
582 SimpleSocket s = new SimpleSocket(new Socket(host, port),
583 "sync client");
584 s.open(true);
585
586 s.sendCommand(Command.LIST_CARD);
587 for (String p : s.receiveBlock()) {
588 files.add(path
589 + p.substring(StringUtils.fromTime(0).length() + 1));
590 }
591 s.close();
592 } catch (Exception e) {
593 e.printStackTrace();
594 }
595
596 return files;
597 }
598
599 /**
600 * Really, really ask for UTF-8 encoding.
601 */
602 static private void utf8() {
603 try {
604 System.setProperty("file.encoding", "UTF-8");
605 Field charset = Charset.class.getDeclaredField("defaultCharset");
606 charset.setAccessible(true);
607 charset.set(null, null);
608 } catch (SecurityException e) {
609 } catch (NoSuchFieldException e) {
610 } catch (IllegalArgumentException e) {
611 } catch (IllegalAccessException e) {
612 }
613 }
614
615 /**
616 * Read display.properties to know if we should fix the FN field when empty,
617 * or always, or never.
618 */
619 static private void readNFN() {
620 DisplayBundle map = new DisplayBundle();
621
622 defaultFn = map.getString(DisplayOption.CONTACT_DETAILS_DEFAULT_FN);
623
624 forceComputedFn = map.getBoolean(
625 DisplayOption.CONTACT_DETAILS_SHOW_COMPUTED_FN, false);
626 }
627 }