FitsFactory.java

package nom.tam.fits;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

import nom.tam.fits.header.hierarch.IHierarchKeyFormatter;
import nom.tam.fits.header.hierarch.StandardIHierarchKeyFormatter;
import nom.tam.image.compression.hdu.CompressedImageData;
import nom.tam.image.compression.hdu.CompressedImageHDU;
import nom.tam.image.compression.hdu.CompressedTableData;
import nom.tam.image.compression.hdu.CompressedTableHDU;

/*
 * #%L
 * nom.tam FITS library
 * %%
 * Copyright (C) 2004 - 2021 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%
 */

/**
 * This class contains the code which associates particular FITS types with
 * header and data configurations. It comprises a set of Factory methods which
 * call appropriate methods in the HDU classes. If -- God forbid -- a new FITS
 * HDU type were created, then the XXHDU, XXData classes would need to be added
 * and this file modified but no other changes should be needed in the FITS
 * libraries.
 */
public final class FitsFactory {

    private static final boolean DEFAULT_USE_ASCII_TABLES = false;
    
    private static final boolean DEFAULT_USE_HIERARCH = true;
    
    private static final boolean DEFAULT_USE_EXPONENT_D = false;
    
    private static final boolean DEFAULT_LONG_STRINGS_ENABLED = true;
    
    private static final boolean DEFAULT_CHECK_ASCII_STRINGS = false;
    
    private static final boolean DEFAULT_ALLOW_TERMINAL_JUNK = true;
    
    private static final boolean DEFAULT_ALLOW_HEADER_REPAIRS = true;
    
    private static final boolean DEFAULT_SKIP_BLANK_AFTER_ASSIGN = false;
    
    private static final boolean DEFAULT_CASE_SENSITIVE_HIERARCH = false;
   
    /** 
     * AK:
     * true is the legacy behavior 
     * TODO If and when it is changed to false, the corresponding Logger warnings in BinaryTable
     * should also be removed. 
     */
    private static final boolean DEFAULT_USE_UNICODE_CHARS = true;
    
    
    private static final IHierarchKeyFormatter DEFAULT_HIERARCH_FORMATTER = new StandardIHierarchKeyFormatter();
    
    
    protected static final class FitsSettings implements Cloneable {

        private boolean useAsciiTables;

        private boolean useHierarch;
        
        private boolean useExponentD;
        
        private boolean checkAsciiStrings;

        private boolean allowTerminalJunk;

        private boolean allowHeaderRepairs;

        private boolean longStringsEnabled;
        
        private boolean useUnicodeChars;

        @Deprecated
        private boolean skipBlankAfterAssign;
        

        private IHierarchKeyFormatter hierarchKeyFormatter = DEFAULT_HIERARCH_FORMATTER;

        private FitsSettings() {
            useAsciiTables = DEFAULT_USE_ASCII_TABLES;
            useHierarch = DEFAULT_USE_HIERARCH;
            useUnicodeChars = DEFAULT_USE_UNICODE_CHARS;
            checkAsciiStrings = DEFAULT_CHECK_ASCII_STRINGS;
            useExponentD = DEFAULT_USE_EXPONENT_D;
            allowTerminalJunk = DEFAULT_ALLOW_TERMINAL_JUNK;
            allowHeaderRepairs = DEFAULT_ALLOW_HEADER_REPAIRS;
            longStringsEnabled = DEFAULT_LONG_STRINGS_ENABLED;
            skipBlankAfterAssign = DEFAULT_SKIP_BLANK_AFTER_ASSIGN;
            hierarchKeyFormatter = DEFAULT_HIERARCH_FORMATTER;
            hierarchKeyFormatter.setCaseSensitive(DEFAULT_CASE_SENSITIVE_HIERARCH);
        }
        
        @Override
        protected FitsSettings clone() {
            try { 
                return (FitsSettings) super.clone();
            } catch (CloneNotSupportedException e) {
                return null;
            }
        }
        
        private FitsSettings copy() {
            return clone();
        }

        protected IHierarchKeyFormatter getHierarchKeyFormatter() {
            return this.hierarchKeyFormatter;
        }
        
        protected boolean isUseExponentD() {
            return this.useExponentD;
        }

        protected boolean isAllowTerminalJunk() {
            return this.allowTerminalJunk;
        }

        protected boolean isCheckAsciiStrings() {
            return this.checkAsciiStrings;
        }

        protected boolean isLongStringsEnabled() {
            return this.longStringsEnabled;
        }

        /**
         * @deprecated The FITS standard is very explicit that assignment must be "= ". If we allow
         *              skipping the space, it will result in a non-standard FITS, that is likely
         *              to break compatibility with other tools.
         * 
         * @return  whether to use only "=", instead of the standard "= " between the keyword
         *          and the value.
         */
        protected boolean isSkipBlankAfterAssign() {
            return this.skipBlankAfterAssign;
        }

        protected boolean isUseAsciiTables() {
            return this.useAsciiTables;
        }

        protected boolean isUseHierarch() {
            return this.useHierarch;
        }

        protected boolean isUseUnicodeChars() {
            return this.useUnicodeChars;
        }
        
        protected boolean isAllowHeaderRepairs() {
            return this.allowHeaderRepairs;
        }

    }

    private static final FitsSettings GLOBAL_SETTINGS = new FitsSettings();

    private static final ThreadLocal<FitsSettings> LOCAL_SETTINGS = new ThreadLocal<>();

    private static ExecutorService threadPool;

    public static final int FITS_BLOCK_SIZE = 2880;

    /**
     * @return Given a Header construct an appropriate data.
     * @param hdr
     *            header to create the data from
     * @throws FitsException
     *             if the header did not contain enough information to detect
     *             the type of the data
     */
    public static Data dataFactory(Header hdr) throws FitsException {

        if (ImageHDU.isHeader(hdr)) {
            Data d = ImageHDU.manufactureData(hdr);
            hdr.afterExtend(); // Fix for positioning error noted by V. Forchi
            return d;
        } else if (RandomGroupsHDU.isHeader(hdr)) {
            return RandomGroupsHDU.manufactureData(hdr);
        } else if (current().isUseAsciiTables() && AsciiTableHDU.isHeader(hdr)) {
            return AsciiTableHDU.manufactureData(hdr);
        } else if (CompressedImageHDU.isHeader(hdr)) {
            return CompressedImageHDU.manufactureData(hdr);
        } else if (CompressedTableHDU.isHeader(hdr)) {
            return CompressedTableHDU.manufactureData(hdr);
        } else if (BinaryTableHDU.isHeader(hdr)) {
            return BinaryTableHDU.manufactureData(hdr);
        } else if (UndefinedHDU.isHeader(hdr)) {
            return UndefinedHDU.manufactureData(hdr);
        } else {
            throw new FitsException("Unrecognizable header in dataFactory");
        }

    }
    

    /**
     * @return Do we allow automatic header repairs, like missing end quotes?
     * 
     * @since 1.16
     */
    public static boolean isUseExponentD() {
        return current().isUseExponentD();
    }

    /**
     * Whether <code>char[]</code> arrays are written as 16-bit integers (<code>short[]</code>)
     * int binary tables as opposed as FITS character arrays (<code>byte[]</code> with column type
     * 'A'). See more explanation in {@link #setUseUnicodeChars(boolean)}.
     * 
     * @return  <code>true</code> if <code>char[]</code> get written as 16-bit integers in binary table
     *          columns (column type 'I'), or as FITS 1-byte ASCII character arrays (as is always
     *          the case for <code>String</code>) with column type 'A'.
     * 
     * @since 1.16
     */
    public static boolean isUseUnicodeChars() {
        return current().isUseUnicodeChars();
    }
    
    /**
     * @return Is terminal junk (i.e., non-FITS data following a valid HDU)
     *         allowed.
     */
    public static boolean getAllowTerminalJunk() {
        return current().isAllowTerminalJunk();
    }

    /**
     * @return Do we allow automatic header repairs, like missing end quotes?
     */
    public static boolean isAllowHeaderRepairs() {
        return current().isAllowHeaderRepairs();
    }

    /**
     * @return the formatter to use for hierarch keys.
     */
    public static IHierarchKeyFormatter getHierarchFormater() {
        return current().getHierarchKeyFormatter();
    }

    /**
     * @return <code>true</code> if we are processing HIERARCH style keywords
     */
    public static boolean getUseHierarch() {
        return current().isUseHierarch();
    }
    
    /**
     * whether ASCII tables should be used where feasible.
     * 
     * @return <code>true</code> if we ASCII tables are allowed.
     * 
     * @see #setUseAsciiTables(boolean)
     */
    public static boolean getUseAsciiTables() {
        return current().isUseAsciiTables();
    }
    

    /**
     * @return Get the current status for string checking.
     */
    public static boolean getCheckAsciiStrings() {
        return current().isCheckAsciiStrings();
    }
    
    /**
     * @return <code>true</code> If long string support is enabled.
     */
    public static boolean isLongStringsEnabled() {
        return current().isLongStringsEnabled();
    }

    /**
     * 
     * @return  whether to use only "=", instead of the standard "= " between the keyword
     *          and the value.
     *          
     * @deprecated The FITS standard is very explicit that assignment must be "= ". If we allow
     *              skipping the space, it will result in a non-standard FITS, that is likely
     *              to break compatibility with other tools.
     */
    @Deprecated
    public static boolean isSkipBlankAfterAssign() {
        return current().isSkipBlankAfterAssign();
    }
    
    
    /**
     * @return Given Header and data objects return the appropriate type of HDU.
     * @param hdr
     *            the header of the date
     * @param d
     *            the data
     * @param <DataClass>
     *            the class of the data
     * @throws FitsException
     *             if the operation failed
     */
    @SuppressWarnings("unchecked")
    public static <DataClass extends Data> BasicHDU<DataClass> hduFactory(Header hdr, DataClass d) throws FitsException {
        if (d instanceof ImageData) {
            return (BasicHDU<DataClass>) new ImageHDU(hdr, (ImageData) d);
        } else if (d instanceof CompressedImageData) {
            return (BasicHDU<DataClass>) new CompressedImageHDU(hdr, (CompressedImageData) d);
        } else if (d instanceof RandomGroupsData) {
            return (BasicHDU<DataClass>) new RandomGroupsHDU(hdr, (RandomGroupsData) d);
        } else if (current().isUseAsciiTables() && d instanceof AsciiTable) {
            return (BasicHDU<DataClass>) new AsciiTableHDU(hdr, (AsciiTable) d);
        } else if (d instanceof CompressedTableData) {
            return (BasicHDU<DataClass>) new CompressedTableHDU(hdr, (CompressedTableData) d);
        } else if (d instanceof BinaryTable) {
            return (BasicHDU<DataClass>) new BinaryTableHDU(hdr, (BinaryTable) d);
        } else if (d instanceof UndefinedData) {
            return (BasicHDU<DataClass>) new UndefinedHDU(hdr, (UndefinedData) d);
        }
        return null;
    }

    /**
     * @return Given an object, create the appropriate FITS header to describe
     *         it.
     * @param o
     *            The object to be described.
     * @throws FitsException
     *             if the parameter could not be converted to a hdu.
     */
    public static BasicHDU<?> hduFactory(Object o) throws FitsException {
        Data d;
        Header h;

        if (o instanceof Header) {
            h = (Header) o;
            d = dataFactory(h);
        } else if (ImageHDU.isData(o)) {
            d = ImageHDU.encapsulate(o);
            h = ImageHDU.manufactureHeader(d);
        } else if (RandomGroupsHDU.isData(o)) {
            d = RandomGroupsHDU.encapsulate(o);
            h = RandomGroupsHDU.manufactureHeader(d);
        } else if (current().isUseAsciiTables() && AsciiTableHDU.isData(o)) {
            d = AsciiTableHDU.encapsulate(o);
            h = AsciiTableHDU.manufactureHeader(d);
        } else if (BinaryTableHDU.isData(o)) {
            d = BinaryTableHDU.encapsulate(o);
            h = BinaryTableHDU.manufactureHeader(d);
        } else if (UndefinedHDU.isData(o)) {
            d = UndefinedHDU.encapsulate(o);
            h = UndefinedHDU.manufactureHeader(d);
        } else {
            throw new FitsException("Invalid data presented to HDUFactory");
        }

        return hduFactory(h, d);
    }

    // CHECKSTYLE:OFF
    /**
     * @return Given Header and data objects return the appropriate type of HDU.
     * @param hdr
     *            the header of the date
     * @param d
     *            the data
     * @param <DataClass>
     *            the class of the data
     * @throws FitsException
     *             if the operation failed
     * @deprecated use {@link #hduFactory(Header, Data)} instead
     */
    @Deprecated
    public static <DataClass extends Data> BasicHDU<DataClass> HDUFactory(Header hdr, DataClass d) throws FitsException {
        return hduFactory(hdr, d);
    }

    // CHECKSTYLE:ON

    // CHECKSTYLE:OFF
    /**
     * @return Given an object, create the appropriate FITS header to describe
     *         it.
     * @param o
     *            The object to be described.
     * @throws FitsException
     *             if the parameter could not be converted to a hdu.
     * @deprecated use {@link #hduFactory(Object)} instead
     */
    @Deprecated
    public static BasicHDU<?> HDUFactory(Object o) throws FitsException {
        return hduFactory(o);
    }

    // CHECKSTYLE:ON
    
    /**
     * Restores all settings to their default values.
     * 
     * @since 1.16
     */
    public static void setDefaults() {
        FitsSettings s = current();
        s.useExponentD = DEFAULT_USE_EXPONENT_D;
        s.allowHeaderRepairs = DEFAULT_ALLOW_HEADER_REPAIRS;
        s.allowTerminalJunk = DEFAULT_ALLOW_TERMINAL_JUNK;
        s.checkAsciiStrings = DEFAULT_CHECK_ASCII_STRINGS;
        s.longStringsEnabled = DEFAULT_LONG_STRINGS_ENABLED;
        s.skipBlankAfterAssign = DEFAULT_SKIP_BLANK_AFTER_ASSIGN;
        s.useAsciiTables = DEFAULT_USE_ASCII_TABLES;
        s.useHierarch = DEFAULT_USE_HIERARCH;      
        s.useUnicodeChars = DEFAULT_USE_UNICODE_CHARS;
        s.hierarchKeyFormatter = DEFAULT_HIERARCH_FORMATTER;
        s.hierarchKeyFormatter.setCaseSensitive(DEFAULT_CASE_SENSITIVE_HIERARCH);
    }
    
    /**
     * Do we allow 'D' instead of E to mark the exponent for a floating point
     * value with precision beyond that of a 32-bit float?
     *
     * @param allowExponentD    if <code>true</code> D will be used instead of E to indicate
     *                          the exponent of a decimal with more precision than a 32-bit float.
     *                          
     * @since 1.16
     */
    public static void setUseExponentD(boolean allowExponentD) {
        current().useExponentD = allowExponentD;
    }

    /**
     * Do we allow junk after a valid FITS file?
     *
     * @param allowTerminalJunk
     *            value to set
     */
    public static void setAllowTerminalJunk(boolean allowTerminalJunk) {
        current().allowTerminalJunk = allowTerminalJunk;
    }

    /**
     * Do we allow automatic header repairs, like missing end quotes?
     *
     * @param allowHeaderRepairs
     *            value to set
     */
    public static void setAllowHeaderRepairs(boolean allowHeaderRepairs) {
        current().allowHeaderRepairs = allowHeaderRepairs;
    }

    /**
     * Enable/Disable checking of strings values used in tables to ensure that
     * they are within the range specified by the FITS standard. The standard
     * only allows the values 0x20 - 0x7E with null bytes allowed in one limited
     * context. Disabled by default.
     *
     * @param checkAsciiStrings
     *            value to set
     */
    public static void setCheckAsciiStrings(boolean checkAsciiStrings) {
        current().checkAsciiStrings = checkAsciiStrings;
    }

    /**
     * There is not a real standard how to write hierarch keys, default we use
     * the one where every key is separated by a blank. If you want or need
     * another format assing the formater here.
     *
     * @param formatter
     *            the hierarch key formatter.
     */
    public static void setHierarchFormater(IHierarchKeyFormatter formatter) {
        current().hierarchKeyFormatter = formatter;
    }

    /**
     * Enable/Disable longstring support.
     *
     * @param longStringsEnabled
     *            value to set
     */
    public static void setLongStringsEnabled(boolean longStringsEnabled) {
        current().longStringsEnabled = longStringsEnabled;
    }

    /**
     * If set to true the blank after the assign in the header cards in not
     * written. The blank is stronly recommendet but in some cases it is
     * important that it can be ommitted.
     *
     * @param skipBlankAfterAssign
     *            value to set
     * 
     * @deprecated The FITS standard is very explicit that assignment must be "= ". If we allow
     *              skipping the space, it will result in a non-standard FITS, that is likely
     *              to break compatibility with other tools.
     * 
     */
    @Deprecated
    public static void setSkipBlankAfterAssign(boolean skipBlankAfterAssign) {
        current().skipBlankAfterAssign = skipBlankAfterAssign;
    }

    /**
     * Indicate whether ASCII tables should be used where feasible.
     *
     * @param useAsciiTables
     *            value to set
     */
    public static void setUseAsciiTables(boolean useAsciiTables) {
        current().useAsciiTables = useAsciiTables;
    }

    /**
     * Enable/Disable hierarchical keyword processing.
     *
     * @param useHierarch
     *            value to set
     */
    public static void setUseHierarch(boolean useHierarch) {
        current().useHierarch = useHierarch;
    }

    /**
     * <p>
     * Enable/Disable writing <code>char[]</code> arrays as <code>short[]</code> in FITS binary tables
     * (with column type 'I'), instead of as standard FITS 1-byte ASCII characters (with column type 'A').
     * The old default of this library has been to use unicode, and that behavior is retained
     * by setting the argument to <code>true</code>. On the flipside, setting it to <code>false</code>
     * provides more ocnvergence between the handling of <code>char[]</code> columns and the nearly
     * identical <code>String</code> columns, which have already been restricted to ASCII before.
     * </p>
     * 
     *
     * @param value     <code>true</code> to write <code>char[]</code> arrays as if <code>short[]</code>
     *                  with column type 'I' to binary tables (old behaviour), or else <code>false</code>
     *                  to write them as <code>byte[]</code> with column type 'A', the same as
     *                  for <code>String</code> (preferred behaviour)
     * 
     * @since 1.16
     *                  
     * @see #isUseUnicodeChars()
     * 
     */
    public static void setUseUnicodeChars(boolean value) {
        current().useUnicodeChars = value;
    }
    
    public static ExecutorService threadPool() {
        if (threadPool == null) {
            initializeThreadPool();
        }
        return threadPool;
    }

    /**
     * Use thread local settings for the current thread instead of the global
     * ones if the parameter is set to true, else use the shared global
     * settings.
     *
     * @param useThreadSettings
     *            true if the thread should not share the global settings.
     */
    public static void useThreadLocalSettings(boolean useThreadSettings) {
        if (useThreadSettings) {
            LOCAL_SETTINGS.set(GLOBAL_SETTINGS.copy());
        } else {
            LOCAL_SETTINGS.remove();
        }
    }

    private static void initializeThreadPool() {
        synchronized (GLOBAL_SETTINGS) {
            if (threadPool == null) {
                // 1.5 thread per core
                threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2, //
                        new ThreadFactory() {

                            private int counter = 1;

                            @Override
                            public Thread newThread(Runnable r) {
                                Thread thread = new Thread(r, "nom-tam-fits worker " + this.counter++);
                                thread.setDaemon(true);
                                return thread;
                            }
                        });
            }
        }
    }

    protected static FitsSettings current() {
        FitsSettings settings = LOCAL_SETTINGS.get();
        if (settings == null) {
            return GLOBAL_SETTINGS;
        }
        return settings;
    }


    private FitsFactory() {
    }
}