Extending Electronic Reporting Sources: Integration with OneDrive (II/II)

To implement the logic, we will create the following classes:

  1. TST2_OneDriveInboundFile: This class defines a custom inbound file and implements the interface ERIImportFile.
  2. TST2_OneDriveInboundFileSource: This class is intended to read and import files. However, we will use it primarily to call TST2_OneDriveAPI, ensuring a better separation of concerns. It implements the ERIFileSource interface.
  3. TST2_OneDriveInboundFileSourceSettings: This class manages, packs, and unpacks source settings. It also moves files to the destination folder. It implements both SysPackable and ERIImportFileSourceSettings.
  4. TST2_OneDriveEROrig_EventHandlers: Event handlers for the source form.
  5. TST2_OneDriveAPI: This class handles OneDrive API interactions, including authentication, file reading, and file movement.
  6. TST2_OneDriveApi_ResponseDataContract and TST2_OneDriveApi_RefreshTokenJson: These classes are used for JSON deserialization.

There are a lot of classes, each with a bunch of methods, but it’s not as complicated as it looks. Most of the methods are just required because we’re implementing an interface, and they’re basically one-liners that I copied from Microsoft’s example. I’ll try to explain the code the best I can, focusing on the parts I actually adapted.

using Microsoft.Dynamics365.LocalizationFramework;
using SL = Microsoft.Dynamics365.LocalizationFramework.XppSupportLayer;
using System.IO;

public class TST2_OneDriveInboundFile implements ERIImportFile
{
    private str folderOrigin;
    private System.IO.Stream fileContent;
    private str additionalInfo;
    private str fileId;
    private str name;
    private SL.utcdatetime createdDateTime;
    private SL.utcdatetime modifiedDateTime;
    private str sourceSettingsKey;

    /// Initializes the object with file metadata and content 
    ///and sets the source settings key.

    protected void new(
        str _fileId,
        str _name,
        utcdatetime _dateTime,
        str _folderOrigin,
        Stream _fileContent
        )
    {
        fileId = _fileId;
        name = _name;
        createdDateTime = ERLFConvert::fromUtcdatetime(_dateTime);
        modifiedDateTime =  ERLFConvert::fromUtcdatetime(_dateTime);
        folderOrigin = _folderOrigin;
        fileContent = _fileContent;
        /// Sets the source settings key, which uniquely identifies the custom source. 
        /// Uses the class name (TST2_OneDriveInboundFileSourceSettings).
        this.set_SourceSettingsKey(TST2_OneDriveInboundFileSourceSettings::SettingsKey);

    }

    [Hookable(false)]
    public str get_SourceSettingsKey()
    {
        return sourceSettingsKey;
    }

    [Hookable(false)]
    public void set_SourceSettingsKey(str _sourceSettingsKey = sourceSettingsKey)
    {
        sourceSettingsKey = _sourceSettingsKey;
    }

    [Hookable(false)]
    public str parmAdditionalInfo(str _additionalInfo = additionalInfo)
    {
        additionalInfo = _additionalInfo;
        return additionalInfo;
    }

    /// Static method to construct a new instance of TST2_OneDriveInboundFile.
    public static TST2_OneDriveInboundFile construct(str _fileId,
        str _name,
        utcdatetime _dateTime,
        str _folderOrigin,
        Stream _fileContent
        )
    {
        return new TST2_OneDriveInboundFile(_fileId, _name, _dateTime, _folderOrigin, _fileContent);
    }

    [Hookable(false)]
    public void set_FileId(str _fileId = fileId)
    {
        fileId = _fileId;
    }

    [Hookable(false)]
    public void set_Name(str _name = name)
    {
        name = _name;
    }

    [Hookable(false)]
    public str get_Name()
    {
        return name;
    }

    [Hookable(false)]
    public SL.utcdatetime get_CreatedDateTime()
    {
        return createdDateTime;
    }

    [Hookable(false)]
    public void set_CreatedDateTime(SL.utcdatetime _createdDateTime = createdDateTime)
    {
        createdDateTime = _createdDateTime;
    }

    [Hookable(false)]
    public void set_ModifiedDateTime(SL.utcdatetime _modifiedDateTime = modifiedDateTime)
    {
        modifiedDateTime = _modifiedDateTime;
    }

    public ERFileLoadResult Load()
    {
        return ERFileLoadResult::Success(fileContent);
    }

    public ERFileDeleteResult Delete()
    {
        return ERFileDeleteResult::Success();
    }

    [Hookable(false)]
    public str GetFileId()
    {
        return fileId;
    }

    [Hookable(false)]
    public SL.utcdatetime GetModifiedDateTime()
    {
        return modifiedDateTime;
    }

}
using Microsoft.Dynamics365.LocalizationFramework;
using System.IO;

public class TST2_OneDriveInboundFileSource implements ERIFileSource
{
    private str folderOrigin;
    /// A file mask is used to filter the types of files we want to read.
    private str fileMask;
    /// The ID of the record that stores the connection credentials.
    private str oneDriveCon;

    /// Initializes the object with the necessary properties
    protected void new(str _folderOrigin, str _fileMask, str _oneDriveCon)
    {
        folderOrigin = _folderOrigin;
        fileMask = _fileMask;
        oneDriveCon = _oneDriveCon;
    }

    /// Static method to construct a new instance of TST2_OneDriveInboundFileSource.
    internal static TST2_OneDriveInboundFileSource construct(str _folderOrigin, str _fileMask, str _oneDriveCon)
    {
        return new TST2_OneDriveInboundFileSource(_folderOrigin, _fileMask, _oneDriveCon);
    }

    public str parmFolder(str _value = folderOrigin)
    {
        folderOrigin = _value;
        return folderOrigin;
    }

    /// This method can be problematic. It is called each time ER runs
    /// with this source, or when the files in the source are refreshed.
    /// It reads (but does not import) all the files, which can cause
    /// performance issues. To avoid this, I functionally 
    /// limit the number of files in the source. After reading a file,
    /// I make sure it moves out of the folder.
    [Hookable(false)]    
    public ERIFiles GetFiles()
    {       
        
        TST2_OneDriveAPI tst2_OneDriveAPI;
        container fileNames;

        if (fileMask == '')
        {
            fileMask = '*.*';
        }
        
        var ret = new ERFiles();
        /// Initializes the connection with the OneDrive API.
        tst2_OneDriveAPI = new TST2_OneDriveAPI(oneDriveCon);

        /// Populates the container with the names of the files from the source folder.
        fileNames = tst2_OneDriveAPI.namesOfFiles(folderOrigin);

        /// Loops the files
        for (int i = 1; i <= conLen(fileNames); i++)
        {
            /// Checks if the current file name matches the file mask.
            if (!fileMask || ERFileNameUtils::IsMaskMatched(conPeek(fileNames, i), fileMask))
            {

                var localStream = new System.IO.MemoryStream();
                System.IO.Stream base = tst2_OneDriveAPI.readFile(folderOrigin, conPeek(fileNames, i));

                /// Transfers data to memory stream
                base.CopyTo(localStream);

                if (localStream.CanSeek)
                {
                    ///  Moves the stream's internal pointer back to the start (position 0).
                    localStream.Seek(0, System.IO.SeekOrigin::Begin);
                }
            
                    ERIFile file = TST2_OneDriveInboundFile::construct(conPeek(fileNames, i), conPeek(fileNames, i), DateTimeUtil::getSystemDateTime(), folderOrigin, localStream);
                    ret.Add(file);
            }
        }          
    
        
        return ret;
    }

}
using Microsoft.Dynamics365.LocalizationFramework;
using System.IO;

public class TST2_OneDriveInboundFileSourceSettings implements SysPackable, ERIImportFileSourceSettings
{    
    /// The key and version are used to identify stored field values 
    /// with a destination. If at any point we modify the fields in the code, 
    /// we should increase the version to avoid unpacking errors.
    internal const str SettingsKey = classStr(TST2_OneDriveInboundFileSourceSettings);    
    public const int CurrentVersion = 1;

    private boolean isEnabled;
    private str folderOrigin;
    private str folderOk;
    private str folderWarn;
    private str folderError;
    private str oneDriveCon;

    [Hookable(false)]
    public str parmOneDriveCon(str _value = oneDriveCon)
    {
        oneDriveCon = _value;
        return oneDriveCon;
    }

    [Hookable(false)]
    public str parmFolderOrigin(str _value = folderOrigin)
    {
        folderOrigin = _value;
        return folderOrigin;
    }

    [Hookable(false)]
    public str parmFolderOk(str _value = folderOk)
    {
        folderOk = _value;
        return folderOk;
    }

    [Hookable(false)]
    public str parmFolderWarn(str _value = folderWarn)
    {
        folderWarn = _value;
        return folderWarn;
    }

    [Hookable(false)]
    public str parmFolderError(str _value = folderError)
    {
        folderError = _value;
        return folderError;
    }

    [Hookable(false)]
    public boolean parmIsEnabled(boolean _value = isEnabled)
    {
        isEnabled = _value;
        return isEnabled;
    }

    [Hookable(false)]
    public void setEnabled(boolean _value)
    {
        isEnabled = _value;
    }

    [Hookable(false)]
    public boolean isEnabled()
    {
        return isEnabled;
    }

    [Hookable(false)]
    public str getName()
    {
        /// Name of settings of a custom source.
        /// This name will be presented in the dialog box that is offered at runtime 
        /// for user to adjust configured settings. Better use a label ;)        
        return "One Drive";
    }

    [Hookable(false)]
    public boolean validate()
    {
        return true;
    }

    [Hookable(false)]
    public str getKey()
    {
        return SettingsKey;
    }

    /// Serialize the fields into a container
    [Hookable(false)]
    public container pack()
    {
        return [CurrentVersion, isEnabled, folderOrigin, folderOk, folderWarn, folderError, oneDriveCon];
    }

    /// Deserialize the fields from a container
    [Hookable(false)]
    public boolean unpack(container packedClass)
    {
        int version = RunBase::getVersion(packedClass);
        switch (version)
        {
            case CurrentVersion:
                [version, isEnabled, folderOrigin, folderOk, folderWarn, folderError, oneDriveCon] = packedClass;
                return true;
            default:
                return false;
        }
    }

    public ERIFileSource createFileSource(str _fileMask)
    {
        return TST2_OneDriveInboundFileSource::construct(folderOrigin, _fileMask, oneDriveCon);
    }

    /// Handles the process after a file is imported
    public void fileImported(ERFileImportedArgs _args)
    {
        TST2_OneDriveAPI oneDriveAPI = new TST2_OneDriveAPI(oneDriveCon);
        ERIImportFile file = _args.getFile();
        str finalFolder = "";
        boolean movedSuccessfully = true;

        /// The destination will vary depending on if ER showed any warnings or errors
        var destination = _args.IsImportOperationSuccessful() ?
                            (_args.hasWarnings() ?
                                ERFormatFileDestinationAfterImport::Warning :
                                ERFormatFileDestinationAfterImport::Archive) :
                                ERFormatFileDestinationAfterImport::Quarantine;
        
        switch(destination)
        {
            case(ERFormatFileDestinationAfterImport::Archive):
                finalFolder = folderOk;
                break;
            case(ERFormatFileDestinationAfterImport::Warning):
                finalFolder = folderWarn;
                break;
            case(ERFormatFileDestinationAfterImport::Quarantine):
                finalFolder = folderError;
                break;
        }

        try
        {
            /// Calls the OneDrive API to move the file to the appropriate folder.
            oneDriveAPI.moveFile(folderOrigin, finalFolder, file.Name);
        }
        catch
        {
            movedSuccessfully = false;
        }

        if (finalFolder != "")
        {
            ttsbegin;
            _args.getState().markAsMoved();

            if (movedSuccessfully)
            {
                _args.getState().getLogger().logMoved(false, strFmt("File Moved",finalFolder), strFmt("File Moved",finalFolder));
            }
            else
            {
                _args.getState().getLogger().logMoved(true, strFmt("File can't be moved'",finalFolder), strFmt("File can't be moved'",finalFolder));
                Error(strFmt("File can't be moved'",finalFolder));
            }

            ttscommit;
        }

        /// delete file
        ttsbegin;
        _args.getFile().Delete();
        _args.getState().markAsDeleted();
        ttscommit;
    }

    
    /// Create new instance of <c>ERImportFormatSourceFolderSettings</c> from packed presentation.   
    internal static TST2_OneDriveInboundFileSourceSettings create(container _packedClass)
    {
        var settings = new TST2_OneDriveInboundFileSourceSettings();
        settings.unpack(_packedClass);
        return settings;
    }
   
    /// (verbatim from Microsoft example)
    /// This code and code below presents the sample how you can memorize additional info about an imported file
    /// in the extended field of the ERImportFormatFileSourceStateTable table.        
    [SubscribesTo(tableStr(ERImportFormatFileSourceStateTable), delegateStr(ERImportFormatFileSourceStateTable, objectInitialized))]
    public static void ERImportFormatFileSourceStateTable_objectInitialized(ERImportFormatFileSourceStateTable _record, ERIImportFile _file)
    {
        anytype file    =_file;
        var folderFile  = file as TST2_OneDriveInboundFile;
        if (folderFile  != null)
        {
            /// folderFile.parmAdditionalInfo(_record.additionalInfo);
        }
    }
    
    /// (verbatim from Microsoft example)
    [SubscribesTo(tableStr(ERImportFormatFileSourceStateTable), delegateStr(ERImportFormatFileSourceStateTable, tableRecordInitialized))]
    public static void ERImportFormatFileSourceStateTable_tableRecordInitialized(ERImportFormatFileSourceStateTable _record, ERIImportFile _file)
    {
        anytype file    =_file;
        var folderFile  = file as TST2_OneDriveInboundFile;
        if (folderFile  != null)
        {
            /// _record.AdditionalInfo = folderFile.parmAdditionalInfo();
        }
    }
}
using Microsoft.Dynamics365.LocalizationFramework;

public class TST2_OneDriveEROrig_EventHandlers
{
    /// Initializes the form controls with stored settings when the form is opened.
    [PostHandlerFor(formStr(ERImportFormatSourceSettings), formMethodStr(
        ERImportFormatSourceSettings, init))]
    public static void ERImportFormatSourceSettings_Post_init(XppPrePostArgs args)
    {
        FormRun formRun                                     = args.getThis();
        ERIImportFileSourceSettingsStorage settingsStorage  = args.getThis();
        
        var settings = TST2_OneDriveEROrig_EventHandlers::getSaveInFolderSettings(settingsStorage);

        FormCheckBoxControl enabledControl = formRun.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveEnable));
        enabledControl.checked(settings.isEnabled());

        FormStringControl oneDriveCon = formRun.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveCon));
        oneDriveCon.text(settings.parmOneDriveCon());
        oneDriveCon.Enabled(settings.isEnabled());

        FormStringControl folderPathControlOrigin = formRun.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveOriginFolderId));
        folderPathControlOrigin.text(settings.parmFolderOrigin());
        folderPathControlOrigin.Enabled(settings.isEnabled());

        FormStringControl folderControlOK = formRun.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveOKFolderId));
        folderControlOK.text(settings.parmFolderOk());
        folderControlOK.Enabled(settings.isEnabled());

        FormStringControl folderControlWarn = formRun.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveWRFolderId));
        folderControlWarn.text(settings.parmFolderWarn());
        folderControlWarn.Enabled(settings.isEnabled());

        FormStringControl folderControlError = formRun.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveERRFolderId));
        folderControlError.text(settings.parmFolderError());
        folderControlError.Enabled(settings.isEnabled());
    }

    /// Stores the form settings when the user closes the form
    [PreHandlerFor(formStr(ERImportFormatSourceSettings), formMethodStr(
        ERImportFormatSourceSettings, closeOk))]
    public static void ERFormatDestinationSettings_Pre_closeOk(XppPrePostArgs args)
    {
        FormRun formRun                                     = args.getThis();
        ERIImportFileSourceSettingsStorage settingsStorage  = args.getThis();
        
        var settings = TST2_OneDriveEROrig_EventHandlers::getSaveInFolderSettings(settingsStorage);

        FormCheckBoxControl enabledControl = formRun.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveEnable));
        settings.setEnabled(enabledControl.checked());

        FormStringControl oneDriveCon = formRun.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveCon));
        settings.parmOneDriveCon(oneDriveCon.text());

        FormStringControl folderControlOrigin = formRun.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveOriginFolderId));
        settings.parmFolderOrigin(folderControlOrigin.text());

        FormStringControl folderControlOK = formRun.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveOKFolderId));
        settings.parmFolderOk(folderControlOK.text());

        FormStringControl folderControlWarn = formRun.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveWRFolderId));
        settings.parmFolderWarn(folderControlWarn.text());

        FormStringControl folderControlError = formRun.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveERRFolderId));
        settings.parmFolderError(folderControlError.text());
    }

    /// Toggles the enabled state of form elements based on the user's selection 
    /// of the Enabled checkbox.
    [FormControlEventHandler(formControlStr(
        ERImportFormatSourceSettings, TST2_OneDriveEnable), FormControlEventType::Clicked)]
    public static void TST2_OneDriveOrig_OnClicked(FormControl sender, FormControlEventArgs e)
    {
        FormRun element                 = sender.formRun();
        FormCheckBoxControl control     = element.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveEnable));
        Boolean checked                 = control.checked();

        FormStringControl oneDriveCon  = element.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveCon));
        oneDriveCon.Enabled(checked);

        FormStringControl folderControlOrigin = element.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveOriginFolderId));
        folderControlOrigin.Enabled(checked);

        FormStringControl folderControlOK = element.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveOKFolderId));
        folderControlOK.Enabled(checked);

        FormStringControl folderControlWarn = element.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveWRFolderId));
        folderControlWarn.Enabled(checked);

        FormStringControl folderControlError = element.design().controlName(
            formControlStr(ERImportFormatSourceSettings, TST2_OneDriveERRFolderId));
        folderControlError.Enabled(checked);
    }

    /// Retrieves the saved settings from storage, or creates new settings if none exist.
    private static TST2_OneDriveInboundFileSourceSettings getSaveInFolderSettings(ERIImportFileSourceSettingsStorage _settingsStorage)
    {
        TST2_OneDriveInboundFileSourceSettings ret = _settingsStorage.getSettingsByKey(TST2_OneDriveInboundFileSourceSettings::SettingsKey);
        if (ret == null)
        {
            ret = new TST2_OneDriveInboundFileSourceSettings();
            _settingsStorage.addSettings(ret);
        }
        return ret;
    }

    /// Transforms the TST2_OneDriveCon text field into a lookup field that 
    /// displays available connections from a custom table with connection 
    /// information and passwords.
    [FormControlEventHandler(formControlStr(ERImportFormatSourceSettings,
                TST2_OneDriveCon), FormControlEventType::Lookup)]
    public static void TST2_OneDriveCon_OnLookup(
                                        FormControl sender,
                                        FormControlEventArgs e)
    {
        Query query = new Query();
        QueryBuildDataSource dataSource = query.addDataSource(tableNum(TST2_OneDriveCon));
        dataSource.fields().addField(fieldNum(TST2_OneDriveCon, Id));
                        
        var lookup = SysTableLookup::newParameters(tableNum(TST2_OneDriveCon), sender);
        lookup.parmQuery(query);
        lookup.addLookupfield(fieldNum(TST2_OneDriveCon, Id), true);
        lookup.performFormLookup();
    }

}
class TST2_OneDriveAPI
{
    /// The token and the connection record will have a class scope
    private TST2_OneDriveCon oneDriveCon;
    private str token;

    /// When creating a new instance of the class,
    /// it will automatically find the connection record
    /// and retrieve the token.
    public void new(str _connectionTableId)
    {
        oneDriveCon = TST2_OneDriveCon::find(_connectionTableId);
        token = this.getToken();        
    }

    public str getToken()
    {
        /// Define the endpoint URL
        str url = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
    
        /// Define the parameters
        str grant_type = "refresh_token";
        str client_id = oneDriveCon.AppClientId;
        str client_secret = oneDriveCon.TST2_AppSecretEdit(false,"");
        str response_type = "code";
        str response_mode = "query";
        str scope = "https://graph.microsoft.com/.default";
        str refresh_token = oneDriveCon.TST2_RefreshTokenEdit(false,"");

        /// Create the form encoded content
        str postData = strFmt(
            "grant_type=%1&client_id=%2&client_secret=%3&response_type=%4&response_mode=%5&scope=%6&refresh_token=%7",
                          grant_type, client_id, client_secret, response_type, response_mode, scope, refresh_token);

        /// Initialize the HttpClient
        System.Net.Http.HttpClient httpClient = new System.Net.Http.HttpClient();
        System.Net.Http.HttpContent content = new System.Net.Http.StringContent(
            postData, System.Text.Encoding::UTF8, "application/x-www-form-urlencoded");

        /// Send the request and get the response
        System.Net.Http.HttpResponseMessage response = httpClient.PostAsync(url, content).Result;

        /// Read the response content
        str responseContent = response.Content.ReadAsStringAsync().Result;

        TST2_OneDriveApi_RefreshTokenJson xppObject = FormJsonSerializer::deserializeObject(
                                                        classNum(TST2_OneDriveApi_RefreshTokenJson),
                                                        responseContent);

        return xppObject.parmaccessToken();

    }

    /// Sends the file from the stream to OneDrive
    /// We are not using this method in the current development
    public void sendFile(System.IO.Stream _stream  , str _folderId, str _filename)
    {
        str url = strFmt("https://graph.microsoft.com/v1.0/me/drive/items/%1:/%2:/content", _folderId, _filename);      

        /// Authorization header
        str headerKey = "Authorization";
        str headerValue = "Bearer " + token;

        /// Create the request
        System.Net.WebRequest request = System.Net.WebRequest::Create(url);
        request.Method = "PUT";
        request.ContentType = "application/octet-stream";
        
        /// Add the authorization header
        System.Net.WebHeaderCollection headers = request.Headers;
        headers.Add(headerKey, headerValue);

        /// Write the binary content to the request stream
        System.IO.Stream requestStream = request.GetRequestStream();
        _stream.CopyTo(requestStream);
        requestStream.Close();
        _stream.Close();

        /// Get the response from the API
        System.Net.HttpWebResponse response = request.GetResponse() as System.Net.HttpWebResponse;
    }


    /// returns a JSON as string with the list of files
    public str getListOfFiles(str _folderId, str _filter = "")
    {
        str url;
        if(_filter)
        {
            url = strFmt("https://graph.microsoft.com/v1.0/me/drive/items/%1/children?$filter=name eq '%2'",_folderId, _filter);
        }
        else
        {
            url = strFmt("https://graph.microsoft.com/v1.0/me/drive/items/%1/children", _folderId);
        }

        /// Authorization header
        str headerKey = "Authorization";
        str headerValue = "Bearer " + token;

        /// Create the request
        System.Net.WebRequest request = System.Net.WebRequest::Create(url);
        request.Method = "GET";

        /// Add the authorization header
        System.Net.WebHeaderCollection headers = request.Headers;
        headers.Add(headerKey, headerValue);

        /// Get the response from the API
        System.Net.HttpWebResponse response = request.GetResponse() as System.Net.HttpWebResponse;

        /// Get the response stream.
        System.IO.Stream  responseStream = response.GetResponseStream();

        /// Use StreamReader to read the response stream into a string.
        System.IO.StreamReader streamReader = new System.IO.StreamReader(responseStream);

        /// Read the entire response as a string.
        str responseText = streamReader.ReadToEnd();

        /// Close the stream and the response to free resources.
        streamReader.Close();
        responseStream.Close();
        response.Close();
       
        return responseText;        
    }

    public System.IO.Stream readFile(str _folder, str _filename)
    {
        
        str url = strFmt("https://graph.microsoft.com/v1.0/me/drive/items/%1:/%2:/content", System.Uri::EscapeDataString(_folder), System.Uri::EscapeDataString(_filename));
        
        /// Authorization header
        str headerKey = "Authorization";
        str headerValue = "Bearer " + token;

        /// Create the request
        System.Net.WebRequest request = System.Net.WebRequest::Create(url);
        request.Method = "GET";

        /// Add the authorization header
        System.Net.WebHeaderCollection headers = request.Headers;
        headers.Add(headerKey, headerValue);

        /// Get the response from the API
        System.Net.HttpWebResponse response = request.GetResponse() as System.Net.HttpWebResponse;

        /// Get the response stream.
        System.IO.Stream  responseStream = response.GetResponseStream();
        
        
        return responseStream;
    }

    public void moveFile(str _folderOrigin, str _finalFolder, str _fileName)
    {
        str fileId = this.getFileId(_folderOrigin, _fileName);

        str url = strFmt("https://graph.microsoft.com/v1.0/me/drive/items/%1", fileId);

        /// Body of the request as JSON.
        /// We could serialize an object, but this approach is faster.
        str body = strFmt("{\"parentReference\":{\"id\": \"%1\"}}", _finalFolder);

        /// Authorization header
        str headerKey = "Authorization";
        str headerValue = "Bearer " + token;

        /// Create the request
        System.Net.WebRequest request = System.Net.WebRequest::Create(url);
        request.Method = "PATCH";

        /// Add the authorization header
        System.Net.WebHeaderCollection headers = request.Headers;
        headers.Add(headerKey, headerValue);

        /// Set the Content-Type header to indicate JSON data
        request.ContentType = "application/json";

        /// Convert the body string to bytes
        System.Text.Encoding encoding = System.Text.Encoding::UTF8;
        System.Byte[] bodyBytes = encoding.GetBytes(body);

        /// Write the body content to the request stream
        System.IO.Stream requestStream = request.GetRequestStream();
        requestStream.Write(bodyBytes, 0, bodyBytes.Length);
        requestStream.Close();

        /// Get the response from the API
        System.Net.HttpWebResponse response = request.GetResponse() as System.Net.HttpWebResponse;

        /// Get the response stream.
        System.IO.Stream responseStream = response.GetResponseStream();

        /// Use StreamReader to read the response stream into a string
        System.IO.StreamReader streamReader = new System.IO.StreamReader(responseStream);
        str responseText = streamReader.ReadToEnd();

        streamReader.Close();
        responseStream.Close();
        response.Close();
    }

    public str getFileId(str _folder, str _name)
    {        
        container c = this.IdsOfFiles(_folder, _name);
        return conPeek(c,1);
    }

    /// Gets fileIds into a container
    public container extractFileIds(str _jsonString)
    {
        List dataMainList = new List(Types::Class);
        container ret;
        TST2_OneDriveApi_ResponseDataContract   dataClassContract = new TST2_OneDriveApi_ResponseDataContract();

        dataMainList = FormJsonSerializer::deserializeCollection(classnum(List), _jsonString, Types::Class, identifierStr('TST2_OneDriveApi_ResponseDataContract'));
        ListEnumerator     le = dataMainList.getEnumerator();
        while (le.moveNext())
        {
            dataClassContract        = le.current();
            /// Since our deserialization is a bit lazy, we also check that fileSize is not 0.
            if(dataClassContract.parmName() && dataClassContract.parmSize())
            {
                ret += dataClassContract.parmId();               
            }
        }

        return ret;
    }

    /// Gets filenames into a container
    public container extractFileNames(str _jsonString)
    {
        List dataMainList = new List(Types::Class);
        container ret;
        TST2_OneDriveApi_ResponseDataContract   dataClassContract = new TST2_OneDriveApi_ResponseDataContract();

        dataMainList = FormJsonSerializer::deserializeCollection(
            classnum(List), _jsonString, Types::Class,
            identifierStr('TST2_OneDriveApi_ResponseDataContract'));
        ListEnumerator le = dataMainList.getEnumerator();
        while (le.moveNext())
        {
            dataClassContract = le.current();

            if(dataClassContract.parmName() && dataClassContract.parmSize())
            {
                ret += dataClassContract.parmName();                
            }
        }

        return ret;
    }

    public container IdsOfFiles(str _folderId, str filter = "")
    {    
        str response = this.getListOfFiles(_folderId, filter);
        return this.extractFileIds(response);        
    }

    public container namesOfFiles(str _folderId)
    {
        str response = this.getListOfFiles(_folderId);
        return this.extractFileNames(response);           
    }

}
[DataContract]
class TST2_OneDriveApi_RefreshTokenJson
{    
    str accessToken;

    [DataMember("access_token")]
    public str parmaccessToken(str _accessToken = accessToken)
    {
        accessToken = _accessToken;
        return accessToken;
    }
}
[DataContractAttribute('data')]
class TST2_OneDriveApi_ResponseDataContract
{
    str id, name;
    int size;   

     [DataMemberAttribute("name")]
    public str parmName(str _name = name)
    {
        name = _name;
        return name;
    }

    [DataMemberAttribute("id")]
    public str parmId(str _Id = Id)
    {
        Id = _Id;
        return Id;
    }

    [DataMemberAttribute("size")]
    public int parmSize(int _size = size)
    {
        size = _size;
        return size;
    }
}

Source forms

Once the code is compiled, we can parameterize our source:

We can add a file mask, for example, to read only .txt files. Also, we can go to “File states for the source” to check all the files in the origin.

Let’s select “Refresh”:

Import

I’m going to modify an import mapping to trigger an error or warning depending on the file content, to check if it moves to the right folder. I won’t explain the steps here, but it’s covered in this post.

Once the source is set up, we can import our files anywhere in the ERP where there’s an ER import integration.

So it seems everything works fine, but when there are warnings, I tweaked the code a bit to debug it:

The system doesn’t detect warnings.

But I already reported this bug to Microsoft! Why didn’t they listen to me? Well, it seems they did, but I didn’t pay much attention to it until now.

So after some research, I found out this is called a “flight,” and it needs to be manually activated. In a dev environment, it seems you can do it with SQL, but in a production environment, it has to be done by Microsoft🙃.


Posted

in

, , , ,

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *