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