IoUtils.java revision 433e3fac172d0c4449051b0c61c0c63b298a0903
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package libcore.io;
18
19import java.io.Closeable;
20import java.io.EOFException;
21import java.io.File;
22import java.io.FileDescriptor;
23import java.io.FileNotFoundException;
24import java.io.IOException;
25import java.io.InputStream;
26import java.io.RandomAccessFile;
27import java.net.BindException;
28import java.net.ConnectException;
29import java.net.InetAddress;
30import java.net.Inet4Address;
31import java.net.Inet6Address;
32import java.io.Reader;
33import java.io.StringWriter;
34import java.net.BindException;
35import java.net.Inet4Address;
36import java.net.Inet6Address;
37import java.net.InetAddress;
38import java.net.InetSocketAddress;
39import java.net.NetworkInterface;
40import java.net.Socket;
41import java.net.SocketAddress;
42import java.net.SocketException;
43import java.net.SocketOptions;
44import java.net.SocketTimeoutException;
45import java.net.UnknownHostException;
46import java.nio.charset.Charsets;
47import java.util.Arrays;
48import libcore.util.MutableInt;
49import static libcore.io.OsConstants.*;
50
51// TODO: kill this!
52import org.apache.harmony.luni.platform.Platform;
53
54public final class IoUtils {
55    private IoUtils() {
56    }
57
58    /**
59     * Implements java.io/java.net "available" semantics.
60     */
61    public static int available(FileDescriptor fd) throws IOException {
62        try {
63            MutableInt available = new MutableInt(0);
64            int rc = Libcore.os.ioctlInt(fd, FIONREAD, available);
65            if (available.value < 0) {
66                // If the fd refers to a regular file, the result is the difference between
67                // the file size and the file position. This may be negative if the position
68                // is past the end of the file. If the fd refers to a special file masquerading
69                // as a regular file, the result may be negative because the special file
70                // may appear to have zero size and yet a previous read call may have
71                // read some amount of data and caused the file position to be advanced.
72                available.value = 0;
73            }
74            return available.value;
75        } catch (ErrnoException errnoException) {
76            if (errnoException.errno == ENOTTY) {
77                // The fd is unwilling to opine about its read buffer.
78                return 0;
79            }
80            throw errnoException.rethrowAsIOException();
81        }
82    }
83
84    /**
85     * java.io only throws FileNotFoundException when opening files, regardless of what actually
86     * went wrong. Additionally, java.io is more restrictive than POSIX when it comes to opening
87     * directories: POSIX says read-only is okay, but java.io doesn't even allow that. We also
88     * have an Android-specific hack to alter the default permissions.
89     */
90    public static FileDescriptor open(String path, int flags) throws FileNotFoundException {
91        FileDescriptor fd = null;
92        try {
93            // On Android, we don't want default permissions to allow global access.
94            int mode = ((flags & O_ACCMODE) == O_RDONLY) ? 0 : 0600;
95            fd = Libcore.os.open(path, flags, mode);
96            if (fd.valid()) {
97                // Posix open(2) fails with EISDIR only if you ask for write permission.
98                // Java disallows reading directories too.
99                boolean isDirectory = false;
100                if (S_ISDIR(Libcore.os.fstat(fd).st_mode)) {
101                    throw new ErrnoException("open", EISDIR);
102                }
103            }
104            return fd;
105        } catch (ErrnoException errnoException) {
106            try {
107                if (fd != null) {
108                    close(fd);
109                }
110            } catch (IOException ignored) {
111            }
112            FileNotFoundException ex = new FileNotFoundException(path + ": " + errnoException.getMessage());
113            ex.initCause(errnoException);
114            throw ex;
115        }
116    }
117
118    /**
119     * java.io thinks that a read at EOF is an error and should return -1, contrary to traditional
120     * Unix practice where you'd read until you got 0 bytes (and any future read would return -1).
121     */
122    public static int read(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount) throws IOException {
123        Arrays.checkOffsetAndCount(bytes.length, byteOffset, byteCount);
124        if (byteCount == 0) {
125            return 0;
126        }
127        try {
128            int readCount = Libcore.os.read(fd, bytes, byteOffset, byteCount);
129            if (readCount == 0) {
130                return -1;
131            }
132            return readCount;
133        } catch (ErrnoException errnoException) {
134            if (errnoException.errno == EAGAIN) {
135                // We return 0 rather than throw if we try to read from an empty non-blocking pipe.
136                return 0;
137            }
138            throw errnoException.rethrowAsIOException();
139        }
140    }
141
142    /**
143     * java.io always writes every byte it's asked to, or fails with an error. (That is, unlike
144     * Unix it never just writes as many bytes as happens to be convenient.)
145     */
146    public static void write(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount) throws IOException {
147        Arrays.checkOffsetAndCount(bytes.length, byteOffset, byteCount);
148        if (byteCount == 0) {
149            return;
150        }
151        try {
152            while (byteCount > 0) {
153                int bytesWritten = Libcore.os.write(fd, bytes, byteOffset, byteCount);
154                byteCount -= bytesWritten;
155                byteOffset += bytesWritten;
156            }
157        } catch (ErrnoException errnoException) {
158            throw errnoException.rethrowAsIOException();
159        }
160    }
161
162    public static void bind(FileDescriptor fd, InetAddress address, int port) throws SocketException {
163        if (address instanceof Inet6Address && ((Inet6Address) address).getScopeId() == 0) {
164            // Linux won't let you bind a link-local address without a scope id. Find one.
165            NetworkInterface nif = NetworkInterface.getByInetAddress(address);
166            if (nif == null) {
167                throw new SocketException("Can't bind to a link-local address without a scope id: " + address);
168            }
169            try {
170                address = Inet6Address.getByAddress(address.getHostName(), address.getAddress(), nif.getIndex());
171            } catch (UnknownHostException ex) {
172                throw new AssertionError(ex); // Can't happen.
173            }
174        }
175        try {
176            Libcore.os.bind(fd, address, port);
177        } catch (ErrnoException errnoException) {
178            throw new BindException(errnoException.getMessage(), errnoException);
179        }
180    }
181
182    /**
183     * Calls close(2) on 'fd'. Also resets the internal int to -1. Does nothing if 'fd' is null
184     * or invalid.
185     */
186    public static void close(FileDescriptor fd) throws IOException {
187        try {
188            if (fd != null && fd.valid()) {
189                Libcore.os.close(fd);
190            }
191        } catch (ErrnoException errnoException) {
192            throw errnoException.rethrowAsIOException();
193        }
194    }
195
196    /**
197     * Closes 'closeable', ignoring any exceptions. Does nothing if 'closeable' is null.
198     */
199    public static void closeQuietly(Closeable closeable) {
200        if (closeable != null) {
201            try {
202                closeable.close();
203            } catch (IOException ignored) {
204            }
205        }
206    }
207
208    /**
209     * Closes 'fd', ignoring any exceptions. Does nothing if 'fd' is null or invalid.
210     */
211    public static void closeQuietly(FileDescriptor fd) {
212        try {
213            IoUtils.close(fd);
214        } catch (IOException ignored) {
215        }
216    }
217
218    /**
219     * Closes 'socket', ignoring any exceptions. Does nothing if 'socket' is null.
220     */
221    public static void closeQuietly(Socket socket) {
222        if (socket != null) {
223            try {
224                socket.close();
225            } catch (Exception ignored) {
226            }
227        }
228    }
229
230    /**
231     * Connects socket 'fd' to 'inetAddress' on 'port', with no timeout. The lack of a timeout
232     * means this method won't throw SocketTimeoutException.
233     */
234    public static boolean connect(FileDescriptor fd, InetAddress inetAddress, int port) throws SocketException {
235        try {
236            return IoUtils.connect(fd, inetAddress, port, 0);
237        } catch (SocketTimeoutException ex) {
238            throw new AssertionError(ex); // Can't happen for a connect without a timeout.
239        }
240    }
241
242    /**
243     * Connects socket 'fd' to 'inetAddress' on 'port', with a the given 'timeoutMs'.
244     * Use timeoutMs == 0 for a blocking connect with no timeout.
245     */
246    public static boolean connect(FileDescriptor fd, InetAddress inetAddress, int port, int timeoutMs) throws SocketException, SocketTimeoutException {
247        try {
248            return connectErrno(fd, inetAddress, port, timeoutMs);
249        } catch (ErrnoException errnoException) {
250            throw new ConnectException(connectDetail(inetAddress, port, timeoutMs) + ": " + errnoException.getMessage(), errnoException);
251        } catch (SocketException ex) {
252            throw ex; // We don't want to doubly wrap these.
253        } catch (SocketTimeoutException ex) {
254            throw ex; // We don't want to doubly wrap these.
255        } catch (IOException ex) {
256            throw new SocketException(ex);
257        }
258    }
259
260    private static boolean connectErrno(FileDescriptor fd, InetAddress inetAddress, int port, int timeoutMs) throws IOException {
261        // With no timeout, just call connect(2) directly.
262        if (timeoutMs == 0) {
263            Libcore.os.connect(fd, inetAddress, port);
264            return true;
265        }
266
267        // With a timeout, we set the socket to non-blocking, connect(2), and then loop
268        // using select(2) to decide whether we're connected, whether we should keep waiting,
269        // or whether we've seen a permanent failure and should give up.
270        long finishTimeMs = System.currentTimeMillis() + timeoutMs;
271        IoUtils.setBlocking(fd, false);
272        try {
273            try {
274                Libcore.os.connect(fd, inetAddress, port);
275                return true; // We connected immediately.
276            } catch (ErrnoException errnoException) {
277                if (errnoException.errno != EINPROGRESS) {
278                    throw errnoException;
279                }
280                // EINPROGRESS means we should keep trying...
281            }
282            int remainingTimeoutMs;
283            do {
284                remainingTimeoutMs = (int) (finishTimeMs - System.currentTimeMillis());
285                if (remainingTimeoutMs <= 0) {
286                    throw new SocketTimeoutException(connectDetail(inetAddress, port, timeoutMs));
287                }
288            } while (!Platform.NETWORK.isConnected(fd, remainingTimeoutMs));
289            return true; // Or we'd have thrown.
290        } finally {
291            IoUtils.setBlocking(fd, true);
292        }
293    }
294
295    private static String connectDetail(InetAddress inetAddress, int port, int timeoutMs) {
296        String detail = "failed to connect to " + inetAddress + " (port " + port + ")";
297        if (timeoutMs > 0) {
298            detail += " after " + timeoutMs + "ms";
299        }
300        return detail;
301    }
302
303    /**
304     * Sets 'fd' to be blocking or non-blocking, according to the state of 'blocking'.
305     */
306    public static void setBlocking(FileDescriptor fd, boolean blocking) throws IOException {
307        try {
308            int flags = Libcore.os.fcntlVoid(fd, F_GETFL);
309            if (!blocking) {
310                flags |= O_NONBLOCK;
311            } else {
312                flags &= ~O_NONBLOCK;
313            }
314            Libcore.os.fcntlLong(fd, F_SETFL, flags);
315        } catch (ErrnoException errnoException) {
316            throw errnoException.rethrowAsIOException();
317        }
318    }
319
320    public static FileDescriptor socket(boolean stream) throws SocketException {
321        FileDescriptor fd;
322        try {
323            fd = Libcore.os.socket(AF_INET6, stream ? SOCK_STREAM : SOCK_DGRAM, 0);
324
325            // The RFC (http://www.ietf.org/rfc/rfc3493.txt) says that IPV6_MULTICAST_HOPS defaults
326            // to 1. The Linux kernel (at least up to 2.6.38) accidentally defaults to 64 (which
327            // would be correct for the *unicast* hop limit).
328            // See http://www.spinics.net/lists/netdev/msg129022.html, though no patch appears to
329            // have been applied as a result of that discussion. If that bug is ever fixed, we can
330            // remove this code. Until then, we manually set the hop limit on IPv6 datagram sockets.
331            // (IPv4 is already correct.)
332            if (!stream) {
333                Libcore.os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, 1);
334            }
335
336            return fd;
337        } catch (ErrnoException errnoException) {
338            throw errnoException.rethrowAsSocketException();
339        }
340    }
341
342    // Socket options used by java.net but not exposed in SocketOptions.
343    public static final int JAVA_MCAST_JOIN_GROUP = 19;
344    public static final int JAVA_MCAST_LEAVE_GROUP = 20;
345    public static final int JAVA_IP_MULTICAST_TTL = 17;
346
347    /**
348     * java.net has its own socket options similar to the underlying Unix ones. We paper over the
349     * differences here.
350     */
351    public static Object getSocketOption(FileDescriptor fd, int option) throws SocketException {
352        try {
353            return getSocketOptionErrno(fd, option);
354        } catch (ErrnoException errnoException) {
355            throw errnoException.rethrowAsSocketException();
356        }
357    }
358
359    private static Object getSocketOptionErrno(FileDescriptor fd, int option) throws SocketException {
360        switch (option) {
361        case SocketOptions.IP_MULTICAST_IF:
362            // This is IPv4-only.
363            return Libcore.os.getsockoptInAddr(fd, IPPROTO_IP, IP_MULTICAST_IF);
364        case SocketOptions.IP_MULTICAST_IF2:
365            // This is IPv6-only.
366            return Libcore.os.getsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF);
367        case SocketOptions.IP_MULTICAST_LOOP:
368            // Since setting this from java.net always sets IPv4 and IPv6 to the same value,
369            // it doesn't matter which we return.
370            return booleanFromInt(Libcore.os.getsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP));
371        case IoUtils.JAVA_IP_MULTICAST_TTL:
372            // Since setting this from java.net always sets IPv4 and IPv6 to the same value,
373            // it doesn't matter which we return.
374            return Libcore.os.getsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS);
375        case SocketOptions.IP_TOS:
376            // Since setting this from java.net always sets IPv4 and IPv6 to the same value,
377            // it doesn't matter which we return.
378            return Libcore.os.getsockoptInt(fd, IPPROTO_IPV6, IPV6_TCLASS);
379        case SocketOptions.SO_BROADCAST:
380            return booleanFromInt(Libcore.os.getsockoptInt(fd, SOL_SOCKET, SO_BROADCAST));
381        case SocketOptions.SO_KEEPALIVE:
382            return booleanFromInt(Libcore.os.getsockoptInt(fd, SOL_SOCKET, SO_KEEPALIVE));
383        case SocketOptions.SO_LINGER:
384            StructLinger linger = Libcore.os.getsockoptLinger(fd, SOL_SOCKET, SO_LINGER);
385            if (!linger.isOn()) {
386                return false;
387            }
388            return linger.l_linger;
389        case SocketOptions.SO_OOBINLINE:
390            return booleanFromInt(Libcore.os.getsockoptInt(fd, SOL_SOCKET, SO_OOBINLINE));
391        case SocketOptions.SO_RCVBUF:
392            return Libcore.os.getsockoptInt(fd, SOL_SOCKET, SO_SNDBUF);
393        case SocketOptions.SO_REUSEADDR:
394            return booleanFromInt(Libcore.os.getsockoptInt(fd, SOL_SOCKET, SO_REUSEADDR));
395        case SocketOptions.SO_SNDBUF:
396            return Libcore.os.getsockoptInt(fd, SOL_SOCKET, SO_SNDBUF);
397        case SocketOptions.SO_TIMEOUT:
398            return (int) Libcore.os.getsockoptTimeval(fd, SOL_SOCKET, SO_RCVTIMEO).toMillis();
399        case SocketOptions.TCP_NODELAY:
400            return booleanFromInt(Libcore.os.getsockoptInt(fd, IPPROTO_TCP, TCP_NODELAY));
401        default:
402            throw new SocketException("Unknown socket option: " + option);
403        }
404    }
405
406    private static boolean booleanFromInt(int i) {
407        return (i != 0);
408    }
409
410    private static int booleanToInt(boolean b) {
411        return b ? 1 : 0;
412    }
413
414    /**
415     * java.net has its own socket options similar to the underlying Unix ones. We paper over the
416     * differences here.
417     */
418    public static void setSocketOption(FileDescriptor fd, int option, Object value) throws SocketException {
419        try {
420            setSocketOptionErrno(fd, option, value);
421        } catch (ErrnoException errnoException) {
422            throw errnoException.rethrowAsSocketException();
423        }
424    }
425
426    private static void setSocketOptionErrno(FileDescriptor fd, int option, Object value) throws SocketException {
427        switch (option) {
428        case SocketOptions.IP_MULTICAST_IF:
429            throw new UnsupportedOperationException("Use IP_MULTICAST_IF2 on Android");
430        case SocketOptions.IP_MULTICAST_IF2:
431            // Although IPv6 was cleaned up to use int, IPv4 uses an ip_mreqn containing an int.
432            Libcore.os.setsockoptIpMreqn(fd, IPPROTO_IP, IP_MULTICAST_IF, (Integer) value);
433            Libcore.os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, (Integer) value);
434            return;
435        case SocketOptions.IP_MULTICAST_LOOP:
436            // Although IPv6 was cleaned up to use int, IPv4 multicast loopback uses a byte.
437            Libcore.os.setsockoptByte(fd, IPPROTO_IP, IP_MULTICAST_LOOP, booleanToInt((Boolean) value));
438            Libcore.os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, booleanToInt((Boolean) value));
439            return;
440        case IoUtils.JAVA_IP_MULTICAST_TTL:
441            // Although IPv6 was cleaned up to use int, and IPv4 non-multicast TTL uses int,
442            // IPv4 multicast TTL uses a byte.
443            Libcore.os.setsockoptByte(fd, IPPROTO_IP, IP_MULTICAST_TTL, (Integer) value);
444            Libcore.os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, (Integer) value);
445            return;
446        case SocketOptions.IP_TOS:
447            Libcore.os.setsockoptInt(fd, IPPROTO_IP, IP_TOS, (Integer) value);
448            Libcore.os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_TCLASS, (Integer) value);
449            return;
450        case SocketOptions.SO_BROADCAST:
451            Libcore.os.setsockoptInt(fd, SOL_SOCKET, SO_BROADCAST, booleanToInt((Boolean) value));
452            return;
453        case SocketOptions.SO_KEEPALIVE:
454            Libcore.os.setsockoptInt(fd, SOL_SOCKET, SO_KEEPALIVE, booleanToInt((Boolean) value));
455            return;
456        case SocketOptions.SO_LINGER:
457            boolean on = false;
458            int seconds = 0;
459            if (value instanceof Integer) {
460                on = true;
461                seconds = Math.min((Integer) value, 65535);
462            }
463            StructLinger linger = new StructLinger(booleanToInt(on), seconds);
464            Libcore.os.setsockoptLinger(fd, SOL_SOCKET, SO_LINGER, linger);
465            return;
466        case SocketOptions.SO_OOBINLINE:
467            Libcore.os.setsockoptInt(fd, SOL_SOCKET, SO_OOBINLINE, booleanToInt((Boolean) value));
468            return;
469        case SocketOptions.SO_RCVBUF:
470            Libcore.os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, (Integer) value);
471            return;
472        case SocketOptions.SO_REUSEADDR:
473            Libcore.os.setsockoptInt(fd, SOL_SOCKET, SO_REUSEADDR, booleanToInt((Boolean) value));
474            return;
475        case SocketOptions.SO_SNDBUF:
476            Libcore.os.setsockoptInt(fd, SOL_SOCKET, SO_SNDBUF, (Integer) value);
477            return;
478        case SocketOptions.SO_TIMEOUT:
479            int millis = (Integer) value;
480            StructTimeval tv = StructTimeval.fromMillis(millis);
481            Libcore.os.setsockoptTimeval(fd, SOL_SOCKET, SO_RCVTIMEO, tv);
482            return;
483        case SocketOptions.TCP_NODELAY:
484            Libcore.os.setsockoptInt(fd, IPPROTO_TCP, TCP_NODELAY, booleanToInt((Boolean) value));
485            return;
486        case IoUtils.JAVA_MCAST_JOIN_GROUP:
487        case IoUtils.JAVA_MCAST_LEAVE_GROUP:
488            StructGroupReq groupReq = (StructGroupReq) value;
489            int level = (groupReq.gr_group instanceof Inet4Address) ? IPPROTO_IP : IPPROTO_IPV6;
490            int op = (option == JAVA_MCAST_JOIN_GROUP) ? MCAST_JOIN_GROUP : MCAST_LEAVE_GROUP;
491            Libcore.os.setsockoptGroupReq(fd, level, op, groupReq);
492            return;
493        default:
494            throw new SocketException("Unknown socket option: " + option);
495        }
496    }
497
498    public static InetAddress getSocketLocalAddress(FileDescriptor fd) {
499        SocketAddress sa = Libcore.os.getsockname(fd);
500        InetSocketAddress isa = (InetSocketAddress) sa;
501        return isa.getAddress();
502    }
503
504    public static int getSocketLocalPort(FileDescriptor fd) {
505        SocketAddress sa = Libcore.os.getsockname(fd);
506        InetSocketAddress isa = (InetSocketAddress) sa;
507        return isa.getPort();
508    }
509
510    /**
511     * Returns the contents of 'path' as a byte array.
512     */
513    public static byte[] readFileAsByteArray(String path) throws IOException {
514        return readFileAsBytes(path).toByteArray();
515    }
516
517    /**
518     * Returns the contents of 'path' as a string. The contents are assumed to be UTF-8.
519     */
520    public static String readFileAsString(String path) throws IOException {
521        return readFileAsBytes(path).toString(Charsets.UTF_8);
522    }
523
524    /**
525     * Returns the remainder of 'reader' as a string, closing it when done.
526     */
527    public static String readReaderAsString(Reader reader) throws IOException {
528        StringWriter writer = new StringWriter();
529        char[] buffer = new char[8192];
530        int count;
531        while ((count = reader.read(buffer)) != -1) {
532            writer.write(buffer, 0, count);
533        }
534        reader.close();
535        return writer.toString();
536    }
537
538    private static UnsafeByteSequence readFileAsBytes(String path) throws IOException {
539        RandomAccessFile f = null;
540        try {
541            f = new RandomAccessFile(path, "r");
542            UnsafeByteSequence bytes = new UnsafeByteSequence((int) f.length());
543            byte[] buffer = new byte[8192];
544            while (true) {
545                int byteCount = f.read(buffer);
546                if (byteCount == -1) {
547                    return bytes;
548                }
549                bytes.write(buffer, 0, byteCount);
550            }
551        } finally {
552            IoUtils.closeQuietly(f);
553        }
554    }
555
556    /**
557     * Recursively delete everything in {@code dir}.
558     */
559    public static void deleteContents(File dir) throws IOException {
560        for (File file : dir.listFiles()) {
561            if (file.isDirectory()) {
562                deleteContents(file);
563            }
564            if (!file.delete()) {
565                throw new IOException("failed to delete file: " + file);
566            }
567        }
568    }
569
570    /**
571     * Returns the ASCII characters up to but not including the next "\r\n", or
572     * "\n".
573     *
574     * @throws EOFException if the stream is exhausted before the next newline
575     *     character.
576     */
577    public static String readLine(InputStream in) throws IOException {
578        // TODO: support UTF-8 here instead
579
580        StringBuilder result = new StringBuilder(80);
581        while (true) {
582            int c = in.read();
583            if (c == -1) {
584                throw new EOFException();
585            } else if (c == '\n') {
586                break;
587            }
588
589            result.append((char) c);
590        }
591        int length = result.length();
592        if (length > 0 && result.charAt(length - 1) == '\r') {
593            result.setLength(length - 1);
594        }
595        return result.toString();
596    }
597}
598