feat: Command Line Interface

This commit is contained in:
2025-07-24 20:31:16 +03:00
parent d2aa30b408
commit f58806de5c
13 changed files with 469 additions and 1 deletions

View File

@@ -34,14 +34,19 @@ dependencies {
implementation("org.apache.httpcomponents.client5:httpclient5:5.5") implementation("org.apache.httpcomponents.client5:httpclient5:5.5")
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
implementation("org.jsoup:jsoup:1.21.1") 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(platform("org.junit:junit-bom:$junitVersion"))
testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.junit.jupiter:junit-jupiter")
//noinspection VulnerableLibrariesLocal //noinspection VulnerableLibrariesLocal
testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.1") testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.1")
testImplementation("org.slf4j:slf4j-simple:$slf4jVersion")
} }
test { test {
useJUnitPlatform() useJUnitPlatform()
} }
application {
mainClass = "ru.di9.ihc.cli.CliApp"
}

View File

@@ -1,6 +1,7 @@
package ru.di9.ihc; package ru.di9.ihc;
import com.fasterxml.jackson.databind.ObjectMapper; 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.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
@@ -27,6 +28,10 @@ public class IhcClient {
private boolean isAuth = false; private boolean isAuth = false;
public IhcClient() {
this("https://my.ihc.ru");
}
public IhcClient(String baseUrl) { public IhcClient(String baseUrl) {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.httpClient = HttpClientBuilder.create().build(); this.httpClient = HttpClientBuilder.create().build();

View File

@@ -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<Integer> {
@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;
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,11 @@
package ru.di9.ihc.cli;
import picocli.CommandLine;
import ru.di9.ihc.RecordType;
public class CliRecordTypeConverter implements CommandLine.ITypeConverter<RecordType> {
@Override
public RecordType convert(String s) {
return RecordType.valueOf(s.toUpperCase());
}
}

View File

@@ -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<Integer> {
@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;
}
}

View File

@@ -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;
}

View File

@@ -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<Integer> {
@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<DomainRecord> 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;
}
}

View File

@@ -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<Integer> {
@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<Domain> 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;
}
}

View File

@@ -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<Integer> {
@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;
}
}

View File

@@ -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<TableCol> 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();
}
}

View File

@@ -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<String> 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<String> getRows() {
return Collections.unmodifiableList(rows);
}
}

View File

@@ -0,0 +1 @@
lombok.log.flagUsage=error