Master.java revision d7955ce24d294fb2014c59d11fca184471056f44
1// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
2
3package org.xbill.DNS;
4
5import java.io.*;
6import java.util.*;
7
8/**
9 * A DNS master file parser.  This incrementally parses the file, returning
10 * one record at a time.  When directives are seen, they are added to the
11 * state and used when parsing future records.
12 *
13 * @author Brian Wellington
14 */
15
16public class Master {
17
18private Name origin;
19private File file;
20private Record last = null;
21private long defaultTTL;
22private Master included = null;
23private Tokenizer st;
24private int currentType;
25private int currentDClass;
26private long currentTTL;
27private boolean needSOATTL;
28
29private Generator generator;
30private List generators;
31private boolean noExpandGenerate;
32
33Master(File file, Name origin, long initialTTL) throws IOException {
34	if (origin != null && !origin.isAbsolute()) {
35		throw new RelativeNameException(origin);
36	}
37	this.file = file;
38	st = new Tokenizer(file);
39	this.origin = origin;
40	defaultTTL = initialTTL;
41}
42
43/**
44 * Initializes the master file reader and opens the specified master file.
45 * @param filename The master file.
46 * @param origin The initial origin to append to relative names.
47 * @param ttl The initial default TTL.
48 * @throws IOException The master file could not be opened.
49 */
50public
51Master(String filename, Name origin, long ttl) throws IOException {
52	this(new File(filename), origin, ttl);
53}
54
55/**
56 * Initializes the master file reader and opens the specified master file.
57 * @param filename The master file.
58 * @param origin The initial origin to append to relative names.
59 * @throws IOException The master file could not be opened.
60 */
61public
62Master(String filename, Name origin) throws IOException {
63	this(new File(filename), origin, -1);
64}
65
66/**
67 * Initializes the master file reader and opens the specified master file.
68 * @param filename The master file.
69 * @throws IOException The master file could not be opened.
70 */
71public
72Master(String filename) throws IOException {
73	this(new File(filename), null, -1);
74}
75
76/**
77 * Initializes the master file reader.
78 * @param in The input stream containing a master file.
79 * @param origin The initial origin to append to relative names.
80 * @param ttl The initial default TTL.
81 */
82public
83Master(InputStream in, Name origin, long ttl) {
84	if (origin != null && !origin.isAbsolute()) {
85		throw new RelativeNameException(origin);
86	}
87	st = new Tokenizer(in);
88	this.origin = origin;
89	defaultTTL = ttl;
90}
91
92/**
93 * Initializes the master file reader.
94 * @param in The input stream containing a master file.
95 * @param origin The initial origin to append to relative names.
96 */
97public
98Master(InputStream in, Name origin) {
99	this(in, origin, -1);
100}
101
102/**
103 * Initializes the master file reader.
104 * @param in The input stream containing a master file.
105 */
106public
107Master(InputStream in) {
108	this(in, null, -1);
109}
110
111private Name
112parseName(String s, Name origin) throws TextParseException {
113	try {
114		return Name.fromString(s, origin);
115	}
116	catch (TextParseException e) {
117		throw st.exception(e.getMessage());
118	}
119}
120
121private void
122parseTTLClassAndType() throws IOException {
123	String s;
124	boolean seen_class = false;
125
126
127	// This is a bit messy, since any of the following are legal:
128	//   class ttl type
129	//   ttl class type
130	//   class type
131	//   ttl type
132	//   type
133	seen_class = false;
134	s = st.getString();
135	if ((currentDClass = DClass.value(s)) >= 0) {
136		s = st.getString();
137		seen_class = true;
138	}
139
140	currentTTL = -1;
141	try {
142		currentTTL = TTL.parseTTL(s);
143		s = st.getString();
144	}
145	catch (NumberFormatException e) {
146		if (defaultTTL >= 0)
147			currentTTL = defaultTTL;
148		else if (last != null)
149			currentTTL = last.getTTL();
150	}
151
152	if (!seen_class) {
153		if ((currentDClass = DClass.value(s)) >= 0) {
154			s = st.getString();
155		} else {
156			currentDClass = DClass.IN;
157		}
158	}
159
160	if ((currentType = Type.value(s)) < 0)
161		throw st.exception("Invalid type '" + s + "'");
162
163	// BIND allows a missing TTL for the initial SOA record, and uses
164	// the SOA minimum value.  If the SOA is not the first record,
165	// this is an error.
166	if (currentTTL < 0) {
167		if (currentType != Type.SOA)
168			throw st.exception("missing TTL");
169		needSOATTL = true;
170		currentTTL = 0;
171	}
172}
173
174private long
175parseUInt32(String s) {
176	if (!Character.isDigit(s.charAt(0)))
177		return -1;
178	try {
179		long l = Long.parseLong(s);
180		if (l < 0 || l > 0xFFFFFFFFL)
181			return -1;
182		return l;
183	}
184	catch (NumberFormatException e) {
185		return -1;
186	}
187}
188
189private void
190startGenerate() throws IOException {
191	String s;
192	int n;
193
194	// The first field is of the form start-end[/step]
195	// Regexes would be useful here.
196	s = st.getIdentifier();
197	n = s.indexOf("-");
198	if (n < 0)
199		throw st.exception("Invalid $GENERATE range specifier: " + s);
200	String startstr = s.substring(0, n);
201	String endstr = s.substring(n + 1);
202	String stepstr = null;
203	n = endstr.indexOf("/");
204	if (n >= 0) {
205		stepstr = endstr.substring(n + 1);
206		endstr = endstr.substring(0, n);
207	}
208	long start = parseUInt32(startstr);
209	long end = parseUInt32(endstr);
210	long step;
211	if (stepstr != null)
212		step = parseUInt32(stepstr);
213	else
214		step = 1;
215	if (start < 0 || end < 0 || start > end || step <= 0)
216		throw st.exception("Invalid $GENERATE range specifier: " + s);
217
218	// The next field is the name specification.
219	String nameSpec = st.getIdentifier();
220
221	// Then the ttl/class/type, in the same form as a normal record.
222	// Only some types are supported.
223	parseTTLClassAndType();
224	if (!Generator.supportedType(currentType))
225		throw st.exception("$GENERATE does not support " +
226				   Type.string(currentType) + " records");
227
228	// Next comes the rdata specification.
229	String rdataSpec = st.getIdentifier();
230
231	// That should be the end.  However, we don't want to move past the
232	// line yet, so put back the EOL after reading it.
233	st.getEOL();
234	st.unget();
235
236	generator = new Generator(start, end, step, nameSpec,
237				  currentType, currentDClass, currentTTL,
238				  rdataSpec, origin);
239	if (generators == null)
240		generators = new ArrayList(1);
241	generators.add(generator);
242}
243
244private void
245endGenerate() throws IOException {
246	// Read the EOL that we put back before.
247	st.getEOL();
248
249	generator = null;
250}
251
252private Record
253nextGenerated() throws IOException {
254	try {
255		return generator.nextRecord();
256	}
257	catch (Tokenizer.TokenizerException e) {
258		throw st.exception("Parsing $GENERATE: " + e.getBaseMessage());
259	}
260	catch (TextParseException e) {
261		throw st.exception("Parsing $GENERATE: " + e.getMessage());
262	}
263}
264
265/**
266 * Returns the next record in the master file.  This will process any
267 * directives before the next record.
268 * @return The next record.
269 * @throws IOException The master file could not be read, or was syntactically
270 * invalid.
271 */
272public Record
273_nextRecord() throws IOException {
274	Tokenizer.Token token;
275	String s;
276
277	if (included != null) {
278		Record rec = included.nextRecord();
279		if (rec != null)
280			return rec;
281		included = null;
282	}
283	if (generator != null) {
284		Record rec = nextGenerated();
285		if (rec != null)
286			return rec;
287		endGenerate();
288	}
289	while (true) {
290		Name name;
291
292		token = st.get(true, false);
293		if (token.type == Tokenizer.WHITESPACE) {
294			Tokenizer.Token next = st.get();
295			if (next.type == Tokenizer.EOL)
296				continue;
297			else if (next.type == Tokenizer.EOF)
298				return null;
299			else
300				st.unget();
301			if (last == null)
302				throw st.exception("no owner");
303			name = last.getName();
304		}
305		else if (token.type == Tokenizer.EOL)
306			continue;
307		else if (token.type == Tokenizer.EOF)
308			return null;
309		else if (((String) token.value).charAt(0) == '$') {
310			s = token.value;
311
312			if (s.equalsIgnoreCase("$ORIGIN")) {
313				origin = st.getName(Name.root);
314				st.getEOL();
315				continue;
316			} else if (s.equalsIgnoreCase("$TTL")) {
317				defaultTTL = st.getTTL();
318				st.getEOL();
319				continue;
320			} else  if (s.equalsIgnoreCase("$INCLUDE")) {
321				String filename = st.getString();
322				File newfile;
323				if (file != null) {
324					String parent = file.getParent();
325					newfile = new File(parent, filename);
326				} else {
327					newfile = new File(filename);
328				}
329				Name incorigin = origin;
330				token = st.get();
331				if (token.isString()) {
332					incorigin = parseName(token.value,
333							      Name.root);
334					st.getEOL();
335				}
336				included = new Master(newfile, incorigin,
337						      defaultTTL);
338				/*
339				 * If we continued, we wouldn't be looking in
340				 * the new file.  Recursing works better.
341				 */
342				return nextRecord();
343			} else  if (s.equalsIgnoreCase("$GENERATE")) {
344				if (generator != null)
345					throw new IllegalStateException
346						("cannot nest $GENERATE");
347				startGenerate();
348				if (noExpandGenerate) {
349					endGenerate();
350					continue;
351				}
352				return nextGenerated();
353			} else {
354				throw st.exception("Invalid directive: " + s);
355			}
356		} else {
357			s = token.value;
358			name = parseName(s, origin);
359			if (last != null && name.equals(last.getName())) {
360				name = last.getName();
361			}
362		}
363
364		parseTTLClassAndType();
365		last = Record.fromString(name, currentType, currentDClass,
366					 currentTTL, st, origin);
367		if (needSOATTL) {
368			long ttl = ((SOARecord)last).getMinimum();
369			last.setTTL(ttl);
370			defaultTTL = ttl;
371			needSOATTL = false;
372		}
373		return last;
374	}
375}
376
377/**
378 * Returns the next record in the master file.  This will process any
379 * directives before the next record.
380 * @return The next record.
381 * @throws IOException The master file could not be read, or was syntactically
382 * invalid.
383 */
384public Record
385nextRecord() throws IOException {
386	Record rec = null;
387	try {
388		rec = _nextRecord();
389	}
390	finally {
391		if (rec == null) {
392			st.close();
393		}
394	}
395	return rec;
396}
397
398/**
399 * Specifies whether $GENERATE statements should be expanded.  Whether
400 * expanded or not, the specifications for generated records are available
401 * by calling {@link #generators}.  This must be called before a $GENERATE
402 * statement is seen during iteration to have an effect.
403 */
404public void
405expandGenerate(boolean wantExpand) {
406	noExpandGenerate = !wantExpand;
407}
408
409/**
410 * Returns an iterator over the generators specified in the master file; that
411 * is, the parsed contents of $GENERATE statements.
412 * @see Generator
413 */
414public Iterator
415generators() {
416	if (generators != null)
417		return Collections.unmodifiableList(generators).iterator();
418	else
419		return Collections.EMPTY_LIST.iterator();
420}
421
422protected void
423finalize() {
424	st.close();
425}
426
427}
428