Skip to content

Commit

Permalink
Read gradle cache bin files to get suggetions (#35)
Browse files Browse the repository at this point in the history
Read gradle cache bin files to get suggetions
  • Loading branch information
FinlayRJW authored Oct 9, 2024
1 parent bb1240f commit 8462b26
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 9 deletions.
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-35.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: improvement
improvement:
description: Read gradle cache bin files to get suggestions
links:
- https://github.com/palantir/gradle-consistent-versions-idea-plugin/pull/35
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,23 @@

public class FolderCompletionContributor extends CompletionContributor {

public FolderCompletionContributor() {
extendCompletion(VersionPropsTypes.GROUP_PART);
private final GradleCacheExplorer gradleCacheExplorer =
new GradleCacheExplorer(List.of("https://repo.maven.apache.org/maven2/"));

extendCompletion(VersionPropsTypes.NAME_KEY);
public FolderCompletionContributor() {
cacheCompletion(VersionPropsTypes.GROUP_PART);
cacheCompletion(VersionPropsTypes.NAME_KEY);
remoteCompletion(VersionPropsTypes.GROUP_PART);
remoteCompletion(VersionPropsTypes.NAME_KEY);
}

private void extendCompletion(IElementType elementType) {
private void remoteCompletion(IElementType elementType) {
extend(CompletionType.BASIC, PlatformPatterns.psiElement(elementType), new CompletionProvider<>() {
@Override
protected void addCompletions(
CompletionParameters parameters, ProcessingContext context, CompletionResultSet resultSet) {

List<String> repositories = List.of("https://repo1.maven.org/maven2/");
List<String> repositories = List.of("https://repo.maven.apache.org/maven2/");

DependencyGroup group = DependencyGroup.groupFromParameters(parameters);

Expand All @@ -56,6 +60,21 @@ protected void addCompletions(
});
}

private void cacheCompletion(IElementType elementType) {
extend(CompletionType.BASIC, PlatformPatterns.psiElement(elementType), new CompletionProvider<>() {
@Override
protected void addCompletions(
CompletionParameters parameters, ProcessingContext context, CompletionResultSet resultSet) {

DependencyGroup group = DependencyGroup.groupFromParameters(parameters);

gradleCacheExplorer.getCompletions(group).stream()
.map(suggestion -> LookupElementBuilder.create(Folder.of(suggestion)))
.forEach(resultSet::addElement);
}
});
}

@Override
public final boolean invokeAutoPopup(PsiElement position, char typeChar) {
return typeChar == ':';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.gradle.versions.intellij;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class GradleCacheExplorer {

private static final Logger log = LoggerFactory.getLogger(GradleCacheExplorer.class);
private static final String GRADLE_CACHE_PATH = System.getProperty("user.home") + "/.gradle/caches/modules-2/";
private final Cache<String, Set<String>> cache =
Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();

private final List<String> projectUrls;

public GradleCacheExplorer(List<String> projectUrls) {
this.projectUrls = projectUrls;
}

public final Set<String> getCompletions(DependencyGroup input) {
String parsedInput = String.join(".", input.parts());
ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
if (indicator == null) {
return Collections.emptySet();
}

try {
Callable<Set<String>> task = () -> extractStrings(indicator);
Future<Set<String>> future = ApplicationManager.getApplication().executeOnPooledThread(task);
Set<String> results =
com.intellij.openapi.application.ex.ApplicationUtil.runWithCheckCanceled(future::get, indicator);

if (parsedInput.isEmpty()) {
return results;
}

return results.stream()
.filter(result -> result.startsWith(parsedInput))
.map(result -> result.substring(parsedInput.length() + 1))
.collect(Collectors.toSet());
} catch (ProcessCanceledException e) {
log.debug("Operation was cancelled", e);
} catch (Exception e) {
log.warn("Failed to get completions", e);
}
return Collections.emptySet();
}

private Set<String> extractStrings(ProgressIndicator indicator) {
return cache.get("metadata", key -> {
try (Stream<Path> allFolders = Files.list(Paths.get(GRADLE_CACHE_PATH))) {

Stream<Path> metadataFolders =
allFolders.filter(path -> path.getFileName().toString().startsWith("metadata-"));

return metadataFolders
.peek(metadataFolder -> {
if (indicator.isCanceled()) {
throw new RuntimeException(
new InterruptedException("Operation was canceled by the user."));
}
})
.map(metadataFolder -> metadataFolder.resolve("resource-at-url.bin"))
.filter(Files::exists)
.flatMap(this::extractStringsFromBinFile)
.filter(this::isValidResourceUrl)
.map(this::extractGroupAndArtifactFromUrl)
.flatMap(Optional::stream)
.collect(Collectors.toSet());
} catch (IOException e) {
log.error("Failed to list metadata folders", e);
return Collections.emptySet();
} catch (RuntimeException e) {
if (e.getCause() instanceof InterruptedException) {
log.debug("Operation was cancelled", e);
return Collections.emptySet();
}
throw e;
}
});
}

final boolean isValidResourceUrl(String url) {
return projectUrls.stream().anyMatch(url::startsWith) && (url.endsWith(".pom") || url.endsWith(".jar"));
}

final Stream<String> extractStringsFromBinFile(Path binFile) {
Set<String> result = new HashSet<>();
try (BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(binFile))) {
StringBuilder currentString = new StringBuilder();
int byteValue;

while ((byteValue = bis.read()) != -1) {
char charValue = (char) byteValue;
if (!Character.isISOControl(charValue)) {
currentString.append(charValue);
continue;
}
if (!currentString.isEmpty()) {
result.add(currentString.toString());
currentString.setLength(0);
}
}

if (!currentString.isEmpty()) {
result.add(currentString.toString());
}
} catch (IOException e) {
log.error("Failed to extract strings from bin file", e);
}
return result.stream();
}

/**
* Extracts the group and artifact identifiers from a given maven2 layout URL.
*
* <p>The method removes the base project URL from the input if it matches any in a predefined list,
* then converts the remaining path to the format "group:artifact".
*
* <p>Example: For the URL "http://example.com/org/example/project/1.0/project-1.0.jar"
* and "http://example.com/" in the list of projectUrls, it returns "org.example.project:project".
*
* @param url the URL to process
* @return an {@link Optional} containing a string in the format "group:artifact" if extraction is successful,
* or {@link Optional#empty()} if no matching project URL is found or the URL does not have the expected structure.
*/
Optional<String> extractGroupAndArtifactFromUrl(String url) {
return projectUrls.stream().filter(url::startsWith).findFirst().flatMap(projectUrl -> {
String mavenLayout = url.substring(projectUrl.length());

int lastSlashIndex = mavenLayout.lastIndexOf('/');
if (lastSlashIndex == -1) {
return Optional.empty();
}

int secondLastSlashIndex = mavenLayout.lastIndexOf('/', lastSlashIndex - 1);
if (secondLastSlashIndex == -1) {
return Optional.empty();
}

int thirdLastSlashIndex = mavenLayout.lastIndexOf('/', secondLastSlashIndex - 1);
if (thirdLastSlashIndex == -1) {
return Optional.empty();
}

String group = mavenLayout.substring(0, thirdLastSlashIndex).replace('/', '.');
String artifact = mavenLayout.substring(thirdLastSlashIndex + 1, secondLastSlashIndex);

return Optional.of(String.format("%s:%s", group, artifact));
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ private List<Folder> loadFolders(DependencyGroup group) {
return new ArrayList<>();
}

return fetchFoldersFromUrl(content.get());
return fetchFoldersFromContent(content.get());
}

public final List<DependencyVersion> getVersions(DependencyGroup group, DependencyName dependencyPackage) {
Expand All @@ -76,7 +76,7 @@ public final List<DependencyVersion> getVersions(DependencyGroup group, Dependen
return new ArrayList<>();
}

return parseVersionsFromMetadata(content.get());
return parseVersionsFromContent(content.get());
}

private Optional<String> fetchContent(String urlString) {
Expand All @@ -89,7 +89,7 @@ private Optional<String> fetchContent(String urlString) {
}
}

private List<Folder> fetchFoldersFromUrl(String contents) {
private List<Folder> fetchFoldersFromContent(String contents) {
List<Folder> folders = new ArrayList<>();

Document doc = Jsoup.parse(contents);
Expand All @@ -104,7 +104,7 @@ private List<Folder> fetchFoldersFromUrl(String contents) {
return folders;
}

private List<DependencyVersion> parseVersionsFromMetadata(String content) {
private List<DependencyVersion> parseVersionsFromContent(String content) {
List<DependencyVersion> versions = new ArrayList<>();
try {
XmlMapper xmlMapper = new XmlMapper();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.gradle.versions.intellij;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

class GradleCacheExplorerTest {

private GradleCacheExplorer explorer;

@BeforeEach
void beforeEach() {
List<String> projectUrls = List.of("https://repo.maven.apache.org/maven2/", "https://jcenter.bintray.com/");
explorer = new GradleCacheExplorer(projectUrls);
}

@Test
void test_gets_valid_urls_only() {
Set<String> projectUrls = Set.of("https://repo.maven.apache.org/maven2/", "https://jcenter.bintray.com/");

assertThat(explorer.isValidResourceUrl(
"https://repo.maven.apache.org/maven2/com/example/artifact/1.0/artifact-1.0.pom"))
.as("because the URL is from a known valid repository and ends with .pom")
.isTrue();

assertThat(explorer.isValidResourceUrl("https://jcenter.bintray.com/com/example/artifact/1.0/artifact-1.0.jar"))
.as("because the URL is from a known valid repository and ends with .jar")
.isTrue();

assertThat(explorer.isValidResourceUrl("https://example.com/com/example/artifact/1.0/artifact-1.0.pom"))
.as("because the URL is not from a known valid repository")
.isFalse();

assertThat(explorer.isValidResourceUrl(
"https://repo.maven.apache.org/maven2/com/example/artifact/1.0/artifact-1.0.txt"))
.as("because the URL ends with an invalid extension")
.isFalse();
}

@Test
void test_gets_all_strings_from_bin(@TempDir File tempDir) throws IOException {
File tempFile = new File(tempDir, "test.bin");
try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile, StandardCharsets.UTF_8))) {
writer.write("hello.jar\nworld.pom\nanother.jar\b\f");
}

Stream<String> result = explorer.extractStringsFromBinFile(tempFile.toPath());
List<String> resultList = result.collect(Collectors.toList());

assertThat(resultList)
.as("because the file contains these specific strings")
.containsOnly("hello.jar", "world.pom", "another.jar");
}

@Test
void test_extract_group_artifact_from_url_correctly() {

assertThat(explorer.extractGroupAndArtifactFromUrl(
"https://repo.maven.apache.org/maven2/com/example/artifact/1.0/artifact-1.0.pom")
.get())
.as("because the URL should be parsed into group and artifact")
.isEqualTo("com.example:artifact");

assertThat(explorer.extractGroupAndArtifactFromUrl(
"https://jcenter.bintray.com/com/example/artifact/1.0/artifact-1.0.jar")
.get())
.as("because the URL should be parsed into group and artifact")
.isEqualTo("com.example:artifact");
assertThat(explorer.extractGroupAndArtifactFromUrl(
"https://not.vaild.com/example/artifact/1.0/artifact-1.0.jar"))
.as("Expected the URL to not match any project URL, resulting in an empty Optional")
.isEmpty();
assertThat(explorer.extractGroupAndArtifactFromUrl("https://jcenter.bintray.com/com/example"))
.as("Could not find second to last slash, resulting in an empty Optional")
.isEmpty();
assertThat(explorer.extractGroupAndArtifactFromUrl(""))
.as("Empty passed in so empty returned")
.isEmpty();
}
}

0 comments on commit 8462b26

Please sign in to comment.