From f58806de5cb561f397764d57d060f4327770b83a Mon Sep 17 00:00:00 2001 From: Voomra Date: Thu, 24 Jul 2025 20:31:16 +0300 Subject: [PATCH] feat: Command Line Interface --- build.gradle | 7 +- src/main/java/ru/di9/ihc/IhcClient.java | 5 ++ .../di9/ihc/cli/AddDomainRecordCommand.java | 56 ++++++++++++++++ src/main/java/ru/di9/ihc/cli/CliApp.java | 39 +++++++++++ .../di9/ihc/cli/CliRecordTypeConverter.java | 11 ++++ .../ihc/cli/DeleteDomainRecordCommand.java | 50 +++++++++++++++ .../ru/di9/ihc/cli/DomainRecordCommand.java | 21 ++++++ .../di9/ihc/cli/DomainRecordsListCommand.java | 62 ++++++++++++++++++ .../ru/di9/ihc/cli/DomainsListCommand.java | 50 +++++++++++++++ .../di9/ihc/cli/EditDomainRecordCommand.java | 64 +++++++++++++++++++ src/main/java/ru/di9/ihc/cli/Table.java | 63 ++++++++++++++++++ src/main/java/ru/di9/ihc/cli/TableCol.java | 41 ++++++++++++ src/main/java/ru/di9/ihc/lombok.config | 1 + 13 files changed, 469 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ru/di9/ihc/cli/AddDomainRecordCommand.java create mode 100644 src/main/java/ru/di9/ihc/cli/CliApp.java create mode 100644 src/main/java/ru/di9/ihc/cli/CliRecordTypeConverter.java create mode 100644 src/main/java/ru/di9/ihc/cli/DeleteDomainRecordCommand.java create mode 100644 src/main/java/ru/di9/ihc/cli/DomainRecordCommand.java create mode 100644 src/main/java/ru/di9/ihc/cli/DomainRecordsListCommand.java create mode 100644 src/main/java/ru/di9/ihc/cli/DomainsListCommand.java create mode 100644 src/main/java/ru/di9/ihc/cli/EditDomainRecordCommand.java create mode 100644 src/main/java/ru/di9/ihc/cli/Table.java create mode 100644 src/main/java/ru/di9/ihc/cli/TableCol.java create mode 100644 src/main/java/ru/di9/ihc/lombok.config diff --git a/build.gradle b/build.gradle index 879fa1b..b12d9d5 100644 --- a/build.gradle +++ b/build.gradle @@ -34,14 +34,19 @@ dependencies { implementation("org.apache.httpcomponents.client5:httpclient5:5.5") implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") implementation("org.jsoup:jsoup:1.21.1") + implementation("org.slf4j:slf4j-simple:$slf4jVersion") + implementation("info.picocli:picocli:4.7.7") testImplementation(platform("org.junit:junit-bom:$junitVersion")) testImplementation("org.junit.jupiter:junit-jupiter") //noinspection VulnerableLibrariesLocal testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.1") - testImplementation("org.slf4j:slf4j-simple:$slf4jVersion") } test { useJUnitPlatform() } + +application { + mainClass = "ru.di9.ihc.cli.CliApp" +} diff --git a/src/main/java/ru/di9/ihc/IhcClient.java b/src/main/java/ru/di9/ihc/IhcClient.java index 9f0015b..b247d8c 100644 --- a/src/main/java/ru/di9/ihc/IhcClient.java +++ b/src/main/java/ru/di9/ihc/IhcClient.java @@ -1,6 +1,7 @@ package ru.di9.ihc; import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; @@ -27,6 +28,10 @@ public class IhcClient { private boolean isAuth = false; + public IhcClient() { + this("https://my.ihc.ru"); + } + public IhcClient(String baseUrl) { this.baseUrl = baseUrl; this.httpClient = HttpClientBuilder.create().build(); diff --git a/src/main/java/ru/di9/ihc/cli/AddDomainRecordCommand.java b/src/main/java/ru/di9/ihc/cli/AddDomainRecordCommand.java new file mode 100644 index 0000000..1817b1e --- /dev/null +++ b/src/main/java/ru/di9/ihc/cli/AddDomainRecordCommand.java @@ -0,0 +1,56 @@ +package ru.di9.ihc.cli; + +import lombok.Getter; +import lombok.Setter; +import picocli.CommandLine; +import ru.di9.ihc.Domain; +import ru.di9.ihc.IhcClient; +import ru.di9.ihc.RecordType; + +import java.util.concurrent.Callable; + +@Getter +@Setter +@CommandLine.Command( + name = "add", + description = "Добавить запись домена") +public class AddDomainRecordCommand implements Callable { + @CommandLine.Option(names = {"-h", "--help"}, description = "Страница помощи", usageHelp = true) + private boolean flagHelp; + + @CommandLine.Option(names = {"-u", "--username"}, description = "Имя пользователя", required = true) + private String username; + + @CommandLine.Option(names = {"-p", "--password"}, description = "Пароль пользователя", required = true, arity = "0..1", interactive = true) + private String password; + + @CommandLine.Parameters(index = "0", description = "Имя домена") + private String domainName; + + @CommandLine.Option(names = {"--name"}, description = "Имя записи") + private String recordName; + + @CommandLine.Option(names = {"--type"}, description = "Тип записи", converter = CliRecordTypeConverter.class, required = true) + private RecordType recordType; + + @CommandLine.Option(names = {"--content"}, description = "Контент", required = true) + private String recordContent; + + @CommandLine.Option(names = {"--priority"}, description = "Приоритет") + private Integer recordPriority; + + @Override + public Integer call() throws Exception { + IhcClient ihc = new IhcClient(); + if (!ihc.auth(username, password)) { + System.err.println("Неверное имя пользователя или пароль"); + return -1; + } + + Domain domain = ihc.getDomains().stream().filter(d -> d.name().equalsIgnoreCase(domainName)).findFirst() + .orElseThrow(() -> new RuntimeException("DOMAIN '%s' NOT EXISTS".formatted(domainName))); + + ihc.addDomainRecord(domain, recordName, recordType, recordContent, recordPriority); + return 0; + } +} diff --git a/src/main/java/ru/di9/ihc/cli/CliApp.java b/src/main/java/ru/di9/ihc/cli/CliApp.java new file mode 100644 index 0000000..effd318 --- /dev/null +++ b/src/main/java/ru/di9/ihc/cli/CliApp.java @@ -0,0 +1,39 @@ +package ru.di9.ihc.cli; + +import lombok.Setter; +import lombok.ToString; +import picocli.CommandLine; + +@Setter +@ToString +@CommandLine.Command( + name = "app", + description = "IHC DNS Tools", + subcommands = { + DomainsListCommand.class, + DomainRecordCommand.class + }, + version = "1.0-SNAPSHOT") +public class CliApp { + @CommandLine.Option(names = {"-h", "--help"}, description = "Страница помощи", usageHelp = true) + private boolean flagHelp; + + @CommandLine.Option(names = {"-v", "--version"}, description = "Версия программы", versionHelp = true) + private boolean flagVersion; + + public static void main(String[] args) { + CliApp cliApp = new CliApp(); + CommandLine commandLine = new CommandLine(cliApp); + commandLine.parseArgs(args); + + if (cliApp.flagHelp) { + CommandLine.usage(cliApp, System.out); + return; + } else if (cliApp.flagVersion) { + commandLine.printVersionHelp(System.out); + return; + } + + System.exit(commandLine.execute(args)); + } +} diff --git a/src/main/java/ru/di9/ihc/cli/CliRecordTypeConverter.java b/src/main/java/ru/di9/ihc/cli/CliRecordTypeConverter.java new file mode 100644 index 0000000..df62852 --- /dev/null +++ b/src/main/java/ru/di9/ihc/cli/CliRecordTypeConverter.java @@ -0,0 +1,11 @@ +package ru.di9.ihc.cli; + +import picocli.CommandLine; +import ru.di9.ihc.RecordType; + +public class CliRecordTypeConverter implements CommandLine.ITypeConverter { + @Override + public RecordType convert(String s) { + return RecordType.valueOf(s.toUpperCase()); + } +} diff --git a/src/main/java/ru/di9/ihc/cli/DeleteDomainRecordCommand.java b/src/main/java/ru/di9/ihc/cli/DeleteDomainRecordCommand.java new file mode 100644 index 0000000..3a4c75d --- /dev/null +++ b/src/main/java/ru/di9/ihc/cli/DeleteDomainRecordCommand.java @@ -0,0 +1,50 @@ +package ru.di9.ihc.cli; + +import lombok.Getter; +import lombok.Setter; +import picocli.CommandLine; +import ru.di9.ihc.Domain; +import ru.di9.ihc.DomainRecord; +import ru.di9.ihc.IhcClient; + +import java.util.concurrent.Callable; + +@Getter +@Setter +@CommandLine.Command( + name = "delete", + description = "Удалить запись домена") +public class DeleteDomainRecordCommand implements Callable { + @CommandLine.Option(names = {"-h", "--help"}, description = "Страница помощи", usageHelp = true) + private boolean flagHelp; + + @CommandLine.Option(names = {"-u", "--username"}, description = "Имя пользователя", required = true) + private String username; + + @CommandLine.Option(names = {"-p", "--password"}, description = "Пароль пользователя", required = true, arity = "0..1", interactive = true) + private String password; + + @CommandLine.Parameters(index = "0", description = "Имя домена") + private String domainName; + + @CommandLine.Parameters(index = "1", description = "Имя записи") + private String recordName; + + @Override + public Integer call() throws Exception { + IhcClient ihc = new IhcClient(); + if (!ihc.auth(username, password)) { + System.err.println("Неверное имя пользователя или пароль"); + return -1; + } + + Domain domain = ihc.getDomains().stream().filter(d -> d.name().equalsIgnoreCase(domainName)).findFirst() + .orElseThrow(() -> new RuntimeException("DOMAIN '%s' NOT EXISTS".formatted(domainName))); + + DomainRecord record = ihc.getDomainRecords(domain).stream().filter(r -> r.getName().equals(recordName)).findFirst() + .orElseThrow(() -> new RuntimeException("RECORD '%s' FOR DOMAIN '%s' NOT EXISTS".formatted(recordName, domainName))); + + ihc.removeDomainRecord(domain, record.getId()); + return 0; + } +} diff --git a/src/main/java/ru/di9/ihc/cli/DomainRecordCommand.java b/src/main/java/ru/di9/ihc/cli/DomainRecordCommand.java new file mode 100644 index 0000000..a081a1e --- /dev/null +++ b/src/main/java/ru/di9/ihc/cli/DomainRecordCommand.java @@ -0,0 +1,21 @@ +package ru.di9.ihc.cli; + +import lombok.Getter; +import lombok.Setter; +import picocli.CommandLine; + +@Getter +@Setter +@CommandLine.Command( + name = "domain-record", + description = "Работа с записями домена", + subcommands = { + DomainRecordsListCommand.class, + AddDomainRecordCommand.class, + EditDomainRecordCommand.class, + DeleteDomainRecordCommand.class + }) +public class DomainRecordCommand { + @CommandLine.Option(names = {"-h", "--help"}, description = "Страница помощи", usageHelp = true) + private boolean flagHelp; +} diff --git a/src/main/java/ru/di9/ihc/cli/DomainRecordsListCommand.java b/src/main/java/ru/di9/ihc/cli/DomainRecordsListCommand.java new file mode 100644 index 0000000..61b647f --- /dev/null +++ b/src/main/java/ru/di9/ihc/cli/DomainRecordsListCommand.java @@ -0,0 +1,62 @@ +package ru.di9.ihc.cli; + +import lombok.Getter; +import lombok.Setter; +import picocli.CommandLine; +import ru.di9.ihc.Domain; +import ru.di9.ihc.DomainRecord; +import ru.di9.ihc.IhcClient; + +import java.util.List; +import java.util.concurrent.Callable; + +@Getter +@Setter +@CommandLine.Command( + name = "list", + description = "Список записей доменов") +public class DomainRecordsListCommand implements Callable { + @CommandLine.Option(names = {"-h", "--help"}, description = "Страница помощи", usageHelp = true) + private boolean flagHelp; + + @CommandLine.Option(names = {"-u", "--username"}, description = "Имя пользователя", required = true) + private String username; + + @CommandLine.Option(names = {"-p", "--password"}, description = "Пароль пользователя", required = true, arity = "0..1", interactive = true) + private String password; + + @CommandLine.Parameters(index = "0", description = "Имя домена") + private String domainName; + + @Override + public Integer call() throws Exception { + IhcClient ihc = new IhcClient(); + if (!ihc.auth(username, password)) { + System.err.println("Неверное имя пользователя или пароль"); + return -1; + } + + Domain domain = ihc.getDomains().stream().filter(d -> d.name().equalsIgnoreCase(domainName)).findFirst() + .orElseThrow(() -> new RuntimeException("DOMAIN '%s' NOT EXISTS".formatted(domainName))); + List domainRecords = ihc.getDomainRecords(domain); + + var colId = new TableCol("ID"); + var colName = new TableCol("Запись"); + var colType = new TableCol("Тип"); + var colPriority = new TableCol("Приоритет"); + var colContent = new TableCol("Контент"); + var colReadOnly = new TableCol("Только для чтения"); + + for (DomainRecord record : domainRecords) { + colId.putRow(record.getId()); + colName.putRow(record.getName()); + colType.putRow(record.getType().name()); + colPriority.putRow(record.getPriority()); + colContent.putRow(record.getContent()); + colReadOnly.putRow(record.isReadOnly() ? "да" : "нет"); + } + + System.out.println(new Table(colId, colName, colType, colPriority, colContent, colReadOnly).print()); + return 0; + } +} diff --git a/src/main/java/ru/di9/ihc/cli/DomainsListCommand.java b/src/main/java/ru/di9/ihc/cli/DomainsListCommand.java new file mode 100644 index 0000000..b57696d --- /dev/null +++ b/src/main/java/ru/di9/ihc/cli/DomainsListCommand.java @@ -0,0 +1,50 @@ +package ru.di9.ihc.cli; + +import lombok.Getter; +import lombok.Setter; +import picocli.CommandLine; +import ru.di9.ihc.Domain; +import ru.di9.ihc.IhcClient; + +import java.util.List; +import java.util.concurrent.Callable; + +@Getter +@Setter +@CommandLine.Command( + name = "domains", + description = "Список доменов") +public class DomainsListCommand implements Callable { + @CommandLine.Option(names = {"-h", "--help"}, description = "Страница помощи", usageHelp = true) + private boolean flagHelp; + + @CommandLine.Option(names = {"-u", "--username"}, description = "Имя пользователя", required = true) + private String username; + + @CommandLine.Option(names = {"-p", "--password"}, description = "Пароль пользователя", required = true, arity = "0..1", interactive = true) + private String password; + + @Override + public Integer call() { + IhcClient ihc = new IhcClient(); + if (!ihc.auth(username, password)) { + System.err.println("Неверное имя пользователя или пароль"); + return -1; + }; + + List domains = ihc.getDomains(); + + var colId = new TableCol("ID"); + var colDomain = new TableCol("Домен"); + var colPunycode = new TableCol("Punycode"); + + for (Domain domain : domains) { + colId.putRow(domain.id()); + colDomain.putRow(domain.name()); + colPunycode.putRow(domain.punycode()); + } + + System.out.println(new Table(colId, colDomain, colPunycode).print()); + return 0; + } +} diff --git a/src/main/java/ru/di9/ihc/cli/EditDomainRecordCommand.java b/src/main/java/ru/di9/ihc/cli/EditDomainRecordCommand.java new file mode 100644 index 0000000..ccccf1f --- /dev/null +++ b/src/main/java/ru/di9/ihc/cli/EditDomainRecordCommand.java @@ -0,0 +1,64 @@ +package ru.di9.ihc.cli; + +import lombok.Getter; +import lombok.Setter; +import picocli.CommandLine; +import ru.di9.ihc.Domain; +import ru.di9.ihc.DomainRecord; +import ru.di9.ihc.IhcClient; +import ru.di9.ihc.RecordType; + +import java.util.concurrent.Callable; + +@Getter +@Setter +@CommandLine.Command( + name = "edit", + description = "Изменить запись домена") +public class EditDomainRecordCommand implements Callable { + @CommandLine.Option(names = {"-h", "--help"}, description = "Страница помощи", usageHelp = true) + private boolean flagHelp; + + @CommandLine.Option(names = {"-u", "--username"}, description = "Имя пользователя", required = true) + private String username; + + @CommandLine.Option(names = {"-p", "--password"}, description = "Пароль пользователя", required = true, arity = "0..1", interactive = true) + private String password; + + @CommandLine.Parameters(index = "0", description = "Имя домена") + private String domainName; + + @CommandLine.Parameters(index = "1", description = "Имя записи") + private String recordName; + + @CommandLine.Option(names = {"--name"}, description = "Новое имя записи") + private String recordNewName; + + @CommandLine.Option(names = {"--content"}, description = "Контент") + private String recordContent; + + @CommandLine.Option(names = {"--priority"}, description = "Приоритет") + private Integer recordPriority; + + @Override + public Integer call() throws Exception { + IhcClient ihc = new IhcClient(); + if (!ihc.auth(username, password)) { + System.err.println("Неверное имя пользователя или пароль"); + return -1; + } + + Domain domain = ihc.getDomains().stream().filter(d -> d.name().equalsIgnoreCase(domainName)).findFirst() + .orElseThrow(() -> new RuntimeException("DOMAIN '%s' NOT EXISTS".formatted(domainName))); + + DomainRecord record = ihc.getDomainRecords(domain).stream().filter(r -> r.getName().equals(recordName)).findFirst() + .orElseThrow(() -> new RuntimeException("RECORD '%s' FOR DOMAIN '%s' NOT EXISTS".formatted(recordName, domainName))); + + if (recordNewName != null) record.setName(recordNewName); + if (recordContent != null) record.setContent(recordContent); + if (recordPriority != null) record.setPriority(recordPriority); + + ihc.updateDomainRecord(domain, record); + return 0; + } +} diff --git a/src/main/java/ru/di9/ihc/cli/Table.java b/src/main/java/ru/di9/ihc/cli/Table.java new file mode 100644 index 0000000..5921cb3 --- /dev/null +++ b/src/main/java/ru/di9/ihc/cli/Table.java @@ -0,0 +1,63 @@ +package ru.di9.ihc.cli; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Table { + private final List cols = new ArrayList<>(); + + public Table(TableCol... cols) { + this.cols.addAll(Arrays.asList(cols)); + } + + public String print() { + Object[] headers = new Object[cols.size()]; + String formatRow; + String strLine; + int maxRows = 0; + + { + StringBuilder sbFormatRow = new StringBuilder(); + StringBuilder sbStrLine = new StringBuilder(); + + for (int i = 0; i < cols.size(); i++) { + TableCol col = cols.get(i); + + headers[i] = col.getHeader(); + maxRows = Math.max(maxRows, col.getRows().size()); + + sbFormatRow.append(" %%-%ds ".formatted(col.getSize())); + sbStrLine.append("-".repeat(col.getSize() + 2)); + + if (i < cols.size() - 1) { + sbFormatRow.append('|'); + sbStrLine.append('|'); + } + } + + formatRow = sbFormatRow.toString(); + strLine = sbStrLine.toString(); + } + + StringBuilder result = new StringBuilder(); + result.append(formatRow.formatted(headers)).append('\n') + .append(strLine).append('\n'); + + Object[] cells; + for (int i = 0; i < maxRows; i++) { + cells = new Object[headers.length]; + for (int j = 0; j < cols.size(); j++) { + cells[j] = cols.get(j).getRows().get(i); + } + + result.append(formatRow.formatted(cells)); + + if (i < maxRows - 1) { + result.append('\n'); + } + } + + return result.toString(); + } +} diff --git a/src/main/java/ru/di9/ihc/cli/TableCol.java b/src/main/java/ru/di9/ihc/cli/TableCol.java new file mode 100644 index 0000000..2d6debf --- /dev/null +++ b/src/main/java/ru/di9/ihc/cli/TableCol.java @@ -0,0 +1,41 @@ +package ru.di9.ihc.cli; + +import lombok.Getter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class TableCol { + private final List rows = new ArrayList<>(); + + @Getter + private final String header; + + @Getter + private int size; + + public TableCol(String header) { + this.header = header; + this.size = header.length(); + } + + public void putRow(Integer row) { + if (row == null) putRow((String) null); + else putRow(String.valueOf(row)); + } + + public void putRow(String row) { + if (row == null) { + rows.add(""); + return; + } + + rows.add(row); + size = Math.max(size, row.length()); + } + + public List getRows() { + return Collections.unmodifiableList(rows); + } +} diff --git a/src/main/java/ru/di9/ihc/lombok.config b/src/main/java/ru/di9/ihc/lombok.config new file mode 100644 index 0000000..4182b13 --- /dev/null +++ b/src/main/java/ru/di9/ihc/lombok.config @@ -0,0 +1 @@ +lombok.log.flagUsage=error \ No newline at end of file