package org.skymaps; import java.io.BufferedReader; import java.io.File; import java.io.FilenameFilter; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.Formatter; import java.util.HashMap; import java.util.NoSuchElementException; import java.util.Vector; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** * StarFormat is a program that takes star subtraction data * compiled by StarSearch and reformats it according to a given * specification. The newly formatted files are outputted as text (.txt) * files that have the same file names as the input files. *

* It is important that the program StarSearch be run first on * a set of data (see the StarSearch documentation for more * information) so that data text files are available to * StarFormat for processing. Only files that have been generated * by StarSearch can be used as input files for * StarFormat, and the initialization file * starsearchinit.xml must remain invariant between calling these * two programs. *

*

* This program is meant to be a convenience program for formatting star data * fetched by StarSearch for analysis in Grapher or similar * programs. *

*

* StarFormat is meant to be called in the following manner (from * a console): *

*
* % java -cp /zshare/smei/java/common org.skymaps.StarFormat [Options] * *
*

* Note that there may be a BASH alias for this program, such as * starformat. *

*

* Options
* The following command-line options are allowed: *

*
*
*
-dest=directory
*
Instructs StarFormat to write text files to * directory. This must be a valid directory, either * absolute or relative, and it must have write permissions enabled. * If this option is not specified, then the default write directory * is the user's current directory (i.e. the directory from which * StarFormat is called).
*
-dir=directory
*
Instructs StarFormat to perform the search for input * files in directory. If this option is not specified, * then the default starting directory is the current user directory * (i.e. the directory from which the user calls * StarFormat). This must be a valid directory, either * absolute or relative, and it must have read permissions enabled.
*
-help
*
Displays a usage message to the user on standard output.
*
*
*

* If illegal or malformed command-line optiosn are entered by the user, then * the program indicates the error and terminates normally. *

*

* File Formats
* The input files must be generated by StarSearch (see the * StarSearch documentation for information about this format). * The output files are generated in a similar format. However, StarFormat * gives the user the ability to choose which data entries (data fields) in * the input files will be written to and in which order they will appear in * the output files. This formatting information is controlled by the XML * initialization file starsearchinit.xml and its associated * DTD file, both of which must be located in the same directory as * StarFormat. *

*

* There is one special transformation that always occurs when * StarFormat is run. The YYYY_DOY_hhmmss field * of the input files is transformed from standard SMEI format to a special * eight digit double-precision floating point number representing the day * offset of the date from 2003_148_010505 (negative values may be generated * if the date is before this "zero date"). This fractional day is stored * under the field title "time". This transformation can be modified in * one of two ways: the FormattedField entry in the initialization file * corresponding to the SMEI date can be removed (in which case the date will * not appear in the output files), or the StarFormat code * can be modified and recompiled. *

*

* The XML Initialization File
*

*

* When StarFormat is run, its initialization file * starsearchinit.xml is parsed. This file should be located * in the same directory as StarFormat and has a corresponding * DTD file named starsearchinit.dtd. The initialization file * contains various kinds of information, but StarFormat only * parses information within the * <Fields></Fields> and * <FormattedFields></FormattedFields> tag pairs. *

*

* The Fields element contains Field elements, which * correspond to the data fields in the text files read by * StarFormat. These include the title of the data field (which * is found at the top of its respective column in the inputted text files), * the size of the field (in characters), and what type of data the field * contains (string, int, float, floate, double, or double). * StarFormat reads in data fields from the inputted text files * in the order they appear in the initialization file. *

*

* The FormattedFields element contains * FormattedField elements, which correspond to the data fields * that will be written to the output text files that StarFormat * generates. Required attributes of FormattedField elements * include the name of the formatted field (which will appear at the top of * the field's respective column in the output files), the size of the * field (in characters), and the name of the Field that the * FormattedField element corresponds to (this name must match * one of the Field elements' names, case-sensitive). * StarFormat writes these formatted fields to the output * text files in the order they appear in the initialization file. *

*

* When StarFormat is run, it parses each located input file * in the given directory according to the number, type, size, and order of * Field elements in the initialization file. This information * is transferred to the output files according to the number, size, order, * and corresponding field identities of the FormattedField * elements in the initialization file. *

*

* Note that in order for the SMEI date conversion described above to work, * the following entry must appear in the FormattedFields section * in the initialization file: *

*
* * <FormattedField name="time" size="10" * correspondingTo="YYYY_DOY_hhmmss" /> * *
*

* YYYY_DOY_hhmmss must be the name of a Field * element that denotes a standard SMEI date. *

* * @author Jordan T. Vaughan * @version 1.0 * @see StarSearch */ public class StarFormat { /** * The default directory where the search for input files is performed. */ public static final String DEFAULT_DIRECTORY = "."; /** * The default directory where the output files are written to. */ public static final String DEFAULT_DESTINATION = "."; /** * The path and name of the XML initialization file (which should be in the * same directory as the StarFormat .class file). Modify this value * (that is, recompile the code with a new file) if the directory of the * initialization file should change. */ public static final String INI_FILE = "/zshare/smei/java/common/org/" + "skymaps/starsearchinit.xml"; private static final String DIR_OPTION = "-dir="; private static final String DEST_OPTION = "-dest="; private static final String HELP_OPTION = "-help"; /** * The number of whitespace characters separating fields in the input * and output files. */ public static final int FIELD_SEPARATION_SIZE = 1; /* Quantities to subtract from converted dates. */ /** * The year of the zero date. */ public static final int ZERO_TIME_YEAR = 2003; /** * The numerical day of the year of the zero date. */ public static final int ZERO_TIME_DOY = 148; /** * The hour of the day of the zero date. */ public static final int ZERO_TIME_HOUR = 1; /** * The minute of the hour of the zero date. */ public static final int ZERO_TIME_MINUTE = 5; /** * The second of the minute of the zero date. */ public static final int ZERO_TIME_SECOND = 5; /** * The decimal precision, not including the sign (if it is displayed), * of the outputted date. */ public static final int TIME_DECIMAL_PRECISION = 8; /** * The program begins here. * * @param args the command line options to parse * @since 1.0 */ public static void main(String[] args) { // Relevant local variables. File searchDirectory = null; File destinationDirectory = null; Vector fields = null; HashMap fieldIndicies = null; Vector formattedFields = null; HashMap> starEntries = null; // Parse command-line options. for(int counter = 0; counter < args.length; counter++) { if(args[counter].length() >= DIR_OPTION.length() && args[counter].substring(0, DIR_OPTION.length()). equalsIgnoreCase(DIR_OPTION)) { // Setting the search directory to the user's choice. if(searchDirectory != null) { System.out.println("ERROR: Search directory specified " + "more than once. Aborting.\n"); return; } args[counter] = args[counter].substring(DIR_OPTION.length()); if(args[counter].length() == 0) { System.out.println("ERROR: No search directory given. " + "Aborting.\n"); return; } searchDirectory = new File(args[counter]); if(!searchDirectory.exists()) { System.out.println("ERROR: Given directory does not " + "exist -- " + args[counter] + "\n"); return; } if(!searchDirectory.isDirectory()) { System.out.println("ERROR: Given directory is not a " + "directory -- " + args[counter] + "\n"); return; } } else if(args[counter].length() >= DEST_OPTION.length() && args[counter].substring(0, DEST_OPTION.length()). equalsIgnoreCase(DEST_OPTION)) { // Setting the destination directory to the user's choice. if(destinationDirectory != null) { System.out.println("ERROR: Destiantion directory " + "specified more than once. Aborting.\n"); return; } args[counter] = args[counter].substring(DEST_OPTION.length()); if(args[counter].length() == 0) { System.out.println("ERROR: No destination directory " + "given. Aborting.\n"); return; } destinationDirectory = new File(args[counter]); if(!destinationDirectory.exists()) { System.out.println("ERROR: Given directory does not " + "exist -- " + args[counter] + "\n"); return; } if(!destinationDirectory.isDirectory()) { System.out.println("ERROR: Given directory is not a " + "directory -- " + args[counter] + "\n"); return; } } else if(args[counter].equalsIgnoreCase(HELP_OPTION)) { // Display the usage message. printUsageMessage(); return; } else { // Unrecognized command-line option. Display an error message. System.out.println("ERROR: Unrecognized option -- " + args[counter] + "\nType \"starformat -help\" for " + "options.\n"); return; } } /* Create default directories if they were not already specified by the * user via the command line. */ if(searchDirectory == null) { searchDirectory = new File(DEFAULT_DIRECTORY); if(!searchDirectory.exists()) { System.out.println("FATAL ERROR: Default search directory " + DEFAULT_DIRECTORY + " does not exist.\n" + "Aborting.\n"); return; } if(!searchDirectory.isDirectory()) { System.out.println("FATAL ERROR: Default search directory " + DEFAULT_DIRECTORY + " is not a directory.\n" + "Aborting.\n"); return; } } if(destinationDirectory == null) { destinationDirectory = new File(DEFAULT_DESTINATION); if(!destinationDirectory.exists()) { System.out.println("FATAL ERROR: Default destination " + "directory " + DEFAULT_DESTINATION + " does not exist.\n" + "Aborting.\n"); return; } if(!destinationDirectory.isDirectory()) { System.out.println("FATAL ERROR: Default destination " + "directory " + DEFAULT_DESTINATION + " is not a " + "directory.\nAborting.\n"); return; } } /* Read in XML initialization file and retrieve entries. But first, * build the XML DOM tree. */ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(true); factory.setIgnoringComments(true); DocumentBuilder builder = null; try { builder = factory.newDocumentBuilder(); } catch(ParserConfigurationException e) { System.out.println("FATAL ERROR: Unable to instantiate an XML " + "parser to parse the\ninitialization file. Aborting.\n"); return; } Document document = null; try { document = builder.parse(new File(INI_FILE)); } catch(IOException e) { System.out.println("ERROR: An IO error occured while attempting " + "to parse the initialization file.\n Aborting.\n"); return; } catch(SAXException e) { System.out.println("ERROR: The initialization file is not a " + "well-formed XML file, or it\ndoes not conform to the " + "specifications provided in the associated DTD. Aborting.\n"); return; } // Read the Field entries. fields = new Vector(); fieldIndicies = new HashMap(); NodeList nodeList = ((Element)document.getDocumentElement(). getElementsByTagName("Fields").item(0)). getElementsByTagName("Field"); int offset = 0; for(int counter = 0; counter < nodeList.getLength(); counter++) { Element field = (Element)nodeList.item(counter); try { Field newField = new Field( field.getAttribute("name"), Integer.parseInt(field.getAttribute("size")), offset, Field.getTypeFlag(field.getAttribute("type"))); fields.add(newField); fieldIndicies.put(field.getAttribute("name"), newField); offset += newField.getSize() + FIELD_SEPARATION_SIZE; } catch(NumberFormatException e) { System.out.println("ERROR: Initialization file contains a " + "Field entry with an illegal size.\nAborting.\n"); return; } } // Read the FormattedField entries. formattedFields = new Vector(); nodeList = ((Element)document.getDocumentElement(). getElementsByTagName("FormattedFields").item(0)). getElementsByTagName("FormattedField"); for(int counter = 0; counter < nodeList.getLength(); counter++) { Element formattedField = (Element)nodeList.item(counter); try { formattedFields.add(new FormattedField( formattedField.getAttribute("name"), Integer.parseInt(formattedField.getAttribute("size")), fieldIndicies.get(formattedField.getAttribute( "correspondingTo")))); } catch(NumberFormatException e) { System.out.println("ERROR: Initialization file contains a " + "FormattedField entry with an illegal size.\nAborting.\n"); return; } catch(NoSuchElementException e) { System.out.println("ERROR: Initialization file contains a " + "FormattedField entry with a nonexistent\ncorresponding " + "Field entry. Aborting.\n"); return; } } /* Now that we're done with initialization, locate files that match * what we are looking for. */ String[] inputFileList = searchDirectory.list( new InputFilenameFilter()); if(inputFileList == null) { System.out.println("ERROR: An IO error occurred while attempting" + " to access the\nfiles in the search directory. Aborting."); return; } if(inputFileList.length == 0) { System.out.println("ERROR: No star data text files with names " + "in the form\nstarname_c(1+2+3)(e+s).txt were found!"); return; } starEntries = new HashMap>(); for(int counter = 0; counter < inputFileList.length; counter++) { // Now, create an entries Vector for this star. Vector entryVector = new Vector(); // Open up a reader and read all lines. BufferedReader reader = null; try { reader = new BufferedReader(new FileReader( new File(searchDirectory, inputFileList[counter]))); // Skip the header line. reader.readLine(); // Now, read in lines until EOF. String line = reader.readLine(); while(line != null) { // Parse the current line into its respective entries. StarEntry entry = new StarEntry(); for(int j = 0; j < fields.size(); j++) { Field currentField = fields.get(j); try { entry.addEntry(currentField, line.substring( currentField.getOffset(), currentField.getOffset()+currentField.getSize() ).trim()); } catch(StringIndexOutOfBoundsException e) { /* We bumped into the end of the line. Just add * whatever is at the end of the file. */ if(currentField.getOffset() < line.length()) { entry.addEntry(currentField, line.substring( currentField.getOffset(), line.length()).trim()); } else { entry.addEntry(currentField, ""); } } } entryVector.add(entry); // Read another line. line = reader.readLine(); } reader.close(); } catch(FileNotFoundException e) { System.out.println("ERROR: File " + inputFileList[counter] + " was located, but apparently does not exist.\n" + "Skipping.\n"); continue; } catch(IOException e) { System.out.println("ERROR: An IO error occurred while " + "reading from the file " + inputFileList[counter] + ".\n" + "Skipping.\n"); continue; } // Add the list of entries to the general HashMap for later access. starEntries.put(inputFileList[counter], entryVector); } /* Now that we have read in all entries from all files, create the * output text files and write the formatted data to it. */ for(String filename: starEntries.keySet()) { // Create and open the file. Formatter writer = null; try { writer = new Formatter( new File(destinationDirectory, filename)); } catch(FileNotFoundException e) { System.out.println("ERROR: Unable to open " + new File(destinationDirectory, filename).getPath() + " for writing. Skipping."); continue; } // Write the formatted header. for(FormattedField field: formattedFields) { writer.format("%" + field.getSize() + "s%" + FIELD_SEPARATION_SIZE + "s", field.getName(), " "); } writer.format("\n"); // Write the entries, one at a time. int numberOfEntries = 0; for(StarEntry lineEntry: starEntries.get(filename)) { for(FormattedField field: formattedFields) { /* Is it the time field? If so, give it special treatment, * for a conversion is required. */ if(field.getName().equalsIgnoreCase("time")) { /* Convert the date from SMEI standard form * (YYYY_DOY_HHMMSS) to floating-point day form * (dddd.ddddd, where d is a digit). */ String SMEIDate = lineEntry.getEntry( field.getCorrespondingField()); SMEIDate = SMEIDate.trim(); if(!java.util.regex.Pattern.matches( "\\d\\d\\d\\d_\\d\\d\\d_\\d\\d\\d\\d\\d\\d", SMEIDate)) { System.out.println("ERROR: Corrupted SMEI date " + "data found in file " + filename + ". Writing a blank date entry."); writer.format("%" + field.getSize() + "s%1$" + FIELD_SEPARATION_SIZE + "s", " "); continue; } // Do the conversion. double convertedTime = 0.0; try { /* Stage 1: Figure out the number of whole years * between the zero date and the date given. */ int year = Integer.parseInt(SMEIDate.substring( 0, 4)); int middleYears = year - ZERO_TIME_YEAR - 1; if(middleYears > 0) { // Yes, we do have some middle years. // Leap years convertedTime += (middleYears + (ZERO_TIME_YEAR % 4)) / 4 * 366; // Regular years convertedTime += (middleYears - (middleYears + (ZERO_TIME_YEAR % 4)) / 4) * 365; } /* Stage 2: Figure out the number of days elapsed * between the zero date and the end of that same * year (if the year of the given date is not the * same as the zero year). */ if(year > ZERO_TIME_YEAR) { convertedTime += (ZERO_TIME_YEAR % 4 == 0 ? 366 : 365); convertedTime -= ZERO_TIME_DOY + (((ZERO_TIME_HOUR * 3600) + (ZERO_TIME_MINUTE * 60) + (ZERO_TIME_SECOND)) / (24.0 * 60.0 * 60.0)); } /* Stage 3: Figure out the number of days elapsed * between the year of the given date and the * given date. */ if(year >= ZERO_TIME_YEAR) { convertedTime += Integer.parseInt( SMEIDate.substring(5, 8)); convertedTime += ((Integer.parseInt(SMEIDate.substring( 9, 11)) * 3600) + (Integer.parseInt(SMEIDate.substring( 11, 13)) * 60) + Integer.parseInt(SMEIDate.substring( 13, 15))) / (24.0 * 60.0 * 60.0); if(year == ZERO_TIME_YEAR) { // Do subtraction based on zero date. convertedTime -= ZERO_TIME_DOY + (((ZERO_TIME_HOUR * 3600) + (ZERO_TIME_MINUTE * 60) + (ZERO_TIME_SECOND)) / (24.0 * 60.0 * 60.0)); } } else if(year < ZERO_TIME_YEAR) { /* For some reason, the date entered has a year * smaller than the zero time year. Take this * into account and generate a negative time. */ /* Stage 1: Figure out the number of years * between the two date years. */ convertedTime = 0.0; middleYears = ZERO_TIME_YEAR - year - 1; if(middleYears > 0) { // Yes, we do have some middle years. // Leap years convertedTime -= (middleYears - (ZERO_TIME_YEAR % 4) + 4) / 4 * 366; // Regular years convertedTime -= (middleYears - (middleYears - (ZERO_TIME_YEAR % 4) + 4) / 4) * 365; } /* Stage 2: Subtract the zero date. */ convertedTime -= ZERO_TIME_DOY + (((ZERO_TIME_HOUR * 3600) + (ZERO_TIME_MINUTE * 60) + (ZERO_TIME_SECOND)) / (24.0 * 60.0 * 60.0)); /* Stage 3: Figure out the number of days * elapsed between the year of the given * date and the given date. */ convertedTime -= (year % 4 == 0 ? 366 : 365) - Integer.parseInt( SMEIDate.substring(5, 8)); convertedTime -= ((24.0 * 60.0 * 60.0) - ((Integer.parseInt(SMEIDate.substring( 9, 11)) * 3600) + (Integer.parseInt(SMEIDate.substring( 11, 13)) * 60) + Integer.parseInt(SMEIDate.substring( 13, 15)))) / (24.0 * 60.0 * 60.0); } } catch(NumberFormatException e) { // This shouldn't happen. Do nothing. } // Output the result as an entry. writer.format("%" + field.getSize() + "." + TIME_DECIMAL_PRECISION + "g%" + FIELD_SEPARATION_SIZE + "s", convertedTime, " "); } else { // Just write the entry and be done with it. writer.format("%" + field.getSize() + "s%" + FIELD_SEPARATION_SIZE + "s", lineEntry.getEntry(field.getCorrespondingField()), " "); } if(writer.ioException() != null) { break; } } writer.format("\n"); numberOfEntries++; } writer.close(); System.out.println("Wrote file: " + filename + " (" + numberOfEntries + ")"); } // We're done! System.out.println("\nDone!"); } /** * Displays a usage message to standard output. * * @since 1.0 */ public static void printUsageMessage() { System.out.println( "Usage:\n" + " % starformat [Options]\n\n" + "Options:\n" + " -dir=directory Directory where input star data text " + "files are\n" + " located [default=" + DEFAULT_DIRECTORY + "]\n" + " -dest=destination Directory where formatted text files " + "are outputted\n" + " to [default=" + DEFAULT_DESTINATION + "]\n" + " -help Displays this usage message.\n\n" + "Examples:\n" + " starformat\n" + " Searches for star data text files in the default search\n" + " directory (" + DEFAULT_DIRECTORY + ") and outputs " + "formatted\n" + " text files to the default destination directory (" + DEFAULT_DESTINATION + ")\n" + " starformat -dir=/fake/directory -dest=/another/fake/dir\n" + " Searches for star data text files in the directory\n" + " /fake/directory and outputs formatted text files to the\n" + " directory /another/fake/dir\n\n" + "All star files found in the search directory are processed and\n"+ "the output file names are identical to those star data text\n" + "files found. Configuration information can be modified in the\n"+ "XML file " + INI_FILE + "."); } /* Filename filter used to determine which files in a given directory * are valid input files for StarFormat. * * @since 1.0 */ private static class InputFilenameFilter implements FilenameFilter { private static final String[] SUFFIXES = { "_c1e.txt", "_c1s.txt", "_c2e.txt", "_c2s.txt", "_c3e.txt", "_c3s.txt" }; private static final int SUFFIX_LENGTH = 8; public InputFilenameFilter() { } public boolean accept(File dir, String name) { if(name == null || name.length() == 0) { return false; } for(int counter = 0; counter < SUFFIXES.length; counter++) { try { if(name.substring(name.length() - SUFFIX_LENGTH). equalsIgnoreCase(SUFFIXES[counter])) { if(name.substring(0, name.length() - SUFFIX_LENGTH). length() != 0) { return true; } return false; } } catch(StringIndexOutOfBoundsException e) { return false; } } return false; } } /* ADT corresponding to a Field entry in the XML initialization file. * * @since 1.0 */ private static class Field { public static final int NULL = -1; public static final int STRING = 0; public static final int INT = 1; public static final int FLOAT = 2; public static final int FLOATE = 3; public static final int DOUBLE = 4; public static final int DOUBLEE = 5; public static final String[] TYPE_STRINGS = { "string", "int", "float", "floate", "double", "doublee" }; private String name; private int size; private int type; private int offset; public Field(String name, int size, int offset, int type) { if(name == null) { throw new NullPointerException(); } if(size <= 0 || offset < 0) { throw new IllegalArgumentException(); } this.name = name; this.size = size; this.offset = offset; if(type != STRING && type != INT && type != FLOAT && type != FLOATE && type != DOUBLE && type != DOUBLEE) { throw new IllegalArgumentException(); } this.type = type; } public String getName() { return name; } public int getOffset() { return offset; } public int getSize() { return size; } public int getType() { return type; } public static int getTypeFlag(String type) { if(type == null) { throw new NullPointerException(); } for(int counter = 0; counter < TYPE_STRINGS.length; counter++) { if(type.equalsIgnoreCase(TYPE_STRINGS[counter])) { return counter; } } return NULL; } } /* ADT corresponding to a FormattedField entry in the XML initialization * file. */ private static class FormattedField { private String name; private int size; private Field correspondingField; public FormattedField(String name, int size, Field correspondingField) { if(name == null || correspondingField == null) { throw new NullPointerException(); } if(size <= 0) { throw new IllegalArgumentException(); } this.name = name; this.size = size; this.correspondingField = correspondingField; } public Field getCorrespondingField() { return correspondingField; } public String getName() { return name; } public int getSize() { return size; } } /* ADT representing a line entry in an input file. The data fields are * stored in the HashMap fields, which can be accessed via Field objects * (pass it the Field object corresponding to the field data that you * wish to obtain). * * @since 1.0 */ private static class StarEntry { private HashMap fields; public StarEntry() { fields = new HashMap(); } public void addEntry(Field field, String value) { if(field == null || value == null) { throw new NullPointerException(); } fields.put(field, value); } public String getEntry(Field field) { return fields.get(field); } } }