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