fix config item '+' repaint
[nikiroo-utils.git] / src / be / nikiroo / utils / serial / SerialUtils.java
CommitLineData
db31c358
NR
1package be.nikiroo.utils.serial;
2
e570f7eb 3import java.io.IOException;
08f80ac5 4import java.io.InputStream;
db31c358 5import java.io.NotSerializableException;
08f80ac5 6import java.io.OutputStream;
ce0974c4 7import java.lang.reflect.Array;
8c8da42a 8import java.lang.reflect.Constructor;
db31c358 9import java.lang.reflect.Field;
e570f7eb 10import java.lang.reflect.Modifier;
f4053377 11import java.net.URL;
72648e75 12import java.util.ArrayList;
db31c358 13import java.util.HashMap;
72648e75 14import java.util.List;
db31c358 15import java.util.Map;
ce0974c4 16import java.util.UnknownFormatConversionException;
db31c358 17
08f80ac5 18import be.nikiroo.utils.IOUtils;
80500544 19import be.nikiroo.utils.Image;
08f80ac5 20import be.nikiroo.utils.StringUtils;
a6a73de3
NR
21import be.nikiroo.utils.streams.Base64InputStream;
22import be.nikiroo.utils.streams.Base64OutputStream;
d2219aa0 23import be.nikiroo.utils.streams.BufferedInputStream;
08f80ac5
NR
24import be.nikiroo.utils.streams.NextableInputStream;
25import be.nikiroo.utils.streams.NextableInputStreamStep;
e570f7eb 26
db31c358 27/**
8c8da42a 28 * Small class to help with serialisation.
db31c358
NR
29 * <p>
30 * Note that we do not support inner classes (but we do support nested classes)
31 * and all objects require an empty constructor to be deserialised.
5bc55b51
NR
32 * <p>
33 * It is possible to add support to custom types (both the encoder and the
34 * decoder will require the custom classes) -- see {@link CustomSerializer}.
35 * <p>
36 * Default supported types are:
37 * <ul>
38 * <li>NULL (as a null value)</li>
39 * <li>String</li>
40 * <li>Boolean</li>
41 * <li>Byte</li>
42 * <li>Character</li>
43 * <li>Short</li>
44 * <li>Long</li>
45 * <li>Float</li>
46 * <li>Double</li>
47 * <li>Integer</li>
48 * <li>Enum (any enum whose name and value is known by the caller)</li>
49 * <li>java.awt.image.BufferedImage (as a {@link CustomSerializer})</li>
50 * <li>An array of the above (as a {@link CustomSerializer})</li>
f4053377 51 * <li>URL</li>
5bc55b51 52 * </ul>
db31c358
NR
53 *
54 * @author niki
55 */
aad14586 56public class SerialUtils {
db31c358
NR
57 private static Map<String, CustomSerializer> customTypes;
58
59 static {
60 customTypes = new HashMap<String, CustomSerializer>();
ce0974c4
NR
61
62 // Array types:
63 customTypes.put("[]", new CustomSerializer() {
64 @Override
08f80ac5
NR
65 protected void toStream(OutputStream out, Object value)
66 throws IOException {
67
ce0974c4
NR
68 String type = value.getClass().getCanonicalName();
69 type = type.substring(0, type.length() - 2); // remove the []
70
08f80ac5 71 write(out, type);
ce0974c4
NR
72 try {
73 for (int i = 0; true; i++) {
74 Object item = Array.get(value, i);
08f80ac5 75
ce0974c4 76 // encode it normally if direct value
08f80ac5
NR
77 write(out, "\r");
78 if (!SerialUtils.encode(out, item)) {
ce0974c4 79 try {
08f80ac5 80 write(out, "B64:");
a6a73de3
NR
81 OutputStream out64 = new Base64OutputStream(
82 out, true);
83 new Exporter(out64).append(item);
cd3d510f 84 out64.flush();
ce0974c4
NR
85 } catch (NotSerializableException e) {
86 throw new UnknownFormatConversionException(e
87 .getMessage());
88 }
89 }
ce0974c4
NR
90 }
91 } catch (ArrayIndexOutOfBoundsException e) {
92 // Done.
93 }
ce0974c4
NR
94 }
95
96 @Override
08f80ac5
NR
97 protected Object fromStream(InputStream in) throws IOException {
98 NextableInputStream stream = new NextableInputStream(in,
99 new NextableInputStreamStep('\r'));
ce0974c4
NR
100
101 try {
08f80ac5
NR
102 List<Object> list = new ArrayList<Object>();
103 stream.next();
104 String type = IOUtils.readSmallStream(stream);
105
106 while (stream.next()) {
107 Object value = new Importer().read(stream).getValue();
108 list.add(value);
109 }
110
ce0974c4 111 Object array = Array.newInstance(
08f80ac5
NR
112 SerialUtils.getClass(type), list.size());
113 for (int i = 0; i < list.size(); i++) {
114 Array.set(array, i, list.get(i));
ce0974c4
NR
115 }
116
117 return array;
118 } catch (Exception e) {
452f38c8
NR
119 if (e instanceof IOException) {
120 throw (IOException) e;
121 }
122 throw new IOException(e.getMessage());
ce0974c4
NR
123 }
124 }
08f80ac5
NR
125
126 @Override
127 protected String getType() {
128 return "[]";
129 }
ce0974c4 130 });
e570f7eb 131
f4053377
NR
132 // URL:
133 customTypes.put("java.net.URL", new CustomSerializer() {
134 @Override
08f80ac5
NR
135 protected void toStream(OutputStream out, Object value)
136 throws IOException {
137 String val = "";
f4053377 138 if (value != null) {
08f80ac5 139 val = ((URL) value).toString();
f4053377 140 }
08f80ac5 141
f8147a0e 142 out.write(StringUtils.getBytes(val));
f4053377
NR
143 }
144
145 @Override
08f80ac5
NR
146 protected Object fromStream(InputStream in) throws IOException {
147 String val = IOUtils.readSmallStream(in);
148 if (!val.isEmpty()) {
149 return new URL(val);
f4053377 150 }
08f80ac5 151
f4053377
NR
152 return null;
153 }
154
155 @Override
156 protected String getType() {
157 return "java.net.URL";
158 }
159 });
160
e570f7eb 161 // Images (this is currently the only supported image type by default)
80500544 162 customTypes.put("be.nikiroo.utils.Image", new CustomSerializer() {
e570f7eb 163 @Override
08f80ac5
NR
164 protected void toStream(OutputStream out, Object value)
165 throws IOException {
166 Image img = (Image) value;
a6a73de3 167 OutputStream encoded = new Base64OutputStream(out, true);
08f80ac5
NR
168 try {
169 InputStream in = img.newInputStream();
170 try {
171 IOUtils.write(in, encoded);
172 } finally {
173 in.close();
174 }
175 } finally {
176 encoded.flush();
177 // Cannot close!
178 }
e570f7eb
NR
179 }
180
181 @Override
182 protected String getType() {
80500544 183 return "be.nikiroo.utils.Image";
e570f7eb
NR
184 }
185
186 @Override
08f80ac5 187 protected Object fromStream(InputStream in) throws IOException {
e570f7eb 188 try {
08f80ac5 189 // Cannot close it!
a6a73de3 190 InputStream decoded = new Base64InputStream(in, false);
08f80ac5 191 return new Image(decoded);
e570f7eb
NR
192 } catch (IOException e) {
193 throw new UnknownFormatConversionException(e.getMessage());
194 }
195 }
196 });
db31c358 197 }
8c8da42a 198
aad14586
NR
199 /**
200 * Create an empty object of the given type.
201 *
202 * @param type
203 * the object type (its class name)
204 *
205 * @return the new object
206 *
8c8da42a
NR
207 * @throws ClassNotFoundException
208 * if the class cannot be found
209 * @throws NoSuchMethodException
210 * if the given class is not compatible with this code
aad14586 211 */
8c8da42a
NR
212 public static Object createObject(String type)
213 throws ClassNotFoundException, NoSuchMethodException {
aad14586 214
72648e75 215 String desc = null;
aad14586
NR
216 try {
217 Class<?> clazz = getClass(type);
aad14586 218 String className = clazz.getName();
72648e75
NR
219 List<Object> args = new ArrayList<Object>();
220 List<Class<?>> classes = new ArrayList<Class<?>>();
aad14586
NR
221 Constructor<?> ctor = null;
222 if (className.contains("$")) {
72648e75
NR
223 for (String parentName = className.substring(0,
224 className.lastIndexOf('$'));; parentName = parentName
949445ee 225 .substring(0, parentName.lastIndexOf('$'))) {
72648e75
NR
226 Object parent = createObject(parentName);
227 args.add(parent);
228 classes.add(parent.getClass());
949445ee 229
72648e75
NR
230 if (!parentName.contains("$")) {
231 break;
232 }
233 }
234
235 // Better error description in case there is no empty
236 // constructor:
237 desc = "";
238 String end = "";
239 for (Class<?> parent = clazz; parent != null
240 && !parent.equals(Object.class); parent = parent
949445ee 241 .getSuperclass()) {
72648e75
NR
242 if (!desc.isEmpty()) {
243 desc += " [:";
244 end += "]";
245 }
246 desc += parent;
247 }
248 desc += end;
249 //
250
cd26ee07
NR
251 try {
252 ctor = clazz.getDeclaredConstructor(classes
253 .toArray(new Class[] {}));
254 } catch (NoSuchMethodException nsme) {
08f80ac5 255 // TODO: it seems we do not always need a parameter for each
cd26ee07
NR
256 // level, so we currently try "ALL" levels or "FIRST" level
257 // only -> we should check the actual rule and use it
258 ctor = clazz.getDeclaredConstructor(classes.get(0));
259 Object firstParent = args.get(0);
260 args.clear();
261 args.add(firstParent);
262 }
72648e75 263 desc = null;
aad14586 264 } else {
aad14586
NR
265 ctor = clazz.getDeclaredConstructor();
266 }
267
268 ctor.setAccessible(true);
72648e75 269 return ctor.newInstance(args.toArray());
8c8da42a
NR
270 } catch (ClassNotFoundException e) {
271 throw e;
aad14586 272 } catch (NoSuchMethodException e) {
72648e75 273 if (desc != null) {
949445ee
NR
274 throw new NoSuchMethodException("Empty constructor not found: "
275 + desc);
72648e75 276 }
8c8da42a
NR
277 throw e;
278 } catch (Exception e) {
279 throw new NoSuchMethodException("Cannot instantiate: " + type);
aad14586 280 }
aad14586 281 }
db31c358 282
8c8da42a
NR
283 /**
284 * Insert a custom serialiser that will take precedence over the default one
285 * or the target class.
286 *
287 * @param serializer
288 * the custom serialiser
289 */
db31c358
NR
290 static public void addCustomSerializer(CustomSerializer serializer) {
291 customTypes.put(serializer.getType(), serializer);
292 }
293
8c8da42a 294 /**
08f80ac5 295 * Serialise the given object into this {@link OutputStream}.
8c8da42a
NR
296 * <p>
297 * <b>Important: </b>If the operation fails (with a
298 * {@link NotSerializableException}), the {@link StringBuilder} will be
299 * corrupted (will contain bad, most probably not importable data).
300 *
08f80ac5
NR
301 * @param out
302 * the output {@link OutputStream} to serialise to
8c8da42a
NR
303 * @param o
304 * the object to serialise
305 * @param map
306 * the map of already serialised objects (if the given object or
307 * one of its descendant is already present in it, only an ID
308 * will be serialised)
309 *
310 * @throws NotSerializableException
311 * if the object cannot be serialised (in this case, the
312 * {@link StringBuilder} can contain bad, most probably not
313 * importable data)
08f80ac5
NR
314 * @throws IOException
315 * in case of I/O errors
8c8da42a 316 */
08f80ac5
NR
317 static void append(OutputStream out, Object o, Map<Integer, Object> map)
318 throws NotSerializableException, IOException {
db31c358
NR
319
320 Field[] fields = new Field[] {};
321 String type = "";
322 String id = "NULL";
323
324 if (o != null) {
325 int hash = System.identityHashCode(o);
326 fields = o.getClass().getDeclaredFields();
327 type = o.getClass().getCanonicalName();
328 if (type == null) {
72648e75
NR
329 // Anonymous inner classes support
330 type = o.getClass().getName();
db31c358
NR
331 }
332 id = Integer.toString(hash);
333 if (map.containsKey(hash)) {
334 fields = new Field[] {};
335 } else {
336 map.put(hash, o);
337 }
338 }
339
08f80ac5
NR
340 write(out, "{\nREF ");
341 write(out, type);
342 write(out, "@");
343 write(out, id);
344 write(out, ":");
345
346 if (!encode(out, o)) { // check if direct value
ce0974c4
NR
347 try {
348 for (Field field : fields) {
349 field.setAccessible(true);
db31c358 350
e570f7eb
NR
351 if (field.getName().startsWith("this$")
352 || field.isSynthetic()
353 || (field.getModifiers() & Modifier.STATIC) == Modifier.STATIC) {
ce0974c4 354 // Do not keep this links of nested classes
e570f7eb
NR
355 // Do not keep synthetic fields
356 // Do not keep final fields
ce0974c4
NR
357 continue;
358 }
db31c358 359
d2219aa0 360 write(out, "\n^");
08f80ac5
NR
361 write(out, field.getName());
362 write(out, ":");
db31c358 363
08f80ac5 364 Object value = field.get(o);
db31c358 365
08f80ac5
NR
366 if (!encode(out, value)) {
367 write(out, "\n");
368 append(out, value, map);
ce0974c4 369 }
db31c358 370 }
ce0974c4
NR
371 } catch (IllegalArgumentException e) {
372 e.printStackTrace(); // should not happen (see
373 // setAccessible)
374 } catch (IllegalAccessException e) {
375 e.printStackTrace(); // should not happen (see
376 // setAccessible)
db31c358 377 }
08f80ac5
NR
378
379 write(out, "\n}");
db31c358 380 }
db31c358
NR
381 }
382
5bc55b51 383 /**
08f80ac5
NR
384 * Encode the object into the given {@link OutputStream} if possible and if
385 * supported.
949445ee
NR
386 * <p>
387 * A supported object in this context means an object we can directly
388 * encode, like an Integer or a String. Custom objects and arrays are also
389 * considered supported, but <b>compound objects are not supported here</b>.
390 * <p>
391 * For compound objects, you should use {@link Exporter}.
5bc55b51 392 *
08f80ac5
NR
393 * @param out
394 * the {@link OutputStream} to append to
5bc55b51
NR
395 * @param value
396 * the object to encode (can be NULL, which will be encoded)
397 *
08f80ac5
NR
398 * @return TRUE if success, FALSE if not (the content of the
399 * {@link OutputStream} won't be changed in case of failure)
400 *
401 * @throws IOException
402 * in case of I/O error
5bc55b51 403 */
08f80ac5 404 static boolean encode(OutputStream out, Object value) throws IOException {
db31c358 405 if (value == null) {
08f80ac5 406 write(out, "NULL");
72648e75
NR
407 } else if (value.getClass().getSimpleName().endsWith("[]")) {
408 // Simple name does support [] suffix and do not return NULL for
409 // inner anonymous classes
08f80ac5 410 customTypes.get("[]").encode(out, value);
db31c358 411 } else if (customTypes.containsKey(value.getClass().getCanonicalName())) {
08f80ac5
NR
412 customTypes.get(value.getClass().getCanonicalName())//
413 .encode(out, value);
db31c358 414 } else if (value instanceof String) {
08f80ac5 415 encodeString(out, (String) value);
db31c358 416 } else if (value instanceof Boolean) {
08f80ac5 417 write(out, value);
db31c358 418 } else if (value instanceof Byte) {
08f80ac5 419 write(out, "b");
d2219aa0 420 write(out, value);
db31c358 421 } else if (value instanceof Character) {
08f80ac5 422 write(out, "c");
d2219aa0 423 encodeString(out, "" + value);
db31c358 424 } else if (value instanceof Short) {
08f80ac5 425 write(out, "s");
d2219aa0 426 write(out, value);
db31c358 427 } else if (value instanceof Integer) {
d2219aa0 428 write(out, "i");
08f80ac5 429 write(out, value);
db31c358 430 } else if (value instanceof Long) {
d2219aa0 431 write(out, "l");
08f80ac5 432 write(out, value);
db31c358 433 } else if (value instanceof Float) {
d2219aa0 434 write(out, "f");
08f80ac5 435 write(out, value);
db31c358 436 } else if (value instanceof Double) {
08f80ac5 437 write(out, "d");
d2219aa0 438 write(out, value);
e570f7eb 439 } else if (value instanceof Enum) {
d2219aa0 440 write(out, "E:");
e570f7eb 441 String type = value.getClass().getCanonicalName();
08f80ac5
NR
442 write(out, type);
443 write(out, ".");
444 write(out, ((Enum<?>) value).name());
445 write(out, ";");
db31c358
NR
446 } else {
447 return false;
448 }
449
450 return true;
451 }
452
d2219aa0
NR
453 static boolean isDirectValue(BufferedInputStream encodedValue)
454 throws IOException {
455 if (CustomSerializer.isCustom(encodedValue)) {
456 return false;
457 }
458
459 for (String fullValue : new String[] { "NULL", "null", "true", "false" }) {
460 if (encodedValue.is(fullValue)) {
461 return true;
462 }
463 }
464
d2219aa0
NR
465 for (String prefix : new String[] { "c\"", "\"", "b", "s", "i", "l",
466 "f", "d", "E:" }) {
467 if (encodedValue.startsWith(prefix)) {
468 return true;
469 }
470 }
471
472 return false;
473 }
474
5bc55b51 475 /**
949445ee
NR
476 * Decode the data into an equivalent supported source object.
477 * <p>
478 * A supported object in this context means an object we can directly
d2219aa0
NR
479 * encode, like an Integer or a String (see
480 * {@link SerialUtils#decode(String)}.
481 * <p>
482 * Custom objects and arrays are also considered supported here, but
483 * <b>compound objects are not</b>.
484 * <p>
485 * For compound objects, you should use {@link Importer}.
486 *
487 * @param encodedValue
488 * the encoded data, cannot be NULL
489 *
490 * @return the object (can be NULL for NULL encoded values)
491 *
492 * @throws IOException
493 * if the content cannot be converted
494 */
495 static Object decode(BufferedInputStream encodedValue) throws IOException {
496 if (CustomSerializer.isCustom(encodedValue)) {
497 // custom^TYPE^ENCODED_VALUE
498 NextableInputStream content = new NextableInputStream(encodedValue,
499 new NextableInputStreamStep('^'));
500 try {
501 content.next();
502 @SuppressWarnings("unused")
503 String custom = IOUtils.readSmallStream(content);
504 content.next();
505 String type = IOUtils.readSmallStream(content);
506 content.nextAll();
507 if (customTypes.containsKey(type)) {
508 return customTypes.get(type).decode(content);
509 }
510 content.end();
511 throw new IOException("Unknown custom type: " + type);
512 } finally {
513 content.close(false);
514 // TODO: check what happens with thrown Exception in finally
515 encodedValue.end();
516 }
517 }
518
519 String encodedString = IOUtils.readSmallStream(encodedValue);
520 return decode(encodedString);
521 }
522
523 /**
524 * Decode the data into an equivalent supported source object.
525 * <p>
526 * A supported object in this context means an object we can directly
527 * encode, like an Integer or a String.
528 * <p>
529 * For custom objects and arrays, you should use
530 * {@link SerialUtils#decode(InputStream)} or directly {@link Importer}.
949445ee
NR
531 * <p>
532 * For compound objects, you should use {@link Importer}.
5bc55b51
NR
533 *
534 * @param encodedValue
535 * the encoded data, cannot be NULL
536 *
537 * @return the object (can be NULL for NULL encoded values)
538 *
452f38c8 539 * @throws IOException
5bc55b51
NR
540 * if the content cannot be converted
541 */
452f38c8 542 static Object decode(String encodedValue) throws IOException {
5bc55b51 543 try {
452f38c8
NR
544 String cut = "";
545 if (encodedValue.length() > 1) {
d2219aa0 546 cut = encodedValue.substring(1);
452f38c8
NR
547 }
548
d2219aa0 549 if (encodedValue.equals("NULL") || encodedValue.equals("null")) {
5bc55b51 550 return null;
d2219aa0 551 } else if (encodedValue.startsWith("\"")) {
5bc55b51
NR
552 return decodeString(encodedValue);
553 } else if (encodedValue.equals("true")) {
554 return true;
555 } else if (encodedValue.equals("false")) {
556 return false;
d2219aa0 557 } else if (encodedValue.startsWith("b")) {
5bc55b51 558 return Byte.parseByte(cut);
d2219aa0 559 } else if (encodedValue.startsWith("c")) {
5bc55b51 560 return decodeString(cut).charAt(0);
d2219aa0 561 } else if (encodedValue.startsWith("s")) {
5bc55b51 562 return Short.parseShort(cut);
d2219aa0 563 } else if (encodedValue.startsWith("l")) {
5bc55b51 564 return Long.parseLong(cut);
d2219aa0 565 } else if (encodedValue.startsWith("f")) {
5bc55b51 566 return Float.parseFloat(cut);
d2219aa0 567 } else if (encodedValue.startsWith("d")) {
5bc55b51 568 return Double.parseDouble(cut);
d2219aa0
NR
569 } else if (encodedValue.startsWith("i")) {
570 return Integer.parseInt(cut);
571 } else if (encodedValue.startsWith("E:")) {
572 cut = cut.substring(1);
573 return decodeEnum(cut);
5bc55b51 574 } else {
d2219aa0 575 throw new IOException("Unrecognized value: " + encodedValue);
db31c358 576 }
5bc55b51 577 } catch (Exception e) {
452f38c8
NR
578 if (e instanceof IOException) {
579 throw (IOException) e;
5bc55b51 580 }
08f80ac5
NR
581 throw new IOException(e.getMessage(), e);
582 }
583 }
584
585 /**
586 * Write the given {@link String} into the given {@link OutputStream} in
587 * UTF-8.
588 *
589 * @param out
590 * the {@link OutputStream}
591 * @param data
592 * the data to write, cannot be NULL
593 *
594 * @throws IOException
595 * in case of I/O error
596 */
597 static void write(OutputStream out, Object data) throws IOException {
f8147a0e 598 out.write(StringUtils.getBytes(data.toString()));
db31c358 599 }
8c8da42a
NR
600
601 /**
602 * Return the corresponding class or throw an {@link Exception} if it
603 * cannot.
604 *
605 * @param type
606 * the class name to look for
607 *
608 * @return the class (will never be NULL)
609 *
610 * @throws ClassNotFoundException
611 * if the class cannot be found
612 * @throws NoSuchMethodException
613 * if the class cannot be created (usually because it or its
614 * enclosing class doesn't have an empty constructor)
615 */
616 static private Class<?> getClass(String type)
617 throws ClassNotFoundException, NoSuchMethodException {
aad14586
NR
618 Class<?> clazz = null;
619 try {
620 clazz = Class.forName(type);
621 } catch (ClassNotFoundException e) {
622 int pos = type.length();
623 pos = type.lastIndexOf(".", pos);
624 if (pos >= 0) {
625 String parentType = type.substring(0, pos);
626 String nestedType = type.substring(pos + 1);
627 Class<?> javaParent = null;
628 try {
629 javaParent = getClass(parentType);
630 parentType = javaParent.getName();
631 clazz = Class.forName(parentType + "$" + nestedType);
632 } catch (Exception ee) {
633 }
db31c358 634
aad14586
NR
635 if (javaParent == null) {
636 throw new NoSuchMethodException(
637 "Class not found: "
638 + type
639 + " (the enclosing class cannot be created: maybe it doesn't have an empty constructor?)");
640 }
641 }
642 }
643
8c8da42a
NR
644 if (clazz == null) {
645 throw new ClassNotFoundException("Class not found: " + type);
646 }
647
aad14586
NR
648 return clazz;
649 }
8c8da42a 650
e570f7eb 651 @SuppressWarnings({ "unchecked", "rawtypes" })
08f80ac5 652 static private Enum<?> decodeEnum(String escaped) {
e570f7eb
NR
653 // escaped: be.xxx.EnumType.VALUE;
654 int pos = escaped.lastIndexOf(".");
655 String type = escaped.substring(0, pos);
656 String name = escaped.substring(pos + 1, escaped.length() - 1);
657
658 try {
659 return Enum.valueOf((Class<Enum>) getClass(type), name);
660 } catch (Exception e) {
e570f7eb
NR
661 throw new UnknownFormatConversionException("Unknown enum: <" + type
662 + "> " + name);
663 }
664 }
665
db31c358 666 // aa bb -> "aa\tbb"
08f80ac5
NR
667 static void encodeString(OutputStream out, String raw) throws IOException {
668 // TODO: not. efficient.
669 out.write('\"');
db31c358 670 for (char car : raw.toCharArray()) {
08f80ac5
NR
671 encodeString(out, car);
672 }
673 out.write('\"');
674 }
675
24604392 676 // for encoding string, NOT to encode a char by itself!
08f80ac5
NR
677 static void encodeString(OutputStream out, char raw) throws IOException {
678 switch (raw) {
679 case '\\':
680 out.write('\\');
681 out.write('\\');
682 break;
683 case '\r':
684 out.write('\\');
685 out.write('r');
686 break;
687 case '\n':
688 out.write('\\');
689 out.write('n');
690 break;
691 case '"':
692 out.write('\\');
693 out.write('\"');
694 break;
695 default:
696 out.write(raw);
697 break;
698 }
db31c358
NR
699 }
700
701 // "aa\tbb" -> aa bb
08f80ac5 702 static String decodeString(String escaped) {
db31c358
NR
703 StringBuilder builder = new StringBuilder();
704
705 boolean escaping = false;
706 for (char car : escaped.toCharArray()) {
707 if (!escaping) {
708 if (car == '\\') {
709 escaping = true;
710 } else {
711 builder.append(car);
712 }
713 } else {
714 switch (car) {
715 case '\\':
716 builder.append('\\');
717 break;
718 case 'r':
719 builder.append('\r');
720 break;
721 case 'n':
722 builder.append('\n');
723 break;
724 case '"':
725 builder.append('"');
726 break;
727 }
728 escaping = false;
729 }
730 }
731
0988831f 732 return builder.substring(1, builder.length() - 1);
db31c358
NR
733 }
734}