3
votes

I'm attempting to use the DNSJava library from clojure. I attempt the following:

dmarced.dns> (def results (.run (Lookup. "google.com" Type/TXT)))
#'dmarced.dns/results
dmarced.dns> (def r (get results 0))
#'dmarced.dns/r
dmarced.dns> r
#object[org.xbill.DNS.TXTRecord 0x687a3556 "google.com.\t\t3599\tIN\tTXT\t\"v=spf1 include:_spf.google.com ~all\""]
dmarced.dns> (class r)
org.xbill.DNS.TXTRecord
dmarced.dns> (instance? TXTRecord r)
true

Great! I know from the docs that I should be able to use .getStrings to get the content of the record.

dmarced.dns> (.getStrings r)
Reflection warning, *cider-repl dmarced*:150:13 - reference to field getStrings can't be resolved.
IllegalArgumentException Can't call public method of non-public class: public java.util.List org.xbill.DNS.TXTBase.getStrings()  clojure.lang.Reflector.invokeMatchingMethod (Reflector.java:88)

Ok quick google tells me this can be solved via type hints:

dmarced.dns> (.getStrings ^TXTRecord r)
Reflection warning, *cider-repl dmarced*:153:13 - call to method getStrings on org.xbill.DNS.TXTRecord can't be resolved (argument types: ).
IllegalArgumentException Can't call public method of non-public class: public java.util.List org.xbill.DNS.TXTBase.getStrings()  clojure.lang.Reflector.invokeMatchingMethod (Reflector.java:88)

Huh. At I've search Google further, and read the full page on interop, but I'm not having any luck.

Looking at the source for TXTRecord I see that it extends TXTBase which is an abstract class where getStrings is implemented. Since TXTRecord extends it, I should have access to getStrings through that (and I went so far as to write a Java program to verify it.

Does anyone know how I can access this via Clojure?

EDIT Working java program

import org.xbill.DNS.*;

class Main {
    public static void main(String [] args) throws TextParseException {
        Lookup l = new Lookup("google.com", Type.TXT);
        Record [] rs = l.run();
        for(int i = 0; i < rs.length; i++) {
            TXTRecord tr = (TXTRecord)rs[i];
            for(int j = 0; j < tr.getStrings().size(); j ++) {
                System.out.println(tr.getStrings().get(j));
            }
        }

    }
}
2
Well, TXTBase is not declared as public. So I don't know why you'd have access to any methods declared on that class outside the org.xbill.dns package. And that's exactly what the exception message hints at -- "Can't call public method on non-public class" (emphasis added). Could you add the Java program that works so we can compare with the Clojure version, please? - Nathan Davis
Added. Both are using dnsjava 2.1.7 off maven. I can provide the gradle and project.clj files if needed. - Walton Hoops

2 Answers

4
votes

It looks that it is a known bug CLJ-1243. You can find more details and probably root cause by reading Netty issue description:

The public methods inherited from AbstractBootstrap cannot be invoked via the Java reflection API due to long-standing bugs such as JDK-4283544.

This is a serious problem for alternative JVM languages which use reflection to discover Java methods. For example, Clojure uses reflection in its compiler, and it cannot invoke these methods at all, as reported in CLJ-1243.

And part of the description from the JDK bug:

java.lang.reflect.Field (get* and set*) and Method (invoke) base their access check on the declaring class. This is contrary to the JLS, which defines accessibility in terms of the reference type.

1
votes

Piotrek seems to be right about why it's occurring. The Clojure bug report suggests writing a Java class to get the information. It's not pretty but I managed to work around it with

dmarced.core> (def txt-strings-method (doto (.getDeclaredMethod TXTBase "getStrings" nil) (.setAccessible true)))
#'dmarced.core/txt-strings-method
dmarced.core> (defn get-txt-strings [r]
                (.invoke m r nil))
#'dmarced.core/get-txt-strings
dmarced.core> (get-txt-strings r)
["v=spf1 include:_spf.google.com ~all"]
dmarced.core>