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