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