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