diff --git a/AlmaApi.lua b/AlmaApi.lua index 73b23cf..4ced691 100644 --- a/AlmaApi.lua +++ b/AlmaApi.lua @@ -1,85 +1,58 @@ -local AlmaApiInternal = {}; -AlmaApiInternal.ApiUrl = nil; -AlmaApiInternal.ApiKey = nil; - - -local types = {}; -types["log4net.LogManager"] = luanet.import_type("log4net.LogManager"); -types["System.Net.WebClient"] = luanet.import_type("System.Net.WebClient"); -types["System.Text.Encoding"] = luanet.import_type("System.Text.Encoding"); -types["System.Xml.XmlTextReader"] = luanet.import_type("System.Xml.XmlTextReader"); -types["System.Xml.XmlDocument"] = luanet.import_type("System.Xml.XmlDocument"); - --- Create a logger -local log = types["log4net.LogManager"].GetLogger(rootLogger .. ".AlmaApi"); - -AlmaApi = AlmaApiInternal; - -local function GetRequest( requestUrl ) - local webClient = types["System.Net.WebClient"](); - local response = nil; - log:Debug("Created Web Client"); - webClient.Encoding = types["System.Text.Encoding"].UTF8; - webClient.Headers:Add("Accept: application/xml"); - webClient.Headers:Add("Content-Type: application/xml"); - - local success, error = pcall(function () - response = webClient:DownloadString(requestUrl); - end); - - webClient:Dispose(); - log:Debug("Disposed Web Client"); - - if(success) then - return response; - else - log:InfoFormat("Unable to get response from the request url: {0}", error); - end -end - -local function ReadResponse( responseString ) - if (responseString and #responseString > 0) then - - local responseDocument = types["System.Xml.XmlDocument"](); - - local documentLoaded, error = pcall(function () - responseDocument:LoadXml(responseString); - end); - - if (documentLoaded) then - return responseDocument; - else - log:InfoFormat("Unable to load response content as XML: {0}", error); - return nil; - end - else - log:Info("Unable to read response content"); - end - - return nil; -end - -local function RetrieveHoldingsList( mmsId ) - local requestUrl = AlmaApiInternal.ApiUrl .."bibs/".. - Utility.URLEncode(mmsId) .."/holdings?apikey=" .. Utility.URLEncode(AlmaApiInternal.ApiKey); - log:DebugFormat("Request URL: {0}", requestUrl); - local response = GetRequest(requestUrl); - log:DebugFormat("response = {0}", response); - - return ReadResponse(response); -end - -local function RetrieveBibs( mmsId ) - local requestUrl = AlmaApiInternal.ApiUrl .. "bibs?apikey=".. - Utility.URLEncode(AlmaApiInternal.ApiKey) .. "&mms_id=" .. Utility.URLEncode(mmsId); - log:DebugFormat("Request URL: {0}", requestUrl); - - local response = GetRequest(requestUrl); - log:DebugFormat("response = {0}", response); - - return ReadResponse(response); -end - --- Exports -AlmaApi.RetrieveHoldingsList = RetrieveHoldingsList; -AlmaApi.RetrieveBibs = RetrieveBibs; \ No newline at end of file +local AlmaApiInternal = {}; +AlmaApiInternal.ApiUrl = nil; +AlmaApiInternal.ApiKey = nil; + + +local types = {}; +types["log4net.LogManager"] = luanet.import_type("log4net.LogManager"); +types["System.Net.WebClient"] = luanet.import_type("System.Net.WebClient"); +types["System.Text.Encoding"] = luanet.import_type("System.Text.Encoding"); +types["System.Xml.XmlTextReader"] = luanet.import_type("System.Xml.XmlTextReader"); +types["System.Xml.XmlDocument"] = luanet.import_type("System.Xml.XmlDocument"); + +-- Create a logger +local log = types["log4net.LogManager"].GetLogger(rootLogger .. ".AlmaApi"); + +AlmaApi = AlmaApiInternal; + +local function RetrieveHoldingsList( mmsId ) + local requestUrl = AlmaApiInternal.ApiUrl .."bibs/".. + Utility.URLEncode(mmsId) .."/holdings?apikey=" .. Utility.URLEncode(AlmaApiInternal.ApiKey); + local headers = {"Accept: application/xml", "Content-Type: application/xml"}; + log:DebugFormat("Request URL: {0}", requestUrl); + local response = WebClient.GetRequest(requestUrl, headers); + log:DebugFormat("response = {0}", response); + + return WebClient.ReadResponse(response); +end + +local function RetrieveBibs( mmsId ) + local requestUrl = AlmaApiInternal.ApiUrl .. "bibs?apikey=".. + Utility.URLEncode(AlmaApiInternal.ApiKey) .. "&mms_id=" .. Utility.URLEncode(mmsId); + local headers = {"Accept: application/xml", "Content-Type: application/xml"}; + log:DebugFormat("Request URL: {0}", requestUrl); + + local response = WebClient.GetRequest(requestUrl, headers); + log:DebugFormat("response = {0}", response); + + return WebClient.ReadResponse(response); +end + +local function RetrieveItemsList( mmsId, hodingId ) + local requestUrl = AlmaApiInternal.ApiUrl .."bibs/".. + Utility.URLEncode(mmsId) .."/holdings/" .. Utility.URLEncode(hodingId) .. "/items?apikey=" .. + Utility.URLEncode(AlmaApiInternal.ApiKey); + + local headers = {"Accept: application/xml", "Content-Type: application/xml"}; + + log:DebugFormat("Request URL: {0}", requestUrl); + local response = WebClient.GetRequest(requestUrl, headers); + log:DebugFormat("response = {0}", response); + + return WebClient.ReadResponse(response); +end + +-- Exports +AlmaApi.RetrieveHoldingsList = RetrieveHoldingsList; +AlmaApi.RetrieveBibs = RetrieveBibs; +AlmaApi.RetrieveItemsList = RetrieveItemsList; diff --git a/Catalog.lua b/Catalog.lua index 1550649..76bec70 100644 --- a/Catalog.lua +++ b/Catalog.lua @@ -1,559 +1,568 @@ -local settings = {}; -settings.AutoSearch = GetSetting("AutoSearch"); -settings.SearchPriorityList = Utility.StringSplit(",", GetSetting("SearchPriorityList")); -settings.HomeUrl = GetSetting("HomeURL"); -settings.CatalogUrl = GetSetting("CatalogURL"); -settings.AutoRetrieveItems = GetSetting("AutoRetrieveItems"); -settings.RemoveTrailingSpecialCharacters = GetSetting("RemoveTrailingSpecialCharacters"); -settings.AlmaApiUrl = GetSetting("AlmaAPIURL"); -settings.AlmaApiKey = GetSetting("AlmaAPIKey"); -settings.MmsIdPrefix = GetSetting("MMS_IDPrefix"); - -local interfaceMngr = nil; - --- The catalogSearchForm table allows us to store all objects related to the specific form inside the table so that we can easily --- prevent naming conflicts if we need to add more than one form and track elements from both. -local catalogSearchForm = {}; -catalogSearchForm.Form = nil; -catalogSearchForm.Browser = nil; -catalogSearchForm.RibbonPage = nil; -catalogSearchForm.ItemsButton = nil; -catalogSearchForm.ImportButton = nil; -catalogSearchForm.SearchButtons = {}; -catalogSearchForm.SearchButtons.Home = nil; -catalogSearchForm.SearchButtons.Title = nil; -catalogSearchForm.SearchButtons.Author = nil; -catalogSearchForm.SearchButtons.CallNumber = nil; -catalogSearchForm.SearchButtons.CatalogNumber = nil; - -local itemsXmlDocCache = {}; - -luanet.load_assembly("System.Data"); -luanet.load_assembly("System.Drawing"); -luanet.load_assembly("System.Xml"); -luanet.load_assembly("System.Windows.Forms"); -luanet.load_assembly("DevExpress.XtraBars"); -luanet.load_assembly("log4net"); - -local types = {}; -types["System.Data.DataTable"] = luanet.import_type("System.Data.DataTable"); -types["System.Drawing.Size"] = luanet.import_type("System.Drawing.Size"); -types["DevExpress.XtraBars.BarShortcut"] = luanet.import_type("DevExpress.XtraBars.BarShortcut"); -types["System.Windows.Forms.Shortcut"] = luanet.import_type("System.Windows.Forms.Shortcut"); -types["System.Windows.Forms.Keys"] = luanet.import_type("System.Windows.Forms.Keys"); -types["System.DBNull"] = luanet.import_type("System.DBNull"); -types["System.Windows.Forms.Application"] = luanet.import_type("System.Windows.Forms.Application"); -types["log4net.LogManager"] = luanet.import_type("log4net.LogManager"); - -local rootLogger = "AtlasSystems.Addons.AlmaBlacklightCatalogSearch"; -local log = types["log4net.LogManager"].GetLogger(rootLogger); -local product = types["System.Windows.Forms.Application"].ProductName; -local lastItemPage = ""; -log:Debug("Finished creating types"); - -function Init() - interfaceMngr = GetInterfaceManager(); - - -- Create a form - catalogSearchForm.Form = interfaceMngr:CreateForm(DataMapping.LabelName, DataMapping.LabelName); - - -- Add a browser - catalogSearchForm.Browser = catalogSearchForm.Form:CreateBrowser("Catalog Search", "Catalog Search Browser", "Catalog Search"); - -- Hide the text label - catalogSearchForm.Browser.TextVisible = false; - catalogSearchForm.Browser.WebBrowser.ScriptErrorsSuppressed = true; - - -- Since we didn't create a ribbon explicitly before creating our browser, it will have created one using the name we passed the CreateBrowser method. We can retrieve that one and add our buttons to it. - catalogSearchForm.RibbonPage = catalogSearchForm.Form:GetRibbonPage("Catalog Search"); - - -- Create the search button(s) - catalogSearchForm.SearchButtons.Home = catalogSearchForm.RibbonPage:CreateButton("New Search", GetClientImage(DataMapping.Icons[product]["Web"]), "ShowCatalogHome", "Search Options"); - catalogSearchForm.SearchButtons.Title = catalogSearchForm.RibbonPage:CreateButton("Title", GetClientImage(DataMapping.Icons[product]["Search"]), "SearchTitle", "Search Options"); - catalogSearchForm.SearchButtons.Author = catalogSearchForm.RibbonPage:CreateButton("Author", GetClientImage(DataMapping.Icons[product]["Search"]), "SearchAuthor", "Search Options"); - catalogSearchForm.SearchButtons.CallNumber = catalogSearchForm.RibbonPage:CreateButton("Call Number", GetClientImage(DataMapping.Icons[product]["Search"]), "SearchCallNumber", "Search Options"); - catalogSearchForm.SearchButtons.CatalogNumber = catalogSearchForm.RibbonPage:CreateButton("Catalog Number", GetClientImage(DataMapping.Icons[product]["Search"]), "SearchCatalogNumber", "Search Options"); - - if (not settings.AutoRetrieveItems) then - catalogSearchForm.ItemsButton = catalogSearchForm.RibbonPage:CreateButton("Retrieve Items", GetClientImage(DataMapping.Icons[product]["Record"]), "RetrieveItems", "Process"); - catalogSearchForm.ItemsButton.BarButton.ItemShortcut = types["DevExpress.XtraBars.BarShortcut"](types["System.Windows.Forms.Shortcut"].CtrlR); - end; - - catalogSearchForm.ImportButton = catalogSearchForm.RibbonPage:CreateButton("Import", GetClientImage(DataMapping.Icons[product]["Import"]), "DoItemImport", "Process"); - catalogSearchForm.ImportButton.BarButton.ItemShortcut = types["DevExpress.XtraBars.BarShortcut"](types["System.Windows.Forms.Shortcut"].CtrlI); - catalogSearchForm.ImportButton.BarButton.Enabled = false; - - BuildItemsGrid(); - - catalogSearchForm.Form:LoadLayout("CatalogLayout.xml"); - - -- After we add all of our buttons and form elements, we can show the form. - catalogSearchForm.Form:Show(); - - -- Initializing the AlmaApi - AlmaApi.ApiUrl = settings.AlmaApiUrl; - AlmaApi.ApiKey = settings.AlmaApiKey; - - -- Search when opened if autoSearch is true - local transactionNumber = GetFieldValue("Transaction", "TransactionNumber"); - if ((settings.AutoSearch) and (transactionNumber) and (transactionNumber > 0)) then - log:Debug("Performing AutoSearch"); - PerformSearch(true, nil); - else - log:Debug("Navigating to Catalog URL because AutoSearch is disabled"); - ShowCatalogHome(); - end - -end - -function InitializeRecordPageHandler() - catalogSearchForm.Browser:RegisterPageHandler("custom", "IsRecordPageLoaded", "RecordPageHandler", false); -end - -function ShowCatalogHome() - InitializeRecordPageHandler(); - catalogSearchForm.Browser:Navigate(settings.HomeUrl); -end - -function SearchTitle() - PerformSearch(false, "Title"); -end - -function SearchAuthor() - PerformSearch(false, "Author"); -end - -function SearchCallNumber() - PerformSearch(false, "Call Number"); -end - -function SearchCatalogNumber() - PerformSearch(false, "Catalog Number"); -end - -function GetSearchType() - local priorityList = settings.SearchPriorityList; - local fieldValue = nil; - - for index = 1, #priorityList do - if DataMapping.SearchTypes[priorityList[index]] ~= nil and DataMapping.SourceFields[product][priorityList[index]] ~= nil then - fieldValue = GetFieldValue("Transaction", DataMapping.SourceFields[product][priorityList[index]]) - log:DebugFormat("fieldValue = {0}", fieldValue); - if fieldValue and fieldValue ~= "" then - return priorityList[index]; - end - end - end - - return nil; -end - -function PerformSearch(autoSearch, searchType) - InitializeRecordPageHandler(); - if (searchType == nil) then - searchType = GetSearchType(); - - if searchType == nil then - local searchTypeError = "The search type could not be determined using the current request information."; - - if (autoSearch) then - catalogSearchForm.Browser.WebBrowser.DocumentText = searchTypeError; - log:Error(searchTypeError); - else - interfaceMngr:ShowMessage(searchTypeError, "No Search Type"); - end - - return; - end - end - - local searchTerm = GetFieldValue("Transaction", DataMapping.SourceFields[product][searchType]); - - if (searchTerm == nil) then - searchTerm = ""; - end - - local searchUrl = ""; - - if (searchType == "Catalog Number") then - -- Catalog Number navigates directly to the item page, so the URL is constructed differently - searchUrl = settings.CatalogUrl .. "/" .. settings.MmsIdPrefix .. Utility.URLEncode(searchTerm); - else - --Construct the search url based on the base catalog url, the search prefix that is defined in DataMapping for each MapType, followed by the search term - searchUrl = settings.CatalogUrl .. DataMapping.SearchTypes[searchType] .. Utility.URLEncode(searchTerm); - end - - - log:DebugFormat("Navigating to {0}", searchUrl); - catalogSearchForm.Browser:Navigate(searchUrl); -end - -function GetMmsId(pageUrl) - log:DebugFormat("pageUrl = {0}", pageUrl); - - if (pageUrl == nil) then - log:Warn("pageUrl was nil"); - end - - -- The URL pattern that indicates if the current page is a record page - local recordPageIndicator = '/catalog/'.. settings.MmsIdPrefix; - - -- Matches the digits after the recordPageIndicator - local mmsId = string.match(pageUrl, recordPageIndicator .. '(%d+)'); - log:DebugFormat("MMS ID = {0}", mmsId); - - return mmsId; -end - -function IsRecordPageLoaded() - log:Debug("Checking if Record Page is loaded"); - - local pageUrl = catalogSearchForm.Browser.WebBrowser.Url:ToString(); - - local isRecordPage = GetMmsId(pageUrl) ~= nil; - - if isRecordPage then - log:DebugFormat("{0} is a record page.", pageUrl); - else - log:DebugFormat("{0} is not a record page.", pageUrl); - ToggleItemsUIElements(false, HasItemPageChanged(catalogSearchForm.Browser.WebBrowser.Url:ToString())); - end - - return isRecordPage; -end - -function HasItemPageChanged(currentItemPage) - log:DebugFormat("Current Item Page: {0}", currentItemPage); - log:DebugFormat("Last Item Page: {0}", lastItemPage); - local itemPageChanged = currentItemPage ~= lastItemPage; - log:DebugFormat("Item Page Changed: {0}", itemPageChanged); - lastItemPage = catalogSearchForm.Browser.WebBrowser.Url:ToString(); - - return itemPageChanged; -end - -function RecordPageHandler() - --The record page has been loaded. We now need to wait to see when the holdings information comes in. - local itemPageChanged = HasItemPageChanged(catalogSearchForm.Browser.WebBrowser.Url:ToString()); - ToggleItemsUIElements(true, itemPageChanged); - - --Re-initialize the record page handler in case the user navigates away from a record page to search again - InitializeRecordPageHandler(); - - return itemPageChanged; -end - -function Truncate(value, size) - if size == nil then - log:Debug("Size was nil. Truncating to 50 characters"); - size = 50; - end - if ((value == nil) or (value == "")) then - log:Debug("Value was nil or empty. Skipping truncation."); - return value; - else - log:DebugFormat("Truncating to {0} characters: {1}", size, value); - return string.sub(value, 0, size); - end -end - -function ImportField(target, newFieldValue, targetSize) - if ((newFieldValue ~= nil) and (newFieldValue ~= "") and (newFieldValue ~= types["System.DBNull"].Value)) then - SetFieldValue("Transaction", target, Truncate(newFieldValue, targetSize)); - end -end - -function ToggleItemsUIElements(enabled, itemPageChanged) - if (enabled) then - log:Debug("Enabling UI."); - if (settings.AutoRetrieveItems) then - if(itemPageChanged) then - local hasRecords = RetrieveItems(); - catalogSearchForm.Grid.GridControl.Enabled = hasRecords; - end - else - catalogSearchForm.ItemsButton.BarButton.Enabled = true; - -- If there's an item in the Item Grid - if(catalogSearchForm.Grid.GridControl.MainView.FocusedRowHandle > -1) then - catalogSearchForm.Grid.GridControl.Enabled = true; - end - end - else - log:Debug("Disabling UI."); - ClearItems(); - catalogSearchForm.Grid.GridControl.Enabled = false; - if (not settings.AutoRetrieveItems) then - catalogSearchForm.ItemsButton.BarButton.Enabled = false; - end - end - log:Debug("Finished Toggling UI Elements"); -end - -function BuildItemsGrid() - log:Debug("BuildItemsGrid"); - - catalogSearchForm.Grid = catalogSearchForm.Form:CreateGrid("CatalogItemsGrid", "Items"); - catalogSearchForm.Grid.GridControl.Enabled = false; - - catalogSearchForm.Grid.TextSize = types["System.Drawing.Size"].Empty; - catalogSearchForm.Grid.TextVisible = false; - - local gridControl = catalogSearchForm.Grid.GridControl; - - gridControl:BeginUpdate(); - - -- Set the grid view options - local gridView = gridControl.MainView; - gridView.OptionsView.ShowIndicator = false; - gridView.OptionsView.ShowGroupPanel = false; - gridView.OptionsView.RowAutoHeight = true; - gridView.OptionsView.ColumnAutoWidth = true; - gridView.OptionsBehavior.AutoExpandAllGroups = true; - gridView.OptionsBehavior.Editable = false; - - -- Item Grid Column Settings - local gridColumn; - gridColumn = gridView.Columns:Add(); - gridColumn.Caption = "MMS ID"; - gridColumn.FieldName = "ReferenceNumber"; - gridColumn.Name = "gridColumnReferenceNumber"; - gridColumn.Visible = false; - gridColumn.OptionsColumn.ReadOnly = true; - gridColumn.Width = 50; - - gridColumn = gridView.Columns:Add(); - gridColumn.Caption = "Holding ID"; - gridColumn.FieldName = "HoldingId"; - gridColumn.Name = "gridColumnHoldingId"; - gridColumn.Visible = false; - gridColumn.OptionsColumn.ReadOnly = true; - gridColumn.Width = 50; - - gridColumn = gridView.Columns:Add(); - gridColumn.Caption = "Location"; - gridColumn.FieldName = "Location"; - gridColumn.Name = "gridColumnLocation"; - gridColumn.Visible = true; - gridColumn.VisibleIndex = 0; - gridColumn.OptionsColumn.ReadOnly = true; - - gridColumn = gridView.Columns:Add(); - gridColumn.Caption = "Library"; - gridColumn.FieldName = "Library"; - gridColumn.Name = "gridColumnLibrary"; - gridColumn.Visible = false; - gridColumn.OptionsColumn.ReadOnly = true; - - gridColumn = gridView.Columns:Add(); - gridColumn.Caption = "Location Code"; - gridColumn.FieldName = "LocationCode"; - gridColumn.Name = "gridColumnLocationCode"; - gridColumn.Visible = false; - gridColumn.OptionsColumn.ReadOnly = true; - - gridColumn = gridView.Columns:Add(); - gridColumn.Caption = "Call Number"; - gridColumn.FieldName = "CallNumber"; - gridColumn.Name = "gridColumnCallNumber"; - gridColumn.Visible = true; - gridColumn.VisibleIndex = 1; - gridColumn.OptionsColumn.ReadOnly = true; - - gridControl:EndUpdate(); - - gridView:add_FocusedRowChanged(ItemsGridFocusedRowChanged); -end - -function ItemsGridFocusedRowChanged(sender, args) - if (args.FocusedRowHandle > -1) then - catalogSearchForm.ImportButton.BarButton.Enabled = true; - catalogSearchForm.Grid.GridControl.Enabled = true; - else - catalogSearchForm.ImportButton.BarButton.Enabled = false; - end; -end - -function RetrieveItems() - local mmsId = GetMmsId(catalogSearchForm.Browser.WebBrowser.Url:ToString()); - local apiKey = settings.AlmaApiKey; - local apiUrl = settings.AlmaApiUrl; - - -- Cache the response if it hasn't been cached - if (itemsXmlDocCache[mmsId] == nil) then - log:DebugFormat("Caching {0}", mmsId); - itemsXmlDocCache[mmsId] = AlmaApi.RetrieveHoldingsList(mmsId); - end - - local response = itemsXmlDocCache[mmsId]; - - -- Check if it has any items available - local totalRecordCount = tonumber(response:SelectSingleNode("holdings/@total_record_count").Value); - log:DebugFormat("Records Available: {0}", totalRecordCount); - - local hasRecords = totalRecordCount > 0; - if (hasRecords) then - -- Fill out Holdings Grid if there are items available - PopulateItemsDataSources( response, mmsId ) - else - ClearItems(); - end; - - return hasRecords -end - -function CreateItemsTable() - local itemsTable = types["System.Data.DataTable"](); - - itemsTable.Columns:Add("ReferenceNumber"); - itemsTable.Columns:Add("HoldingId"); - itemsTable.Columns:Add("Library"); - itemsTable.Columns:Add("Location"); - itemsTable.Columns:Add("LocationCode"); - itemsTable.Columns:Add("CallNumber"); - - return itemsTable; -end - -function ClearItems() - catalogSearchForm.Grid.GridControl:BeginUpdate(); - catalogSearchForm.Grid.GridControl.DataSource = CreateItemsTable(); - catalogSearchForm.Grid.GridControl:EndUpdate(); -end - -function BuildItemsDataSource(holdingsXmlDoc, mmsId) - local itemsDataTable = CreateItemsTable(); - - local itemNodes = holdingsXmlDoc:GetElementsByTagName("holding"); - log:DebugFormat("Holding nodes found: {0}", itemNodes.Count); - - for i = 0, itemNodes.Count - 1 do - local itemRow = itemsDataTable:NewRow(); - local itemNode = itemNodes:Item(i); - log:DebugFormat("itemNode = {0}", itemNode.OuterXml); - - itemRow:set_Item("ReferenceNumber", mmsId); - log:DebugFormat("Reference Number = {0}", mmsId); - - itemRow:set_Item("HoldingId", itemNode["holding_id"].InnerXml); - log:DebugFormat("HoldingId = {0}", itemNode["holding_id"].InnerXml); - - -- If the location code isn't specified in the Data Mapping, use the code - if(CustomizedMapping.Locations[itemNode["location"].InnerXml] ~= nil and CustomizedMapping.Locations[itemNode["location"].InnerXml] ~= "") then - itemRow:set_Item("Location", CustomizedMapping.Locations[itemNode["location"].InnerXml]); - log:DebugFormat("Location = {0}", CustomizedMapping.Locations[itemNode["location"].InnerXml]); - else - itemRow:set_Item("Location", itemNode["location"].InnerXml); - log:DebugFormat("Location = {0}", itemNode["location"].InnerXml); - end - - itemRow:set_Item("LocationCode", itemNode["location"].InnerXml); - log:DebugFormat("Location Code = {0}", itemNode["location"].InnerXml); - - itemRow:set_Item("Library", itemNode["library"].InnerXml); - log:DebugFormat("Library = {0}", itemNode["library"].InnerXml); - - itemRow:set_Item("CallNumber", itemNode["call_number"].InnerXml); - log:DebugFormat("CallNumber = {0}", itemNode["call_number"].InnerXml); - - itemsDataTable.Rows:Add(itemRow); - end - - return itemsDataTable; -end - -function PopulateItemsDataSources( response, mmsId ) - log:DebugFormat("response type = {0}", response); - catalogSearchForm.Grid.GridControl:BeginUpdate(); - catalogSearchForm.Grid.GridControl.DataSource = BuildItemsDataSource(response, mmsId); - catalogSearchForm.Grid.GridControl:EndUpdate(); - catalogSearchForm.Grid.GridControl:Focus(); -end - -function DoItemImport() - log:Debug("Performing Import"); - - log:Debug("Retrieving import row."); - local importRow = catalogSearchForm.Grid.GridControl.MainView:GetFocusedRow(); - - if (importRow == nil) then - log:Debug("Import row was nil. Cancelling the import."); - return; - end; - - -- Update the transaction object with values. - log:Debug("Updating the transaction object."); - - for _, target in ipairs(DataMapping.ImportFields.Holding["Aeon"]) do - ImportField(target.Field, importRow:get_Item(target.Value), target.MaxSize); - end - - local mmsId = GetMmsId(catalogSearchForm.Browser.WebBrowser.Url:ToString()); - local apiUrl = settings.AlmaApiUrl; - local apiKey = settings.AlmaApiKey; - local bibXmlDoc = AlmaApi.RetrieveBibs(mmsId); - - local recordNodes = bibXmlDoc:SelectNodes("//record"); - - if (recordNodes) then - log:DebugFormat("Found {0} MARC records", recordNodes.Count); - - -- Loops through each record - for recordNodeIndex = 0, (recordNodes.Count - 1) do - log:DebugFormat("Processing record {0}", recordNodeIndex); - local recordNode = recordNodes:Item(recordNodeIndex); - - -- Loops through each Bibliographic mapping - for _, target in ipairs(DataMapping.ImportFields.Bibliographic[product]) do - if (target and target.Field and target.Field ~= "") then - log:DebugFormat("Value: {0}", target.Value); - log:DebugFormat("Target: {0}", target.Field); - local marcSets = Utility.StringSplit(',', target.Value ); - log:DebugFormat("marcSets.Count = {0}", #marcSets); - - -- Loops through the MARC sets array - for _, xPath in ipairs(marcSets) do - log:DebugFormat("xPath = {0}", xPath); - local datafieldNode = recordNode:SelectNodes(xPath); - log:DebugFormat("DataField Node Match Count: {0}", datafieldNode.Count); - - if (datafieldNode.Count > 0) then - local fieldValue = ""; - - -- Loops through each data field node retured from xPath and concatenates them (generally only 1) - for datafieldNodeIndex = 0, (datafieldNode.Count - 1) do - log:DebugFormat("datafieldnode value is: {0}", datafieldNode:Item(datafieldNodeIndex).InnerText); - fieldValue = fieldValue .. " " .. datafieldNode:Item(datafieldNodeIndex).InnerText; - end - - log:DebugFormat("target.Field: {0}", target.Field); - log:DebugFormat("target.MaxSize: {0}", target.MaxSize); - - if(settings.RemoveTrailingSpecialCharacters) then - fieldValue = RemoveTrailingSpecialCharacters(fieldValue); - else - fieldValue = Utility.Trim(fieldValue); - end - - ImportField(target.Field, fieldValue, target.MaxSize); - - -- Need to break from MARC Set loop so the first record isn't overwritten - break; - end - end - end - end - end - end - - log:Debug("Switching to the detail tab."); - ExecuteCommand("SwitchTab", "Detail"); -end - -function RemoveTrailingSpecialCharacters(item) - local trailingCharacters = { '\\', '/', ',', '.', ';', ':', '-', '=' }; - for _, value in ipairs(trailingCharacters) do - if (string.find(item, value, -1, true)) then - return Utility.Trim(item:sub(1, -2)) - end - end - return item; -end \ No newline at end of file +local settings = {}; +settings.AutoSearch = GetSetting("AutoSearch"); +settings.SearchPriorityList = Utility.StringSplit(",", GetSetting("SearchPriorityList")); +settings.HomeUrl = GetSetting("HomeURL"); +settings.CatalogUrl = GetSetting("CatalogURL"); +settings.AutoRetrieveItems = GetSetting("AutoRetrieveItems"); +settings.RemoveTrailingSpecialCharacters = GetSetting("RemoveTrailingSpecialCharacters"); +settings.AlmaApiUrl = GetSetting("AlmaAPIURL"); +settings.AlmaApiKey = GetSetting("AlmaAPIKey"); +settings.MmsIdPrefix = GetSetting("MMS_IDPrefix"); + +local interfaceMngr = nil; + +-- The catalogSearchForm table allows us to store all objects related to the specific form inside the table so that we can easily +-- prevent naming conflicts if we need to add more than one form and track elements from both. +local catalogSearchForm = {}; +catalogSearchForm.Form = nil; +catalogSearchForm.Browser = nil; +catalogSearchForm.RibbonPage = nil; +catalogSearchForm.ItemsButton = nil; +catalogSearchForm.ImportButton = nil; +catalogSearchForm.SearchButtons = {}; +catalogSearchForm.SearchButtons.Home = nil; +catalogSearchForm.SearchButtons.Title = nil; +catalogSearchForm.SearchButtons.Author = nil; +catalogSearchForm.SearchButtons.CallNumber = nil; +catalogSearchForm.SearchButtons.CatalogNumber = nil; + +local itemsXmlDocCache = {}; + +luanet.load_assembly("System.Data"); +luanet.load_assembly("System.Drawing"); +luanet.load_assembly("System.Xml"); +luanet.load_assembly("System.Windows.Forms"); +luanet.load_assembly("DevExpress.XtraBars"); +luanet.load_assembly("log4net"); + +local types = {}; +types["System.Data.DataTable"] = luanet.import_type("System.Data.DataTable"); +types["System.Drawing.Size"] = luanet.import_type("System.Drawing.Size"); +types["DevExpress.XtraBars.BarShortcut"] = luanet.import_type("DevExpress.XtraBars.BarShortcut"); +types["System.Windows.Forms.Shortcut"] = luanet.import_type("System.Windows.Forms.Shortcut"); +types["System.Windows.Forms.Keys"] = luanet.import_type("System.Windows.Forms.Keys"); +types["System.DBNull"] = luanet.import_type("System.DBNull"); +types["System.Windows.Forms.Application"] = luanet.import_type("System.Windows.Forms.Application"); +types["log4net.LogManager"] = luanet.import_type("log4net.LogManager"); + +local rootLogger = "AtlasSystems.Addons.AlmaBlacklightCatalogSearch"; +local log = types["log4net.LogManager"].GetLogger(rootLogger); +local product = types["System.Windows.Forms.Application"].ProductName; +local lastItemPage = ""; +log:Debug("Finished creating types"); + +function Init() + interfaceMngr = GetInterfaceManager(); + + -- Create a form + catalogSearchForm.Form = interfaceMngr:CreateForm(DataMapping.LabelName, DataMapping.LabelName); + + -- Add a browser + catalogSearchForm.Browser = catalogSearchForm.Form:CreateBrowser("Catalog Search", "Catalog Search Browser", "Catalog Search"); + -- Hide the text label + catalogSearchForm.Browser.TextVisible = false; + catalogSearchForm.Browser.WebBrowser.ScriptErrorsSuppressed = true; + + -- Since we didn't create a ribbon explicitly before creating our browser, it will have created one using the name we passed the CreateBrowser method. We can retrieve that one and add our buttons to it. + catalogSearchForm.RibbonPage = catalogSearchForm.Form:GetRibbonPage("Catalog Search"); + + -- Create the search button(s) + catalogSearchForm.SearchButtons.Home = catalogSearchForm.RibbonPage:CreateButton("New Search", GetClientImage(DataMapping.Icons[product]["Web"]), "ShowCatalogHome", "Search Options"); + catalogSearchForm.SearchButtons.Title = catalogSearchForm.RibbonPage:CreateButton("Title", GetClientImage(DataMapping.Icons[product]["Search"]), "SearchTitle", "Search Options"); + catalogSearchForm.SearchButtons.Author = catalogSearchForm.RibbonPage:CreateButton("Author", GetClientImage(DataMapping.Icons[product]["Search"]), "SearchAuthor", "Search Options"); + catalogSearchForm.SearchButtons.CallNumber = catalogSearchForm.RibbonPage:CreateButton("Call Number", GetClientImage(DataMapping.Icons[product]["Search"]), "SearchCallNumber", "Search Options"); + catalogSearchForm.SearchButtons.CatalogNumber = catalogSearchForm.RibbonPage:CreateButton("Catalog Number", GetClientImage(DataMapping.Icons[product]["Search"]), "SearchCatalogNumber", "Search Options"); + + if (not settings.AutoRetrieveItems) then + catalogSearchForm.ItemsButton = catalogSearchForm.RibbonPage:CreateButton("Retrieve Items", GetClientImage(DataMapping.Icons[product]["Record"]), "RetrieveItems", "Process"); + catalogSearchForm.ItemsButton.BarButton.ItemShortcut = types["DevExpress.XtraBars.BarShortcut"](types["System.Windows.Forms.Shortcut"].CtrlR); + end; + + catalogSearchForm.ImportButton = catalogSearchForm.RibbonPage:CreateButton("Import", GetClientImage(DataMapping.Icons[product]["Import"]), "DoItemImport", "Process"); + catalogSearchForm.ImportButton.BarButton.ItemShortcut = types["DevExpress.XtraBars.BarShortcut"](types["System.Windows.Forms.Shortcut"].CtrlI); + catalogSearchForm.ImportButton.BarButton.Enabled = false; + + BuildItemsGrid(); + + catalogSearchForm.Form:LoadLayout("CatalogLayout.xml"); + + -- After we add all of our buttons and form elements, we can show the form. + catalogSearchForm.Form:Show(); + + -- Initializing the AlmaApi + AlmaApi.ApiUrl = settings.AlmaApiUrl; + AlmaApi.ApiKey = settings.AlmaApiKey; + + -- Search when opened if autoSearch is true + + local transactionNumber = GetFieldValue( + DataMapping.SourceFields[product]["TransactionNumber"].Table, + DataMapping.SourceFields[product]["TransactionNumber"].Field + ); + + if ((settings.AutoSearch) and (transactionNumber) and (transactionNumber > 0)) then + log:Debug("Performing AutoSearch"); + PerformSearch(true, nil); + else + log:Debug("Navigating to Catalog URL because AutoSearch is disabled"); + ShowCatalogHome(); + end + +end + +function InitializeRecordPageHandler() + catalogSearchForm.Browser:RegisterPageHandler("custom", "IsRecordPageLoaded", "RecordPageHandler", false); +end + +function ShowCatalogHome() + InitializeRecordPageHandler(); + catalogSearchForm.Browser:Navigate(settings.HomeUrl); +end + +function SearchTitle() + PerformSearch(false, "Title"); +end + +function SearchAuthor() + PerformSearch(false, "Author"); +end + +function SearchCallNumber() + PerformSearch(false, "Call Number"); +end + +function SearchCatalogNumber() + PerformSearch(false, "Catalog Number"); +end + +function GetSearchType() + local priorityList = settings.SearchPriorityList; + + for _, searchType in ipairs(priorityList) do + if DataMapping.SearchTypes[searchType] and DataMapping.SourceFields[product][searchType] then + + local fieldDefinition = DataMapping.SourceFields[product][searchType] + local fieldValue = GetFieldValue(fieldDefinition.Table, fieldDefinition.Field) + + log:DebugFormat("fieldValue = {0}", fieldValue); + if fieldValue and fieldValue ~= "" then + return searchType; + end + end + end + + return nil; +end + +function PerformSearch(autoSearch, searchType) + InitializeRecordPageHandler(); + if (searchType == nil) then + searchType = GetSearchType(); + + if searchType == nil then + local searchTypeError = "The search type could not be determined using the current request information."; + + if (autoSearch) then + catalogSearchForm.Browser.WebBrowser.DocumentText = searchTypeError; + log:Error(searchTypeError); + else + interfaceMngr:ShowMessage(searchTypeError, "No Search Type"); + end + + return; + end + end + + local fieldDefinition = DataMapping.SourceFields[product][searchType] + local searchTerm = GetFieldValue(fieldDefinition.Table, fieldDefinition.Field); + + if (searchTerm == nil) then + searchTerm = ""; + end + + local searchUrl = ""; + + if (searchType == "Catalog Number") then + -- Catalog Number navigates directly to the item page, so the URL is constructed differently + searchUrl = settings.CatalogUrl .. "/" .. settings.MmsIdPrefix .. Utility.URLEncode(searchTerm); + else + --Construct the search url based on the base catalog url, the search prefix that is defined in DataMapping for each MapType, followed by the search term + searchUrl = settings.CatalogUrl .. DataMapping.SearchTypes[searchType] .. Utility.URLEncode(searchTerm); + end + + + log:DebugFormat("Navigating to {0}", searchUrl); + catalogSearchForm.Browser:Navigate(searchUrl); +end + +function GetMmsId(pageUrl) + log:DebugFormat("pageUrl = {0}", pageUrl); + + if (pageUrl == nil) then + log:Warn("pageUrl was nil"); + end + + -- The URL pattern that indicates if the current page is a record page + local recordPageIndicator = '/catalog/'.. settings.MmsIdPrefix; + + -- Matches the digits after the recordPageIndicator + local mmsId = string.match(pageUrl, recordPageIndicator .. '(%d+)'); + log:DebugFormat("MMS ID = {0}", mmsId); + + return mmsId; +end + +function IsRecordPageLoaded() + log:Debug("Checking if Record Page is loaded"); + + local pageUrl = catalogSearchForm.Browser.WebBrowser.Url:ToString(); + + local isRecordPage = GetMmsId(pageUrl) ~= nil; + + if isRecordPage then + log:DebugFormat("{0} is a record page.", pageUrl); + else + log:DebugFormat("{0} is not a record page.", pageUrl); + ToggleItemsUIElements(false, HasItemPageChanged(catalogSearchForm.Browser.WebBrowser.Url:ToString())); + end + + return isRecordPage; +end + +function HasItemPageChanged(currentItemPage) + log:DebugFormat("Current Item Page: {0}", currentItemPage); + log:DebugFormat("Last Item Page: {0}", lastItemPage); + local itemPageChanged = currentItemPage ~= lastItemPage; + log:DebugFormat("Item Page Changed: {0}", itemPageChanged); + lastItemPage = catalogSearchForm.Browser.WebBrowser.Url:ToString(); + + return itemPageChanged; +end + +function RecordPageHandler() + --The record page has been loaded. We now need to wait to see when the holdings information comes in. + local itemPageChanged = HasItemPageChanged(catalogSearchForm.Browser.WebBrowser.Url:ToString()); + ToggleItemsUIElements(true, itemPageChanged); + + --Re-initialize the record page handler in case the user navigates away from a record page to search again + InitializeRecordPageHandler(); + + return itemPageChanged; +end + +function Truncate(value, size) + if size == nil then + log:Debug("Size was nil. Truncating to 50 characters"); + size = 50; + end + if ((value == nil) or (value == "")) then + log:Debug("Value was nil or empty. Skipping truncation."); + return value; + else + log:DebugFormat("Truncating to {0} characters: {1}", size, value); + return string.sub(value, 0, size); + end +end + +function ImportField(targetTable, targetField, newFieldValue, targetSize) + if (newFieldValue and (newFieldValue ~= "") and (newFieldValue ~= types["System.DBNull"].Value)) then + SetFieldValue(targetTable, targetField, Truncate(newFieldValue, targetSize)); + end +end + +function ToggleItemsUIElements(enabled, itemPageChanged) + if (enabled) then + log:Debug("Enabling UI."); + if (settings.AutoRetrieveItems) then + if(itemPageChanged) then + local hasRecords = RetrieveItems(); + catalogSearchForm.Grid.GridControl.Enabled = hasRecords; + end + else + catalogSearchForm.ItemsButton.BarButton.Enabled = true; + -- If there's an item in the Item Grid + if(catalogSearchForm.Grid.GridControl.MainView.FocusedRowHandle > -1) then + catalogSearchForm.Grid.GridControl.Enabled = true; + end + end + else + log:Debug("Disabling UI."); + ClearItems(); + catalogSearchForm.Grid.GridControl.Enabled = false; + if (not settings.AutoRetrieveItems) then + catalogSearchForm.ItemsButton.BarButton.Enabled = false; + end + end + log:Debug("Finished Toggling UI Elements"); +end + +function BuildItemsGrid() + log:Debug("BuildItemsGrid"); + + catalogSearchForm.Grid = catalogSearchForm.Form:CreateGrid("CatalogItemsGrid", "Items"); + catalogSearchForm.Grid.GridControl.Enabled = false; + + catalogSearchForm.Grid.TextSize = types["System.Drawing.Size"].Empty; + catalogSearchForm.Grid.TextVisible = false; + + local gridControl = catalogSearchForm.Grid.GridControl; + + gridControl:BeginUpdate(); + + -- Set the grid view options + local gridView = gridControl.MainView; + gridView.OptionsView.ShowIndicator = false; + gridView.OptionsView.ShowGroupPanel = false; + gridView.OptionsView.RowAutoHeight = true; + gridView.OptionsView.ColumnAutoWidth = true; + gridView.OptionsBehavior.AutoExpandAllGroups = true; + gridView.OptionsBehavior.Editable = false; + + -- Item Grid Column Settings + local gridColumn; + gridColumn = gridView.Columns:Add(); + gridColumn.Caption = "MMS ID"; + gridColumn.FieldName = "ReferenceNumber"; + gridColumn.Name = "gridColumnReferenceNumber"; + gridColumn.Visible = false; + gridColumn.OptionsColumn.ReadOnly = true; + gridColumn.Width = 50; + + gridColumn = gridView.Columns:Add(); + gridColumn.Caption = "Holding ID"; + gridColumn.FieldName = "HoldingId"; + gridColumn.Name = "gridColumnHoldingId"; + gridColumn.Visible = false; + gridColumn.OptionsColumn.ReadOnly = true; + gridColumn.Width = 50; + + gridColumn = gridView.Columns:Add(); + gridColumn.Caption = "Location"; + gridColumn.FieldName = "Location"; + gridColumn.Name = "gridColumnLocation"; + gridColumn.Visible = true; + gridColumn.VisibleIndex = 0; + gridColumn.OptionsColumn.ReadOnly = true; + + gridColumn = gridView.Columns:Add(); + gridColumn.Caption = "Library"; + gridColumn.FieldName = "Library"; + gridColumn.Name = "gridColumnLibrary"; + gridColumn.Visible = false; + gridColumn.OptionsColumn.ReadOnly = true; + + gridColumn = gridView.Columns:Add(); + gridColumn.Caption = "Location Code"; + gridColumn.FieldName = "LocationCode"; + gridColumn.Name = "gridColumnLocationCode"; + gridColumn.Visible = false; + gridColumn.OptionsColumn.ReadOnly = true; + + gridColumn = gridView.Columns:Add(); + gridColumn.Caption = "Call Number"; + gridColumn.FieldName = "CallNumber"; + gridColumn.Name = "gridColumnCallNumber"; + gridColumn.Visible = true; + gridColumn.VisibleIndex = 1; + gridColumn.OptionsColumn.ReadOnly = true; + + gridControl:EndUpdate(); + + gridView:add_FocusedRowChanged(ItemsGridFocusedRowChanged); +end + +function ItemsGridFocusedRowChanged(sender, args) + if (args.FocusedRowHandle > -1) then + catalogSearchForm.ImportButton.BarButton.Enabled = true; + catalogSearchForm.Grid.GridControl.Enabled = true; + else + catalogSearchForm.ImportButton.BarButton.Enabled = false; + end; +end + +function RetrieveItems() + local mmsId = GetMmsId(catalogSearchForm.Browser.WebBrowser.Url:ToString()); + local apiKey = settings.AlmaApiKey; + local apiUrl = settings.AlmaApiUrl; + + -- Cache the response if it hasn't been cached + if (itemsXmlDocCache[mmsId] == nil) then + log:DebugFormat("Caching {0}", mmsId); + itemsXmlDocCache[mmsId] = AlmaApi.RetrieveHoldingsList(mmsId); + end + + local response = itemsXmlDocCache[mmsId]; + + -- Check if it has any items available + local totalRecordCount = tonumber(response:SelectSingleNode("holdings/@total_record_count").Value); + log:DebugFormat("Records Available: {0}", totalRecordCount); + + local hasRecords = totalRecordCount > 0; + if (hasRecords) then + -- Fill out Holdings Grid if there are items available + PopulateItemsDataSources( response, mmsId ) + else + ClearItems(); + end; + + return hasRecords +end + +function CreateItemsTable() + local itemsTable = types["System.Data.DataTable"](); + + itemsTable.Columns:Add("ReferenceNumber"); + itemsTable.Columns:Add("HoldingId"); + itemsTable.Columns:Add("Library"); + itemsTable.Columns:Add("Location"); + itemsTable.Columns:Add("LocationCode"); + itemsTable.Columns:Add("CallNumber"); + + return itemsTable; +end + +function ClearItems() + catalogSearchForm.Grid.GridControl:BeginUpdate(); + catalogSearchForm.Grid.GridControl.DataSource = CreateItemsTable(); + catalogSearchForm.Grid.GridControl:EndUpdate(); +end + +function BuildItemsDataSource(holdingsXmlDoc, mmsId) + local itemsDataTable = CreateItemsTable(); + + local itemNodes = holdingsXmlDoc:GetElementsByTagName("holding"); + log:DebugFormat("Holding nodes found: {0}", itemNodes.Count); + + for i = 0, itemNodes.Count - 1 do + local itemRow = itemsDataTable:NewRow(); + local itemNode = itemNodes:Item(i); + log:DebugFormat("itemNode = {0}", itemNode.OuterXml); + + itemRow:set_Item("ReferenceNumber", mmsId); + log:DebugFormat("Reference Number = {0}", mmsId); + + itemRow:set_Item("HoldingId", itemNode["holding_id"].InnerXml); + log:DebugFormat("HoldingId = {0}", itemNode["holding_id"].InnerXml); + + -- If the location code isn't specified in the Data Mapping, use the code + if(CustomizedMapping.Locations[itemNode["location"].InnerXml] ~= nil and CustomizedMapping.Locations[itemNode["location"].InnerXml] ~= "") then + itemRow:set_Item("Location", CustomizedMapping.Locations[itemNode["location"].InnerXml]); + log:DebugFormat("Location = {0}", CustomizedMapping.Locations[itemNode["location"].InnerXml]); + else + itemRow:set_Item("Location", itemNode["location"].InnerXml); + log:DebugFormat("Location = {0}", itemNode["location"].InnerXml); + end + + itemRow:set_Item("LocationCode", itemNode["location"].InnerXml); + log:DebugFormat("Location Code = {0}", itemNode["location"].InnerXml); + + itemRow:set_Item("Library", itemNode["library"].InnerXml); + log:DebugFormat("Library = {0}", itemNode["library"].InnerXml); + + itemRow:set_Item("CallNumber", itemNode["call_number"].InnerXml); + log:DebugFormat("CallNumber = {0}", itemNode["call_number"].InnerXml); + + itemsDataTable.Rows:Add(itemRow); + end + + return itemsDataTable; +end + +function PopulateItemsDataSources( response, mmsId ) + log:DebugFormat("response type = {0}", response); + catalogSearchForm.Grid.GridControl:BeginUpdate(); + catalogSearchForm.Grid.GridControl.DataSource = BuildItemsDataSource(response, mmsId); + catalogSearchForm.Grid.GridControl:EndUpdate(); + catalogSearchForm.Grid.GridControl:Focus(); +end + +function DoItemImport() + log:Debug("Performing Import"); + + log:Debug("Retrieving import row."); + local importRow = catalogSearchForm.Grid.GridControl.MainView:GetFocusedRow(); + + if (importRow == nil) then + log:Debug("Import row was nil. Cancelling the import."); + return; + end; + + -- Update the transaction object with values. + log:Debug("Updating the transaction object."); + + for _, target in ipairs(DataMapping.ImportFields.Holding["Aeon"]) do + ImportField(target.Table, target.Field, importRow:get_Item(target.Value), target.MaxSize); + end + + local mmsId = GetMmsId(catalogSearchForm.Browser.WebBrowser.Url:ToString()); + local apiUrl = settings.AlmaApiUrl; + local apiKey = settings.AlmaApiKey; + local bibXmlDoc = AlmaApi.RetrieveBibs(mmsId); + + local recordNodes = bibXmlDoc:SelectNodes("//record"); + + if (recordNodes) then + log:DebugFormat("Found {0} MARC records", recordNodes.Count); + + -- Loops through each record + for recordNodeIndex = 0, (recordNodes.Count - 1) do + log:DebugFormat("Processing record {0}", recordNodeIndex); + local recordNode = recordNodes:Item(recordNodeIndex); + + -- Loops through each Bibliographic mapping + for _, target in ipairs(DataMapping.ImportFields.Bibliographic[product]) do + if (target and target.Field and target.Field ~= "") then + log:DebugFormat("Value: {0}", target.Value); + log:DebugFormat("Target: {0}.{1}", target.Table, target.Field); + local marcSets = Utility.StringSplit(',', target.Value ); + log:DebugFormat("marcSets.Count = {0}", #marcSets); + + -- Loops through the MARC sets array + for _, xPath in ipairs(marcSets) do + log:DebugFormat("xPath = {0}", xPath); + local datafieldNode = recordNode:SelectNodes(xPath); + log:DebugFormat("DataField Node Match Count: {0}", datafieldNode.Count); + + if (datafieldNode.Count > 0) then + local fieldValue = ""; + + -- Loops through each data field node retured from xPath and concatenates them (generally only 1) + for datafieldNodeIndex = 0, (datafieldNode.Count - 1) do + log:DebugFormat("datafieldnode value is: {0}", datafieldNode:Item(datafieldNodeIndex).InnerText); + fieldValue = fieldValue .. " " .. datafieldNode:Item(datafieldNodeIndex).InnerText; + end + + log:DebugFormat("target.Table: {0}", target.Table); + log:DebugFormat("target.Field: {0}", target.Field); + log:DebugFormat("target.MaxSize: {0}", target.MaxSize); + + if(settings.RemoveTrailingSpecialCharacters) then + fieldValue = RemoveTrailingSpecialCharacters(fieldValue); + else + fieldValue = Utility.Trim(fieldValue); + end + + ImportField(target.Table, target.Field, fieldValue, target.MaxSize); + + -- Need to break from MARC Set loop so the first record isn't overwritten + break; + end + end + end + end + end + end + + log:Debug("Switching to the detail tab."); + ExecuteCommand("SwitchTab", "Detail"); +end + +function RemoveTrailingSpecialCharacters(item) + local trailingCharacters = { '\\', '/', ',', '.', ';', ':', '-', '=' }; + for _, value in ipairs(trailingCharacters) do + if (string.find(item, value, -1, true)) then + return Utility.Trim(item:sub(1, -2)) + end + end + return item; +end diff --git a/CatalogLayout.xml b/CatalogLayout.xml index 02098cf..aab1f2b 100644 --- a/CatalogLayout.xml +++ b/CatalogLayout.xml @@ -1,261 +1,261 @@ - - - - - 0 - true - Vertical - false - 0, 0, 0, 0 - @3,Width=983@3,Height=521 - false - Normal - 0, 0, 0, 0 - false - - Top - - - - - - - Tahoma, 8.25pt - Horizontal - - - - - - - Tahoma, 8.25pt - Horizontal - - - - - - - Tahoma, 8.25pt - Horizontal - - - - - - - Tahoma, 8.25pt - Horizontal - - - - - - - Tahoma, 8.25pt - Horizontal - - - - - - - - Tahoma, 8.25pt - Horizontal - - - - - - - Tahoma, 8.25pt - Horizontal - - Root - - Default - Default - - - - - None - - - None - true - - @1,X=0@1,Y=0 - false - Default - True - Default - Always - -1 - true - false - true - - 3 - UseParentOptions - - LayoutGroup - Main Group - Main Group - true - - - - 5 - @1,Width=0@1,Height=0 - @1,Width=0@1,Height=0 - @3,Width=104@2,Height=24 - @3,Width=983@3,Height=424 - @1,X=0@1,Y=0 - - Default - Default - - - - - None - - - None - true - - 2, 2, 2, 2 - Catalog Search - - - - - - Tahoma, 8.25pt - Horizontal - - 0, 0, 0, 0 - 0 - Root - MiddleLeft - -1 - false - true - - Catalog Search Browser - Catalog Search - Default - Left - CustomSize - AtlasSystems.Scripting.UI.AddonControls.Browser - Always - false - Catalog Search - - - Root - 5 - @1,Width=0@1,Height=0 - @3,Width=104@2,Height=24 - @1,X=0@3,Y=428 - - Default - Default - - - - - None - - - None - true - - 2, 2, 2, 2 - 0, 0, 0, 0 - 0 - CatalogItemsGrid - - - - - - Tahoma, 8.25pt - Horizontal - - @1,Width=0@1,Height=0 - false - -1 - @3,Width=983@2,Height=93 - MiddleLeft - - true - Items - Default - CatalogItemsGrid - CustomSize - Left - AtlasSystems.Scripting.UI.AddonControls.Grid - Always - false - CatalogItemsGrid - - - MiddleLeft - Default - item0 - true - - false - - 0 - @1,X=0@3,Y=424 - 5 - Root - - Default - Default - - - - - None - - - None - true - - -1 - @1,Width=0@1,Height=4 - item0 - AutoSize - item0 - @1,Width=4@1,Height=4 - - - - - - Tahoma, 8.25pt - Horizontal - - 0, 0, 0, 0 - 2, 2, 2, 2 - false - Default - @3,Width=983@1,Height=4 - SplitterItem - Always - OnlyAdjacentControls - @1,Width=0@1,Height=0 - - - - DevExpress Style - Skin - true - false - - \ No newline at end of file + + + + + 0 + true + Vertical + false + 0, 0, 0, 0 + @3,Width=983@3,Height=521 + false + Normal + 0, 0, 0, 0 + false + + Top + + + + + + + Tahoma, 8.25pt + Horizontal + + + + + + + Tahoma, 8.25pt + Horizontal + + + + + + + Tahoma, 8.25pt + Horizontal + + + + + + + Tahoma, 8.25pt + Horizontal + + + + + + + Tahoma, 8.25pt + Horizontal + + + + + + + + Tahoma, 8.25pt + Horizontal + + + + + + + Tahoma, 8.25pt + Horizontal + + Root + + Default + Default + + + + + None + + + None + true + + @1,X=0@1,Y=0 + false + Default + True + Default + Always + -1 + true + false + true + + 3 + UseParentOptions + + LayoutGroup + Main Group + Main Group + true + + + + 5 + @1,Width=0@1,Height=0 + @1,Width=0@1,Height=0 + @3,Width=104@2,Height=24 + @3,Width=983@3,Height=424 + @1,X=0@1,Y=0 + + Default + Default + + + + + None + + + None + true + + 2, 2, 2, 2 + Catalog Search + + + + + + Tahoma, 8.25pt + Horizontal + + 0, 0, 0, 0 + 0 + Root + MiddleLeft + -1 + false + true + + Catalog Search Browser + Catalog Search + Default + Left + CustomSize + AtlasSystems.Scripting.UI.AddonControls.Browser + Always + false + Catalog Search + + + Root + 5 + @1,Width=0@1,Height=0 + @3,Width=104@2,Height=24 + @1,X=0@3,Y=428 + + Default + Default + + + + + None + + + None + true + + 2, 2, 2, 2 + 0, 0, 0, 0 + 0 + CatalogItemsGrid + + + + + + Tahoma, 8.25pt + Horizontal + + @1,Width=0@1,Height=0 + false + -1 + @3,Width=983@2,Height=93 + MiddleLeft + + true + Items + Default + CatalogItemsGrid + CustomSize + Left + AtlasSystems.Scripting.UI.AddonControls.Grid + Always + false + CatalogItemsGrid + + + MiddleLeft + Default + item0 + true + + false + + 0 + @1,X=0@3,Y=424 + 5 + Root + + Default + Default + + + + + None + + + None + true + + -1 + @1,Width=0@1,Height=4 + item0 + AutoSize + item0 + @1,Width=4@1,Height=4 + + + + + + Tahoma, 8.25pt + Horizontal + + 0, 0, 0, 0 + 2, 2, 2, 2 + false + Default + @3,Width=983@1,Height=4 + SplitterItem + Always + OnlyAdjacentControls + @1,Width=0@1,Height=0 + + + + DevExpress Style + Skin + true + false + + diff --git a/Config.xml b/Config.xml index f9a0d35..a749381 100644 --- a/Config.xml +++ b/Config.xml @@ -1,48 +1,49 @@ - - - Alma Blacklight Catalog Search - Atlas Systems - 1.2.1 - True - Addon - Catalog Search and Import Addon that uses Alma as the catalog and Blacklight as the discovery layer - -
FormRequest
-
- - - The base URL that the query strings are appended to. - - - Home page of the catalog. - - - Defines whether the search should be automatically performed when the form opens. - - - Defines whether to remove trailing special characters on import or not. - - - The fields that should be searched on, in order of search priority. Each field in the string will be checked for a valid corresponding search value in the request, and the first search type with a valid corresponding value will be used. - - - Defines whether or not the addon should automatically retrieve items related to a record being viewed. - - - The URL to the Alma API - - - API key used for interacting with the Alma API. - - - The MMS_ID Prefix are any characters that proceed the mms_id of a record in the URL. - - - - Utility.lua - DataMapping.lua - CustomizedMapping.lua - Catalog.lua - AlmaApi.lua - -
\ No newline at end of file + + + Alma Blacklight Catalog Search + Atlas Systems + 1.3.0 + True + Addon + Catalog Search and Import Addon that uses Alma as the catalog and Blacklight as the discovery layer + +
FormRequest
+
+ + + The base URL that the query strings are appended to. + + + Home page of the catalog. + + + Defines whether the search should be automatically performed when the form opens. + + + Defines whether to remove trailing special characters on import or not. + + + The fields that should be searched on, in order of search priority. Each field in the string will be checked for a valid corresponding search value in the request, and the first search type with a valid corresponding value will be used. + + + Defines whether or not the addon should automatically retrieve items related to a record being viewed. + + + The URL to the Alma API + + + API key used for interacting with the Alma API. + + + The MMS_ID Prefix are any characters that proceed the mms_id of a record in the URL. + + + + Utility.lua + DataMapping.lua + CustomizedMapping.lua + Catalog.lua + WebClient.lua + AlmaApi.lua + +
diff --git a/CustomizedMapping.lua b/CustomizedMapping.lua index ce0f81a..4332909 100644 --- a/CustomizedMapping.lua +++ b/CustomizedMapping.lua @@ -1,9 +1,9 @@ -CustomizedMapping = {} -CustomizedMapping.Locations = {}; - ---Note: The mappings listed are prefix matches. The addon will verify if the location value listed below is the prefix of the location code found in the MARC XML data. ---Since the addon is matching based on prefixes, more specific mappings should be listed first. ---If a mapping code is not found, the code will be used as its location. - --- Example Location Mapping: --- CustomizedMapping.Locations["finelock"] = "Fine Locked Case"; +CustomizedMapping = {} +CustomizedMapping.Locations = {}; + +--Note: The mappings listed are prefix matches. The addon will verify if the location value listed below is the prefix of the location code found in the MARC XML data. +--Since the addon is matching based on prefixes, more specific mappings should be listed first. +--If a mapping code is not found, the code will be used as its location. + +-- Example Location Mapping: +-- CustomizedMapping.Locations["finelock"] = "Fine Locked Case"; diff --git a/DataMapping.lua b/DataMapping.lua index 5a6a07c..d8ede47 100644 --- a/DataMapping.lua +++ b/DataMapping.lua @@ -1,84 +1,102 @@ -DataMapping = {} -DataMapping.Icons = {}; -DataMapping.SearchTypes = {}; -DataMapping.SourceFields = {}; -DataMapping.ImportFields = {}; -DataMapping.ImportFields.Bibliographic = {}; -DataMapping.ImportFields.Holding = {}; -DataMapping.ImportFields.StaticHolding = {}; - ---Typical Settings that shoudn't need user configuration -DataMapping.LabelName = "Catalog Search"; - --- Icons: Aeon -DataMapping.Icons["Aeon"] = {}; -DataMapping.Icons["Aeon"]["Search"] = "srch_32x32"; -DataMapping.Icons["Aeon"]["Home"] = "home_32x32"; -DataMapping.Icons["Aeon"]["Web"] = "web_32x32"; -DataMapping.Icons["Aeon"]["Record"] = "record_32x32"; -DataMapping.Icons["Aeon"]["Import"] = "impt_32x32"; - --- SearchTypes (query strings appended to the base URL) -DataMapping.SearchTypes["Title"] = "?search_field=title_search&q="; -DataMapping.SearchTypes["Author"] = "?search_field=author_search&q="; -DataMapping.SearchTypes["Call Number"] = "?search_field=call_number_xfacet&q="; - --- Source Fields: Aeon -DataMapping.SourceFields["Aeon"] = {}; -DataMapping.SourceFields["Aeon"]["Title"] = "ItemTitle"; -DataMapping.SourceFields["Aeon"]["Author"] = "ItemAuthor"; -DataMapping.SourceFields["Aeon"]["Call Number"] = "CallNumber"; -DataMapping.SourceFields["Aeon"]["Catalog Number"] = "ReferenceNumber"; - - --- Import Fields -DataMapping.ImportFields.Bibliographic["Aeon"] = { - { - Field = "ItemTitle", MaxSize = 255, - Value = "//datafield[@tag='245']/subfield[@code='a']|//datafield[@tag='245']/subfield[@code='b']" - }, - { - Field = "ItemAuthor", MaxSize = 255, - Value = "//datafield[@tag='100']/subfield[@code='a']|//datafield[@tag='100']/subfield[@code='b'],//datafield[@tag='110']/subfield[@code='a']|//datafield[@tag='110']/subfield[@code='b'],//datafield[@tag='111']/subfield[@code='a']|//datafield[@tag='111']/subfield[@code='b']" - }, - { - Field ="ItemPublisher", MaxSize = 255, - Value = "//datafield[@tag='260']/subfield[@code='b']" - }, - { - Field ="ItemPlace", MaxSize = 255, - Value = "//datafield[@tag='260']/subfield[@code='a']" - }, - { - Field ="ItemDate", MaxSize = 50, - Value = "//datafield[@tag='260']/subfield[@code='c']" - }, - { - Field ="ItemEdition", MaxSize = 50, - Value = "//datafield[@tag='250']/subfield[@code='a']" - }, - { - Field ="ItemIssue", MaxSize = 255, - Value = "//datafield[@tag='773']/subfield[@code='g']" - } -}; - - -DataMapping.ImportFields.Holding["Aeon"] = { - { - Field = "ReferenceNumber", MaxSize = 50, - Value = "ReferenceNumber" - }, - { - Field = "CallNumber", MaxSize = 255, - Value = "CallNumber" - }, - { - Field = "Location", MaxSize = 255, - Value = "Location" - }, - { - Field = "SubLocation", MaxSize = 255, - Value = "LocationCode" - } -}; \ No newline at end of file +DataMapping = {} +DataMapping.Icons = {}; +DataMapping.SearchTypes = {}; +DataMapping.SourceFields = {}; +DataMapping.ImportFields = {}; +DataMapping.ImportFields.Bibliographic = {}; +DataMapping.ImportFields.Holding = {}; +DataMapping.ImportFields.StaticHolding = {}; + +-- Typical Settings that shouldn't need user configuration +DataMapping.LabelName = "Catalog Search"; + +-- Icons: Aeon +DataMapping.Icons["Aeon"] = {}; +DataMapping.Icons["Aeon"]["Search"] = "srch_32x32"; +DataMapping.Icons["Aeon"]["Home"] = "home_32x32"; +DataMapping.Icons["Aeon"]["Web"] = "web_32x32"; +DataMapping.Icons["Aeon"]["Record"] = "record_32x32"; +DataMapping.Icons["Aeon"]["Import"] = "impt_32x32"; + +-- SearchTypes (query strings appended to the base URL) +DataMapping.SearchTypes["Title"] = "?search_field=title_search&q="; +DataMapping.SearchTypes["Author"] = "?search_field=author_search&q="; +DataMapping.SearchTypes["Call Number"] = "?search_field=call_number_xfacet&q="; + +-- Source Fields: Aeon +DataMapping.SourceFields["Aeon"] = {}; +DataMapping.SourceFields["Aeon"]["Title"] = { Table = "Transaction", Field = "ItemTitle" }; +DataMapping.SourceFields["Aeon"]["Author"] = { Table = "Transaction", Field = "ItemAuthor" }; +DataMapping.SourceFields["Aeon"]["Call Number"] = { Table = "Transaction", Field = "CallNumber" }; +DataMapping.SourceFields["Aeon"]["Catalog Number"] = { Table = "Transaction", Field = "ReferenceNumber" }; +DataMapping.SourceFields["Aeon"]["TransactionNumber"] = { Table = "Transaction", Field = "TransactionNumber" }; + +-- Import Fields +DataMapping.ImportFields.Bibliographic["Aeon"] = { + { + Table = "Transaction", + Field = "ItemTitle", + MaxSize = 255, + Value = "//datafield[@tag='245']/subfield[@code='a']|//datafield[@tag='245']/subfield[@code='b']" + }, + { + Table = "Transaction", + Field = "ItemAuthor", + MaxSize = 255, + Value = "//datafield[@tag='100']/subfield[@code='a']|//datafield[@tag='100']/subfield[@code='b'],//datafield[@tag='110']/subfield[@code='a']|//datafield[@tag='110']/subfield[@code='b'],//datafield[@tag='111']/subfield[@code='a']|//datafield[@tag='111']/subfield[@code='b']" + }, + { + Table = "Transaction", + Field = "ItemPublisher", + MaxSize = 255, + Value = "//datafield[@tag='260']/subfield[@code='b']" + }, + { + Table = "Transaction", + Field = "ItemPlace", + MaxSize = 255, + Value = "//datafield[@tag='260']/subfield[@code='a']" + }, + { + Table = "Transaction", + Field = "ItemDate", + MaxSize = 50, + Value = "//datafield[@tag='260']/subfield[@code='c']" + }, + { + Table = "Transaction", + Field = "ItemEdition", + MaxSize = 50, + Value = "//datafield[@tag='250']/subfield[@code='a']" + }, + { + Table = "Transaction", + Field = "ItemIssue", + MaxSize = 255, + Value = "//datafield[@tag='773']/subfield[@code='g']" + } +}; + + +DataMapping.ImportFields.Holding["Aeon"] = { + { + Table = "Transaction", + Field = "ReferenceNumber", MaxSize = 50, + Value = "ReferenceNumber" + }, + { + Table = "Transaction", + Field = "CallNumber", MaxSize = 255, + Value = "CallNumber" + }, + { + Table = "Transaction", + Field = "Location", MaxSize = 255, + Value = "Location" + }, + { + Table = "Transaction", + Field = "SubLocation", MaxSize = 255, + Value = "LocationCode" + } +}; diff --git a/README.md b/README.md index 934d208..0c136db 100644 --- a/README.md +++ b/README.md @@ -1,160 +1,177 @@ -# Alma Blacklight Catalog Search - -## Versions -**1.0.0 -** Initial release - -**1.1.0 -** Added LocationCode row. Corrected library mapping. - -**1.2.0 -** Correct bug that removes a incorrect characters from certain fields when `RemoveTrailingSpecialCharacters` is on. - -## Summary -The addon is located within an item record of an Atlas Product. It is found on the `"Catalog Search"` tab. The addon takes information from the fields in the Atlas Product and searches the catalog in the configured ordered. When the item is found, one selects the desired holding in the *Item Grid* below the browser and clicks *Import*. The addon then makes the necessary API calls to the Alma API and imports the item's information into the Atlas Product. - -> **Note:** Only records with a valid MMS ID can be imported with this addon. An example of a record that may not have an MMS ID within your catalog is a record coming from an external resource like HathiTrust. - -## Settings - -> **CatalogURL:** The base URL that the query strings are appended to. -> -> **HomeURL:** Home page of the catalog. -> -> **AutoSearch:** Defines whether the search should be automatically performed when the form opens. -> ->**RemoveTrailingSpecialCharacters:** Defines whether to remove trailing special characters on import or not. The included special characters are "` \/+,.;:-=.`". ->*Examples: `Baton Rouge, La.,` becomes `Baton Rouge, La.`* -> ->**SearchPriorityList:** The fields that should be searched on, in order of search priority. Each field in the string will be checked for a valid corresponding search value in the request, and the first search type with a valid corresponding value will be used. Each search type must be separated by a comma. ->*Default: Catalog Number,Title,Author,Call Number* -> ->**AutoRetrieveItems:** Defines whether or not the addon should automatically retrieve items related to a record being viewed. Disabling this setting can save the site on Alma API calls because it will only make a [Retrieve Holdings List](https://developers.exlibrisgroup.com/alma/apis/bibs/GET/gwPcGly021om4RTvtjbPleCklCGxeYAfEqJOcQOaLEvEGUPgvJFpUQ==/af2fb69d-64f4-42bc-bb05-d8a0ae56936e) call when the button is pressed. -> ->**AlmaAPIURL:** The URL to the Alma API. The API URL is generally the same between sites. (ex. `https://api-na.hosted.exlibrisgroup.com/almaws/v1/`) More information can be found on [Ex Libris' Site](https://developers.exlibrisgroup.com/alma/apis). -> ->**AlmaAPIKey:** API key used for interacting with the Alma API. -> ->**MMS_IDPrefix:** The MMS_ID Prefix are any characters that precede the mms_id of a record in the URL. The addon gets the current record's MMS ID by looking at the URL of the record's page. A typical Blacklight record url is `/catalog/{MMS ID}`, but if your site has anything that precedes the MMS ID, it must be specified in this setting. ->*Example: UPenn's record URL is `/catalog/Franklin_{MMS ID}`, so their MMS_IDPrefix is "`Franklin_` "* - -## Buttons -The buttons for the Alma Blacklight Catalog Search addon are located in the *"Catalog Search"* ribbon in the top left of the requests. - ->**Back:** Navigate back one page. -> ->**Forward:** Navigate forward one page. -> ->**Stop:** Stop loading the page. -> ->**Refresh:** Refresh the page. -> ->**New Search:** Goes to the home page of the catalog. -> ->**Title:** Performs a title search on the catalog using the contents of the title field. -> ->**Author:** Performs an author search on the catalog using the contents of the author field. -> ->**Call Number:** Performs a call number search on the catalog using the contents of the call number field. -> ->**Catalog Number:** Navigates directly to the item's page on the catalog using the contents of the reference field. ->*Note: If the catalog number is not a valid number, the browser will navigate to a page that does not exist.* -> ->**Retrieve Items:** Retrieves the holding records for that item. ->*Note:* This button will not appear when AutoRetrieveItems is enabled. -> ->**Import:** Imports the selected record in the items grid. - -## Data Mappings -Below are the default configurations for the catalog addon. The mappings within `DataMappings.lua` are settings that typically do not have to be modified from site to site. However, these data mappings can be changed to customize the fields, search queries, and xPath queries to the data. - ->**Caution:** Be sure to backup the `DataMappings.lua` file before making modifications Incorrectly configured mappings may cause the addon to stop functioning correctly. - -### SearchTypes -The query string is appended to the base catalog url (defined in the settings) when performing the corresponding search *(title, author, etc).* - -*Default Configuration:* - -| Search Type | Query String | -| ----------------------------------------- | ------------------------------------- | -| DataMapping.SearchTypes["Title"] | `?search_field=title_search&q=` | -| DataMapping.SearchTypes["Author"] | `?search_field=author_search&q=` | -| DataMapping.SearchTypes["Call Number"] | `?search_field=call_number_xfacet&q=` | - ->**Note:** The *Catalog Number* search type is not listed here because the Catalog Number button goes directly to item page instead of searching the catalog, therefore, the URL is constructed differently. - -### Source Fields -The field that the addon reads from to perform the search. - -#### Aeon - -*Default Configuration:* - -| Field | Source Field | -| -------------------------------------------------- | ----------------- | -| DataMapping.SourceFields["Aeon"]["Title"] | `ItemTitle` | -| DataMapping.SourceFields["Aeon"]["Author"] | `ItemAuthor` | -| DataMapping.SourceFields["Aeon"]["Call Number"] | `CallNumber` | -| DataMapping.SourceFields["Aeon"]["Catalog Number"] | `ReferenceNumber` | - -### Bibliographic Import -The information within this data mapping is used to perform the bibliographic api call. The `Field` is the product field that the data will be imported into, `MaxSize` is the maximum character size the data going into the product field can be, and `Value` is the xPath queries to the information. - ->**Note:** One may specify multiple xPath queries for a single field by separating them with a comma. The addon will try each xPath query and returns the first successful one. -> ->*Example:* An author can be inside of `100$a and 100$b` or `110$a and 110$b`. To accomplish this, provide an xPath query for the 100 datafields and an xPath query for the 110 datafields separated by a comma. ->``` ->//datafield[@tag='100']/subfield[@code='a']|//datafield[@tag='100']/subfield[@code='b'], ->//datafield[@tag='110']/subfield[@code='a']|//datafield[@tag='110']/subfield[@code='b'] ->``` - -### Holding Import -The information within this data mapping is used import the correct information from the items grid. The `Field` is the product field that the data will be imported into, `MaxSize` is the maximum character size the data going into the product field can be, and `Value` is the FieldName of the column within the item grid. - -| Product Field | Value | Alma API XML Node | Description | -| --------------- | --------------- | ------------------- | --------------------------------------------------------------------- | -| ReferenceNumber | ReferenceNumber | mms_id | The catalog identifier for the record (MMS ID) | -| CallNumber | CallNumber | call_number | The item's call number | -| Location | Location | location (expanded) | The location name of the item (Configured in `CustomizedMapping.lua`) | -| Sublocation | LocationCode | location | The location code returned by the Alma API | -| Library | Library | library | The library where the item is held | - -> **Note:** The Holding ID can also be imported by adding another table with a Value of `HoldingId`. - -## Customized Mapping -The `CustomizedMapping.lua` file contains the mappings to variables that are more site specific. - -### Location Mapping -Maps an item's location code to a full name. If a location mapping isn't given, the addon will display the location code. The location code is taken from the `location` node returned by a [Retrieve Holdings List](https://developers.exlibrisgroup.com/alma/apis/bibs/GET/gwPcGly021om4RTvtjbPleCklCGxeYAfEqJOcQOaLEvEGUPgvJFpUQ==/af2fb69d-64f4-42bc-bb05-d8a0ae56936e) API call. - -```lua -CustomizedMapping.Locations["{Location Code}"] = "{Full Location Name}" -``` - - -## FAQ - -### How to add or change what information is displayed in the item grid? -There's more holdings information gathered than what is displayed in the item grid. If you wish to display or hide additional columns on the item grid, find the comment `-- Item Grid Column Settings` within the `BuildItemGrid()` function in the *Catalog.lua* file and change the `gridColumn.Visible` variable of the column you wish to modify. - -### How to modify what bibliographic information is imported? -To import additional bibliographic fields, add another lua table to the `DataMapping.ImportFields.Bibliographic[{Product Name}]` mapping. To remove a record from the importing remove it from the lua table. - -The table takes a `Field` which is the product's field name, a `MaxSize` which is the maximum characters to be imported into the product, and `Value` which is the xPath query to the data returned by the [Retrieve Bibs](https://developers.exlibrisgroup.com/alma/apis/bibs/GET/gwPcGly021q2Z+qBbnVJzw==/af2fb69d-64f4-42bc-bb05-d8a0ae56936e) Alma API call. - -## Developers - -The addon is developed to support Alma Catalogs that use Blacklight as its discovery layer in [Aeon](https://www.atlas-sys.com/aeon/), [Ares](https://www.atlas-sys.com/ares), and [ILLiad](https://www.atlas-sys.com/illiad/). - -Atlas welcomes developers to extend the addon with additional support. All pull requests will be merged and posted to the [addon directories](https://prometheus.atlas-sys.com/display/ILLiadAddons/Addon+Directory). - -### Addon Files - -* **Config.xml** - The addon configuration file. - -* **DataMapping.lua** - The data mapping file contains mappings for the items that do not typically change from site to site. - -* **CustomizedMapping.lua** - The a data mapping file that contains settings that are more site specific and likely to change (e.g. location codes). - -* **Catalog.lua** - The Catalog.lua is the main file for the addon. It contains the main business logic for importing the data from the Alma API into the Atlas Product. - -* **AlmaApi.lua** - The AlmaApi file is used to make the API calls against the Alma API. - -* **Utility.lua** - The Utility file is used for common lua functions. \ No newline at end of file +# Alma Blacklight Catalog Search + +## Versions + +**1.0.0 -** Initial release + +**1.1.0 -** Added LocationCode row. Corrected library mapping. + +**1.2.0 -** Correct bug that removes a incorrect characters from certain +fields when `RemoveTrailingSpecialCharacters` is on. + +**1.2.2 -** Updated AlmaApi.lua file + +**1.3.0 -** Removed inline references to Aeon's Transactions table. These references have been moved to the DataMapping.lua file. This will allow the addon to support Ares and ILLiad in the future. + + +## Summary + +The addon is located within an item record of an Atlas Product. It is found on the `"Catalog Search"` tab. The addon takes information from the fields in the Atlas Product and searches the catalog in the configured ordered. When the item is found, one selects the desired holding in the *Item Grid* below the browser and clicks *Import*. The addon then makes the necessary API calls to the Alma API and imports the item's information into the Atlas Product. + +> **Note:** Only records with a valid MMS ID can be imported with this addon. An example of a record that may not have an MMS ID within your catalog is a record coming from an external resource like HathiTrust. + + +## Settings + +> **CatalogURL:** The base URL that the query strings are appended to. +> +> **HomeURL:** Home page of the catalog. +> +> **AutoSearch:** Defines whether the search should be automatically performed when the form opens. +> +>**RemoveTrailingSpecialCharacters:** Defines whether to remove trailing special characters on import or not. The included special characters are "` \/+,.;:-=.`". +>*Examples: `Baton Rouge, La.,` becomes `Baton Rouge, La.`* +> +>**SearchPriorityList:** The fields that should be searched on, in order of search priority. Each field in the string will be checked for a valid corresponding search value in the request, and the first search type with a valid corresponding value will be used. Each search type must be separated by a comma. +>*Default: Catalog Number,Title,Author,Call Number* +> +>**AutoRetrieveItems:** Defines whether or not the addon should automatically retrieve items related to a record being viewed. Disabling this setting can save the site on Alma API calls because it will only make a [Retrieve Holdings List](https://developers.exlibrisgroup.com/alma/apis/bibs/GET/gwPcGly021om4RTvtjbPleCklCGxeYAfEqJOcQOaLEvEGUPgvJFpUQ==/af2fb69d-64f4-42bc-bb05-d8a0ae56936e) call when the button is pressed. +> +>**AlmaAPIURL:** The URL to the Alma API. The API URL is generally the same between sites. (ex. `https://api-na.hosted.exlibrisgroup.com/almaws/v1/`) More information can be found on [Ex Libris' Site](https://developers.exlibrisgroup.com/alma/apis). +> +>**AlmaAPIKey:** API key used for interacting with the Alma API. +> +>**MMS_IDPrefix:** The MMS_ID Prefix are any characters that precede the mms_id of a record in the URL. The addon gets the current record's MMS ID by looking at the URL of the record's page. A typical Blacklight record url is `/catalog/{MMS ID}`, but if your site has anything that precedes the MMS ID, it must be specified in this setting. +>*Example: UPenn's record URL is `/catalog/Franklin_{MMS ID}`, so their MMS_IDPrefix is "`Franklin_` "* + + +## Buttons + +The buttons for the Alma Blacklight Catalog Search addon are located in the *"Catalog Search"* ribbon in the top left of the requests. + +>**Back:** Navigate back one page. +> +>**Forward:** Navigate forward one page. +> +>**Stop:** Stop loading the page. +> +>**Refresh:** Refresh the page. +> +>**New Search:** Goes to the home page of the catalog. +> +>**Title:** Performs a title search on the catalog using the contents of the title field. +> +>**Author:** Performs an author search on the catalog using the contents of the author field. +> +>**Call Number:** Performs a call number search on the catalog using the contents of the call number field. +> +>**Catalog Number:** Navigates directly to the item's page on the catalog using the contents of the reference field. +>*Note: If the catalog number is not a valid number, the browser will navigate to a page that does not exist.* +> +>**Retrieve Items:** Retrieves the holding records for that item. +>*Note:* This button will not appear when AutoRetrieveItems is enabled. +> +>**Import:** Imports the selected record in the items grid. + + +## Data Mappings + +Below are the default configurations for the catalog addon. The mappings within `DataMappings.lua` are settings that typically do not have to be modified from site to site. However, these data mappings can be changed to customize the fields, search queries, and xPath queries to the data. + +>**Caution:** Be sure to backup the `DataMappings.lua` file before making modifications Incorrectly configured mappings may cause the addon to stop functioning correctly. + +### SearchTypes +The query string is appended to the base catalog url (defined in the settings) when performing the corresponding search *(title, author, etc).* + +*Default Configuration:* + +| Search Type | Query String | +| -------------------------------------- | ------------------------------------- | +| DataMapping.SearchTypes["Title"] | `?search_field=title_search&q=` | +| DataMapping.SearchTypes["Author"] | `?search_field=author_search&q=` | +| DataMapping.SearchTypes["Call Number"] | `?search_field=call_number_xfacet&q=` | + +>**Note:** The *Catalog Number* search type is not listed here because the Catalog Number button goes directly to item page instead of searching the catalog, therefore, the URL is constructed differently. + +### Source Fields +The field that the addon reads from to perform the search. + +#### Aeon + +*Default Configuration:* + +| Mapping | Source Table | Source Field | +| ----------------------------------------------------- | ------------- | ------------------- | +| DataMapping.SourceFields["Aeon"]["Title"] | `Transaction` | `ItemTitle` | +| DataMapping.SourceFields["Aeon"]["Author"] | `Transaction` | `ItemAuthor` | +| DataMapping.SourceFields["Aeon"]["Call Number"] | `Transaction` | `CallNumber` | +| DataMapping.SourceFields["Aeon"]["Catalog Number"] | `Transaction` | `ReferenceNumber` | +| DataMapping.SourceFields["Aeon"]["TransactionNumber"] | `Transaction` | `TransactionNumber` | + +### Bibliographic Import +The information within this data mapping is used to perform the bibliographic api call. The `Field` is the product field that the data will be imported into, `MaxSize` is the maximum character size the data going into the product field can be, and `Value` is the xPath queries to the information. + +>**Note:** One may specify multiple xPath queries for a single field by separating them with a comma. The addon will try each xPath query and returns the first successful one. +> +>*Example:* An author can be inside of `100$a and 100$b` or `110$a and 110$b`. To accomplish this, provide an xPath query for the 100 datafields and an xPath query for the 110 datafields separated by a comma. +>``` +>//datafield[@tag='100']/subfield[@code='a']|//datafield[@tag='100']/subfield[@code='b'], +>//datafield[@tag='110']/subfield[@code='a']|//datafield[@tag='110']/subfield[@code='b'] +>``` + +### Holding Import +The information within this data mapping is used import the correct information from the items grid. The `Field` is the product field that the data will be imported into, `MaxSize` is the maximum character size the data going into the product field can be, and `Value` is the FieldName of the column within the item grid. + +| Product Field | Value | Alma API XML Node | Description | +| --------------- | --------------- | ------------------- | --------------------------------------------------------------------- | +| ReferenceNumber | ReferenceNumber | mms_id | The catalog identifier for the record (MMS ID) | +| CallNumber | CallNumber | call_number | The item's call number | +| Location | Location | location (expanded) | The location name of the item (Configured in `CustomizedMapping.lua`) | +| Sublocation | LocationCode | location | The location code returned by the Alma API | +| Library | Library | library | The library where the item is held | + +> **Note:** The Holding ID can also be imported by adding another table with a Value of `HoldingId`. + + +## Customized Mapping + +The `CustomizedMapping.lua` file contains the mappings to variables that are more site specific. + +### Location Mapping +Maps an item's location code to a full name. If a location mapping isn't given, the addon will display the location code. The location code is taken from the `location` node returned by a [Retrieve Holdings List](https://developers.exlibrisgroup.com/alma/apis/bibs/GET/gwPcGly021om4RTvtjbPleCklCGxeYAfEqJOcQOaLEvEGUPgvJFpUQ==/af2fb69d-64f4-42bc-bb05-d8a0ae56936e) API call. + +```lua +CustomizedMapping.Locations["{Location Code}"] = "{Full Location Name}" +``` + + +## FAQ + +### How to add or change what information is displayed in the item grid? +There's more holdings information gathered than what is displayed in the item grid. If you wish to display or hide additional columns on the item grid, find the comment `-- Item Grid Column Settings` within the `BuildItemGrid()` function in the *Catalog.lua* file and change the `gridColumn.Visible` variable of the column you wish to modify. + +### How to modify what bibliographic information is imported? +To import additional bibliographic fields, add another lua table to the `DataMapping.ImportFields.Bibliographic[{Product Name}]` mapping. To remove a record from the importing remove it from the lua table. + +The table takes a `Table` and `Field` which correspond to a table and column name in the product, a `MaxSize` which is the maximum characters to be imported into the specified table column, and a `Value` which is the xPath query to the data returned by the [Retrieve Bibs](https://developers.exlibrisgroup.com/alma/apis/bibs/GET/gwPcGly021q2Z+qBbnVJzw==/af2fb69d-64f4-42bc-bb05-d8a0ae56936e) Alma API call. + + +## Developers + +The addon is developed to support Alma Catalogs that use Blacklight as its discovery layer in [Aeon](https://www.atlas-sys.com/aeon/), [Ares](https://www.atlas-sys.com/ares), and [ILLiad](https://www.atlas-sys.com/illiad/). + +Atlas welcomes developers to extend the addon with additional support. All pull requests will be merged and posted to the [addon directories](https://prometheus.atlas-sys.com/display/ILLiadAddons/Addon+Directory). + +### Addon Files + +* **Config.xml** - The addon configuration file. + +* **DataMapping.lua** - The data mapping file contains mappings for the items that do not typically change from site to site. + +* **CustomizedMapping.lua** - The a data mapping file that contains settings that are more site specific and likely to change (e.g. location codes). + +* **Catalog.lua** - The Catalog.lua is the main file for the addon. It contains the main business logic for importing the data from the Alma API into the Atlas Product. + +* **AlmaApi.lua** - The AlmaApi file is used to make the API calls against the Alma API. + +* **Utility.lua** - The Utility file is used for common lua functions. diff --git a/Utility.lua b/Utility.lua index 3500a0b..0def70d 100644 --- a/Utility.lua +++ b/Utility.lua @@ -1,240 +1,230 @@ -local UtilityInternal = {}; -UtilityInternal.DebugLogging = false; - -Utility = UtilityInternal; - -luanet.load_assembly("System"); - -local types = {}; -types["System.Type"] = luanet.import_type("System.Type"); -types["System.Action"] = luanet.import_type("System.Action"); - -local function Log(input, debugOnly) - debugOnly = debugOnly or false; - - if ((not debugOnly) or (debugOnly and UtilityInternal.DebugLogging)) then - local t = type(input); - - if (t == "string" or t == "number") then - LogDebug(input); - elseif (t == "table") then - LogTable(input); - elseif (t == "nil") then - LogDebug("(nil)"); - elseif (t == "boolean") then - if (input == true) then - LogDebug("True"); - else - LogDebug("False"); - end - elseif (t == "function") then - local success, result = pcall(input); - - if (success) then - Log(result, debugOnly); - end - elseif (t == "userdata") then - if (IsType(input, "System.Exception")) then - LogException(input); - else - pcall(function() - LogDebug(input:ToString()); - end); - end - end - end -end - -local function Trim(s) - local n = s:find"%S" - return n and s:match(".*%S", n) or "" -end - -local function IsType(o, t, checkFullName) - if ((o and type(o) == "userdata") and (t and type(t) == "string")) then - local comparisonType = types["System.Type"].GetType(t); - if (comparisonType) then - -- The comparison type was successfully loaded so we can do a check - -- that the object can be assigned to the comparison type. - return comparisonType:IsAssignableFrom(o:GetType()), true; - else - -- The comparison type was could not be loaded so we can only check - -- based on the names of the types. - if(checkFullName) then - return (o:GetType().FullName == t), false; - else - return (o:GetType().Name == t), false; - end - end - end - - return false, false; -end - -local function LogIndented(entry, depth) - depth = (depth or 0); - LogDebug(string.rep("> ", depth) .. entry); -end - -local function LogTable(input, depth) - assert(type(input) == "table", "LogTable expects a LUA table"); - - depth = (depth or 0); - - for key, value in pairs(input) do - if (value and type(value) == "table") then - LogIndented("Key: " .. key, depth); - LogTable(value, depth + 1); - else - local success, result = pcall(string.format, "%s", (value or "(nil)")); - - if (success) then - LogIndented("Key: " .. key .. " = " .. (value or "(nil)"), depth); - else - LogIndented("Key: " .. key .. " = (?)", depth); - end - end - end - - for index, value in ipairs(input) do - if (value and type(value) == "table") then - LogIndented("Index: " .. index, depth); - LogTable(value, depth + 1); - else - LogIndented("Index: " .. index .. " = " .. (value or "(nil)"), depth); - end - end -end - -function LogException(exception, depth) - depth = (depth or 0); - - if (exception) then - LogIndented(exception.Message, depth); - LogException(exception.InnerException, depth + 1); - end -end - - - -function URLDecode(s) - s = string.gsub(s, "+", " "); - s = string.gsub(s, "%%(%x%x)", function(h) - return string.char(tonumber(h, 16)); - end); - - s = string.gsub(s, "\r\n", "\n"); - - return s; -end - -function StringSplit(delimiter, text) - local list = {}; - local pos = 1; - - if string.find("", delimiter, 1) then -- this would result in endless loops - error("Delimiter cannot be an empty string."); - end - - while 1 do - local first,last = string.find(text, delimiter, pos); - - if first then -- found? - table.insert(list, string.sub(text, pos, first-1)); - pos = last+1; - else - table.insert(list, string.sub(text, pos)); - break; - end - end - - return list; -end - -local function URLEncode(s) - if (s) then - s = string.gsub(s, "\n", "\r\n") - s = string.gsub(s, "([^%w %-%_%.%~])", - function (c) - return string.format("%%%02X", string.byte(c)) - end); - s = string.gsub(s, " ", "+") - end - return s - end - - local function CreateQueryString(t) - - local query = nil; - - if (t and type(t) == "table") then - for k, v in pairs(t) do - if (v) then - local success, value = pcall(URLEncode, v); - if (success) then - query = string.format("%s%s=%s", ((query and (query .. "&")) or ""), k, value); - end - end - end - end - - return query; - end - - local function GetNodeCount(xmlElement, xPath, namespaceManager) - if (xmlElement == nil or xPath == nil) then - Log("Invalid Element/Path to retrieve value", true); - return nil; - end - - Log("GetNodeCount Path: ".. xPath, true); - - local datafieldNode = nil; - - if (namespaceManager ~= nil) then - datafieldNode = xmlElement:SelectNodes(xPath, namespaceManager); - else - datafieldNode = xmlElement:SelectNodes(xPath); - end - - return datafieldNode.Count; - end - - local function GetChildValue(xmlElement, xPath, namespaceManager) - Log("[Utility.GetChildValue] "..xPath); - if (xmlElement == nil or xPath == nil) then - Log("Invalid Element/Path to retrieve value."); - return nil; - end - - local datafieldNode = nil; - - if (namespaceManager ~= nil) then - datafieldNode = xmlElement:SelectNodes(xPath, namespaceManager); - else - datafieldNode = xmlElement:SelectNodes(xPath); - end - - Log("Found "..datafieldNode.Count.." node elements matching "..xPath); - local fieldValue = ""; - for d = 0, (datafieldNode.Count - 1) do - Log("datafieldnode value is: " .. datafieldNode:Item(d).InnerText, true); - fieldValue = fieldValue .. " " .. datafieldNode:Item(d).InnerText; - end - - fieldValue = Trim(fieldValue); - Log("GetChildValue Result: " .. fieldValue, true); - - return fieldValue; - end - - - UtilityInternal.Trim = Trim; - UtilityInternal.IsType = IsType; - UtilityInternal.Log = Log; - UtilityInternal.URLDecode = URLDecode; - UtilityInternal.URLEncode = URLEncode; - UtilityInternal.StringSplit = StringSplit; - UtilityInternal.CreateQueryString = CreateQueryString; - UtilityInternal.GetXmlChildValue = GetChildValue; - UtilityInternal.GetXmlNodeCount = GetNodeCount; +local UtilityInternal = {}; +UtilityInternal.DebugLogging = false; + +Utility = UtilityInternal; + +luanet.load_assembly("System"); + +local types = {}; +types["System.Type"] = luanet.import_type("System.Type"); +types["System.Action"] = luanet.import_type("System.Action"); + +local function Log(input, debugOnly) + debugOnly = debugOnly or false; + + if ((not debugOnly) or (debugOnly and UtilityInternal.DebugLogging)) then + local t = type(input); + + if (t == "string" or t == "number") then + LogDebug(input); + elseif (t == "table") then + LogTable(input); + elseif (t == "nil") then + LogDebug("(nil)"); + elseif (t == "boolean") then + if (input == true) then + LogDebug("True"); + else + LogDebug("False"); + end + elseif (t == "function") then + local success, result = pcall(input); + + if (success) then + Log(result, debugOnly); + end + elseif (t == "userdata") then + if (IsType(input, "System.Exception")) then + LogException(input); + else + pcall(function() + LogDebug(input:ToString()); + end); + end + end + end +end + +local function Trim(s) + local n = s:find"%S" + return n and s:match(".*%S", n) or "" +end + +local function IsType(o, t, checkFullName) + if ((o and type(o) == "userdata") and (t and type(t) == "string")) then + local comparisonType = types["System.Type"].GetType(t); + if (comparisonType) then + -- The comparison type was successfully loaded so we can do a check + -- that the object can be assigned to the comparison type. + return comparisonType:IsAssignableFrom(o:GetType()), true; + else + -- The comparison type was could not be loaded so we can only check + -- based on the names of the types. + if(checkFullName) then + return (o:GetType().FullName == t), false; + else + return (o:GetType().Name == t), false; + end + end + end + + return false, false; +end + +local function LogIndented(entry, depth) + depth = (depth or 0); + LogDebug(string.rep("> ", depth) .. entry); +end + +local function LogTable(input, depth) + assert(type(input) == "table", "LogTable expects a LUA table"); + + depth = (depth or 0); + + for key, value in pairs(input) do + if (value and type(value) == "table") then + LogIndented("Key: " .. key, depth); + LogTable(value, depth + 1); + else + local success, result = pcall(string.format, "%s", (value or "(nil)")); + + if (success) then + LogIndented("Key: " .. key .. " = " .. (value or "(nil)"), depth); + else + LogIndented("Key: " .. key .. " = (?)", depth); + end + end + end + + for index, value in ipairs(input) do + if (value and type(value) == "table") then + LogIndented("Index: " .. index, depth); + LogTable(value, depth + 1); + else + LogIndented("Index: " .. index .. " = " .. (value or "(nil)"), depth); + end + end +end + +local function LogException(exception, depth) + depth = (depth or 0); + + if (exception) then + LogIndented(exception.Message, depth); + LogException(exception.InnerException, depth + 1); + end +end + + + +local function URLDecode(s) + s = string.gsub(s, "+", " "); + s = string.gsub(s, "%%(%x%x)", function(h) + return string.char(tonumber(h, 16)); + end); + + s = string.gsub(s, "\r\n", "\n"); + + return s; +end + +local function StringSplit(delimiter, text) + if delimiter == nil then + delimiter = "%s" + end + local t={}; + local i=1; + for str in string.gmatch(text, "([^"..delimiter.."]+)") do + t[i] = str + i = i + 1 + end + return t +end + +local function URLEncode(s) + if (s) then + s = string.gsub(s, "\n", "\r\n") + s = string.gsub(s, "([^%w %-%_%.%~])", + function (c) + return string.format("%%%02X", string.byte(c)) + end); + s = string.gsub(s, " ", "+") + end + return s + end + + local function CreateQueryString(t) + + local query = nil; + + if (t and type(t) == "table") then + for k, v in pairs(t) do + if (v) then + local success, value = pcall(URLEncode, v); + if (success) then + query = string.format("%s%s=%s", ((query and (query .. "&")) or ""), k, value); + end + end + end + end + + return query; + end + + local function GetNodeCount(xmlElement, xPath, namespaceManager) + if (xmlElement == nil or xPath == nil) then + Log("Invalid Element/Path to retrieve value", true); + return nil; + end + + Log("GetNodeCount Path: ".. xPath, true); + + local datafieldNode = nil; + + if (namespaceManager ~= nil) then + datafieldNode = xmlElement:SelectNodes(xPath, namespaceManager); + else + datafieldNode = xmlElement:SelectNodes(xPath); + end + + return datafieldNode.Count; + end + + local function GetChildValue(xmlElement, xPath, namespaceManager) + Log("[Utility.GetChildValue] "..xPath); + if (xmlElement == nil or xPath == nil) then + Log("Invalid Element/Path to retrieve value."); + return nil; + end + + local datafieldNode = nil; + + if (namespaceManager ~= nil) then + datafieldNode = xmlElement:SelectNodes(xPath, namespaceManager); + else + datafieldNode = xmlElement:SelectNodes(xPath); + end + + Log("Found "..datafieldNode.Count.." node elements matching "..xPath); + local fieldValue = ""; + for d = 0, (datafieldNode.Count - 1) do + Log("datafieldnode value is: " .. datafieldNode:Item(d).InnerText, true); + fieldValue = fieldValue .. " " .. datafieldNode:Item(d).InnerText; + end + + fieldValue = Trim(fieldValue); + Log("GetChildValue Result: " .. fieldValue, true); + + return fieldValue; + end + + + UtilityInternal.Trim = Trim; + UtilityInternal.IsType = IsType; + UtilityInternal.Log = Log; + UtilityInternal.URLDecode = URLDecode; + UtilityInternal.URLEncode = URLEncode; + UtilityInternal.StringSplit = StringSplit; + UtilityInternal.CreateQueryString = CreateQueryString; + UtilityInternal.GetXmlChildValue = GetChildValue; + UtilityInternal.GetXmlNodeCount = GetNodeCount; diff --git a/WebClient.lua b/WebClient.lua new file mode 100644 index 0000000..ae7e8dd --- /dev/null +++ b/WebClient.lua @@ -0,0 +1,61 @@ +WebClient = {}; + +local types = {}; +types["log4net.LogManager"] = luanet.import_type("log4net.LogManager"); +types["System.Net.WebClient"] = luanet.import_type("System.Net.WebClient"); +types["System.Text.Encoding"] = luanet.import_type("System.Text.Encoding"); +types["System.Xml.XmlTextReader"] = luanet.import_type("System.Xml.XmlTextReader"); +types["System.Xml.XmlDocument"] = luanet.import_type("System.Xml.XmlDocument"); + +-- Create a logger +local log = types["log4net.LogManager"].GetLogger(rootLogger .. ".AlmaApi"); + +local function GetRequest(requestUrl, headers) + local webClient = types["System.Net.WebClient"](); + local response = nil; + log:Debug("Created Web Client"); + webClient.Encoding = types["System.Text.Encoding"].UTF8; + + for _, header in ipairs(headers) do + webClient.Headers:Add(header); + end + + local success, error = pcall(function () + response = webClient:DownloadString(requestUrl); + end); + + webClient:Dispose(); + log:Debug("Disposed Web Client"); + + if(success) then + return response; + else + log:InfoFormat("Unable to get response from the request url: {0}", error); + end +end + +local function ReadResponse( responseString ) + if (responseString and #responseString > 0) then + + local responseDocument = types["System.Xml.XmlDocument"](); + + local documentLoaded, error = pcall(function () + responseDocument:LoadXml(responseString); + end); + + if (documentLoaded) then + return responseDocument; + else + log:InfoFormat("Unable to load response content as XML: {0}", error); + return nil; + end + else + log:Info("Unable to read response content"); + end + + return nil; +end + +--Exports +WebClient.GetRequest = GetRequest; +WebClient.ReadResponse = ReadResponse;