View Javadoc
1   /*
2    * #%L
3    * nom.tam FITS library
4    * %%
5    * Copyright (C) 2004 - 2024 nom-tam-fits
6    * %%
7    * This is free and unencumbered software released into the public domain.
8    *
9    * Anyone is free to copy, modify, publish, use, compile, sell, or
10   * distribute this software, either in source code form or as a compiled
11   * binary, for any purpose, commercial or non-commercial, and by any
12   * means.
13   *
14   * In jurisdictions that recognize copyright laws, the author or authors
15   * of this software dedicate any and all copyright interest in the
16   * software to the public domain. We make this dedication for the benefit
17   * of the public at large and to the detriment of our heirs and
18   * successors. We intend this dedication to be an overt act of
19   * relinquishment in perpetuity of all present and future rights to this
20   * software under copyright law.
21   *
22   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
25   * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
26   * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
27   * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
28   * OTHER DEALINGS IN THE SOFTWARE.
29   * #L%
30   */
31  
32  package nom.tam.fits;
33  
34  import java.io.ByteArrayInputStream;
35  import java.io.EOFException;
36  import java.io.IOException;
37  import java.math.BigDecimal;
38  import java.math.BigInteger;
39  import java.util.Arrays;
40  import java.util.logging.Logger;
41  
42  import nom.tam.fits.FitsFactory.FitsSettings;
43  import nom.tam.fits.header.IFitsHeader;
44  import nom.tam.fits.header.NonStandard;
45  import nom.tam.fits.header.Standard;
46  import nom.tam.fits.header.hierarch.IHierarchKeyFormatter;
47  import nom.tam.util.ArrayDataInput;
48  import nom.tam.util.AsciiFuncs;
49  import nom.tam.util.ComplexValue;
50  import nom.tam.util.CursorValue;
51  import nom.tam.util.FitsInputStream;
52  import nom.tam.util.FlexFormat;
53  import nom.tam.util.InputReader;
54  
55  import static nom.tam.fits.header.Standard.BLANKS;
56  import static nom.tam.fits.header.Standard.COMMENT;
57  import static nom.tam.fits.header.Standard.CONTINUE;
58  import static nom.tam.fits.header.Standard.HISTORY;
59  
60  /**
61   * An individual entry in the FITS header, such as a key/value pair with an optional comment field, or a comment-style
62   * entry without a value field.
63   */
64  public class HeaderCard implements CursorValue<String>, Cloneable {
65  
66      private static final Logger LOG = Logger.getLogger(HeaderCard.class.getName());
67  
68      /** The number of characters per header card (line). */
69      public static final int FITS_HEADER_CARD_SIZE = 80;
70  
71      /** Maximum length of a FITS keyword field */
72      public static final int MAX_KEYWORD_LENGTH = 8;
73  
74      /** The length of two single quotes that must surround string values. */
75      public static final int STRING_QUOTES_LENGTH = 2;
76  
77      /** Maximum length of a FITS value field. */
78      public static final int MAX_VALUE_LENGTH = 70;
79  
80      /** Maximum length of a comment-style card comment field. */
81      public static final int MAX_COMMENT_CARD_COMMENT_LENGTH = MAX_VALUE_LENGTH + 1;
82  
83      /** Maximum length of a FITS string value field. */
84      public static final int MAX_STRING_VALUE_LENGTH = MAX_VALUE_LENGTH - 2;
85  
86      /** Maximum length of a FITS long string value field. the &amp; for the continuation needs one char. */
87      public static final int MAX_LONG_STRING_VALUE_LENGTH = MAX_STRING_VALUE_LENGTH - 1;
88  
89      /** if a commend needs the be specified 2 extra chars are needed to start the comment */
90      public static final int MAX_LONG_STRING_VALUE_WITH_COMMENT_LENGTH = MAX_LONG_STRING_VALUE_LENGTH - 2;
91  
92      /** Maximum HIERARCH keyword length (80 chars must fit [&lt;keyword&gt;=T] at minimum... */
93      public static final int MAX_HIERARCH_KEYWORD_LENGTH = FITS_HEADER_CARD_SIZE - 2;
94  
95      /** The start and end quotes of the string and the ampasant to continue the string. */
96      public static final int MAX_LONG_STRING_CONTINUE_OVERHEAD = 3;
97  
98      /** The first ASCII character that may be used in header records */
99      public static final char MIN_VALID_CHAR = 0x20;
100 
101     /** The last ASCII character that may be used in header records */
102     public static final char MAX_VALID_CHAR = 0x7e;
103 
104     /** The default keyword to use instead of null or any number of blanks. */
105     public static final String EMPTY_KEY = "";
106 
107     /** The string "HIERARCH." */
108     private static final String HIERARCH_WITH_DOT = NonStandard.HIERARCH.key() + ".";
109 
110     /** The keyword part of the card (set to null if there's no keyword) */
111     private String key;
112 
113     /** The keyword part of the card (set to null if there's no value / empty string) */
114     private String value;
115 
116     /** The comment part of the card (set to null if there's no comment) */
117     private String comment;
118 
119     private IFitsHeader standardKey;
120 
121     /**
122      * The Java class associated to the value
123      *
124      * @since 1.16
125      */
126     private Class<?> type;
127 
128     /** For thread synchronization */
129     private Object lock = new Object();
130 
131     /**
132      * Value type checking policies for when setting values for standardized keywords.
133      * 
134      * @author Attila Kovacs
135      * 
136      * @since  1.19
137      */
138     public enum ValueCheck {
139         /** No value type checking will be performed */
140         NONE,
141         /** Attempting to set values of the wrong type for standardized keywords will log warnings */
142         LOGGING,
143         /** Throw exception when setting a value of the wrong type for a standardized keyword */
144         EXCEPTION
145     }
146 
147     /**
148      * Default value type checking policy for cards with standardized {@link IFitsHeader} keywords.
149      * 
150      * @since 1.19
151      */
152     public static final ValueCheck DEFAULT_VALUE_CHECK_POLICY = ValueCheck.EXCEPTION;
153 
154     private static ValueCheck valueCheck = DEFAULT_VALUE_CHECK_POLICY;
155 
156     /** Private constructor for an empty card, used by other constructors. */
157     private HeaderCard() {
158     }
159 
160     /**
161      * Creates a new header card, but reading from the specified data input stream. The card is expected to be describes
162      * by one or more 80-character wide header 'lines'. If long string support is not enabled, then a new card is
163      * created from the next 80-characters. When long string support is enabled, cunsecutive lines starting with
164      * [<code>CONTINUE </code>] after the first line will be aggregated into a single new card.
165      *
166      * @param  dis                    the data input stream
167      *
168      * @throws UnclosedQuoteException if the line contained an unclosed single quote.
169      * @throws TruncatedFileException if we reached the end of file unexpectedly before fully parsing an 80-character
170      *                                    line.
171      * @throws IOException            if there was some IO issue.
172      *
173      * @see                           FitsFactory#setLongStringsEnabled(boolean)
174      */
175     @SuppressWarnings("deprecation")
176     public HeaderCard(ArrayDataInput dis) throws UnclosedQuoteException, TruncatedFileException, IOException {
177         this(new HeaderCardCountingArrayDataInput(dis));
178     }
179 
180     /**
181      * Creates a new header card, but reading from the specified data input. The card is expected to be describes by one
182      * or more 80-character wide header 'lines'. If long string support is not enabled, then a new card is created from
183      * the next 80-characters. When long string support is enabled, cunsecutive lines starting with
184      * [<code>CONTINUE </code>] after the first line will be aggregated into a single new card.
185      * 
186      * @deprecated                        (<i>for internal use</i>) Its visibility may be reduced or may be removed
187      *                                        entirely in the future. Card counting should be internal to
188      *                                        {@link HeaderCard}.
189      *
190      * @param      dis                    the data input
191      *
192      * @throws     UnclosedQuoteException if the line contained an unclosed single quote.
193      * @throws     TruncatedFileException if we reached the end of file unexpectedly before fully parsing an
194      *                                        80-character line.
195      * @throws     IOException            if there was some IO issue.
196      *
197      * @see                               #HeaderCard(ArrayDataInput)
198      * @see                               FitsFactory#setLongStringsEnabled(boolean)
199      */
200     @Deprecated
201     public HeaderCard(HeaderCardCountingArrayDataInput dis)
202             throws UnclosedQuoteException, TruncatedFileException, IOException {
203         this();
204         key = null;
205         value = null;
206         comment = null;
207         type = null;
208 
209         String card = readOneHeaderLine(dis);
210         HeaderCardParser parsed = new HeaderCardParser(card);
211 
212         // extract the key
213         key = parsed.getKey();
214         type = parsed.getInferredType();
215 
216         if (FitsFactory.isLongStringsEnabled() && parsed.isString() && parsed.getValue().endsWith("&")) {
217             // Potentially a multi-record long string card...
218             parseLongStringCard(dis, parsed);
219         } else {
220             value = parsed.getValue();
221             type = parsed.getInferredType();
222             comment = parsed.getTrimmedComment();
223         }
224 
225     }
226 
227     /**
228      * Creates a new card with a number value. The card will be created either in the integer, fixed-decimal, or format,
229      * with the native precision. If the native precision cannot be fitted in the available card space, the value will
230      * be represented with reduced precision with at least {@link FlexFormat#DOUBLE_DECIMALS}. Trailing zeroes will be
231      * omitted.
232      *
233      * @param  key                 keyword
234      * @param  value               value (can be <code>null</code>, in which case the card type defaults to
235      *                                 <code>Integer.class</code>)
236      *
237      * @throws HeaderCardException for any invalid keyword or value.
238      *
239      * @since                      1.16
240      *
241      * @see                        #HeaderCard(String, Number, String)
242      * @see                        #HeaderCard(String, Number, int, String)
243      * @see                        #create(IFitsHeader, Number)
244      * @see                        FitsFactory#setUseExponentD(boolean)
245      */
246     public HeaderCard(String key, Number value) throws HeaderCardException {
247         this(key, value, FlexFormat.AUTO_PRECISION, null);
248     }
249 
250     /**
251      * Creates a new card with a number value and a comment. The card will be created either in the integer,
252      * fixed-decimal, or format. If the native precision cannot be fitted in the available card space, the value will be
253      * represented with reduced precision with at least {@link FlexFormat#DOUBLE_DECIMALS}. Trailing zeroes will be
254      * omitted.
255      *
256      * @param  key                 keyword
257      * @param  value               value (can be <code>null</code>, in which case the card type defaults to
258      *                                 <code>Integer.class</code>)
259      * @param  comment             optional comment, or <code>null</code>
260      *
261      * @throws HeaderCardException for any invalid keyword or value
262      *
263      * @see                        #HeaderCard(String, Number)
264      * @see                        #HeaderCard(String, Number, int, String)
265      * @see                        #create(IFitsHeader, Number)
266      * @see                        FitsFactory#setUseExponentD(boolean)
267      */
268     public HeaderCard(String key, Number value, String comment) throws HeaderCardException {
269         this(key, value, FlexFormat.AUTO_PRECISION, comment);
270     }
271 
272     /**
273      * Creates a new card with a number value, using scientific notation, with up to the specified decimal places
274      * showing between the decimal place and the exponent. For example, if <code>decimals</code> is set to 2, then
275      * {@link Math#PI} gets formatted as <code>3.14E0</code> (or <code>3.14D0</code> if
276      * {@link FitsFactory#setUseExponentD(boolean)} is enabled).
277      *
278      * @param  key                 keyword
279      * @param  value               value (can be <code>null</code>, in which case the card type defaults to
280      *                                 <code>Integer.class</code>)
281      * @param  decimals            the number of decimal places to show in the scientific notation.
282      * @param  comment             optional comment, or <code>null</code>
283      *
284      * @throws HeaderCardException for any invalid keyword or value
285      *
286      * @see                        #HeaderCard(String, Number)
287      * @see                        #HeaderCard(String, Number, String)
288      * @see                        #create(IFitsHeader, Number)
289      * @see                        FitsFactory#setUseExponentD(boolean)
290      */
291     public HeaderCard(String key, Number value, int decimals, String comment) throws HeaderCardException {
292         if (value == null) {
293             set(key, null, comment, Integer.class);
294             return;
295         }
296 
297         try {
298             checkNumber(value);
299         } catch (NumberFormatException e) {
300             throw new HeaderCardException("FITS headers may not contain NaN or Infinite values", e);
301         }
302         set(key, new FlexFormat().setWidth(spaceForValue(key)).setPrecision(decimals).format(value), comment,
303                 value.getClass());
304     }
305 
306     /**
307      * Creates a new card with a boolean value (and no comment).
308      *
309      * @param  key                 keyword
310      * @param  value               value (can be <code>null</code>)
311      *
312      * @throws HeaderCardException for any invalid keyword
313      *
314      * @see                        #HeaderCard(String, Boolean, String)
315      * @see                        #create(IFitsHeader, Boolean)
316      */
317     public HeaderCard(String key, Boolean value) throws HeaderCardException {
318         this(key, value, null);
319     }
320 
321     /**
322      * Creates a new card with a boolean value, and a comment.
323      *
324      * @param  key                 keyword
325      * @param  value               value (can be <code>null</code>)
326      * @param  comment             optional comment, or <code>null</code>
327      *
328      * @throws HeaderCardException for any invalid keyword or value
329      *
330      * @see                        #HeaderCard(String, Boolean)
331      * @see                        #create(IFitsHeader, Boolean)
332      */
333     public HeaderCard(String key, Boolean value, String comment) throws HeaderCardException {
334         this(key, value == null ? null : (value ? "T" : "F"), comment, Boolean.class);
335     }
336 
337     /**
338      * Creates a new card with a complex value. The real and imaginary parts will be shown either in the fixed decimal
339      * format or in the exponential notation, whichever preserves more digits, or else whichever is the more compact
340      * notation. Trailing zeroes will be omitted.
341      *
342      * @param  key                 keyword
343      * @param  value               value (can be <code>null</code>)
344      *
345      * @throws HeaderCardException for any invalid keyword or value.
346      *
347      * @see                        #HeaderCard(String, ComplexValue, String)
348      * @see                        #HeaderCard(String, ComplexValue, int, String)
349      */
350     public HeaderCard(String key, ComplexValue value) throws HeaderCardException {
351         this(key, value, null);
352     }
353 
354     /**
355      * Creates a new card with a complex value and a comment. The real and imaginary parts will be shown either in the
356      * fixed decimal format or in the exponential notation, whichever preserves more digits, or else whichever is the
357      * more compact notation. Trailing zeroes will be omitted.
358      *
359      * @param  key                 keyword
360      * @param  value               value (can be <code>null</code>)
361      * @param  comment             optional comment, or <code>null</code>
362      *
363      * @throws HeaderCardException for any invalid keyword or value.
364      *
365      * @see                        #HeaderCard(String, ComplexValue)
366      * @see                        #HeaderCard(String, ComplexValue, int, String)
367      */
368     public HeaderCard(String key, ComplexValue value, String comment) throws HeaderCardException {
369         this();
370 
371         if (value == null) {
372             set(key, null, comment, ComplexValue.class);
373             return;
374         }
375 
376         if (!value.isFinite()) {
377             throw new HeaderCardException("Cannot represent " + value + " in FITS headers.");
378         }
379         set(key, value.toBoundedString(spaceForValue(key)), comment, ComplexValue.class);
380     }
381 
382     /**
383      * Creates a new card with a complex number value, using scientific (exponential) notation, with up to the specified
384      * number of decimal places showing between the decimal point and the exponent. Trailing zeroes will be omitted. For
385      * example, if <code>decimals</code> is set to 2, then (&pi;, 12) gets formatted as <code>(3.14E0,1.2E1)</code>.
386      *
387      * @param  key                 keyword
388      * @param  value               value (can be <code>null</code>)
389      * @param  decimals            the number of decimal places to show.
390      * @param  comment             optional comment, or <code>null</code>
391      *
392      * @throws HeaderCardException for any invalid keyword or value.
393      *
394      * @see                        #HeaderCard(String, ComplexValue)
395      * @see                        #HeaderCard(String, ComplexValue, String)
396      */
397     public HeaderCard(String key, ComplexValue value, int decimals, String comment) throws HeaderCardException {
398         this();
399 
400         if (value == null) {
401             set(key, null, comment, ComplexValue.class);
402             return;
403         }
404 
405         if (!value.isFinite()) {
406             throw new HeaderCardException("Cannot represent " + value + " in FITS headers.");
407         }
408         set(key, value.toString(decimals), comment, ComplexValue.class);
409     }
410 
411     /**
412      * <p>
413      * This constructor is now <b>DEPRECATED</b>. You should use {@link #HeaderCard(String, String, String)} to create
414      * cards with <code>null</code> strings, or else {@link #createCommentStyleCard(String, String)} to create any
415      * comment-style card, or {@link #createCommentCard(String)} or {@link #createHistoryCard(String)} to create COMMENT
416      * or HISTORY cards.
417      * </p>
418      * <p>
419      * Creates a card with a string value or comment.
420      * </p>
421      *
422      * @param      key                 The key for the comment or nullable field.
423      * @param      comment             The comment
424      * @param      withNullValue       If <code>true</code> the new card will be a value stle card with a null string
425      *                                     value. Otherwise it's a comment-style card.
426      *
427      * @throws     HeaderCardException for any invalid keyword or value
428      *
429      * @see                            #HeaderCard(String, String, String)
430      * @see                            #createCommentStyleCard(String, String)
431      * @see                            #createCommentCard(String)
432      * @see                            #createHistoryCard(String)
433      *
434      * @deprecated                     Use {@link #HeaderCard(String, String, String)}, or
435      *                                     {@link #createCommentStyleCard(String, String)} instead.
436      */
437     @Deprecated
438     public HeaderCard(String key, String comment, boolean withNullValue) throws HeaderCardException {
439         this(key, null, comment, withNullValue);
440     }
441 
442     /**
443      * <p>
444      * This constructor is now <b>DEPRECATED</b>. It has always been a poor construct. You should use
445      * {@link #HeaderCard(String, String, String)} to create cards with <code>null</code> strings, or else
446      * {@link #createCommentStyleCard(String, String)} to create any comment-style card, or
447      * {@link #createCommentCard(String)} or {@link #createHistoryCard(String)} to create COMMENT or HISTORY cards.
448      * </p>
449      * Creates a comment style card. This may be a comment style card in which case the nullable field should be false,
450      * or a value field which has a null value, in which case the nullable field should be true.
451      *
452      * @param      key                 The key for the comment or nullable field.
453      * @param      value               The value (can be <code>null</code>)
454      * @param      comment             The comment
455      * @param      nullable            If <code>true</code> a null value is a valid value. Otherwise, a
456      *                                     <code>null</code> value turns this into a comment-style card.
457      *
458      * @throws     HeaderCardException for any invalid keyword or value
459      *
460      * @see                            #HeaderCard(String, String, String)
461      * @see                            #createCommentStyleCard(String, String)
462      * @see                            #createCommentCard(String)
463      * @see                            #createHistoryCard(String)
464      *
465      * @deprecated                     Use {@link #HeaderCard(String, String, String)}, or
466      *                                     {@link #createCommentStyleCard(String, String)} instead.
467      */
468     @Deprecated
469     public HeaderCard(String key, String value, String comment, boolean nullable) throws HeaderCardException {
470         this(key, value, comment, (nullable || value != null) ? String.class : null);
471     }
472 
473     /**
474      * Creates a new card with a string value (and no comment).
475      *
476      * @param  key                 keyword
477      * @param  value               value
478      *
479      * @throws HeaderCardException for any invalid keyword or value
480      *
481      * @see                        #HeaderCard(String, String, String)
482      * @see                        #create(IFitsHeader, String)
483      */
484     public HeaderCard(String key, String value) throws HeaderCardException {
485         this(key, value, null, String.class);
486     }
487 
488     /**
489      * Creates a new card with a string value, and a comment
490      *
491      * @param  key                 keyword
492      * @param  value               value
493      * @param  comment             optional comment, or <code>null</code>
494      *
495      * @throws HeaderCardException for any invalid keyword or value
496      *
497      * @see                        #HeaderCard(String, String)
498      * @see                        #create(IFitsHeader, String)
499      */
500     public HeaderCard(String key, String value, String comment) throws HeaderCardException {
501         this(key, value, comment, String.class);
502     }
503 
504     /**
505      * Creates a new card from its component parts. Use locally only...
506      *
507      * @param  key                 Case-sensitive keyword (can be null for COMMENT)
508      * @param  value               the serialized value (tailing spaces will be removed)
509      * @param  comment             an optional comment or null.
510      * @param  type                The Java class from which the value field was derived, or null if it's a
511      *                                 comment-style card with a null value.
512      *
513      * @throws HeaderCardException for any invalid keyword or value
514      *
515      * @see                        #set(String, String, String, Class)
516      */
517     private HeaderCard(String key, String value, String comment, Class<?> type) throws HeaderCardException {
518         if (key != null) {
519             key = trimEnd(key);
520         }
521 
522         if (key == null || key.isEmpty() || key.equals(Standard.COMMENT.key()) || key.equals(Standard.HISTORY.key())) {
523             if (value != null) {
524                 throw new HeaderCardException("Standard commentary keywords may not have an assigned value.");
525             }
526 
527             // Force comment
528             type = null;
529         }
530 
531         set(key, value, comment, type);
532         this.type = type;
533     }
534 
535     /**
536      * Sets all components of the card to the specified values. For internal use only.
537      *
538      * @param  aKey                Case-sensitive keyword (can be <code>null</code> for an unkeyed comment)
539      * @param  aValue              the serialized value (tailing spaces will be removed), or <code>null</code>
540      * @param  aComment            an optional comment or <code>null</code>.
541      * @param  aType               The Java class from which the value field was derived, or null if it's a
542      *                                 comment-style card.
543      *
544      * @throws HeaderCardException for any invalid keyword or value
545      */
546     private void set(String aKey, String aValue, String aComment, Class<?> aType) throws HeaderCardException {
547         // TODO we never call with null type and non-null value internally, so this is dead code here...
548         // if (aType == null && aValue != null) {
549         // throw new HeaderCardException("Null type for value: [" + sanitize(aValue) + "]");
550         // }
551 
552         synchronized (lock) {
553             type = aType;
554 
555             // Remove trailing spaces
556             if (aKey != null) {
557                 aKey = trimEnd(aKey);
558             }
559 
560             // AK: Map null and blank keys to BLANKS.key()
561             // This simplifies things as we won't have to check for null keys separately!
562             if ((aKey == null) || aKey.isEmpty()) {
563                 aKey = EMPTY_KEY;
564             }
565 
566             try {
567                 validateKey(aKey);
568             } catch (RuntimeException e) {
569                 throw new HeaderCardException("Invalid FITS keyword: [" + sanitize(aKey) + "]", e);
570             }
571 
572             key = aKey;
573 
574             try {
575                 validateChars(aComment);
576             } catch (IllegalArgumentException e) {
577                 throw new HeaderCardException("Invalid FITS comment: [" + sanitize(aComment) + "]", e);
578             }
579 
580             comment = aComment;
581 
582             try {
583                 validateChars(aValue);
584             } catch (IllegalArgumentException e) {
585                 throw new HeaderCardException("Invalid FITS value: [" + sanitize(aValue) + "]", e);
586             }
587 
588             if (aValue == null) {
589                 value = null;
590                 return;
591             }
592             if (isStringValue()) {
593                 try {
594                     setValue(aValue);
595                 } catch (Exception e) {
596                     throw new HeaderCardException("Value too long: [" + sanitize(aValue) + "]", e);
597                 }
598             } else {
599                 aValue = aValue.trim();
600 
601                 // Check that the value fits in the space available for it.
602                 if (aValue.length() > spaceForValue()) {
603                     throw new HeaderCardException("Value too long: [" + sanitize(aValue) + "]",
604                             new LongValueException(key, spaceForValue()));
605                 }
606 
607                 value = aValue;
608             }
609         }
610     }
611 
612     @Override
613     protected HeaderCard clone() {
614         try {
615             HeaderCard clone = (HeaderCard) super.clone();
616             clone.lock = new Object();
617             return clone;
618         } catch (CloneNotSupportedException e) {
619             return null;
620         }
621     }
622 
623     /**
624      * Returns the number of 80-character header lines needed to store the data from this card.
625      *
626      * @return the size of the card in blocks of 80 bytes. So normally every card will return 1. only long stings can
627      *             return more than one, provided support for long string is enabled.
628      */
629     public int cardSize() {
630         synchronized (lock) {
631             if (FitsFactory.isLongStringsEnabled() && isStringValue() && value != null) {
632                 // this is very bad for performance but it is to difficult to
633                 // keep the cardSize and the toString compatible at all times
634                 return toString().length() / FITS_HEADER_CARD_SIZE;
635             }
636             return 1;
637         }
638     }
639 
640     /**
641      * Returns an independent copy of this card. Both this card and the returned value will have identical content, but
642      * modifying one is guaranteed to not affect the other.
643      *
644      * @return a copy of this carf.
645      */
646     public HeaderCard copy() {
647         HeaderCard copy = clone();
648         return copy;
649     }
650 
651     /**
652      * Returns the keyword component of this card, which may be empty but never <code>null</code>, but it may be an
653      * empty string.
654      *
655      * @return the keyword from this card, guaranteed to be not <code>null</code>).
656      *
657      * @see    #getValue()
658      * @see    #getComment()
659      */
660     @Override
661     public final String getKey() {
662         synchronized (lock) {
663             return key;
664         }
665     }
666 
667     /**
668      * Returns the serialized value component of this card, which may be null.
669      *
670      * @return the value from this card
671      *
672      * @see    #getValue(Class, Object)
673      * @see    #getKey()
674      * @see    #getComment()
675      */
676     public final String getValue() {
677         synchronized (lock) {
678             return value;
679         }
680     }
681 
682     /**
683      * Returns the comment component of this card, which may be null.
684      *
685      * @return the comment from this card
686      *
687      * @see    #getKey()
688      * @see    #getValue()
689      */
690     public final String getComment() {
691         synchronized (lock) {
692             return comment;
693         }
694     }
695 
696     /**
697      * @deprecated                       Not supported by the FITS standard, so do not use. It was included due to a
698      *                                       misreading of the standard itself.
699      *
700      * @return                           the value from this card
701      *
702      * @throws     NumberFormatException if the card's value is null or cannot be parsed as a hexadecimal value.
703      *
704      * @see                              #getValue()
705      */
706     @Deprecated
707     public final long getHexValue() throws NumberFormatException {
708         synchronized (lock) {
709             if (value == null) {
710                 throw new NumberFormatException("Card has a null value");
711             }
712             return Long.decode("0x" + value);
713         }
714     }
715 
716     /**
717      * <p>
718      * Returns the value cast to the specified type, if possible, or the specified default value if the value is
719      * <code>null</code> or if the value is incompatible with the requested type.
720      * </p>
721      * <p>
722      * For number types and values, if the requested type has lesser range or precision than the number stored in the
723      * FITS header, the value is automatically downcast (i.e. possible rounded and/or truncated) -- the same as if an
724      * explicit cast were used in Java. As long as the header value is a proper decimal value, it will be returned as
725      * any requested number type.
726      * </p>
727      *
728      * @param  asType                   the requested class of the value
729      * @param  defaultValue             the value to use if the card has a null value, or a value that cannot be cast to
730      *                                      the specified type.
731      * @param  <T>                      the generic type of the requested class
732      *
733      * @return                          the value from this card as a specific type, or the specified default value
734      *
735      * @throws IllegalArgumentException if the specified Java type of not one that is supported for use in FITS headers.
736      */
737     public <T> T getValue(Class<T> asType, T defaultValue) throws IllegalArgumentException {
738         synchronized (lock) {
739             if (value == null) {
740                 return defaultValue;
741             }
742             if (String.class.isAssignableFrom(asType)) {
743                 return asType.cast(value);
744             }
745             if (value.isEmpty()) {
746                 return defaultValue;
747             }
748             if (Boolean.class.isAssignableFrom(asType)) {
749                 return asType.cast(getBooleanValue((Boolean) defaultValue));
750             }
751             if (ComplexValue.class.isAssignableFrom(asType)) {
752                 return asType.cast(new ComplexValue(value));
753             }
754             if (Number.class.isAssignableFrom(asType)) {
755                 try {
756                     BigDecimal big = new BigDecimal(value.toUpperCase().replace('D', 'E'));
757 
758                     if (Byte.class.isAssignableFrom(asType)) {
759                         return asType.cast(big.byteValue());
760                     }
761                     if (Short.class.isAssignableFrom(asType)) {
762                         return asType.cast(big.shortValue());
763                     }
764                     if (Integer.class.isAssignableFrom(asType)) {
765                         return asType.cast(big.intValue());
766                     }
767                     if (Long.class.isAssignableFrom(asType)) {
768                         return asType.cast(big.longValue());
769                     }
770                     if (Float.class.isAssignableFrom(asType)) {
771                         return asType.cast(big.floatValue());
772                     }
773                     if (Double.class.isAssignableFrom(asType)) {
774                         return asType.cast(big.doubleValue());
775                     }
776                     if (BigInteger.class.isAssignableFrom(asType)) {
777                         return asType.cast(big.toBigInteger());
778                     }
779                     // All possibilities have been exhausted, it must be a BigDecimal...
780                     return asType.cast(big);
781                 } catch (NumberFormatException e) {
782                     // The value is not a decimal number, so return the default value by contract.
783                     return defaultValue;
784                 }
785             }
786         }
787 
788         throw new IllegalArgumentException("unsupported class " + asType);
789     }
790 
791     /**
792      * Checks if this card has both a valid keyword and a (non-null) value.
793      *
794      * @return Is this a key/value card?
795      *
796      * @see    #isCommentStyleCard()
797      */
798     public boolean isKeyValuePair() {
799         synchronized (lock) {
800             return !isCommentStyleCard() && value != null;
801         }
802     }
803 
804     /**
805      * Checks if this card has a string value (which may be <code>null</code>).
806      *
807      * @return <code>true</code> if this card has a string value, otherwise <code>false</code>.
808      *
809      * @see    #isDecimalType()
810      * @see    #isIntegerType()
811      * @see    #valueType()
812      */
813     public boolean isStringValue() {
814         synchronized (lock) {
815             if (type == null) {
816                 return false;
817             }
818             return String.class.isAssignableFrom(type);
819         }
820     }
821 
822     /**
823      * Checks if this card has a decimal (floating-point) type value (which may be <code>null</code>).
824      *
825      * @return <code>true</code> if this card has a decimal (not integer) type number value, otherwise
826      *             <code>false</code>.
827      *
828      * @see    #isIntegerType()
829      * @see    #isStringValue()
830      * @see    #valueType()
831      *
832      * @since  1.16
833      */
834     public boolean isDecimalType() {
835         synchronized (lock) {
836             if (type == null) {
837                 return false;
838             }
839             return Float.class.isAssignableFrom(type) || Double.class.isAssignableFrom(type)
840                     || BigDecimal.class.isAssignableFrom(type);
841         }
842     }
843 
844     /**
845      * Checks if this card has an integer type value (which may be <code>null</code>).
846      *
847      * @return <code>true</code> if this card has an integer type value, otherwise <code>false</code>.
848      *
849      * @see    #isDecimalType()
850      * @see    #isStringValue()
851      * @see    #valueType()
852      *
853      * @since  1.16
854      */
855     public boolean isIntegerType() {
856         synchronized (lock) {
857             if (type == null) {
858                 return false;
859             }
860             return Number.class.isAssignableFrom(type) && !isDecimalType();
861         }
862     }
863 
864     /**
865      * Checks if this card is a comment-style card with no associated value field.
866      *
867      * @return <code>true</code> if this card is a comment-style card, otherwise <code>false</code>.
868      *
869      * @see    #isKeyValuePair()
870      * @see    #isStringValue()
871      * @see    #valueType()
872      *
873      * @since  1.16
874      */
875     public final boolean isCommentStyleCard() {
876         synchronized (lock) {
877             return (type == null);
878         }
879     }
880 
881     /**
882      * Checks if this card cas a hierarch style long keyword.
883      *
884      * @return <code>true</code> if the card has a non-standard HIERARCH style long keyword, with dot-separated
885      *             components. Otherwise <code>false</code>.
886      *
887      * @since  1.16
888      */
889     public final boolean hasHierarchKey() {
890         synchronized (lock) {
891             return isHierarchKey(key);
892         }
893     }
894 
895     /**
896      * Sets a new comment component for this card. The specified comment string will be sanitized to ensure it onlly
897      * contains characters suitable for FITS headers. Invalid characters will be replaced with '?'.
898      *
899      * @param comment the new comment text.
900      */
901     public void setComment(String comment) {
902         synchronized (lock) {
903             this.comment = sanitize(comment);
904         }
905     }
906 
907     /**
908      * Sets a new number value for this card. The new value will be shown in the integer, fixed-decimal, or format,
909      * whichever preserves more digits, or else whichever is the more compact notation. Trailing zeroes will be omitted.
910      *
911      * @param  update                the new value to set (can be <code>null</code>, in which case the card type
912      *                                   defaults to <code>Integer.class</code>)
913      *
914      * @return                       the card itself
915      *
916      * @throws NumberFormatException if the input value is NaN or Infinity.
917      * @throws LongValueException    if the decimal value cannot be represented in the alotted space
918      *
919      * @see                          #setValue(Number, int)
920      */
921     public final HeaderCard setValue(Number update) throws NumberFormatException, LongValueException {
922         return setValue(update, FlexFormat.AUTO_PRECISION);
923     }
924 
925     /**
926      * Sets a new number value for this card, using scientific (exponential) notation, with up to the specified decimal
927      * places showing between the decimal point and the exponent. For example, if <code>decimals</code> is set to 2,
928      * then &pi; gets formatted as <code>3.14E0</code>.
929      *
930      * @param  update                the new value to set (can be <code>null</code>, in which case the card type
931      *                                   defaults to <code>Integer.class</code>)
932      * @param  decimals              the number of decimal places to show in the scientific notation.
933      *
934      * @return                       the card itself
935      *
936      * @throws NumberFormatException if the input value is NaN or Infinity.
937      * @throws LongValueException    if the decimal value cannot be represented in the alotted space
938      *
939      * @see                          #setValue(Number)
940      */
941     public HeaderCard setValue(Number update, int decimals) throws NumberFormatException, LongValueException {
942         synchronized (lock) {
943             if (update instanceof Float || update instanceof Double || update instanceof BigDecimal
944                     || update instanceof BigInteger) {
945                 checkValueType(IFitsHeader.VALUE.REAL);
946             } else {
947                 checkValueType(IFitsHeader.VALUE.INTEGER);
948             }
949 
950             if (update == null) {
951                 value = null;
952                 type = Integer.class;
953             } else {
954                 type = update.getClass();
955                 checkNumber(update);
956                 setUnquotedValue(new FlexFormat().forCard(this).setPrecision(decimals).format(update));
957             }
958         }
959         return this;
960     }
961 
962     private static void checkKeyword(IFitsHeader keyword) throws IllegalArgumentException {
963         if (keyword.key().contains("n")) {
964             throw new IllegalArgumentException("Keyword " + keyword.key() + " has unfilled index(es)");
965         }
966     }
967 
968     private void checkValueType(IFitsHeader.VALUE valueType) throws ValueTypeException {
969         if (standardKey != null) {
970             checkValueType(key, standardKey.valueType(), valueType);
971         }
972     }
973 
974     private static void checkValueType(String key, IFitsHeader.VALUE expect, IFitsHeader.VALUE valueType)
975             throws ValueTypeException {
976         if (expect == IFitsHeader.VALUE.ANY || valueCheck == ValueCheck.NONE) {
977             return;
978         }
979 
980         if (valueType != expect) {
981             if (expect == IFitsHeader.VALUE.REAL && valueType == IFitsHeader.VALUE.INTEGER) {
982                 return;
983             }
984 
985             ValueTypeException e = new ValueTypeException(key, valueType.name());
986 
987             if (valueCheck == ValueCheck.LOGGING) {
988                 LOG.warning(e.getMessage());
989             } else {
990                 throw e;
991             }
992         }
993     }
994 
995     /**
996      * Sets a new boolean value for this cardvalueType
997      *
998      * @param  update             the new value to se (can be <code>null</code>).
999      *
1000      * @throws LongValueException if the card has no room even for the single-character 'T' or 'F'. This can never
1001      *                                happen with cards created programmatically as they will not allow setting
1002      *                                HIERARCH-style keywords long enough to ever trigger this condition. But, it is
1003      *                                possible to read cards from a non-standard header, which breaches this limit, by
1004      *                                ommitting some required spaces (esp. after the '='), and have a null value. When
1005      *                                that happens, we can be left without room for even a single character.
1006      * @throws ValueTypeException if the card's standard keyword does not support boolean values.
1007      *
1008      * @return                    the card itself
1009      */
1010     public HeaderCard setValue(Boolean update) throws LongValueException, ValueTypeException {
1011         synchronized (lock) {
1012             checkValueType(IFitsHeader.VALUE.LOGICAL);
1013 
1014             if (update == null) {
1015                 value = null;
1016             } else if (spaceForValue() < 1) {
1017                 throw new LongValueException(key, spaceForValue());
1018             } else {
1019                 // There is always room for a boolean value. :-)
1020                 value = update ? "T" : "F";
1021             }
1022 
1023             type = Boolean.class;
1024         }
1025         return this;
1026     }
1027 
1028     /**
1029      * Sets a new complex number value for this card. The real and imaginary part will be shown in the integer,
1030      * fixed-decimal, or format, whichever preserves more digits, or else whichever is the more compact notation.
1031      * Trailing zeroes will be omitted.
1032      *
1033      * @param  update                the new value to set (can be <code>null</code>)
1034      *
1035      * @return                       the card itself
1036      *
1037      * @throws NumberFormatException if the input value is NaN or Infinity.
1038      * @throws LongValueException    if the decimal value cannot be represented in the alotted space
1039      *
1040      * @see                          #setValue(ComplexValue, int)
1041      *
1042      * @since                        1.16
1043      */
1044     public final HeaderCard setValue(ComplexValue update) throws NumberFormatException, LongValueException {
1045         return setValue(update, FlexFormat.AUTO_PRECISION);
1046     }
1047 
1048     /**
1049      * Sets a new complex number value for this card, using scientific (exponential) notation, with up to the specified
1050      * number of decimal places showing between the decimal point and the exponent. Trailing zeroes will be omitted. For
1051      * example, if <code>decimals</code> is set to 2, then (&pi;, 12) gets formatted as <code>(3.14E0,1.2E1)</code>.
1052      *
1053      * @param  update                the new value to set (can be <code>null</code>)
1054      * @param  decimals              the number of decimal places to show in the scientific notation.
1055      *
1056      * @return                       the HeaderCard itself
1057      *
1058      * @throws NumberFormatException if the input value is NaN or Infinity.
1059      * @throws LongValueException    if the decimal value cannot be represented in the alotted space
1060      *
1061      * @see                          #setValue(ComplexValue)
1062      *
1063      * @since                        1.16
1064      */
1065     public HeaderCard setValue(ComplexValue update, int decimals) throws LongValueException {
1066         synchronized (lock) {
1067             checkValueType(IFitsHeader.VALUE.COMPLEX);
1068 
1069             if (update == null) {
1070                 value = null;
1071             } else {
1072                 if (!update.isFinite()) {
1073                     throw new NumberFormatException("Cannot represent " + update + " in FITS headers.");
1074                 }
1075                 setUnquotedValue(update.toString(decimals));
1076             }
1077 
1078             type = ComplexValue.class;
1079         }
1080         return this;
1081     }
1082 
1083     /**
1084      * Sets a new unquoted value for this card, checking to make sure it fits in the available header space. If the
1085      * value is too long to fit, an IllegalArgumentException will be thrown.
1086      *
1087      * @param  update             the new unquoted header value for this card, as a string.
1088      *
1089      * @throws LongValueException if the value is too long to fit in the available space.
1090      */
1091     private void setUnquotedValue(String update) throws LongValueException {
1092         synchronized (lock) {
1093             if (update.length() > spaceForValue()) {
1094                 throw new LongValueException(spaceForValue(), key, value);
1095             }
1096             value = update;
1097         }
1098     }
1099 
1100     /**
1101      * @deprecated                    Not supported by the FITS standard, so do not use. It was included due to a
1102      *                                    misreading of the standard itself.
1103      *
1104      * @param      update             the new value to set
1105      *
1106      * @return                        the HeaderCard itself
1107      *
1108      * @throws     LongValueException if the value is too long to fit in the available space.
1109      *
1110      * @since                         1.16
1111      */
1112     @Deprecated
1113     public HeaderCard setHexValue(long update) throws LongValueException {
1114         synchronized (lock) {
1115             setUnquotedValue(Long.toHexString(update));
1116             type = (update == (int) update) ? Integer.class : Long.class;
1117             return this;
1118         }
1119     }
1120 
1121     /**
1122      * Sets a new string value for this card.
1123      *
1124      * @param  update                         the new value to set
1125      *
1126      * @return                                the HeaderCard itself
1127      *
1128      * @throws ValueTypeException             if the card's keyword does not support string values.
1129      * @throws IllegalStateException          if the card has a HIERARCH keyword that is too long to fit any string
1130      *                                            value.
1131      * @throws IllegalArgumentException       if the new value contains characters that cannot be added to the the FITS
1132      *                                            header.
1133      * @throws LongStringsNotEnabledException if the card contains a long string but support for long strings is
1134      *                                            currently disabled.
1135      *
1136      * @see                                   FitsFactory#setLongStringsEnabled(boolean)
1137      * @see                                   #validateChars(String)
1138      */
1139     public HeaderCard setValue(String update)
1140             throws ValueTypeException, IllegalStateException, IllegalArgumentException, LongStringsNotEnabledException {
1141         synchronized (lock) {
1142             checkValueType(IFitsHeader.VALUE.STRING);
1143 
1144             int space = spaceForValue(key);
1145             if (space < STRING_QUOTES_LENGTH) {
1146                 throw new IllegalStateException("No space for string value for [" + key + "]");
1147             }
1148 
1149             if (update == null) {
1150                 // There is always room for a null string...
1151                 value = null;
1152             } else {
1153                 validateChars(update);
1154                 update = trimEnd(update);
1155                 int l = getHeaderValueSize(update);
1156 
1157                 if (space < l) {
1158                     if (FitsFactory.isLongStringsEnabled()) {
1159                         throw new IllegalStateException("No space for long string value for [" + key + "]");
1160                     }
1161 
1162                     throw new LongStringsNotEnabledException("New string value for [" + key + "] is too long."
1163                             + "\n\n --> You can enable long string support by FitsFactory.setLongStringEnabled(true).\n");
1164                 }
1165                 value = update;
1166             }
1167 
1168             type = String.class;
1169         }
1170         return this;
1171     }
1172 
1173     /**
1174      * Returns the modulo 80 character card image, the toString tries to preserve as much as possible of the comment
1175      * value by reducing the alignment of the Strings if the comment is longer and if longString is enabled the string
1176      * can be split into one more card to have more space for the comment.
1177      *
1178      * @return                                the FITS card as one or more 80-character string blocks.
1179      *
1180      * @throws LongValueException             if the card has a long string value that is too long to contain in the
1181      *                                            space available after the keyword.
1182      * @throws LongStringsNotEnabledException if the card contains a long string but support for long strings is
1183      *                                            currently disabled.
1184      * @throws HierarchNotEnabledException    if the card contains a HIERARCH-style long keyword but support for these
1185      *                                            is currently disabled.
1186      *
1187      * @see                                   FitsFactory#setLongStringsEnabled(boolean)
1188      */
1189     @Override
1190     public String toString() throws LongValueException, LongStringsNotEnabledException, HierarchNotEnabledException {
1191         return toString(FitsFactory.current());
1192     }
1193 
1194     /**
1195      * Same as {@link #toString()} just with a prefetched settings object
1196      *
1197      * @param  settings                       the settings to use for writing the header card
1198      *
1199      * @return                                the string representing the card.
1200      *
1201      * @throws LongValueException             if the card has a long string value that is too long to contain in the
1202      *                                            space available after the keyword.
1203      * @throws LongStringsNotEnabledException if the card contains a long string but support for long strings is
1204      *                                            disabled in the settings.
1205      * @throws HierarchNotEnabledException    if the card contains a HIERARCH-style long keyword but support for these
1206      *                                            is disabled in the settings.
1207      *
1208      * @see                                   FitsFactory#setLongStringsEnabled(boolean)
1209      */
1210     protected String toString(final FitsSettings settings)
1211             throws LongValueException, LongStringsNotEnabledException, HierarchNotEnabledException {
1212         synchronized (lock) {
1213             return new HeaderCardFormatter(settings).toString(this);
1214         }
1215     }
1216 
1217     /**
1218      * Returns the class of the associated value, or null if it's a comment-style card.
1219      *
1220      * @return the type of the value.
1221      *
1222      * @see    #isCommentStyleCard()
1223      * @see    #isKeyValuePair()
1224      * @see    #isIntegerType()
1225      * @see    #isDecimalType()
1226      */
1227     public Class<?> valueType() {
1228         synchronized (lock) {
1229             return type;
1230         }
1231     }
1232 
1233     /**
1234      * Returns the value as a boolean, or the default value if the card has no associated value or it is not a boolean.
1235      *
1236      * @param  defaultValue the default value to return if the card has no associated value or is not a boolean.
1237      *
1238      * @return              the boolean value of this card, or else the default value.
1239      */
1240     private Boolean getBooleanValue(Boolean defaultValue) {
1241         if ("T".equals(value)) {
1242             return true;
1243         }
1244         if ("F".equals(value)) {
1245             return false;
1246         }
1247         return defaultValue;
1248     }
1249 
1250     /**
1251      * Parses a continued long string value and comment for this card, which may occupy one or more consecutive
1252      * 80-character header records.
1253      *
1254      * @param  dis                    the input stream from which to parse the value and comment fields of this card.
1255      * @param  next                   the parser to use for each 80-character record.
1256      *
1257      * @throws IOException            if there was an IO error reading the stream.
1258      * @throws TruncatedFileException if the stream endedc ubnexpectedly in the middle of an 80-character record.
1259      */
1260     @SuppressWarnings("deprecation")
1261     private void parseLongStringCard(HeaderCardCountingArrayDataInput dis, HeaderCardParser next)
1262             throws IOException, TruncatedFileException {
1263 
1264         StringBuilder longValue = new StringBuilder();
1265         StringBuilder longComment = null;
1266 
1267         while (next != null) {
1268             if (!next.isString()) {
1269                 break;
1270             }
1271             String valuePart = next.getValue();
1272             String untrimmedComment = next.getUntrimmedComment();
1273 
1274             if (valuePart == null) {
1275                 // The card cannot have a null value. If it does it wasn't a string card...
1276                 break;
1277             }
1278 
1279             // The end point of the value
1280             int valueEnd = valuePart.length();
1281 
1282             // Check if there card continues into the next record. The value
1283             // must end with '&' and the next card must be a CONTINUE card.
1284             // If so, remove the '&' from the value part, and parse in the next
1285             // card for the next iteration...
1286             if (!dis.markSupported()) {
1287                 throw new IOException("InputStream does not support mark/reset");
1288             }
1289 
1290             // Peek at the next card.
1291             dis.mark();
1292 
1293             try {
1294                 // Check if we should continue parsing this card...
1295                 next = new HeaderCardParser(readOneHeaderLine(dis));
1296                 if (valuePart.endsWith("&") && CONTINUE.key().equals(next.getKey())) {
1297                     // Remove '& from the value part...
1298                     valueEnd--;
1299                 } else {
1300                     // ok move the input stream one card back.
1301                     dis.reset();
1302                     // Clear the parser also.
1303                     next = null;
1304                 }
1305             } catch (EOFException e) {
1306                 // Nothing left to parse after the current one...
1307                 next = null;
1308             }
1309 
1310             // Append the value part from the record last parsed.
1311             longValue.append(valuePart, 0, valueEnd);
1312 
1313             // Append any comment from the card last parsed.
1314             if (untrimmedComment != null) {
1315                 if (longComment == null) {
1316                     longComment = new StringBuilder(untrimmedComment);
1317                 } else {
1318                     longComment.append(untrimmedComment);
1319                 }
1320             }
1321         }
1322 
1323         synchronized (lock) {
1324             comment = longComment == null ? null : longComment.toString().trim();
1325             value = trimEnd(longValue.toString());
1326             type = String.class;
1327         }
1328     }
1329 
1330     /**
1331      * Removes the trailing spaces (if any) from a string. According to the FITS standard, trailing spaces in string are
1332      * not significant (but leading spaces are). As such we should remove trailing spaces when parsing header string
1333      * values.
1334      * 
1335      * @param  s the string as it appears in the FITS header
1336      * 
1337      * @return   the input string if it has no trailing spaces, or else a new string with the trailing spaces removed.
1338      */
1339     private String trimEnd(String s) {
1340         int end = s.length();
1341         for (; end > 0; end--) {
1342             if (!Character.isSpaceChar(s.charAt(end - 1))) {
1343                 break;
1344             }
1345         }
1346         return end == s.length() ? s : s.substring(0, end);
1347     }
1348 
1349     /**
1350      * Returns the minimum number of characters the value field will occupy in the header record, including quotes
1351      * around string values, and quoted quotes inside. The actual header may add padding (e.g. to ensure the end quote
1352      * does not come before byte 20).
1353      *
1354      * @return the minimum number of bytes needed to represent this value in a header record.
1355      *
1356      * @since  1.16
1357      *
1358      * @see    #getHeaderValueSize(String)
1359      * @see    #spaceForValue()
1360      */
1361     int getHeaderValueSize() {
1362         synchronized (lock) {
1363             return getHeaderValueSize(value);
1364         }
1365     }
1366 
1367     /**
1368      * Returns the minimum number of characters the value field will occupy in the header record, including quotes
1369      * around string values, and quoted quotes inside. The actual header may add padding (e.g. to ensure the end quote
1370      * does not come before byte 20). If the long string convention is enabled, this method returns the minimum number
1371      * of characters needed in the leading 80-character record only. The call assumes that the value has been
1372      * appropriately trimmed of trailing and leading spaces as appropriate.
1373      * 
1374      * @param  aValue The proposed value for this card
1375      * 
1376      * @return        the minimum number of bytes needed to represent this value in a header record.
1377      *
1378      * @since         1.16
1379      *
1380      * @see           #spaceForValue()
1381      * @see           #trimEnd(String)
1382      */
1383     private int getHeaderValueSize(String aValue) {
1384         if (aValue == null) {
1385             return 0;
1386         }
1387 
1388         synchronized (lock) {
1389             if (!isStringValue()) {
1390                 return aValue.length();
1391             }
1392 
1393             int n = STRING_QUOTES_LENGTH;
1394 
1395             if (FitsFactory.isLongStringsEnabled()) {
1396                 // If not empty string we need to write at least &...
1397                 return aValue.isEmpty() ? n : n + 1;
1398             }
1399 
1400             n += aValue.length();
1401             for (int i = aValue.length(); --i >= 0;) {
1402                 if (aValue.charAt(i) == '\'') {
1403                     // Add the number of quotes that need escaping.
1404                     n++;
1405                 }
1406             }
1407             return n;
1408         }
1409     }
1410 
1411     /**
1412      * Returns the space available for value and/or comment in a single record the keyword.
1413      *
1414      * @return the number of characters available in a single 80-character header record for a standard (non long
1415      *             string) value and/or comment.
1416      *
1417      * @since  1.16
1418      */
1419     public final int spaceForValue() {
1420         synchronized (lock) {
1421             return spaceForValue(key);
1422         }
1423     }
1424 
1425     /**
1426      * Updates the keyword for this card.
1427      *
1428      * @param  newKey                         the new FITS header keyword to use for this card.
1429      *
1430      * @throws HierarchNotEnabledException    if the new key is a HIERARCH-style long key but support for these is not
1431      *                                            currently enabled.
1432      * @throws IllegalArgumentException       if the keyword contains invalid characters
1433      * @throws LongValueException             if the new keyword does not leave sufficient room for the current
1434      *                                            non-string value.
1435      * @throws LongStringsNotEnabledException if the new keyword does not leave sufficient rooom for the current string
1436      *                                            value without enabling long string support.
1437      *
1438      * @see                                   FitsFactory#setLongStringsEnabled(boolean)
1439      * @see                                   #spaceForValue()
1440      * @see                                   #getValue()
1441      */
1442     public void changeKey(String newKey) throws HierarchNotEnabledException, LongValueException,
1443             LongStringsNotEnabledException, IllegalArgumentException {
1444 
1445         validateKey(newKey);
1446 
1447         synchronized (lock) {
1448             int l = getHeaderValueSize();
1449             int space = spaceForValue(newKey);
1450 
1451             if (l > space) {
1452                 if (isStringValue() && !FitsFactory.isLongStringsEnabled() && space > STRING_QUOTES_LENGTH) {
1453                     throw new LongStringsNotEnabledException(newKey);
1454                 }
1455                 throw new LongValueException(spaceForValue(newKey), newKey + "= " + value);
1456             }
1457             key = newKey;
1458             standardKey = null;
1459         }
1460     }
1461 
1462     /**
1463      * Checks if the card is blank, that is if it contains only empty spaces.
1464      *
1465      * @return <code>true</code> if the card contains nothing but blank spaces.
1466      */
1467     public boolean isBlank() {
1468         synchronized (lock) {
1469             if (!isCommentStyleCard() || !key.isEmpty()) {
1470                 return false;
1471             }
1472             if (comment == null) {
1473                 return true;
1474             }
1475             return comment.isEmpty();
1476         }
1477     }
1478 
1479     /**
1480      * Returns the current policy for checking if set values are of the allowed type for cards with standardized
1481      * {@link IFitsHeader} keywords.
1482      * 
1483      * @return the current value type checking policy
1484      * 
1485      * @since  1.19
1486      * 
1487      * @see    #setValueCheckingPolicy(ValueCheck)
1488      */
1489     public static ValueCheck getValueCheckingPolicy() {
1490         return valueCheck;
1491     }
1492 
1493     /**
1494      * Sets the policy to used for checking if set values conform to the expected types for cards that use standardized
1495      * FITS keywords via the {@link IFitsHeader} interface.
1496      * 
1497      * @param policy the new polict to use for checking value types.
1498      * 
1499      * @see          #getValueCheckingPolicy()
1500      * @see          Header#setKeywordChecking(nom.tam.fits.Header.KeywordCheck)
1501      * 
1502      * @since        1.19
1503      */
1504     public static void setValueCheckingPolicy(ValueCheck policy) {
1505         valueCheck = policy;
1506     }
1507 
1508     /**
1509      * <p>
1510      * Creates a new FITS header card from a FITS stream representation of it, which is how the key/value and comment
1511      * are represented inside the FITS file, normally as an 80-character wide entry. The parsing of header 'lines'
1512      * conforms to all FITS standards, and some optional conventions, such as HIERARCH keywords (if
1513      * {@link FitsFactory#setUseHierarch(boolean)} is enabled), COMMENT and HISTORY entries, and OGIP 1.0 long CONTINUE
1514      * lines (if {@link FitsFactory#setLongStringsEnabled(boolean)} is enabled).
1515      * </p>
1516      * <p>
1517      * However, the parsing here is permissive beyond the standards and conventions, and will do its best to support a
1518      * wide range of FITS files, which may deviate from the standard in subtle (or no so subtle) ways.
1519      * </p>
1520      * <p>
1521      * Here is a brief summary of the rules that guide the parsing of keywords, values, and comment 'fields' from the
1522      * single header line:
1523      * </p>
1524      * <p>
1525      * <b>A. Keywords</b>
1526      * </p>
1527      * <ul>
1528      * <li>The standard FITS keyword is the first 8 characters of the line, or up to an equal [=] character, whichever
1529      * comes first, with trailing spaces removed, and always converted to upper-case.</li>
1530      * <li>If {@link FitsFactory#setUseHierarch(boolean)} is enabled, structured longer keywords can be composed after a
1531      * <code>HIERARCH</code> base key, followed by space (and/or dot ].]) separated parts, up to an equal sign [=]. The
1532      * library will represent the same components (including <code>HIERARCH</code>) but separated by single dots [.].
1533      * For example, the header line starting with [<code>HIERARCH SMA OBS TARGET =</code>], will be referred as
1534      * [<code>HIERARCH.SMA.OBS.TARGET</code>] withing this library. The keyword parts can be composed of any ASCII
1535      * characters except dot [.], white spaces, or equal [=].</li>
1536      * <li>By default, all parts of the key are converted to upper-case. Case sensitive HIERARCH keywords can be
1537      * retained after enabling
1538      * {@link nom.tam.fits.header.hierarch.IHierarchKeyFormatter#setCaseSensitive(boolean)}.</li>
1539      * </ul>
1540      * <p>
1541      * <b>B. Values</b>
1542      * </p>
1543      * <p>
1544      * Values are the part of the header line, that is between the keyword and an optional ending comment. Legal header
1545      * values follow the following parse patterns:
1546      * <ul>
1547      * <li>Begin with an equal sign [=], or else come after a CONTINUE keyword.</li>
1548      * <li>Next can be a quoted value such as <code>'hello'</code>, placed inside two single quotes. Or an unquoted
1549      * value, such as <code>123</code>.</li>
1550      * <li>Quoted values must begin with a single quote ['] and and with the next single quote. If there is no end-quote
1551      * in the line, it is not considered a string value but rather a comment, unless
1552      * {@link FitsFactory#setAllowHeaderRepairs(boolean)} is enabled, in which case the entire remaining line after the
1553      * opening quote is assumed to be a malformed value.</li>
1554      * <li>Unquoted values end at the fist [/] character, or else go until the line end.</li>
1555      * <li>Quoted values have trailing spaces removed, s.t. [<code>'  value   '</code>] becomes
1556      * [<code>  value</code>].</li>
1557      * <li>Unquoted values are trimmed, with both leading and trailing spaces removed, e.g. [<code>  123  </code>]
1558      * becomes [<code>123</code>].</li>
1559      * </ul>
1560      * <p>
1561      * <b>C. Comments</b>
1562      * </p>
1563      * <p>
1564      * The following rules guide the parsing of the values component:
1565      * <ul>
1566      * <li>If a value is present (see above), the comment is what comes after it. That is, for quoted values, everything
1567      * that follows the closing quote. For unquoted values, it's what comes after the first [/], with the [/] itself
1568      * removed.</li>
1569      * <li>If a value is not present, then everything following the keyword is considered the comment.</li>
1570      * <li>Comments are trimmed, with both leading and trailing spaces removed.</li>
1571      * </ul>
1572      *
1573      * @return                          a newly created HeaderCard from a FITS card string.
1574      *
1575      * @param  line                     the card image (typically 80 characters if in a FITS file).
1576      *
1577      * @throws IllegalArgumentException if the card was malformed, truncated, or if there was an IO error.
1578      *
1579      * @see                             FitsFactory#setUseHierarch(boolean)
1580      * @see                             nom.tam.fits.header.hierarch.IHierarchKeyFormatter#setCaseSensitive(boolean)
1581      */
1582     public static HeaderCard create(String line) throws IllegalArgumentException {
1583         try (ArrayDataInput in = stringToArrayInputStream(line)) {
1584             return new HeaderCard(in);
1585         } catch (Exception e) {
1586             throw new IllegalArgumentException("card not legal", e);
1587         }
1588     }
1589 
1590     final IFitsHeader getStandardKey() {
1591         return standardKey;
1592     }
1593 
1594     /**
1595      * Creates a new card with a standard or conventional keyword and a boolean value, with the default comment
1596      * associated with the keyword. Unlike {@link #HeaderCard(String, Boolean)}, this call does not throw an exception,
1597      * since the keyword and comment should be valid by design.
1598      *
1599      * @param  key                      The standard or conventional keyword with its associated default comment.
1600      * @param  value                    the boolean value associated to the keyword
1601      *
1602      * @return                          A new header card with the speficied standard-style key and comment and the
1603      *                                      specified value, or <code>null</code> if the standard key itself is
1604      *                                      malformed or illegal.
1605      *
1606      * @throws IllegalArgumentException if the standard key was ill-defined.
1607      *
1608      * @since                           1.16
1609      */
1610     public static HeaderCard create(IFitsHeader key, Boolean value) throws IllegalArgumentException {
1611         checkKeyword(key);
1612 
1613         try {
1614             HeaderCard hc = new HeaderCard(key.key(), (Boolean) null, key.comment());
1615             hc.standardKey = key;
1616             hc.setValue(value);
1617             return hc;
1618         } catch (HeaderCardException e) {
1619             throw new IllegalArgumentException(e.getMessage(), e);
1620         }
1621     }
1622 
1623     /**
1624      * <p>
1625      * Creates a new card with a standard or conventional keyword and a number value, with the default comment
1626      * associated with the keyword. Unlike {@link #HeaderCard(String, Number)}, this call does not throw a hard
1627      * {@link HeaderCardException} exception, since the keyword and comment should be valid by design. (A runtime
1628      * {@link IllegalArgumentException} may still be thrown in the event that the supplied conventional keywords itself
1629      * is ill-defined -- but this should not happen unless something was poorly coded in this library, on in an
1630      * extension of it).
1631      * </p>
1632      * <p>
1633      * If the value is not compatible with the convention of the keyword, a warning message is logged but no exception
1634      * is thrown (at this point).
1635      * </p>
1636      *
1637      * @param  key                      The standard or conventional keyword with its associated default comment.
1638      * @param  value                    the integer value associated to the keyword.
1639      *
1640      * @return                          A new header card with the speficied standard-style key and comment and the
1641      *                                      specified value.
1642      *
1643      * @throws IllegalArgumentException if the standard key itself was ill-defined.
1644      *
1645      * @since                           1.16
1646      */
1647     public static HeaderCard create(IFitsHeader key, Number value) throws IllegalArgumentException {
1648         checkKeyword(key);
1649 
1650         try {
1651             HeaderCard hc = new HeaderCard(key.key(), (Number) null, key.comment());
1652             hc.standardKey = key;
1653             hc.setValue(value);
1654             return hc;
1655         } catch (HeaderCardException e) {
1656             throw new IllegalArgumentException(e.getMessage(), e);
1657         }
1658     }
1659 
1660     /**
1661      * Creates a new card with a standard or conventional keyword and a number value, with the default comment
1662      * associated with the keyword. Unlike {@link #HeaderCard(String, Number)}, this call does not throw an exception,
1663      * since the keyword and comment should be valid by design.
1664      *
1665      * @param  key                      The standard or conventional keyword with its associated default comment.
1666      * @param  value                    the integer value associated to the keyword.
1667      *
1668      * @return                          A new header card with the speficied standard-style key and comment and the
1669      *                                      specified value.
1670      *
1671      * @throws IllegalArgumentException if the standard key was ill-defined.
1672      *
1673      * @since                           1.16
1674      */
1675     public static HeaderCard create(IFitsHeader key, ComplexValue value) throws IllegalArgumentException {
1676         checkKeyword(key);
1677 
1678         try {
1679             HeaderCard hc = new HeaderCard(key.key(), (ComplexValue) null, key.comment());
1680             hc.standardKey = key;
1681             hc.setValue(value);
1682             return hc;
1683         } catch (HeaderCardException e) {
1684             throw new IllegalArgumentException(e.getMessage(), e);
1685         }
1686     }
1687 
1688     /**
1689      * Creates a new card with a standard or conventional keyword and an integer value, with the default comment
1690      * associated with the keyword. Unlike {@link #HeaderCard(String, Number)}, this call does not throw a hard
1691      * exception, since the keyword and comment sohould be valid by design. The string value however will be checked,
1692      * and an appropriate runtime exception is thrown if it cannot be included in a FITS header.
1693      *
1694      * @param  key                      The standard or conventional keyword with its associated default comment.
1695      * @param  value                    the string associated to the keyword.
1696      *
1697      * @return                          A new header card with the speficied standard-style key and comment and the
1698      *                                      specified value.
1699      *
1700      * @throws IllegalArgumentException if the string value contains characters that are not allowed in FITS headers,
1701      *                                      that is characters outside of the 0x20 thru 0x7E range, or if the standard
1702      *                                      key was ill-defined.
1703      */
1704     public static HeaderCard create(IFitsHeader key, String value) throws IllegalArgumentException {
1705         checkKeyword(key);
1706         validateChars(value);
1707 
1708         try {
1709             HeaderCard hc = new HeaderCard(key.key(), (String) null, key.comment());
1710             hc.standardKey = key;
1711             hc.setValue(value);
1712             return hc;
1713         } catch (HeaderCardException e) {
1714             throw new IllegalArgumentException(e.getMessage(), e);
1715         }
1716     }
1717 
1718     /**
1719      * Creates a comment-style card with no associated value field.
1720      *
1721      * @param  key                 The keyword, or <code>null</code> blank/empty string for an unkeyed comment.
1722      * @param  comment             The comment text.
1723      *
1724      * @return                     a new comment-style header card with the specified key and comment text.
1725      *
1726      * @throws HeaderCardException if the key or value were invalid.
1727      * @throws LongValueException  if the comment text is longer than the space available in comment-style cards (71
1728      *                                 characters max)
1729      *
1730      * @see                        #createUnkeyedCommentCard(String)
1731      * @see                        #createCommentCard(String)
1732      * @see                        #createHistoryCard(String)
1733      * @see                        Header#insertCommentStyle(String, String)
1734      * @see                        Header#insertCommentStyleMultiline(String, String)
1735      */
1736     public static HeaderCard createCommentStyleCard(String key, String comment)
1737             throws HeaderCardException, LongValueException {
1738         if (comment == null) {
1739             comment = "";
1740         } else if (comment.length() > MAX_COMMENT_CARD_COMMENT_LENGTH) {
1741             throw new LongValueException(MAX_COMMENT_CARD_COMMENT_LENGTH, key, comment);
1742         }
1743         HeaderCard card = new HeaderCard();
1744         card.set(key, null, comment, null);
1745         return card;
1746     }
1747 
1748     /**
1749      * Creates a new unkeyed comment card for th FITS header. These are comment-style cards with no associated value
1750      * field, and with a blank keyword. They are commonly used to add explanatory notes in the FITS header. Keyed
1751      * comments are another alternative...
1752      *
1753      * @param  text                a concise descriptive entry (max 71 characters).
1754      *
1755      * @return                     a new COMMENT card with the specified key and comment text.
1756      *
1757      * @throws HeaderCardException if the text contains invalid charaters.
1758      * @throws LongValueException  if the comment text is longer than the space available in comment-style cards (71
1759      *                                 characters max)
1760      *
1761      * @see                        #createCommentCard(String)
1762      * @see                        #createCommentStyleCard(String, String)
1763      * @see                        Header#insertUnkeyedComment(String)
1764      */
1765     public static HeaderCard createUnkeyedCommentCard(String text) throws HeaderCardException, LongValueException {
1766         return createCommentStyleCard(BLANKS.key(), text);
1767     }
1768 
1769     /**
1770      * Creates a new keyed comment card for th FITS header. These are comment-style cards with no associated value
1771      * field, and with COMMENT as the keyword. They are commonly used to add explanatory notes in the FITS header.
1772      * Unkeyed comments are another alternative...
1773      *
1774      * @param  text                a concise descriptive entry (max 71 characters).
1775      *
1776      * @return                     a new COMMENT card with the specified key and comment text.
1777      *
1778      * @throws HeaderCardException if the text contains invalid charaters.
1779      * @throws LongValueException  if the comment text is longer than the space available in comment-style cards (71
1780      *                                 characters max)
1781      *
1782      * @see                        #createUnkeyedCommentCard(String)
1783      * @see                        #createCommentStyleCard(String, String)
1784      * @see                        Header#insertComment(String)
1785      */
1786     public static HeaderCard createCommentCard(String text) throws HeaderCardException, LongValueException {
1787         return createCommentStyleCard(COMMENT.key(), text);
1788     }
1789 
1790     /**
1791      * Creates a new history record for the FITS header. These are comment-style cards with no associated value field,
1792      * and with HISTORY as the keyword. They are commonly used to document the sequence operations that were performed
1793      * on the data before it arrived to the state represented by the FITS file. The text field for history entries is
1794      * limited to 70 characters max per card. However there is no limit to how many such entries are in a FITS header.
1795      *
1796      * @param  text                a concise descriptive entry (max 71 characters).
1797      *
1798      * @return                     a new HISTORY card with the specified key and comment text.
1799      *
1800      * @throws HeaderCardException if the text contains invalid charaters.
1801      * @throws LongValueException  if the comment text is longer than the space available in comment-style cards (71
1802      *                                 characters max)
1803      *
1804      * @see                        #createCommentStyleCard(String, String)
1805      * @see                        Header#insertHistory(String)
1806      */
1807     public static HeaderCard createHistoryCard(String text) throws HeaderCardException, LongValueException {
1808         return createCommentStyleCard(HISTORY.key(), text);
1809     }
1810 
1811     /**
1812      * @deprecated                     Not supported by the FITS standard, so do not use. It was included due to a
1813      *                                     misreading of the standard itself.
1814      *
1815      * @param      key                 the keyword
1816      * @param      value               the integer value
1817      *
1818      * @return                         A new header card, with the specified integer in hexadecomal representation.
1819      *
1820      * @throws     HeaderCardException if the card is invalid (for example the keyword is not valid).
1821      *
1822      * @see                            #createHexValueCard(String, long, String)
1823      * @see                            #getHexValue()
1824      * @see                            Header#getHexValue(String)
1825      */
1826     @Deprecated
1827     public static HeaderCard createHexValueCard(String key, long value) throws HeaderCardException {
1828         return createHexValueCard(key, value, null);
1829     }
1830 
1831     /**
1832      * @deprecated                     Not supported by the FITS standard, so do not use. It was included due to a
1833      *                                     misreading of the standard itself.
1834      *
1835      * @param      key                 the keyword
1836      * @param      value               the integer value
1837      * @param      comment             optional comment, or <code>null</code>.
1838      *
1839      * @return                         A new header card, with the specified integer in hexadecomal representation.
1840      *
1841      * @throws     HeaderCardException if the card is invalid (for example the keyword is not valid).
1842      *
1843      * @see                            #createHexValueCard(String, long)
1844      * @see                            #getHexValue()
1845      * @see                            Header#getHexValue(String)
1846      */
1847     @Deprecated
1848     public static HeaderCard createHexValueCard(String key, long value, String comment) throws HeaderCardException {
1849         return new HeaderCard(key, Long.toHexString(value), comment, Long.class);
1850     }
1851 
1852     /**
1853      * Reads an 80-byte card record from an input.
1854      *
1855      * @param  in                     The input to read from
1856      *
1857      * @return                        The raw, undigested header record as a string.
1858      *
1859      * @throws IOException            if already at the end of file.
1860      * @throws TruncatedFileException if there was not a complete record available in the input.
1861      */
1862     private static String readRecord(InputReader in) throws IOException, TruncatedFileException {
1863         byte[] buffer = new byte[FITS_HEADER_CARD_SIZE];
1864 
1865         int got = 0;
1866 
1867         try {
1868             // Read as long as there is more available, even if it comes in a trickle...
1869             while (got < buffer.length) {
1870                 int n = in.read(buffer, got, buffer.length - got);
1871                 if (n < 0) {
1872                     break;
1873                 }
1874                 got += n;
1875             }
1876         } catch (EOFException e) {
1877             // Just in case read throws EOFException instead of returning -1 by contract.
1878         }
1879 
1880         if (got == 0) {
1881             // Nothing left to read.
1882             throw new EOFException();
1883         }
1884 
1885         if (got < buffer.length) {
1886             // Got an incomplete header card...
1887             throw new TruncatedFileException(
1888                     "Got only " + got + " of " + buffer.length + " bytes expected for a header card");
1889         }
1890 
1891         return AsciiFuncs.asciiString(buffer);
1892     }
1893 
1894     /**
1895      * Read exactly one complete fits header line from the input.
1896      *
1897      * @param  dis                    the data input stream to read the line
1898      *
1899      * @return                        a string of exactly 80 characters
1900      *
1901      * @throwa                        EOFException if already at the end of file.
1902      *
1903      * @throws TruncatedFileException if there was not a complete line available in the input.
1904      * @throws IOException            if the input stream could not be read
1905      */
1906     @SuppressWarnings({"resource", "deprecation"})
1907     private static String readOneHeaderLine(HeaderCardCountingArrayDataInput dis)
1908             throws IOException, TruncatedFileException {
1909         String s = readRecord(dis.in());
1910         dis.cardRead();
1911         return s;
1912     }
1913 
1914     /**
1915      * Returns the maximum number of characters that can be used for a value field in a single FITS header record (80
1916      * characters wide), after the specified keyword.
1917      *
1918      * @param  key the header keyword, which may be a HIERARCH-style key...
1919      *
1920      * @return     the space available for the value field in a single record, after the keyword, and the assigmnent
1921      *                 sequence (or equivalent blank space).
1922      */
1923     private static int spaceForValue(String key) {
1924         if (key.length() > MAX_KEYWORD_LENGTH) {
1925             IHierarchKeyFormatter fmt = FitsFactory.getHierarchFormater();
1926             int keyLen = Math.max(key.length(), MAX_KEYWORD_LENGTH) + fmt.getExtraSpaceRequired(key);
1927             return FITS_HEADER_CARD_SIZE - keyLen - fmt.getMinAssignLength();
1928         }
1929         return FITS_HEADER_CARD_SIZE - (Math.max(key.length(), MAX_KEYWORD_LENGTH) + HeaderCardFormatter.getAssignLength());
1930     }
1931 
1932     private static ArrayDataInput stringToArrayInputStream(String card) {
1933         byte[] bytes = AsciiFuncs.getBytes(card);
1934         if (bytes.length % FITS_HEADER_CARD_SIZE != 0) {
1935             byte[] newBytes = new byte[bytes.length + FITS_HEADER_CARD_SIZE - bytes.length % FITS_HEADER_CARD_SIZE];
1936             System.arraycopy(bytes, 0, newBytes, 0, bytes.length);
1937             Arrays.fill(newBytes, bytes.length, newBytes.length, (byte) ' ');
1938             bytes = newBytes;
1939         }
1940         return new FitsInputStream(new ByteArrayInputStream(bytes));
1941     }
1942 
1943     /**
1944      * This method was designed for use internally. It is 'safe' (not save!) in the sense that the runtime exception it
1945      * may throw does not need to be caught.
1946      *
1947      * @param      key                 keyword
1948      * @param      comment             optional comment, or <code>null</code>
1949      * @param      hasValue            does this card have a (<code>null</code>) value field? If <code>true</code> a
1950      *                                     null value of type <code>String.class</code> is assumed (for backward
1951      *                                     compatibility).
1952      *
1953      * @return                         the new HeaderCard
1954      *
1955      * @throws     HeaderCardException if the card could not be created for some reason (noted as the cause).
1956      *
1957      * @deprecated                     This was to be used internally only, without public visibility. It will become
1958      *                                     unexposed to users in a future release...
1959      */
1960     @Deprecated
1961     public static HeaderCard saveNewHeaderCard(String key, String comment, boolean hasValue) throws HeaderCardException {
1962         return new HeaderCard(key, null, comment, hasValue ? String.class : null);
1963     }
1964 
1965     /**
1966      * Checks if the specified keyword is a HIERARCH-style long keyword.
1967      *
1968      * @param  key The keyword to check.
1969      *
1970      * @return     <code>true</code> if the specified key may be a HIERARC-style key, otehrwise <code>false</code>.
1971      */
1972     private static boolean isHierarchKey(String key) {
1973         return key.toUpperCase().startsWith(HIERARCH_WITH_DOT);
1974     }
1975 
1976     /**
1977      * Replaces illegal characters in the string ith '?' to be suitable for FITS header records. According to the FITS
1978      * standard, headers may only contain ASCII characters in the range 0x20 and 0x7E (inclusive).
1979      *
1980      * @param  str the input string.
1981      *
1982      * @return     the sanitized string for use in a FITS header, with illegal characters replaced by '?'.
1983      *
1984      * @see        #isValidChar(char)
1985      * @see        #validateChars(String)
1986      */
1987     public static String sanitize(String str) {
1988         int nc = str.length();
1989         char[] cbuf = new char[nc];
1990         for (int ic = 0; ic < nc; ic++) {
1991             char c = str.charAt(ic);
1992             cbuf[ic] = isValidChar(c) ? c : '?';
1993         }
1994         return new String(cbuf);
1995     }
1996 
1997     /**
1998      * Checks if a character is valid for inclusion in a FITS header record. The FITS standard specifies that only ASCII
1999      * characters between 0x20 thru 0x7E may be used in FITS headers.
2000      *
2001      * @param  c the character to check
2002      *
2003      * @return   <code>true</code> if the character is allowed in the FITS header, otherwise <code>false</code>.
2004      *
2005      * @see      #validateChars(String)
2006      * @see      #sanitize(String)
2007      */
2008     public static boolean isValidChar(char c) {
2009         return (c >= MIN_VALID_CHAR && c <= MAX_VALID_CHAR);
2010     }
2011 
2012     /**
2013      * Checks the specified string for characters that are not allowed in FITS headers, and throws an exception if any
2014      * are found. According to the FITS standard, headers may only contain ASCII characters in the range 0x20 and 0x7E
2015      * (inclusive).
2016      *
2017      * @param  text                     the input string
2018      *
2019      * @throws IllegalArgumentException if the unput string contains any characters that cannot be in a FITS header,
2020      *                                      that is characters outside of the 0x20 to 0x7E range.
2021      *
2022      * @since                           1.16
2023      *
2024      * @see                             #isValidChar(char)
2025      * @see                             #sanitize(String)
2026      * @see                             #validateKey(String)
2027      */
2028     public static void validateChars(String text) throws IllegalArgumentException {
2029         if (text == null) {
2030             return;
2031         }
2032 
2033         for (int i = text.length(); --i >= 0;) {
2034             char c = text.charAt(i);
2035             if (c < MIN_VALID_CHAR) {
2036                 throw new IllegalArgumentException(
2037                         "Non-printable character(s), e.g. 0x" + (int) c + ", in [" + sanitize(text) + "].");
2038             }
2039             if (c > MAX_VALID_CHAR) {
2040                 throw new IllegalArgumentException(
2041                         "Extendeed ASCII character(s) in [" + sanitize(text) + "]. Only 0x20 through 0x7E are allowed.");
2042             }
2043         }
2044     }
2045 
2046     /**
2047      * Checks if the specified string may be used as a FITS header keyword according to the FITS standard and currently
2048      * settings for supporting extensions to the standard, such as HIERARCH-style keywords.
2049      *
2050      * @param  key                      the proposed keyword string
2051      *
2052      * @throws IllegalArgumentException if the string cannot be used as a FITS keyword with the current settings. The
2053      *                                      exception will contain an informative message describing the issue.
2054      *
2055      * @since                           1.16
2056      *
2057      * @see                             #validateChars(String)
2058      * @see                             FitsFactory#setUseHierarch(boolean)
2059      */
2060     public static void validateKey(String key) throws IllegalArgumentException {
2061         int maxLength = MAX_KEYWORD_LENGTH;
2062         if (isHierarchKey(key)) {
2063             if (!FitsFactory.getUseHierarch()) {
2064                 throw new HierarchNotEnabledException(key);
2065             }
2066             maxLength = MAX_HIERARCH_KEYWORD_LENGTH;
2067             validateHierarchComponents(key);
2068         }
2069 
2070         if (key.length() > maxLength) {
2071             throw new IllegalArgumentException("Keyword is too long: [" + sanitize(key) + "]");
2072         }
2073 
2074         // Check the whole key for non-printable, non-standard ASCII
2075         for (int i = key.length(); --i >= 0;) {
2076             char c = key.charAt(i);
2077             if (c < MIN_VALID_CHAR) {
2078                 throw new IllegalArgumentException(
2079                         "Keyword contains non-printable character 0x" + (int) c + ": [" + sanitize(key) + "].");
2080             }
2081             if (c > MAX_VALID_CHAR) {
2082                 throw new IllegalArgumentException("Keyword contains extendeed ASCII characters: [" + sanitize(key)
2083                         + "]. Only 0x20 through 0x7E are allowed.");
2084             }
2085         }
2086 
2087         // Check if the first 8 characters conform to strict FITS specification...
2088         for (int i = Math.min(MAX_KEYWORD_LENGTH, key.length()); --i >= 0;) {
2089             char c = key.charAt(i);
2090             if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
2091                 continue;
2092             }
2093             if ((c >= '0' && c <= '9') || (c == '-') || (c == '_')) {
2094                 continue;
2095             }
2096             throw new IllegalArgumentException(
2097                     "Keyword [" + sanitize(key) + "] contains invalid characters. Only [A-Z][a-z][0-9][-][_] are allowed.");
2098         }
2099     }
2100 
2101     /**
2102      * Additional checks the extended components of the HIEARCH key (in bytes 9-77), to make sure they conform to our
2103      * own standards of storing hierarch keys as a dot-separated list of components. That is, the keyword must not have
2104      * any spaces...
2105      *
2106      * @param  key                      the HIERARCH keyword to check.
2107      *
2108      * @throws IllegalArgumentException if the keyword is not a proper dot-separated set of non-empty hierarchical
2109      *                                      components
2110      */
2111     private static void validateHierarchComponents(String key) throws IllegalArgumentException {
2112         for (int i = key.length(); --i >= 0;) {
2113             if (Character.isSpaceChar(key.charAt(i))) {
2114                 throw new IllegalArgumentException(
2115                         "No spaces allowed in HIERARCH keywords used internally: [" + sanitize(key) + "].");
2116             }
2117         }
2118 
2119         if (key.indexOf("..") >= 0) {
2120             throw new IllegalArgumentException("HIERARCH keywords with empty component: [" + sanitize(key) + "].");
2121         }
2122     }
2123 
2124     /**
2125      * Checks that a number value is not NaN or Infinite, since FITS does not have a standard for describing those
2126      * values in the header. If the value is not suitable for the FITS header, an exception is thrown.
2127      *
2128      * @param  value                 The number to check
2129      *
2130      * @throws NumberFormatException if the input value is NaN or infinite.
2131      */
2132     private static void checkNumber(Number value) throws NumberFormatException {
2133         if (value instanceof Double) {
2134             if (!Double.isFinite(value.doubleValue())) {
2135                 throw new NumberFormatException("Cannot represent " + value + " in FITS headers.");
2136             }
2137         } else if (value instanceof Float) {
2138             if (!Float.isFinite(value.floatValue())) {
2139                 throw new NumberFormatException("Cannot represent " + value + " in FITS headers.");
2140             }
2141         }
2142     }
2143 
2144 }