// Copyright (c) 2002-2004 Brian Wellington (bwelling@xbill.org)

package org.xbill.DNS;

import java.util.*;
import java.io.*;
import java.net.*;

/**
 * The Lookup object issues queries to caching DNS servers.  The input consists
 * of a name, an optional type, and an optional class.  Caching is enabled
 * by default and used when possible to reduce the number of DNS requests.
 * A Resolver, which defaults to an ExtendedResolver initialized with the
 * resolvers located by the ResolverConfig class, performs the queries.  A
 * search path of domain suffixes is used to resolve relative names, and is
 * also determined by the ResolverConfig class.
 * <p/>
 * A Lookup object may be reused, but should not be used by multiple threads.
 *
 * @author Brian Wellington
 * @see Cache
 * @see Resolver
 * @see ResolverConfig
 */

public final class Lookup {

    private static Resolver defaultResolver;
    private static Name[] defaultSearchPath;
    private static Map defaultCaches;
    private static int defaultNdots;

    private Resolver resolver;
    private Name[] searchPath;
    private Cache cache;
    private boolean temporary_cache;
    private int credibility;
    private Name name;
    private int type;
    private int dclass;
    private boolean verbose;
    private int iterations;
    private boolean foundAlias;
    private boolean done;
    private boolean doneCurrent;
    private List aliases;
    private Record[] answers;
    private int result;
    private String error;
    private boolean nxdomain;
    private boolean badresponse;
    private String badresponse_error;
    private boolean networkerror;
    private boolean timedout;
    private boolean nametoolong;
    private boolean referral;

    private static final Name[] noAliases = new Name[0];

    /**
     * The lookup was successful.
     */
    public static final int SUCCESSFUL = 0;

    /**
     * The lookup failed due to a data or server error. Repeating the lookup
     * would not be helpful.
     */
    public static final int UNRECOVERABLE = 1;

    /**
     * The lookup failed due to a network error. Repeating the lookup may be
     * helpful.
     */
    public static final int TRY_AGAIN = 2;

    /**
     * The host does not exist.
     */
    public static final int HOST_NOT_FOUND = 3;

    /**
     * The host exists, but has no records associated with the queried type.
     */
    public static final int TYPE_NOT_FOUND = 4;

    public static synchronized void
    refreshDefault() {

        try {
            defaultResolver = new ExtendedResolver();
        } catch (UnknownHostException e) {
            throw new RuntimeException("Failed to initialize resolver");
        }
        defaultSearchPath = ResolverConfig.getCurrentConfig().searchPath();
        defaultCaches = new HashMap();
        defaultNdots = ResolverConfig.getCurrentConfig().ndots();
    }

    static {
        refreshDefault();
    }

    /**
     * Gets the Resolver that will be used as the default by future Lookups.
     *
     * @return The default resolver.
     */
    public static synchronized Resolver
    getDefaultResolver() {
        return defaultResolver;
    }

    /**
     * Sets the default Resolver to be used as the default by future Lookups.
     *
     * @param resolver The default resolver.
     */
    public static synchronized void
    setDefaultResolver(Resolver resolver) {
        defaultResolver = resolver;
    }

    /**
     * Gets the Cache that will be used as the default for the specified
     * class by future Lookups.
     *
     * @param dclass The class whose cache is being retrieved.
     * @return The default cache for the specified class.
     */
    public static synchronized Cache
    getDefaultCache(int dclass) {
        DClass.check(dclass);
        Cache c = (Cache) defaultCaches.get(Mnemonic.toInteger(dclass));
        if (c == null) {
            c = new Cache(dclass);
            defaultCaches.put(Mnemonic.toInteger(dclass), c);
        }
        return c;
    }

    /**
     * Sets the Cache to be used as the default for the specified class by future
     * Lookups.
     *
     * @param cache  The default cache for the specified class.
     * @param dclass The class whose cache is being set.
     */
    public static synchronized void
    setDefaultCache(Cache cache, int dclass) {
        DClass.check(dclass);
        defaultCaches.put(Mnemonic.toInteger(dclass), cache);
    }

    /**
     * Gets the search path that will be used as the default by future Lookups.
     *
     * @return The default search path.
     */
    public static synchronized Name[]
    getDefaultSearchPath() {
        return defaultSearchPath;
    }

    /**
     * Sets the search path to be used as the default by future Lookups.
     *
     * @param domains The default search path.
     */
    public static synchronized void
    setDefaultSearchPath(Name[] domains) {
        defaultSearchPath = domains;
    }

    /**
     * Sets the search path that will be used as the default by future Lookups.
     *
     * @param domains The default search path.
     * @throws TextParseException A name in the array is not a valid DNS name.
     */
    public static synchronized void
    setDefaultSearchPath(String[] domains) throws TextParseException {
        if (domains == null) {
            defaultSearchPath = null;
            return;
        }
        Name[] newdomains = new Name[domains.length];
        for (int i = 0; i < domains.length; i++)
            newdomains[i] = Name.fromString(domains[i], Name.root);
        defaultSearchPath = newdomains;
    }

    private final void
    reset() {
        iterations = 0;
        foundAlias = false;
        done = false;
        doneCurrent = false;
        aliases = null;
        answers = null;
        result = -1;
        error = null;
        nxdomain = false;
        badresponse = false;
        badresponse_error = null;
        networkerror = false;
        timedout = false;
        nametoolong = false;
        referral = false;
        if (temporary_cache)
            cache.clearCache();
    }

    /**
     * Create a Lookup object that will find records of the given name, type,
     * and class.  The lookup will use the default cache, resolver, and search
     * path, and look for records that are reasonably credible.
     *
     * @param name   The name of the desired records
     * @param type   The type of the desired records
     * @param dclass The class of the desired records
     * @throws IllegalArgumentException The type is a meta type other than ANY.
     * @see Cache
     * @see Resolver
     * @see Credibility
     * @see Name
     * @see Type
     * @see DClass
     */
    public Lookup(Name name, int type, int dclass) {
        Type.check(type);
        DClass.check(dclass);
        if (!Type.isRR(type) && type != Type.ANY)
            throw new IllegalArgumentException("Cannot query for " +
                    "meta-types other than ANY");
        this.name = name;
        this.type = type;
        this.dclass = dclass;
        synchronized (Lookup.class) {
            this.resolver = getDefaultResolver();
            this.searchPath = getDefaultSearchPath();
            this.cache = getDefaultCache(dclass);
        }
        this.credibility = Credibility.NORMAL;
        this.verbose = Options.check("verbose");
        this.result = -1;
    }

    /**
     * Create a Lookup object that will find records of the given name and type
     * in the IN class.
     *
     * @param name The name of the desired records
     * @param type The type of the desired records
     * @throws IllegalArgumentException The type is a meta type other than ANY.
     * @see #Lookup(Name, int, int)
     */
    public Lookup(Name name, int type) {
        this(name, type, DClass.IN);
    }

    /**
     * Create a Lookup object that will find records of type A at the given name
     * in the IN class.
     *
     * @param name The name of the desired records
     * @see #Lookup(Name, int, int)
     */
    public Lookup(Name name) {
        this(name, Type.A, DClass.IN);
    }

    /**
     * Create a Lookup object that will find records of the given name, type,
     * and class.
     *
     * @param name   The name of the desired records
     * @param type   The type of the desired records
     * @param dclass The class of the desired records
     * @throws TextParseException       The name is not a valid DNS name
     * @throws IllegalArgumentException The type is a meta type other than ANY.
     * @see #Lookup(Name, int, int)
     */
    public Lookup(String name, int type, int dclass) throws TextParseException {
        this(Name.fromString(name), type, dclass);
    }

    /**
     * Create a Lookup object that will find records of the given name and type
     * in the IN class.
     *
     * @param name The name of the desired records
     * @param type The type of the desired records
     * @throws TextParseException       The name is not a valid DNS name
     * @throws IllegalArgumentException The type is a meta type other than ANY.
     * @see #Lookup(Name, int, int)
     */
    public Lookup(String name, int type) throws TextParseException {
        this(Name.fromString(name), type, DClass.IN);
    }

    /**
     * Create a Lookup object that will find records of type A at the given name
     * in the IN class.
     *
     * @param name The name of the desired records
     * @throws TextParseException The name is not a valid DNS name
     * @see #Lookup(Name, int, int)
     */
    public Lookup(String name) throws TextParseException {
        this(Name.fromString(name), Type.A, DClass.IN);
    }

    /**
     * Sets the resolver to use when performing this lookup.  This overrides the
     * default value.
     *
     * @param resolver The resolver to use.
     */
    public void
    setResolver(Resolver resolver) {
        this.resolver = resolver;
    }

    /**
     * Sets the search path to use when performing this lookup.  This overrides the
     * default value.
     *
     * @param domains An array of names containing the search path.
     */
    public void
    setSearchPath(Name[] domains) {
        this.searchPath = domains;
    }

    /**
     * Sets the search path to use when performing this lookup. This overrides the
     * default value.
     *
     * @param domains An array of names containing the search path.
     * @throws TextParseException A name in the array is not a valid DNS name.
     */
    public void
    setSearchPath(String[] domains) throws TextParseException {
        if (domains == null) {
            this.searchPath = null;
            return;
        }
        Name[] newdomains = new Name[domains.length];
        for (int i = 0; i < domains.length; i++)
            newdomains[i] = Name.fromString(domains[i], Name.root);
        this.searchPath = newdomains;
    }

    /**
     * Sets the cache to use when performing this lookup.  This overrides the
     * default value.  If the results of this lookup should not be permanently
     * cached, null can be provided here.
     *
     * @param cache The cache to use.
     */
    public void
    setCache(Cache cache) {
        if (cache == null) {
            this.cache = new Cache(dclass);
            this.temporary_cache = true;
        } else {
            this.cache = cache;
            this.temporary_cache = false;
        }
    }

    /**
     * Sets ndots to use when performing this lookup, overriding the default value.
     * Specifically, this refers to the number of "dots" which, if present in a
     * name, indicate that a lookup for the absolute name should be attempted
     * before appending any search path elements.
     *
     * @param ndots The ndots value to use, which must be greater than or equal to
     *              0.
     */
    public void
    setNdots(int ndots) {
        if (ndots < 0)
            throw new IllegalArgumentException("Illegal ndots value: " +
                    ndots);
        defaultNdots = ndots;
    }

    /**
     * Sets the minimum credibility level that will be accepted when performing
     * the lookup.  This defaults to Credibility.NORMAL.
     *
     * @param credibility The minimum credibility level.
     */
    public void
    setCredibility(int credibility) {
        this.credibility = credibility;
    }

    private void
    follow(Name name, Name oldname) {
        foundAlias = true;
        badresponse = false;
        networkerror = false;
        timedout = false;
        nxdomain = false;
        referral = false;
        iterations++;
        if (iterations >= 6 || name.equals(oldname)) {
            result = UNRECOVERABLE;
            error = "CNAME loop";
            done = true;
            return;
        }
        if (aliases == null)
            aliases = new ArrayList();
        aliases.add(oldname);
        lookup(name);
    }

    private void
    processResponse(Name name, SetResponse response) {
        if (response.isSuccessful()) {
            RRset[] rrsets = response.answers();
            List l = new ArrayList();
            Iterator it;
            int i;

            for (i = 0; i < rrsets.length; i++) {
                it = rrsets[i].rrs();
                while (it.hasNext())
                    l.add(it.next());
            }

            result = SUCCESSFUL;
            answers = (Record[]) l.toArray(new Record[l.size()]);
            done = true;
        } else if (response.isNXDOMAIN()) {
            nxdomain = true;
            doneCurrent = true;
            if (iterations > 0) {
                result = HOST_NOT_FOUND;
                done = true;
            }
        } else if (response.isNXRRSET()) {
            result = TYPE_NOT_FOUND;
            answers = null;
            done = true;
        } else if (response.isCNAME()) {
            CNAMERecord cname = response.getCNAME();
            follow(cname.getTarget(), name);
        } else if (response.isDNAME()) {
            DNAMERecord dname = response.getDNAME();
            try {
                follow(name.fromDNAME(dname), name);
            } catch (NameTooLongException e) {
                result = UNRECOVERABLE;
                error = "Invalid DNAME target";
                done = true;
            }
        } else if (response.isDelegation()) {
            // We shouldn't get a referral.  Ignore it.
            referral = true;
        }
    }

    private void
    lookup(Name current) {
        SetResponse sr = cache.lookupRecords(current, type, credibility);
        if (verbose) {
            System.err.println("lookup " + current + " " +
                    Type.string(type));
            System.err.println(sr);
        }
        processResponse(current, sr);
        if (done || doneCurrent)
            return;

        Record question = Record.newRecord(current, type, dclass);
        Message query = Message.newQuery(question);
        Message response = null;
        try {
            response = resolver.send(query);
        } catch (IOException e) {
            // A network error occurred.  Press on.
            if (e instanceof InterruptedIOException)
                timedout = true;
            else
                networkerror = true;
            return;
        }
        int rcode = response.getHeader().getRcode();
        if (rcode != Rcode.NOERROR && rcode != Rcode.NXDOMAIN) {
            // The server we contacted is broken or otherwise unhelpful.
            // Press on.
            badresponse = true;
            badresponse_error = Rcode.string(rcode);
            return;
        }

        if (!query.getQuestion().equals(response.getQuestion())) {
            // The answer doesn't match the question.  That's not good.
            badresponse = true;
            badresponse_error = "response does not match query";
            return;
        }

        sr = cache.addMessage(response);
        if (sr == null)
            sr = cache.lookupRecords(current, type, credibility);
        if (verbose) {
            System.err.println("queried " + current + " " +
                    Type.string(type));
            System.err.println(sr);
        }
        processResponse(current, sr);
    }

    private void
    resolve(Name current, Name suffix) {
        doneCurrent = false;
        Name tname = null;
        if (suffix == null)
            tname = current;
        else {
            try {
                tname = Name.concatenate(current, suffix);
            } catch (NameTooLongException e) {
                nametoolong = true;
                return;
            }
        }
        lookup(tname);
    }

    /**
     * Performs the lookup, using the specified Cache, Resolver, and search path.
     *
     * @return The answers, or null if none are found.
     */
    public Record[]
    run() {
        if (done)
            reset();
        if (name.isAbsolute())
            resolve(name, null);
        else if (searchPath == null)
            resolve(name, Name.root);
        else {
            if (name.labels() > defaultNdots)
                resolve(name, Name.root);
            if (done)
                return answers;

            for (int i = 0; i < searchPath.length; i++) {
                resolve(name, searchPath[i]);
                if (done)
                    return answers;
                else if (foundAlias)
                    break;
            }
        }
        if (!done) {
            if (badresponse) {
                result = TRY_AGAIN;
                error = badresponse_error;
                done = true;
            } else if (timedout) {
                result = TRY_AGAIN;
                error = "timed out";
                done = true;
            } else if (networkerror) {
                result = TRY_AGAIN;
                error = "network error";
                done = true;
            } else if (nxdomain) {
                result = HOST_NOT_FOUND;
                done = true;
            } else if (referral) {
                result = UNRECOVERABLE;
                error = "referral";
                done = true;
            } else if (nametoolong) {
                result = UNRECOVERABLE;
                error = "name too long";
                done = true;
            }
        }
        return answers;
    }

    private void
    checkDone() {
        if (done && result != -1)
            return;
        StringBuffer sb = new StringBuffer("Lookup of " + name + " ");
        if (dclass != DClass.IN)
            sb.append(DClass.string(dclass) + " ");
        sb.append(Type.string(type) + " isn't done");
        throw new IllegalStateException(sb.toString());
    }

    /**
     * Returns the answers from the lookup.
     *
     * @return The answers, or null if none are found.
     * @throws IllegalStateException The lookup has not completed.
     */
    public Record[]
    getAnswers() {
        checkDone();
        return answers;
    }

    /**
     * Returns all known aliases for this name.  Whenever a CNAME/DNAME is
     * followed, an alias is added to this array.  The last element in this
     * array will be the owner name for records in the answer, if there are any.
     *
     * @return The aliases.
     * @throws IllegalStateException The lookup has not completed.
     */
    public Name[]
    getAliases() {
        checkDone();
        if (aliases == null)
            return noAliases;
        return (Name[]) aliases.toArray(new Name[aliases.size()]);
    }

    /**
     * Returns the result code of the lookup.
     *
     * @return The result code, which can be SUCCESSFUL, UNRECOVERABLE, TRY_AGAIN,
     * HOST_NOT_FOUND, or TYPE_NOT_FOUND.
     * @throws IllegalStateException The lookup has not completed.
     */
    public int
    getResult() {
        checkDone();
        return result;
    }

    /**
     * Returns an error string describing the result code of this lookup.
     *
     * @return A string, which may either directly correspond the result code
     * or be more specific.
     * @throws IllegalStateException The lookup has not completed.
     */
    public String
    getErrorString() {
        checkDone();
        if (error != null)
            return error;
        switch (result) {
            case SUCCESSFUL:
                return "successful";
            case UNRECOVERABLE:
                return "unrecoverable error";
            case TRY_AGAIN:
                return "try again";
            case HOST_NOT_FOUND:
                return "host not found";
            case TYPE_NOT_FOUND:
                return "type not found";
        }
        throw new IllegalStateException("unknown result");
    }

}
