1 2 import java.io.*; 3 import java.util.*; 4 5 // usage: java ZoneCompiler <setup file> <data directory> <output directory> <tzdata version> 6 // 7 // Compile a set of tzfile-formatted files into a single file containing an index. 8 // 9 // The compilation is controlled by a setup file, which is provided as a 10 // command-line argument. The setup file has the form: 11 // 12 // Link <toName> <fromName> 13 // ... 14 // <zone filename> 15 // ... 16 // 17 // Note that the links must be declared prior to the zone names. 18 // A zone name is a filename relative to the source directory such as 19 // 'GMT', 'Africa/Dakar', or 'America/Argentina/Jujuy'. 20 // 21 // Use the 'zic' command-line tool to convert from flat files 22 // (such as 'africa' or 'northamerica') to a directory 23 // hierarchy suitable for this tool (containing files such as 'data/Africa/Abidjan'). 24 // 25 26 public class ZoneCompactor { 27 // Maximum number of characters in a zone name, including '\0' terminator. 28 private static final int MAXNAME = 40; 29 30 // Zone name synonyms. 31 private Map<String,String> links = new HashMap<String,String>(); 32 33 // File offsets by zone name. 34 private Map<String,Integer> offsets = new HashMap<String,Integer>(); 35 36 // File lengths by zone name. 37 private Map<String,Integer> lengths = new HashMap<String,Integer>(); 38 39 // Concatenate the contents of 'inFile' onto 'out'. 40 private static void copyFile(File inFile, OutputStream out) throws Exception { 41 byte[] ret = new byte[0]; 42 43 InputStream in = new FileInputStream(inFile); 44 byte[] buf = new byte[8192]; 45 while (true) { 46 int nbytes = in.read(buf); 47 if (nbytes == -1) { 48 break; 49 } 50 out.write(buf, 0, nbytes); 51 52 byte[] nret = new byte[ret.length + nbytes]; 53 System.arraycopy(ret, 0, nret, 0, ret.length); 54 System.arraycopy(buf, 0, nret, ret.length, nbytes); 55 ret = nret; 56 } 57 out.flush(); 58 } 59 60 public ZoneCompactor(String setupFile, String dataDirectory, String zoneTabFile, String outputDirectory, String version) throws Exception { 61 // Read the setup file and concatenate all the data. 62 ByteArrayOutputStream allData = new ByteArrayOutputStream(); 63 BufferedReader reader = new BufferedReader(new FileReader(setupFile)); 64 String s; 65 int offset = 0; 66 while ((s = reader.readLine()) != null) { 67 s = s.trim(); 68 if (s.startsWith("Link")) { 69 StringTokenizer st = new StringTokenizer(s); 70 st.nextToken(); 71 String to = st.nextToken(); 72 String from = st.nextToken(); 73 links.put(from, to); 74 } else { 75 String link = links.get(s); 76 if (link == null) { 77 File sourceFile = new File(dataDirectory, s); 78 long length = sourceFile.length(); 79 offsets.put(s, offset); 80 lengths.put(s, (int) length); 81 82 offset += length; 83 copyFile(sourceFile, allData); 84 } 85 } 86 } 87 reader.close(); 88 89 // Fill in fields for links. 90 Iterator<String> it = links.keySet().iterator(); 91 while (it.hasNext()) { 92 String from = it.next(); 93 String to = links.get(from); 94 95 offsets.put(from, offsets.get(to)); 96 lengths.put(from, lengths.get(to)); 97 } 98 99 // Create/truncate the destination file. 100 RandomAccessFile f = new RandomAccessFile(new File(outputDirectory, "tzdata"), "rw"); 101 f.setLength(0); 102 103 // Write the header. 104 105 // byte[12] tzdata_version -- 'tzdata2012f\0' 106 // int index_offset -- so we can slip in extra header fields in a backwards-compatible way 107 // int data_offset 108 // int zonetab_offset 109 110 // tzdata_version 111 f.write(toAscii(new byte[12], version)); 112 113 // Write dummy values for the three offsets, and remember where we need to seek back to later 114 // when we have the real values. 115 int index_offset_offset = (int) f.getFilePointer(); 116 f.writeInt(0); 117 int data_offset_offset = (int) f.getFilePointer(); 118 f.writeInt(0); 119 int zonetab_offset_offset = (int) f.getFilePointer(); 120 f.writeInt(0); 121 122 int index_offset = (int) f.getFilePointer(); 123 124 // Write the index. 125 ArrayList<String> sortedOlsonIds = new ArrayList<String>(); 126 sortedOlsonIds.addAll(offsets.keySet()); 127 Collections.sort(sortedOlsonIds); 128 it = sortedOlsonIds.iterator(); 129 while (it.hasNext()) { 130 String zoneName = it.next(); 131 if (zoneName.length() >= MAXNAME) { 132 throw new RuntimeException("zone filename too long: " + zoneName.length()); 133 } 134 135 // Follow the chain of links to work out where the real data for this zone lives. 136 String actualZoneName = zoneName; 137 while (links.get(actualZoneName) != null) { 138 actualZoneName = links.get(actualZoneName); 139 } 140 141 f.write(toAscii(new byte[MAXNAME], zoneName)); 142 f.writeInt(offsets.get(actualZoneName)); 143 f.writeInt(lengths.get(actualZoneName)); 144 f.writeInt(0); // Used to be raw GMT offset. No longer used. 145 } 146 147 int data_offset = (int) f.getFilePointer(); 148 149 // Write the data. 150 f.write(allData.toByteArray()); 151 152 int zonetab_offset = (int) f.getFilePointer(); 153 154 // Copy the zone.tab. 155 reader = new BufferedReader(new FileReader(zoneTabFile)); 156 while ((s = reader.readLine()) != null) { 157 if (!s.startsWith("#")) { 158 f.writeBytes(s); 159 f.write('\n'); 160 } 161 } 162 reader.close(); 163 164 // Go back and fix up the offsets in the header. 165 f.seek(index_offset_offset); 166 f.writeInt(index_offset); 167 f.seek(data_offset_offset); 168 f.writeInt(data_offset); 169 f.seek(zonetab_offset_offset); 170 f.writeInt(zonetab_offset); 171 172 f.close(); 173 } 174 175 private static byte[] toAscii(byte[] dst, String src) { 176 for (int i = 0; i < src.length(); ++i) { 177 if (src.charAt(i) > '~') { 178 throw new RuntimeException("non-ASCII string: " + src); 179 } 180 dst[i] = (byte) src.charAt(i); 181 } 182 return dst; 183 } 184 185 public static void main(String[] args) throws Exception { 186 if (args.length != 5) { 187 System.err.println("usage: java ZoneCompactor <setup file> <data directory> <zone.tab file> <output directory> <tzdata version>"); 188 System.exit(0); 189 } 190 new ZoneCompactor(args[0], args[1], args[2], args[3], args[4]); 191 } 192 } 193