Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task to update plugin and dependencies #193

Merged
merged 16 commits into from
Dec 7, 2021
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Gradle plugin to create a java and kotlin application based on Clean Architectur
- [Generate Acceptance Tests](#generate-acceptance-test)
- [Validate Structure](#validate-structure)
- [Delete Module](#delete-module)
- [Update Project](#update-project)
- [How can I help?](#how-can-i-help)
- [Whats Next?](#whats-next)

Expand Down Expand Up @@ -438,6 +439,17 @@ The **`deleteModule | dm`** task will delete a sub project, this task has one re
```

<br><br><br>

## Update Project

The **`updateCleanArchitecture | u`** task will update plugin and dependencies in all sub projects, this task has one optional parameter `dependencies`
if you only want to update some dependencies the dependency need to contain the group, and the artifact for example for the dependency **cleanArchitecture** you will need to append **co.com.bancolombia:cleanArchitecture**.

```shell
gradle updateCleanArchitecture --dependencies=[dependency1, dependency2, ...]
gradle u --dependencies=[dependency1, dependency2, ...]
```


# How can I help?

Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies {
api 'com.github.spullara.mustache.java:compiler:0.9.6'
api 'com.fasterxml.jackson.core:jackson-databind:2.11.0'
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.0'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.0'
api 'commons-io:commons-io:2.7'
api gradleApi()
testImplementation "org.mockito:mockito-core:2.9.0"
Expand All @@ -56,6 +57,8 @@ dependencies {

compileOnly 'org.projectlombok:lombok:1.18.10'
annotationProcessor 'org.projectlombok:lombok:1.18.10'
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
}

gradlePlugin {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.GradleRunner;
import org.gradle.testkit.runner.TaskOutcome;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;

/** A simple functional test for the 'co.com.bancolombia.greeting' plugin. */
public class PluginCleanFunctionalTest {
File projectDir = new File("build/functionalTest");
static File projectDir = new File("build/functionalTest");
GradleRunner runner;

@Before
Expand All @@ -41,7 +42,12 @@ public void init() throws IOException {
runner.withPluginClasspath();
}

private void deleteStructure(Path sourcePath) {
@AfterClass
public static void clean() {
deleteStructure(projectDir.toPath());
}

private static void deleteStructure(Path sourcePath) {

try {
Files.walkFileTree(
Expand Down Expand Up @@ -937,6 +943,31 @@ public void shouldGenerateMQDrivenAdapterNoReactive() {
assertEquals(result.task(":" + task).getOutcome(), TaskOutcome.SUCCESS);
}

@Test
public void shouldUpdateProject() {
canRunTaskGenerateStructureWithOutParameters();

String task = "updateCleanArchitecture";

runner.withArguments(task);
runner.withProjectDir(projectDir);
BuildResult result = runner.build();

assertEquals(result.task(":" + task).getOutcome(), TaskOutcome.SUCCESS);
}

@Test
public void shouldUpdateProjectWithOneDependency() {
canRunTaskGenerateStructureWithOutParameters();
String task = "updateCleanArchitecture";

runner.withArguments(task, "--dependencies=org.mockito:mockito-core org.projectlombok:lombok");
runner.withProjectDir(projectDir);
BuildResult result = runner.build();

assertEquals(result.task(":" + task).getOutcome(), TaskOutcome.SUCCESS);
}

private void writeString(File file, String string) throws IOException {
try (Writer writer = new FileWriter(file)) {
writer.write(string);
Expand Down
9 changes: 8 additions & 1 deletion src/main/java/co/com/bancolombia/PluginClean.java
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,14 @@ private List<TaskModel> initTasks() {
.group(Constants.PLUGIN_TASK_GROUP)
.taskAction(GenerateHelperTask.class)
.build());

tasksModels.add(
TaskModel.builder()
.name("updateCleanArchitecture")
.shortcut("u")
.description("Update project dependencies")
.group(Constants.PLUGIN_TASK_GROUP)
.taskAction(UpdateProjectTask.class)
.build());
return tasksModels;
}

Expand Down
29 changes: 28 additions & 1 deletion src/main/java/co/com/bancolombia/factory/ModuleBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.Getter;
import org.gradle.api.Project;
Expand Down Expand Up @@ -125,6 +127,26 @@ public void removeFromSettings(String module) throws IOException {
});
}

public void updateExpression(String path, String regex, String value) throws IOException {
updateFile(path, properties -> Utils.replaceExpression(properties, regex, value));
}

public Collection<? extends String> findExpressions(String path, String regex)
throws IOException {
logger.lifecycle(
"find "
+ Pattern.compile(regex).matcher(readFile(path)).results().count()
+ " dependencies in "
+ path);
return Pattern.compile(regex)
.matcher(readFile(path))
.results()
.map(MatchResult::group)
.map(s -> s.replaceAll("'", ""))
.map(s -> s.replaceAll("\"", ""))
.collect(Collectors.toList());
}

public void appendDependencyToModule(String module, String dependency) throws IOException {
logger.lifecycle("adding dependency {} to module {}", dependency, module);
String buildFilePath = project.getChildProjects().get(module).getBuildFile().getPath();
Expand Down Expand Up @@ -254,14 +276,19 @@ private Boolean getABooleanProperty(String property) {
}

private void updateFile(String path, FileUpdater updater) throws IOException {
String content = readFile(path);
addFile(path, updater.update(content));
}

private String readFile(String path) throws IOException {
FileModel current = files.get(path);
String content;
if (current == null) {
content = FileUtils.readFile(getProject(), path).collect(Collectors.joining("\n"));
} else {
content = current.getContent();
}
addFile(path, updater.update(content));
return content;
}

private ObjectNode getNode(ObjectNode node, List<String> attributes) {
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/co/com/bancolombia/models/DependencyRelease.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package co.com.bancolombia.models;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
@JsonDeserialize(using = DependencyReleasesDeserializer.class)
public class DependencyRelease {
@JsonProperty("v")
private String version;

@JsonProperty("g")
private String group;

@JsonProperty("a")
private String artifact;

@Override
public String toString() {
return String.format("%s:%s:%s", this.getGroup(), this.getArtifact(), this.getVersion());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package co.com.bancolombia.models;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import java.io.IOException;

public class DependencyReleasesDeserializer extends StdDeserializer<DependencyRelease> {

public DependencyReleasesDeserializer() {
this(null);
}

public DependencyReleasesDeserializer(Class<?> vc) {
super(vc);
}

@Override
public DependencyRelease deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {

JsonNode productNode = jp.getCodec().readTree(jp);
DependencyRelease dependencyRelease = new DependencyRelease();
dependencyRelease.setGroup(productNode.get("response").get("docs").get(0).get("g").textValue());
dependencyRelease.setArtifact(
productNode.get("response").get("docs").get(0).get("a").textValue());
dependencyRelease.setVersion(
productNode.get("response").get("docs").get(0).get("v").textValue());
return dependencyRelease;
}
}
16 changes: 16 additions & 0 deletions src/main/java/co/com/bancolombia/models/Release.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package co.com.bancolombia.models;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.OffsetDateTime;
import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class Release {
@JsonProperty("tag_name")
private String tagName;

@JsonProperty("published_at")
private OffsetDateTime publishedAt;
}
123 changes: 123 additions & 0 deletions src/main/java/co/com/bancolombia/task/UpdateProjectTask.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package co.com.bancolombia.task;

import co.com.bancolombia.models.DependencyRelease;
import co.com.bancolombia.models.Release;
import co.com.bancolombia.utils.RestConsumer;
import co.com.bancolombia.utils.Utils;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.options.Option;

public class UpdateProjectTask extends CleanArchitectureDefaultTask {
public static final String PLUGIN_RELEASES =
"https://api.github.com/repos/bancolombia/scaffold-clean-architecture/releases";
public static final String DEPENDENCY_RELEASES =
"https://search.maven.org/solrsearch/select?q=g:%22%s%22+AND+a:%22%s%22&core=gav&rows=1&wt=json";
private List<String> dependencies = new LinkedList<>();

@Option(option = "dependencies", description = "Set dependencies to update")
public void setDependencies(String dependencies) {
this.dependencies.addAll(Arrays.asList(dependencies.split("[ ,]+")));
}

@TaskAction
public void updateProject() throws IOException {
logger.lifecycle("Clean Architecture plugin version: {}", Utils.getVersionPlugin());
logger.lifecycle(
"Dependencies to update: {}",
(dependencies.isEmpty() ? "all" : String.join(", ", dependencies)));

Release latestRelease = getLatestPluginVersion();
if (latestRelease != null) {
logger.lifecycle("Latest version: {}", latestRelease.getTagName());

updatePlugin(latestRelease.getTagName());
}
updateDependencies();

builder.persist();
}

private Release getLatestPluginVersion() throws IOException {
try {
return RestConsumer.callRequest(PLUGIN_RELEASES, Release[].class)[0];
} catch (Exception e) {
logger.lifecycle("\tx Can't update the plugin " + e.getMessage());
return null;
}
}

private DependencyRelease getDependencyReleases(String dependency) throws IOException {
try {
return RestConsumer.callRequest(getDependencyEndpoint(dependency), DependencyRelease.class);
} catch (NullPointerException e) {
logger.lifecycle("\tx Can't update this dependency " + dependency);
return null;
}
}

private String getDependencyEndpoint(String dependency) {
String[] id = dependency.split(":");
if (id.length >= 2) {
return DEPENDENCY_RELEASES.replaceFirst("%s", id[0]).replace("%s", id[1]);
}
throw new IllegalArgumentException(
dependency
+ "is not a valid dependency usage: gradle u "
+ "--dependency "
+ "dependency.group:artifact");
}

private void updatePlugin(String lastRelease) throws IOException {

if (lastRelease.equals(Utils.getVersionPlugin())) {
logger.lifecycle("You are already using the latest version of the plugin");
return;
}
logger.lifecycle("Updating the plugin ");

if (builder.isKotlin()) {
builder.updateExpression(
"build.gradle.kts",
"(id\\(\"co.com.bancolombia.cleanArchitecture\"\\)\\s?version\\s?).+",
"$1\"" + lastRelease + "\"");
return;
}
builder.updateExpression(
"gradle.properties", "(systemProp.version\\s?=\\s?).+", "$1" + lastRelease);
builder.updateExpression(
"build.gradle", "(cleanArchitectureVersion\\s?=\\s?)'.+'", "$1'" + lastRelease + "'");

logger.lifecycle("Plugin updated");
}

private void updateDependencies() throws IOException {
logger.lifecycle("Updating dependencies");
List<String> gradleFiles = Utils.getAllFilesWithExtension(builder.isKotlin());

if (dependencies.isEmpty()) {
santitigaga marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that this 'if' can be removed because if the collections is empty don't do noting. the only inconvenient in when the collection is null, i don know if this case occur

// find all dependencies
for (String gradleFile : gradleFiles) {
dependencies.addAll(builder.findExpressions(gradleFile, "['\"].+:.+:[^\\$].+['\"]"));
}
}
dependencies = dependencies.stream().distinct().collect(Collectors.toList());
santitigaga marked this conversation as resolved.
Show resolved Hide resolved
logger.lifecycle(dependencies.size() + " different dependencies to update");
for (String dependency : dependencies) {
DependencyRelease latestDependency = getDependencyReleases(dependency);
santitigaga marked this conversation as resolved.
Show resolved Hide resolved
if (latestDependency != null) {
santitigaga marked this conversation as resolved.
Show resolved Hide resolved
logger.lifecycle("\t- " + latestDependency.toString());
for (String gradleFile : gradleFiles) {
builder.updateExpression(
gradleFile,
"['\"]" + dependency + ":[^\\$].+",
"'" + latestDependency.toString() + "'");
}
}
}
}
}
Loading