github.com/replit/upm@v0.0.0-20240423230255-9ce4fc3ea24c/internal/backends/java/java.go (about)

     1  // Package java provides a backend for Java using maven.
     2  package java
     3  
     4  import (
     5  	"context"
     6  	"encoding/xml"
     7  	"fmt"
     8  	"os"
     9  	"os/exec"
    10  	"regexp"
    11  
    12  	"github.com/replit/upm/internal/api"
    13  	"github.com/replit/upm/internal/nix"
    14  	"github.com/replit/upm/internal/util"
    15  	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
    16  )
    17  
    18  type Dependency struct {
    19  	XMLName     xml.Name `xml:"dependency"`
    20  	GroupId     string   `xml:"groupId"`
    21  	ArtifactId  string   `xml:"artifactId"`
    22  	Version     string   `xml:"version"`
    23  	PackageType string   `xml:"type"`
    24  }
    25  
    26  type DynamicDependency struct {
    27  	XMLName        xml.Name `xml:"DynamicDependency"`
    28  	GroupId        string   `xml:"groupId"`
    29  	ArtifactId     string   `xml:"artifactId"`
    30  	Version        string   `xml:"version"`
    31  	Classifier     string   `xml:"classifier"`
    32  	RepositoryType string   `xml:"repositoryType"`
    33  }
    34  
    35  type PluginConfiguration struct {
    36  	XMLName             xml.Name            `xml:"configuration"`
    37  	DynamicDependencies []DynamicDependency `xml:"dynamicDependencies>DynamicDependency"`
    38  }
    39  
    40  type Plugin struct {
    41  	XMLName       xml.Name            `xml:"plugin"`
    42  	GroupId       string              `xml:"groupId"`
    43  	ArtifactId    string              `xml:"artifactId"`
    44  	Version       string              `xml:"version"`
    45  	Configuration PluginConfiguration `xml:"configuration"`
    46  }
    47  
    48  type Project struct {
    49  	XMLName      xml.Name     `xml:"project"`
    50  	ModelVersion string       `xml:"modelVersion"`
    51  	GroupId      string       `xml:"groupId"`
    52  	ArtifactId   string       `xml:"artifactId"`
    53  	Version      string       `xml:"version"`
    54  	Dependencies []Dependency `xml:"dependencies>dependency"`
    55  	Plugins      []Plugin     `xml:"build>plugins>plugin"`
    56  }
    57  
    58  const initialPomXml = `
    59  <project>
    60    <modelVersion>4.0.0</modelVersion>
    61    <groupId>mygroupid</groupId>
    62    <artifactId>myartifactid</artifactId>
    63    <version>0.0-SNAPSHOT</version>
    64    <build>
    65      <plugins>
    66        <plugin>
    67            <groupId>de.qaware.maven</groupId>
    68            <artifactId>go-offline-maven-plugin</artifactId>
    69            <version>1.2.5</version>
    70            <configuration>
    71                <dynamicDependencies>
    72                    <DynamicDependency>
    73                        <groupId>org.apache.maven.surefire</groupId>
    74                        <artifactId>surefire-junit4</artifactId>
    75                        <version>2.20.1</version>
    76                        <repositoryType>PLUGIN</repositoryType>
    77                    </DynamicDependency>
    78                    <DynamicDependency>
    79                        <groupId>com.querydsl</groupId>
    80                        <artifactId>querydsl-apt</artifactId>
    81                        <version>4.2.1</version>
    82                        <classifier>jpa</classifier>
    83                        <repositoryType>MAIN</repositoryType>
    84                    </DynamicDependency>
    85                </dynamicDependencies>
    86            </configuration>
    87        </plugin>
    88      </plugins>
    89    </build>
    90  </project>
    91  `
    92  
    93  var pkgNameRegexp = regexp.MustCompile("^([^:]+):([^:]+)") // groupid:artifactid
    94  
    95  // javaPatterns is the FilenamePatterns value for JavaBackend.
    96  var javaPatterns = []string{"*.java"}
    97  
    98  func readProjectOrMakeEmpty(path string) Project {
    99  	var project Project
   100  	var xmlbytes []byte
   101  	if util.Exists("pom.xml") {
   102  		var err error
   103  		xmlbytes, err = os.ReadFile("pom.xml")
   104  		if err != nil {
   105  			util.DieIO("error reading pom.xml: %s", err)
   106  		}
   107  	} else {
   108  		xmlbytes = []byte(initialPomXml)
   109  	}
   110  	err := xml.Unmarshal(xmlbytes, &project)
   111  	if err != nil {
   112  		util.DieProtocol("error unmarshalling pom.xml: %s", err)
   113  	}
   114  	return project
   115  }
   116  
   117  const pomdotxml = "pom.xml"
   118  
   119  func isAvailable() bool {
   120  	_, err := exec.LookPath("mvn")
   121  	return err == nil
   122  }
   123  
   124  func addPackages(ctx context.Context, pkgs map[api.PkgName]api.PkgSpec, projectName string) {
   125  	//nolint:ineffassign,wastedassign,staticcheck
   126  	span, ctx := tracer.StartSpanFromContext(ctx, "Java add package")
   127  	defer span.Finish()
   128  	project := readProjectOrMakeEmpty(pomdotxml)
   129  	existingDependencies := map[api.PkgName]api.PkgVersion{}
   130  	for _, dependency := range project.Dependencies {
   131  		pkgName := api.PkgName(
   132  			fmt.Sprintf("%s:%s", dependency.GroupId, dependency.ArtifactId),
   133  		)
   134  		pkgVersion := api.PkgVersion(dependency.Version)
   135  		existingDependencies[pkgName] = pkgVersion
   136  	}
   137  
   138  	newDependencies := []Dependency{}
   139  	for pkgName, pkgSpec := range pkgs {
   140  		submatches := pkgNameRegexp.FindStringSubmatch(string(pkgName))
   141  		if nil == submatches {
   142  			util.DieConsistency(
   143  				"package name %s does not match groupid:artifactid pattern",
   144  				pkgName,
   145  			)
   146  		}
   147  
   148  		groupId := submatches[1]
   149  		artifactId := submatches[2]
   150  		if _, ok := existingDependencies[pkgName]; ok {
   151  			// this package is already in the lock file
   152  			continue
   153  		}
   154  
   155  		var query string
   156  		if pkgSpec == "" {
   157  			query = fmt.Sprintf("g:%s AND a:%s", groupId, artifactId)
   158  		} else {
   159  			query = fmt.Sprintf("g:%s AND a:%s AND v:%s", groupId, artifactId, pkgSpec)
   160  		}
   161  		searchDocs, err := Search(query)
   162  		if err != nil {
   163  			util.DieNetwork(
   164  				"error searching maven for latest version of %s:%s: %s",
   165  				groupId,
   166  				artifactId,
   167  				err,
   168  			)
   169  		}
   170  		if len(searchDocs) == 0 {
   171  			if pkgSpec == "" {
   172  				util.DieConsistency("did not find a package %s:%s", groupId, artifactId)
   173  			} else {
   174  				util.DieConsistency("did not find a package %s:%s:%s", groupId, artifactId, pkgSpec)
   175  			}
   176  		}
   177  		searchDoc := searchDocs[0]
   178  
   179  		var versionString string
   180  		if pkgSpec == "" {
   181  			versionString = searchDoc.Version
   182  		} else {
   183  			versionString = string(pkgSpec)
   184  		}
   185  
   186  		var packageType string
   187  		if searchDoc.PackageType == "pom" {
   188  			packageType = "pom"
   189  		} else {
   190  			packageType = "jar"
   191  		}
   192  
   193  		dependency := Dependency{
   194  			GroupId:     submatches[1],
   195  			ArtifactId:  submatches[2],
   196  			Version:     versionString,
   197  			PackageType: packageType,
   198  		}
   199  		newDependencies = append(newDependencies, dependency)
   200  
   201  	}
   202  
   203  	project.Dependencies = append(project.Dependencies, newDependencies...)
   204  	marshalled, err := xml.MarshalIndent(project, "", "  ")
   205  	if err != nil {
   206  		util.DieProtocol("could not marshal pom: %s", err)
   207  	}
   208  
   209  	contentsB := []byte(marshalled)
   210  	util.ProgressMsg("write pom.xml")
   211  	util.TryWriteAtomic("pom.xml", contentsB)
   212  }
   213  
   214  func removePackages(ctx context.Context, pkgs map[api.PkgName]bool) {
   215  	//nolint:ineffassign,wastedassign,staticcheck
   216  	span, ctx := tracer.StartSpanFromContext(ctx, "Java remove package")
   217  	defer span.Finish()
   218  	project := readProjectOrMakeEmpty(pomdotxml)
   219  
   220  	dependenciesToKeep := []Dependency{}
   221  	for _, dependency := range project.Dependencies {
   222  		pkgName := api.PkgName(
   223  			fmt.Sprintf("%s:%s", dependency.GroupId, dependency.ArtifactId),
   224  		)
   225  		if _, ok := pkgs[pkgName]; ok {
   226  			// removing this dependency
   227  		} else {
   228  			dependenciesToKeep = append(dependenciesToKeep, dependency)
   229  		}
   230  	}
   231  
   232  	projectWithFilteredDependencies := project
   233  	projectWithFilteredDependencies.Dependencies = dependenciesToKeep
   234  
   235  	marshalled, err := xml.MarshalIndent(projectWithFilteredDependencies, "", "  ")
   236  	if err != nil {
   237  		util.DieProtocol("error marshalling pom.xml: %s", err)
   238  	}
   239  	contentsB := []byte(marshalled)
   240  	util.ProgressMsg("write pom.xml")
   241  	util.TryWriteAtomic("pom.xml", contentsB)
   242  
   243  	os.RemoveAll("target/dependency")
   244  }
   245  
   246  func listSpecfile(mergeAllGroups bool) map[api.PkgName]api.PkgSpec {
   247  	project := readProjectOrMakeEmpty(pomdotxml)
   248  	pkgs := map[api.PkgName]api.PkgSpec{}
   249  	for _, dependency := range project.Dependencies {
   250  		pkgName := api.PkgName(
   251  			fmt.Sprintf("%s:%s", dependency.GroupId, dependency.ArtifactId),
   252  		)
   253  		pkgSpec := api.PkgSpec(dependency.Version)
   254  		pkgs[pkgName] = pkgSpec
   255  	}
   256  	return pkgs
   257  }
   258  
   259  func listLockfile() map[api.PkgName]api.PkgVersion {
   260  	project := readProjectOrMakeEmpty(pomdotxml)
   261  	pkgs := map[api.PkgName]api.PkgVersion{}
   262  	for _, dependency := range project.Dependencies {
   263  		pkgName := api.PkgName(
   264  			fmt.Sprintf("%s:%s", dependency.GroupId, dependency.ArtifactId),
   265  		)
   266  		pkgVersion := api.PkgVersion(dependency.Version)
   267  		pkgs[pkgName] = pkgVersion
   268  	}
   269  	return pkgs
   270  }
   271  
   272  func search(query string) []api.PkgInfo {
   273  	searchDocs, err := Search(query)
   274  	if err != nil {
   275  		util.DieNetwork("error searching maven %s", err)
   276  	}
   277  	pkgInfos := []api.PkgInfo{}
   278  	for _, searchDoc := range searchDocs {
   279  		pkgInfo := api.PkgInfo{
   280  			Name:    fmt.Sprintf("%s:%s", searchDoc.Group, searchDoc.Artifact),
   281  			Version: searchDoc.Version,
   282  		}
   283  		pkgInfos = append(pkgInfos, pkgInfo)
   284  	}
   285  	return pkgInfos
   286  }
   287  
   288  func info(pkgName api.PkgName) api.PkgInfo {
   289  	searchDoc, err := Info(string(pkgName))
   290  
   291  	if err != nil {
   292  		util.DieNetwork("error searching maven %s", err)
   293  	}
   294  
   295  	if searchDoc.Artifact == "" {
   296  		return api.PkgInfo{}
   297  	}
   298  
   299  	pkgInfo := api.PkgInfo{
   300  		Name:    fmt.Sprintf("%s:%s", searchDoc.Group, searchDoc.Artifact),
   301  		Version: searchDoc.CurrentVersion,
   302  	}
   303  	return pkgInfo
   304  }
   305  
   306  // JavaBackend is the UPM language backend for Java using Maven.
   307  var JavaBackend = api.LanguageBackend{
   308  	Name:             "java-maven",
   309  	Specfile:         pomdotxml,
   310  	Lockfile:         pomdotxml,
   311  	IsAvailable:      isAvailable,
   312  	FilenamePatterns: javaPatterns,
   313  	Quirks:           api.QuirksAddRemoveAlsoLocks,
   314  	GetPackageDir: func() string {
   315  		return "target/dependency"
   316  	},
   317  	Search: search,
   318  	Info:   info,
   319  	Add:    addPackages,
   320  	Remove: removePackages,
   321  	Install: func(ctx context.Context) {
   322  		//nolint:ineffassign,wastedassign,staticcheck
   323  		span, ctx := tracer.StartSpanFromContext(ctx, "Maven install")
   324  		defer span.Finish()
   325  		util.RunCmd([]string{
   326  			"mvn",
   327  			"de.qaware.maven:go-offline-maven-plugin:resolve-dependencies",
   328  			"dependency:copy-dependencies",
   329  		})
   330  	},
   331  	ListSpecfile:                       listSpecfile,
   332  	ListLockfile:                       listLockfile,
   333  	Lock:                               func(ctx context.Context) {},
   334  	InstallReplitNixSystemDependencies: nix.DefaultInstallReplitNixSystemDependencies,
   335  }