1 package be
.nikiroo
.jvcard
.launcher
;
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
;
11 import javax
.imageio
.ImageIO
;
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
;
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
39 static public final String APPLICATION_TITLE
= "jVcard";
40 static public final String APPLICATION_VERSION
= "1.0-beta3-dev";
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
;
47 static private String defaultFn
;
48 static private boolean forceComputedFn
;
51 CONTACT_MANAGER
, I18N
, SERVER
, LOAD_PHOTO
, SAVE_PHOTO
, ONLY_PHOTO
, SAVE_CONFIG
55 * Translate the given {@link StringId} into user text.
60 * the values to insert instead of the place holders in the
63 * @return the translated text with the given value where required
65 static public String
trans(StringId id
, Object
... values
) {
66 return transService
.getString(id
, (Object
[]) values
);
70 * Check if unicode characters should be used.
72 * @return TRUE to allow unicode
74 static public boolean isUnicode() {
75 return transService
.isUnicode();
79 * Start the application.
82 * The returned exit codes are:
84 * <li>1: no files to open</li>
85 * <li>2: invalid syntax</li>
86 * <li>3: internal error</li>
91 * the parameters (see <tt>--help</tt> to know which are
94 public static void main(String
[] args
) {
95 Boolean textMode
= null;
96 boolean noMoreParams
= false;
97 boolean filesTried
= false;
99 // get the "system default" language to help translate the --help
101 String language
= null;
102 transService
= new TransBundle(language
);
104 boolean unicode
= transService
.isUnicode();
106 List
<String
> files
= new LinkedList
<String
>();
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("--")) {
114 } else if (!noMoreParams
&& arg
.equals("--help")) {
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");
135 } else if (!noMoreParams
&& arg
.equals("--tui")) {
137 } else if (!noMoreParams
&& arg
.equals("--gui")) {
139 } else if (!noMoreParams
&& arg
.equals("--noutf")) {
141 transService
.setUnicode(unicode
);
142 } else if (!noMoreParams
&& arg
.equals("--lang")) {
144 if (index
>= args
.length
) {
145 System
.err
.println("Syntax error: no language given");
146 System
.exit(ERR_SYNTAX
);
150 language
= args
[index
];
151 transService
= new TransBundle(language
);
152 transService
.setUnicode(unicode
);
153 } else if (!noMoreParams
&& arg
.equals("--config")) {
155 if (index
>= args
.length
) {
157 .println("Syntax error: no config directory given");
158 System
.exit(ERR_SYNTAX
);
162 Bundles
.setDirectory(args
[index
]);
163 transService
= new TransBundle(language
);
164 transService
.setUnicode(unicode
);
165 } else if (!noMoreParams
&& arg
.equals("--save-config")) {
167 if (index
>= args
.length
) {
169 .println("Syntax error: no config directory given");
170 System
.exit(ERR_SYNTAX
);
175 if (mode
!= Mode
.CONTACT_MANAGER
) {
177 .println("Syntax error: you can only use one of: \n"
182 + "--save-photo\n" + "--only-photo\n");
183 System
.exit(ERR_SYNTAX
);
186 mode
= Mode
.SAVE_CONFIG
;
187 } else if (!noMoreParams
&& arg
.equals("--server")) {
188 if (mode
!= Mode
.CONTACT_MANAGER
) {
190 .println("Syntax error: you can only use one of: \n"
195 + "--save-photo\n" + "--only-photo\n");
196 System
.exit(ERR_SYNTAX
);
202 if (index
>= args
.length
) {
203 System
.err
.println("Syntax error: no port given");
204 System
.exit(ERR_SYNTAX
);
209 port
= Integer
.parseInt(args
[index
]);
210 } catch (NumberFormatException e
) {
211 System
.err
.println("Invalid port number: " + args
[index
]);
212 System
.exit(ERR_SYNTAX
);
215 } else if (!noMoreParams
&& arg
.equals("--i18n")) {
216 if (mode
!= Mode
.CONTACT_MANAGER
) {
218 .println("Syntax error: you can only use one of: \n"
223 + "--save-photo\n" + "--only-photo\n");
224 System
.exit(ERR_SYNTAX
);
230 if (index
>= args
.length
) {
232 .println("Syntax error: no .properties directory given");
233 System
.exit(ERR_SYNTAX
);
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
) {
244 .println("Syntax error: you can only use one of: \n"
249 + "--save-photo\n" + "--only-photo\n");
250 System
.exit(ERR_SYNTAX
);
254 if (arg
.equals("--load-photo")) {
255 mode
= Mode
.LOAD_PHOTO
;
256 } else if (arg
.equals("--save-photo")) {
257 mode
= Mode
.SAVE_PHOTO
;
259 mode
= Mode
.ONLY_PHOTO
;
263 if (index
>= args
.length
) {
264 System
.err
.println("Syntax error: photo directory given");
265 System
.exit(ERR_SYNTAX
);
272 if (index
>= args
.length
) {
273 System
.err
.println("Syntax error: photo format given");
274 System
.exit(ERR_SYNTAX
);
278 format
= args
[index
];
281 files
.addAll(open(arg
));
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");
295 // N/FN fix information:
299 if (mode
== Mode
.SERVER
&& files
.size() > 0) {
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) {
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) {
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("."));
317 if (files
.size() == 0) {
318 System
.err
.println("No files to open");
319 System
.exit(ERR_NO_FILE
);
328 if (!new File(dir
).isDirectory()) {
329 if (!new File(dir
).mkdir()) {
331 .println("Cannot create configuration directory: "
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
) {
343 System
.exit(ERR_INTERNAL
);
349 Optional
.runServer(port
);
350 } catch (Exception e
) {
351 if (e
instanceof IOException
) {
353 .println("I/O Exception: Cannot start the server");
355 System
.err
.println("Remoting support not available");
356 System
.exit(ERR_INTERNAL
);
363 transService
.updateFile(dir
);
364 } catch (IOException e
) {
366 .println("I/O Exception: Cannot create/update a language in directory: "
374 for (String file
: files
) {
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
);
383 String b64
= StringUtils
.fromImage(ImageIO
386 if (mode
== Mode
.ONLY_PHOTO
) {
387 for (Data photo
= contact
388 .getPreferredData("PHOTO"); photo
!= null; photo
= contact
389 .getPreferredData("PHOTO")) {
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);
399 } catch (IOException e
) {
400 System
.err
.println("Cannot read photo: "
406 } catch (IOException e
) {
407 System
.err
.println("Card cannot be opened: " + file
);
413 for (String file
: files
) {
415 Card card
= getCard(file
, null).getCard();
416 for (Contact contact
: card
) {
417 Data photo
= contact
.getPreferredData("PHOTO");
419 String filename
= contact
.toString(format
, "");
420 File f
= new File(dir
, filename
+ ".png");
423 StringUtils
.toImage(photo
.getValue()),
425 } catch (IOException e
) {
427 .println("Cannot save photo of contact: "
429 .getPreferredDataValue("FN"));
433 } catch (IOException e
) {
434 System
.err
.println("Card cannot be opened: " + file
);
439 case CONTACT_MANAGER
: {
441 Optional
.startTui(textMode
, files
);
442 } catch (Exception e
) {
443 if (e
instanceof IOException
) {
445 .println("I/O Exception: Cannot start the program with the given cards");
447 System
.err
.println("TUI support not available");
448 System
.exit(ERR_INTERNAL
);
457 * Return the {@link Card} corresponding to the given resource name -- a
458 * file or a remote jvcard URL.
461 * Will also fix the FN if required (see display.properties).
465 * a filename or a remote jvcard url with named resource (e.g.:
466 * <tt>jvcard://localhost:4444/coworkers.vcf</tt>)
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)
472 * @return the {@link Card}
474 * @throws IOException
475 * in case of IO error or remoting not available
477 static public CardResult
getCard(String input
, MergeCallback callback
)
479 boolean remote
= false;
480 Format format
= Format
.Abook
;
482 if (ext
.contains(".")) {
483 String tab
[] = ext
.split("\\.");
484 if (tab
.length
> 1 && tab
[tab
.length
- 1].equalsIgnoreCase("vcf")) {
485 format
= Format
.VCard21
;
489 if (input
.contains("://")) {
490 format
= Format
.VCard21
;
494 CardResult card
= null;
497 card
= Optional
.syncCard(input
, callback
);
499 card
= new CardResult(new Card(new File(input
), format
), false,
502 } catch (IOException ioe
) {
504 } catch (Exception e
) {
505 throw new IOException("Remoting support not available", e
);
509 if (defaultFn
!= null) {
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
, ""));
518 } catch (Exception e
) {
519 // sync failed -> getCard() throws.
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.
534 * @return the list of opened files
536 static private List
<String
> open(String path
) {
537 List
<String
> files
= new LinkedList
<String
>();
539 if (path
!= null && path
.startsWith("jvcard://")) {
540 if (path
.endsWith("/")) {
541 files
.addAll(list(path
));
546 File file
= new File(path
);
548 if (file
.isDirectory()) {
549 for (File subfile
: file
.listFiles()) {
550 if (!subfile
.isDirectory())
551 files
.add(subfile
.getAbsolutePath());
554 files
.add(file
.getAbsolutePath());
557 System
.err
.println("File or directory not found: \"" + path
566 * List all the available {@link Card}s on the given network location (which
567 * is expected to be a jVCard remote server, obviously).
570 * the jVCard remote server path (e.g.:
571 * <tt>jvcard://localhost:4444/</tt>)
573 * @return the list of {@link Card}s
575 static private List
<String
> list(String path
) {
576 List
<String
> files
= new LinkedList
<String
>();
579 String host
= path
.split("\\:")[1].substring(2);
580 int port
= Integer
.parseInt(path
.split("\\:")[2].replaceAll("/$",
582 SimpleSocket s
= new SimpleSocket(new Socket(host
, port
),
586 s
.sendCommand(Command
.LIST_CARD
);
587 for (String p
: s
.receiveBlock()) {
589 + p
.substring(StringUtils
.fromTime(0).length() + 1));
592 } catch (Exception e
) {
600 * Really, really ask for UTF-8 encoding.
602 static private void utf8() {
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
) {
616 * Read display.properties to know if we should fix the FN field when empty,
617 * or always, or never.
619 static private void readNFN() {
620 DisplayBundle map
= new DisplayBundle();
622 defaultFn
= map
.getString(DisplayOption
.CONTACT_DETAILS_DEFAULT_FN
);
624 forceComputedFn
= map
.getBoolean(
625 DisplayOption
.CONTACT_DETAILS_SHOW_COMPUTED_FN
, false);