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 }