0

I have a JSONObject I am using to create a table. However, when I use the keys a second time to create each of the cells, it does nothing. I included log statements to test this out. No singular keys are printed. Call me stumped.

class MovieFragment : Fragment(), FinraDataInterface {

    private lateinit var binding: MovieFragmentBinding

    val finraDataInterface: FinraDataInterface = this

    override fun drawTable(jsonArrayData: JSONArray) {
//         ------ Draw the table -----
//         Draw the keys first
        val headerRow = TableRow(requireActivity())
        Log.d("jsonArray", jsonArrayData[0].toString())
        val firstRecord = jsonArrayData[0] as JSONObject
        val keys = firstRecord.keys()

        for (key in keys) {
            val textView = TextView(requireActivity())
            textView.text = key
            headerRow.addView(textView)
        }
        binding.table.addView(headerRow)

        // Draw the data second
        for (i in 0 until jsonArrayData.length()) {
            val record = jsonArrayData[i] as JSONObject
            Log.d("Record", record.toString())
            val tableRow = TableRow(requireActivity())
            Log.d("Table Row", "Table Row created")
            Log.d("Keys", listOf(keys).toString())

            // Error is right here.
            for (key in keys) {
                Log.d("key", key)
                val textView = TextView(requireActivity())
                val cellText = record[key] as String
                textView.text = cellText
                tableRow.addView(textView)
            }
            binding.table.addView(tableRow)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = MovieFragmentBinding.inflate(layoutInflater)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        finraGetBondInfo("AMC")
    }




    private fun finraGetBondInfo(companyName: String) {
        val url = "https://www.finra.org/finra-data/fixed-income/corp-and-agency"

        val queue = Volley.newRequestQueue(requireActivity())
        var cfruid = ""

        val stringRequest = object: StringRequest(Request.Method.GET, url,
            { response ->
                Log.d("A", "Response is: " + response.substring(0,500))
            },
            { error ->
                Log.e("Error", error.toString())
            })
        {
            override fun  parseNetworkResponse(response : NetworkResponse) : Response<String> {
                // since we don't know which of the two underlying network vehicles
                // will Volley use, we have to handle and store session cookies manually
                Log.i("response", response.headers.toString());
                val cookies = HttpCookie.parse(response.headers?.get("Set-Cookie"))
                Log.d("Cookies:", cookies[0].toString())
                val indexOfEqualSign = cookies[0].toString().indexOf('=')
                cfruid = cookies[0].toString().substring(indexOfEqualSign + 1)
                Log.d("Cfruid:", cfruid)


                return super.parseNetworkResponse(response)
            }
        }

        queue.add(stringRequest)

        // Create our JSON Object for the finra call
        val json = JSONObject()
        val fieldsArray = JSONArray()
        fieldsArray.put("issueSymbolIdentifier")
        fieldsArray.put("issuerName")
        fieldsArray.put("isCallable")
        fieldsArray.put("productSubTypeCode")
        fieldsArray.put("couponRate")
        fieldsArray.put("maturityDate")
        fieldsArray.put("industryGroup")
        fieldsArray.put("moodysRating")
        fieldsArray.put("standardAndPoorsRating")
        fieldsArray.put("lastSalePrice")
        fieldsArray.put("lastSaleYield")
        json.put("fields", fieldsArray)

        json.put("dateRangeFilters", JSONArray())
        json.put("domainFilters", JSONArray())
        json.put("compareFilters", JSONArray())

        val multiJson = JSONObject()
        multiJson.put("fuzzy", false)
        multiJson.put("searchValue", companyName)
        multiJson.put("synonym", true)
        val subfieldsJson = JSONObject()
        subfieldsJson.put("name", "issuerName")
        subfieldsJson.put("boost", 1)
        val fieldsArray2 = JSONArray()
        fieldsArray2.put(subfieldsJson)
        multiJson.put("fields", fieldsArray2)
        val multiArray = JSONArray()
        multiArray.put(multiJson)

        json.put("multiFieldMatchFilters", multiArray)
        json.put("orFilters", JSONArray())
        json.put("aggregationFilter", JSONObject.NULL)
        val sortFieldsArray = JSONArray()
        sortFieldsArray.put("+issuerName")
        json.put("sortFields", sortFieldsArray)
        json.put("limit", 50)
        json.put("offset", 0)
        json.put("delimiter", JSONObject.NULL)
        json.put("quoteValues", false)

        Log.d("JSON", json.toString())


        val url2 = "https://services-dynarep.ddwa.finra.org/public/reporting/v2/data/group/FixedIncomeMarket/name/CorporateAndAgencySecurities"
        val request2 = object: JsonObjectRequest(Request.Method.POST, url2, json,
            { response ->
                // TODO replace this with our calls to make tab2 display bond data
                val returnBody =  response["returnBody"] as JSONObject
                var stringData = returnBody["data"] as String
                stringData = stringData.replace("\\n", "").replace("\\", "")
                val jsonArrayData = JSONArray(stringData)

                finraDataInterface.drawTable(jsonArrayData)
            },
            { error ->
                Log.e("Error", error.toString())
            })
        {
            override fun getHeaders(): MutableMap<String, String> {
                val headers = HashMap<String, String>()
                headers["Authority"] = "services-dynarep.ddwa.finra.org"
                headers["Accept"] = "application/json, text/plain, */*"
                headers["Cookie"] = "XSRF-TOKEN=$cfruid;"
                headers["Origin"] = "https://www.finra.org"
                headers["Referer"] = "https://www.finra.org/"
                headers["X-XSRF-token"] = cfruid
                headers["user-agent"] =  "python-requests/2.31.0"
                Log.d("headers:", headers.toString())
                return headers
            }

            override fun getBodyContentType(): String {
                return "application/json"
            }
        }
        queue.add(request2)

    }

}

What has me particularly confused is that the first for loop of using the keys to create the header row works fine. I tried casting stuff to different types to see if it was some type error. However, it is just the second for loop that is not running. I do not get any error messages. What am I missing? Thanks.

ndc85430
  • 1,395
  • 3
  • 11
  • 17
David Frick
  • 641
  • 1
  • 9
  • 25
  • Put a breakpoint in the problematic loop to see if it's being called. That will narrow down the problem. If it is being called, then your issue is that the text views you're creating aren't becoming visible. – Tenfour04 Aug 28 '23 at 18:40
  • This isn't related to the issue you're talking about, but you have a race condition in `finraGetBondInfo()`. Two different asynchronous tasks are submitted and both use the `cfruid` variable (one writes, the other reads), but we don't know which will use it first because we don't know which one will finish first. – Tenfour04 Aug 28 '23 at 18:41
  • @Tenfour04 alright, I'll need to fix that. Thanks for the heads up. – David Frick Aug 28 '23 at 19:27
  • @Tenfour04 is using an interface the proper way to handle that. That is how I did it. – David Frick Aug 28 '23 at 19:47
  • I don't understand your last comment. Are you asking a question? – Tenfour04 Aug 28 '23 at 20:49
  • yes, I need to return cfruid from the first async code segment, See https://stackoverflow.com/questions/56576086/how-to-callback-a-value-from-volley-on-response-function-in-kotlin for the solution I followed to solve it. I used an interface. Is that the correct way? – David Frick Aug 28 '23 at 21:21
  • Well, you have to use an interface to get the result. The problem is that you are submitting the two requests in parallel so there’s no guarantee of which order the callbacks will be called. The code in your second request might get called first, so cfruid would still be an empty string when it’s being used there. To do things sequentially, you would have to make the second request from *inside* the callback of the first request. This does lead to heavily nested code that is hard to read. It’s nicknamed “callback hell” and is one of the things solved by coroutines. – Tenfour04 Aug 28 '23 at 22:05
  • For some reason, Volley still doesn’t support coroutines out of the box. Honestly it looks mostly abandoned by Google. But there might be a library that adds coroutine support so you don’t have to roll your own. I don’t use Volley myself so I don’t know exactly how I would implement the conversion to coroutines. – Tenfour04 Aug 28 '23 at 22:08
  • Okay, thanks, this has been enlightening. I will need to look more into coroutines. `Callback hell` haha. Makes sense. Not sure, what I will switch to from Volley. Definitely need to do some research. – David Frick Aug 28 '23 at 22:09
  • Another possible solution is that you create two nullable variables, one for each of the two callbacks’ results. In each callback you set the result to one of the variables, check if the other variable is non-null yet, and then process both results together if it is. Kind of convoluted but this allows the requests to run in parallel so it would be faster. – Tenfour04 Aug 28 '23 at 22:12
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/255090/discussion-between-david-frick-and-tenfour04). – David Frick Aug 28 '23 at 22:17
  • @Tenfour04 do you recommend any good sources of information on using coroutines. I find the documentation ambigious at times and it seems every tutorial online is super basic. – David Frick Aug 28 '23 at 22:17
  • I don’t know of a good one. I read the documentation and Medium articles by Roman Elizarov, but it took a few read-throughs and browsing source code of the Kotlin extensions in Retrofit before it clicked for me. – Tenfour04 Aug 29 '23 at 00:27

1 Answers1

2

Assuming you use the JSON-java (org.json) library here, please note JSONObject.keys() method returns an Iterator over keys. Iterator is different than Iterable or collections - it is a "live" iteration over some data and it can be consumed only once. After we consume all items, the iterator is constantly at the end of the data and it doesn't provide any additional items.

This is easy to miss as most libraries don't provide iterators, but iterables which can be consumed multiple times.

To fix the problem we need to either create another iterator using keys() or we can instead use keySet(), which returns a set, so can be iterated multiple times. If keySet() is not available, we can convert an iterator to a list with: asSequence().toList().

broot
  • 21,588
  • 3
  • 30
  • 35
  • could you edit an example of getting a `keySet()` I am unable to find any documentation on it. Continuing to look now. – David Frick Aug 28 '23 at 21:40
  • Oddly enough, my program does not recognize `keySet()` which makes more sense to me to use. I needed to tweak some other portions of the code and got it working. Thank you. – David Frick Aug 28 '23 at 22:10
  • @DavidFrick To be honest, I'm not sure what is the meaning of this. This is the official webpage for the library: https://github.com/stleary/JSON-java and `keySet()` exists there. Android provides almost the same API, but there is no `keySet()`: https://developer.android.com/reference/org/json/JSONObject I updated the answer and provided and alternative. – broot Aug 28 '23 at 22:43
  • I import `import org.json.JSONObject` so you would think it would have `keySet()`. Android must package its version behind the scenes. – David Frick Aug 29 '23 at 00:44