Laboratory Training 3
Working with Files
1 Training Tasks
1.1 Individual Task
Create and implement classes that represent entities given in an individual assignment of the previous Laboratory training. The decision should be based on the hierarchy of classes.
You need to create two derived classes from the class that represents the main entity. The first derived class should be supplemented by the ability to read data from the appropriately prepared text file and write this data to another file after sorting. The second derived class should implement support reading data from a pre-generated XML document, storing data in structures that are automatically created using data binding technology, and writing data to another XML document after sorting. To avoid duplication of data in the program, you must also redefine the class representing the second entity. Derived classes should implement a common interface, which declare the functions of reading from a file and writing to a file.
An additional console output should be provided.
1.2 Sorting Integers
Implement a program that reads positive integer values from the text file (the numbers are separated by spaces, it should be read to the end of the file), places these numbers into an array, sorts them in descending order and increasing the sum of digits and stores both results in two new text files. The above actions should be implemented in separate static functions. To define the sort order, create two classes that implement the Comparator
interface.
1.3 Implementation of Serialization and Deserialization
Create classes Student
and AcademicGroup
(with an array of students as a field). Create objects, implement their binary serialization and deserialization as well as XML serialization and deserialization.
1.4 Working with ZIP Archive (Additional Task)
Create classes Student
and Academic Group
(with an array of students as a field). Create objects, store data about the students of the academic group in the archive. Read data from the archive in another program.
1.5 Use of SAX and DOM Technologies (Additional Task)
Prepare an XML document with data about the students of the academic group. Using SAX technology read data from an XML document and display data on the console. Using DOM technology, read data from the same XML document, modify the data, and write it to a new document.
2 Instructions
2.1 Exception Handling
2.1.1 Basic Concepts
The exception handling mechanism is a very important part of Java programming practice. Almost every Java program contains some parts of this mechanism. Exception objects allow programmer to separate points where runtime errors occur from points of error handling. It allows programmers to create more reliable multi-purpose classes and libraries.
An exception is an event that occurs during the execution of a program that disrupts the normal flow of instructions. The mechanism of generation and handling exceptions allows transferring information about the error from place of origin to the place where this error can be handled. Exceptions in Java are divided into synchronous (runtime error, exception generated by throw
operation) and asynchronous (system failures of the Java virtual machine). The place of occurrence of the second group of exceptions is difficult to detect.
Exception mechanism is present in all modern languages of object-oriented programming. In comparison to C++, Java implements more strict exception handling mechanism.
2.1.2 Exception Throwing Syntax
To generate an exception, operator throw
is used. The throw
statement must be followed by an object that is of type Throwable
or one of its subclasses. For program-defined exceptions, you typically want an exception object to be an instance of a subclass of the Exception
class (derived from Throwable
). In most cases, it makes sense to define a new subclass of Exception
that is specific to your program. These derived classes usually reflect the specifics of a particular application.
class SpecificException extends Exception { }
There is also a base class for generation of system errors: the Error
class. Exception
and Error
classes have a common base class - Throwable
.
In most cases, exception object is created on the spot using new
operator. For example, a typical throw
statement might look like this:
void f() . . . . . . if (/* some error */) { throw new SpecificException(); }
You must specify all exception types that method could throw using throws
keyword:
void f() throws SpecificException, AnotherException { . . . if (/* some error */) { throw new SpecificException(); } if (/* another error */) { throw new AnotherException(); } . . . }
In the following example, function reciprocal()
raises exception in case of division by zero.
class DivisionByZero extends Exception { } class Test { double reciprocal(double x) throws DivisionByZero { if (x == 0) { throw new DivisionByZero(); } return 1 / x; } }
Unlike C++, Java does not allow use of primitive types for exceptions. Only objects of types derived from Throwable
or Exception
are allowed.
The exception list must be preserved for overridden methods by inheritance.
2.1.3 Exception Handling Syntax
An exception that has been thrown in a particular part of a code should be caught in another part. If we want to call a function that can potentially throw an exception, the call to this function is placed in the try { }
block:
double x, y; . . . try { y = reciprocal(x); }
A try
block is followed by a sequence of one or more catch
statements, or handlers, each of which handles a different type of exception. For example:
catch (DivisionByZero d) { // exception handling } catch (Exception ex) { // exception handling }
Exceptions form an object hierarchy. The base type handler also accepts exceptions to all types derived from it. What you have to do here is put catch
blocks for the more specific exceptions before those for the more general exceptions. Suppose there is such an exception class hierarchy:
class BaseException extends Exception { } class FileException extends BaseException { } class FileNotFoundException extends FileException { } class WrongFormatException extends FileException { } class MathException extends BaseException { } class DivisionByZero extends MathException { } class WrongArgument extends MathException { }
Suppose there is some function that can throw all types of these exceptions:
public class Exceptions { public static void badFunc() throws BaseException { // there may be various exceptions } }
Depending on the program logic, different types of exceptions can be processed in more detail:
try { Exceptions.badFunc(); } catch (FileNotFoundException ex) { // file not found } catch (WrongFormatException ex) { // wrong format } catch (FileException ex) { // other file-related errors } catch (MathException ex) { // all mathematical errors are handled together } catch (BaseException ex) { // handle all other exceptions of badFunc() function } catch (Exception ex) { // just in case }
Following the last catch
block you can also include a finally
block. This code is guaranteed to run whether or not an exception is thrown, even if some block contains an exit from the function.
try { openFile(); // other actions } catch (FileError f) { // exception handling } catch (Exception ex) { // exception handling } finally { closeFile(); }
In the Java 7 version, new constructs have been added to the exception syntax, which makes working with exceptions more convenient. For example, you can create an event handler of various types using the bitwise OR operation:
public void newMultiCatch() { try { methodThatThrowsThreeExceptions(); } catch (ExceptionOne | ExceptionTwo | ExceptionThree e) { // exceptions handling } }
Other additional features are associated with the so-called "try-with-resources" syntax construct. You can place creation of objects those implement java.lang.AutoCloseable
interface directly after try
. This guarantee invocation of close()
method by leaving of try {} catch {}
block (similar to running the code in the finally
block):
try (ClassThatImplementsAutoCloseable sc = new ClassThatImplementsAutoCloseable()) { // actions that may lead to an exception } catch (Exception f) { // exception handling } // automatic call of sc.close()
In contrast to C++, you cannot use catch
(...)
for catch of any exception. Instead of this, use objects of base classes of all exceptions:
catch (Exception ex) { // exception handling }
or
catch (Throwable ex) { // exception handling }
The typical implementation of exception handler is call of printStackTrace()
method.
catch (Throwable ex) { ex.printStackTrace(); }
This method prints a stack trace for this Throwable
object on the error output stream (System.err
). The following example may be regarded as typical:
java.lang.NullPointerException at SomeClass.g(SomeClass.java:9) at SomeClass.f(SomeClass.java:6) at SomeClass.main(SomeClass.java:3)
If some exception cannot be completely handled within the catch
() { }
block, it can be passed on to other handlers:
catch (SomeException ex) { // local exception handling throw ex; }
Sometimes some extra information is needed to adequately handle the exception information. For example, we create a function inside which a square root should be found. If the argument is negative, you need to generate an exception. It is useful to know which negative value was obtained for debugging the program. You can create an exception class whose object will store this value. It is set in the constructor and at the exception handling point it can be obtained using the getter. This approach can be demonstrated in the following example. Assume, such exception class is created:
public class WrongArgumentException extends Exception { private double arg; public WrongArgumentException(double arg) { this.arg = arg; } public double getArg() { return arg; } }
An exception can be thrown in some function if the argument cannot be used:
public class SomeLib { public static void doSomeUseful(double x) throws WrongArgumentException { // check x if (x < 0) throw new WrongArgumentException(x); double y = Math.sqrt(x); // further work } }
Now a caught exception object can be applied to get more detailed information.
public class ExceptionTest { public static void main(String[] args) { double x = new java.util.Scanner(System.in).nextDouble(); try { // . . . SomeLib.doSomeUseful(x); // . . . } catch (WrongArgumentException e) { System.err.println(e.getClass().getName() + e.getArg()); } } }
As you can see from the following example, invocation of the getClass().getName()
method allows getting the class name. Such invocation can be applied to any object (not only to exception).
The call of functions those might throw an exception outside of try
block produces a compiler error. The exception checking should always be performed:
double f(double x) { double y; try { y = reciprocal(x); } catch (DivisionByZero ex) { ex.printStackTrace(); y = 0; } return y; }
You can add throws
declaration to calling function. Now this uncaught exception is transmitted to outer handlers:
double g(double x) throws DivisionByZero { double y; y = reciprocal(x); return y; }
This rule is obligatory for all Java exceptions, apart from objects of RuntimeException
class and its descendants. If a function produces such exception, you can either place its call into try block or ignore an exception. Functions that throw such exception do not declare them in their header. A typical exception from RuntimeException
class hierarchy is NullPointerException
.
The IntelliJ IDEA environment allows you to automate the process of creating blocks of catching and processing exceptions. If you select the block in the text editor and then apply the Code | Surround With... | try / catch function, the selected block will be surrounded with try
block, and there will be added catch
blocks that will contain the standard processing of all possible exceptions.
Note. The Eclipse also allows you to automate the process of creating blocks of catching and processing exceptions. This can be done using the Source | Surround With | Try/catch Block function of the main menu.
2.2 Input and Output Streams. Character Streams
2.2.1 Overview
Like most modern languages and platforms, Java generalizes the concept of streams, extending common approaches to file, console, network, and other I/O processes.
Classes that perform file input and output, as well as other actions with streams, are located in the java.io
package. The classes defined in this package offer a number of methods for creating streams, reading, writing, etc. Objects of FileReader
and FileWriter
classes directly work with text files. There are two groups of classes: for working with text and binary files respectively.
All stream-aware functions require exception handle. All work with streams other than standard System.in
and System.out
streams, requires handling of I/O-related exceptions. These are objects of IOException
class or objects of derived classes (FileNotFoundException
, ObjectStreamException
, etc.).
It is very important to close all files that were open. When closing files, the data remaining in the buffer is written to the file, the release of the buffer and other resources associated with the file are made. You can close the file using the close()
method. For example, for stream called in
:
in.close();
If an application that requires file input is executed in an Eclipse environment (or in IntelliJ IDEA), files with source data should be placed in the project directory (not in the package folder). In the project directory, you can find the resulting files that appear after finishing execution of a program that involved the file output.
The program can simultaneously open multiple streams for input and output.
2.2.2 Working with Character Streams
Streams that work with textual information are called character streams. Names of corresponding stream classes end with the words "...Reader"
and "...Writer"
respectively.
The BufferedReader
class implements so called buffering input. This class reads text from a character-input stream, buffering characters so as to provide for the efficient reading. The BufferedWriter
class writes text to a character-output stream, buffering characters to provide for the efficient writing. The print()
and println()
methods of PrintWriter
class realize formatted output.
An important concept concerned with file streams is buffering. Buffering involves the creation of a special memory area (buffer), into which data is retrieved from a file for later element-wise reading or for element-wise recording data before copying to the disk. BufferedReader
class objects perform such buffered reading.
BufferedWriter
objects are used for buffered output. Direct formatted output is performed by print()
and println()
methods of the PrintWriter
class.
The following program reads one integer and one real value from data.txt
file and writes their sum into results.txt
file.
package ua.inf.iwanoff.oop.third; import java.io.*; import java.util.StringTokenizer; public class FileTest { void readWrite() { try { FileReader fr = new FileReader("data.txt"); BufferedReader br = new BufferedReader(fr); String s = br.readLine(); int x; double y; try { StringTokenizer st = new StringTokenizer(s); x = Integer.parseInt(st.nextToken()); y = Double.parseDouble(st.nextToken()); } finally { br.close(); } double z = x + y; FileWriter fw = new FileWriter("results.txt"); PrintWriter pw = new PrintWriter(fw); pw.println(z); pw.close(); } catch (IOException ex) { ex.printStackTrace(); } } public static void main(String[] args) { new FileTest().readWrite(); } }
To open a file, an object of the FileReader
class is created, the constructor gets file name as an argument. The reference to the created object is passed to the BufferedReader
class constructor. Reading string data from a file can be done using the readLine()
method, which returns a reference to the character string, or null
, if the end of the file is reached.
The variable s
refers to a string containing two numbers. To receive separate tokens from this string the object of the class StringTokenizer
is used, whose constructor gets this string. References to individual parts of a string can be successively obtained using the nextToken()
method. These references can be used directly, or used to convert data into numeric values (static parseDouble()
and parseInt()
classes of Double
and Integer
, respectively).
You can use the Scanner
class to read from a file. The actual parameter of the constructor may be the file stream. The previous example can be implemented using the Scanner
class. You can also shorten the code by excluding unnecessary variables. In addition, it is advisable to use the try () { }
Java 7 construct for automatic stream closing:
package ua.inf.iwanoff.oop.third; import java.io.*; import java.util.Scanner; public class FileTest { void readWrite() { try (Scanner scanner = new Scanner(new FileReader("data.txt"))) { try (PrintWriter pw = new PrintWriter("results.txt")) { pw.println(scanner.nextInt() + scanner.nextDouble()); } } catch (IOException ex) { ex.printStackTrace(); } } public static void main(String[] args) { new FileTest().readWrite(); } }
The advantage of this approach is the arbitrary location of the source data (not necessarily in a single line). As can be seen from the example above, several try ()
blocks can use one catch ()
block. An alternative is to place several statements inside the brackets:
try (Scanner scanner = new Scanner(new FileReader("data.txt")); PrintWriter pw = new PrintWriter("results.txt")) { pw.println(scanner.nextInt() + scanner.nextDouble()); } catch (IOException ex) { ex.printStackTrace(); }
While working with the Scanner
class, you can define additional parameters, for example, set a separator character (or a sequence of characters). For example, you can add the following line before reading the data:
scanner.useDelimiter(",");
Now the scanner object will accept commas as delimiters (instead of spaces).
2.3 Working with Binary Streams (Byte Streams)
To work with non-textual (binary) files, Java offers streams whose names end with "...Stream"
instead of "...Reader"
and "...Writer"
, for example, InputStream
, FileInputStream
, OutputStream
, FileOutputStream
, etc. Such streams are called byte streams. The following example demonstrates copying a binary FileCopy.class
to a project folder with a new name:
package ua.inf.iwanoff.oop.third; import java.io.*; public class FileCopy { public static void copy(String inFile, String outFile) { byte[] buffer = new byte[1024]; // Byte buffer try (InputStream input = new FileInputStream(inFile); OutputStream output = new FileOutputStream(outFile)) { int bytesRead; while ((bytesRead = input.read(buffer)) >= 0) { output.write(buffer, 0, bytesRead); } } catch (IOException ex) { ex.printStackTrace(); } } public static void main(String[] args) { copy("bin/ua/inf/iwanoff/oop/third/FileCopy.class", "FileCopy.copy"); } }
As seen from the above example, Java allows normal slash (/
) instead of backslash character. This is more universal approach suitable for different operating systems. In addition, the backslash character should be written twice (\\
).
To work with binary files there are additional possibilities: the use of data streams and object streams. The so-called data streams support binary input / output of values of simple data types (boolean
, char
, byte
, short
, int
, long
, float
and double
), as well as values of type String
. All data streams implement the DataInput
or DataOutput
interfaces. For most tasks, standard implementations of these interfaces are sufficient: DataInputStream
and DataOutputStream
. The data in the file is stored in the form in which they are presented in memory. To write rows use the writeUTF()
method. The following example demonstrates writing data to binary file:
package ua.inf.iwanoff.oop.third; import java.io.*; public class DataStreamDemo { public static void main(String[] args) { double x = 4.5; String s = "all"; int[] a = { 1, 2, 3 }; try (DataOutputStream out = new DataOutputStream( new FileOutputStream("data.dat"))) { out.writeDouble(x); out.writeUTF(s); for (int k : a) out.writeInt(k); } catch (IOException e) { e.printStackTrace(); } } }
Now data can be read in another program:
package ua.inf.iwanoff.oop.third; import java.io.*; import java.util.*; public class DataReadDemo { public static void main(String[] args) { try (DataInputStream in = new DataInputStream( new FileInputStream("data.dat"))) { double x = in.readDouble(); String s = in.readUTF(); List<Integer> list = new ArrayList<>(); try { while (true) { int k = in.readInt(); list.add(k); } } catch (Exception e) { } System.out.println(x); System.out.println(s); System.out.println(list); } catch (Exception e) { e.printStackTrace(); } } }
Note. In the above program, the termination of the cycle is carried out through catch of an exception. This approach is not recommended because the exception mechanism reduces the performance of the program. In our case, it would be advisable to separately store the length of the array before its items, and then use this length to organize the for
loop while reading.
You can also use the java.io.RandomAccessFile
class to read and write data. The object of this class allows you to shift freely within the file in the forward and reverse direction. The main advantage of the RandomAccessFile
class is the ability to read and write data being at an arbitrary location within the file.
In order to create an object of the class RandomAccessFile
, it is necessary to call its constructor with two parameters: the name of the file for input / output and the mode of access to the file. You can use special strings such as "r"
(for reading), "rw"
(for reading and writing), etc. to define the mode. For example, opening a data file can be as follows:
RandomAccessFile file1 = new RandomAccessFile("file1.dat", "r"); // for reading RandomAccessFile file2 = new RandomAccessFile("file2.dat", "rw"); // for reading and writing
Once the file is open, you can use methods like readDouble()
, readInt()
, readUTF()
, etc. to read or writeDouble()
, writeInt()
, writeUTF(),
etc. for output.
File management is based on a pointer to the current position where data is read or written. At the time the RandomAccessFile
object is created, the pointer is set to the beginning of the file and is set to 0. The calls to the read...()
and write...()
methods shift the position of the current pointer to the number of bytes read or written. To arbitrarily shift the pointer to a certain number of bytes, you can use the skipBytes()
method, or set the pointer to a certain file location by calling the seek()
method. To get the current position in which the pointer is located, you need to call the getFilePointer()
method. For example, the first program writes data to a new file:
RandomAccessFile fileOut = new RandomAccessFile("new.dat", "rw"); int a = 1, b = 2; fileOut.writeInt(a); fileOut.writeInt(b); fileOut.close();
Another program reads a second integer:
RandomAccessFile fileIn = new RandomAccessFile("new.dat", "rw"); fileIn.skipBytes(4); // move the file pointer to the second number int c = fileIn.readInt(); System.out.println(c); fileIn.close();
To find the length of a file in bytes, you can use the length()
function.
2.4 Binary Serialization of Objects
The serialization mechanism involves recording objects into the stream of bits for storage in a file or for the file transfer over computer networks. Deserialization involves reading the stream of bits, creating saved objects and recreating their state at the time of preservation. In order for the objects of a certain class to be serialized, the class must implement the java.io.Serializable
interface. This interface does not define any method, its presence only indicates that objects of this class can be serialized. However, guaranteed serialization and deserialization require the presence of a special static field called serialVersionUID
, which ensures the uniqueness of the class. The Eclipse environment allows you to generate the required value automatically (Quick Fix | Adds a generated serial version ID from the context menu).
The ObjectOutputStream
and ObjectInputStream
classes allow serialization and deserialization. They implement the ObjectOutput
and ObjectInput
interfaces respectively. The mechanisms of serialization and deserialization will be considered in the following example. Suppose the Point
class is described:
package ua.inf.iwanoff.oop.third; import java.io.Serializable; public class Point implements Serializable { private static final long serialVersionUID = -3861862668546826739L; 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; } }
Also created Line
class:
package ua.inf.iwanoff.oop.third; import java.io.Serializable; public class Line implements Serializable { private static final long serialVersionUID = -4909779210010719389L; 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; } }
In the following program (in the same package), objects are created with subsequent serialization:
package ua.inf.iwanoff.oop.third; import java.io.*; public class SerializationTest { 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 (ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream("temp.dat"))) { out.writeObject(line); } catch (IOException e) { e.printStackTrace(); } } }
In another program, we can deserialize:
package ua.inf.iwanoff.oop.third; import java.io.*; public class DeserializationTest { public static void main(String[] args) throws ClassNotFoundException { try (ObjectInputStream in = new ObjectInputStream( new FileInputStream("temp.dat"))) { Line line = (Line) in.readObject(); System.out.println(line.getFirst().getX() + " " + line.getFirst().getY() + " " + line.getSecond().getX() + " " + line.getSecond().getY()); } catch (IOException e) { e.printStackTrace(); } ; } }
You can also serialize objects that contain arrays of other objects. For example:
package ua.inf.iwanoff.oop.third; import java.io.*; class Pair implements Serializable { private static final long serialVersionUID = 6802552080830378203L; double x, y; public Pair(double x, double y) { this.x = x; this.y = y; } } class ArrayOfPairs implements Serializable { private static final long serialVersionUID = 5308689750632711432L; Pair[] pairs; public ArrayOfPairs(Pair[] pairs) { this.pairs = pairs; } } public class ArraySerialization { public static void main(String[] args) { Pair[] points = { new Pair(1, 2), new Pair(3, 4), new Pair(5, 6) }; ArrayOfPairs arrayOfPoints = new ArrayOfPairs(points); try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("temp.dat"))) { out.writeObject(arrayOfPoints); } catch (IOException e) { e.printStackTrace(); } } }
Now we can accomplish deserialization:
package ua.inf.iwanoff.oop.third; import java.io.*; public class ArrayDeserialization { public static void main(String[] args) throws ClassNotFoundException { try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("temp.dat"))) { ArrayOfPairs arrayOfPairs = (ArrayOfPairs) in.readObject(); for (Pair p : arrayOfPairs.pairs) { System.out.println(p.x + " " + p.y); } } catch (Exception e) { e.printStackTrace(); } } }
Some class fields whose values do not affect the state of an object can be described with the transient
modifier. For example:
class SomeClass implements Serializable { transient int someUnnecessaryField; }
Values in such fields will not be saved in the stream during serialization and will not be restored during deserialization.
Serialization and deserialization can be used instead of file input and output. The main disadvantage of binary serialization is the need to work with binary (non-textual) files.
2.5 Working with Archives
2.5.1 Standard Means of Working with Archives
The java.util.zip
package provides the ability to work with standard ZIP and GZIP file formats.
To write to the archive, use the class ZipOutputStream
. Using the setMethod()
function of this class, you can define the archiving method: ZipOutputStream.DEFLATED
(with compression) or ZipOutputStream.STORED
(without compression). The setLevel()
method defines the compression level (from 0 to 9, by default Deflater.DEFAULT_COMPRESSION
, as a rule, the maximum compression). The setComment()
method allows you to add a comment to the archive.
For each data block to be placed in a zip file, the ZipEntry
object is created. The necessary file name is passed to the ZipEntry
constructor. It is possible to set separate parameters similarly. Next, using the putNextEntry()
method of the ZipOutputStream
class, the corresponding entry point to the archive is "opened". By using file streaming tools, the data is archived, then the ZipEntry
object should be closed by calling closeEntry()
.
The following example demonstrates the creation of the Source.zip
archive, to which the contents of the ZipCreator.java
source file is written:
package ua.inf.iwanoff.oop.third; import java.io.*; import java.util.zip.*; public class ZipCreator { public static void main(String[] args) { try (ZipOutputStream zOut = new ZipOutputStream(new FileOutputStream("Source.zip"))) { ZipEntry zipEntry = new ZipEntry("src/ua/inf/iwanoff/oop/third/ZipCreator.java"); zOut.putNextEntry(zipEntry); try (FileInputStream in = new FileInputStream("src/ua/inf/iwanoff/oop/third/ZipCreator.java")) { byte[] bytes = new byte[1024]; int length; while ((length = in.read(bytes)) >= 0) { zOut.write(bytes, 0, length); } } zOut.closeEntry(); } catch (IOException e) { e.printStackTrace(); } } }
The newly created archive contains a relative path to the file. If this is not required, only the name of the path should be specified when creating a ZipEntry
object:
ZipEntry zipEntry = new ZipEntry("ZipCreator.java");
In order to read data from an archive, you should use the ZipInputStream
class. Each such archive always needs to get individual entries. The getNextEntry()
method returns an object of type ZipEntry
. The read()
method of the ZipInputStream
class returns -1 at the end of the current record (and not only at the end of the zip file). Next, the closeEntry()
method is invoked to allow the switch to read the next record. In the following example, ZipCreator.java
is read from the previously created archive and output of its content in the console window:
package ua.inf.iwanoff.oop.third; import java.io.*; import java.util.zip.*; public class ZipExtractor { public static void main(String[] args) { try (ZipInputStream zIn = new ZipInputStream(new FileInputStream("Source.zip"))) { ZipEntry entry; byte[] buffer = new byte[1024]; while ((entry = zIn.getNextEntry()) != null) { int bytesRead; System.out.println("------------" + entry.getName() + "------------"); while ((bytesRead = zIn.read(buffer)) >= 0) { System.out.write(buffer, 0, bytesRead); } zIn.closeEntry(); } } catch (IOException e) { e.printStackTrace(); } } }
Similarly, work with archives of the GZIP format is carried out. The appropriate streams of reading and writing are GZIPInputStream
and GZIPOutputStream
.
2.5.2 Creation and Use of JAR Files
To deploy the Java application on the client's computer, it is enough to copy all the necessary .class
files in the required package folders. Java ideology does not allow you to create executable files (.exe
). However, the files needed to execute the program can be packed into a special type archive, the so-called JAR. This is actually a ZIP archive, but it contains a MANIFEST.MF
file in the META-INF
directory, in which the program's main class (this class must contain main()
method). This class name should be set by the Main-Class
parameter. JAR version number is set by Manifest-Version
parameter.
The program collected in the JAR file can be launched in such a command:
java -jar file_name.jar
GUI applications should be launched in such a command:
javaw -jar file_name.jar
In this case, the console window will not be created.
If the JDK tools have been installed on your computer and correctly identified all paths, you can run the application double-clicking on the JAR file.
In order to create a JAR file in the IntelliJ IDEA environment, you must should adjust certain settings. In particular, in the main menu, select File | Project Structure | Project Settings | Artifacts, then click the "+" button and add Jar | From modules with dependencies.... Next in the dialog window, specify the module (Module, usually fills automatically) and the main class (Main Class, class with the main()
function, which is a starting point of application). The best option for choosing the main class is to use the files icon (Browse...). The next step is the direct creation of the archive. In the main menu, select Build | Build Artifacts..., then Action | Build. The artifacts branch that contains a created JAR file appears in the out subtree of the Project tree.
2.6 Working with XML Documents
2.5.1 Overview
XML (eXtensible Markup Language) is a platform-independent way of structuring information. Because XML separates the content of a document from its structure, it is successfully used for the exchange of information. For example, XML may be used to transfer data between the application and the database or between databases having different formats.
XML documents are always text files. The syntax of XML is similar to the syntax of HTML, which is used for marking up texts published over the Internet. XML language can also be applied directly to the markup text.
Most often, an XML document begins with a so-called prefix. The prefix for the document in general is as follows:
<?xml version="1.0" [other-attributes] ?>
Among the possible attributes, the encoding = "character-set"
attribute is most useful. It specifies the encoding for the text. If you want to use non-UNICODE Cyrillic characters, you can define it, for example, in the following way:
<?xml version="1.0" encoding="Windows-1251"?>
The next line may contain information about document type. The rest of document contains a set of XML elements. Elements are delimited by tags. Start tags begin with <
followed by the element name. End tags begin with </
followed by the element name. Both start and end-tags terminate with >
. Everything in between the two tags is the content of the element. All start tags must be matched by end tags. All attribute values must be quoted. Each document must contain the only root element in which all other elements are inserted.
Unlike HTML, XML allows you to use an unlimited set of tag pairs, each of which represents not what the data it contains should look like, but what it means. XML allows you to create your own set of tags for each class of documents. Thus, it is more accurate to call it not a language, but a meta-language.
Having formally described document structure, you can check its correctness. The presence of markup tags allows both the person and the program to analyze the document. XML documents, in the first place, are intended for software analysis of their contents.
The following XML-document stores prime numbers.
<?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>
The Numbers
and Number
tags are invented by author. Text indents are used for better readability.
Tags may contain attributes - additional information about the elements contained inside the corner brackets. Values of attributes must be taken with quotation marks. The followings example shows Message
tag with to
and from
attributes:
<Message to="you" from="me"> <Text> How to use XML? </Text> </Message>
Use of end-tags is obligatory in XML. Furthermore, you must close inner elements before outer ones. Such code snippet produces an error:
<A> <B> text </A> </B>
And such fragment is correct:
<A> <B> text </B> </A>
Tags can be empty. Such tags end with backslash symbol. For example, you can write <Nothing/>
instead of <Nothing></Nothing>
.
In contrast HTML-tags, XML-tags are case sensitive, therefore <cat>
and <CAT>
are different tags.
XML-documents can contain comments:
<!-- Here are comments -->
XML recognition programs, the so-called XML parsers, perform the analysis of a document before finding the first error, in contrast to the HTML parsers embedded in the browser. Browsers are trying to display a document, even if the code contains errors.
An XML document that conforms to all XML syntax rules is considered to be a well-formed document.
2.6.2 Standard Approaches to Working with XML Documents
There are two standard approaches to working with XML documents in your program:
- event-based document model (Simple API for XML, SAX) supports processing events concerned with particular XML tags by reading XML data;
- Document Object Model, DOM allows creation and processing of collection of objects organized in a hierarchy.
Event-based approach does not allow the developer to change the data in the source document. If the part of the data needs to be corrected, the document must be completely updated. In contrast, the DOM provides API, which allows developers to add or remove nodes in any part of the tree in the application.
Both approaches use concept of parser. Parser is an application program, which parses document and split it into tokens. The parser can initiate events (as in SAX), or build a data tree.
In order to implement standard approaches to working with XML in Java SE, we use Java API for XML Processing (JAXP). JAXP provides tools for validating and parsing XML documents. To implement the object model, the JAXP document includes the DOM software interface, SAX implemented with the appropriate software interface. In addition to them, the Streaming API for XML (StAX) and the XSLT (XML Stylesheet Language Transformations) tools are provided.
2.6.3 Using Simple API for XML and StAX
Simple API for XML (SAX, a simple application programming interface for working with XML) provides a consistent mechanism for analyzing an XML document. The analyzer that implements the SAX interface (SAX Parser) processes information from an XML document as a single data stream. This data stream is only available in one direction, that is, previously processed data cannot be re-read without re-analysis. Most programmers agree that the processing of XML documents using SAX is generally faster than using DOM. This is because SAX stream requires much less memory compared with the construction of a complete DOM tree.
SAX analyzers implement an event-driven approach when the programmer needs to create event handlers that are called by the parsers when processing an XML document.
The Java SE tools for working with SAX are implemented in the packages javax.xml.parsers
and org.xml.sax
, as well as in the packages included in them. To create an object of javax.xml.parsers.SAXParser
class, you should use the class javax.xml.parsers.SAXParserFactory
, representing the corresponding factory methods. The SAX parser does not create an XML document view in memory. Instead, the SAX parser informs clients about the structure of the XML document using the callback mechanism. You can create a class by yourself implementing a number of necessary interfaces, in particular org.xml.sax.ContentHandler
. However, the simplest and most recommended way is to use the org.xml.sax.helpers.DefaultHandler
class, creating a derived class and overriding its methods that should be called when various events in the process of document analysis occurs. Most often overridden methods are:
-
startDocument()
andendDocument()
: methods that are called at the beginning and end of the analysis of an XML document startElement()
andendElement()
: methods that are called at the beginning and end of the document element analysis-
characters()
: method called when retrieving the text content of an XML document element.
The following example illustrates the use of SAX to read a document. Suppose the Hello.xml file in the project directory has the following content:
<?xml version="1.0" encoding="UTF-8" ?> <Greetings> <Hello Text="Hi, this is an attribute!"> Hi, this is the text! </Hello> </Greetings>
Note. When saving the file, you must specify the UTF-8 encoding.
The code of the program that reads data from XML will be:
package ua.inf.iwanoff.oop.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("Attributes: "); 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(); } } } }
Since the characters()
method is called for each tag, it is advisable to output the contents if the string is not empty.
StAX was designed as a cross between DOM and SAX interfaces. This programming interface uses a cursor metaphor that represents the entry point within the document. The application moves the cursor forward by reading the information, receiving information from the parser as needed.
2.6.4 Using the Document Object Model (DOM)
The DOM is a series of Recommendations produced by the World Wide Web Consortium (W3C). DOM began as a way of identifying and manipulating items on an HTML page (DOM Level 0).
The current DOM Recommendation (DOM Level 3) is an API that defines the objects represented in the XML document, as well as the methods and properties that are used to access and manipulate them.
Beginning with Level 1 DOM, the DOM API contains interfaces that represent all kinds of information that can be found in an XML document. It also includes the methods needed to work with these objects. Some of the most common methods of standard DOM interfaces are listed below.
The Node
interface is the primary data type of the DOM. It defines a number of useful methods for obtaining data about nodes and navigating through them:
getFirstChild()
andgetLastChild()
return the first or last child of this node;getNextSibling()
andgetPreviousSibling()
return the next or previous sibling of this node;getChildNodes()
returns a reference to the list ofNodeList
type of children of this node; using theNodeList
interface methods, you can get the i-th node (item(i)
method) and the total number of such nodes (getLength()
method);getParentNode()
returns the parent node;getAttributes()
returns an associative array of typeNamedNodeMap
attributes of this node;hasChildNodes()
returnstrue
if the node has children.
There are a number of methods for modifying an XML document: insertBefore()
, replaceChild()
, removeChild()
, appendChild()
, etc.
In addition to the Node
, DOM also defines several subinterfaces of the Node
interface:
-
Element
represents the XML element in the source document; the element includes a pair of tags (opening and closing) and all the text between them; Attr
represents the attribute of the element;Text
represents the element content;Document
represents the entire XML document; Only oneDocument
object exists for each XML document; having theDocument
object, you can find the root of the DOM tree using thegetDocumentElement()
method; from the root you can manipulate the entire tree.
Additional types of nodes are:
Comment
represents a comment in an XML fileProcessingInstruction
represents the processing instructionCDATASection
represents theCDATA
section.
XML parsers require the creation of an instance of a particular class. The disadvantage is that when changing the parsers, you need to change the source code. For some parsers, you can use so-called factory classes. Using the static newInstance()
method, an instance of the factory object is created, which creates a class object that implements the DocumentBuilder
interface. Such an object is directly a necessary parser: it implements DOM methods that are needed to parse and process the XML file. When creating a parser object, exceptions may be thrown that need to be handled. Then you can create an object of type Document
, load data from a file with a name, for example, fileName
and pares it:
try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(fileName); . . .
After traversing and modifying the tree, you can save it in another file.
Using DOM will considered on the example with the previous file (Hello.xml
). The following program outputs the text of the attribute to the console, modifies it and stores it in a new XML document:
package ua.inf.iwanoff.oop.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; // Create a document builder using the factory method: DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder(); doc = db.parse(new File("Hello.xml")); // Find the root tag: Node rootNode = doc.getDocumentElement(); // View all child tags: for (int i = 0; i < rootNode.getChildNodes().getLength(); i++) { Node currentNode = rootNode.getChildNodes().item(i); if (currentNode.getNodeName().equals("Hello")) { // View all attributes: for (int j = 0; j < currentNode.getAttributes().getLength(); j++) { if (currentNode.getAttributes().item(j).getNodeName().equals("Text")) { // Found the required attribute. Display the text of the attribute (greeting): System.out.println(currentNode.getAttributes().item(j).getNodeValue()); // Changing the contents of the attribute: currentNode.getAttributes().item(j).setNodeValue("Hi, there was DOM here!"); // Further search is inappropriate: break; } } // Change the text: System.out.println(currentNode.getTextContent()); currentNode.setTextContent("\n Hi, here was also DOM!\n"); break; } } // Create a converter object (in this case, to write to a file). // We use the factory method: Transformer transformer = TransformerFactory.newInstance().newTransformer(); // Write to file: transformer.transform(new DOMSource(doc), new StreamResult(new FileOutputStream(new File("HelloDOM.xml")))); } }
After running the program in the project folder, you will be able to find the following file (HelloDOM.xml
):
<?xml version="1.0" encoding="UTF-8" standalone="no"?><Greetings> <Hello Text="Hi, there was DOM here!"> Hi, here was also DOM! </Hello> </Greetings>
In the above example, the javax.xml.transform.Transformer
class is used to save the modified document in the file. In general, this class is used in the implementation of the so-called XSLT-transformation. XSLT (eXtensible Stylesheet Language Transformations) is a language of converting XML documents to other XML documents or other objects such as HTML, plain text, etc. The XSLT processor accepts one or more XML source documents, as well as one or more modules, and processes them to obtain the output document. The transformation contains a set of template rules: instructions and other directives that guide the XSLT processor when generating an output document.
2.6.5 Use of Document Template Definition and XML Schema
Structured data stored in XML document need additional information about rules of elements' order. The most commonly used are two ways of structure representation: Document Template Definition (DTD) and XML Schema (XSD).
DTD (Document Template Definition) is a simple set of rules, which describe structure of XML documents of particular type. DTD is not an XML document itself. DTD is very simple, but it does not describe types of elements. The DTD directives can be present both in the header of the XML document itself (internal DTD) and in a separate file (external DTD). Availability of DTD is optional.
For example, we have the following XML file:
<?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>
The DTD file that describes the structure of this document will look like this:
<?xml version="1.0" encoding="UTF-8"?> <!ELEMENT Pair (x, y)> <!ELEMENT x (#PCDATA)> <!ELEMENT y (#PCDATA)> <!ELEMENT Pairs (Pair+)>
The plus sign in the last line indicates that the Pairs
tag can contain one or many Pair
elements inside. In addition, you can also use * (0 or many), a question mark (0 or 1). The absence of a sign means that only one element can be present.
XML Schema is an alternative to DTD method for description of a document structure. The schema is more convenient than DTD in that the description of the structure of the document is performed on the XML itself. In addition, the XML scheme of its capabilities significantly exceeds the DTD. For example, in a schema you can specify tag types and attributes, define restrictions, and more.
An XML document that is well-formed, refers to the grammatical rules and fully responds to it, is called the valid document.
In order to prevent conflicts of tag names, XML allows you to create so-called namespaces. The namespace defines the prefix associated with a particular schema of the document and is attached to the tags. Custom namespace is determined using the following construct:
<root xmlns:pref="http://www.someaddress.org/">
In this example, root
is the root XML document tag, pref
is the prefix that defines the namespace, "http://www.someaddress.org/
" is some address, such as the domain name of the author of the schema. Applications that handle XML documents never check this address. It is only necessary to ensure the uniqueness of the namespace.
The schema itself uses the namespace xs
.
The use of a document schema can be demonstrated in the following example. Suppose we have such an XML file:
<?xml version="1.0" encoding="Windows-1251" ?> <Student Name="John" Surname="Smith"> <Marks> <Mark Subject="Mathematics" Value="B"/> <Mark Subject="Physics" Value="A"/> <Mark Subject="Programming" Value="C"/> </Marks> <Comments> Strange student </Comments> </Student>
Creating a schema file should start with a standard construct:
<?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> </xs:schema>
The information about document structure must be placed between <xs:schema>
and </xs:schema>
tags. In order to describe the tags of a document, you can add standard tags inside it. For complex tags that are embedded in others or have parameters:
<xs:element name="tag name"> <xs:complexType> . . . </xs:complexType> </xs:element>
Inside the tag you can place a list of items:
<xs:sequence> . . . </xs:sequence>
The reference to another tag:
<xs:element ref="tag_name"/>
The following element contains data:
<xs:element name="tag_name" type="type_name"/>
The following table contains some standard data types used in schema:
Name |
Description |
xs:string |
string value that contains a sequence of Unicode (or ISO/IEC) characters, including spaces, tabs, LF and CR symbols. |
xs:integer |
integer value |
xs:boolean |
binary logical values: true or false,1 or 0. |
xs:float |
32-bit floating point value |
xs:double |
64-bit floating point value |
xs:anyURI |
Uniform Resource Identifier |
The following tag
<xs:attribute name="attribute_name" type="type_name"/>
provides a way for an attribute description.
There is also a large number of additional tag parameters. The maxOccurs
parameter specifies the maximum number of occurrences for the element, minOccurs
specifies the minimum number of occurrences for an element, unbounded
determines an unlimited number of occurrences, required
specifies the mandatory entry, mixed
specifies an element that has a mixed type, and so on.
We can offer such a scheme file for our student (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.6.6 Data Binding Technology
Using Java language provides a convenient way to work with XML files, namely, the mechanism of data binding. This mechanism involves generation of a set of classes that describe the elements of a file, and the creation of a corresponding structure of objects in memory.
The XML data binding tool contains a schema compiler that translates the schema into a set of schema-specific classes with associated access methods (getters and setters). It also contains a marshalling mechanism (recording of structured data in an XML document), supports the unmarshalling of XML documents in the corresponding structure of interconnected instances. The automatically created data structure can be used without manually placing data in lists or arrays.
Traditionally, the first technology of data binding was Castor technology. Later, the JAXB API (Java Architecture for XML Binding) was standardized. Version 2 of the JAXB specification implies both the generation of classes according to the scheme and the generation of the scheme according to the existing class structure.
Data binding technology is most often used to generate classes according to an existing schema.
In order to support the standard JAXB APIs in the IntelliJ IDEA Community Edition environment, you need to make some adjustments. One way to implement JAXB technology is the connection the xjc.exe
utility included in the JDK toolkit. This utility can be launched at the command prompt, but it is advisable to configure the context menu. In the Settings window, we select Tools | External Tools and press the "+" button. In the Edit Tool dialog box, we enter the name (Name:) of the new Generate JAXB Classes
command, the path to the xjc.exe
utility (Program:), which should be selected on the particular computer in the file selection dialog (button "..." ), and parameters (Parameters:), which in our case will be as follows:
-p $FileFQPackage$ -d "$SourcepathEntry$" "$FilePath$"
Note. Beginning with the JDK 9 xjc.exe
is excluded from the JDK. The best way to overcome this problem is the use of Maven Plugin cxf XJC
.
In order for the created command to work correctly, the schema file should be placed in a new package, in which generated files will appear.
Note: the settings of the Eclipse environment will not be considered further.
Consider the following example. In the project folder, we create the XML document Hello.xml
(New | File the context menu of the project). It is desirable to open this file with the help of a text editor (Open With | Text Editor) and add the following text:
<?xml version="1.0" encoding="UTF-8" ?> <Greetings> <Hello Text="Hello, XML!" /> </Greetings>
This file corresponds to the Hello.xsd
schema file, which we also create in the project directory. It is also desirable to open this file using a text editor and add the following text:
<?xml version="1.0" encoding="utf-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="Greetings"> <xs:complexType> <xs:sequence> <xs:element name="Hello"> <xs:complexType> <xs:attribute name="Text" type="xs:string" use="required" /> </xs:complexType> </xs:element> </xs:sequence> </xs:complexType> </xs:element> </xs:schema>
Now we can generate new classes. The Greetings.java
and ObjectFactory.java
files appear in the project tree (in the corresponding package).
The Greetings
class represents the root of the XML document and contains the nested Hello
class inside. In general, all nested tags correspond to nested classes located inside the class that is responsible for the root tag. Accordingly, the Greetings
class contains a field of Greetings.Hello
type and provides methods getHello()
and setHello()
. Regarding the Greetings.Hello
class, this class, in accordance with the XML document schema, contains a text
field of String type to represent the corresponding attribute, as well as the getText()
and setText()
methods. The annotations in the code control the representation of data in an XML document.
The ObjectFactory
class provides the so-called factory methods for creating generated class objects: createGreetings()
and createGreetingsHello()
. Since class generation tool always creates a class named ObjectFactory
, classes that correspond to different schemata should be located in different packages.
Now in the main()
function, we can load the document, read and change the attribute value and write to the new file:
package ua.inf.iwanoff.oop.third; import java.io.*; import javax.xml.bind.*; public class HelloJAXB { public static void main(String[] args) { try { // The JAXBContext class object provides access to the JAXB API: JAXBContext jaxbContext = JAXBContext.newInstance("ua.inf.iwanoff.oop.third"); // package with necessary classes // Read data from the file and put it into the object of the generated class: Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); Greetings greetings = (Greetings)unmarshaller.unmarshal(new FileInputStream("Hello.xml")); // Output the old value of the attribute: System.out.println(greetings.getHello().getText()); // Change the attribute value: greetings.getHello().setText("Hello, JAXB!"); // Create a Marshaller object for output to a file: Marshaller marshaller = jaxbContext.createMarshaller(); // Turn on formatting: marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); // Save the object in a new file: marshaller.marshal(greetings, new FileWriter("HelloJAXB.xml")); } catch (JAXBException | IOException e) { e.printStackTrace(); } } }
The new HelloJAXB.xml
file will be as follows:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Greetings> <Hello Text="Hello, JAXB!"/> </Greetings>
As can be seen from the example, data binding technology provides better formatting of an XML document.
2.6.7 Serialization into XML Files
The main disadvantage of the binary serialization described before (Laboratory training # 1) is the need to work with binary (non-textual) files. Usually such files are used not for long-term storage of data, but for one-time storage and recovery of objects. Certainly, serialization in a text file, in particular in an XML document, is more convenient and manageable. There are several approaches to serialization and deserialization, built on XML. The easiest approach is to use java.beans.XMLEncoder
and java.beans.XMLDecoder
classes. The most natural use of these classes is the storage and restoring of the graphical interface visual controls. But you can also store objects of all classes that meet the Java Beans specification.
Java Bean is a Java class that confirms to the following requirements:
- class is public
- class does not contain public data (only methods can be public)
- the default constructor (without arguments) must be present in the class
- class must implement the
java.io.Serializable
interface - the pair of access methods
setNnn()
andgetNnn()
build so called property with the namennn
and appropriate type. Theboolean
type properties require getters with prefix "is" instead of "get" (isNnn()
)
Previously, the Line
and Point
classes were implemented. XML serialization does not require the implementation of the Serializable
interface. However, classes must be public, have public access functions (getters and setters) to private fields. The Point
class:
package ua.inf.iwanoff.oop.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; } }
Class Line
:
package ua.inf.iwanoff.oop.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; } }
The following code provides XML serialization:
package ua.inf.iwanoff.oop.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(); } } }
After executing the program, we will receive the following XML file:
<?xml version="1.0" encoding="UTF-8"?> <java version="1.8.0_66" class="java.beans.XMLDecoder"> <object class="ua.inf.iwanoff.oop.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>
Now deserialization may be accomplished using such code:
package ua.inf.iwanoff.oop.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(); } } }
There are also other (non-standard) implementations of XML serialization. One of the most popular libraries are XStream. This free library allows you to serialize and deseriate XML and JSON files very easily. To work with this library, you need to download the necessary JAR files. But a more convenient and modern approach provides the use of Maven to connect the library. The corresponding example will be discussed later.
2.7 Use of Build Automation Tools
2.7.1 Overview
Build automation provides for use of special tools for automatic tracking of dependencies between files within the project. Build automation also involves performing typical actions such as, for example,
- source code compiling;
- programs assembly from individual parts;
- preparation of documentation;
- creation of a JAR archive;
- deployment of the program.
Integrated development environments (IDE) are most often assuming project assembly management. However, these tools are usually limited and not compatible in different IDEs. Sometimes, the need for transferring the project to another IDE occurs. In addition, it would be convenient to describe and fix a sequence of some actions over the project artifacts while performing a typical set of tasks of the development process.
An alternative offers independent build automation tools. The most popular build tools are Apache Ant, Gradle and Apache Maven.Apache Ant is a Java-based set of tools for automating software build process compatible with different platforms. This is Apache Software Foundation project. Management of the build process takes place with the XML scenario, the so-called build file ( build.xml by default), which corresponds to certain rules.
Actions that can be performed with Ant are described by targets . Targets may depend on each other. If another target should be performed to perform a certain goal, then you can make a dependence of one target from another. Targets contain invocations of tasks. Every task is a command that performs some elementary action. There are several predefined tasks that are designed to describe typical actions: compiling using javac , running a program, creation of JAR, deployment, etc.
There is the possibility of expanding the set of ANT tasks. Ant tasks also include work with a file system (creating directories, copying and deleting files), documentation generation, etc.
Today, Ant has become less popular, compared with Gradle and Maven because of their limitations. In addition, compared to Maven, Ant offers an imperative (command) approach to projecting: The developer must describe the sequence of actions performed during harvesting rather than the expected result.
The Gradle build automation tool was first established in 2007 under the Apache License 2.0. In September 2012, a stable implementation was issued 2.7. Gradle uses concepts of Apache Ant and Apache Maven, but instead of XML it uses the language built on the Groovy language syntax. Gradle means are mainly used in Android development.
2.7.2 Apache Maven
Apache Maven is a build automation tool that uses XML syntax to specify the build options, but compared with Ant it provides a higher level of automation. Maven is created and published by Apache Software Foundation since 2004 . To determine the build options, POM (Project Object Model) is used. Unlike Apache Ant, Maven provides a declarative, and not imperative description of a project: pom.xml project files contains its declarative description (which we want to get), not separate commands.
Like Ant, Maven allows you to start the compiling processes, the creation of JAR files, documentation generation, etc.
The most important function of Maven is the management of dependencies that are present in projects using third-party libraries (which, in turn, use other third-party libraries). Also, Maven allows you to solve libraries version conflicts.
Maven is based on Plugin architecture that allows you to apply plugins for various tasks (compile
, test
, build
, deploy
, checkstyle
, pmd
, scp-transfer
) without having to install them in a specific project. There is a large number of plugins developed for different purposes.
Information for the Maven support project is contained in a pom.xml
file, which specifies dependencies of the Maven-controlled package, from other packages and libraries.
IntelliJ IDEA establishes support for maven projects. To create a new project with Maven support in the New Project window, you can select Maven in the left side. For the project, the JDK version (Project SDK) is determined. Suppose it's JDK 11. In addition, the project can be created, based on archetype. For the first Maven project you can bypass without archetypes.
On the next page of the wizard, we select the Name (for example, HelloMaven
), Location and Artifact Coordinates, which includes the so-called GAV (groupId
, artifactId
, version
).
- groupId: reference to the author or organization (subdivision) where the project has been created; the corresponding identifier is built by the rules of constructing package names: an inverted domain name;
- artifactId: project name; It does not necessarily have to coincide with the name of the Intellij IDEA project, but the use of the same names in this context is desirable; When creating a project, this field is automatically completed by the project name;
- version: project version; The 1
1.0-SNAPSHOT
is established, that is, this is the first version of the project that is in development; This means that the project is under development.
For our first project IntelliJ IDEA automatically creates a pom.xml
file with such content :
<?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.oop.third</groupId> <artifactId>HelloMaven</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> </project>
The properties
section specifies the JDK version (JDK 11).
Let's take a look on the project structure. This is a typical Maven project structure. At the project level, the src
folder is created with this structure:
src main java resources test java
The src
directory is a root directory of the source code and the code of test classes. The main
directory is a root directory for a source code that is concerned directly with the application (without tests). The test
directory contains a source code of test classes. Packages of the source code are allocated in the subdirectories of the java
directory. The resources
directory contains other resources required for the project. This can be properties that are used to internationalize programs, GUI markup files, styles, or something.
After compiling the project, a target
directory with compiled classes will be added to the project structure.
The Maven tool window, whose shortcut is usually located on the right, contains a list of Maven commands that provide the life cycle of the project:
clean
– project cleaning and deleting all files that have been created by previous build;validate
– checking the correctness of metinformation about the project;compile
– compiling project;test
– testing with JUnit;package
– creation of an archive jar, war or ear;verify
– verifying the correctness of the package and compliance with quality requirements;install
– installation (copying) of .jar, .war or .ear files in local repository;site
– site generation;deploy
– project publication in a remote repository.
Note: if you use Maven outside of IntelliJ IDEA, these commands are entered in a command prompt, for example: mvn clean
; to use Maven without IntelliJ IDEA, it should be downloaded and installed separately.
Some commands for their successful execution require the implementation of previous lifecycle commands. For example, package
involves performing compile
. Performing previous commands is automatically done. Execution of commands involves output of typical Maven messages in a console window.
In the java
directory, we create a package and a class on which a Maven demonstration will be performed. For example, a class has such a source code:
package ua.inf.iwanoff.oop.third; public class MavenDemo { 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)); } }
Among Maven commands there is no direct execution of the program. In order to execute the program, IntelliJ IDEA Run menu functions should be used. But the necessary Maven commands that cover certain phases of the lifecycle are automatically performed.
Note: a set of standard Maven commands can be extended with the plugin mechanism.
Very important function of Maven is dependency management. Usually a real project uses numerous libraries to connect which JAR files must be downloaded. These libraries are based on the use of other libraries, which also needs to be loaded. A separate problem arises with versions of libraries and their compatibility.
Maven provides a simple declarative approach to dependency management. It is enough to add information about the required library in the <dependencies>
section. For example, to test our project, it is advisable to use JUnit 5. You can, of course, add the necessary dependency manually, but it is better to use IntelliJ IDEA interactive capabilities. By selecting the pom.xml
file window, you can click Alt+Insert, then in the Generate list you should select Dependency, then in the Search For Artifact dialog box type junit
and select org.junit.jupiter:junit-jupiter-api:5.8.1
. The necessary group <dependencies>
will be added:
<dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.8.1</version> </dependency> </dependencies>
In this code, the data are marked as an error. You need to reload the Maven project. In the Maven tool window, you can find and press the first button (Reload All Maven Projects). Errors in pom.xml
should disappear.
Now you can add a test. Use Generate... | Test... context menu function. A parallel hierarchy of packages, as well as necessary class will be added to test
branch of the project tree.
2.8 Logging
2.8.1 Overview
Working with logging is used to register in a spical file (usually a text file) protocol of events that occur during the program execution. This is, for example, tracing constructors, methods, processing exceptions and other evident messages concerned with debugging.
Logger is an entry point to the logging system. Each logger can be considered as a named channel of messages to which they are sent for future processing.
An important concept of logging is log level, which determines the relative importance of messages to be logged. When the message is sent to the logger, the message logging level is compared with logger logging level. If the logging level of the message above or is equal to the logger logging level, the message will be processed, otherwise ignored.
2.8.2 Standard Java Tools for Logging
Standard tools of java.util.logging
package give ways to protocol events. There are such levels of logging in increasing order: FINEST
, FINER
, FINE
, CONFIG
, INFO
, WARNING
, SEVERE
, as well as ALL
and OFF
, which turns on and offs all levels respectively. To create a log you should use static methods of java.util.logging.Logger
class. For example:
Logger log = Logger.getLogger("MyLog"); log.setLevel(Level.ALL);
The log name is determined arbitrarily. Now you can write data, in particular, the messages:
log.log(Level.INFO, "OK"); // output to the console
If we want to put the messages also to the file, you should use the java.util.logging.FileHandler
class:
FileHandler fileHandler = new FileHandler("C:/MyFile.log"); log.addHandler(fileHandler); log.log(Level.INFO, "OK"); // output to the console and into a file
Note: recording to the file involves catch of the java.io.IOException
.
In the following example, a log that receives messages to all levels is created. Simultaneously with the output on the console messages are recorded in a defined file:
package ua.inf.iwanoff.oop.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"); // output to the console and into a file } }
For the configuration of standard logging tools, a special file of properties (with .properties
extension) is used. In particular, you can separately set the logging options for output to the console and into a file.
2.8.3 Using Log4j Library
There are drawbacks of standard logging tools (java.util.logging
). These are the difficulties of setting up, low efficiency, limited logging capabilities, configuration is not enough intuitive. These disadvantages stimulated the independent development of alternative login libraries.
Apache Log4j 2 is Java logging library, which actually become an industrial standard. It provides significant improvements over its predecessor, Log4j 1. Since 2015, the version of Log4J 1 is not recommended for use.
Currently, version 2.14 is relevant. The log4j API can be downloaded at https://logging.apache.org/log4j/2.x/.
In order to take advantage of Log4J 2 library capabilities, you can create a new Maven project, e.g. log4j-test
. You should add such dependencies to the pom.xml
file:
<dependencies> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.14.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.14.1</version> </dependency> </dependencies>
After reloading the project (Reload All Maven Projects button), you can use Log4J 2.
Now we create a class with main()
function. We create an object of org.apache.logging.log4j.Logger
class. This object allows recording messages in accordance with the level.
package ua.in.iwanoff.oop.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!"); } }
The information about the date and time, function and class precedes the text "Hello, Log4j!
".
Logging options are stored in a special configuration file. Since the configuration of the login is not yet defined (there is no corresponding file), the default configuration is operated, according to which only error
and fatal
messages are displayed. Logging of fatal
, which is used to output the message, has the highest priority. All messages are shown on the console.
In order to change the logging policy, you must create a configuration file. His name is log4j2.xml
. Such a file should be created in the java\src\resources
folder. Its content in the simplest case will be as follows:
<?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>
The file contains a group <Appenders>
, which indicates that the output is carried out on the console and to a file whose name contains the "hello-app
" string and the current date. The <Loggers>
group contains levels of output. In our case, this is "debug
".
Log4J supports such levels of output, in order of growing priority:
trace debug info warn error fatal
Setting a certain level means that only messages of this or a higher priority are recorded. Therefore, in our case, the output of fatal
level is also performed.
Since default configuration is no longer used, information about the date and time, function and class disappeared. It can be restored by changing the log4j2.xml
file content:
<?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>
In addition to XML format, the configuration file can be created in JSON, YAML, or PROPERTIES formats.
3 Sample Programs
3.1 Copying of Text Files Line by Line
Suppose we need to create an application that performs copying text files line by line. File names are specified by command line arguments. The text of the program can be as follows:
package ua.inf.iwanoff.oop.third; import java.io.*; public class TextFileCopy { public static void main(String[] args) { if (args.length < 2) { System.out.println("Arguments are needed!"); return; } try (BufferedReader in = new BufferedReader(new FileReader(args[0])); PrintWriter out = new PrintWriter(new FileWriter(args[1]))) { String line; while ((line = in.readLine()) != null) { out.println(line); } } catch (IOException e) { e.printStackTrace(); } } }
3.2 Sorting Real Numbers
Suppose it is necessary to implement a program that reads from a text file real values in the range from -1000 to 1000, sorts them by increment and by decreasing absolute values and stores both results in two new text files. Numbers in the output file are separated by spaces, they should be read to the end of file.
In the DoubleNumbers
class that we are designing, we create a nested static class to represent an exception associated with an incorrect real value (less than -1000 or more than 1000). In addition, while the sortDoubles()
function that performs the main task, can throw IOException
(file not found, file can not be created, etc.) and InputMismatchException
(an object of type Scanner
tries to get Double
from a token that cannot be converted into number). To sort absolute values in descending order, we create a separate static function comareByAbsValues()
, in which a local class is created and its object is returned. The source code will look like this:
package ua.inf.iwanoff.oop.third; import java.io.*; import java.util.*; import static java.lang.Math.*; public class DoubleNumbers { /** * An internal exception class that allows you to keep the invalid * real value read from file (less than -1000 or more than 1000) * */ public static class DoubleValueException extends Exception { private double wrongValue; public DoubleValueException(double wrongValue) { this.wrongValue = wrongValue; } public double getWrongValue() { return wrongValue; } } /** * A static function that determines the method of comparing * real numbers when sorting by decreasing the absolute value * * @return an object that implements the Comparator interface * */ public static Comparator<Double> comareByAbsValues() { // Local class: class LocalComparator implements Comparator<Double> { @Override public int compare(Double d1, Double d2) { return -Double.compare(abs(d1), abs(d2)); } } return new LocalComparator(); } /** * The function reads real numbers in the range from -1000 to 1000, sorts numbers * in two ways and puts results into two resulting files * * @param inFileName source file name * @param firstOutFileName name of the file containing numbers sorted ascending * @param secondOutFileName name of the file containing numbers * sorted by increasing absolute values * @throws DoubleValueException * @throws IOException * @throws InputMismatchException */ public static void sortDoubles(String inFileName, String firstOutFileName, String secondOutFileName) throws DoubleValueException, IOException, InputMismatchException { Double[] arr = {}; try (BufferedReader reader = new BufferedReader(new FileReader(inFileName)); Scanner scanner = new Scanner(reader)) { while (scanner.hasNext()) { double d = scanner.nextDouble(); if (abs(d) > 1000) { throw new DoubleValueException(d); } Double[] arr1 = new Double[arr.length + 1]; System.arraycopy(arr, 0, arr1, 0, arr.length); arr1[arr.length] = d; arr = arr1; } } PrintWriter firstWriter = new PrintWriter(new FileWriter(firstOutFileName)); PrintWriter secondWriter = new PrintWriter(new FileWriter(secondOutFileName)); try { Arrays.sort(arr); for (Double x : arr) firstWriter.print(x + " "); Arrays.sort(arr, comareByAbsValues()); for (Double x : arr) secondWriter.print(x + " "); } // The resulting files should be closed in the finally block: finally { firstWriter.close(); secondWriter.close(); } } public static void main(String[] args) { try { sortDoubles("in.txt", "out1.txt", "out2.txt"); } // Invalid real value: catch (DoubleValueException e) { e.printStackTrace(); System.err.println("Wrong value: " + e.getWrongValue()); } // Error associated with files: catch (IOException e) { e.printStackTrace(); } // The file contains something that is not a real number: catch (InputMismatchException e) { e.printStackTrace(); } } }
The hasNext()
function returns true
if the Scanner
object can read the next value.
3.3 Binary Serialization and Deserialization
Suppose we need to create the Country
and Continent
classes, create an object of type Continent
, perform its serialization and deserialization. The Country
class can be as follows:
package ua.inf.iwanoff.oop.third; import java.io.Serializable; public class Country implements Serializable { private static final long serialVersionUID = -6755942443306500892L; private String name; private double area; private int population; public Country(String name, double area, int population) { this.name = name; this.area = area; this.population = population; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getArea() { return area; } public void setArea(double area) { this.area = area; } public int getPopulation() { return population; } public void setPopulation(int population) { this.population = population; } }
The Continent
class can be as follows:
package ua.inf.iwanoff.oop.third; import java.io.Serializable; public class Continent implements Serializable { private static final long serialVersionUID = 8433147861334322335L; private String name; private Country[] countries; public Continent(String name, Country... countries) { this.name = name; this.countries = countries; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Country[] getCountries() { return countries; } public void setCountries(Country[] countries) { this.countries = countries; } }
The following program creates and serializes the Continent
object:
package ua.inf.iwanoff.oop.third; import java.io.*; public class DataSerialization { public static void main(String[] args) { Continent c = new Continent("Europe", new Country("Ukraine", 603700, 46314736), new Country("France", 547030, 61875822), new Country("Germany", 357022, 82310000) ); try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Countries.dat"))) { out.writeObject(c); } catch (IOException e) { e.printStackTrace(); }; } }
In this way, we can make deserialization:
package ua.inf.iwanoff.oop.third; import java.io.*; public class DataDeserialization { public static void main(String[] args) throws ClassNotFoundException { try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("Countries.dat"))) { Continent continent = (Continent) in.readObject(); for (Country c : continent.getCountries()) { System.out.println(c.getName() + " " + c.getArea() + " " + c.getPopulation()); } } catch (IOException e) { e.printStackTrace(); }; } }
3.4 Working with Archives
The object data from Example 3.3 can be saved in the archive. The following program creates a Continent
object and stores the data in the archive. Each country has its own ZipEntry
entry point:
package ua.inf.iwanoff.oop.third; import java.io.*; import java.util.zip.*; public class StoreToZip { public static void main(String[] args) { Continent continent = new Continent("Europe", new Country("Ukraine", 603700, 46314736), new Country("France", 547030, 61875822), new Country("Germany", 357022, 82310000) ); try (ZipOutputStream zOut = new ZipOutputStream(new FileOutputStream("Continent.zip")); DataOutputStream out = new DataOutputStream(zOut)) { for (Country country : continent.getCountries()) { ZipEntry zipEntry = new ZipEntry(country.getName()); zOut.putNextEntry(zipEntry); out.writeDouble(country.getArea()); out.writeInt(country.getPopulation()); zOut.closeEntry(); } } catch (IOException e) { e.printStackTrace(); } } }
So we can read from the archive:
package ua.inf.iwanoff.oop.third; import java.io.*; import java.util.zip.*; public class ReadFromZip { public static void main(String[] args) { try (ZipInputStream zIn = new ZipInputStream(new FileInputStream("Continent.zip")); DataInputStream in = new DataInputStream(zIn)) { ZipEntry entry; while ((entry = zIn.getNextEntry()) != null) { System.out.println("Country: " + entry.getName()); System.out.println("Area: " + in.readDouble()); System.out.println("Population: " + in.readInt()); System.out.println(); zIn.closeEntry(); } } catch (IOException e) { e.printStackTrace(); } } }
3.5 Using DOM Technology
Suppose a XML document with about the continent data (Continent.xml
) is prepared:
<?xml version="1.0" encoding="UTF-8"?> <ContinentData Name="Europe"> <CountriesData> <CountryData Name="Ukraine" Area="603700" Population="46314736" > <CapitalData Name="Kiev" /> </CountryData> <CountryData Name="France" Area="547030" Population="61875822" > <CapitalData Name="Moscow" /> </CountryData> <CountryData Name="Germany" Area="357022" Population="82310000" > <CapitalData Name="Berlin" /> </CountryData> </CountriesData> </ContinentData>
Note: error with the capital of France is intentional.
It is necessary use DOM tools to read the data, fix the error and save it in a new file. The program will look like this:
package ua.inf.iwanoff.oop.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")) { // Find the attribute by name: if (countryNode.getAttributes().getNamedItem("Name").getNodeValue().equals("France")) { 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("Paris"); 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.6 XML Serialization Using the XStream Library
Suppose it is necessary to serialize and deserialize data about the line, which is described by two points. We create a new Maven-project XStreamTest
. We add dependency on the XStream library to the pom.xml
file. Now we get the following pom.xml
file:
<?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.oop.third</groupId> <artifactId>XStreamTest</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.18</version> </dependency> </dependencies> </project>
We create Line
and Point
classes. These classes have neither default constructors nor public properties, so they cannot be serialized using java.beans.XMLEncoder
and java.beans.XMLDecoder
. But XStream allows serialization because this library serializes fields, but not properties. Point
class:
package ua.inf.iwanoff.oop.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
class:
package ua.inf.iwanoff.oop.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; } }
We can create such a class to write to a file:
package ua.inf.iwanoff.oop.third; import com.thoughtworks.xstream.XStream; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; public class XStreamSerialization { 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(); } } }
We get an XML file:
<line> <first> <x>1.0</x> <y>2.0</y> </first> <second> <x>3.0</x> <y>4.0</y> </second> </line>
Note: if not to use alias, root tag will be like that: <ua.inf.iwanoff.oop.third.Line>
In another program we read from XML file:
package ua.inf.iwanoff.oop.third; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.security.AnyTypePermission; import java.io.File; public class XStreamDeserialization { 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); } }
The library also provides other ways to work with files.
3.7 Classes "Country" and "Census"
Suppose we need to supplement the previously created program with classes that perform the tasks of the example given in the previous Laboratory training, but data on the population census are read from file, and the results of sorting by population are recorded in another file. We can create new derived classes from previously created classes, one of which will read data from a text file and write modified (sorted) data into a text file, and the other provides these functions by working with XML documents. The best option is not to create a new project, but to add a new package to a previously created project that will allow you to refer to previously created classes.
To unify the work of the program, it is useful for us to create an interface that declares the corresponding read and write operations. This interface, for example, can be as follows:
package ua.inf.iwanoff.oop.third; public interface FileIO { void readFromFile(String fileName) throws Exception; void writeToFile(String fileName) throws Exception; }
The TextFileCountry
class for working with text files will be derived from the CountryWithArray
class (implemented as an example of the previous Laboratory training) and implement the FileIO
interface:
package ua.inf.iwanoff.oop.third; import ua.inf.iwanoff.oop.first.AbstractCensus; import ua.inf.iwanoff.oop.first.CensusWithData; import ua.inf.iwanoff.oop.first.CountryWithArray; import java.io.*; import java.util.InputMismatchException; import java.util.Scanner; public class TextFileCountry extends CountryWithArray implements FileIO { @Override public void readFromFile(String fileName) throws FileNotFoundException, InputMismatchException { try (Scanner scanner = new Scanner(new FileReader(fileName))) { setName(scanner.next()); setArea(scanner.nextDouble()); while (scanner.hasNext()) { int year = scanner.nextInt(); int population = scanner.nextInt(); String comments = scanner.nextLine(); addCensus(new CensusWithData(year, population, comments)); } } } @Override public void writeToFile(String fileName) throws IOException { try (PrintWriter out = new PrintWriter(new FileWriter(fileName))) { out.println(getName() + " " + getArea()); for (AbstractCensus census : getCensuses()) { out.print(census.getYear() + " " + census.getPopulation()); out.println(census.getComments()); } } } public static void main(String[] args) { TextFileCountry country = new TextFileCountry(); try { country.readFromFile("Ukraine.txt"); country.testCountry(); country.writeToFile("ByComments.txt"); } catch (FileNotFoundException e) { System.err.println("Read failed"); e.printStackTrace(); } catch (IOException e) { System.err.println("Write failed"); e.printStackTrace(); } catch (InputMismatchException e) { e.printStackTrace(); System.err.println("Wrong format"); } } }
Before program launching we'll create a file with the source data The source data format assumes that the first line contains data about country name and area, then the separate lines contain year of census, population, and comments, separated by spaces. For instance, the following source file can be prepared (Ukraine.txt
):
Ukraine 603628 1959 41869000 The first postwar census 1970 47126500 Population increases 1979 49754600 No comments 1989 51706700 The last soviet census 2001 48475100 The first census in the independent Ukraine
This file can be created by various text editors and placed into the root folder of the project. It is important to specify a UTF-8 code page.
After executing the main()
function of the TextFileCountry
class, a ByComments.txt
file will appear in the project's root folder. The census data in this file will be sorted by the alphabet of comments.
To implement a version of an application that works with XML documents using JAXB, we first need to develop a document and its schema. We can offer, for example, such an XML document (Ukraine.xml
). It should be placed in the root directory of the project:
<?xml version="1.0" encoding="UTF-8"?> <CountryData Name="Ukraine" Area="603628" > <CensusData Year="1959" Population="41869000" Comments="The first postwar census" /> <CensusData Year="1970" Population="47126500" Comments="Population increases" /> <CensusData Year="1979" Population="49754600" Comments="No comments" /> <CensusData Year="1989" Population="51706700" Comments="The last soviet census" /> <CensusData Year="2001" Population="48475100" Comments="The first census in the independent Ukraine" /> </CountryData>
To generate classes using JAXB technology, we create a new xml
subpackage inside the package ua.inf.iwanoff.java.third
. In this package, we place the file of the document schema (Country.xsd
):
<?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="CountryData"> <xs:complexType> <xs:sequence> <xs:element maxOccurs="unbounded" name="CensusData"> <xs:complexType> <xs:attribute name="Year" type="xs:int" use="required" /> <xs:attribute name="Population" type="xs:int" use="required" /> <xs:attribute name="Comments" type="xs:string" use="required" /> </xs:complexType> </xs:element> </xs:sequence> <xs:attribute name="Name" type="xs:string" use="required" /> <xs:attribute name="Area" type="xs:double" use="required" /> </xs:complexType> </xs:element> </xs:schema>
Note. For the project, you should install the UTF-8 code table through the properties of the project. Unfortunately, re-defining a code table for a previously created project can lead to the loss of Cyrillic data in a pre-written code. In this case, it is more correct to create a new project with the UTF-8 code table installed and transfer the required code through the clipboard.
Next, we generate the required classes using JAXB tools. The ObjectFactory
and CountryData
classes will be created. The last class describes the country data according to the scheme. Inside this class we can find the nested static class CensusData
. A reference to this class can be used in the new XMLCensus
class, which will represent a separate census for the case of reading data from an XML file. This class actually adapts CensusData
to the requirements of the hierarchy of previously created classes. The XMLCensus
class code will be
package ua.inf.iwanoff.oop.third; import ua.inf.iwanoff.oop.first.AbstractCensus; import ua.inf.iwanoff.oop.third.xml.CountryData; public class XMLCensus extends AbstractCensus { CountryData.CensusData censusData; public XMLCensus(CountryData.CensusData censusData) { this.censusData = censusData; } @Override public int getYear() { return censusData.getYear(); } @Override public void setYear(int year) { censusData.setYear(year); } @Override public int getPopulation() { return censusData.getPopulation(); } @Override public void setPopulation(int population) { censusData.setPopulation(population); } @Override public String getComments() { return censusData.getComments(); } @Override public void setComments(String comments) { censusData.setComments(comments); } }
Now we are starting to create an XMLCountry
class. The most interesting of the automatically generated classes is the CountryData
class. It is advisable to describe the XMLCountry
class field to work with the data of an XML file, namely, a reference to the root element:
private CountryData countryData = new CountryData();
The data structure will use the structure of objects of automatically generated classes. This structure appears in memory after reading the data from the XML document. Access to individual data will be done using methods of automatically generated classes. For sorting, we create a temporary array. The entire output file of XMLCountry.java
will look like this::
package ua.inf.iwanoff.oop.third; import ua.inf.iwanoff.oop.first.AbstractCensus; import ua.inf.iwanoff.oop.first.AbstractCountry; import ua.inf.iwanoff.oop.third.xml.CountryData; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; import java.util.Collections; import java.util.Comparator; public class XMLCountry extends AbstractCountry implements FileIO { private CountryData countryData = new CountryData(); @Override public String getName() { return countryData.getName(); } @Override public void setName(String name) { countryData.setName(name); } @Override public double getArea() { return countryData.getArea(); } @Override public void setArea(double area) { countryData.setArea(area); } @Override public AbstractCensus getCensus(int i) { return new XMLCensus(countryData.getCensusData().get(i)); } @Override public void setCensus(int i, AbstractCensus census) { countryData.getCensusData().get(i).setYear(census.getYear()); countryData.getCensusData().get(i).setPopulation(census.getPopulation()); countryData.getCensusData().get(i).setComments(census.getComments()); } @Override public boolean addCensus(AbstractCensus census) { CountryData.CensusData censusData = new CountryData.CensusData(); boolean result = countryData.getCensusData().add(censusData); setCensus(censusesCount() - 1, census); return result; } @Override public boolean addCensus(int year, int population, String comments) { CountryData.CensusData censusData = new CountryData.CensusData(); censusData.setYear(year); censusData.setPopulation(population); censusData.setComments(comments); return countryData.getCensusData().add(censusData); } @Override public int censusesCount() { return countryData.getCensusData().size(); } @Override public void clearCensuses() { countryData.getCensusData().clear(); } @Override public void sortByPopulation() { Collections.sort(countryData.getCensusData(), Comparator.comparing(CountryData.CensusData::getPopulation)); } @Override public void sortByComments() { Collections.sort(countryData.getCensusData(), Comparator.comparing(CountryData.CensusData::getComments)); } @Override public AbstractCensus[] getCensuses() { AbstractCensus[] censuses = new AbstractCensus[censusesCount()]; for (int i = 0; i < censusesCount(); i++) { censuses[i] = new XMLCensus(countryData.getCensusData().get(i)); } return censuses; } @Override public void setCensuses(AbstractCensus[] censuses) { clearCensuses(); for (AbstractCensus census : censuses) { addCensus(census); } } @Override public void readFromFile(String fileName) throws JAXBException, FileNotFoundException { JAXBContext jaxbContext = JAXBContext.newInstance("ua.inf.iwanoff.oop.third.xml"); Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); countryData = (CountryData) unmarshaller.unmarshal(new FileInputStream(fileName)); } @Override public void writeToFile(String fileName) throws JAXBException, IOException { JAXBContext jaxbContext = JAXBContext.newInstance("ua.inf.iwanoff.oop.third.xml"); Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); marshaller.marshal(countryData, new FileWriter(fileName)); } public CountryData.CensusData getCensusData(int i) { return countryData.getCensusData().get(i); } public static void main(String[] args) { XMLCountry country = new XMLCountry(); try { country.readFromFile("Ukraine.xml"); country.testCountry(); country.writeToFile("ByComments.xml"); } catch (FileNotFoundException e) { System.out.println("Read failed"); e.printStackTrace(); } catch (IOException e) { System.out.println("Write failed"); e.printStackTrace(); } catch (JAXBException e) { e.printStackTrace(); System.out.println("Wrong format"); } } }
As you can see from the above text, to sort the censuses, we should sort the list of CensusData
objects in the object structure that was created during deserialization. To check the sorting condition, we can use the standard static method Comparator.comparing()
and a reference to the method of calculating the criterion.
After executing the program, the ByComments.xml
file is automatically created in the project's root folder, in which the census data is arranged according to the population growth.
4 Exercises
- Read real values from a text file (to the end of file), find their sum and output this sum to another text file.
- Read the real values from the text file (to the end of the file), find the product of the modules of non-zero elements, and output it to another text file.
- Read the integer values from the text file (to the end of the file), find the product of even numbers, and output it to another text file.
- Read the whole value from the text file (to the end of the file), replace the negative values with absolute values, replace the positive values with zeros, and output the values to another text file.
- Read the integer values from the text file (to the end of the file), divide the even values by 2, multiply the odd values by 2 and write the resulting values to another text file.
- Create classes Faculty and Institute (with an array of faculties as a field). Create objects, implement their binary serialization and deserialization.
- Create a schema and XML document that describes information about the user. Generate classes using JAXB technology.
- Create a schema and XML document that describes information about the book. Generate classes using JAXB technology.
- Create a schema and XML document that describes information about the city. Generate classes using JAXB technology.
- Create a schema and XML document that describes information about the movie. Generate classes using JAXB technology.
- Create classes Faculty and Institute (with an array of faculties as a field). Create objects, implement their serialization and deserialization in XML.
5 Quiz
- Can you use the main result of some function that has thrown an exception?
- Some function can throw an exception. Can you invoke this function outside of
try
block? - What is the purpose of
printStackTrace()
invocation? - What additional features of the syntax of exceptions appeared in the version of Java 7?
- What is the difference between byte streams and character streams in their application?
- What classes provide the work with text files and binary files?
- Why do you need to close files
?
- Is it possible to open multiple I/O streams at the same time?
- What is the way of automatic closing of I/O streams?
- What are the advantages of using
RandomAccessFile
class? - What are the
DataOutputStream
andDataInputStream
data files used for? What are their advantages and disadvantages? - What is serialization and what is it used for?
- What are the advantages and disadvantages of serialization?
- What functions should be defined for the implementation of the
java.io.Serializable
interface? - What is the
transient
modifier used for? - How does Java work with archives?
- What are purposes of XML documents?
- What restrictions are imposed on the structure of the XML document, the syntax and location of the tags?
- What is the difference between SAX and DOM technologies?
- How do I read and write XML documents?
- What is XSLT?
- What is the difference between a valid and well-formed XML document?
- What are differences between document template definition and document schema?
- Is document template definition (DTD) also XML document?
- Is document schema also XML document?
- Why do XML documents require namespaces?
- What are marshalling and unmarshalling?
- What are advantages of data binding technology?
- What are the standard and non-standard data binding technologies?
- Which classes correspond to the Java Beans specification?
- What are the disadvantages and advantages of XML serialization?