Source code: com/lutris/util/ConfigFile.java
1 /*
2 * Enhydra Java Application Server Project
3 *
4 * The contents of this file are subject to the Enhydra Public License
5 * Version 1.1 (the "License"); you may not use this file except in
6 * compliance with the License. You may obtain a copy of the License on
7 * the Enhydra web site ( http://www.enhydra.org/ ).
8 *
9 * Software distributed under the License is distributed on an "AS IS"
10 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
11 * the License for the specific terms governing rights and limitations
12 * under the License.
13 *
14 * The Initial Developer of the Enhydra Application Server is Lutris
15 * Technologies, Inc. The Enhydra Application Server and portions created
16 * by Lutris Technologies, Inc. are Copyright Lutris Technologies, Inc.
17 * All Rights Reserved.
18 *
19 * Contributor(s):
20 *
21 * $Id: ConfigFile.java,v 1.21.2.1.2.1 2000/10/19 17:58:53 jasona Exp $
22 */
23
24 package com.lutris.util;
25
26 import java.io.*;
27 import java.util.*;
28
29 /**
30 * ConfigFile is used to manipulate Multiserver configuration
31 * (.conf) files.
32 *
33 * Presents all configuration elements in the form of a keyed
34 * table. Configuration elements are grouped by string "keys" according
35 * to the function they configure. The syntax is described more formally
36 * below.
37 * <PRE>
38 * stream ::= entry | stream entry
39 * entry ::= key "=" value_list | comment | blank
40 * value_list ::= value | value_list "," value
41 * value ::= fragment | value "+" fragment
42 * fragment ::= key | quoted_string
43 * quoted_string ::= (C/C++ style quoted string)
44 * key ::= (A string matching [A-Za-z_\./][A-Za-z0-9_-\./]*)
45 * comment ::= "#" (any text up to a newline)
46 * blank ::= (A line containing only white space)
47 * </PRE>
48 * In addition to the above syntax, some additional semantic rules apply.
49 * The operator "+" concatenates the fragment immediately to the left
50 * and to the right of it. Thus <code>ab + cd</code> results in
51 * <code>abcd</code>. The operator "," terminates one element and begins
52 * the next element. Thus, the line <code>val = hello, world</code>
53 * creates a configuration entry with the key "val" and the two elements
54 * "hello", and "world". If the characters "+", ",", or "\" occur at
55 * the end of a line, then the entry is continued on the next line.
56 * Trailing and leading whitespaces are ignored, and multiple whitespace
57 * characters are converted by default into a single space. The "+"
58 * operator leaves no whitespace between operands.
59 * Finally, within quoted strings, C-style backslash escapes are
60 * recognized. These include "\n" for newline, "\t" for tab, etc.
61 * An example configuration input file is given below.
62 * <PRE>
63 * #==============================================================
64 * # Sample.config
65 * #==============================================================
66 * LDAP_SERVERS[] = "server1.ldap.gov:338",
67 * "server2.ldap.gov:1000"
68 * USER_TIMEOUT = 3600 # seconds. Comments can follow on same line.
69 * STRANGE_ENTRY = "This is a long long long long long " +
70 * "long long line. Notice how the \"+\" " +
71 * "operator is used to break this line up " +
72 * "into more than one line in the config file." ,
73 * "And this is the second element."
74 * # etc.
75 * </PRE>
76 *
77 * @see Config
78 * @author Shawn McMurdo
79 * @version $Revision: 1.21.2.1.2.1 $
80 */
81 public class ConfigFile {
82 private Config config;
83 private Vector order;
84 private Hashtable comments;
85
86 /** The associated config file (if any) */
87 private File file = null;
88
89 /**
90 * The key under which the trailing comment is stored.
91 */
92 public static final String TRAILING_COMMENT = "ConfigFileTrailingComment";
93
94 /**
95 * Default constructor for an empty config file.
96 */
97 public ConfigFile() {
98 config = new Config();
99 order = new Vector();
100 comments = new Hashtable();
101 }
102
103 /**
104 * Constructor from an InputStream.
105 *
106 * @param inputStream
107 * The input stream from which to parse the config file.
108 */
109 public ConfigFile(InputStream inputStream) throws ConfigException {
110 config = new Config();
111 order = new Vector();
112 comments = new Hashtable();
113 ConfigParser parser = new ConfigParser(inputStream);
114 try {
115 parser.process(this);
116 } catch (ParseException e) {
117 throw new ConfigException(ConfigException.SYNTAX, e.getMessage());
118 }
119 }
120
121 /**
122 * Constructor from a File. Allows to later write back the
123 * configuration to the same file.
124 *
125 * @param configFile
126 * The name of a local file to parse.
127 */
128 public ConfigFile(File configFile) throws ConfigException, IOException {
129 this(new FileInputStream(configFile));
130 this.file = configFile;
131 config.setConfigFile(this);
132 }
133
134 /**
135 * Constructor from a KeywordValueTable.
136 *
137 * @param kvt
138 * A KeywordValueTable from which to populate the config file.
139 */
140 public ConfigFile(KeywordValueTable kvt) throws ConfigException {
141 config = new Config(kvt);
142 order = new Vector();
143 comments = new Hashtable();
144 }
145
146 /**
147 * Returns the Config object representing the config data in the file.
148 *
149 * @return The Config object for this ConfigFile.
150 */
151 public Config getConfig() {
152 return config;
153 }
154
155 /**
156 * Returns the comment associated with a given key, or <code>null</code>
157 * if there is no comment. Pass in
158 * <code>ConfigFile.TRAILING_COMMENT</code> to get the trailing comment.
159 *
160 * @param key the key to look up.
161 *
162 * @return the associated comment or <code>null</code>
163 */
164 public String getComment(String key) {
165 return (String)comments.get(key);
166 }
167
168 /**
169 * Add an entry to the config file.
170 *
171 * @param key
172 * The config element name.
173 * @param values
174 * A string array of values.
175 * @param comment
176 * A string containing a properly commented config
177 * file comment (i.e. beginning with "#")
178 */
179 public void addEntry(String key, String[] values, String comment)
180 throws KeywordValueException {
181
182 // Don't add an actual config entry for the trailing comment
183 if (!key.equals(TRAILING_COMMENT)) {
184 config.set(key, values);
185 if (!order.contains(key)) {
186 order.addElement(key);
187 }
188 }
189 comments.put(key, comment);
190 }
191
192 /**
193 * Add an entry to the config file.
194 *
195 * @param key
196 * The config element name.
197 * @param value
198 * A String value.
199 * @param comment
200 * A string containing a properly commented config
201 * file comment (i.e. beginning with "#")
202 */
203 public void addEntry(String key, String value, String comment)
204 throws KeywordValueException {
205
206 // Don't add an actual config entry for the trailing comment
207 if (!key.equals(TRAILING_COMMENT)) {
208 config.set(key, value);
209 if (!order.contains(key)) {
210 order.addElement(key);
211 }
212 }
213 comments.put(key, comment);
214 }
215
216 /**
217 * Remove an entry from the config file.
218 *
219 * @param key
220 * The config element name.
221 */
222 public void removeEntry(String key) throws KeywordValueException {
223 // There is no config entry for the trailing comment
224 if (!key.equals(TRAILING_COMMENT)) {
225 config.remove(key);
226 order.removeElement(key);
227 }
228 comments.remove(key);
229 }
230
231 /**
232 * Get the associated file. If no file is associated with this
233 * config, <code>null</code> is returned.
234 */
235 public File getFile() {
236 return file;
237 }
238
239 /**
240 * Writes this config to the associated file. If no file
241 * is associated with this config, throws a
242 * <code>FileNotFoundException</code>
243 */
244 public void write() throws IOException, FileNotFoundException {
245 if (file == null) {
246 throw new FileNotFoundException("No file associated with this object");
247 }
248 FileOutputStream out = new FileOutputStream(file);
249 write(out);
250 out.close();
251 }
252
253 /**
254 * Writes out a config file to the OutputStream specified.
255 * Note that Objects other than String or String[] will
256 * be converted into a String.
257 *
258 * @param outputStream
259 * The output stream on which to write the config file.
260 */
261 public void write(OutputStream outputStream) {
262 PrintWriter out = new PrintWriter(outputStream, true);
263 boolean isArray = false;
264 String key;
265 String comment;
266 String[] values;
267 Hashtable remaining = new Hashtable();
268 String[] remainingkeys = config.leafKeys();
269
270 // Set up the remaining keys list
271 // The value doesn't matter, just the key
272 for (int i = 0; i < remainingkeys.length; i++) {
273 remaining.put(remainingkeys[i], "X");
274 }
275
276 // Do all the entries for which we have comments
277 for (int i = 0; i < order.size(); i++) {
278 key = (String) order.elementAt(i);
279 comment = (String) comments.get(key);
280 isArray = false;
281 try {
282 Object o = config.get(key);
283 if (o == null) {
284 continue;
285 }
286 isArray = o.getClass().isArray();
287 if (isArray) {
288 Object[] oa = (Object[])o;
289 if ((oa.length > 0) && (oa[0] instanceof java.lang.String)) {
290 values = (String[]) o;
291 } else {
292 values = new String[oa.length];
293 for (int k = 0; k < oa.length; k++) {
294 values[k] = oa[k].toString();
295 }
296 }
297 } else {
298 values = new String[1];
299 if (o instanceof java.lang.String) {
300 values[0] = (String) o;
301 } else {
302 values[0] = o.toString();
303 }
304 }
305 } catch (KeywordValueException e) {
306 values = null;
307 }
308 // write out entry
309 if ((values == null) || (values.length == 0)) {
310 if ((comment != null) && !(comment.equals(""))) {
311 if (comment.endsWith("\n")) {
312 out.print(comment);
313 } else {
314 out.println(comment);
315 }
316 }
317 out.print(key);
318 if (isArray) out.print("[]");
319 out.println(" =");
320 } else {
321 if ((comment != null) && !(comment.equals(""))) {
322 if (comment.endsWith("\n")) {
323 out.print(comment);
324 } else {
325 out.println(comment);
326 }
327 }
328 if (isArray) {
329 out.print(key + "[] = " + quoteStr(values[0]));
330 } else {
331 out.print(key + " = " + quoteStr(values[0]));
332 }
333 for (int j = 1; j < values.length; j++) {
334 out.print(", " + quoteStr(values[j]));
335 }
336 out.println();
337 }
338 remaining.remove(key);
339 }
340
341 // Do the trailing comment
342 comment = (String) comments.get(TRAILING_COMMENT);
343 if ((comment != null) && !(comment.equals(""))) {
344 if (comment.endsWith("\n")) {
345 out.print(comment);
346 } else {
347 out.println(comment);
348 }
349 }
350 remaining.remove(TRAILING_COMMENT);
351
352
353 // The new keys are in a hash table with no order
354 // sort them here so that they will be grouped correctly
355 // in the conf file. It makes it easier to read.
356 // XXX This is dependent on JDK 1.2 so comment it out
357 // XXX until we don't support JDK 1.1 anymore.
358 /* XXX
359 java.util.Arrays.sort(remainingkeys,new Comparator() {
360 public int compare(Object o1, Object o2) {
361 String s1 = (String) o1;
362 return s1.compareTo((String) o2);
363 }
364 });
365 XXX */
366
367
368 int i=0;
369 String lastWord = "";
370 while (i != remainingkeys.length) {
371 key = remainingkeys[i];
372 i++;
373 // Do the remaining config entries
374 if (remaining.get(key) != null) {
375 isArray = false;
376 if (!key.startsWith(lastWord)) {
377 out.println("");
378 }
379 int dot = key.indexOf('.');
380 if (dot == -1)
381 dot = key.length();
382 lastWord = key.substring(0,dot);
383
384 try {
385 Object o = config.get(key);
386 if (o == null) {
387 continue;
388 }
389 isArray = o.getClass().isArray();
390 if (isArray) {
391 Object[] oa = (Object[])o;
392 if (oa[0] instanceof java.lang.String) {
393 values = (String[]) o;
394 } else {
395 values = new String[oa.length];
396 for (int k = 0; k < oa.length; k++) {
397 values[k] = oa[k].toString();
398 }
399 }
400 } else {
401 values = new String[1];
402 if (o instanceof java.lang.String) {
403 values[0] = (String) o;
404 } else {
405 values[0] = o.toString();
406 }
407 }
408 } catch (KeywordValueException e) {
409 values = null;
410 }
411 // write out entry
412 if ((values == null) || (values.length == 0)) {
413 out.println(key + " =");
414 } else {
415 if (isArray) {
416 out.print(key + "[] = " + quoteStr(values[0]));
417 } else {
418 out.print(key + " = " + quoteStr(values[0]));
419 }
420
421 for (int j = 1; j < values.length; j++) {
422 out.print(key + ", " + quoteStr(values[j]));
423 }
424 out.println();
425 }
426 }
427 }
428 }
429
430
431 private boolean containsWhiteSpace(String str) {
432 if (str.indexOf(" ") != -1) {
433 return true;
434 } else if (str.indexOf("\t") != -1) {
435 return true;
436 }
437 return false;
438 }
439
440 private static final String quoteStr(String s) {
441 if ((s == null) || (s.length() < 1)) return "";
442 char[] chars = s.toCharArray();
443 StringBuffer sb = new StringBuffer();
444 boolean needQuotes = false;
445 for (int i=0; i<chars.length; i++) {
446 switch (chars[i]) {
447 // Chars that get special backquotes
448 case '\n':
449 needQuotes = true;
450 sb.append("\\n");
451 break;
452 case '\b':
453 needQuotes = true;
454 sb.append("\\b");
455 break;
456 case '\r':
457 needQuotes = true;
458 sb.append("\\r");
459 break;
460 case '\f':
461 needQuotes = true;
462 sb.append("\\f");
463 break;
464 case '"':
465 needQuotes = true;
466 sb.append("\\\"");
467 break;
468 case '\\':
469 needQuotes = true;
470 sb.append("\\\\");
471 break;
472
473 // Chars that cause the string to be enclosed in
474 // double quotes.
475 case '\t': case ' ': case '!': case '#': case '$':
476 case '%': case '&': case '\'': case '(': case ')':
477 case '*': case '+': case ',': case '/': case ':':
478 case ';': case '<': case '=': case '>': case '?':
479 case '[': case ']': case '^': case '`': case '{':
480 case '|': case '}': case '~':
481 needQuotes = true;
482 sb.append(chars[i]);
483 break;
484
485 // All other characters.
486 default:
487 if ((chars[i] < ' ') || (chars[i] == 0x7f)) {
488 needQuotes = true;
489 int ival = (int) chars[i];
490 sb.append('\\');
491 sb.append(digits[(ival & 0xc0) >> 6]);
492 sb.append(digits[(ival & 0x38) >> 3]);
493 sb.append(digits[(ival & 0x07)]);
494 } else if (chars[i] > 0x7f) {
495 needQuotes = true;
496 int ival = (int) chars[i];
497 sb.append("\\u");
498 sb.append(digits[(ival & 0xf000) >> 12]);
499 sb.append(digits[(ival & 0x0f00) >> 8]);
500 sb.append(digits[(ival & 0x00f0) >> 4]);
501 sb.append(digits[(ival & 0x000f)]);
502 } else {
503 sb.append(chars[i]);
504 }
505 }
506 }
507 if (needQuotes) {
508 return( "\"" + sb.toString() + "\"");
509 }
510 return sb.toString();
511 }
512
513 private static final char[] digits = {
514 '0', '1', '2', '3', '4', '5', '6', '7',
515 '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
516 };
517
518 }
519