/* * $Id$ * * Copyright 2007 Glencoe Software, Inc. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashSet; import java.util.Properties; import java.util.Set; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; /** * Command-line (and static) utility for working with the {@link #ROOT * omero.prefs} {@link Preferences} node in order to store Java properties-file * like values on a user basis. This simplifies configuration and permits * quicker re-installs, and less wrangling with configuration files. * * A single string value is stored as {@link #DEFAULT} (which by default is the * value {@link #DEFAULT}, and points to the name of some node under * "omero.prefs". This value can be overridden by the "OMERO_CONFIG" environment * variable. Almost all commands work on this default node, referred to here as * a "profile". * * @author Josh Moore, josh at glencoesoftware.com * @since 3.0-Beta3 * TODO USE JSCH FOR PUBKEY ENCRYPTION * TODO backing store sync * TODO test nondispatchable */ public class prefs { /** * Storing the standard in as a {@link Properties} instance as early as * possible to prevent lost data. */ public final static Properties STDIN; static { Properties p = new Properties(); try { if (System.in.available() > 0) { p.load(System.in); } } catch (Exception e) { // ignore } STDIN = p; } /** * Activated by setting DBEUG=true in the environment. Various information * is printed to {@link System#err}. */ public final static boolean DEBUG = Boolean.valueOf(System.getenv("DEBUG")) || "1".equals(System.getenv("DEBUG")); static { if (DEBUG) { printErr(args("STANDARD IN:")); try { STDIN.store(System.err, null); } catch (Exception e) { // ignore } } } /** * "omero.prefs", the value of the root {@link Preferences} node used for * all configuration work. */ public final static String ROOT = "/omero/prefs"; /** * Key (and default value) of the property under {@link #ROOT} which defines * which "profile" (subnode} is in effect. */ public final static String DEFAULT = "default"; /** * Environment variable which can be set to override the current, active * profile. */ public final static String ENV = "OMERO_CONFIG"; /** * Static exception created at initialization to prevent our needing to * subclass. For internal use only. */ private final static RuntimeException USAGE = new RuntimeException("usage"); /** * Static exception created at initialization to prevent our needing to * subclass. For internal use only. */ private final static RuntimeException CONFLICT = new RuntimeException( "conflict"); /** * Cache the root node at start up */ private static Preferences prefs = Preferences.userRoot().node(ROOT); /** * Entry point to the prefs command line too. Uses the * {@link #dispatch(String[])} method to invoke a public static method which * takes a {@link #pop(String[]) popped} String-argument array. * * @param args * Not null. Can be empty. */ public static void main(String[] args) { try { if (DEBUG) { printErr(args("Debugging profile " + def(args())[0])); } exit(print(dispatch(notNull(args)))); } catch (Throwable e) { if (DEBUG) { e.printStackTrace(); } if (e == USAGE) { exit(printErr(usage(args))); } else if (e == CONFLICT) { exit(printErr(args("Conflict found in properties. Use drop or load_nowarn"))); } else if (e instanceof BackingStoreException) { exit(printErr(args("Error accessing preferences:", e .getMessage()))); } else if (e instanceof IOException) { exit(printErr(args("IO Error:", e.getMessage()))); } else { exit(printErr(args("Unknown error:", e.getMessage()))); } } } /** * Returns a usage string array. * * @param args * Ignored. * @return Never null. */ public static String[] usage(String[] args) { return new String[] { " ", " usage: prefs COMMAND [ARGS] ", " ", " all : list all profiles under omero.prefs", " def [NEWDEFAULT] : list (or set) current default profile", " drop : deletes current profile", " get [KEY [KEY [...]]] : get keys from the current profile. All by default", " export [FILE ] : export to a file or standard out", " keys : list all keys for the current profile", " load [FILE...] : read into current profile from a file or standard in (error on conflict)", " load_nowarn [FILE...] : read current profile from a file or standard in", " set KEY VALUE : set value on current profile", " sys COMMANDS : applies commands as above to system preferences", " ", "Note: profiles are created on demand. Later properties override earlier ones." }; } /** * Returns a help string array. Currently calls {@link #usage(String[])} but * may eventually return a more man page-like statement. * * @param args * Ignored. * @return Never null. */ public static String[] help(String[] args) { return usage(args); // Currently just an alias } // ~ For Main Only (and testing) // ========================================================================= /** * Prints the arg array and returns an empty array (for exit purposes) */ public static String[] print(String[] args) { for (String string : args) { if (string != null) { System.out.println(string); } } return new String[] {}; } /** * Prints the arg array and returns the input array (for exit purposes) */ public static String[] printErr(String[] args) { for (String string : args) { if (string != null) { System.err.println(string); } } return args; } /** * Delegates to {@link #printErr(String[])} iff {@link #DEBUG} is true. */ public static String[] printDebug(String[] args) { if (DEBUG) { printErr(args); } return args; } /** * Uses the length of the argument array as the exit code. */ public static String[] exit(String[] args) { try { Thread.sleep(1L); System.out.flush(); Thread.sleep(1L); } catch (InterruptedException e) { e.printStackTrace(); } System.exit(args.length); return null; } // ~ General Purpose // ========================================================================= /** * Uses the first string in the argument array to reflectively invoke * another method with the same signature on {@link prefs}. If the array is * null, empty, or begins with an non-extant method, {@link #USAGE} will be * thrown. Otherwise, invokes the named method, returning its return value * or returning the cause of any {@link InvocationTargetException}. */ public static String[] dispatch(String[] args) throws Throwable { if (args == null || args.length == 0) { USAGE.fillInStackTrace(); throw USAGE; } String method = args[0]; args = pop(args); Method m; try { m = prefs.class.getMethod(method, String[].class); } catch (Exception e) { USAGE.fillInStackTrace(); throw USAGE; } try { return (String[]) m.invoke(null, (Object) args); } catch (InvocationTargetException ite) { throw ite.getCause(); } } /** * Replaces the user {@link Preferences} instance with the system * {@link Preferences} and continues dispatching. */ public static String[] sys(String[] args) throws Throwable { prefs = Preferences.systemRoot().node(ROOT); return dispatch(args); } /** * Calls {@link Preferences#childrenNames()} and returns the array. * * @param args * Ignored. */ public static String[] all(String[] args) throws BackingStoreException { return prefs.childrenNames(); } /** * Returns the current default if no argument is given, or sets the default * to the given string (or "" if null) otherwise returing "Default set to: * ... ". * * @param args * String array of length 0 or 1. Otherwise {@link #USAGE} is * thrown. */ public static String[] def(String[] args) { args = notNull(args); if (args.length == 0) { String OMERO = System.getenv(ENV); if (OMERO == null) { OMERO = prefs.get(DEFAULT, null); if (OMERO == null) { prefs.put(DEFAULT, DEFAULT); OMERO = DEFAULT; } } return args(OMERO); } else if (args.length == 1) { prefs.put(DEFAULT, args[0] == null ? "" : args[0]); return new String[] { "Default set to: " + def(args())[0] }; } else { USAGE.fillInStackTrace(); throw USAGE; } } /** * Drops the entire profile ({@link Preferences subnode}) via * {@link Preferences#removeNode()}. * * @param args * Ignored. */ public static String[] drop(String[] args) throws BackingStoreException { _node().removeNode(); return args(); } /** * Returns {@link Preferences#keys()} * * @param args * Ignored. */ public static String[] keys(String[] args) throws BackingStoreException { return _node().keys(); } /** * Returns either the given key=value pairs (or all if no argument is * given). When a single key is specified the return format is simply * "value". */ public static String[] get(String[] args) throws BackingStoreException { args = notNull(args); String[] keys = _node().keys(); if (args.length == 0) { if (keys.length == 0) { return args; } else if (keys.length == 1) { return get(args(keys[0], "UNKNOWNKEYJUSTTOFORCELENGTH2")); } else { return get(keys); } } else if (args.length == 1) { String key = args[0] == null ? "" : args[0]; String value = _node().get(key, ""); return args(value); } Set<String> availableKeys = new HashSet<String>(Arrays.asList(_node() .keys())); Set<String> askedKeys = new HashSet<String>(Arrays.asList(args)); availableKeys.retainAll(askedKeys); String[] rv = new String[availableKeys.size()]; for (int i = 0; i < rv.length;) { String key = args[i] == null ? "" : args[i]; if (availableKeys.contains(key)) { String value = _node().get(key, ""); rv[i] = key + "=" + value; i++; } } Arrays.sort(rv); return rv; } /** * Takes an array of length 2, using args[0] as the key and args[1] as the * value to be set. For more advanced usage, see {@link #load(String[])}. * * @param args * @return a * @throws BackingStoreException */ public static String[] set(String[] args) throws BackingStoreException { if (args == null || args.length < 1) { USAGE.fillInStackTrace(); throw USAGE; } String key = args[0] == null ? "" : args[0]; String val = args.length == 1 ? null : args[1]; if (val == null) { _node().remove(key); } else { _node().put(key, val); } return args(); } /** * Exports all properties in the current profile to {@link System#out}, or * if a single argument is given, that is take to be the name of a target * ouput file. Export will fail if the file already exists. This method may * be removed in favor of using piping with {@link #get(String[])}. * * Properties are in standard Java {@link Properties} format. */ public static String[] export(String[] args) throws BackingStoreException, IOException { if (args.length == 0) { _export(System.out); return args(); } else if (args.length == 1) { File f = new File(args[0]); if (f.exists()) { throw new IOException("File " + f.getAbsolutePath() + " exists!"); } BufferedOutputStream bos = null; try { bos = new BufferedOutputStream(new FileOutputStream(args[0])); _export(bos); } finally { if (bos != null) { try { bos.close(); } catch (IOException ioe) { // must ignore } } } return args(); } else { USAGE.fillInStackTrace(); throw USAGE; } } /** * Loads a profile from {@link Properties} files, or properly formatted * {@link System#in} input if no files are given. If a key to be loaded * already exists in the configuration, {@link #CONFLICT} will be thrown. * Use {@link #load_nowarn(String[])} instead, or {@link #drop(String[])} * the profile before loading. */ public static String[] load(String[] args) throws IOException { args = notNull(args); Properties p; if (args.length == 0) { p = STDIN; } else { p = _merge(args); } Preferences node = _node(); for (Object obj : p.keySet()) { String key = obj.toString(); String currentValue = node.get(key, ""); if (currentValue != null && currentValue.length() > 0) { printErr(args(key + " already present!")); throw CONFLICT; } } _load(p); return args(); } /** * Performs the same actions as {@link #load(String[])} but does not throw * {@link #CONFLICT} if a key already exists. */ public static String[] load_nowarn(String[] args) throws IOException { args = notNull(args); Properties p; if (args.length == 0) { p = STDIN; } else { p = _merge(args); } _load(p); return args(); } // ~ Utilities // ========================================================================= /** * Converts varargs to a String-array. */ public static String[] args(String... args) { return args; } /** * Joins all the String arguments given into a single String (joined with " * "), and returns that String as the first element of a new array. */ public static String[] join(String... args) { StringBuilder sb = new StringBuilder(); for (String string : args) { sb.append(string); sb.append(" "); } return new String[] { sb.toString() }; } /** * Returns an empty array if the argument is null. */ public static String[] notNull(String[] args) { if (args == null) { return new String[] {}; } return args; } /** * Creates a new subarray from the argument, effectively popping off the * first element. */ public static String[] pop(String[] args) { int sz = args.length - 1; if (sz < 0) { return args; } String[] newArgs = new String[sz]; System.arraycopy(args, 1, newArgs, 0, sz); return newArgs; } // ~ Testing // ========================================================================= /** * Tests for success by dispatching to the first argument and expecting no * exception. */ private static String[] _ok(String... args) throws Throwable { testcount++; String test = join(args)[0]; String[] rv = args(); try { rv = dispatch(args); } catch (Exception e) { printErr(join("fail...", test)); failures++; if (DEBUG) { e.printStackTrace(); } } printErr(join("ok...", test)); printDebug(rv); return rv; } /** * Tests for failure by dispatching to the first argument, and expecting an * exception. */ private static String[] _fail(String... args) throws Throwable { testcount++; String test = join(args)[0]; String[] rv = args(); try { rv = dispatch(args); printErr(join("fail...", test, "No exception thrown.")); failures++; } catch (Exception e) { printErr(join("ok...", test)); } printDebug(rv); return rv; } private static boolean testing = false; private static int testcount = 0; private static int failures = 0; /** * Simple test framework, callable from the command line via "java prefs * test" */ public static String[] test(String[] args) throws Throwable { if (testing) { throw new RuntimeException("testing already running"); } testing = true; failures = 0; String origconf = def(args())[0]; String testconf = "testconf"; def(args(testconf)); try { // Can't test main since it exits. // Some stuff you probably shouldn't do _fail("test"); // Prevent infinite recursion. _fail("_node"); _fail("NOMETHOD"); _fail("_ok", "printDebug"); // Can't find since private. _fail("_fail", "NOMETHOD"); // Ditto. // Printing _ok("print", "stuff"); _ok("printErr", "stuff"); _ok("printDebug", "stuff"); _ok("print", null); _ok("printErr", null); _ok("printDebug", null); _ok("print"); _ok("printErr"); _ok("printDebug"); // These throw usage _fail(); _fail("dispatch"); // No infinite recursion! (Stop via pop) // Basic utilities _ok("join", "a", "b", "c"); _ok("join", "a"); _ok("join"); _ok("join", null); _ok("pop", "a", "b", "c"); _ok("pop", "a"); _ok("pop"); _ok("pop", null); _ok("notNull", "a", "b", "c"); _ok("notNull", "a"); _ok("notNull"); _ok("notNull", null); _ok("args", "a", "b", "c"); _ok("args", "a"); _ok("args"); _ok("args", null); // User help _ok("usage"); _ok("usage", "stuff"); _ok("usage", null); _ok("help"); _ok("help", "stuff"); _ok("help", null); // User commands // ////////////// _ok("all"); _ok("keys"); _ok("keys", null); _ok("keys", "stuff"); // Def _ok("def", "new_test_configuration"); _ok("def", testconf); _ok("def", null); _fail("def", "too", "many", "args"); // Set _fail("set"); _fail("set", null); _ok("set", "SHOULDBEFOO", "FOO"); // Get _ok("get"); _ok("get", null); _ok("get", "NONEXTANTVALUE"); _ok("get", "SHOULDBEFOO"); // Drop _ok("def", "droptest"); _ok("set", "a", "b"); _ok("drop"); _ok("def", testconf); print(args("============================================")); print(args(failures + " from " + testcount + " failed.")); } finally { def(args(origconf)); } return args(); } // ~ Nondispatchable (signature differs) // ========================================================================= /** * Returns the {@link Preferences node} (or profile) designated by the * current {@link #def(String[]) default}. */ private static Preferences _node() { Preferences p = prefs.node(def(args())[0]); return p; } /** * Prints all properties as returned by {@link #get(String[])} to the * {@link OutputStream}. */ private static void _export(OutputStream os) throws BackingStoreException, IOException { String[] values = get(args()); OutputStreamWriter osw = new OutputStreamWriter(os); for (String string : values) { osw.write(string); osw.write("\n"); } osw.flush(); } /** * Loads the {@link Properties} instance into the {@link #_node() current * profile}. */ private static void _load(Properties properties) { Preferences p = _node(); for (Object obj : properties.keySet()) { Object value = properties.get(obj); if (value == null) { value = ""; } p.put(obj.toString(), value.toString()); } } /** * {@link Properties#load(java.io.InputStream) Loads} any number of files * into a new {@link Properties} instance. Later values overwrite earlier * oens. */ private static Properties _merge(String... args) throws IOException { Properties p = new Properties(); for (String string : args) { File f = new File(string); FileInputStream fis = new FileInputStream(f); try { p.load(fis); } finally { fis.close(); } } return p; } }