001: /*
002: * @(#)X509Factory.java 1.4 06/10/10
003: *
004: * Copyright 1990-2006 Sun Microsystems, Inc. All Rights Reserved.
005: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License version
009: * 2 only, as published by the Free Software Foundation.
010: *
011: * This program is distributed in the hope that it will be useful, but
012: * WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * General Public License version 2 for more details (a copy is
015: * included at /legal/license.txt).
016: *
017: * You should have received a copy of the GNU General Public License
018: * version 2 along with this work; if not, write to the Free Software
019: * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA
021: *
022: * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
023: * Clara, CA 95054 or visit www.sun.com if you need additional
024: * information or have any questions.
025: *
026: */
027:
028: /*
029: * Note that there are two versions of X509Factory, this subsetted
030: * version for CDC/FP and another for the security optional package.
031: * Be sure you're editing the right one!
032: */
033:
034: package sun.security.provider;
035:
036: import java.io.*;
037: import java.util.Collection;
038: import java.util.*;
039: import java.security.cert.*;
040: import sun.security.x509.X509CertImpl;
041: import sun.security.x509.X509CRLImpl;
042: import sun.security.pkcs.PKCS7;
043: import sun.security.provider.certpath.X509CertPath; // CDC/FP X509CertificatePair subsetted out for space savings.
044: // import sun.security.provider.certpath.X509CertificatePair;
045: import sun.security.util.DerValue;
046: import sun.security.util.Cache;
047: import sun.misc.BASE64Decoder;
048:
049: /**
050: * This class defines a certificate factory for X.509 v3 certificates &
051: * certification paths, and X.509 v2 certificate revocation lists (CRLs).
052: *
053: * @author Jan Luehe
054: * @author Hemma Prafullchandra
055: * @author Sean Mullan
056: *
057: * @version 1.4, 10/10/06
058: *
059: * @see java.security.cert.CertificateFactorySpi
060: * @see java.security.cert.Certificate
061: * @see java.security.cert.CertPath
062: * @see java.security.cert.CRL
063: * @see java.security.cert.X509Certificate
064: * @see java.security.cert.X509CRL
065: * @see sun.security.x509.X509CertImpl
066: * @see sun.security.x509.X509CRLImpl
067: */
068:
069: public class X509Factory extends CertificateFactorySpi {
070:
071: public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
072: public static final String END_CERT = "-----END CERTIFICATE-----";
073:
074: private static final int defaultExpectedLineLength = 80;
075:
076: private static final char[] endBoundary = "-----END".toCharArray();
077:
078: private static final int ENC_MAX_LENGTH = 4096 * 1024; // 4 MB MAX
079:
080: private static final Cache certCache = Cache
081: .newSoftMemoryCache(750);
082: private static final Cache crlCache = Cache.newSoftMemoryCache(750);
083:
084: /**
085: * Generates an X.509 certificate object and initializes it with
086: * the data read from the input stream <code>is</code>.
087: *
088: * @param is an input stream with the certificate data.
089: *
090: * @return an X.509 certificate object initialized with the data
091: * from the input stream.
092: *
093: * @exception CertificateException on parsing errors.
094: */
095: public Certificate engineGenerateCertificate(InputStream is)
096: throws CertificateException {
097: if (is == null) {
098: // clear the caches (for debugging)
099: certCache.clear();
100: // CDC/FP - X509CertificatePair has been subsetted
101: // out to save space.
102: // X509CertificatePair.clearCache();
103: throw new CertificateException("Missing input stream");
104: }
105: try {
106: if (is.markSupported() == false) {
107: // consume the entire input stream
108: byte[] totalBytes;
109: totalBytes = getTotalBytes(new BufferedInputStream(is));
110: is = new ByteArrayInputStream(totalBytes);
111: }
112: byte[] encoding = readSequence(is);
113: if (encoding != null) {
114: X509CertImpl cert = (X509CertImpl) getFromCache(
115: certCache, encoding);
116: if (cert != null) {
117: return cert;
118: }
119: cert = new X509CertImpl(encoding);
120: addToCache(certCache, cert.getEncodedInternal(), cert);
121: return cert;
122: } else {
123: X509CertImpl cert;
124: // determine if binary or Base64 encoding. If Base64 encoding,
125: // the certificate must be bounded at the beginning by
126: // "-----BEGIN".
127: if (isBase64(is)) {
128: // Base64
129: byte[] data = base64_to_binary(is);
130: cert = new X509CertImpl(data);
131: } else {
132: // binary
133: cert = new X509CertImpl(new DerValue(is));
134: }
135: return intern(cert);
136: }
137: } catch (IOException ioe) {
138: throw (CertificateException) new CertificateException(
139: "Could not parse certificate: " + ioe.toString())
140: .initCause(ioe);
141: }
142: }
143:
144: /**
145: * Read a DER SEQUENCE from an InputStream and return the encoding.
146: * If data does not represent a SEQUENCE, it uses indefinite length
147: * encoding, or is longer than ENC_MAX_LENGTH, the stream is reset
148: * and this method returns null.
149: */
150: private static byte[] readSequence(InputStream in)
151: throws IOException {
152: in.mark(ENC_MAX_LENGTH);
153: byte[] b = new byte[4];
154: int i = readFully(in, b, 0, b.length);
155: if ((i != b.length) || (b[0] != 0x30)) { // first byte must be SEQUENCE
156: in.reset();
157: return null;
158: }
159: i = b[1] & 0xff;
160: int totalLength;
161: if (i < 0x80) {
162: int valueLength = i;
163: totalLength = valueLength + 2;
164: } else if (i == 0x81) {
165: int valueLength = b[2] & 0xff;
166: totalLength = valueLength + 3;
167: } else if (i == 0x82) {
168: int valueLength = ((b[2] & 0xff) << 8) | (b[3] & 0xff);
169: totalLength = valueLength + 4;
170: } else { // ignore longer length forms
171: in.reset();
172: return null;
173: }
174: if (totalLength > ENC_MAX_LENGTH) {
175: in.reset();
176: return null;
177: }
178: byte[] encoding = new byte[totalLength];
179: System.arraycopy(b, 0, encoding, 0, b.length);
180: int n = totalLength - b.length;
181: i = readFully(in, encoding, b.length, n);
182: if (i != n) {
183: in.reset();
184: return null;
185: }
186: return encoding;
187: }
188:
189: /**
190: * Read from the stream until length bytes have been read or EOF has
191: * been reached. Return the number of bytes actually read.
192: */
193: private static int readFully(InputStream in, byte[] buffer,
194: int offset, int length) throws IOException {
195: int read = 0;
196: while (length > 0) {
197: int n = in.read(buffer, offset, length);
198: if (n <= 0) {
199: break;
200: }
201: read += n;
202: length -= n;
203: offset += n;
204: }
205: return read;
206: }
207:
208: /**
209: * Return an interned X509CertImpl for the given certificate.
210: * If the given X509Certificate or X509CertImpl is already present
211: * in the cert cache, the cached object is returned. Otherwise,
212: * if it is a X509Certificate, it is first converted to a X509CertImpl.
213: * Then the X509CertImpl is added to the cache and returned.
214: *
215: * Note that all certificates created via generateCertificate(InputStream)
216: * are already interned and this method does not need to be called.
217: * It is useful for certificates that cannot be created via
218: * generateCertificate() and for converting other X509Certificate
219: * implementations to an X509CertImpl.
220: */
221: public static synchronized X509CertImpl intern(X509Certificate c)
222: throws CertificateException {
223: if (c == null) {
224: return null;
225: }
226: boolean isImpl = c instanceof X509CertImpl;
227: byte[] encoding;
228: if (isImpl) {
229: encoding = ((X509CertImpl) c).getEncodedInternal();
230: } else {
231: encoding = c.getEncoded();
232: }
233: X509CertImpl newC = (X509CertImpl) getFromCache(certCache,
234: encoding);
235: if (newC != null) {
236: return newC;
237: }
238: if (isImpl) {
239: newC = (X509CertImpl) c;
240: } else {
241: newC = new X509CertImpl(encoding);
242: encoding = newC.getEncodedInternal();
243: }
244: addToCache(certCache, encoding, newC);
245: return newC;
246: }
247:
248: /**
249: * Return an interned X509CRLImpl for the given certificate.
250: * For more information, see intern(X509Certificate).
251: */
252: public static synchronized X509CRLImpl intern(X509CRL c)
253: throws CRLException {
254: if (c == null) {
255: return null;
256: }
257: boolean isImpl = c instanceof X509CRLImpl;
258: byte[] encoding;
259: if (isImpl) {
260: encoding = ((X509CRLImpl) c).getEncodedInternal();
261: } else {
262: encoding = c.getEncoded();
263: }
264: X509CRLImpl newC = (X509CRLImpl) getFromCache(crlCache,
265: encoding);
266: if (newC != null) {
267: return newC;
268: }
269: if (isImpl) {
270: newC = (X509CRLImpl) c;
271: } else {
272: newC = new X509CRLImpl(encoding);
273: encoding = newC.getEncodedInternal();
274: }
275: addToCache(crlCache, encoding, newC);
276: return newC;
277: }
278:
279: /**
280: * Get the X509CertImpl or X509CRLImpl from the cache.
281: */
282: private static synchronized Object getFromCache(Cache cache,
283: byte[] encoding) {
284: Object key = new Cache.EqualByteArray(encoding);
285: Object value = cache.get(key);
286: return value;
287: }
288:
289: /**
290: * Add the X509CertImpl or X509CRLImpl to the cache.
291: */
292: private static synchronized void addToCache(Cache cache,
293: byte[] encoding, Object value) {
294: if (encoding.length > ENC_MAX_LENGTH) {
295: return;
296: }
297: Object key = new Cache.EqualByteArray(encoding);
298: cache.put(key, value);
299: }
300:
301: /**
302: * Generates a <code>CertPath</code> object and initializes it with
303: * the data read from the <code>InputStream</code> inStream. The data
304: * is assumed to be in the default encoding.
305: *
306: * @param inStream an <code>InputStream</code> containing the data
307: * @return a <code>CertPath</code> initialized with the data from the
308: * <code>InputStream</code>
309: * @exception CertificateException if an exception occurs while decoding
310: * @since 1.4
311: */
312: public CertPath engineGenerateCertPath(InputStream inStream)
313: throws CertificateException {
314: if (inStream == null) {
315: throw new CertificateException("Missing input stream");
316: }
317: try {
318: if (inStream.markSupported() == false) {
319: // consume the entire input stream
320: byte[] totalBytes;
321: totalBytes = getTotalBytes(new BufferedInputStream(
322: inStream));
323: inStream = new ByteArrayInputStream(totalBytes);
324: }
325: // determine if binary or Base64 encoding. If Base64 encoding,
326: // each certificate must be bounded at the beginning by
327: // "-----BEGIN".
328: if (isBase64(inStream)) {
329: // Base64
330: byte[] data = base64_to_binary(inStream);
331: return new X509CertPath(new ByteArrayInputStream(data));
332: } else {
333: return new X509CertPath(inStream);
334: }
335: } catch (IOException ioe) {
336: throw new CertificateException(ioe.getMessage());
337: }
338: }
339:
340: /**
341: * Generates a <code>CertPath</code> object and initializes it with
342: * the data read from the <code>InputStream</code> inStream. The data
343: * is assumed to be in the specified encoding.
344: *
345: * @param inStream an <code>InputStream</code> containing the data
346: * @param encoding the encoding used for the data
347: * @return a <code>CertPath</code> initialized with the data from the
348: * <code>InputStream</code>
349: * @exception CertificateException if an exception occurs while decoding or
350: * the encoding requested is not supported
351: * @since 1.4
352: */
353: public CertPath engineGenerateCertPath(InputStream inStream,
354: String encoding) throws CertificateException {
355: if (inStream == null) {
356: throw new CertificateException("Missing input stream");
357: }
358: try {
359: if (inStream.markSupported() == false) {
360: // consume the entire input stream
361: byte[] totalBytes;
362: totalBytes = getTotalBytes(new BufferedInputStream(
363: inStream));
364: inStream = new ByteArrayInputStream(totalBytes);
365: }
366: // determine if binary or Base64 encoding. If Base64 encoding,
367: // each certificate must be bounded at the beginning by
368: // "-----BEGIN".
369: if (isBase64(inStream)) {
370: // Base64
371: byte[] data = base64_to_binary(inStream);
372: return new X509CertPath(new ByteArrayInputStream(data),
373: encoding);
374: } else {
375: return (new X509CertPath(inStream, encoding));
376: }
377: } catch (IOException ioe) {
378: throw new CertificateException(ioe.getMessage());
379: }
380: }
381:
382: /**
383: * Generates a <code>CertPath</code> object and initializes it with
384: * a <code>List</code> of <code>Certificate</code>s.
385: * <p>
386: * The certificates supplied must be of a type supported by the
387: * <code>CertificateFactory</code>. They will be copied out of the supplied
388: * <code>List</code> object.
389: *
390: * @param certificates a <code>List</code> of <code>Certificate</code>s
391: * @return a <code>CertPath</code> initialized with the supplied list of
392: * certificates
393: * @exception CertificateException if an exception occurs
394: * @since 1.4
395: */
396: public CertPath engineGenerateCertPath(List certificates)
397: throws CertificateException {
398: return (new X509CertPath(certificates));
399: }
400:
401: /**
402: * Returns an iteration of the <code>CertPath</code> encodings supported
403: * by this certificate factory, with the default encoding first.
404: * <p>
405: * Attempts to modify the returned <code>Iterator</code> via its
406: * <code>remove</code> method result in an
407: * <code>UnsupportedOperationException</code>.
408: *
409: * @return an <code>Iterator</code> over the names of the supported
410: * <code>CertPath</code> encodings (as <code>String</code>s)
411: * @since 1.4
412: */
413: public Iterator engineGetCertPathEncodings() {
414: return (X509CertPath.getEncodingsStatic());
415: }
416:
417: /**
418: * Returns a (possibly empty) collection view of X.509 certificates read
419: * from the given input stream <code>is</code>.
420: *
421: * @param is the input stream with the certificates.
422: *
423: * @return a (possibly empty) collection view of X.509 certificate objects
424: * initialized with the data from the input stream.
425: *
426: * @exception CertificateException on parsing errors.
427: */
428: public Collection engineGenerateCertificates(InputStream is)
429: throws CertificateException {
430: if (is == null) {
431: throw new CertificateException("Missing input stream");
432: }
433: try {
434: if (is.markSupported() == false) {
435: // consume the entire input stream
436: is = new ByteArrayInputStream(
437: getTotalBytes(new BufferedInputStream(is)));
438: }
439: return parseX509orPKCS7Cert(is);
440: } catch (IOException ioe) {
441: ioe.printStackTrace();
442: throw new CertificateException(ioe.getMessage());
443: }
444: }
445:
446: /**
447: * Generates an X.509 certificate revocation list (CRL) object and
448: * initializes it with the data read from the given input stream
449: * <code>is</code>.
450: *
451: * @param is an input stream with the CRL data.
452: *
453: * @return an X.509 CRL object initialized with the data
454: * from the input stream.
455: *
456: * @exception CRLException on parsing errors.
457: */
458: public CRL engineGenerateCRL(InputStream is) throws CRLException {
459: if (is == null) {
460: // clear the cache (for debugging)
461: crlCache.clear();
462: throw new CRLException("Missing input stream");
463: }
464: try {
465: if (is.markSupported() == false) {
466: // consume the entire input stream
467: byte[] totalBytes;
468: totalBytes = getTotalBytes(new BufferedInputStream(is));
469: is = new ByteArrayInputStream(totalBytes);
470: }
471: byte[] encoding = readSequence(is);
472: if (encoding != null) {
473: X509CRLImpl crl = (X509CRLImpl) getFromCache(crlCache,
474: encoding);
475: if (crl != null) {
476: return crl;
477: }
478: crl = new X509CRLImpl(encoding);
479: addToCache(crlCache, crl.getEncodedInternal(), crl);
480: return crl;
481: } else {
482: X509CRLImpl crl;
483: // determine if binary or Base64 encoding. If Base64 encoding,
484: // the CRL must be bounded at the beginning by
485: // "-----BEGIN".
486: if (isBase64(is)) {
487: // Base64
488: byte[] data = base64_to_binary(is);
489: crl = new X509CRLImpl(data);
490: } else {
491: // binary
492: crl = new X509CRLImpl(new DerValue(is));
493: }
494: return intern(crl);
495: }
496: } catch (IOException ioe) {
497: throw new CRLException(ioe.getMessage());
498: }
499: }
500:
501: /**
502: * Returns a (possibly empty) collection view of X.509 CRLs read
503: * from the given input stream <code>is</code>.
504: *
505: * @param is the input stream with the CRLs.
506: *
507: * @return a (possibly empty) collection view of X.509 CRL objects
508: * initialized with the data from the input stream.
509: *
510: * @exception CRLException on parsing errors.
511: */
512: public Collection engineGenerateCRLs(InputStream is)
513: throws CRLException {
514: if (is == null) {
515: throw new CRLException("Missing input stream");
516: }
517: try {
518: if (is.markSupported() == false) {
519: // consume the entire input stream
520: is = new ByteArrayInputStream(
521: getTotalBytes(new BufferedInputStream(is)));
522: }
523: return parseX509orPKCS7CRL(is);
524: } catch (IOException ioe) {
525: throw new CRLException(ioe.getMessage());
526: }
527: }
528:
529: /*
530: * Parses the data in the given input stream as a sequence of DER
531: * encoded X.509 certificates (in binary or base 64 encoded format) OR
532: * as a single PKCS#7 encoded blob (in binary or base64 encoded format).
533: */
534: private Collection parseX509orPKCS7Cert(InputStream is)
535: throws CertificateException, IOException {
536: Collection coll = new ArrayList();
537: boolean first = true;
538: while (is.available() != 0) {
539: // determine if binary or Base64 encoding. If Base64 encoding,
540: // each certificate must be bounded at the beginning by
541: // "-----BEGIN".
542: InputStream is2 = is;
543: if (isBase64(is2)) {
544: // Base64
545: is2 = new ByteArrayInputStream(base64_to_binary(is2));
546: }
547: if (first)
548: is2.mark(is2.available());
549: try {
550: // treat as X.509 cert
551: coll.add(intern(new X509CertImpl(new DerValue(is2))));
552: } catch (CertificateException e) {
553: Throwable cause = e.getCause();
554: // only treat as PKCS#7 if this is the first cert parsed
555: // and the root cause of the decoding failure is an IOException
556: if (first && cause != null
557: && (cause instanceof IOException)) {
558: // treat as PKCS#7
559: is2.reset();
560: PKCS7 pkcs7 = new PKCS7(is2);
561: X509Certificate[] certs = pkcs7.getCertificates();
562: // certs are optional in PKCS #7
563: if (certs != null) {
564: return Arrays.asList(certs);
565: } else {
566: // no certs provided
567: return new ArrayList(0);
568: }
569: } else {
570: throw e;
571: }
572: }
573: first = false;
574: }
575: return coll;
576: }
577:
578: /*
579: * Parses the data in the given input stream as a sequence of DER encoded
580: * X.509 CRLs (in binary or base 64 encoded format) OR as a single PKCS#7
581: * encoded blob (in binary or base 64 encoded format).
582: */
583: private Collection parseX509orPKCS7CRL(InputStream is)
584: throws CRLException, IOException {
585: Collection coll = new ArrayList();
586: boolean first = true;
587: while (is.available() != 0) {
588: // determine if binary or Base64 encoding. If Base64 encoding,
589: // the CRL must be bounded at the beginning by
590: // "-----BEGIN".
591: InputStream is2 = is;
592: if (isBase64(is)) {
593: // Base64
594: is2 = new ByteArrayInputStream(base64_to_binary(is2));
595: }
596: if (first)
597: is2.mark(is2.available());
598: try {
599: // treat as X.509 CRL
600: coll.add(new X509CRLImpl(is2));
601: } catch (CRLException e) {
602: // only treat as PKCS#7 if this is the first CRL parsed
603: if (first) {
604: is2.reset();
605: PKCS7 pkcs7 = new PKCS7(is2);
606: X509CRL[] crls = pkcs7.getCRLs();
607: // CRLs are optional in PKCS #7
608: if (crls != null) {
609: return Arrays.asList(crls);
610: } else {
611: // no crls provided
612: return new ArrayList(0);
613: }
614: }
615: }
616: first = false;
617: }
618: return coll;
619: }
620:
621: /*
622: * Converts a Base64-encoded X.509 certificate or X.509 CRL or PKCS#7 data
623: * to binary encoding.
624: * In all cases, the data must be bounded at the beginning by
625: * "-----BEGIN", and must be bounded at the end by "-----END".
626: */
627: private byte[] base64_to_binary(InputStream is) throws IOException {
628: long len = 0; // total length of base64 encoding, including boundaries
629:
630: is.mark(is.available());
631:
632: BufferedInputStream bufin = new BufferedInputStream(is);
633: BufferedReader br = new BufferedReader(new InputStreamReader(
634: bufin));
635:
636: // First read all of the data that is found between
637: // the "-----BEGIN" and "-----END" boundaries into a buffer.
638: String temp;
639: if ((temp = readLine(br)) == null
640: || !temp.startsWith("-----BEGIN")) {
641: throw new IOException("Unsupported encoding");
642: } else {
643: len += temp.length();
644: }
645: StringBuffer strBuf = new StringBuffer();
646: while ((temp = readLine(br)) != null
647: && !temp.startsWith("-----END")) {
648: strBuf.append(temp);
649: }
650: if (temp == null) {
651: throw new IOException("Unsupported encoding");
652: } else {
653: len += temp.length();
654: }
655:
656: // consume only as much as was needed
657: len += strBuf.length();
658: is.reset();
659: is.skip(len);
660:
661: // Now, that data is supposed to be a single X.509 certificate or
662: // X.509 CRL or PKCS#7 formatted data... Base64 encoded.
663: // Decode into binary and return the result.
664: BASE64Decoder decoder = new BASE64Decoder();
665: return decoder.decodeBuffer(strBuf.toString());
666: }
667:
668: /*
669: * Reads the entire input stream into a byte array.
670: */
671: private byte[] getTotalBytes(InputStream is) throws IOException {
672: byte[] buffer = new byte[8192];
673: ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);
674: int n;
675: baos.reset();
676: while ((n = is.read(buffer, 0, buffer.length)) != -1) {
677: baos.write(buffer, 0, n);
678: }
679: return baos.toByteArray();
680: }
681:
682: /*
683: * Determines if input is binary or Base64 encoded.
684: */
685: private boolean isBase64(InputStream is) throws IOException {
686: if (is.available() >= 10) {
687: is.mark(10);
688: int c1 = is.read();
689: int c2 = is.read();
690: int c3 = is.read();
691: int c4 = is.read();
692: int c5 = is.read();
693: int c6 = is.read();
694: int c7 = is.read();
695: int c8 = is.read();
696: int c9 = is.read();
697: int c10 = is.read();
698: is.reset();
699: if (c1 == '-' && c2 == '-' && c3 == '-' && c4 == '-'
700: && c5 == '-' && c6 == 'B' && c7 == 'E' && c8 == 'G'
701: && c9 == 'I' && c10 == 'N') {
702: return true;
703: } else {
704: return false;
705: }
706: } else {
707: return false;
708: }
709: }
710:
711: /*
712: * Read a line of text. A line is considered to be terminated by any one
713: * of a line feed ('\n'), a carriage return ('\r'), a carriage return
714: * followed immediately by a linefeed, or an end-of-certificate marker.
715: *
716: * @return A String containing the contents of the line, including
717: * any line-termination characters, or null if the end of the
718: * stream has been reached.
719: */
720: private String readLine(BufferedReader br) throws IOException {
721: int c;
722: int i = 0;
723: boolean isMatch = true;
724: boolean matched = false;
725: StringBuffer sb = new StringBuffer(defaultExpectedLineLength);
726: do {
727: c = br.read();
728: if (isMatch && (i < endBoundary.length)) {
729: isMatch = ((char) c != endBoundary[i++]) ? false : true;
730: }
731: if (!matched)
732: matched = (isMatch && (i == endBoundary.length));
733: sb.append((char) c);
734: } while ((c != -1) && (c != '\n') && (c != '\r'));
735:
736: if (!matched && c == -1) {
737: return null;
738: }
739: if (c == '\r') {
740: br.mark(1);
741: int c2 = br.read();
742: if (c2 == '\n') {
743: sb.append((char) c);
744: } else {
745: br.reset();
746: }
747: }
748: return sb.toString();
749: }
750: }
|