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