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

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

/*
 * #%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.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> = '&'] at minimum... */
    public static final int MAX_HIERARCH_KEYWORD_LENGTH = FITS_HEADER_CARD_SIZE - 6;

    /** 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()) { // Discard trailing spaces aValue = trimEnd(aValue); // Remember that quotes get doubled in the value... String printValue = aValue.replace("'", "''"); // Check that the value fits in the space available for it. if (!FitsFactory.isLongStringsEnabled() && (printValue.length() + STRING_QUOTES_LENGTH) > spaceForValue()) { throw new HeaderCardException("value too long: [" + sanitize(aValue) + "]", new LongStringsNotEnabledException(key)); } } 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 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 IllegalArgumentException, LongStringsNotEnabledException { checkValueType(IFitsHeader.VALUE.STRING); if (update == null) { // There is always room for an empty string... value = null; } else { validateChars(update); int l = STRING_QUOTES_LENGTH + update.length(); if (!FitsFactory.isLongStringsEnabled() && l > spaceForValue(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 = trimEnd(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 #spaceForValue() */ private synchronized int getHeaderValueSize() { if (isStringValue() && FitsFactory.isLongStringsEnabled()) { return Integer.MAX_VALUE; } int n = isStringValue() ? 2 : 0; if (value == null) { return n; } n += value.length(); for (int i = value.length(); --i >= 0;) { if (value.charAt(i) == '\'') { // Add the number of quotes that need quoting. 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); if (getHeaderValueSize() > spaceForValue(newKey)) { if (!isStringValue()) { throw new LongValueException(spaceForValue(newKey), newKey + "= " + value); } if (!FitsFactory.isLongStringsEnabled()) { throw new LongStringsNotEnabledException(newKey); } } 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) { return FITS_HEADER_CARD_SIZE - (Math.max(key.length(), MAX_KEYWORD_LENGTH) + FitsFactory.getHierarchFormater().getExtraSpaceRequired(key)); } 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 - 2024 Weber Informatics LLC | Privacy Policy