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 = &hellip;
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    }