Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/main/java/com/javarush/emirzakov/input.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
When Al Templeton called me on the seventh of June 2011, and told me to come down to the diner, he sounded like a man who was gargling with nails.
He also sounded like he was about to cry.
Since Al was the toughest man I had ever known (my old man included), this was scary.
"What's wrong?" I asked.
"Are you sick?"
"Just get here, Jake.
Please."
He hung up.
I went.
He’d given me a job when I needed one, washing dishes and helping him short-order.
I owed him.
Besides, I was worried.
I hadn't seen him in...
when?
Mid-May, I guess.
That was when he'd put the CLOSED FOR REPAIRS sign on the door of the fat-boy diner.
When I asked him what repairs, he’d been vague.
The sign was still on the door, but the door was unlocked.
I went in.
The place was dark and hot.
The shades were pulled.
"Al?"
"In the booth," he whispered.
I walked past the counter, and my footsteps echoed in the empty room.
He was in the back booth, the one we called the Office.
He was sitting in the shadows.
"Al, what's going on?
You look..."
I stopped.
Because he didn't look.
Not like Al.
The Al Templeton I knew was a big man, six-two and at least two-thirty, most of it gut.
He was hearty.
He was loud.
The man in the booth was a scarecrow.
His cheeks were hollows.
His eyes were huge and staring.
His hair, which had been graying, was now almost entirely white, and it was thin, wispy.
He was wearing one of his endless supply of flannel shirts, and it hung on him like a tent.
He must have lost sixty pounds.
"Jesus, Al," I said.
"What happened to you?
Cancer?"
He nodded.
"Lung.
Stage four.
They found it when I...
when I came back."
"Came back?
From where?
Florida?
I thought you were just closing for repairs."
"I lied," he said, and his voice was a dry rasp.
"I haven't been in Florida, Jake.
I've been in 1958."
I stared at him.
He stared back.
His eyes were perfectly steady.
He meant it.
"You're insane," I said.
"No.
I'm dying.
And I need you to listen to me.
I need you to listen to me very, very carefully.
Because I’m going to tell you something that will change your life.
And maybe change the world."
He pointed toward the pantry.
"It started in there.
In the back.
There's a...
a bubble.
A rabbit-hole.
And it goes to the past."
17 changes: 17 additions & 0 deletions src/main/java/com/javarush/emirzakov/io/FileReaderService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.javarush.emirzakov.io;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class FileReaderService {

public String readFromFile(String path) {
try {
return Files.readString(Path.of(path));
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
return "";
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Никогда не 'проглатывайте' исключения, возвращая пустую строку. Выбрасывайте кастомное RuntimeException или используйте запечатанные интерфейсы (Sealed Interfaces) для обработки ошибок в Java 21 стиле.

}
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/javarush/emirzakov/io/FileWriterService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.javarush.emirzakov.io;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class FileWriterService {
public void writeFile(String path, CharSequence content) {
try {
Files.writeString(Path.of(path), content);
System.out.println("Files successfully written to: " + path);
} catch (IOException e) {
System.out.println("Error writing to file: " + e.getMessage());
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Логирование через System.out.println в сервисах — плохая практика. Используйте SLF4J/Logback или верните статус выполнения.

}
}
}
110 changes: 110 additions & 0 deletions src/main/java/com/javarush/emirzakov/main/java/Runner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.javarush.emirzakov.main.java;


import com.javarush.emirzakov.io.FileReaderService;
import com.javarush.emirzakov.io.FileWriterService;
import com.javarush.emirzakov.service.BruteForceAction;
import com.javarush.emirzakov.service.DecryptAction;
import com.javarush.emirzakov.service.EncryptAction;
import com.javarush.emirzakov.service.TextUtils;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Scanner;

public class Runner {
public static void main(String[] args) {

FileReaderService fileReader = new FileReaderService();

try (Scanner scanner = new Scanner(System.in)) {
System.out.println("Enter text directly, or specify a file path: ");
String input = scanner.nextLine();

String text;
Path path = Path.of(input);
if (Files.exists(path) && Files.isRegularFile(path)) {
System.out.println("File has been found and read!");
text = fileReader.readFromFile(input);
} else {
text = input;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Используйте try-with-resources для всех I/O операций. Хотя Scanner закрыт, логика работы с файлами разбросана. Инкапсулируйте проверку существования файла в сервис.

System.out.println("Using input text directly");
}
String normalized = TextUtils.normalize(text);

System.out.println("Normalized text: ");
System.out.println(normalized.substring(0, Math.min(300, normalized.length())) + "...");


System.out.println("\nChoose action: ");
System.out.println("1 - Encrypt");
System.out.println("2 - Decrypt");
System.out.println("3 - Brute Force (try to find key automatically)");

int choice = readInt(scanner, "Enter choice (1-3): ");
String message = "Enter key (integer): ";
String result = null;

switch (choice) {
case 1 -> {
int key = readInt(scanner, message);
EncryptAction encryptAction = new EncryptAction();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Названия переменных choice и result слишком общие. В Clean Code приветствуются говорящие названия, например userAction и processedText.

result = encryptAction.execute(text, key);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вместо switch-case с ручным созданием объектов, используйте Map<Integer, CryptoAction> для реализации паттерна Strategy. Это избавит от 'лестницы' условий.

}
case 2 -> {
int key = readInt(scanner, message);
DecryptAction decryptAction = new DecryptAction();
result = decryptAction.execute(text, key);
}
case 3 -> {
BruteForceAction bf = new BruteForceAction();
BruteForceAction.Result best = bf.findBestByStopWords(text);

if (best != null) {
System.out.println("\n Brute-force result:");
System.out.println("Key: " + best.key + " Score: " + best.score);
System.out.println("Candidate text:\n" + best.text);
result = best.text;
} else {
System.out.println("No likely candidate found by brute force.");
}
}
default -> System.out.println("Unknown action. Exiting.");
}
if (result != null) {
System.out.println("\n=== Result ===");
System.out.println(result);

System.out.println("\nDo you want to save the result to a file? (y/n)");
String answer = scanner.nextLine().trim().toLowerCase();
if (answer.equals("y")) {
System.out.println("Enter path to file: ");
String filePath = scanner.nextLine().trim();

FileWriterService fileWriter = new FileWriterService();
fileWriter.writeFile(filePath, result);
}
}
}
}

private static int readInt(Scanner scanner, String prompt) {
while (true) {
System.out.println(prompt);
String line = scanner.nextLine().trim();
if (line.isEmpty()) {
System.out.println("Please enter 1, 2, or 3, then press Enter.");
continue;
}
if (!line.matches("\\d+")) {
System.out.println("Please enter a valid integer.");
continue;
}
try {
return Integer.parseInt(line);
} catch (NumberFormatException e) {
System.out.println("Number too large. Try again.");
}
}
}
}
38 changes: 38 additions & 0 deletions src/main/java/com/javarush/emirzakov/model/Alphabet.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.javarush.emirzakov.model;

public class Alphabet {
private final char[] ALPHABET;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Поле ALPHABET нарушает naming convention. Поля экземпляра должны быть в camelCase (alphabet), даже если они final.


private static final char[] ENGLISH = "abcdefghijklmnopqrstuvwxyz".toCharArray();

private static final char[] RUSSIAN = "абвгдеёжзийклмнопрстуфхцчшщъыьэюя".toCharArray();


public Alphabet(String text) {
if (text.matches(".*[а-яА-ЯёЁ].*")) {
ALPHABET = RUSSIAN;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Логика выбора алфавита в конструкторе нарушает SRP. Лучше использовать паттерн Factory или статические фабричные методы, возвращающие разные экземпляры Alphabet.

} else {
ALPHABET = ENGLISH;
}
}

public int getCharIndex(char c) {
for (int i = 0; i < ALPHABET.length; i++) {
if (Character.toLowerCase(ALPHABET[i]) == Character.toLowerCase(c)) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Линейный поиск в массиве getCharIndex неэффективен (O(n)). Используйте Map<Character, Integer> для поиска за O(1).

return i;
}
}
return -1;
}

public char getCharByIndex(int index) {
int i = index % ALPHABET.length;
if (i < 0) i += ALPHABET.length;
return ALPHABET[i];
}

public int length() {
return ALPHABET.length;
}

}
20 changes: 20 additions & 0 deletions src/main/java/com/javarush/emirzakov/model/StopWords.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.javarush.emirzakov.model;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public final class StopWords {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Класс StopWords стоит сделать Enum или использовать Sealed class, если планируется разная логика для разных языков.

public static final Set<String> RUSSIAN = new HashSet<>(Arrays.asList(
"и", "в", "на", "не", "что", "как", "я", "он", "она", "но", "с",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Используйте Set.of(...) вместо HashSet(Arrays.asList(...)). В Java 9+ это создает немодифицируемое множество и выглядит чище.

"за", "по", "то", "этот", "это", "для", "вы", "мы", "они"
));
public static final Set<String> ENGLISH = new HashSet<>(Arrays.asList(
"and", "the", "in", "of", "to", "a", "is", "it", "by", "for",
"on", "that", "this", "with", "as", "you", "I", "we", "he", "she"
));

private StopWords() {

}
}
71 changes: 71 additions & 0 deletions src/main/java/com/javarush/emirzakov/service/BruteForceAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.javarush.emirzakov.service;

import com.javarush.emirzakov.model.Alphabet;
import com.javarush.emirzakov.model.StopWords;

import java.util.List;
import java.util.Set;

public class BruteForceAction {

public static class Result {
public final int key;
public final String text;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Используйте Java 16+ Records вместо вложенного статического класса Result. Это сократит код и сделает данные иммутабельными (Data Transfer Object).

public final int score;

public Result(int key, String text, int score) {
this.key = key;
this.text = text;
this.score = score;
}
}

public Result findBestByStopWords(String cipherText) {
if (cipherText == null || cipherText.isBlank()) return null;

DecryptAction decryptAction = new DecryptAction();
int N = new Alphabet(cipherText).length();

int bestScore = -1;
Result best = null;

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Метод findBestByStopWords слишком длинный. Разбейте его на мелкие методы: calculateScore, processCandidate.

for (int k = 0; k < N; k++) {
String candidate = decryptAction.execute(cipherText, k);
String sample = shortenForAnalysis(candidate);
String norm = TextUtils.normalize(sample);
List<String> tokens = TextUtils.tokenize(norm);

int rusMatches = countStopWords(tokens, StopWords.RUSSIAN);
int engMatches = countStopWords(tokens, StopWords.ENGLISH);
int matches = Math.max(rusMatches, engMatches);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вызов new Alphabet(cipherText) внутри цикла — это избыточное выделение памяти. Алфавит не меняется, его нужно создать один раз до цикла.


if (matches > bestScore) {
bestScore = matches;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Магическое число 1000 для shortenForAnalysis следует вынести в именованную константу (static final).

best = new Result(k, candidate, matches);
}
}
return best;
}

private String shortenForAnalysis(String text) {
int maxLen = 1000;
if (text == null) return "";
if (text.length() <= maxLen) return text;

int cutIndex = text.lastIndexOf(' ', maxLen);
if (cutIndex == -1) {
cutIndex = maxLen;
}
return text.substring(0, cutIndex).trim();
}

private int countStopWords(List<String> tokens, Set<String> stopSet) {
if (tokens == null || tokens.isEmpty() || stopSet == null || stopSet.isEmpty()) return 0;
int count = 0;
for (String t : tokens) {
if (stopSet.contains(t)) count++;
}
return count;
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Используйте Stream API для подсчета слов: tokens.stream().filter(stopSet::contains).count(). Это декларативно и читаемо.

}
22 changes: 22 additions & 0 deletions src/main/java/com/javarush/emirzakov/service/CryptoAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.javarush.emirzakov.service;

import com.javarush.emirzakov.model.Alphabet;

public abstract class CryptoAction {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сделайте CryptoAction sealed интерфейсом, разрешая только конкретные реализации (Encrypt/Decrypt). Это обеспечит безопасность иерархии в Java 21.


protected char shiftChar(char c, int key, Alphabet alphabet) {
if (!Character.isLetter(c)) return c;

boolean isUpper = Character.isUpperCase(c);
char lower = Character.toLowerCase(c);
int index = alphabet.getCharIndex(lower);

if (index == -1) {
return c;
}
char shifted = alphabet.getCharByIndex(index + key);
return isUpper ? Character.toUpperCase(shifted) : shifted;
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В Java 21+ для выбора реализации или обработки данных стоит использовать Pattern Matching для switch, если планируется расширение типов действий.

public abstract String execute(String text, int key);
}
17 changes: 17 additions & 0 deletions src/main/java/com/javarush/emirzakov/service/DecryptAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.javarush.emirzakov.service;

import com.javarush.emirzakov.model.Alphabet;

public class DecryptAction extends CryptoAction {

@Override
public String execute(String text, int key) {
Alphabet alphabet = new Alphabet(text);
StringBuilder result = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
result.append(shiftChar(ch, -key, alphabet));
}
return result.toString();
}
}
Loading