From 5d2eff8d071342caa8dd218af87835c65fa75634 Mon Sep 17 00:00:00 2001 From: Hendrik Ebbers Date: Mon, 19 May 2025 09:21:06 +0200 Subject: [PATCH 01/17] openAI fix Signed-off-by: Daniel Ntege --- .../openai/OpenAiBasedConductChecker.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/openelements/conduct/integration/openai/OpenAiBasedConductChecker.java b/src/main/java/com/openelements/conduct/integration/openai/OpenAiBasedConductChecker.java index 5cfe033..6327334 100644 --- a/src/main/java/com/openelements/conduct/integration/openai/OpenAiBasedConductChecker.java +++ b/src/main/java/com/openelements/conduct/integration/openai/OpenAiBasedConductChecker.java @@ -88,7 +88,7 @@ The message has the title (can be null and should be ignored than): %s @Override public @NonNull CheckResult check(@NonNull final Message message) { final String prompt = createPrompt(message); - final JsonNode jsonNode = calOpenAIEndpoint(prompt); + final JsonNode jsonNode = callOpenAIEndpoint(endpoint, prompt); final String result = jsonNode.get("result").asText(); final String reason = jsonNode.get("reason").asText(); final ViolationState violationState = ViolationState.valueOf(result); @@ -100,7 +100,8 @@ The message has the title (can be null and should be ignored than): %s } @Nullable - private JsonNode calOpenAIEndpoint(@NotNull final String prompt) { + private JsonNode callOpenAIEndpoint(@NotNull final String url, @NotNull final String prompt) { + Objects.requireNonNull(url, "url must not be null"); Objects.requireNonNull(prompt, "prompt must not be null"); try { final ObjectMapper objectMapper = new ObjectMapper(); @@ -118,7 +119,7 @@ private JsonNode calOpenAIEndpoint(@NotNull final String prompt) { final HttpClient httpClient = HttpClient.newBuilder().version(Version.HTTP_1_1) .build(); final HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(endpoint)) + .uri(URI.create(url)) .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) .header(HttpHeaders.CONTENT_TYPE, "application/json") .POST(HttpRequest.BodyPublishers.ofString(requestNode.toString())) @@ -126,6 +127,12 @@ private JsonNode calOpenAIEndpoint(@NotNull final String prompt) { final HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); final String responseBody = response.body(); log.debug("Response from OpenAI API: {}", responseBody); + if (response.statusCode() == 307) { + final String location = response.headers().firstValue("Location") + .orElseThrow(() -> new IllegalStateException("No Location header found in 307 response")); + log.info("Received 307 redirect from OpenAI API. Redirecting to: {}", location); + return callOpenAIEndpoint(location, prompt); + } if (response.statusCode() != 200) { throw new IllegalStateException("Error calling OpenAI API: " + responseBody); } From d43aabc34ad2db189e177da5b0ddcb007e484546 Mon Sep 17 00:00:00 2001 From: Hendrik Ebbers Date: Mon, 19 May 2025 09:38:43 +0200 Subject: [PATCH 02/17] prompt as file Signed-off-by: Daniel Ntege --- .../openai/OpenAiBasedConductChecker.java | 27 +++++++++---------- .../conduct/integration/openai/prompt.txt | 16 +++++++++++ 2 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 src/main/resources/com/openelements/conduct/integration/openai/prompt.txt diff --git a/src/main/java/com/openelements/conduct/integration/openai/OpenAiBasedConductChecker.java b/src/main/java/com/openelements/conduct/integration/openai/OpenAiBasedConductChecker.java index 6327334..cb340d3 100644 --- a/src/main/java/com/openelements/conduct/integration/openai/OpenAiBasedConductChecker.java +++ b/src/main/java/com/openelements/conduct/integration/openai/OpenAiBasedConductChecker.java @@ -10,6 +10,7 @@ import com.openelements.conduct.data.Message; import com.openelements.conduct.data.TextfileType; import com.openelements.conduct.data.ViolationState; +import java.io.InputStream; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Version; @@ -39,6 +40,8 @@ public class OpenAiBasedConductChecker implements ConductChecker { private final String model; + private final String prompt; + public OpenAiBasedConductChecker(@NonNull final String endpoint, @NonNull final String apiKey, @NonNull final String model, @@ -60,26 +63,22 @@ public OpenAiBasedConductChecker(@NonNull final String endpoint, .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); + + try (final InputStream inputStream = OpenAiBasedConductChecker.class.getResourceAsStream("prompt.txt")) { + if (inputStream == null) { + throw new IllegalStateException("Prompt file not found"); + } + this.prompt = new String(inputStream.readAllBytes()); + } catch (Exception e) { + throw new RuntimeException("Error loading prompt file", e); + } } private String createPrompt(@NonNull Message message) { Objects.requireNonNull(message, "message must not be null"); if (codeOfConductProvider.supports(TextfileType.MARKDOWN)) { String codeOfConduct = codeOfConductProvider.getCodeOfConduct(TextfileType.MARKDOWN); - return """ - You are a code of conduct checker for an open source project. - Your task is to check if the following message violates the code of conduct of the project. - - Your answer must be in a JSON format with the following fields: - - result: the result of the check. Allowed values are NONE, POSSIBLE_VIOLATION or VIOLATION - - reason: a short text explaining the result - - The message has the title (can be null and should be ignored than): %s - The message has the text: %s - - The code of conduct is: - %s - """.formatted(message.title(), message.message(), codeOfConduct); + return prompt.formatted(message.title(), message.message(), codeOfConduct); } else { throw new UnsupportedOperationException("Not implemented yet other texttype than markdown."); } diff --git a/src/main/resources/com/openelements/conduct/integration/openai/prompt.txt b/src/main/resources/com/openelements/conduct/integration/openai/prompt.txt new file mode 100644 index 0000000..217b3af --- /dev/null +++ b/src/main/resources/com/openelements/conduct/integration/openai/prompt.txt @@ -0,0 +1,16 @@ +You are an automated code of conduct reviewer for an open source project. + +Your task is to analyze the following message and determine whether it violates the project’s code of conduct. + +Please respond only in valid JSON format with the following fields: +- result: One of the following values: "NONE" (no violation), "POSSIBLE_VIOLATION" (potential concern), or "VIOLATION" (clear violation). +- reason: A brief explanation (1–3 sentences) justifying your result. + +Ignore the message title if it is null. + +Message: +Title: %s +Text: %s + +Code of Conduct: +%s From 1206a1174c21c909856666845c1fe420c209a5a1 Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Sun, 1 Jun 2025 14:36:18 +0300 Subject: [PATCH 03/17] Update GitHub Code of Conduct provider configuration Signed-off-by: Daniel Ntege --- src/main/resources/application.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ae2c918..40827bb 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -21,6 +21,6 @@ guardian.integration.coc.file.enabled=false guardian.integration.log.enabled=true # GitHub Code of Conduct Provider Configuration guardian.integration.github.coc.enabled=${GITHUB_COC_ENABLED:true} -guardian.integration.github.coc.owner=${GITHUB_COC_OWNER:OpenElements} -guardian.integration.github.coc.repo=${GITHUB_COC_REPO:Conduct-Guardian} +guardian.integration.github.coc.owner=${GITHUB_COC_OWNER:ClyCites} +guardian.integration.github.coc.repo=${GITHUB_COC_REPO:ClyCites-Frontend} From 0f93651cf1f2907cea95e81cf2ea37cfb78b489f Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Sun, 1 Jun 2025 15:23:20 +0300 Subject: [PATCH 04/17] Refactor GitHub integration: streamline Code of Conduct file handling and add GitHubClient implementation Signed-off-by: Daniel Ntege --- .../integration/github/GitHubClient.java | 7 ++ .../integration/github/GitHubClientImpl.java | 40 +++++++++++ .../github/GitHubCodeOfConductProvider.java | 66 ++++++------------- .../integration/github/GitHubConfig.java | 4 +- src/main/resources/application.properties | 1 + 5 files changed, 70 insertions(+), 48 deletions(-) create mode 100644 src/main/java/com/openelements/conduct/integration/github/GitHubClient.java create mode 100644 src/main/java/com/openelements/conduct/integration/github/GitHubClientImpl.java diff --git a/src/main/java/com/openelements/conduct/integration/github/GitHubClient.java b/src/main/java/com/openelements/conduct/integration/github/GitHubClient.java new file mode 100644 index 0000000..8fda3bf --- /dev/null +++ b/src/main/java/com/openelements/conduct/integration/github/GitHubClient.java @@ -0,0 +1,7 @@ +package com.openelements.conduct.integration.github; + +import com.fasterxml.jackson.databind.JsonNode; + +public interface GitHubClient { + JsonNode getRepositoryFileContent(String owner, String repo, String path, String branch) throws Exception; +} diff --git a/src/main/java/com/openelements/conduct/integration/github/GitHubClientImpl.java b/src/main/java/com/openelements/conduct/integration/github/GitHubClientImpl.java new file mode 100644 index 0000000..f3d8e10 --- /dev/null +++ b/src/main/java/com/openelements/conduct/integration/github/GitHubClientImpl.java @@ -0,0 +1,40 @@ +package com.openelements.conduct.integration.github; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class GitHubClientImpl implements GitHubClient { + + private final RestTemplate restTemplate = new RestTemplate(); + private final ObjectMapper objectMapper = new ObjectMapper(); + private final String githubToken; + + public GitHubClientImpl(@Value("${guardian.integration.github.coc.token:}") String githubToken) { + this.githubToken = githubToken; + } + + @Override + public JsonNode getRepositoryFileContent(String owner, String repo, String path, String branch) throws Exception { + String url = String.format("https://api.github.com/repos/%s/%s/contents/%s?ref=%s", owner, repo, path, branch); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Accept", "application/vnd.github.v3+json"); + if (githubToken != null && !githubToken.isBlank()) { + headers.setBearerAuth(githubToken); + } + + HttpEntity request = new HttpEntity<>(headers); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, request, String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + return objectMapper.readTree(response.getBody()); + } + + throw new RuntimeException("Failed to fetch file from GitHub: " + response.getStatusCode()); + } +} diff --git a/src/main/java/com/openelements/conduct/integration/github/GitHubCodeOfConductProvider.java b/src/main/java/com/openelements/conduct/integration/github/GitHubCodeOfConductProvider.java index 6fa866c..085abf4 100644 --- a/src/main/java/com/openelements/conduct/integration/github/GitHubCodeOfConductProvider.java +++ b/src/main/java/com/openelements/conduct/integration/github/GitHubCodeOfConductProvider.java @@ -16,85 +16,59 @@ public class GitHubCodeOfConductProvider implements CodeOfConductProvider { private static final Logger log = LoggerFactory.getLogger(GitHubCodeOfConductProvider.class); private static final String[] COMMON_COC_FILENAMES = { - "CODE_OF_CONDUCT.md", - "CODE_OF_CONDUCT.txt", - "CODE_OF_CONDUCT", - "CODE-OF-CONDUCT.md", - "code-of-conduct.md", - "code_of_conduct.md", - "CONDUCT.md", - "CONDUCT.txt" + "CODE_OF_CONDUCT.md", "CODE_OF_CONDUCT.txt", "CODE_OF_CONDUCT", + "CODE-OF-CONDUCT.md", "code-of-conduct.md", "code_of_conduct.md", + "CONDUCT.md", "CONDUCT.txt" }; private final String codeOfConduct; public GitHubCodeOfConductProvider(@NonNull GitHubClient gitHubClient, - @NonNull String owner, - @NonNull String repo) { + @NonNull String owner, + @NonNull String repo) { log.info("Initialized GitHub Code of Conduct provider for {}/{}", owner, repo); - codeOfConduct = findCodeOfConduct(gitHubClient, owner, repo); + this.codeOfConduct = findCodeOfConduct(gitHubClient, owner, repo); } private String findCodeOfConduct(@NonNull GitHubClient gitHubClient, - @NonNull String owner, - @NonNull String repo) { + @NonNull String owner, + @NonNull String repo) { return findCodeOfConduct(gitHubClient, owner, repo, "main") .or(() -> findCodeOfConduct(gitHubClient, owner, repo, "master")) + .or(() -> findCodeOfConduct(gitHubClient, owner, repo, "staging")) .or(() -> findCodeOfConduct(gitHubClient, owner, ".github", "main")) .or(() -> findCodeOfConduct(gitHubClient, owner, ".github", "master")) - .orElseThrow(() -> new RuntimeException("No code of conduct found for " + owner + " " + repo)); + .orElseThrow(() -> new RuntimeException("No Code of Conduct file found for " + owner + "/" + repo)); } - private Optional findCodeOfConduct(@NonNull GitHubClient gitHubClient, - @NonNull String owner, - @NonNull String repo, @NonNull String branch) { - Objects.requireNonNull(gitHubClient, "gitHubClient must not be null"); - Objects.requireNonNull(owner, "owner must not be null"); - Objects.requireNonNull(repo, "repo must not be null"); - Objects.requireNonNull(branch, "branch must not be null"); - if (owner.isBlank()) { - throw new IllegalArgumentException("owner must not be blank"); - } - if (repo.isBlank()) { - throw new IllegalArgumentException("repo must not be blank"); - } - if (branch.isBlank()) { - throw new IllegalArgumentException("branch must not be blank"); - } + private Optional findCodeOfConduct(GitHubClient client, String owner, String repo, String branch) { try { - for (final String filename : COMMON_COC_FILENAMES) { + for (String filename : COMMON_COC_FILENAMES) { try { - final JsonNode fileContent = gitHubClient.getRepositoryFileContent(owner, repo, filename, branch); - if (fileContent != null && fileContent.has("content")) { - log.info("Code of Conduct file {} found in {}/{}#{}", filename, owner, repo, branch); - final String content = fileContent.get("content").asText(); - final String decodedContent = new String(Base64.getDecoder().decode(content.replace("\n", ""))); - log.debug("Code of Conduct file {} decoded content: {}", filename, decodedContent); - return Optional.of(decodedContent); - } else { - log.debug("Code of Conduct file {} not found in {}/{}#{}", filename, owner, repo, branch); + JsonNode file = client.getRepositoryFileContent(owner, repo, filename, branch); + if (file != null && file.has("content")) { + String encoded = file.get("content").asText().replace("\n", ""); + String decoded = new String(Base64.getDecoder().decode(encoded)); + log.info("Found Code of Conduct file '{}' in {}/{}#{}", filename, owner, repo, branch); + return Optional.of(decoded); } } catch (Exception e) { - log.info("Code of Conduct file {} not found in {}/{}#{}", filename, owner, repo, branch); + log.debug("Could not fetch file {} in {}/{}#{}", filename, owner, repo, branch); } } } catch (Exception e) { - throw new RuntimeException("Failed to fetch Code of Conduct from GitHub", e); + log.error("Failed to search for Code of Conduct file in {}/{}#{}", owner, repo, branch, e); } return Optional.empty(); } - @Override public boolean supports(@NonNull TextfileType type) { - Objects.requireNonNull(type, "type must not be null"); return true; } @Override public @NonNull String getCodeOfConduct(@NonNull TextfileType type) { - Objects.requireNonNull(type, "type must not be null"); return codeOfConduct; } - } diff --git a/src/main/java/com/openelements/conduct/integration/github/GitHubConfig.java b/src/main/java/com/openelements/conduct/integration/github/GitHubConfig.java index 9485995..724dbcd 100644 --- a/src/main/java/com/openelements/conduct/integration/github/GitHubConfig.java +++ b/src/main/java/com/openelements/conduct/integration/github/GitHubConfig.java @@ -21,10 +21,10 @@ public class GitHubConfig { @Value("${guardian.integration.github.coc.repo:Conduct-Guardian}") private String repo; - + @Bean @Primary - CodeOfConductProvider githubCodeOfConductProvider(GitHubClient gitHubClient) { + public CodeOfConductProvider githubCodeOfConductProvider(GitHubClient gitHubClient) { return new GitHubCodeOfConductProvider(gitHubClient, owner, repo); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 40827bb..d0f4c4a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -23,4 +23,5 @@ guardian.integration.log.enabled=true guardian.integration.github.coc.enabled=${GITHUB_COC_ENABLED:true} guardian.integration.github.coc.owner=${GITHUB_COC_OWNER:ClyCites} guardian.integration.github.coc.repo=${GITHUB_COC_REPO:ClyCites-Frontend} +guardian.integration.github.coc.token=${GITHUB_TOKEN} From 5edca0a733fe3d51937d1599ec8ac594fdf2c157 Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Sun, 1 Jun 2025 16:18:29 +0300 Subject: [PATCH 05/17] Add ViolationReport and ViolationReportRepository classes with CRUD operations for violation reports Signed-off-by: Daniel Ntege --- .../conduct/repository/ViolationReport.java | 56 ++++++++++++++++ .../repository/ViolationReportRepository.java | 66 +++++++++++++++++++ .../conduct/service/AnalysisService.java | 5 ++ .../service/ViolationReportService.java | 5 ++ 4 files changed, 132 insertions(+) create mode 100644 src/main/java/com/openelements/conduct/repository/ViolationReport.java create mode 100644 src/main/java/com/openelements/conduct/repository/ViolationReportRepository.java create mode 100644 src/main/java/com/openelements/conduct/service/AnalysisService.java create mode 100644 src/main/java/com/openelements/conduct/service/ViolationReportService.java diff --git a/src/main/java/com/openelements/conduct/repository/ViolationReport.java b/src/main/java/com/openelements/conduct/repository/ViolationReport.java new file mode 100644 index 0000000..c8beab7 --- /dev/null +++ b/src/main/java/com/openelements/conduct/repository/ViolationReport.java @@ -0,0 +1,56 @@ +package com.openelements.conduct.repository; + +import com.openelements.conduct.data.ViolationState; +import org.jspecify.annotations.NonNull; + +import java.net.URI; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.UUID; + +public class ViolationReport { + private final String id; + private final String messageTitle; + private final String messageContent; + private final URI messageUrl; + private final ViolationState violationState; + private final String reason; + private final LocalDateTime timestamp; + private final String severity; + + public ViolationReport(@NonNull String messageTitle, @NonNull String messageContent, + @NonNull URI messageUrl, @NonNull ViolationState violationState, + @NonNull String reason) { + this.id = UUID.randomUUID().toString(); + this.messageTitle = Objects.requireNonNull(messageTitle, "messageTitle must not be null"); + this.messageContent = Objects.requireNonNull(messageContent, "messageContent must not be null"); + this.messageUrl = Objects.requireNonNull(messageUrl, "messageUrl must not be null"); + this.violationState = Objects.requireNonNull(violationState, "violationState must not be null"); + this.reason = Objects.requireNonNull(reason, "reason must not be null"); + this.timestamp = LocalDateTime.now(); + this.severity = determineSeverity(violationState, reason); + } + + private String determineSeverity(ViolationState state, String reason) { + if (state == ViolationState.VIOLATION) { + if (reason.toLowerCase().contains("severe") || reason.toLowerCase().contains("harassment")) { + return "HIGH"; + } else if (reason.toLowerCase().contains("moderate") || reason.toLowerCase().contains("inappropriate")) { + return "MEDIUM"; + } else { + return "LOW"; + } + } + return "NONE"; + } + + // Getters + public String getId() { return id; } + public String getMessageTitle() { return messageTitle; } + public String getMessageContent() { return messageContent; } + public URI getMessageUrl() { return messageUrl; } + public ViolationState getViolationState() { return violationState; } + public String getReason() { return reason; } + public LocalDateTime getTimestamp() { return timestamp; } + public String getSeverity() { return severity; } +} diff --git a/src/main/java/com/openelements/conduct/repository/ViolationReportRepository.java b/src/main/java/com/openelements/conduct/repository/ViolationReportRepository.java new file mode 100644 index 0000000..eabac1a --- /dev/null +++ b/src/main/java/com/openelements/conduct/repository/ViolationReportRepository.java @@ -0,0 +1,66 @@ +package com.openelements.conduct.repository; + +import com.openelements.conduct.data.ViolationState; +import org.jspecify.annotations.NonNull; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.stream.Collectors; + +@Repository +public class ViolationReportRepository { + + private static final int MAX_REPORTS = 1000; + private final ConcurrentLinkedDeque reports = new ConcurrentLinkedDeque<>(); + + public void save(@NonNull ViolationReport report) { + Objects.requireNonNull(report, "report must not be null"); + + synchronized (reports) { + reports.addFirst(report); + + // Maintain size limit + while (reports.size() > MAX_REPORTS) { + reports.removeLast(); + } + } + } + + public List findAll() { + return new ArrayList<>(reports); + } + + public List findByViolationState(@NonNull ViolationState state) { + return reports.stream() + .filter(report -> report.getViolationState() == state) + .collect(Collectors.toList()); + } + + public List findBySeverity(@NonNull String severity) { + return reports.stream() + .filter(report -> report.getSeverity().equals(severity)) + .collect(Collectors.toList()); + } + + public List findByDateRange(@NonNull LocalDateTime start, @NonNull LocalDateTime end) { + return reports.stream() + .filter(report -> !report.getTimestamp().isBefore(start) && !report.getTimestamp().isAfter(end)) + .collect(Collectors.toList()); + } + + public Optional findById(@NonNull String id) { + return reports.stream() + .filter(report -> report.getId().equals(id)) + .findFirst(); + } + + public long count() { + return reports.size(); + } + + public void clear() { + reports.clear(); + } +} diff --git a/src/main/java/com/openelements/conduct/service/AnalysisService.java b/src/main/java/com/openelements/conduct/service/AnalysisService.java new file mode 100644 index 0000000..311fb5e --- /dev/null +++ b/src/main/java/com/openelements/conduct/service/AnalysisService.java @@ -0,0 +1,5 @@ +package com.openelements.conduct.service; + +public class AnalysisService { + +} diff --git a/src/main/java/com/openelements/conduct/service/ViolationReportService.java b/src/main/java/com/openelements/conduct/service/ViolationReportService.java new file mode 100644 index 0000000..b793c9f --- /dev/null +++ b/src/main/java/com/openelements/conduct/service/ViolationReportService.java @@ -0,0 +1,5 @@ +package com.openelements.conduct.service; + +public class ViolationReportService { + +} From 0e34a74325a7501d6cd97dc89fa1029d532bae2a Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Sun, 1 Jun 2025 16:23:47 +0300 Subject: [PATCH 06/17] Implement AnalysisService and ViolationReportService with analysis and reporting functionalities; add AnalysisController, ConfigurationController, and ViolationReportController skeletons. Signed-off-by: Daniel Ntege --- .../controller/AnalysisController.java | 5 + .../controller/ConfigurationController.java | 5 + .../controller/ViolationReportController.java | 5 + .../conduct/service/AnalysisService.java | 150 +++++++++++++++++- .../service/ViolationReportService.java | 113 ++++++++++++- 5 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/openelements/conduct/controller/AnalysisController.java create mode 100644 src/main/java/com/openelements/conduct/controller/ConfigurationController.java create mode 100644 src/main/java/com/openelements/conduct/controller/ViolationReportController.java diff --git a/src/main/java/com/openelements/conduct/controller/AnalysisController.java b/src/main/java/com/openelements/conduct/controller/AnalysisController.java new file mode 100644 index 0000000..84ea273 --- /dev/null +++ b/src/main/java/com/openelements/conduct/controller/AnalysisController.java @@ -0,0 +1,5 @@ +package com.openelements.conduct.controller; + +public class AnalysisController { + +} diff --git a/src/main/java/com/openelements/conduct/controller/ConfigurationController.java b/src/main/java/com/openelements/conduct/controller/ConfigurationController.java new file mode 100644 index 0000000..a120ce9 --- /dev/null +++ b/src/main/java/com/openelements/conduct/controller/ConfigurationController.java @@ -0,0 +1,5 @@ +package com.openelements.conduct.controller; + +public class ConfigurationController { + +} diff --git a/src/main/java/com/openelements/conduct/controller/ViolationReportController.java b/src/main/java/com/openelements/conduct/controller/ViolationReportController.java new file mode 100644 index 0000000..a64aa7c --- /dev/null +++ b/src/main/java/com/openelements/conduct/controller/ViolationReportController.java @@ -0,0 +1,5 @@ +package com.openelements.conduct.controller; + +public class ViolationReportController { + +} diff --git a/src/main/java/com/openelements/conduct/service/AnalysisService.java b/src/main/java/com/openelements/conduct/service/AnalysisService.java index 311fb5e..0454e47 100644 --- a/src/main/java/com/openelements/conduct/service/AnalysisService.java +++ b/src/main/java/com/openelements/conduct/service/AnalysisService.java @@ -1,5 +1,153 @@ package com.openelements.conduct.service; +import com.openelements.conduct.api.dto.AnalysisDto; +import com.openelements.conduct.api.dto.TrendAnalysis; +import com.openelements.conduct.repository.ViolationReport; +import com.openelements.conduct.repository.ViolationReportRepository; +import org.jspecify.annotations.NonNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +@Service public class AnalysisService { - + + private final ViolationReportRepository repository; + + @Autowired + public AnalysisService(@NonNull ViolationReportRepository repository) { + this.repository = Objects.requireNonNull(repository, "repository must not be null"); + } + + public AnalysisDto generateAnalysis() { + List reports = repository.findAll(); + + if (reports.isEmpty()) { + return createEmptyAnalysis(); + } + + Map violationsByState = reports.stream() + .collect(Collectors.groupingBy( + report -> report.getViolationState().toString(), + Collectors.counting() + )); + + Map violationsBySeverity = reports.stream() + .collect(Collectors.groupingBy( + ViolationReport::getSeverity, + Collectors.counting() + )); + + Map violationsByHour = reports.stream() + .collect(Collectors.groupingBy( + report -> String.valueOf(report.getTimestamp().getHour()), + Collectors.counting() + )); + + Map violationsByDay = reports.stream() + .collect(Collectors.groupingBy( + report -> report.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), + Collectors.counting() + )); + + double averageViolationsPerDay = calculateAverageViolationsPerDay(reports); + String mostCommonViolationType = findMostCommonViolationType(violationsByState); + TrendAnalysis trends = analyzeTrends(reports); + + return new AnalysisDto( + violationsByState, + violationsBySeverity, + violationsByHour, + violationsByDay, + averageViolationsPerDay, + mostCommonViolationType, + LocalDateTime.now(), + reports.size(), + trends + ); + } + + private AnalysisDto createEmptyAnalysis() { + return new AnalysisDto( + Map.of(), + Map.of(), + Map.of(), + Map.of(), + 0.0, + "No violations", + LocalDateTime.now(), + 0L, + new TrendAnalysis("STABLE", 0.0, "No data available for trend analysis") + ); + } + + private double calculateAverageViolationsPerDay(List reports) { + if (reports.isEmpty()) return 0.0; + + Map dailyCounts = reports.stream() + .collect(Collectors.groupingBy( + report -> report.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), + Collectors.counting() + )); + + return dailyCounts.values().stream() + .mapToLong(Long::longValue) + .average() + .orElse(0.0); + } + + private String findMostCommonViolationType(Map violationsByState) { + return violationsByState.entrySet().stream() + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse("No violations"); + } + + private TrendAnalysis analyzeTrends(List reports) { + if (reports.size() < 2) { + return new TrendAnalysis("STABLE", 0.0, "Insufficient data for trend analysis"); + } + + // Analyze last 7 days vs previous 7 days + LocalDateTime now = LocalDateTime.now(); + LocalDateTime weekAgo = now.minusDays(7); + LocalDateTime twoWeeksAgo = now.minusDays(14); + + long recentWeekCount = reports.stream() + .filter(report -> report.getTimestamp().isAfter(weekAgo)) + .count(); + + long previousWeekCount = reports.stream() + .filter(report -> report.getTimestamp().isAfter(twoWeeksAgo) && + report.getTimestamp().isBefore(weekAgo)) + .count(); + + if (previousWeekCount == 0) { + return new TrendAnalysis("INCREASING", 100.0, "New violations detected this week"); + } + + double changePercentage = ((double) (recentWeekCount - previousWeekCount) / previousWeekCount) * 100; + + String trend; + String description; + + if (Math.abs(changePercentage) < 10) { + trend = "STABLE"; + description = "Violation rates remain relatively stable"; + } else if (changePercentage > 0) { + trend = "INCREASING"; + description = String.format("Violations increased by %.1f%% compared to previous week", changePercentage); + } else { + trend = "DECREASING"; + description = String.format("Violations decreased by %.1f%% compared to previous week", Math.abs(changePercentage)); + } + + return new TrendAnalysis(trend, changePercentage, description); + } } diff --git a/src/main/java/com/openelements/conduct/service/ViolationReportService.java b/src/main/java/com/openelements/conduct/service/ViolationReportService.java index b793c9f..42335ab 100644 --- a/src/main/java/com/openelements/conduct/service/ViolationReportService.java +++ b/src/main/java/com/openelements/conduct/service/ViolationReportService.java @@ -1,5 +1,116 @@ package com.openelements.conduct.service; +import com.openelements.conduct.api.dto.PagedResponse; +import com.openelements.conduct.api.dto.ViolationReportDto; +import com.openelements.conduct.data.CheckResult; +import com.openelements.conduct.data.ViolationState; +import com.openelements.conduct.repository.ViolationReport; +import com.openelements.conduct.repository.ViolationReportRepository; +import org.jspecify.annotations.NonNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service public class ViolationReportService { - + + private final ViolationReportRepository repository; + + @Autowired + public ViolationReportService(@NonNull ViolationReportRepository repository) { + this.repository = Objects.requireNonNull(repository, "repository must not be null"); + } + + public void saveReport(@NonNull CheckResult checkResult) { + Objects.requireNonNull(checkResult, "checkResult must not be null"); + + String title = checkResult.message().title() != null ? checkResult.message().title() : "No Title"; + String content = checkResult.message().content(); + + ViolationReport report = new ViolationReport( + title, + content, + checkResult.message().link(), + checkResult.state(), + checkResult.reason() + ); + + repository.save(report); + } + + public PagedResponse getReports(int page, int size, String sortBy, String sortDir, + ViolationState violationState, String severity, + LocalDateTime startDate, LocalDateTime endDate) { + List allReports = repository.findAll(); + + // Apply filters + List filteredReports = allReports.stream() + .filter(report -> violationState == null || report.getViolationState() == violationState) + .filter(report -> severity == null || report.getSeverity().equals(severity)) + .filter(report -> startDate == null || !report.getTimestamp().isBefore(startDate)) + .filter(report -> endDate == null || !report.getTimestamp().isAfter(endDate)) + .collect(Collectors.toList()); + + // Apply sorting + Comparator comparator = getComparator(sortBy); + if ("desc".equalsIgnoreCase(sortDir)) { + comparator = comparator.reversed(); + } + filteredReports.sort(comparator); + + // Apply pagination + int totalElements = filteredReports.size(); + int totalPages = (int) Math.ceil((double) totalElements / size); + int start = page * size; + int end = Math.min(start + size, totalElements); + + List pageContent = filteredReports.subList(start, end); + List dtoContent = pageContent.stream() + .map(this::convertToDto) + .collect(Collectors.toList()); + + return new PagedResponse<>( + dtoContent, + page, + size, + totalElements, + totalPages, + page == 0, + page >= totalPages - 1 + ); + } + + public Optional getReportById(@NonNull String id) { + return repository.findById(id) + .map(this::convertToDto); + } + + private Comparator getComparator(String sortBy) { + return switch (sortBy) { + case "timestamp" -> Comparator.comparing(ViolationReport::getTimestamp); + case "severity" -> Comparator.comparing(ViolationReport::getSeverity); + case "violationState" -> Comparator.comparing(ViolationReport::getViolationState); + case "messageTitle" -> Comparator.comparing(ViolationReport::getMessageTitle); + default -> Comparator.comparing(ViolationReport::getTimestamp); + }; + } + + private ViolationReportDto convertToDto(ViolationReport report) { + return new ViolationReportDto( + report.getId(), + report.getMessageTitle(), + report.getMessageContent(), + report.getMessageUrl(), + report.getViolationState(), + report.getReason(), + report.getTimestamp(), + report.getSeverity() + ); + } } From aabdc3aeb27afa048cc85a2b499873879fbd8bbb Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Sun, 1 Jun 2025 16:25:42 +0300 Subject: [PATCH 07/17] Add DTO classes for analysis and violation reporting: AnalysisDto, PagedResponse, TrendAnalysis, and ViolationReportDto Signed-off-by: Daniel Ntege --- .../java/com/openelements/conduct/api/dto/AnalysisDto.java | 5 +++++ .../java/com/openelements/conduct/api/dto/PagedResponse.java | 5 +++++ .../java/com/openelements/conduct/api/dto/TrendAnalysis.java | 5 +++++ .../com/openelements/conduct/api/dto/ViolationReportDto.java | 5 +++++ 4 files changed, 20 insertions(+) create mode 100644 src/main/java/com/openelements/conduct/api/dto/AnalysisDto.java create mode 100644 src/main/java/com/openelements/conduct/api/dto/PagedResponse.java create mode 100644 src/main/java/com/openelements/conduct/api/dto/TrendAnalysis.java create mode 100644 src/main/java/com/openelements/conduct/api/dto/ViolationReportDto.java diff --git a/src/main/java/com/openelements/conduct/api/dto/AnalysisDto.java b/src/main/java/com/openelements/conduct/api/dto/AnalysisDto.java new file mode 100644 index 0000000..a4ce466 --- /dev/null +++ b/src/main/java/com/openelements/conduct/api/dto/AnalysisDto.java @@ -0,0 +1,5 @@ +package com.openelements.conduct.api.dto; + +public class AnalysisDto { + +} diff --git a/src/main/java/com/openelements/conduct/api/dto/PagedResponse.java b/src/main/java/com/openelements/conduct/api/dto/PagedResponse.java new file mode 100644 index 0000000..00a0493 --- /dev/null +++ b/src/main/java/com/openelements/conduct/api/dto/PagedResponse.java @@ -0,0 +1,5 @@ +package com.openelements.conduct.api.dto; + +public class PagedResponse { + +} diff --git a/src/main/java/com/openelements/conduct/api/dto/TrendAnalysis.java b/src/main/java/com/openelements/conduct/api/dto/TrendAnalysis.java new file mode 100644 index 0000000..b589606 --- /dev/null +++ b/src/main/java/com/openelements/conduct/api/dto/TrendAnalysis.java @@ -0,0 +1,5 @@ +package com.openelements.conduct.api.dto; + +public class TrendAnalysis { + +} diff --git a/src/main/java/com/openelements/conduct/api/dto/ViolationReportDto.java b/src/main/java/com/openelements/conduct/api/dto/ViolationReportDto.java new file mode 100644 index 0000000..5674482 --- /dev/null +++ b/src/main/java/com/openelements/conduct/api/dto/ViolationReportDto.java @@ -0,0 +1,5 @@ +package com.openelements.conduct.api.dto; + +public class ViolationReportDto { + +} From 45cad4e121d2ce318cf79fe85389ff19510de530 Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Sun, 1 Jun 2025 16:27:57 +0300 Subject: [PATCH 08/17] Refactor DTOs to use records for AnalysisDto, PagedResponse, TrendAnalysis, and ViolationReportDto; implement AnalysisController and ConfigurationController with endpoints for analysis and configuration retrieval, including validation logic. Signed-off-by: Daniel Ntege --- .../conduct/api/dto/AnalysisDto.java | 19 +++- .../conduct/api/dto/PagedResponse.java | 16 ++- .../conduct/api/dto/TrendAnalysis.java | 10 +- .../conduct/api/dto/ViolationReportDto.java | 19 +++- .../controller/AnalysisController.java | 48 ++++++++- .../controller/ConfigurationController.java | 97 ++++++++++++++++++- 6 files changed, 195 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/openelements/conduct/api/dto/AnalysisDto.java b/src/main/java/com/openelements/conduct/api/dto/AnalysisDto.java index a4ce466..fd7d198 100644 --- a/src/main/java/com/openelements/conduct/api/dto/AnalysisDto.java +++ b/src/main/java/com/openelements/conduct/api/dto/AnalysisDto.java @@ -1,5 +1,18 @@ package com.openelements.conduct.api.dto; -public class AnalysisDto { - -} +import org.jspecify.annotations.NonNull; + +import java.time.LocalDateTime; +import java.util.Map; + +public record AnalysisDto( + @NonNull Map violationsByState, + @NonNull Map violationsBySeverity, + @NonNull Map violationsByHour, + @NonNull Map violationsByDay, + double averageViolationsPerDay, + @NonNull String mostCommonViolationType, + @NonNull LocalDateTime analysisTimestamp, + long totalReports, + @NonNull TrendAnalysis trends +) {} diff --git a/src/main/java/com/openelements/conduct/api/dto/PagedResponse.java b/src/main/java/com/openelements/conduct/api/dto/PagedResponse.java index 00a0493..4e918ce 100644 --- a/src/main/java/com/openelements/conduct/api/dto/PagedResponse.java +++ b/src/main/java/com/openelements/conduct/api/dto/PagedResponse.java @@ -1,5 +1,15 @@ package com.openelements.conduct.api.dto; -public class PagedResponse { - -} +import org.jspecify.annotations.NonNull; + +import java.util.List; + +public record PagedResponse( + @NonNull List content, + int page, + int size, + long totalElements, + int totalPages, + boolean first, + boolean last +) {} diff --git a/src/main/java/com/openelements/conduct/api/dto/TrendAnalysis.java b/src/main/java/com/openelements/conduct/api/dto/TrendAnalysis.java index b589606..0b22d03 100644 --- a/src/main/java/com/openelements/conduct/api/dto/TrendAnalysis.java +++ b/src/main/java/com/openelements/conduct/api/dto/TrendAnalysis.java @@ -1,5 +1,9 @@ package com.openelements.conduct.api.dto; -public class TrendAnalysis { - -} +import org.jspecify.annotations.NonNull; + +public record TrendAnalysis( + @NonNull String trend, + double changePercentage, + @NonNull String description +) {} diff --git a/src/main/java/com/openelements/conduct/api/dto/ViolationReportDto.java b/src/main/java/com/openelements/conduct/api/dto/ViolationReportDto.java index 5674482..b25ec0d 100644 --- a/src/main/java/com/openelements/conduct/api/dto/ViolationReportDto.java +++ b/src/main/java/com/openelements/conduct/api/dto/ViolationReportDto.java @@ -1,5 +1,18 @@ package com.openelements.conduct.api.dto; -public class ViolationReportDto { - -} +import com.openelements.conduct.data.ViolationState; +import org.jspecify.annotations.NonNull; + +import java.net.URI; +import java.time.LocalDateTime; + +public record ViolationReportDto( + @NonNull String id, + @NonNull String messageTitle, + @NonNull String messageContent, + @NonNull URI messageUrl, + @NonNull ViolationState violationState, + @NonNull String reason, + @NonNull LocalDateTime timestamp, + @NonNull String severity +) {} diff --git a/src/main/java/com/openelements/conduct/controller/AnalysisController.java b/src/main/java/com/openelements/conduct/controller/AnalysisController.java index 84ea273..be169b0 100644 --- a/src/main/java/com/openelements/conduct/controller/AnalysisController.java +++ b/src/main/java/com/openelements/conduct/controller/AnalysisController.java @@ -1,5 +1,51 @@ package com.openelements.conduct.controller; +import com.openelements.conduct.api.dto.AnalysisDto; +import com.openelements.conduct.service.AnalysisService; +import org.jspecify.annotations.NonNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Objects; + +@RestController +@RequestMapping("/api/v1/analysis") public class AnalysisController { - + + private final AnalysisService analysisService; + + @Autowired + public AnalysisController(@NonNull AnalysisService analysisService) { + this.analysisService = Objects.requireNonNull(analysisService, "analysisService must not be null"); + } + + @GetMapping + public ResponseEntity getAnalysis() { + AnalysisDto analysis = analysisService.generateAnalysis(); + return ResponseEntity.ok(analysis); + } + + @GetMapping("/trends") + public ResponseEntity getTrends() { + AnalysisDto analysis = analysisService.generateAnalysis(); + TrendSummary summary = new TrendSummary( + analysis.trends().trend(), + analysis.trends().changePercentage(), + analysis.trends().description(), + analysis.totalReports(), + analysis.averageViolationsPerDay() + ); + return ResponseEntity.ok(summary); + } + + public record TrendSummary( + String trend, + double changePercentage, + String description, + long totalReports, + double averageViolationsPerDay + ) {} } diff --git a/src/main/java/com/openelements/conduct/controller/ConfigurationController.java b/src/main/java/com/openelements/conduct/controller/ConfigurationController.java index a120ce9..b3d60d6 100644 --- a/src/main/java/com/openelements/conduct/controller/ConfigurationController.java +++ b/src/main/java/com/openelements/conduct/controller/ConfigurationController.java @@ -1,5 +1,100 @@ package com.openelements.conduct.controller; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/config") public class ConfigurationController { - + + private final Environment environment; + + @Autowired + public ConfigurationController(Environment environment) { + this.environment = environment; + } + + @GetMapping + public ResponseEntity> getConfiguration() { + Map config = new HashMap<>(); + + // Only expose non-sensitive configuration + config.put("application.name", environment.getProperty("spring.application.name", "Conduct Guardian")); + config.put("discord.enabled", environment.getProperty("guardian.integration.discord.enabled", "false")); + config.put("slack.enabled", environment.getProperty("guardian.integration.slack.enabled", "false")); + config.put("openai.enabled", environment.getProperty("guardian.integration.openai.enabled", "false")); + config.put("openai.model", environment.getProperty("guardian.integration.openai.model", "gpt-3.5-turbo")); + config.put("github.coc.enabled", environment.getProperty("guardian.integration.github.coc.enabled", "true")); + config.put("log.enabled", environment.getProperty("guardian.integration.log.enabled", "true")); + + return ResponseEntity.ok(config); + } + + @GetMapping("/integrations") + public ResponseEntity getIntegrationStatus() { + IntegrationStatus status = new IntegrationStatus( + Boolean.parseBoolean(environment.getProperty("guardian.integration.discord.enabled", "false")), + Boolean.parseBoolean(environment.getProperty("guardian.integration.slack.enabled", "false")), + Boolean.parseBoolean(environment.getProperty("guardian.integration.openai.enabled", "false")), + Boolean.parseBoolean(environment.getProperty("guardian.integration.github.coc.enabled", "true")), + Boolean.parseBoolean(environment.getProperty("guardian.integration.log.enabled", "true")) + ); + + return ResponseEntity.ok(status); + } + + @PostMapping("/validate") + public ResponseEntity validateConfiguration(@RequestBody ConfigValidationRequest request) { + // Validate configuration without exposing sensitive data + boolean isValid = true; + String message = "Configuration is valid"; + + // Add validation logic here + if (request.checkOpenAI() && !hasOpenAIConfig()) { + isValid = false; + message = "OpenAI configuration is incomplete"; + } + + if (request.checkDiscord() && !hasDiscordConfig()) { + isValid = false; + message = "Discord configuration is incomplete"; + } + + return ResponseEntity.ok(new ValidationResult(isValid, message)); + } + + private boolean hasOpenAIConfig() { + return environment.getProperty("guardian.integration.openai.apiKey") != null && + !environment.getProperty("guardian.integration.openai.apiKey", "").isEmpty(); + } + + private boolean hasDiscordConfig() { + return environment.getProperty("guardian.integration.discord.token") != null && + !environment.getProperty("guardian.integration.discord.token", "").isEmpty(); + } + + public record IntegrationStatus( + boolean discordEnabled, + boolean slackEnabled, + boolean openaiEnabled, + boolean githubCocEnabled, + boolean logEnabled + ) {} + + public record ConfigValidationRequest( + boolean checkOpenAI, + boolean checkDiscord, + boolean checkSlack, + boolean checkGitHub + ) {} + + public record ValidationResult( + boolean isValid, + String message + ) {} } From 319d890e996b72e0232842aaeae86561f4f58666 Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Sun, 1 Jun 2025 16:33:47 +0300 Subject: [PATCH 09/17] Implement ViolationReportController with endpoints for retrieving violation reports and statistics; update ViolationReportService to fix message retrieval; add repository and API configuration properties. Signed-off-by: Daniel Ntege --- .../controller/ViolationReportController.java | 60 ++++++++++++++++++- .../service/ViolationReportService.java | 2 +- src/main/resources/application.properties | 13 ++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/openelements/conduct/controller/ViolationReportController.java b/src/main/java/com/openelements/conduct/controller/ViolationReportController.java index a64aa7c..cff4fb6 100644 --- a/src/main/java/com/openelements/conduct/controller/ViolationReportController.java +++ b/src/main/java/com/openelements/conduct/controller/ViolationReportController.java @@ -1,5 +1,63 @@ package com.openelements.conduct.controller; +import com.openelements.conduct.api.dto.PagedResponse; +import com.openelements.conduct.api.dto.ViolationReportDto; +import com.openelements.conduct.data.ViolationState; +import com.openelements.conduct.service.ViolationReportService; +import org.jspecify.annotations.NonNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.Objects; + +@RestController +@RequestMapping("/api/v1/violation-reports") public class ViolationReportController { - + + private final ViolationReportService violationReportService; + + @Autowired + public ViolationReportController(@NonNull ViolationReportService violationReportService) { + this.violationReportService = Objects.requireNonNull(violationReportService, "violationReportService must not be null"); + } + + @GetMapping + public ResponseEntity> getReports( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(defaultValue = "timestamp") String sortBy, + @RequestParam(defaultValue = "desc") String sortDir, + @RequestParam(required = false) ViolationState violationState, + @RequestParam(required = false) String severity, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate) { + + // Validate pagination parameters + if (page < 0) page = 0; + if (size <= 0 || size > 100) size = 20; + + PagedResponse response = violationReportService.getReports( + page, size, sortBy, sortDir, violationState, severity, startDate, endDate + ); + + return ResponseEntity.ok(response); + } + + @GetMapping("/{id}") + public ResponseEntity getReportById(@PathVariable String id) { + return violationReportService.getReportById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @GetMapping("/stats") + public ResponseEntity getStats() { + // This could be expanded to include more detailed statistics + return ResponseEntity.ok(new ReportStats("Statistics endpoint - implement as needed")); + } + + public record ReportStats(String message) {} } diff --git a/src/main/java/com/openelements/conduct/service/ViolationReportService.java b/src/main/java/com/openelements/conduct/service/ViolationReportService.java index 42335ab..64141aa 100644 --- a/src/main/java/com/openelements/conduct/service/ViolationReportService.java +++ b/src/main/java/com/openelements/conduct/service/ViolationReportService.java @@ -31,7 +31,7 @@ public void saveReport(@NonNull CheckResult checkResult) { Objects.requireNonNull(checkResult, "checkResult must not be null"); String title = checkResult.message().title() != null ? checkResult.message().title() : "No Title"; - String content = checkResult.message().content(); + String content = checkResult.message().message(); ViolationReport report = new ViolationReport( title, diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d0f4c4a..043dfa7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -25,3 +25,16 @@ guardian.integration.github.coc.owner=${GITHUB_COC_OWNER:ClyCites} guardian.integration.github.coc.repo=${GITHUB_COC_REPO:ClyCites-Frontend} guardian.integration.github.coc.token=${GITHUB_TOKEN} +# Repository configuration +guardian.repository.max-reports=${MAX_REPORTS:1000} +guardian.repository.cleanup-enabled=${CLEANUP_ENABLED:true} +guardian.repository.cleanup-interval=${CLEANUP_INTERVAL:3600000} + +# API configuration +guardian.api.max-page-size=${MAX_PAGE_SIZE:100} +guardian.api.default-page-size=${DEFAULT_PAGE_SIZE:20} +guardian.api.enable-analysis=${ENABLE_ANALYSIS:true} + +# Performance configuration +guardian.performance.cache-enabled=${CACHE_ENABLED:true} +guardian.performance.cache-ttl=${CACHE_TTL:300} \ No newline at end of file From b23f66868e2a3e03b68f90505ac48c5f0b49892f Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Sun, 1 Jun 2025 18:31:34 +0300 Subject: [PATCH 10/17] Update GitHub Code of Conduct provider configuration to use OpenElements as owner and Conduct-Guardian as repository Signed-off-by: Daniel Ntege --- src/main/resources/application.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 043dfa7..0f2de48 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -21,8 +21,8 @@ guardian.integration.coc.file.enabled=false guardian.integration.log.enabled=true # GitHub Code of Conduct Provider Configuration guardian.integration.github.coc.enabled=${GITHUB_COC_ENABLED:true} -guardian.integration.github.coc.owner=${GITHUB_COC_OWNER:ClyCites} -guardian.integration.github.coc.repo=${GITHUB_COC_REPO:ClyCites-Frontend} +guardian.integration.github.coc.owner=${GITHUB_COC_OWNER:OpenElements} +guardian.integration.github.coc.repo=${GITHUB_COC_REPO:Conduct-Guardian} guardian.integration.github.coc.token=${GITHUB_TOKEN} # Repository configuration From e007b388dfa18b0509ceb646427f982ad052c421 Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 4 Jul 2025 12:51:11 +0300 Subject: [PATCH 11/17] Refactor AnalysisDto and ViolationReportDto to use records; update AnalysisService for new metrics and trends; implement TrendSummaryDto; modify ViolationReport and ViolationReportService for record usage; enhance AnalysisController for trend retrieval. Signed-off-by: Daniel Ntege --- .../conduct/api/dto/AnalysisDto.java | 37 +++- ...rendAnalysis.java => TrendSummaryDto.java} | 6 +- .../conduct/api/dto/ViolationReportDto.java | 8 +- .../controller/AnalysisController.java | 27 +-- .../conduct/repository/ViolationReport.java | 62 ++---- .../repository/ViolationReportRepository.java | 6 +- .../conduct/service/AnalysisService.java | 203 ++++++++++-------- .../service/ViolationReportService.java | 39 ++-- 8 files changed, 195 insertions(+), 193 deletions(-) rename src/main/java/com/openelements/conduct/api/dto/{TrendAnalysis.java => TrendSummaryDto.java} (54%) diff --git a/src/main/java/com/openelements/conduct/api/dto/AnalysisDto.java b/src/main/java/com/openelements/conduct/api/dto/AnalysisDto.java index fd7d198..bc89308 100644 --- a/src/main/java/com/openelements/conduct/api/dto/AnalysisDto.java +++ b/src/main/java/com/openelements/conduct/api/dto/AnalysisDto.java @@ -3,16 +3,33 @@ import org.jspecify.annotations.NonNull; import java.time.LocalDateTime; -import java.util.Map; public record AnalysisDto( - @NonNull Map violationsByState, - @NonNull Map violationsBySeverity, - @NonNull Map violationsByHour, - @NonNull Map violationsByDay, - double averageViolationsPerDay, - @NonNull String mostCommonViolationType, - @NonNull LocalDateTime analysisTimestamp, - long totalReports, - @NonNull TrendAnalysis trends + // Total counts + int totalNoViolationCount, + int totalPossibleViolationCount, + int totalViolationCount, + + // Daily averages + int averageNoViolationCountPerDay, + int averagePossibleViolationCountPerDay, + int averageViolationCountPerDay, + + // Daily maximums + int maxNoViolationCountPerDay, + int maxPossibleViolationCountPerDay, + int maxViolationCountPerDay, + + // This week averages + int averageNoViolationCountPerDayInThisWeek, + int averagePossibleViolationCountPerDayInThisWeek, + int averageViolationCountPerDayInThisWeek, + + // Growth metrics + double generalGrowthOfChecksInPercentage, + double growthOfNoViolationCountAgainstLastWeek, + double growthOfPossibleViolationCountAgainstLastWeek, + double growthOfViolationCountAgainstLastWeek, + + @NonNull LocalDateTime analysisTimestamp ) {} diff --git a/src/main/java/com/openelements/conduct/api/dto/TrendAnalysis.java b/src/main/java/com/openelements/conduct/api/dto/TrendSummaryDto.java similarity index 54% rename from src/main/java/com/openelements/conduct/api/dto/TrendAnalysis.java rename to src/main/java/com/openelements/conduct/api/dto/TrendSummaryDto.java index 0b22d03..d8bdf66 100644 --- a/src/main/java/com/openelements/conduct/api/dto/TrendAnalysis.java +++ b/src/main/java/com/openelements/conduct/api/dto/TrendSummaryDto.java @@ -2,8 +2,10 @@ import org.jspecify.annotations.NonNull; -public record TrendAnalysis( +public record TrendSummaryDto( @NonNull String trend, double changePercentage, - @NonNull String description + @NonNull String description, + long totalReports, + double averageViolationsPerDay ) {} diff --git a/src/main/java/com/openelements/conduct/api/dto/ViolationReportDto.java b/src/main/java/com/openelements/conduct/api/dto/ViolationReportDto.java index b25ec0d..e53ffa8 100644 --- a/src/main/java/com/openelements/conduct/api/dto/ViolationReportDto.java +++ b/src/main/java/com/openelements/conduct/api/dto/ViolationReportDto.java @@ -2,17 +2,17 @@ import com.openelements.conduct.data.ViolationState; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import java.net.URI; import java.time.LocalDateTime; public record ViolationReportDto( @NonNull String id, - @NonNull String messageTitle, + @Nullable String messageTitle, @NonNull String messageContent, - @NonNull URI messageUrl, + @NonNull URI linkToViolation, @NonNull ViolationState violationState, @NonNull String reason, - @NonNull LocalDateTime timestamp, - @NonNull String severity + @NonNull LocalDateTime timestamp ) {} diff --git a/src/main/java/com/openelements/conduct/controller/AnalysisController.java b/src/main/java/com/openelements/conduct/controller/AnalysisController.java index be169b0..c8da164 100644 --- a/src/main/java/com/openelements/conduct/controller/AnalysisController.java +++ b/src/main/java/com/openelements/conduct/controller/AnalysisController.java @@ -1,10 +1,10 @@ package com.openelements.conduct.controller; import com.openelements.conduct.api.dto.AnalysisDto; +import com.openelements.conduct.api.dto.TrendSummaryDto; import com.openelements.conduct.service.AnalysisService; import org.jspecify.annotations.NonNull; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -23,29 +23,12 @@ public AnalysisController(@NonNull AnalysisService analysisService) { } @GetMapping - public ResponseEntity getAnalysis() { - AnalysisDto analysis = analysisService.generateAnalysis(); - return ResponseEntity.ok(analysis); + public AnalysisDto getAnalysis() { + return analysisService.generateAnalysis(); } @GetMapping("/trends") - public ResponseEntity getTrends() { - AnalysisDto analysis = analysisService.generateAnalysis(); - TrendSummary summary = new TrendSummary( - analysis.trends().trend(), - analysis.trends().changePercentage(), - analysis.trends().description(), - analysis.totalReports(), - analysis.averageViolationsPerDay() - ); - return ResponseEntity.ok(summary); + public TrendSummaryDto getTrends() { + return analysisService.generateTrendSummary(); } - - public record TrendSummary( - String trend, - double changePercentage, - String description, - long totalReports, - double averageViolationsPerDay - ) {} } diff --git a/src/main/java/com/openelements/conduct/repository/ViolationReport.java b/src/main/java/com/openelements/conduct/repository/ViolationReport.java index c8beab7..3517221 100644 --- a/src/main/java/com/openelements/conduct/repository/ViolationReport.java +++ b/src/main/java/com/openelements/conduct/repository/ViolationReport.java @@ -2,55 +2,35 @@ import com.openelements.conduct.data.ViolationState; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import java.net.URI; import java.time.LocalDateTime; import java.util.Objects; import java.util.UUID; -public class ViolationReport { - private final String id; - private final String messageTitle; - private final String messageContent; - private final URI messageUrl; - private final ViolationState violationState; - private final String reason; - private final LocalDateTime timestamp; - private final String severity; +public record ViolationReport( + @NonNull String id, + @Nullable String messageTitle, + @NonNull String messageContent, + @NonNull URI messageUrl, + @NonNull ViolationState violationState, + @NonNull String reason, + @NonNull LocalDateTime timestamp +) { + public ViolationReport { + Objects.requireNonNull(id, "id must not be null"); + Objects.requireNonNull(messageContent, "messageContent must not be null"); + Objects.requireNonNull(messageUrl, "messageUrl must not be null"); + Objects.requireNonNull(violationState, "violationState must not be null"); + Objects.requireNonNull(reason, "reason must not be null"); + Objects.requireNonNull(timestamp, "timestamp must not be null"); + } - public ViolationReport(@NonNull String messageTitle, @NonNull String messageContent, + public ViolationReport(@Nullable String messageTitle, @NonNull String messageContent, @NonNull URI messageUrl, @NonNull ViolationState violationState, @NonNull String reason) { - this.id = UUID.randomUUID().toString(); - this.messageTitle = Objects.requireNonNull(messageTitle, "messageTitle must not be null"); - this.messageContent = Objects.requireNonNull(messageContent, "messageContent must not be null"); - this.messageUrl = Objects.requireNonNull(messageUrl, "messageUrl must not be null"); - this.violationState = Objects.requireNonNull(violationState, "violationState must not be null"); - this.reason = Objects.requireNonNull(reason, "reason must not be null"); - this.timestamp = LocalDateTime.now(); - this.severity = determineSeverity(violationState, reason); - } - - private String determineSeverity(ViolationState state, String reason) { - if (state == ViolationState.VIOLATION) { - if (reason.toLowerCase().contains("severe") || reason.toLowerCase().contains("harassment")) { - return "HIGH"; - } else if (reason.toLowerCase().contains("moderate") || reason.toLowerCase().contains("inappropriate")) { - return "MEDIUM"; - } else { - return "LOW"; - } - } - return "NONE"; + this(UUID.randomUUID().toString(), messageTitle, messageContent, messageUrl, + violationState, reason, LocalDateTime.now()); } - - // Getters - public String getId() { return id; } - public String getMessageTitle() { return messageTitle; } - public String getMessageContent() { return messageContent; } - public URI getMessageUrl() { return messageUrl; } - public ViolationState getViolationState() { return violationState; } - public String getReason() { return reason; } - public LocalDateTime getTimestamp() { return timestamp; } - public String getSeverity() { return severity; } } diff --git a/src/main/java/com/openelements/conduct/repository/ViolationReportRepository.java b/src/main/java/com/openelements/conduct/repository/ViolationReportRepository.java index eabac1a..1bed1f3 100644 --- a/src/main/java/com/openelements/conduct/repository/ViolationReportRepository.java +++ b/src/main/java/com/openelements/conduct/repository/ViolationReportRepository.java @@ -34,7 +34,7 @@ public List findAll() { public List findByViolationState(@NonNull ViolationState state) { return reports.stream() - .filter(report -> report.getViolationState() == state) + .filter(report -> report.violationState() == state) .collect(Collectors.toList()); } @@ -46,13 +46,13 @@ public List findBySeverity(@NonNull String severity) { public List findByDateRange(@NonNull LocalDateTime start, @NonNull LocalDateTime end) { return reports.stream() - .filter(report -> !report.getTimestamp().isBefore(start) && !report.getTimestamp().isAfter(end)) + .filter(report -> !report.timestamp().isBefore(start) && !report.timestamp().isAfter(end)) .collect(Collectors.toList()); } public Optional findById(@NonNull String id) { return reports.stream() - .filter(report -> report.getId().equals(id)) + .filter(report -> report.id().equals(id)) .findFirst(); } diff --git a/src/main/java/com/openelements/conduct/service/AnalysisService.java b/src/main/java/com/openelements/conduct/service/AnalysisService.java index 0454e47..d65a4e3 100644 --- a/src/main/java/com/openelements/conduct/service/AnalysisService.java +++ b/src/main/java/com/openelements/conduct/service/AnalysisService.java @@ -1,7 +1,8 @@ package com.openelements.conduct.service; import com.openelements.conduct.api.dto.AnalysisDto; -import com.openelements.conduct.api.dto.TrendAnalysis; +import com.openelements.conduct.api.dto.TrendSummaryDto; +import com.openelements.conduct.data.ViolationState; import com.openelements.conduct.repository.ViolationReport; import com.openelements.conduct.repository.ViolationReportRepository; import org.jspecify.annotations.NonNull; @@ -32,122 +33,146 @@ public AnalysisDto generateAnalysis() { return createEmptyAnalysis(); } - Map violationsByState = reports.stream() - .collect(Collectors.groupingBy( - report -> report.getViolationState().toString(), - Collectors.counting() - )); + // Calculate total counts + int totalNoViolationCount = (int) reports.stream() + .filter(r -> r.violationState() == ViolationState.NONE).count(); + int totalPossibleViolationCount = (int) reports.stream() + .filter(r -> r.violationState() == ViolationState.POSSIBLE_VIOLATION).count(); + int totalViolationCount = (int) reports.stream() + .filter(r -> r.violationState() == ViolationState.VIOLATION).count(); - Map violationsBySeverity = reports.stream() + // Calculate daily averages + Map> reportsByDay = reports.stream() .collect(Collectors.groupingBy( - ViolationReport::getSeverity, - Collectors.counting() + report -> report.timestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) )); - Map violationsByHour = reports.stream() - .collect(Collectors.groupingBy( - report -> String.valueOf(report.getTimestamp().getHour()), - Collectors.counting() - )); + int averageNoViolationCountPerDay = calculateDailyAverage(reportsByDay, ViolationState.NONE); + int averagePossibleViolationCountPerDay = calculateDailyAverage(reportsByDay, ViolationState.POSSIBLE_VIOLATION); + int averageViolationCountPerDay = calculateDailyAverage(reportsByDay, ViolationState.VIOLATION); - Map violationsByDay = reports.stream() + // Calculate daily maximums + int maxNoViolationCountPerDay = calculateDailyMaximum(reportsByDay, ViolationState.NONE); + int maxPossibleViolationCountPerDay = calculateDailyMaximum(reportsByDay, ViolationState.POSSIBLE_VIOLATION); + int maxViolationCountPerDay = calculateDailyMaximum(reportsByDay, ViolationState.VIOLATION); + + // Calculate this week averages + LocalDateTime weekStart = LocalDateTime.now().minusDays(7); + List thisWeekReports = reports.stream() + .filter(r -> r.timestamp().isAfter(weekStart)) + .collect(Collectors.toList()); + + Map> thisWeekReportsByDay = thisWeekReports.stream() .collect(Collectors.groupingBy( - report -> report.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), - Collectors.counting() + report -> report.timestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) )); - double averageViolationsPerDay = calculateAverageViolationsPerDay(reports); - String mostCommonViolationType = findMostCommonViolationType(violationsByState); - TrendAnalysis trends = analyzeTrends(reports); + int averageNoViolationCountPerDayInThisWeek = calculateDailyAverage(thisWeekReportsByDay, ViolationState.NONE); + int averagePossibleViolationCountPerDayInThisWeek = calculateDailyAverage(thisWeekReportsByDay, ViolationState.POSSIBLE_VIOLATION); + int averageViolationCountPerDayInThisWeek = calculateDailyAverage(thisWeekReportsByDay, ViolationState.VIOLATION); + + // Calculate growth metrics + LocalDateTime lastWeekStart = LocalDateTime.now().minusDays(14); + LocalDateTime lastWeekEnd = LocalDateTime.now().minusDays(7); + + List lastWeekReports = reports.stream() + .filter(r -> r.timestamp().isAfter(lastWeekStart) && r.timestamp().isBefore(lastWeekEnd)) + .collect(Collectors.toList()); + + double generalGrowthOfChecksInPercentage = calculateGrowthPercentage(lastWeekReports.size(), thisWeekReports.size()); + double growthOfNoViolationCountAgainstLastWeek = calculateGrowthPercentage( + (int) lastWeekReports.stream().filter(r -> r.violationState() == ViolationState.NONE).count(), + (int) thisWeekReports.stream().filter(r -> r.violationState() == ViolationState.NONE).count() + ); + double growthOfPossibleViolationCountAgainstLastWeek = calculateGrowthPercentage( + (int) lastWeekReports.stream().filter(r -> r.violationState() == ViolationState.POSSIBLE_VIOLATION).count(), + (int) thisWeekReports.stream().filter(r -> r.violationState() == ViolationState.POSSIBLE_VIOLATION).count() + ); + double growthOfViolationCountAgainstLastWeek = calculateGrowthPercentage( + (int) lastWeekReports.stream().filter(r -> r.violationState() == ViolationState.VIOLATION).count(), + (int) thisWeekReports.stream().filter(r -> r.violationState() == ViolationState.VIOLATION).count() + ); return new AnalysisDto( - violationsByState, - violationsBySeverity, - violationsByHour, - violationsByDay, - averageViolationsPerDay, - mostCommonViolationType, - LocalDateTime.now(), - reports.size(), - trends + totalNoViolationCount, + totalPossibleViolationCount, + totalViolationCount, + averageNoViolationCountPerDay, + averagePossibleViolationCountPerDay, + averageViolationCountPerDay, + maxNoViolationCountPerDay, + maxPossibleViolationCountPerDay, + maxViolationCountPerDay, + averageNoViolationCountPerDayInThisWeek, + averagePossibleViolationCountPerDayInThisWeek, + averageViolationCountPerDayInThisWeek, + generalGrowthOfChecksInPercentage, + growthOfNoViolationCountAgainstLastWeek, + growthOfPossibleViolationCountAgainstLastWeek, + growthOfViolationCountAgainstLastWeek, + LocalDateTime.now() + ); + } + + public TrendSummaryDto generateTrendSummary() { + AnalysisDto analysis = generateAnalysis(); + + String trend = determineTrend(analysis.generalGrowthOfChecksInPercentage()); + String description = String.format("General growth: %.1f%%, Violations growth: %.1f%%", + analysis.generalGrowthOfChecksInPercentage(), + analysis.growthOfViolationCountAgainstLastWeek()); + + return new TrendSummaryDto( + trend, + analysis.generalGrowthOfChecksInPercentage(), + description, + analysis.totalNoViolationCount() + analysis.totalPossibleViolationCount() + analysis.totalViolationCount(), + analysis.averageViolationCountPerDay() ); } private AnalysisDto createEmptyAnalysis() { return new AnalysisDto( - Map.of(), - Map.of(), - Map.of(), - Map.of(), - 0.0, - "No violations", - LocalDateTime.now(), - 0L, - new TrendAnalysis("STABLE", 0.0, "No data available for trend analysis") + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0, LocalDateTime.now() ); } - private double calculateAverageViolationsPerDay(List reports) { - if (reports.isEmpty()) return 0.0; - - Map dailyCounts = reports.stream() - .collect(Collectors.groupingBy( - report -> report.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), - Collectors.counting() - )); + private int calculateDailyAverage(Map> reportsByDay, ViolationState state) { + if (reportsByDay.isEmpty()) return 0; - return dailyCounts.values().stream() - .mapToLong(Long::longValue) + double average = reportsByDay.values().stream() + .mapToInt(dayReports -> (int) dayReports.stream() + .filter(r -> r.violationState() == state) + .count()) .average() .orElse(0.0); + + return (int) Math.round(average); } - private String findMostCommonViolationType(Map violationsByState) { - return violationsByState.entrySet().stream() - .max(Map.Entry.comparingByValue()) - .map(Map.Entry::getKey) - .orElse("No violations"); + private int calculateDailyMaximum(Map> reportsByDay, ViolationState state) { + return reportsByDay.values().stream() + .mapToInt(dayReports -> (int) dayReports.stream() + .filter(r -> r.violationState() == state) + .count()) + .max() + .orElse(0); } - private TrendAnalysis analyzeTrends(List reports) { - if (reports.size() < 2) { - return new TrendAnalysis("STABLE", 0.0, "Insufficient data for trend analysis"); - } - - // Analyze last 7 days vs previous 7 days - LocalDateTime now = LocalDateTime.now(); - LocalDateTime weekAgo = now.minusDays(7); - LocalDateTime twoWeeksAgo = now.minusDays(14); - - long recentWeekCount = reports.stream() - .filter(report -> report.getTimestamp().isAfter(weekAgo)) - .count(); - - long previousWeekCount = reports.stream() - .filter(report -> report.getTimestamp().isAfter(twoWeeksAgo) && - report.getTimestamp().isBefore(weekAgo)) - .count(); - - if (previousWeekCount == 0) { - return new TrendAnalysis("INCREASING", 100.0, "New violations detected this week"); + private double calculateGrowthPercentage(int oldValue, int newValue) { + if (oldValue == 0) { + return newValue > 0 ? 100.0 : 0.0; } + return ((double) (newValue - oldValue) / oldValue) * 100.0; + } - double changePercentage = ((double) (recentWeekCount - previousWeekCount) / previousWeekCount) * 100; - - String trend; - String description; - - if (Math.abs(changePercentage) < 10) { - trend = "STABLE"; - description = "Violation rates remain relatively stable"; - } else if (changePercentage > 0) { - trend = "INCREASING"; - description = String.format("Violations increased by %.1f%% compared to previous week", changePercentage); + private String determineTrend(double growthPercentage) { + if (Math.abs(growthPercentage) < 10) { + return "STABLE"; + } else if (growthPercentage > 0) { + return "INCREASING"; } else { - trend = "DECREASING"; - description = String.format("Violations decreased by %.1f%% compared to previous week", Math.abs(changePercentage)); + return "DECREASING"; } - - return new TrendAnalysis(trend, changePercentage, description); } } diff --git a/src/main/java/com/openelements/conduct/service/ViolationReportService.java b/src/main/java/com/openelements/conduct/service/ViolationReportService.java index 64141aa..d19d225 100644 --- a/src/main/java/com/openelements/conduct/service/ViolationReportService.java +++ b/src/main/java/com/openelements/conduct/service/ViolationReportService.java @@ -30,12 +30,9 @@ public ViolationReportService(@NonNull ViolationReportRepository repository) { public void saveReport(@NonNull CheckResult checkResult) { Objects.requireNonNull(checkResult, "checkResult must not be null"); - String title = checkResult.message().title() != null ? checkResult.message().title() : "No Title"; - String content = checkResult.message().message(); - ViolationReport report = new ViolationReport( - title, - content, + checkResult.message().title(), + checkResult.message().message(), checkResult.message().link(), checkResult.state(), checkResult.reason() @@ -51,10 +48,9 @@ public PagedResponse getReports(int page, int size, String s // Apply filters List filteredReports = allReports.stream() - .filter(report -> violationState == null || report.getViolationState() == violationState) - .filter(report -> severity == null || report.getSeverity().equals(severity)) - .filter(report -> startDate == null || !report.getTimestamp().isBefore(startDate)) - .filter(report -> endDate == null || !report.getTimestamp().isAfter(endDate)) + .filter(report -> violationState == null || report.violationState() == violationState) + .filter(report -> startDate == null || !report.timestamp().isBefore(startDate)) + .filter(report -> endDate == null || !report.timestamp().isAfter(endDate)) .collect(Collectors.toList()); // Apply sorting @@ -93,24 +89,23 @@ public Optional getReportById(@NonNull String id) { private Comparator getComparator(String sortBy) { return switch (sortBy) { - case "timestamp" -> Comparator.comparing(ViolationReport::getTimestamp); - case "severity" -> Comparator.comparing(ViolationReport::getSeverity); - case "violationState" -> Comparator.comparing(ViolationReport::getViolationState); - case "messageTitle" -> Comparator.comparing(ViolationReport::getMessageTitle); - default -> Comparator.comparing(ViolationReport::getTimestamp); + case "timestamp" -> Comparator.comparing(ViolationReport::timestamp); + case "violationState" -> Comparator.comparing(ViolationReport::violationState); + case "messageTitle" -> Comparator.comparing(ViolationReport::messageTitle, + Comparator.nullsLast(Comparator.naturalOrder())); + default -> Comparator.comparing(ViolationReport::timestamp); }; } private ViolationReportDto convertToDto(ViolationReport report) { return new ViolationReportDto( - report.getId(), - report.getMessageTitle(), - report.getMessageContent(), - report.getMessageUrl(), - report.getViolationState(), - report.getReason(), - report.getTimestamp(), - report.getSeverity() + report.id(), + report.messageTitle(), + report.messageContent(), + report.messageUrl(), + report.violationState(), + report.reason(), + report.timestamp() ); } } From 424fa2e8403b1c6707d5ad81a85f6bbb28657b3c Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 4 Jul 2025 12:52:30 +0300 Subject: [PATCH 12/17] Remove severity parameter from getReports method in ViolationReportController Signed-off-by: Daniel Ntege --- .../conduct/controller/ViolationReportController.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/openelements/conduct/controller/ViolationReportController.java b/src/main/java/com/openelements/conduct/controller/ViolationReportController.java index cff4fb6..cee9f48 100644 --- a/src/main/java/com/openelements/conduct/controller/ViolationReportController.java +++ b/src/main/java/com/openelements/conduct/controller/ViolationReportController.java @@ -31,7 +31,6 @@ public ResponseEntity> getReports( @RequestParam(defaultValue = "timestamp") String sortBy, @RequestParam(defaultValue = "desc") String sortDir, @RequestParam(required = false) ViolationState violationState, - @RequestParam(required = false) String severity, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate) { @@ -40,7 +39,7 @@ public ResponseEntity> getReports( if (size <= 0 || size > 100) size = 20; PagedResponse response = violationReportService.getReports( - page, size, sortBy, sortDir, violationState, severity, startDate, endDate + page, size, sortBy, sortDir, violationState, startDate, endDate ); return ResponseEntity.ok(response); From 4e2d4d22f16d0fb3c0cf12ae3416ece987c6931d Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 4 Jul 2025 12:57:03 +0300 Subject: [PATCH 13/17] Remove severity filtering method from ViolationReportRepository; update ViolationReportController to handle missing severity parameter in report retrieval. Signed-off-by: Daniel Ntege --- .../conduct/controller/ViolationReportController.java | 3 ++- .../conduct/repository/ViolationReportRepository.java | 6 ------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/openelements/conduct/controller/ViolationReportController.java b/src/main/java/com/openelements/conduct/controller/ViolationReportController.java index cee9f48..a5e2b87 100644 --- a/src/main/java/com/openelements/conduct/controller/ViolationReportController.java +++ b/src/main/java/com/openelements/conduct/controller/ViolationReportController.java @@ -38,8 +38,9 @@ public ResponseEntity> getReports( if (page < 0) page = 0; if (size <= 0 || size > 100) size = 20; + // Pass null (or appropriate value) for the missing String parameter PagedResponse response = violationReportService.getReports( - page, size, sortBy, sortDir, violationState, startDate, endDate + page, size, sortBy, sortDir, violationState, null, startDate, endDate ); return ResponseEntity.ok(response); diff --git a/src/main/java/com/openelements/conduct/repository/ViolationReportRepository.java b/src/main/java/com/openelements/conduct/repository/ViolationReportRepository.java index 1bed1f3..c8574be 100644 --- a/src/main/java/com/openelements/conduct/repository/ViolationReportRepository.java +++ b/src/main/java/com/openelements/conduct/repository/ViolationReportRepository.java @@ -38,12 +38,6 @@ public List findByViolationState(@NonNull ViolationState state) .collect(Collectors.toList()); } - public List findBySeverity(@NonNull String severity) { - return reports.stream() - .filter(report -> report.getSeverity().equals(severity)) - .collect(Collectors.toList()); - } - public List findByDateRange(@NonNull LocalDateTime start, @NonNull LocalDateTime end) { return reports.stream() .filter(report -> !report.timestamp().isBefore(start) && !report.timestamp().isAfter(end)) From aee988bed4abcac0c93b15a36da71395ae60998b Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 4 Jul 2025 12:59:18 +0300 Subject: [PATCH 14/17] Update default page size to 100 in ViolationReportController for improved pagination handling Signed-off-by: Daniel Ntege --- .../conduct/controller/ViolationReportController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/openelements/conduct/controller/ViolationReportController.java b/src/main/java/com/openelements/conduct/controller/ViolationReportController.java index a5e2b87..7aac7ec 100644 --- a/src/main/java/com/openelements/conduct/controller/ViolationReportController.java +++ b/src/main/java/com/openelements/conduct/controller/ViolationReportController.java @@ -36,7 +36,7 @@ public ResponseEntity> getReports( // Validate pagination parameters if (page < 0) page = 0; - if (size <= 0 || size > 100) size = 20; + if (size <= 0 || size > 100) size = 100; // Pass null (or appropriate value) for the missing String parameter PagedResponse response = violationReportService.getReports( From 3dafab6434d817a0141111dca3f9584cbf79aedd Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 4 Jul 2025 12:59:42 +0300 Subject: [PATCH 15/17] Refactor pagination parameter validation in ViolationReportController to ensure default values are set correctly Signed-off-by: Daniel Ntege --- .../conduct/controller/ViolationReportController.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/openelements/conduct/controller/ViolationReportController.java b/src/main/java/com/openelements/conduct/controller/ViolationReportController.java index 7aac7ec..f4d4a0f 100644 --- a/src/main/java/com/openelements/conduct/controller/ViolationReportController.java +++ b/src/main/java/com/openelements/conduct/controller/ViolationReportController.java @@ -33,12 +33,10 @@ public ResponseEntity> getReports( @RequestParam(required = false) ViolationState violationState, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate) { - - // Validate pagination parameters + if (page < 0) page = 0; if (size <= 0 || size > 100) size = 100; - - // Pass null (or appropriate value) for the missing String parameter + PagedResponse response = violationReportService.getReports( page, size, sortBy, sortDir, violationState, null, startDate, endDate ); From 28048bf49a2febb87b0aa080e7242c0905d47f62 Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 4 Jul 2025 13:01:24 +0300 Subject: [PATCH 16/17] Remove comment placeholder in getStats method of ViolationReportController Signed-off-by: Daniel Ntege --- .../conduct/controller/ViolationReportController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/openelements/conduct/controller/ViolationReportController.java b/src/main/java/com/openelements/conduct/controller/ViolationReportController.java index f4d4a0f..4cd388a 100644 --- a/src/main/java/com/openelements/conduct/controller/ViolationReportController.java +++ b/src/main/java/com/openelements/conduct/controller/ViolationReportController.java @@ -53,7 +53,6 @@ public ResponseEntity getReportById(@PathVariable String id) @GetMapping("/stats") public ResponseEntity getStats() { - // This could be expanded to include more detailed statistics return ResponseEntity.ok(new ReportStats("Statistics endpoint - implement as needed")); } From 009631038206cf3080b2bfd64ee5a774b1088c78 Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 4 Jul 2025 23:32:21 +0300 Subject: [PATCH 17/17] `Removed TrendSummaryDto and related methods from AnalysisController and AnalysisService` Signed-off-by: Daniel Ntege --- .../controller/AnalysisController.java | 6 ---- .../conduct/service/AnalysisService.java | 28 ------------------- 2 files changed, 34 deletions(-) diff --git a/src/main/java/com/openelements/conduct/controller/AnalysisController.java b/src/main/java/com/openelements/conduct/controller/AnalysisController.java index c8da164..291f4c7 100644 --- a/src/main/java/com/openelements/conduct/controller/AnalysisController.java +++ b/src/main/java/com/openelements/conduct/controller/AnalysisController.java @@ -1,7 +1,6 @@ package com.openelements.conduct.controller; import com.openelements.conduct.api.dto.AnalysisDto; -import com.openelements.conduct.api.dto.TrendSummaryDto; import com.openelements.conduct.service.AnalysisService; import org.jspecify.annotations.NonNull; import org.springframework.beans.factory.annotation.Autowired; @@ -26,9 +25,4 @@ public AnalysisController(@NonNull AnalysisService analysisService) { public AnalysisDto getAnalysis() { return analysisService.generateAnalysis(); } - - @GetMapping("/trends") - public TrendSummaryDto getTrends() { - return analysisService.generateTrendSummary(); - } } diff --git a/src/main/java/com/openelements/conduct/service/AnalysisService.java b/src/main/java/com/openelements/conduct/service/AnalysisService.java index d65a4e3..93ca52c 100644 --- a/src/main/java/com/openelements/conduct/service/AnalysisService.java +++ b/src/main/java/com/openelements/conduct/service/AnalysisService.java @@ -1,7 +1,6 @@ package com.openelements.conduct.service; import com.openelements.conduct.api.dto.AnalysisDto; -import com.openelements.conduct.api.dto.TrendSummaryDto; import com.openelements.conduct.data.ViolationState; import com.openelements.conduct.repository.ViolationReport; import com.openelements.conduct.repository.ViolationReportRepository; @@ -114,23 +113,6 @@ public AnalysisDto generateAnalysis() { ); } - public TrendSummaryDto generateTrendSummary() { - AnalysisDto analysis = generateAnalysis(); - - String trend = determineTrend(analysis.generalGrowthOfChecksInPercentage()); - String description = String.format("General growth: %.1f%%, Violations growth: %.1f%%", - analysis.generalGrowthOfChecksInPercentage(), - analysis.growthOfViolationCountAgainstLastWeek()); - - return new TrendSummaryDto( - trend, - analysis.generalGrowthOfChecksInPercentage(), - description, - analysis.totalNoViolationCount() + analysis.totalPossibleViolationCount() + analysis.totalViolationCount(), - analysis.averageViolationCountPerDay() - ); - } - private AnalysisDto createEmptyAnalysis() { return new AnalysisDto( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0, LocalDateTime.now() @@ -165,14 +147,4 @@ private double calculateGrowthPercentage(int oldValue, int newValue) { } return ((double) (newValue - oldValue) / oldValue) * 100.0; } - - private String determineTrend(double growthPercentage) { - if (Math.abs(growthPercentage) < 10) { - return "STABLE"; - } else if (growthPercentage > 0) { - return "INCREASING"; - } else { - return "DECREASING"; - } - } }