View Javadoc
1   package nom.tam.fits.header;
2   
3   import java.io.Serializable;
4   import java.util.HashMap;
5   import java.util.HashSet;
6   import java.util.Map;
7   
8   import nom.tam.fits.HeaderCard;
9   
10  /*
11   * #%L
12   * nom.tam FITS library
13   * %%
14   * Copyright (C) 1996 - 2024 nom-tam-fits
15   * %%
16   * This is free and unencumbered software released into the public domain.
17   *
18   * Anyone is free to copy, modify, publish, use, compile, sell, or
19   * distribute this software, either in source code form or as a compiled
20   * binary, for any purpose, commercial or non-commercial, and by any
21   * means.
22   *
23   * In jurisdictions that recognize copyright laws, the author or authors
24   * of this software dedicate any and all copyright interest in the
25   * software to the public domain. We make this dedication for the benefit
26   * of the public at large and to the detriment of our heirs and
27   * successors. We intend this dedication to be an overt act of
28   * relinquishment in perpetuity of all present and future rights to this
29   * software under copyright law.
30   *
31   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
32   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
33   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
34   * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
35   * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
36   * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
37   * OTHER DEALINGS IN THE SOFTWARE.
38   * #L%
39   */
40  
41  /**
42   * <p>
43   * A concrete implementation of standardized FITS header keywords. Users may instantiate this class or extend it to
44   * define commonly used keywords for their applications, and benefit for the extra checks they afford, and to avoid
45   * typos when using these.
46   * </p>
47   * <p>
48   * FITS keywords must be composed of uppper-case 'A'-'Z', digits, underscore ('_') and hyphen ('-') characters.
49   * Additionally, lower case 'n' may be used as a place-holder for a numerical index, and the keyword name may end with a
50   * lower-case 'a' to indicate that it may be used for/with alternate WCS coordinate systems. (We also allow '/' because
51   * some STScI keywords use it even though it violates the FITS standard.)
52   * </p>
53   * 
54   * @since 1.19
55   */
56  public class FitsKey implements IFitsHeader, Serializable {
57  
58      /** Generated serial version ID */
59      private static final long serialVersionUID = -8312303744399173040L;
60  
61      /** Standard header comment */
62      private final String comment;
63  
64      /** The type of HDU in which this keyword may appear */
65      private final HDU hdu;
66  
67      /** The FITS keyword pattern that produces the actual keyword as it appears in the header. */
68      private final String key;
69  
70      /** Documentation source for the keyword */
71      private final SOURCE status;
72  
73      /** The type of value expected for this keyword */
74      private final VALUE valueType;
75  
76      /** A list of known comment-style keyword, which do not take an assigned value */
77      private static HashSet<String> commentStyleKeys = new HashSet<>();
78  
79      /**
80       * Creates a new standardized FITS keyword with the specific usage constraints. The keyword must be composed of
81       * uppper-case 'A'-'Z', digits, underscore ('_') and hyphen ('-') characters. Additionally, lower case 'n' may be
82       * used as a place-holder for a numerical index, and the keyword name may end with a lower-case 'a' to indicate that
83       * it may be used for/with alternate WCS coordinate systems. (We also allow '/' because some STScI keywords use it
84       * even though it violates the FITS standard.)
85       * 
86       * @param  headerName               The keyword as it will appear in the FITS headers, usually a string with up to 8
87       *                                      characters, containing uppper case letters (A-Z), digits (0-9), and/or
88       *                                      underscore (<code>_</code>) or hyphen (<code>-</code>) characters for
89       *                                      standard FITS keywords.
90       * @param  status                   The convention that defines this keyword
91       * @param  hdu                      the type of HDU this keyword may appear in
92       * @param  valueType                the type of value that may be associated with this keyword
93       * @param  comment                  the standard comment to include with this keyword
94       * 
95       * @throws IllegalArgumentException if the keyword name is invalid.
96       */
97      public FitsKey(String headerName, SOURCE status, HDU hdu, VALUE valueType, String comment)
98              throws IllegalArgumentException {
99          if (headerName.length() > HeaderCard.MAX_KEYWORD_LENGTH) {
100             throw new IllegalArgumentException(
101                     "Keyword " + headerName + " exceeeds the FITS " + HeaderCard.MAX_KEYWORD_LENGTH + " character limit");
102         }
103 
104         for (int i = 0; i < headerName.length(); i++) {
105             char c = headerName.charAt(i);
106 
107             if (c >= 'A' && c <= 'Z') {
108                 continue;
109             }
110             if (c >= '0' && c <= '9') {
111                 continue;
112             }
113             if (c == '-' || c == '_' || c == '/') {
114                 continue;
115             }
116             if (c == 'n') {
117                 continue;
118             }
119             if (c == 'a' && (i + 1) == headerName.length()) {
120                 continue;
121             }
122 
123             throw new IllegalArgumentException("Invalid FITS keyword: " + headerName);
124         }
125 
126         this.key = headerName;
127         this.status = status;
128         this.hdu = hdu;
129         this.valueType = valueType;
130         this.comment = comment;
131 
132         if (valueType == VALUE.NONE) {
133             commentStyleKeys.add(headerName);
134         }
135     }
136 
137     /**
138      * Creates a new standardized user-defined FITS keyword. The keyword will have source set to
139      * {@link IFitsHeader.SOURCE#UNKNOWN}. The keyword must be composed of uppper-case 'A'-'Z', digits, underscore ('_')
140      * and hyphen ('-') characters. Additionally, lower case 'n' may be used as a place-holder for a numerical index,
141      * and the keyword name may end with a lower-case 'a' to indicate that it may be used for/with alternate WCS
142      * coordinate systems. (We also allow '/' because some STScI keywords use it even though it violates the FITS
143      * standard.)
144      * 
145      * @param  headerName               The keyword as it will appear in the FITS headers, usually a string with up to 8
146      *                                      characters, containing uppper case letters (A-Z), digits (0-9), and/or
147      *                                      underscore (<code>_</code>) or hyphen (<code>-</code>) characters for
148      *                                      standard FITS keywords.
149      * @param  hdu                      the type of HDU this keyword may appear in
150      * @param  valueType                the type of value that may be associated with this keyword
151      * @param  comment                  the standard comment to include with this keyword
152      * 
153      * @throws IllegalArgumentException if the keyword name is invalid.
154      * 
155      * @since                           1.19
156      */
157     public FitsKey(String headerName, HDU hdu, VALUE valueType, String comment) throws IllegalArgumentException {
158         this(headerName, SOURCE.UNKNOWN, hdu, valueType, comment);
159     }
160 
161     /**
162      * Creates a new standardized user-defined FITS keyword. The keyword will have source set to
163      * {@link IFitsHeader.SOURCE#UNKNOWN} and HDY type to {@link IFitsHeader.HDU#ANY}. The keyword must be composed of
164      * uppper-case 'A'-'Z', digits, underscore ('_') and hyphen ('-') characters. Additionally, lower case 'n' may be
165      * used as a place-holder for a numerical index, and the keyword name may end with a lower-case 'a' to indicate that
166      * it may be used for/with alternate WCS coordinate systems. (We also allow '/' because some STScI keywords use it
167      * even though it violates the FITS standard.)
168      * 
169      * @param  headerName               The keyword as it will appear in the FITS headers, usually a string with up to 8
170      *                                      characters, containing uppper case letters (A-Z), digits (0-9), and/or
171      *                                      underscore (<code>_</code>) or hyphen (<code>-</code>) characters for
172      *                                      standard FITS keywords.
173      * @param  valueType                the type of value that may be associated with this keyword
174      * @param  comment                  the standard comment to include with this keyword
175      * 
176      * @throws IllegalArgumentException if the keyword name is invalid.
177      * 
178      * @since                           1.19
179      */
180     public FitsKey(String headerName, VALUE valueType, String comment) throws IllegalArgumentException {
181         this(headerName, HDU.ANY, valueType, comment);
182     }
183 
184     @Override
185     public final FitsKey impl() {
186         return this;
187     }
188 
189     @Override
190     public String comment() {
191         return comment;
192     }
193 
194     @Override
195     public HDU hdu() {
196         return hdu;
197     }
198 
199     @Override
200     public String key() {
201         if (key.endsWith("a")) {
202             return key.substring(0, key.length() - 1);
203         }
204         return key;
205     }
206 
207     @Override
208     public SOURCE status() {
209         return status;
210     }
211 
212     @Override
213     public VALUE valueType() {
214         return valueType;
215     }
216 
217     /**
218      * (<i>for internal use</i>) Checks if a keywords is known to be a comment-style keyword. That is, it checks if the
219      * <code>key</code> argument matches any {@link IFitsHeader} constructed via this implementation with a
220      * <code>valueType</code> argument that was <code>null</code>, or if the key is empty.
221      *
222      * @param  key the keyword to check
223      *
224      * @return     <code>true</code> if the key is empty or if it matches any known {@link IFitsHeader} keywords
225      *                 implemented through this class that have valueType of <code>null</code>. Otherwise
226      *                 <code>false</code>.
227      *
228      * @since      1.17
229      */
230     public static boolean isCommentStyleKey(String key) {
231         return commentStyleKeys.contains(key) || key.trim().isEmpty();
232     }
233 
234     /**
235      * A registry of all standard keys, for reusing the standards. We keep the registry outside of the enums that define
236      * keys to avoid circular dependency issues.
237      */
238     private static final Map<String, IFitsHeader> STANDARD_KEYS = new HashMap<>();
239 
240     static void registerStandard(IFitsHeader key) throws IllegalArgumentException {
241         STANDARD_KEYS.put(key.key(), key);
242     }
243 
244     /**
245      * Returns the standard FITS keyword that matches the specified actual key.
246      * 
247      * @param  key The key as it may appear in a FITS header, e.g. "CTYPE1A"
248      * 
249      * @return     The standard FITS keyword/pattern that matches, e.g. {@link WCS#CTYPEna}.
250      * 
251      * @see        IFitsHeader#extractIndices(String)
252      * 
253      * @since      1.19
254      */
255     static IFitsHeader matchStandard(String key) {
256         int i = 0, l = key.length();
257         StringBuilder pattern = new StringBuilder();
258 
259         // If ends with digit + letter, then it must alt coordinate if standard...
260         if (l > 1 && Character.isAlphabetic(key.charAt(l - 1)) && Character.isDigit(key.charAt(l - 2))) {
261             key = key.substring(0, --l);
262         }
263 
264         // If the first digit is a number it may be a coordinate index
265         if (i < l && Character.isDigit(key.charAt(i))) {
266             pattern.append('n');
267             i++;
268 
269             // If the second digit is a number it may be a coordinate index
270             if (i < l && Character.isDigit(key.charAt(i))) {
271                 pattern.append('n');
272                 i++;
273             }
274         }
275 
276         // Replace sequence of digits with 'n'
277         while (i < l) {
278             char c = key.charAt(i);
279 
280             if (Character.isDigit(c)) {
281                 pattern.append('n');
282 
283                 // Skip successive digits.
284                 while (i < l && Character.isDigit(key.charAt(i))) {
285                     i++;
286                 }
287             } else {
288                 pattern.append(c);
289                 i++;
290             }
291         }
292 
293         return STANDARD_KEYS.get(pattern.toString());
294     }
295 
296     /** We define this here with package level visibility for IFitsHeader */
297     static final int BASE_10 = 10;
298 }