Resources system rewrite + new "--save-config DIR" option
[jvcard.git] / src / be / nikiroo / jvcard / launcher / Main.java
index 925ade4a4e23a0133385162cf4a03297a0fca5de..7f59539826155b76ada41bd9c9b9ef018879eae7 100644 (file)
@@ -3,21 +3,29 @@ package be.nikiroo.jvcard.launcher;
 import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
 import java.net.Socket;
 import java.nio.charset.Charset;
 import java.util.LinkedList;
 import java.util.List;
 
+import javax.imageio.ImageIO;
+
 import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.Contact;
+import be.nikiroo.jvcard.Data;
+import be.nikiroo.jvcard.TypeInfo;
+import be.nikiroo.jvcard.launcher.CardResult.MergeCallback;
 import be.nikiroo.jvcard.parsers.Format;
 import be.nikiroo.jvcard.remote.Command;
 import be.nikiroo.jvcard.remote.SimpleSocket;
 import be.nikiroo.jvcard.resources.Bundles;
 import be.nikiroo.jvcard.resources.StringUtils;
-import be.nikiroo.jvcard.resources.Trans;
-import be.nikiroo.jvcard.resources.Trans.StringId;
+import be.nikiroo.jvcard.resources.bundles.ColorBundle;
+import be.nikiroo.jvcard.resources.bundles.DisplayBundle;
+import be.nikiroo.jvcard.resources.bundles.RemoteBundle;
+import be.nikiroo.jvcard.resources.bundles.TransBundle;
+import be.nikiroo.jvcard.resources.enums.DisplayOption;
+import be.nikiroo.jvcard.resources.enums.StringId;
 
 /**
  * This class contains the runnable Main method. It will parse the user supplied
@@ -29,23 +37,33 @@ import be.nikiroo.jvcard.resources.Trans.StringId;
  */
 public class Main {
        static public final String APPLICATION_TITLE = "jVcard";
-       static public final String APPLICATION_VERSION = "1.0-beta2-dev";
+       static public final String APPLICATION_VERSION = "1.0-beta3-dev";
 
        static private final int ERR_NO_FILE = 1;
        static private final int ERR_SYNTAX = 2;
        static private final int ERR_INTERNAL = 3;
-       static private Trans transService;
+       static private TransBundle transService;
+
+       static private String defaultFn;
+       static private boolean forceComputedFn;
+
+       enum Mode {
+               CONTACT_MANAGER, I18N, SERVER, LOAD_PHOTO, SAVE_PHOTO, ONLY_PHOTO, SAVE_CONFIG
+       }
 
        /**
-        * Translate the given {@link StringId}.
+        * Translate the given {@link StringId} into user text.
         * 
-        * @param id
+        * @param stringId
         *            the ID to translate
+        * @param values
+        *            the values to insert instead of the place holders in the
+        *            translation
         * 
-        * @return the translation
+        * @return the translated text with the given value where required
         */
-       static public String trans(StringId id) {
-               return transService.trans(id);
+       static public String trans(StringId id, Object... values) {
+               return transService.getString(id, (Object[]) values);
        }
 
        /**
@@ -81,12 +99,14 @@ public class Main {
                // get the "system default" language to help translate the --help
                // message if needed
                String language = null;
-               transService = new Trans(language);
+               transService = new TransBundle(language);
 
                boolean unicode = transService.isUnicode();
-               String i18nDir = null;
+               String dir = null;
                List<String> files = new LinkedList<String>();
-               Integer port = null;
+               int port = -1;
+               Mode mode = Mode.CONTACT_MANAGER;
+               String format = null;
                for (int index = 0; index < args.length; index++) {
                        String arg = args[index];
                        if (!noMoreParams && arg.equals("--")) {
@@ -101,9 +121,13 @@ public class Main {
                                                                + "\t--tui: force pure text mode even if swing treminal is available\n"
                                                                + "\t--gui: force swing terminal mode\n"
                                                                + "\t--noutf: force non-utf8 mode if you need it\n"
-                                                               + "\t--config DIRECTORY: force the given directory as a CONFIG_DIR\n"
+                                                               + "\t--config DIRECTORY: use the given directory as a CONFIG_DIR\n"
+                                                               + "\t--save-config DIRECTORY: save the current config to DIRECTORY (lang: only current)\n"
                                                                + "\t--server PORT: start a remoting server instead of a client\n"
                                                                + "\t--i18n DIR: generate the translation file for the given language (can be \"\") to/from the .properties given dir\n"
+                                                               + "\t--save-photo DIR FORMAT: save the contacts' photos to DIR, named after FORMAT\n"
+                                                               + "\t--load-photo DIR FORMAT: load the contacts' photos from DIR, named after FORMAT\n"
+                                                               + "\t--only-photo DIR FORMAT: load the contacts' photos from DIR, named after FORMAT, overwrite all other photos of selected contacts\n"
                                                                + "everyhing else is either a file to open or a directory to open\n"
                                                                + "(we will only open 1st level files in given directories)\n"
                                                                + "('jvcard://hostname:8888/file' links -- or without 'file' -- are also ok)\n");
@@ -124,7 +148,7 @@ public class Main {
                                }
 
                                language = args[index];
-                               transService = new Trans(language);
+                               transService = new TransBundle(language);
                                transService.setUnicode(unicode);
                        } else if (!noMoreParams && arg.equals("--config")) {
                                index++;
@@ -136,9 +160,44 @@ public class Main {
                                }
 
                                Bundles.setDirectory(args[index]);
-                               transService = new Trans(language);
+                               transService = new TransBundle(language);
                                transService.setUnicode(unicode);
+                       } else if (!noMoreParams && arg.equals("--save-config")) {
+                               index++;
+                               if (index >= args.length) {
+                                       System.err
+                                                       .println("Syntax error: no config directory given");
+                                       System.exit(ERR_SYNTAX);
+                                       return;
+                               }
+                               dir = args[index];
+
+                               if (mode != Mode.CONTACT_MANAGER) {
+                                       System.err
+                                                       .println("Syntax error: you can only use one of: \n"
+                                                                       + "--server\n"
+                                                                       + "--save-config\n"
+                                                                       + "--i18n\n"
+                                                                       + "--load-photo\n"
+                                                                       + "--save-photo\n" + "--only-photo\n");
+                                       System.exit(ERR_SYNTAX);
+                                       return;
+                               }
+                               mode = Mode.SAVE_CONFIG;
                        } else if (!noMoreParams && arg.equals("--server")) {
+                               if (mode != Mode.CONTACT_MANAGER) {
+                                       System.err
+                                                       .println("Syntax error: you can only use one of: \n"
+                                                                       + "--server\n"
+                                                                       + "--save-config\n"
+                                                                       + "--i18n\n"
+                                                                       + "--load-photo\n"
+                                                                       + "--save-photo\n" + "--only-photo\n");
+                                       System.exit(ERR_SYNTAX);
+                                       return;
+                               }
+                               mode = Mode.SERVER;
+
                                index++;
                                if (index >= args.length) {
                                        System.err.println("Syntax error: no port given");
@@ -154,6 +213,19 @@ public class Main {
                                        return;
                                }
                        } else if (!noMoreParams && arg.equals("--i18n")) {
+                               if (mode != Mode.CONTACT_MANAGER) {
+                                       System.err
+                                                       .println("Syntax error: you can only use one of: \n"
+                                                                       + "--server\n"
+                                                                       + "--save-config\n"
+                                                                       + "--i18n\n"
+                                                                       + "--load-photo\n"
+                                                                       + "--save-photo\n" + "--only-photo\n");
+                                       System.exit(ERR_SYNTAX);
+                                       return;
+                               }
+                               mode = Mode.I18N;
+
                                index++;
                                if (index >= args.length) {
                                        System.err
@@ -162,35 +234,82 @@ public class Main {
                                        return;
                                }
 
-                               i18nDir = args[index];
+                               dir = args[index];
+                       } else if (!noMoreParams
+                                       && (arg.equals("--load-photo")
+                                                       || arg.equals("--save-photo") || arg
+                                                               .equals("--only-photo"))) {
+                               if (mode != Mode.CONTACT_MANAGER) {
+                                       System.err
+                                                       .println("Syntax error: you can only use one of: \n"
+                                                                       + "--server\n"
+                                                                       + "--save-config\n"
+                                                                       + "--i18n\n"
+                                                                       + "--load-photo\n"
+                                                                       + "--save-photo\n" + "--only-photo\n");
+                                       System.exit(ERR_SYNTAX);
+                                       return;
+                               }
+
+                               if (arg.equals("--load-photo")) {
+                                       mode = Mode.LOAD_PHOTO;
+                               } else if (arg.equals("--save-photo")) {
+                                       mode = Mode.SAVE_PHOTO;
+                               } else {
+                                       mode = Mode.ONLY_PHOTO;
+                               }
+
+                               index++;
+                               if (index >= args.length) {
+                                       System.err.println("Syntax error: photo directory given");
+                                       System.exit(ERR_SYNTAX);
+                                       return;
+                               }
+
+                               dir = args[index];
+
+                               index++;
+                               if (index >= args.length) {
+                                       System.err.println("Syntax error: photo format given");
+                                       System.exit(ERR_SYNTAX);
+                                       return;
+                               }
+
+                               format = args[index];
                        } else {
                                filesTried = true;
                                files.addAll(open(arg));
                        }
                }
 
+               // Force headless mode if we run in forced-text mode
+               if (mode != Mode.CONTACT_MANAGER || (textMode != null && textMode)) {
+                       // same as -Djava.awt.headless=true
+                       System.setProperty("java.awt.headless", "true");
+               }
+
                if (unicode) {
                        utf8();
                }
 
+               // N/FN fix information:
+               readNFN();
+
                // Error management:
-               if (port != null && files.size() > 0) {
+               if (mode == Mode.SERVER && files.size() > 0) {
                        System.err
                                        .println("Invalid syntax: you cannot both use --server and provide card files");
                        System.exit(ERR_SYNTAX);
-               } else if (i18nDir != null && files.size() > 0) {
+               } else if (mode == Mode.I18N && files.size() > 0) {
                        System.err
                                        .println("Invalid syntax: you cannot both use --i18n and provide card files");
                        System.exit(ERR_SYNTAX);
-               } else if (port != null && i18nDir != null) {
-                       System.err
-                                       .println("Invalid syntax: you cannot both use --server and --i18n");
-                       System.exit(ERR_SYNTAX);
-               } else if (i18nDir != null && language == null) {
+               } else if (mode == Mode.I18N && language == null) {
                        System.err
                                        .println("Invalid syntax: you cannot use --i18n without --lang");
                        System.exit(ERR_SYNTAX);
-               } else if (port == null && i18nDir == null && files.size() == 0) {
+               } else if ((mode == Mode.CONTACT_MANAGER || mode == Mode.SAVE_PHOTO || mode == Mode.LOAD_PHOTO)
+                               && files.size() == 0) {
                        if (files.size() == 0 && !filesTried) {
                                files.addAll(open("."));
                        }
@@ -203,9 +322,31 @@ public class Main {
                }
                //
 
-               if (port != null) {
+               switch (mode) {
+               case SAVE_CONFIG: {
+                       try {
+                               if (!new File(dir).isDirectory()) {
+                                       if (!new File(dir).mkdir()) {
+                                               System.err
+                                                               .println("Cannot create configuration directory: "
+                                                                               + dir);
+                                       }
+                               }
+
+                               transService.updateFile(dir); // current lang TransBundle
+                               new TransBundle().updateFile(dir);
+                               new ColorBundle().updateFile(dir);
+                               new DisplayBundle().updateFile(dir);
+                               new RemoteBundle().updateFile(dir);
+                       } catch (IOException e) {
+                               e.printStackTrace();
+                               System.exit(ERR_INTERNAL);
+                       }
+                       break;
+               }
+               case SERVER: {
                        try {
-                               runServer(port);
+                               Optional.runServer(port);
                        } catch (Exception e) {
                                if (e instanceof IOException) {
                                        System.err
@@ -215,17 +356,89 @@ public class Main {
                                        System.exit(ERR_INTERNAL);
                                }
                        }
-               } else if (i18nDir != null) {
+                       break;
+               }
+               case I18N: {
                        try {
-                               Trans.generateTranslationFile(i18nDir, language);
+                               transService.updateFile(dir);
                        } catch (IOException e) {
                                System.err
                                                .println("I/O Exception: Cannot create/update a language in directory: "
-                                                               + i18nDir);
+                                                               + dir);
+                               e.printStackTrace();
                        }
-               } else {
+                       break;
+               }
+               case ONLY_PHOTO:
+               case LOAD_PHOTO: {
+                       for (String file : files) {
+                               try {
+                                       Card card = getCard(file, null).getCard();
+                                       for (Contact contact : card) {
+                                               String filename = contact.toString(format, "");
+                                               File f = new File(dir, filename);
+
+                                               if (f.exists()) {
+                                                       try {
+                                                               String b64 = StringUtils.fromImage(ImageIO
+                                                                               .read(f));
+
+                                                               if (mode == Mode.ONLY_PHOTO) {
+                                                                       for (Data photo = contact
+                                                                                       .getPreferredData("PHOTO"); photo != null; photo = contact
+                                                                                       .getPreferredData("PHOTO")) {
+                                                                               photo.delete();
+                                                                       }
+                                                               }
+
+                                                               List<TypeInfo> types = new LinkedList<TypeInfo>();
+                                                               types.add(new TypeInfo("ENCODING", "b"));
+                                                               types.add(new TypeInfo("TYPE", "png"));
+                                                               Data photo = new Data(types, "PHOTO", b64, null);
+                                                               contact.add(photo);
+                                                       } catch (IOException e) {
+                                                               System.err.println("Cannot read photo: "
+                                                                               + filename);
+                                                       }
+                                               }
+                                       }
+                                       card.save();
+                               } catch (IOException e) {
+                                       System.err.println("Card cannot be opened: " + file);
+                               }
+                       }
+                       break;
+               }
+               case SAVE_PHOTO: {
+                       for (String file : files) {
+                               try {
+                                       Card card = getCard(file, null).getCard();
+                                       for (Contact contact : card) {
+                                               Data photo = contact.getPreferredData("PHOTO");
+                                               if (photo != null) {
+                                                       String filename = contact.toString(format, "");
+                                                       File f = new File(dir, filename + ".png");
+                                                       try {
+                                                               ImageIO.write(
+                                                                               StringUtils.toImage(photo.getValue()),
+                                                                               "png", f);
+                                                       } catch (IOException e) {
+                                                               System.err
+                                                                               .println("Cannot save photo of contact: "
+                                                                                               + contact
+                                                                                                               .getPreferredDataValue("FN"));
+                                                       }
+                                               }
+                                       }
+                               } catch (IOException e) {
+                                       System.err.println("Card cannot be opened: " + file);
+                               }
+                       }
+                       break;
+               }
+               case CONTACT_MANAGER: {
                        try {
-                               startTui(textMode, files);
+                               Optional.startTui(textMode, files);
                        } catch (Exception e) {
                                if (e instanceof IOException) {
                                        System.err
@@ -235,23 +448,34 @@ public class Main {
                                        System.exit(ERR_INTERNAL);
                                }
                        }
+                       break;
+               }
                }
        }
 
        /**
         * Return the {@link Card} corresponding to the given resource name -- a
-        * file or a remote jvcard URL
+        * file or a remote jvcard URL.
+        * 
+        * <p>
+        * Will also fix the FN if required (see display.properties).
+        * </p>
         * 
         * @param input
         *            a filename or a remote jvcard url with named resource (e.g.:
         *            <tt>jvcard://localhost:4444/coworkers.vcf</tt>)
+        * @param callback
+        *            the {@link MergeCallback} to call in case of conflict, or NULL
+        *            to disallow conflict management (the {@link Card} will not be
+        *            allowed to synchronise in case of conflicts)
         * 
         * @return the {@link Card}
         * 
         * @throws IOException
         *             in case of IO error or remoting not available
         */
-       static public Card getCard(String input) throws IOException {
+       static public CardResult getCard(String input, MergeCallback callback)
+                       throws IOException {
                boolean remote = false;
                Format format = Format.Abook;
                String ext = input;
@@ -267,12 +491,13 @@ public class Main {
                        remote = true;
                }
 
-               Card card = null;
+               CardResult card = null;
                try {
                        if (remote) {
-                               card = syncCard(input);
+                               card = Optional.syncCard(input, callback);
                        } else {
-                               card = new Card(new File(input), format);
+                               card = new CardResult(new Card(new File(input), format), false,
+                                               false, false);
                        }
                } catch (IOException ioe) {
                        throw ioe;
@@ -280,120 +505,21 @@ public class Main {
                        throw new IOException("Remoting support not available", e);
                }
 
-               return card;
-       }
-
-       /**
-        * Create a new jVCard server on the given port, then run it.
-        * 
-        * @param port
-        *            the port to run on
-        *
-        * @throws SecurityException
-        *             in case of internal error
-        * @throws NoSuchMethodException
-        *             in case of internal error
-        * @throws ClassNotFoundException
-        *             in case of internal error
-        * @throws IllegalAccessException
-        *             in case of internal error
-        * @throws InstantiationException
-        *             in case of internal error
-        * @throws InvocationTargetException
-        *             in case of internal error
-        * @throws IllegalArgumentException
-        *             in case of internal error
-        * @throws IOException
-        *             in case of IO error
-        */
-       @SuppressWarnings("unchecked")
-       static private void runServer(int port) throws NoSuchMethodException,
-                       SecurityException, ClassNotFoundException, InstantiationException,
-                       IllegalAccessException, IllegalArgumentException,
-                       InvocationTargetException {
-               @SuppressWarnings("rawtypes")
-               Class serverClass = Class.forName("be.nikiroo.jvcard.remote.Server");
-               Method run = serverClass.getDeclaredMethod("run", new Class[] {});
-               run.invoke(serverClass.getConstructor(int.class).newInstance(port));
-       }
-
-       /**
-        * Start the TUI program.
-        * 
-        * @param textMode
-        *            TRUE to force text mode, FALSE to force the Swing terminal
-        *            emulator, null to automatically determine the best choice
-        * @param files
-        *            the files to show at startup
-        * 
-        * @throws SecurityException
-        *             in case of internal error
-        * @throws NoSuchMethodException
-        *             in case of internal error
-        * @throws ClassNotFoundException
-        *             in case of internal error
-        * @throws IllegalAccessException
-        *             in case of internal error
-        * @throws InstantiationException
-        *             in case of internal error
-        * @throws InvocationTargetException
-        *             in case of internal error
-        * @throws IllegalArgumentException
-        *             in case of internal error
-        * @throws IOException
-        *             in case of IO error
-        */
-       @SuppressWarnings("unchecked")
-       static private void startTui(Boolean textMode, List<String> files)
-                       throws NoSuchMethodException, SecurityException,
-                       ClassNotFoundException, InstantiationException,
-                       IllegalAccessException, IllegalArgumentException,
-                       InvocationTargetException {
-               @SuppressWarnings("rawtypes")
-               Class launcherClass = Class
-                               .forName("be.nikiroo.jvcard.tui.TuiLauncher");
-               Method start = launcherClass.getDeclaredMethod("start", new Class[] {
-                               Boolean.class, List.class });
-               start.invoke(launcherClass.newInstance(), textMode, files);
-       }
-
-       /**
-        * Return the {@link Card} corresponding to the given URL, synchronised if
-        * necessary.
-        * 
-        * @param input
-        *            the jvcard:// with resource name URL (e.g.:
-        *            <tt>jvcard://localhost:4444/coworkers</tt>)
-        * 
-        * @throws SecurityException
-        *             in case of internal error
-        * @throws NoSuchMethodException
-        *             in case of internal error
-        * @throws ClassNotFoundException
-        *             in case of internal error
-        * @throws IllegalAccessException
-        *             in case of internal error
-        * @throws InstantiationException
-        *             in case of internal error
-        * @throws InvocationTargetException
-        *             in case of internal error
-        * @throws IllegalArgumentException
-        *             in case of internal error
-        * @throws IOException
-        *             in case of IO error
-        */
-       @SuppressWarnings("unchecked")
-       static private Card syncCard(String input) throws ClassNotFoundException,
-                       NoSuchMethodException, SecurityException, InstantiationException,
-                       IllegalAccessException, IllegalArgumentException,
-                       InvocationTargetException, IOException {
-               @SuppressWarnings("rawtypes")
-               Class syncClass = Class.forName("be.nikiroo.jvcard.remote.Sync");
-               Method sync = syncClass.getDeclaredMethod("sync",
-                               new Class[] { boolean.class });
-
-               Object o = syncClass.getConstructor(String.class).newInstance(input);
-               Card card = (Card) sync.invoke(o, false);
+               // Fix the FN value
+               if (defaultFn != null) {
+                       try {
+                               for (Contact contact : card.getCard()) {
+                                       Data name = contact.getPreferredData("FN");
+                                       if (name == null || name.getValue().length() == 0
+                                                       || forceComputedFn) {
+                                               name.setValue(contact.toString(defaultFn, ""));
+                                       }
+                               }
+                       } catch (Exception e) {
+                               // sync failed -> getCard() throws.
+                               // do not update.
+                       }
+               }
 
                return card;
        }
@@ -485,4 +611,17 @@ public class Main {
                } catch (IllegalAccessException e) {
                }
        }
+
+       /**
+        * Read display.properties to know if we should fix the FN field when empty,
+        * or always, or never.
+        */
+       static private void readNFN() {
+               DisplayBundle map = new DisplayBundle();
+
+               defaultFn = map.getString(DisplayOption.CONTACT_DETAILS_DEFAULT_FN);
+
+               forceComputedFn = map.getBoolean(
+                               DisplayOption.CONTACT_DETAILS_SHOW_COMPUTED_FN, false);
+       }
 }