Перейти к основному содержимому

Практикум 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 и потоки).
Swing в 2026 — зачем этот практикум

Для новых 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Результат проверки, без UIValidationResultтолько JDK
ServiceЛогика XSD-валидацииXsdValidatorServicemodel, javax.xml.*
UI (View + Controller)Окно, ввод, реакция на кнопкиXmlValidatorFrame, FilePanel, ResultPanelmodel, 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 — API javax.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.

Предварительные знания:

Готовый проект для сверки: F:\Projects\JVM\Java\JavaXMLValidator.

Что получится

Действие пользователяКомпонент SwingЧто происходит
"Обзор…" для XML/XSDJFileChooser, JTextAreaФайл читается в редактор
Вставить текст вручнуюJTextAreaВалидация по строкам, без файла на диске
"Проверить"JButton, SwingWorkerXSD проверяет XML, результат внизу
"Очистить"JButtonСброс полей и статуса
Невалидный XMLResultPanelСписок ошибок с [error], строка, столбец
Enter в формеgetRootPane().setDefaultButtonТот же сценарий, что и клик "Проверить"

Карта этапов

ЭтапФокусРезультат
0Каркас MavenПустая структура каталогов
1pom.xmlJAR, mainClass, shade
2ValidationResultМодель результата
3XsdValidatorServiceВалидация по файлу и строкам
4FilePanelРедактор + выбор файла
5ResultPanelСтатус и список ошибок
6XmlValidatorFrameКомпоновка, SwingWorker
7XmlValidatorAppТочка входа, 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 и ResultPanel package-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-pluginFat 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
Вложенный BuilderSAX вызывает ErrorHandler многократно; накапливаем ошибки, в конце build()

На этом этапе:

mvn -q compile

Этап 3 — сервис XsdValidatorService

Цель этапа — реализовать валидацию по путям файлов и по строкам из редакторов, скрыв детали SAX/XML API от UI.

Цепочка API javax.xml.validation

Типичный pipeline (см. также JAXP в документации JDK):

  1. SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI) — фабрика для XSD 1.0.
  2. factory.newSchema(xsdSource) — компиляция XSD; синтаксическая ошибка в схеме → SAXException здесь.
  3. schema.newValidator() — экземпляр валидатора для этой схемы.
  4. validator.setErrorHandler(...) — куда стекаются warning/error/fatal.
  5. 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

Проверка:

mvn -q compile

Этап 4 — панель FilePanel

Цель этапа — переиспользуемый блок: путь к файлу, кнопка "Обзор…", моноширинный редактор текста.

Компоненты Swing в этой панели

КлассРоль в UI
JPanelКонтейнер с BorderLayout
JTextFieldRead-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)✓ зелёный или ✗ красный + список ошибок
Сбой workershowError(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:

  1. doInBackground() — worker-поток; здесь только сервис и чтение текста.
  2. 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_CLOSEJVM завершится при закрытии окна — иначе процесс может висеть (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)
Не блокируйте EDT

Вызов 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

ОбязанностьГде живёт
Точка входа JVMXmlValidatorApp.main
EDT + L&F + setVisibleXmlValidatorFrame.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 элемент book invalid.

Валидный документ 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 в манифесте.

Чек-лист ручного тестирования

  1. Окно открывается 960×640, заголовок "XML Validator — проверка по XSD".
  2. "Обзор…" для XML и XSD — загружаются book-valid.xml и book.xsd → "Проверить" → зелёный ✓.
  3. Заменить XML на содержимое book-invalid.xml (вставкой или вторым файлом) → "Проверить" → список ошибок с номерами строк.
  4. "Очистить" — поля и статус "Готов к проверке".
  5. Вставить минимальный XML/XSD без выбора файла → проверка по строкам работает.
  6. Пустой XML → сообщение "XML не может быть пустым" (configurationError, не SAX).
  7. Во время проверки кнопки неактивны, статус "Проверка…" — UI отзывчив (можно двигать разделитель split).
  8. Alt+V / Enter — запуск проверки (mnemonic и default button).
Отладка в IDE

Импортируйте 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 создан не в EDTSwingUtilities.invokeLater в launch()
UI "замирает" при проверкеВалидация в ActionListenerПеренести в SwingWorker
UnsupportedOperationException на L&FNimbus недоступенFallback на system L&F уже в коде
Тесты не находят examples/Запуск не из корня проектаmvn test из каталога с pom.xml
Кракозябры в XMLНе UTF-8Кодировка файлов и project.build.sourceEncoding
Пустой список ошибок при невалидном XMLПустая XSD или не та схемаСодержимое панели XSD
JVM не завершается после закрытияНе EXIT_ON_CLOSEsetDefaultCloseOperation в конструкторе frame
"Ошибка схемы" при валидном XMLXSD syntactically brokenОткройте XSD отдельно, проверьте well-formed
Разные результаты file vs pasteТекст в редакторе ≠ файлЛогика сравнения в runValidation()

Куда развивать дальше

  1. Меню "Файл" — открыть/сохранить XML, недавние файлы (3112 — JMenuBar).
  2. Подсветка строки с ошибкойHighlighter / DefaultHighlighter в JTextArea XML-панели.
  3. Snapshot текста в EDT — передавать строки в SwingWorker через constructor (потокобезопасность).
  4. DTD и Relax NG — другие grammar / сторонние библиотеки; XSD остаётся стандартом для конфигов (XML — обзор).
  5. Drag-and-dropTransferHandler для .xml / .xsd на панели.
  6. JavaFX-версия — те же model/service, UI на TextArea + Task (3111.md).
  7. CImvn test в pipeline (292.md).

Связанные статьи


Десктоп на JavaJavaFX и GUI, Первая программа на JavaFX, Практикум Swing — XML-валидатор, Lab — Swing, Архитектура десктопа.

Содержание
Освоение главы0%