Begin versioning.
[fits.git] / src / nom / tam / fits / HeaderCard.java
1 package nom.tam.fits;
2
3 /*
4  * Copyright: Thomas McGlynn 1997-1999.
5  * This code may be used for any purpose, non-commercial
6  * or commercial so long as this copyright notice is retained
7  * in the source code or included in or referred to in any
8  * derived software.
9  * Many thanks to David Glowacki (U. Wisconsin) for substantial
10  * improvements, enhancements and bug fixes -- including
11  * this class.
12  */
13 /** This class describes methods to access and manipulate the individual
14  * cards for a FITS Header.
15  */
16 public class HeaderCard {
17
18     /** The keyword part of the card (set to null if there's no keyword) */
19     private String key;
20     /** The value part of the card (set to null if there's no value) */
21     private String value;
22     /** The comment part of the card (set to null if there's no comment) */
23     private String comment;
24     /** Does this card represent a nullable field. ? */
25     private boolean nullable;
26     /** A flag indicating whether or not this is a string value */
27     private boolean isString;
28     /** Maximum length of a FITS keyword field */
29     public static final int MAX_KEYWORD_LENGTH = 8;
30     /** Maximum length of a FITS value field */
31     public static final int MAX_VALUE_LENGTH = 70;
32     /** padding for building card images */
33     private static String space80 = "                                                                                ";
34
35     /** Create a HeaderCard from its component parts
36      * @param key keyword (null for a comment)
37      * @param value value (null for a comment or keyword without an '=')
38      * @param comment comment
39      * @exception HeaderCardException for any invalid keyword
40      */
41     public HeaderCard(String key, double value, String comment)
42             throws HeaderCardException {
43         this(key, dblString(value), comment);
44         isString = false;
45     }
46
47     /** Create a HeaderCard from its component parts
48      * @param key keyword (null for a comment)
49      * @param value value (null for a comment or keyword without an '=')
50      * @param comment comment
51      * @exception HeaderCardException for any invalid keyword
52      */
53     public HeaderCard(String key, boolean value, String comment)
54             throws HeaderCardException {
55         this(key, value ? "T" : "F", comment);
56         isString = false;
57     }
58
59     /** Create a HeaderCard from its component parts
60      * @param key keyword (null for a comment)
61      * @param value value (null for a comment or keyword without an '=')
62      * @param comment comment
63      * @exception HeaderCardException for any invalid keyword
64      */
65     public HeaderCard(String key, int value, String comment)
66             throws HeaderCardException {
67         this(key, String.valueOf(value), comment);
68         isString = false;
69     }
70
71     /** Create a HeaderCard from its component parts
72      * @param key keyword (null for a comment)
73      * @param value value (null for a comment or keyword without an '=')
74      * @param comment comment
75      * @exception HeaderCardException for any invalid keyword
76      */
77     public HeaderCard(String key, long value, String comment)
78             throws HeaderCardException {
79         this(key, String.valueOf(value), comment);
80         isString = false;
81     }
82
83     /** Create a HeaderCard from its component parts
84      * @param key keyword (null for a comment)
85      * @param value value (null for a comment or keyword without an '=')
86      * @param comment comment
87      * @exception HeaderCardException for any invalid keyword or value
88      */
89     public HeaderCard(String key, String value, String comment)
90             throws HeaderCardException {
91         this(key, value, comment, false);
92     }
93
94     /** Create a comment style card.
95      *  This constructor builds a card which has no value.
96      *  This may be either a comment style card in which case the
97      *  nullable field should be false, or a value field which
98      *  has a null value, in which case the nullable field should be
99      *  true.
100      *  @param key      The key for the comment or nullable field.
101      *  @param comment  The comment
102      *  @param nullable Is this a nullable field or a comment-style card?
103      */
104     public HeaderCard(String key, String comment, boolean nullable)
105             throws HeaderCardException {
106         this(key, null, comment, nullable);
107     }
108
109     /** Create a string from a double making sure that it's
110      *  not more than 20 characters long.
111      *  Probably would be better if we had a way to override this
112      *  since we can loose precision for some doubles.
113      */
114     private static String dblString(double input) {
115         String value = String.valueOf(input);
116         if (value.length() > 20) {
117             value = new java.util.Formatter().format("%20.13G", input).out().toString();
118         }
119         return value;
120     }
121
122     /** Create a HeaderCard from its component parts
123      * @param key      Keyword (null for a COMMENT)
124      * @param value    Value
125      * @param comment  Comment
126      * @param nullable Is this a nullable value card?
127      * @exception HeaderCardException for any invalid keyword or value
128      */
129     public HeaderCard(String key, String value, String comment, boolean nullable)
130             throws HeaderCardException {
131         if (comment != null && comment.startsWith("ntf::")) {
132             String ckey = comment.substring(5); // Get rid of ntf:: prefix
133             comment = HeaderCommentsMap.getComment(ckey);
134         }
135         if (key == null && value != null) {
136             throw new HeaderCardException("Null keyword with non-null value");
137         }
138
139         if (key != null && key.length() > MAX_KEYWORD_LENGTH) {
140             if (!FitsFactory.getUseHierarch()
141                     || !key.substring(0, 9).equals("HIERARCH.")) {
142                 throw new HeaderCardException("Keyword too long");
143             }
144         }
145
146         if (value != null) {
147             value = value.replaceAll(" *$", "");
148
149             if (value.length() > MAX_VALUE_LENGTH) {
150                 throw new HeaderCardException("Value too long");
151             }
152
153             if (value.startsWith("'")) {
154                 if (value.charAt(value.length() - 1) != '\'') {
155                     throw new HeaderCardException("Missing end quote in string value");
156                 }
157
158                 value = value.substring(1, value.length() - 1).trim();
159
160             }
161         }
162
163         this.key = key;
164         this.value = value;
165         this.comment = comment;
166         this.nullable = nullable;
167         isString = true;
168     }
169
170     /** Create a HeaderCard from a FITS card image
171      * @param card the 80 character card image
172      */
173     public HeaderCard(String card) {
174         key = null;
175         value = null;
176         comment = null;
177         isString = false;
178
179         if (card.length() > 80) {
180             card = card.substring(0, 80);
181         }
182
183         if (FitsFactory.getUseHierarch()
184                 && card.length() > 9
185                 && card.substring(0, 9).equals("HIERARCH ")) {
186             hierarchCard(card);
187             return;
188         }
189
190         // We are going to assume that the value has no blanks in
191         // it unless it is enclosed in quotes.  Also, we assume that
192         // a / terminates the string (except inside quotes)
193
194         // treat short lines as special keywords
195         if (card.length() < 9) {
196             key = card;
197             return;
198         }
199
200         // extract the key
201         key = card.substring(0, 8).trim();
202
203         // if it is an empty key, assume the remainder of the card is a comment
204         if (key.length() == 0) {
205             key = "";
206             comment = card.substring(8);
207             return;
208         }
209
210         // Non-key/value pair lines are treated as keyed comments
211         if (key.equals("COMMENT") || key.equals("HISTORY")
212                 || !card.substring(8, 10).equals("= ")) {
213             comment = card.substring(8).trim();
214             return;
215         }
216
217         // extract the value/comment part of the string
218         String valueAndComment = card.substring(10).trim();
219
220         // If there is no value/comment part, we are done.
221         if (valueAndComment.length() == 0) {
222             value = "";
223             return;
224         }
225
226         int vend = -1;
227         boolean quote = false;
228
229         // If we have a ' then find the matching  '.
230         if (valueAndComment.charAt(0) == '\'') {
231
232             int offset = 1;
233             while (offset < valueAndComment.length()) {
234
235                 // look for next single-quote character
236                 vend = valueAndComment.indexOf("'", offset);
237
238                 // if the quote character is the last character on the line...
239                 if (vend == valueAndComment.length() - 1) {
240                     break;
241                 }
242
243                 // if we did not find a matching single-quote...
244                 if (vend == -1) {
245                     // pretend this is a comment card
246                     key = null;
247                     comment = card;
248                     return;
249                 }
250
251                 // if this is not an escaped single-quote, we are done
252                 if (valueAndComment.charAt(vend + 1) != '\'') {
253                     break;
254                 }
255
256                 // skip past escaped single-quote
257                 offset = vend + 2;
258             }
259
260             // break apart character string
261             value = valueAndComment.substring(1, vend).trim();
262             value = value.replace("''", "'");
263
264
265             if (vend + 1 >= valueAndComment.length()) {
266                 comment = null;
267             } else {
268
269                 comment = valueAndComment.substring(vend + 1).trim();
270                 if (comment.charAt(0) == '/') {
271                     if (comment.length() > 1) {
272                         comment = comment.substring(1);
273                     } else {
274                         comment = "";
275                     }
276                 }
277
278                 if (comment.length() == 0) {
279                     comment = null;
280                 }
281
282             }
283             isString = true;
284
285
286         } else {
287
288             // look for a / to terminate the field.
289             int slashLoc = valueAndComment.indexOf('/');
290             if (slashLoc != -1) {
291                 comment = valueAndComment.substring(slashLoc + 1).trim();
292                 value = valueAndComment.substring(0, slashLoc).trim();
293             } else {
294                 value = valueAndComment;
295             }
296         }
297     }
298
299     /** Process HIERARCH style cards...
300      *  HIERARCH LEV1 LEV2 ...  = value / comment
301      *  The keyword for the card will be "HIERARCH.LEV1.LEV2..."
302      *  A '/' is assumed to start a comment.
303      */
304     private void hierarchCard(String card) {
305
306         String name = "";
307         String token = null;
308         String separator = "";
309         int[] tokLimits;
310         int posit = 0;
311         int commStart = -1;
312
313         // First get the hierarchy levels
314         while ((tokLimits = getToken(card, posit)) != null) {
315             token = card.substring(tokLimits[0], tokLimits[1]);
316             if (!token.equals("=")) {
317                 name += separator + token;
318                 separator = ".";
319             } else {
320                 tokLimits = getToken(card, tokLimits[1]);
321                 if (tokLimits != null) {
322                     token = card.substring(tokLimits[0], tokLimits[1]);
323                 } else {
324                     key = name;
325                     value = null;
326                     comment = null;
327                     return;
328                 }
329                 break;
330             }
331             posit = tokLimits[1];
332         }
333         key = name;
334
335
336         // At the end?
337         if (tokLimits == null) {
338             value = null;
339             comment = null;
340             isString = false;
341             return;
342         }
343
344         // Really should consolidate the two instances
345         // of this test in this class!
346         if (token.charAt(0) == '\'') {
347             // Find the next undoubled quote...
348             isString = true;
349             if (token.length() > 1 && token.charAt(1) == '\''
350                     && (token.length() == 2 || token.charAt(2) != '\'')) {
351                 value = "";
352                 commStart = tokLimits[0] + 2;
353             } else if (card.length() < tokLimits[0] + 2) {
354                 value = null;
355                 comment = null;
356                 isString = false;
357                 return;
358             } else {
359                 int i;
360                 for (i = tokLimits[0] + 1; i < card.length(); i += 1) {
361                     if (card.charAt(i) == '\'') {
362                         if (i == card.length() - 1) {
363                             value = card.substring(tokLimits[0] + 1, i);
364                             commStart = i + 1;
365                             break;
366                         } else if (card.charAt(i + 1) == '\'') {
367                             // Doubled quotes.
368                             i += 1;
369                             continue;
370                         } else {
371                             value = card.substring(tokLimits[0] + 1, i);
372                             commStart = i + 1;
373                             break;
374                         }
375                     }
376                 }
377             }
378             if (commStart < 0) {
379                 value = null;
380                 comment = null;
381                 isString = false;
382                 return;
383             }
384             for (int i = commStart; i < card.length(); i += 1) {
385                 if (card.charAt(i) == '/') {
386                     comment = card.substring(i + 1).trim();
387                     break;
388                 } else if (card.charAt(i) != ' ') {
389                     comment = null;
390                     break;
391                 }
392             }
393         } else {
394             isString = false;
395             int sl = token.indexOf('/');
396             if (sl == 0) {
397                 value = null;
398                 comment = card.substring(tokLimits[0] + 1);
399             } else if (sl > 0) {
400                 value = token.substring(0, sl);
401                 comment = card.substring(tokLimits[0] + sl + 1);
402             } else {
403                 value = token;
404
405                 for (int i = tokLimits[1]; i < card.length(); i += 1) {
406                     if (card.charAt(i) == '/') {
407                         comment = card.substring(i + 1).trim();
408                         break;
409                     } else if (card.charAt(i) != ' ') {
410                         comment = null;
411                         break;
412                     }
413                 }
414             }
415         }
416     }
417
418     /** Get the next token.  Can't use StringTokenizer
419      *  since we sometimes need to know the position within
420      *  the string.
421      */
422     private int[] getToken(String card, int posit) {
423
424         int i;
425         for (i = posit; i < card.length(); i += 1) {
426             if (card.charAt(i) != ' ') {
427                 break;
428             }
429         }
430
431         if (i >= card.length()) {
432             return null;
433         }
434
435         if (card.charAt(i) == '=') {
436             return new int[]{i, i + 1};
437         }
438
439         int j;
440         for (j = i + 1; j < card.length(); j += 1) {
441             if (card.charAt(j) == ' ' || card.charAt(j) == '=') {
442                 break;
443             }
444         }
445         return new int[]{i, j};
446     }
447
448     /** Does this card contain a string value?
449      */
450     public boolean isStringValue() {
451         return isString;
452     }
453
454     /** Is this a key/value card?
455      */
456     public boolean isKeyValuePair() {
457         return (key != null && value != null);
458     }
459
460     /** Set the key.
461      */
462     void setKey(String newKey) {
463         key = newKey;
464     }
465
466     /** Return the keyword from this card
467      */
468     public String getKey() {
469         return key;
470     }
471
472     /** Return the value from this card
473      */
474     public String getValue() {
475         return value;
476     }
477
478     /** Set the value for this card.
479      */
480     public void setValue(String update) {
481         value = update;
482     }
483
484     /** Return the comment from this card
485      */
486     public String getComment() {
487         return comment;
488     }
489
490     /** Return the 80 character card image
491      */
492     public String toString() {
493         StringBuffer buf = new StringBuffer(80);
494
495         // start with the keyword, if there is one
496         if (key != null) {
497             if (key.length() > 9 && key.substring(0, 9).equals("HIERARCH.")) {
498                 return hierarchToString();
499             }
500             buf.append(key);
501             if (key.length() < 8) {
502                 buf.append(space80.substring(0, 8 - buf.length()));
503             }
504         }
505
506         if (value != null || nullable) {
507             buf.append("= ");
508
509             if (value != null) {
510
511                 if (isString) {
512                     // left justify the string inside the quotes
513                     buf.append('\'');
514                     buf.append(value.replace("'", "''"));
515                     if (buf.length() < 19) {
516
517                         buf.append(space80.substring(0, 19 - buf.length()));
518                     }
519                     buf.append('\'');
520                     // Now add space to the comment area starting at column 40
521                     if (buf.length() < 30) {
522                         buf.append(space80.substring(0, 30 - buf.length()));
523                     }
524
525                 } else {
526
527                     int offset = buf.length();
528                     if (value.length() < 20) {
529                         buf.append(space80.substring(0, 20 - value.length()));
530                     }
531
532                     buf.append(value);
533
534                 }
535             } else {
536                 // Pad out a null value.
537                 buf.append(space80.substring(0, 20));
538             }
539
540             // if there is a comment, add a comment delimiter
541             if (comment != null) {
542                 buf.append(" / ");
543             }
544
545         } else if (comment != null && comment.startsWith("= ")) {
546             buf.append("  ");
547         }
548
549         // finally, add any comment
550         if (comment != null) {
551             buf.append(comment);
552         }
553
554         // make sure the final string is exactly 80 characters long
555         if (buf.length() > 80) {
556             buf.setLength(80);
557
558         } else {
559
560             if (buf.length() < 80) {
561                 buf.append(space80.substring(0, 80 - buf.length()));
562             }
563         }
564
565         return buf.toString();
566     }
567
568     private String hierarchToString() {
569
570
571         StringBuffer b = new StringBuffer(80);
572         int p = 0;
573         String space = "";
574         while (p < key.length()) {
575             int q = key.indexOf('.', p);
576             if (q < 0) {
577                 b.append(space + key.substring(p));
578                 break;
579             } else {
580                 b.append(space + key.substring(p, q));
581             }
582             space = " ";
583             p = q + 1;
584         }
585
586         if (value != null || nullable) {
587             b.append("= ");
588
589             if (value != null) {
590                 // Try to align values
591                 int avail = 80 - (b.length() + value.length());
592
593                 if (isString) {
594                     avail -= 2;
595                 }
596                 if (comment != null) {
597                     avail -= 3 + comment.length();
598                 }
599
600                 if (avail > 0 && b.length() < 29) {
601                     b.append(space80.substring(0, Math.min(avail, 29 - b.length())));
602                 }
603
604                 if (isString) {
605                     b.append('\'');
606                 } else if (avail > 0 && value.length() < 10) {
607                     b.append(space80.substring(0, Math.min(avail, 10 - value.length())));
608                 }
609                 b.append(value);
610                 if (isString) {
611                     b.append('\'');
612                 }
613             } else if (b.length() < 30) {
614
615                 // Pad out a null value
616                 b.append(space80.substring(0, 30 - b.length()));
617             }
618         }
619
620
621         if (comment != null) {
622             b.append(" / " + comment);
623         }
624         if (b.length() < 80) {
625             b.append(space80.substring(0, 80 - b.length()));
626         }
627         String card = new String(b);
628         if (card.length() > 80) {
629             card = card.substring(0, 80);
630         }
631         return card;
632     }
633 }