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