Begin versioning.
[fits.git] / src / nom / tam / fits / FitsUtil.java
1 package nom.tam.fits;
2
3 /* Copyright: Thomas McGlynn 1999.
4  * This code may be used for any purpose, non-commercial
5  * or commercial so long as this copyright notice is retained
6  * in the source code or included in or referred to in any
7  * derived software.
8  */
9 import nom.tam.util.RandomAccess;
10
11 import java.io.IOException;
12 import java.io.File;
13 import java.io.FileInputStream;
14 import java.io.FilterInputStream;
15 import java.io.InputStream;
16 import java.io.OutputStream;
17 import java.io.PushbackInputStream;
18 import java.io.UnsupportedEncodingException;
19 import java.lang.reflect.Constructor;
20
21 import java.net.URL;
22 import java.net.URLConnection;
23
24 import java.util.Map;
25 import java.util.List;
26 import java.util.zip.GZIPInputStream;
27 import nom.tam.util.ArrayDataOutput;
28 import nom.tam.util.AsciiFuncs;
29
30 /** This class comprises static
31  *  utility functions used throughout
32  *  the FITS classes.
33  */
34 public class FitsUtil {
35
36     private static boolean wroteCheckingError = false;
37
38     /** Reposition a random access stream to a requested offset */
39     public static void reposition(Object o, long offset)
40             throws FitsException {
41
42         if (o == null) {
43             throw new FitsException("Attempt to reposition null stream");
44         }
45         if (!(o instanceof RandomAccess)
46                 || offset < 0) {
47             throw new FitsException("Invalid attempt to reposition stream " + o
48                     + " of type " + o.getClass().getName()
49                     + " to " + offset);
50         }
51
52         try {
53             ((RandomAccess) o).seek(offset);
54         } catch (IOException e) {
55             throw new FitsException("Unable to repostion stream " + o
56                     + " of type " + o.getClass().getName()
57                     + " to " + offset + "   Exception:" + e);
58         }
59     }
60
61     /** Find out where we are in a random access file */
62     public static long findOffset(Object o) {
63
64         if (o instanceof RandomAccess) {
65             return ((RandomAccess) o).getFilePointer();
66         } else {
67             return -1;
68         }
69     }
70
71     /** How many bytes are needed to fill the last 2880 block? */
72     public static int padding(int size) {
73         return padding((long) size);
74     }
75
76     public static int padding(long size) {
77
78         int mod = (int) (size % 2880);
79         if (mod > 0) {
80             mod = 2880 - mod;
81         }
82         return mod;
83     }
84
85     /** Total size of blocked FITS element */
86     public static int addPadding(int size) {
87         return size + padding(size);
88     }
89
90     public static long addPadding(long size) {
91         return size + padding(size);
92     }
93
94     /** This method decompresses a compressed
95      *  input stream.  The decompression method is
96      *  selected automatically based upon the first two bytes read.
97      * @param compressed  The compressed input stram
98      * @return A stream which wraps the input stream and decompresses
99      *         it.  If the input stream is not compressed, a
100      *         pushback input stream wrapping the original stream is returned.
101      */
102     static InputStream decompress(InputStream compressed) throws FitsException {
103
104         PushbackInputStream pb = new PushbackInputStream(compressed, 2);
105
106         int mag1 = -1;
107         int mag2 = -1;
108
109         try {
110             mag1 = pb.read();
111             mag2 = pb.read();
112
113             if (mag1 == 0x1f && mag2 == 0x8b) {
114                 // Push the data back into the stream
115                 pb.unread(mag2);
116                 pb.unread(mag1);
117                 return new GZIPInputStream(pb);
118             } else if (mag1 == 0x1f && mag2 == 0x9d) {
119                 // Push the data back into the stream
120                 pb.unread(mag2);
121                 pb.unread(mag1);
122                 return compressInputStream(pb);
123             } else if (mag1 == 'B' && mag2 == 'Z') {
124                 if (System.getenv("BZIP_DECOMPRESSOR") != null) {
125                     pb.unread(mag2);
126                     pb.unread(mag1);
127                     return bunzipper(pb);
128                 }
129                 // Don't pushback
130                 String cname = "org.apache.tools.bzip2.CBZip2InputStream";
131                 // Note that we forego generics here since we don't
132                 // want any explicit mention of this class so that users
133                 // can compile and run without worrying about having the class in hand.
134                 try {
135                     Constructor con = Class.forName(cname).getConstructor(InputStream.class);
136                     return (InputStream) con.newInstance(pb);
137                 } catch (Exception e) {
138                     System.err.println("Unable to find constructor for BZIP2 decompression.  Is the Apache BZIP jar in the classpath?");
139                     throw new FitsException("No CBZip2InputStream class found for bzip2 compressed file");
140                 }
141             } else {
142                 // Push the data back into the stream
143                 pb.unread(mag2);
144                 pb.unread(mag1);
145                 return pb;
146             }
147
148         } catch (IOException e) {
149             // This is probably a prelude to failure...
150             throw new FitsException("Unable to analyze input stream");
151         }
152     }
153
154     static InputStream compressInputStream(final InputStream compressed) throws FitsException {
155         try {
156             Process proc = new ProcessBuilder("uncompress", "-c").start();
157
158             // This is the input to the process -- but
159             // an output from here.
160             final OutputStream input = proc.getOutputStream();
161
162             // Now copy everything in a separate thread.
163             Thread copier = new Thread(
164                     new Runnable() {
165
166                         public void run() {
167                             try {
168                                 byte[] buffer = new byte[8192];
169                                 int len;
170                                 while ((len = compressed.read(buffer, 0, buffer.length)) > 0) {
171                                     input.write(buffer, 0, len);
172                                 }
173                                 compressed.close();
174                                 input.close();
175                             } catch (IOException e) {
176                                 return;
177                             }
178                         }
179                     });
180             copier.start();
181             return proc.getInputStream();
182         } catch (Exception e) {
183             throw new FitsException("Unable to read .Z compressed stream.\nIs `uncompress' in the path?\n:" + e);
184         }
185     }
186
187     /** Is a file compressed? */
188     public static boolean isCompressed(File test) {
189         InputStream fis = null;
190         try {
191             if (test.exists()) {
192                 fis = new FileInputStream(test);
193                 int mag1 = fis.read();
194                 int mag2 = fis.read();
195                 fis.close();
196                 if (mag1 == 0x1f && (mag2 == 0x8b || mag2 == 0x9d)) {
197                     return true;
198                 } else if (mag1 == 'B' && mag2 == 'Z') {
199                     return true;
200                 } else {
201                     return false;
202                 }
203             }
204
205         } catch (IOException e) {
206             // This is probably a prelude to failure...
207             return false;
208
209         } finally {
210             if (fis != null) {
211                 try {
212                     fis.close();
213                 } catch (IOException e) {
214                 }
215             }
216         }
217         return false;
218     }
219
220     /** Check if a file seems to be compressed.
221      */
222     public static boolean isCompressed(String filename) {
223         if (filename == null) {
224             return false;
225         }
226         FileInputStream fis = null;
227         File test = new File(filename);
228         if (test.exists()) {
229             return isCompressed(test);
230         }
231
232         int len = filename.length();
233         return len > 2 && (filename.substring(len - 3).equalsIgnoreCase(".gz") || filename.substring(len - 2).equals(".Z"));
234     }
235
236     /** Get the maximum length of a String in a String array.
237      */
238     public static int maxLength(String[] o) throws FitsException {
239
240         int max = 0;
241         for (int i = 0; i < o.length; i += 1) {
242             if (o[i] != null && o[i].length() > max) {
243                 max = o[i].length();
244             }
245         }
246         return max;
247     }
248
249     /** Copy an array of Strings to bytes.*/
250     public static byte[] stringsToByteArray(String[] o, int maxLen) {
251         byte[] res = new byte[o.length * maxLen];
252         for (int i = 0; i < o.length; i += 1) {
253             byte[] bstr = null;
254             if (o[i] == null) {
255                 bstr = new byte[0];
256             } else {
257                 bstr = AsciiFuncs.getBytes(o[i]);
258             }
259             int cnt = bstr.length;
260             if (cnt > maxLen) {
261                 cnt = maxLen;
262             }
263             System.arraycopy(bstr, 0, res, i * maxLen, cnt);
264             for (int j = cnt; j < maxLen; j += 1) {
265                 res[i * maxLen + j] = (byte) ' ';
266             }
267         }
268         return res;
269     }
270
271     /** Convert bytes to Strings */
272     public static String[] byteArrayToStrings(byte[] o, int maxLen) {
273         boolean checking = FitsFactory.getCheckAsciiStrings();
274
275         // Note that if a String in a binary table contains an internal 0,
276         // the FITS standard says that it is to be considered as terminating
277         // the string at that point, so that software reading the
278         // data back may not include subsequent characters.
279         // No warning of this truncation is given.
280
281         String[] res = new String[o.length / maxLen];
282         for (int i = 0; i < res.length; i += 1) {
283
284             int start = i * maxLen;
285             int end = start + maxLen;
286             // Pre-trim the string to avoid keeping memory
287             // hanging around. (Suggested by J.C. Segovia, ESA).
288
289             // Note that the FITS standard does not mandate
290             // that we should be trimming the string at all, but
291             // this seems to best meet the desires of the community.
292             for (; start < end; start += 1) {
293                 if (o[start] != 32) {
294                     break; // Skip only spaces.
295                 }
296             }
297
298             for (; end > start; end -= 1) {
299                 if (o[end - 1] != 32) {
300                     break;
301                 }
302             }
303
304             // For FITS binary tables, 0  values are supposed
305             // to terminate strings, a la C.  [They shouldn't appear in
306             // any other context.]
307             // Other non-printing ASCII characters
308             // should always be an error which we can check for
309             // if the user requests.
310
311             // The lack of handling of null bytes was noted by Laurent Bourges.
312             boolean errFound = false;
313             for (int j = start; j < end; j += 1) {
314
315                 if (o[j] == 0) {
316                     end = j;
317                     break;
318                 }
319                 if (checking) {
320                     if (o[j] < 32 || o[j] > 126) {
321                         errFound = true;
322                         o[j] = 32;
323                     }
324                 }
325             }
326             res[i] = AsciiFuncs.asciiString(o, start, end - start);
327             if (errFound && !wroteCheckingError) {
328                 System.err.println("Warning: Invalid ASCII character[s] detected in string:" + res[i]);
329                 System.err.println("   Converted to space[s].  Any subsequent invalid characters will be converted silently");
330                 wroteCheckingError = true;
331             }
332         }
333         return res;
334
335     }
336
337     /** Convert an array of booleans to bytes */
338     static byte[] booleanToByte(boolean[] bool) {
339
340         byte[] byt = new byte[bool.length];
341         for (int i = 0; i < bool.length; i += 1) {
342             byt[i] = bool[i] ? (byte) 'T' : (byte) 'F';
343         }
344         return byt;
345     }
346
347     /** Convert an array of bytes to booleans */
348     static boolean[] byteToBoolean(byte[] byt) {
349         boolean[] bool = new boolean[byt.length];
350
351
352
353
354         for (int i = 0; i < byt.length; i += 1) {
355             bool[i] = (byt[i] == 'T');
356         }
357         return bool;
358     }
359
360     /** Get a stream to a URL accommodating possible redirections.
361      *  Note that if a redirection request points to a different
362      *  protocol than the original request, then the redirection
363      *  is not handled automatically.
364      */
365     public static InputStream getURLStream(URL url, int level) throws IOException {
366
367         // Hard coded....sigh
368         if (level > 5) {
369             throw new IOException("Two many levels of redirection in URL");
370         }
371         URLConnection conn = url.openConnection();
372 //      Map<String,List<String>> hdrs = conn.getHeaderFields();
373         Map hdrs = conn.getHeaderFields();
374
375         // Read through the headers and see if there is a redirection header.
376         // We loop (rather than just do a get on hdrs)
377         // since we want to match without regard to case.
378         String[] keys = (String[]) hdrs.keySet().toArray(new String[0]);
379 //      for (String key: hdrs.keySet()) {
380         for (int i = 0; i < keys.length; i += 1) {
381             String key = keys[i];
382
383             if (key != null && key.toLowerCase().equals("location")) {
384 //              String val = hdrs.get(key).get(0);
385                 String val = (String) ((List) hdrs.get(key)).get(0);
386                 if (val != null) {
387                     val = val.trim();
388                     if (val.length() > 0) {
389                         // Redirect
390                         return getURLStream(new URL(val), level + 1);
391                     }
392                 }
393             }
394         }
395         // No redirection
396         return conn.getInputStream();
397     }
398
399     /** Add padding to an output stream. */
400     public static void pad(ArrayDataOutput stream, long size) throws FitsException {
401         pad(stream, size, (byte) 0);
402     }
403
404     /** Add padding to an output stream. */
405     public static void pad(ArrayDataOutput stream, long size, byte fill)
406             throws FitsException {
407         int len = padding(size);
408         if (len > 0) {
409             byte[] buf = new byte[len];
410             for (int i = 0; i < len; i += 1) {
411                 buf[i] = fill;
412             }
413             try {
414                 stream.write(buf);
415                 stream.flush();
416             } catch (Exception e) {
417                 throw new FitsException("Unable to write padding", e);
418             }
419         }
420     }
421
422     static InputStream bunzipper(final InputStream pb) throws FitsException {
423         String cmd = System.getenv("BZIP_DECOMPRESSOR");
424         // Allow the user to have already specified the - option.
425         if (cmd.indexOf(" -") < 0) {
426             cmd += " -";
427         }
428         final OutputStream out;
429         String[] flds = cmd.split(" +");
430         Thread t;
431         Process p;
432         try {
433             p = new ProcessBuilder(flds).start();
434             out = p.getOutputStream();
435
436             t = new Thread(new Runnable() {
437
438                 public void run() {
439                     try {
440                         byte[] buf = new byte[16384];
441                         int len;
442                         long total = 0;
443                         while ((len = pb.read(buf)) > 0) {
444                             try {
445                                 out.write(buf, 0, len);
446                             } catch (Exception e) {
447                                 // Skip this. It can happen when we
448                                 // stop reading the compressed file in mid stream.
449                                 break;
450                             }
451                             total += len;
452                         }
453                         pb.close();
454                         out.close();
455
456                     } catch (IOException e) {
457                         throw new Error("Error reading BZIP compression using: " + System.getenv("BZIP_DECOMPRESSOR"), e);
458                     }
459                 }
460             });
461
462         } catch (Exception e) {
463             throw new FitsException("Error initiating BZIP decompression: " + e);
464         }
465         t.start();
466         return new CloseIS(p.getInputStream(), pb, out);
467
468     }
469 }
470
471 class CloseIS extends FilterInputStream {
472
473     InputStream i;
474     OutputStream o;
475
476     CloseIS(InputStream inp, InputStream i, OutputStream o) {
477         super(inp);
478         this.i = i;
479         this.o = o;
480     }
481
482     public void close() throws IOException {
483         super.close();
484         o.close();
485         i.close();
486     }
487 }
488