001 // Copyright 2008 Waterken Inc. under the terms of the MIT X license 002 // found at http://www.opensource.org/licenses/mit-license.html 003 package org.waterken.syntax.config; 004 005 import java.io.File; 006 import java.io.OutputStream; 007 import java.lang.reflect.Type; 008 009 import org.joe_e.array.ByteArray; 010 import org.joe_e.array.ConstArray; 011 import org.joe_e.array.PowerlessArray; 012 import org.joe_e.file.Filesystem; 013 import org.waterken.syntax.Exporter; 014 import org.waterken.syntax.Importer; 015 import org.waterken.syntax.Syntax; 016 import org.waterken.syntax.json.JSONDeserializer; 017 import org.waterken.syntax.json.JSONSerializer; 018 019 /** 020 * A folder of serialized configuration settings. 021 * <p> 022 * This class provides convenient access to a folder of JSON files; each of 023 * which represents a particular configuration setting. The class provides 024 * methods for {@linkplain #init initializing} and {@linkplain #read reading} 025 * these settings. 026 * </p> 027 * <p> 028 * For example, consider a folder with contents: 029 * </p> 030 * <pre> 031 * config/ 032 * - username.json 033 * { "=" : "tyler.close" } 034 * - port.json 035 * { "=" : 8088 } 036 * - home.json 037 * { 038 * "class" : [ "org.example.hypertext.Anchor" ], 039 * "icon" : "home.png", 040 * "href" : "http://waterken.sourceforge.net/", 041 * "tooltip" : "Home page" 042 * } 043 * </pre> 044 * <p> 045 * These settings can be read with code: 046 * </p> 047 * <pre> 048 * final Config config = new Config(new File("config/"), 049 * getClass().getClassLoader()); 050 * final String username = config.read("username"); 051 * final int port = config.read("port"); 052 * final Anchor home = config.read("home"); 053 * </pre> 054 */ 055 public final class 056 Config { 057 058 /** 059 * JSON syntax 060 */ 061 static private final Syntax json = 062 new Syntax(".json", new JSONSerializer(), new JSONDeserializer()); 063 064 /** 065 * each known syntax 066 */ 067 static private final PowerlessArray<Syntax> known = PowerlessArray.array( 068 json 069 ); 070 071 private final File root; 072 protected final ClassLoader code; 073 protected final String namespace; 074 protected final Importer connect; 075 protected final File home; 076 protected final PowerlessArray<Syntax> supported; 077 private final Syntax output; 078 079 protected PowerlessArray<String> cacheKeys; 080 protected ConstArray<Object> cacheValues; 081 082 /** 083 * Constructs an instance. 084 * @param root root folder for configuration files 085 * @param code class loader for serialized objects 086 * @param namespace global identifier for stored object namespace 087 * @param connect remote reference importer, may be <code>null</code> 088 * @param home root folder for a path beginning with <code>~</code> 089 * @param supported each supported {@linkplain #read input} syntax 090 * @param output {@linkplain #init output} syntax 091 */ 092 public 093 Config(final File root, final ClassLoader code, final String namespace, 094 final Importer connect, final File home, 095 final PowerlessArray<Syntax> supported, final Syntax output) { 096 this.root = root; 097 this.code = code; 098 this.namespace = namespace; 099 this.connect = connect; 100 this.home = home; 101 this.supported = supported; 102 this.output = output; 103 104 cacheKeys = PowerlessArray.array(); 105 cacheValues = ConstArray.array(); 106 } 107 108 /** 109 * Constructs an instance. 110 * @param root root folder for configuration files 111 * @param code class loader for serialized objects 112 * @param namespace global identifier for stored object namespace 113 * @param connect remote reference importer, may be <code>null</code> 114 * @param home root folder for a path beginning with <code>~</code> 115 */ 116 public 117 Config(final File root, final ClassLoader code, final String namespace, 118 final Importer connect, final File home) { 119 this(root, code, namespace, connect, home, known, json); 120 } 121 122 /** 123 * Constructs an instance. 124 * @param root root folder for configuration files 125 * @param code class loader for serialized objects 126 */ 127 public 128 Config(final File root, final ClassLoader code) { 129 this(root, code, "file:///", null, null); 130 } 131 132 /** 133 * Reads a configuration setting. 134 * @param <T> expected value type 135 * @param name setting name 136 * @return <code>{@link #read(String, Type) read}(name, Object.class)</code> 137 * @throws Exception any problem connecting to the identified reference 138 */ 139 public @SuppressWarnings("unchecked") <T> T 140 read(final String name) throws Exception { 141 final Object r = read(name, Object.class); 142 return (T)r; 143 } 144 145 /** 146 * Reads a configuration setting. 147 * <p> 148 * Any <code>name</code> argument containing a period is assumed to refer 149 * to a configuration file, rather than the serialized object. For example: 150 * </p> 151 * <pre> 152 * final Config config = … 153 * final String username = config.read("username"); 154 * final File usernameFile = config.read("username.json"); 155 * </pre> 156 * <p> 157 * The configuration folder itself can be accessed using the code: 158 * </p> 159 * <pre> 160 * final File root = config.read(""); 161 * </pre> 162 * @param <T> expected value type 163 * @param name setting name 164 * @param type expected value type, used as a hint to help deserialization 165 * @return setting value, or <code>null</code> if not set 166 * @throws Exception any problem connecting to the identified reference 167 */ 168 public @SuppressWarnings("unchecked") <T> T 169 read(final String name, final Type type) throws Exception { 170 return (T)sub(root, namespace).apply(name, namespace, type); 171 } 172 173 protected Importer 174 sub(final File top, final String topspace) { return new Importer() { 175 public Object 176 apply(final String href, final String base, 177 final Type... type) throws Exception { 178 if (!topspace.equals(base) || -1 != href.indexOf(':')) { 179 return connect.apply(href, base, type); 180 } 181 182 File folder; // folder containing file 183 String subspace; // namespace for containing folder 184 String name = href; // simple name 185 if (name.startsWith("~/")) { 186 folder = Config.this.home; 187 subspace = Config.this.namespace + "~/"; 188 name = name.substring("~/".length()); 189 } else { 190 folder = top; 191 subspace = topspace; 192 } 193 194 // check the cache 195 final String key = subspace + name; 196 for (int i = cacheKeys.length(); 0 != i--;) { 197 if (cacheKeys.get(i).equals(key)) { 198 return cacheValues.get(i); 199 } 200 } 201 202 // descend to the named file 203 while (true) { 204 final int i = name.indexOf('/'); 205 if (-1 == i) { break; } 206 folder = Filesystem.file(folder, name.substring(0, i)); 207 subspace += name.substring(0, i + 1); 208 name = name.substring(i + 1); 209 } 210 if ("".equals(name)) { return folder; } 211 if (-1 != name.indexOf('.')) {return Filesystem.file(folder, name);} 212 213 // deserialize the named object 214 Object r = null; 215 for (final Syntax syntax : supported) { 216 final String filename = name + syntax.ext; 217 final File file = Filesystem.file(folder, filename); 218 if (!file.isFile()) { continue; } 219 r = syntax.deserializer.deserialize(Filesystem.read(file), 220 sub(folder, subspace), subspace, code, 221 0 == type.length ? Object.class : type[0]); 222 break; 223 } 224 cacheKeys = cacheKeys.with(key); 225 cacheValues = cacheValues.with(r); 226 return r; 227 } 228 }; } 229 230 /** 231 * Initializes a configuration setting. 232 * @param name setting name 233 * @param value setting value 234 * @param export remote reference exporter, may be <code>null</code> 235 * @throws Exception any problem persisting the <code>value</code> 236 */ 237 public void 238 init(final String name, final Object value, 239 final Exporter export) throws Exception { 240 final ByteArray content = 241 output.serializer.serialize(export, Object.class, value); 242 final OutputStream out = 243 Filesystem.writeNew(Filesystem.file(root, name + output.ext)); 244 out.write(content.toByteArray()); 245 out.flush(); 246 out.close(); 247 } 248 249 /** 250 * Creates a temporary override of a configuration setting. 251 * @param name setting name 252 * @param value transient setting value 253 */ 254 public void 255 override(final String name, final Object value) { 256 Filesystem.file(root, name); 257 cacheKeys = cacheKeys.with(namespace + name); 258 cacheValues = cacheValues.with(value); 259 } 260 }