Практикум Swing — XML-валидатор
О практикуме
Соберём десктопное приложение "XML Validator" на Swing и Maven: два редактора (XML и XSD), кнопка "Проверить", панель результата с номерами строк и столбцов ошибок. Пользователь может выбрать файлы через диалог или вставить текст прямо в окно — типичный сценарий утилиты поддержки, когда нужно быстро проверить фрагмент конфигурации или целый документ.
Стек проекта:
- Maven JAR — стандартная структура Java-проекта (структура и сборки);
- Swing (
javax.swing) — окно, панели, кнопки,JTextArea,JFileChooser(обзор GUI, рецепты элементов); javax.xml.validation— валидация XML по XSD из JDK, без сторонних библиотек (общая база по разметке — XML);SwingWorker— проверка в фоне, чтобы UI не "замирал" (EDT и потоки).
Для новых desktop-проектов чаще берут JavaFX, но Swing остаётся в JDK, встречается в legacy-утилитах и internal tools. Здесь — полноценный Maven-проект с разделением model / service / ui, фоновой задачей и работой с XML. Те же идеи переносятся на JavaFX: Task вместо SwingWorker, Platform.runLater вместо SwingUtilities.invokeLater — см. Swing и потоки.
Well-formed и valid — два уровня проверки XML
Прежде чем писать код, разделим два понятия из раздела про XML:
| Уровень | Вопрос | Кто отвечает |
|---|---|---|
| Well-formed | Документ синтаксически корректен? (закрытые теги, одна корневая нода) | SAX-парсер |
| Valid | Документ соответствует схеме (XSD)? | Validator из javax.xml.validation |
Наш валидатор проверяет valid относительно загруженной XSD. Если XML вообще не парсится, SAX вернёт fatal-ошибку — мы покажем её так же, как ошибку схемы.
XML Schema (XSD) — язык описания структуры: имена элементов, типы полей, обязательные атрибуты, кардинальность (maxOccurs="unbounded"). Валидатор компилирует XSD в объект Schema, создаёт Validator и прогоняет через него XML. При нарушении правил вызывается ErrorHandler с номером строки и столбца — именно эти координаты мы выводим пользователю.
Архитектура приложения
В нашем приложении три слоя — упрощённый layered architecture, близкий к MVC:
| Слой | Роль | Классы | Зависит от |
|---|---|---|---|
| Model | Результат проверки, без UI | ValidationResult | только JDK |
| Service | Логика XSD-валидации | XsdValidatorService | model, javax.xml.* |
| UI (View + Controller) | Окно, ввод, реакция на кнопки | XmlValidatorFrame, FilePanel, ResultPanel | model, service, Swing |
Точка входа — XmlValidatorApp.main, как в первой программе и точке входа JVM. UI не импортирует SAX напрямую: frame вызывает сервис, сервис возвращает ValidationResult, панель результата только отображает данные. Так проще тестировать сервис без Swing и позже заменить UI на JavaFX, не трогая валидацию.
Пользователь → FilePanel / кнопки → XmlValidatorFrame
│
▼ (SwingWorker, фон)
XsdValidatorService
│
▼
ValidationResult → ResultPanel
Ключевые термины
- Swing — графическая библиотека JDK (
javax.swing); рисует "лёгкие" компоненты на Java, не зависит от нативных виджетов ОС. Обзор — 311.md, экосистема Java. - EDT (Event Dispatch Thread) — единственный поток, где безопасно создавать и менять Swing-компоненты. Старт UI — через
SwingUtilities.invokeLater. Подробнее — 112.md. SwingWorker— шаблон "долгая работа в фоне → результат в EDT";doInBackground()не трогает UI,done()обновляет виджеты.JFrame— главное окно приложения; содержитcontentPaneс layout-менеджерами.SchemaFactory/Validator— APIjavax.xml.validation: фабрика компилирует XSD, валидатор проверяет XML.ErrorHandler(SAX) — интерфейс обратных вызововwarning,error,fatalErrorс координатамиSAXParseException.JFileChooser— диалог выбора файла; фильтр расширений —FileNameExtensionFilter(3112 — JFileChooser).JSplitPane— разделитель между областями; пользователь тянет границу мышью.- Look-and-Feel (L&F) — тема оформления Swing (Metal, Nimbus, System); задаётся через
UIManager.setLookAndFeel.
Почему именно этот проект
- охватывает сценарий форма + фоновая операция + structured errors — основа многих desktop-утилит;
- Swing без внешних GUI-зависимостей — только JDK;
- сочетает I/O файлов (297.md) и XML API стандартной библиотеки;
- показывает разделение слоёв — навык из культуры кода;
- компактен для одного вечера (2–3 часа), но близок к реальным internal tools;
- после прохождения проще читать 311.md и Lab — Swing.
Предварительные знания:
- JDK 17+, Maven 3.8+;
- классы, пакеты,
final, record (ООП); - базовый Swing или Lab — 1143;
- желательно — структура Maven-проекта, исключения.
Готовый проект для сверки: F:\Projects\JVM\Java\JavaXMLValidator.
Что получится
| Действие пользователя | Компонент Swing | Что происходит |
|---|---|---|
| "Обзор…" для XML/XSD | JFileChooser, JTextArea | Файл читается в редактор |
| Вставить текст вручную | JTextArea | Валидация по строкам, без файла на диске |
| "Проверить" | JButton, SwingWorker | XSD проверяет XML, результат внизу |
| "Очистить" | JButton | Сброс полей и статуса |
| Невалидный XML | ResultPanel | Список ошибок с [error], строка, столбец |
| Enter в форме | getRootPane().setDefaultButton | Тот же сценарий, что и клик "Проверить" |
Карта этапов
| Этап | Фокус | Результат |
|---|---|---|
| 0 | Каркас Maven | Пустая структура каталогов |
| 1 | pom.xml | JAR, mainClass, shade |
| 2 | ValidationResult | Модель результата |
| 3 | XsdValidatorService | Валидация по файлу и строкам |
| 4 | FilePanel | Редактор + выбор файла |
| 5 | ResultPanel | Статус и список ошибок |
| 6 | XmlValidatorFrame | Компоновка, SwingWorker |
| 7 | XmlValidatorApp | Точка входа, Look-and-Feel |
| 8 | Примеры XML/XSD | Тестовые данные в examples/ |
| 9 | Запуск и проверка | mvn package, GUI, сценарии |
После каждого этапа имеет смысл mvn -q compile — привычка из разработки и отладки: маленькие шаги, частая проверка.
Этап 0 — каркас проекта
Цель этапа — создать стандартную структуру JAR-проекта Maven и разложить пакеты по слоям.
Desktop vs web — другая точка входа
В отличие от JSF (практикум "Список задач"), desktop-приложение начинается с public static void main. JVM загружает класс XmlValidatorApp, вызывает main, тот делегирует создание окна методу XmlValidatorFrame.launch(). Контейнер servlet'ов не нужен — процесс живёт, пока открыто окно (или пока не завершатся не-daemon потоки). См. точку входа JVM и архитектуру десктопа.
Maven для таких проектов использует packaging jar и каталог src/main/java — подробно в 12.md.
Создание каталогов
Корневая папка:
mkdir java-xml-validator && cd java-xml-validator
Windows PowerShell:
New-Item -ItemType Directory -Force -Path `
src/main/java/com/xmlvalidator/model, `
src/main/java/com/xmlvalidator/service, `
src/main/java/com/xmlvalidator/ui, `
src/test/java/com/xmlvalidator/service, `
examples
Итоговая схема:
java-xml-validator/
├── pom.xml
├── examples/
│ ├── book.xsd
│ ├── book-valid.xml
│ └── book-invalid.xml
└── src/
├── main/java/com/xmlvalidator/
│ ├── XmlValidatorApp.java
│ ├── model/ValidationResult.java
│ ├── service/XsdValidatorService.java
│ └── ui/
│ ├── XmlValidatorFrame.java
│ ├── FilePanel.java
│ └── ResultPanel.java
└── test/java/com/xmlvalidator/service/
└── XsdValidatorServiceTest.java
Разбор структуры
src/main/java— компилируемый код; путь папок = имя пакета (com.xmlvalidator.service).model— plain Java, без Swing и безjavax.xml; удобно покрыть unit-тестами.service— доменная логика валидации; UI только вызываетvalidate(...).ui— только Swing; классыFilePanelиResultPanelpackage-private — детали реализации, наружу торчитXmlValidatorFrame.launch().examples— образцы для ручной проверки и тестов; не в classpath, путь относительно корня проекта приmvn test.src/test/java— зеркало production-пакетов; тестируем сервис без поднятия GUI.
Этап 1 — pom.xml
Цель этапа — описать координаты артефакта, Java 17, JUnit для тестов и исполняемый JAR.
Зависимости и JDK-модули
Swing (javax.swing, java.awt) и XML Validation (javax.xml.validation, org.xml.sax) входят в JDK — в <dependencies> для runtime ничего не кладём. Это отличие от JavaFX, где OpenJFX подключают отдельно.
Единственная зависимость — JUnit 5 со scope test: GUI мы проверяем вручную, сервис — автоматически.
Плагин maven-shade-plugin соберёт "fat JAR" — один файл со всеми классами и манифестом Main-Class, удобно передать коллеге без IDE.
Создайте pom.xml в корне:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xmlvalidator</groupId>
<artifactId>java-xml-validator</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Java XML Validator</name>
<description>XML validator against XSD schema with Swing GUI</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
<junit.version>5.11.4</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>
<mainClass>com.xmlvalidator.XmlValidatorApp</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.xmlvalidator.XmlValidatorApp</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Разбор pom.xml
| Элемент | Назначение |
|---|---|
groupId / artifactId / version | Координаты артефакта в Maven-репозитории |
project.build.sourceEncoding UTF-8 | Кириллица в строках и XML без "кракозябр" |
maven.compiler.release 17 | Язык и API JDK 17; в модели используем record |
maven-jar-plugin + mainClass | Тонкий JAR: java -jar target/java-xml-validator-1.0.0.jar |
maven-shade-plugin | Fat JAR после mvn package — один файл для раздачи |
maven-surefire-plugin | Запуск JUnit 5 на фазе test |
JUnit 5 (scope test) | Только тесты; не попадает в production JAR |
Проверка синтаксиса POM:
mvn -q validate
Если Maven не находит артефакты — проверьте интернет и настройки репозитория (первая программа на Java).
Этап 2 — модель ValidationResult
Цель этапа — описать результат валидации в типах Java: успех, список ошибок с координатами, краткий summary для заголовка панели.
Зачем отдельная model, а не boolean
UI мог бы интерпретировать true/false, но реальным пользователям нужны детали: сколько ошибок, на какой строке, warning или error. Модель инкапсулирует три случая:
- документ валиден;
- документ не валиден, список
ValidationError; - ошибка конфигурации (пустой ввод, битая XSD, файл не найден) — не путать с нарушением схемы в XML.
Файл src/main/java/com/xmlvalidator/model/ValidationResult.java:
package com.xmlvalidator.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class ValidationResult {
private final boolean valid;
private final List<ValidationError> errors;
private final String summary;
private ValidationResult(boolean valid, List<ValidationError> errors, String summary) {
this.valid = valid;
this.errors = List.copyOf(errors);
this.summary = summary;
}
public static ValidationResult success() {
return new ValidationResult(true, List.of(), "Документ соответствует XSD-схеме.");
}
public static ValidationResult failure(List<ValidationError> errors) {
String summary = errors.isEmpty()
? "Документ не прошёл валидацию."
: "Найдено ошибок: " + errors.size();
return new ValidationResult(false, errors, summary);
}
public static ValidationResult configurationError(String message) {
ValidationError error = new ValidationError(message, null, null, null);
return new ValidationResult(false, List.of(error), message);
}
public boolean isValid() {
return valid;
}
public List<ValidationError> getErrors() {
return errors;
}
public String getSummary() {
return summary;
}
public record ValidationError(
String message,
Integer lineNumber,
Integer columnNumber,
String severity
) {
public String formatted() {
StringBuilder sb = new StringBuilder();
if (severity != null && !severity.isBlank()) {
sb.append('[').append(severity).append("] ");
}
if (lineNumber != null && columnNumber != null) {
sb.append("Строка ").append(lineNumber)
.append(", столбец ").append(columnNumber).append(": ");
}
sb.append(message);
return sb.toString();
}
}
public static final class Builder {
private final List<ValidationError> errors = new ArrayList<>();
public void addError(String message, Integer line, Integer column, String severity) {
errors.add(new ValidationError(message, line, column, severity));
}
public ValidationResult build() {
if (errors.isEmpty()) {
return success();
}
return failure(Collections.unmodifiableList(new ArrayList<>(errors)));
}
}
}
Построчный разбор
| Фрагмент | Смысл |
|---|---|
public final class | Класс не наследуют; явный value-object для результата |
Приватный конструктор + фабрики success / failure / configurationError | Единые точки создания; снаружи нельзя собрать неконсистентное состояние |
List.copyOf(errors) | Иммутабельная копия — UI не испортит список после получения |
record ValidationError | Компактный DTO (современный синтаксис); поля Integer, а не int, чтобы отличать "нет координаты" от нуля |
formatted() | Presentation-логика для JTextArea; сервис не знает про Swing |
Вложенный Builder | SAX вызывает ErrorHandler многократно; накапливаем ошибки, в конце build() |
На этом этапе:
mvn -q compile
Этап 3 — сервис XsdValidatorService
Цель этапа — реализовать валидацию по путям файлов и по строкам из редакторов, скрыв детали SAX/XML API от UI.
Цепочка API javax.xml.validation
Типичный pipeline (см. также JAXP в документации JDK):
SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI)— фабрика для XSD 1.0.factory.newSchema(xsdSource)— компиляция XSD; синтаксическая ошибка в схеме →SAXExceptionздесь.schema.newValidator()— экземпляр валидатора для этой схемы.validator.setErrorHandler(...)— куда стекаются warning/error/fatal.validator.validate(xmlSource)— один проход по XML.
Источники — javax.xml.transform.Source; для файлов удобен StreamSource, для строк из редактора — ByteArrayInputStream + UTF-8.
Два публичных метода validate
| Метод | Когда используется |
|---|---|
validate(Path xml, Path xsd) | Текст в редакторе совпадает с файлом на диске — читаем напрямую с systemId = URI файла |
validate(String xml, String xsd) | Пользователь редактировал текст или не выбирал файл |
Frame (этап 6) решает, какой overload вызвать, сравнивая textArea.getText() с Files.readString(path).
Файл src/main/java/com/xmlvalidator/service/XsdValidatorService.java:
package com.xmlvalidator.service;
import com.xmlvalidator.model.ValidationResult;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import javax.xml.XMLConstants;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public final class XsdValidatorService {
public ValidationResult validate(Path xmlPath, Path xsdPath) {
if (xmlPath == null || !Files.isRegularFile(xmlPath)) {
return ValidationResult.configurationError("Укажите существующий XML-файл.");
}
if (xsdPath == null || !Files.isRegularFile(xsdPath)) {
return ValidationResult.configurationError("Укажите существующий XSD-файл.");
}
Source xmlSource = new StreamSource(xmlPath.toFile());
xmlSource.setSystemId(xmlPath.toUri().toString());
return validate(xmlSource, new StreamSource(xsdPath.toFile()));
}
public ValidationResult validate(String xmlContent, String xsdContent) {
if (xmlContent == null || xmlContent.isBlank()) {
return ValidationResult.configurationError("XML не может быть пустым.");
}
if (xsdContent == null || xsdContent.isBlank()) {
return ValidationResult.configurationError("XSD не может быть пустым.");
}
Source xmlSource = streamSource(xmlContent, "inline.xml");
Source xsdSource = streamSource(xsdContent, "inline.xsd");
return validate(xmlSource, xsdSource);
}
private ValidationResult validate(Source xmlSource, Source xsdSource) {
try {
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file");
Schema schema = factory.newSchema(xsdSource);
Validator validator = schema.newValidator();
validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file");
ValidationResult.Builder builder = new ValidationResult.Builder();
validator.setErrorHandler(new CollectingErrorHandler(builder));
validator.validate(xmlSource);
return builder.build();
} catch (SAXException ex) {
if (ex instanceof SAXParseException parseEx) {
return ValidationResult.failure(List.of(new ValidationResult.ValidationError(
parseEx.getMessage(),
parseEx.getLineNumber() > 0 ? parseEx.getLineNumber() : null,
parseEx.getColumnNumber() > 0 ? parseEx.getColumnNumber() : null,
"fatal"
)));
}
return ValidationResult.configurationError("Ошибка схемы или XML: " + ex.getMessage());
} catch (IOException ex) {
return ValidationResult.configurationError("Ошибка чтения: " + ex.getMessage());
}
}
private static Source streamSource(String content, String systemId) {
InputStream input = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
StreamSource source = new StreamSource(input, systemId);
source.setSystemId(systemId);
return source;
}
private static final class CollectingErrorHandler implements ErrorHandler {
private final ValidationResult.Builder builder;
private CollectingErrorHandler(ValidationResult.Builder builder) {
this.builder = builder;
}
@Override
public void warning(SAXParseException exception) {
add(exception, "warning");
}
@Override
public void error(SAXParseException exception) {
add(exception, "error");
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
add(exception, "fatal");
throw exception;
}
private void add(SAXParseException exception, String severity) {
builder.addError(
exception.getMessage(),
exception.getLineNumber() > 0 ? exception.getLineNumber() : null,
exception.getColumnNumber() > 0 ? exception.getColumnNumber() : null,
severity
);
}
}
}
Разбор сервиса
Проверки на входе (Path)
Files.isRegularFile— отсекаем каталоги и несуществующие пути до SAX; возвращаемconfigurationErrorс понятным текстом для UI.
setSystemId
- Для файла — URI файла; для inline — псевдо-имя
inline.xml. БезsystemIdсообщения SAX иногда бесполезны ("unknown location").
ACCESS_EXTERNAL_DTD / ACCESS_EXTERNAL_SCHEMA
- Начиная с JDK 8+ ужесточили загрузку внешних сущностей (XXE). Пустая строка запрещает DTD из сети;
"file"для schema — разумный компромисс для локальных XSD.
CollectingErrorHandler
warning— замечание, валидация может продолжиться;error— нарушение схемы;fatalError— записываем и пробрасываемSAXException(контракт SAX); дублирующий catch дляSAXParseExceptionвvalidateподстраховывает обрыв с одной fatal-ошибкой.
Pattern matching instanceof SAXParseException parseEx
- Java 16+; альтернатива — отдельная переменная после cast (современный синтаксис).
Проверка:
mvn -q compile
Этап 4 — панель FilePanel
Цель этапа — переиспользуемый блок: путь к файлу, кнопка "Обзор…", моноширинный редактор текста.
Компоненты Swing в этой панели
| Класс | Роль в UI |
|---|---|
JPanel | Контейнер с BorderLayout |
JTextField | Read-only путь к выбранному файлу |
JButton | Открывает JFileChooser |
JTextArea | Многострочный редактор XML/XSD |
JScrollPane | Прокрутка длинного текста |
JLabel | Подсказка пользователю |
Один класс FilePanel параметризуется заголовком и расширением — DRY: две панели (XML и XSD) без копипасты. Подробнее про виджеты — 3112 — Swing.
Файл src/main/java/com/xmlvalidator/ui/FilePanel.java:
package com.xmlvalidator.ui;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.nio.file.Path;
final class FilePanel extends JPanel {
private final JTextField pathField = new JTextField();
private final JTextArea textArea = new JTextArea();
private final JButton browseButton = new JButton("Обзор…");
private final String extension;
private final String dialogTitle;
private Path selectedPath;
FilePanel(String title, String extension, String dialogTitle) {
this.extension = extension;
this.dialogTitle = dialogTitle;
setLayout(new BorderLayout(8, 8));
setBorder(BorderFactory.createTitledBorder(title));
pathField.setEditable(false);
pathField.setPreferredSize(new Dimension(200, 28));
JPanel top = new JPanel(new BorderLayout(6, 0));
top.add(pathField, BorderLayout.CENTER);
top.add(browseButton, BorderLayout.EAST);
add(top, BorderLayout.NORTH);
textArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
textArea.setTabSize(2);
textArea.setLineWrap(false);
textArea.setWrapStyleWord(false);
JScrollPane scrollPane = new JScrollPane(textArea);
scrollPane.setPreferredSize(new Dimension(400, 220));
add(scrollPane, BorderLayout.CENTER);
JPanel footer = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
footer.add(new JLabel("Можно вставить текст или выбрать файл."));
add(footer, BorderLayout.SOUTH);
browseButton.addActionListener(e -> chooseFile());
}
private void chooseFile() {
var chooser = new javax.swing.JFileChooser();
chooser.setDialogTitle(dialogTitle);
chooser.setFileFilter(new FileNameExtensionFilter(
extension.toUpperCase() + " (*." + extension + ")", extension));
if (chooser.showOpenDialog(this) == javax.swing.JFileChooser.APPROVE_OPTION) {
selectedPath = chooser.getSelectedFile().toPath();
pathField.setText(selectedPath.toString());
try {
textArea.setText(java.nio.file.Files.readString(selectedPath));
textArea.setCaretPosition(0);
} catch (Exception ex) {
javax.swing.JOptionPane.showMessageDialog(
this,
"Не удалось прочитать файл: " + ex.getMessage(),
"Ошибка",
javax.swing.JOptionPane.ERROR_MESSAGE
);
}
}
}
String getText() {
return textArea.getText();
}
Path getSelectedPath() {
return selectedPath;
}
void clear() {
selectedPath = null;
pathField.setText("");
textArea.setText("");
}
}
Разбор компоновки и поведения
Layout
BorderLayoutна корневой панели — NORTH (путь), CENTER (редактор), SOUTH (подсказка). Отступы8, 8между зонами (3112 — BorderLayout).createTitledBorder(title)— визуальная группа "XML-документ" / "XSD-схема".
Шрифт и перенос
Font.MONOSPACED— выравнивание вложенных тегов;setTabSize(2)— два пробела на уровень отступа.setLineWrap(false)— XML обычно не переносят по словам; горизонтальная прокрутка черезJScrollPane.
chooseFile()
showOpenDialog(this)— модальный диалог относительно панели.FileNameExtensionFilter— в списке только.xmlили.xsd.Files.readString— Java 11+, UTF-8 по умолчанию (297.md).- Ошибки чтения —
JOptionPaneсERROR_MESSAGE, не silent fail.
Инкапсуляция
final class, package-private — снаружи пакетаuiвиден только frame; методыgetText/clearбез модификатора — API дляXmlValidatorFrame.
Этап 5 — панель ResultPanel
Цель этапа — показать статус (цвет + краткий текст) и детали ошибок в читаемом виде.
Машина состояний UI
Панель результата проходит четыре состояния:
| Состояние | Метод | Что видит пользователь |
|---|---|---|
| Готов | reset() | Серый текст "Готов к проверке", пустая область деталей |
| Идёт проверка | showPending() | "Проверка…", кнопки disabled (управляет frame) |
| Результат | showResult(ValidationResult) | ✓ зелёный или ✗ красный + список ошибок |
| Сбой worker | showError(String) | ✗ "Ошибка" + текст исключения |
Такой паттерн описан в 112.md для любых long-running операций в desktop.
Файл src/main/java/com/xmlvalidator/ui/ResultPanel.java:
package com.xmlvalidator.ui;
import com.xmlvalidator.model.ValidationResult;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
final class ResultPanel extends JPanel {
private static final Color SUCCESS = new Color(34, 120, 70);
private static final Color FAILURE = new Color(180, 45, 45);
private static final Color NEUTRAL = new Color(70, 70, 70);
private final JLabel statusLabel = new JLabel("Готов к проверке");
private final JTextArea detailsArea = new JTextArea();
ResultPanel() {
setLayout(new BorderLayout(8, 8));
setBorder(BorderFactory.createTitledBorder("Результат"));
statusLabel.setFont(statusLabel.getFont().deriveFont(Font.BOLD, 14f));
add(statusLabel, BorderLayout.NORTH);
detailsArea.setEditable(false);
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
detailsArea.setBackground(new Color(250, 250, 250));
detailsArea.setLineWrap(true);
detailsArea.setWrapStyleWord(true);
JScrollPane scrollPane = new JScrollPane(detailsArea);
scrollPane.setPreferredSize(new Dimension(200, 140));
add(scrollPane, BorderLayout.CENTER);
}
void reset() {
statusLabel.setText("Готов к проверке");
statusLabel.setForeground(NEUTRAL);
detailsArea.setText("");
detailsArea.setCaretPosition(0);
}
void showPending() {
statusLabel.setText("Проверка…");
statusLabel.setForeground(NEUTRAL);
detailsArea.setText("");
}
void showResult(ValidationResult result) {
if (result.isValid()) {
statusLabel.setText("✓ " + result.getSummary());
statusLabel.setForeground(SUCCESS);
detailsArea.setText("Ошибок не обнаружено.");
} else {
statusLabel.setText("✗ " + result.getSummary());
statusLabel.setForeground(FAILURE);
StringBuilder sb = new StringBuilder();
int index = 1;
for (ValidationResult.ValidationError error : result.getErrors()) {
if (index > 1) {
sb.append('\n').append('\n');
}
sb.append(index++).append(". ").append(error.formatted());
}
detailsArea.setText(sb.toString());
}
detailsArea.setCaretPosition(0);
}
void showError(String message) {
statusLabel.setText("✗ Ошибка");
statusLabel.setForeground(FAILURE);
detailsArea.setText(message);
detailsArea.setCaretPosition(0);
}
}
Разбор
JLabel+JTextArea— заголовок статуса крупнее (deriveFont(BOLD, 14f)), детали — моноширинный многострочный блок.setEditable(false)— область только для чтения; пользователь копирует текст ошибки в буфер.setLineWrap(true)в details — длинные сообщения SAX переносятся; в редакторах XML wrap выключен намеренно.- Нумерация
1. 2. 3.— удобно ссылаться при разборе с коллегой ("смотри ошибку 2"). setCaretPosition(0)— после обновления прокрутка в начало, а не в конец.
Этап 6 — главное окно XmlValidatorFrame
Цель этапа — собрать layout, обработчики кнопок и SwingWorker для валидации без блокировки EDT.
Иерархия компонентов окна
JFrame "XML Validator — проверка по XSD"
└── content (BorderLayout)
├── CENTER: JSplitPane (VERTICAL)
│ ├── TOP: JPanel GridLayout(1,2)
│ │ ├── FilePanel "XML-документ"
│ │ └── FilePanel "XSD-схема"
│ └── BOTTOM: ResultPanel
└── SOUTH: JPanel FlowLayout — [Очистить] [Проверить]
Layout-менеджеры (теория — 311.md — геометрия Swing):
BorderLayout— якорные зоны frame;GridLayout(1, 2)— два редактора одинаковой ширины;JSplitPane.VERTICAL_SPLIT— пользователь меняет долю места редакторам и результату;FlowLayout(RIGHT)— кнопки прижаты к правому краю, порядок "Очистить" → "Проверить".
Почему SwingWorker, а не голый Thread
Валидация большого XML может занять секунды. Если вызвать validator.validate прямо в ActionListener, EDT занят — окно не перерисовывается, курсор "часики", пользователь думает, что приложение зависло.
SwingWorker:
doInBackground()— worker-поток; здесь только сервис и чтение текста.done()— снова EDT; включаем кнопки, вызываемresultPanel.showResult(get()).
Аналог в JavaFX — Task + Platform.runLater (3111.md).
Файл src/main/java/com/xmlvalidator/ui/XmlValidatorFrame.java:
package com.xmlvalidator.ui;
import com.xmlvalidator.model.ValidationResult;
import com.xmlvalidator.service.XsdValidatorService;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.WindowConstants;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.KeyEvent;
import java.nio.file.Path;
public final class XmlValidatorFrame extends JFrame {
private final XsdValidatorService validatorService = new XsdValidatorService();
private final FilePanel xmlPanel = new FilePanel("XML-документ", "xml", "Выберите XML-файл");
private final FilePanel xsdPanel = new FilePanel("XSD-схема", "xsd", "Выберите XSD-файл");
private final ResultPanel resultPanel = new ResultPanel();
private final JButton validateButton = new JButton("Проверить");
private final JButton clearButton = new JButton("Очистить");
public XmlValidatorFrame() {
super("XML Validator — проверка по XSD");
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setMinimumSize(new Dimension(960, 640));
setLocationRelativeTo(null);
JPanel content = new JPanel(new BorderLayout(12, 12));
content.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
JPanel editors = new JPanel(new GridLayout(1, 2, 12, 0));
editors.add(xmlPanel);
editors.add(xsdPanel);
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, editors, resultPanel);
splitPane.setResizeWeight(0.72);
splitPane.setContinuousLayout(true);
content.add(splitPane, BorderLayout.CENTER);
JPanel actions = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
validateButton.setMnemonic(KeyEvent.VK_V);
clearButton.setMnemonic(KeyEvent.VK_C);
actions.add(clearButton);
actions.add(validateButton);
content.add(actions, BorderLayout.SOUTH);
setContentPane(content);
validateButton.addActionListener(e -> runValidation());
clearButton.addActionListener(e -> clearAll());
getRootPane().setDefaultButton(validateButton);
}
private void clearAll() {
xmlPanel.clear();
xsdPanel.clear();
resultPanel.reset();
}
private void runValidation() {
validateButton.setEnabled(false);
clearButton.setEnabled(false);
resultPanel.showPending();
SwingWorker<ValidationResult, Void> worker = new SwingWorker<>() {
@Override
protected ValidationResult doInBackground() {
Path xmlPath = xmlPanel.getSelectedPath();
Path xsdPath = xsdPanel.getSelectedPath();
String xmlText = xmlPanel.getText();
String xsdText = xsdPanel.getText();
boolean hasXmlFile = xmlPath != null && !xmlText.isBlank();
boolean hasXsdFile = xsdPath != null && !xsdText.isBlank();
if (hasXmlFile && hasXsdFile
&& xmlText.equals(readQuietly(xmlPath))
&& xsdText.equals(readQuietly(xsdPath))) {
return validatorService.validate(xmlPath, xsdPath);
}
return validatorService.validate(xmlText, xsdText);
}
@Override
protected void done() {
try {
resultPanel.showResult(get());
} catch (Exception ex) {
resultPanel.showError(ex.getMessage() != null
? ex.getMessage()
: "Неизвестная ошибка при валидации.");
} finally {
validateButton.setEnabled(true);
clearButton.setEnabled(true);
}
}
};
worker.execute();
}
private static String readQuietly(Path path) {
try {
return java.nio.file.Files.readString(path);
} catch (Exception ignored) {
return "";
}
}
public static void launch() {
applyLookAndFeel();
SwingUtilities.invokeLater(() -> {
XmlValidatorFrame frame = new XmlValidatorFrame();
frame.setVisible(true);
});
}
private static void applyLookAndFeel() {
try {
for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
UIManager.setLookAndFeel(info.getClassName());
return;
}
}
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ignored) {
// оставляем L&F по умолчанию
}
}
}
Разбор ключевых решений
| Решение | Пояснение |
|---|---|
EXIT_ON_CLOSE | JVM завершится при закрытии окна — иначе процесс может висеть (311.md — типичные ошибки) |
setMinimumSize(960, 640) | Два редактора + результат не сжимаются в нечитаемую полоску |
setLocationRelativeTo(null) | Окно по центру экрана при первом показе |
resizeWeight(0.72) | При ресайзе окна ~72% высоты split — редакторам |
continuousLayout(true) | Панели перерисовываются во время перетаскивания разделителя |
| Сравнение текста с файлом | Если пользователь правил текст после "Обзор…", валидируем строки, а не устаревший файл на диске |
setDefaultButton(validateButton) | Enter в форме = "Проверить" |
setMnemonic Alt+V / Alt+C | Клавиатурная доступность |
| Nimbus → System L&F | Единый современный вид; если Nimbus нет — native (121.md — swing.defaultlaf) |
Вызов validator.validate(...) напрямую в ActionListener на больших файлах "подвесит" окно. Всегда выносите тяжёлую работу в SwingWorker или фоновый поток + SwingUtilities.invokeLater — см. Swing и потоки и 112.md.
Замечание про потоки и UI: в doInBackground() мы читаем getText() из Swing-компонентов. Формально Swing не потокобезопасен; здесь worker стартует сразу после клика, до параллельного редактирования — для учебного проекта достаточно. В production надёжнее снимать snapshot строк в EDT перед worker.execute() и передавать их в worker через constructor — см. JVM и потоки.
Этап 7 — точка входа XmlValidatorApp
Цель этапа — минимальный main, делегирующий запуск окну; отделить bootstrap от UI-кода.
Роль класса с main
| Обязанность | Где живёт |
|---|---|
| Точка входа JVM | XmlValidatorApp.main |
EDT + L&F + setVisible | XmlValidatorFrame.launch() |
| Разметка и события | конструктор XmlValidatorFrame |
Так Maven shade-plugin, IDE Run Configuration и документация ссылаются на одно имя класса — com.xmlvalidator.XmlValidatorApp.
Файл src/main/java/com/xmlvalidator/XmlValidatorApp.java:
package com.xmlvalidator;
import com.xmlvalidator.ui.XmlValidatorFrame;
public final class XmlValidatorApp {
public static void main(String[] args) {
XmlValidatorFrame.launch();
}
}
Почему не main прямо во frame
JFrameне обязан знать проmain— проще тестировать и переиспользовать frame из другого launcher'а.launch()вызывают до любого Swing-кода — сначала L&F, потомinvokeLater.- Соответствует рекомендациям по структуре — тонкий entry point.
Запуск:
- IDE — Run на
XmlValidatorApp(отладка); - Maven (если добавите
exec-maven-plugin):
mvn -q exec:java -Dexec.mainClass=com.xmlvalidator.XmlValidatorApp
В эталонном проекте достаточно Run в IDEA или java -jar после package.
Этап 8 — примеры XML и XSD
Цель этапа — добавить файлы для ручной проверки, разбора схемы и unit-тестов.
Схема book.xsd
Описывает каталог книг: корень library, внутри ноль или больше book. У каждой книги обязательны title, author, year (положительное целое) и атрибут isbn.
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="library">
<xs:complexType>
<xs:sequence>
<xs:element name="book" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="title" type="xs:string"/>
<xs:element name="author" type="xs:string"/>
<xs:element name="year" type="xs:positiveInteger"/>
</xs:sequence>
<xs:attribute name="isbn" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
Разбор XSD
xmlns:xs="http://www.w3.org/2001/XMLSchema"— префиксxs:для встроенных типов (string,positiveInteger).complexType+sequence— дочерние элементы в фиксированном порядке.maxOccurs="unbounded"— список книг любой длины.use="required"на атрибуте — безisbnэлементbookinvalid.
Валидный документ book-valid.xml
<?xml version="1.0" encoding="UTF-8"?>
<library>
<book isbn="978-0-123456-78-9">
<title>Java XML Processing</title>
<author>Jane Doe</author>
<year>2024</year>
</book>
</library>
Все элементы на месте, year > 0, атрибут isbn задан — ожидаем ✓ в UI.
Невалидный документ book-invalid.xml
<?xml version="1.0" encoding="UTF-8"?>
<library>
<book>
<title>Missing ISBN and bad year</title>
<author>John Smith</author>
<year>-1</year>
</book>
</library>
Две ошибки схемы
- нет обязательного атрибута
isbn; -1не укладывается вxs:positiveInteger.
Валидатор может вернуть несколько записей в ValidationResult — проверьте нумерацию в ResultPanel.
Unit-тест сервиса
GUI не гоняем в JUnit — тестируем сервис (113 — тестирование). Три сценария: valid file, invalid file, inline strings (text blocks Java 15+).
Файл src/test/java/com/xmlvalidator/service/XsdValidatorServiceTest.java:
package com.xmlvalidator.service;
import com.xmlvalidator.model.ValidationResult;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class XsdValidatorServiceTest {
private XsdValidatorService service;
private Path examplesDir;
@BeforeEach
void setUp() {
service = new XsdValidatorService();
examplesDir = Path.of("examples").toAbsolutePath();
}
@Test
void validXmlPassesValidation() {
ValidationResult result = service.validate(
examplesDir.resolve("book-valid.xml"),
examplesDir.resolve("book.xsd")
);
assertTrue(result.isValid(), result.getSummary());
}
@Test
void invalidXmlFailsValidation() {
ValidationResult result = service.validate(
examplesDir.resolve("book-invalid.xml"),
examplesDir.resolve("book.xsd")
);
assertFalse(result.isValid());
assertFalse(result.getErrors().isEmpty());
}
@Test
void inlineContentValidationWorks() {
String xsd = """
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="note" type="xs:string"/>
</xs:schema>
""";
String xml = """
<?xml version="1.0" encoding="UTF-8"?>
<note>Hello</note>
""";
ValidationResult result = service.validate(xml, xsd);
assertTrue(result.isValid());
}
}
Разбор тестов
examplesDir = Path.of("examples").toAbsolutePath()— работает, еслиmvn testзапускают из корня проекта (рабочая директория Maven = каталог сpom.xml).@BeforeEach— новый сервис на каждый тест, без shared state.- Третий тест — минимальная XSD "корень = строка"; проверяет overload
validate(String, String)без файлов.
mvn -q test
Этап 9 — сборка, запуск и проверка
Цель этапа — убедиться, что JAR собирается и все пользовательские сценарии в GUI работают.
mvn clean package
java -jar target/java-xml-validator-1.0.0.jar
После package в target/ появятся и обычный JAR, и shaded (fat) — оба с Main-Class в манифесте.
Чек-лист ручного тестирования
- Окно открывается 960×640, заголовок "XML Validator — проверка по XSD".
- "Обзор…" для XML и XSD — загружаются
book-valid.xmlиbook.xsd→ "Проверить" → зелёный ✓. - Заменить XML на содержимое
book-invalid.xml(вставкой или вторым файлом) → "Проверить" → список ошибок с номерами строк. - "Очистить" — поля и статус "Готов к проверке".
- Вставить минимальный XML/XSD без выбора файла → проверка по строкам работает.
- Пустой XML → сообщение "XML не может быть пустым" (
configurationError, не SAX). - Во время проверки кнопки неактивны, статус "Проверка…" — UI отзывчив (можно двигать разделитель split).
- Alt+V / Enter — запуск проверки (mnemonic и default button).
Импортируйте Maven-проект в IntelliJ IDEA. Полезные breakpoint'ы: XsdValidatorService.validate (логика XSD), CollectingErrorHandler.add (каждая SAX-ошибка), SwingWorker.done (граница фон / UI). Общие приёмы — Отладка Java-кода в IDE.
Схема потока данных
[XmlValidatorApp.main]
│
▼
XmlValidatorFrame.launch() ──► EDT + Nimbus L&F
│
▼
Пользователь: XML + XSD (файл или текст)
│
▼
SwingWorker.doInBackground()
│
▼
XsdValidatorService.validate(...)
│
├── SchemaFactory.newSchema(XSD)
├── Validator.validate(XML)
└── ErrorHandler → ValidationResult.Builder
│
▼
SwingWorker.done() → ResultPanel.showResult()
Что вы освоили за практикум
- Maven JAR с mainClass и shade (12.md);
- Swing — layout, события, диалоги, split pane (3112.md);
- EDT и SwingWorker — отзывчивый UI (112.md);
- Layered design — model / service / ui;
- javax.xml.validation — XSD и SAX ErrorHandler;
- JUnit 5 для сервиса без GUI.
Частые ошибки
| Симптом | Вероятная причина | Что проверить |
|---|---|---|
| Окно не появляется | UI создан не в EDT | SwingUtilities.invokeLater в launch() |
| UI "замирает" при проверке | Валидация в ActionListener | Перенести в SwingWorker |
UnsupportedOperationException на L&F | Nimbus недоступен | Fallback на system L&F уже в коде |
Тесты не находят examples/ | Запуск не из корня проекта | mvn test из каталога с pom.xml |
| Кракозябры в XML | Не UTF-8 | Кодировка файлов и project.build.sourceEncoding |
| Пустой список ошибок при невалидном XML | Пустая XSD или не та схема | Содержимое панели XSD |
| JVM не завершается после закрытия | Не EXIT_ON_CLOSE | setDefaultCloseOperation в конструкторе frame |
| "Ошибка схемы" при валидном XML | XSD syntactically broken | Откройте XSD отдельно, проверьте well-formed |
| Разные результаты file vs paste | Текст в редакторе ≠ файл | Логика сравнения в runValidation() |
Куда развивать дальше
- Меню "Файл" — открыть/сохранить XML, недавние файлы (3112 — JMenuBar).
- Подсветка строки с ошибкой —
Highlighter/DefaultHighlighterвJTextAreaXML-панели. - Snapshot текста в EDT — передавать строки в
SwingWorkerчерез constructor (потокобезопасность). - DTD и Relax NG — другие grammar / сторонние библиотеки; XSD остаётся стандартом для конфигов (XML — обзор).
- Drag-and-drop —
TransferHandlerдля.xml/.xsdна панели. - JavaFX-версия — те же
model/service, UI наTextArea+Task(3111.md). - CI —
mvn testв pipeline (292.md).
Связанные статьи
- JavaFX и GUI — обзор Swing, EDT,
SwingWorker - Справочник JavaFX и Swing — элементы UI —
JButton,JTextArea,JFileChooser - Lab — Java Swing — построчный разбор простых окон
- Структура и сборки Java-проектов — Maven, JAR, плагины
- Ввод-вывод и файлы —
Path,Files.readString - XML — well-formed, valid, пространства имён
- Особенности десктопных приложений — EDT, отзывчивость UI
- Архитектура десктопных приложений — элементы UI, MVC
- Отладка Java-кода в IDE — breakpoint в сервисе и UI
- Современный синтаксис Java — record, pattern matching
- Практикум JSF — список задач — другой Maven-практикум в разделе Java
Десктоп на Java — JavaFX и GUI, Первая программа на JavaFX, Практикум Swing — XML-валидатор, Lab — Swing, Архитектура десктопа.