All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nom.tam.fits.HeaderCard Maven / Gradle / Ivy

Go to download

Java library for reading and writing FITS files. FITS, the Flexible Image Transport System, is the format commonly used in the archiving and transport of astronomical data.

There is a newer version: 1.21.0
Show newest version
/*
 * #%L
 * nom.tam FITS library
 * %%
 * Copyright (C) 2004 - 2024 nom-tam-fits
 * %%
 * This is free and unencumbered software released into the public domain.
 *
 * Anyone is free to copy, modify, publish, use, compile, sell, or
 * distribute this software, either in source code form or as a compiled
 * binary, for any purpose, commercial or non-commercial, and by any
 * means.
 *
 * In jurisdictions that recognize copyright laws, the author or authors
 * of this software dedicate any and all copyright interest in the
 * software to the public domain. We make this dedication for the benefit
 * of the public at large and to the detriment of our heirs and
 * successors. We intend this dedication to be an overt act of
 * relinquishment in perpetuity of all present and future rights to this
 * software under copyright law.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 * #L%
 */

package nom.tam.fits;

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.logging.Logger;

import nom.tam.fits.FitsFactory.FitsSettings;
import nom.tam.fits.header.IFitsHeader;
import nom.tam.fits.header.NonStandard;
import nom.tam.fits.header.hierarch.IHierarchKeyFormatter;
import nom.tam.util.ArrayDataInput;
import nom.tam.util.AsciiFuncs;
import nom.tam.util.ComplexValue;
import nom.tam.util.CursorValue;
import nom.tam.util.FitsInputStream;
import nom.tam.util.FlexFormat;
import nom.tam.util.InputReader;

import static nom.tam.fits.header.Standard.BLANKS;
import static nom.tam.fits.header.Standard.COMMENT;
import static nom.tam.fits.header.Standard.CONTINUE;
import static nom.tam.fits.header.Standard.HISTORY;

/**
 * An individual entry in the FITS header, such as a key/value pair with an optional comment field, or a comment-style
 * entry without a value field.
 */
public class HeaderCard implements CursorValue, Cloneable {

    private static final Logger LOG = Logger.getLogger(HeaderCard.class.getName());

    /** The number of characters per header card (line). */
    public static final int FITS_HEADER_CARD_SIZE = 80;

    /** Maximum length of a FITS keyword field */
    public static final int MAX_KEYWORD_LENGTH = 8;

    /** The length of two single quotes that must surround string values. */
    public static final int STRING_QUOTES_LENGTH = 2;

    /** Maximum length of a FITS value field. */
    public static final int MAX_VALUE_LENGTH = 70;

    /** Maximum length of a comment-style card comment field. */
    public static final int MAX_COMMENT_CARD_COMMENT_LENGTH = MAX_VALUE_LENGTH + 1;

    /** Maximum length of a FITS string value field. */
    public static final int MAX_STRING_VALUE_LENGTH = MAX_VALUE_LENGTH - 2;

    /** Maximum length of a FITS long string value field. the & for the continuation needs one char. */
    public static final int MAX_LONG_STRING_VALUE_LENGTH = MAX_STRING_VALUE_LENGTH - 1;

    /** if a commend needs the be specified 2 extra chars are needed to start the comment */
    public static final int MAX_LONG_STRING_VALUE_WITH_COMMENT_LENGTH = MAX_LONG_STRING_VALUE_LENGTH - 2;

    /** Maximum HIERARCH keyword length (80 chars must fit [<keyword>=T] at minimum... */
    public static final int MAX_HIERARCH_KEYWORD_LENGTH = FITS_HEADER_CARD_SIZE - 2;

    /** The start and end quotes of the string and the ampasant to continue the string. */
    public static final int MAX_LONG_STRING_CONTINUE_OVERHEAD = 3;

    /** The first ASCII character that may be used in header records */
    public static final char MIN_VALID_CHAR = 0x20;

    /** The last ASCII character that may be used in header records */
    public static final char MAX_VALID_CHAR = 0x7e;

    /** The default keyword to use instead of null or any number of blanks. */
    public static final String EMPTY_KEY = "";

    /** The string "HIERARCH." */
    private static final String HIERARCH_WITH_DOT = NonStandard.HIERARCH.key() + ".";

    /** The keyword part of the card (set to null if there's no keyword) */
    private String key;

    /** The keyword part of the card (set to null if there's no value / empty string) */
    private String value;

    /** The comment part of the card (set to null if there's no comment) */
    private String comment;

    private IFitsHeader standardKey;

    /**
     * The Java class associated to the value
     *
     * @since 1.16
     */
    private Class type;

    /**
     * Value type checking policies for when setting values for standardized keywords.
     * 
     * @author Attila Kovacs
     * 
     * @since  1.19
     */
    public enum ValueCheck {
        /** No value type checking will be performed */
        NONE,
        /** Attempting to set values of the wrong type for standardized keywords will log warnings */
        LOGGING,
        /** Throw exception when setting a value of the wrong type for a standardized keyword */
        EXCEPTION
    }

    /**
     * Default value type checking policy for cards with standardized {@link IFitsHeader} keywords.
     * 
     * @since 1.19
     */
    public static final ValueCheck DEFAULT_VALUE_CHECK_POLICY = ValueCheck.EXCEPTION;

    private static ValueCheck valueCheck = DEFAULT_VALUE_CHECK_POLICY;

    /** Private constructor for an empty card, used by other constructors. */
    private HeaderCard() {
    }

    /**
     * Creates a new header card, but reading from the specified data input stream. The card is expected to be describes
     * by one or more 80-character wide header 'lines'. If long string support is not enabled, then a new card is
     * created from the next 80-characters. When long string support is enabled, cunsecutive lines starting with
     * [CONTINUE ] after the first line will be aggregated into a single new card.
     *
     * @param  dis                    the data input stream
     *
     * @throws UnclosedQuoteException if the line contained an unclosed single quote.
     * @throws TruncatedFileException if we reached the end of file unexpectedly before fully parsing an 80-character
     *                                    line.
     * @throws IOException            if there was some IO issue.
     *
     * @see                           FitsFactory#setLongStringsEnabled(boolean)
     */
    @SuppressWarnings("deprecation")
    public HeaderCard(ArrayDataInput dis) throws UnclosedQuoteException, TruncatedFileException, IOException {
        this(new HeaderCardCountingArrayDataInput(dis));
    }

    /**
     * Creates a new header card, but reading from the specified data input. The card is expected to be describes by one
     * or more 80-character wide header 'lines'. If long string support is not enabled, then a new card is created from
     * the next 80-characters. When long string support is enabled, cunsecutive lines starting with
     * [CONTINUE ] after the first line will be aggregated into a single new card.
     * 
     * @deprecated                        (for internal use) Its visibility may be reduced or may be removed
     *                                        entirely in the future. Card counting should be internal to
     *                                        {@link HeaderCard}.
     *
     * @param      dis                    the data input
     *
     * @throws     UnclosedQuoteException if the line contained an unclosed single quote.
     * @throws     TruncatedFileException if we reached the end of file unexpectedly before fully parsing an
     *                                        80-character line.
     * @throws     IOException            if there was some IO issue.
     *
     * @see                               #HeaderCard(ArrayDataInput)
     * @see                               FitsFactory#setLongStringsEnabled(boolean)
     */
    @Deprecated
    public HeaderCard(HeaderCardCountingArrayDataInput dis)
            throws UnclosedQuoteException, TruncatedFileException, IOException {
        this();
        key = null;
        value = null;
        comment = null;
        type = null;

        String card = readOneHeaderLine(dis);
        HeaderCardParser parsed = new HeaderCardParser(card);

        // extract the key
        key = parsed.getKey();
        type = parsed.getInferredType();

        if (FitsFactory.isLongStringsEnabled() && parsed.isString() && parsed.getValue().endsWith("&")) {
            // Potentially a multi-record long string card...
            parseLongStringCard(dis, parsed);
        } else {
            value = parsed.getValue();
            type = parsed.getInferredType();
            comment = parsed.getTrimmedComment();
        }

    }

    /**
     * Creates a new card with a number value. The card will be created either in the integer, fixed-decimal, or format,
     * with the native precision. If the native precision cannot be fitted in the available card space, the value will
     * be represented with reduced precision with at least {@link FlexFormat#DOUBLE_DECIMALS}. Trailing zeroes will be
     * omitted.
     *
     * @param  key                 keyword
     * @param  value               value (can be null, in which case the card type defaults to
     *                                 Integer.class)
     *
     * @throws HeaderCardException for any invalid keyword or value.
     *
     * @since                      1.16
     *
     * @see                        #HeaderCard(String, Number, String)
     * @see                        #HeaderCard(String, Number, int, String)
     * @see                        #create(IFitsHeader, Number)
     * @see                        FitsFactory#setUseExponentD(boolean)
     */
    public HeaderCard(String key, Number value) throws HeaderCardException {
        this(key, value, FlexFormat.AUTO_PRECISION, null);
    }

    /**
     * Creates a new card with a number value and a comment. The card will be created either in the integer,
     * fixed-decimal, or format. If the native precision cannot be fitted in the available card space, the value will be
     * represented with reduced precision with at least {@link FlexFormat#DOUBLE_DECIMALS}. Trailing zeroes will be
     * omitted.
     *
     * @param  key                 keyword
     * @param  value               value (can be null, in which case the card type defaults to
     *                                 Integer.class)
     * @param  comment             optional comment, or null
     *
     * @throws HeaderCardException for any invalid keyword or value
     *
     * @see                        #HeaderCard(String, Number)
     * @see                        #HeaderCard(String, Number, int, String)
     * @see                        #create(IFitsHeader, Number)
     * @see                        FitsFactory#setUseExponentD(boolean)
     */
    public HeaderCard(String key, Number value, String comment) throws HeaderCardException {
        this(key, value, FlexFormat.AUTO_PRECISION, comment);
    }

    /**
     * Creates a new card with a number value, using scientific notation, with up to the specified decimal places
     * showing between the decimal place and the exponent. For example, if decimals is set to 2, then
     * {@link Math#PI} gets formatted as 3.14E0 (or 3.14D0 if
     * {@link FitsFactory#setUseExponentD(boolean)} is enabled).
     *
     * @param  key                 keyword
     * @param  value               value (can be null, in which case the card type defaults to
     *                                 Integer.class)
     * @param  decimals            the number of decimal places to show in the scientific notation.
     * @param  comment             optional comment, or null
     *
     * @throws HeaderCardException for any invalid keyword or value
     *
     * @see                        #HeaderCard(String, Number)
     * @see                        #HeaderCard(String, Number, String)
     * @see                        #create(IFitsHeader, Number)
     * @see                        FitsFactory#setUseExponentD(boolean)
     */
    public HeaderCard(String key, Number value, int decimals, String comment) throws HeaderCardException {
        if (value == null) {
            set(key, null, comment, Integer.class);
            return;
        }

        try {
            checkNumber(value);
        } catch (NumberFormatException e) {
            throw new HeaderCardException("FITS headers may not contain NaN or Infinite values", e);
        }
        set(key, new FlexFormat().setWidth(spaceForValue(key)).setPrecision(decimals).format(value), comment,
                value.getClass());
    }

    /**
     * Creates a new card with a boolean value (and no comment).
     *
     * @param  key                 keyword
     * @param  value               value (can be null)
     *
     * @throws HeaderCardException for any invalid keyword
     *
     * @see                        #HeaderCard(String, Boolean, String)
     * @see                        #create(IFitsHeader, Boolean)
     */
    public HeaderCard(String key, Boolean value) throws HeaderCardException {
        this(key, value, null);
    }

    /**
     * Creates a new card with a boolean value, and a comment.
     *
     * @param  key                 keyword
     * @param  value               value (can be null)
     * @param  comment             optional comment, or null
     *
     * @throws HeaderCardException for any invalid keyword or value
     *
     * @see                        #HeaderCard(String, Boolean)
     * @see                        #create(IFitsHeader, Boolean)
     */
    public HeaderCard(String key, Boolean value, String comment) throws HeaderCardException {
        this(key, value == null ? null : (value ? "T" : "F"), comment, Boolean.class);
    }

    /**
     * Creates a new card with a complex value. The real and imaginary parts will be shown either in the fixed decimal
     * format or in the exponential notation, whichever preserves more digits, or else whichever is the more compact
     * notation. Trailing zeroes will be omitted.
     *
     * @param  key                 keyword
     * @param  value               value (can be null)
     *
     * @throws HeaderCardException for any invalid keyword or value.
     *
     * @see                        #HeaderCard(String, ComplexValue, String)
     * @see                        #HeaderCard(String, ComplexValue, int, String)
     */
    public HeaderCard(String key, ComplexValue value) throws HeaderCardException {
        this(key, value, null);
    }

    /**
     * Creates a new card with a complex value and a comment. The real and imaginary parts will be shown either in the
     * fixed decimal format or in the exponential notation, whichever preserves more digits, or else whichever is the
     * more compact notation. Trailing zeroes will be omitted.
     *
     * @param  key                 keyword
     * @param  value               value (can be null)
     * @param  comment             optional comment, or null
     *
     * @throws HeaderCardException for any invalid keyword or value.
     *
     * @see                        #HeaderCard(String, ComplexValue)
     * @see                        #HeaderCard(String, ComplexValue, int, String)
     */
    public HeaderCard(String key, ComplexValue value, String comment) throws HeaderCardException {
        this();

        if (value == null) {
            set(key, null, comment, ComplexValue.class);
            return;
        }

        if (!value.isFinite()) {
            throw new HeaderCardException("Cannot represent " + value + " in FITS headers.");
        }
        set(key, value.toBoundedString(spaceForValue(key)), comment, ComplexValue.class);
    }

    /**
     * Creates a new card with a complex number value, using scientific (exponential) notation, with up to the specified
     * number of decimal places showing between the decimal point and the exponent. Trailing zeroes will be omitted. For
     * example, if decimals is set to 2, then (π, 12) gets formatted as (3.14E0,1.2E1).
     *
     * @param  key                 keyword
     * @param  value               value (can be null)
     * @param  decimals            the number of decimal places to show.
     * @param  comment             optional comment, or null
     *
     * @throws HeaderCardException for any invalid keyword or value.
     *
     * @see                        #HeaderCard(String, ComplexValue)
     * @see                        #HeaderCard(String, ComplexValue, String)
     */
    public HeaderCard(String key, ComplexValue value, int decimals, String comment) throws HeaderCardException {
        this();

        if (value == null) {
            set(key, null, comment, ComplexValue.class);
            return;
        }

        if (!value.isFinite()) {
            throw new HeaderCardException("Cannot represent " + value + " in FITS headers.");
        }
        set(key, value.toString(decimals), comment, ComplexValue.class);
    }

    /**
     * 

* This constructor is now DEPRECATED. You should use {@link #HeaderCard(String, String, String)} to create * cards with null strings, or else {@link #createCommentStyleCard(String, String)} to create any * comment-style card, or {@link #createCommentCard(String)} or {@link #createHistoryCard(String)} to create COMMENT * or HISTORY cards. *

*

* Creates a card with a string value or comment. *

* * @param key The key for the comment or nullable field. * @param comment The comment * @param withNullValue If true the new card will be a value stle card with a null string * value. Otherwise it's a comment-style card. * * @throws HeaderCardException for any invalid keyword or value * * @see #HeaderCard(String, String, String) * @see #createCommentStyleCard(String, String) * @see #createCommentCard(String) * @see #createHistoryCard(String) * * @deprecated Use {@link #HeaderCard(String, String, String)}, or * {@link #createCommentStyleCard(String, String)} instead. */ @Deprecated public HeaderCard(String key, String comment, boolean withNullValue) throws HeaderCardException { this(key, null, comment, withNullValue); } /** *

* This constructor is now DEPRECATED. It has always been a poor construct. You should use * {@link #HeaderCard(String, String, String)} to create cards with null strings, or else * {@link #createCommentStyleCard(String, String)} to create any comment-style card, or * {@link #createCommentCard(String)} or {@link #createHistoryCard(String)} to create COMMENT or HISTORY cards. *

* Creates a comment style card. This may be a comment style card in which case the nullable field should be false, * or a value field which has a null value, in which case the nullable field should be true. * * @param key The key for the comment or nullable field. * @param value The value (can be null) * @param comment The comment * @param nullable If true a null value is a valid value. Otherwise, a * null value turns this into a comment-style card. * * @throws HeaderCardException for any invalid keyword or value * * @see #HeaderCard(String, String, String) * @see #createCommentStyleCard(String, String) * @see #createCommentCard(String) * @see #createHistoryCard(String) * * @deprecated Use {@link #HeaderCard(String, String, String)}, or * {@link #createCommentStyleCard(String, String)} instead. */ @Deprecated public HeaderCard(String key, String value, String comment, boolean nullable) throws HeaderCardException { this(key, value, comment, (nullable || value != null) ? String.class : null); } /** * Creates a new card with a string value (and no comment). * * @param key keyword * @param value value * * @throws HeaderCardException for any invalid keyword or value * * @see #HeaderCard(String, String, String) * @see #create(IFitsHeader, String) */ public HeaderCard(String key, String value) throws HeaderCardException { this(key, value, null, String.class); } /** * Creates a new card with a string value, and a comment * * @param key keyword * @param value value * @param comment optional comment, or null * * @throws HeaderCardException for any invalid keyword or value * * @see #HeaderCard(String, String) * @see #create(IFitsHeader, String) */ public HeaderCard(String key, String value, String comment) throws HeaderCardException { this(key, value, comment, String.class); } /** * Creates a new card from its component parts. Use locally only... * * @param key Case-sensitive keyword (can be null for COMMENT) * @param value the serialized value (tailing spaces will be removed) * @param comment an optional comment or null. * @param type The Java class from which the value field was derived, or null if it's a * comment-style card with a null value. * * @throws HeaderCardException for any invalid keyword or value * * @see #set(String, String, String, Class) */ private HeaderCard(String key, String value, String comment, Class type) throws HeaderCardException { set(key, value, comment, type); this.type = type; } /** * Sets all components of the card to the specified values. For internal use only. * * @param aKey Case-sensitive keyword (can be null for an unkeyed comment) * @param aValue the serialized value (tailing spaces will be removed), or null * @param aComment an optional comment or null. * @param aType The Java class from which the value field was derived, or null if it's a * comment-style card. * * @throws HeaderCardException for any invalid keyword or value */ private synchronized void set(String aKey, String aValue, String aComment, Class aType) throws HeaderCardException { // TODO we never call with null type and non-null value internally, so this is dead code here... // if (aType == null && aValue != null) { // throw new HeaderCardException("Null type for value: [" + sanitize(aValue) + "]"); // } type = aType; // AK: Map null and blank keys to BLANKS.key() // This simplifies things as we won't have to check for null keys separately! if ((aKey == null) || aKey.trim().isEmpty()) { aKey = EMPTY_KEY; } if (aKey.isEmpty() && aValue != null) { throw new HeaderCardException("Blank or null key for value: [" + sanitize(aValue) + "]"); } try { validateKey(aKey); } catch (RuntimeException e) { throw new HeaderCardException("Invalid FITS keyword: [" + sanitize(aKey) + "]", e); } key = aKey; try { validateChars(aComment); } catch (IllegalArgumentException e) { throw new HeaderCardException("Invalid FITS comment: [" + sanitize(aComment) + "]", e); } comment = aComment; try { validateChars(aValue); } catch (IllegalArgumentException e) { throw new HeaderCardException("Invalid FITS value: [" + sanitize(aValue) + "]", e); } if (aValue == null) { value = null; return; } if (isStringValue()) { try { setValue(aValue); } catch (Exception e) { throw new HeaderCardException("Value too long: [" + sanitize(aValue) + "]", e); } } else { aValue = aValue.trim(); // Check that the value fits in the space available for it. if (aValue.length() > spaceForValue()) { throw new HeaderCardException("Value too long: [" + sanitize(aValue) + "]", new LongValueException(key, spaceForValue())); } value = aValue; } } @Override protected HeaderCard clone() { try { return (HeaderCard) super.clone(); } catch (CloneNotSupportedException e) { return null; } } /** * Returns the number of 80-character header lines needed to store the data from this card. * * @return the size of the card in blocks of 80 bytes. So normally every card will return 1. only long stings can * return more than one, provided support for long string is enabled. */ public synchronized int cardSize() { if (FitsFactory.isLongStringsEnabled() && isStringValue() && value != null) { // this is very bad for performance but it is to difficult to // keep the cardSize and the toString compatible at all times return toString().length() / FITS_HEADER_CARD_SIZE; } return 1; } /** * Returns an independent copy of this card. Both this card and the returned value will have identical content, but * modifying one is guaranteed to not affect the other. * * @return a copy of this carf. */ public HeaderCard copy() { HeaderCard copy = clone(); return copy; } /** * Returns the keyword component of this card, which may be empty but never null, but it may be an * empty string. * * @return the keyword from this card, guaranteed to be not null). * * @see #getValue() * @see #getComment() */ @Override public final synchronized String getKey() { return key; } /** * Returns the serialized value component of this card, which may be null. * * @return the value from this card * * @see #getValue(Class, Object) * @see #getKey() * @see #getComment() */ public final synchronized String getValue() { return value; } /** * Returns the comment component of this card, which may be null. * * @return the comment from this card * * @see #getKey() * @see #getValue() */ public final synchronized String getComment() { return comment; } /** * @deprecated Not supported by the FITS standard, so do not use. It was included due to a * misreading of the standard itself. * * @return the value from this card * * @throws NumberFormatException if the card's value is null or cannot be parsed as a hexadecimal value. * * @see #getValue() */ @Deprecated public final synchronized long getHexValue() throws NumberFormatException { if (value == null) { throw new NumberFormatException("Card has a null value"); } return Long.decode("0x" + value); } /** *

* Returns the value cast to the specified type, if possible, or the specified default value if the value is * null or if the value is incompatible with the requested type. *

*

* For number types and values, if the requested type has lesser range or precision than the number stored in the * FITS header, the value is automatically downcast (i.e. possible rounded and/or truncated) -- the same as if an * explicit cast were used in Java. As long as the header value is a proper decimal value, it will be returned as * any requested number type. *

* * @param asType the requested class of the value * @param defaultValue the value to use if the card has a null value, or a value that cannot be cast to * the specified type. * @param the generic type of the requested class * * @return the value from this card as a specific type, or the specified default value * * @throws IllegalArgumentException if the specified Java type of not one that is supported for use in FITS headers. */ public synchronized T getValue(Class asType, T defaultValue) throws IllegalArgumentException { if (value == null) { return defaultValue; } if (String.class.isAssignableFrom(asType)) { return asType.cast(value); } if (value.isEmpty()) { return defaultValue; } if (Boolean.class.isAssignableFrom(asType)) { return asType.cast(getBooleanValue((Boolean) defaultValue)); } if (ComplexValue.class.isAssignableFrom(asType)) { return asType.cast(new ComplexValue(value)); } if (Number.class.isAssignableFrom(asType)) { try { BigDecimal big = new BigDecimal(value.toUpperCase().replace('D', 'E')); if (Byte.class.isAssignableFrom(asType)) { return asType.cast(big.byteValue()); } if (Short.class.isAssignableFrom(asType)) { return asType.cast(big.shortValue()); } if (Integer.class.isAssignableFrom(asType)) { return asType.cast(big.intValue()); } if (Long.class.isAssignableFrom(asType)) { return asType.cast(big.longValue()); } if (Float.class.isAssignableFrom(asType)) { return asType.cast(big.floatValue()); } if (Double.class.isAssignableFrom(asType)) { return asType.cast(big.doubleValue()); } if (BigInteger.class.isAssignableFrom(asType)) { return asType.cast(big.toBigInteger()); } // All possibilities have been exhausted, it must be a BigDecimal... return asType.cast(big); } catch (NumberFormatException e) { // The value is not a decimal number, so return the default value by contract. return defaultValue; } } throw new IllegalArgumentException("unsupported class " + asType); } /** * Checks if this card has both a valid keyword and a (non-null) value. * * @return Is this a key/value card? * * @see #isCommentStyleCard() */ public synchronized boolean isKeyValuePair() { return !isCommentStyleCard() && !(key.isEmpty() || value == null); } /** * Checks if this card has a string value (which may be null). * * @return true if this card has a string value, otherwise false. * * @see #isDecimalType() * @see #isIntegerType() * @see #valueType() */ public synchronized boolean isStringValue() { if (type == null) { return false; } return String.class.isAssignableFrom(type); } /** * Checks if this card has a decimal (floating-point) type value (which may be null). * * @return true if this card has a decimal (not integer) type number value, otherwise * false. * * @see #isIntegerType() * @see #isStringValue() * @see #valueType() * * @since 1.16 */ public synchronized boolean isDecimalType() { if (type == null) { return false; } return Float.class.isAssignableFrom(type) || Double.class.isAssignableFrom(type) || BigDecimal.class.isAssignableFrom(type); } /** * Checks if this card has an integer type value (which may be null). * * @return true if this card has an integer type value, otherwise false. * * @see #isDecimalType() * @see #isStringValue() * @see #valueType() * * @since 1.16 */ public synchronized boolean isIntegerType() { if (type == null) { return false; } return Number.class.isAssignableFrom(type) && !isDecimalType(); } /** * Checks if this card is a comment-style card with no associated value field. * * @return true if this card is a comment-style card, otherwise false. * * @see #isKeyValuePair() * @see #isStringValue() * @see #valueType() * * @since 1.16 */ public final synchronized boolean isCommentStyleCard() { return (type == null); } /** * Checks if this card cas a hierarch style long keyword. * * @return true if the card has a non-standard HIERARCH style long keyword, with dot-separated * components. Otherwise false. * * @since 1.16 */ public final synchronized boolean hasHierarchKey() { return isHierarchKey(key); } /** * Sets a new comment component for this card. The specified comment string will be sanitized to ensure it onlly * contains characters suitable for FITS headers. Invalid characters will be replaced with '?'. * * @param comment the new comment text. */ public synchronized void setComment(String comment) { this.comment = sanitize(comment); } /** * Sets a new number value for this card. The new value will be shown in the integer, fixed-decimal, or format, * whichever preserves more digits, or else whichever is the more compact notation. Trailing zeroes will be omitted. * * @param update the new value to set (can be null, in which case the card type * defaults to Integer.class) * * @return the card itself * * @throws NumberFormatException if the input value is NaN or Infinity. * @throws LongValueException if the decimal value cannot be represented in the alotted space * * @see #setValue(Number, int) */ public final HeaderCard setValue(Number update) throws NumberFormatException, LongValueException { return setValue(update, FlexFormat.AUTO_PRECISION); } /** * Sets a new number value for this card, using scientific (exponential) notation, with up to the specified decimal * places showing between the decimal point and the exponent. For example, if decimals is set to 2, * then π gets formatted as 3.14E0. * * @param update the new value to set (can be null, in which case the card type * defaults to Integer.class) * @param decimals the number of decimal places to show in the scientific notation. * * @return the card itself * * @throws NumberFormatException if the input value is NaN or Infinity. * @throws LongValueException if the decimal value cannot be represented in the alotted space * * @see #setValue(Number) */ public synchronized HeaderCard setValue(Number update, int decimals) throws NumberFormatException, LongValueException { if (update instanceof Float || update instanceof Double || update instanceof BigDecimal || update instanceof BigInteger) { checkValueType(IFitsHeader.VALUE.REAL); } else { checkValueType(IFitsHeader.VALUE.INTEGER); } if (update == null) { value = null; type = Integer.class; } else { type = update.getClass(); checkNumber(update); setUnquotedValue(new FlexFormat().forCard(this).setPrecision(decimals).format(update)); } return this; } private static void checkKeyword(IFitsHeader keyword) throws IllegalArgumentException { if (keyword.key().contains("n")) { throw new IllegalArgumentException("Keyword " + keyword.key() + " has unfilled index(es)"); } } private void checkValueType(IFitsHeader.VALUE valueType) throws ValueTypeException { if (standardKey != null) { checkValueType(key, standardKey.valueType(), valueType); } } private static void checkValueType(String key, IFitsHeader.VALUE expect, IFitsHeader.VALUE valueType) throws ValueTypeException { if (expect == IFitsHeader.VALUE.ANY || valueCheck == ValueCheck.NONE) { return; } if (valueType != expect) { if (expect == IFitsHeader.VALUE.REAL && valueType == IFitsHeader.VALUE.INTEGER) { return; } ValueTypeException e = new ValueTypeException(key, valueType.name()); if (valueCheck == ValueCheck.LOGGING) { LOG.warning(e.getMessage()); } else { throw e; } } } /** * Sets a new boolean value for this cardvalueType * * @param update the new value to se (can be null). * * @throws LongValueException if the card has no room even for the single-character 'T' or 'F'. This can never * happen with cards created programmatically as they will not allow setting * HIERARCH-style keywords long enough to ever trigger this condition. But, it is * possible to read cards from a non-standard header, which breaches this limit, by * ommitting some required spaces (esp. after the '='), and have a null value. When * that happens, we can be left without room for even a single character. * @throws ValueTypeException if the card's standard keyword does not support boolean values. * * @return the card itself */ public synchronized HeaderCard setValue(Boolean update) throws LongValueException, ValueTypeException { checkValueType(IFitsHeader.VALUE.LOGICAL); if (update == null) { value = null; } else if (spaceForValue() < 1) { throw new LongValueException(key, spaceForValue()); } else { // There is always room for a boolean value. :-) value = update ? "T" : "F"; } type = Boolean.class; return this; } /** * Sets a new complex number value for this card. The real and imaginary part will be shown in the integer, * fixed-decimal, or format, whichever preserves more digits, or else whichever is the more compact notation. * Trailing zeroes will be omitted. * * @param update the new value to set (can be null) * * @return the card itself * * @throws NumberFormatException if the input value is NaN or Infinity. * @throws LongValueException if the decimal value cannot be represented in the alotted space * * @see #setValue(ComplexValue, int) * * @since 1.16 */ public final HeaderCard setValue(ComplexValue update) throws NumberFormatException, LongValueException { return setValue(update, FlexFormat.AUTO_PRECISION); } /** * Sets a new complex number value for this card, using scientific (exponential) notation, with up to the specified * number of decimal places showing between the decimal point and the exponent. Trailing zeroes will be omitted. For * example, if decimals is set to 2, then (π, 12) gets formatted as (3.14E0,1.2E1). * * @param update the new value to set (can be null) * @param decimals the number of decimal places to show in the scientific notation. * * @return the HeaderCard itself * * @throws NumberFormatException if the input value is NaN or Infinity. * @throws LongValueException if the decimal value cannot be represented in the alotted space * * @see #setValue(ComplexValue) * * @since 1.16 */ public synchronized HeaderCard setValue(ComplexValue update, int decimals) throws LongValueException { checkValueType(IFitsHeader.VALUE.COMPLEX); if (update == null) { value = null; } else { if (!update.isFinite()) { throw new NumberFormatException("Cannot represent " + update + " in FITS headers."); } setUnquotedValue(update.toString(decimals)); } type = ComplexValue.class; return this; } /** * Sets a new unquoted value for this card, checking to make sure it fits in the available header space. If the * value is too long to fit, an IllegalArgumentException will be thrown. * * @param update the new unquoted header value for this card, as a string. * * @throws LongValueException if the value is too long to fit in the available space. */ private synchronized void setUnquotedValue(String update) throws LongValueException { if (update.length() > spaceForValue()) { throw new LongValueException(spaceForValue(), key, value); } value = update; } /** * @deprecated Not supported by the FITS standard, so do not use. It was included due to a * misreading of the standard itself. * * @param update the new value to set * * @return the HeaderCard itself * * @throws LongValueException if the value is too long to fit in the available space. * * @since 1.16 */ @Deprecated public synchronized HeaderCard setHexValue(long update) throws LongValueException { setUnquotedValue(Long.toHexString(update)); type = (update == (int) update) ? Integer.class : Long.class; return this; } /** * Sets a new string value for this card. * * @param update the new value to set * * @return the HeaderCard itself * * @throws ValueTypeException if the card's keyword does not support string values. * @throws IllegalStateException if the card has a HIERARCH keyword that is too long to fit any string * value. * @throws IllegalArgumentException if the new value contains characters that cannot be added to the the FITS * header. * @throws LongStringsNotEnabledException if the card contains a long string but support for long strings is * currently disabled. * * @see FitsFactory#setLongStringsEnabled(boolean) * @see #validateChars(String) */ public synchronized HeaderCard setValue(String update) throws ValueTypeException, IllegalStateException, IllegalArgumentException, LongStringsNotEnabledException { checkValueType(IFitsHeader.VALUE.STRING); int space = spaceForValue(key); if (space < STRING_QUOTES_LENGTH) { throw new IllegalStateException("No space for string value for [" + key + "]"); } if (update == null) { // There is always room for a null string... value = null; } else { validateChars(update); update = trimEnd(update); int l = getHeaderValueSize(update); if (space < l) { if (FitsFactory.isLongStringsEnabled()) { throw new IllegalStateException("No space for long string value for [" + key + "]"); } throw new LongStringsNotEnabledException("New string value for [" + key + "] is too long." + "\n\n --> You can enable long string support by FitsFactory.setLongStringEnabled(true).\n"); } value = update; } type = String.class; return this; } /** * Returns the modulo 80 character card image, the toString tries to preserve as much as possible of the comment * value by reducing the alignment of the Strings if the comment is longer and if longString is enabled the string * can be split into one more card to have more space for the comment. * * @return the FITS card as one or more 80-character string blocks. * * @throws LongValueException if the card has a long string value that is too long to contain in the * space available after the keyword. * @throws LongStringsNotEnabledException if the card contains a long string but support for long strings is * currently disabled. * @throws HierarchNotEnabledException if the card contains a HIERARCH-style long keyword but support for these * is currently disabled. * * @see FitsFactory#setLongStringsEnabled(boolean) */ @Override public String toString() throws LongValueException, LongStringsNotEnabledException, HierarchNotEnabledException { return toString(FitsFactory.current()); } /** * Same as {@link #toString()} just with a prefetched settings object * * @param settings the settings to use for writing the header card * * @return the string representing the card. * * @throws LongValueException if the card has a long string value that is too long to contain in the * space available after the keyword. * @throws LongStringsNotEnabledException if the card contains a long string but support for long strings is * disabled in the settings. * @throws HierarchNotEnabledException if the card contains a HIERARCH-style long keyword but support for these * is disabled in the settings. * * @see FitsFactory#setLongStringsEnabled(boolean) */ protected synchronized String toString(final FitsSettings settings) throws LongValueException, LongStringsNotEnabledException, HierarchNotEnabledException { return new HeaderCardFormatter(settings).toString(this); } /** * Returns the class of the associated value, or null if it's a comment-style card. * * @return the type of the value. * * @see #isCommentStyleCard() * @see #isKeyValuePair() * @see #isIntegerType() * @see #isDecimalType() */ public synchronized Class valueType() { return type; } /** * Returns the value as a boolean, or the default value if the card has no associated value or it is not a boolean. * * @param defaultValue the default value to return if the card has no associated value or is not a boolean. * * @return the boolean value of this card, or else the default value. */ private Boolean getBooleanValue(Boolean defaultValue) { if ("T".equals(value)) { return true; } if ("F".equals(value)) { return false; } return defaultValue; } /** * Parses a continued long string value and comment for this card, which may occupy one or more consecutive * 80-character header records. * * @param dis the input stream from which to parse the value and comment fields of this card. * @param next the parser to use for each 80-character record. * * @throws IOException if there was an IO error reading the stream. * @throws TruncatedFileException if the stream endedc ubnexpectedly in the middle of an 80-character record. */ @SuppressWarnings("deprecation") private synchronized void parseLongStringCard(HeaderCardCountingArrayDataInput dis, HeaderCardParser next) throws IOException, TruncatedFileException { StringBuilder longValue = new StringBuilder(); StringBuilder longComment = null; while (next != null) { if (!next.isString()) { break; } String valuePart = next.getValue(); String untrimmedComment = next.getUntrimmedComment(); if (valuePart == null) { // The card cannot have a null value. If it does it wasn't a string card... break; } // The end point of the value int valueEnd = valuePart.length(); // Check if there card continues into the next record. The value // must end with '&' and the next card must be a CONTINUE card. // If so, remove the '&' from the value part, and parse in the next // card for the next iteration... if (!dis.markSupported()) { throw new IOException("InputStream does not support mark/reset"); } // Peek at the next card. dis.mark(); try { // Check if we should continue parsing this card... next = new HeaderCardParser(readOneHeaderLine(dis)); if (valuePart.endsWith("&") && CONTINUE.key().equals(next.getKey())) { // Remove '& from the value part... valueEnd--; } else { // ok move the input stream one card back. dis.reset(); // Clear the parser also. next = null; } } catch (EOFException e) { // Nothing left to parse after the current one... next = null; } // Append the value part from the record last parsed. longValue.append(valuePart, 0, valueEnd); // Append any comment from the card last parsed. if (untrimmedComment != null) { if (longComment == null) { longComment = new StringBuilder(untrimmedComment); } else { longComment.append(untrimmedComment); } } } comment = longComment == null ? null : longComment.toString().trim(); value = trimEnd(longValue.toString()); type = String.class; } /** * Removes the trailing spaces (if any) from a string. According to the FITS standard, trailing spaces in string are * not significant (but leading spaces are). As such we should remove trailing spaces when parsing header string * values. * * @param s the string as it appears in the FITS header * * @return the input string if it has no trailing spaces, or else a new string with the trailing spaces removed. */ private String trimEnd(String s) { int end = s.length(); for (; end > 0; end--) { if (!Character.isSpaceChar(s.charAt(end - 1))) { break; } } return end == s.length() ? s : s.substring(0, end); } /** * Returns the minimum number of characters the value field will occupy in the header record, including quotes * around string values, and quoted quotes inside. The actual header may add padding (e.g. to ensure the end quote * does not come before byte 20). * * @return the minimum number of bytes needed to represent this value in a header record. * * @since 1.16 * * @see #getHeaderValueSize(String) * @see #spaceForValue() */ synchronized int getHeaderValueSize() { return getHeaderValueSize(value); } /** * Returns the minimum number of characters the value field will occupy in the header record, including quotes * around string values, and quoted quotes inside. The actual header may add padding (e.g. to ensure the end quote * does not come before byte 20). If the long string convention is enabled, this method returns the minimum number * of characters needed in the leading 80-character record only. The call assumes that the value has been * appropriately trimmed of trailing and leading spaces as appropriate. * * @param aValue The proposed value for this card * * @return the minimum number of bytes needed to represent this value in a header record. * * @since 1.16 * * @see #spaceForValue() * @see #trimEnd(String) */ private synchronized int getHeaderValueSize(String aValue) { if (aValue == null) { return 0; } if (!isStringValue()) { return aValue.length(); } int n = STRING_QUOTES_LENGTH; if (FitsFactory.isLongStringsEnabled()) { // If not empty string we need to write at least &... return aValue.isEmpty() ? n : n + 1; } n += aValue.length(); for (int i = aValue.length(); --i >= 0;) { if (aValue.charAt(i) == '\'') { // Add the number of quotes that need escaping. n++; } } return n; } /** * Returns the space available for value and/or comment in a single record the keyword. * * @return the number of characters available in a single 80-character header record for a standard (non long * string) value and/or comment. * * @since 1.16 */ public final synchronized int spaceForValue() { return spaceForValue(key); } /** * Updates the keyword for this card. * * @param newKey the new FITS header keyword to use for this card. * * @throws HierarchNotEnabledException if the new key is a HIERARCH-style long key but support for these is not * currently enabled. * @throws IllegalArgumentException if the keyword contains invalid characters * @throws LongValueException if the new keyword does not leave sufficient room for the current * non-string value. * @throws LongStringsNotEnabledException if the new keyword does not leave sufficient rooom for the current string * value without enabling long string support. * * @see FitsFactory#setLongStringsEnabled(boolean) * @see #spaceForValue() * @see #getValue() */ public synchronized void changeKey(String newKey) throws HierarchNotEnabledException, LongValueException, LongStringsNotEnabledException, IllegalArgumentException { validateKey(newKey); int l = getHeaderValueSize(); int space = spaceForValue(newKey); if (l > space) { if (isStringValue() && !FitsFactory.isLongStringsEnabled() && space > STRING_QUOTES_LENGTH) { throw new LongStringsNotEnabledException(newKey); } throw new LongValueException(spaceForValue(newKey), newKey + "= " + value); } key = newKey; standardKey = null; } /** * Checks if the card is blank, that is if it contains only empty spaces. * * @return true if the card contains nothing but blank spaces. */ public synchronized boolean isBlank() { if (!isCommentStyleCard() || !key.isEmpty()) { return false; } if (comment == null) { return true; } return comment.isEmpty(); } /** * Returns the current policy for checking if set values are of the allowed type for cards with standardized * {@link IFitsHeader} keywords. * * @return the current value type checking policy * * @since 1.19 * * @see #setValueCheckingPolicy(ValueCheck) */ public static ValueCheck getValueCheckingPolicy() { return valueCheck; } /** * Sets the policy to used for checking if set values conform to the expected types for cards that use standardized * FITS keywords via the {@link IFitsHeader} interface. * * @param policy the new polict to use for checking value types. * * @see #getValueCheckingPolicy() * @see Header#setKeywordChecking(nom.tam.fits.Header.KeywordCheck) * * @since 1.19 */ public static void setValueCheckingPolicy(ValueCheck policy) { valueCheck = policy; } /** *

* Creates a new FITS header card from a FITS stream representation of it, which is how the key/value and comment * are represented inside the FITS file, normally as an 80-character wide entry. The parsing of header 'lines' * conforms to all FITS standards, and some optional conventions, such as HIERARCH keywords (if * {@link FitsFactory#setUseHierarch(boolean)} is enabled), COMMENT and HISTORY entries, and OGIP 1.0 long CONTINUE * lines (if {@link FitsFactory#setLongStringsEnabled(boolean)} is enabled). *

*

* However, the parsing here is permissive beyond the standards and conventions, and will do its best to support a * wide range of FITS files, which may deviate from the standard in subtle (or no so subtle) ways. *

*

* Here is a brief summary of the rules that guide the parsing of keywords, values, and comment 'fields' from the * single header line: *

*

* A. Keywords *

*
    *
  • The standard FITS keyword is the first 8 characters of the line, or up to an equal [=] character, whichever * comes first, with trailing spaces removed, and always converted to upper-case.
  • *
  • If {@link FitsFactory#setUseHierarch(boolean)} is enabled, structured longer keywords can be composed after a * HIERARCH base key, followed by space (and/or dot ].]) separated parts, up to an equal sign [=]. The * library will represent the same components (including HIERARCH) but separated by single dots [.]. * For example, the header line starting with [HIERARCH SMA OBS TARGET =], will be referred as * [HIERARCH.SMA.OBS.TARGET] withing this library. The keyword parts can be composed of any ASCII * characters except dot [.], white spaces, or equal [=].
  • *
  • By default, all parts of the key are converted to upper-case. Case sensitive HIERARCH keywords can be * retained after enabling * {@link nom.tam.fits.header.hierarch.IHierarchKeyFormatter#setCaseSensitive(boolean)}.
  • *
*

* B. Values *

*

* Values are the part of the header line, that is between the keyword and an optional ending comment. Legal header * values follow the following parse patterns: *

    *
  • Begin with an equal sign [=], or else come after a CONTINUE keyword.
  • *
  • Next can be a quoted value such as 'hello', placed inside two single quotes. Or an unquoted * value, such as 123.
  • *
  • Quoted values must begin with a single quote ['] and and with the next single quote. If there is no end-quote * in the line, it is not considered a string value but rather a comment, unless * {@link FitsFactory#setAllowHeaderRepairs(boolean)} is enabled, in which case the entire remaining line after the * opening quote is assumed to be a malformed value.
  • *
  • Unquoted values end at the fist [/] character, or else go until the line end.
  • *
  • Quoted values have trailing spaces removed, s.t. [' value '] becomes * [ value].
  • *
  • Unquoted values are trimmed, with both leading and trailing spaces removed, e.g. [ 123 ] * becomes [123].
  • *
*

* C. Comments *

*

* The following rules guide the parsing of the values component: *

    *
  • If a value is present (see above), the comment is what comes after it. That is, for quoted values, everything * that follows the closing quote. For unquoted values, it's what comes after the first [/], with the [/] itself * removed.
  • *
  • If a value is not present, then everything following the keyword is considered the comment.
  • *
  • Comments are trimmed, with both leading and trailing spaces removed.
  • *
* * @return a newly created HeaderCard from a FITS card string. * * @param line the card image (typically 80 characters if in a FITS file). * * @throws IllegalArgumentException if the card was malformed, truncated, or if there was an IO error. * * @see FitsFactory#setUseHierarch(boolean) * @see nom.tam.fits.header.hierarch.IHierarchKeyFormatter#setCaseSensitive(boolean) */ public static HeaderCard create(String line) throws IllegalArgumentException { try (ArrayDataInput in = stringToArrayInputStream(line)) { return new HeaderCard(in); } catch (Exception e) { throw new IllegalArgumentException("card not legal", e); } } final IFitsHeader getStandardKey() { return standardKey; } /** * Creates a new card with a standard or conventional keyword and a boolean value, with the default comment * associated with the keyword. Unlike {@link #HeaderCard(String, Boolean)}, this call does not throw an exception, * since the keyword and comment should be valid by design. * * @param key The standard or conventional keyword with its associated default comment. * @param value the boolean value associated to the keyword * * @return A new header card with the speficied standard-style key and comment and the * specified value, or null if the standard key itself is * malformed or illegal. * * @throws IllegalArgumentException if the standard key was ill-defined. * * @since 1.16 */ public static HeaderCard create(IFitsHeader key, Boolean value) throws IllegalArgumentException { checkKeyword(key); try { HeaderCard hc = new HeaderCard(key.key(), (Boolean) null, key.comment()); hc.standardKey = key; hc.setValue(value); return hc; } catch (HeaderCardException e) { throw new IllegalArgumentException(e.getMessage(), e); } } /** *

* Creates a new card with a standard or conventional keyword and a number value, with the default comment * associated with the keyword. Unlike {@link #HeaderCard(String, Number)}, this call does not throw a hard * {@link HeaderCardException} exception, since the keyword and comment should be valid by design. (A runtime * {@link IllegalArgumentException} may still be thrown in the event that the supplied conventional keywords itself * is ill-defined -- but this should not happen unless something was poorly coded in this library, on in an * extension of it). *

*

* If the value is not compatible with the convention of the keyword, a warning message is logged but no exception * is thrown (at this point). *

* * @param key The standard or conventional keyword with its associated default comment. * @param value the integer value associated to the keyword. * * @return A new header card with the speficied standard-style key and comment and the * specified value. * * @throws IllegalArgumentException if the standard key itself was ill-defined. * * @since 1.16 */ public static HeaderCard create(IFitsHeader key, Number value) throws IllegalArgumentException { checkKeyword(key); try { HeaderCard hc = new HeaderCard(key.key(), (Number) null, key.comment()); hc.standardKey = key; hc.setValue(value); return hc; } catch (HeaderCardException e) { throw new IllegalArgumentException(e.getMessage(), e); } } /** * Creates a new card with a standard or conventional keyword and a number value, with the default comment * associated with the keyword. Unlike {@link #HeaderCard(String, Number)}, this call does not throw an exception, * since the keyword and comment should be valid by design. * * @param key The standard or conventional keyword with its associated default comment. * @param value the integer value associated to the keyword. * * @return A new header card with the speficied standard-style key and comment and the * specified value. * * @throws IllegalArgumentException if the standard key was ill-defined. * * @since 1.16 */ public static HeaderCard create(IFitsHeader key, ComplexValue value) throws IllegalArgumentException { checkKeyword(key); try { HeaderCard hc = new HeaderCard(key.key(), (ComplexValue) null, key.comment()); hc.standardKey = key; hc.setValue(value); return hc; } catch (HeaderCardException e) { throw new IllegalArgumentException(e.getMessage(), e); } } /** * Creates a new card with a standard or conventional keyword and an integer value, with the default comment * associated with the keyword. Unlike {@link #HeaderCard(String, Number)}, this call does not throw a hard * exception, since the keyword and comment sohould be valid by design. The string value however will be checked, * and an appropriate runtime exception is thrown if it cannot be included in a FITS header. * * @param key The standard or conventional keyword with its associated default comment. * @param value the string associated to the keyword. * * @return A new header card with the speficied standard-style key and comment and the * specified value. * * @throws IllegalArgumentException if the string value contains characters that are not allowed in FITS headers, * that is characters outside of the 0x20 thru 0x7E range, or if the standard * key was ill-defined. */ public static HeaderCard create(IFitsHeader key, String value) throws IllegalArgumentException { checkKeyword(key); validateChars(value); try { HeaderCard hc = new HeaderCard(key.key(), (String) null, key.comment()); hc.standardKey = key; hc.setValue(value); return hc; } catch (HeaderCardException e) { throw new IllegalArgumentException(e.getMessage(), e); } } /** * Creates a comment-style card with no associated value field. * * @param key The keyword, or null blank/empty string for an unkeyed comment. * @param comment The comment text. * * @return a new comment-style header card with the specified key and comment text. * * @throws HeaderCardException if the key or value were invalid. * @throws LongValueException if the comment text is longer than the space available in comment-style cards (71 * characters max) * * @see #createUnkeyedCommentCard(String) * @see #createCommentCard(String) * @see #createHistoryCard(String) * @see Header#insertCommentStyle(String, String) * @see Header#insertCommentStyleMultiline(String, String) */ public static HeaderCard createCommentStyleCard(String key, String comment) throws HeaderCardException, LongValueException { if (comment == null) { comment = ""; } else if (comment.length() > MAX_COMMENT_CARD_COMMENT_LENGTH) { throw new LongValueException(MAX_COMMENT_CARD_COMMENT_LENGTH, key, comment); } HeaderCard card = new HeaderCard(); card.set(key, null, comment, null); return card; } /** * Creates a new unkeyed comment card for th FITS header. These are comment-style cards with no associated value * field, and with a blank keyword. They are commonly used to add explanatory notes in the FITS header. Keyed * comments are another alternative... * * @param text a concise descriptive entry (max 71 characters). * * @return a new COMMENT card with the specified key and comment text. * * @throws HeaderCardException if the text contains invalid charaters. * @throws LongValueException if the comment text is longer than the space available in comment-style cards (71 * characters max) * * @see #createCommentCard(String) * @see #createCommentStyleCard(String, String) * @see Header#insertUnkeyedComment(String) */ public static HeaderCard createUnkeyedCommentCard(String text) throws HeaderCardException, LongValueException { return createCommentStyleCard(BLANKS.key(), text); } /** * Creates a new keyed comment card for th FITS header. These are comment-style cards with no associated value * field, and with COMMENT as the keyword. They are commonly used to add explanatory notes in the FITS header. * Unkeyed comments are another alternative... * * @param text a concise descriptive entry (max 71 characters). * * @return a new COMMENT card with the specified key and comment text. * * @throws HeaderCardException if the text contains invalid charaters. * @throws LongValueException if the comment text is longer than the space available in comment-style cards (71 * characters max) * * @see #createUnkeyedCommentCard(String) * @see #createCommentStyleCard(String, String) * @see Header#insertComment(String) */ public static HeaderCard createCommentCard(String text) throws HeaderCardException, LongValueException { return createCommentStyleCard(COMMENT.key(), text); } /** * Creates a new history record for the FITS header. These are comment-style cards with no associated value field, * and with HISTORY as the keyword. They are commonly used to document the sequence operations that were performed * on the data before it arrived to the state represented by the FITS file. The text field for history entries is * limited to 70 characters max per card. However there is no limit to how many such entries are in a FITS header. * * @param text a concise descriptive entry (max 71 characters). * * @return a new HISTORY card with the specified key and comment text. * * @throws HeaderCardException if the text contains invalid charaters. * @throws LongValueException if the comment text is longer than the space available in comment-style cards (71 * characters max) * * @see #createCommentStyleCard(String, String) * @see Header#insertHistory(String) */ public static HeaderCard createHistoryCard(String text) throws HeaderCardException, LongValueException { return createCommentStyleCard(HISTORY.key(), text); } /** * @deprecated Not supported by the FITS standard, so do not use. It was included due to a * misreading of the standard itself. * * @param key the keyword * @param value the integer value * * @return A new header card, with the specified integer in hexadecomal representation. * * @throws HeaderCardException if the card is invalid (for example the keyword is not valid). * * @see #createHexValueCard(String, long, String) * @see #getHexValue() * @see Header#getHexValue(String) */ @Deprecated public static HeaderCard createHexValueCard(String key, long value) throws HeaderCardException { return createHexValueCard(key, value, null); } /** * @deprecated Not supported by the FITS standard, so do not use. It was included due to a * misreading of the standard itself. * * @param key the keyword * @param value the integer value * @param comment optional comment, or null. * * @return A new header card, with the specified integer in hexadecomal representation. * * @throws HeaderCardException if the card is invalid (for example the keyword is not valid). * * @see #createHexValueCard(String, long) * @see #getHexValue() * @see Header#getHexValue(String) */ @Deprecated public static HeaderCard createHexValueCard(String key, long value, String comment) throws HeaderCardException { return new HeaderCard(key, Long.toHexString(value), comment, Long.class); } /** * Reads an 80-byte card record from an input. * * @param in The input to read from * * @return The raw, undigested header record as a string. * * @throws IOException if already at the end of file. * @throws TruncatedFileException if there was not a complete record available in the input. */ private static String readRecord(InputReader in) throws IOException, TruncatedFileException { byte[] buffer = new byte[FITS_HEADER_CARD_SIZE]; int got = 0; try { // Read as long as there is more available, even if it comes in a trickle... while (got < buffer.length) { int n = in.read(buffer, got, buffer.length - got); if (n < 0) { break; } got += n; } } catch (EOFException e) { // Just in case read throws EOFException instead of returning -1 by contract. } if (got == 0) { // Nothing left to read. throw new EOFException(); } if (got < buffer.length) { // Got an incomplete header card... throw new TruncatedFileException( "Got only " + got + " of " + buffer.length + " bytes expected for a header card"); } return AsciiFuncs.asciiString(buffer); } /** * Read exactly one complete fits header line from the input. * * @param dis the data input stream to read the line * * @return a string of exactly 80 characters * * @throwa EOFException if already at the end of file. * * @throws TruncatedFileException if there was not a complete line available in the input. * @throws IOException if the input stream could not be read */ @SuppressWarnings({"resource", "deprecation"}) private static String readOneHeaderLine(HeaderCardCountingArrayDataInput dis) throws IOException, TruncatedFileException { String s = readRecord(dis.in()); dis.cardRead(); return s; } /** * Returns the maximum number of characters that can be used for a value field in a single FITS header record (80 * characters wide), after the specified keyword. * * @param key the header keyword, which may be a HIERARCH-style key... * * @return the space available for the value field in a single record, after the keyword, and the assigmnent * sequence (or equivalent blank space). */ private static int spaceForValue(String key) { if (key.length() > MAX_KEYWORD_LENGTH) { IHierarchKeyFormatter fmt = FitsFactory.getHierarchFormater(); int keyLen = Math.max(key.length(), MAX_KEYWORD_LENGTH) + fmt.getExtraSpaceRequired(key); return FITS_HEADER_CARD_SIZE - keyLen - fmt.getMinAssignLength(); } return FITS_HEADER_CARD_SIZE - (Math.max(key.length(), MAX_KEYWORD_LENGTH) + HeaderCardFormatter.getAssignLength()); } private static ArrayDataInput stringToArrayInputStream(String card) { byte[] bytes = AsciiFuncs.getBytes(card); if (bytes.length % FITS_HEADER_CARD_SIZE != 0) { byte[] newBytes = new byte[bytes.length + FITS_HEADER_CARD_SIZE - bytes.length % FITS_HEADER_CARD_SIZE]; System.arraycopy(bytes, 0, newBytes, 0, bytes.length); Arrays.fill(newBytes, bytes.length, newBytes.length, (byte) ' '); bytes = newBytes; } return new FitsInputStream(new ByteArrayInputStream(bytes)); } /** * This method was designed for use internally. It is 'safe' (not save!) in the sense that the runtime exception it * may throw does not need to be caught. * * @param key keyword * @param comment optional comment, or null * @param hasValue does this card have a (null) value field? If true a * null value of type String.class is assumed (for backward * compatibility). * * @return the new HeaderCard * * @throws HeaderCardException if the card could not be created for some reason (noted as the cause). * * @deprecated This was to be used internally only, without public visibility. It will become * unexposed to users in a future release... */ @Deprecated public static HeaderCard saveNewHeaderCard(String key, String comment, boolean hasValue) throws HeaderCardException { return new HeaderCard(key, null, comment, hasValue ? String.class : null); } /** * Checks if the specified keyword is a HIERARCH-style long keyword. * * @param key The keyword to check. * * @return true if the specified key may be a HIERARC-style key, otehrwise false. */ private static boolean isHierarchKey(String key) { return key.toUpperCase().startsWith(HIERARCH_WITH_DOT); } /** * Replaces illegal characters in the string ith '?' to be suitable for FITS header records. According to the FITS * standard, headers may only contain ASCII characters in the range 0x20 and 0x7E (inclusive). * * @param str the input string. * * @return the sanitized string for use in a FITS header, with illegal characters replaced by '?'. * * @see #isValidChar(char) * @see #validateChars(String) */ public static String sanitize(String str) { int nc = str.length(); char[] cbuf = new char[nc]; for (int ic = 0; ic < nc; ic++) { char c = str.charAt(ic); cbuf[ic] = isValidChar(c) ? c : '?'; } return new String(cbuf); } /** * Checks if a character is valid for inclusion in a FITS header record. The FITS standard specifies that only ASCII * characters between 0x20 thru 0x7E may be used in FITS headers. * * @param c the character to check * * @return true if the character is allowed in the FITS header, otherwise false. * * @see #validateChars(String) * @see #sanitize(String) */ public static boolean isValidChar(char c) { return (c >= MIN_VALID_CHAR && c <= MAX_VALID_CHAR); } /** * Checks the specified string for characters that are not allowed in FITS headers, and throws an exception if any * are found. According to the FITS standard, headers may only contain ASCII characters in the range 0x20 and 0x7E * (inclusive). * * @param text the input string * * @throws IllegalArgumentException if the unput string contains any characters that cannot be in a FITS header, * that is characters outside of the 0x20 to 0x7E range. * * @since 1.16 * * @see #isValidChar(char) * @see #sanitize(String) * @see #validateKey(String) */ public static void validateChars(String text) throws IllegalArgumentException { if (text == null) { return; } for (int i = text.length(); --i >= 0;) { char c = text.charAt(i); if (c < MIN_VALID_CHAR) { throw new IllegalArgumentException( "Non-printable character(s), e.g. 0x" + (int) c + ", in [" + sanitize(text) + "]."); } if (c > MAX_VALID_CHAR) { throw new IllegalArgumentException( "Extendeed ASCII character(s) in [" + sanitize(text) + "]. Only 0x20 through 0x7E are allowed."); } } } /** * Checks if the specified string may be used as a FITS header keyword according to the FITS standard and currently * settings for supporting extensions to the standard, such as HIERARCH-style keywords. * * @param key the proposed keyword string * * @throws IllegalArgumentException if the string cannot be used as a FITS keyword with the current settings. The * exception will contain an informative message describing the issue. * * @since 1.16 * * @see #validateChars(String) * @see FitsFactory#setUseHierarch(boolean) */ public static void validateKey(String key) throws IllegalArgumentException { int maxLength = MAX_KEYWORD_LENGTH; if (isHierarchKey(key)) { if (!FitsFactory.getUseHierarch()) { throw new HierarchNotEnabledException(key); } maxLength = MAX_HIERARCH_KEYWORD_LENGTH; validateHierarchComponents(key); } if (key.length() > maxLength) { throw new IllegalArgumentException("Keyword is too long: [" + sanitize(key) + "]"); } // Check the whole key for non-printable, non-standard ASCII for (int i = key.length(); --i >= 0;) { char c = key.charAt(i); if (c < MIN_VALID_CHAR) { throw new IllegalArgumentException( "Keyword contains non-printable character 0x" + (int) c + ": [" + sanitize(key) + "]."); } if (c > MAX_VALID_CHAR) { throw new IllegalArgumentException("Keyword contains extendeed ASCII characters: [" + sanitize(key) + "]. Only 0x20 through 0x7E are allowed."); } } // Check if the first 8 characters conform to strict FITS specification... for (int i = Math.min(MAX_KEYWORD_LENGTH, key.length()); --i >= 0;) { char c = key.charAt(i); if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { continue; } if ((c >= '0' && c <= '9') || (c == '-') || (c == '_')) { continue; } throw new IllegalArgumentException( "Keyword [" + sanitize(key) + "] contains invalid characters. Only [A-Z][a-z][0-9][-][_] are allowed."); } } /** * Additional checks the extended components of the HIEARCH key (in bytes 9-77), to make sure they conform to our * own standards of storing hierarch keys as a dot-separated list of components. That is, the keyword must not have * any spaces... * * @param key the HIERARCH keyword to check. * * @throws IllegalArgumentException if the keyword is not a proper dot-separated set of non-empty hierarchical * components */ private static void validateHierarchComponents(String key) throws IllegalArgumentException { for (int i = key.length(); --i >= 0;) { if (Character.isSpaceChar(key.charAt(i))) { throw new IllegalArgumentException( "No spaces allowed in HIERARCH keywords used internally: [" + sanitize(key) + "]."); } } if (key.indexOf("..") >= 0) { throw new IllegalArgumentException("HIERARCH keywords with empty component: [" + sanitize(key) + "]."); } } /** * Checks that a number value is not NaN or Infinite, since FITS does not have a standard for describing those * values in the header. If the value is not suitable for the FITS header, an exception is thrown. * * @param value The number to check * * @throws NumberFormatException if the input value is NaN or infinite. */ private static void checkNumber(Number value) throws NumberFormatException { if (value instanceof Double) { if (!Double.isFinite(value.doubleValue())) { throw new NumberFormatException("Cannot represent " + value + " in FITS headers."); } } else if (value instanceof Float) { if (!Float.isFinite(value.floatValue())) { throw new NumberFormatException("Cannot represent " + value + " in FITS headers."); } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy