Fix FN when empty (with a configurable option) + some i18n
[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 import java.util.MissingResourceException;
11 import java.util.ResourceBundle;
12
13 import javax.imageio.ImageIO;
14
15 import be.nikiroo.jvcard.Card;
16 import be.nikiroo.jvcard.Contact;
17 import be.nikiroo.jvcard.Data;
18 import be.nikiroo.jvcard.TypeInfo;
19 import be.nikiroo.jvcard.launcher.CardResult.MergeCallback;
20 import be.nikiroo.jvcard.parsers.Format;
21 import be.nikiroo.jvcard.remote.Command;
22 import be.nikiroo.jvcard.remote.SimpleSocket;
23 import be.nikiroo.jvcard.resources.Bundles;
24 import be.nikiroo.jvcard.resources.StringUtils;
25 import be.nikiroo.jvcard.resources.Trans;
26 import be.nikiroo.jvcard.resources.Trans.StringId;
27
28 /**
29 * This class contains the runnable Main method. It will parse the user supplied
30 * parameters and take action based upon those. Most of the time, it will start
31 * a MainWindow.
32 *
33 * @author niki
34 *
35 */
36 public class Main {
37 static public final String APPLICATION_TITLE = "jVcard";
38 static public final String APPLICATION_VERSION = "1.0-beta3-dev";
39
40 static private final int ERR_NO_FILE = 1;
41 static private final int ERR_SYNTAX = 2;
42 static private final int ERR_INTERNAL = 3;
43 static private Trans transService;
44
45 static private String defaultFn;
46 static private boolean forceComputedFn;
47
48 enum Mode {
49 CONTACT_MANAGER, I18N, SERVER, LOAD_PHOTO, SAVE_PHOTO, ONLY_PHOTO,
50 }
51
52 /**
53 * Translate the given {@link StringId} into user text.
54 *
55 * @param stringId
56 * the ID to translate
57 * @param values
58 * the values to insert instead of the place holders in the
59 * translation
60 *
61 * @return the translated text with the given value where required
62 */
63 static public String trans(StringId id, String... values) {
64 return transService.trans(id, (String[]) values);
65 }
66
67 /**
68 * Check if unicode characters should be used.
69 *
70 * @return TRUE to allow unicode
71 */
72 static public boolean isUnicode() {
73 return transService.isUnicode();
74 }
75
76 /**
77 * Start the application.
78 *
79 * <p>
80 * The returned exit codes are:
81 * <ul>
82 * <li>1: no files to open</li>
83 * <li>2: invalid syntax</li>
84 * <li>3: internal error</li>
85 * </ul>
86 * </p>
87 *
88 * @param args
89 * the parameters (see <tt>--help</tt> to know which are
90 * supported)
91 */
92 public static void main(String[] args) {
93 Boolean textMode = null;
94 boolean noMoreParams = false;
95 boolean filesTried = false;
96
97 // get the "system default" language to help translate the --help
98 // message if needed
99 String language = null;
100 transService = new Trans(language);
101
102 boolean unicode = transService.isUnicode();
103 String dir = null;
104 List<String> files = new LinkedList<String>();
105 int port = -1;
106 Mode mode = Mode.CONTACT_MANAGER;
107 String format = null;
108 for (int index = 0; index < args.length; index++) {
109 String arg = args[index];
110 if (!noMoreParams && arg.equals("--")) {
111 noMoreParams = true;
112 } else if (!noMoreParams && arg.equals("--help")) {
113 System.out
114 .println("TODO: implement some help text.\n"
115 + "Usable switches:\n"
116 + "\t--: stop looking for switches\n"
117 + "\t--help: this here thingy\n"
118 + "\t--lang LANGUAGE: choose the language, for instance en_GB\n"
119 + "\t--tui: force pure text mode even if swing treminal is available\n"
120 + "\t--gui: force swing terminal mode\n"
121 + "\t--noutf: force non-utf8 mode if you need it\n"
122 + "\t--config DIRECTORY: force the given directory as a CONFIG_DIR\n"
123 + "\t--server PORT: start a remoting server instead of a client\n"
124 + "\t--i18n DIR: generate the translation file for the given language (can be \"\") to/from the .properties given dir\n"
125 + "\t--save-photo DIR FORMAT: save the contacts' photos to DIR, named after FORMAT\n"
126 + "\t--load-photo DIR FORMAT: load the contacts' photos from DIR, named after FORMAT\n"
127 + "\t--only-photo DIR FORMAT: load the contacts' photos from DIR, named after FORMAT, overwrite all other photos of selected contacts\n"
128 + "everyhing else is either a file to open or a directory to open\n"
129 + "(we will only open 1st level files in given directories)\n"
130 + "('jvcard://hostname:8888/file' links -- or without 'file' -- are also ok)\n");
131 return;
132 } else if (!noMoreParams && arg.equals("--tui")) {
133 textMode = true;
134 } else if (!noMoreParams && arg.equals("--gui")) {
135 textMode = false;
136 } else if (!noMoreParams && arg.equals("--noutf")) {
137 unicode = false;
138 transService.setUnicode(unicode);
139 } else if (!noMoreParams && arg.equals("--lang")) {
140 index++;
141 if (index >= args.length) {
142 System.err.println("Syntax error: no language given");
143 System.exit(ERR_SYNTAX);
144 return;
145 }
146
147 language = args[index];
148 transService = new Trans(language);
149 transService.setUnicode(unicode);
150 } else if (!noMoreParams && arg.equals("--config")) {
151 index++;
152 if (index >= args.length) {
153 System.err
154 .println("Syntax error: no config directory given");
155 System.exit(ERR_SYNTAX);
156 return;
157 }
158
159 Bundles.setDirectory(args[index]);
160 transService = new Trans(language);
161 transService.setUnicode(unicode);
162 } else if (!noMoreParams && arg.equals("--server")) {
163 if (mode != Mode.CONTACT_MANAGER) {
164 System.err
165 .println("Syntax error: you can only use one of: \n"
166 + "--server\n"
167 + "--i18n\n"
168 + "--load-photo\n"
169 + "--save-photo\n"
170 + "--only-photo\n");
171 System.exit(ERR_SYNTAX);
172 return;
173 }
174 mode = Mode.SERVER;
175
176 index++;
177 if (index >= args.length) {
178 System.err.println("Syntax error: no port given");
179 System.exit(ERR_SYNTAX);
180 return;
181 }
182
183 try {
184 port = Integer.parseInt(args[index]);
185 } catch (NumberFormatException e) {
186 System.err.println("Invalid port number: " + args[index]);
187 System.exit(ERR_SYNTAX);
188 return;
189 }
190 } else if (!noMoreParams && arg.equals("--i18n")) {
191 if (mode != Mode.CONTACT_MANAGER) {
192 System.err
193 .println("Syntax error: you can only use one of: \n"
194 + "--server\n"
195 + "--i18n\n"
196 + "--load-photo\n"
197 + "--save-photo\n"
198 + "--only-photo\n");
199 System.exit(ERR_SYNTAX);
200 return;
201 }
202 mode = Mode.I18N;
203
204 index++;
205 if (index >= args.length) {
206 System.err
207 .println("Syntax error: no .properties directory given");
208 System.exit(ERR_SYNTAX);
209 return;
210 }
211
212 dir = args[index];
213 } else if (!noMoreParams
214 && (arg.equals("--load-photo")
215 || arg.equals("--save-photo") || arg
216 .equals("--only-photo"))) {
217 if (mode != Mode.CONTACT_MANAGER) {
218 System.err
219 .println("Syntax error: you can only use one of: \n"
220 + "--server\n"
221 + "--i18n\n"
222 + "--load-photo\n"
223 + "--save-photo\n"
224 + "--only-photo\n");
225 System.exit(ERR_SYNTAX);
226 return;
227 }
228
229 if (arg.equals("--load-photo")) {
230 mode = Mode.LOAD_PHOTO;
231 } else if (arg.equals("--save-photo")) {
232 mode = Mode.SAVE_PHOTO;
233 } else {
234 mode = Mode.ONLY_PHOTO;
235 }
236
237 index++;
238 if (index >= args.length) {
239 System.err.println("Syntax error: photo directory given");
240 System.exit(ERR_SYNTAX);
241 return;
242 }
243
244 dir = args[index];
245
246 index++;
247 if (index >= args.length) {
248 System.err.println("Syntax error: photo format given");
249 System.exit(ERR_SYNTAX);
250 return;
251 }
252
253 format = args[index];
254 } else {
255 filesTried = true;
256 files.addAll(open(arg));
257 }
258 }
259
260 // Force headless mode if we run in forced-text mode
261 if (mode != Mode.CONTACT_MANAGER || (textMode != null && textMode)) {
262 // same as -Djava.awt.headless=true
263 System.setProperty("java.awt.headless", "true");
264 }
265
266 if (unicode) {
267 utf8();
268 }
269
270 // N/FN fix information:
271 readNFN();
272
273 // Error management:
274 if (mode == Mode.SERVER && files.size() > 0) {
275 System.err
276 .println("Invalid syntax: you cannot both use --server and provide card files");
277 System.exit(ERR_SYNTAX);
278 } else if (mode == Mode.I18N && files.size() > 0) {
279 System.err
280 .println("Invalid syntax: you cannot both use --i18n and provide card files");
281 System.exit(ERR_SYNTAX);
282 } else if (mode == Mode.I18N && language == null) {
283 System.err
284 .println("Invalid syntax: you cannot use --i18n without --lang");
285 System.exit(ERR_SYNTAX);
286 } else if ((mode == Mode.CONTACT_MANAGER || mode == Mode.SAVE_PHOTO || mode == Mode.LOAD_PHOTO)
287 && files.size() == 0) {
288 if (files.size() == 0 && !filesTried) {
289 files.addAll(open("."));
290 }
291
292 if (files.size() == 0) {
293 System.err.println("No files to open");
294 System.exit(ERR_NO_FILE);
295 return;
296 }
297 }
298 //
299
300 switch (mode) {
301 case SERVER: {
302 try {
303 Optional.runServer(port);
304 } catch (Exception e) {
305 if (e instanceof IOException) {
306 System.err
307 .println("I/O Exception: Cannot start the server");
308 } else {
309 System.err.println("Remoting support not available");
310 System.exit(ERR_INTERNAL);
311 }
312 }
313 break;
314 }
315 case I18N: {
316 try {
317 Trans.generateTranslationFile(dir, language);
318 } catch (IOException e) {
319 System.err
320 .println("I/O Exception: Cannot create/update a language in directory: "
321 + dir);
322 }
323 break;
324 }
325 case ONLY_PHOTO:
326 case LOAD_PHOTO: {
327 for (String file : files) {
328 try {
329 Card card = getCard(file, null).getCard();
330 for (Contact contact : card) {
331 String filename = contact.toString(format, "");
332 File f = new File(dir, filename);
333
334 if (f.exists()) {
335 try {
336 String b64 = StringUtils.fromImage(ImageIO
337 .read(f));
338
339 if (mode == Mode.ONLY_PHOTO) {
340 for (Data photo = contact
341 .getPreferredData("PHOTO"); photo != null; photo = contact
342 .getPreferredData("PHOTO")) {
343 photo.delete();
344 }
345 }
346
347 List<TypeInfo> types = new LinkedList<TypeInfo>();
348 types.add(new TypeInfo("ENCODING", "b"));
349 types.add(new TypeInfo("TYPE", "png"));
350 Data photo = new Data(types, "PHOTO", b64, null);
351 contact.add(photo);
352 } catch (IOException e) {
353 System.err.println("Cannot read photo: "
354 + filename);
355 }
356 }
357 }
358 card.save();
359 } catch (IOException e) {
360 System.err.println("Card cannot be opened: " + 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 try {
375 ImageIO.write(
376 StringUtils.toImage(photo.getValue()),
377 "png", f);
378 } catch (IOException e) {
379 System.err
380 .println("Cannot save photo of contact: "
381 + contact
382 .getPreferredDataValue("FN"));
383 }
384 }
385 }
386 } catch (IOException e) {
387 System.err.println("Card cannot be opened: " + file);
388 }
389 }
390 break;
391 }
392 case CONTACT_MANAGER: {
393 try {
394 Optional.startTui(textMode, files);
395 } catch (Exception e) {
396 if (e instanceof IOException) {
397 System.err
398 .println("I/O Exception: Cannot start the program with the given cards");
399 } else {
400 System.err.println("TUI support not available");
401 System.exit(ERR_INTERNAL);
402 }
403 }
404 break;
405 }
406 }
407 }
408
409 /**
410 * Return the {@link Card} corresponding to the given resource name -- a
411 * file or a remote jvcard URL.
412 *
413 * <p>
414 * Will also fix the FN if required (see display.properties).
415 * </p>
416 *
417 * @param input
418 * a filename or a remote jvcard url with named resource (e.g.:
419 * <tt>jvcard://localhost:4444/coworkers.vcf</tt>)
420 * @param callback
421 * the {@link MergeCallback} to call in case of conflict, or NULL
422 * to disallow conflict management (the {@link Card} will not be
423 * allowed to synchronise in case of conflicts)
424 *
425 * @return the {@link Card}
426 *
427 * @throws IOException
428 * in case of IO error or remoting not available
429 */
430 static public CardResult getCard(String input, MergeCallback callback)
431 throws IOException {
432 boolean remote = false;
433 Format format = Format.Abook;
434 String ext = input;
435 if (ext.contains(".")) {
436 String tab[] = ext.split("\\.");
437 if (tab.length > 1 && tab[tab.length - 1].equalsIgnoreCase("vcf")) {
438 format = Format.VCard21;
439 }
440 }
441
442 if (input.contains("://")) {
443 format = Format.VCard21;
444 remote = true;
445 }
446
447 CardResult card = null;
448 try {
449 if (remote) {
450 card = Optional.syncCard(input, callback);
451 } else {
452 card = new CardResult(new Card(new File(input), format), false,
453 false, false);
454 }
455 } catch (IOException ioe) {
456 throw ioe;
457 } catch (Exception e) {
458 throw new IOException("Remoting support not available", e);
459 }
460
461 // Fix the FN value
462 if (defaultFn != null) {
463 try {
464 for (Contact contact : card.getCard()) {
465 Data name = contact.getPreferredData("FN");
466 if (name == null || name.getValue().length() == 0
467 || forceComputedFn) {
468 name.setValue(contact.toString(defaultFn, ""));
469 }
470 }
471 } catch (Exception e) {
472 // sync failed -> getCard() throws.
473 // do not update.
474 }
475 }
476
477 return card;
478 }
479
480 /**
481 * Open the given path and add all its files if it is a directory or just
482 * this one if not to the returned list.
483 *
484 * @param path
485 * the path to open
486 *
487 * @return the list of opened files
488 */
489 static private List<String> open(String path) {
490 List<String> files = new LinkedList<String>();
491
492 if (path != null && path.startsWith("jvcard://")) {
493 if (path.endsWith("/")) {
494 files.addAll(list(path));
495 } else {
496 files.add(path);
497 }
498 } else {
499 File file = new File(path);
500 if (file.exists()) {
501 if (file.isDirectory()) {
502 for (File subfile : file.listFiles()) {
503 if (!subfile.isDirectory())
504 files.add(subfile.getAbsolutePath());
505 }
506 } else {
507 files.add(file.getAbsolutePath());
508 }
509 } else {
510 System.err.println("File or directory not found: \"" + path
511 + "\"");
512 }
513 }
514
515 return files;
516 }
517
518 /**
519 * List all the available {@link Card}s on the given network location (which
520 * is expected to be a jVCard remote server, obviously).
521 *
522 * @param path
523 * the jVCard remote server path (e.g.:
524 * <tt>jvcard://localhost:4444/</tt>)
525 *
526 * @return the list of {@link Card}s
527 */
528 static private List<String> list(String path) {
529 List<String> files = new LinkedList<String>();
530
531 try {
532 String host = path.split("\\:")[1].substring(2);
533 int port = Integer.parseInt(path.split("\\:")[2].replaceAll("/$",
534 ""));
535 SimpleSocket s = new SimpleSocket(new Socket(host, port),
536 "sync client");
537 s.open(true);
538
539 s.sendCommand(Command.LIST_CARD);
540 for (String p : s.receiveBlock()) {
541 files.add(path
542 + p.substring(StringUtils.fromTime(0).length() + 1));
543 }
544 s.close();
545 } catch (Exception e) {
546 e.printStackTrace();
547 }
548
549 return files;
550 }
551
552 /**
553 * Really, really ask for UTF-8 encoding.
554 */
555 static private void utf8() {
556 try {
557 System.setProperty("file.encoding", "UTF-8");
558 Field charset = Charset.class.getDeclaredField("defaultCharset");
559 charset.setAccessible(true);
560 charset.set(null, null);
561 } catch (SecurityException e) {
562 } catch (NoSuchFieldException e) {
563 } catch (IllegalArgumentException e) {
564 } catch (IllegalAccessException e) {
565 }
566 }
567
568 /**
569 * Read display.properties to know if we should fix the FN field when empty,
570 * or always, or never.
571 */
572 static private void readNFN() {
573 ResourceBundle map = Bundles.getBundle("display");
574 try {
575 defaultFn = map.getString("CONTACT_DETAILS_DEFAULT_FN");
576 if (defaultFn.trim().length() == 0)
577 defaultFn = null;
578 } catch (MissingResourceException e) {
579 e.printStackTrace();
580 }
581
582 try {
583 String forceComputedFnStr = map
584 .getString("CONTACT_DETAILS_SHOW_COMPUTED_FN");
585 if (forceComputedFnStr.length() > 0
586 && forceComputedFnStr.equalsIgnoreCase("true"))
587 forceComputedFn = true;
588 } catch (MissingResourceException e) {
589 e.printStackTrace();
590 }
591 }
592 }