;
; NOSA HEADER START
;
; The contents of this file are subject to the terms of the NASA Open 
; Source Agreement (NOSA), Version 1.3 only (the "Agreement").  You may 
; not use this file except in compliance with the Agreement.
;
; You can obtain a copy of the agreement at
;   docs/NASA_Open_Source_Agreement_1.3.txt
; or 
;   https://cdaweb.gsfc.nasa.gov/WebServices/NASA_Open_Source_Agreement_1.3.txt.
;
; See the Agreement for the specific language governing permissions
; and limitations under the Agreement.
;
; When distributing Covered Code, include this NOSA HEADER in each
; file and include the Agreement file at 
; docs/NASA_Open_Source_Agreement_1.3.txt.  If applicable, add the 
; following below this NOSA HEADER, with the fields enclosed by 
; brackets "[]" replaced with your own identifying information: 
; Portions Copyright [yyyy] [name of copyright owner}
;
; NOSA HEADER END
;
; Copyright (c) 2010-2026 United States Government as represented by the
; National Aeronautics and Space Administration. No copyright is claimed
; in the United States under Title 17, U.S.Code. All Other Rights 
; Reserved.
;
;


;+
; This class represents the remotely callable interface to 
; <a href="https://www.nasa.gov/">NASA</a>'s
; <a href="https://spdf.gsfc.nasa.gov/">Space Physics Data Facility</a> 
; (SPDF)
; <a href="https://cdaweb.gsfc.nasa.gov/">Coordinated Data Analysis 
; System</a> (CDAS).
;
; @copyright Copyright (c) 2010-2026 United States Government as 
;     represented by the National Aeronautics and Space Administration.
;     No copyright is claimed in the United States under Title 17, 
;     U.S.Code. All Other Rights Reserved.
;
; @author B. Harris
;-


;+
; Creates an object representing CDAS.  
; 
; If access to the Internet is through an HTTP proxy, the caller 
; should ensure that the HTTP_PROXY environment is correctly set 
; before this method is called.  The HTTP_PROXY value should be of 
; the form 
; http://username:password@hostname:port/.
;
; @keyword endpoint {in} {optional} {type=string}
;              {default='https://cdaweb.gsfc.nasa.gov/WS/cdasr/1'}
;              URL of CDAS web service.
; @keyword userAgent {in} {optional} {type=string} {default=WsExample}
;              HTTP user-agent value used in communications with CDAS.
; @keyword defaultDataview {in} {optional} {type=string} 
;              {default=sp_phys}
;              default CDAS dataview value to use in subsequent calls
;              when no value is specified.
; @keyword sslVerifyPeer {in} {optional} {type=int} 
;              {default=SpdfGetDefaultSslVerifyPeer()}
;              Specifies whether the authenticity of the peer's SSL
;              certificate should be verified.  When 0, the connection 
;              succeeds regardless of what the peer SSL certificate 
;              contains.
; @returns a reference to a CDAS object.
;-
function SpdfCdas::init, $
    endpoint = endpoint, $
    userAgent = userAgent, $
    defaultDataview = defaultDataview, $
    sslVerifyPeer = sslVerifyPeer
    compile_opt idl2

    self.endpoint = 'https://cdaweb.gsfc.nasa.gov/WS/cdasr/1'
    self.version = '%VERSION*'
    self.currentVersionUrl = $
        'https://berniegsfc.github.io/cdasws/spdfCdasVersion.txt'
;        'https://cdaweb.gsfc.nasa.gov/WebServices/REST/spdfCdasVersion.txt'

    if keyword_set(endpoint) then self.endpoint = endpoint

    if ~keyword_set(userAgent) then userAgent = 'WsExample'

    self.userAgent = 'User-Agent: ' + userAgent + '/' + $
        self.version + ' (' + !version.os + ' ' + !version.arch + $
        ') IDL/' + !version.release

    self.defaultDataview = 'sp_phys'

    if keyword_set(defaultDataview) then begin

        self.defaultDataview = defaultDataview
    endif

    self.ssl_verify_peer = SpdfGetDefaultSslVerifyPeer()

    if n_elements(sslVerifyPeer) gt 0 then begin

        self.ssl_verify_peer = sslVerifyPeer
    endif

    self.proxySettings = obj_new('SpdfHttpProxy')

    self.retryLimit = 100

    return, self
end


;+
; Performs cleanup operations when this object is destroyed.
;-
pro SpdfCdas::cleanup
    compile_opt idl2

    if obj_valid(self.proxySettings) then obj_destroy, self.proxySettings
end


;+
; Gets the current endpoint value.
;
; @returns current endpoint string value.
;-
function SpdfCdas::getEndpoint
    compile_opt idl2

    return, self.endpoint
end


;+
; Gets the current userAgent value.
;
; @returns current userAgent string value.
;-
function SpdfCdas::getUserAgent
    compile_opt idl2

    return, self.userAgent
end


;+
; Gets the current defaultDataview value.
;
; @returns current defaultDataview string value.
;-
function SpdfCdas::getDefaultDataview
    compile_opt idl2

    return, self.defaultDataview
end


;+
; Gets the version of this class.
;
; @returns version of this class.
;-
function SpdfCdas::getVersion
    compile_opt idl2

    return, self.version
end


;+
; Gets the most up to date version of this class.
;
; @returns most up to date version of this class.
;-
function SpdfCdas::getCurrentVersion
    compile_opt idl2

    catch, errorStatus
    if (errorStatus ne 0) then begin

        catch, /cancel

        url->getProperty, response_code=responseCode

        print, 'SpdfCdas::getCurrentVersion failed with responseCode ', $
               responseCode, ' for ', self.currentVersionUrl
        print, '    with HTTP_PROXY = ', getenv('HTTP_PROXY')

        return, ''
    endif

    url = obj_new('IDLnetURL', $
                  proxy_authentication = $
                      self.proxySettings.getAuthentication(), $
                  proxy_hostname = self.proxySettings.getHostname(), $
                  proxy_port = self.proxySettings.getPort(), $
                  proxy_username = self.proxySettings.getUsername(), $
                  proxy_password = self.proxySettings.getPassword())

    version = url->get(/string_array, url=self.currentVersionUrl)

    if (n_elements(version) ne 1) then begin
        ; May have been redirected to pleasecontactus.html
        return, ''
    endif
    ; May want to add a regex validation here.

    return, version
end


;+
; Compares getVersion() and getCurrentVersion() to determine if this
; class is up to date.
;
; @returns true if getVersion() >= getCurrentVersion().  Otherwise 
;     false.
;-
function SpdfCdas::isUpToDate
    compile_opt idl2

    version = strsplit(self->getVersion(), '.', /extract)
    versionElements = n_elements(version)
    currentVersion = strsplit(self->getCurrentVersion(), '.', /extract)
    currentVersionElements = n_elements(currentVersion)

    if versionElements eq 1 or currentVersionElements eq 1 then begin

        ; Do not know what the versions are so return up-to-date
        return, 1
    endif

    if versionElements lt currentVersionElements then begin

        elements = versionElements
    endif else begin

        elements = currentVersionElements
    endelse

    for i = 0, elements - 1 do begin

        if 0 + version[i] lt 0 + currentVersion[i] then return, 0
    endfor

    if versionElements lt currentVersionElements then begin

        return, 0
    endif else begin

        return, 1
    endelse
end


;+
; "Percent encodes" the given string to escape reserved charaters.  This
; method is merely a wrapper around the IDLnetURL::URLEncode method which
; does nothing on versions of IDL where the IDLnetURL::URLEncode method
; does not exist.
;
; @param value {in} {required} {type=string}
;     the string to be encoded.
; @returns percent encoded representation of the given value or the
;     orignal value if called on a version of IDL without the
;     IDLnetURL::URLEncode method.
;-
function SpdfCdas::encode, $
    value
    compile_opt idl2
;    compile_opt static (idl >= 8.3)

    catch, errorStatus
    if (errorStatus ne 0) then begin

        ; idl version < 8.5.1
        catch, /cancel

        return, value
    endif

    return, IDLnetURL.URLEncode(value)
end


;+
; Gets the node's value of the first child of the first item of the
; specified element of the given DOM document.
;
; @private
;
; @param domElement {in} {required} {type=IDLffXMLDOMDocument}
;                DOM element to search
; @param tagName {in} {required} {type=string}
;                A scalar string containing the tag name of the desired
;                element.
; @returns node's string value(s) of the first child of the item(s)
;              of the specified element of the given DOM document. An
;              empty string is returned if the value cannot be found.
;-
function SpdfCdas::getNamedElementsFirstChildValue, $
    domElement, tagName
    compile_opt idl2

    nodeList = domElement->getElementsByTagName(tagName)

    if nodeList->getLength() eq 0 then return, ''

    values = strarr(nodeList->getLength())

    for i = 0, nodeList->getLength() - 1 do begin

        domNode = nodeList->item(i)

        child = domNode->getFirstChild()

        if obj_valid(child) then begin

            values[i] = child->getNodeValue()
        endif else begin

            values[i] = ''
        endelse
    endfor

    if n_elements(values) eq 1 then return, values[0] $
                               else return, values
end


;+
; Gets a description of all the dataviews that are available.
;
; @keyword httpErrorReporter {in} {optional} 
;              {type=SpdfHttpErrorReporter}
;              used to report an HTTP error.
; @returns array of SpdfDataviewDescription objects.
;              If there are no dataviews, an array of length 
;              one is returned with the first element being a null 
;              object reference.
;-
function SpdfCdas::getDataviews, $
    httpErrorReporter=errorReporter
    compile_opt idl2

    url = self.endpoint + '/dataviews'

    dvDom = self->makeGetRequest('N/A', url, $
                errorReporter=errorReporter)

    if ~obj_valid(dvDom) then return, objarr(1)

    dataviewElements = $
        dvDom->getElementsByTagName('DataviewDescription')

    dataviews = objarr(dataviewElements->getLength(), /nozero)

    for i = 0, dataviewElements->getLength() - 1 do begin

        dvElement = dataviewElements->item(i)

        id = $
            self->getNamedElementsFirstChildValue(dvElement, 'Id')
        endpointAddress = $
            self->getNamedElementsFirstChildValue(dvElement, $
                'EndpointAddress')
        title = $
            self->getNamedElementsFirstChildValue(dvElement, 'Title')
        subtitle = $
            self->getNamedElementsFirstChildValue(dvElement, 'SubTitle')
        overview = $
            self->getNamedElementsFirstChildValue(dvElement, 'Overview')
        underConstructionStr = $
            self->getNamedElementsFirstChildValue(dvElement, $
                'UnderConstruction')
        if strcmp(underConstructionStr, 'true', /fold_case) eq 1 $
        then begin
            underConstruction = 1b
        endif else begin
            underConstruction = 0b
        endelse
        noticeUrl = $
            self->getNamedElementsFirstChildValue(dvElement, $
                'NoticeUrl')
        publicAccessStr = $
            self->getNamedElementsFirstChildValue(dvElement, $
                'PublicAccess')
        if (strcmp(publicAccessStr, 'true', /fold_case) eq 1) then begin
            publicAccess = 1b
        endif else begin
            publicAccess = 0b
        endelse

        dataviews[i] = $
            obj_new('SpdfDataviewDescription', id, endpointAddress, $
                    title, subtitle, overview, underConstruction, $
                    noticeUrl, publicAccess)
    endfor

    obj_destroy, dvDom

    return, dataviews
end
    

;+
; Gets a description of all the observatory groups.
;
; @keyword dataview {in} {optional} {type=string}
;              name of dataview to access.
; @keyword instrumentTypes {in} {optional} {type=strarr}
;              names of instrument-types which restrict the returned
;              observatory groups to only those supporting the specified
;              instrument-types.  Valid values are those returned by
;              getInstrumentTypes.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword httpErrorReporter {in} {optional} 
;              {type=SpdfHttpErrorReporter}
;              used to report an HTTP error.
; @returns array of SpdfObservatoryGroupDescription objects.
;              If there are no observatory groups, an array of length 
;              one is returned with the first element being a null 
;              object reference.
;-
function SpdfCdas::getObservatoryGroups, $
    dataview = dataview, $
    instrumentTypes = iTypes, $
    authenticator = authenticator, $
    httpErrorReporter = errorReporter
    compile_opt idl2

    if ~keyword_set(dataview) then dataview = self.defaultDataview

    url = self.endpoint + '/dataviews/' + dataview + $
              '/observatoryGroups?'

    for i = 0, n_elements(iTypes) - 1 do begin

        url = url + 'instrumentType=' + iTypes[i] + '&'
    endfor

    url = strmid(url, 0, strlen(url) - 1)

    ogDom = self->makeGetRequest(dataview, url, $
                authenticator = authenticator, $
                errorReporter = errorReporter)

    if ~obj_valid(ogDom) then return, objarr(1)

    ogElements = $
        ogDom->getElementsByTagName('ObservatoryGroupDescription')

    observatoryGroups = objarr(ogElements->getLength(), /nozero)

    for i = 0, ogElements->getLength() - 1 do begin

        ogElement = ogElements->item(i)

        name = $
            self->getNamedElementsFirstChildValue(ogElement, 'Name')

        observatoryIds = $
            self->getNamedElementsFirstChildValue(ogElement, $
                'ObservatoryId')

        observatoryGroups[i] = $
            obj_new('SpdfObservatoryGroupDescription', name, $
                observatoryIds)
    endfor

    obj_destroy, ogDom

    return, observatoryGroups
end
    

;+
; Gets a description of all the instrument types.
;
; @keyword dataview {in} {optional} {type=string}
;              name of dataview to access.
; @keyword observatoryGroups {in} {optional} {type=strarr}
;              names of observatory-groups which restrict the returned
;              instrument types to only those supporting the specified
;              observatory-groups.  Valid values are those returned by
;              getObservatoryGroups.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword httpErrorReporter {in} {optional} 
;              {type=SpdfHttpErrorReporter}
;              used to report an HTTP error.
; @returns array of SpdfInstrumentTypeDescription objects.
;              If there are no intrument types, an array of length one 
;              is returned with the first element being a null object 
;              reference.
;-
function SpdfCdas::getInstrumentTypes, $
    dataview = dataview, $
    observatoryGroups = oGroups, $
    authenticator = authenticator, $
    httpErrorReporter = errorReporter
    compile_opt idl2

    if ~keyword_set(dataview) then dataview = self.defaultDataview

    url = self.endpoint + '/dataviews/' + dataview + '/instrumentTypes?'

    for i = 0, n_elements(oGroups) - 1 do begin

        url = url + 'observatoryGroup=' + oGroups[i] + '&'
    endfor

    url = strmid(url, 0, strlen(url) - 1)

    itDom = self->makeGetRequest(dataview, url, $
                authenticator = authenticator, $
                errorReporter = errorReporter)

    if ~obj_valid(itDom) then return, objarr(1)

    itElements = $
        itDom->getElementsByTagName('InstrumentTypeDescription')

    instrumentTypes = objarr(itElements->getLength(), /nozero)

    for i = 0, itElements->getLength() - 1 do begin

        itElement = itElements->item(i)

        name = $
            self->getNamedElementsFirstChildValue(itElement, 'Name')

        instrumentTypes[i] = $
            obj_new('SpdfInstrumentTypeDescription', name)
    endfor

    obj_destroy, itDom

    return, instrumentTypes
end
    

;+
; Gets a description of all the datasets.
;
; @keyword dataview {in} {optional} {type=string}
;              name of dataview to access.
; @keyword observatoryGroups {in} {optional} {type=strarr}
;              names of observatory-groups which restrict the returned
;              datasets to only those supporting the specified
;              observatory-groups.  Valid values are those returned by
;              getObservatoryGroups.
; @keyword instrumentTypes {in} {optional} {type=strarr}
;              names of instrument-types which restrict the returned
;              datasets to only those supporting the specified
;              instrument-types.  Valid values are those returned by
;              getInstrumentTypes.
; @keyword observatories {in} {optional} {type=strarr}
;              names of observatories which restrict the returned
;              datasets to only those supporting the specified
;              observatories.  Valid values are those returned by
;              getObservatories.
; @keyword instruments {in} {optional} {type=strarr}
;              names of instruments which restrict the returned
;              datasets to only those supporting the specified
;              instruments.  Valid values are those returned by
;              getInstruments.
; @keyword startDate {in} {optional} {type=julday}
;              value that restricts the returned dataset to only
;              those that contain data after this date.
; @keyword stopDate {in} {optional} {type=julday}
;              value that restricts the returned dataset to only
;              those that contain data before this date.
; @keyword identifier {in} {optional} {type=string}
;              a dataset identifier. The value may be a CDAS 
;              (e.g., AC_H2_MFI), DOI (e.g., 10.48322/fh85-fj47),
;              or SPASE [ResourceID] 
;              (e.g., spase://NASA/NumericalData/ACE/MAG/L2/PT1H)
;              identifier. If specified, all other keywords are
;              ignored.
; @keyword idPattern {in} {optional} {type=string}
;              a java.util.regex compatible 
;              <a href="https://en.wikipedia.org/wiki/Regex">regular 
;              expression</a> that must match the dataset's identifier 
;              value.  Omitting this parameter is equivalent to ".*".
; @keyword labelPattern {in} {optional} {type=string}
;              a java.util.regex compatible 
;              <a href="https://en.wikipedia.org/wiki/Regex">regular 
;              expression</a> that must match the dataset's label 
;              text.  Omitting this parameter is equivalent to ".*".
;              Embedded matching flag expressions (e.g., (?i) for 
;              case insensitive match mode) are supported and likely 
;              to be useful in this case.
; @keyword notesPattern {in} {optional} {type=string}
;              a java.util.regex compatible 
;              <a href="https://en.wikipedia.org/wiki/Regex">regular 
;              expression</a> that must match the dataset's notes 
;              text.  Omitting this parameter is equivalent to ".*".
;              Embedded matching flag expressions (e.g., (?i) for 
;              case insensitive match mode) are supported and likely 
;              to be useful in this case.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword httpErrorReporter {in} {optional} 
;              {type=SpdfHttpErrorReporter}
;              used to report an HTTP error.
; @returns array of SpdfDatasetDescription objects.  If there are no
;              datasets, an array of length one is returned with the
;              first element being a null object reference.
;-
function SpdfCdas::getDatasets, $
    dataview = dataview, $
    observatoryGroups = observatoryGroups, $
    instrumentTypes = instrumentTypes, $
    observatories = observatories, $
    instruments = instruments, $
    startDate = startDate, $
    stopDate = stopDate, $
    identifier = identifier, $
    idPattern = idPattern, $
    labelPattern = labelPattern, $
    notesPattern = notesPattern, $
    authenticator = authenticator, $
    httpErrorReporter = errorReporter
    compile_opt idl2

    if ~keyword_set(dataview) then dataview = self.defaultDataview

    url = self.endpoint + '/dataviews/' + dataview + '/datasets?'

    for i = 0, n_elements(observatoryGroups) - 1 do begin

        url = url + 'observatoryGroup=' + observatoryGroups[i] + '&'
    endfor

    for i = 0, n_elements(instrumentTypes) - 1 do begin

        url = url + 'instrumentType=' + instrumentTypes[i] + '&'
    endfor

    for i = 0, n_elements(observatories) - 1 do begin

        url = url + 'observatory=' + observatories[i] + '&'
    endfor

    for i = 0, n_elements(instruments) - 1 do begin

        url = url + 'instrument=' + instruments[i] + '&'
    endfor

    if keyword_set(startDate) then begin

        url = url + 'startDate=' + self->julDay2Iso8601(startDate) + '&'
    endif

    if keyword_set(stopDate) then begin

        url = url + 'stopDate=' + self->julDay2Iso8601(stopDate) + '&'
    endif

    if keyword_set(identifier) then begin

         url = url + 'id=' + identifier+ '&'
    endif

    if keyword_set(idPattern) then begin

         url = url + 'idPattern=' + idPattern + '&'
    endif

    if keyword_set(labelPattern) then begin

         url = url + 'labelPattern=' + labelPattern + '&'
    endif

    if keyword_set(notesPattern) then begin

         url = url + 'notesPattern=' + notesPattern + '&'
    endif

    url = strmid(url, 0, strlen(url) - 1)

    dsDom = self->makeGetRequest(dataview, url, $
                authenticator = authenticator, $
                errorReporter = errorReporter)

    if ~obj_valid(dsDom) then begin

        return, objarr(1)
    endif

    dsElements = $
        dsDom->getElementsByTagName('DatasetDescription')

    datasets = objarr(dsElements->getLength(), /nozero)

    for i = 0, dsElements->getLength() - 1 do begin

        dsElement = dsElements->item(i)

        id = $
            self->getNamedElementsFirstChildValue(dsElement, 'Id')

        doi = $
            self->getNamedElementsFirstChildValue(dsElement, 'Doi')

        resourceId = $
            self->getNamedElementsFirstChildValue(dsElement, 'SpaseResourceId')

        observatories = $
            self->getNamedElementsFirstChildValue(dsElement, $
                'Observatory')

        instruments = $
            self->getNamedElementsFirstChildValue(dsElement, $
                'Instrument')

        observatoryGroups = $
            self->getNamedElementsFirstChildValue(dsElement, $
                'ObservatoryGroup')

        instrumentTypes = $
            self->getNamedElementsFirstChildValue(dsElement, $
                'InstrumentTypes')

        label = $
            self->getNamedElementsFirstChildValue(dsElement, 'Label')

        timeInterval = self->getTimeIntervalChild(dsElement)

        piName = $
            self->getNamedElementsFirstChildValue(dsElement, 'PiName')

        piAffiliation = self->getNamedElementsFirstChildValue($
                            dsElement, 'PiAffiliation')

        notes = $
            self->getNamedElementsFirstChildValue(dsElement, 'Notes')

        linkElements = dsElement->getElementsByTagName('DatasetLink')

        if linkElements->getLength() gt 0 then begin

            datasetLinks = objarr(linkElements->getLength(), /nozero)

            for j = 0, linkElements->getLength() - 1 do begin

                linkElement = linkElements->item(j)

                title = $
                    self->getNamedElementsFirstChildValue(linkElement, $
                        'Title')
                text = $
                    self->getNamedElementsFirstChildValue(linkElement, $
                        'Text')
                url = $
                    self->getNamedElementsFirstChildValue(linkElement, $
                        'Url')

                datasetLinks[j] = $
                    obj_new('SpdfDatasetLink', title, text, url)
            endfor
        endif else begin

            datasetLinks = obj_new()
        endelse

        mdElements = dsElement->getElementsByTagName('AdditionalMetadata')

        if mdElements->getLength() gt 0 then begin

            additionalMd = objarr(mdElements->getLength(), /nozero)

            for j = 0, mdElements->getLength() - 1 do begin

                mdElement = mdElements->item(j)

                attrs = mdElement->GetAttributes()

                typeNode = attrs->getNamedItem('Type')
                type = typeNode->GetNodeValue()
                mdElementChild = mdElement->getFirstChild()
                url = mdElementChild->GetNodeValue()

                additionalMd[j] = $
                    obj_new('SpdfAdditionalMetadata', type, url)
            endfor
        endif else begin

            additionalMd = obj_new()
        endelse

        datasets[i] = $
            obj_new('SpdfDatasetDescription', id, observatories, $
                instruments, observatoryGroups, instrumentTypes, $
                label, timeInterval, piName, piAffiliation, notes, $
                datasetLinks, doi, resourceId, additionalMd)
    endfor

    obj_destroy, dsDom

    return, datasets
end
    

;+
; Gets a description of a dataset's data inventory.
;
; @keyword dataview {in} {optional} {type=string}
;              name of dataview to access.
; @param dataset {in} {type=string}
;              identifies the dataset.  A <a href="https://www.doi.org/">DOI</a>
;              value requires IDL 8.5.1 higher.
; @keyword timeInterval {in} {optional} {type=SpdfTimeInterval}
;              TimeInteval to restrict return inventory.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword httpErrorReporter {in} {optional} 
;              {type=SpdfHttpErrorReporter}
;              used to report an HTTP error.
; @returns an SpdfInventoryDescription or a null reference if no
;              inventory is available.
;-
function SpdfCdas::getInventory, $
    dataview = dataview, dataset, $
    timeInterval = timeInterval, $
    authenticator = authenticator, $
    httpErrorReporter = errorReporter
    compile_opt idl2

    if ~keyword_set(dataview) then dataview = self.defaultDataview

    url = self.endpoint + '/dataviews/' + dataview + '/datasets/' + $
          self->encode(dataset) + '/inventory'

    if keyword_set(timeInterval) then begin

        url = url + '/' + timeInterval.getIso8601Start(basicFormat=1) + $
              ',' + timeInterval.getIso8601Stop(basicFormat=1)
    endif

    inventoryDom = self->makeGetRequest(dataview, url, $
                       authenticator = authenticator, $
                       errorReporter = errorReporter)

    inventoryElements = $
        inventoryDom->getElementsByTagName('InventoryDescription')

    if inventoryElements->getLength() eq 0 then begin

        obj_destroy, inventoryDom

        return, obj_new()
    endif

    inventoryElement = inventoryElements->item(0)

    id = self->getNamedElementsFirstChildValue(inventoryElement, 'Id')

    timeIntervalElements = $
        inventoryElement->getElementsByTagName('TimeInterval')

    if timeIntervalElements->getLength() gt 0 then begin

        timeIntervals = $
            objarr(timeIntervalElements->getLength(), /nozero)

        for i = 0, timeIntervalElements->getLength() - 1 do begin

            timeIntervals[i] = $
                self->getTimeInterval(timeIntervalElements->item(i))
        endfor
    endif else begin

        timeIntervals = obj_new()
    endelse

    obj_destroy, inventoryDom

    return, obj_new('SpdfInventoryDescription', id, timeIntervals)
end
    

;+
; Gets a small example time interval for the specified dataset.  The
; interval is near the end of the dataset's data inventory.  The
; returned interval is not guaranteed to have non-fill data for any
; specific variable.
;
; @keyword dataview {in} {optional} {type=string}
;              name of dataview to access.
; @param dataset {in} {type=string}
;              identifies the dataset.  A <a href="https://www.doi.org/">DOI</a>
;              value requires IDL 8.5.1 higher.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword httpErrorReporter {in} {optional} 
;              {type=SpdfHttpErrorReporter}
;              used to report an HTTP error.
; @returns an SpdfTimeInterval that is likely, but not guaranteed, to 
;     have data or a null reference if no interval is found.
;-
function SpdfCdas::getExampleTimeInterval, $
    dataview = dataview, $
    dataset, $
    authenticator = authenticator, $
    httpErrorReporter = errorReporter
    compile_opt idl2

    inventory = self->getInventory(dataview = dataview, dataset, $
                    authenticator = authenticator, $
                    HttpErrorReporter = errorReporter)

    if obj_valid(inventory) then begin

        intervals = inventory.getTimeIntervals()

        example = intervals[-1]

        if stregex(dataset, 'MMS[1-4]_.+_BRST_.+') ne -1 then begin

            delta = 1.d / 86400.d
        endif else begin

            delta = 2.d / 24.d
        endelse

        example.setStart, example.getStop() - delta

        return, example
    endif 

    return, obj_new()
end


;+
; Gets a description of a dataset's variables.
;
; @keyword dataview {in} {optional} {type=string}
;              name of dataview to access.
; @param dataset {in} {type=string}
;              identifies the dataset.  A <a href="https://www.doi.org/">DOI</a>
;              value requires IDL 8.5.1 higher.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword httpErrorReporter {in} {optional} 
;              {type=SpdfHttpErrorReporter}
;              used to report an HTTP error.
; @returns array of SpdfVariableDescription objects.  If the dataset
;              has no variables, an array of one null object is 
;              returned.
;-
function SpdfCdas::getVariables, $
    dataview = dataview, dataset, $
    authenticator = authenticator, $
    httpErrorReporter = errorReporter
    compile_opt idl2

    if ~keyword_set(dataview) then dataview = self.defaultDataview

    url = self.endpoint + '/dataviews/' + dataview + '/datasets/' + $
          self->encode(dataset) + '/variables'

    varDom = self->makeGetRequest(dataview, url, $
                authenticator = authenticator, $
                errorReporter = errorReporter)

    if ~obj_valid(varDom) then return, objarr(1)

    varElements = $
        varDom->getElementsByTagName('VariableDescription')

    if varElements->getLength() eq 0 then begin

        obj_destroy, varDom

        return, objarr(1)
    endif

    varDescriptions = objarr(varElements->getLength(), /nozero)

    for i = 0, varElements->getLength() - 1 do begin

        varElement = varElements->item(i)

        name = $
            self->getNamedElementsFirstChildValue(varElement, 'Name')

        shortDescription = $
            self->getNamedElementsFirstChildValue(varElement, $
                'ShortDescription')

        longDescription = $
            self->getNamedElementsFirstChildValue(varElement, $
                'LongDescription')

        parent = $
            self->getNamedElementsFirstChildValue(varElement, 'Parent')

        children = $
            self->getNamedElementsFirstChildValue(varElement, $
                'Children')
 
        varDescriptions[i] = $
            obj_new('SpdfVariableDescription', name, shortDescription, $
                longDescription, parent = parent, children = children)
    endfor

    obj_destroy, varDom

    return, varDescriptions
end
    

;+
; Gets the names of a dataset's variables.  This method is like the
; getVariables method except that it only return the variable names and
; not the other metadata in a SpdfVariableDescription object.
;
; @keyword dataview {in} {optional} {type=string}
;              name of dataview to access.
; @param dataset {in} {type=string}
;              identifies the dataset.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword httpErrorReporter {in} {optional} 
;              {type=SpdfHttpErrorReporter}
;              used to report an HTTP error.
; @returns array of strings containing the dataset's variable names.  If 
;              the dataset has no variables, !null is returned.
;-
function SpdfCdas::getVariableNames, $
    dataview = dataview, dataset, $
    authenticator = authenticator, $
    httpErrorReporter = errorReporter
    compile_opt idl2

    descriptions = self->getVariables(dataview=dataview, dataset, $
                                      authenticator=authenticator, $
                                      httpErrorReporter=httpErrorReporter)

    if ~obj_valid(descriptions[0]) then begin

        return, !null
    endif

    names = strarr(n_elements(descriptions))

    for i = 0, n_elements(descriptions) - 1 do begin

        names[i] = descriptions[i]->getName()
    endfor

    obj_destroy, descriptions

    return, names
end
    

;+
; Gets <a href="https://cdf.gsfc.nasa.gov/">Common Data Format</a>
; data from the specified dataset.
;
; @keyword dataview {in} {optional} {type=string}
;              name of dataview to access.
; @param timeIntervals {in} {type=objarr of SpdfTimeIntervals}
;              time intervals of data to get.
; @param dataset {in} {type=string}
;              identifies the dataset from which data is being
;              requested.
; @param variables {in} {type=strarr}
;              names of variable's whose data is being requested.
;              If the first (only) name is "ALL-VARIABLES", then the 
;              resulting CDF will contain all variables.
; @keyword cdfVersion {in} {optional} {type=int}
;              is the CDF file version that any created CDF files 
;              should be (2 or 3). 
; @keyword cdfFormat {in} {optional} {type=string}
;              CDF format of returned data.  Valid values are:
;              Binary, CDFML, GzipCDFML, ZipCDFML.
; @keyword binData {in} {optional} {type=SpdfBinData}
;              data binning parameters to apply to the result file.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword httpErrorReporter {in} {optional} 
;              {type=SpdfHttpErrorReporter}
;              used to report an HTTP error.
; @returns SpdfCdasDataResult object.
;-
function SpdfCdas::getCdfData, $
    dataview = dataview, $
    timeIntervals, $
    dataset, $
    variables, $
    cdfVersion = cdfVersion, $
    cdfFormat = cdfFormat, $
    binData = binData, $
    authenticator = authenticator, $
    httpErrorReporter = errorReporter
    compile_opt idl2

    if ~keyword_set(dataview) then dataview = self.defaultDataview

    sizeTimeIntervals = size(timeIntervals)

    if sizeTimeIntervals[0] eq 0 then begin

        timeIntervals = [timeIntervals]
    endif

    datasetRequest = $
        obj_new('SpdfDatasetRequest', dataset, variables)

    cdfRequest = $
        obj_new('SpdfCdfRequest', timeIntervals, datasetRequest, $
            cdfVersion = cdfVersion, cdfFormat = cdfFormat, $
            binData = binData)

    dataRequest = $
        obj_new('SpdfCdasDataRequest', cdfRequest)

    result = self->getData(dataview = dataview, dataRequest, $
                 authenticator = authenticator, $
                 httpErrorReporter = errorReporter)

    obj_destroy, datasetRequest
    obj_destroy, cdfRequest
    obj_destroy, dataRequest

    return, result
end


;+
; Gets a textual representation of data from the specified dataset.
;
; @keyword dataview {in} {optional} {type=string}
;              name of dataview to access.
; @param timeInterval {in} {type=SpdfTimeInterval}
;              time range of data to get.
; @param dataset {in} {type=string}
;              identifies the dataset from which data is being
;              requested.
; @param variables {in} {type=strarr}
;              names of variable's whose data is being requested.
;              If no names are specified, the data of all variables
;              is returned.
; @keyword compression {in} {optional} {type=int}
;              the type of compression to use on the result file.
;              Valid values are: Uncompressed, Gzip, Bzip2, Zip.
; @keyword format {in} {optional} {type=string} {default='Plain'}
;              format of result file.  Valid values are: Plain, CSV.
; @keyword binData {in} {optional} {type=SpdfBinData}
;              data binning parameters to apply to the result file.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword httpErrorReporter {in} {optional} 
;              {type=SpdfHttpErrorReporter}
;              used to report an HTTP error.
; @returns SpdfCdasDataResult object.
;-
function SpdfCdas::getTextData, $
    dataview = dataview, timeInterval, dataset, variables, $
    compression = compression, $
    format = format, $
    binData = binData, $
    authenticator = authenticator, $
    httpErrorReporter = errorReporter
    compile_opt idl2

    if ~keyword_set(dataview) then dataview = self.defaultDataview

    datasetRequest = $
        obj_new('SpdfDatasetRequest', dataset, variables)

    textRequest = $
        obj_new('SpdfTextRequest', timeInterval, datasetRequest, $
            compression = compression, format = format, binData = binData)

    dataRequest = $
        obj_new('SpdfCdasDataRequest', textRequest)

    result = self->getData(dataview = dataview, dataRequest, $
                 authenticator = authenticator, $
                 httpErrorReporter = errorReporter)

    obj_destroy, datasetRequest
    obj_destroy, textRequest
    obj_destroy, dataRequest

    return, result
end


;+
; Gets a graphical representation of data from the specified dataset.
;
; @keyword dataview {in} {optional} {type=string}
;              name of dataview to access.
; @param timeInterval {in} {type=SpdfTimeInterval}
;              time range of data to get.
; @param datasetRequests {in} {type=objarr of SpdfDatasetRequest}
;              identifies the datasets and variables from which data 
;              is being requested.
; @keyword graphOptions {in} {optional} {type=int}
;              graphing options.  Valid values are:
;              CoarseNoiseFilter, DoubleHeightYAxis, CombineGraphs.
; @keyword imageFormat {in} {optional} {type=strarr}
;              Format options for graph.  Valid values are:
;              GIF, PNG, PS, PDF.
; @keyword binData {in} {optional} {type=SpdfBinData}
;              data binning parameters to apply to the result file.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword httpErrorReporter {in} {optional} 
;              {type=SpdfHttpErrorReporter}
;              used to report an HTTP error.
; @returns SpdfCdasDataResult object.
;-
function SpdfCdas::getGraphData, $
    dataview = dataview, timeInterval, datasetRequests, $
    graphOptions = graphOptions, imageFormat = imageFormat, $
    binData = binData, $
    authenticator = authenticator, $
    httpErrorReporter = errorReporter
    compile_opt idl2

    if ~keyword_set(dataview) then dataview = self.defaultDataview

    graphRequest = $
        obj_new('SpdfGraphRequest', $
            timeInterval, datasetRequests, $
            graphOptions = graphOptions, $
            imageFormats = imageFormats, $
            binData = binData)

    dataRequest = $
        obj_new('SpdfCdasDataRequest', graphRequest)

    result = self->getData(dataview = dataview, dataRequest, $
                 authenticator = authenticator, $
                 httpErrorReporter = errorReporter)

    obj_destroy, graphRequest
    obj_destroy, dataRequest

    return, result
end


;+
; Gets data from the specified dataset and returns it in a 
; <a href="https://spdf.gsfc.nasa.gov/CDAWlib.html">CDAWlib</a>
; structure.
;
; @keyword dataview {in} {optional} {type=string}
;              name of dataview to access.
; @param timeIntervals {in} {type=objarr of SpdfTimeIntervals}
;              time intervals of data to get.
; @param dataset {in} {type=string}
;              identifies the dataset from which data is being
;              requested.
; @param variables {in} {type=strarr}
;              names of variable's whose data is being requested.
;              If the first (only) name is "ALL-VARIABLES", then the 
;              resulting CDF will contain all variables.
; @keyword binData {in} {optional} {type=SpdfBinData}
;              data binning parameters to apply to the result file.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword httpErrorReporter {in} {optional} 
;              {type=SpdfHttpErrorReporter}
;              used to report an HTTP error.
; @keyword keepfiles {in} {optional} {type=boolean} {default=false}
;            The KEEPFILES keyword causes downloaded data files to be
;            retained.  Normally these files are deleted
;            after the data is read into the IDL environment.
; @keyword quiet {in} {optional} {type=boolean} {default=false}
;            Suppresses error messages.
; @keyword verbose {in} {optional} {type=boolean} {default=false}
;            The VERBOSE keyword causes additional status and debugging
;            information to be printed.
; @keyword callback_function {in} {optional} {type=string}
;            this keyword value is the name of the IDL function that
;            is to be called during this retrieval operation.  The
;            callbacks provide feedback to the user about the ongoing
;            operation, as well as provide a method to cancel an
;            ongoing operation. If this keyword is not set, no
;            callback to the caller is made.  For information on
;            creating a callback function, see "Using Callbacks with
;            the IDLnetURL Object" in the IDL documentation.
; @keyword callback_data {in} {optional} {type=reference}
;            this keyword value contains data that is passed to the
;            caller when a callback is made. The data contained in
;            this variable is defined and set by the caller. The
;            variable is passed, unmodified, directly to the caller
;            as a parameter in the callback function. If this keyword
;            is not set, the corresponding callback parameter's value
;            is undefined.
; @returns CDAWlib structure.
;-
function SpdfCdas::getCdawlibData, $
    dataview = dataview, $
    timeIntervals, $
    dataset, $
    variables, $
    binData = binData, $
    authenticator = authenticator, $
    httpErrorReporter = errorReporter, $
    keepfiles = keepfiles, $
    quiet = quiet, $
    verbose = verbose, $
    callback_function = callback_function, $
    callback_data = callback_data
    compile_opt idl2

    dataResults = $
        self->getCdfData(dataview = dataview, timeIntervals, $
                 dataset, variables, binData = binData, $
                 authenticator = authenticator, $
                 httpErrorReporter = errorReporter)

    if ~obj_valid(dataResults) then begin

        return, obj_new()
    endif
    fileDescriptions = dataResults->getFileDescriptions()

    if n_elements(fileDescriptions) gt 0 then begin

        localCdfNames = $
            self->download(fileDescriptions, keepfiles = keepfiles, $
                quiet = quiet, verbose = verbose, $
                callback_function = callback_function, $
                callback_data = callback_data)

        localCdfNames2 = localCdfNames
        allVars = ''
        ; reads data into handles (memory) should be fastest
        data = read_mycdf(allVars, localCdfNames2, all = 1, $
                          /nodata) 

        ; reads data into .dat structure tags
        ; data = read_mycdf(variables, localCdfNames) 

        if n_tags(data) eq 3 && $
           array_equal (tag_names(data), $
               ['DATASET', 'ERROR', 'STATUS']) then begin
 
            if keyword_set(verbose) then begin

                print, 'Error in read_mycdf()'
                print, '  ERROR: ', data.error
                print, '  STATUS: ', data.status
            endif

            obj_destroy, fileDescriptions
            obj_destroy, dataResults

            return, 1
        endif

        newbuf = hsave_struct(data, /nosave) 
        ; don't use the /nosave if you want it saved
        ; to a save file, need to specify a file name though.

        if ~keyword_set(keepfiles) then begin

            file_delete, localCdfNames
        endif
        if keyword_set(verbose) then begin

            if keyword_set(keepfiles) then begin

                print, 'Data sub/superset returned in local CDF file: ', $
                       localCdfNames
            endif
        endif

    endif else begin

        if ~keyword_set(quiet) then begin

            print, 'No data found'
        endif
    endelse

    obj_destroy, fileDescriptions
    obj_destroy, dataResults

    return, newbuf  ; data
end


;+
; Gets data from the specified dataset and returns it in an IDL Hash as
; defined by the 
; <a href="https://spdf.gsfc.nasa.gov/pub/software/cdf/dist/latest/idl/CDF_READCDF.txt">CDF_READCDF</a> 
; function.  Use of this function requires CDF 3.8.1 or higher.  The 
; following is a brief example of what the returned hash looks like:<pre>
; {
;     "CDFInfo": {
;         "FileName": "test",
;         "NumGlobalAttrs": 5,
;         "NumVariableAttrs": 5,
;         "NumVariables": 2,
;         "Format": "SINGLE_FILE",
;         "Majority": "ROW_MAJOR",
;         "Version": "3.8.1",
;         ...
;    },
;     "GlobalAttrs": {
;         "Project": {
;             0: "LWS>Living With a Star"
;         },
;         "Source_name": {
;             0: "RBSP-B>Radiation Belt Storm Probe B"
;         },
;         ... 
;     },
;     "Variables": {
;         "Latitude": {
;             "VarInfo": {
;                 "DataType": "CDF_UINT1",
;                 "NumElems": 1,
;                 "RecVary": "VARY",
;                 "NumDims": 1,
;                 "DimSizes": [3],
;                 ...
;             },
;            "VarAttrs": {
;                 "VALIDMIN": -10,
;                 "VALIDMAX": 90,
;                 ...
;             }
;             "VarData": [[25, 54, 5], [15, 25, 35], [10, 28, 55]]
;         },
;         ...
;     }
; }
; </pre>
;
; @keyword dataview {in} {optional} {type=string}
;              name of dataview to access.
; @param timeIntervals {in} {type=objarr of SpdfTimeIntervals}
;              time intervals of data to get.
; @param dataset {in} {type=string}
;              identifies the dataset from which data is being
;              requested.
; @param variables {in} {type=strarr}
;              names of variable's whose data is being requested.
;              If the first (only) name is "ALL-VARIABLES", then the 
;              resulting CDF will contain all variables.
; @keyword binData {in} {optional} {type=SpdfBinData}
;              data binning parameters to apply to the result file.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword httpErrorReporter {in} {optional} 
;              {type=SpdfHttpErrorReporter}
;              used to report an HTTP error.
; @keyword keepfiles {in} {optional} {type=boolean} {default=false}
;            The KEEPFILES keyword causes downloaded data files to be
;            retained.  Normally these files are deleted
;            after the data is read into the IDL environment.
; @keyword quiet {in} {optional} {type=boolean} {default=false}
;            Suppresses error messages.
; @keyword verbose {in} {optional} {type=boolean} {default=false}
;            The VERBOSE keyword causes additional status and debugging
;            information to be printed.
; @keyword callback_function {in} {optional} {type=string}
;            this keyword value is the name of the IDL function that
;            is to be called during this retrieval operation.  The
;            callbacks provide feedback to the user about the ongoing
;            operation, as well as provide a method to cancel an
;            ongoing operation. If this keyword is not set, no
;            callback to the caller is made.  For information on
;            creating a callback function, see "Using Callbacks with
;            the IDLnetURL Object" in the IDL documentation.
; @keyword callback_data {in} {optional} {type=reference}
;            this keyword value contains data that is passed to the
;            caller when a callback is made. The data contained in
;            this variable is defined and set by the caller. The
;            variable is passed, unmodified, directly to the caller
;            as a parameter in the callback function. If this keyword
;            is not set, the corresponding callback parameter's value
;            is undefined.
; @returns HASH containing the data.
;-
function SpdfCdas::getCdfDataHash, $
    dataview = dataview, $
    timeIntervals, $
    dataset, $
    variables, $
    binData = binData, $
    authenticator = authenticator, $
    httpErrorReporter = errorReporter, $
    keepfiles = keepfiles, $
    quiet = quiet, $
    verbose = verbose, $
    callback_function = callback_function, $
    callback_data = callback_data
    compile_opt idl2

    catch, errorStatus
    if (errorStatus ne 0) then begin

        ; most likely, old CDF without CDF_READCDF

        catch, /cancel
    
        print, !error_state.msg
        return, hash()
    endif
    dataResults = $
        self->getCdfData(dataview = dataview, timeIntervals, $
                 dataset, variables, binData = binData, $
                 authenticator = authenticator, $
                 httpErrorReporter = errorReporter)

    if ~obj_valid(dataResults) then begin

        return, hash()
    endif
    fileDescriptions = dataResults->getFileDescriptions()

    if n_elements(fileDescriptions) gt 0 then begin

        if n_elements(fileDescriptions) gt 1 and $
           ~keyword_set(quiet) then begin

            print, 'WARNING:', n_elements(fileDescriptions), $
                   'CDF files returned.'
            print, 'All but the first will be ignored.'
        endif

        localCdfNames = $
            self->download(fileDescriptions, keepfiles = keepfiles, $
                quiet = quiet, verbose = verbose, $
                callback_function = callback_function, $
                callback_data = callback_data)

        data = cdf_readcdf(localCdfNames[0])

        if ~keyword_set(keepfiles) then begin

            file_delete, localCdfNames
        endif
        if keyword_set(verbose) then begin

            if keyword_set(keepfiles) then begin

                print, 'Data sub/superset returned in local CDF file: ', $
                       localCdfNames
            endif
        endif

    endif else begin

        if ~keyword_set(quiet) then begin

            print, 'No data found'
        endif
    endelse

    obj_destroy, fileDescriptions
    obj_destroy, dataResults

    return, data
end


;+
; Downloads the specified remote files and returns their local filenames.
;
; @private
;
; @param fileDescriptions {in} {type=objarr of SpdfFileDescription}
;            The remote files to download.
; @keyword keepfiles {in} {optional} {type=boolean} {default=false}
;            The KEEPFILES keyword causes downloaded data files to be
;            retained.  Normally these files are deleted
;            after the data is read into the IDL environment.
; @keyword quiet {in} {optional} {type=boolean} {default=false}
;            Suppresses error messages.
; @keyword verbose {in} {optional} {type=boolean} {default=false}
;            The VERBOSE keyword causes additional status and debugging
;            information to be printed.
; @keyword callback_function {in} {optional} {type=string}
;            this keyword value is the name of the IDL function that
;            is to be called during this retrieval operation.  The
;            callbacks provide feedback to the user about the ongoing
;            operation, as well as provide a method to cancel an
;            ongoing operation. If this keyword is not set, no
;            callback to the caller is made.  For information on
;            creating a callback function, see "Using Callbacks with
;            the IDLnetURL Object" in the IDL documentation.
; @keyword callback_data {in} {optional} {type=reference}
;            this keyword value contains data that is passed to the
;            caller when a callback is made. The data contained in
;            this variable is defined and set by the caller. The
;            variable is passed, unmodified, directly to the caller
;            as a parameter in the callback function. If this keyword
;            is not set, the corresponding callback parameter's value
;            is undefined.
; @returns HASH containing the data.
;-
function SpdfCdas::download, $
    fileDescriptions, $
    keepfiles = keepfiles, $
    quiet = quiet, $
    verbose = verbose, $
    callback_function = callback_function, $
    callback_data = callback_data
    compile_opt idl2

    localCdfNames = strarr(n_elements(fileDescriptions))

    if n_elements(fileDescriptions) gt 0 then begin

        cd, current=cwd

        if ~file_test(cwd, /write) then begin

            tmpDir = getenv('IDL_TMPDIR')
            pushd, tmpDir

            if ~keyword_set(quiet) then begin

                print, 'Warning: The current working directory (', $
                       cwd, ') is not writable.'
                print, 'Temporarily changing working directory to ', $
                       tmpDir
                if keyword_set(keepfiles) then begin

                    print, 'Downloaded files will be saved to ', tmpDir
                endif
            endif
        endif

        for i = 0, n_elements(fileDescriptions) - 1 do begin

            if obj_valid(fileDescriptions[i]) then begin

                if keyword_set(callback_function) then begin

                    statusInfo = strarr(1)
                    statusInfo[0] = $
                        string(i + 1, n_elements(fileDescriptions), $
                               fileDescriptions[i]->getName(), $
                               format='(%"Beginning file retrieval (%d) of (%d) (%s)")')

                    progressInfo = lon64arr(5)
                    progressInfo[0] = 0

                    continue = call_function(callback_function, $
                                             statusInfo, $
                                             progressInfo, $
                                             callback_data)
                    if ~continue then begin

                        if n_elements(tmpDir) ne 0 then popd
                        return, localCdfNames
                    endif
                endif
                localCdfNames[i] = $
                    fileDescriptions[i]->getFile($
                        callback_function=callback_function, $
                        callback_data=callback_data, $
                        sslVerifyPeer=self.ssl_verify_peer)
            endif 
        endfor
    endif
    return, localCdfNames
end


;+
; Converts the given Julian Day value to an ISO 8601 string 
; representation.
;
; @private
;
; @param value {in} {type=julDay}
;            Julian day value to convert.
; @returns ISO 8601 string representation of the given value
;-
function SpdfCdas::julDay2Iso8601, $
    value
    compile_opt idl2

    caldat, value, month, day, year, hour, minute, second

    return, string(year, month, day, hour, minute, second, $
                   format=self.iso8601Format)
end


;+
; Make a request to CDAS for the specified data.
;
; @private
;
; @keyword dataview {in} {optional} {type=string}
;              name of dataview to access.
; @param dataRequest {in} {type=SpdfCdasDataRequest}
;              specifies the data to get.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword httpErrorReporter {in} {optional} 
;              {type=SpdfHttpErrorReporter}
;              used to report an HTTP error.
; @returns SpdfCdasDataResult object.
;-
function SpdfCdas::getData, $
    dataview = dataview, $
    dataRequest, $
    authenticator = authenticator, $
    httpErrorReporter = errorReporter
    compile_opt idl2

    if ~keyword_set(dataview) then dataview = self.defaultDataview

    url = self.endpoint + '/dataviews/' + dataview + '/datasets'

    requestDoc = obj_new('IDLffXMLDOMDocument')

    requestDom = $
        requestDoc->appendChild( $
            dataRequest->createDomElement(requestDoc))

    requestDoc->save, string=xmlRequest

    obj_destroy, requestDoc

    dataDoc = self->makePostRequest(dataview, url, xmlRequest, $
                  authenticator = authenticator, $
                  errorReporter = errorReporter)

    if ~obj_valid(dataDoc) then return, obj_new()

    dataResult = self->getDataResult(dataDoc)

    obj_destroy, dataDoc

    return, dataResult
end


;+
; Creates an SpdfCdasDataResult object from the given cdas:DataResult
; XML document.
;
; @private
;
; @param doc {in} {type=IDLffXMLDOMDocument}
;              cdas:DataResult XML document.
; @returns SpdfCdasDataResult object.
;-
function SpdfCdas::getDataResult, $
    doc
    compile_opt idl2

    fileDescriptions = self->getFileDescriptions(doc)

    messages = self->getDataResultText(doc, 'Message')

    warnings = self->getDataResultText(doc, 'Warning')

    statuses = self->getDataResultText(doc, 'Status')

    errors = self->getDataResultText(doc, 'Error')

    dataResult = obj_new('SpdfCdasDataResult', $
        fileDescriptions, $
        messages = messages, warnings = warnings, $
        statuses = statuses, errors = errors)

    return, dataResult
end
    

;+
; Creates SpdfFileDescription object(s) from the FileDescription
; elements in the given cdas:DataResult XML document.
;
; @private
;
; @param doc {in} {type=IDLffXMLDOMDocument}
;              cdas:DataResult XML document.
; @returns objarr of SpdfFileDescription objects.
;-
function SpdfCdas::getFileDescriptions, $
    doc
    compile_opt idl2

    fileDescriptions = obj_new()

    fileElements = doc->getElementsByTagName('FileDescription')

    if fileElements->getLength() gt 0 then begin

        fileDescriptions = objarr(fileElements->getLength(), /nozero)

        for i = 0, fileElements->getLength() - 1 do begin

            fileElement = fileElements->item(i)

            name = $
                self->getNamedElementsFirstChildValue(fileElement, $
                    'Name')

            mimeType = $
                self->getNamedElementsFirstChildValue(fileElement, $
                    'MimeType')

            startTime = $
                self->getJulDate((fileElement->$
                    getElementsByTagName('StartTime'))->item(0))

            endTime = $
                self->getJulDate((fileElement->$
                     getElementsByTagName('EndTime'))->item(0))

            timeInterval = $
                obj_new('SpdfTimeInterval', startTime, endTime)

            length = $
                self->getNamedElementsFirstChildValue(fileElement, $
                    'Length')

            lastModified = $
                self->getNamedElementsFirstChildValue(fileElement, $
                    'LastModified')

            thumbnailDescription = $
                self->getThumbnailDescription(fileElement)

            thumbnailId = self->getThumbnailId(fileElement)

            fileDescriptions[i] = obj_new('SpdfFileDescription', $
                name, mimeType, timeInterval, length, lastModified, $
                thumbnailDescription = thumbnailDescription, $
                thumbnailId = thumbnailId)

            obj_destroy, timeInterval
            obj_destroy, thumbnailDescription
        endfor
    end

    return, fileDescriptions
end


;+
; Creates an SpdfThumbnailDescription object from the FileDescription
; element in the given cdas:DataResult XML document.
;
; @private
;
; @param fileElement {in} {type=IDLffXMLDOMDocument}
;              cdas:FileDescription element from a cdas:DataResult XML 
;              document.
; @returns SpdfThumbnailDescription object.
;-
function SpdfCdas::getThumbnailDescription, $
    fileElement
    compile_opt idl2

    thumbnailDescription = obj_new()

    thumbnailDescriptionElements = $
        fileElement->getElementsByTagName('ThumbnailDescription')

    if thumbnailDescriptionElements->getLength() gt 0 then begin

        thumbnailDescriptionElement = $
            thumbnailDescriptionElements->item(0)

        type = $
            self->getNamedElementsFirstChildValue(thumbnailElement, $
                'Type')

        name = $
            self->getNamedElementsFirstChildValue(thumbnailElement, $
                'Name')

        dataset = $
            self->getNamedElementsFirstChildValue(thumbnailElement, $
                'Dataset')

        timeInterval = self->getTimeIntervalChild(thumbnailElement)

        varName = $
            self->getNamedElementsFirstChildValue(thumbnailElement, $
                'VarName')

        options = $
            self->getNamedElementsFirstChildValue(thumbnailElement, $
                'Options')

        numFrames = $
            self->getNamedElementsFirstChildValue(thumbnailElement, $
                'NumFrames')

        numRows = $
            self->getNamedElementsFirstChildValue(thumbnailElement, $
                'NumRows')

        numCols = $
            self->getNamedElementsFirstChildValue(thumbnailElement, $
                'NumCols')

        titleHeight = $
            self->getNamedElementsFirstChildValue(thumbnailElement, $
                'TitleHeight')

        thumbnailHeight = $
            self->getNamedElementsFirstChildValue(thumbnailElement, $
                'ThumbnailHeight')

        thumbnailWidth = $
            self->getNamedElementsFirstChildValue(thumbnailElement, $
                'ThumbnailWidth')

        startRecord = $
            self->getNamedElementsFirstChildValue(thumbnailElement, $
                'StartRecord')

        myScale = $
            self->getNamedElementsFirstChildValue(thumbnailElement, $
                'MyScale')

        xyStep = $
            self->getNamedElementsFirstChildValue(thumbnailElement, $
                'XyStep')

        thumbnailDescription = obj_new('SpdfThumbnailDescription', $
            type, name, dataset, timeInterval, varName, options, $
            numFrames, numRows, numCols, titleHeight, thumbnailHeight, $
            thumbnailWidth, startRecord, myScale, xyStep)

        obj_destroy, timeInterval
    end

    return, thumbnailDescription
end


;+
; Creates a SpdfTimeInterval object from a child TimeInterval element
; of the given node from a cdas:DataResult XML document.
;
; @private
;
; @param domNode {in} {type=IDLffXMLDOMNode}
;              node from a cdas:DataResult XML document.
; @returns a reference to a SpdfTimeInterval object.
;-
function SpdfCdas::getTimeIntervalChild, $
    domNode
    compile_opt idl2

    timeInterval = obj_new()

    timeIntervalElements = domNode->getElementsByTagName('TimeInterval')

    if timeIntervalElements->getLength() gt 0 then begin

        timeInterval = $
            self->getTimeInterval(timeIntervalElements->item(0))
    end

    return, timeInterval
end


;+
; Creates a SpdfTimeInterval object from the given TimeInterval element
; from a cdas:DataResult XML document.
;
; @private
;
; @param timeIntervalElement {in} {type=IDLffXMLDOMNode}
;              element from a cdas:DataResult XML document.
; @returns a reference to a SpdfTimeInterval object.
;-
function SpdfCdas::getTimeInterval, $
    timeIntervalElement
    compile_opt idl2

    startDate = $
        self->getJulDate((timeIntervalElement->$
                          getElementsByTagName('Start'))->item(0))

    endDate = $
        self->getJulDate((timeIntervalElement->$
                          getElementsByTagName('End'))->item(0))

    
    return, obj_new('SpdfTimeInterval', startDate, endDate)
end


;+
; Creates a julday object from the given time element from a 
; cdas:DataResult XML document.
;
; @private
;
; @param dateTimeElement {in} {type=IDLffXMLDOMNodeList}
;              list whose first child is to be converted into a julday
; @returns julday representation of first child of given 
;              dateTimeElement.
;-
function SpdfCdas::getJulDate, $
    dateTimeElement
    compile_opt idl2

    dateFormat='(I4, 1X, I2, 1X, I2, 1X, I2, 1X, I2, 1X, I2)'

    dateTimeStr = (dateTimeElement->getFirstChild())->getNodeValue()

    reads, dateTimeStr, format=dateFormat, $
            year, month, day, hour, minute, second

    return, julday(month, day, year, hour, minute, second)
end


;+
; Gets the ThumbnailId value from the given cdas:FileDescription 
; element.
;
; @private
;
; @param fileElement {in} {type=IDLffXMLDOMNode}
;              cdas:FileDescription node.
; @returns ThumbnailId string value.
;-
function SpdfCdas::getThumbnailId, $
    fileElement
    compile_opt idl2

    return, self->getNamedElementsFirstChildValue(fileElement, $
                'ThumbnailId')
end


;+
; Gets the specified node values from the given cdas:DataResult XML 
; document.
;
; @private
;
; @param resultDoc {in} {type=IDLffXMLDOMDocument}
;            cdas:DataResult XML document.
; @param type {in} {type=string}
;            name of elements whose value is to be gotten.  This is
;            expected to be one of Message, Warning, Status, or Error.
; @returns strarr of the specified node values from the given document.
;-
function SpdfCdas::getDataResultText, $
    resultDoc, type

    return, self->getNamedElementsFirstChildValue(resultDoc, type)
end


;+
; Perform an HTTP GET request to the given URL.  This method provides 
; functionality similar to doing    
;     obj_new('IDLffXMLDOMDocument', filename=url)
; except that this method will catch an authorization error (401),
; call the supplied authenticator function, and then retry the
; request with the authentication credentials obtained from the
; call to the callers authentication function.
;
; @private
;
; @param dataview {in} {type=string}
;            name of dataview to access.
; @param url {in} {type=string}
;            URL of GET request to make.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword errorReporter {in} {optional} {type=string}
;              name of IDL procedure to call if an HTTP error occurs.
; @returns reference to IDLffXMLDOMDocument representation of HTTP
;     response entity.
;-
function SpdfCdas::makeGetRequest, $
    dataview, url, $
    authenticator = authenticator, $
    errorReporter = errorReporter
    compile_opt idl2

    username = ''
    password = ''
    retries = 0

    catch, errorStatus
    if (errorStatus ne 0) then begin

        reply = $
            self->handleHttpError( $
                requestUrl, dataview, username, password, $
                authenticator = authenticator, $
                errorReporter = errorReporter)

        obj_destroy, requestUrl

        if reply eq 0 || retries gt self.retryLimit then begin

            catch, /cancel
            return, obj_new()
        endif

        retries = retries + 1

    endif

    requestUrl = self->getRequestUrl(url, username, password)

; The following stopped working so don't bother (for now).
;    contentType = self->getResponseHeader(requestUrl, 'Content-Type')
;
;    if (strlowcase(contentType) ne 'application/xml') then begin
;
;        ; Probably an html error page
;
;        obj_destroy, requestUrl
;
;        return, obj_new()
;    endif

    result = string(requestUrl->get(/buffer))

    obj_destroy, requestUrl

    return, obj_new('IDLffXMLDOMDocument', string=result)
end


;+
; Perform an HTTP POST request to the given URL.  If an authorization
; error (401) occurs, the supplied authenticator function is called, 
; and then the request is retried with the authentication credentials 
; obtained from the call to the callers authentication function.
;
; @private
;
; @param dataview {in} {type=string}
;            name of dataview to access.
; @param url {in} {type=string}
;            URL of GET request to make.
; @param xmlRequest {in} {type=string}
;            XML entity body to be include in the request.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword errorReporter {in} {optional} {type=string}
;              name of IDL procedure to call if an HTTP error occurs.
; @returns reference to IDLffXMLDOMDocument representation of HTTP
;     response entity.
;-
function SpdfCdas::makePostRequest, $
    dataview, url, xmlRequest, $
    authenticator = authenticator, $
    errorReporter = errorReporter
    compile_opt idl2

    username = ''
    password = ''
    retries = 0

    catch, errorStatus
    if (errorStatus ne 0) then begin

        reply = $
            self->handleHttpError( $
                requestUrl, dataview, username, password, $
                authenticator = authenticator, $
                errorReporter = errorReporter)

        obj_destroy, requestUrl

        if reply eq 0 || retries gt self.retryLimit then begin

            catch, /cancel
            return, obj_new()
        endif

        retries = retries + 1

    endif

    requestUrl = self->getRequestUrl(url, username, password)

    requestUrl->setProperty, header='Content-Type: application/xml'

; print, 'POSTing ', xmlRequest
; print, 'POSTing to ', url
; requestUrl.GetProperty, ssl_verify_peer=sslVerifyPeer
; print, 'ssl_verify_peer =', sslVerifyPeer
; requestUrl.SetProperty, ssl_verify_host=0
; requestUrl.GetProperty, ssl_verify_host=sslVerifyHost
; print, 'ssl_verify_host =', sslVerifyHost

    result = requestUrl->put(xmlRequest, /buffer, /post, url=url)

    obj_destroy, requestUrl

    return, obj_new('IDLffXMLDOMDocument', filename=result)
end


;+
; Function to get an HTTP response header.
;
; @private
;
; @param request {in} {type=IDLnetURL}
;            HTTP request.
; @param header {in} {type=string}
;            name of response header to get.
; @returns the requested response header value or '' if it does not 
;     exist.
;-
function SpdfCdas::getResponseHeader, $
    request, header
    compile_opt idl2

    request->getProperty, response_header=responseHeader

    headers = strsplit(responseHeader, string(10B), /extract)

    for i = 0, n_elements(headers) - 1 do begin
        value = strsplit(headers[i], ':', /extract)
        if (strlowcase(value[0]) eq strlowcase(header)) then return, value[1]
    endfor

    return, ''
end


;+
; Function to handle HTTP request errors.  If an authorization error
; (401) has occurred and an authenticator is provided, the given
; authenticator is called to obtain authentication credentials.
; If 429 or 503 with a Retry-After header has occurred, execution is
; suspended for the Retry-After value.
; For any other error, if an errorReporter has been provided, it is
; called.
;
; @private
;
; @param request {in} {type=IDLnetURL}
;            HTTP request that caused the error.
; @param dataview {in} {type=string}
;            name of dataview to access.
; @param username {out} {type=string}
;            username value obtained by calling the given authenticator.
; @param password {out} {type=string}
;            password value obtained by calling the given authenticator.
; @keyword authenticator {in} {optional} {type=SpdfAuthenticator}
;              authenticator that is used when a dataview requiring
;              authentication is specified.
; @keyword errorReporter {in} {optional} {type=string}
;              name of IDL procedure to call if an HTTP error occurs.
; @returns a value of 1 corrective action has occurred and a value of
;     0 if not.  The corrective action for a 401 is setting the username 
;     and password.  Corrective action for a 429/503 is waiting the
;     specified time.  The corrective action for a 408 is to wait 30
;     seconds.
;-
function SpdfCdas::handleHttpError, $
    request, dataview, username, password, $
    authenticator = authenticator, $
    errorReporter = errorReporter
    compile_opt idl2

    request->getProperty, $
        response_code=responseCode, $
        response_header=responseHeader, $
        response_filename=responseFilename

    if responseCode eq 401 && $
       keyword_set(authenticator) then begin

        reply = $
            call_method('getCredentials', authenticator, $
                dataview, username, password)

        if reply eq 0 then begin

            return, 0
        endif
    endif else if responseCode eq 429 || responseCode eq 503 then begin

        retryAfter = stregex(responseHeader, $
                             'Retry-After: ([0-9]+)' + string(13b), $
                             /extract, /subexpr)

        if n_elements(retryAfter) eq 2 && $
           strlen(retryAfter[1]) gt 0 then begin

            wait, fix(retryAfter[1])
        endif

    endif else if responseCode eq 408 then begin

        wait, 30

    endif else begin

        if keyword_set(errorReporter) then begin

            call_method, 'reportError', errorReporter, $
                    responseCode, responseHeader, responseFilename
        endif

        return, 0
    endelse

    return, 1
end


;+
; Create an IDLnetURL object from the given URL with any supplied
; authentication values set.
;
; @private
;
; @param url {in} {type=string}
;            URL.
; @param username {in} {type=string}
;            username.
; @param password {in} {type=string}
;            password.
; @returns reference to a IDLnetURL with any supplied authentication
;     values set.
;-
function SpdfCdas::getRequestUrl, $
    url, username, password
    compile_opt idl2

    requestUrl = $
        obj_new('IDLnetURL', $
                proxy_authentication = $
                    self.proxySettings.getAuthentication(), $
                proxy_hostname = self.proxySettings.getHostname(), $
                proxy_port = self.proxySettings.getPort(), $
                proxy_username = self.proxySettings.getUsername(), $
                proxy_password = self.proxySettings.getPassword(), $
                ssl_verify_peer = self.ssl_verify_peer)

    urlComponents = parse_url(url)

    requestUrl->setProperty, $
        header=self.userAgent, $
        url_scheme=urlComponents.scheme, $
        url_host=urlComponents.host, $
        url_port=urlComponents.port, $
        url_path=urlComponents.path, $
        url_query=urlComponents.query

    if username ne '' then begin

        requestUrl->setProperty, $
            authentication=3, $
            url_username=username, $
            url_password=password
    endif

    return, requestUrl
end


;+
; Defines the SpdfCdas class.
;
; @field endpoint URL of CDAS web service.
; @field defaultDataview CDAS dataview to access when the dataview is
;               not specified.
; @field userAgent HTTP 
;            <a href="https://tools.ietf.org/html/rfc2616#section-14.43">
;               user-agent value</a> to use in communications with CDAS.
; @field version identifies the version of this class.
; @field currentVersionUrl URL to the file identifying the most up to 
;            date version of this class.
; @field proxySettings HTTP proxy settings to use.
; @field ssl_verify_peer IDLnetURL SSL_VERIFY_PEER property value.
; @field retryLimit retry limit for requests that fail with an http
;            status of 429 or 503 with a Retry-After header.
;-
pro SpdfCdas__define
    compile_opt idl2
    struct = { SpdfCdas, $
        endpoint:'', $
        userAgent:'', $
        defaultDataview:'', $
        version:'', $
        currentVersionUrl:'', $
        proxySettings:obj_new(), $
        ssl_verify_peer:1, $
        retryLimit:100 $
    }
end
