From 3c1e464f711bb03a77dc16396194f95482a438b1 Mon Sep 17 00:00:00 2001
From: tobias <thinkdifferent055@gmail.com>
Date: Fri, 27 Oct 2023 22:30:11 +0200
Subject: [PATCH] Fetch version and build code information from artifactory

---
 pom.xml                                       | 46 ++++++++++
 ...nController.java => PluginController.java} | 12 +--
 .../ArtifactoryConfigurationProperties.java   | 20 ++++
 .../ArtifactoryWebClientBean.java             | 24 +++++
 .../java/de/tobias/playwall/model/Plugin.java | 19 ++++
 .../tobias/playwall/model/PluginManifest.java | 14 +++
 .../playwall/model/artifactory/Folder.java    | 32 +++++++
 .../playwall/model/artifactory/Version.java   | 15 +++
 .../playwall/service/ArtifactoryClient.java   | 92 +++++++++++++++++++
 .../playwall/service/PluginService.java       | 35 +++++++
 .../playwall/service/VersionTokenizer.java    | 45 +++++++++
 11 files changed, 348 insertions(+), 6 deletions(-)
 rename src/main/java/de/tobias/playwall/controller/{PluginDescriptionController.java => PluginController.java} (56%)
 create mode 100644 src/main/java/de/tobias/playwall/infrastructure/ArtifactoryConfigurationProperties.java
 create mode 100644 src/main/java/de/tobias/playwall/infrastructure/ArtifactoryWebClientBean.java
 create mode 100644 src/main/java/de/tobias/playwall/model/Plugin.java
 create mode 100644 src/main/java/de/tobias/playwall/model/PluginManifest.java
 create mode 100644 src/main/java/de/tobias/playwall/model/artifactory/Folder.java
 create mode 100644 src/main/java/de/tobias/playwall/model/artifactory/Version.java
 create mode 100644 src/main/java/de/tobias/playwall/service/ArtifactoryClient.java
 create mode 100644 src/main/java/de/tobias/playwall/service/PluginService.java
 create mode 100644 src/main/java/de/tobias/playwall/service/VersionTokenizer.java

diff --git a/pom.xml b/pom.xml
index 687846f..773d7c2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,6 +23,16 @@
 			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-starter-web</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-webflux</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-configuration-processor</artifactId>
+			<optional>true</optional>
+		</dependency>
 
 		<dependency>
 			<groupId>com.fasterxml.jackson.dataformat</groupId>
@@ -53,4 +63,40 @@
 			</plugin>
 		</plugins>
 	</build>
+
+	<distributionManagement>
+		<repository>
+			<id>release</id>
+			<name>Internal-release</name>
+			<url>https://maven.thecodelabs.de/artifactory/Internal-release</url>
+		</repository>
+		<snapshotRepository>
+			<id>snapshots</id>
+			<name>Internal-snapshots</name>
+			<url>https://maven.thecodelabs.de/artifactory/Internal-snapshots</url>
+		</snapshotRepository>
+	</distributionManagement>
+
+	<repositories>
+		<repository>
+			<id>release</id>
+			<url>https://maven.thecodelabs.de/artifactory/Releases</url>
+			<releases>
+				<enabled>true</enabled>
+			</releases>
+			<snapshots>
+				<enabled>false</enabled>
+			</snapshots>
+		</repository>
+		<repository>
+			<id>snapshots</id>
+			<url>https://maven.thecodelabs.de/artifactory/Snapshots</url>
+			<releases>
+				<enabled>false</enabled>
+			</releases>
+			<snapshots>
+				<enabled>true</enabled>
+			</snapshots>
+		</repository>
+	</repositories>
 </project>
diff --git a/src/main/java/de/tobias/playwall/controller/PluginDescriptionController.java b/src/main/java/de/tobias/playwall/controller/PluginController.java
similarity index 56%
rename from src/main/java/de/tobias/playwall/controller/PluginDescriptionController.java
rename to src/main/java/de/tobias/playwall/controller/PluginController.java
index 3683f29..17868ff 100644
--- a/src/main/java/de/tobias/playwall/controller/PluginDescriptionController.java
+++ b/src/main/java/de/tobias/playwall/controller/PluginController.java
@@ -1,7 +1,7 @@
 package de.tobias.playwall.controller;
 
-import de.tobias.playwall.model.PluginDescription;
-import de.tobias.playwall.repository.PluginDescriptionRepository;
+import de.tobias.playwall.model.Plugin;
+import de.tobias.playwall.service.PluginService;
 import lombok.AllArgsConstructor;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -12,13 +12,13 @@ import java.util.List;
 @RestController
 @RequestMapping("/plugins")
 @AllArgsConstructor
-public class PluginDescriptionController
+public class PluginController
 {
-	private final PluginDescriptionRepository repository;
+	private final PluginService service;
 
 	@GetMapping
-	List<PluginDescription> getAllPlugins()
+	List<Plugin> getAllPlugins()
 	{
-		return repository.findAll();
+		return service.getAllPlugins();
 	}
 }
diff --git a/src/main/java/de/tobias/playwall/infrastructure/ArtifactoryConfigurationProperties.java b/src/main/java/de/tobias/playwall/infrastructure/ArtifactoryConfigurationProperties.java
new file mode 100644
index 0000000..1c97627
--- /dev/null
+++ b/src/main/java/de/tobias/playwall/infrastructure/ArtifactoryConfigurationProperties.java
@@ -0,0 +1,20 @@
+package de.tobias.playwall.infrastructure;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ConfigurationProperties("playwall.artifactory")
+@Getter
+@Setter
+public class ArtifactoryConfigurationProperties
+{
+	private String baseUrl;
+	private String username;
+	private String password;
+
+	private String repository;
+	private String groupId;
+}
diff --git a/src/main/java/de/tobias/playwall/infrastructure/ArtifactoryWebClientBean.java b/src/main/java/de/tobias/playwall/infrastructure/ArtifactoryWebClientBean.java
new file mode 100644
index 0000000..4880151
--- /dev/null
+++ b/src/main/java/de/tobias/playwall/infrastructure/ArtifactoryWebClientBean.java
@@ -0,0 +1,24 @@
+package de.tobias.playwall.infrastructure;
+
+import lombok.AllArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
+
+@Component
+@AllArgsConstructor
+public class ArtifactoryWebClientBean
+{
+	private final ArtifactoryConfigurationProperties configurationProperties;
+
+	@Bean
+	WebClient artifactoryWebClient()
+	{
+		return WebClient.builder()
+				.filter(basicAuthentication(configurationProperties.getUsername(), configurationProperties.getPassword()))
+				.baseUrl(configurationProperties.getBaseUrl())
+				.build();
+	}
+}
diff --git a/src/main/java/de/tobias/playwall/model/Plugin.java b/src/main/java/de/tobias/playwall/model/Plugin.java
new file mode 100644
index 0000000..ad56937
--- /dev/null
+++ b/src/main/java/de/tobias/playwall/model/Plugin.java
@@ -0,0 +1,19 @@
+package de.tobias.playwall.model;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@Builder
+public class Plugin
+{
+	private String name;
+	private String displayName;
+	private String icon;
+	private String description;
+
+	private String version;
+	private Integer build;
+}
diff --git a/src/main/java/de/tobias/playwall/model/PluginManifest.java b/src/main/java/de/tobias/playwall/model/PluginManifest.java
new file mode 100644
index 0000000..1b9f55e
--- /dev/null
+++ b/src/main/java/de/tobias/playwall/model/PluginManifest.java
@@ -0,0 +1,14 @@
+package de.tobias.playwall.model;
+
+import lombok.Data;
+
+@Data
+public class PluginManifest
+{
+	private String main;
+	private String name;
+	private String artifactId;
+	private String groupId;
+	private String version;
+	private Integer build;
+}
diff --git a/src/main/java/de/tobias/playwall/model/artifactory/Folder.java b/src/main/java/de/tobias/playwall/model/artifactory/Folder.java
new file mode 100644
index 0000000..beec1ab
--- /dev/null
+++ b/src/main/java/de/tobias/playwall/model/artifactory/Folder.java
@@ -0,0 +1,32 @@
+package de.tobias.playwall.model.artifactory;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+public class Folder
+{
+	public static final String ISO_DATE_TIME = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX";
+
+	@Data
+	public static class FolderItem {
+		private String uri;
+		private boolean folder;
+	}
+
+	private String uri;
+	private String repo;
+	private String path;
+	@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = ISO_DATE_TIME)
+	private LocalDateTime created;
+	private String createdBy;
+	@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = ISO_DATE_TIME)
+	private LocalDateTime lastModified;
+	private String modifiedBy;
+	@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = ISO_DATE_TIME)
+	private LocalDateTime lastUpdated;
+	private List<FolderItem> children;
+}
diff --git a/src/main/java/de/tobias/playwall/model/artifactory/Version.java b/src/main/java/de/tobias/playwall/model/artifactory/Version.java
new file mode 100644
index 0000000..6441dec
--- /dev/null
+++ b/src/main/java/de/tobias/playwall/model/artifactory/Version.java
@@ -0,0 +1,15 @@
+package de.tobias.playwall.model.artifactory;
+
+public record Version(int major, int minor, int patch, boolean snapshot)
+{
+	public String toVersionString()
+	{
+		String version = String.format("%d.%d.%d", major, minor, patch);
+		if(snapshot)
+		{
+			version += "-SNAPSHOT";
+		}
+		return version;
+	}
+
+}
diff --git a/src/main/java/de/tobias/playwall/service/ArtifactoryClient.java b/src/main/java/de/tobias/playwall/service/ArtifactoryClient.java
new file mode 100644
index 0000000..0680ed3
--- /dev/null
+++ b/src/main/java/de/tobias/playwall/service/ArtifactoryClient.java
@@ -0,0 +1,92 @@
+package de.tobias.playwall.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import de.tobias.playwall.infrastructure.ArtifactoryConfigurationProperties;
+import de.tobias.playwall.model.PluginDescription;
+import de.tobias.playwall.model.PluginManifest;
+import de.tobias.playwall.model.artifactory.Folder;
+import de.tobias.playwall.model.artifactory.Version;
+import jakarta.annotation.PostConstruct;
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import java.util.Comparator;
+import java.util.List;
+
+@Service
+@AllArgsConstructor
+public class ArtifactoryClient
+{
+	private final WebClient webClient;
+	private final ArtifactoryConfigurationProperties configurationProperties;
+
+	private final VersionTokenizer versionTokenizer;
+
+	private ObjectMapper mapper;
+
+	@PostConstruct
+	private void init()
+	{
+		mapper = new ObjectMapper(new YAMLFactory());
+		mapper.findAndRegisterModules();
+	}
+
+	public Version getLatestVersion(PluginDescription plugin)
+	{
+		final ResponseEntity<Folder> folderResponse = webClient.get()
+				.uri("/artifactory/api/storage/%s/%s/%s".formatted(configurationProperties.getRepository(), configurationProperties.getGroupId().replace(".", "/"), plugin.getName()))
+				.retrieve()
+				.toEntity(Folder.class)
+				.block();
+		if(folderResponse == null || folderResponse.getBody() == null)
+		{
+			return null;
+		}
+		final List<Version> version = folderResponse.getBody().getChildren().stream()
+				.filter(Folder.FolderItem::isFolder)
+				.map(child -> versionTokenizer.getVersion(child.getUri()))
+				.sorted(Comparator.comparing(Version::major).thenComparing(Version::minor).thenComparing(Version::patch))
+				.toList();
+		return version.getLast();
+	}
+
+	private record ArchiveViewSourceRequest(String archivePath, String repoKey, String sourcePath)
+	{
+	}
+
+	private record ArchiveViewSourceResponse(String source)
+	{
+	}
+
+	@SneakyThrows
+	public PluginManifest getPluginManifest(PluginDescription description, Version version)
+	{
+		final ResponseEntity<ArchiveViewSourceResponse> response = webClient.post()
+				.uri("/ui/api/v1/ui/archiveViewSource")
+				.header("X-Requested-With", "XMLHttpRequest")
+				.bodyValue(new ArchiveViewSourceRequest(
+						"%s/%s/%s/%s-%s.jar".formatted(
+								configurationProperties.getGroupId().replace(".", "/"),
+								description.getName(),
+								version.toVersionString(),
+								description.getName(),
+								version.toVersionString()
+						),
+						configurationProperties.getRepository(),
+						"plugin.yml"
+				))
+				.retrieve()
+				.toEntity(ArchiveViewSourceResponse.class)
+				.block();
+
+		if(response == null || response.getBody() == null)
+		{
+			return null;
+		}
+		return mapper.readValue(response.getBody().source(), PluginManifest.class);
+	}
+}
diff --git a/src/main/java/de/tobias/playwall/service/PluginService.java b/src/main/java/de/tobias/playwall/service/PluginService.java
new file mode 100644
index 0000000..9a1f15b
--- /dev/null
+++ b/src/main/java/de/tobias/playwall/service/PluginService.java
@@ -0,0 +1,35 @@
+package de.tobias.playwall.service;
+
+import de.tobias.playwall.model.Plugin;
+import de.tobias.playwall.model.PluginManifest;
+import de.tobias.playwall.model.artifactory.Version;
+import de.tobias.playwall.repository.PluginDescriptionRepository;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@AllArgsConstructor
+public class PluginService
+{
+	private final PluginDescriptionRepository pluginDescriptionRepository;
+	private final ArtifactoryClient artifactoryClient;
+
+	public List<Plugin> getAllPlugins()
+	{
+		return pluginDescriptionRepository.findAll().stream().map(pluginDescription -> {
+			final Version version = artifactoryClient.getLatestVersion(pluginDescription);
+			final PluginManifest manifest = artifactoryClient.getPluginManifest(pluginDescription, version);
+
+			return Plugin.builder()
+					.name(pluginDescription.getName())
+					.displayName(pluginDescription.getDisplayName())
+					.description(pluginDescription.getDescription())
+					.icon(pluginDescription.getIcon())
+					.version(version.toVersionString())
+					.build(manifest.getBuild())
+					.build();
+		}).toList();
+	}
+}
diff --git a/src/main/java/de/tobias/playwall/service/VersionTokenizer.java b/src/main/java/de/tobias/playwall/service/VersionTokenizer.java
new file mode 100644
index 0000000..893bb0c
--- /dev/null
+++ b/src/main/java/de/tobias/playwall/service/VersionTokenizer.java
@@ -0,0 +1,45 @@
+package de.tobias.playwall.service;
+
+import de.tobias.playwall.model.artifactory.Version;
+import org.springframework.stereotype.Service;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Service
+public class VersionTokenizer
+{
+	private VersionTokenizer()
+	{
+	}
+
+	public Version getVersion(String value)
+	{
+		final Pattern pattern = Pattern.compile("(\\d+).(\\d+).(\\d+)(-SNAPSHOT)?");
+		final Matcher matcher = pattern.matcher(value);
+
+		if(matcher.find())
+		{
+			final int major = Integer.parseInt(matcher.group(1));
+			final int minor = Integer.parseInt(matcher.group(2));
+			final int fix = Integer.parseInt(matcher.group(3));
+			final boolean snapshot = matcher.group(4) != null;
+
+			return new Version(major, minor, fix, snapshot);
+		}
+		return null;
+	}
+
+	public int getRevision(String value)
+	{
+		final Pattern pattern = Pattern.compile("\\w*-[\\d.]*-[\\d]{8}.[\\d]{6}-(\\d*).[\\w]*");
+		final Matcher matcher = pattern.matcher(value);
+
+		if(matcher.find())
+		{
+			return Integer.parseInt(matcher.group(1));
+		}
+		return 0;
+	}
+
+}
-- 
GitLab