All Downloads are FREE. Search and download functionalities are using the official Maven repository.

uk.num.numlib.api.NumAPIImpl Maven / Gradle / Ivy

/*
 * Copyright (c) 2019. NUM Technology Ltd
 */

package uk.num.numlib.api;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xbill.DNS.ExtendedResolver;
import org.xbill.DNS.Lookup;
import org.xbill.DNS.Record;
import org.xbill.DNS.SimpleResolver;
import uk.num.numlib.exc.*;
import uk.num.numlib.internal.ctx.AppContext;
import uk.num.numlib.internal.ctx.NumAPIContextBase;
import uk.num.numlib.internal.dns.DNSServices;
import uk.num.numlib.internal.dns.DNSServicesDefaultImpl;
import uk.num.numlib.internal.dns.PossibleMultiPartRecordException;
import uk.num.numlib.internal.modl.ModlServices;
import uk.num.numlib.internal.modl.NumLookupRedirect;
import uk.num.numlib.internal.modl.NumQueryRedirect;
import uk.num.numlib.internal.modl.PopulatorResponse;
import uk.num.numlib.internal.module.Module;
import uk.num.numlib.internal.module.ModuleConfig;
import uk.num.numlib.internal.module.ModuleDNSQueries;
import uk.num.numlib.internal.module.ModuleFactory;
import uk.num.numlib.internal.util.EncryptionUtils;
import uk.num.numlib.internal.util.PopulatorRetryConfig;

import java.io.IOException;
import java.net.UnknownHostException;
import java.security.Key;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import static uk.num.numlib.api.NumAPICallbacks.Location.*;

/**
 * This is the main class for using the num-client-library.
 * Use the default constructor to use the DNS servers configured on your local machine,
 * or override these by supplying a specific DNS host domain name using the alternative constructor.
 *
 * @author tonywalmsley
 */
public class NumAPIImpl implements NumAPI {

    private static final Logger LOG = LoggerFactory.getLogger(NumAPIImpl.class);
    private static final int MAX_NUMBER_OF_MULTI_PARTS = 30;
    /**
     * The ApplicationContext to use for this NUM API Session
     */
    private final AppContext appContext = new AppContext();

    private final ModuleFactory moduleFactory = new ModuleFactory();
    /**
     * Services for accessing DNS and processing the resulting records.
     */
    private DNSServices dnsServices;
    /**
     * Services for running the MODL Interpreter
     */
    private ModlServices modlServices;

    /**
     * Supports running DNS queries asynchronously.
     */
    private ExecutorService executor = Executors.newSingleThreadExecutor();

    /**
     * Default constructor to initialise the default DNS services and MODL services.
     */
    public NumAPIImpl() {
        LOG.info("enter - NumAPI()");
        dnsServices = new DNSServicesDefaultImpl();
        modlServices = new ModlServices();
        LOG.info("NumAPI object created.");
    }

    /**
     * Constructor allowing the core services to be overridden, mainly for testing purposes, but could also be useful for other purposes.
     *
     * @param dnsServices  Replacement DNSServices implementation.
     * @param modlServices Replacement ModlServices implementation.
     */
    public NumAPIImpl(final DNSServices dnsServices, final ModlServices modlServices) {
        LOG.info("enter - NumAPI(dnsServices, modlServices)");
        this.dnsServices = dnsServices;
        this.modlServices = modlServices;
        LOG.info("NumAPI object created.");
    }

    /**
     * Alternative constructor used to override the default DNS hosts. Unit tests rely on this constructor.
     *
     * @param dnsHost The DNS host to override the defaults configured for the local machine.
     * @throws NumInvalidDNSHostException on error
     */
    public NumAPIImpl(final String dnsHost) throws NumInvalidDNSHostException {
        this();
        LOG.info("enter - NumAPI({})", dnsHost);
        try {
            if (!StringUtils.isEmpty(dnsHost)) {
                final SimpleResolver resolver = new SimpleResolver(dnsHost);
                Lookup.setDefaultResolver(resolver);
            }
        } catch (UnknownHostException e) {
            LOG.error("UnknownHostException", e);
            throw new NumInvalidDNSHostException("Invalid DNS host.", e);
        }
        LOG.info("NumAPI object created.");
    }

    /**
     * Support multiple DNS hosts.
     *
     * @param dnsHosts The DNS host String arraymto override the defaults configured for the local machine.
     * @throws NumInvalidDNSHostException   on error
     * @throws NumInvalidParameterException on error
     */
    public NumAPIImpl(final String[] dnsHosts) throws NumInvalidDNSHostException, NumInvalidParameterException {
        this();
        LOG.info("enter - NumAPI({})", Arrays.toString(dnsHosts));
        if (dnsHosts == null || dnsHosts.length == 0) {
            LOG.error("No DNS hosts supplied.");
            throw new NumInvalidParameterException("No DNS hosts supplied.");
        }
        try {
            // Use a SimpleResolver if there is only one host
            if (dnsHosts.length == 1) {
                final String dnsHost = dnsHosts[0];
                if (!StringUtils.isEmpty(dnsHost)) {
                    final SimpleResolver resolver = new SimpleResolver(dnsHost);
                    Lookup.setDefaultResolver(resolver);
                } else {
                    LOG.error("Empty hostname in the dnsHosts parameter.");
                    throw new NumInvalidDNSHostException("Empty hostname in the dnsHosts parameter.");
                }
            } else {
                final ExtendedResolver resolver = new ExtendedResolver(dnsHosts);
                Lookup.setDefaultResolver(resolver);
            }
        } catch (UnknownHostException e) {
            LOG.error("UnknownHostException", e);
            throw new NumInvalidDNSHostException("Invalid DNS host.", e);
        }
        LOG.info("NumAPI object created.");
    }

    /**
     * Alternative constructor used to override the default DNS host and port.
     *
     * @param dnsHost The DNS host to override the defaults configured for the local machine.
     * @param port    The port to use on the DNS host.
     * @throws NumInvalidDNSHostException on error
     */
    public NumAPIImpl(final String dnsHost, final int port) throws NumInvalidDNSHostException {
        this();
        LOG.info("NumAPI({}, {})", dnsHost, port);
        try {
            if (!StringUtils.isEmpty(dnsHost)) {
                final SimpleResolver resolver = new SimpleResolver(dnsHost);
                resolver.setPort(port);
                Lookup.setDefaultResolver(resolver);
                setTCPOnly(true);
            }
        } catch (UnknownHostException e) {
            LOG.error("UnknownHostException", e);
            throw new NumInvalidDNSHostException("Invalid DNS host.", e);
        }
        LOG.info("NumAPI object created.");
    }

    /**
     * Tell dnsjava to use TCP and not UDP.
     *
     * @param flag true to use TCP only.
     */
    @Override
    public void setTCPOnly(final boolean flag) {
        LOG.info("Use TCP only : {}", flag);
        Lookup.getDefaultResolver()
                .setTCP(flag);
    }

    /**
     * Override the top-level zone from 'num.uk' to 'myzone.com' for example.
     *
     * @param zone The top level zone to use for DNS lookups. Replaces the default of 'num.uk'
     * @throws NumInvalidParameterException if the zone is null or empty
     */
    @Override
    public void setTopLevelZone(final String zone) throws NumInvalidParameterException {
        LOG.info("setTopLevelZone({})", zone);
        if (StringUtils.isEmpty(zone)) {
            throw new NumInvalidParameterException("zone cannot be null or empty");
        }
        appContext.stringConstants.setTopLevelZone(zone);
    }

    /**
     * Initialise a new NumAPIContextBase object for a specific module/domain combination.
     * The returned context object can be used to obtain the list of required user variables that must be set
     * before moving on to retrieveNumRecord().
     *
     * @param moduleId      E.g. "1" for the Contacts module.
     * @param netString     a domain name, URL, or email address that identifies the location in DNS of a NUM record.
     * @param timeoutMillis the timeout in milliseconds to wait for responses from DNS.
     * @return a new NumAPIContextBase object.
     * @throws NumBadModuleIdException         on error
     * @throws NumBadModuleConfigDataException on error
     * @throws NumBadURLException              on error
     * @throws NumInvalidParameterException    on error
     * @throws NumBadRecordException           on error
     * @throws NumDNSQueryException            on error
     * @throws NumInvalidDNSQueryException     on error
     */
    @Override
    public NumAPIContext begin(final String moduleId, final String netString, final int timeoutMillis) throws
                                                                                                       NumBadModuleIdException,
                                                                                                       NumBadModuleConfigDataException,
                                                                                                       NumBadURLException,
                                                                                                       NumInvalidParameterException,
                                                                                                       NumBadRecordException,
                                                                                                       NumDNSQueryException,
                                                                                                       NumInvalidDNSQueryException {
        LOG.info("enter - begin({}, {}, {})", moduleId, netString, timeoutMillis);
        assert moduleId != null && moduleId.trim()
                .length() > 0;
        assert netString != null && netString.trim()
                .length() > 0;
        assert timeoutMillis > 0;

        // Create the context object and the validated ModuleDNSQueries object.
        final NumAPIContextBase ctx = new NumAPIContextBase();

        final ModuleDNSQueries moduleDNSQueries = moduleFactory.getInstance(appContext, moduleId, netString);
        ctx.setModuleDNSQueries(moduleDNSQueries);

        // Get the moduleDNSQueries config data
        final Record[] records = dnsServices.getConfigFileTXTRecords(appContext, moduleId, timeoutMillis);

        if (records == null || records.length == 0) {
            LOG.error("No configuration moduleDNSQueries file available. Check that the moduleDNSQueries ID is correct.: {}", moduleId);
            throw new NumBadModuleIdException("No configuration moduleDNSQueries file available. Check that the moduleDNSQueries ID is correct.: " + moduleId);
        }

        final String configTxt = dnsServices.rebuildTXTRecordContent(records)
                .replaceAll("\\\\", "");

        final ModuleConfig moduleConfig = modlServices.interpretModuleConfig(configTxt);
        if (!moduleConfig.isValid()) {
            LOG.error("The module config file is invalid. {}", configTxt);
            throw new NumBadModuleConfigDataException("Invalid module config data: " + configTxt);
        }
        ctx.setModuleConfig(moduleConfig);

        LOG.info("Module configuration: {}", moduleConfig);
        LOG.info("exit - begin()");
        return ctx;
    }

    /**
     * This method uses the module context and the supplied Required User Variable values to obtain a fully expanded
     * JSON object from DNS. The supplied handler will be notified when the results are available or an error occurs.
     *
     * @param ctx           The context object returned by the begin() method.
     * @param handler       a handler object to receive the JSON results or processing errors.
     * @param timeoutMillis the maximum duration of each DNS request, the total wait time could be up to 4 times this value.
     * @return A Future object
     */
    @Override
    public Future retrieveNumRecord(final NumAPIContext ctx, final NumAPICallbacks handler, final int timeoutMillis) {
        LOG.info("retrieveNumRecord()");
        assert ctx != null;
        assert handler != null;

        // 1. Re-interpret the module config now that we have the user variables.
        // 2. Get the NUM record from DNS (either independent, managed, prepopulated, or populator)
        // 2. Prepend the RCF to the NUM record from DNS as a *LOAD entry (i.e. the module URL)
        // 3. Run the resulting record through the Java MODL Interpreter and make the results available to the client via the handler.

        // Do the rest of the operation asynchronously.
        // This submits a Callable object, so exceptions should be reported to the user when they call the get() method on the Future object.
        LOG.info("Submitting background query.");
        final Future future = executor.submit(() -> {
            final String result = numLookup(ctx, handler, timeoutMillis);
            if (result == null) {
                LOG.error("Unable to retrieve a NUM record.");
                handler.setLocation(null);
                ctx.setLocation(null);
                return null;
            } else {
                handler.setResult(result);
                return result;
            }
        });
        LOG.info("Background query running.");
        return future;
    }

    /**
     * Main lookup method with fairly complex state behaviour to handle the various lookup locations and retry scenarios.
     *
     * @param ctx           the NumAPIContext
     * @param handler       the NumAPICallbacks
     * @param timeoutMillis the timeoutMillis
     * @return a NUM record String
     * @throws NumBadRecordException                      on error
     * @throws NumRecordEncryptionRequiredException       on error
     * @throws NumNoDecryptionKeyException                on error
     * @throws NumUnsupportedEncryptionAlgorithmException on error
     * @throws NumDecryptionException                     on error
     * @throws NumBadURLException                         on error
     * @throws NumInvalidRedirectException                on error
     * @throws NumInvalidParameterException               on error
     * @throws NumNotImplementedException                 on error
     * @throws NumBadMultipartRecordException             on error
     * @throws NumInvalidDNSQueryException                on error
     * @throws NumMaximumRedirectsExceededException       on error
     * @throws NumNoRecordAvailableException              on error
     * @throws NumPopulatorErrorException                 on error
     * @throws NumInvalidPopulatorResponseCodeException   on error
     */
    private String numLookup(final NumAPIContext ctx, final NumAPICallbacks handler, final int timeoutMillis) throws
                                                                                                              NumBadRecordException,
                                                                                                              NumRecordEncryptionRequiredException,
                                                                                                              NumNoDecryptionKeyException,
                                                                                                              NumUnsupportedEncryptionAlgorithmException,
                                                                                                              NumDecryptionException,
                                                                                                              NumBadURLException,
                                                                                                              NumInvalidRedirectException,
                                                                                                              NumInvalidParameterException,
                                                                                                              NumNotImplementedException,
                                                                                                              NumBadMultipartRecordException,
                                                                                                              NumInvalidDNSQueryException,
                                                                                                              NumMaximumRedirectsExceededException,
                                                                                                              NumNoRecordAvailableException,
                                                                                                              NumPopulatorErrorException,
                                                                                                              NumInvalidPopulatorResponseCodeException {
        final NumAPIContextBase context = (NumAPIContextBase) ctx;
        context.setLocation(INDEPENDENT);
        LOG.info("Trying the INDEPENDENT location.");
        final ModuleConfig moduleConfig = context.getModuleConfig();
        do {
            try {
                // Attempt to get the record from DNS
                String numRecord = getNumRecord(timeoutMillis, context);

                // If that failed then try the managed record.
                final Module module = moduleConfig.getModule();
                if (numRecord == null) {
                    LOG.info("Lookup returned no result.");
                    final boolean rootQuery = context.getModuleDNSQueries()
                            .isRootQuery();
                    switch (context.getLocation()) {
                        case INDEPENDENT:
                            LOG.info("Trying the MANAGED location.");
                            context.setLocation(MANAGED);
                            break;
                        case MANAGED:
                            if (rootQuery) {
                                if (module.isRprq()) {
                                    LOG.info("Trying the POPULATED location.");
                                    context.setLocation(POPULATED);
                                } else {
                                    LOG.info("Not configured to use the POPULATED location.");
                                    context.setLocation(STOP);
                                }
                            } else {
                                // Assume its a branch query
                                if (module.isBprq()) {
                                    LOG.info("Trying the POPULATED location.");
                                    context.setLocation(POPULATED);
                                } else {
                                    LOG.info("Not configured to use the POPULATED location.");
                                    context.setLocation(STOP);
                                }
                            }
                            break;
                        case POPULATED:
                            if (rootQuery) {
                                if (module.isRpsq()) {
                                    LOG.info("Trying the POPULATOR.");
                                    context.setLocation(POPULATOR);
                                } else {
                                    LOG.info("Not configured to use the POPULATOR.");
                                    context.setLocation(STOP);
                                }
                            } else {
                                // Assume its a branch query
                                if (module.isBpsq()) {
                                    LOG.info("Trying the POPULATOR.");
                                    context.setLocation(POPULATOR);
                                } else {
                                    LOG.info("Not configured to use the POPULATOR.");
                                    context.setLocation(STOP);
                                }
                            }
                            // fall through to the POPULATOR
                        case POPULATOR:
                            LOG.info("Trying the POPULATOR.");
                            final String fromPopulator = getNumRecordFromPopulator(timeoutMillis, context);
                            String json = interpretNumRecord(fromPopulator, context);
                            if (json != null) {
                                checkForEncryption(json, handler, context);
                            }
                            return handler.getResult();
                        case ENCRYPTED:
                            LOG.info("Processing an ENCRYPTED record.");
                            processDecryption(handler.getResult(), handler, context);
                            return handler.getResult();
                        case STOP:
                        default:
                            return null;
                    }
                } else {
                    String json = interpretNumRecord(numRecord, context);
                    checkForEncryption(json, handler, context);
                    return handler.getResult();
                }
            } catch (final NumLookupRedirect numLookupRedirect) {
                context.setLocation(INDEPENDENT);
                context.handleQueryRedirect(appContext, numLookupRedirect.getRedirect(), context);
            } catch (final NumQueryRedirect numQueryRedirect) {
                context.handleQueryRedirect(appContext, numQueryRedirect.getRedirect(), context);
            }
        } while (true);
    }

    /**
     * Check for and handle encrypted records. TODO: Not fully implemented in this version.
     *
     * @param jsonResult the interpreted and possibly encrypted JSON String
     * @param handler    the NumAPICallbacks
     * @param context    the NumAPIContextBase
     * @throws NumRecordEncryptionRequiredException       on error
     * @throws NumUnsupportedEncryptionAlgorithmException on error
     * @throws NumNoDecryptionKeyException                on error
     */
    private void checkForEncryption(final String jsonResult, final NumAPICallbacks handler, final NumAPIContextBase context) throws
                                                                                                                             NumRecordEncryptionRequiredException,
                                                                                                                             NumUnsupportedEncryptionAlgorithmException,
                                                                                                                             NumNoDecryptionKeyException {
        if (jsonResult.contains("\"e_\"")) {
            handler.setEncrypted(true);// record is currently encrypted - this will change if we can decrypt it.
            handler.setWasEncrypted(true);// record was encrypted when received - this will not change.

            context.setLocation(ENCRYPTED);
            // Parse the JSON and extract the encrypted record.
            try {
                final ObjectMapper mapper = new ObjectMapper();
                final JsonNode node = mapper.readTree(jsonResult);

                // ---------------------------------
                // Check for an encryption algorithm
                // ---------------------------------
                final JsonNode a_ = node.findValue("a_");
                String algorithm;
                if (a_ == null || a_.asText()
                        .trim()
                        .length() == 0) {
                    // No algorithm specified so check for a default.
                    algorithm = context.getModuleConfig()
                            .getModule()
                            .getDea();
                    if (StringUtils.isEmpty(algorithm)) {
                        LOG.info("No decryption algorithm specified in the NUM record and no default algorithm in the module configuration. Defaulting to AES.");
                        algorithm = "AES";
                    }
                } else {
                    algorithm = a_.asText()
                            .trim();
                }
                // -------------------------------
                // Save the full Base64 encoded MODL record.
                // -------------------------------
                handler.setEncryptionAlgorithm(algorithm);
                handler.setResult(jsonResult);

                // -------------------------------------------------
                // Check whether we support the encryption algorithm
                // -------------------------------------------------
                if (!EncryptionUtils.isSupported(algorithm)) {
                    throw new NumUnsupportedEncryptionAlgorithmException(algorithm);
                }

                final Key key = handler.getKey();
                if (key == null) {
                    throw new NumNoDecryptionKeyException("A decryption key is required for " + algorithm);
                }
            } catch (final IOException e) {
                // This shouldn't happen since we generated the JSON string, so just log it rather than throw an exception
                LOG.error("Error parsing JSON String.", e);
            }
        } else {
            handler.setEncrypted(false);
            handler.setWasEncrypted(false);
            if (context.getModuleConfig()
                    .getModule()
                    .isRer()) {
                LOG.error("Encryption is required but the record was not encrypted.");
                throw new NumRecordEncryptionRequiredException("Encryption is required but the record was not encrypted.");
            }
            handler.setResult(jsonResult);
        }
    }

    /**
     * Decrypt an encrypted record if a key is available.
     *
     * @param base64value the base64 encoded and encrypted String.
     * @param handler     the NumAPICallbacks
     * @param context     the NumAPIContextBase
     * @throws NumDecryptionException      on error
     * @throws NumNoDecryptionKeyException on error
     * @throws NumBadRecordException       on error
     * @throws NumLookupRedirect           on error
     * @throws NumQueryRedirect            on error
     */
    @Override
    public void processDecryption(final String base64value, final NumAPICallbacks handler, final NumAPIContextBase context) throws
                                                                                                                            NumDecryptionException,
                                                                                                                            NumNoDecryptionKeyException,
                                                                                                                            NumBadRecordException,
                                                                                                                            NumLookupRedirect,
                                                                                                                            NumQueryRedirect {

        final String algorithm = handler.getEncryptionAlgorithm();
        final byte[] raw = Base64.getDecoder()
                .decode(base64value);
        final Key key = handler.getKey();
        if (key == null) {
            throw new NumNoDecryptionKeyException("A decryption key is required for " + algorithm);
        }

        final String decrypted = EncryptionUtils.decrypt(raw, algorithm, key);
        handler.setEncrypted(false);// We managed to decrypt the record.
        interpretNumRecord(decrypted, context);
        final String interpretedResult = modlServices.interpretNumRecord(decrypted);
        handler.setResult(interpretedResult);
    }

    /**
     * We had a response with the Truncated Flag set.
     *
     * @param timeoutMillis  The number of milliseconds we're prepared to wait per DNS request.
     * @param context        The NumAPIContextBase
     * @param recordLocation The DNS query String.
     * @return An array of Record objects
     * @throws NumBadMultipartRecordException on error
     * @throws NumInvalidDNSQueryException    on error
     * @throws NumNotImplementedException     on error
     * @throws NumNoRecordAvailableException if a CNAME or SPF record is received instead of a TXT record
     */
    private Record[] getMultiPartRecords(final int timeoutMillis, final NumAPIContextBase context, final String recordLocation) throws
                                                                                                                                NumBadMultipartRecordException,
                                                                                                                                NumInvalidDNSQueryException,
                                                                                                                                NumNotImplementedException,
                                                                                                                                NumNoRecordAvailableException {
        LOG.info("getMultiPartRecords(recordLocation={})", recordLocation);
        Record[] recordFromDns;// First get the number of parts and check it is valid.
        final Record[] numberOfPartsRecord = dnsServices.getRecordFromDnsNoCache("0." + recordLocation, timeoutMillis, context.getModuleConfig()
                .getModule()
                .isDsr());
        if (numberOfPartsRecord == null || numberOfPartsRecord.length == 0) {
            return null;
        }
        final String numberOfPartsStr = dnsServices.rebuildTXTRecordContent(numberOfPartsRecord);
        if (!numberOfPartsStr.startsWith("parts=")) {
            throw new NumBadMultipartRecordException("Invalid record 0 for multi-part record: " + numberOfPartsStr);
        }
        final int numberOfParts = Integer.parseInt(numberOfPartsStr.substring(6));
        if (numberOfParts > MAX_NUMBER_OF_MULTI_PARTS) {
            throw new NumBadMultipartRecordException("Too many parts for a multi-part record: " + numberOfPartsStr);
        }

        // Now get each part and add them all to a list.
        final List parts = new ArrayList<>();
        for (int i = 1; i <= numberOfParts; i++) {
            final Record[] partNRecords = dnsServices.getRecordFromDnsNoCache("" + i + "." + recordLocation, timeoutMillis, context.getModuleConfig()
                    .getModule()
                    .isDsr());
            if (partNRecords != null && partNRecords.length > 0) {
                parts.addAll(Arrays.asList(partNRecords));
            }
        }
        recordFromDns = parts.toArray(new Record[]{});
        return recordFromDns;
    }

    /**
     * Try retrieving a record from the populator
     *
     * @param timeoutMillis The timeout
     * @param context       The context obtained from the NumAPI.begin() method
     * @return The String result or null
     * @throws NumPopulatorErrorException               on error
     * @throws NumNoRecordAvailableException            on error
     * @throws NumInvalidPopulatorResponseCodeException on error
     * @throws NumBadMultipartRecordException           on error
     * @throws NumBadRecordException                    on error
     * @throws NumNotImplementedException               on error
     * @throws NumInvalidDNSQueryException              on error
     */
    private String getNumRecordFromPopulator(int timeoutMillis, final NumAPIContextBase context) throws
                                                                                                 NumPopulatorErrorException,
                                                                                                 NumNoRecordAvailableException,
                                                                                                 NumInvalidPopulatorResponseCodeException,
                                                                                                 NumBadMultipartRecordException,
                                                                                                 NumBadRecordException,
                                                                                                 NumNotImplementedException,
                                                                                                 NumInvalidDNSQueryException {
        LOG.info("getNumRecordFromPopulator()");
        final String recordLocation = context.getModuleDNSQueries()
                .getPopulatorLocation();
        LOG.info("Querying the populator service: {}", recordLocation);

        String numRecord = null;
        while (numRecord == null) {
            numRecord = getNumRecordNoCache(timeoutMillis, context, recordLocation);
            if (numRecord == null) {
                // This is unrecoverable, we should get status_ or error_ object.
                break;
            }

            LOG.info("Response from Populator: {}.", numRecord);
            // Parse the MODL response
            final PopulatorResponse response = modlServices.interpretPopulatorResponse(numRecord);
            if (!response.isValid()) {
                throw new NumInvalidPopulatorResponseCodeException("Bad response received from the populator service.");
            }
            // Handle the status_ response codes
            if (response.getStatus_() != null) {
                numRecord = handlePopulatorStatusCodes(timeoutMillis, context, response);
            }
            // Handle the error_ response codes
            if (response.getError_() != null) {
                if (response.getError_()
                        .getCode() == 100) {// Enter the populated zone retry loop
                    LOG.error("NUM Populator error: {}, {}", response.getError_()
                            .getCode(), response.getError_()
                            .getDescription());
                    try {
                        int i = 0;
                        while (i < PopulatorRetryConfig.ERROR_RETRIES) {
                            LOG.info("Sleeping for {} seconds.", PopulatorRetryConfig.ERROR_RETRY_DELAYS[i]);
                            TimeUnit.SECONDS.sleep(PopulatorRetryConfig.ERROR_RETRY_DELAYS[i]);
                            LOG.info("Retrying...");
                            numRecord = getNumRecord(timeoutMillis, context);

                            final PopulatorResponse retryResponse = modlServices.interpretPopulatorResponse(numRecord);
                            if (retryResponse.getStatus_() != null) {
                                return handlePopulatorStatusCodes(timeoutMillis, context, retryResponse);
                            }
                            i++;
                        }
                    } catch (final InterruptedException e) {
                        LOG.error("Interrupted", e);
                    }
                    LOG.error("Cannot retrieve NUM record from any location.");
                    throw new NumNoRecordAvailableException("Cannot retrieve NUM record from any location.");
                } else {
                    LOG.error("NUM Populator error: {}, {}", response.getError_()
                            .getCode(), response.getError_()
                            .getDescription());
                    throw new NumPopulatorErrorException(response.getError_()
                            .getDescription());
                }
            }
        }
        return numRecord;
    }

    /**
     * Populator status codes tell us how to retry the queries while the populator works in the background to get the necessary data
     *
     * @param timeoutMillis the timeout
     * @param context       the NumAPIContextBase object.
     * @param response      the response from the populator
     * @return null or a valid NUM record
     * @throws NumNoRecordAvailableException            on error
     * @throws NumInvalidPopulatorResponseCodeException on error
     * @throws NumBadMultipartRecordException           on error
     * @throws NumInvalidDNSQueryException              on error
     * @throws NumNotImplementedException               on error
     */
    private String handlePopulatorStatusCodes(int timeoutMillis, NumAPIContextBase context, PopulatorResponse response) throws
                                                                                                                        NumNoRecordAvailableException,
                                                                                                                        NumInvalidPopulatorResponseCodeException,
                                                                                                                        NumBadMultipartRecordException,
                                                                                                                        NumInvalidDNSQueryException,
                                                                                                                        NumNotImplementedException {
        LOG.info("handlePopulatorStatusCodes()");
        String numRecord = null;
        switch (response.getStatus_()
                .getCode()) {
            case 1:
                LOG.info("Populator Status code: 1");
                // Enter the populated zone retry loop
                try {
                    //
                    // In a change to the specification, we're going to retry the POPULATOR rather than the POPULATED
                    // zone because that will be the first to respond when a scraper completes.
                    //
                    context.setLocation(POPULATOR);
                    int i = 0;
                    while (i < PopulatorRetryConfig.RETRY_DELAYS.length) {
                        LOG.info("Sleeping for {} seconds.", PopulatorRetryConfig.RETRY_DELAYS[i]);
                        TimeUnit.SECONDS.sleep(PopulatorRetryConfig.RETRY_DELAYS[i]);
                        LOG.info("Retrying...");
                        numRecord = getNumRecord(timeoutMillis, context);
                        if (numRecord != null && !numRecord.contains("status_") && !numRecord.contains("error_")) {
                            return numRecord;
                        }
                        i++;
                    }
                    LOG.error("Cannot retrieve NUM record from any location.");
                    throw new NumNoRecordAvailableException("Cannot retrieve NUM record from any location.");
                } catch (InterruptedException e) {
                    break;
                }
            case 2:
                LOG.info("Populator Status code: 2");
                // The record is available at the authoritative server
                context.setLocation(INDEPENDENT);
                numRecord = getNumRecord(timeoutMillis, context);
                if (numRecord == null) {
                    LOG.error("Cannot retrieve NUM record from any location.");
                    throw new NumNoRecordAvailableException("Cannot retrieve NUM record from any location.");
                }
                break;
            case 3:
                LOG.info("Populator Status code: 3");
                // The record exists in the managed zone.
                context.setLocation(MANAGED);
                numRecord = getNumRecord(timeoutMillis, context);
                if (numRecord == null) {
                    LOG.error("Cannot retrieve NUM record from any location.");
                    throw new NumNoRecordAvailableException("Cannot retrieve NUM record from any location.");
                }
                break;
            case PopulatorResponse.VALID_TXT_RECORD_CODE:
                numRecord = response.getNumRecord();
                break;
            default:
                context.setLocation(null);
                LOG.error("Invalid response code from DNS populator service: {}", response.getStatus_()
                        .getCode());
                throw new NumInvalidPopulatorResponseCodeException("Invalid response code from DNS populator service: " + response.getStatus_()
                        .getCode());
        }
        return numRecord;
    }

    /**
     * Interpret the supplied NUM record from DNS using the RCF value from the ModuleConfig object.
     *
     * @param moduleNumber The module number
     * @param moduleConfig The ModuleConfig
     * @param numRecord    The NUM record from DNS
     * @return The JSON String result of the fully expanded NUM record.
     * @throws NumBadRecordException on error
     * @throws NumQueryRedirect      on error
     * @throws NumLookupRedirect     on error
     */
    private String getInterpretedNumRecordAsJson(final String moduleNumber, final ModuleConfig moduleConfig, final String numRecord) throws
                                                                                                                                     NumBadRecordException,
                                                                                                                                     NumQueryRedirect,
                                                                                                                                     NumLookupRedirect {
        LOG.info("getInterpretedNumRecordAsJson({}, moduleConfig, {})", moduleNumber, numRecord);
        final StringBuilder numRecordBuffer = new StringBuilder();

        final RequiredUserVariable[] ruv = moduleConfig.getModule()
                .getRuv();
        if (ruv != null) {
            for (RequiredUserVariable v : ruv) {
                numRecordBuffer.append(v.getKey());
                numRecordBuffer.append("=");
                numRecordBuffer.append(v.getValue());
                numRecordBuffer.append(";");
            }
        }
        numRecordBuffer.append("*load=\"http://modules.num.uk/");
        numRecordBuffer.append(moduleNumber);
        numRecordBuffer.append("/rcf.txt!\";");
        numRecordBuffer.append(numRecord);

        LOG.info("Interpret NUM record: {}", numRecordBuffer.toString());
        return modlServices.interpretNumRecord(numRecordBuffer.toString());
    }

    /**
     * Convert a NUM record String to an interpreted JSON String. Handle any redirect instructions in the interpreted MODL record
     *
     * @param numRecord the uninterpreted NUM record.
     * @param context   the NumAPIContext
     * @return the interpreted NUM record as a JSON string.
     * @throws NumLookupRedirect     on error
     * @throws NumBadRecordException on error
     * @throws NumQueryRedirect      on error
     */
    private String interpretNumRecord(final String numRecord, final NumAPIContextBase context) throws NumLookupRedirect,
                                                                                                      NumBadRecordException,
                                                                                                      NumQueryRedirect {
        LOG.info("interpretNumRecord({}, context)", numRecord);
        String json = null;
        if (numRecord != null && numRecord.trim()
                .length() > 0) {
            // Build a MODL object using the required user variables, the RCF, and the NUM record from DNS.
            json = getInterpretedNumRecordAsJson(context.getModuleDNSQueries()
                    .getModuleId(), context.getModuleConfig(), numRecord);
        }
        return json;
    }

    /**
     * Get a NUM record for the given query string. Try multi-part queries if necessary.
     *
     * @param timeoutMillis The timeout
     * @param context       The context obtained from the NumAPI.begin() method
     * @return The raw NUM record from DNS.
     * @throws NumBadMultipartRecordException on error
     * @throws NumInvalidDNSQueryException    on error
     * @throws NumNotImplementedException     on error
     * @throws NumNoRecordAvailableException if a CNAME or SPF record is received instead of a TXT record
     */
    private String getNumRecord(int timeoutMillis, NumAPIContextBase context) throws
                                                                              NumBadMultipartRecordException,
                                                                              NumInvalidDNSQueryException,
                                                                              NumNotImplementedException,
                                                                              NumNoRecordAvailableException {
        final String recordLocation = context.getRecordLocation();
        LOG.info("getNumRecord({}, context, {})", timeoutMillis, recordLocation);
        Record[] recordFromDns;
        try {
            recordFromDns = dnsServices.getRecordFromDnsNoCache(recordLocation, timeoutMillis, context.getModuleConfig()
                    .getModule()
                    .isDsr());
        } catch (final PossibleMultiPartRecordException e) {
            LOG.info("Possible multi-part record - checking.");
            recordFromDns = getMultiPartRecords(timeoutMillis, context, recordLocation);
        }
        if (recordFromDns == null || recordFromDns.length == 0) {
            return null;
        }
        return dnsServices.rebuildTXTRecordContent(recordFromDns);
    }

    /**
     * Get a NUM record for the given query string. Try multi-part queries if necessary. Don't cache the responses
     *
     * @param timeoutMillis  The timeout
     * @param context        The context obtained from the NumAPI.begin() method
     * @param recordLocation The DNS query String.
     * @return The raw NUM record from DNS.
     * @throws NumBadMultipartRecordException on error
     * @throws NumInvalidDNSQueryException    on error
     * @throws NumNotImplementedException     on error
     * @throws NumNoRecordAvailableException if a CNAME or SPF record is received instead of a TXT record
     */
    private String getNumRecordNoCache(int timeoutMillis, NumAPIContextBase context, String recordLocation) throws
                                                                                                            NumBadMultipartRecordException,
                                                                                                            NumInvalidDNSQueryException,
                                                                                                            NumNotImplementedException,
                                                                                                            NumNoRecordAvailableException {
        LOG.info("getNumRecordNoCache({}, context, {})", timeoutMillis, recordLocation);
        Record[] recordFromDns;
        try {
            recordFromDns = dnsServices.getRecordFromDnsNoCache(recordLocation, timeoutMillis, context.getModuleConfig()
                    .getModule()
                    .isDsr());
        } catch (final PossibleMultiPartRecordException e) {
            LOG.info("Possible multi-part record - checking.");
            recordFromDns = getMultiPartRecords(timeoutMillis, context, recordLocation);
        }
        if (recordFromDns == null || recordFromDns.length == 0) {
            return null;
        }
        return dnsServices.rebuildTXTRecordContent(recordFromDns);
    }

    /**
     * Stop any outstanding DNS queries still in the Executor.
     */
    @Override
    public void shutdown() {
        LOG.info("shutdown()");
        try {
            executor.shutdown();
            executor.awaitTermination(1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            LOG.error("Shutdown interrupted: ", e);
        } finally {
            if (!executor.isTerminated()) {
                LOG.info("Failed to shutdown after 1 second, so forcing shutdown.");
                executor.shutdownNow();
            }
        }
        LOG.info("Shutdown complete.");
    }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy