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