Лабораторна робота 3
Розширені можливості роботи з файлами
1 Завдання на лабораторну роботу
1.1 Індивідуальне завдання
Створити новий Maven-проєкт, в який перенести раніше створені класи, які представляють сутності індивідуальних завдань лабораторних робіт № 3 і № 4 курсу "Основи програмування Java".
Програма повинна демонструвати:
- відтворення реалізації завдань лабораторних робіт № 3 і № 4 курсу "Основи програмування Java";
- використання засобів Stream API для обробки та виведення послідовностей;
- виведення даних в текстовий файл засобами Stream API з подальшим читанням;
- серіалізацію об'єктів у XML-файл і JSON-файл і відповідну десеріалізацію із застосуванням бібліотеки XStream;
- запис подій, пов'язаних з виконанням програми, в системний журнал;
- тестування окремих класів з використанням JUnit.
Для представлення індивідуальних даних слід використати класи, які були створені під час виконання індивідуального завдання лабораторної роботи № 1 цього курсу.
Примітка: локалізацію і переклад текстів можна проводити за бажанням студента.
1.2 Список файлів усіх підкаталогів
Увести з клавіатури ім'я певної теки. Вивести на екран імена усіх файлів цієї теки, а також усіх файлів підкаталогів, їхніх підкаталогів тощо. Реалізувати два підходи:
- пошук за допомогою класу
java.io.File
через рекурсивну функцію; - пошук засобами
пакету
java.nio.file
.
Обидва результати послідовно вивести на екран. Якщо тека не існує, вивести повідомлення про помилку.
1.3 Робота з текстовими файлами засобами Stream API
Прочитати функцією Files.lines()
рядки з текстового файлу, розсортувати за збільшенням довжини й вивести в інший
файл рядки, які містять літеру "a".
1.4 Створення файлів і читання з файлів даних про студента й академічну групу
Описати класи Студент і Академічна група (з полем – масивом студентів). Створити об'єкти. Забезпечити створення файлів і читання з файлів, застосувавши такі підходи:
- використання засобів Stream API для роботи з текстовими файлами;
- серіалізація й десеріалізація в XML і JSON(засобами XStream).
1.5 Робота з бібліотекою org.json (додаткове завдання)
Виконати завдання 1.4 із застосуванням засобів для роботи з JSON-файлами бібліотеки org.json.
1.6 Використання технологій SAX і DOM (додаткове завдання)
Підготувати XML-документ з даними про студентів академічної групи. За допомогою технології SAX здійснити читання даних з XML-документа і виведення даних на консоль. За допомогою технології DOM здійснити читання даних з того ж XML-документа, модифікацію даних і запис їх в новий документ.
2 Методичні вказівки
2.1 Засоби Java для роботи з файлами
Робота з файлами – одна з найбільш розповсюджених у сучасних операційних системах.
Починаючи з першої версії JDK, Java надає засоби для роботи зі вмістом файлів, втілені в концепцію потоків (streams) і реалізовані відповідними класами пакета java.io:
- засоби для роботи з текстовими файлами (наприклад,
FileReader
,FileWriter
,BufferedReader
,PrintWriter
тощо); - засоби для роботи з бінарними файлами (наприклад,
FileInputStream
,FileOutputStream
,DataInputStream
,DataOutputStream
тощо) - засоби серіалізації й десеріалізації;
- засоби для роботи з архівами.
До цих засобів можна додати класи пакету java.nio.file
, які будуть розглянуті в цій лабораторній роботі.
Існують також окремі засоби для роботи зі спеціальними форматами файлів – XML, JSON тощо.
Окремо від роботи зі вмістом файлів слід розглядати роботу з файловою системою.
2.2 Використання мови XML
2.2.1 Загальні концепції
Розширювана мова розмічування XML (eXtensible Markup Language) – це незалежний від платформи метод структурування інформації. Оскільки XML відокремлює зміст документа від його структури, його успішно використовують для обміну інформацією. Наприклад, XML можна використовувати для передачі даних між програмою та базами даних, або між базами даних, що мають різні формати.
Файли формату XML – це завжди текстові файли. Синтаксис мови XML багато в чому схожий на синтаксис мови HTML, яка застосовується для розмічування текстів, що публікуються в Internet. Мова XML також може бути безпосередньо застосована для розмітки текстів.
Найчастіше документ у форматі XML починається з так званого префіксу. Префікс для документа в загальному випадку має такий вигляд:
<?xml version="1.0" [інші-атрибути] ?>
Серед можливих атрибутів найбільш корисним є атрибут encoding="кодування"
. Він задає кодування
для тексту. Якщо необхідно використовувати однобайтові (не UNICODE) символи кирилиці, це можна визначити, наприклад,
в такий спосіб:
<?xml version="1.0" encoding="Windows-1251"?>
Після заголовку може міститись інформація про тип документа. Решта XML-документа складається з набору XML-елементів.
Елементи розділені тегами. Початкові теги починаються з символу <
з розміщеним далі ім'ям елементу.
Кінцеві теги починаються з символів </
, за якими слід ім'я елементу. Початковий і кінцевий теги
закінчуються символом >
. Все, що знаходиться між двома тегами, – це вміст елементу. Всі стартові
теги повинні бути співставлені кінцевими тегами. Всі значення атрибутів повинні бути вказані в лапках. Документ
обов'язково повинен мати кореневий тег, у який вкладають усі інші теги.
На відміну від HTML, XML дозволяє використовувати необмежений набір пар тегів, кожна з яких представляє не те, як включені в неї дані повинні виглядати, а те, що вони означають. XML дозволяє створювати свій набір тегів для кожного класу документів. Таким чином, точніше назвати його не мовою, а метамовою.
Маючи формально описану структуру документа, можна перевірити його коректність. Наявність тегів розмітки дозволяє аналізувати документ як людині, так і програмі. XML-документи, в першу чергу, призначені для програмного аналізу їхнього вмісту.
У наведеному нижче прикладі в XML-файлі зберігаються прості числа.
<?xml version="1.0" encoding="UTF-8"?> <Numbers> <Number>1</Number> <Number>2</Number> <Number>3</Number> <Number>5</Number> <Number>7</Number> <Number>11</Number> </Numbers>
Теги Numbers
та Number
вигадав автор документа. Відступи в тексті файлу використані для
поліпшення його сприйняття людиною.
Теги можуть містити в собі атрибути – додаткову інформацію про елементи, яка міститься всередині кутових дужок.
Значення атрибутів обов'язково брати у лапки. Наприклад, можна запропонувати тег Message
з атрибутами to
та from
:
<Message to="you" from="me"> <Text> Для чого потрібен XML? </Text> </Message>
Важливим правилом формування XML є обов'язковість вживання кінцевих тегів. Крім того, не можна плутати порядок кінцевих тегів. Цей текст містить помилку:
<A> <B> text </A> </B>
Необхідно писати так:
<A> <B> text </B> </A>
Допускається використання порожніх тегів. Такі теги закінчуються символом /. Наприклад, можна вживати <Nothing/>
замість
пари <Nothing></Nothing>
.
На відміну від HTML-тегів, XML-теги залежать від регістру, тому <cat>
та <CAT>
– це
різні теги.
У XML-документі можна вживати коментарі, синтаксис яких збігається з синтаксисом коментарів HTML-документів:
<!-- Це коментар -->
Програми розпізнавання XML-документів – так звані XML-парсери – здійснюють розбір документа до знаходження першої помилки, на відміну від HTML-парсерів, вбудованих в браузер. Браузери намагаються відобразити HTML-документ, навіть, якщо код містить помилки.
XML-документ, який відповідає всім синтаксичним правилам XML, вважається правильно оформленим документом (коректним документом, well-formed document).
2.2.2 Стандартні підходи до роботи з XML-документами. Засоби JAXP
Існує два стандартних підходи до роботи з XML-документами в програмах:
- подіє-орієнтована модель документа (Simple API for XML, SAX), що працює на потоці даних, обробляючи його, коли виникають пов'язані з різними тегами події;
- об'єктна модель документа (Document Object Model, DOM), що дозволяє створити в пам'яті колекцію пов'язаних з тегами об'єктів, організованих в ієрархію.
Базований на подіях підхід не дозволяє розробнику змінювати дані в вихідному документі. В разі необхідності коригування частини даних документ треба повністю оновити. На відміну від нього DOM забезпечує API, який дозволяє розробникові додавати або видаляти вузли в будь-якій точці дерева в застосунку.
Обидва підходи використовують поняття парсера. Парсер (parser) – це програмний застосунок, призначений для того, щоб аналізувати документ шляхом розділення його на лексеми (tokens). Парсер може ініціювати події (як у SAX), або будувати в пам'яті дерево даних.
Для реалізації стандартних підходів до роботи з XML в Java SE використовують засоби Java API for XML Processing (JAXP, інтерфейс програмування застосунків Java для роботи з XML). JAXP надає засоби валідації і розбору XML-документів. Для реалізації об'єктної моделі документа JAXP включає програмний інтерфейс DOM, SAX реалізований однойменною програмним інтерфейсом. На додаток до них надано програмний інтерфейс Streaming API for XML (StAX, потоковий API для XML), а також засоби XSLT (XML Stylesheet Language Transformations, мова перетворення XML-документів).
2.2.3 Використання Simple API for XML і StAX
Simple API for XML (SAX, простий програмний інтерфейс для роботи з XML) надає послідовний механізм аналізу XML-документа. Аналізатор, який реалізує інтерфейс SAX (SAX Parser), обробляє інформацію з XML документа як єдиний потік даних. Цей потік даних доступний тільки в одному напрямку, тобто, раніше оброблені дані неможливо повторно прочитати без повторного аналізу. Більшість програмістів збігається в думці, що обробка XML документів з використанням SAX, в цілому, швидше, ніж під час використання DOM. Це пояснюється тим, що потік SAX вимагає набагато меншого обсягу пам'яті в порівнянні з побудовою повного дерева DOM.
SAX аналізатори реалізують з використанням підходу, керованого подіями (event-driven approach) коли програмісту необхідно описати оброблювачі подій, які викликаються аналізаторами під час обробки XML-документа.
Засоби Java SE для роботи з SAX реалізовані в пакетах javax.xml.parsers
і org.xml.sax
,
а також у вкладених в них пакетах. Для створення об'єкта класу javax.xml.parsers.SAXParser
слід скористатися
класом javax.xml.parsers.SAXParserFactory
, що надає відповідні фабричні методи. SAX парсер не створює
в пам'яті представлення документа XML. Замість цього, SAX парсер інформує клієнтів про структуру документа XML,
використовуючи механізм зворотного виклику. Можна самостійно створити клас, який реалізує низку необхідних інтерфейсів,
зокрема org.xml.sax.ContentHandler
. Однак простіший й рекомендований спосіб – використовувати
клас org.xml.sax.helpers.DefaultHandler
, створивши похідний клас і перекривши його методи, які повинні
викликатися під час виникнення різних подій в процесі аналізу документа. Найбільш часто перекриваються такі методи:
startDocument()
іendDocument()
– методи, які викликаються на початку і наприкінці аналізу XML-документаstartElement()
іendElement()
– методи, які викликаються на початку і наприкінці аналізу елемента документаcharacters()
– метод, що викликається під час отримання текстового вмісту елемента XML-документа.
Наведений нижче приклад ілюструє використання SAX для читання документа. Припустимо, в каталозі проєкту створено файл Hello.xml з таким вмістом:
<?xml version="1.0" encoding="UTF-8" ?> <Greetings> <Hello Text="Привіт, це атрибут!"> Привіт, це текст! </Hello> </Greetings>
Примітка. Під час збереження файлу слід вказати кодування UTF-8.
Код програми, яка читає дані з XML, буде таким:
package ua.inf.iwanoff.java.advanced.third; import java.io.IOException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; public class HelloSAX extends DefaultHandler { @Override public void startDocument() { System.out.println("Opening document"); } @Override public void endDocument() { System.out.println("Done"); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { System.out.println("Opening tag: " + qName); if (attributes.getLength() > 0) { System.out.println("Атрибути: "); for (int i = 0; i < attributes.getLength(); i++) { System.out.println(" " + attributes.getQName(i) + ": " + attributes.getValue(i)); } } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { System.out.println("Closin tag: " + qName); } @Override public void characters(char[] ch, int start, int length) throws SAXException { String s = new String(ch).substring(start, start + length).trim(); if (s.length() > 0) { System.out.println(s); } } public static void main(String[] args) { SAXParser parser = null; try { parser = SAXParserFactory.newInstance().newSAXParser(); } catch (ParserConfigurationException | SAXException e) { e.printStackTrace(); } if (parser != null) { InputSource input = new InputSource("Hello.xml"); try { parser.parse(input, new HelloSAX()); } catch (SAXException | IOException e) { e.printStackTrace(); } } } }
Оскільки метод characters()
викликається для кожного тегу, вміст є сенс виводити, якщо рядок не порожній.
StAX був розроблений як щось середнє між інтерфейсами DOM і SAX. У цьому програмному інтерфейсі використана метафора курсора, що представляє точку входу в межах документа. Застосунок переміщує курсор вперед, читаючи інформацію та отримуючи інформацію від синтаксичного аналізатора за необхідності.
2.2.4 Використання Об'єктної моделі документа (DOM)
DOM є серією Рекомендацій, що виробляються Консорціумом World Wide Web (W3C). DOM починалася як спосіб ідентифікації і маніпулювання елементами на HTML-сторінці (DOM Level 0).
Актуальна Рекомендація DOM (DOM Level 3) є API, який визначає об'єкти, представлені в XML-документі, а також методи й властивості, які використовуються для доступу до них і маніпулювання ними.
Починаючи з DOM Рівня 1, DOM API містить інтерфейси, які представляють різні типи інформації, що можуть бути знайдені в XML-документі. Він також містить методи, необхідні для роботи з цими об'єктами. Можна навести деякі найбільш вживані методи стандартних інтерфейсів DOM.
Інтерфейс Node
є основним типом даних DOM. Він визначає низку корисних методів для отримання даних
про вузли та навігації вузлами:
getFirstChild()
іgetLastChild()
повертають перший або останній дочірній елемент цього вузла;getNextSibling()
іgetPreviousSibling()
повертають наступного або попереднього "брата" цього вузла;getChildNodes()
повертає посилання на список типуNodeList
дочірніх елементів цього вузла; за допомогою методів інтерфейсуNodeList
можна отримати i-й вузол (методitem(i)
) і загальну кількість таких вузлів (методgetLength()
);getParentNode()
повертає "батьківський" вузол;getAttributes()
повертає асоціативний масив типуNamedNodeMap
атрибутів цього вузла;hasChildNodes()
повертаєtrue
, якщо вузол має дочірні елементи.
Існує низка методів, що забезпечують модифікацію XML-документа – insertBefore()
, replaceChild()
, removeChild()
, appendChild()
тощо.
Крім Node
, DOM також визначає кілька підінтерфейсів інтерфейсу Node
:
Element
– представляє елемент XML в вихідному документі; в елемент входить пара тегів (що відкривається і що закривається) і весь текст між ними;Attr
– представляє атрибут елемента;Text
– вміст елемента;Document
– представляє весь XML-документ; тільки один об'єкт типуDocument
існує для кожного XML-документа; маючи об'єктDocument
, можна знайти корінь дерева DOM за допомогою методуgetDocumentElement()
; від кореня можна маніпулювати всім деревом.
Додатковими типами вузлів є:
Comment
– представляє коментар в XML-файлі;ProcessingInstruction
– представляє інструкцію обробки;CDATASection
– представляє розділCDATA
.
XML-парсери вимагають створення екземпляра певного класу. Недоліком цього є те, що під час зміни парсерів необхідно
змінювати вихідний код. Для деяких парсерів іноді можна використовувати так звані фабричні класи. За допомогою статичного
методу newInstance()
створюється екземпляр "фабричного" об'єкта, за допомогою якого створюється
об'єкт класу, що реалізує інтерфейс DocumentBuilder
. Такий об'єкт безпосередньо є необхідним парсером:
реалізує методи DOM, які потрібні для аналізу й обробки XML-файлу. Під час створення об'єкта-парсера можуть генеруватися
винятки, які необхідно перехоплювати. Далі можна створювати об'єкт типу Document
, завантажувати дані
з файлу з ім'ям, наприклад, fileName
і здійснювати його аналіз:
try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(fileName); . . .
Після обходу й модифікації дерева його можна зберегти в іншому файлі.
Використання DOM розглянемо на прикладі попереднього файлу (Hello.xml
). Наведена нижче програма виводить
на консоль текст атрибута і тексту, змінює їх і зберігає зміни в новому XML-документі:
package ua.inf.iwanoff.java.advanced.third; import java.io.*; import org.w3c.dom.*; import javax.xml.parsers.*; import javax.xml.transform.*; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; public class HelloDOM { public static void main(String[] args) throws Exception { Document doc; // посилання на об'єкт "документ" // Створюємо "будівник документів" за допомогою "фабричного методу": DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder(); doc = db.parse(new File("Hello.xml")); // Знаходимо кореневий тег: Node rootNode = doc.getDocumentElement(); // Переглядаємо всі "дочірні" теги: for (int i = 0; i < rootNode.getChildNodes().getLength(); i++) { Node currentNode = rootNode.getChildNodes().item(i); if (currentNode.getNodeName().equals("Hello")) { // Переглядаємо всі атрибути: for (int j = 0; j < currentNode.getAttributes().getLength(); j++) { if (currentNode.getAttributes().item(j).getNodeName().equals("Text")) { // Знайшли потрібний атрибут. Виводимо текст атрибуту – вітання: System.out.println(currentNode.getAttributes().item(j).getNodeValue()); // Змінюємо вміст атрибуту: currentNode.getAttributes().item(j).setNodeValue("Привіт, тут був DOM!"); // Подальший пошук є недоцільним: break; } } // Змінюємо текст: System.out.println(currentNode.getTextContent()); currentNode.setTextContent("\n Привіт, тут теж був DOM!\n"); break; } } // Створення об'єкта-перетворювача (в цьому випадку – для запису в файл). // Використовуємо фабричний метод: Transformer transformer = TransformerFactory.newInstance().newTransformer(); // Запис у файл: transformer.transform(new DOMSource(doc), new StreamResult(new FileOutputStream(new File("HelloDOM.xml")))); } }
Після виконання програми у теці проєкту можна буде знайти такий файл (HelloDOM.xml
):
<?xml version="1.0" encoding="UTF-8" standalone="no"?><Greetings> <Hello Text="Привіт, тут був DOM!"> Привіт, тут теж був DOM! </Hello> </Greetings>
У наведеному прикладі для збереження зміненого документа в файлі використано клас javax.xml.transform.Transformer
.
У загальному випадку цей клас використовують для реалізації так званого XSLT-перетворення. XSLT (eXtensible Stylesheet
Language Transformations) – мова перетворень XML-документів у інші XML-документи або інші об'єкти, такі як
HTML, звичайний текст тощо. XSLT-процесор приймає один або кілька XML-документів джерела, а також один або кілька
модулів перетворення, і обробляє їх для отримання вихідного документа. Перетворення містить набір правил шаблону:
інструкції та інші директиви, якими керується XSLT-процесор під час генерації вихідного документа.
2.2.5 Використання шаблону і схеми документа
Структуровані дані, які можуть бути представленими у формі XML-файлу, потребують додаткової інформації. Найбільш розповсюдженими є два основних формати представлення такої інформації – Визначення шаблону документа (DTD) та Схема документа (XSD).
DTD (Document Template Definition) – набір правил, що дозволяють однозначно визначити структуру певного класу XML-документів. DTD не є XML-документом. DTD дуже простий, але не описує типів елементів. Директиви DTD можуть бути присутніми як у заголовку самого XML-документа (internal DTD), так і в іншому файлі (external DTD). Наявність DTD не є обов'язковою.
Наприклад, ми маємо такий XML-файл:
<?xml version="1.0" encoding="UTF-8"?> <Pairs> <Pair> <x>1</x> <y>4</y> </Pair> <Pair> <x>2</x> <y>2</y> </Pair> . . . </Pairs>
DTD-файл, який описує структуру цього документа, матиме такий вигляд:
<?xml version="1.0" encoding="UTF-8"?> <!ELEMENT Pair (x, y)> <!ELEMENT x (#PCDATA)> <!ELEMENT y (#PCDATA)> <!ELEMENT Pairs (Pair+)>
Знак плюс в останньому рядку означає що елементів Pair
всередині тегу Pairs
може бути
один або багато. Крім того, можна також використовувати * (0 або багато), знак питання (0 або 1). Відсутність знака
означає, що може бути присутнім саме один елемент.
XML Schema – це альтернативний DTD спосіб завдання структури документа. На відміну від визначення шаблону, схема є XML-документом. Крім того, XML схема своїми можливостями істотно перевершує DTD. Наприклад, у схемі можна вказувати типи тегів та атрибутів, визначати обмеження тощо.
XML-документ, який є правильно оформленим, посилається на граматичні правила та повністю їм відповідає, має назву валідного (valid) документа.
Для того, щоб запобігти конфліктам імен тегів, у XML можна створювати так звані простори імен. Простір імен визначає префікс, пов'язаний з певною схемою документа та додається до тегів. Власний простір імен визначається за допомогою конструкції на кшталт такої:
<root xmlns:pref="http://www.someaddress.org/">
У цьому прикладі root
– кореневий тег XML-документа, pref
– префікс,
який визначає простір імен, "http://www.someaddress.org/
" – якась адреса, наприклад,
доменне ім'я автора схеми. Програми, які обробляють XML-документи, ніколи не перевіряють цю адресу. Вона необхідна
лише для забезпечення унікальності простору імен.
Безпосередньо схема використовує простір імен xs
.
Використання схеми документа можна продемонструвати на такому прикладі. Припустимо, ми маємо такий XML-файл:
<?xml version="1.0" encoding="Windows-1251" ?> <Student Name="Джон" Surname="Сміт"> <Marks> <Mark Subject="Математика" Value="B"/> <Mark Subject="Фізика" Value="A"/> <Mark Subject="Програмування" Value="C"/> </Marks> <Comments> Не наш студент </Comments> </Student>
Створення файлу схеми слід починати зі стандартної конструкції:
<?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> </xs:schema>
Проміж тегами <xs:schema>
та </xs:schema>
буде розташована інформація про
схему документа. Для того, щоб описати теги документа, всередині його можна додавати стандартні теги. Для складних
тегів, у які вкладаються інші, або які мають параметри:
<xs:element name="ім'я тегу"> <xs:complexType> . . . </xs:complexType> </xs:element>
Всередині тегу можна розмістити список елементів:
<xs:sequence> . . . </xs:sequence>
Можна розмістити посилання на інший тег:
<xs:element ref="ім'я іншого тегу"/>
Для елементів, які безпосередньо містять дані, використовують такий тег
<xs:element name="ім'я тегу" type="ім'я типу"/>
Наведена нижче таблиця містить деякі стандартні типи даних, що використовуються в схемі:
Ім'я |
Опис |
xs:string |
Рядок символів як послідовність 10646 символів Unicode або ISO/IEC, включаючи пропуск, символ табуляції, повернення каретки й переведення рядка |
xs:integer |
ціле значення |
xs:boolean |
Бінарні логічні значення: true або false ,1 або 0. |
xs:float |
32-бітне число з плаваючою комою |
xs:double |
64-бітне число з плаваючою комою |
xs:anyURI |
Універсальний ідентифікатор ресурсу (Uniform Resource Identifier) |
Тег
<xs:attribute name="ім'я атрибуту" type="ім'я типу" />
дозволяє описувати атрибути.
Існує також велика кількість додаткових параметрів тегів. Параметр maxOccurs
задає максимальну кількість
входжень елемента, minOccurs
задає мінімальну кількість входжень елемента, unbounded
визначає
необмежену кількість входжень, required
визначає обов'язкове входження, mixed
визначає
елемент, який має змішаний тип та ін.
Для нашого студента можна запропонувати такий файл схеми (Student.xsd
):
<?xml version="1.0"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="Student"> <xs:complexType> <xs:sequence> <xs:element name="Comments" type="xs:string"/> <xs:element name="Marks"> <xs:complexType> <xs:sequence> <xs:element ref="Mark" maxOccurs="unbounded"/> </xs:sequence> </xs:complexType> </xs:element> </xs:sequence> <xs:attribute name="Name" type="xs:string" /> <xs:attribute name="Surname" type="xs:string" /> </xs:complexType> </xs:element> <xs:element name="Mark"> <xs:complexType> <xs:attribute name="Subject" type="xs:string" /> <xs:attribute name="Value" type="xs:string" /> </xs:complexType> </xs:element> </xs:schema>
2.2.6 Серіалізація в XML-файли
Головним недоліком описаної раніше бінарної серіалізації є необхідність роботи з двійковими (не текстовими) файлами.
Зазвичай такі файли використовують не для довгострокового зберігання даних, а для одноразового зберігання і відновлення
об'єктів. Безумовно, більш зручною й керованою є серіалізація в текстовий файл, зокрема, в XML-документ. Існує декілька
підходів до серіалізації й десеріалізації, побудованої на XML. Найпростішим є підхід, заснований на використанні
класів java.beans.XMLEncoder
і java.beans.XMLDecoder
. Найбільш природне застосування цих
класів – зберігання та відтворення елементів графічного інтерфейсу. Але можна також зберігати об'єкти інших
класів, які відповідають специфікації Java Beans.
Java Bean – це клас, що задовольняє таким вимогам:
- клас відкритий (
public
) - відсутні відкриті дані (відкритими можуть бути тільки методи)
- клас повинен містити усталений конструктор (конструктор без аргументів)
- клас повинен реалізовувати інтерфейс
java.io.Serializable
- пара методів з іменами
setNnn()
іgetNnn()
утворюють властивість з іменемnnn
і відповідним типом. Для властивостей типуboolean
використовують "is
" замість "get
" (isNnn()
).
Раніше були реалізовані класи Line
і Point
. XML-серіалізація не вимагає реалізації інтерфейсу Serializable
.
Однак класи повинні бути відкритими, мати відкриті функції доступу (гетери й сетери) до закритих полів. Клас Point
:
package ua.inf.iwanoff.java.advanced.third; public class Point { private double x, y; public void setX(double x) { this.x = x; } public void setY(double y) { this.y = y; } public double getX() { return x; } public double getY() { return y; } }
Клас Line
:
package ua.inf.iwanoff.java.advanced.third; public class Line { private Point first = new Point(), second = new Point(); public void setFirst(Point first) { this.first = first; } public Point getFirst() { return first; } public Point getSecond() { return second; } public void setSecond(Point second) { this.second = second; } }
Можна запропонувати такий код, який забезпечує XML-серіалізацію:
package ua.inf.iwanoff.java.advanced.third; import java.beans.XMLEncoder; import java.io.*; public class XMLSerialization { public static void main(String[] args) { Line line = new Line(); line.getFirst().setX(1); line.getFirst().setY(2); line.getSecond().setX(3); line.getSecond().setY(4); try (XMLEncoder xmlEncoder = new XMLEncoder(new FileOutputStream("Line.xml"))) { xmlEncoder.writeObject(line); xmlEncoder.flush(); } catch (IOException e) { e.printStackTrace(); } } }
Після виконання програми ми отримаємо такий XML-файл:
<?xml version="1.0" encoding="UTF-8"?> <java version="1.8.0_66" class="java.beans.XMLDecoder"> <object class="ua.inf.iwanoff.java.advanced.third.Line"> <void property="first"> <void property="x"> <double>1.0</double> </void> <void property="y"> <double>2.0</double> </void> </void> <void property="second"> <void property="x"> <double>3.0</double> </void> <void property="y"> <double>4.0</double> </void> </void> </object> </java>
Тепер можна здійснити десеріалізацію за допомогою такого коду:
package ua.inf.iwanoff.java.advanced.third; import java.beans.XMLDecoder; import java.io.*; public class XMLDeserialization { public static void main(String[] args) { try (XMLDecoder xmlDecoder = new XMLDecoder(new FileInputStream("Line.xml"))) { Line line = (Line)xmlDecoder.readObject(); System.out.println(line.getFirst().getX() + " " + line.getFirst().getY() + " " + line.getSecond().getX() + " " + line.getSecond().getY()); } catch (IOException e) { e.printStackTrace(); } } }
Існують також інші (нестандартні) реалізації XML-серіалізації. Одна з найбільш поширених бібліотек – XStream. Ця бібліотека, яка вільно розповсюджується, дозволяє дуже легко серіалізувати та десеріалізувати файли XML і JSON. Для успішної роботи з XStream або іншими зовнішніми бібліотеками, які не є частиною стандартної платформи Java SE, доцільно скористатися засобами управління складанням, зокрема Apache Maven.
2.3 Використання засобів автоматизації складання проєктів
2.3.1 Системи автоматизації складання
Під автоматизацією складання (build automation) розуміють використання спеціальних засобів для автоматичного відстеження взаємних залежностей між файлами в рамках проєкту. Автоматизація складання також передбачає виконання типових дій, таких як, наприклад,
- компіляція сирцевого коду;
- збирання програми з окремих частин;
- підготовка супровідної документації;
- створення jar-архіву;
- розгортання програми.
Інтегровані середовища розробки (IDE) найчастіше беруть на себе управління складанням проєкту. Однак ці засоби, як правило, обмежені й не сумісні в різних IDE. Разом з тим, потреба в перенесенні проєкту в іншу IDE виникає досить часто. Крім того, зручно було б описати й зафіксувати послідовність деяких дій над артефактами проєкту під час виконання типового набору завдань процесу розробки.
Альтернативу пропонують незалежні засоби автоматизації збирання. Зараз серед найбільш популярних засобів автоматизації збирання можна назвати Apache Ant, Gradle й Apache Maven.
Apache Ant (ant – мураха) – заснований на Java сумісний із різними платформами набір
засобів для автоматизації процесу складання програмного продукту (проєкт організації Apache Software Foundation).
Управління процесом складання відбувається за допомогою XML-сценарію – так званого build-файлу (усталено називається build.xml
),
який відповідає певним правилам. Дії, які можна виконувати за допомогою Ant, описуються цілями (targets).
Цілі можуть залежати одна від одної. Якщо до виконання певної мети повинна бути виконана інша мета, то можна визначити залежність однієї
мети від іншої. Цілі містять виклики команд-завдань (завдань) Tasks. Кожне завдання є командою, що виконує
деяку елементарну дію. Існує декілька наперед визначених завдань, які призначені для опису типових дій: компіляція
з допомогою javac
, запуск програми, створення jar, розгортання тощо. Існує можливість самостійного
розширення множини завдань Ant. Завдання Ant включають роботу з файловою системою (створення каталогів, копіювання
і видалення файлів), виклик компілятора, створення jar-архіву, виконання Java-застосунку, генерацію документації
тощо.
На сьогодні засоби Ant стали менш популярні, у порівнянні з Gradle і Maven через їх обмеженість. Крім того, в порівнянні з Maven, Ant пропонує імперативний (командний) підхід до складання проєкту: розробник повинен описати послідовність дій, які виконуються під час збирання, а не очікуваний результат.
Засіб автоматизації збирання Gradle вперше створено в 2007 році під ліцензією Apache License 2.0. У вересні 2012 року вийшла стабільна реалізація 2.7. Gradle використовує концепції Apache Ant and Apache Maven, але замість XML використовує мову, побудовану на синтаксисі мови Groovy. Засоби Gradle використовуються переважно в Android-розробці.
2.3.2 Apache Maven
Apache Maven – це набір управління складанням проєктів, який використовує синтаксис XML для специфікації
опцій складання, але у порівнянні з Ant забезпечує більш високий рівень автоматизації. Maven створюється і публікується
Apache Software Foundation починаючи з 2004 р. Для визначення опцій складання використовують побудовану на XML мову
POM (Project Object Model). На відміну від Apache Ant, Maven, забезпечує декларативне, а не імперативне складання
проєкту в файлах проєкту pom.xml
міститься його декларативний опис (що ми хочемо отримати), а не окремі
команди.
Як і Ant, Maven дозволяє здійснити запуск процесів компіляції, створення jar-файлів, створення дистрибутиву програми, генерації документації тощо.
Найважливішою функцією Maven є управління залежностями, які присутні в проєктах, що використовують сторонні бібліотеки (які, своєю чергою, використовують інші сторонні бібліотеки). Також Maven дозволяє вирішувати конфлікти версій бібліотек.
Maven базується на Plugin-архітектурі, яка дозволяє застосовувати плагіни для різних завдань (compile
, test
, build
, deploy
, checkstyle
, pmd
, scp-transfer
)
без необхідності інсталювати їх в конкретний проєкт. Існує велика кількість готових плагінів.
Інформація для проєкту, що підтримується Maven, міститься у файлі pom.xml
, в якому, зокрема, задають
залежності проєкту, керованого Maven, від інших пакетів і бібліотек.
Для кожного Maven-проєкту визначають так звані координати артефакту (artifact coordinates), в які входить
так звана "трійка" GAV (groupId
, artifactId
, version
).
groupId
– посилання на автора або організацію (підрозділ), де створено проєкт; відповідний ідентифікатор будують за правилами побудови імен пакетів – інвертоване доменне ім'я;artifactId
– назва проєкту; вона не обов'язково повинна збігатися з ім'ям проєкту IntelliJ IDEA, але використання однакових імен в цьому контексті є бажаним; під час створення проєкту це поле автоматично заповнюється ім'ям проєкту;version
– версія проєкту; усталено визначається1.0-SNAPSHOT
, тобто це перша версія проєкту, який знаходиться у стані розробки; для нового проєкту – це 1.0-SNAPSHOT; це означає що проєкт знаходиться у стадії розробки.
Середовище IntelliJ IDEA усталено містить у собі підтримку роботи з Maven-проєктами. Для створення нового проєкту
з підтримкою Maven у вікні New Project у правій частині окрім визначення імені (Name), місця розташування
проєкту в файловій системі (Location) і версії JDK (припустимо, це JDK 17), у рядку Build system слід
вибрати кнопку Maven. Коректне створення Maven-проєкту передбачає визначення координат артефакту. Це можна
зробити, розкривши додаткові налаштування (Advanced Settings), в яких необхідні дані вводяться в рядках GroupId (для
проєктів автора в цьому курсі – ua.inf.iwanoff.java.advanced
) і ArtifactId (наприклад, HelloMaven
).
Версію можна буде визначити пізніше в коді файлу.
Примітка: проєкт можна створити, базуючись на архетипі (archetype) – готовому шаблоні проєкту, але для першого проєкту Maven можна обійтись без архетипів.
Тепер можна натиснути Create та отримати новий порожній проєкт. Для нашого першого проєкту IntelliJ IDEA
автоматично створює файл 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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>ua.inf.iwanoff.java.advanced.third</groupId> <artifactId>HelloMaven</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties> </project>
Як видно, версія артефакту автоматично визначається як 1.0-SNAPSHOT
(робоча версія, пре-реліз).
Примітка: у секції properties
можуть бути також розташовані додаткові опції, наприклад project.build.sourceEncoding
.
Слід звернути увагу на структуру проєкту. Це типова структура проєктів Maven. На рівні проєкту створюється тека src з такою структурою:
src main java resources test java
Каталог src
– це кореневий каталог сирцевого коду і коду тестових класів. Каталог main
– це
кореневий каталог для сирцевого коду, пов'язаного безпосередньо з програмою (без тестів). Каталог test
містить
сирцевий код тесту. Безпосередньо пакети сирцевого коду розташовують у підкаталогах java. Каталог resources
містить
інші ресурси, необхідні для проєкту. Це можуть бути файли властивостей (properties), які використовують для інтернаціоналізації
програми, файли розмічення вікон графічного інтерфейсу користувача, стилів, або щось інше.
Після компіляції проєкту до структури тек на рівні проєкту буде додано каталог target
зі скомпільованими
класами.
Слід звернути увагу на інструментальне вікно Maven, ярлик якого зазвичай розташований праворуч. Якщо розкрити це вікно, можна побачити список команд Maven, які забезпечують життєвий цикл проєкту:
clean
– очищення проєкту і видалення всіх файлів, які були створені попереднім складанням;validate
– перевірка коректності метаінформації про проєкт;compile
– компіляція проєкту;test
– тестування за допомогою JUnit;package
– створення архіву jar, war або ear;verify
– перевірка коректності пакета і відповідності вимогам якості;install
– інсталяція (копіювання) файлів .jar, war або ear в локальний репозиторій;site
– генерація сайту;deploy
– публікація файлів у віддалений репозиторій.
Примітка: якщо використовувати Maven поза IntelliJ IDEA, ці команди вводяться у командному рядку, наприклад: mvn
clean
; для використання Maven без IntelliJ IDEA, його слід завантажити й встановити окремо.
Деякі команди для свого успішного виконання вимагають виконання попередніх команд життєвого циклу. Наприклад, package
передбачає
виконання compile
. Виконання попередніх команд здійснюється автоматично. Виконання команд передбачає
виведення в консольному вікні характерних для Maven повідомлень.
Якщо під час створення проєкту було вибрано опцію Add sample code, у каталозі java
було згенеровано
пакет і клас з функцією main()
, яка виводить "Hello world!":
package ua.inf.iwanoff.java.advanced.third; public class Main { public static void main(String[] args) { System.out.println("Hello world!"); } }
Код класу можна змінити:
package ua.inf.iwanoff.java.advanced.third; public class Main { public static int multiply(int i, int k) { return i * k; } public static void main(String[] args) { System.out.println("Hello, Maven!"); System.out.println("2 * 2 = " + multiply(2, 2)); } }
Серед команд Maven немає безпосереднього виконання програми. Для того, щоб завантажити програму на виконання, слід скористатися засобами IntelliJ IDEA (через меню Run). Але при цьому автоматично виконуються необхідні команди Maven, які покривають певні фази життєвого циклу.
Примітка: набір стандартних команд Maven може бути розширений за допомогою механізму плагінів.
Дуже важлива функція Maven – управління залежностями. Зазвичай реальний проєкт окрім API, яке надає JDK, використовує численні бібліотеки, для підключення яких необхідно завантажувати JAR-файли. Ці бібліотеки базуються на використанні інших бібліотек, які теж треба завантажувати тощо. Окрема проблема виникає з версіями бібліотек та їх сумісністю.
Maven надає простий декларативний підхід до управління залежностями. Достатньо додати відомості про необхідну бібліотеку
в розділі <dependencies>
. Наприклад, для тестування коду нашого проєкту доцільно
додати можливість використовувати JUnit 5. Можна, звичайно додати необхідну залежність вручну, але краще скористатися
інтерактивними можливостями IntelliJ IDEA. Вибравши у вікні редактору файл pom.xml
, слід натиснути Alt+Insert,
далі у списку Generate вибрати Dependency Template і до тексту файлу буде додано секцію <dependencies>.
Потім
у списку артефактів необхідно вибрати junit-jupiter-api
, вказати groupId
рядок org.junit.jupiter
і
версію 5.8.1
. Отримаємо таку секцію <dependencies>
:
<dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.8.1</version> </dependency> </dependencies>
Тепер можна додати тест. Можна використати функцію контекстного меню Generate... | Test... у вікні редактору
сирцевого коду. Паралельну ієрархію пакетів, а також необхідний клас буде додано до гілки test
проєкту.
Якщо у згенерованому коді деякі рядки помічені як помилка, необхідно перезавантажити проєкт Maven. В інструментальному
вікні Maven знаходимо першу кнопку (Reload All Maven Projects). Помилки в pom.xml
повинні
зникнути.
2.4 Робота з файловою системою
2.4.1 Загальні концепції
Java надає можливість роботи не тільки з вмістом файлів, але також з файловою системою в цілому. Файлова система – це спосіб організації даних, який використовується операційною системою для зберігання інформації у вигляді файлів на носіях інформації. Також цим поняттям позначають сукупність файлів і каталогів (тек), які розміщуються на логічному або фізичному пристрої.
До типових функцій взаємодії з файловою системою можна віднести:
- перевірку існування файлу або каталогу
- отримання списку файлів і підкаталогів заданого каталогу
- створення файлів і посилань на файли
- копіювання файлів
- перейменування і переміщення файлів
- управління атрибутами файлів
- видалення файлів
- обхід дерева підкаталогів
- відстеження змін файлів
Для роботи з файловою системою Java надає два підходи:
- використання класу
java.io.File
; - використання засобів пакету
java.nio.file
.
2.4.2 Використання класу File
Пакет java.io
надає можливість роботи як із вмістом файлів, так і з файловою системою в цілому. Цю
можливість реалізує клас File
. Для створення об'єкта цього класу як параметр конструктора слід визначити
повний або відносний шлях до файлу. Наприклад:
File dir = new File("C:\\Users"); File currentDir = new File("."); // Тека проєкту (поточна)
Клас File
містить методи для отримання списку файлів визначеної теки (list()
, listFiles()
),
отримання та модифікації атрибутів файлів (setLastModified()
, setReadOnly()
, isHidden()
, isDirectory()
тощо),
створення нового файлу (createNewFile()
, createTempFile()
), створення тек (mkdir()
),
видалення файлів та тек (delete()
) та багато інших. Роботу деяких з цих методів можна продемонструвати
на наведеному нижче прикладі:
package ua.inf.iwanoff.java.advanced.third; import java.io.*; import java.util.*; public class FileTest { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); System.out.print("Уведіть ім\'я теки, яку ви хочете створити:"); String dirName = scanner.next(); File dir = new File(dirName); // Створюємо нову теку: if (!dir.mkdir()) { System.out.println("Не можна створити теку!"); return; } // Створюємо новий файл всередині нової теки: File file = new File(dir + "\\temp.txt"); file.createNewFile(); // Показуємо список файлів теки: System.out.println(Arrays.asList(dir.list())); file.delete(); // Видаляємо файл dir.delete(); // Видаляємо теку } }
Функція list()
без параметрів дозволяє отримати масив рядків – усіх файлів та підкаталогів теки,
визначеної під час створення об'єкта типу File
. Виводяться відносні імена файлів (без шляхів). У наведеному
нижче прикладі ми отримуємо список файлів та підкаталогів теки, ім'я якої вводиться з клавіатури:
package ua.inf.iwanoff.java.advanced.third; import java.io.File; import java.io.FilenameFilter; import java.util.Scanner; public class ListOfFiles { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.print("Уведіть ім\'я теки:"); String dirName = scanner.next(); File dir = new File(dirName); if (!dir.isDirectory()) { System.out.println("Хибне ім\'я теки!"); return; } String[] list = dir.list(); for(String name : list) { System.out.println(name); } } }
На відміну від list()
, функція listFiles()
повертає масив об'єктів типу File
.
Це надає додаткові можливості – отримання імен файлів з повним шляхом, перевірки значень атрибутів файлів,
окрему роботу з теками тощо. Ці додаткові можливості продемонструємо на такому прикладі:
File[] list = dir.listFiles(); // Виводяться дані про файли в усталеній формі: for(File file : list) { System.out.println(file); } // Виводиться повний шлях: for(File file : list) { System.out.println(file.getCanonicalPath()); } // Виводяться тільки підкаталоги: for(File file : list) { if (file.isDirectory()) System.out.println(file.getCanonicalPath()); }
Для визначення маски-фільтру необхідно створювати об'єкт класу, який реалізує інтерфейс FilenameFilter
.
У наведеному нижче прикладі ми отримуємо список файлів та підкаталогів, імена яких починаються з літери 's'
:
String[] list = dir.list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.toLowerCase().charAt(0) == 's'; } }); for(String name : list) { System.out.println(name); }
Аналогічний параметр типу FilenameFilter
можна застосувати до функції listFiles()
2.4.3 Робота з пакетом java.nio
Пакет java.nio
, який з'явився в JDK 1.4, спочатку включав альтернативні засоби введення-виведення.
Версія Java 7 надає альтернативний підхід до роботи з файловою системою – набір класів, описаних в пакеті java.nio.file
.
Пакет java.nio.file
надає клас Path
, який забезпечує надання шляху в файловій системі.
Окремі складові цього шляху можна уявити деякою колекцією імен проміжних підкаталогів і імені самого файлу (підкаталогу).
Отримати об'єкт класу Path
можна за допомогою методу get()
класу Path
. Методу get()
передається
рядок – шлях:
Path path = Paths.get("c:/Users/Public");
Тепер можна отримати інформацію про шлях:
System.out.println(path.toString()); // c:\Users\Public System.out.println(path.getFileName()); // Public System.out.println(path.getName(0)); // Users System.out.println(path.getNameCount()); // 2 System.out.println(path.subpath(0, 2)); // Users\Public System.out.println(path.getParent()); // c:\Users System.out.println(path.getRoot()); // c:\
Після того, як об'єкт класу Path
створений, його можна використовувати як аргумент статичних функцій
класу java.nio.file.Files
. Для перевірки наявності (відсутності) файлу використовують відповідно функції exists()
і notExists()
:
Path dir = Paths.get("c:/Windows"); System.out.println(Files.exists(dir)); // швидше за все, true System.out.println(Files.notExists(dir)); // швидше за все, false
Наявність двох окремих функцій пов'язано з можливістю отримання невизначеного результату (заборонений доступ до файлу).
Для того, щоб переконатися, що програма може отримати необхідний доступ до файлу, можна використовувати методи isReadable(Path)
, isWritable(Path)
і isExecutable(Path)
.
Припустимо, створений об'єкт file типу Path
і заданий шлях до файлу. Наведений нижче фрагмент коду
перевіряє, чи існує конкретний файл, і чи можна завантажити його на виконання:
boolean isRegularExecutableFile = Files.isRegularFile(file) & Files.isReadable(file) & Files.isExecutable(file);
Для отримання метаданих (даних про файлах і каталогах) клас Files
надає низку статичних методів:
Методи | Пояснення |
---|---|
size(Path) |
Повертає розмір зазначеного файлу в байтах |
isDirectory(Path, LinkOption...) |
Повертає |
isRegularFile(Path, LinkOption...) |
Повертає true , якщо вказаний Path вказує на звичайний файл |
isHidden(Path) |
Повертає true , якщо вказаний Path вказує на прихований
файл |
getLastModifiedTime(Path, LinkOption...) setLastModifiedTime(Path, FileTime) |
Повертає / встановлює час останньої зміни зазначеного файлу |
getOwner(Path, LinkOption...) setOwner(Path, UserPrincipal) |
Повертає / встановлює власника файлу |
getAttribute(Path, String, LinkOption...) setAttribute(Path, String, Object, LinkOption...) |
Повертає або встановлює значення атрибута файлу |
Для ОС Windows різних версій рядок атрибута повинен починатися з префікса "dos:
". Наприклад,
так можна встановити необхідні атрибути деякого файлу:
Path file = ... Files.setAttribute(file, "dos:archive", false); Files.setAttribute(file, "dos:hidden", true); Files.setAttribute(file, "dos:readonly", true); Files.setAttribute(file, "dos:system", true);
Читання необхідних атрибутів може також здійснюватися за допомогою методу readAttributes()
. Його другий
параметр – метадані тип результату. Ці метадані можуть бути отримані через значення поля class
(метадані
типів будуть розглянуті пізніше). Найбільш відповідний тип результату – це клас java.nio.file.attribute.BasicFileAttributes
.
Наприклад, так можна отримати деякі дані про файл:
package ua.inf.iwanoff.java.advanced.third; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.Scanner; public class Attributes { public static void main(String[] args) throws Exception { System.out.println("Введіть ім\'я файлу або каталогу:"); Path path = Paths.get(new Scanner(System.in).nextLine()); BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class); System.out.println("Час створення: " + attr.creationTime()); System.out.println("Час останнього доступу: " + attr.lastAccessTime()); System.out.println("Час останньої зміни: " + attr.lastModifiedTime()); System.out.println("Каталог: " + attr.isDirectory()); System.out.println("Звичайний файл: " + attr.isRegularFile()); System.out.println("Розмір: " + attr.size()); } }
Клас DosFileAttributes
, похідний від BasicFileAttributes
, надає також функції isReadOnly()
, isHidden()
, isArchive()
і isSystem()
.
На відміну від java.io
, клас java.nio.file.Files
надає функцію copy()
для
копіювання файлів. Наприклад:
Files.copy(Paths.get("c:/autoexec.bat"), Paths.get("c:/Users/autoexec.bat")); Files.copy(Paths.get("c:/autoexec.bat"), Paths.get("c:/Users/autoexec.bat"), StandardCopyOption.REPLACE_EXISTING);
Існують також опції StandardCopyOption.ATOMIC_MOVE
і StandardCopyOption.COPY_ATTRIBUTES
.
Опції можна вказувати через кому.
Для переміщення файлів використовують функцію move()
(з аналогічними атрибутами або без них). Перейменування
виконується тією ж функцією:
Files.move(Paths.get("c:/Users/autoexec.bat"), Paths.get("d:/autoexec.bat"));// переміщення Files.move(Paths.get("d:/autoexec.bat"), Paths.get("d:/unnecessary.bat"));// перейменування
Створення нових каталогів здійснюється за допомогою функції createDirectory()
класу Files
.
Параметр функції має тип Path
.
Path dir = Paths.get("c:/NewDir"); Files.createDirectory(dir);
Для створення каталогу кількох рівнів в глибину, коли один або кілька батьківських каталогів, можливо, ще не існує,
можна використовувати метод createDirectories()
:
Path dir = Paths.get("c:/NewDir/1/2"); Files.createDirectories(dir);
Для отримання списку файлів підкаталогу можна скористатися класом DirectoryStream
.
package ua.inf.iwanoff.java.advanced.third; import java.io.IOException; import java.nio.file.*; public class FileListDemo { public static void main(String[] args) { Path dir = Paths.get("c:/Windows"); try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) { for (Path p : ds) { System.out.println(p.getFileName()); } } catch (IOException e) { e.printStackTrace(); } } }
Видалення файлів і тек здійснюється за допомогою функцій delete()
і deleteIfExists()
:
Files.delete(Paths.get("d:/unnecessary.bat")); Files.deleteIfExists(Paths.get("d:/unnecessary.bat"));
Для обходу дерева каталогів пакет java.nio.file
надає засоби, які потребують реалізації рекурсивних
алгоритмів. Існує метод walkFileTree()
класу Files
, що забезпечує обхід дерева підкаталогів.
Як параметри необхідно вказати початковий каталог (об'єкт типу Path
), а також об'єкт, який реалізує
узагальнений інтерфейс FileVisitor
.
Примітка: існує інший варіант методу, що дозволяє задавати також опції обходу каталогів і обмеження на глибину обходу підкаталогів.
Для реалізації інтерфейсу FileVisitor
треба визначити методи preVisitDirectory()
, postVisitDirectory()
, visitFile()
і visitFileFailed()
.
Результат цих функцій – перелік типу FileVisitResult
. Можливі значення цього перерахування – CONTINUE
(продовжувати
пошук), TERMINATE
(припинити пошук), SKIP_SUBTREE
(пропустити піддерево) і SKIP_SIBLINGS
(пропустити
елементи того ж рівня).
Щоб кожного разу не реалізовувати всі методи інтерфейсу FileVisitor
, можна скористатися узагальненим
класом SimpleFileVisitor
. Цей клас надає усталену реалізацію функцій інтерфейсу. В цьому випадку необхідно
тільки перекрити потрібні функції. У наведеному нижче прикладі здійснюється пошук всіх файлів заданого каталогу
і його підкаталогів:
package ua.inf.iwanoff.java.advanced.third; import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.Scanner; public class FindAllFiles { private static class Finder extends SimpleFileVisitor<Path> { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { System.out.println("----------------" + dir + "----------------"); return FileVisitResult.CONTINUE; } } public static void main(String[] args) { String dirName = new Scanner(System.in).nextLine(); try { Files.walkFileTree(Paths.get(dirName), new Finder()); // Поточний каталог } catch (IOException e) { e.printStackTrace(); } } }
Для пошуку файлів можна користуватися масками (так звані "glob"-маски), які активно застосовують в усіх
операційних системах. Приклади таких масок – "a*.*
" (Імена файлів починаються з літери a
), "*.txt
" (файли
з розширенням *.txt
) тощо. Припустимо, рядок pattern
містить таку маску. Далі створюємо
об'єкт PathMatcher
:
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern);
У наведеному нижче прикладі у визначеному каталозі здійснюється пошук файлів за вказаною маскою:
package uua.inf.iwanoff.java.advanced.third; import java.io.IOException; import java.nio.file.*; import java.util.Scanner; public class FindMatched { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); String dirName = scanner.nextLine(); String pattern = scanner.nextLine(); Path dir = Paths.get(dirName); PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern); try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) { for (Path file : ds) { if (matcher.matches(file.getFileName())) { System.out.println(file.getFileName()); } } } catch (IOException e) { e.printStackTrace(); } } }
Маски можуть поєднуватися з обходом дерева каталогів.
Одне із завдань файлової системи – відстеження стану зазначеного каталогу. Наприклад, програма повинна оновлювати
дані про файли й підкаталогах деякого каталогу, якщо інші процеси або потоки управління зумовили виникнення, зміну,
видалення файлів і тек тощо. Пакет java.nio.file
надає засоби для реєстрації таких каталогів і відстеження
їх стану. Для відстеження змін можна реалізувати інтерфейс WatchService
. Відповідну реалізацію можна
отримати за допомогою функції FileSystems.getDefault().newWatchService()
). Клас StandardWatchEventKinds
надає
необхідні константи для можливих подій.
Спочатку необхідно зареєструвати необхідний каталог, а потім в нескінченному циклі читати інформацію про події
пов'язані з його змінами. Інтерфейс WatchEvent
надає опис можливої події. Наприклад, можна запропонувати
таку програму:
package ua.inf.iwanoff.java.advanced.third; import java.nio.file.*; import java.util.Scanner; import static java.nio.file.StandardWatchEventKinds.*; public class WatchDir { public static void main(String[] args) throws Exception { System.out.println("Введіть ім\'я каталогу:"); Path dir = Paths.get(new Scanner(System.in).nextLine()); // Створюємо об'єкт WatchService: WatchService watcher = FileSystems.getDefault().newWatchService(); // Реєструємо події, які ми відстежуємо: WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); while (true) { // нескінчений цикл key = watcher.take(); // чекаємо на наступний набір подій for (WatchEvent<?> event: key.pollEvents()) { WatchEvent<Path> ev = (WatchEvent<Path>)event; System.out.printf("%s: %s\n", ev.kind().name(), dir.resolve(ev.context())); } key.reset(); // оновлюємо стан набору подій } } }
Бібліотека java.nio.file
підтримує роботу як з символьними посиланнями (symlinks, soft links),
так і з жорсткими посиланнями (hard links). Метод createSymbolicLink(нове_посилання, об'єкт_що_існує)
класу Files
створює
символьне посилання, метод createLink(нове_посилання, файл_що_існує)
створює жорстке посилання.
Метод isSymbolicLink()
повертає true
, якщо переданий йому об'єкт – символьне
посилання. Метод readSymbolicLink()
) дозволяє знайти об'єкт, на який посилається символьне посилання.
2.5 Використання засобів java.nio для читання і запису даних
2.5.1 Загальні концепції
У порівнянні з традиційними потоками введення-виведення, java.nio
забезпечує вищу ефективність
операцій введення-виведення. Це досягається через те, що традиційні засоби введення-виведення працюють з
даними в потоках, в той час як java.nio
працює з даними в блоках. Центральними об'єктами в java.nio
є "Канал" (Channel
)
і "Буфер" (Buffer
).
- Канали використовують для забезпечення передачі даних, наприклад, між файлами та буферами. Окрім роботи з файлами, канали також застосовують для роботи з дейтаграмами й сокетами.
- Буфер – це контейнерний об'єкт. Всі дані, які передаються в канал, повинні бути спочатку поміщені в буфер.
Будь-які дані, які зчитуються з каналу, потрапляють в буфер. Додаткова ефективність забезпечується через інтеграцію об'єктів-буферів з буферами операційної системи.
Засоби java.nio
ефективні при роботі з двійковими файлами, в першу чергу,
в умовах багатопотоковості, де використовують спеціальні об'єкти – селектори.
2.5.2 Використання класу Files для роботи з текстовими файлами
Окрім складних механізмів роботи з каналами, буферами й селекторами, пакет java.nio.file
надає прості засоби
читання з текстових файлів і запису в текстові файли.
Для читання даних можна використати такі статичні функції:
readString()
читає поточний рядок зі вказаного файлу (змінна типуPath
);readAllLines()
читає всі рядки зі вказаного файлу.
Так, наприклад можна прочитати перший рядок з файлу:
Path path = Paths.get("SomeFile.txt"); String s = Files.readString(path);
А так можна прочитати всі рядки текстового файлу:
Path path = Paths.get("SomeFile.txt"); List<String> lines = Files.readAllLines(path); for (String s: lines) { System.out.println(s); }
Наведені нижче функції використовують для запису:
-
writeString()
записує рядок в поточну позицію файлу write()
– більш універсальна функція, яка записує масив байтів.
Приклад використання функції writeString()
:
Path path = Paths.get("newFile.txt"); String question = "Бути чи не бути?"; Files.writeString(path, question);
Усталено використовується кодування проєкту. Для того, щоб, наприклад явно вказати кодову таблицю UTF-8, слід скористатися складнішою формою функції. Можна визначати додаткові опції:
Files.writeString(path, question, StandardCharsets.UTF_8, StandardOpenOption.CREATE);
Приклад використання функції write()
:
Path path = Paths.get("newFile.txt"); String question = "Бути чи не бути?"; Files.write(path, question.getBytes());
Є також статичні функції для взаємодії з потоками java.io
. Наприклад, можна отримати потік BufferedReader
:
BufferedReader bufferedReader = Files.newBufferedReader(Paths.get("SomeFile.txt")); System.out.println(bufferedReader.readLine());
Можна також скористатися класом Scanner
:
Scanner scanner = new Scanner(Files.newBufferedReader(Paths.get("SomeFile.txt"))); System.out.println(scanner.nextLine());
Аналогічно можна здійснювати виведення за допомогою потоків java.io
. Наприклад:
PrintWriter printWriter = new PrintWriter(Files.newBufferedWriter(Paths.get("newFile.txt"))); printWriter.println("Done!");
Слід не забувати закривати потоки. Для коректного закриття файлу дії з потоками доцільно виконувати з використанням
конструкції try-with-resources
.
2.6 Робота з текстовими файлами засобами Stream API
Потоки Stream API інтегровані з роботою з текстовими файлами і засобами java.nio.file
.
Можливості читання з текстових файлів продемонструємо, читаючи з файлу source.txt
. Припустимо, такий файл розташовано
в теці проєкту і він має такий вміст:
First Second Third
Статичний метод lines()
класу Files
використовують для читання з текстового файлу рядків і створення потоку. В
наведеному нижче прикладі всі рядки файлу source.txt
читаються й виводяться на консоль. Створення потоку
доцільно розташувати в блоку try-with-resources:
try (Stream<String> strings = Files.lines(Path.of("source.txt"))) { strings.forEach(System.out::println); } catch (IOException e) { throw new RuntimeException(e); }
Примітка: якби необхідно було працювати лише з одним рядком, або з частиною рядків, тільки необхідні рядки були б зчитані з файлу.
Такі ж результати можна отримати, скориставшись класом java.io.BufferedReader
:
try (BufferedReader bufferedReader = Files.newBufferedReader(Paths.get("source.txt"))) { Stream<String> strings = bufferedReader.lines(); strings.forEach(System.out::println); } catch (IOException e) { throw new RuntimeException(e); }
Можна також створити список:
List<String> list = Files.readAllLines(Path.of("source.txt")); Stream<String> lines = list.stream();
Для запису в файл можна скористатися функцією Files.write()
:
Stream<String> digits = Stream.of("1", "3", "2", "4"); Files.write(Path.of("digits.txt"), digits.toList());
Приклад 3.2 демонструє роботу з файлами у поєднанні зі Stream API.
2.7 Робота з файлами формату JSON
2.7.1 Формат файлів JSON та його особливості
JSON – це легковагий формат обміну даних, який переважно використовують для обміну даними між комп'ютерами на різних рівнях взаємодії. Назва JSON – скорочення від JavaScript Object Notation (нотація запису об'єктів JavaScript). Попри те, що синтаксис JSON – це синтаксис для об'єктів JavaScript, файли JSON можна використовувати окремо від JavaScript. Зараз робота з цим форматом підтримується багатьма мовами програмування.
JSON можна розглядати як легку і сучасну альтернативу XML. Є багато спільного в XML-документів та файлів JSON:
- файли завжди текстові;
- формат не вимагає додаткових пояснень для людини;
- дані передбачають можливість ієрархічного представлення.
Але на відміну від XML-документів, файли JSON коротші, легші для читання, а також пропонують деякі додаткові можливості.
Припустимо, раніше було створено такий XML-документ:
<students> <student> <firstName>Frodo</firstName> <lastName>Baggins</lastName> </student> <student> <firstName>Samwise</firstName> <lastName>Gamgee</lastName> </student> </students>
Відповідний файл JSON буде таким (students.json
):
{"students":[ { "firstName":"Frodo", "lastName":"Baggins" }, { "firstName":"Samwise", "lastName":"Gamgee" } ]}
У наведеному файлі присутні основні елементи синтаксису JSON:
- масив "students", елементи якого розташовані в квадратних дужках;
- об'єкти, розташовані в фігурних дужках;
- рядки.
Окрім рядків, значеннями можуть бути числа, булеві значення (false
і true
)
і null
.
2.7.2 Використання бібліотеки org.json для роботи з JSON-файлами
Бібліотека org.json існує з кінця 2010 року і спочатку була реалізована Дугласом Крокфордом, автором JSON. Тому можна розглядати цю бібліотеку як еталонну реалізацію для JSON в Java.
Найпростіший шлях підключення бібліотеки org.json до проєкту – додати до файлу pom.xml
необхідну
залежність:
<dependencies> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20230227</version> </dependency> </dependencies>
Примітка: актуальна версія бібліотеки може змінюватися.
Відповідно до типів значень в JSON-файлі в бібліотеці визначені типи JSONObject
і JSONArray
.
Є різні способи створення об'єкту JSONObject
. Можна створити його вручну, наприклад:
JSONObject someObject = new JSONObject() .put("number", 10) .put("object", new JSONObject() .put("greetings", "Hello")) .put("array", new JSONArray() .put(12.95) .put("Some text"));
Можна прочитати його з рядка:
JSONObject theSame = new JSONObject(new JSONTokener(""" { "number": 10, "object": { "greetings":"Hello" }, "array": [ 12.95, "Some text" ] } """));
Для того, щоб прочитати дані з JSON-файлу, який існує, можна скористатися статичною функцією readAllBytes()
класу Files
.
Створивши об'єкт JSONObject
, можна розділити його на окремі об'єкти й масиви. За допомогою класу FileWriter
можна
записати дані в новий файл. Ці дії ми розглянемо на прикладі.
Раніше було створено файл students.json
:
{"students":[ { "firstName":"Frodo", "lastName":"Baggins" }, { "firstName":"Samwise", "lastName":"Gamgee" } ]}
Прочитавши з цього файлу дані, ми можемо додати ще двох студентів і записати в новий JSON-файл. Код програми може бути таким:
package ua.inf.iwanoff.java.advanced.third; import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import org.json.JSONArray; import org.json.JSONObject; public class JsonTest { public static void main(String[] args) throws IOException { JSONObject object = new JSONObject(new String(Files.readAllBytes(Paths.get("students.json")))); System.out.println(object.toString(1)); JSONArray students = object.getJSONArray("students"); for (int i = 0; i < students.length(); i++) { JSONObject student = students.getJSONObject(i); System.out.println(" - " + student.getString("firstName")); } students.put(new JSONObject().put("firstName", "Merry").put("lastName", "Brandybuck")); students.put(new JSONObject().put("firstName", "Pippin").put("lastName", "Took")); try (FileWriter file = new FileWriter("newStudents.json")) { file.write(object.toString(1)); } } }
Використання методу toString(1)
дозволяє отримати форматований JSON-файл:
{"students": [ { "firstName": "Frodo", "lastName": "Baggins" }, { "firstName": "Samwise", "lastName": "Gamgee" }, { "firstName": "Merry", "lastName": "Brandybuck" }, { "firstName": "Pippin", "lastName": "Took" } ]}
Існують також інші бібліотеки для роботи з JSON-файлами – Gson (від Google), Jackson, JSON-P, JSON-B тощо.
2.8 Серіалізація в файли XML і JSON за допомогою засобів XStream
В курсі "Основи програмування Java" були розглянуті технології серіалізації й десеріалізації об'єктів – запису й відтворення стану об'єктів з використанням послідовних потоків, зокрема, файлів.
Окрім бінарної серіалізації були розглянуті стандартні засоби XML-серіалізації. Недоліками стандартних засобів серіалізації в XML є:
- обмеження на типи об'єктів (JavaBeans);
- можливість серіалізації лише властивостей, які визначені публічними сетерами й гетерами;
- відсутність можливості керувати форматом та іменами тегів.
Існують нестандартні реалізації XML-серіалізації. Одна з найбільш поширених бібліотек – XStream. Ця бібліотека,
яка вільно розповсюджується, дозволяє дуже легко серіалізувати та десеріалізувати файли XML. Для роботи з цією бібліотекою
достатньо завантажити необхідні JAR-файли. Але зручніший і сучасніший підхід забезпечує використання Maven для підключення
бібліотеки. Слід додати до файлу pom.xml
необхідну залежність:
<dependencies> <dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> <version>1.4.20</version> </dependency> </dependencies>
Бібліотека також дозоляє серіалізувати та десеріалізувати файли JSON. У прикладі 3.3 наведено програмний код, який дозволяє здійснити серіалізацію й десеріалізацію даних.
2.9 Робота з системним журналом
2.9.1 Загальні відомості про логування
Робота з системним журналом (логування) використовується для реєстрації в спеціальному файлі (як правило, текстовому) протоколу подій, які відбуваються під час роботи програми. Це, наприклад, трасування виконання конструкторів, методів, блоків обробки критичних ситуацій та інші повідомлення, пов'язані зі зневадженням.
Логер (logger) являє собою точку входу в систему логування. Кожен логер можна уявити як іменований канал для повідомлень, в який вони відправляються для обробки.
Важливим поняттям логування є рівень логування (log level), що визначає відносну важливість повідомлень, які протоколюються. Коли повідомлення передається в логер, рівень логування повідомлення порівнюється з рівнем логування логера. Якщо рівень логування повідомлення вище або дорівнює рівню логування логера, повідомлення буде оброблено, в іншому випадку – проігноровано.
2.9.2 Стандартні засоби Java для логування
Стандартні засоби пакету java.util.logging
надають можливість протоколювання подій. Є такі рівні
логування за зростанням: FINEST
, FINER
, FINE
, CONFIG
,
INFO
, WARNING
, SEVERE
, а також ALL
і OFF
, що
вмикає та вимикає всі рівні відповідно. Для створення логера слід використовувати статичні методи класу java.util.logging.Logger
.
Наприклад:
Logger log = Logger.getLogger("MyLog"); log.setLevel(Level.ALL);
Ім'я журналу визначають довільно. Тепер у створений таким чином журнал можна писати дані, зокрема, повідомлення:
log.log(Level.INFO, "Все OK"); // виведення на консоль
Якщо ми хочемо, щоб повідомлення також заносилися в файл, слід скористатися класом java.util.logging.FileHandler
:
FileHandler fileHandler = new FileHandler("C:/MyFile.log"); log.addHandler(fileHandler); log.log(Level.INFO, "Все OK"); // виведення на консоль і у файл
Примітка: використання запису журналу в файл передбачає перехоплення винятку
java.io.IOException
.
У наведеному нижче прикладі створюється об'єкт-журнал, в який заноситимуться повідомлення всіх рівнів. Одночасно з виведенням на консоль здійснюється запис у визначений файл:
package ua.inf.iwanoff.java.advanced.third; import java.io.IOException; import java.util.logging.FileHandler; import java.util.logging.Level; import java.util.logging.Logger; public class LogDemo { public static void main(String[] args) throws IOException { Logger log = Logger.getLogger("MyLog"); log.setLevel(Level.ALL); FileHandler fileHandler = new FileHandler("C:/MyFile.log"); log.addHandler(fileHandler); log.log(Level.INFO, "Все OK"); // виведення на консоль і у файл } }
Для конфігурації стандартних засобів логування використовують спеціальний файл властивостей (файл з розширенням
.properties
). Зокрема, можна окремо задавати параметри логування для виведення на консоль і в файл.
2.9.3 Використання бібліотеки Log4j
Є істотні недоліки стандартних інструментів логування (java.util.logging
). Це труднощі
налаштування, низька ефективність, обмежені можливості журналювання, недостатньо інтуїтивна конфігурація. Ці
недоліки стимулювали незалежний розвиток альтернативних бібліотек логування.
Apache Log4j 2 – бібліотека логування (протоколювання) програм Java, яка фактично стала промисловим стандартом. Вона забезпечує значні покращення у порівнянні зі своїм попередником Log4j 1. З 2015 р версія Log4j 1 не рекомендується до використання.
Зараз актуальною є версія 2.20. API Log4j можна завантажити за адресою: https://logging.apache.org/log4j/2.x/.
Для того, щоб скористатися можливостями бібліотеки Log4j 2, можна в середовищі IntelliJ IDEA створити новий
проєкт з підтримкою Maven, наприклад, log4j-test
. До файлу pom.xml
додаємо залежності:
<dependencies> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.20.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.20.0</version> </dependency> </dependencies>
Після перезавантаження проєкту (кнопка Reload All Maven Projects) у проєкті можна користатися Log4j 2.
Створюємо клас з функцією main()
. Для здійснення логування у програмі слід створити об'єкт класу
org.apache.logging.log4j.Logger
. Цей об'єкт дозволяє здійснювати запис в журнал повідомлення
відповідно до встановленого рівня.
package ua.inf.iwanoff.java.advanced.third; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class HelloLog4J { public static void main(String[] args) { Logger logger = LogManager.getLogger(HelloLog4J.class); logger.fatal("Hello, Log4j!"); } }
Повідомленню "Hello, Log4j!
" передує інформація про дату та час, функцію і клас.
Параметри логування зберігаються в спеціальному файлі конфігурації. Оскільки конфігурацію логування поки не
визначено (немає відповідного файлу), працює усталена (default) конфігурація, згідно з якою виводяться тільки
повідомлення рівня error
та fatal
. Рівень логування fatal
, який
використано для виведення повідомлення, має найвищий пріоритет. Усталено всі повідомлення виводяться на консоль.
Для того, щоб змінити політику логування, слід створити файл конфігурації. Його ім'я –
log4j2.xml
. Такий файл слід створити у теці java\src\resources
. Його вміст в
найпростішому випадку буде таким:
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="INFO"> <Appenders> <Console name="ConsoleAppender" target="SYSTEM_OUT"/> <File name="FileAppender" fileName="hello-app-${date:yyyyMMdd}.log" immediateFlush="false" append="true"/> </Appenders> <Loggers> <Root level="debug"> <AppenderRef ref="ConsoleAppender" /> <AppenderRef ref="FileAppender"/> </Root> </Loggers> </Configuration>
У файлі присутня група <Appenders>
, в якій вказано, що виведення здійснюється на консоль і в
файл, ім'я якого будується з "hello-app
" і поточної дати. У групі
<Loggers>
вказані рівні виведення з напрямками виведення. В нашому випадку це "debug
".
Log4J підтримує такі рівні виведення, в порядку зростання пріоритету:
trace – трасування всіх повідомлень в зазначений апендер debug - детальна інформація для зневадження info - інформація warn - попередження error - помилка fatal - фатальна помилка - у цього повідомлення найвищий пріоритет.
Встановлення певного рівня означає, що будуть запротокольовані тільки повідомлення, що виводяться з цим або
вищим пріоритетом. Тому в нашому випадку здійснюється також виведення повідомлень рівня
fatal
.
Оскільки усталена конфігурація більше не працює, з повідомлень зникла інформація про дату та час, функцію і
клас. Її можна відтворити змінивши вміст файлу log4j2.xml
:
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="INFO"> <Appenders> <Console name="ConsoleAppender" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %logger{36} - %msg%n" /> </Console> <File name="FileAppender" fileName="hello-app-${date:yyyyMMdd}.log" immediateFlush="false" append="true"> <PatternLayout pattern="%d{yyy-MM-dd HH:mm:ss.SSS} [%t] %logger{36} - %msg%n"/> </File> </Appenders> <Loggers> <Root level="debug"> <AppenderRef ref="ConsoleAppender" /> <AppenderRef ref="FileAppender"/> </Root> </Loggers> </Configuration>
Окрім формату XML, конфігураційний файл можна створювати у форматах JSON, YAML, або PROPERTIES.
3 Приклади програм
3.1 Використання технології DOM
Припустимо, підготовлений XML-документ з даними про континент (Continent.xml):
<?xml version="1.0" encoding="UTF-8"?> <ContinentData Name="Європа"> <CountriesData> <CountryData Name="Україна" Area="603700" Population="46314736" > <CapitalData Name="Київ" /> </CountryData> <CountryData Name="Франція" Area="547030" Population="61875822" > <CapitalData Name="Москва" /> </CountryData> <CountryData Name="Німеччина" Area="357022" Population="82310000" > <CapitalData Name="Берлін" /> </CountryData> </CountriesData> </ContinentData>
Примітка: помилка зі столицею Франції зроблена навмисне.
Необхідно засобами DOM прочитати дані, виправити помилку і зберегти в новому файлі. Програма матиме такий вигляд:
package ua.inf.iwanoff.java.advanced.third; import java.io.*; import org.w3c.dom.*; import javax.xml.parsers.*; import javax.xml.transform.*; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; public class ContinentWithDOM { public static void main(String[] args) { try { Document doc; DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder(); doc = db.parse(new File("Continent.xml")); Node rootNode = doc.getDocumentElement(); mainLoop: for (int i = 0; i < rootNode.getChildNodes().getLength(); i++) { Node countriesNode = rootNode.getChildNodes().item(i); if (countriesNode.getNodeName().equals("CountriesData")) { for (int j = 0; j < countriesNode.getChildNodes().getLength(); j++) { Node countryNode = countriesNode.getChildNodes().item(j); if (countryNode.getNodeName().equals("CountryData")) { // Знаходимо атрибут за іменем: if (countryNode.getAttributes().getNamedItem("Name").getNodeValue().equals("Франція")) { for (int k = 0; k < countryNode.getChildNodes().getLength(); k++) { Node capitalNode = countryNode.getChildNodes().item(k); if (capitalNode.getNodeName().equals("CapitalData")) { capitalNode.getAttributes().getNamedItem("Name").setNodeValue("Париж"); break mainLoop; } } } } } } } Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.transform(new DOMSource(doc), new StreamResult(new FileOutputStream(new File("CorrectedConinent.xml")))); } catch (Exception e) { e.printStackTrace(); } } }
3.2 Читання з файлу й сортування рядків
Припустимо, нам необхідно прочитати з текстового файлу рядки, розсортувати за порядком, зворотним до алфавітного та записати в новий файл рядки, які починаються з літери "F".
Файл з рядками (strings.txt
) може бути таким:
First Second Third Fourth Fifth
Програма може бути такою:
package uua.inf.iwanoff.java.advanced.third; import java.io.BufferedReader; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.stream.Stream; public class ReadAndSort { public static void main(String[] args) throws IOException { try (BufferedReader reader = Files.newBufferedReader(Paths.get("strings.txt"))) { Stream<String> stream = reader.lines().sorted((s1, s2) -> s2.compareTo(s1)). filter(s -> s.startsWith("F")); Files.write(Paths.get("results.txt"), stream.toList()); } } }
Після роботи програми отримаємо файл results.txt
:
Fourth First Fifth
3.3 Серіалізація і десеріалізація за допомогою бібліотеки XStream
Припустимо, необхідно здійснити серіалізацію і десеріалізацію даних про лінію, яка описується двома точками. Створюємо
новий Maven-проєкт LineAndPoints
. До файлу pom.xml
додаємо залежність від бібліотеки xstream.
Отримаємо такий файл 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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>ua.inf.iwanoff.java.advanced.third</groupId> <artifactId>LineAndPoints</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> <version>1.4.20</version> </dependency> </dependencies> </project>
Створюємо класи Line
і Point
. Ці класи не мають конструкторів без параметрів і публічних
властивостей, тому вони не можуть бути серіалізовані за допомогою java.beans.XMLEncoder
і java.beans.XMLDecoder
.
Але XStream дозволяє їх серіалізувати, оскільки ця бібліотека орієнтується на поля, а не на властивості.
Клас Point
:
package ua.inf.iwanoff.java.advanced.third; public class Point { private double x, y; public Point(double x, double y) { this.x = x; this.y = y; } @Override public String toString() { return x + " " + y; } }
Клас Line
:
package ua.inf.iwanoff.java.advanced.third; public class Line { private Point first, second; public Line(double firstX, double firstY, double secondX, double secondY) { first = new Point(firstX, firstY); second = new Point(secondX, secondY); } @Override public String toString() { return first + " " + second; } }
Для запису в файл можна створити такий клас:
package ua.inf.iwanoff.java.advanced.third; import com.thoughtworks.xstream.XStream; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; public class XMLSerialization { public static void main(String[] args) { XStream xStream = new XStream(); Line line = new Line(1, 2, 3, 4); xStream.alias("line", Line.class); String xml = xStream.toXML(line); try (FileWriter fw = new FileWriter("Line.xml"); PrintWriter out = new PrintWriter(fw)) { out.println(xml); } catch (IOException e) { e.printStackTrace(); } } }
Отримуємо XML-файл:
<line> <first> <x>1.0</x> <y>2.0</y> </first> <second> <x>3.0</x> <y>4.0</y> </second> </line>
Примітка: якщо не використовувати аліас, кореневий тег буде таким: <ua.inf.iwanoff.java.advanced.third.Line>
В іншій програмі здійснюємо читання:
package ua.inf.iwanoff.java.advanced.third; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.security.AnyTypePermission; import java.io.File; public class XMLDeserialization { public static void main(String[] args) { XStream xStream = new XStream(); xStream.addPermission(AnyTypePermission.ANY); xStream.alias("line", Line.class); Line newLine = (Line) xStream.fromXML(new File("Line.xml")); System.out.println(newLine); } }
Для того, щоб скористатися засобами роботи XStream з JSON-файлами, до файлу pom.xml
необхідно додати ще одну залежність:
<dependency> <groupId>org.codehaus.jettison</groupId> <artifactId>jettison</artifactId> <version>1.5.2</version> </dependency>
Програма серіалізації в JSON-файл буде такою:
package ua.inf.iwanoff.java.advanced.third; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.json.JsonHierarchicalStreamDriver; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; public class JSONSerialization { public static void main(String[] args) { XStream xStream = new XStream(new JsonHierarchicalStreamDriver()); Line line = new Line(1, 2, 3, 4); xStream.alias("line", Line.class); String xml = xStream.toXML(line); try (FileWriter fw = new FileWriter("Line.json"); PrintWriter out = new PrintWriter(fw)) { out.println(xml); } catch (IOException e) { e.printStackTrace(); } } }
Отримаємо такий JSON-файл:
{"line": { "first": { "x": 1.0, "y": 2.0 }, "second": { "x": 3.0, "y": 4.0 } }}
Програма десеріалізації з JSON-файлу буде такою:
package uua.inf.iwanoff.java.advanced.third; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.json.JettisonMappedXmlDriver; import com.thoughtworks.xstream.security.AnyTypePermission; import java.io.File; public class JSONDeserialization { public static void main(String[] args) { XStream xStream = new XStream(new JettisonMappedXmlDriver()); xStream.addPermission(AnyTypePermission.ANY); xStream.alias("line", Line.class); Line newLine = (Line) xStream.fromXML(new File("Line.json")); System.out.println(newLine); } }
3.4 Класи "Країна" та "Перепис населення"
Припустимо, ми поставили за мету переробити попередньо створений проєкт, пов'язаний з країною й переписами. Базові класи, які реалізують основну функціональність класів для представлення країни й переписів, були представлені в прикладах лабораторних робіт № 3 і № 4 курсу "Основи програмування Java". До класів, створених в прикладах лабораторних робіт № 2 і № 3, необхідно додати похідні класи, в яких перевизначити реалізацію всіх методів, пов'язаних з обробкою послідовностей через використання засобів Stream API. Окрім відтворення реалізації функціональності, новий проєкт повинен включати:
- виведення даних в текстовий файл засобами Stream API з подальшим читанням;
- серіалізацію об'єктів у XML-файл і JSON-файл і відповідну десеріалізацію із застосуванням бібліотеки XStream.
- запис подій, пов'язаних з виконанням програми, в системний журнал;
- тестування окремих класів з використанням JUnit.
С урахуванням додавання залежностей від зовнішніх бібліотек доцільно створити новий Maven-проєкт, в який перенести раніше створені класи. Скопіювати файли з одного проєкту в інший можна через буфер обміну: в підвікні Projects вибираємо необхідні файли та копіюємо їх у буфер обміну (функція Copy контекстного меню); в іншому проєкті вибираємо необхідний пакет і вставляємо файли за допомогою функції Paste. Також можна скопіювати пакет цілком.
Клас FileUtils
відповідатиме за зберігання в текстовому файлі, читання з текстового файлу, серіалізацію
і десеріалізацію даних (з XML і JSON), а також за запис у журнал подій у програмі. Запис у журнал здійснюється кожного
разу, коли ми читаємо або записуємо дані у файли різних форматів. Для роботи з засобами XStream і log4j до файлу pom.xml
необхідно
додати такі залежності:
<dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> <version>1.4.20</version> </dependency> <dependency> <groupId>org.codehaus.jettison</groupId> <artifactId>jettison</artifactId> <version>1.5.2</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.20.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.20.0</version> </dependency>
Необхідно також налаштувати засоби log4j, додавши файл log4j2.xml
до теки java\src\resources
.
В нашому випадку цей файл буде таким:
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="INFO"> <Appenders> <Console name="ConsoleAppender" target="SYSTEM_OUT"/> <File name="FileAppender" fileName="country-${date:yyyyMMdd}.log" immediateFlush="false" append="true"/> </Appenders> <Loggers> <Root level="debug"> <AppenderRef ref="FileAppender"/> </Root> </Loggers> </Configuration>
Окремі дії, пов'язані з читанням і записом, можна реалізувати як статичні методи. Код класу FileUtils
буде
таким:
package ua.inf.iwanoff.java.advanced.third; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.json.JettisonMappedXmlDriver; import com.thoughtworks.xstream.security.AnyTypePermission; import org.apache.logging.log4j.Logger; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import ua.inf.iwanoff.java.advanced.first.CensusWithStreams; import ua.inf.iwanoff.java.advanced.first.CountryWithStreams; /** * Клас реалізує запис і читання даних форматів TXT, XML і JSON. * Читаються й записуються дані про країни та переписи населення. * Одночасно події фіксуються в системному журналі. */ public class FileUtils { private static Logger logger = null; public static Logger getLogger() { return logger; } public static void setLogger(Logger logger) { FileUtils.logger = logger; } /** * Здійснює запис даних про країну та переписи в указаний файл * * @param country країна * @param fileName ім'я файлу */ public static void writeToTxt(CountryWithStreams country, String fileName) { if (logger != null) { logger.info("Write to text file"); } try { Files.write(Path.of(fileName), country.toListOfStrings()); } catch (IOException e) { if (logger != null) { logger.error(e.toString()); } throw new RuntimeException(e); } } /** * Здійснює читання даних про країну та переписи зі вказаного файлу * * @param fileName ім'я файлу * @return об'єкт, який було створено */ public static CountryWithStreams readFromTxt(String fileName) { CountryWithStreams country = new CountryWithStreams(); if (logger != null) { logger.info("Read from text file"); } try { List<String> list = Files.readAllLines(Path.of(fileName)); country.fromListOfStrings(list); } catch (IOException e) { if (logger != null) { logger.error(e.toString()); } throw new RuntimeException(e); } return country; } /** * Здійснює серіалізацію даних про країну та переписи в указаний XML-файл * * @param country країна * @param fileName ім'я файлу */ public static void serializeToXML(CountryWithStreams country, String fileName) { if (logger != null) { logger.info("Serializing to XML"); } XStream xStream = new XStream(); xStream.alias("country", CountryWithStreams.class); xStream.alias("census", CensusWithStreams.class); String xml = xStream.toXML(country); try (FileWriter fw = new FileWriter(fileName); PrintWriter out = new PrintWriter(fw)) { out.println(xml); } catch (IOException e) { if (logger != null) { logger.error(e.toString()); } throw new RuntimeException(e); } } /** * Здійснює десеріалізацію даних про країну та переписи зі вказаного XML-файлу * * @param fileName ім'я файлу * @return об'єкт, який було створено */ public static CountryWithStreams deserializeFromXML(String fileName) { if (logger != null) { logger.info("Deserializing from XML"); } try { XStream xStream = new XStream(); xStream.addPermission(AnyTypePermission.ANY); xStream.alias("country", CountryWithStreams.class); xStream.alias("census", CensusWithStreams.class); CountryWithStreams country = (CountryWithStreams) xStream.fromXML(new File(fileName)); return country; } catch (Exception e) { if (logger != null) { logger.error(e.toString()); } throw new RuntimeException(e); } } /** * Здійснює серіалізацію даних про країну та переписи в указаний JSON-файл * * @param country країна * @param fileName ім'я файлу */ public static void serializeToJSON(CountryWithStreams country, String fileName) { if (logger != null) { logger.info("Serializing to JSON"); } XStream xStream = new XStream(new JettisonMappedXmlDriver()); xStream.alias("country", CountryWithStreams.class); xStream.alias("census", CensusWithStreams.class); String xml = xStream.toXML(country); try (FileWriter fw = new FileWriter(fileName); PrintWriter out = new PrintWriter(fw)) { out.println(xml); } catch (IOException e) { if (logger != null) { logger.error(e.toString()); } throw new RuntimeException(e); } } /** * Здійснює десеріалізацію даних про країну та переписи зі вказаного JSON-файлу * * @param fileName ім'я файлу * @return об'єкт, який було створено */ public static CountryWithStreams deserializeFromJSON(String fileName) { if (logger != null) { logger.info("Deserializing from JSON"); } try { XStream xStream = new XStream(new JettisonMappedXmlDriver()); xStream.addPermission(AnyTypePermission.ANY); xStream.alias("country", CountryWithStreams.class); xStream.alias("census", CensusWithStreams.class); CountryWithStreams country = (CountryWithStreams) xStream.fromXML(new File(fileName)); return country; } catch (Exception e) { if (logger != null) { logger.error(e.toString()); } throw new RuntimeException(e); } } }
Код класу Program
, в якому здійснюється демонстрація всіх створених функцій, буде
таким:
package ua.inf.iwanoff.java.advanced.third; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import ua.inf.iwanoff.java.advanced.first.CountryWithStreams ; import static ua.inf.iwanoff.java.advanced.third.FileUtils .*; /** * Клас демонструє запис і читання даних форматів TXT, XML і JSON. * Одночасно події фіксуються в системному журналі. */ public class Program { /** * Демонстрація роботи програми. * Послідовно записуються й читаються дані форматів TXT, XML і JSON. * Одночасно події фіксуються в системному журналі * * @param args аргументи командного рядка (не використовуються) */ public static void main(String[] args) { Logger logger = LogManager.getLogger(Program.class); FileUtils.setLogger(logger); logger.info("Program started"); CountryWithStreams country = CountryWithStreams.createCountryWithStreams(); writeToTxt(country, "Country.txt"); country = readFromTxt("Country.txt"); System.out.println(country); serializeToXML(country, "Country.xml"); country = deserializeFromXML("Country.xml"); System.out.println(country); serializeToJSON(country, "Country.json"); country = deserializeFromJSON("Country.json"); System.out.println(country); logger.info("Program finished"); } }
Результатом роботи буде виведення на консоль даних про країну, прочитаних з різних джерел. У кореневій
теці проєкту з'являться нові файли. Текстовий файл (Country.txt
):
Україна 603628.0 1959 41869000 Перший післявоєнний перепис 1970 47126500 Нас побільшало 1979 49754600 Просто перепис 1989 51706700 Останній радянський перепис 2001 48475100 Перший перепис у незалежній Україні
Файл XML (Country.xml
):
<country> <name>Україна</name> <area>603628.0</area> <list> <census> <year>1959</year> <population>41869000</population> <comments>Перший післявоєнний перепис</comments> </census> <census> <year>1970</year> <population>47126500</population> <comments>Нас побільшало</comments> </census> <census> <year>1979</year> <population>49754600</population> <comments>Просто перепис</comments> </census> <census> <year>1989</year> <population>51706700</population> <comments>Останній радянський перепис</comments> </census> <census> <year>2001</year> <population>48475100</population> <comments>Перший перепис у незалежній Україні</comments> </census> </list> </country>
На жаль файл JSON (Country.json
), який згенерує програма, буде погано відформатований (весь вміст
файлу в одному рядку). Але якщо цей файл відкрити в середовищі IntelliJ IDEA і застосувати форматування коду (Code
| Reformat Code), його вміст у вікні редактора буде таким:
{ "country": { "name": "Україна", "area": 603628, "list": [ { "census": [ { "year": 1959, "population": 41869000, "comments": "Перший післявоєнний перепис" }, { "year": 1970, "population": 47126500, "comments": "Нас побільшало" }, { "year": 1979, "population": 49754600, "comments": "Просто перепис" }, { "year": 1989, "population": 51706700, "comments": "Останній радянський перепис" }, { "year": 2001, "population": 48475100, "comments": "Перший перепис у незалежній Україні" } ] } ] } }
Крім того, буде створено файл журналу з розширенням .log
, до якого після кожного запуску програми дописуватиметься
такий фрагмент тексту:
Program started Write to text file Read from text file Serializing to XML Deserializing from XML Serializing to JSON Deserializing from JSON Program finished
Крім того, до журналу записуватиметься інформація про винятки, які виникали під час роботи з файлами.
4 Вправи для контролю
- Прочитати з текстового файлу дійсні значення (до кінця файлу), знайти їх суму та вивести в інший текстовий файл. Застосувати засоби Stream API.
- Прочитати з текстового файлу цілі значення, замінити від'ємні значення модулями, додатні - нулями та вивести отримані значення в інший текстовий файл. Застосувати засоби Stream API.
- Прочитати з текстового файлу цілі значення, розділити парні елементи на 2, непарні – збільшити у 2 рази та вивести отримані значення в інший текстовий файл. Застосувати засоби Stream API.
- Описати класи "Бібліотека" (з полем – масивом книг) та "Книга". Створити об'єкти, здійснити їх серіалізацію й десеріалізацію в XML і JSON засобами XStream.
- Описати класи Факультет та Інститут (з полем – масивом факультетів). Створити об'єкти, здійснити їх серіалізацію
й десеріалізацію в XML засобами пакету
java.beans
. - Описати класи Факультет та Інститут (з полем – масивом факультетів). Створити об'єкти, здійснити їх серіалізацію й десеріалізацію в XML і JSON засобами XStream.
5 Контрольні запитання
- Для яких цілей використовуються XML-документи?
- Які обмеження накладаються на структуру XML-документа, синтаксис і розташування тегів?
- Чим відрізняються технології SAX і DOM?
- Яким чином здійснюється читання і запис XML-документів?
- Що таке XSLT?
- Чим відрізняється валідний (valid) та правильно оформлений (well-formed) XML-документ?
- Чим відрізняються шаблони документа і схеми документа?
- Для чого в XML-документах необхідні простори імен?
- Які класи відповідають специфікації Java Beans?
- Які є недоліки й переваги XML-серіалізації?
- Які завдання вирішують системи автоматизації складання?
- Яка основна відмінність між Apache Maven та Apache Ant?
- Що таке "трійка" GAV?
- Яка структура файлу
pom.xml
? - Які є типові завдання роботи з файловою системою?
- Які стандартні засоби Java надають можливість роботи з файловою системою? В чому полягають відмінності між цими засобами?
- Які є способи отримання інформації про файли та підкаталоги?
- Як здійснюється читання й запис даних засобами Stream API?
- Що таке формат JSON, які його переваги?
- Які є основні елементи JSON-файлу?
- Які засоби існують для підтримки роботи з JSON-файлами?
- Які здійснюється XML-серіалізація засобами XStream?
- Які здійснюється JSON-серіалізація засобами XStream?
- Що таке логер і рівень логування?
- Які засоби існують для підтримки системного журналу?
- Які переваги бібліотеки Log4j у порівнянні зі стандартними засобами логування?
- Що таке рівні (пріоритети) виведення?