package se.unlogic.eagledns;

import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.net.UnknownHostException;
import java.rmi.AccessException;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Timer;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
import org.xbill.DNS.Address;
import org.xbill.DNS.CNAMERecord;
import org.xbill.DNS.DClass;
import org.xbill.DNS.DNAMERecord;
import org.xbill.DNS.ExtendedFlags;
import org.xbill.DNS.Flags;
import org.xbill.DNS.Header;
import org.xbill.DNS.Message;
import org.xbill.DNS.NSRecord;
import org.xbill.DNS.Name;
import org.xbill.DNS.NameTooLongException;
import org.xbill.DNS.OPTRecord;
import org.xbill.DNS.Opcode;
import org.xbill.DNS.RRset;
import org.xbill.DNS.Rcode;
import org.xbill.DNS.Record;
import org.xbill.DNS.Section;
import org.xbill.DNS.SetResponse;
import org.xbill.DNS.TSIG;
import org.xbill.DNS.TSIGRecord;
import org.xbill.DNS.Type;
import org.xbill.DNS.Zone;

import se.unlogic.standardutils.reflection.ReflectionUtils;
import se.unlogic.standardutils.settings.SettingNode;
import se.unlogic.standardutils.settings.XMLSettingNode;
import se.unlogic.standardutils.string.StringUtils;
import se.unlogic.standardutils.time.MillisecondTimeUnits;
import se.unlogic.standardutils.timer.RunnableTimerTask;

/**
 * EagleDNS copyright Robert "Unlogic" Olofsson (unlogic@unlogic.se)
 * 
 * Based on the jnamed class from the dnsjava project (http://www.dnsjava.org/) copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
 * 
 * @author Robert "Unlogic" Olofsson
 * @author Michael Neale, Red Hat (JBoss division)
 */

public class EagleDNS implements Runnable, EagleManager {

	public static final String VERSION = "Eagle DNS 1.0";

	static final int FLAG_DNSSECOK = 1;
	static final int FLAG_SIGONLY = 2;

	private final Logger log = Logger.getLogger(this.getClass());

	private final ConcurrentHashMap<Name, CachedPrimaryZone> primaryZoneMap = new ConcurrentHashMap<Name, CachedPrimaryZone>();
	private final ConcurrentHashMap<Name, CachedSecondaryZone> secondaryZoneMap = new ConcurrentHashMap<Name, CachedSecondaryZone>();
	private final HashMap<Name, TSIG> TSIGs = new HashMap<Name, TSIG>();

	private final HashMap<String, ZoneProvider> zoneProviders = new HashMap<String, ZoneProvider>();

	private int tcpThreadPoolSize = 20;
	private int udpThreadPoolSize = 20;

	private int tcpThreadPoolShutdownTimeout = 60;
	private int udpThreadPoolShutdownTimeout = 60;

	private ArrayList<TCPSocketMonitor> tcpMonitorThreads = new ArrayList<TCPSocketMonitor>();
	private ArrayList<UDPSocketMonitor> udpMonitorThreads = new ArrayList<UDPSocketMonitor>();

	private ThreadPoolExecutor tcpThreadPool;
	private ThreadPoolExecutor udpThreadPool;

	private int axfrTimeout = 60;

	private String remotePassword;
	private Integer remotePort;
	private LoginHandler loginHandler;

	private Timer secondaryZoneUpdateTimer;
	private RunnableTimerTask timerTask;
   private String configFilePath;

	private boolean shutdown = false;

   public EagleDNS() throws UnknownHostException {

   }

   public void setConfigFilePath(String configFilePath)
   {
      this.configFilePath = configFilePath;
   }

   public void setConfigClassPath(String path)
   {
      URL url = Thread.currentThread().getContextClassLoader().getResource(path);
      this.configFilePath = url.getPath();
   }

   public void start() throws UnknownHostException {

      //URL logurl = Thread.currentThread().getContextClassLoader().getResource("log4j.xml");
      //DOMConfigurator.configure(logurl);
      //DOMConfigurator.configure("conf/log4j.xml");

		log.info(VERSION + " starting...");

		XMLSettingNode configFile;

		try {
			log.debug("Parsing config file..." + configFilePath);
			configFile = new XMLSettingNode(configFilePath);

		} catch (Exception e) {

			log.fatal("Unable to open config file " + configFilePath + ", aborting startup!");
			System.out.println("Unable to open config file " + configFilePath + ", aborting startup!");
			return;
		}

		List<Integer> ports = configFile.getIntegers("/Config/System/Port");

		if (ports.isEmpty()) {

			log.debug("No ports found in config file " + configFilePath + ", using default port 53");
			ports.add(new Integer(53));
		}

		List<InetAddress> addresses = new ArrayList<InetAddress>();
		List<String> addressStrings = configFile.getStrings("/Config/System/Address");

		if (addressStrings == null || addressStrings.isEmpty()) {

			log.debug("No addresses found in config, listening on all addresses (0.0.0.0)");
			addresses.add(Address.getByAddress("0.0.0.0"));

		} else {

			for (String addressString : addressStrings) {

				try {

					addresses.add(Address.getByAddress(addressString));

				} catch (UnknownHostException e) {

					log.error("Invalid address " + addressString + " specified in config file, skipping address " + e);
				}
			}

			if (addresses.isEmpty()) {

				log.fatal("None of the " + addressStrings.size() + " addresses specified in the config file are valid, aborting startup!\n" + "Correct the addresses or remove them from the config file if you want to listen on all interfaces.");
            return;
			}
		}

		Integer tcpThreadPoolSize = configFile.getInteger("/Config/System/TCPThreadPoolSize");

		if (tcpThreadPoolSize != null) {

			log.debug("Setting TCP thread pool size to " + tcpThreadPoolSize);
			this.tcpThreadPoolSize = tcpThreadPoolSize;
		}

		Integer tcpThreadPoolShutdownTimeout = configFile.getInteger("/Config/System/TCPThreadPoolShutdownTimeout");

		if (tcpThreadPoolShutdownTimeout != null) {

			log.debug("Setting TCP thread pool shutdown timeout to " + tcpThreadPoolSize + " seconds");
			this.tcpThreadPoolShutdownTimeout = tcpThreadPoolShutdownTimeout;
		}

		Integer udpThreadPoolSize = configFile.getInteger("/Config/System/UDPThreadPoolSize");

		if (udpThreadPoolSize != null) {

			log.debug("Setting UDP thread pool size to " + udpThreadPoolSize);
			this.udpThreadPoolSize = udpThreadPoolSize;
		}

		Integer udpThreadPoolShutdownTimeout = configFile.getInteger("/Config/System/UDPThreadPoolShutdownTimeout");

		if (udpThreadPoolShutdownTimeout != null) {

			log.debug("Setting UDP thread pool shutdown timeout to " + udpThreadPoolSize + " seconds");
			this.udpThreadPoolShutdownTimeout = udpThreadPoolShutdownTimeout;
		}

		this.remotePassword = configFile.getString("/Config/System/RemoteManagementPassword");

		log.debug("Remote management password set to " + remotePassword);

		this.remotePort = configFile.getInteger("/Config/System/RemoteManagementPort");

		log.debug("Remote management port set to " + remotePort);

		Integer axfrTimeout = configFile.getInteger("/Config/System/AXFRTimeout");

		if (axfrTimeout != null) {

			log.debug("Setting AXFR timeout to " + axfrTimeout);
			this.axfrTimeout = axfrTimeout;
		}

		// TODO TSIG stuff

		List<XMLSettingNode> zoneProviderElements = configFile.getSettings("/Config/ZoneProviders/ZoneProvider");

		for (XMLSettingNode settingNode : zoneProviderElements) {

			String name = settingNode.getString("Name");

			if (StringUtils.isEmpty(name)) {

				log.error("ZoneProvider element with no name set found in config, ignoring element.");
				continue;
			}

			String className = settingNode.getString("Class");

			if (StringUtils.isEmpty(className)) {

				log.error("ZoneProvider element with no class set found in config, ignoring element.");
				continue;
			}

			try {

				log.debug("Instantiating zone provider " + name + " (" + className + ")");

				ZoneProvider zoneProvider = (ZoneProvider) Class.forName(className).newInstance();

				log.debug("Zone provider " + name + " successfully instantiated");

				List<XMLSettingNode> propertyElements = settingNode.getSettings("Properties/Property");

				for (SettingNode propertyElement : propertyElements) {

					String propertyName = propertyElement.getString("@name");

					if (StringUtils.isEmpty(propertyName)) {

						log.error("Property element with no name set found in config, ignoring element");
						continue;
					}

					String value = propertyElement.getString(".");

					log.debug("Found value " + value + " for property " + propertyName);

					try {
						Method method = zoneProvider.getClass().getMethod("set" + StringUtils.toFirstLetterUppercase(propertyName), String.class);

						ReflectionUtils.fixMethodAccess(method);

						log.debug("Setting property " + propertyName);

						try {

							method.invoke(zoneProvider, value);

						} catch (IllegalArgumentException e) {

							log.error("Unable to set property " + propertyName + " on zone provider " + name + " (" + className + ")", e);

						} catch (InvocationTargetException e) {

							log.error("Unable to set property " + propertyName + " on zone provider " + name + " (" + className + ")", e);
						}

					} catch (SecurityException e) {

						log.error("Unable to find matching setter method for property " + propertyName + " in zone provider " + name + " (" + className + ")", e);

					} catch (NoSuchMethodException e) {

						log.error("Unable to find matching setter method for property " + propertyName + " in zone provider " + name + " (" + className + ")", e);
					}
				}

				try {

					if (zoneProvider instanceof ZoneProviderUpdatable) {
						((ZoneProviderUpdatable) zoneProvider).setChangeListener(new ZoneChangeCallback() {
							public void zoneDataChanged() {
								reloadZones();
							}
						});
					}

					zoneProvider.init(name);

					log.debug("Zone provider " + name + " (" + className + ") successfully initialized!");

					this.zoneProviders.put(name, zoneProvider);

				} catch (Throwable e) {

					log.error("Error initializing zone provider " + name + " (" + className + ")", e);
				}

			} catch (InstantiationException e) {

				log.error("Unable to create instance of class " + className + " for zone provider " + name, e);

			} catch (IllegalAccessException e) {

				log.error("Unable to create instance of class " + className + " for zone provider " + name, e);

			} catch (ClassNotFoundException e) {

				log.error("Unable to create instance of class " + className + " for zone provider " + name, e);
			}
		}

		if (zoneProviders.isEmpty()) {
			log.fatal("No zone providers found or started, aborting startup!");
			return;
		}

		this.reloadZones();

		if(remotePassword == null || remotePort == null){

			log.debug("Remote managed port and/or password not set, remote managent will not be available.");

		}else{

			log.debug("Starting remote interface on port " + remotePort);

			this.loginHandler = new LoginHandler(this, this.remotePassword);

			try {
				EagleLogin eagleLogin = (EagleLogin) UnicastRemoteObject.exportObject(loginHandler,remotePort);
				UnicastRemoteObject.exportObject(this,remotePort);

				Registry registry = LocateRegistry.createRegistry(remotePort);

				registry.bind("eagleLogin", eagleLogin);

			} catch (AccessException e) {

				log.fatal("Unable to start remote manangement interface, aborting startup!",e);
				return;

			} catch (RemoteException e) {

				log.fatal("Unable to start remote manangement interface, aborting startup!",e);
				return;

			} catch (AlreadyBoundException e) {

				log.fatal("Unable to start remote manangement interface, aborting startup!",e);
				return;
			}
		}

		log.debug("Initializing TCP thread pool...");
		this.tcpThreadPool = new ThreadPoolExecutor(this.tcpThreadPoolSize, this.tcpThreadPoolSize, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());

		log.debug("Initializing UDP thread pool...");
		this.udpThreadPool = new ThreadPoolExecutor(this.udpThreadPoolSize, this.udpThreadPoolSize, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());

		Iterator<InetAddress> iaddr = addresses.iterator();
		while (iaddr.hasNext()) {
			InetAddress addr = iaddr.next();
			Iterator<Integer> iport = ports.iterator();
			while (iport.hasNext()) {
				int port = iport.next().intValue();

				try {
					this.udpMonitorThreads.add(new UDPSocketMonitor(this, addr, port));
				} catch (SocketException e) {
					log.error("Unable to open UDP server socket on address " + addr + ":" + port + ", " + e);
				}

				try {
					this.tcpMonitorThreads.add(new TCPSocketMonitor(this, addr, port));
				} catch (IOException e) {
					log.error("Unable to open TCP server socket on address " + addr + ":" + port + ", " + e);
				}
			}
		}

		if (this.tcpMonitorThreads.isEmpty() && this.udpMonitorThreads.isEmpty()) {

			log.fatal("Not bound on any sockets, aborting startup!");
			return;
		}

		log.debug("Starting secondary zone update timer...");
		this.timerTask = new RunnableTimerTask(this);
		this.secondaryZoneUpdateTimer = new Timer();
		this.secondaryZoneUpdateTimer.schedule(timerTask, MillisecondTimeUnits.SECOND * 60, MillisecondTimeUnits.SECOND * 60);

		log.info(VERSION + " started with " + this.primaryZoneMap.size() + " primary zones and " + this.secondaryZoneMap.size() + " secondary zones");
	}

   protected CountDownLatch shutdownLatch = new CountDownLatch(1);

	public synchronized void shutdown() {

		new Thread(){@Override
			public void run(){

			//RMI thread workaround
			actualShutdown();

		}}.start();
      try
      {
         log.debug("Awaiting shutdown latch");
         shutdownLatch.await();
      }
      catch (InterruptedException e)
      {
         throw new RuntimeException(e);
      }
   }

	void actualShutdown(){

      log.debug("In shutdown...");
		if (shutdown == false) {

         try
         {
            log.info("Shutting down " + VERSION + "...");

            shutdown = true;

            log.debug("Stopping secondary zone update timer...");
            timerTask.cancel();
            secondaryZoneUpdateTimer.cancel();

            log.debug("Stopping TCP thread pool...");
            tcpThreadPool.shutdown();

            try {
               tcpThreadPool.awaitTermination(tcpThreadPoolShutdownTimeout, TimeUnit.SECONDS);

            } catch (InterruptedException e1) {

               log.error("Timeout waiting " + tcpThreadPoolShutdownTimeout + " seconds for TCP thread pool to shutdown, forcing thread pool shutdown...");
               tcpThreadPool.shutdownNow();
            }

            log.debug("Stopping UDP thread pool...");
            udpThreadPool.shutdown();

            try {
               udpThreadPool.awaitTermination(udpThreadPoolShutdownTimeout, TimeUnit.SECONDS);

            } catch (InterruptedException e1) {

               log.error("Timeout waiting " + udpThreadPoolShutdownTimeout + " seconds for UDP thread pool to shutdown, forcing thread pool shutdown...");
               udpThreadPool.shutdownNow();
            }

            log.debug("Stopping sockets...");
            for (UDPSocketMonitor monitor : udpMonitorThreads)
            {
               try
               {
                  monitor.closeSocket();
               }
               catch (IOException e)
               {

               }
            }

            for (TCPSocketMonitor monitor : tcpMonitorThreads)
            {
               try
               {
                  monitor.closeSocket();
               }
               catch (IOException e)
               {

               }
            }

            log.info(VERSION + " stopped");
         }
         finally
         {
            shutdownLatch.countDown();
         }

         //System.exit(0);
		}
	}

	public synchronized void reloadZones() {

		this.primaryZoneMap.clear();
		this.secondaryZoneMap.clear();

		for (Entry<String, ZoneProvider> zoneProviderEntry : this.zoneProviders.entrySet()) {

			log.debug("Getting primary zones from zone provider " + zoneProviderEntry.getKey());

			Collection<Zone> primaryZones;

			try {
				primaryZones = zoneProviderEntry.getValue().getPrimaryZones();

			} catch (Throwable e) {

				log.error("Error getting primary zones from zone provider " + zoneProviderEntry.getKey(), e);
				continue;
			}

			if (primaryZones != null) {

				for (Zone zone : primaryZones) {

					log.debug("Got zone " + zone.getOrigin());

					this.primaryZoneMap.put(zone.getOrigin(), new CachedPrimaryZone(zone, zoneProviderEntry.getValue()));
				}
			}

			log.debug("Getting secondary zones from zone provider " + zoneProviderEntry.getKey());

			Collection<SecondaryZone> secondaryZones;

			try {
				secondaryZones = zoneProviderEntry.getValue().getSecondaryZones();

			} catch (Throwable e) {

				log.error("Error getting secondary zones from zone provider " + zoneProviderEntry.getKey(), e);
				continue;
			}

			if (secondaryZones != null) {

				for (SecondaryZone zone : secondaryZones) {

					log.debug("Got zone " + zone.getZoneName() + " (" + zone.getRemoteServerAddress() + ")");

					CachedSecondaryZone cachedSecondaryZone = new CachedSecondaryZone(zoneProviderEntry.getValue(), zone);

					this.secondaryZoneMap.put(cachedSecondaryZone.getSecondaryZone().getZoneName(), cachedSecondaryZone);
				}
			}
		}
	}

	// @SuppressWarnings("unused")
	// private void addPrimaryZone(String zname, String zonefile) throws IOException {
	// Name origin = null;
	// if (zname != null) {
	// origin = Name.fromString(zname, Name.root);
	// }
	// Zone newzone = new Zone(origin, zonefile);
	// primaryZoneMap.put(newzone.getOrigin(), newzone);
	// }
	//
	// @SuppressWarnings("unused")
	// private void addSecondaryZone(String zone, String remote) throws IOException, ZoneTransferException {
	// Name zname = Name.fromString(zone, Name.root);
	// Zone newzone = new Zone(zname, DClass.IN, remote);
	// primaryZoneMap.put(zname, newzone);
	// }

	@SuppressWarnings("unused")
	private void addTSIG(String algstr, String namestr, String key) throws IOException {
		Name name = Name.fromString(namestr, Name.root);
		TSIGs.put(name, new TSIG(algstr, namestr, key));
	}

	private Zone findBestZone(Name name) {

		Zone foundzone = getZone(name);

		if (foundzone != null) {
			return foundzone;
		}

		int labels = name.labels();

		for (int i = 1; i < labels; i++) {

			Name tname = new Name(name, i);
			foundzone = getZone(tname);

			if (foundzone != null) {
				return foundzone;
			}
		}

		return null;
	}

	private Zone getZone(Name name) {

		CachedPrimaryZone cachedPrimaryZone = this.primaryZoneMap.get(name);

		if (cachedPrimaryZone != null) {
			return cachedPrimaryZone.getZone();
		}

		CachedSecondaryZone cachedSecondaryZone = this.secondaryZoneMap.get(name);

		if (cachedSecondaryZone != null && cachedSecondaryZone.getSecondaryZone().getZoneCopy() != null) {

			return cachedSecondaryZone.getSecondaryZone().getZoneCopy();
		}

		return null;
	}

	private RRset findExactMatch(Name name, int type, int dclass, boolean glue) {
		Zone zone = findBestZone(name);

		if (zone != null) {
			return zone.findExactMatch(name, type);
		}

		return null;
	}

	private void addRRset(Name name, Message response, RRset rrset, int section, int flags) {
		for (int s = 1; s <= section; s++) {
			if (response.findRRset(name, rrset.getType(), s)) {
				return;
			}
		}
		if ((flags & FLAG_SIGONLY) == 0) {
			Iterator<?> it = rrset.rrs();
			while (it.hasNext()) {
				Record r = (Record) it.next();
				if (r.getName().isWild() && !name.isWild()) {
					r = r.withName(name);
				}
				response.addRecord(r, section);
			}
		}
		if ((flags & (FLAG_SIGONLY | FLAG_DNSSECOK)) != 0) {
			Iterator<?> it = rrset.sigs();
			while (it.hasNext()) {
				Record r = (Record) it.next();
				if (r.getName().isWild() && !name.isWild()) {
					r = r.withName(name);
				}
				response.addRecord(r, section);
			}
		}
	}

	private final void addSOA(Message response, Zone zone) {
		response.addRecord(zone.getSOA(), Section.AUTHORITY);
	}

	private final void addNS(Message response, Zone zone, int flags) {
		RRset nsRecords = zone.getNS();
		addRRset(nsRecords.getName(), response, nsRecords, Section.AUTHORITY, flags);
	}

	private void addGlue(Message response, Name name, int flags) {
		RRset a = findExactMatch(name, Type.A, DClass.IN, true);
		if (a == null) {
			return;
		}
		addRRset(name, response, a, Section.ADDITIONAL, flags);
	}

	private void addAdditional2(Message response, int section, int flags) {
		Record[] records = response.getSectionArray(section);
		for (Record r : records) {
			Name glueName = r.getAdditionalName();
			if (glueName != null) {
				addGlue(response, glueName, flags);
			}
		}
	}

	private final void addAdditional(Message response, int flags) {
		addAdditional2(response, Section.ANSWER, flags);
		addAdditional2(response, Section.AUTHORITY, flags);
	}

	private byte addAnswer(Message response, Name name, int type, int dclass, int iterations, int flags) {
		SetResponse sr;
		byte rcode = Rcode.NOERROR;

		if (iterations > 6) {
			return Rcode.NOERROR;
		}

		if (type == Type.SIG || type == Type.RRSIG) {
			type = Type.ANY;
			flags |= FLAG_SIGONLY;
		}

		Zone zone = findBestZone(name);
		if (zone != null) {
			sr = zone.findRecords(name, type);

			if (sr.isNXDOMAIN()) {
				response.getHeader().setRcode(Rcode.NXDOMAIN);
				if (zone != null) {
					addSOA(response, zone);
					if (iterations == 0) {
						response.getHeader().setFlag(Flags.AA);
					}
				}
				rcode = Rcode.NXDOMAIN;
			} else if (sr.isNXRRSET()) {
				if (zone != null) {
					addSOA(response, zone);
					if (iterations == 0) {
						response.getHeader().setFlag(Flags.AA);
					}
				}
			} else if (sr.isDelegation()) {
				RRset nsRecords = sr.getNS();
				addRRset(nsRecords.getName(), response, nsRecords, Section.AUTHORITY, flags);
			} else if (sr.isCNAME()) {
				CNAMERecord cname = sr.getCNAME();
				RRset rrset = new RRset(cname);
				addRRset(name, response, rrset, Section.ANSWER, flags);
				if (zone != null && iterations == 0) {
					response.getHeader().setFlag(Flags.AA);
				}
				rcode = addAnswer(response, cname.getTarget(), type, dclass, iterations + 1, flags);
			} else if (sr.isDNAME()) {
				DNAMERecord dname = sr.getDNAME();
				RRset rrset = new RRset(dname);
				addRRset(name, response, rrset, Section.ANSWER, flags);
				Name newname;
				try {
					newname = name.fromDNAME(dname);
				} catch (NameTooLongException e) {
					return Rcode.YXDOMAIN;
				}
				rrset = new RRset(new CNAMERecord(name, dclass, 0, newname));
				addRRset(name, response, rrset, Section.ANSWER, flags);
				if (zone != null && iterations == 0) {
					response.getHeader().setFlag(Flags.AA);
				}
				rcode = addAnswer(response, newname, type, dclass, iterations + 1, flags);
			} else if (sr.isSuccessful()) {
				RRset[] rrsets = sr.answers();
				for (RRset rrset : rrsets) {
					addRRset(name, response, rrset, Section.ANSWER, flags);
				}
				if (zone != null) {
					addNS(response, zone, flags);
					if (iterations == 0) {
						response.getHeader().setFlag(Flags.AA);
					}
				}
			}
		}

		return rcode;
	}

	private byte[] doAXFR(Name name, Message query, TSIG tsig, TSIGRecord qtsig, Socket s) {

		boolean first = true;

		Zone zone = this.findBestZone(name);

		if (zone == null) {

			return errorMessage(query, Rcode.REFUSED);

		}

		// Check that the IP requesting the AXFR is present as a NS in this zone
		boolean axfrAllowed = false;

		Iterator<?> nsIterator = zone.getNS().rrs();

		while (nsIterator.hasNext()) {

			NSRecord record = (NSRecord) nsIterator.next();

			try {
				String nsIP = InetAddress.getByName(record.getTarget().toString()).getHostAddress();

				if (s.getInetAddress().getHostAddress().equals(nsIP)) {

					axfrAllowed = true;
					break;
				}

			} catch (UnknownHostException e) {

				log.warn("Unable to resolve hostname of nameserver " + record.getTarget() + " in zone " + zone.getOrigin() + " while processing AXFR request from " + s.getRemoteSocketAddress());
			}
		}

		if (!axfrAllowed) {
			log.warn("AXFR request of zone " + zone.getOrigin() + " from " + s.getRemoteSocketAddress() + " refused!");
			return errorMessage(query, Rcode.REFUSED);
		}

		Iterator<?> it = zone.AXFR();

		try {
			DataOutputStream dataOut;
			dataOut = new DataOutputStream(s.getOutputStream());
			int id = query.getHeader().getID();
			while (it.hasNext()) {
				RRset rrset = (RRset) it.next();
				Message response = new Message(id);
				Header header = response.getHeader();
				header.setFlag(Flags.QR);
				header.setFlag(Flags.AA);
				addRRset(rrset.getName(), response, rrset, Section.ANSWER, FLAG_DNSSECOK);
				if (tsig != null) {
					tsig.applyStream(response, qtsig, first);
					qtsig = response.getTSIG();
				}
				first = false;
				byte[] out = response.toWire();
				dataOut.writeShort(out.length);
				dataOut.write(out);
			}
		} catch (IOException ex) {
			log.warn("AXFR failed", ex);
		}
		try {
			s.close();
		} catch (IOException ex) {
		}
		return null;
	}

	/*
	 * Note: a null return value means that the caller doesn't need to do
	 * anything.  Currently this only happens if this is an AXFR request over
	 * TCP.
	 */
	byte[] generateReply(Message query, byte[] in, int length, Socket socket) throws IOException {
		Header header;
		// boolean badversion;
		int maxLength;
		int flags = 0;

		header = query.getHeader();
		if (header.getFlag(Flags.QR)) {
			return null;
		}
		if (header.getRcode() != Rcode.NOERROR) {
			return errorMessage(query, Rcode.FORMERR);
		}
		if (header.getOpcode() != Opcode.QUERY) {
			return errorMessage(query, Rcode.NOTIMP);
		}

		Record queryRecord = query.getQuestion();

		TSIGRecord queryTSIG = query.getTSIG();
		TSIG tsig = null;
		if (queryTSIG != null) {
			tsig = TSIGs.get(queryTSIG.getName());
			if (tsig == null || tsig.verify(query, in, length, null) != Rcode.NOERROR) {
				return formerrMessage(in);
			}
		}

		OPTRecord queryOPT = query.getOPT();
		if (queryOPT != null && queryOPT.getVersion() > 0) {
			// badversion = true;
		}

		if (socket != null) {
			maxLength = 65535;
		} else if (queryOPT != null) {
			maxLength = Math.max(queryOPT.getPayloadSize(), 512);
		} else {
			maxLength = 512;
		}

		if (queryOPT != null && (queryOPT.getFlags() & ExtendedFlags.DO) != 0) {
			flags = FLAG_DNSSECOK;
		}

		Message response = new Message(query.getHeader().getID());
		response.getHeader().setFlag(Flags.QR);
		if (query.getHeader().getFlag(Flags.RD)) {
			response.getHeader().setFlag(Flags.RD);
		}
		response.addRecord(queryRecord, Section.QUESTION);

		Name name = queryRecord.getName();
		int type = queryRecord.getType();
		int dclass = queryRecord.getDClass();
		if (type == Type.AXFR && socket != null) {
			return doAXFR(name, query, tsig, queryTSIG, socket);
		}
		if (!Type.isRR(type) && type != Type.ANY) {
			return errorMessage(query, Rcode.NOTIMP);
		}

		byte rcode = addAnswer(response, name, type, dclass, 0, flags);
		if (rcode != Rcode.NOERROR && rcode != Rcode.NXDOMAIN) {
			return errorMessage(query, rcode);
		}

		addAdditional(response, flags);

		if (queryOPT != null) {
			int optflags = (flags == FLAG_DNSSECOK) ? ExtendedFlags.DO : 0;
			OPTRecord opt = new OPTRecord((short) 4096, rcode, (byte) 0, optflags);
			response.addRecord(opt, Section.ADDITIONAL);
		}

		response.setTSIG(tsig, Rcode.NOERROR, queryTSIG);
		return response.toWire(maxLength);
	}

	private byte[] buildErrorMessage(Header header, int rcode, Record question) {
		Message response = new Message();
		response.setHeader(header);
		for (int i = 0; i < 4; i++) {
			response.removeAllRecords(i);
		}
		if (rcode == Rcode.SERVFAIL) {
			response.addRecord(question, Section.QUESTION);
		}
		header.setRcode(rcode);
		return response.toWire();
	}

	byte[] formerrMessage(byte[] in) {
		Header header;
		try {
			header = new Header(in);
		} catch (IOException e) {
			return null;
		}
		return buildErrorMessage(header, Rcode.FORMERR, null);
	}

	private byte[] errorMessage(Message query, int rcode) {
		return buildErrorMessage(query.getHeader(), rcode, query.getQuestion());
	}

	protected void UDPClient(DatagramSocket socket, DatagramPacket inDataPacket) {

	}

	public static String toString(Record record) {

		if (record == null) {

			return null;
		}

		StringBuilder stringBuilder = new StringBuilder();

		stringBuilder.append(record.getName());

		stringBuilder.append(" ");

		stringBuilder.append(record.getTTL());

		stringBuilder.append(" ");

		stringBuilder.append(DClass.string(record.getDClass()));

		stringBuilder.append(" ");

		stringBuilder.append(Type.string(record.getType()));

		String rdata = record.rdataToString();

		if (!rdata.equals("")) {
			stringBuilder.append(" ");
			stringBuilder.append(rdata);
		}

		return stringBuilder.toString();
	}

	public void run() {

		log.debug("Checking secondary zones...");

		for(CachedSecondaryZone cachedSecondaryZone : this.secondaryZoneMap.values()){

			SecondaryZone secondaryZone = cachedSecondaryZone.getSecondaryZone();

			if(secondaryZone.getZoneCopy() == null || secondaryZone.getDownloaded() == null || (System.currentTimeMillis() - secondaryZone.getDownloaded().getTime()) > (secondaryZone.getZoneCopy().getSOA().getRefresh() * 1000)){

				cachedSecondaryZone.update(this.axfrTimeout);
			}
		}
	}

	protected ThreadPoolExecutor getTcpThreadPool() {

		return tcpThreadPool;
	}

	protected ThreadPoolExecutor getUdpThreadPool() {

		return udpThreadPool;
	}

	public boolean isShutdown() {

		return shutdown;
	}
}
