cmd/gobind,bind: generate complete Java interface with gobind

Output every Java class, including the support classes, from gobind
-lang=java. In addition, replace Go package export data parsing with
converting from go/ast to go/types. That way, gobind can tolerate
unknown imports as long as the exported Go API doesn't use them.

In a follow-up CL, the gobind gradle plugin will use gobind for a first
pass to expose the generated Java classes to the android plugin.

Change-Id: I8134899ec818c7fee79e4d9df8afcae9dd679add
Reviewed-on: https://go-review.googlesource.com/30093
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/bind/genjava.go b/bind/genjava.go
index 99ab7aa..c0856b0 100644
--- a/bind/genjava.go
+++ b/bind/genjava.go
@@ -816,22 +816,31 @@
 	"try", "void", "volatile", "while", "false", "null", "true"})
 
 func (g *JavaGen) javaPkgName(pkg *types.Package) string {
+	return JavaPkgName(g.JavaPkg, pkg)
+}
+
+// JavaPkgName returns the Java package name for a Go package
+// given a pkg prefix. If the prefix is empty, "go" is used
+// instead.
+func JavaPkgName(pkgPrefix string, pkg *types.Package) string {
 	if pkg == nil {
 		return "go"
 	}
 	s := javaNameReplacer(pkg.Name())
-	if g.JavaPkg != "" {
-		return g.JavaPkg + "." + s
+	if pkgPrefix != "" {
+		return pkgPrefix + "." + s
 	} else {
 		return "go." + s
 	}
 }
 
 func (g *JavaGen) className() string {
-	return className(g.Pkg)
+	return JavaClassName(g.Pkg)
 }
 
-func className(pkg *types.Package) string {
+// JavaClassName returns the name of the Java class that
+// contains Go package level identifiers.
+func JavaClassName(pkg *types.Package) string {
 	if pkg == nil {
 		return "Universe"
 	}
@@ -1296,7 +1305,7 @@
 	}
 	for _, iface := range g.interfaces {
 		pkg := iface.obj.Pkg()
-		g.Printf("clazz = (*env)->FindClass(env, %q);\n", g.jniClassSigPrefix(pkg)+className(pkg)+"$proxy"+iface.obj.Name())
+		g.Printf("clazz = (*env)->FindClass(env, %q);\n", g.jniClassSigPrefix(pkg)+JavaClassName(pkg)+"$proxy"+iface.obj.Name())
 		g.Printf("proxy_class_%s_%s = (*env)->NewGlobalRef(env, clazz);\n", g.pkgPrefix, iface.obj.Name())
 		g.Printf("proxy_class_%s_%s_cons = (*env)->GetMethodID(env, clazz, \"<init>\", \"(Lgo/Seq$Ref;)V\");\n", g.pkgPrefix, iface.obj.Name())
 		if isErrorType(iface.obj.Type()) {
@@ -1394,7 +1403,7 @@
 	if g.Pkg != nil {
 		for _, p := range g.Pkg.Imports() {
 			if g.validPkg(p) {
-				g.Printf("%s.%s.touch();\n", g.javaPkgName(p), className(p))
+				g.Printf("%s.%s.touch();\n", g.javaPkgName(p), JavaClassName(p))
 			}
 		}
 	}
diff --git a/bind/testpkg/javapkg/classes.go b/bind/testpkg/javapkg/classes.go
index 891ebbe..a6545d0 100644
--- a/bind/testpkg/javapkg/classes.go
+++ b/bind/testpkg/javapkg/classes.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+// +build android
+
 package javapkg
 
 import (
diff --git a/cmd/gobind/gen.go b/cmd/gobind/gen.go
index b7aa5cf..f30da5a 100644
--- a/cmd/gobind/gen.go
+++ b/cmd/gobind/gen.go
@@ -6,18 +6,24 @@
 
 import (
 	"bytes"
+	"fmt"
+	"go/build"
 	"go/token"
 	"go/types"
 	"io"
+	"io/ioutil"
 	"os"
+	"os/exec"
 	"path/filepath"
+	"strings"
 	"unicode"
 	"unicode/utf8"
 
 	"golang.org/x/mobile/bind"
+	"golang.org/x/mobile/internal/importers/java"
 )
 
-func genPkg(p *types.Package, allPkg []*types.Package) {
+func genPkg(p *types.Package, allPkg []*types.Package, classes []*java.Class) {
 	fname := defaultFileName(*lang, p)
 	conf := &bind.GeneratorConfig{
 		Fset:   fset,
@@ -36,39 +42,73 @@
 				Pkg:     conf.Pkg,
 			},
 		}
-		g.Init(nil)
+		g.Init(classes)
 
+		pkgname := bind.JavaPkgName(*javaPkg, p)
+		pkgDir := strings.Replace(pkgname, ".", "/", -1)
 		buf.Reset()
-		w, closer := writer(fname)
+		w, closer := writer(filepath.Join(pkgDir, fname))
 		processErr(g.GenJava())
 		io.Copy(w, &buf)
 		closer()
 		for i, name := range g.ClassNames() {
 			buf.Reset()
-			w, closer := writer(name + ".java")
+			w, closer := writer(filepath.Join(pkgDir, name+".java"))
 			processErr(g.GenClass(i))
 			io.Copy(w, &buf)
 			closer()
 		}
 		buf.Reset()
-		cname := "java_" + p.Name() + ".c"
+		pn := "universe"
+		if p != nil {
+			pn = p.Name()
+		}
+		cname := "java_" + pn + ".c"
 		w, closer = writer(cname)
 		processErr(g.GenC())
 		io.Copy(w, &buf)
 		closer()
 		buf.Reset()
-		hname := p.Name() + ".h"
+		hname := pn + ".h"
 		w, closer = writer(hname)
 		processErr(g.GenH())
 		io.Copy(w, &buf)
 		closer()
+		// Generate support files along with the universe package
+		if p == nil {
+			p, err := build.Default.Import("golang.org/x/mobile/bind", ".", build.ImportComment)
+			if err != nil {
+				errorf(`"golang.org/x/mobile/bind" is not found; run go get golang.org/x/mobile/bind: %v`)
+				return
+			}
+			repo := filepath.Clean(filepath.Join(p.Dir, "..")) // golang.org/x/mobile directory.
+			for _, javaFile := range []string{"Seq.java", "LoadJNI.java"} {
+				src := filepath.Join(repo, "bind/java/"+javaFile)
+				in, err := os.Open(src)
+				if err != nil {
+					errorf("failed to open Java support file: %v", err)
+				}
+				defer in.Close()
+				w, closer := writer(filepath.Join("go", javaFile))
+				defer closer()
+				if _, err := io.Copy(w, in); err != nil {
+					errorf("failed to copy Java support file: %v", err)
+					return
+				}
+			}
+		}
 	case "go":
 		w, closer := writer(fname)
 		conf.Writer = w
 		processErr(bind.GenGo(conf))
 		closer()
 	case "objc":
-		gohname := p.Name() + ".h"
+		var gohname string
+		if p != nil {
+			gohname = p.Name() + ".h"
+		} else {
+			gohname = "GoUniverse.h"
+		}
 		w, closer := writer(gohname)
 		conf.Writer = w
 		processErr(bind.GenObjc(conf, *prefix, bind.ObjcGoH))
@@ -87,6 +127,51 @@
 	}
 }
 
+func genJavaPackages(ctx *build.Context, dir string, classes []*java.Class) error {
+	var buf bytes.Buffer
+	cg := &bind.ClassGen{
+		Printer: &bind.Printer{
+			IndentEach: []byte("\t"),
+			Buf:        &buf,
+		},
+	}
+	cg.Init(classes)
+	pkgBase := filepath.Join(dir, "src", "Java")
+	if err := os.MkdirAll(pkgBase, 0700); err != nil {
+		return err
+	}
+	for i, jpkg := range cg.Packages() {
+		pkgDir := filepath.Join(pkgBase, jpkg)
+		if err := os.MkdirAll(pkgDir, 0700); err != nil {
+			return err
+		}
+		pkgFile := filepath.Join(pkgDir, "package.go")
+		buf.Reset()
+		cg.GenPackage(i)
+		if err := ioutil.WriteFile(pkgFile, buf.Bytes(), 0600); err != nil {
+			return err
+		}
+	}
+	buf.Reset()
+	cg.GenInterfaces()
+	clsFile := filepath.Join(pkgBase, "interfaces.go")
+	if err := ioutil.WriteFile(clsFile, buf.Bytes(), 0600); err != nil {
+		return err
+	}
+
+	cmd := exec.Command(
+		"go",
+		"install",
+		"-pkgdir="+filepath.Join(dir, "pkg", ctx.GOOS+"_"+ctx.GOARCH),
+		"Java/...",
+	)
+	cmd.Env = append(cmd.Env, "GOPATH="+dir)
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to go install the generated Java wrappers: %v: %s", err, string(out))
+	}
+	return nil
+}
+
 func processErr(err error) {
 	if err != nil {
 		if list, _ := err.(bind.ErrorList); len(list) > 0 {
@@ -129,12 +214,21 @@
 func defaultFileName(lang string, pkg *types.Package) string {
 	switch lang {
 	case "java":
+		if pkg == nil {
+			return "Universe.java"
+		}
 		firstRune, size := utf8.DecodeRuneInString(pkg.Name())
 		className := string(unicode.ToUpper(firstRune)) + pkg.Name()[size:]
 		return className + ".java"
 	case "go":
+		if pkg == nil {
+			return "go_universe.go"
+		}
 		return "go_" + pkg.Name() + ".go"
 	case "objc":
+		if pkg == nil {
+			return "GoUniverse.m"
+		}
 		firstRune, size := utf8.DecodeRuneInString(pkg.Name())
 		className := string(unicode.ToUpper(firstRune)) + pkg.Name()[size:]
 		return "Go" + className + ".m"
diff --git a/cmd/gobind/gobind_test.go b/cmd/gobind/gobind_test.go
new file mode 100644
index 0000000..873ffde
--- /dev/null
+++ b/cmd/gobind/gobind_test.go
@@ -0,0 +1,66 @@
+// Copyright 2016 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"fmt"
+	"os/exec"
+	"testing"
+)
+
+var tests = []struct {
+	name string
+	lang string
+	pkg  string
+}{
+	{"ObjC-Testpkg", "objc", "golang.org/x/mobile/bind/testpkg"},
+	{"Java-Testpkg", "java", "golang.org/x/mobile/bind/testpkg"},
+	{"Go-Testpkg", "go", "golang.org/x/mobile/bind/testpkg"},
+	{"Java-Javapkg", "java", "golang.org/x/mobile/bind/testpkg/javapkg"},
+	{"Go-Javapkg", "go", "golang.org/x/mobile/bind/testpkg/javapkg"},
+}
+
+func installGobind() error {
+	if out, err := exec.Command("go", "install", "golang.org/x/mobile/cmd/gobind").CombinedOutput(); err != nil {
+		return fmt.Errorf("gobind install failed: %v: %s", err, out)
+	}
+	return nil
+}
+
+func runGobind(lang, pkg string) error {
+	cmd := exec.Command("gobind", "-lang", lang, pkg)
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("gobind -lang %s %s failed: %v: %s", lang, pkg, err, out)
+	}
+	return nil
+}
+
+func TestGobind(t *testing.T) {
+	if err := installGobind(); err != nil {
+		t.Fatal(err)
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			if err := runGobind(test.lang, test.pkg); err != nil {
+				t.Error(err)
+			}
+		})
+	}
+}
+
+func BenchmarkGobind(b *testing.B) {
+	if err := installGobind(); err != nil {
+		b.Fatal(err)
+	}
+	for _, test := range tests {
+		b.Run(test.name, func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				if err := runGobind(test.lang, test.pkg); err != nil {
+					b.Error(err)
+				}
+			}
+		})
+	}
+}
diff --git a/cmd/gobind/main.go b/cmd/gobind/main.go
index c0d2f9b..ef6e808 100644
--- a/cmd/gobind/main.go
+++ b/cmd/gobind/main.go
@@ -7,19 +7,28 @@
 import (
 	"flag"
 	"fmt"
+	"go/ast"
+	"go/build"
 	"go/importer"
+	"go/parser"
+	"go/token"
 	"go/types"
+	"io/ioutil"
 	"log"
 	"os"
-	"os/exec"
-	"strings"
+	"path/filepath"
+
+	"golang.org/x/mobile/internal/importers"
+	"golang.org/x/mobile/internal/importers/java"
 )
 
 var (
-	lang    = flag.String("lang", "java", "target language for bindings, either java, go, or objc (experimental).")
-	outdir  = flag.String("outdir", "", "result will be written to the directory instead of stdout.")
-	javaPkg = flag.String("javapkg", "", "custom Java package path prefix used instead of the default 'go'. Valid only with -lang=java.")
-	prefix  = flag.String("prefix", "", "custom Objective-C name prefix used instead of the default 'Go'. Valid only with -lang=objc.")
+	lang          = flag.String("lang", "java", "target language for bindings, either java, go, or objc (experimental).")
+	outdir        = flag.String("outdir", "", "result will be written to the directory instead of stdout.")
+	javaPkg       = flag.String("javapkg", "", "custom Java package path prefix used instead of the default 'go'. Valid only with -lang=java.")
+	prefix        = flag.String("prefix", "", "custom Objective-C name prefix used instead of the default 'Go'. Valid only with -lang=objc.")
+	bootclasspath = flag.String("bootclasspath", "", "Java bootstrap classpath.")
+	classpath     = flag.String("classpath", "", "Java classpath.")
 )
 
 var usage = `The Gobind tool generates Java language bindings for Go.
@@ -35,31 +44,70 @@
 		log.Fatalf("Invalid option -prefix for gobind -lang=%s", *lang)
 	}
 
-	// Make sure the export data for the packages being compiled is up to
-	// date. Also use the go tool to provide good error messages for any
-	// type checking errors in the provided packages.
-	cmd := exec.Command("go", "install")
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	cmd.Args = append(cmd.Args, flag.Args()...)
-	if err := cmd.Run(); err != nil {
-		fmt.Fprintf(os.Stderr, "%s failed: %v", strings.Join(cmd.Args, " "), err)
-		os.Exit(1)
-	}
-
-	var allPkg []*types.Package
-	imp := importer.Default()
-	for _, arg := range flag.Args() {
-		pkg, err := imp.Import(arg)
+	oldCtx := build.Default
+	ctx := &build.Default
+	var allPkg []*build.Package
+	for _, path := range flag.Args() {
+		pkg, err := ctx.Import(path, ".", build.ImportComment)
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "could not import package %s: %v", arg, err)
-			os.Exit(1)
+			log.Fatalf("package %q: %v", path, err)
 		}
 		allPkg = append(allPkg, pkg)
 	}
-	for _, pkg := range allPkg {
-		genPkg(pkg, allPkg)
+	var classes []*java.Class
+	refs, err := importers.AnalyzePackages(allPkg, "Java/")
+	if err != nil {
+		log.Fatal(err)
 	}
+	if len(refs.Refs) > 0 {
+		classes, err = java.Import(*bootclasspath, *classpath, refs)
+		if err != nil {
+			log.Fatal(err)
+		}
+		if len(classes) > 0 {
+			tmpGopath, err := ioutil.TempDir(os.TempDir(), "gobind-")
+			if err != nil {
+				log.Fatal(err)
+			}
+			defer os.RemoveAll(tmpGopath)
+			if err := genJavaPackages(ctx, tmpGopath, classes); err != nil {
+				log.Fatal(err)
+			}
+			gopath := ctx.GOPATH
+			if gopath != "" {
+				gopath = string(filepath.ListSeparator)
+			}
+			ctx.GOPATH = gopath + tmpGopath
+		}
+	}
+
+	typePkgs := make([]*types.Package, len(allPkg))
+	fset := token.NewFileSet()
+	conf := &types.Config{
+		Importer: importer.Default(),
+	}
+	conf.Error = func(err error) {
+		// Ignore errors. They're probably caused by as-yet undefined
+		// Java wrappers.
+	}
+	for i, pkg := range allPkg {
+		var files []*ast.File
+		for _, name := range pkg.GoFiles {
+			f, err := parser.ParseFile(fset, filepath.Join(pkg.Dir, name), nil, 0)
+			if err != nil {
+				log.Fatalf("Failed to parse Go file %s: %v", name, err)
+			}
+			files = append(files, f)
+		}
+		tpkg, _ := conf.Check(pkg.Name, fset, files, nil)
+		typePkgs[i] = tpkg
+	}
+	build.Default = oldCtx
+	for _, pkg := range typePkgs {
+		genPkg(pkg, typePkgs, classes)
+	}
+	// Generate the error package and support files
+	genPkg(nil, typePkgs, classes)
 	os.Exit(exitStatus)
 }
 
diff --git a/cmd/gomobile/bind.go b/cmd/gomobile/bind.go
index 1659d8b..963f0c2 100644
--- a/cmd/gomobile/bind.go
+++ b/cmd/gomobile/bind.go
@@ -364,7 +364,9 @@
 	return classes, nil
 }
 
-func (b *binder) GenJava(pkg *types.Package, allPkg []*types.Package, classes []*java.Class, outdir, javadir string) error {
+func (b *binder) GenJava(pkg *types.Package, allPkg []*types.Package, classes []*java.Class, outdir, androidDir string) error {
+	jpkgname := bind.JavaPkgName(bindJavaPkg, pkg)
+	javadir := filepath.Join(androidDir, strings.Replace(jpkgname, ".", "/", -1))
 	var className string
 	pkgName := ""
 	pkgPath := ""
diff --git a/cmd/gomobile/bind_androidapp.go b/cmd/gomobile/bind_androidapp.go
index cad67c5..cb86f00 100644
--- a/cmd/gomobile/bind_androidapp.go
+++ b/cmd/gomobile/bind_androidapp.go
@@ -108,17 +108,13 @@
 		}
 		repo := filepath.Clean(filepath.Join(p.Dir, "..")) // golang.org/x/mobile directory.
 
+		jclsDir := filepath.Join(androidDir, "src", "main", "java")
 		for _, pkg := range binder.pkgs {
-			pkgpath := bindJavaPkg
-			if pkgpath == "" {
-				pkgpath = "go." + pkg.Name()
-			}
-			jclsDir := filepath.Join(androidDir, "src/main/java/"+strings.Replace(pkgpath, ".", "/", -1))
 			if err := binder.GenJava(pkg, binder.pkgs, classes, srcDir, jclsDir); err != nil {
 				return err
 			}
 		}
-		if err := binder.GenJava(nil, binder.pkgs, classes, srcDir, filepath.Join(androidDir, "src/main/java/go")); err != nil {
+		if err := binder.GenJava(nil, binder.pkgs, classes, srcDir, jclsDir); err != nil {
 			return err
 		}
 		if err := binder.GenJavaSupport(srcDir); err != nil {
diff --git a/cmd/gomobile/bind_test.go b/cmd/gomobile/bind_test.go
index 0e4e708..4f19be6 100644
--- a/cmd/gomobile/bind_test.go
+++ b/cmd/gomobile/bind_test.go
@@ -50,7 +50,7 @@
 		{
 			javaPkg:    "com.example.foo",
 			wantGobind: "gobind -lang=java -javapkg=com.example.foo",
-			wantPkgDir: "com/example/foo",
+			wantPkgDir: "com/example/foo/asset",
 		},
 	}
 	for _, tc := range tests {