Skip to content

Commit 3dc9cec

Browse files
KasinhouMatus KasakPaurikova2milanmajchrakclaude
authored
Health report, report diff fixes (#1254)
* fix(health-report): fix CLI args, multi-check support, and report-diff comparison logic * Fix ReportDiff setup and date validation * fix failed integration test * added tests, used -c 1 2 instead of -c 1 -c 2 * used UNLIMITED_VALUES unstead of MAX_VALUE * improved doc * used multilist for -c , removed unused method * improved doc * WIP updated health report and report diff * Complete update of health-report and report-diff * Sorting reports and updating tests, plus enable multiple -c in report-diff * Improved docs, output and info and logic * Improved comparision of reports w/o changes, or with only one report specified * Updated tests * Removed unused Report and refactor getChecks * Address review comments: simplify null checks, deduplicate skipped-checks section, validate report IDs before defaulting Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk> Co-authored-by: Paurikova2 <michaela.paurikova@dataquest.sk> Co-authored-by: milanmajchrak <milan.majchrak@dataquest.sk> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 82085cb commit 3dc9cec

14 files changed

Lines changed: 1428 additions & 626 deletions

File tree

dspace-api/src/main/java/org/dspace/app/healthreport/HealthReport.java

Lines changed: 103 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@
1313
import java.io.InputStream;
1414
import java.nio.charset.StandardCharsets;
1515
import java.text.SimpleDateFormat;
16+
import java.util.ArrayList;
1617
import java.util.Date;
1718
import java.util.LinkedHashMap;
19+
import java.util.LinkedHashSet;
20+
import java.util.List;
1821
import java.util.Locale;
1922
import java.util.Map;
23+
import java.util.Set;
2024
import javax.mail.MessagingException;
2125

2226
import org.apache.commons.cli.Option;
@@ -29,10 +33,11 @@
2933
import org.dspace.core.Context;
3034
import org.dspace.core.Email;
3135
import org.dspace.core.I18nUtil;
36+
import org.dspace.core.factory.CoreServiceFactory;
37+
import org.dspace.core.service.PluginService;
3238
import org.dspace.eperson.factory.EPersonServiceFactory;
3339
import org.dspace.eperson.service.EPersonService;
3440
import org.dspace.health.Check;
35-
import org.dspace.health.Report;
3641
import org.dspace.health.ReportInfo;
3742
import org.dspace.scripts.DSpaceRunnable;
3843
import org.dspace.services.ConfigurationService;
@@ -56,22 +61,23 @@ public class HealthReport extends DSpaceRunnable<HealthReportScriptConfiguration
5661
/**
5762
* Checks to be performed.
5863
*/
59-
private static final LinkedHashMap<String, Check> checks = Report.checks();
64+
private static final LinkedHashMap<String, Check> checks = getChecks();
6065

6166
/**
62-
* `-i`: Info, show help information.
67+
* `-h`: Help, show help information.
6368
*/
64-
private boolean info = false;
69+
private boolean help = false;
6570

6671
/**
6772
* `-e`: Email, send report to specified email address.
6873
*/
6974
private String[] emails;
7075

7176
/**
72-
* `-c`: Check, perform only specific check by index (0-`getNumberOfChecks()`).
77+
* `-c`: Check, perform only specific checks by index (0-`getNumberOfChecks()`).
78+
* Supports multiple values.
7379
*/
74-
private int specificCheck = -1;
80+
private List<Integer> specificChecks = new ArrayList<>();
7581

7682
/**
7783
* `-f`: For, specify the last N days to consider.
@@ -80,9 +86,9 @@ public class HealthReport extends DSpaceRunnable<HealthReportScriptConfiguration
8086
private int forLastNDays = configurationService.getIntProperty("healthcheck.last_n_days");
8187

8288
/**
83-
* `-o`: Output, specify a file to save the report.
89+
* `-r`: Report, specify a file to save the report.
8490
*/
85-
private String fileName;
91+
private String reportFile;
8692

8793
@Override
8894
public HealthReportScriptConfiguration getScriptConfiguration() {
@@ -93,52 +99,64 @@ public HealthReportScriptConfiguration getScriptConfiguration() {
9399
@Override
94100
public void setup() throws ParseException {
95101
ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
96-
// `-i`: Info, show help information.
97-
if (commandLine.hasOption('i')) {
98-
info = true;
102+
// `-h`: Help, show help information.
103+
if (commandLine.hasOption('h')) {
104+
help = true;
99105
return;
100106
}
101107

102108
// `-e`: Email, send report to specified email address.
103109
if (commandLine.hasOption('e')) {
104110
emails = commandLine.getOptionValues('e');
105-
handler.logInfo("\nReport sent to this email address: " + String.join(", ", emails));
106111
}
107112

108-
// `-c`: Check, perform only specific check by index (0-`getNumberOfChecks()`).
113+
// `-c`: Check, perform only specific checks by index (0-`getNumberOfChecks()`).
114+
// Supports multiple values e.g. -c 0 -c 3 -c 4
109115
if (commandLine.hasOption('c')) {
110-
String checkOption = commandLine.getOptionValue('c');
111-
try {
112-
specificCheck = Integer.parseInt(checkOption);
113-
if (specificCheck < 0 || specificCheck >= getNumberOfChecks()) {
114-
specificCheck = -1;
116+
String[] checkOptions = commandLine.getOptionValues('c');
117+
for (String checkOption : checkOptions) {
118+
try {
119+
int checkIndex = Integer.parseInt(checkOption);
120+
if (checkIndex < 0 || checkIndex >= getNumberOfChecks()) {
121+
handler.logError("Invalid value for check: " + checkOption +
122+
". Must be an integer from 0 to " + (getNumberOfChecks() - 1) + ".");
123+
throw new ParseException("Invalid check index: " + checkOption);
124+
}
125+
specificChecks.add(checkIndex);
126+
} catch (NumberFormatException e) {
127+
handler.logError("Invalid value for check: '" + checkOption +
128+
"'. It has to be an integer number from 0 to " + (getNumberOfChecks() - 1) + ".");
129+
throw new ParseException("Invalid check value: " + checkOption);
115130
}
116-
} catch (NumberFormatException e) {
117-
log.info("Invalid value for check. It has to be a number from the displayed range.");
118-
return;
119131
}
120132
}
121133

122-
// `-f`: For, specify the last N days to consider.
134+
// `-f`: For, specify the last N days to consider. Must be a positive integer.
123135
if (commandLine.hasOption('f')) {
124136
String daysOption = commandLine.getOptionValue('f');
125137
try {
126138
forLastNDays = Integer.parseInt(daysOption);
139+
if (forLastNDays <= 0) {
140+
handler.logError("Invalid value for -f: " + daysOption +
141+
". Must be a positive integer (greater than 0).");
142+
throw new ParseException("Invalid -f value: " + daysOption);
143+
}
127144
} catch (NumberFormatException e) {
128-
log.info("Invalid value for last N days. Argument f has to be a number.");
129-
return;
145+
handler.logError("Invalid value for -f: '" + daysOption +
146+
"'. Must be a positive integer.");
147+
throw new ParseException("Invalid -f value: " + daysOption);
130148
}
131149
}
132150

133-
// `-o`: Output, specify a file to save the report.
134-
if (commandLine.hasOption('o')) {
135-
fileName = commandLine.getOptionValue('o');
151+
// `-r`: Report, specify a file to save the report.
152+
if (commandLine.hasOption('r')) {
153+
reportFile = commandLine.getOptionValue('r');
136154
}
137155
}
138156

139157
@Override
140158
public void internalRun() throws Exception {
141-
if (info) {
159+
if (help) {
142160
printHelp();
143161
return;
144162
}
@@ -149,15 +167,14 @@ public void internalRun() throws Exception {
149167
ReportInfo ri = new ReportInfo(this.forLastNDays);
150168

151169
StringBuilder sbReport = new StringBuilder();
152-
sbReport.append("\n\nHEALTH REPORT:\n");
153170

154171
int position = -1;
155172
JSONObject root = new JSONObject();
156173
// Create the array
157174
JSONArray checksArray = new JSONArray();
158-
for (Map.Entry<String, Check> check_entry : Report.checks().entrySet()) {
175+
for (Map.Entry<String, Check> check_entry : checks.entrySet()) {
159176
++position;
160-
if (specificCheck != -1 && specificCheck != position) {
177+
if (!specificChecks.isEmpty() && !specificChecks.contains(position)) {
161178
continue;
162179
}
163180

@@ -197,10 +214,14 @@ public void internalRun() throws Exception {
197214
reportResultService.update(context, reportResult);
198215
context.commit();
199216

217+
// Prepend the header with the persisted report ID so users can refer to it later
218+
String finalReport = "\n\nHEALTH REPORT " + reportResult.getID() + ":\n" + sbReport.toString();
219+
200220
// save output to file
201-
if (fileName != null) {
202-
InputStream inputStream = toInputStream(sbReport.toString(), StandardCharsets.UTF_8);
203-
handler.writeFilestream(context, fileName, inputStream, "export");
221+
if (reportFile != null) {
222+
InputStream inputStream = toInputStream(finalReport, StandardCharsets.UTF_8);
223+
handler.writeFilestream(context, reportFile, inputStream, "export");
224+
context.commit();
204225

205226
context.restoreAuthSystemState();
206227

@@ -213,27 +234,34 @@ public void internalRun() throws Exception {
213234
for (String recipient : emails) {
214235
e.addRecipient(recipient);
215236
}
216-
e.addArgument(sbReport.toString());
237+
e.addArgument(finalReport);
217238
e.send();
239+
handler.logInfo("Report sent to: " + String.join(", ", emails));
218240
} catch (IOException | MessagingException e) {
219241
log.error("Error sending email:", e);
242+
handler.logError("Error sending email to " + String.join(", ", emails)
243+
+ ": " + e.getMessage());
220244
}
221245
}
222246

223-
handler.logInfo(sbReport.toString());
247+
handler.logInfo(finalReport);
224248
}
225249
}
226250

227251
@Override
228252
public void printHelp() {
229-
handler.logInfo("\n\nINFORMATION\nThis process creates a health report of your DSpace.\n" +
253+
int configuredForLastNDays = configurationService.getIntProperty("healthcheck.last_n_days");
254+
handler.logInfo("\n\nHELP\nThis process creates a health report of your DSpace.\n" +
230255
"You can choose from these available options:\n" +
231-
" -i, --info Show help information\n" +
256+
" -h, --help Show help information\n" +
232257
" -e, --email Send report to specified email address\n" +
233-
" -c, --check Perform only specific check by index (0-" + (getNumberOfChecks() - 1) + ")\n" +
234-
" -f, --for Specify the last N days to consider\n" +
235-
" -o, --output Specify a file to save the report\n\n" +
236-
"If you want to execute only one check using -c, use check index:\n" + checksNamesToString() + "\n"
258+
" -c, --check Perform specific check(s) by index (0-" + (getNumberOfChecks() - 1) +
259+
"). Repeat the flag (e.g. -c 1 -c 3) to run multiple checks. " +
260+
"Default: All checks\n" +
261+
" -f, --for Specify the last N days to consider (positive integer). " +
262+
"Default: " + configuredForLastNDays + "\n" +
263+
" -r, --report Specify a file to save the report\n\n" +
264+
"Available checks:\n" + checksNamesToString() + "\n"
237265
);
238266
}
239267

@@ -242,13 +270,21 @@ public void printHelp() {
242270
* This method is used to print the options used for the report.
243271
*/
244272
private String printCommandlineOptions() {
245-
// Return key-value pairs of options
246273
StringBuilder options = new StringBuilder();
274+
Set<String> processedOptions = new LinkedHashSet<>();
275+
247276
for (Option option : commandLine.getOptions()) {
248277
String key = option.getOpt();
249-
String value = commandLine.getOptionValue(key);
250-
if (value != null) {
251-
options.append(String.format(" -%s: %s\n", key, value));
278+
if (key == null || processedOptions.contains(key)) {
279+
continue;
280+
}
281+
processedOptions.add(key);
282+
283+
String[] values = commandLine.getOptionValues(key);
284+
if (values != null && values.length > 0) {
285+
for (String value : values) {
286+
options.append(String.format(" -%s: %s\n", key, value));
287+
}
252288
} else {
253289
options.append(String.format(" -%s\n", key));
254290
}
@@ -295,4 +331,25 @@ public static String getCheckName(int specificCheck) {
295331
}
296332
return null; // should not happen
297333
}
334+
335+
/**
336+
* Create check list from configured healthcheck plugins.
337+
*/
338+
private static LinkedHashMap<String, Check> getChecks() {
339+
LinkedHashMap<String, Check> loadedChecks = new LinkedHashMap<>();
340+
String[] checkNames = DSpaceServicesFactory.getInstance().getConfigurationService()
341+
.getArrayProperty("healthcheck.checks");
342+
PluginService pluginService = CoreServiceFactory.getInstance().getPluginService();
343+
344+
for (String checkName : checkNames) {
345+
Check check = (Check) pluginService.getNamedPlugin(Check.class, checkName);
346+
if (check != null) {
347+
loadedChecks.put(checkName, check);
348+
} else {
349+
log.warn("Could not find implementation for [{}]", checkName);
350+
}
351+
}
352+
353+
return loadedChecks;
354+
}
298355
}

dspace-api/src/main/java/org/dspace/app/healthreport/HealthReportScriptConfiguration.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88
package org.dspace.app.healthreport;
99

10+
import org.apache.commons.cli.Option;
1011
import org.apache.commons.cli.Options;
1112
import org.dspace.scripts.configuration.ScriptConfiguration;
1213

@@ -32,20 +33,24 @@ public void setDspaceRunnableClass(Class<T> dspaceRunnableClass) {
3233
public Options getOptions() {
3334
if (options == null) {
3435
Options options = new Options();
35-
options.addOption("i", "info", false,
36+
options.addOption("h", "help", false,
3637
"Show help information.");
3738
options.addOption("e", "email", true,
3839
"Send report to this email address.");
3940
options.getOption("e").setType(String.class);
40-
options.addOption("c", "check", true,
41-
String.format("Perform only specific check (use index from 0 to %d, " +
42-
"otherwise perform default checks).", HealthReport.getNumberOfChecks() - 1));
43-
options.getOption("c").setType(String.class);
41+
Option checkOption = Option.builder("c").longOpt("check").hasArgs()
42+
.desc(String.format("Perform specific check(s) by index (0 to %d). " +
43+
"Repeat the flag (e.g. -c 1 -c 3) to run multiple checks. " +
44+
"Default: All checks.",
45+
HealthReport.getNumberOfChecks() - 1))
46+
.type(String.class)
47+
.build();
48+
options.addOption(checkOption);
4449
options.addOption("f", "for", true,
45-
"Report for last N days. Used only in general information for now.");
50+
"Report for last N days (positive integer). Used only in general information for now.");
4651
options.getOption("f").setType(String.class);
47-
options.addOption("o", "output", true,
48-
"Save report to the file.");
52+
options.addOption("r", "report", true,
53+
"Specify the report file to store the output.");
4954

5055
super.options = options;
5156
}

0 commit comments

Comments
 (0)