github.com/cobalt77/jfrog-client-go@v0.14.5/artifactory/services/utils/aqlquerybuilder.go (about) 1 package utils 2 3 import ( 4 "fmt" 5 "strconv" 6 "strings" 7 8 "github.com/cobalt77/jfrog-client-go/utils/io/fileutils" 9 10 "github.com/cobalt77/jfrog-client-go/utils" 11 ) 12 13 // Returns an AQL body string to search file in Artifactory by pattern, according the the specified arguments requirements. 14 func CreateAqlBodyForSpecWithPattern(params *ArtifactoryCommonParams) (string, error) { 15 searchPattern := prepareSourceSearchPattern(params.Pattern, params.Target, true) 16 repoPathFileTriples := createRepoPathFileTriples(searchPattern, params.Recursive) 17 includeRoot := strings.Count(searchPattern, "/") < 2 18 triplesSize := len(repoPathFileTriples) 19 20 propsQueryPart, err := buildPropsQueryPart(params.Props, params.ExcludeProps) 21 if err != nil { 22 return "", err 23 } 24 itemTypeQuery := buildItemTypeQueryPart(params) 25 nePath := buildNePathPart(triplesSize == 0 || includeRoot) 26 excludeQuery := buildExcludeQueryPart(params, triplesSize == 0 || params.Recursive, params.Recursive) 27 releaseBundle, err := buildReleaseBundleQuery(params) 28 if err != nil { 29 return "", err 30 } 31 32 json := fmt.Sprintf(`{%s"$or":[`, propsQueryPart+itemTypeQuery+nePath+excludeQuery+releaseBundle) 33 34 // Get archive search parameters 35 archivePathFilePairs := createArchiveSearchParams(params) 36 37 json += handleRepoPathFileTriples(repoPathFileTriples, archivePathFilePairs, triplesSize) + "]}" 38 return json, nil 39 } 40 41 func createArchiveSearchParams(params *ArtifactoryCommonParams) []RepoPathFile { 42 var archivePathFilePairs []RepoPathFile 43 44 if params.ArchiveEntries != "" { 45 archiveSearchPattern := prepareSearchPattern(params.ArchiveEntries, false) 46 archivePathFilePairs = createPathFilePairs("", archiveSearchPattern, true) 47 } 48 49 return archivePathFilePairs 50 } 51 52 // Handle building aql query when having PathFilePairs 53 func handleRepoPathFileTriples(pathFilePairs []RepoPathFile, archivePathFilePairs []RepoPathFile, pathPairSize int) string { 54 var query string 55 archivePathPairSize := len(archivePathFilePairs) 56 57 for i := 0; i < pathPairSize; i++ { 58 if archivePathPairSize > 0 { 59 query += handleArchiveSearch(pathFilePairs[i], archivePathFilePairs) 60 } else { 61 query += buildInnerQueryPart(pathFilePairs[i]) 62 } 63 64 if i+1 < pathPairSize { 65 query += "," 66 } 67 } 68 69 return query 70 } 71 72 // Handle building aql query including archive search 73 func handleArchiveSearch(triple RepoPathFile, archivePathFilePairs []RepoPathFile) string { 74 var query string 75 archivePathPairSize := len(archivePathFilePairs) 76 for i := 0; i < archivePathPairSize; i++ { 77 query += buildInnerArchiveQueryPart(triple, archivePathFilePairs[i].path, archivePathFilePairs[i].file) 78 79 if i+1 < archivePathPairSize { 80 query += "," 81 } 82 } 83 return query 84 } 85 86 func createAqlBodyForBuild(buildName, buildNumber string) string { 87 itemsPart := 88 `{` + 89 `"artifact.module.build.name":"%s",` + 90 `"artifact.module.build.number":"%s"` + 91 `}` 92 return fmt.Sprintf(itemsPart, buildName, buildNumber) 93 } 94 95 func createAqlQueryForBuild(buildName, buildNumber, includeQueryPart string) string { 96 queryBody := createAqlBodyForBuild(buildName, buildNumber) 97 itemsPart := `items.find(%s)%s` 98 return fmt.Sprintf(itemsPart, queryBody, includeQueryPart) 99 } 100 101 //noinspection GoUnusedExportedFunction 102 func CreateAqlQueryForNpm(npmName, npmVersion string) string { 103 itemsPart := 104 `items.find({` + 105 `"@npm.name":"%s",` + 106 `"@npm.version":"%s"` + 107 `})%s` 108 return fmt.Sprintf(itemsPart, npmName, npmVersion, buildIncludeQueryPart([]string{"name", "repo", "path", "actual_sha1", "actual_md5"})) 109 } 110 111 func CreateAqlQueryForPypi(repo, file string) string { 112 itemsPart := 113 `items.find({` + 114 `"repo": "%s",` + 115 `"$or": [{` + 116 `"$and":[{` + 117 `"path": {"$match": "*"},` + 118 `"name": {"$match": "%s"}` + 119 `}]` + 120 `}]` + 121 `})%s` 122 return fmt.Sprintf(itemsPart, repo, file, buildIncludeQueryPart([]string{"name", "repo", "path", "actual_md5", "actual_sha1"})) 123 } 124 125 func prepareSearchPattern(pattern string, repositoryExists bool) string { 126 addWildcardIfNeeded(&pattern, repositoryExists) 127 // Remove parenthesis 128 pattern = strings.Replace(pattern, "(", "", -1) 129 pattern = strings.Replace(pattern, ")", "", -1) 130 return pattern 131 } 132 133 func buildPropsQueryPart(props, excludeProps string) (string, error) { 134 propsQuery := "" 135 properties, err := ParseProperties(props, JoinCommas) 136 if err != nil { 137 return "", err 138 } 139 for _, v := range properties.Properties { 140 propsQuery += buildKeyValQueryPart(v.Key, v.Value) + `,` 141 } 142 143 excludePropsQuery := "" 144 excludeProperties, err := ParseProperties(excludeProps, JoinCommas) 145 if err != nil { 146 return "", err 147 } 148 excludePropsLen := len(excludeProperties.Properties) 149 if excludePropsLen == 1 { 150 singleProp := &excludeProperties.Properties[0] 151 excludePropsQuery = buildExcludedKeyValQueryPart(singleProp.Key, singleProp.Value) + `,` 152 } else if excludePropsLen > 1 { 153 excludePropsQuery = `"$or":[` 154 for _, v := range excludeProperties.Properties { 155 excludePropsQuery += `{` + buildExcludedKeyValQueryPart(v.Key, v.Value) + `},` 156 } 157 excludePropsQuery = strings.TrimSuffix(excludePropsQuery, ",") + `],` 158 } 159 return propsQuery + excludePropsQuery, nil 160 } 161 162 func buildKeyValQueryPart(key string, value string) string { 163 return fmt.Sprintf(`"@%s":%s`, key, getAqlValue(value)) 164 } 165 166 func buildExcludedKeyValQueryPart(key string, value string) string { 167 return fmt.Sprintf(`"@%s":{"$ne":%s}`, key, getAqlValue(value)) 168 } 169 170 func buildItemTypeQueryPart(params *ArtifactoryCommonParams) string { 171 if params.IncludeDirs { 172 return `"type":"any",` 173 } 174 return "" 175 } 176 177 func buildNePathPart(includeRoot bool) string { 178 if !includeRoot { 179 return `"path":{"$ne":"."},` 180 } 181 return "" 182 } 183 184 func buildInnerQueryPart(triple RepoPathFile) string { 185 innerQueryPattern := `{"$and":` + 186 `[{` + 187 `"repo":%s,` + 188 `"path":%s,` + 189 `"name":%s` + 190 `}]}` 191 return fmt.Sprintf(innerQueryPattern, getAqlValue(triple.repo), getAqlValue(triple.path), getAqlValue(triple.file)) 192 } 193 194 func buildInnerArchiveQueryPart(triple RepoPathFile, archivePath, archiveName string) string { 195 innerQueryPattern := `{"$and":` + 196 `[{` + 197 `"repo":%s,` + 198 `"path":%s,` + 199 `"name":%s,` + 200 `"archive.entry.path":%s,` + 201 `"archive.entry.name":%s` + 202 `}]}` 203 return fmt.Sprintf(innerQueryPattern, getAqlValue(triple.repo), getAqlValue(triple.path), getAqlValue(triple.file), getAqlValue(archivePath), getAqlValue(archiveName)) 204 } 205 206 func buildExcludeQueryPart(params *ArtifactoryCommonParams, useLocalPath, recursive bool) string { 207 excludeQuery := "" 208 var excludeTriples []RepoPathFile 209 if len(params.GetExclusions()) > 0 { 210 for _, excludePattern := range params.GetExclusions() { 211 excludeTriples = append(excludeTriples, createRepoPathFileTriples(prepareSearchPattern(excludePattern, true), recursive)...) 212 } 213 } else { 214 // Support legacy exclude patterns. 'Exclude patterns' are deprecated and replaced by 'exclusions'. 215 for _, excludePattern := range params.GetExcludePatterns() { 216 excludeTriples = append(excludeTriples, createPathFilePairs("", prepareSearchPattern(excludePattern, false), recursive)...) 217 } 218 } 219 220 for _, excludeTriple := range excludeTriples { 221 excludePath := excludeTriple.path 222 if !useLocalPath && excludePath == "." { 223 excludePath = "*" 224 } 225 excludeRepoStr := "" 226 if excludeTriple.repo != "" { 227 excludeRepoStr = fmt.Sprintf(`"repo":{"$nmatch":"%s"},`, excludeTriple.repo) 228 } 229 excludeQuery += fmt.Sprintf(`"$or":[{%s"path":{"$nmatch":"%s"},"name":{"$nmatch":"%s"}}],`, excludeRepoStr, excludePath, excludeTriple.file) 230 } 231 return excludeQuery 232 } 233 234 func buildReleaseBundleQuery(params *ArtifactoryCommonParams) (string, error) { 235 bundleName, bundleVersion, err := parseNameAndVersion(params.Bundle, false) 236 if bundleName == "" || err != nil { 237 return "", err 238 } 239 itemsPart := `"$and":` + 240 `[{` + 241 `"release_artifact.release.name":%s,` + 242 `"release_artifact.release.version":%s` + 243 `}],` 244 return fmt.Sprintf(itemsPart, getAqlValue(bundleName), getAqlValue(bundleVersion)), nil 245 } 246 247 // Creates a list of basic required return fields. The list will include the sortBy field if needed. 248 // If requiredArtifactProps is NONE or 'includePropertiesInAqlForSpec' return false, 249 // "property" field won't be included due to a limitation in the AQL implementation in Artifactory. 250 func getQueryReturnFields(specFile *ArtifactoryCommonParams, requiredArtifactProps RequiredArtifactProps) []string { 251 returnFields := []string{"name", "repo", "path", "actual_md5", "actual_sha1", "size", "type", "modified", "created"} 252 if !includePropertiesInAqlForSpec(specFile) { 253 // Sort dose not work when property is in the include section. in this case we will append properties in later stage. 254 return appendMissingFields(specFile.SortBy, returnFields) 255 } 256 if requiredArtifactProps != NONE { 257 // If any prop is needed we just add all the properties to the result. 258 return append(returnFields, "property") 259 } 260 return returnFields 261 } 262 263 // If specFile includes sortBy or limit, the produced AQL won't include property in the include section. 264 // This due to an Artifactory limitation related to using these flags with props in an AQL statement. 265 // Meaning - the result won't contain properties. 266 func includePropertiesInAqlForSpec(specFile *ArtifactoryCommonParams) bool { 267 return !(len(specFile.SortBy) > 0 || specFile.Limit > 0) 268 } 269 270 func appendMissingFields(fields []string, defaultFields []string) []string { 271 for _, field := range fields { 272 if !fileutils.IsStringInSlice(field, defaultFields) { 273 defaultFields = append(defaultFields, field) 274 } 275 } 276 return defaultFields 277 } 278 279 func prepareFieldsForQuery(fields []string) []string { 280 // Since a slice is basically a pointer, we don't want to modify the underlying fields array because it might be used again (like in delete service) 281 // We will create new slice with the quoted values and will return it. 282 var queryFields []string 283 for _, val := range fields { 284 queryFields = append(queryFields, `"`+val+`"`) 285 } 286 return queryFields 287 } 288 289 // Creates an aql query from a spec file. 290 func BuildQueryFromSpecFile(specFile *ArtifactoryCommonParams, requiredArtifactProps RequiredArtifactProps) string { 291 aqlBody := specFile.Aql.ItemsFind 292 query := fmt.Sprintf(`items.find(%s)%s`, aqlBody, buildIncludeQueryPart(getQueryReturnFields(specFile, requiredArtifactProps))) 293 query = appendSortQueryPart(specFile, query) 294 query = appendOffsetQueryPart(specFile, query) 295 return appendLimitQueryPart(specFile, query) 296 } 297 298 func appendOffsetQueryPart(specFile *ArtifactoryCommonParams, query string) string { 299 if specFile.Offset > 0 { 300 query = fmt.Sprintf(`%s.offset(%s)`, query, strconv.Itoa(specFile.Offset)) 301 } 302 return query 303 } 304 305 func appendLimitQueryPart(specFile *ArtifactoryCommonParams, query string) string { 306 if specFile.Limit > 0 { 307 query = fmt.Sprintf(`%s.limit(%s)`, query, strconv.Itoa(specFile.Limit)) 308 } 309 return query 310 } 311 312 func appendSortQueryPart(specFile *ArtifactoryCommonParams, query string) string { 313 if len(specFile.SortBy) > 0 { 314 query = fmt.Sprintf(`%s.sort({%s})`, query, buildSortQueryPart(specFile.SortBy, specFile.SortOrder)) 315 } 316 return query 317 } 318 319 func buildSortQueryPart(sortFields []string, sortOrder string) string { 320 if sortOrder == "" { 321 sortOrder = "asc" 322 } 323 return fmt.Sprintf(`"$%s":[%s]`, sortOrder, strings.Join(prepareFieldsForQuery(sortFields), `,`)) 324 } 325 326 func createPropsQuery(aqlBody, propKey, propVal string) string { 327 propKeyValQueryPart := buildKeyValQueryPart(propKey, propVal) 328 propsQuery := 329 `items.find({` + 330 `"$and":[%s,{%s}]` + 331 `})%s` 332 return fmt.Sprintf(propsQuery, aqlBody, propKeyValQueryPart, buildIncludeQueryPart([]string{"name", "repo", "path", "actual_sha1", "property"})) 333 } 334 335 func buildIncludeQueryPart(fieldsToInclude []string) string { 336 fieldsToInclude = prepareFieldsForQuery(fieldsToInclude) 337 return fmt.Sprintf(`.include(%s)`, strings.Join(fieldsToInclude, `,`)) 338 } 339 340 // Optimization - If value is a wildcard pattern, return `{"$match":"value"}`. Otherwise, return `"value"`. 341 func getAqlValue(val string) string { 342 var aqlValuePattern string 343 if strings.Contains(val, "*") { 344 aqlValuePattern = `{"$match":"%s"}` 345 } else { 346 aqlValuePattern = `"%s"` 347 } 348 return fmt.Sprintf(aqlValuePattern, val) 349 } 350 351 func prepareSourceSearchPattern(pattern, target string, repositoryExists bool) string { 352 addWildcardIfNeeded(&pattern, repositoryExists) 353 pattern = utils.RemovePlaceholderParentheses(pattern, target) 354 return pattern 355 } 356 357 func addWildcardIfNeeded(pattern *string, repositoryExists bool) { 358 if strings.HasSuffix(*pattern, "/") || (*pattern == "" && repositoryExists) { 359 *pattern += "*" 360 } 361 }