github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/java/internal/maven/resolver.go (about) 1 package maven 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "os" 11 "path" 12 "path/filepath" 13 "reflect" 14 "regexp" 15 "slices" 16 "strings" 17 "time" 18 19 "github.com/vifraa/gopom" 20 21 "github.com/anchore/syft/internal" 22 "github.com/anchore/syft/internal/cache" 23 "github.com/anchore/syft/internal/log" 24 "github.com/anchore/syft/syft/file" 25 ) 26 27 // ID is the unique identifier for a package in Maven 28 type ID struct { 29 GroupID string 30 ArtifactID string 31 Version string 32 } 33 34 func NewID(groupID, artifactID, version string) ID { 35 return ID{ 36 GroupID: groupID, 37 ArtifactID: artifactID, 38 Version: version, 39 } 40 } 41 42 func (m ID) String() string { 43 return fmt.Sprintf("(groupId: %s artifactId: %s version: %s)", m.GroupID, m.ArtifactID, m.Version) 44 } 45 46 // Valid indicates that the given maven ID has values for groupId, artifactId, and version 47 func (m ID) Valid() bool { 48 return m.GroupID != "" && m.ArtifactID != "" && m.Version != "" 49 } 50 51 var expressionMatcher = regexp.MustCompile("[$][{][^}]+[}]") 52 53 // Resolver is a short-lived utility to resolve maven poms from multiple sources, including: 54 // the scanned filesystem, local maven cache directories, remote maven repositories, and the syft cache 55 type Resolver struct { 56 cfg Config 57 cache cache.Cache 58 resolved map[ID]*Project 59 remoteRequestTimeout time.Duration 60 checkedLocalRepo bool 61 // fileResolver and pomLocations are used to resolve parent poms by relativePath 62 fileResolver file.Resolver 63 pomLocations map[*Project]file.Location 64 } 65 66 // NewResolver constructs a new Resolver with the given configuration. 67 // NOTE: the fileResolver is optional and if provided will be used to resolve parent poms by relative path 68 func NewResolver(fileResolver file.Resolver, cfg Config) *Resolver { 69 return &Resolver{ 70 cfg: cfg, 71 cache: cache.GetManager().GetCache("java/maven/repo", "v1"), 72 resolved: map[ID]*Project{}, 73 remoteRequestTimeout: time.Second * 10, 74 fileResolver: fileResolver, 75 pomLocations: map[*Project]file.Location{}, 76 } 77 } 78 79 // ResolveProperty gets property values by emulating maven property resolution logic, looking in the project's variables 80 // as well as supporting the project expressions like ${project.parent.groupId}. 81 // Properties which are not resolved result in empty string "" 82 func (r *Resolver) ResolveProperty(ctx context.Context, pom *Project, propertyValue *string) string { 83 return r.resolvePropertyValue(ctx, propertyValue, nil, pom) 84 } 85 86 // resolvePropertyValue resolves property values by emulating maven property resolution logic, looking in the project's variables 87 // as well as supporting the project expressions like ${project.parent.groupId}. 88 // Properties which are not resolved result in empty string "" 89 func (r *Resolver) resolvePropertyValue(ctx context.Context, propertyValue *string, resolvingProperties []string, resolutionContext ...*Project) string { 90 if propertyValue == nil { 91 return "" 92 } 93 resolved, err := r.resolveExpression(ctx, resolutionContext, *propertyValue, resolvingProperties) 94 if err != nil { 95 log.WithFields("error", err, "propertyValue", *propertyValue).Trace("error resolving maven property") 96 return "" 97 } 98 return resolved 99 } 100 101 // resolveExpression resolves an expression, which may be a plain string or a string with ${ property.references } 102 func (r *Resolver) resolveExpression(ctx context.Context, resolutionContext []*Project, expression string, resolvingProperties []string) (string, error) { 103 log.Tracef("resolving expression: '%v' in context: %v", expression, resolutionContext) 104 105 var errs error 106 return expressionMatcher.ReplaceAllStringFunc(expression, func(match string) string { 107 log.Tracef("resolving property: '%v' in context: %v", expression, resolutionContext) 108 propertyExpression := strings.TrimSpace(match[2 : len(match)-1]) // remove leading ${ and trailing } 109 resolved, err := r.resolveProperty(ctx, resolutionContext, propertyExpression, resolvingProperties) 110 if err != nil { 111 errs = errors.Join(errs, err) 112 return "" 113 } 114 return resolved 115 }), errs 116 } 117 118 // resolveProperty resolves properties recursively from the root project 119 func (r *Resolver) resolveProperty(ctx context.Context, resolutionContext []*Project, propertyExpression string, resolvingProperties []string) (string, error) { 120 // prevent cycles 121 if slices.Contains(resolvingProperties, propertyExpression) { 122 return "", fmt.Errorf("cycle detected resolving: %s", propertyExpression) 123 } 124 if len(resolutionContext) == 0 { 125 return "", fmt.Errorf("no project variable resolution context provided for expression: '%s'", propertyExpression) 126 } 127 resolvingProperties = append(resolvingProperties, propertyExpression) 128 129 // only resolve project. properties in the context of the current project pom 130 value, err := r.resolveProjectProperty(ctx, resolutionContext, resolutionContext[len(resolutionContext)-1], propertyExpression, resolvingProperties) 131 if err != nil { 132 return value, err 133 } 134 if value != "" { 135 return value, nil 136 } 137 138 var resolvingParents []*Project 139 for _, pom := range resolutionContext { 140 current := pom 141 for parentDepth := 0; current != nil; parentDepth++ { 142 if slices.Contains(resolvingParents, current) { 143 log.WithFields("property", propertyExpression, "mavenID", r.resolveID(ctx, resolvingProperties, resolvingParents...)).Error("got circular reference while resolving property") 144 break // some sort of circular reference -- we've already seen this project 145 } 146 if r.cfg.MaxParentRecursiveDepth > 0 && parentDepth > r.cfg.MaxParentRecursiveDepth { 147 return "", fmt.Errorf("maximum parent recursive depth (%v) reached resolving property: %v", r.cfg.MaxParentRecursiveDepth, propertyExpression) 148 } 149 if current.Properties != nil && current.Properties.Entries != nil { 150 if value, ok := current.Properties.Entries[propertyExpression]; ok { 151 return r.resolveExpression(ctx, resolutionContext, value, resolvingProperties) // property values can contain expressions 152 } 153 } 154 resolvingParents = append(resolvingParents, current) 155 current, err = r.resolveParent(ctx, current, resolvingProperties...) 156 if err != nil { 157 return "", err 158 } 159 } 160 } 161 162 return "", fmt.Errorf("unable to resolve property: %s", propertyExpression) 163 } 164 165 // resolveProjectProperty resolves properties on the project 166 // 167 //nolint:gocognit 168 func (r *Resolver) resolveProjectProperty(ctx context.Context, resolutionContext []*Project, pom *Project, propertyExpression string, resolving []string) (string, error) { 169 // see if we have a project.x expression and process this based 170 // on the xml tags in gopom 171 parts := strings.Split(propertyExpression, ".") 172 numParts := len(parts) 173 if numParts > 1 && strings.TrimSpace(parts[0]) == "project" { 174 pomValue := reflect.ValueOf(pom).Elem() 175 pomValueType := pomValue.Type() 176 for partNum := 1; partNum < numParts; partNum++ { 177 if pomValueType.Kind() != reflect.Struct { 178 break 179 } 180 181 part := parts[partNum] 182 // these two fields are directly inherited from the pom parent values 183 if partNum == 1 && pom.Parent != nil { 184 switch part { 185 case "version": 186 if pom.Version == nil && pom.Parent.Version != nil { 187 return r.resolveExpression(ctx, resolutionContext, *pom.Parent.Version, resolving) 188 } 189 case "groupID": 190 if pom.GroupID == nil && pom.Parent.GroupID != nil { 191 return r.resolveExpression(ctx, resolutionContext, *pom.Parent.GroupID, resolving) 192 } 193 } 194 } 195 for fieldNum := 0; fieldNum < pomValueType.NumField(); fieldNum++ { 196 f := pomValueType.Field(fieldNum) 197 tag := f.Tag.Get("xml") 198 tag = strings.Split(tag, ",")[0] 199 // a segment of the property name matches the xml tag for the field, 200 // so we need to recurse down the nested structs or return a match 201 // if we're done. 202 if part != tag { 203 continue 204 } 205 206 pomValue = pomValue.Field(fieldNum) 207 pomValueType = pomValue.Type() 208 if pomValueType.Kind() == reflect.Ptr { 209 // we were recursing down the nested structs, but one of the steps 210 // we need to take is a nil pointer, so give up 211 if pomValue.IsNil() { 212 return "", fmt.Errorf("property undefined: %s", propertyExpression) 213 } 214 pomValue = pomValue.Elem() 215 if !pomValue.IsZero() { 216 // we found a non-zero value whose tag matches this part of the property name 217 pomValueType = pomValue.Type() 218 } 219 } 220 // If this was the last part of the property name, return the value 221 if partNum == numParts-1 { 222 value := fmt.Sprintf("%v", pomValue.Interface()) 223 return r.resolveExpression(ctx, resolutionContext, value, resolving) 224 } 225 break 226 } 227 } 228 } 229 return "", nil 230 } 231 232 // ResolveParent resolves the parent definition, and returns a POM for the parent, which is possibly incomplete, or nil 233 func (r *Resolver) ResolveParent(ctx context.Context, pom *Project) (*Project, error) { 234 if pom == nil || pom.Parent == nil { 235 return nil, nil 236 } 237 238 parent, err := r.resolveParent(ctx, pom) 239 if parent != nil { 240 return parent, err 241 } 242 243 groupID := r.ResolveProperty(ctx, pom, pom.Parent.GroupID) 244 if groupID == "" { 245 groupID = r.ResolveProperty(ctx, pom, pom.GroupID) 246 } 247 artifactID := r.ResolveProperty(ctx, pom, pom.Parent.ArtifactID) 248 version := r.ResolveProperty(ctx, pom, pom.Parent.Version) 249 250 if artifactID != "" && version != "" { 251 return &Project{ 252 GroupID: &groupID, 253 ArtifactID: &artifactID, 254 Version: &version, 255 }, nil 256 } 257 258 return nil, fmt.Errorf("unsufficient information to create a parent pom project, id: %s", NewID(groupID, artifactID, version)) 259 } 260 261 // ResolveID creates an ID from a pom, resolving parent information as necessary 262 func (r *Resolver) ResolveID(ctx context.Context, pom *Project) ID { 263 return r.resolveID(ctx, nil, pom) 264 } 265 266 // resolveID creates a new ID from a pom, resolving parent information as necessary 267 func (r *Resolver) resolveID(ctx context.Context, resolvingProperties []string, resolutionContext ...*Project) ID { 268 if len(resolutionContext) == 0 || resolutionContext[0] == nil { 269 return ID{} 270 } 271 pom := resolutionContext[len(resolutionContext)-1] // get topmost pom 272 if pom == nil { 273 return ID{} 274 } 275 276 groupID := r.resolvePropertyValue(ctx, pom.GroupID, resolvingProperties, resolutionContext...) 277 artifactID := r.resolvePropertyValue(ctx, pom.ArtifactID, resolvingProperties, resolutionContext...) 278 version := r.resolvePropertyValue(ctx, pom.Version, resolvingProperties, resolutionContext...) 279 if pom.Parent != nil { 280 // groupId and version are able to be inherited from the parent, but importantly: not artifactId. see: 281 // https://maven.apache.org/guides/introduction/introduction-to-the-pom.html#the-solution 282 if groupID == "" && deref(pom.GroupID) == "" { 283 groupID = r.resolvePropertyValue(ctx, pom.Parent.GroupID, resolvingProperties, resolutionContext...) 284 } 285 if version == "" && deref(pom.Version) == "" { 286 version = r.resolvePropertyValue(ctx, pom.Parent.Version, resolvingProperties, resolutionContext...) 287 } 288 } 289 return ID{groupID, artifactID, version} 290 } 291 292 // ResolveDependencyID creates an ID from a dependency element in a pom, resolving information as necessary 293 func (r *Resolver) ResolveDependencyID(ctx context.Context, pom *Project, dep Dependency) ID { 294 if pom == nil { 295 return ID{} 296 } 297 298 groupID := r.resolvePropertyValue(ctx, dep.GroupID, nil, pom) 299 artifactID := r.resolvePropertyValue(ctx, dep.ArtifactID, nil, pom) 300 version := r.resolvePropertyValue(ctx, dep.Version, nil, pom) 301 302 var err error 303 if version == "" { 304 version, err = r.resolveInheritedVersion(ctx, pom, groupID, artifactID) 305 } 306 307 depID := ID{groupID, artifactID, version} 308 309 if err != nil { 310 log.WithFields("error", err, "ID", r.ResolveID(ctx, pom), "dependencyID", depID) 311 } 312 313 return depID 314 } 315 316 // FindPom gets a pom from cache, local repository, or from a remote Maven repository depending on configuration 317 func (r *Resolver) FindPom(ctx context.Context, groupID, artifactID, version string) (*Project, error) { 318 if groupID == "" || artifactID == "" || version == "" { 319 return nil, fmt.Errorf("invalid maven pom specification, require non-empty values for groupID: '%s', artifactID: '%s', version: '%s'", groupID, artifactID, version) 320 } 321 322 id := ID{groupID, artifactID, version} 323 existingPom := r.resolved[id] 324 325 if existingPom != nil { 326 return existingPom, nil 327 } 328 329 var errs error 330 331 // try to resolve first from local maven repo 332 if r.cfg.UseLocalRepository { 333 pom, err := r.findPomInLocalRepository(groupID, artifactID, version) 334 if pom != nil { 335 r.resolved[id] = pom 336 return pom, nil 337 } 338 errs = errors.Join(errs, err) 339 } 340 341 // resolve via network maven repository 342 if r.cfg.UseNetwork { 343 pom, err := r.findPomInRemotes(ctx, groupID, artifactID, version) 344 if pom != nil { 345 r.resolved[id] = pom 346 return pom, nil 347 } 348 errs = errors.Join(errs, err) 349 } 350 351 return nil, fmt.Errorf("unable to resolve pom %s %s %s: %w", groupID, artifactID, version, errs) 352 } 353 354 // findPomInLocalRepository attempts to get the POM from the users local maven repository 355 func (r *Resolver) findPomInLocalRepository(groupID, artifactID, version string) (*Project, error) { 356 groupPath := filepath.Join(strings.Split(groupID, ".")...) 357 pomFilePath := filepath.Join(r.cfg.LocalRepositoryDir, groupPath, artifactID, version, artifactID+"-"+version+".pom") 358 pomFile, err := os.Open(pomFilePath) 359 if err != nil { 360 if !r.checkedLocalRepo && errors.Is(err, os.ErrNotExist) { 361 r.checkedLocalRepo = true 362 // check if the directory exists at all, and if not just stop trying to resolve local maven files 363 fi, err := os.Stat(r.cfg.LocalRepositoryDir) 364 if errors.Is(err, os.ErrNotExist) || !fi.IsDir() { 365 log.WithFields("error", err, "repositoryDir", r.cfg.LocalRepositoryDir). 366 Info("local maven repository is not a readable directory, stopping local resolution") 367 r.cfg.UseLocalRepository = false 368 } 369 } 370 return nil, err 371 } 372 defer internal.CloseAndLogError(pomFile, pomFilePath) 373 374 return ParsePomXML(pomFile) 375 } 376 377 // findPomInRemotes download the pom file from all configured Maven repositories over HTTP 378 func (r *Resolver) findPomInRemotes(ctx context.Context, groupID, artifactID, version string) (*Project, error) { 379 var errs error 380 for _, repo := range r.cfg.Repositories { 381 pom, err := r.findPomInRemoteRepository(ctx, repo, groupID, artifactID, version) 382 if err != nil { 383 errs = errors.Join(errs, err) 384 } 385 if pom != nil { 386 return pom, err 387 } 388 } 389 return nil, fmt.Errorf("pom for %v not found in any remote repository: %w", ID{groupID, artifactID, version}, errs) 390 } 391 392 // findPomInRemoteRepository download the pom file from a (remote) Maven repository over HTTP 393 func (r *Resolver) findPomInRemoteRepository(ctx context.Context, repo string, groupID, artifactID, version string) (*Project, error) { 394 if groupID == "" || artifactID == "" || version == "" { 395 return nil, fmt.Errorf("missing/incomplete maven artifact coordinates -- groupId: '%s' artifactId: '%s', version: '%s'", groupID, artifactID, version) 396 } 397 398 requestURL, err := remotePomURL(repo, groupID, artifactID, version) 399 if err != nil { 400 return nil, fmt.Errorf("unable to find pom in remote due to: %w", err) 401 } 402 403 // Downloading snapshots requires additional steps to determine the latest snapshot version. 404 // See: https://maven.apache.org/ref/3-LATEST/maven-repository-metadata/ 405 if strings.HasSuffix(version, "-SNAPSHOT") { 406 return nil, fmt.Errorf("downloading snapshot artifacts is not supported, got: %s", requestURL) 407 } 408 409 cacheKey := strings.TrimPrefix(strings.TrimPrefix(requestURL, "http://"), "https://") 410 reader, err := r.cacheResolveReader(cacheKey, func() (io.ReadCloser, error) { 411 if err != nil { 412 return nil, err 413 } 414 log.WithFields("url", requestURL).Info("fetching parent pom from remote maven repository") 415 416 req, err := http.NewRequest(http.MethodGet, requestURL, nil) 417 if err != nil { 418 return nil, fmt.Errorf("unable to create request for Maven central: %w", err) 419 } 420 421 req = req.WithContext(ctx) 422 423 client := http.Client{ 424 Timeout: r.remoteRequestTimeout, 425 } 426 427 resp, err := client.Do(req) 428 if err != nil { 429 return nil, fmt.Errorf("unable to get pom from Maven repository %v: %w", requestURL, err) 430 } 431 if resp.StatusCode == http.StatusNotFound { 432 return nil, fmt.Errorf("pom not found in Maven repository at: %v", requestURL) 433 } 434 return resp.Body, err 435 }) 436 if err != nil { 437 return nil, err 438 } 439 if reader, ok := reader.(io.Closer); ok { 440 defer internal.CloseAndLogError(reader, requestURL) 441 } 442 pom, err := ParsePomXML(reader) 443 if err != nil { 444 return nil, fmt.Errorf("unable to parse pom from Maven repository url %v: %w", requestURL, err) 445 } 446 return pom, nil 447 } 448 449 // cacheResolveReader attempts to get a reader from cache, otherwise caches the contents of the resolve() function. 450 // this function is guaranteed to return an unread reader for the correct contents. 451 // NOTE: this could be promoted to the internal cache package as a specialized version of the cache.Resolver 452 // if there are more users of this functionality 453 func (r *Resolver) cacheResolveReader(key string, resolve func() (io.ReadCloser, error)) (io.Reader, error) { 454 reader, err := r.cache.Read(key) 455 if err == nil && reader != nil { 456 return reader, err 457 } 458 459 contentReader, err := resolve() 460 if err != nil { 461 return nil, err 462 } 463 defer internal.CloseAndLogError(contentReader, key) 464 465 // store the contents to return a new reader with the same content 466 contents, err := io.ReadAll(contentReader) 467 if err != nil { 468 return nil, err 469 } 470 err = r.cache.Write(key, bytes.NewBuffer(contents)) 471 return bytes.NewBuffer(contents), err 472 } 473 474 // resolveParent attempts to resolve the parent for the given pom 475 func (r *Resolver) resolveParent(ctx context.Context, pom *Project, resolvingProperties ...string) (*Project, error) { 476 if pom == nil || pom.Parent == nil { 477 return nil, nil 478 } 479 parent := pom.Parent 480 pomWithoutParent := *pom 481 pomWithoutParent.Parent = nil 482 groupID := r.resolvePropertyValue(ctx, parent.GroupID, resolvingProperties, &pomWithoutParent) 483 artifactID := r.resolvePropertyValue(ctx, parent.ArtifactID, resolvingProperties, &pomWithoutParent) 484 version := r.resolvePropertyValue(ctx, parent.Version, resolvingProperties, &pomWithoutParent) 485 486 // check cache before resolving 487 parentID := ID{groupID, artifactID, version} 488 if resolvedParent, ok := r.resolved[parentID]; ok { 489 return resolvedParent, nil 490 } 491 492 // check if the pom exists in the fileResolver 493 parentPom := r.findParentPomByRelativePath(ctx, pom, parentID, resolvingProperties) 494 if parentPom != nil { 495 return parentPom, nil 496 } 497 498 // find POM normally 499 return r.FindPom(ctx, groupID, artifactID, version) 500 } 501 502 // resolveInheritedVersion attempts to find the version of a dependency (groupID, artifactID) by searching all parent poms and imported managed dependencies 503 // 504 //nolint:gocognit 505 func (r *Resolver) resolveInheritedVersion(ctx context.Context, pom *Project, groupID, artifactID string, resolutionContext ...*Project) (string, error) { 506 if pom == nil { 507 return "", fmt.Errorf("nil pom provided to findInheritedVersion") 508 } 509 if r.cfg.MaxParentRecursiveDepth > 0 && len(resolutionContext) > r.cfg.MaxParentRecursiveDepth { 510 return "", fmt.Errorf("maximum depth reached attempting to resolve version for: %s:%s at: %v", groupID, artifactID, r.ResolveID(ctx, pom)) 511 } 512 if slices.Contains(resolutionContext, pom) { 513 return "", fmt.Errorf("cycle detected attempting to resolve version for: %s:%s at: %v", groupID, artifactID, r.ResolveID(ctx, pom)) 514 } 515 resolutionContext = append(resolutionContext, pom) 516 517 var err error 518 var version string 519 520 // check for entries in dependencyManagement first 521 for _, dep := range pomManagedDependencies(pom) { 522 depGroupID := r.resolvePropertyValue(ctx, dep.GroupID, nil, resolutionContext...) 523 depArtifactID := r.resolvePropertyValue(ctx, dep.ArtifactID, nil, resolutionContext...) 524 if depGroupID == groupID && depArtifactID == artifactID { 525 version = r.resolvePropertyValue(ctx, dep.Version, nil, resolutionContext...) 526 if version != "" { 527 return version, nil 528 } 529 } 530 531 // imported pom files should be treated just like parent poms, they are used to define versions of dependencies 532 if deref(dep.Type) == "pom" && deref(dep.Scope) == "import" { 533 depVersion := r.resolvePropertyValue(ctx, dep.Version, nil, resolutionContext...) 534 535 depPom, err := r.FindPom(ctx, depGroupID, depArtifactID, depVersion) 536 if err != nil || depPom == nil { 537 log.WithFields("error", err, "ID", r.ResolveID(ctx, pom), "dependencyID", ID{depGroupID, depArtifactID, depVersion}). 538 Debug("unable to find imported pom looking for managed dependencies") 539 continue 540 } 541 version, err = r.resolveInheritedVersion(ctx, depPom, groupID, artifactID, resolutionContext...) 542 if err != nil { 543 log.WithFields("error", err, "ID", r.ResolveID(ctx, pom), "dependencyID", ID{depGroupID, depArtifactID, depVersion}). 544 Debug("error during findInheritedVersion") 545 } 546 if version != "" { 547 return version, nil 548 } 549 } 550 } 551 552 // recursively check parents 553 parent, err := r.resolveParent(ctx, pom) 554 if err != nil { 555 return "", err 556 } 557 if parent != nil { 558 version, err = r.resolveInheritedVersion(ctx, parent, groupID, artifactID, resolutionContext...) 559 if err != nil { 560 return "", err 561 } 562 if version != "" { 563 return version, nil 564 } 565 } 566 567 // check for inherited dependencies 568 for _, dep := range DirectPomDependencies(pom) { 569 depGroupID := r.resolvePropertyValue(ctx, dep.GroupID, nil, resolutionContext...) 570 depArtifactID := r.resolvePropertyValue(ctx, dep.ArtifactID, nil, resolutionContext...) 571 if depGroupID == groupID && depArtifactID == artifactID { 572 version = r.resolvePropertyValue(ctx, dep.Version, nil, resolutionContext...) 573 if version != "" { 574 return version, nil 575 } 576 } 577 } 578 579 return "", nil 580 } 581 582 // FindLicenses attempts to find a pom, and once found attempts to resolve licenses traversing 583 // parent poms as necessary 584 func (r *Resolver) FindLicenses(ctx context.Context, groupID, artifactID, version string) ([]gopom.License, error) { 585 pom, err := r.FindPom(ctx, groupID, artifactID, version) 586 if pom == nil || err != nil { 587 return nil, err 588 } 589 return r.resolveLicenses(ctx, pom) 590 } 591 592 // ResolveLicenses searches the pom for license, resolving and traversing parent poms if needed 593 func (r *Resolver) ResolveLicenses(ctx context.Context, pom *Project) ([]License, error) { 594 return r.resolveLicenses(ctx, pom) 595 } 596 597 // resolveLicenses searches the pom for license, traversing parent poms if needed 598 func (r *Resolver) resolveLicenses(ctx context.Context, pom *Project, processing ...ID) ([]License, error) { 599 id := r.ResolveID(ctx, pom) 600 if slices.Contains(processing, id) { 601 return nil, fmt.Errorf("cycle detected resolving licenses for: %v", id) 602 } 603 if r.cfg.MaxParentRecursiveDepth > 0 && len(processing) > r.cfg.MaxParentRecursiveDepth { 604 return nil, fmt.Errorf("maximum parent recursive depth (%v) reached: %v", r.cfg.MaxParentRecursiveDepth, processing) 605 } 606 607 directLicenses := r.pomLicenses(ctx, pom) 608 if len(directLicenses) > 0 { 609 return directLicenses, nil 610 } 611 612 parent, err := r.resolveParent(ctx, pom) 613 if err != nil { 614 return nil, err 615 } 616 if parent == nil { 617 return nil, nil 618 } 619 return r.resolveLicenses(ctx, parent, append(processing, id)...) 620 } 621 622 // pomLicenses appends the directly specified licenses with non-empty name or url 623 func (r *Resolver) pomLicenses(ctx context.Context, pom *Project) []License { 624 var out []License 625 for _, license := range deref(pom.Licenses) { 626 // if we find non-empty licenses, return them 627 name := r.resolvePropertyValue(ctx, license.Name, nil, pom) 628 url := r.resolvePropertyValue(ctx, license.URL, nil, pom) 629 if name != "" || url != "" { 630 out = append(out, license) 631 } 632 } 633 return out 634 } 635 636 func (r *Resolver) findParentPomByRelativePath(ctx context.Context, pom *Project, parentID ID, resolvingProperties []string) *Project { 637 // can't resolve without a file resolver 638 if r.fileResolver == nil { 639 return nil 640 } 641 642 pomLocation, hasPomLocation := r.pomLocations[pom] 643 if !hasPomLocation || pom == nil || pom.Parent == nil { 644 return nil 645 } 646 relativePath := r.resolvePropertyValue(ctx, pom.Parent.RelativePath, resolvingProperties, pom) 647 if relativePath == "" { 648 return nil 649 } 650 p := pomLocation.Path() 651 p = path.Dir(p) 652 p = path.Join(p, relativePath) 653 p = path.Clean(p) 654 if !strings.HasSuffix(p, ".xml") { 655 p = path.Join(p, "pom.xml") 656 } 657 parentLocations, err := r.fileResolver.FilesByPath(p) 658 if err != nil || len(parentLocations) == 0 { 659 log.WithFields("error", err, "mavenID", r.resolveID(ctx, resolvingProperties, pom), "parentID", parentID, "relativePath", relativePath). 660 Trace("parent pom not found by relative path") 661 return nil 662 } 663 parentLocation := parentLocations[0] 664 665 parentContents, err := r.fileResolver.FileContentsByLocation(parentLocation) 666 if err != nil || parentContents == nil { 667 log.WithFields("error", err, "mavenID", r.resolveID(ctx, resolvingProperties, pom), "parentID", parentID, "parentLocation", parentLocation). 668 Debug("unable to get contents of parent pom by relative path") 669 return nil 670 } 671 defer internal.CloseAndLogError(parentContents, parentLocation.RealPath) 672 parentPom, err := ParsePomXML(parentContents) 673 if err != nil || parentPom == nil { 674 log.WithFields("error", err, "mavenID", r.resolveID(ctx, resolvingProperties, pom), "parentID", parentID, "parentLocation", parentLocation). 675 Debug("unable to parse parent pom") 676 return nil 677 } 678 // ensure parent matches 679 newParentID := r.resolveID(ctx, resolvingProperties, parentPom) 680 if newParentID.ArtifactID != parentID.ArtifactID { 681 log.WithFields("newParentID", newParentID, "mavenID", r.resolveID(ctx, resolvingProperties, pom), "parentID", parentID, "parentLocation", parentLocation). 682 Debug("parent IDs do not match resolving parent by relative path") 683 return nil 684 } 685 686 r.resolved[parentID] = parentPom 687 r.pomLocations[parentPom] = parentLocation // for any future parent relativePath lookups 688 689 return parentPom 690 } 691 692 // AddPom allows for adding known pom files with locations within the file resolver, these locations may be used 693 // while resolving parent poms by relative path 694 func (r *Resolver) AddPom(ctx context.Context, pom *Project, location file.Location) { 695 r.pomLocations[pom] = location 696 // by calling resolve ID here, this will lookup necessary parent poms by relative path, and 697 // track any poms we found with complete version information if enough is available to resolve 698 id := r.ResolveID(ctx, pom) 699 if id.Valid() { 700 _, existing := r.resolved[id] 701 if !existing { 702 r.resolved[id] = pom 703 } 704 } 705 } 706 707 // DirectPomDependencies returns all dependencies directly defined in a project, including all defined in profiles. 708 // This does not resolve any parent or transitive dependencies 709 func DirectPomDependencies(pom *Project) []Dependency { 710 dependencies := deref(pom.Dependencies) 711 for _, profile := range deref(pom.Profiles) { 712 dependencies = append(dependencies, deref(profile.Dependencies)...) 713 } 714 return dependencies 715 } 716 717 // pomManagedDependencies returns all directly defined managed dependencies in a project pom, including all defined in profiles. 718 // does not resolve parent managed dependencies 719 func pomManagedDependencies(pom *Project) []Dependency { 720 var dependencies []Dependency 721 if pom.DependencyManagement != nil { 722 dependencies = append(dependencies, deref(pom.DependencyManagement.Dependencies)...) 723 } 724 for _, profile := range deref(pom.Profiles) { 725 if profile.DependencyManagement != nil { 726 dependencies = append(dependencies, deref(profile.DependencyManagement.Dependencies)...) 727 } 728 } 729 return dependencies 730 } 731 732 // deref dereferences ptr if not nil, or returns the type default value if ptr is nil 733 func deref[T any](ptr *T) T { 734 if ptr == nil { 735 var t T 736 return t 737 } 738 return *ptr 739 }