1/*
2 *  Licensed to the Apache Software Foundation (ASF) under one or more
3 *  contributor license agreements.  See the NOTICE file distributed with
4 *  this work for additional information regarding copyright ownership.
5 *  The ASF licenses this file to You under the Apache License, Version 2.0
6 *  (the "License"); you may not use this file except in compliance with
7 *  the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 */
17
18package com.badlogic.gdx.utils;
19
20import java.io.BufferedReader;
21import java.io.IOException;
22import java.io.InputStream;
23import java.io.InputStreamReader;
24import java.io.OutputStream;
25import java.io.OutputStreamWriter;
26import java.io.Reader;
27import java.io.Writer;
28import java.util.Date;
29
30import com.badlogic.gdx.utils.ObjectMap.Entry;
31
32/** {@code PropertiesUtils} is a helper class that allows you to load and store key/value pairs of an
33 * {@code ObjectMap<String,String>} with the same line-oriented syntax supported by {@code java.util.Properties}. */
34public final class PropertiesUtils {
35
36	private static final int NONE = 0, SLASH = 1, UNICODE = 2, CONTINUE = 3, KEY_DONE = 4, IGNORE = 5;
37
38	private static final String LINE_SEPARATOR = "\n";
39
40	private PropertiesUtils () {
41	}
42
43	/** Adds to the specified {@code ObjectMap} the key/value pairs loaded from the {@code Reader} in a simple line-oriented format
44	 * compatible with <code>java.util.Properties</code>.
45	 * <p>
46	 * The input stream remains open after this method returns.
47	 *
48	 * @param properties the map to be filled.
49	 * @param reader the input character stream reader.
50	 * @throws IOException if an error occurred when reading from the input stream.
51	 * @throws IllegalArgumentException if a malformed Unicode escape appears in the input. */
52	@SuppressWarnings("deprecation")
53	public static void load (ObjectMap<String, String> properties, Reader reader) throws IOException {
54		if (properties == null) throw new NullPointerException("ObjectMap cannot be null");
55		if (reader == null) throw new NullPointerException("Reader cannot be null");
56		int mode = NONE, unicode = 0, count = 0;
57		char nextChar, buf[] = new char[40];
58		int offset = 0, keyLength = -1, intVal;
59		boolean firstChar = true;
60
61		BufferedReader br = new BufferedReader(reader);
62
63		while (true) {
64			intVal = br.read();
65			if (intVal == -1) {
66				break;
67			}
68			nextChar = (char)intVal;
69
70			if (offset == buf.length) {
71				char[] newBuf = new char[buf.length * 2];
72				System.arraycopy(buf, 0, newBuf, 0, offset);
73				buf = newBuf;
74			}
75			if (mode == UNICODE) {
76				int digit = Character.digit(nextChar, 16);
77				if (digit >= 0) {
78					unicode = (unicode << 4) + digit;
79					if (++count < 4) {
80						continue;
81					}
82				} else if (count <= 4) {
83					throw new IllegalArgumentException("Invalid Unicode sequence: illegal character");
84				}
85				mode = NONE;
86				buf[offset++] = (char)unicode;
87				if (nextChar != '\n') {
88					continue;
89				}
90			}
91			if (mode == SLASH) {
92				mode = NONE;
93				switch (nextChar) {
94				case '\r':
95					mode = CONTINUE; // Look for a following \n
96					continue;
97				case '\n':
98					mode = IGNORE; // Ignore whitespace on the next line
99					continue;
100				case 'b':
101					nextChar = '\b';
102					break;
103				case 'f':
104					nextChar = '\f';
105					break;
106				case 'n':
107					nextChar = '\n';
108					break;
109				case 'r':
110					nextChar = '\r';
111					break;
112				case 't':
113					nextChar = '\t';
114					break;
115				case 'u':
116					mode = UNICODE;
117					unicode = count = 0;
118					continue;
119				}
120			} else {
121				switch (nextChar) {
122				case '#':
123				case '!':
124					if (firstChar) {
125						while (true) {
126							intVal = br.read();
127							if (intVal == -1) {
128								break;
129							}
130							nextChar = (char)intVal;
131							if (nextChar == '\r' || nextChar == '\n') {
132								break;
133							}
134						}
135						continue;
136					}
137					break;
138				case '\n':
139					if (mode == CONTINUE) { // Part of a \r\n sequence
140						mode = IGNORE; // Ignore whitespace on the next line
141						continue;
142					}
143					// fall into the next case
144				case '\r':
145					mode = NONE;
146					firstChar = true;
147					if (offset > 0 || (offset == 0 && keyLength == 0)) {
148						if (keyLength == -1) {
149							keyLength = offset;
150						}
151						String temp = new String(buf, 0, offset);
152						properties.put(temp.substring(0, keyLength), temp.substring(keyLength));
153					}
154					keyLength = -1;
155					offset = 0;
156					continue;
157				case '\\':
158					if (mode == KEY_DONE) {
159						keyLength = offset;
160					}
161					mode = SLASH;
162					continue;
163				case ':':
164				case '=':
165					if (keyLength == -1) { // if parsing the key
166						mode = NONE;
167						keyLength = offset;
168						continue;
169					}
170					break;
171				}
172				// if (Character.isWhitespace(nextChar)) { <-- not supported by GWT; replaced with isSpace.
173				if (Character.isSpace(nextChar)) {
174					if (mode == CONTINUE) {
175						mode = IGNORE;
176					}
177					// if key length == 0 or value length == 0
178					if (offset == 0 || offset == keyLength || mode == IGNORE) {
179						continue;
180					}
181					if (keyLength == -1) { // if parsing the key
182						mode = KEY_DONE;
183						continue;
184					}
185				}
186				if (mode == IGNORE || mode == CONTINUE) {
187					mode = NONE;
188				}
189			}
190			firstChar = false;
191			if (mode == KEY_DONE) {
192				keyLength = offset;
193				mode = NONE;
194			}
195			buf[offset++] = nextChar;
196		}
197		if (mode == UNICODE && count <= 4) {
198			throw new IllegalArgumentException("Invalid Unicode sequence: expected format \\uxxxx");
199		}
200		if (keyLength == -1 && offset > 0) {
201			keyLength = offset;
202		}
203		if (keyLength >= 0) {
204			String temp = new String(buf, 0, offset);
205			String key = temp.substring(0, keyLength);
206			String value = temp.substring(keyLength);
207			if (mode == SLASH) {
208				value += "\u0000";
209			}
210			properties.put(key, value);
211		}
212	}
213
214	/** Writes the key/value pairs of the specified <code>ObjectMap</code> to the output character stream in a simple line-oriented
215	 * format compatible with <code>java.util.Properties</code>.
216	 * <p>
217	 * Every entry in the <code>ObjectMap</code> is written out, one per line. For each entry the key string is written, then an
218	 * ASCII <code>=</code>, then the associated element string. For the key, all space characters are written with a preceding
219	 * <code>\</code> character. For the element, leading space characters, but not embedded or trailing space characters, are
220	 * written with a preceding <code>\</code> character. The key and element characters <code>#</code>, <code>!</code>,
221	 * <code>=</code>, and <code>:</code> are written with a preceding backslash to ensure that they are properly loaded.
222	 * <p>
223	 * After the entries have been written, the output stream is flushed. The output stream remains open after this method returns.
224	 *
225	 * @param properties the {@code ObjectMap}.
226	 * @param writer an output character stream writer.
227	 * @param comment an optional comment to be written, or null.
228	 * @exception IOException if writing this property list to the specified output stream throws an <tt>IOException</tt>.
229	 * @exception NullPointerException if <code>writer</code> is null. */
230	public static void store (ObjectMap<String, String> properties, Writer writer, String comment) throws IOException {
231		storeImpl(properties, writer, comment, false);
232	}
233
234	private static void storeImpl (ObjectMap<String, String> properties, Writer writer, String comment, boolean escapeUnicode)
235		throws IOException {
236		if (comment != null) {
237			writeComment(writer, comment);
238		}
239		writer.write("#");
240		writer.write(new Date().toString());
241		writer.write(LINE_SEPARATOR);
242
243		StringBuilder sb = new StringBuilder(200);
244		for (Entry<String, String> entry : properties.entries()) {
245			dumpString(sb, entry.key, true, escapeUnicode);
246			sb.append('=');
247			dumpString(sb, entry.value, false, escapeUnicode);
248			writer.write(LINE_SEPARATOR);
249			writer.write(sb.toString());
250			sb.setLength(0);
251		}
252		writer.flush();
253	}
254
255	private static void dumpString (StringBuilder outBuffer, String string, boolean escapeSpace, boolean escapeUnicode) {
256		int len = string.length();
257		for (int i = 0; i < len; i++) {
258			char ch = string.charAt(i);
259			// Handle common case first
260			if ((ch > 61) && (ch < 127)) {
261				outBuffer.append(ch == '\\' ? "\\\\" : ch);
262				continue;
263			}
264			switch (ch) {
265			case ' ':
266				if (i == 0 || escapeSpace) outBuffer.append("\\ ");
267				break;
268			case '\n':
269				outBuffer.append("\\n");
270				break;
271			case '\r':
272				outBuffer.append("\\r");
273				break;
274			case '\t':
275				outBuffer.append("\\t");
276				break;
277			case '\f':
278				outBuffer.append("\\f");
279				break;
280			case '=': // Fall through
281			case ':': // Fall through
282			case '#': // Fall through
283			case '!':
284				outBuffer.append('\\').append(ch);
285				break;
286			default:
287				if (((ch < 0x0020) || (ch > 0x007e)) & escapeUnicode) {
288					String hex = Integer.toHexString(ch);
289					outBuffer.append("\\u");
290					for (int j = 0; j < 4 - hex.length(); j++) {
291						outBuffer.append('0');
292					}
293					outBuffer.append(hex);
294				} else {
295					outBuffer.append(ch);
296				}
297				break;
298			}
299		}
300	}
301
302	private static void writeComment (Writer writer, String comment) throws IOException {
303		writer.write("#");
304		int len = comment.length();
305		int curIndex = 0;
306		int lastIndex = 0;
307		while (curIndex < len) {
308			char c = comment.charAt(curIndex);
309			if (c > '\u00ff' || c == '\n' || c == '\r') {
310				if (lastIndex != curIndex) writer.write(comment.substring(lastIndex, curIndex));
311				if (c > '\u00ff') {
312					String hex = Integer.toHexString(c);
313					writer.write("\\u");
314					for (int j = 0; j < 4 - hex.length(); j++) {
315						writer.write('0');
316					}
317					writer.write(hex);
318				} else {
319					writer.write(LINE_SEPARATOR);
320					if (c == '\r' && curIndex != len - 1 && comment.charAt(curIndex + 1) == '\n') {
321						curIndex++;
322					}
323					if (curIndex == len - 1 || (comment.charAt(curIndex + 1) != '#' && comment.charAt(curIndex + 1) != '!'))
324						writer.write("#");
325				}
326				lastIndex = curIndex + 1;
327			}
328			curIndex++;
329		}
330		if (lastIndex != curIndex) writer.write(comment.substring(lastIndex, curIndex));
331		writer.write(LINE_SEPARATOR);
332	}
333}
334