github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/manifest/maven/pomxml.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package maven provides the manifest parsing and writing for the Maven pom.xml format. 16 package maven 17 18 import ( 19 "bytes" 20 "cmp" 21 "context" 22 "errors" 23 "fmt" 24 "io" 25 "iter" 26 "maps" 27 "os" 28 "path/filepath" 29 "slices" 30 "strings" 31 32 "deps.dev/util/maven" 33 "deps.dev/util/resolve" 34 "deps.dev/util/resolve/dep" 35 "github.com/google/osv-scalibr/clients/datasource" 36 "github.com/google/osv-scalibr/extractor/filesystem" 37 scalibrfs "github.com/google/osv-scalibr/fs" 38 "github.com/google/osv-scalibr/guidedremediation/internal/manifest" 39 "github.com/google/osv-scalibr/guidedremediation/result" 40 "github.com/google/osv-scalibr/guidedremediation/strategy" 41 "github.com/google/osv-scalibr/internal/mavenutil" 42 forkedxml "github.com/michaelkedar/xml" 43 ) 44 45 // RequirementKey is a comparable type that uniquely identifies a package dependency in a manifest. 46 type RequirementKey struct { 47 resolve.PackageKey 48 49 ArtifactType string 50 Classifier string 51 } 52 53 var _ map[RequirementKey]any 54 55 // MakeRequirementKey constructs a maven RequirementKey from the given RequirementVersion. 56 func MakeRequirementKey(requirement resolve.RequirementVersion) RequirementKey { 57 // Maven dependencies must have unique groupId:artifactId:type:classifier. 58 artifactType, _ := requirement.Type.GetAttr(dep.MavenArtifactType) 59 classifier, _ := requirement.Type.GetAttr(dep.MavenClassifier) 60 61 return RequirementKey{ 62 PackageKey: requirement.PackageKey, 63 ArtifactType: artifactType, 64 Classifier: classifier, 65 } 66 } 67 68 // ManifestSpecific is ecosystem-specific information needed for the pom.xml manifest. 69 type ManifestSpecific struct { 70 Parent maven.Parent 71 ParentPaths []string // Paths to the parent pom.xml files 72 Properties []PropertyWithOrigin // Properties from the base project and any local parent projects 73 OriginalRequirements []DependencyWithOrigin // Dependencies from the base project 74 LocalRequirements []DependencyWithOrigin // Dependencies from the base project and any local parent projects 75 RequirementsForUpdates []resolve.RequirementVersion // Requirements that we only need for updates 76 Repositories []maven.Repository 77 } 78 79 // PropertyWithOrigin is a maven property with the origin where it comes from. 80 type PropertyWithOrigin struct { 81 maven.Property 82 83 Origin string // Origin indicates where the property comes from 84 } 85 86 // DependencyWithOrigin is a maven dependency with the origin where it comes from. 87 type DependencyWithOrigin struct { 88 maven.Dependency 89 90 Origin string // Origin indicates where the dependency comes from 91 } 92 93 type mavenManifest struct { 94 filePath string 95 root resolve.Version 96 requirements []resolve.RequirementVersion 97 groups map[manifest.RequirementKey][]string 98 specific ManifestSpecific 99 } 100 101 // FilePath returns the path to the manifest file. 102 func (m *mavenManifest) FilePath() string { 103 return m.filePath 104 } 105 106 // Root returns the Version representing this package. 107 func (m *mavenManifest) Root() resolve.Version { 108 return m.root 109 } 110 111 // System returns the ecosystem of this manifest. 112 func (m *mavenManifest) System() resolve.System { 113 return resolve.Maven 114 } 115 116 // Requirements returns all direct requirements (including dev). 117 func (m *mavenManifest) Requirements() []resolve.RequirementVersion { 118 return m.requirements 119 } 120 121 // Groups returns the dependency groups that the direct requirements belong to. 122 func (m *mavenManifest) Groups() map[manifest.RequirementKey][]string { 123 return m.groups 124 } 125 126 // LocalManifests returns Manifests of any local packages. 127 func (m *mavenManifest) LocalManifests() []manifest.Manifest { 128 return nil 129 } 130 131 // EcosystemSpecific returns any ecosystem-specific information for this manifest. 132 func (m *mavenManifest) EcosystemSpecific() any { 133 return m.specific 134 } 135 136 // Clone returns a copy of this manifest that is safe to modify. 137 func (m *mavenManifest) Clone() manifest.Manifest { 138 clone := &mavenManifest{ 139 filePath: m.filePath, 140 root: m.root, 141 requirements: slices.Clone(m.requirements), 142 groups: maps.Clone(m.groups), 143 specific: ManifestSpecific{ 144 Parent: m.specific.Parent, 145 ParentPaths: slices.Clone(m.specific.ParentPaths), 146 Properties: slices.Clone(m.specific.Properties), 147 OriginalRequirements: slices.Clone(m.specific.OriginalRequirements), 148 LocalRequirements: slices.Clone(m.specific.LocalRequirements), 149 RequirementsForUpdates: slices.Clone(m.specific.RequirementsForUpdates), 150 Repositories: slices.Clone(m.specific.Repositories), 151 }, 152 } 153 clone.root.AttrSet = m.root.Clone() 154 155 return clone 156 } 157 158 // PatchRequirement modifies the manifest's requirements to include the new requirement version. 159 // If the package already is in the requirements, updates the version. 160 // Otherwise, adds req to the dependencyManagement of the root pom.xml. 161 func (m *mavenManifest) PatchRequirement(req resolve.RequirementVersion) error { 162 found := false 163 i := 0 164 for _, r := range m.requirements { 165 if r.PackageKey != req.PackageKey { 166 m.requirements[i] = r 167 i++ 168 169 continue 170 } 171 origin, hasOrigin := r.Type.GetAttr(dep.MavenDependencyOrigin) 172 if !hasOrigin || origin == mavenutil.OriginManagement { 173 found = true 174 r.Version = req.Version 175 m.requirements[i] = r 176 i++ 177 } 178 } 179 m.requirements = m.requirements[:i] 180 if !found { 181 req.Type.AddAttr(dep.MavenDependencyOrigin, mavenutil.OriginManagement) 182 m.requirements = append(m.requirements, req) 183 } 184 185 return nil 186 } 187 188 type readWriter struct { 189 *datasource.MavenRegistryAPIClient 190 } 191 192 // GetReadWriter returns a ReadWriter for pom.xml manifest files. 193 func GetReadWriter(client *datasource.MavenRegistryAPIClient) (manifest.ReadWriter, error) { 194 return readWriter{MavenRegistryAPIClient: client}, nil 195 } 196 197 // System returns the ecosystem of this ReadWriter. 198 func (r readWriter) System() resolve.System { 199 return resolve.Maven 200 } 201 202 // SupportedStrategies returns the remediation strategies supported for this manifest. 203 func (r readWriter) SupportedStrategies() []strategy.Strategy { 204 return []strategy.Strategy{strategy.StrategyOverride} 205 } 206 207 // Read parses the manifest from the given file. 208 func (r readWriter) Read(path string, fsys scalibrfs.FS) (manifest.Manifest, error) { 209 // TODO(#472): much of this logic is duplicated with the pomxmlnet extractor. 210 ctx := context.Background() 211 path = filepath.ToSlash(path) 212 f, err := fsys.Open(path) 213 if err != nil { 214 return nil, err 215 } 216 defer f.Close() 217 218 var project maven.Project 219 if err := datasource.NewMavenDecoder(f).Decode(&project); err != nil { 220 return nil, fmt.Errorf("failed to unmarshal project: %w", err) 221 } 222 properties := buildPropertiesWithOrigins(project, "") 223 origRequirements := buildOriginalRequirements(project, "") 224 225 var reqsForUpdates []resolve.RequirementVersion 226 if project.Parent.GroupID != "" && project.Parent.ArtifactID != "" { 227 reqsForUpdates = append(reqsForUpdates, resolve.RequirementVersion{ 228 VersionKey: resolve.VersionKey{ 229 PackageKey: resolve.PackageKey{ 230 System: resolve.Maven, 231 Name: project.Parent.Name(), 232 }, 233 // Parent version is a concrete version, but we model parent as dependency here. 234 VersionType: resolve.Requirement, 235 Version: string(project.Parent.Version), 236 }, 237 Type: resolve.MavenDepType(maven.Dependency{Type: "pom"}, mavenutil.OriginParent), 238 }) 239 } 240 241 // Empty JDK and ActivationOS indicates merging the default profiles. 242 if err := project.MergeProfiles("", maven.ActivationOS{}); err != nil { 243 return nil, fmt.Errorf("failed to merge profiles: %w", err) 244 } 245 246 // Interpolate the project in case there are properties in any repository. 247 if err := project.InterpolateRepositories(); err != nil { 248 return nil, fmt.Errorf("failed to interpolate project: %w", err) 249 } 250 for _, repo := range project.Repositories { 251 if repo.URL.ContainsProperty() { 252 continue 253 } 254 if err := r.AddRegistry(ctx, datasource.MavenRegistry{ 255 URL: string(repo.URL), 256 ID: string(repo.ID), 257 ReleasesEnabled: repo.Releases.Enabled.Boolean(), 258 SnapshotsEnabled: repo.Snapshots.Enabled.Boolean(), 259 }); err != nil { 260 return nil, fmt.Errorf("failed to add registry %s: %w", repo.URL, err) 261 } 262 } 263 264 // Merging parents data by parsing local parent pom.xml or fetching from upstream. 265 if err := mavenutil.MergeParents(ctx, project.Parent, &project, mavenutil.Options{ 266 Input: &filesystem.ScanInput{FS: fsys, Path: path}, 267 Client: r.MavenRegistryAPIClient, 268 AddRegistry: true, 269 AllowLocal: true, 270 InitialParentIndex: 1, 271 }); err != nil { 272 return nil, fmt.Errorf("failed to merge parents: %w", err) 273 } 274 275 // For dependency management imports, the dependencies that imports 276 // dependencies from other projects will be replaced by the imported 277 // dependencies, so add them to requirements first. 278 for _, dep := range project.DependencyManagement.Dependencies { 279 if dep.Scope == "import" && dep.Type == "pom" { 280 reqsForUpdates = append(reqsForUpdates, makeRequirementVersion(dep, mavenutil.OriginManagement)) 281 } 282 } 283 284 // Process the dependencies: 285 // - dedupe dependencies and dependency management 286 // - import dependency management 287 // - fill in missing dependency version requirement 288 project.ProcessDependencies(func(groupID, artifactID, version maven.String) (maven.DependencyManagement, error) { 289 return mavenutil.GetDependencyManagement(ctx, r.MavenRegistryAPIClient, groupID, artifactID, version) 290 }) 291 292 groups := make(map[manifest.RequirementKey][]string) 293 requirements := addRequirements([]resolve.RequirementVersion{}, groups, project.Dependencies, "") 294 requirements = addRequirements(requirements, groups, project.DependencyManagement.Dependencies, mavenutil.OriginManagement) 295 296 // Requirements may not appear in the dependency graph but needs to be updated. 297 for _, profile := range project.Profiles { 298 reqsForUpdates = addRequirements(reqsForUpdates, groups, profile.Dependencies, "") 299 reqsForUpdates = addRequirements(reqsForUpdates, groups, profile.DependencyManagement.Dependencies, mavenutil.OriginManagement) 300 } 301 for _, plugin := range project.Build.PluginManagement.Plugins { 302 reqsForUpdates = addRequirements(reqsForUpdates, groups, plugin.Dependencies, "") 303 } 304 305 // Get the local dependencies and properties from all parent projects. 306 localDeps, localProps, paths, err := getLocalDepsAndProps(fsys, path, project.Parent) 307 if err != nil { 308 return nil, err 309 } 310 311 return &mavenManifest{ 312 filePath: path, 313 root: resolve.Version{ 314 VersionKey: resolve.VersionKey{ 315 PackageKey: resolve.PackageKey{ 316 System: resolve.Maven, 317 Name: project.ProjectKey.Name(), 318 }, 319 VersionType: resolve.Concrete, 320 Version: string(project.Version), 321 }, 322 }, 323 requirements: requirements, 324 groups: groups, 325 specific: ManifestSpecific{ 326 Parent: project.Parent, 327 ParentPaths: paths, 328 Properties: append(properties, localProps...), 329 OriginalRequirements: origRequirements, 330 LocalRequirements: append(origRequirements, localDeps...), 331 RequirementsForUpdates: reqsForUpdates, 332 Repositories: project.Repositories, 333 }, 334 }, nil 335 } 336 337 func addRequirements(reqs []resolve.RequirementVersion, groups map[manifest.RequirementKey][]string, deps []maven.Dependency, origin string) []resolve.RequirementVersion { 338 for _, d := range deps { 339 reqVer := makeRequirementVersion(d, origin) 340 reqs = append(reqs, reqVer) 341 if d.Scope != "" { 342 reqKey := MakeRequirementKey(reqVer) 343 groups[reqKey] = append(groups[reqKey], string(d.Scope)) 344 } 345 } 346 347 return reqs 348 } 349 350 func buildPropertiesWithOrigins(project maven.Project, originPrefix string) []PropertyWithOrigin { 351 count := len(project.Properties.Properties) 352 for _, prof := range project.Profiles { 353 count += len(prof.Properties.Properties) 354 } 355 properties := make([]PropertyWithOrigin, 0, count) 356 for _, prop := range project.Properties.Properties { 357 properties = append(properties, PropertyWithOrigin{Property: prop}) 358 } 359 for _, profile := range project.Profiles { 360 for _, prop := range profile.Properties.Properties { 361 properties = append(properties, PropertyWithOrigin{ 362 Property: prop, 363 Origin: mavenOrigin(originPrefix, mavenutil.OriginProfile, string(profile.ID)), 364 }) 365 } 366 } 367 368 return properties 369 } 370 371 func buildOriginalRequirements(project maven.Project, originPrefix string) []DependencyWithOrigin { 372 var dependencies []DependencyWithOrigin //nolint:prealloc 373 if project.Parent.GroupID != "" && project.Parent.ArtifactID != "" { 374 dependencies = append(dependencies, DependencyWithOrigin{ 375 Dependency: maven.Dependency{ 376 GroupID: project.Parent.GroupID, 377 ArtifactID: project.Parent.ArtifactID, 378 Version: project.Parent.Version, 379 Type: "pom", 380 }, 381 Origin: mavenOrigin(originPrefix, mavenutil.OriginParent), 382 }) 383 } 384 for _, d := range project.Dependencies { 385 dependencies = append(dependencies, DependencyWithOrigin{Dependency: d, Origin: originPrefix}) 386 } 387 for _, d := range project.DependencyManagement.Dependencies { 388 dependencies = append(dependencies, DependencyWithOrigin{ 389 Dependency: d, 390 Origin: mavenOrigin(originPrefix, mavenutil.OriginManagement), 391 }) 392 } 393 for _, prof := range project.Profiles { 394 for _, d := range prof.Dependencies { 395 dependencies = append(dependencies, DependencyWithOrigin{ 396 Dependency: d, 397 Origin: mavenOrigin(originPrefix, mavenutil.OriginProfile, string(prof.ID)), 398 }) 399 } 400 for _, d := range prof.DependencyManagement.Dependencies { 401 dependencies = append(dependencies, DependencyWithOrigin{ 402 Dependency: d, 403 Origin: mavenOrigin(originPrefix, mavenutil.OriginProfile, string(prof.ID), mavenutil.OriginManagement), 404 }) 405 } 406 } 407 for _, plugin := range project.Build.PluginManagement.Plugins { 408 for _, d := range plugin.Dependencies { 409 dependencies = append(dependencies, DependencyWithOrigin{ 410 Dependency: d, 411 Origin: mavenOrigin(originPrefix, mavenutil.OriginPlugin, plugin.Name()), 412 }) 413 } 414 } 415 416 return dependencies 417 } 418 419 // For dependencies in profiles and plugins, we use origin to indicate where they are from. 420 // The origin is in the format prefix@identifier[@postfix] (where @ is the separator): 421 // - prefix indicates it is from profile or plugin 422 // - identifier to locate the profile/plugin which is profile ID or plugin name 423 // - (optional) suffix indicates if this is a dependency management 424 func makeRequirementVersion(dep maven.Dependency, origin string) resolve.RequirementVersion { 425 // Treat test & optional dependencies as regular dependencies to force the resolver to resolve them. 426 if dep.Scope == "test" { 427 dep.Scope = "" 428 } 429 dep.Optional = "" 430 431 return resolve.RequirementVersion{ 432 VersionKey: resolve.VersionKey{ 433 PackageKey: resolve.PackageKey{ 434 System: resolve.Maven, 435 Name: dep.Name(), 436 }, 437 VersionType: resolve.Requirement, 438 Version: string(dep.Version), 439 }, 440 Type: resolve.MavenDepType(dep, origin), 441 } 442 } 443 444 func mavenOrigin(list ...string) string { 445 result := "" 446 for _, str := range list { 447 if result != "" && str != "" { 448 result += "@" 449 } 450 if str != "" { 451 result += str 452 } 453 } 454 455 return result 456 } 457 458 // TODO: refactor MergeParents to return local requirements and properties 459 func getLocalDepsAndProps(fsys scalibrfs.FS, path string, parent maven.Parent) ([]DependencyWithOrigin, []PropertyWithOrigin, []string, error) { 460 var localDeps []DependencyWithOrigin 461 var localProps []PropertyWithOrigin 462 463 // Walk through local parent pom.xml for original dependencies and properties. 464 currentPath := path 465 visited := make(map[maven.ProjectKey]bool, mavenutil.MaxParent) 466 paths := []string{currentPath} 467 for range mavenutil.MaxParent { 468 if parent.GroupID == "" || parent.ArtifactID == "" || parent.Version == "" { 469 break 470 } 471 if visited[parent.ProjectKey] { 472 // A cycle of parents is detected 473 return nil, nil, nil, errors.New("a cycle of parents is detected") 474 } 475 visited[parent.ProjectKey] = true 476 477 currentPath = mavenutil.ParentPOMPath(&filesystem.ScanInput{FS: fsys}, currentPath, string(parent.RelativePath)) 478 if currentPath == "" { 479 // No more local parent pom.xml exists. 480 break 481 } 482 483 f, err := fsys.Open(currentPath) 484 if err != nil { 485 return nil, nil, nil, fmt.Errorf("failed to open parent file %s: %w", currentPath, err) 486 } 487 488 var proj maven.Project 489 err = datasource.NewMavenDecoder(f).Decode(&proj) 490 f.Close() 491 if err != nil { 492 return nil, nil, nil, fmt.Errorf("failed to unmarshal project: %w", err) 493 } 494 if mavenutil.ProjectKey(proj) != parent.ProjectKey || proj.Packaging != "pom" { 495 // This is not the project that we are looking for, we should fetch from upstream 496 // that we don't have write access so we give up here. 497 break 498 } 499 500 origin := mavenOrigin(mavenutil.OriginParent, currentPath) 501 localDeps = append(localDeps, buildOriginalRequirements(proj, origin)...) 502 localProps = append(localProps, buildPropertiesWithOrigins(proj, origin)...) 503 paths = append(paths, currentPath) 504 parent = proj.Parent 505 } 506 507 return localDeps, localProps, paths, nil 508 } 509 510 // Write writes the manifest after applying the patches to outputPath. 511 // 512 // original is the manifest without patches. fsys is the FS that the manifest was read from. 513 // outputPath is the path on disk (*not* in fsys) to write the entire patched manifest to (this can overwrite the original manifest). 514 // 515 // If the original manifest referenced local parent POMs, they will be written alongside the patched manifest, maintaining the relative path structure as it existed in the original location. 516 func (r readWriter) Write(original manifest.Manifest, fsys scalibrfs.FS, patches []result.Patch, outputPath string) error { 517 specific, ok := original.EcosystemSpecific().(ManifestSpecific) 518 if !ok { 519 return errors.New("invalid maven ManifestSpecific data") 520 } 521 522 allPatches, err := buildPatches(patches, specific) 523 if err != nil { 524 return err 525 } 526 527 for _, patchPath := range specific.ParentPaths { 528 patches := allPatches[patchPath] 529 if patchPath == original.FilePath() { 530 patches = allPatches[""] 531 } 532 depFile, err := fsys.Open(patchPath) 533 if err != nil { 534 return err 535 } 536 in := new(bytes.Buffer) 537 if _, err := in.ReadFrom(depFile); err != nil { 538 return fmt.Errorf("failed to read from filesystem: %w", err) 539 } 540 depFile.Close() // Make sure the file is closed before we start writing to it. 541 542 out := new(bytes.Buffer) 543 if err := write(in.String(), out, patches); err != nil { 544 return err 545 } 546 // Write the patched parent relative to the new outputPath 547 relativePatch, err := filepath.Rel(original.FilePath(), patchPath) 548 if err != nil { 549 return err 550 } 551 patchPath = filepath.Join(outputPath, relativePatch) 552 if err := os.MkdirAll(filepath.Dir(patchPath), 0755); err != nil { 553 return err 554 } 555 if err := os.WriteFile(patchPath, out.Bytes(), 0644); err != nil { 556 return err 557 } 558 } 559 560 return nil 561 } 562 563 // Patches represents all the dependencies and properties to be updated 564 type Patches struct { 565 DependencyPatches DependencyPatches 566 PropertyPatches PropertyPatches 567 } 568 569 // Patch represents an individual dependency to be upgraded, and the version to upgrade to 570 type Patch struct { 571 maven.DependencyKey 572 573 NewRequire string 574 } 575 576 // DependencyPatches represent the dependencies to be updated, which 577 // is a map of dependency patches of each origin. 578 type DependencyPatches map[string]map[Patch]bool // origin -> patch -> whether from this project 579 580 // addPatch adds a patch to the patches map indexed by origin. 581 // exist indicates whether this patch comes from the project. 582 func (m DependencyPatches) addPatch(changedDep result.PackageUpdate, exist bool) error { 583 d, o, err := resolve.MavenDepTypeToDependency(changedDep.Type) 584 if err != nil { 585 return fmt.Errorf("MavenDepTypeToDependency: %w", err) 586 } 587 588 // If this dependency did not already exist in the project, we want to add it to the dependencyManagement section 589 if !exist { 590 o = mavenutil.OriginManagement 591 } 592 593 substrings := strings.Split(changedDep.Name, ":") 594 if len(substrings) != 2 { 595 return fmt.Errorf("invalid Maven name: %s", changedDep.Name) 596 } 597 d.GroupID = maven.String(substrings[0]) 598 d.ArtifactID = maven.String(substrings[1]) 599 600 if _, ok := m[o]; !ok { 601 m[o] = make(map[Patch]bool) 602 } 603 m[o][Patch{ 604 DependencyKey: d.Key(), 605 NewRequire: changedDep.VersionTo, 606 }] = exist 607 608 return nil 609 } 610 611 // PropertyPatches represent the properties to be updated, which 612 // is a map of properties of each origin. 613 type PropertyPatches map[string]map[string]string // origin -> tag -> value 614 615 // parentPathFromOrigin returns the parent path embedded in origin, 616 // as well as the remaining origin string. 617 func parentPathFromOrigin(origin string) (string, string) { 618 tokens := strings.Split(origin, "@") 619 if len(tokens) <= 1 { 620 return "", origin 621 } 622 if tokens[0] != mavenutil.OriginParent { 623 return "", origin 624 } 625 626 return tokens[1], strings.Join(tokens[2:], "") 627 } 628 629 func iterUpgrades(patches []result.Patch) iter.Seq[result.PackageUpdate] { 630 return func(yield func(result.PackageUpdate) bool) { 631 for _, patch := range patches { 632 for _, update := range patch.PackageUpdates { 633 if !yield(update) { 634 return 635 } 636 } 637 } 638 } 639 } 640 641 // buildPatches returns dependency patches ready for updates. 642 func buildPatches(patches []result.Patch, specific ManifestSpecific) (map[string]Patches, error) { 643 result := make(map[string]Patches) 644 for patch := range iterUpgrades(patches) { 645 var path string 646 origDep := OriginalDependency(patch, specific.LocalRequirements) 647 path, origDep.Origin = parentPathFromOrigin(origDep.Origin) 648 if _, ok := result[path]; !ok { 649 result[path] = Patches{ 650 DependencyPatches: DependencyPatches{}, 651 PropertyPatches: PropertyPatches{}, 652 } 653 } 654 if origDep.Name() == ":" { 655 // An empty name indicates the dependency is not found, so the original dependency is not in the base project. 656 // Add it so that it will be written into the dependencyManagement section. 657 if err := result[path].DependencyPatches.addPatch(patch, false); err != nil { 658 return nil, err 659 } 660 661 continue 662 } 663 664 patch.Type = resolve.MavenDepType(origDep.Dependency, origDep.Origin) 665 if !origDep.Version.ContainsProperty() { 666 // The original requirement does not contain a property placeholder. 667 if err := result[path].DependencyPatches.addPatch(patch, true); err != nil { 668 return nil, err 669 } 670 671 continue 672 } 673 674 properties, ok := generatePropertyPatches(string(origDep.Version), patch.VersionTo) 675 if !ok { 676 // Not able to update properties to update the requirement. 677 // Update the dependency directly instead. 678 if err := result[path].DependencyPatches.addPatch(patch, true); err != nil { 679 return nil, err 680 } 681 682 continue 683 } 684 685 depOrigin := origDep.Origin 686 if strings.HasPrefix(depOrigin, mavenutil.OriginProfile) { 687 // Dependency management is not indicated in property origin. 688 depOrigin, _ = strings.CutSuffix(depOrigin, "@"+mavenutil.OriginManagement) 689 } else { 690 // Properties are defined either universally or in a profile. For property 691 // origin not starting with 'profile', this is an universal property. 692 depOrigin = "" 693 } 694 695 for name, value := range properties { 696 // A dependency in a profile may contain properties from this profile or 697 // properties universally defined. We need to figure out the origin of these 698 // properties. If a property is defined both universally and in the profile, 699 // we use the profile's origin. 700 propertyOrigin := "" 701 for _, p := range specific.Properties { 702 if p.Name == name && p.Origin != "" && p.Origin == depOrigin { 703 propertyOrigin = depOrigin 704 } 705 } 706 if _, ok := result[path].PropertyPatches[propertyOrigin]; !ok { 707 result[path].PropertyPatches[propertyOrigin] = make(map[string]string) 708 } 709 // This property has been set to update to a value. If both values are the 710 // same, we do nothing; otherwise, instead of updating the property, we 711 // should update the dependency directly. 712 if preset, ok := result[path].PropertyPatches[propertyOrigin][name]; !ok { 713 result[path].PropertyPatches[propertyOrigin][name] = value 714 } else if preset != value { 715 if err := result[path].DependencyPatches.addPatch(patch, true); err != nil { 716 return nil, err 717 } 718 } 719 } 720 } 721 722 return result, nil 723 } 724 725 // OriginalDependency returns the original dependency of a dependency patch. 726 // If the dependency is not found in any local pom.xml, an empty dependency is returned. 727 func OriginalDependency(patch result.PackageUpdate, origDeps []DependencyWithOrigin) DependencyWithOrigin { 728 IDs := strings.Split(patch.Name, ":") 729 if len(IDs) != 2 { 730 return DependencyWithOrigin{} 731 } 732 733 dependency, _, _ := resolve.MavenDepTypeToDependency(patch.Type) 734 dependency.GroupID = maven.String(IDs[0]) 735 dependency.ArtifactID = maven.String(IDs[1]) 736 737 for _, d := range origDeps { 738 if d.Key() == dependency.Key() && d.Version != "" { 739 // If the version is empty, keep looking until we find some non-empty requirement. 740 return d 741 } 742 } 743 744 return DependencyWithOrigin{} 745 } 746 747 // generatePropertyPatches returns whether we are able to assign values to 748 // placeholder keys to convert s1 to s2, as well as the generated patches. 749 // s1 contains property placeholders like '${name}' and s2 is the target string. 750 func generatePropertyPatches(s1, s2 string) (map[string]string, bool) { 751 patches := make(map[string]string) 752 ok := generatePropertyPatchesAux(s1, s2, patches) 753 754 return patches, ok 755 } 756 757 // generatePropertyPatchesAux generates property patches and store them in patches. 758 // TODO: property may refer to another property ${${name}.version} 759 func generatePropertyPatchesAux(s1, s2 string, patches map[string]string) bool { 760 start := strings.Index(s1, "${") 761 if s1[:start] != s2[:start] { 762 // Cannot update property to match the prefix 763 return false 764 } 765 end := strings.Index(s1, "}") 766 next := strings.Index(s1[end+1:], "${") 767 if next < 0 { 768 // There are no more placeholders. 769 remainder := s1[end+1:] 770 if remainder == s2[len(s2)-len(remainder):] { 771 patches[s1[start+2:end]] = s2[start : len(s2)-len(remainder)] 772 return true 773 } 774 } else if match := strings.Index(s2[start:], s1[end+1:end+1+next]); match > 0 { 775 // Try to match the substring between two property placeholders. 776 patches[s1[start+2:end]] = s2[start : start+match] 777 return generatePropertyPatchesAux(s1[end+1:], s2[start+match:], patches) 778 } 779 780 return false 781 } 782 783 func projectStartElement(raw string) string { 784 start := strings.Index(raw, "<project") 785 if start < 0 { 786 return "" 787 } 788 end := strings.Index(raw[start:], ">") 789 if end < 0 { 790 return "" 791 } 792 793 return raw[start : start+end+1] 794 } 795 796 // Only for writing dependencies that are not from the base project. 797 type dependencyManagement struct { 798 Dependencies []dependency `xml:"dependencies>dependency,omitempty"` 799 } 800 801 type dependency struct { 802 GroupID string `xml:"groupId,omitempty"` 803 ArtifactID string `xml:"artifactId,omitempty"` 804 Version string `xml:"version,omitempty"` 805 Type string `xml:"type,omitempty"` 806 Classifier string `xml:"classifier,omitempty"` 807 } 808 809 func makeDependency(patch Patch) dependency { 810 d := dependency{ 811 GroupID: string(patch.GroupID), 812 ArtifactID: string(patch.ArtifactID), 813 Version: patch.NewRequire, 814 Classifier: string(patch.Classifier), 815 } 816 if patch.Type != "" && patch.Type != "jar" { 817 d.Type = string(patch.Type) 818 } 819 820 return d 821 } 822 823 func compareDependency(d1, d2 dependency) int { 824 if i := cmp.Compare(d1.GroupID, d2.GroupID); i != 0 { 825 return i 826 } 827 if i := cmp.Compare(d1.ArtifactID, d2.ArtifactID); i != 0 { 828 return i 829 } 830 if i := cmp.Compare(d1.Type, d2.Type); i != 0 { 831 return i 832 } 833 if i := cmp.Compare(d1.Classifier, d2.Classifier); i != 0 { 834 return i 835 } 836 837 return cmp.Compare(d1.Version, d2.Version) 838 } 839 840 func write(raw string, w io.Writer, patches Patches) error { 841 dec := forkedxml.NewDecoder(bytes.NewReader([]byte(raw))) 842 enc := forkedxml.NewEncoder(w) 843 844 for { 845 token, err := dec.Token() 846 if errors.Is(err, io.EOF) { 847 break 848 } 849 if err != nil { 850 return fmt.Errorf("getting token: %w", err) 851 } 852 853 if tt, ok := token.(forkedxml.StartElement); ok { 854 if tt.Name.Local == "project" { 855 type RawProject struct { 856 InnerXML string `xml:",innerxml"` 857 } 858 var rawProj RawProject 859 if err := dec.DecodeElement(&rawProj, &tt); err != nil { 860 return err 861 } 862 863 // xml.EncodeToken writes a start element with its all name spaces. 864 // It's very common to have a start project element with a few name spaces in Maven. 865 // Thus this would cause a big diff when we try to encode the start element of project. 866 867 // We first capture the raw start element string and write it. 868 projectStart := projectStartElement(raw) 869 if projectStart == "" { 870 return errors.New("unable to get start element of project") 871 } 872 if _, err := w.Write([]byte(projectStart)); err != nil { 873 return fmt.Errorf("writing start element of project: %w", err) 874 } 875 876 // Then we update the project by passing the innerXML and name spaces are not passed. 877 updated := make(map[string]bool) // origin -> updated 878 if err := writeProject(w, enc, rawProj.InnerXML, "", "", patches.DependencyPatches, patches.PropertyPatches, updated); err != nil { 879 return fmt.Errorf("updating project: %w", err) 880 } 881 882 // Check whether dependency management is updated, if not, add a new section of dependency management. 883 if dmPatches := patches.DependencyPatches[mavenutil.OriginManagement]; len(dmPatches) > 0 && !updated[mavenutil.OriginManagement] { 884 enc.Indent(" ", " ") 885 var dm dependencyManagement 886 for p := range dmPatches { 887 dm.Dependencies = append(dm.Dependencies, makeDependency(p)) 888 } 889 // Sort dependency management for consistency in testing. 890 slices.SortFunc(dm.Dependencies, compareDependency) 891 if err := enc.Encode(dm); err != nil { 892 return err 893 } 894 if _, err := w.Write([]byte("\n\n")); err != nil { 895 return err 896 } 897 enc.Indent("", "") 898 } 899 900 // Finally we write the end element of project. 901 if _, err := w.Write([]byte("</project>")); err != nil { 902 return fmt.Errorf("writing start element of project: %w", err) 903 } 904 905 continue 906 } 907 } 908 if err := enc.EncodeToken(token); err != nil { 909 return err 910 } 911 if err := enc.Flush(); err != nil { 912 return err 913 } 914 } 915 916 return nil 917 } 918 919 func writeProject(w io.Writer, enc *forkedxml.Encoder, raw, prefix, id string, patches DependencyPatches, properties PropertyPatches, updated map[string]bool) error { 920 dec := forkedxml.NewDecoder(bytes.NewReader([]byte(raw))) 921 for { 922 token, err := dec.Token() 923 if errors.Is(err, io.EOF) { 924 break 925 } 926 if err != nil { 927 return err 928 } 929 930 if tt, ok := token.(forkedxml.StartElement); ok { 931 switch tt.Name.Local { 932 case "parent": 933 updated["parent"] = true 934 type RawParent struct { 935 maven.ProjectKey 936 937 InnerXML string `xml:",innerxml"` 938 } 939 var rawParent RawParent 940 if err := dec.DecodeElement(&rawParent, &tt); err != nil { 941 return err 942 } 943 req := string(rawParent.Version) 944 if parentPatches, ok := patches["parent"]; ok { 945 // There should only be one parent patch 946 if len(parentPatches) > 1 { 947 return fmt.Errorf("multiple parent patches: %v", parentPatches) 948 } 949 for k := range parentPatches { 950 req = k.NewRequire 951 } 952 } 953 if err := writeString(enc, "<parent>"+rawParent.InnerXML+"</parent>", map[string]string{"version": req}); err != nil { 954 return fmt.Errorf("updating parent: %w", err) 955 } 956 957 continue 958 case "properties": 959 type RawProperties struct { 960 InnerXML string `xml:",innerxml"` 961 } 962 var rawProperties RawProperties 963 if err := dec.DecodeElement(&rawProperties, &tt); err != nil { 964 return err 965 } 966 if err := writeString(enc, "<properties>"+rawProperties.InnerXML+"</properties>", properties[mavenOrigin(prefix, id)]); err != nil { 967 return fmt.Errorf("updating properties: %w", err) 968 } 969 970 continue 971 case "profile": 972 if prefix != "" || id != "" { 973 // Skip updating if prefix or id is set to avoid infinite recursion 974 break 975 } 976 type RawProfile struct { 977 maven.Profile 978 979 InnerXML string `xml:",innerxml"` 980 } 981 var rawProfile RawProfile 982 if err := dec.DecodeElement(&rawProfile, &tt); err != nil { 983 return err 984 } 985 if err := writeProject(w, enc, "<profile>"+rawProfile.InnerXML+"</profile>", mavenutil.OriginProfile, string(rawProfile.ID), patches, properties, updated); err != nil { 986 return fmt.Errorf("updating profile: %w", err) 987 } 988 989 continue 990 case "plugin": 991 if prefix != "" || id != "" { 992 // Skip updating if prefix or id is set to avoid infinite recursion 993 break 994 } 995 type RawPlugin struct { 996 maven.Plugin 997 998 InnerXML string `xml:",innerxml"` 999 } 1000 var rawPlugin RawPlugin 1001 if err := dec.DecodeElement(&rawPlugin, &tt); err != nil { 1002 return err 1003 } 1004 if err := writeProject(w, enc, "<plugin>"+rawPlugin.InnerXML+"</plugin>", mavenutil.OriginPlugin, rawPlugin.Name(), patches, properties, updated); err != nil { 1005 return fmt.Errorf("updating profile: %w", err) 1006 } 1007 1008 continue 1009 case "dependencyManagement": 1010 type RawDependencyManagement struct { 1011 maven.DependencyManagement 1012 1013 InnerXML string `xml:",innerxml"` 1014 } 1015 var rawDepMgmt RawDependencyManagement 1016 if err := dec.DecodeElement(&rawDepMgmt, &tt); err != nil { 1017 return err 1018 } 1019 o := mavenOrigin(prefix, id, mavenutil.OriginManagement) 1020 updated[o] = true 1021 dmPatches := patches[o] 1022 if err := writeDependency(w, enc, "<dependencyManagement>"+rawDepMgmt.InnerXML+"</dependencyManagement>", dmPatches); err != nil { 1023 return fmt.Errorf("updating dependency management: %w", err) 1024 } 1025 1026 continue 1027 case "dependencies": 1028 type RawDependencies struct { 1029 Dependencies []maven.Dependency `xml:"dependencies"` 1030 InnerXML string `xml:",innerxml"` 1031 } 1032 var rawDeps RawDependencies 1033 if err := dec.DecodeElement(&rawDeps, &tt); err != nil { 1034 return err 1035 } 1036 o := mavenOrigin(prefix, id) 1037 updated[o] = true 1038 depPatches := patches[o] 1039 if err := writeDependency(w, enc, "<dependencies>"+rawDeps.InnerXML+"</dependencies>", depPatches); err != nil { 1040 return fmt.Errorf("updating dependencies: %w", err) 1041 } 1042 1043 continue 1044 } 1045 } 1046 if err := enc.EncodeToken(token); err != nil { 1047 return err 1048 } 1049 } 1050 1051 return enc.Flush() 1052 } 1053 1054 // indentation returns the indentation of the dependency element. 1055 // If dependencies or dependency elements are not found, the default 1056 // indentation (four space) is returned. 1057 func indentation(raw string) string { 1058 i := strings.Index(raw, "<dependencies>") 1059 if i < 0 { 1060 return " " 1061 } 1062 1063 raw = raw[i+len("<dependencies>"):] 1064 // Find the first dependency element. 1065 j := strings.Index(raw, "<dependency>") 1066 if j < 0 { 1067 return " " 1068 } 1069 1070 raw = raw[:j] 1071 // Find the last new line and get the space between. 1072 k := strings.LastIndex(raw, "\n") 1073 if k < 0 { 1074 return " " 1075 } 1076 1077 return raw[k+1:] 1078 } 1079 1080 func writeDependency(w io.Writer, enc *forkedxml.Encoder, raw string, patches map[Patch]bool) error { 1081 dec := forkedxml.NewDecoder(bytes.NewReader([]byte(raw))) 1082 for { 1083 token, err := dec.Token() 1084 if errors.Is(err, io.EOF) { 1085 break 1086 } 1087 if err != nil { 1088 return err 1089 } 1090 1091 if tt, ok := token.(forkedxml.StartElement); ok { 1092 if tt.Name.Local == "dependencies" { 1093 // We still need to write the start element <dependencies> 1094 if err := enc.EncodeToken(token); err != nil { 1095 return err 1096 } 1097 if err := enc.Flush(); err != nil { 1098 return err 1099 } 1100 1101 // Write patches that are not in the base project. 1102 var deps []dependency 1103 for p, ok := range patches { 1104 if !ok { 1105 deps = append(deps, makeDependency(p)) 1106 } 1107 } 1108 if len(deps) == 0 { 1109 // No dependencies to add 1110 continue 1111 } 1112 // Sort dependencies for consistency in testing. 1113 slices.SortFunc(deps, compareDependency) 1114 1115 enc.Indent(indentation(raw), " ") 1116 // Write a new line to keep the format. 1117 if _, err := w.Write([]byte("\n")); err != nil { 1118 return err 1119 } 1120 for _, d := range deps { 1121 if err := enc.Encode(d); err != nil { 1122 return err 1123 } 1124 } 1125 enc.Indent("", "") 1126 1127 continue 1128 } 1129 if tt.Name.Local == "dependency" { 1130 type RawDependency struct { 1131 maven.Dependency 1132 1133 InnerXML string `xml:",innerxml"` 1134 } 1135 var rawDep RawDependency 1136 if err := dec.DecodeElement(&rawDep, &tt); err != nil { 1137 return err 1138 } 1139 req := string(rawDep.Version) 1140 for patch := range patches { 1141 // A Maven dependency key consists of Type and Classifier together with GroupID and ArtifactID. 1142 if patch.DependencyKey == rawDep.Key() { 1143 req = patch.NewRequire 1144 } 1145 } 1146 // xml.EncodeElement writes all empty elements and may not follow the existing format. 1147 // Passing the innerXML can help to keep the original format. 1148 if err := writeString(enc, "<dependency>"+rawDep.InnerXML+"</dependency>", map[string]string{"version": req}); err != nil { 1149 return fmt.Errorf("updating dependency: %w", err) 1150 } 1151 1152 continue 1153 } 1154 } 1155 1156 if err := enc.EncodeToken(token); err != nil { 1157 return err 1158 } 1159 } 1160 1161 return enc.Flush() 1162 } 1163 1164 // writeString writes XML string specified by raw with replacements specified in values. 1165 func writeString(enc *forkedxml.Encoder, raw string, values map[string]string) error { 1166 dec := forkedxml.NewDecoder(bytes.NewReader([]byte(raw))) 1167 for { 1168 token, err := dec.Token() 1169 if errors.Is(err, io.EOF) { 1170 break 1171 } 1172 if err != nil { 1173 return err 1174 } 1175 if tt, ok := token.(forkedxml.StartElement); ok { 1176 if value, ok2 := values[tt.Name.Local]; ok2 { 1177 var str string 1178 if err := dec.DecodeElement(&str, &tt); err != nil { 1179 return err 1180 } 1181 if err := enc.EncodeElement(value, tt); err != nil { 1182 return err 1183 } 1184 1185 continue 1186 } 1187 } 1188 if err := enc.EncodeToken(token); err != nil { 1189 return err 1190 } 1191 } 1192 1193 return enc.Flush() 1194 }