52969641424cc16aed6cd584dbf034d3e232d13a
[jalviewjs.git] / src / javajs / util / Rdr.java
1 /* $RCSfile$
2  * $Author: hansonr $
3  * $Date: 2007-04-05 09:07:28 -0500 (Thu, 05 Apr 2007) $
4  * $Revision: 7326 $
5  *
6  * Copyright (C) 2003-2005  The Jmol Development Team
7  *
8  * Contact: jmol-developers@lists.sf.net
9  *
10  *  This library is free software; you can redistribute it and/or
11  *  modify it under the terms of the GNU Lesser General Public
12  *  License as published by the Free Software Foundation; either
13  *  version 2.1 of the License, or (at your option) any later version.
14  *
15  *  This library is distributed in the hope that it will be useful,
16  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18  *  Lesser General Public License for more details.
19  *
20  *  You should have received a copy of the GNU Lesser General Public
21  *  License along with this library; if not, write to the Free Software
22  *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
23  */
24 package javajs.util;
25
26 import java.io.BufferedInputStream;
27 import java.io.BufferedReader;
28 import java.io.ByteArrayInputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.InputStreamReader;
32 import java.io.StringReader;
33 import java.io.UnsupportedEncodingException;
34
35 import java.util.Map;
36
37
38 import javajs.api.GenericCifDataParser;
39 import javajs.api.GenericLineReader;
40 import javajs.api.GenericZipTools;
41
42 /**
43  * A general helper class for a variety of stream and reader functionality
44  * including:
45  * 
46  *  stream and byte magic-number decoding for PNG, PNGJ, ZIP, and GZIP streams
47  *  
48  *  various stream/reader methods, including UTF-encoded stream reading
49  *  
50  *  reflection-protected access to a CIF parser and ZIP tools
51  *  
52  *   
53  * 
54  * 
55  */
56 public class Rdr implements GenericLineReader {
57
58   BufferedReader reader;
59
60   public Rdr(BufferedReader reader) {
61     this.reader = reader;
62   }
63   
64   @Override
65   public String readNextLine() throws Exception {
66     return reader.readLine();
67   }
68
69   public static Map<String, Object> readCifData(GenericCifDataParser parser, BufferedReader br) {
70     return parser.set(null, br).getAllCifData();
71   }
72   
73   
74   ///////////
75   
76   public static String fixUTF(byte[] bytes) {    
77     Encoding encoding = getUTFEncoding(bytes);
78     if (encoding != Encoding.NONE)
79     try {
80       String s = new String(bytes, encoding.name().replace('_', '-'));
81       switch (encoding) {
82       case UTF8:
83       case UTF_16BE:
84       case UTF_16LE:
85         // extra byte at beginning removed
86         s = s.substring(1);
87         break;
88       default:
89         break;        
90       }
91       return s;
92     } catch (UnsupportedEncodingException e) {
93       System.out.println(e);
94     }
95     return new String(bytes);
96   }
97
98   private static Encoding getUTFEncoding(byte[] bytes) {
99     if (bytes.length >= 3 && bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF)
100       return Encoding.UTF8;
101     if (bytes.length >= 4 && bytes[0] == (byte) 0 && bytes[1] == (byte) 0 
102         && bytes[2] == (byte) 0xFE && bytes[3] == (byte) 0xFF)
103       return Encoding.UTF_32BE;
104     if (bytes.length >= 4 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE 
105         && bytes[2] == (byte) 0 && bytes[3] == (byte) 0)
106       return Encoding.UTF_32LE;
107     if (bytes.length >= 2 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE)
108       return Encoding.UTF_16LE;
109     if (bytes.length >= 2 && bytes[0] == (byte) 0xFE && bytes[1] == (byte) 0xFF)
110       return Encoding.UTF_16BE;
111     return Encoding.NONE;
112   
113   }
114   
115   ////////// stream type checking //////////
116   
117
118   private static Encoding getUTFEncodingForStream(BufferedInputStream is) throws IOException {
119     /**
120      * @j2sNative
121      * 
122      *  is.resetStream();
123      * 
124      */
125     {
126     }
127     byte[] abMagic = new byte[4];
128     abMagic[3] = 1;
129     try{
130     is.mark(5);
131     } catch (Exception e) {
132       return Encoding.NONE;
133     }
134     is.read(abMagic, 0, 4);
135     is.reset();
136     return getUTFEncoding(abMagic);
137   }
138
139   public static boolean isBase64(SB sb) {
140     return (sb.indexOf(";base64,") == 0);
141   }
142
143   public static boolean isCompoundDocumentS(InputStream is) {
144     return isCompoundDocumentB(getMagic(is, 8));
145   }
146
147   public static boolean isCompoundDocumentB(byte[] bytes) {
148     return (bytes.length >= 8 && bytes[0] == (byte) 0xD0
149         && bytes[1] == (byte) 0xCF && bytes[2] == (byte) 0x11
150         && bytes[3] == (byte) 0xE0 && bytes[4] == (byte) 0xA1
151         && bytes[5] == (byte) 0xB1 && bytes[6] == (byte) 0x1A 
152         && bytes[7] == (byte) 0xE1);
153   }
154
155   public static boolean isGzipS(InputStream is) {
156     return isGzipB(getMagic(is, 2));
157   }
158
159   public static boolean isGzipB(byte[] bytes) {    
160       return (bytes != null && bytes.length >= 2 
161           && bytes[0] == (byte) 0x1F && bytes[1] == (byte) 0x8B);
162   }
163
164   public static boolean isPickleS(InputStream is) {
165     return Rdr.isPickleB(getMagic(is, 2));
166   }
167
168   public static boolean isPickleB(byte[] bytes) {    
169       return (bytes != null && bytes.length >= 2 
170           && bytes[0] == (byte) 0x7D && bytes[1] == (byte) 0x71);
171   }
172
173   public static boolean isPngZipStream(InputStream is) {
174     return isPngZipB(getMagic(is, 55));
175   }
176
177   public static boolean isPngZipB(byte[] bytes) {
178     // \0PNGJ starting at byte 50
179     return (bytes[50] == 0 && bytes[51] == 0x50 && bytes[52] == 0x4E && bytes[53] == 0x47 && bytes[54] == 0x4A);
180   }
181
182   public static boolean isZipS(InputStream is) {
183     return isZipB(getMagic(is, 4));
184   }
185
186   public static boolean isZipB(byte[] bytes) {
187     return (bytes.length >= 4 
188         && bytes[0] == 0x50  //PK<03><04> 
189         && bytes[1] == 0x4B
190         && bytes[2] == 0x03 
191         && bytes[3] == 0x04);
192   }
193
194   private static byte[] getMagic(InputStream is, int n) {
195     byte[] abMagic = new byte[n];
196     /**
197      * @j2sNative
198      * 
199      * is.resetStream();
200      * 
201      */
202     {
203     }
204     try {
205       is.mark(n + 1);
206       is.read(abMagic, 0, n);
207     } catch (IOException e) {
208     }
209     try {
210       is.reset();
211     } catch (IOException e) {
212     }
213     return abMagic;
214   }
215
216   public static String guessMimeTypeForBytes(byte[] bytes) {
217      // only options here are JPEG, PNG, GIF, and BMP
218     switch (bytes.length < 2 ? -1 : bytes[1]) {
219     case 0:
220       return "image/jpg"; // 0xFF 0x00 ...
221     case 0x49:
222       return "image/gif"; // GIF89a...
223     case 0x4D:
224       return "image/BMP"; // BM...
225     case 0x50:
226       return "image/png";
227     default:
228       return  "image/unknown";
229     }
230   }
231
232
233   ////////// stream/byte methods ///////////
234   
235   public static BufferedInputStream getBIS(byte[] bytes) {
236     return new BufferedInputStream(new ByteArrayInputStream(bytes));
237   }
238
239   public static BufferedReader getBR(String string) {
240     return new BufferedReader(new StringReader(string));
241   }
242
243   /**
244    * Drill down into a GZIP stack until no more layers.
245    * @param jzt 
246    * 
247    * @param bis
248    * @return non-gzipped buffered input stream.
249    * 
250    * @throws IOException
251    */
252   public static BufferedInputStream getUnzippedInputStream(GenericZipTools jzt, BufferedInputStream bis) throws IOException {
253     while (isGzipS(bis))
254       bis = new BufferedInputStream(jzt.newGZIPInputStream(bis));
255     return bis;
256   }
257
258   /**
259    * Allow for base64-encoding check.
260    * 
261    * @param sb
262    * @return byte array
263    */
264   public static byte[] getBytesFromSB(SB sb) {
265     return (isBase64(sb) ? Base64.decodeBase64(sb.substring(8)) : sb.toBytes(0, -1));    
266   }
267
268  /**
269    * Read a an entire BufferedInputStream for its bytes, and 
270    * either return them or leave them in the designated output channel.
271    *  
272    * @param bis
273    * @param out a destination output channel, or null
274    * @return byte[] (if out is null) or a message indicating length (if not)
275    * 
276    * @throws IOException
277    */
278   public static Object getStreamAsBytes(BufferedInputStream bis,
279                                          OC out) throws IOException {
280     byte[] buf = new byte[1024];
281     byte[] bytes = (out == null ? new byte[4096] : null);
282     int len = 0;
283     int totalLen = 0;
284     while ((len = bis.read(buf, 0, 1024)) > 0) {
285       totalLen += len;
286       if (out == null) {
287         if (totalLen >= bytes.length)
288           bytes = AU.ensureLengthByte(bytes, totalLen * 2);
289         System.arraycopy(buf, 0, bytes, totalLen - len, len);
290       } else {
291         out.write(buf, 0, len);
292       }
293     }
294     bis.close();
295     if (out == null) {
296       return AU.arrayCopyByte(bytes, totalLen);
297     }
298     return totalLen + " bytes";
299   }
300
301   /**
302    * Read an input stream fully, saving a byte array, then
303    * return a buffered reader to those bytes converted to string form.
304    * 
305    * @param bis
306    * @param charSet
307    * @return Reader
308    * @throws IOException
309    */
310   public static BufferedReader getBufferedReader(BufferedInputStream bis, String charSet)
311       throws IOException {
312     // could also just make sure we have a buffered input stream here.
313     if (getUTFEncodingForStream(bis) == Encoding.NONE)
314       return new BufferedReader(new InputStreamReader(bis, (charSet == null ? "UTF-8" : charSet)));
315     byte[] bytes = getLimitedStreamBytes(bis, -1);
316     bis.close();
317     return getBR(charSet == null ? fixUTF(bytes) : new String(bytes, charSet));
318   }
319
320   /**
321    * Read a possibly limited number of bytes (when n > 0) from a stream, 
322    * leaving the stream open.
323    * 
324    * @param is an input stream, not necessarily buffered.
325    * @param n the maximum number of bytes to read, or -1 for all
326    * @return the bytes read
327    * 
328    * @throws IOException
329    */
330   public static byte[] getLimitedStreamBytes(InputStream is, long n)
331       throws IOException {
332
333     //Note: You cannot use InputStream.available() to reliably read
334     //      zip data from the web. 
335
336     int buflen = (n > 0 && n < 1024 ? (int) n : 1024);
337     byte[] buf = new byte[buflen];
338     byte[] bytes = new byte[n < 0 ? 4096 : (int) n];
339     int len = 0;
340     int totalLen = 0;
341     if (n < 0)
342       n = Integer.MAX_VALUE;
343     while (totalLen < n && (len = is.read(buf, 0, buflen)) > 0) {
344       totalLen += len;
345       if (totalLen > bytes.length)
346         bytes = AU.ensureLengthByte(bytes, totalLen * 2);
347       System.arraycopy(buf, 0, bytes, totalLen - len, len);
348       if (n != Integer.MAX_VALUE && totalLen + buflen > bytes.length)
349         buflen = bytes.length - totalLen;
350
351     }
352     if (totalLen == bytes.length)
353       return bytes;
354     buf = new byte[totalLen];
355     System.arraycopy(bytes, 0, buf, 0, totalLen);
356     return buf;
357   }
358
359   /**
360    * 
361    * Read a UTF-8 stream fully, converting it to a String.
362    * Called by Jmol's XMLReaders
363    * 
364    * @param bis
365    * @return a UTF-8 string
366    */
367   public static String StreamToUTF8String(BufferedInputStream bis) {
368     String[] data = new String[1];
369     try {
370       readAllAsString(getBufferedReader(bis, "UTF-8"), -1, true, data, 0);
371     } catch (IOException e) {
372     }
373     return data[0];
374   }
375
376   /**
377    * This method fills data[i] with string data from a file that may or may not
378    * be binary even though it is being read by a reader. It is meant to be used
379    * simple text-based files only.
380    *  
381    * @param br
382    * @param nBytesMax
383    * @param allowBinary
384    * @param data
385    * @param i
386    * @return true if data[i] holds the data; false if data[i] holds an error message. 
387    */
388   public static boolean readAllAsString(BufferedReader br, int nBytesMax, boolean allowBinary, String[] data, int i) {
389     try {
390       SB sb = SB.newN(8192);
391       String line;
392       if (nBytesMax < 0) {
393         line = br.readLine();
394         if (allowBinary || line != null && line.indexOf('\0') < 0
395             && (line.length() != 4 || line.charAt(0) != 65533
396             || line.indexOf("PNG") != 1)) {
397           sb.append(line).appendC('\n');
398           while ((line = br.readLine()) != null)
399             sb.append(line).appendC('\n');
400         }
401       } else {
402         int n = 0;
403         int len;
404         while (n < nBytesMax && (line = br.readLine()) != null) {
405           if (nBytesMax - n < (len = line.length()) + 1)
406             line = line.substring(0, nBytesMax - n - 1);
407           sb.append(line).appendC('\n');
408           n += len + 1;
409         }
410       }
411       br.close();
412       data[i] = sb.toString();
413       return true;
414     } catch (Exception ioe) {
415       data[i] = ioe.toString();
416       return false;
417     }
418   }
419
420   
421   /////////// PNGJ support /////////////
422   
423   
424   /**
425    * Look at byte 50 for "\0PNGJxxxxxxxxx+yyyyyyyyy" where xxxxxxxxx is a byte
426    * offset to the JMOL data and yyyyyyyyy is the length of the data.
427    * 
428    * @param bis
429    * @return same stream or byte stream
430    */
431
432   /**
433    * Retrieve the two numbers in a PNG iTXt tag indicating the 
434    * file pointer for the start of the ZIP data as well as its length.
435    * 
436    * @param bis
437    * @param pt_count
438    */
439   static void getPngZipPointAndCount(BufferedInputStream bis, int[] pt_count) {
440     bis.mark(75);
441     try {
442       byte[] data = getLimitedStreamBytes(bis, 74);
443       bis.reset();
444       int pt = 0;
445       for (int i = 64, f = 1; --i > 54; f *= 10)
446         pt += (data[i] - '0') * f;
447       int n = 0;
448       for (int i = 74, f = 1; --i > 64; f *= 10)
449         n += (data[i] - '0') * f;
450       pt_count[0] = pt;
451       pt_count[1] = n;
452     } catch (Throwable e) {
453       pt_count[1] = 0;
454     }
455   }
456
457   /**
458    * Either advance a PNGJ stream to its zip file data or pull out the ZIP data
459    * bytes and create a new stream for them from which a ZIP utility can start
460    * extracting files.
461    * 
462    * @param bis
463    * @param asNewStream  
464    * @return new buffered ByteArrayInputStream, possibly with no data if there is an error
465    */
466   public static BufferedInputStream getPngZipStream(BufferedInputStream bis, boolean asNewStream) {
467     if (!isPngZipStream(bis))
468       return bis;
469     byte[] data = new byte[0];
470     bis.mark(75);
471     try {
472       int pt_count[] = new int[2];
473       getPngZipPointAndCount(bis, pt_count);
474       if (pt_count[1] != 0) {
475         int pt = pt_count[0];
476         while (pt > 0)
477           pt -= bis.skip(pt);
478         if (!asNewStream)
479           return bis;
480         data = getLimitedStreamBytes(bis, pt_count[1]);
481       }
482     } catch (Throwable e) {
483     } finally {
484       try {
485         if (asNewStream)
486           bis.close();
487       } catch (Exception e) {
488         // ignore
489       }
490     }
491     return getBIS(data);
492   }
493
494   /** We define a request for zip file extraction by vertical bar:
495    *  zipName|interiorFileName. These may be nested if there is a
496    *  zip file contained in a zip file. 
497    *  
498    * @param fileName
499    * @return filename trimmed of interior fileName
500    * 
501    */
502   public static String getZipRoot(String fileName) {
503     int pt = fileName.indexOf("|");
504     return (pt < 0 ? fileName : fileName.substring(0, pt));
505   }
506
507   
508 }
509