001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package java.util;
019:
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.io.OutputStream;
023: import java.io.OutputStreamWriter;
024: import java.io.PrintStream;
025: import java.io.PrintWriter;
026: import java.io.StringReader;
027: import java.nio.charset.Charset;
028: import java.nio.charset.IllegalCharsetNameException;
029: import java.nio.charset.UnsupportedCharsetException;
030: import java.security.AccessController;
031:
032: import javax.xml.parsers.DocumentBuilder;
033: import javax.xml.parsers.DocumentBuilderFactory;
034: import javax.xml.parsers.ParserConfigurationException;
035:
036: import org.xml.sax.EntityResolver;
037: import org.xml.sax.ErrorHandler;
038: import org.xml.sax.InputSource;
039: import org.xml.sax.SAXException;
040: import org.xml.sax.SAXParseException;
041:
042: import org.w3c.dom.Document;
043: import org.w3c.dom.Element;
044: import org.w3c.dom.NodeList;
045:
046: import org.apache.harmony.luni.internal.nls.Messages;
047: import org.apache.harmony.luni.util.PriviAction;
048:
049: /**
050: * Properties is a Hashtable where the keys and values must be Strings. Each
051: * Properties can have a default Properties which specifies the default values
052: * which are used if the key is not in this Properties.
053: *
054: * @see Hashtable
055: * @see java.lang.System#getProperties
056: */
057: public class Properties extends Hashtable<Object, Object> {
058:
059: private static final long serialVersionUID = 4112578634029874840L;
060:
061: private transient DocumentBuilder builder = null;
062:
063: private static final String PROP_DTD_NAME = "http://java.sun.com/dtd/properties.dtd";
064:
065: private static final String PROP_DTD = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
066: + " <!ELEMENT properties (comment?, entry*) >"
067: + " <!ATTLIST properties version CDATA #FIXED \"1.0\" >"
068: + " <!ELEMENT comment (#PCDATA) >"
069: + " <!ELEMENT entry (#PCDATA) >"
070: + " <!ATTLIST entry key CDATA #REQUIRED >";
071:
072: /**
073: * The default values for this Properties.
074: */
075: protected Properties defaults;
076:
077: private static final int NONE = 0, SLASH = 1, UNICODE = 2,
078: CONTINUE = 3, KEY_DONE = 4, IGNORE = 5;
079:
080: /**
081: * Constructs a new Properties object.
082: */
083: public Properties() {
084: super ();
085: }
086:
087: /**
088: * Constructs a new Properties object using the specified default
089: * properties.
090: *
091: * @param properties
092: * the default properties
093: */
094: public Properties(Properties properties) {
095: defaults = properties;
096: }
097:
098: private void dumpString(StringBuilder buffer, String string,
099: boolean key) {
100: int i = 0;
101: if (!key && i < string.length() && string.charAt(i) == ' ') {
102: buffer.append("\\ "); //$NON-NLS-1$
103: i++;
104: }
105:
106: for (; i < string.length(); i++) {
107: char ch = string.charAt(i);
108: switch (ch) {
109: case '\t':
110: buffer.append("\\t"); //$NON-NLS-1$
111: break;
112: case '\n':
113: buffer.append("\\n"); //$NON-NLS-1$
114: break;
115: case '\f':
116: buffer.append("\\f"); //$NON-NLS-1$
117: break;
118: case '\r':
119: buffer.append("\\r"); //$NON-NLS-1$
120: break;
121: default:
122: if ("\\#!=:".indexOf(ch) >= 0 || (key && ch == ' ')) {
123: buffer.append('\\');
124: }
125: if (ch >= ' ' && ch <= '~') {
126: buffer.append(ch);
127: } else {
128: String hex = Integer.toHexString(ch);
129: buffer.append("\\u"); //$NON-NLS-1$
130: for (int j = 0; j < 4 - hex.length(); j++) {
131: buffer.append("0"); //$NON-NLS-1$
132: }
133: buffer.append(hex);
134: }
135: }
136: }
137: }
138:
139: /**
140: * Searches for the property with the specified name. If the property is not
141: * found, look in the default properties. If the property is not found in
142: * the default properties, answer null.
143: *
144: * @param name
145: * the name of the property to find
146: * @return the named property value
147: */
148: public String getProperty(String name) {
149: Object result = super .get(name);
150: String property = result instanceof String ? (String) result
151: : null;
152: if (property == null && defaults != null) {
153: property = defaults.getProperty(name);
154: }
155: return property;
156: }
157:
158: /**
159: * Searches for the property with the specified name. If the property is not
160: * found, look in the default properties. If the property is not found in
161: * the default properties, answer the specified default.
162: *
163: * @param name
164: * the name of the property to find
165: * @param defaultValue
166: * the default value
167: * @return the named property value
168: */
169: public String getProperty(String name, String defaultValue) {
170: Object result = super .get(name);
171: String property = result instanceof String ? (String) result
172: : null;
173: if (property == null && defaults != null) {
174: property = defaults.getProperty(name);
175: }
176: if (property == null) {
177: return defaultValue;
178: }
179: return property;
180: }
181:
182: /**
183: * Lists the mappings in this Properties to the specified PrintStream in a
184: * human readable form.
185: *
186: * @param out
187: * the PrintStream
188: */
189: public void list(PrintStream out) {
190: if (out == null) {
191: throw new NullPointerException();
192: }
193: StringBuffer buffer = new StringBuffer(80);
194: Enumeration<?> keys = propertyNames();
195: while (keys.hasMoreElements()) {
196: String key = (String) keys.nextElement();
197: buffer.append(key);
198: buffer.append('=');
199: String property = (String) super .get(key);
200: Properties def = defaults;
201: while (property == null) {
202: property = (String) def.get(key);
203: def = def.defaults;
204: }
205: if (property.length() > 40) {
206: buffer.append(property.substring(0, 37));
207: buffer.append("..."); //$NON-NLS-1$
208: } else {
209: buffer.append(property);
210: }
211: out.println(buffer.toString());
212: buffer.setLength(0);
213: }
214: }
215:
216: /**
217: * Lists the mappings in this Properties to the specified PrintWriter in a
218: * human readable form.
219: *
220: * @param writer
221: * the PrintWriter
222: */
223: public void list(PrintWriter writer) {
224: if (writer == null) {
225: throw new NullPointerException();
226: }
227: StringBuffer buffer = new StringBuffer(80);
228: Enumeration<?> keys = propertyNames();
229: while (keys.hasMoreElements()) {
230: String key = (String) keys.nextElement();
231: buffer.append(key);
232: buffer.append('=');
233: String property = (String) super .get(key);
234: Properties def = defaults;
235: while (property == null) {
236: property = (String) def.get(key);
237: def = def.defaults;
238: }
239: if (property.length() > 40) {
240: buffer.append(property.substring(0, 37));
241: buffer.append("..."); //$NON-NLS-1$
242: } else {
243: buffer.append(property);
244: }
245: writer.println(buffer.toString());
246: buffer.setLength(0);
247: }
248: }
249:
250: /**
251: * Loads properties from the specified InputStream. The properties are of
252: * the form <code>key=value</code>, one property per line.
253: *
254: * @param in
255: * the input stream
256: * @throws IOException
257: */
258: public synchronized void load(InputStream in) throws IOException {
259: int mode = NONE, unicode = 0, count = 0;
260: char nextChar, buf[] = new char[40];
261: int offset = 0, keyLength = -1;
262: boolean firstChar = true;
263: byte[] inbuf = new byte[256];
264: int inbufCount = 0, inbufPos = 0;
265:
266: while (true) {
267: if (inbufPos == inbufCount) {
268: if ((inbufCount = in.read(inbuf)) == -1) {
269: break;
270: }
271: inbufPos = 0;
272: }
273: nextChar = (char) (inbuf[inbufPos++] & 0xff);
274:
275: if (offset == buf.length) {
276: char[] newBuf = new char[buf.length * 2];
277: System.arraycopy(buf, 0, newBuf, 0, offset);
278: buf = newBuf;
279: }
280: if (mode == UNICODE) {
281: int digit = Character.digit(nextChar, 16);
282: if (digit >= 0) {
283: unicode = (unicode << 4) + digit;
284: if (++count < 4) {
285: continue;
286: }
287: } else if (count <= 4) {
288: // luni.09=Invalid Unicode sequence: illegal character
289: throw new IllegalArgumentException(Messages
290: .getString("luni.09"));
291: }
292: mode = NONE;
293: buf[offset++] = (char) unicode;
294: if (nextChar != '\n') {
295: continue;
296: }
297: }
298: if (mode == SLASH) {
299: mode = NONE;
300: switch (nextChar) {
301: case '\r':
302: mode = CONTINUE; // Look for a following \n
303: continue;
304: case '\n':
305: mode = IGNORE; // Ignore whitespace on the next line
306: continue;
307: case 'b':
308: nextChar = '\b';
309: break;
310: case 'f':
311: nextChar = '\f';
312: break;
313: case 'n':
314: nextChar = '\n';
315: break;
316: case 'r':
317: nextChar = '\r';
318: break;
319: case 't':
320: nextChar = '\t';
321: break;
322: case 'u':
323: mode = UNICODE;
324: unicode = count = 0;
325: continue;
326: }
327: } else {
328: switch (nextChar) {
329: case '#':
330: case '!':
331: if (firstChar) {
332: while (true) {
333: if (inbufPos == inbufCount) {
334: if ((inbufCount = in.read(inbuf)) == -1) {
335: inbufPos = -1;
336: break;
337: }
338: inbufPos = 0;
339: }
340: nextChar = (char) inbuf[inbufPos++]; // & 0xff
341: // not
342: // required
343: if (nextChar == '\r' || nextChar == '\n') {
344: break;
345: }
346: }
347: continue;
348: }
349: break;
350: case '\n':
351: if (mode == CONTINUE) { // Part of a \r\n sequence
352: mode = IGNORE; // Ignore whitespace on the next line
353: continue;
354: }
355: // fall into the next case
356: case '\r':
357: mode = NONE;
358: firstChar = true;
359: if (offset > 0) {
360: if (keyLength == -1) {
361: keyLength = offset;
362: }
363: String temp = new String(buf, 0, offset);
364: put(temp.substring(0, keyLength), temp
365: .substring(keyLength));
366: }
367: keyLength = -1;
368: offset = 0;
369: continue;
370: case '\\':
371: if (mode == KEY_DONE) {
372: keyLength = offset;
373: }
374: mode = SLASH;
375: continue;
376: case ':':
377: case '=':
378: if (keyLength == -1) { // if parsing the key
379: mode = NONE;
380: keyLength = offset;
381: continue;
382: }
383: break;
384: }
385: if (Character.isWhitespace(nextChar)) {
386: if (mode == CONTINUE) {
387: mode = IGNORE;
388: }
389: // if key length == 0 or value length == 0
390: if (offset == 0 || offset == keyLength
391: || mode == IGNORE) {
392: continue;
393: }
394: if (keyLength == -1) { // if parsing the key
395: mode = KEY_DONE;
396: continue;
397: }
398: }
399: if (mode == IGNORE || mode == CONTINUE) {
400: mode = NONE;
401: }
402: }
403: firstChar = false;
404: if (mode == KEY_DONE) {
405: keyLength = offset;
406: mode = NONE;
407: }
408: buf[offset++] = nextChar;
409: }
410: if (mode == UNICODE && count <= 4) {
411: // luni.08=Invalid Unicode sequence: expected format \\uxxxx
412:throw new IllegalArgumentException(Messages
413: .getString("luni.08"));
414: }
415: if (keyLength == -1 && offset > 0) {
416: keyLength = offset;
417: }
418: if (keyLength >= 0) {
419: String temp = new String(buf, 0, offset);
420: String key = temp.substring(0, keyLength);
421: String value = temp.substring(keyLength);
422: if (mode == SLASH) {
423: value += "\u0000";
424: }
425: put(key, value);
426: }
427: }
428:
429: /**
430: * Answers all of the property names that this Properties contains.
431: *
432: * @return an Enumeration containing the names of all properties
433: */
434: public Enumeration<?> propertyNames() {
435: if (defaults == null) {
436: return keys();
437: }
438:
439: Hashtable<Object, Object> set = new Hashtable<Object, Object>(
440: defaults.size() + size());
441: Enumeration<?> keys = defaults.propertyNames();
442: while (keys.hasMoreElements()) {
443: set.put(keys.nextElement(), set);
444: }
445: keys = keys();
446: while (keys.hasMoreElements()) {
447: set.put(keys.nextElement(), set);
448: }
449: return set.keys();
450: }
451:
452: /**
453: * Saves the mappings in this Properties to the specified OutputStream,
454: * putting the specified comment at the beginning. The output from this
455: * method is suitable for being read by the load() method.
456: *
457: * @param out
458: * the OutputStream
459: * @param comment
460: * the comment
461: *
462: * @exception ClassCastException
463: * when the key or value of a mapping is not a String
464: *
465: * @deprecated Does not throw an IOException, use store()
466: */
467: @Deprecated
468: public void save(OutputStream out, String comment) {
469: try {
470: store(out, comment);
471: } catch (IOException e) {
472: }
473: }
474:
475: /**
476: * Maps the specified key to the specified value. If the key already exists,
477: * the old value is replaced. The key and value cannot be null.
478: *
479: * @param name
480: * the key
481: * @param value
482: * the value
483: * @return the old value mapped to the key, or null
484: */
485: public Object setProperty(String name, String value) {
486: return put(name, value);
487: }
488:
489: private static String lineSeparator;
490:
491: /**
492: * Stores the mappings in this Properties to the specified OutputStream,
493: * putting the specified comment at the beginning. The output from this
494: * method is suitable for being read by the load() method.
495: *
496: * @param out
497: * the OutputStream
498: * @param comment
499: * the comment
500: * @throws IOException
501: *
502: * @exception ClassCastException
503: * when the key or value of a mapping is not a String
504: */
505: public synchronized void store(OutputStream out, String comment)
506: throws IOException {
507: if (lineSeparator == null) {
508: lineSeparator = AccessController
509: .doPrivileged(new PriviAction<String>(
510: "line.separator")); //$NON-NLS-1$
511: }
512:
513: StringBuilder buffer = new StringBuilder(200);
514: OutputStreamWriter writer = new OutputStreamWriter(out,
515: "ISO8859_1"); //$NON-NLS-1$
516: if (comment != null) {
517: writer.write("#"); //$NON-NLS-1$
518: writer.write(comment);
519: writer.write(lineSeparator);
520: }
521: writer.write("#"); //$NON-NLS-1$
522: writer.write(new Date().toString());
523: writer.write(lineSeparator);
524:
525: for (Map.Entry<Object, Object> entry : entrySet()) {
526: String key = (String) entry.getKey();
527: dumpString(buffer, key, true);
528: buffer.append('=');
529: dumpString(buffer, (String) entry.getValue(), false);
530: buffer.append(lineSeparator);
531: writer.write(buffer.toString());
532: buffer.setLength(0);
533: }
534: writer.flush();
535: }
536:
537: public synchronized void loadFromXML(InputStream in)
538: throws IOException, InvalidPropertiesFormatException {
539: if (in == null) {
540: throw new NullPointerException();
541: }
542:
543: if (builder == null) {
544: DocumentBuilderFactory factory = DocumentBuilderFactory
545: .newInstance();
546: factory.setValidating(true);
547:
548: try {
549: builder = factory.newDocumentBuilder();
550: } catch (ParserConfigurationException e) {
551: throw new Error(e);
552: }
553:
554: builder.setErrorHandler(new ErrorHandler() {
555: public void warning(SAXParseException e)
556: throws SAXException {
557: throw e;
558: }
559:
560: public void error(SAXParseException e)
561: throws SAXException {
562: throw e;
563: }
564:
565: public void fatalError(SAXParseException e)
566: throws SAXException {
567: throw e;
568: }
569: });
570:
571: builder.setEntityResolver(new EntityResolver() {
572: public InputSource resolveEntity(String publicId,
573: String systemId) throws SAXException,
574: IOException {
575: if (systemId.equals(PROP_DTD_NAME)) {
576: InputSource result = new InputSource(
577: new StringReader(PROP_DTD));
578: result.setSystemId(PROP_DTD_NAME);
579: return result;
580: }
581: throw new SAXException(
582: "Invalid DOCTYPE declaration: " + systemId);
583: }
584: });
585: }
586:
587: try {
588: Document doc = builder.parse(in);
589: NodeList entries = doc.getElementsByTagName("entry");
590: if (entries == null) {
591: return;
592: }
593: int entriesListLength = entries.getLength();
594:
595: for (int i = 0; i < entriesListLength; i++) {
596: Element entry = (Element) entries.item(i);
597: String key = entry.getAttribute("key");
598: String value = entry.getTextContent();
599:
600: /*
601: * key != null & value != null but key or(and) value can be
602: * empty String
603: */
604: put(key, value);
605: }
606: } catch (IOException e) {
607: throw e;
608: } catch (SAXException e) {
609: throw new InvalidPropertiesFormatException(e);
610: }
611: }
612:
613: public void storeToXML(OutputStream os, String comment)
614: throws IOException {
615: storeToXML(os, comment, "UTF-8");
616: }
617:
618: public synchronized void storeToXML(OutputStream os,
619: String comment, String encoding) throws IOException {
620:
621: if (os == null || encoding == null) {
622: throw new NullPointerException();
623: }
624:
625: /*
626: * We can write to XML file using encoding parameter but note that some
627: * aliases for encodings are not supported by the XML parser. Thus we
628: * have to know canonical name for encoding used to store data in XML
629: * since the XML parser must recognize encoding name used to store data.
630: */
631:
632: String encodingCanonicalName;
633: try {
634: encodingCanonicalName = Charset.forName(encoding).name();
635: } catch (IllegalCharsetNameException e) {
636: System.out.println("Warning: encoding name " + encoding
637: + " is illegal, using UTF-8 as default encoding");
638: encodingCanonicalName = "UTF-8";
639: } catch (UnsupportedCharsetException e) {
640: System.out
641: .println("Warning: encoding "
642: + encoding
643: + " is not supported, using UTF-8 as default encoding");
644: encodingCanonicalName = "UTF-8";
645: }
646:
647: PrintStream printStream = new PrintStream(os, false,
648: encodingCanonicalName);
649:
650: printStream.print("<?xml version=\"1.0\" encoding=\"");
651: printStream.print(encodingCanonicalName);
652: printStream.println("\"?>");
653:
654: printStream.print("<!DOCTYPE properties SYSTEM \"");
655: printStream.print(PROP_DTD_NAME);
656: printStream.println("\">");
657:
658: printStream.println("<properties>");
659:
660: if (comment != null) {
661: printStream.print("<comment>");
662: printStream.print(substitutePredefinedEntries(comment));
663: printStream.println("</comment>");
664: }
665:
666: for (Map.Entry<Object, Object> entry : entrySet()) {
667: String keyValue = (String) entry.getKey();
668: String entryValue = (String) entry.getValue();
669: printStream.print("<entry key=\"");
670: printStream.print(substitutePredefinedEntries(keyValue));
671: printStream.print("\">");
672: printStream.print(substitutePredefinedEntries(entryValue));
673: printStream.println("</entry>");
674: }
675: printStream.println("</properties>");
676: printStream.flush();
677: }
678:
679: private String substitutePredefinedEntries(String s) {
680:
681: /*
682: * substitution for predefined character entities to use them safely in
683: * XML
684: */
685: return s.replaceAll("&", "&").replaceAll("<", "<")
686: .replaceAll(">", ">").replaceAll("\u0027", "'")
687: .replaceAll("\"", """);
688: }
689: }
|