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