Browse Source

improve repl and mvc structure

master
283375 3 weeks ago
parent
commit
5878ed1eac
Failed to extract signature
  1. 10
      pom.xml
  2. 4
      src/main/java/internal/hw/crawler/Main.java
  3. 25
      src/main/java/internal/hw/crawler/MainController.java
  4. 4
      src/main/java/internal/hw/crawler/commands/Command.java
  5. 11
      src/main/java/internal/hw/crawler/commands/CrawlCommand.java
  6. 5
      src/main/java/internal/hw/crawler/commands/ExitCommand.java
  7. 41
      src/main/java/internal/hw/crawler/commands/HelpCommand.java
  8. 32
      src/main/java/internal/hw/crawler/commands/InputParser.java
  9. 5
      src/main/java/internal/hw/crawler/commands/ListCommand.java
  10. 7
      src/main/java/internal/hw/crawler/commands/SaveCommand.java
  11. 10
      src/main/java/internal/hw/crawler/strategies/crawl/CrawlUtils.java
  12. 1
      src/main/java/internal/hw/crawler/views/CommandOutput.java
  13. 58
      src/main/java/internal/hw/crawler/views/ConsoleView.java
  14. 31
      src/main/java/internal/hw/crawler/views/ProgressTracker.java

10
pom.xml

@ -34,5 +34,15 @@
<artifactId>logback-classic</artifactId>
<version>1.5.25</version>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline</artifactId>
<version>3.27.1</version>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline-terminal-jansi</artifactId>
<version>3.27.1</version>
</dependency>
</dependencies>
</project>

4
src/main/java/internal/hw/crawler/Main.java

@ -20,7 +20,7 @@ public class Main {
controller.registerCommand(new SaveCommand(repository, view));
controller.registerCommand(new HelpCommand(controller.getCommands(), view));
view.printSuccess("Welcome to crawler. Type `help` for a list of available commands.");
view.success("Welcome to crawler. Type `help` for a list of available commands.");
while (true) {
try {
String line = view.readLine();
@ -32,7 +32,7 @@ public class Main {
controller.handleInput(line);
} catch (Exception e) {
log.error("Unhandled exception in REPL loop", e);
view.printError("Unexpected error: " + e.getMessage());
view.error("Unexpected error: " + e.getMessage());
}
}
}

25
src/main/java/internal/hw/crawler/MainController.java

@ -2,6 +2,7 @@ package internal.hw.crawler;
import internal.hw.crawler.commands.Command;
import internal.hw.crawler.commands.CommandArg;
import internal.hw.crawler.commands.InputParser;
import internal.hw.crawler.strategies.crawl.CrawlerException;
import internal.hw.crawler.views.ConsoleView;
import org.slf4j.Logger;
@ -30,31 +31,29 @@ public class MainController {
}
public void handleInput(String input) {
String text = input == null ? "" : input.trim();
if (text.isEmpty()) {
InputParser.ParsedInput parsed = InputParser.parse(input);
if (parsed == null) {
return;
}
String[] args = text.split("\\s+");
String cmdName = args[0].toLowerCase();
Command command = commands.get(cmdName);
Command command = commands.get(parsed.command());
if (command == null) {
view.printError("Unknown command: " + cmdName);
view.error("Unknown command: " + parsed.command());
return;
}
if (!validateArgs(command, args)) {
if (!validateArgs(command, parsed.args())) {
return;
}
try {
command.execute(args);
command.execute(parsed.args());
} catch (CrawlerException e) {
log.warn("Crawler error in command '{}'", cmdName, e);
view.printError(e.getMessage());
log.warn("Crawler error in command '{}'", parsed.command(), e);
view.error(e.getMessage());
} catch (Exception e) {
log.error("Unexpected error in command '{}'", cmdName, e);
view.printError("Internal error: " + e.getMessage());
log.error("Unexpected error in command '{}'", parsed.command(), e);
view.error("Internal error: " + e.getMessage());
}
}
@ -64,7 +63,7 @@ public class MainController {
int provided = args.length - 1;
if (provided < required) {
view.printError("Usage: " + command.getName() + " " + formatUsage(cmdArgs));
view.error("Usage: " + command.getName() + " " + formatUsage(cmdArgs));
return false;
}
return true;

4
src/main/java/internal/hw/crawler/commands/Command.java

@ -10,4 +10,8 @@ public interface Command {
}
void execute(String[] args) throws Exception;
default String getDescription() {
return "";
}
}

11
src/main/java/internal/hw/crawler/commands/CrawlCommand.java

@ -9,6 +9,7 @@ import internal.hw.crawler.strategies.crawl.CrawlNetworkException;
import internal.hw.crawler.strategies.crawl.CrawlParseException;
import internal.hw.crawler.strategies.crawl.CrawlUnsupportedException;
import internal.hw.crawler.views.CommandOutput;
import internal.hw.crawler.views.ProgressTracker;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.slf4j.Logger;
@ -35,6 +36,11 @@ public class CrawlCommand implements Command {
return "crawl";
}
@Override
public String getDescription() {
return "Crawl articles from a supported news website";
}
@Override
public List<CommandArg> getArgs() {
return List.of(new CommandArg("url", "The website to crawl", true));
@ -47,10 +53,11 @@ public class CrawlCommand implements Command {
URL url = new URL(urlRaw);
CrawlStrategy strategy = crawlStrategyFactory.getStrategy(url);
Document doc = Jsoup.connect(url.toString()).timeout(5000).get();
CrawlUtils.setProgressCallback(msg -> System.out.print("\r" + msg));
ProgressTracker tracker = new ProgressTracker(out);
CrawlUtils.setProgressCallback(tracker::update);
try {
List<Article> articles = strategy.parse(url, doc);
System.out.println();
tracker.done();
articles.stream().filter(Objects::nonNull).forEach(repository::add);
out.success(String.format("Crawled %d articles from %s", articles.size(), urlRaw));
} finally {

5
src/main/java/internal/hw/crawler/commands/ExitCommand.java

@ -14,6 +14,11 @@ public class ExitCommand implements Command {
return "exit";
}
@Override
public String getDescription() {
return "Exit the application";
}
@Override
public void execute(String[] args) {
out.info("Goodbye.");

41
src/main/java/internal/hw/crawler/commands/HelpCommand.java

@ -19,6 +19,11 @@ public class HelpCommand implements Command {
return "help";
}
@Override
public String getDescription() {
return "Show available commands or detailed help for a specific command";
}
@Override
public List<CommandArg> getArgs() {
return List.of(
@ -37,10 +42,19 @@ public class HelpCommand implements Command {
private void showAll() {
out.info("Available commands:");
out.print("");
int nameWidth = commands.values().stream()
.mapToInt(c -> c.getName().length()).max().orElse(0) + 4;
for (Command cmd : commands.values()) {
out.print(" " + cmd.getName());
String desc = cmd.getDescription();
if (desc.isEmpty()) {
out.print(" " + cmd.getName());
} else {
out.print(String.format(" %-" + nameWidth + "s %s", cmd.getName(), desc));
}
}
out.info("Type `help <command>` to show detailed help for a specific command.");
out.print("");
out.info("Type `help <command>` for detailed usage of a specific command.");
}
private void showDetail(String name) {
@ -50,18 +64,33 @@ public class HelpCommand implements Command {
return;
}
out.info("Command: " + cmd.getName());
out.info("Command: " + cmd.getName());
String desc = cmd.getDescription();
if (!desc.isEmpty()) {
out.print("Description: " + desc);
}
List<CommandArg> cmdArgs = cmd.getArgs();
if (cmdArgs.isEmpty()) {
out.print(" No arguments.");
out.print("Usage: " + cmd.getName());
return;
}
StringBuilder usage = new StringBuilder(cmd.getName());
for (CommandArg arg : cmdArgs) {
if (arg.required()) {
usage.append(" <").append(arg.name()).append(">");
} else {
usage.append(" [").append(arg.name()).append("]");
}
}
out.print("Usage: " + usage);
out.info("Arguments:");
for (CommandArg arg : cmdArgs) {
out.print(String.format(" %s %s %s",
arg.required() ? "[Required]" : " ",
out.print(String.format(" %-12s %s%s",
arg.name(),
arg.required() ? "(required) " : "(optional) ",
arg.description()));
}
}

32
src/main/java/internal/hw/crawler/commands/InputParser.java

@ -0,0 +1,32 @@
package internal.hw.crawler.commands;
public class InputParser {
private InputParser() {}
public static ParsedInput parse(String input) {
String text = input == null ? "" : input.trim();
if (text.isEmpty()) {
return null;
}
String[] parts = text.split("\\s+");
return new ParsedInput(parts[0].toLowerCase(), parts);
}
public static class ParsedInput {
private final String command;
private final String[] args;
public ParsedInput(String command, String[] args) {
this.command = command;
this.args = args;
}
public String command() {
return command;
}
public String[] args() {
return args;
}
}
}

5
src/main/java/internal/hw/crawler/commands/ListCommand.java

@ -18,6 +18,11 @@ public class ListCommand implements Command {
return "list";
}
@Override
public String getDescription() {
return "List all crawled articles";
}
@Override
public void execute(String[] args) {
for (Article article : articleRepository.getAll()) {

7
src/main/java/internal/hw/crawler/commands/SaveCommand.java

@ -13,7 +13,7 @@ import java.util.function.Function;
import java.util.stream.Collectors;
public class SaveCommand implements Command {
Gson gson = new Gson();
private final Gson gson = new Gson();
private final ArticleRepository articleRepository;
private final CommandOutput out;
@ -27,6 +27,11 @@ public class SaveCommand implements Command {
return "save";
}
@Override
public String getDescription() {
return "Save articles to JSON file";
}
@Override
public void execute(String[] args) throws IOException {
String filename = "articles.output.json";

10
src/main/java/internal/hw/crawler/strategies/crawl/CrawlUtils.java

@ -16,17 +16,17 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class CrawlUtils {
private static final Logger log = LoggerFactory.getLogger(CrawlUtils.class);
private static final int THREAD_POOL_SIZE = 4;
private static final ThreadLocal<Consumer<String>> progressCallback = new ThreadLocal<>();
private static final ThreadLocal<BiConsumer<Integer, Integer>> progressCallback = new ThreadLocal<>();
public static void setProgressCallback(Consumer<String> callback) {
public static void setProgressCallback(BiConsumer<Integer, Integer> callback) {
progressCallback.set(callback);
}
@ -37,7 +37,7 @@ public class CrawlUtils {
public static List<Article> parseHomepage(Document doc, Pattern idRegex,
BiFunction<URL, Document, Article> singleParser) {
HttpCrawler crawler = new HttpCrawler();
Consumer<String> callback = progressCallback.get();
BiConsumer<Integer, Integer> callback = progressCallback.get();
List<String> hrefs = new ArrayList<>();
for (Element link : doc.getElementsByTag("a")) {
@ -70,7 +70,7 @@ public class CrawlUtils {
} finally {
int completed = done.incrementAndGet();
if (callback != null) {
callback.accept("Progress: " + completed + "/" + total);
callback.accept(completed, total);
}
}
}, executor));

1
src/main/java/internal/hw/crawler/views/CommandOutput.java

@ -5,4 +5,5 @@ public interface CommandOutput {
void info(String msg);
void success(String msg);
void error(String msg);
void printInline(String msg);
}

58
src/main/java/internal/hw/crawler/views/ConsoleView.java

@ -1,7 +1,12 @@
package internal.hw.crawler.views;
import java.util.NoSuchElementException;
import java.util.Scanner;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import java.io.IOException;
import java.io.PrintWriter;
public class ConsoleView implements CommandOutput {
private static final String ANSI_RESET = "\u001B[0m";
@ -9,46 +14,59 @@ public class ConsoleView implements CommandOutput {
private static final String ANSI_RED = "\u001B[31m";
private static final String ANSI_BLUE = "\u001B[34m";
private final Scanner scanner = new Scanner(System.in);
private final LineReader reader;
private final PrintWriter writer;
public ConsoleView() {
try {
Terminal terminal = TerminalBuilder.builder()
.system(true)
.build();
this.reader = LineReaderBuilder.builder()
.terminal(terminal)
.build();
this.writer = terminal.writer();
} catch (IOException e) {
throw new RuntimeException("Failed to initialize terminal", e);
}
}
public String readLine() {
System.out.print("> ");
try {
return scanner.nextLine();
} catch (NoSuchElementException | IllegalStateException e) {
String line = reader.readLine("> ");
return (line == null || line.isBlank()) ? null : line.trim();
} catch (org.jline.reader.UserInterruptException | org.jline.reader.EndOfFileException e) {
return null;
}
}
@Override
public void print(String msg) {
System.out.println(msg);
writer.println(msg);
writer.flush();
}
public void printSuccess(String msg) {
System.out.println(ANSI_GREEN + msg + ANSI_RESET);
}
public void printError(String msg) {
System.out.println(ANSI_RED + msg + ANSI_RESET);
}
public void printInfo(String msg) {
System.out.println(ANSI_BLUE + msg + ANSI_RESET);
@Override
public void printInline(String msg) {
writer.print("\r" + msg);
writer.flush();
}
@Override
public void success(String msg) {
printSuccess(msg);
writer.println(ANSI_GREEN + msg + ANSI_RESET);
writer.flush();
}
@Override
public void error(String msg) {
printError(msg);
writer.println(ANSI_RED + msg + ANSI_RESET);
writer.flush();
}
@Override
public void info(String msg) {
printInfo(msg);
writer.println(ANSI_BLUE + msg + ANSI_RESET);
writer.flush();
}
}

31
src/main/java/internal/hw/crawler/views/ProgressTracker.java

@ -0,0 +1,31 @@
package internal.hw.crawler.views;
public class ProgressTracker {
private static final int BAR_WIDTH = 30;
private final CommandOutput out;
public ProgressTracker(CommandOutput out) {
this.out = out;
}
public void update(int done, int total) {
if (total <= 0) return;
int filled = (int) ((double) done / total * BAR_WIDTH);
StringBuilder bar = new StringBuilder("[");
for (int i = 0; i < BAR_WIDTH; i++) {
if (i < filled) {
bar.append("=");
} else if (i == filled) {
bar.append(">");
} else {
bar.append(" ");
}
}
bar.append("] ").append(done).append("/").append(total);
out.printInline(bar.toString());
}
public void done() {
out.print("");
}
}
Loading…
Cancel
Save