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