org.codice.alliance.nsili.source.NsiliSource.java Source code

Java tutorial

Introduction

Here is the source code for org.codice.alliance.nsili.source.NsiliSource.java

Source

/**
 * Copyright (c) Codice Foundation
 * <p>
 * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
 * General Public License as published by the Free Software Foundation, either version 3 of the
 * License, or any later version.
 * <p>
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details. A copy of the GNU Lesser General Public License
 * is distributed along with this program and can be found at
 * <http://www.gnu.org/licenses/lgpl.html>.
 */
package org.codice.alliance.nsili.source;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.cxf.helpers.IOUtils;
import org.codice.alliance.nsili.common.GIAS.AccessCriteria;
import org.codice.alliance.nsili.common.GIAS.AttributeInformation;
import org.codice.alliance.nsili.common.GIAS.CatalogMgr;
import org.codice.alliance.nsili.common.GIAS.CatalogMgrHelper;
import org.codice.alliance.nsili.common.GIAS.DataModelMgr;
import org.codice.alliance.nsili.common.GIAS.DataModelMgrHelper;
import org.codice.alliance.nsili.common.GIAS.HitCountRequest;
import org.codice.alliance.nsili.common.GIAS.Library;
import org.codice.alliance.nsili.common.GIAS.LibraryDescription;
import org.codice.alliance.nsili.common.GIAS.LibraryHelper;
import org.codice.alliance.nsili.common.GIAS.LibraryManager;
import org.codice.alliance.nsili.common.GIAS.OrderMgr;
import org.codice.alliance.nsili.common.GIAS.OrderMgrHelper;
import org.codice.alliance.nsili.common.GIAS.Polarity;
import org.codice.alliance.nsili.common.GIAS.ProductMgr;
import org.codice.alliance.nsili.common.GIAS.ProductMgrHelper;
import org.codice.alliance.nsili.common.GIAS.RequirementMode;
import org.codice.alliance.nsili.common.GIAS.SortAttribute;
import org.codice.alliance.nsili.common.GIAS.SubmitQueryRequest;
import org.codice.alliance.nsili.common.GIAS.View;
import org.codice.alliance.nsili.common.NsilCorbaExceptionUtil;
import org.codice.alliance.nsili.common.Nsili;
import org.codice.alliance.nsili.common.NsiliConstants;
import org.codice.alliance.nsili.common.UCO.DAG;
import org.codice.alliance.nsili.common.UCO.DAGListHolder;
import org.codice.alliance.nsili.common.UCO.InvalidInputParameter;
import org.codice.alliance.nsili.common.UCO.NameValue;
import org.codice.alliance.nsili.common.UCO.ProcessingFault;
import org.codice.alliance.nsili.common.UCO.SystemFault;
import org.codice.alliance.nsili.orb.api.CorbaOrb;
import org.codice.alliance.nsili.orb.api.CorbaServiceListener;
import org.codice.alliance.nsili.transformer.DAGConverter;
import org.codice.ddf.cxf.SecureCxfClientFactory;
import org.codice.ddf.spatial.ogc.catalog.common.AvailabilityCommand;
import org.codice.ddf.spatial.ogc.catalog.common.AvailabilityTask;
import org.omg.CORBA.Any;
import org.omg.CORBA.IntHolder;
import org.omg.CORBA.ORB;
import org.opengis.filter.sort.SortBy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ddf.catalog.data.ContentType;
import ddf.catalog.data.Metacard;
import ddf.catalog.data.Result;
import ddf.catalog.data.impl.ResultImpl;
import ddf.catalog.filter.FilterAdapter;
import ddf.catalog.operation.Query;
import ddf.catalog.operation.QueryRequest;
import ddf.catalog.operation.ResourceResponse;
import ddf.catalog.operation.SourceResponse;
import ddf.catalog.operation.impl.SourceResponseImpl;
import ddf.catalog.resource.ResourceNotFoundException;
import ddf.catalog.resource.ResourceNotSupportedException;
import ddf.catalog.resource.ResourceReader;
import ddf.catalog.service.ConfiguredService;
import ddf.catalog.source.ConnectedSource;
import ddf.catalog.source.FederatedSource;
import ddf.catalog.source.SourceMonitor;
import ddf.catalog.source.UnsupportedQueryException;
import ddf.catalog.util.impl.MaskableImpl;

public class NsiliSource extends MaskableImpl
        implements FederatedSource, ConnectedSource, ConfiguredService, CorbaServiceListener {

    public static final String SERVER_PASSWORD = "serverPassword";

    public static final String SERVER_USERNAME = "serverUsername";

    public static final String CLIENT_TIMEOUT = "clientTimeout";

    public static final String ID = "id";

    public static final String KEY = "key";

    public static final String IOR_URL = "iorUrl";

    public static final String ADDITIONAL_QUERY_PARAMS = "additionalQueryParams";

    public static final String POLL_INTERVAL = "pollInterval";

    public static final String MAX_HIT_COUNT = "maxHitCount";

    public static final String ACCESS_USERID = "accessUserId";

    public static final String ACCESS_PASSWORD = "accessPassword";

    public static final String ACCESS_LICENSE_KEY = "accessLicenseKey";

    private static final Logger LOGGER = LoggerFactory.getLogger(NsiliSource.class);

    private static final String DESCRIBABLE_PROPERTIES_FILE = "/describable.properties";

    private static final String DESCRIPTION = "description";

    private static final String ORGANIZATION = "organization";

    private static final String VERSION = "version";

    private static final String TITLE = "name";

    private static final String WGS84 = "WGS84";

    private static final String GEOGRAPHIC_DATUM = "GeographicDatum";

    private static final String ASC = "ASC";

    private static final String CATALOG_MGR = "CatalogMgr";

    private static final String ORDER_MGR = "OrderMgr";

    private static final String PRODUCT_MGR = "ProductMgr";

    private static final String DATA_MODEL_MGR = "DataModelMgr";

    private static final String DEFAULT_USER_INFO = "Alliance";

    private static final String HTTP_SCHEME = "http";

    private static final String HTTPS_SCHEME = "https";

    private static final String FILE_SCHEME = "file";

    private static final String FTP_SCHEME = "ftp";

    private static Library library;

    private static Properties describableProperties = new Properties();

    /* Mandatory STANAG 4559 Managers */

    private CatalogMgr catalogMgr;

    private OrderMgr orderMgr;

    private ProductMgr productMgr;

    private DataModelMgr dataModelMgr;

    /* ---------------------------  */

    private AvailabilityTask availabilityTask;

    private String iorUrl;

    private String serverUsername;

    private String serverPassword;

    private int clientTimeout;

    private String id;

    private String iorString;

    private Integer maxHitCount;

    private FilterAdapter filterAdapter;

    private org.omg.CORBA.ORB orb;

    private AccessCriteria accessCriteria;

    private Set<SourceMonitor> sourceMonitors = new HashSet<>();

    private ScheduledFuture<?> availabilityPollFuture;

    private ScheduledExecutorService scheduler;

    private Integer pollInterval;

    private String configurationPid;

    private SecureCxfClientFactory<Nsili> factory;

    private NsiliFilterDelegate nsiliFilterDelegate;

    private Set<ContentType> contentTypes = NsiliConstants.CONTENT_TYPES;

    private View[] views;

    private HashMap<String, List<AttributeInformation>> queryableAttributes;

    private HashMap<String, String[]> resultAttributes;

    private HashMap<String, List<String>> sortableAttributes;

    private String description;

    private String ddfOrgName = DEFAULT_USER_INFO;

    private ResourceReader resourceReader;

    private String accessUserId = "";

    private String accessPassword = "";

    private String accessLicenseKey = "";

    private boolean excludeSortOrder = false;

    private boolean swapCoordinates = false;

    private String additionalQueryParams = "";

    private ExecutorService executorService;

    private CompletionService<Result> completionService;

    private CorbaOrb corbaOrb = null;

    private Object queryLockObj = new Object();

    static {
        try (InputStream properties = NsiliSource.class.getResourceAsStream(DESCRIBABLE_PROPERTIES_FILE)) {
            describableProperties.load(properties);
        } catch (IOException e) {
            LOGGER.info("Failed to load properties", e);
        }
    }

    /**
     * Constructor used for testing.
     */
    NsiliSource(SecureCxfClientFactory factory, HashMap<String, String[]> resultAttributes,
            HashMap<String, List<String>> sortableAttributes, NsiliFilterDelegate filterDelegate, ORB orb) {
        scheduler = Executors.newSingleThreadScheduledExecutor();
        this.factory = factory;
        this.resultAttributes = resultAttributes;
        this.nsiliFilterDelegate = filterDelegate;
        this.sortableAttributes = sortableAttributes;
        ddfOrgName = System.getProperty("org.codice.ddf.system.organization", DEFAULT_USER_INFO);
        this.orb = orb;
    }

    public NsiliSource(CorbaOrb corbaOrb) {
        scheduler = Executors.newSingleThreadScheduledExecutor();
        setCorbaOrb(corbaOrb);
    }

    public void init() {
        corbaOrb.addCorbaServiceListener(this);
        initCorbaClient();
        setupAvailabilityPoll();
    }

    @Override
    public void corbaInitialized() {
        orb = corbaOrb.getOrb();
        initCorbaClient();
    }

    @Override
    public void corbaShutdown() {
        orb = null;
        corbaOrb = null;
    }

    private void createClientFactory() {
        int timeoutMsec = clientTimeout * 1000;
        if (StringUtils.isNotBlank(serverUsername) && StringUtils.isNotBlank(serverPassword)) {
            factory = new SecureCxfClientFactory(iorUrl, Nsili.class, null, null, true, true, timeoutMsec,
                    timeoutMsec, serverUsername, serverPassword);
        } else {
            factory = new SecureCxfClientFactory(iorUrl, Nsili.class, null, null, true, true, timeoutMsec,
                    timeoutMsec);
        }
    }

    /**
     * Initializes the Corba Client ORB and gets the mandatory interfaces that are required for a
     * STANAG 4559 complaint Federated Source, also queries the source for the queryable attributes
     * and views that it provides.
     */
    private void initCorbaClient() {
        getIorString();
        if (iorString != null) {
            initLibrary();
            setSourceDescription();
            initMandatoryManagers();
            initServerViews();
            initQueryableAttributes();
            initSortableAndResultAttributes();
            setFilterDelegate();

            LOGGER.debug("Initialized source {} with IOR: {}", getId(), iorString);
        }
    }

    /**
     * Determines which protocol is specified and attempts to retrive the IOR String appropriately.
     */
    private void getIorString() {
        URI uri;
        try {
            uri = new URI(iorUrl);
        } catch (URISyntaxException e) {
            LOGGER.error("{} : Invalid URL specified for IOR string: {} {}", id, iorUrl, e.getMessage());
            LOGGER.debug("{} : Invalid URL specified for IOR string: {}", id, iorUrl, e);
            return;
        }

        if (uri.getScheme().equals(HTTP_SCHEME) || uri.getScheme().equals(HTTPS_SCHEME)) {
            getIorStringFromHttpSource();
        } else if (uri.getScheme().equals(FTP_SCHEME)) {
            getIorStringFromFtpSource();
        } else if (uri.getScheme().equals(FILE_SCHEME)) {
            getIorStringFromLocalDisk();
        } else {
            LOGGER.error("Invalid protocol specified for IOR string: {}", iorUrl);
        }

        if (StringUtils.isNotBlank(iorString)) {
            LOGGER.debug("{} : Successfully obtained IOR file from {}", getId(), iorUrl);
        } else {
            LOGGER.error("{} : Received an empty or null IOR String.", id);
        }
    }

    /**
     * Obtains the IOR string from a local file.
     */
    private void getIorStringFromLocalDisk() {
        try (InputStream inputStream = new FileInputStream(iorUrl.substring(7))) {
            iorString = IOUtils.toString(inputStream, StandardCharsets.ISO_8859_1.name());
        } catch (IOException e) {
            LOGGER.error("{} : Unable to process IOR String.", id, e);
        }
    }

    /**
     * Uses the SecureClientCxfFactory to obtain the IOR string from the provided URL via HTTP(S).
     */
    private void getIorStringFromHttpSource() {
        createClientFactory();
        Nsili nsili = factory.getClient();

        try (InputStream inputStream = nsili.getIorFile()) {
            iorString = IOUtils.toString(inputStream, StandardCharsets.ISO_8859_1.name());
            //Remove leading/trailing whitespace as the CORBA init can't handle that.
            iorString = iorString.trim();
        } catch (IOException e) {
            LOGGER.error("{} : Unable to process IOR String. {}", id, e.getMessage());
            LOGGER.debug("{} : Unable to process IOR String.", id, e);
        } catch (Exception e) {
            LOGGER.warn("{} : Error retrieving IOR file for {}. {}", id, iorUrl, e.getMessage());
            LOGGER.debug("{} : Error retrieving IOR file for {}.", id, iorUrl, e);
        }
    }

    /**
     * Uses FTPClient to obtain the IOR string from the provided URL via FTP.
     */
    private void getIorStringFromFtpSource() {
        URI uri = null;
        try {
            uri = new URI(iorUrl);
        } catch (URISyntaxException e) {
            LOGGER.error("{} : Invalid URL specified for IOR string: {} {}", id, iorUrl, e.getMessage());
            LOGGER.debug("{} : Invalid URL specified for IOR string: {}", id, iorUrl, e);
        }

        FTPClient ftpClient = new FTPClient();
        try {
            if (uri.getPort() > 0) {
                ftpClient.connect(uri.getHost(), uri.getPort());
            } else {
                ftpClient.connect(uri.getHost());
            }

            if (!ftpClient.login(serverUsername, serverPassword)) {
                LOGGER.error("{} : FTP server log in unsuccessful.", id);
            } else {
                int timeoutMsec = clientTimeout * 1000;
                ftpClient.setConnectTimeout(timeoutMsec);
                ftpClient.setControlKeepAliveReplyTimeout(timeoutMsec);
                ftpClient.setDataTimeout(timeoutMsec);

                ftpClient.enterLocalPassiveMode();
                ftpClient.setFileType(FTP.BINARY_FILE_TYPE);

                InputStream inputStream = ftpClient.retrieveFileStream(uri.getPath());

                iorString = IOUtils.toString(inputStream, StandardCharsets.ISO_8859_1.name());
                //Remove leading/trailing whitespace as the CORBA init can't handle that.
                iorString = iorString.trim();
            }
        } catch (Exception e) {
            LOGGER.warn("{} : Error retrieving IOR file for {}. {}", id, iorUrl, e.getMessage());
            LOGGER.debug("{} : Error retrieving IOR file for {}.", id, iorUrl, e);
        }
    }

    /**
     * Initializes the Root STANAG 4559 Library Interface
     */
    private void initLibrary() {
        if (iorString != null) {
            org.omg.CORBA.Object obj = orb.string_to_object(iorString);

            library = LibraryHelper.narrow(obj);
            if (library != null) {
                LOGGER.debug("{} : Initialized Library Interface", getId());
            } else {
                LOGGER.error("{} : Unable to initialize the library interface.", getId());
            }
        }
    }

    /**
     * Initializes all STANAG 4559 mandatory managers:
     * CatalogMgr
     * OrderMgr
     * DataModelMgr
     * ProductMgr
     */
    private void initMandatoryManagers() {
        try {
            accessCriteria = new AccessCriteria(accessUserId, accessPassword, accessLicenseKey);

            LibraryManager libraryManager = library.get_manager(CATALOG_MGR, accessCriteria);
            setCatalogMgr(CatalogMgrHelper.narrow(libraryManager));

            libraryManager = library.get_manager(ORDER_MGR, accessCriteria);
            setOrderMgr(OrderMgrHelper.narrow(libraryManager));

            libraryManager = library.get_manager(PRODUCT_MGR, accessCriteria);
            setProductMgr(ProductMgrHelper.narrow(libraryManager));

            libraryManager = library.get_manager(DATA_MODEL_MGR, accessCriteria);
            setDataModelMgr(DataModelMgrHelper.narrow(libraryManager));

        } catch (ProcessingFault | SystemFault | InvalidInputParameter e) {
            LOGGER.error("{} : Unable to retrieve mandatory managers.", id, e);
        }

        if (catalogMgr != null && orderMgr != null && productMgr != null && dataModelMgr != null) {
            LOGGER.debug("{} : Initialized STANAG mandatory managers.", getId());
        } else {
            LOGGER.error("{} : Unable to initialize mandatory mangers.", getId());
        }
    }

    /**
     * Obtains all possible views that the Federated Source can provide. EX: NSIL_ALL_VIEW, NSIL_IMAGERY
     * According to ANNEX D, TABLE D-6, the passed parameter in get_view_names is an empty list(not used).
     *
     * @return an array of views
     */
    private void initServerViews() {
        View[] views = null;
        try {
            views = dataModelMgr.get_view_names(new NameValue[0]);
        } catch (ProcessingFault | SystemFault | InvalidInputParameter e) {
            LOGGER.error("{} : Unable to retrieve views.", id, e);
        }
        if (views == null) {
            LOGGER.error("{} : Unable to retrieve views.", id);
        }
        this.views = views;
    }

    /**
     * Obtains all possible attributes for all possible views that the Federated Source can provide, and
     * populates a sortableAttributes map, as well as resultAttributes map that will be used for querying
     * the server.
     * According to ANNEX D, TABLE D-6, the passed parameter in get_view_names is an empty list(not used).
     *
     * @return a map of each view and the attributes provided by the source for that view
     */
    private void initSortableAndResultAttributes() {
        if (views == null || views.length == 0) {
            return;
        }
        HashMap<String, String[]> resultAttributesMap = new HashMap<>();
        HashMap<String, List<String>> sortableAttributesMap = new HashMap<>();

        try {
            for (int i = 0; i < views.length; i++) {

                List<String> sortableAttributesList = new ArrayList<>();

                AttributeInformation[] attributeInformationArray = dataModelMgr.get_attributes(views[i].view_name,
                        new NameValue[0]);
                String[] resultAttributes = new String[attributeInformationArray.length];

                LOGGER.debug("Attributes for view: {}", views[i].view_name);

                for (int c = 0; c < attributeInformationArray.length; c++) {
                    AttributeInformation attributeInformation = attributeInformationArray[c];
                    resultAttributes[c] = attributeInformation.attribute_name;

                    if (LOGGER.isDebugEnabled()) {
                        if (attributeInformation.mode == RequirementMode.MANDATORY) {
                            String modeStr = getMode(attributeInformation.mode);
                            LOGGER.debug("\t {} mode: {}, sortable: {}", attributeInformation.attribute_name,
                                    modeStr, String.valueOf(attributeInformation.sortable));
                        } else {
                            if (LOGGER.isTraceEnabled()) {
                                String modeStr = getMode(attributeInformation.mode);
                                LOGGER.trace("\t {} mode: {}, sortable: {}", attributeInformation.attribute_name,
                                        modeStr, String.valueOf(attributeInformation.sortable));
                            }
                        }
                    }

                    if (attributeInformation.sortable) {
                        sortableAttributesList.add(attributeInformation.attribute_name);
                    }

                }
                sortableAttributesMap.put(views[i].view_name, sortableAttributesList);
                resultAttributesMap.put(views[i].view_name, resultAttributes);

            }
        } catch (ProcessingFault | SystemFault | InvalidInputParameter e) {
            LOGGER.error("{} : Unable to retrieve queryable attributes.", id, e);
        }

        if (resultAttributesMap.size() == 0) {
            LOGGER.warn("{} : Received empty attributes list from STANAG source.", getId());
        }

        this.sortableAttributes = sortableAttributesMap;
        this.resultAttributes = resultAttributesMap;
    }

    /**
     * Obtains all queryable attributes for all possible views that the Federated Source can provide.
     * According to ANNEX D, TABLE D-6, the passed parameter in get_view_names is an empty list(not used).
     *
     * @return a map of each view and the queryable attributes provided by the source for that view
     */
    private void initQueryableAttributes() {
        if (views == null || views.length == 0) {
            return;
        }
        HashMap<String, List<AttributeInformation>> map = new HashMap<>();

        try {
            for (int i = 0; i < views.length; i++) {
                AttributeInformation[] attributeInformationArray = dataModelMgr
                        .get_queryable_attributes(views[i].view_name, new NameValue[0]);
                List<AttributeInformation> attributeInformationList = new ArrayList<>();
                for (int c = 0; c < attributeInformationArray.length; c++) {
                    attributeInformationList.add(attributeInformationArray[c]);
                }
                map.put(views[i].view_name, attributeInformationList);
            }
        } catch (ProcessingFault | SystemFault | InvalidInputParameter e) {
            LOGGER.error("{} : Unable to retrieve queryable attributes.", id, e);
        }

        if (map.size() == 0) {
            LOGGER.warn("{} : Received empty queryable attributes from STANAG source.", getId());
        }
        queryableAttributes = map;
    }

    /**
     * Obtains the description of the source from the Library interface.
     *
     * @return a description of the source
     */
    private void setSourceDescription() {
        StringBuilder stringBuilder = new StringBuilder();
        try {
            LibraryDescription libraryDescription = library.get_library_description();
            stringBuilder.append(libraryDescription.library_name + " : ");
            stringBuilder.append(libraryDescription.library_description);
        } catch (ProcessingFault | SystemFault e) {
            LOGGER.error("{} : Unable to retrieve source description. {}", id, e.getMessage());
            LOGGER.debug("{} : Unable to retrieve source description.", id, e);
        }
        String description = stringBuilder.toString();
        if (StringUtils.isBlank(description)) {
            LOGGER.warn("{} :  Unable to retrieve source description.", getId());
        }
        this.description = description;
    }

    public void destroy() {
        if (corbaOrb != null) {
            corbaOrb.removeCorbaServiceListener(this);
        }
        availabilityPollFuture.cancel(true);
        scheduler.shutdownNow();
    }

    public void refresh(Map<String, Object> configuration) {
        LOGGER.debug("Entering Refresh : {}", getId());

        if (MapUtils.isEmpty(configuration)) {
            LOGGER.error("{} {} : Received null or empty configuration during refresh.",
                    this.getClass().getSimpleName(), getId());
            return;
        }

        String serverUsername = (String) configuration.get(SERVER_USERNAME);
        if (StringUtils.isNotBlank(serverUsername) && !serverUsername.equals(this.serverUsername)) {
            setServerUsername(serverUsername);
        }
        String serverPassword = (String) configuration.get(SERVER_PASSWORD);
        if (StringUtils.isNotBlank(serverPassword) && !serverPassword.equals(this.serverPassword)) {
            setServerPassword(serverPassword);
        }
        Integer clientTimeout = (Integer) configuration.get(CLIENT_TIMEOUT);
        if (clientTimeout != null && clientTimeout != this.clientTimeout) {
            setClientTimeout(clientTimeout);
        }
        String id = (String) configuration.get(ID);
        if (StringUtils.isNotBlank(id) && !id.equals(this.id)) {
            setId(id);
        }
        String iorUrl = (String) configuration.get(IOR_URL);
        if (StringUtils.isNotBlank(iorUrl) && !iorUrl.equals(this.iorUrl)) {
            setIorUrl(iorUrl);
        }

        String additionalQueryParams = (String) configuration.get(ADDITIONAL_QUERY_PARAMS);
        setAdditionalQueryParams(additionalQueryParams);

        Integer pollInterval = (Integer) configuration.get(POLL_INTERVAL);
        if (pollInterval != null && !pollInterval.equals(this.pollInterval)) {
            setPollInterval(pollInterval);
        }
        Integer maxHitCount = (Integer) configuration.get(MAX_HIT_COUNT);
        if (maxHitCount != null && !maxHitCount.equals(this.maxHitCount)) {
            setMaxHitCount(maxHitCount);
        }
        String accessUserId = (String) configuration.get(ACCESS_USERID);
        if (StringUtils.isNotBlank(accessUserId)) {
            setAccessUserId(accessUserId);
        }
        String accessPassword = (String) configuration.get(ACCESS_PASSWORD);
        if (StringUtils.isNotBlank(accessPassword)) {
            setAccessPassword(accessPassword);
        }
        String accessLicenseKey = (String) configuration.get(ACCESS_LICENSE_KEY);
        if (StringUtils.isNotBlank(accessLicenseKey)) {
            setAccessLicenseKey(accessLicenseKey);
        }
        init();
    }

    @Override
    public String getDescription() {
        StringBuilder sb = new StringBuilder();
        sb.append(describableProperties.getProperty(DESCRIPTION)).append(System.getProperty(System.lineSeparator()))
                .append(description);
        return sb.toString();
    }

    @Override
    public String getId() {
        String sourceId = super.getId();
        return sourceId;
    }

    @Override
    public SourceResponse query(QueryRequest queryRequest) throws UnsupportedQueryException {
        org.codice.alliance.nsili.common.GIAS.Query query = createQuery(queryRequest.getQuery());

        String[] results = resultAttributes.get(NsiliConstants.NSIL_ALL_VIEW);

        SortAttribute[] sortAttributes = getSortAttributes(queryRequest.getQuery().getSortBy());
        NameValue[] propertiesList = getDefaultPropertyList();
        LOGGER.debug("{} : Sending BQS query to source.\n Sort Attributes : {}", getId(), sortAttributes);
        return submitQuery(queryRequest, query, results, sortAttributes, propertiesList);
    }

    /**
     * Uses the NsiliFilterDelegate to create a STANAG 4559 BQS (Boolean Syntax Query) from the DDF Query
     *
     * @param query - the query recieved from the Search-Ui
     * @return - a STANAG4559 complaint query
     * @throws UnsupportedQueryException
     */
    private org.codice.alliance.nsili.common.GIAS.Query createQuery(Query query) throws UnsupportedQueryException {
        String filter = createFilter(query);
        if (StringUtils.isNotBlank(additionalQueryParams)) {
            filter = filter + " " + additionalQueryParams;
        }
        LOGGER.debug("{} : BQS Query : {}", getId(), filter);
        return new org.codice.alliance.nsili.common.GIAS.Query(NsiliConstants.NSIL_ALL_VIEW, filter);
    }

    /**
     * Obtains the number of hits that the given query has received from the server.
     *
     * @param query      - a BQS query
     * @param properties - a list of properties for the query
     * @return - the hit count
     */
    private int getHitCount(org.codice.alliance.nsili.common.GIAS.Query query, NameValue[] properties) {
        IntHolder intHolder = new IntHolder();
        try {
            synchronized (queryLockObj) {
                HitCountRequest hitCountRequest = catalogMgr.hit_count(query, properties);
                hitCountRequest.complete(intHolder);
            }
        } catch (ProcessingFault | SystemFault | InvalidInputParameter e) {
            LOGGER.error("{} : Unable to get hit count for query. : {}", getId(),
                    NsilCorbaExceptionUtil.getExceptionDetails(e));
            LOGGER.debug("{} : HitCount Query error", getId(), e);
        }

        LOGGER.debug("{} :  Received {} hit(s) from query.", getId(), intHolder.value);
        return intHolder.value;
    }

    /**
     * Submits and completes a BQS Query to the STANAG 4559 server and returns the response.
     *
     * @param queryRequest     - the query request generated from the search
     * @param query            - a BQS query
     * @param resultAttributes - a list of desired result attributes
     * @param sortAttributes   - a list of attributes to sort by
     * @param properties       - a list of properties for the query
     * @return - the server's response
     */
    private SourceResponse submitQuery(QueryRequest queryRequest, org.codice.alliance.nsili.common.GIAS.Query query,
            String[] resultAttributes, SortAttribute[] sortAttributes, NameValue[] properties) {
        DAGListHolder dagListHolder = new DAGListHolder();

        SourceResponseImpl sourceResponse = null;

        long numHits = 0;
        try {
            synchronized (queryLockObj) {
                LOGGER.debug("{} : Submit query: {}", id, query.bqs_query);
                LOGGER.debug("{} : Requesting result attributes: {}", id, Arrays.toString(resultAttributes));
                LOGGER.debug("{} : Sort Attributes: {}", id, Arrays.toString(sortAttributes));
                LOGGER.debug("{} : Properties: {}", id, Arrays.toString(properties));
                HitCountRequest hitCountRequest = catalogMgr.hit_count(query, properties);
                IntHolder hitHolder = new IntHolder();
                hitCountRequest.complete(hitHolder);
                numHits = hitHolder.value;
                SubmitQueryRequest submitQueryRequest;
                if (hitHolder.value > 1) {
                    submitQueryRequest = catalogMgr.submit_query(query, resultAttributes, sortAttributes,
                            properties);
                } else {
                    submitQueryRequest = catalogMgr.submit_query(query, resultAttributes, new SortAttribute[0],
                            new NameValue[0]);
                }
                submitQueryRequest.set_user_info(ddfOrgName);
                submitQueryRequest.set_number_of_hits(maxHitCount);
                submitQueryRequest.complete_DAG_results(dagListHolder);
            }
        } catch (ProcessingFault | SystemFault | InvalidInputParameter e) {
            LOGGER.error("{} : Unable to query source. {}", id, NsilCorbaExceptionUtil.getExceptionDetails(e));
            LOGGER.debug("{} : Query error", id, e);
        }

        if (dagListHolder.value != null) {
            List<Result> results = new ArrayList<>();
            String id = getId();
            List<Future> futures = new ArrayList<>(dagListHolder.value.length);

            for (DAG dag : dagListHolder.value) {
                Callable<Result> convertRunner = () -> {
                    DAGConverter dagConverter = new DAGConverter(resourceReader);
                    Metacard card = dagConverter.convertDAG(dag, swapCoordinates, id);
                    if (card != null) {
                        if (LOGGER.isTraceEnabled()) {
                            DAGConverter.logMetacard(card, getId());
                        }
                        return new ResultImpl(card);
                    } else {
                        LOGGER.warn("{} : Unable to convert DAG to metacard, returned card is null", getId());
                    }
                    return null;
                };
                futures.add(completionService.submit(convertRunner));
            }

            Future<Result> completedFuture;
            while (!futures.isEmpty()) {
                try {
                    completedFuture = completionService.take();
                    futures.remove(completedFuture);
                    results.add(completedFuture.get());
                } catch (ExecutionException e) {
                    LOGGER.warn("Unable to create result.", e);
                } catch (InterruptedException ignore) {
                    //ignore
                }
            }

            sourceResponse = new SourceResponseImpl(queryRequest, results, numHits);

        } else {
            LOGGER.warn("{} : Source returned empty DAG list", getId());
        }

        return sourceResponse;
    }

    private void setFilterDelegate() {
        nsiliFilterDelegate = new NsiliFilterDelegate(queryableAttributes, NsiliConstants.NSIL_ALL_VIEW);
    }

    @Override
    public void maskId(String newSourceId) {
        final String methodName = "maskId";
        LOGGER.debug("ENTERING: {} with sourceId = {}", methodName, newSourceId);
        if (newSourceId != null) {
            super.maskId(newSourceId);
        }
        LOGGER.debug("EXITING: {}", methodName);
    }

    @Override
    public String getOrganization() {
        return describableProperties.getProperty(ORGANIZATION);
    }

    @Override
    public String getTitle() {
        return describableProperties.getProperty(TITLE);
    }

    @Override
    public String getVersion() {
        return describableProperties.getProperty(VERSION);
    }

    @Override
    public boolean isAvailable() {
        return availabilityTask.isAvailable();
    }

    @Override
    public boolean isAvailable(SourceMonitor sourceMonitor) {
        sourceMonitors.add(sourceMonitor);
        return isAvailable();
    }

    @Override
    public Set<String> getOptions(Metacard arg0) {
        return null;
    }

    @Override
    public Set<String> getSupportedSchemes() {
        return null;
    }

    @Override
    public ResourceResponse retrieveResource(URI resourceUri, Map<String, Serializable> requestProperties)
            throws IOException, ResourceNotFoundException, ResourceNotSupportedException {
        LOGGER.debug("{}, {}, {}, {}", resourceUri.getHost(), resourceUri.getPath(), resourceUri.getPort(),
                requestProperties.toString());
        return resourceReader.retrieveResource(resourceUri, requestProperties);
    }

    @Override
    public String getConfigurationPid() {
        return configurationPid;
    }

    @Override
    public void setConfigurationPid(String configurationPid) {
        this.configurationPid = configurationPid;
    }

    @Override
    public Set<ContentType> getContentTypes() {
        return contentTypes;
    }

    private String createFilter(Query query) throws UnsupportedQueryException {
        String filter = filterAdapter.adapt(query, nsiliFilterDelegate);
        LOGGER.debug("Converted internal filter to BQS: {}", filter);
        return filter;
    }

    public void setServerUsername(String serverUsername) {
        this.serverUsername = serverUsername;
    }

    public void setServerPassword(String serverPassword) {
        this.serverPassword = serverPassword;
    }

    public Integer getClientTimeout() {
        return clientTimeout;
    }

    public void setClientTimeout(Integer clientTimeout) {
        this.clientTimeout = clientTimeout;
    }

    public void setId(String id) {
        this.id = id;
        super.setId(id);
    }

    public void setIorUrl(String iorUrl) {
        if (iorUrl != null) {
            this.iorUrl = iorUrl.trim();
        }
    }

    public void setMaxHitCount(Integer maxHitCount) {
        this.maxHitCount = maxHitCount;
    }

    public void setCorbaOrb(CorbaOrb corbaOrb) {
        this.corbaOrb = corbaOrb;
        this.orb = corbaOrb.getOrb();
        corbaOrb.addCorbaServiceListener(this);
    }

    public String getIorUrl() {
        return iorUrl;
    }

    public String getServerPassword() {
        return serverPassword;
    }

    public String getServerUsername() {
        return serverUsername;
    }

    public Integer getMaxHitCount() {
        return maxHitCount;
    }

    public Integer getPollInterval() {
        return pollInterval;
    }

    public boolean getExcludeSortOrder() {
        return excludeSortOrder;
    }

    public void setExcludeSortOrder(boolean excludeSortOrder) {
        this.excludeSortOrder = excludeSortOrder;
    }

    public boolean getSwapCoordinates() {
        return swapCoordinates;
    }

    public void setSwapCoordinates(boolean swapCoordinates) {
        this.swapCoordinates = swapCoordinates;
    }

    public String getAdditionalQueryParams() {
        return additionalQueryParams;
    }

    public void setAdditionalQueryParams(String additionalQueryParams) {
        this.additionalQueryParams = additionalQueryParams;
    }

    public String getAccessUserId() {
        return accessUserId;
    }

    public void setAccessUserId(String accessUserId) {
        this.accessUserId = accessUserId;
    }

    public String getAccessPassword() {
        return accessPassword;
    }

    public void setAccessPassword(String accessPassword) {
        this.accessPassword = accessPassword;
    }

    public String getAccessLicenseKey() {
        return accessLicenseKey;
    }

    public void setAccessLicenseKey(String accessLicenseKey) {
        this.accessLicenseKey = accessLicenseKey;
    }

    public void setNumberWorkerThreads(int numberWorkerThreads) {
        List<Runnable> waitingTasks = null;
        if (executorService != null) {
            waitingTasks = executorService.shutdownNow();
        }

        executorService = Executors.newFixedThreadPool(numberWorkerThreads);
        completionService = new ExecutorCompletionService(executorService);
        if (waitingTasks != null) {
            for (Runnable task : waitingTasks) {
                executorService.submit(task);
            }
        }
    }

    public void setResourceReader(ResourceReader resourceReader) {
        this.resourceReader = resourceReader;
    }

    public void setPollInterval(Integer interval) {
        this.pollInterval = interval;
    }

    public void setFilterAdapter(FilterAdapter filterAdapter) {
        this.filterAdapter = filterAdapter;
    }

    public void setCatalogMgr(CatalogMgr catalogMgr) {
        this.catalogMgr = catalogMgr;
    }

    public void setOrderMgr(OrderMgr orderMgr) {
        this.orderMgr = orderMgr;
    }

    public void setDataModelMgr(DataModelMgr dataModelMgr) {
        this.dataModelMgr = dataModelMgr;
    }

    public void setProductMgr(ProductMgr productMgr) {
        this.productMgr = productMgr;
    }

    public void setSortableAttributes(HashMap<String, List<String>> sortableAttributes) {
        this.sortableAttributes = sortableAttributes;
    }

    public void setAvailabilityTask(AvailabilityTask availabilityTask) {
        this.availabilityTask = availabilityTask;
    }

    public void setupAvailabilityPoll() {
        LOGGER.debug("Setting Availability poll task for {} minute(s) on Source {}", getPollInterval(), getId());
        Stanag4559AvailabilityCommand command = new Stanag4559AvailabilityCommand();
        long interval = TimeUnit.MINUTES.toMillis(getPollInterval());
        if (availabilityPollFuture == null || availabilityPollFuture.isCancelled()) {
            if (availabilityTask == null) {
                availabilityTask = new AvailabilityTask(interval, command, getId());
            } else {
                availabilityTask.setInterval(interval);
            }

            // Run the availability check immediately prior to scheduling it in a thread.
            // This is necessary to allow the catalog framework to have the correct
            // availability when the source is bound
            availabilityTask.run();
            // Schedule the availability check every 1 second. The actually call to
            // the remote server will only occur if the pollInterval has
            // elapsed.
            availabilityPollFuture = scheduler.scheduleWithFixedDelay(availabilityTask, AvailabilityTask.NO_DELAY,
                    AvailabilityTask.ONE_SECOND, TimeUnit.SECONDS);
        } else {
            LOGGER.debug("No changes being made on the poller.");
        }

    }

    private void availabilityChanged(boolean isAvailable) {

        if (isAvailable) {
            LOGGER.info("STANAG 4559 source {} is available.", getId());
        } else {
            LOGGER.info("STANAG 4559 source {} is unavailable.", getId());
        }

        for (SourceMonitor monitor : this.sourceMonitors) {
            if (isAvailable) {
                LOGGER.debug("Notifying source monitor that STANAG 4559 source {} is available.", getId());
                monitor.setAvailable();
            } else {
                LOGGER.debug("Notifying source monitor that STANAG 4559 source {} is unavailable.", getId());
                monitor.setUnavailable();
            }
        }
    }

    /**
     * Returns the Default Property List defined in the STANAG 4559 specification.
     *
     * @return - default WGS84 Geographic Datum.
     */
    private NameValue[] getDefaultPropertyList() {
        Any defaultAnyProperty = orb.create_any();
        defaultAnyProperty.insert_string(WGS84);
        NameValue[] result = { new NameValue(GEOGRAPHIC_DATUM, defaultAnyProperty) };
        return result;
    }

    /**
     * Sets a SortAttribute[] to be used in a query.  The STANAG 4559 Spec has no mechanism to sort
     * queries by RELEVANCE or Shortest/Longest distance from a point, so they are ignored.
     *
     * @param sortBy - sortBy object specified in the Search UI
     * @return - an array of SortAttributes sent in the query to the source.
     */
    private SortAttribute[] getSortAttributes(SortBy sortBy) {
        if (excludeSortOrder) {
            return new SortAttribute[0];
        }

        if (sortBy == null || sortableAttributes == null) {
            //Default to sorting by Date/Time modified if no sorting provided
            return new SortAttribute[] { new SortAttribute(
                    NsiliConstants.NSIL_CARD + "." + NsiliConstants.DATE_TIME_MODIFIED, Polarity.DESCENDING) };
        }

        String sortAttribute = sortBy.getPropertyName().getPropertyName();
        Polarity sortPolarity;

        if (sortBy.getSortOrder().toSQL().equals(ASC)) {
            sortPolarity = Polarity.ASCENDING;
        } else {
            sortPolarity = Polarity.DESCENDING;
        }

        String cardDateTimeModifiedAttribute = NsiliConstants.NSIL_CARD + "." + NsiliConstants.DATE_TIME_MODIFIED;
        String cardSourceDateTimeModified = NsiliConstants.NSIL_CARD + "."
                + NsiliConstants.SOURCE_DATE_TIME_MODIFIED;
        String dateTimeDeclaredAttribute = NsiliConstants.NSIL_FILE + "." + NsiliConstants.DATE_TIME_DECLARED;

        if (sortAttribute.equals(Metacard.MODIFIED)) {
            List<SortAttribute> modifiedAttrs = new ArrayList<>();
            if (isAttributeSupported(cardDateTimeModifiedAttribute)) {
                modifiedAttrs.add(new SortAttribute(cardDateTimeModifiedAttribute, sortPolarity));
            }
            if (isAttributeSupported(cardSourceDateTimeModified)) {
                modifiedAttrs.add(new SortAttribute(cardSourceDateTimeModified, sortPolarity));
            }

            return modifiedAttrs.toArray(new SortAttribute[0]);
        } else if (sortAttribute.equals(Metacard.CREATED) && isAttributeSupported(dateTimeDeclaredAttribute)) {
            SortAttribute[] sortAttributeArray = { new SortAttribute(dateTimeDeclaredAttribute, sortPolarity) };
            return sortAttributeArray;
        } else {
            return new SortAttribute[0];
        }
    }

    /**
     * Verifies that a given attribute exists in the list of sortableAttributes for NSIL_ALL_VIEW
     */
    private boolean isAttributeSupported(String attribute) {
        List<String> attributeInformationList = sortableAttributes.get(NsiliConstants.NSIL_ALL_VIEW);

        for (String sortableAttribute : attributeInformationList) {
            if (attribute.equals(sortableAttribute)) {
                return true;
            }
        }
        return false;
    }

    private String getMode(RequirementMode requirementMode) {
        if (requirementMode == RequirementMode.MANDATORY) {
            return "MANDATORY";
        } else if (requirementMode == RequirementMode.OPTIONAL) {
            return "OPTIONAL";
        } else {
            return String.valueOf(requirementMode);
        }
    }

    /**
     * Callback class to check the Availability of the NsiliSource.
     * <p>
     * NOTE: Ideally, the framework would call isAvailable on the Source and the SourcePoller would
     * have an AvailabilityTask that cached each Source's availability. Until that is done, allow
     * the command to handle the logic of managing availability.
     */
    private class Stanag4559AvailabilityCommand implements AvailabilityCommand {

        @Override
        public boolean isAvailable() {
            LOGGER.debug("Checking availability for source {} ", getId());
            boolean oldAvailability = NsiliSource.this.isAvailable();
            String[] managers = null;

            // Refresh IOR String when polling for availability in case server conditions change
            try {
                getIorString();
                initLibrary();
                managers = library.get_manager_types();
            } catch (Exception e) {
                LOGGER.error("{} : Connection Failure for source. {}", getId(), e.getMessage());
                LOGGER.debug("{} : Connection Failure for source.", getId(), e);
            }

            // If the IOR string is not valid, or the source cannot communicate with the library, the
            // source is unavailable
            boolean newAvailability = (managers != null && StringUtils.isNotBlank(iorString));
            if (oldAvailability != newAvailability) {
                availabilityChanged(newAvailability);
                // If the source becomes available, configure it.
                if (newAvailability) {
                    initCorbaClient();
                }
            }
            return newAvailability;
        }
    }
}