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