View Javadoc
1   package nom.tam.fits;
2   
3   /*-
4    * #%L
5    * nom.tam.fits
6    * %%
7    * Copyright (C) 1996 - 2024 nom-tam-fits
8    * %%
9    * This is free and unencumbered software released into the public domain.
10   * 
11   * Anyone is free to copy, modify, publish, use, compile, sell, or
12   * distribute this software, either in source code form or as a compiled
13   * binary, for any purpose, commercial or non-commercial, and by any
14   * means.
15   * 
16   * In jurisdictions that recognize copyright laws, the author or authors
17   * of this software dedicate any and all copyright interest in the
18   * software to the public domain. We make this dedication for the benefit
19   * of the public at large and to the detriment of our heirs and
20   * successors. We intend this dedication to be an overt act of
21   * relinquishment in perpetuity of all present and future rights to this
22   * software under copyright law.
23   * 
24   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
27   * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
28   * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
29   * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30   * OTHER DEALINGS IN THE SOFTWARE.
31   * #L%
32   */
33  
34  import java.io.IOException;
35  import java.lang.reflect.Array;
36  import java.util.Arrays;
37  
38  import nom.tam.fits.header.Bitpix;
39  import nom.tam.fits.header.IFitsHeader;
40  import nom.tam.fits.header.Standard;
41  import nom.tam.util.ArrayDataInput;
42  import nom.tam.util.ArrayDataOutput;
43  import nom.tam.util.ArrayFuncs;
44  import nom.tam.util.ByteFormatter;
45  import nom.tam.util.ByteParser;
46  import nom.tam.util.Cursor;
47  import nom.tam.util.FormatException;
48  
49  import static nom.tam.fits.header.Standard.NAXIS1;
50  import static nom.tam.fits.header.Standard.NAXIS2;
51  import static nom.tam.fits.header.Standard.TBCOLn;
52  import static nom.tam.fits.header.Standard.TDMAXn;
53  import static nom.tam.fits.header.Standard.TDMINn;
54  import static nom.tam.fits.header.Standard.TFIELDS;
55  import static nom.tam.fits.header.Standard.TFORMn;
56  import static nom.tam.fits.header.Standard.TLMAXn;
57  import static nom.tam.fits.header.Standard.TLMINn;
58  import static nom.tam.fits.header.Standard.TNULLn;
59  
60  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
61  
62  /**
63   * ASCII table data. ASCII tables are meant for human readability without any special tools. However, they are far less
64   * flexible or compact than {@link BinaryTable}. As such, users are generally discouraged from using this type of table
65   * to represent FITS table data. This class only supports scalar entries of type <code>int</code>, <code>long</code>,
66   * <code>float</code>, <code>double</code>, or else <code>String</code> types.
67   * 
68   * @see AsciiTableHDU
69   * @see BinaryTable
70   */
71  @SuppressWarnings("deprecation")
72  public class AsciiTable extends AbstractTableData {
73  
74      private static final int MAX_INTEGER_LENGTH = 10;
75  
76      private static final int FLOAT_MAX_LENGTH = 16;
77  
78      private static final int LONG_MAX_LENGTH = 20;
79  
80      private static final int INT_MAX_LENGTH = 10;
81  
82      private static final int DOUBLE_MAX_LENGTH = 24;
83  
84      /** Whether I10 columns should be treated as <code>int</code> provided that defined limits allow for it. */
85      private static boolean isI10PreferInt = true;
86  
87      // private static final Logger LOG = Logger.getLogger(AsciiTable.class.getName());
88  
89      /** The number of rows in the table */
90      private int nRows;
91  
92      /** The number of fields in the table */
93      private int nFields;
94  
95      /** The number of bytes in a row */
96      private int rowLen;
97  
98      /** The null string for the field */
99      private String[] nulls;
100 
101     /** The type of data in the field */
102     private Class<?>[] types;
103 
104     /** The offset from the beginning of the row at which the field starts */
105     private int[] offsets;
106 
107     /** The number of bytes in the field */
108     private int[] lengths;
109 
110     /** The byte buffer used to read/write the ASCII table */
111     private byte[] buffer;
112 
113     /** Markers indicating fields that are null */
114     private boolean[] isNull;
115 
116     /** Column names */
117     private String[] names;
118 
119     /**
120      * An array of arrays giving the data in the table in binary numbers
121      */
122     private Object[] data;
123 
124     /**
125      * The parser used to convert from buffer to data.
126      */
127     private ByteParser bp;
128 
129     /** The actual stream used to input data */
130     private ArrayDataInput currInput;
131 
132     /** Create an empty ASCII table */
133     public AsciiTable() {
134         data = new Object[0];
135         buffer = null;
136         nFields = 0;
137         nRows = 0;
138         rowLen = 0;
139         types = new Class[0];
140         lengths = new int[0];
141         offsets = new int[0];
142         nulls = new String[0];
143         names = new String[0];
144     }
145 
146     /**
147      * Creates an ASCII table given a header. For tables that contain integer-valued columns of format <code>I10</code>,
148      * the {@link #setI10PreferInt(boolean)} mayb be used to control whether to treat them as <code>int</code> or as
149      * <code>long</code> values (the latter is the default).
150      *
151      * @param      hdr           The header describing the table
152      *
153      * @throws     FitsException if the operation failed
154      * 
155      * @deprecated               (<i>for internal use</i>) Visibility may be reduced to the package level in the future.
156      */
157     public AsciiTable(Header hdr) throws FitsException {
158         this(hdr, isI10PreferInt);
159     }
160 
161     /**
162      * <p>
163      * Create an ASCII table given a header, with custom integer handling support.
164      * </p>
165      * <p>
166      * The <code>preferInt</code> parameter controls how columns with format "<code>I10</code>" are handled; this is
167      * tricky because some, but not all, integers that can be represented in 10 characters can be represented as 32-bit
168      * integers. Setting it <code>true</code> may make it more likely to avoid unexpected type changes during
169      * round-tripping, but it also means that some (large number) data in I10 columns may be impossible to read.
170      * </p>
171      * 
172      * @param      hdr           The header describing the table
173      * @param      preferInt     if <code>true</code>, format "I10" columns will be assumed <code>int.class</code>,
174      *                               provided TLMINn/TLMAXn or TDMINn/TDMAXn limits (if defined) allow it. if
175      *                               <code>false</code>, I10 columns that have no clear indication of data range will be
176      *                               assumed <code>long.class</code>.
177      *
178      * @throws     FitsException if the operation failed
179      * 
180      * @deprecated               Use {@link #setI10PreferInt(boolean)} instead prior to reading ASCII tables.
181      */
182     public AsciiTable(Header hdr, boolean preferInt) throws FitsException {
183         String ext = hdr.getStringValue(Standard.XTENSION, Standard.XTENSION_IMAGE);
184 
185         if (!ext.equalsIgnoreCase(Standard.XTENSION_ASCIITABLE)) {
186             throw new FitsException("Not an ASCII table header (XTENSION = " + hdr.getStringValue(Standard.XTENSION) + ")");
187         }
188 
189         nRows = hdr.getIntValue(NAXIS2);
190         nFields = hdr.getIntValue(TFIELDS);
191         rowLen = hdr.getIntValue(NAXIS1);
192 
193         types = new Class[nFields];
194         offsets = new int[nFields];
195         lengths = new int[nFields];
196         nulls = new String[nFields];
197         names = new String[nFields];
198 
199         for (int i = 0; i < nFields; i++) {
200             names[i] = hdr.getStringValue(Standard.TTYPEn.n(i + 1), TableHDU.getDefaultColumnName(i));
201             offsets[i] = hdr.getIntValue(TBCOLn.n(i + 1)) - 1;
202             String s = hdr.getStringValue(TFORMn.n(i + 1));
203             if (offsets[i] < 0 || s == null) {
204                 throw new FitsException("Invalid Specification for column:" + (i + 1));
205             }
206             s = s.trim();
207             char c = s.charAt(0);
208             s = s.substring(1);
209             if (s.indexOf('.') > 0) {
210                 s = s.substring(0, s.indexOf('.'));
211             }
212             lengths[i] = Integer.parseInt(s);
213 
214             switch (c) {
215             case 'A':
216                 types[i] = String.class;
217                 break;
218             case 'I':
219                 if (lengths[i] == MAX_INTEGER_LENGTH) {
220                     types[i] = guessI10Type(i, hdr, preferInt);
221                 } else {
222                     types[i] = lengths[i] > MAX_INTEGER_LENGTH ? long.class : int.class;
223                 }
224                 break;
225             case 'F':
226             case 'E':
227                 types[i] = float.class;
228                 break;
229             case 'D':
230                 types[i] = double.class;
231                 break;
232             default:
233                 throw new FitsException("could not parse column type of ascii table");
234             }
235 
236             nulls[i] = hdr.getStringValue(TNULLn.n(i + 1));
237             if (nulls[i] != null) {
238                 nulls[i] = nulls[i].trim();
239             }
240         }
241     }
242 
243     /**
244      * Creates an ASCII table from existing data in column-major format order.
245      *
246      * @param  columns       The data for scalar-valued columns. Each column must be an array of <code>int[]</code>,
247      *                           <code>long[]</code>, <code>float[]</code>, <code>double[]</code>, or else
248      *                           <code>String[]</code>, containing the same number of elements in each column (the
249      *                           number of rows).
250      * 
251      * @return               a new ASCII table with the data. The tables data may be partially independent from the
252      *                           argument. Modifications to the table data, or that to the argument have undefined
253      *                           effect on the other object. If it is important to decouple them, you can use a
254      *                           {@link ArrayFuncs#deepClone(Object)} of your original data as an argument.
255      * 
256      * @throws FitsException if the argument is not a suitable representation of FITS data in columns
257      * 
258      * @see                  BinaryTable#fromColumnMajor(Object[])
259      * 
260      * @since                1.19
261      */
262     public static AsciiTable fromColumnMajor(Object[] columns) throws FitsException {
263         AsciiTable t = new AsciiTable();
264         for (int i = 0; i < columns.length; i++) {
265             try {
266                 t.addColumn(columns[i]);
267             } catch (Exception e) {
268                 throw new FitsException("col[" + i + "]: " + e.getMessage(), e);
269             }
270         }
271         return t;
272     }
273 
274     void setColumnName(int col, String value)
275             throws IllegalArgumentException, IndexOutOfBoundsException, HeaderCardException {
276         HeaderCard.validateChars(value);
277         names[col] = value;
278     }
279 
280     /**
281      * Checks if the integer value of a specific key requires <code>long</code> value type to store.
282      *
283      * @param  h   the header
284      * @param  key the keyword to check
285      *
286      * @return     <code>true</code> if the keyword exists and has an integer value that is outside the range of
287      *                 <code>int</code>. Otherwise <code>false</code>
288      *
289      * @see        #guessI10Type(int, Header, boolean)
290      */
291     private boolean requiresLong(Header h, IFitsHeader key, Long dft) {
292         long l = h.getLongValue(key, dft);
293         if (l == dft) {
294             return false;
295         }
296 
297         return (l < Integer.MIN_VALUE || l > Integer.MAX_VALUE);
298     }
299 
300     /**
301      * Guesses what type of values to use to return I10 type table values. Depending on the range of represented values
302      * I10 may fit into <code>int</code> types, or else require <code>long</code> type arrays. Therefore, the method
303      * checks for the presence of standard column limit keywords TLMINn/TLMAXn and TDMINn/TDMAXn and if these exist and
304      * are outside of the range of an <code>int</code> then the call will return <code>long.class</code>. If the header
305      * does not define the data limits (fully), it will return the class the caller prefers. Otherwise (data limits were
306      * defined and fit into the <code>int</code> range) <code>int.class</code> will be returned.
307      *
308      * @param  col       the 0-based table column index
309      * @param  h         the header
310      * @param  preferInt whether we prefer <code>int.class</code> over <code>long.class</code> in case the header does
311      *                       not provide us with a clue.
312      *
313      * @return           <code>long.class</code> if the data requires long or we prefer it. Othwerwise
314      *                       <code>int.class</code>
315      *
316      * @see              #AsciiTable(Header, boolean)
317      */
318     private Class<?> guessI10Type(int col, Header h, boolean preferInt) {
319         col++;
320 
321         if (requiresLong(h, TLMINn.n(col), Long.MAX_VALUE) || requiresLong(h, TLMAXn.n(col), Long.MIN_VALUE)
322                 || requiresLong(h, TDMINn.n(col), Long.MAX_VALUE) || requiresLong(h, TDMAXn.n(col), Long.MIN_VALUE)) {
323             return long.class;
324         }
325 
326         if ((h.containsKey(TLMINn.n(col)) || h.containsKey(TDMINn.n(col))) //
327                 && (h.containsKey(TLMAXn.n(col)) || h.containsKey(TDMAXn.n(col)))) {
328             // There are keywords defining both min/max values, and none of them require long types...
329             return int.class;
330         }
331 
332         return preferInt ? int.class : long.class;
333     }
334 
335     /**
336      * Return the data type in the specified column, such as <code>int.class</code> or <code>String.class</code>.
337      *
338      * @param  col The 0-based column index
339      *
340      * @return     the class of data in the specified column.
341      *
342      * @since      1.16
343      */
344     public final Class<?> getColumnType(int col) {
345         return types[col];
346     }
347 
348     int addColInfo(int col, Cursor<String, HeaderCard> iter) {
349         String tform = null;
350         if (types[col] == String.class) {
351             tform = "A" + lengths[col];
352         } else if (types[col] == int.class || types[col] == long.class) {
353             tform = "I" + lengths[col];
354         } else if (types[col] == float.class) {
355             tform = "E" + lengths[col] + ".0";
356         } else if (types[col] == double.class) {
357             tform = "D" + lengths[col] + ".0";
358         }
359 
360         Standard.context(AsciiTable.class);
361         if (names[col] != null) {
362             iter.add(HeaderCard.create(Standard.TTYPEn.n(col + 1), names[col]));
363         }
364         iter.add(HeaderCard.create(Standard.TFORMn.n(col + 1), tform));
365         iter.add(HeaderCard.create(Standard.TBCOLn.n(col + 1), offsets[col] + 1));
366         Standard.context(null);
367         return lengths[col];
368     }
369 
370     @Override
371     public int addColumn(Object newCol) throws FitsException, IllegalArgumentException {
372         if (newCol == null) {
373             throw new FitsException("data is null");
374         }
375 
376         if (!newCol.getClass().isArray()) {
377             throw new IllegalArgumentException("Not an array: " + newCol.getClass().getName());
378         }
379 
380         int maxLen = 1;
381         if (newCol instanceof String[]) {
382             String[] sa = (String[]) newCol;
383             for (String element : sa) {
384                 if (element != null && element.length() > maxLen) {
385                     maxLen = element.length();
386                 }
387             }
388         } else if (newCol instanceof double[]) {
389             maxLen = DOUBLE_MAX_LENGTH;
390         } else if (newCol instanceof int[]) {
391             maxLen = INT_MAX_LENGTH;
392         } else if (newCol instanceof long[]) {
393             maxLen = LONG_MAX_LENGTH;
394         } else if (newCol instanceof float[]) {
395             maxLen = FLOAT_MAX_LENGTH;
396         } else {
397             throw new FitsException(
398                     "No AsciiTable support for elements of " + newCol.getClass().getComponentType().getName());
399         }
400         addColumn(newCol, maxLen);
401 
402         // Invalidate the buffer
403         buffer = null;
404 
405         return nFields;
406     }
407 
408     /**
409      * Adds an ASCII table column with the specified ASCII text width for storing its elements.
410      *
411      * @param  newCol                   The new column data, which must be one of: <code>int[]</code>,
412      *                                      <code>long[]</code>, <code>float[]</code>, <code>double[]</code>, or else
413      *                                      <code>String[]</code>. If the table already contains data, the length of the
414      *                                      array must match the number of rows already contained in the table.
415      * @param  width                    the ASCII text width of the for the column entries (without the string
416      *                                      termination).
417      *
418      * @return                          the number of columns after this one is added.
419      *
420      * @throws IllegalArgumentException if the column data is not an array or the specified text <code>width</code> is
421      *                                      &le;1.
422      * @throws FitsException            if the column us of an unsupported data type or if the number of entries does
423      *                                      not match the number of rows already contained in the table.
424      * 
425      * @see                             #addColumn(Object)
426      */
427     public int addColumn(Object newCol, int width) throws FitsException, IllegalArgumentException {
428         if (width < 1) {
429             throw new IllegalArgumentException("Illegal ASCII column width: " + width);
430         }
431 
432         if (!newCol.getClass().isArray()) {
433             throw new IllegalArgumentException("Not an array: " + newCol.getClass().getName());
434         }
435 
436         if (nFields > 0 && Array.getLength(newCol) != nRows) {
437             throw new FitsException(
438                     "Mismatched number of rows: expected " + nRows + ", got " + Array.getLength(newCol) + "rows.");
439         }
440 
441         if (nFields == 0) {
442             nRows = Array.getLength(newCol);
443         }
444 
445         Class<?> type = ArrayFuncs.getBaseClass(newCol);
446         if (type != int.class && type != long.class && type != float.class && type != double.class
447                 && type != String.class) {
448             throw new FitsException("No AsciiTable support for elements of " + type.getName());
449         }
450 
451         data = Arrays.copyOf(data, nFields + 1);
452         offsets = Arrays.copyOf(offsets, nFields + 1);
453         lengths = Arrays.copyOf(lengths, nFields + 1);
454         types = Arrays.copyOf(types, nFields + 1);
455         nulls = Arrays.copyOf(nulls, nFields + 1);
456         names = Arrays.copyOf(names, nFields + 1);
457 
458         data[nFields] = newCol;
459         offsets[nFields] = rowLen + 1;
460         lengths[nFields] = width;
461         types[nFields] = ArrayFuncs.getBaseClass(newCol);
462         names[nFields] = TableHDU.getDefaultColumnName(nFields);
463 
464         rowLen += width + 1;
465         if (isNull != null) {
466             boolean[] newIsNull = new boolean[nRows * (nFields + 1)];
467             // Fix the null pointers.
468             int add = 0;
469             for (int i = 0; i < isNull.length; i++) {
470                 if (i % nFields == 0) {
471                     add++;
472                 }
473                 if (isNull[i]) {
474                     newIsNull[i + add] = true;
475                 }
476             }
477             isNull = newIsNull;
478         }
479         nFields++;
480 
481         // Invalidate the buffer
482         buffer = null;
483 
484         return nFields;
485     }
486 
487     /**
488      * Beware that adding rows to ASCII tables may be very inefficient. Avoid addding more than a few rows if you can.
489      */
490     @Override
491     public int addRow(Object[] newRow) throws FitsException {
492         try {
493             // If there are no fields, then this is the
494             // first row. We need to add in each of the columns
495             // to get the descriptors set up.
496             if (nFields == 0) {
497                 for (Object element : newRow) {
498                     addColumn(element);
499                 }
500             } else {
501                 for (int i = 0; i < nFields; i++) {
502                     Object o = ArrayFuncs.newInstance(types[i], nRows + 1);
503                     System.arraycopy(data[i], 0, o, 0, nRows);
504                     System.arraycopy(newRow[i], 0, o, nRows, 1);
505                     data[i] = o;
506                 }
507                 nRows++;
508             }
509             // Invalidate the buffer
510             buffer = null;
511             return nRows;
512         } catch (Exception e) {
513             throw new FitsException("Error adding row:" + e.getMessage(), e);
514         }
515     }
516 
517     @Override
518     public void deleteColumns(int start, int len) throws FitsException {
519         ensureData();
520 
521         Object[] newData = new Object[nFields - len];
522         int[] newOffsets = new int[nFields - len];
523         int[] newLengths = new int[nFields - len];
524         Class<?>[] newTypes = new Class[nFields - len];
525         String[] newNulls = new String[nFields - len];
526 
527         // Copy in the initial stuff...
528         System.arraycopy(data, 0, newData, 0, start);
529         // Don't do the offsets here.
530         System.arraycopy(lengths, 0, newLengths, 0, start);
531         System.arraycopy(types, 0, newTypes, 0, start);
532         System.arraycopy(nulls, 0, newNulls, 0, start);
533 
534         // Copy in the final
535         System.arraycopy(data, start + len, newData, start, nFields - start - len);
536         // Don't do the offsets here.
537         System.arraycopy(lengths, start + len, newLengths, start, nFields - start - len);
538         System.arraycopy(types, start + len, newTypes, start, nFields - start - len);
539         System.arraycopy(nulls, start + len, newNulls, start, nFields - start - len);
540 
541         for (int i = start; i < start + len; i++) {
542             rowLen -= lengths[i] + 1;
543         }
544 
545         data = newData;
546         offsets = newOffsets;
547         lengths = newLengths;
548         types = newTypes;
549         nulls = newNulls;
550 
551         if (isNull != null) {
552             boolean found = false;
553 
554             boolean[] newIsNull = new boolean[nRows * (nFields - len)];
555             for (int i = 0; i < nRows; i++) {
556                 int oldOff = nFields * i;
557                 int newOff = (nFields - len) * i;
558                 for (int col = 0; col < start; col++) {
559                     newIsNull[newOff + col] = isNull[oldOff + col];
560                     found = found || isNull[oldOff + col];
561                 }
562                 for (int col = start + len; col < nFields; col++) {
563                     newIsNull[newOff + col - len] = isNull[oldOff + col];
564                     found = found || isNull[oldOff + col];
565                 }
566             }
567             if (found) {
568                 isNull = newIsNull;
569             } else {
570                 isNull = null;
571             }
572         }
573 
574         // Invalidate the buffer
575         buffer = null;
576 
577         nFields -= len;
578     }
579 
580     /**
581      * Beware that repeatedly deleting rows from ASCII tables may be very inefficient. Avoid calling this more than once
582      * (or a few times) if you can.
583      */
584     @Override
585     public void deleteRows(int start, int len) throws FitsException {
586         if (nRows == 0 || start < 0 || start >= nRows || len <= 0) {
587             return;
588         }
589         if (start + len > nRows) {
590             len = nRows - start;
591         }
592 
593         ensureData();
594 
595         for (int i = 0; i < nFields; i++) {
596             try {
597                 Object o = ArrayFuncs.newInstance(types[i], nRows - len);
598                 System.arraycopy(data[i], 0, o, 0, start);
599                 System.arraycopy(data[i], start + len, o, start, nRows - len - start);
600                 data[i] = o;
601             } catch (Exception e) {
602                 throw new FitsException("Error deleting row: " + e.getMessage(), e);
603             }
604 
605         }
606         nRows -= len;
607     }
608 
609     @Override
610     protected void loadData(ArrayDataInput in) throws IOException, FitsException {
611         currInput = in;
612 
613         if (buffer == null) {
614             getBuffer((long) nRows * rowLen, 0);
615         }
616 
617         data = new Object[nFields];
618         for (int i = 0; i < nFields; i++) {
619             data[i] = ArrayFuncs.newInstance(types[i], nRows);
620         }
621 
622         bp.setOffset(0);
623 
624         int rowOffset;
625         for (int i = 0; i < nRows; i++) {
626             rowOffset = rowLen * i;
627             for (int j = 0; j < nFields; j++) {
628                 try {
629                     if (!extractElement(rowOffset + offsets[j], lengths[j], data, j, i, nulls[j])) {
630                         if (isNull == null) {
631                             isNull = new boolean[nRows * nFields];
632                         }
633 
634                         isNull[j + i * nFields] = true;
635                     }
636                 } catch (ArrayIndexOutOfBoundsException e) {
637                     throw new FitsException("not enough data: " + e, e);
638                 }
639             }
640         }
641     }
642 
643     @Override
644     public void read(ArrayDataInput in) throws FitsException {
645         currInput = in;
646         super.read(in);
647     }
648 
649     /**
650      * Move an element from the buffer into a data array.
651      *
652      * @param  offset        The offset within buffer at which the element starts.
653      * @param  length        The number of bytes in the buffer for the element.
654      * @param  array         An array of objects, each of which is a simple array.
655      * @param  col           Which element of array is to be modified?
656      * @param  row           Which index into that element is to be modified?
657      * @param  nullFld       What string signifies a null element?
658      *
659      * @throws FitsException if the operation failed
660      */
661     private boolean extractElement(int offset, int length, Object[] array, int col, int row, String nullFld)
662             throws FitsException {
663 
664         bp.setOffset(offset);
665 
666         if (nullFld != null) {
667             String s = bp.getString(length);
668             if (s.trim().equals(nullFld)) {
669                 return false;
670             }
671             bp.skip(-length);
672         }
673         try {
674             if (array[col] instanceof String[]) {
675                 ((String[]) array[col])[row] = bp.getString(length);
676             } else if (array[col] instanceof int[]) {
677                 ((int[]) array[col])[row] = bp.getInt(length);
678             } else if (array[col] instanceof float[]) {
679                 ((float[]) array[col])[row] = bp.getFloat(length);
680             } else if (array[col] instanceof double[]) {
681                 ((double[]) array[col])[row] = bp.getDouble(length);
682             } else if (array[col] instanceof long[]) {
683                 ((long[]) array[col])[row] = bp.getLong(length);
684             } else {
685                 throw new FitsException("Invalid type for ASCII table conversion:" + array[col]);
686             }
687         } catch (FormatException e) {
688             throw new FitsException("Error parsing data at row,col:" + row + "," + col + "  ", e);
689         }
690         return true;
691     }
692 
693     @Override
694     protected void fillHeader(Header h) {
695         h.deleteKey(Standard.SIMPLE);
696         h.deleteKey(Standard.EXTEND);
697 
698         Standard.context(AsciiTable.class);
699 
700         Cursor<String, HeaderCard> c = h.iterator();
701         c.add(HeaderCard.create(Standard.XTENSION, Standard.XTENSION_ASCIITABLE));
702         c.add(HeaderCard.create(Standard.BITPIX, Bitpix.BYTE.getHeaderValue()));
703         c.add(HeaderCard.create(Standard.NAXIS, 2));
704         c.add(HeaderCard.create(Standard.NAXIS1, rowLen));
705         c.add(HeaderCard.create(Standard.NAXIS2, nRows));
706         c.add(HeaderCard.create(Standard.PCOUNT, 0));
707         c.add(HeaderCard.create(Standard.GCOUNT, 1));
708         c.add(HeaderCard.create(Standard.TFIELDS, nFields));
709 
710         for (int i = 0; i < nFields; i++) {
711             addColInfo(i, c);
712         }
713 
714         Standard.context(null);
715     }
716 
717     /**
718      * Read some data into the buffer.
719      */
720     private void getBuffer(long size, long offset) throws IOException, FitsException {
721 
722         if (currInput == null) {
723             throw new IOException("No stream open to read");
724         }
725 
726         if (size > Integer.MAX_VALUE) {
727             throw new FitsException("Cannot read ASCII table > 2 GB");
728         }
729 
730         buffer = new byte[(int) size];
731         if (offset != 0) {
732             FitsUtil.reposition(currInput, offset);
733         }
734         currInput.readFully(buffer);
735         bp = new ByteParser(buffer);
736     }
737 
738     /**
739      * <p>
740      * Returns the data for a particular column in as a flattened 1D array of elements. See {@link #addColumn(Object)}
741      * for more information about the format of data elements in general.
742      * </p>
743      * 
744      * @param  col           The 0-based column index.
745      * 
746      * @return               an array of primitives (for scalar columns), or else an <code>Object[]</code> array.
747      * 
748      * @throws FitsException if the table could not be accessed
749      *
750      * @see                  #setColumn(int, Object)
751      * @see                  #getElement(int, int)
752      * @see                  #getNCols()
753      */
754     @Override
755     public Object getColumn(int col) throws FitsException {
756         ensureData();
757         return data[col];
758     }
759 
760     @Override
761     @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "intended exposure of mutable data")
762     protected Object[] getCurrentData() {
763         return data;
764     }
765 
766     @Override
767     public Object[] getData() throws FitsException {
768         return (Object[]) super.getData();
769     }
770 
771     @Override
772     public Object getElement(int row, int col) throws FitsException {
773         if (data != null) {
774             return singleElement(row, col);
775         }
776         return parseSingleElement(row, col);
777     }
778 
779     @Override
780     public int getNCols() {
781         return nFields;
782     }
783 
784     @Override
785     public int getNRows() {
786         return nRows;
787     }
788 
789     @Override
790     public Object[] getRow(int row) throws FitsException {
791 
792         if (data != null) {
793             return singleRow(row);
794         }
795         return parseSingleRow(row);
796     }
797 
798     /**
799      * Get the number of bytes in a row
800      *
801      * @return The number of bytes for a single row in the table.
802      */
803     public int getRowLen() {
804         return rowLen;
805     }
806 
807     @Override
808     protected long getTrueSize() {
809         return (long) nRows * rowLen;
810     }
811 
812     /**
813      * Checks if an element is <code>null</code>.
814      *
815      * @param  row The 0-based row
816      * @param  col The 0-based column
817      *
818      * @return     if the given element has been nulled.
819      */
820     public boolean isNull(int row, int col) {
821         if (isNull != null) {
822             return isNull[row * nFields + col];
823         }
824         return false;
825     }
826 
827     /**
828      * Read a single element from the table. This returns an array of dimension 1.
829      *
830      * @throws FitsException if the operation failed
831      */
832     private Object parseSingleElement(int row, int col) throws FitsException {
833 
834         Object[] res = new Object[1];
835         try {
836             getBuffer(lengths[col], getFileOffset() + (long) row * (long) rowLen + offsets[col]);
837         } catch (IOException e) {
838             buffer = null;
839             throw new FitsException("Unable to read element", e);
840         }
841         res[0] = ArrayFuncs.newInstance(types[col], 1);
842 
843         boolean success = extractElement(0, lengths[col], res, 0, 0, nulls[col]);
844         buffer = null;
845 
846         return success ? res[0] : null;
847     }
848 
849     /**
850      * Read a single row from the table. This returns a set of arrays of dimension 1.
851      *
852      * @throws FitsException if the operation failed
853      */
854     private Object[] parseSingleRow(int row) throws FitsException {
855 
856         Object[] res = new Object[nFields];
857 
858         try {
859             getBuffer(rowLen, getFileOffset() + (long) row * (long) rowLen);
860         } catch (IOException e) {
861             throw new FitsException("Unable to read row", e);
862         }
863 
864         for (int i = 0; i < nFields; i++) {
865             res[i] = ArrayFuncs.newInstance(types[i], 1);
866             if (!extractElement(offsets[i], lengths[i], res, i, 0, nulls[i])) {
867                 res[i] = null;
868             }
869         }
870 
871         // Invalidate buffer for future use.
872         buffer = null;
873         return res;
874     }
875 
876     @Override
877     public void setColumn(int col, Object newData) throws FitsException {
878         ensureData();
879         if (col < 0 || col >= nFields || newData.getClass() != data[col].getClass()
880                 || Array.getLength(newData) != Array.getLength(data[col])) {
881             throw new FitsException("Invalid column/column mismatch:" + col);
882         }
883         data[col] = newData;
884 
885         // Invalidate the buffer.
886         buffer = null;
887     }
888 
889     @Override
890     public void setElement(int row, int col, Object newData) throws FitsException {
891         ensureData();
892         try {
893             System.arraycopy(newData, 0, data[col], row, 1);
894         } catch (Exception e) {
895             throw new FitsException("Incompatible element:" + row + "," + col, e);
896         }
897         setNull(row, col, false);
898 
899         // Invalidate the buffer
900         buffer = null;
901 
902     }
903 
904     /**
905      * Mark (or unmark) an element as null. Note that if this FITS file is latter written out, a TNULL keyword needs to
906      * be defined in the corresponding header. This routine does not add an element for String columns.
907      *
908      * @param row  The 0-based row.
909      * @param col  The 0-based column.
910      * @param flag True if the element is to be set to null.
911      */
912     public void setNull(int row, int col, boolean flag) {
913         if (flag) {
914             if (isNull == null) {
915                 isNull = new boolean[nRows * nFields];
916             }
917             isNull[col + row * nFields] = true;
918         } else if (isNull != null) {
919             isNull[col + row * nFields] = false;
920         }
921 
922         // Invalidate the buffer
923         buffer = null;
924     }
925 
926     /**
927      * Set the null string for a columns. This is not a public method since we want users to call the method in
928      * AsciiTableHDU and update the header also.
929      */
930     void setNullString(int col, String newNull) {
931         if (col >= 0 && col < nulls.length) {
932             nulls[col] = newNull;
933         }
934     }
935 
936     @Override
937     public void setRow(int row, Object[] newData) throws FitsException {
938         if (row < 0 || row > nRows) {
939             throw new FitsException("Invalid row in setRow");
940         }
941         ensureData();
942         for (int i = 0; i < nFields; i++) {
943             try {
944                 System.arraycopy(newData[i], 0, data[i], row, 1);
945             } catch (Exception e) {
946                 throw new FitsException("Unable to modify row: incompatible data:" + row, e);
947             }
948             setNull(row, i, false);
949         }
950 
951         // Invalidate the buffer
952         buffer = null;
953 
954     }
955 
956     /**
957      * Extract a single element from a table. This returns an array of length 1.
958      */
959     private Object singleElement(int row, int col) {
960 
961         Object res = null;
962         if (isNull == null || !isNull[row * nFields + col]) {
963             res = ArrayFuncs.newInstance(types[col], 1);
964             System.arraycopy(data[col], row, res, 0, 1);
965         }
966         return res;
967     }
968 
969     /**
970      * Extract a single row from a table. This returns an array of Objects each of which is an array of length 1.
971      */
972     private Object[] singleRow(int row) {
973 
974         Object[] res = new Object[nFields];
975         for (int i = 0; i < nFields; i++) {
976             if (isNull == null || !isNull[row * nFields + i]) {
977                 res[i] = ArrayFuncs.newInstance(types[i], 1);
978                 System.arraycopy(data[i], row, res[i], 0, 1);
979             }
980         }
981         return res;
982     }
983 
984     /**
985      * @deprecated It is not entirely foolproof for keeping the header in sync -- it is better to (re)wrap tables in a
986      *                 new HDU and editing the header as necessary to incorporate custom entries. May be removed from
987      *                 the API in the future.
988      */
989     @Override
990     public void updateAfterDelete(int oldNCol, Header hdr) throws FitsException {
991 
992         int offset = 0;
993         for (int i = 0; i < nFields; i++) {
994             offsets[i] = offset;
995             hdr.addValue(TBCOLn.n(i + 1), offset + 1);
996             offset += lengths[i] + 1;
997         }
998         for (int i = nFields; i < oldNCol; i++) {
999             hdr.deleteKey(TBCOLn.n(i + 1));
1000         }
1001 
1002         hdr.addValue(NAXIS1, rowLen);
1003     }
1004 
1005     @Override
1006     public void write(ArrayDataOutput str) throws FitsException {
1007         // Make sure we have the data in hand.
1008         if (str != currInput) {
1009             ensureData();
1010         }
1011 
1012         // If buffer is still around we can just reuse it,
1013         // since nothing we've done has invalidated it.
1014         if (data == null) {
1015             throw new FitsException("Attempt to write undefined ASCII Table");
1016         }
1017 
1018         if ((long) nRows * rowLen > Integer.MAX_VALUE) {
1019             throw new FitsException("Cannot write ASCII table > 2 GB");
1020         }
1021 
1022         buffer = new byte[nRows * rowLen];
1023 
1024         bp = new ByteParser(buffer);
1025         for (int i = 0; i < buffer.length; i++) {
1026             buffer[i] = (byte) ' ';
1027         }
1028 
1029         ByteFormatter bf = new ByteFormatter();
1030 
1031         for (int i = 0; i < nRows; i++) {
1032 
1033             for (int j = 0; j < nFields; j++) {
1034                 int offset = i * rowLen + offsets[j];
1035                 int len = lengths[j];
1036                 if (isNull != null && isNull[i * nFields + j]) {
1037                     if (nulls[j] == null) {
1038                         throw new FitsException("No null value set when needed");
1039                     }
1040                     bf.format(nulls[j], buffer, offset, len);
1041                 } else if (types[j] == String.class) {
1042                     String[] s = (String[]) data[j];
1043                     bf.format(s[i], buffer, offset, len);
1044                 } else if (types[j] == int.class) {
1045                     int[] ia = (int[]) data[j];
1046                     bf.format(ia[i], buffer, offset, len);
1047                 } else if (types[j] == float.class) {
1048                     float[] fa = (float[]) data[j];
1049                     bf.format(fa[i], buffer, offset, len);
1050                 } else if (types[j] == double.class) {
1051                     double[] da = (double[]) data[j];
1052                     bf.format(da[i], buffer, offset, len);
1053                 } else if (types[j] == long.class) {
1054                     long[] la = (long[]) data[j];
1055                     bf.format(la[i], buffer, offset, len);
1056                 }
1057             }
1058         }
1059 
1060         // Now write the buffer.
1061         try {
1062             str.write(buffer);
1063             FitsUtil.pad(str, buffer.length, (byte) ' ');
1064         } catch (IOException e) {
1065             throw new FitsException("Error writing ASCII Table data", e);
1066         }
1067     }
1068 
1069     @Override
1070     public AsciiTableHDU toHDU() {
1071         Header h = new Header();
1072         fillHeader(h);
1073         return new AsciiTableHDU(h, this);
1074     }
1075 
1076     /**
1077      * <p>
1078      * Controls how columns with format "<code>I10</code>" are handled; this is tricky because some, but not all,
1079      * integers that can be represented in 10 characters form 32-bit integers. Setting it <code>true</code> may make it
1080      * more likely to avoid unexpected type changes during round-tripping, but it also means that some values in I10
1081      * columns may be impossible to read. The default behavior is to assume <code>true</code>, and thus to treat I10
1082      * columns as <code>int</code> values.
1083      * </p>
1084      * 
1085      * @param value if <code>true</code>, format "I10" columns will be assumed <code>int.class</code>, provided
1086      *                  TLMINn/TLMAXn or TDMINn/TDMAXn limits (if defined) allow it. if <code>false</code>, I10 columns
1087      *                  that have no clear indication of data range will be assumed <code>long.class</code>.
1088      *
1089      * @since       1.19
1090      * 
1091      * @see         AsciiTable#isI10PreferInt()
1092      */
1093     public static void setI10PreferInt(boolean value) {
1094         isI10PreferInt = value;
1095     }
1096 
1097     /**
1098      * Checks if I10 columns should be treated as containing 32-bit <code>int</code> values, rather than 64-bit
1099      * <code>long</code> values, when possible.
1100      * 
1101      * @return <code>true</code> if I10 columns should be treated as containing 32-bit <code>int</code> values,
1102      *             otherwise <code>false</code>.
1103      * 
1104      * @since  1.19
1105      * 
1106      * @see    #setI10PreferInt(boolean)
1107      */
1108     public static boolean isI10PreferInt() {
1109         return isI10PreferInt;
1110     }
1111 }