Tuesday, May 31, 2022

Design Pattern: Factory Pattern

Chapters

Factory Pattern

Factory Pattern is a design pattern that provides a platform for creating objects in superclass, but sub-classes determine the structure of the created objects. This pattern consists of factory class, factory method, object to be created and classes that extends/implements factory class.

Factory class is a class that contains factory method. Factory method is a method that is overriden by factory subclasses of factory super class. Then overriding methods generate an object that we wanna create such as buttons, etc. This example demonstrates implementation of Factory pattern in java.
import buttonfactory;

public class ClientCode{

  public static void main(String[] args){
    
    ButtonFactory factory = 
    ButtonFactory.
    selectButtonFactory("Undecorated");
    Button createdButton = null;
    
    if(factory != null){
      createdButton = 
      factory.createButton("MyButton");
      System.out.println
      ("Created Button: " + 
       createdButton.getName());
    }
  }
}

/*
Assume classes below are in buttonfactory package
*/

public interface ButtonFactory{
  
  Button createButton(String name);
  
  static ButtonFactory selectButtonFactory(String name){
    ButtonFactory factory = null;
    switch(name){
    
      case "Standard":
      factory = 
      new CreateStandardButton();
      break;
      
      case "Undecorated":
      factory = 
      new CreateUndecoratedButton();
      break;
    }
    return factory;
  }
}

class CreateStandardButton implements ButtonFactory{
  
  CreateStandardButton(){}
  
  @Override
  public Button createButton(String name){
    return new StandardButton(name);
  }
}

class CreateUndecoratedButton implements ButtonFactory{
  
  CreateUndecoratedButton(){}
  
  @Override
  public Button createButton(String name){
    return new UndecoratedButton(name);
  }
}

public abstract class Button{
  private String name;
  
  public String getName(){
    return name;
  }
  
  Button(String name){
    this.name = name;
  }
  
}

class StandardButton extends Button{
  StandardButton(String name){
    super(name);
    System.out.println(getName() + 
    " standard button has been created!");
  }
}

class UndecoratedButton extends Button{
  UndecoratedButton(String name){
    super(name);
    System.out.println(getName() + 
    " undecorated button has been created!");
  }
}

Result
MyButton undecorated button has been created!
Created Button: MyButton
Typically, factory superclass and objects superclass should be non-instantiable. In short, they can't be instantiated.

One of the advantages of this pattern is that we can add objects, such as new button type, and another factory class subclass without affecting other button types and subclasses of factory class. We can also modify factory sub class and object subclass with little to no effect on other subclasses.

One of the disadvantages of this pattern is that adding more objects or subclasses of factory class can make this pattern complicated real quick.

For more information, you may visit this website.

Monday, May 30, 2022

Java Tutorial: Zip4j - A Java library for zip files/streams

Chapters

Introduction

Zip4J is the most comprehensive Java library for zip files or streams. As of this writing, it is the only Java library which has support for zip encryption, apart from several other features. It tries to make handling zip files/streams a lot more easier. No more clunky boiler plate code with input streams and output streams.

Requirements
JDK 7 or later*

* Zip4j is written on JDK 8, as some of the features (NIO) that Zip4j supports requires features available only in JDK 8. However, considering the fact that Zip4j is widely used in Android, and to support older versions of Android, Zip4j supports JDK 7 as well. In cases where the feature/class from JDK 8 is missing, Zip4j falls back to the features available in JDK 7. In other words, when running on JDK 7, not all features will be supported.

zip4j also supports Zip64. Zip64 removes some limitations that ZIP format has. Zip4j will automatically make a zip file with Zip64 format and add appropriate headers, when it detects the zip file to be crossing these limitations. You do not have to explicitly specify any flag for Zip4j to use this feature.

Note: If you're using maven, add this to your pom.xml and you don't need to add a new classpath in order to use zip4j.
<dependency>
    <groupId>net.lingala.zip4j</groupId>
    <artifactId>zip4j</artifactId>
    <version>2.10.0</version>
</dependency>
</pre>
Latest version can be found here.

Before running the example above, we will temporarily add a new classpath where "src\main\java" folder is located in zip4j folder. Command syntax:
set classpath=[root]:\[path];
e.g.
set classpath=C:\test\zip4j-2.10.0-master\src\main\java;
Once the new classpath is added, we can execute the example above. Once we close cmd/terminal, number of classpaths in our system will return to normal.

Create a Zip File or Add a File to a Zip File

First off, let's create a zip file and add a single file in it.
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException{
    ZipFile zip = null;
    
    try{
      zip = new ZipFile("myzip.zip");
      zip.addFile("img1.jpg");
      zip.addFile("img2.jpg");
    }
    finally{
      if(zip != null)
        zip.close();
    }
    
    System.out.println
    ("myzip.zip has been created!");
  }
}
ZipFile has add*() methods that can be used to create a zip; add a files/folder to a zip file; extract and remove files from zip file. Initializing a ZipFile instance doesn't create a new zip file.

addFile(File fileToAdd) Adds input source file to the zip file with default zip parameters. If zip file does not exist, this method creates a new zip file. This method throws an exception if the file to be added doesn't exist.

ZipParameters Encapsulates the parameters that that control how Zip4J encodes data.

I think closing a ZipFile instance is not necessary because I think the stream that is used by ZipFile is automatically closed. I'm not really sure though that's why I use try-finally clause. Although, in the documentation, the examples there regarding ZipFile don't use try-finally clause.

We can use addFiles(List<File> filesToAdd) to add multiple files to a zip file. This method adds the list of input files to the zip file with default zip parameters. Example:
import java.util.Arrays;
...
ZipFile zip = null;
...
zip = new ZipFile("myzip.zip");
zip.addFiles(Arrays.asList(
new File("img1.jpg"),
new File("img2.jpg")));
...
This method throws an exception if one of the files in the list doesn't exist. We can use addFolder(File folderToAdd) to add a directory and all of its content to our zip file. This method adds the folder in the given file object to the zip file with default zip parameters. Example:
...
zip = new ZipFile("myzip.zip");
zip.addFolder(new File("folder1/folderA"));
//valid in windows
//new File("folder1\\folderA");
...
If we want to filter files that can be put in a zip file, we can use setExcludeFileFilter(ExcludeFileFilter excludeFileFilter) method from ZipParameters. Example:
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.ZipParameters;
import java.io.File;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException,
                            IOException{
    ZipFile zip = null;
    
    try{
      ZipParameters zp = new ZipParameters();
      zp.setExcludeFileFilter
      (exclude -> {
        if(exclude.toString().endsWith(".JPEG"))
          return true;
        else return false;
      });
      zip = new ZipFile("myzip.zip");
      zip.addFolder(new File("folder"), zp);
    }
    finally{
      if(zip != null)
        zip.close();
    }
    
    System.out.println("Done!");
  }
}
In the example above, a file with .JPEG file extension won't be included in the zip file. ZipParameters() creates a ZipParameters instance with default parameters. ExcludeFileFilter is a functional interface.

Create a Zip File or Add a File to a Zip File Using a Stream

If you need to use an input stream to add data to your zip file, you can use addStream(InputStream inputStream, ZipParameters parameters). For example:
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.ZipParameters;
import java.io.IOException;
import java.io.ByteArrayInputStream;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException,
                            IOException{
    ZipFile zip = null;
    byte[] bytes = {80,81,82,83,84,85};
    
    try{
      try(ByteArrayInputStream bai = 
         new ByteArrayInputStream(bytes)){
         zip = new ZipFile("myzip.zip");
         ZipParameters zp = new ZipParameters();
         zp.setFileNameInZip("text.txt");
         
         zip.addStream(bai, zp);
      }
      
    }
    finally{
      if(zip != null)
        zip.close();
    }
    
    System.out.println("Done!");
  }
}
setFileNameInZip(String fileNameInZip) sets the name of the file where the stream data will be stored. It's required to set the name of the destination file if we're using streams to put data to our zip file. The file extension of the destination file should be equivalent to the intended file extension of the stream data. The path name must be relative and use "/" forward slash as directory separator.

Create a Zip File with STORE Compression Mode

There are two types of compression that are available to zip4j: DEFLATE and STORE. DEFLATE uses Deflate compression algorithm. DEFLATE is the default compression algorithm used by zip4j.

STORE denotes uncompressed zip file. This method just put files in a zip file without any compression. This example demonstrates using STORE compression mode.
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.model.enums.CompressionMethod;
import java.io.File;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException,
                            IOException{
    ZipFile zip = null;
    
    try{
      zip = new ZipFile("myzip.zip");
      ZipParameters zp = new ZipParameters();
      zp.setCompressionMethod
      (CompressionMethod.STORE);
      zip.addFolder(new File("folder"), zp);
      zip.addFile(new File("img.jpg"));
    }
    finally{
      if(zip != null)
        zip.close();
    }
    
    System.out.println
    ("main thread end");
  }
}
In the example above, "folder" and all of its content use STORE compression method whereas "img.jpg" uses the default compression method which is DEFLATE. seCompressionMethod sets the ZIP compression method. CompressionMethod is an enum class that contains compression methods.

There are three compression methods in this enum. However, we can only use two because "AES_INTERNAL_ONLY" is for internal use only.

Create a Password Protected Zip File

We can also create a password protected zip file using zip4j library. EncryptionMethod is an enum class that contains encryption methods. There are three encryption methods that we can use. In this example I'm gonna use AES encryption method.

This example demonstrates creating a password protected zip file.
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.model.enums.EncryptionMethod;
import net.lingala.zip4j.model.enums.AesKeyStrength;
import java.io.File;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException,
                            IOException{
    ZipFile zip = null;
    
    try{
      ZipParameters zp = new ZipParameters();
      zp.setEncryptFiles(true);
      zp.setEncryptionMethod
      (EncryptionMethod.AES);
      zp.setAesKeyStrength
      (AesKeyStrength.KEY_STRENGTH_256);
      
      zip = new ZipFile("myzip.zip","password".toCharArray());
      zip.addFolder(new File("folder"), zp);
      zip.addFile(new File("img.jpg"));
    }
    finally{
      if(zip != null)
        zip.close();
    }
    
    System.out.println
    ("main thread end");
  }
}
ZipFile(String zipFile, char[] password) Creates a new ZipFile instance with the zip file at the location specified in zipFile parameter. password parameter is the password of our zip file.

setEncryptFiles(boolean encryptFiles) Set the flag indicating that files are to be encrypted. We need to invoke this method in order to enable/disable zip encryption. Once the zip encryption is enabled, we add an encryption method. setEncryptionMethod(EncryptionMethod encryptionMethod) sets the encryption method used to encrypt files.

setAesKeyStrength(AesKeyStrength aesKeyStrength) sets the key strength of the AES encryption key. AesKeyStrength is an enum class that contains AES encryption key length.

There are three available key lengths that we can use. However, KEY_STRENGTH_256 is the best key length that we can use in zip4j. KEY_STRENGTH_128 is too low and KEY_STRENGTH_192 is supported only for extracting.

In the example above, "folder" is password protected inside zip file. However, "img.jpg" is not. add zp to the addFile argument-list to make "img.jpg" password protected. For example:
...
zip.addFile(new File("img.jpg"), zp);
...
If you didn't add the ZipParameters instance with setEncryptFiles(true) and setEncryptionMethod(EncryptionMethod.AES) to one of the add*() methods that you're gonna invoke, your zip file won't be password protected.

Create a Split Zip File

To store files in split zip file we can use createSplitZipFile to split files into multiple zip files/folders and createSplitZipFileFromFolder to split a folder into multiple zip files. Take a look at this example.
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.ZipParameters;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Arrays;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException,
                            IOException{
    ZipFile zip = null;
    
    try{
      zip = new ZipFile("myzip.zip");
      List<File> filesToAdd =
      Arrays.asList(
      new File("img1.jpg"),
      new File("img2.jpg"),
      new File("img3.jpg"),
      new File("img4.jpg"));
      
      zip.createSplitZipFile
      (filesToAdd, 
       new ZipParameters(),
       true,
       81920);
    }
    finally{
      if(zip != null)
        zip.close();
    }
    
    System.out.println
    ("main thread end");
  }
}
If we want to split files/folders, we need to put them in a single folder and invoke createSplitZipFileFromFolder method. Take a look at this example.
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.ZipParameters;
import java.io.File;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException,
                            IOException{
    ZipFile zip = null;
    
    try{
      zip = new ZipFile("myzip.zip");
      
      zip.createSplitZipFileFromFolder
      (new File("folder"), 
       new ZipParameters(),
       true,
       81920);
    }
    finally{
      if(zip != null)
        zip.close();
    }
    
    System.out.println
    ("main thread end");
  }
}
Now, let's take a look at the methods' forms:

createSplitZipFile(List<File> filesToAdd, ZipParameters parameters, boolean splitArchive, long splitLength)
createSplitZipFileFromFolder(File folderToAdd, ZipParameters parameters, boolean splitArchive, long splitLength)

filesToAdd parameter is the list of files that is gonna be added to our split zip file. folderToAdd is the folder that is gonna be added to our split zip file. parameters parameter consists of parameters that will be applied to a zip file.

splitArchive parameter is a flag that enables/disables split zip file mode. splitLength parameter is the split size in bytes. Note that zip file format has a minimum split size of 65536 bytes (64KB)(1024*64=65536). An exception will be thrown if we choose a split size lower than 64KB.

If we want to create a password protected split zip file, we instantiate a ZipParamaters instance and set the necessary parameters to create a password protected zip file:
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.model.enums.EncryptionMethod;
import net.lingala.zip4j.model.enums.AesKeyStrength;
import java.io.File;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException,
                            IOException{
    ZipFile zip = null;
    
    try{
      zip = new ZipFile
      ("myzip.zip", "password".toCharArray());
      ZipParameters zp = new ZipParameters();
      zp.setEncryptFiles(true);
      zp.setEncryptionMethod
      (EncryptionMethod.AES);
      zp.setAesKeyStrength
      (AesKeyStrength.KEY_STRENGTH_256);
      
      zip.createSplitZipFileFromFolder
      (new File("folder"), zp, true, 81920);
    }
    finally{
      if(zip != null)
        zip.close();
    }
    
    System.out.println
    ("main thread end");
  }
}

Extracting Zip File

To extract all files in a zip file, we use extractAll method. Take a look at this example.
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException,
                             IOException{
    ZipFile zip = null;
    
    try{
      zip = new ZipFile
      ("myzip.zip");
      zip.extractAll("extracted");
    }
    finally{
      if(zip != null)
        zip.close();
    }
    
    System.out.println
    ("main thread end");
  }
}
extractAll(String destinationPath) method one parameter. destinationPath is the destination directory. extractAll method has another form:

extractAll(String destinationPath, UnzipParameters unzipParameters)

We use this form if we're dealing with symbolic links. As of this writing, UnzipParameters is not well-documented. I guess this class refers to symbolic links extraction in a zip file.

To extract a single file/directory in a zip file, we use extractFile method. Take a look at this example:
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException,
                             IOException{
    ZipFile zip = null;
    
    try{
      zip = new ZipFile
      ("myzip.zip");
      zip.extractFile("img.jpg", "extracted");
      zip.extractFile("folder/", "extracted");
    }
    finally{
      if(zip != null)
        zip.close();
    }
    
    System.out.println
    ("main thread end");
  }
}
extractFile(String fileName, String destinationPath) has two parameters. filename parameter refers to the path in the zip entry. When referring to a zip entry, directory separator must be forward slash("/") and path must be relative. In zip entry, a file name with "/" in the path denotes a directory.

Folder extraction using extractFile method is available to version v2.6.0 and above. destinationPath is the destination path of extracted file. Remember that the file type in destination path must be a directory/folder. Java will create destination directory if it doesn't exist.

If we want to extract a single file and give it a new name once it's extracted, we use this form of extractFile method.

extractFile(String fileName, String destinationPath, String newFileName)

For example:
...
ZipFile zip = new ZipFile
("myzip.zip", password.toCharArray());
zip.extractFile("img.jpg", "extracted", "image.jpg");
...
fileName parameter is the path name of the file in the zip file. destinationPath parameter is the destination directory. newFileName parameter is the new name of the file in the zip file once it's extracted.

Take note that the path in fileName parameter should follow zip specification. It means that the directory separator must be "/" and the path must be relative.

If we want to stream file data in a zip entry, we can get an input stream for an entry. With this, we can read data from the input stream and write the data in an output stream. To do this, we use getInputStream(FileHeader fileHeader) method. For example, we want to get the bytes of an image:
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import java.io.InputStream;
import java.io.IOException;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException,
                             IOException{
    ZipFile zip = null;
    
    try{
      zip = new ZipFile
      ("myzip.zip");
      FileHeader header = 
      zip.getFileHeader("img.jpg");
      
      try(BufferedInputStream bif =
        new BufferedInputStream(zip.getInputStream(header));
        ByteArrayOutputStream baos = 
        new ByteArrayOutputStream()){
          
          bif.transferTo(baos);
          for(byte b : baos.toByteArray())
            System.out.print(b + " ");
          
        }
    }
    finally{
      if(zip != null)
        zip.close();
    }
  }
}
FileHeader is a class that contains file headers of a zip entry. getFileHeader(String fileName) returns FileHeader of a zip entry if a file header with the given path equivalent to fileName parameter exists in the zip model. Otherwise, returns null.

Take note that the path in fileName parameter should follow zip specification. It means that the directory separator must be "/" and the path must be relative.

extractFile has other forms that you can check them out in the documentation.

If we want to extract a password-protected zip file, we use one of ZipFile constructors:
ZipFile(File zipFile, char[] password)
ZipFile(String zipFile, char[] password)
Example:
...
ZipFile zip = new ZipFile
("myzip.zip", password.toCharArray());
zip.extractAll("destination-dir");
...
Using extractFile method.
...
ZipFile zip = 
new ZipFile
("myzip.zip", password.toCharArray());
zip.extractFile("myfile.txt", "destination-dir");
...

Rename Zip Entry

To rename a file in a zip entry, we can use renameFile(String fileNameToRename, String newFileName) method from ZipFile class. Take a look at this example:
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException,
                             IOException{
    ZipFile zip = null;
    
    try{
      zip = new ZipFile
      ("myzip.zip");
      zip.renameFile
      ("image1.jpg", "img1.jpg");
    }
    finally{
      if(zip != null)
        zip.close();
    }
    
    System.out.println("main thread end.");
  }
}
We can use renameFile method to move an entry to another directory entry. For example:
...
zip.renameFile("image1.jpg", "folder/image1.jpg");
...
In the example above, "image1.jpg" will be moved to "folder" directory entry. We can also move and rename file at the same time. For example:
...
zip.renameFile("image1.jpg", "folder/moved-image1.jpg");
...
In the example above, "image1.jpg" will be moved to "folder" directory entry and will be renamed as "moved-image1.jpg". If the directory where a file is going to be moved doesn't exist, java will create one and place the file there.

If we want to rename multiple files by using renameFiles(Map<String,String> fileNamesMap) method. Take a look at this example:
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import java.io.IOException;
import java.util.HashMap;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException,
                             IOException{
    ZipFile zip = null;
    
    try{
      HashMap<String, String> fileNamesMap =
      new HashMap<>();
      fileNamesMap.put
      ("folder/img1.jpg","folder/image1.jpg");
      fileNamesMap.put
      ("folder/img2.jpg","folder/image2.jpg");
      fileNamesMap.put
      ("folder/img3.jpg","folder/image3.jpg");
      
      zip = new ZipFile
      ("myzip.zip");
      zip.renameFiles(fileNamesMap);
      
    }
    finally{
      if(zip != null)
        zip.close();
    }
    
    System.out.println("main thread end.");
  }
}
A map consists of key-value pairs. In the example above, the keys are the current path name of entries and the values are the new path name of entries.

Note that zip entries can have equivalent file paths. If we rename a file in a zip file, all zip entries that have file names that are equivalent to the target file will be renamed. Also, we can rename a directory. Renaming a directory in a zip entry will update all file paths of entries in the directory.

Note that entry paths should follow zip specification. It means that the directory separator must be "/" and the path must be relative. Zip file format does not allow modifying split zip files, and Zip4j will throw an exception if an attempt is made to rename files in a split zip file.

Remove Zip Entry

If we want to remove an entry from a zip file, we can use removeFile(String fileName) method. Take a look at this example:
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException,
                             IOException{
    ZipFile zip = null;
    
    try{
      zip = new ZipFile
      ("myzip.zip");
      //remove a file
      zip.removeFile("img.jpg");
      //remove a directory (since v2.5.0 of Zip4j)
      zip.removeFile("folder/folderA/");
    }
    finally{
      if(zip != null)
        zip.close();
    }
    
    System.out.println("main thread end.");
  }
}
If we want to check if the file that we wanna remove exists in a zip file, we can get a FileHeader instance from a zip entry and check if the instance is null or not. If it's null, the file that we wanna delete doesn't exist in the zip file. Take a look at this example:
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.FileHeader;
import java.io.IOException;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException,
                             IOException{
    ZipFile zip = null;
    
    try{
      zip = new ZipFile
      ("myzip.zip");
      FileHeader header = 
      zip.getFileHeader("img.jpg");
      
      if(header != null){
        zip.removeFile(header);
        System.out.println("\"img.jpg\""+
        " has been removed.");
      }
    }
    finally{
      if(zip != null)
        zip.close();
    }
    
  }
}
In the example above, we use another form of removeFile method which is removeFile(FileHeader fileHeader)

If we want to remove multiple files using a single method, we use removeFiles(List<String> fileNames) method. Since v2.5.0 of zip4j, we can include a directory in the fileNames list and all of its content will be removed. This example demonstrates removeFiles method.
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import java.io.IOException;
import java.util.Arrays;

public class SampleClass{

  public static void main(String[] args)
                     throws ZipException,
                             IOException{
    ZipFile zip = null;
    
    try{
      zip = new ZipFile
      ("myzip.zip");
      zip.removeFiles(Arrays.asList(
      "img.jpg", "folder/img1.jpg",
      "folder/img2.jpg","folder/folderA/"));
    }
    finally{
      if(zip != null)
        zip.close();
    }
    
    System.out.println("main thread end.");
  }
}

Working with ZipInputStream and ZipOutputStream

If we want more control on how we compress/extract zip files, we can use ZipInputStream and ZipOutputStream instead of ZipFile class. ZipInputStream and ZipOutputStream in Zip4j is closely similar to ZipInputStream and ZipOutputStream in java.util.zip package.

If you're not familiar with ZipInputStream and ZipOutputStream, you should read this blogpost that I've created. The blogpost contains tutorial about java.util.zip package.

One of the differences between ZipInputStream and ZipOutputStream of java.util.zip package and Zip4j is that the zip input and output streams of Zip4j has constructors that supports password protected zip. java.util.zip package doesn't support password protected zip files.

This example demonstrates creating a password-protected zip file using ZipOutputStream of Zip4j. To view this example with color highlights and enable full screen, Click Here
import net.lingala.zip4j.io.outputstream.ZipOutputStream;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.model.enums.AesKeyStrength;
import net.lingala.zip4j.model.enums.CompressionMethod;
import net.lingala.zip4j.model.enums.EncryptionMethod;

import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

public class SampleClass{

  public static void main(String[] args)
                     throws IOException{
    
    try(ZipOutputStream zos = 
        new ZipOutputStream(
        new FileOutputStream(
            new File("myzip.zip")), 
            "password".toCharArray())){
        
        ZipParameters zp = 
        new ZipParameters();
        zp.setCompressionMethod
        (CompressionMethod.STORE);
        zp.setEncryptFiles(true);
        zp.setEncryptionMethod
        (EncryptionMethod.AES);
        zp.setAesKeyStrength
        (AesKeyStrength.KEY_STRENGTH_256);
        
        List<FilesToAdd> filesToAdd = 
        Arrays.asList(
        new FilesToAdd
        (new File("folder/img1.jpg"), zp),
        new FilesToAdd
        (new File("folder/img2.jpg"), zp),
        new FilesToAdd
        (new File("folder/img3.jpg"), zp)
        );
        
        for(FilesToAdd fta : filesToAdd){
          
          //Entry size(bytes) must be set if we want to use
          //STORE to be used as compression method in
          //an entry.
          //
          //DEFLATE compression doesn't have this
          //requirement
          if(fta.getZipParameters().
             getCompressionMethod() == CompressionMethod.STORE)
             fta.getZipParameters().
             setEntrySize(fta.getFile().length());
          
          //This parameter must be set if we're sending
          //data to a zip file via stream
          fta.getZipParameters().setFileNameInZip
          (fta.getFile().getName());
          zos.putNextEntry(fta.getZipParameters());
          
          try(InputStream is = 
              new FileInputStream(fta.getFile())){
              is.transferTo(zos);
          }
          
          zos.closeEntry();
        }
     }
  }
}

class FilesToAdd{
  private File file;
  private ZipParameters zp;
  
  FilesToAdd(File file, ZipParameters zp){
    this.file = file;
    this.zp = zp;
  }
  
  ZipParameters getZipParameters(){
    return zp;
  }
  
  File getFile(){
    return file;
  }
  
}
Next, this example demonstrates extracting password-protected zip file using ZipInputStream of Zip4j. To view this example with color highlights and enable full screen, Click Here
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.LocalFileHeader;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.File;
import java.io.IOException;

public class SampleClass{
  
  public static void main(String[] args)
                     throws IOException{
    LocalFileHeader lfh = null;
    byte[] buffer = new byte[3072];
    
    FileInputStream fis = 
    new FileInputStream(new File("myzip.zip"));
    try(ZipInputStream zis = 
        new ZipInputStream(fis, "password".toCharArray())){
        
      while((lfh = zis.getNextEntry()) != null){
        File extractedFile = new File(lfh.getFileName());
          
        try(FileOutputStream fos = new 
            FileOutputStream("extracted/"+extractedFile)){
            
            int len = 0;
            while((len = zis.read(buffer)) != -1)
              fos.write(buffer);
        }
      }
    }
  }
}
One of the differences between FileHeader and LocalFileHeader is that FileHeader consists of general-purpose zip headers whereas LocalFileHeader consists of headers that are local from an entry.

ProgressMonitor

If we want to monitor progress of a single action, we can use ProgressMonitor. This class can monitor the progress of some methods from ZipFile class such as addFolder, addFiles, removeFiles and extractFiles.

This example demonstrates ProgressMonitor. To view this example with color highlights and enable full screen, Click Here
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.progress.ProgressMonitor;
import net.lingala.zip4j.progress.ProgressMonitor.State;
import net.lingala.zip4j.progress.ProgressMonitor.Result;

import java.io.IOException;

public class SampleClass{

  public static void main(String[] args) 
                     throws IOException,
                            InterruptedException{
    ZipFile zip = 
    new ZipFile("myzip.zip", 
                "password".toCharArray());
    zip.setRunInThread(true);
    
    ProgressMonitor pm = 
    zip.getProgressMonitor();
    zip.extractAll("extracted");
    
    while(!pm.getState().equals
         (ProgressMonitor.State.READY)){
      System.out.println("Percentage: " + 
                         pm.getPercentDone());
      System.out.println("File: " + 
                         pm.getFileName());
      System.out.println("Task: " + 
                         pm.getCurrentTask());
                         
      Thread.sleep(50);
    }
    
    if(pm.getResult().
       equals(ProgressMonitor.Result.SUCCESS))
       System.out.println("Success!");
       
    else if(pm.getResult().
            equals(ProgressMonitor.Result.ERROR))
            System.err.println("Error: " + 
            pm.getException().getMessage());
            
    else if(pm.getResult().
            equals(ProgressMonitor.Result.CANCELLED))
            System.out.println("Operation Cancelled!");
  }
}

Result(may vary)
Percentage: 0
File: null
Task: EXTRACT_ENTRY
...
Success!
Take note that this is just a demonstration. That's why the result is not very pretty. We need to put more time and effort on the example above to make a pretty result. Also take note that ProgressMonitor instance from ZipFile class may not be thread-safe. Therefore, proceed with caution when you want multiple threads to access ProgressMonitor instance from ZipFile class.

Alright, let's discuss the example above. First off, we need to invoke setRunInThread(boolean runInThread) method and set its flag to true.

This enables a background thread that monitors some actions happening in ZipFile class. setRunInThread is used in conjunction with ProgressMonitor. Thus, we need to get a ProgressMonitor instance from ZipFile to manage the progress of a task in ZipFile.

To do that, we use getProgressMonitor method. This method returns a ProgressMonitor instance from a ZipFile instance.

ProgressMonitor monitors results and tasks of an action. ProgressMonitor.State has two states: BUSY and READY. READY means that ProgressMonitor is idle and ready to monitor an action. BUSY means that ProgressMonitor is already monitoring an action.

ProgressMonitor.Task contains constants that denote tasks that may occur during compression and extraction. ProgressMonitor.Result contains constants that denote the result of an operation in ZipFile class.

getPercentDone returns the progress of an action in percentage form. getFileName method from ProgressMonitor class returns the absolute path of a file being processed in our file system. getCurrentTask method returns ProgressMonitor.Task task that is currently monitored.

Some Helpful Methods of ZipFile Class

ZipFile class has some helpful methods that can come in handy.

isSplitArchive() returns true if a zip file is a split zip file. Otherwise, returns false;
...
ZipFile zip = new ZipFile("myzip.zip");
...
System.out.println(zip.isSplitArchive());
...
getSplitZipFiles() returns a list of split zip files.
...
ZipFile zip = new ZipFile("myzip.zip");
...
if(zip.isSplitArchive())
  List<File> splitZip = zip.getSplitZipFiles();
...
mergeSplitFiles(File outputZipFile) Merges split zip files into a single zip file without the need to extract the files in the archive. This method doesn't delete the split zip file.
...
ZipFile zip = new ZipFile("myzip.zip");
...
if(zip.isSplitArchive())
  zip.mergeSplitFiles(new File("merged.zip"));
...
isEncrypted() Checks to see if the zip file is encrypted.
...
ZipFile zip = new ZipFile("myzip.zip");
...
System.out.println(zip.isEncrypted());
...
isValidZipFile() Checks to see if the input zip file is a valid zip file. Note this method only checks for the validity of the headers and not the validity of each entry in the zip file.
 ...
ZipFile zip = new ZipFile("myzip.zip");
...
System.out.println(zip.isValidZipFile());
... 
setComment(String comment) Sets comment for the Zip file. Note that the zip file must exist in our file system first before we can set comments on it.
 ...
ZipFile zip = new ZipFile("myzip.zip");
...
zip.setComment("Comment1" + "\n" + "Comment2");
... 
To remove a comment, use empty string "" as argument for setComment method. getComment() returns the comment set for the Zip file.

getFileHeaders() Returns the list of file headers in the zip file. We can use file headers to list all files in every entry of a zip file.
 ...
ZipFile zip = new ZipFile("myzip.zip");
...
List<FileHeader> fileHeaders = 
zip.getFileHeaders();
fileHeaders.stream().
forEach(fileHeader -> 
        System.out.println
        (fileHeader.getFileName()));
... 
ZipParameters

ZipParameters contains parameters that define the structure of a zip file. If we instantiate ZipParameters using its default constructor. Default values of parameters are gonna be used.

These are the default values of zip parameters.
CompressionMethod.DEFLATE
CompressionLevel.NORMAL
EncryptionMethod.NONE
AesKeyStrength.KEY_STRENGTH_256
AesVerson.Two
SymbolicLinkAction.INCLUDE_LINKED_FILE_ONLY
readHiddenFiles is true
readHiddenFolders is true
includeRootInFolder is true
writeExtendedLocalFileHeader is true
CompressionMethod.DEFLATE is the default compression method. We can change this value by calling setCompressionMethod(CompressionMethod compressionMethod). Refer to CompressionMethod class for compression method types.

CompressionLevel.NORMAL is the compression level. This parameter is only applicable to DEFLATE compression method. We can change this value by calling setCompressionLevel(CompressionLevel compressionLevel) method. Refer to CompressionLevel class for compression level types.

EncryptionMethod.NONE is the default encryption method. We change this value if we want to create a password-protected zip file. To change this value, we call setEncryptionMethod(EncryptionMethod encryptionMethod) method. Refer to EncryptionMethod class for encryption method types.

AesKeyStrength.KEY_STRENGTH_256 is the default key length of AES encryption method. To change this value, we call setAesKeyStrength(AesKeyStrength aesKeyStrength) method. Refer to AesKeyStrength for available AES key length.

AesVerson.Two is the default version of AES encryption method. To change this value, call setAesVersion(AesVersion aesVersion) method. Refer to AesVersion for AES versions.

SymbolicLinkAction.INCLUDE_LINKED_FILE_ONLY is the default action for symbolic links. To change this value, we call setSymbolicLinkAction(ZipParameters.SymbolicLinkAction symbolicLinkAction) method. Refer to ZipParameters.SymbolicLinkAction for actions for symbolic links.

readHiddenFiles parameter default value is true. To change this value, we call setReadHiddenFiles(boolean readHiddenFiles) method.

readHiddenFolders parameter default value is true. To change this value, we call setReadHiddenFolders(boolean readHiddenFolders) method.

includeRootInFolder parameter default value is true. To change this value, we call setIncludeRootFolder(boolean includeRootFolder) method. You can see the effect of this parameter if you compress a file in a directory. For example, you add this "folder/file.txt" to your zip file. If includeRootInFolder parameter is true, only "file.txt" will be included to your zip file.

writeExtendedLocalFileHeader parameter default value is true. To change this value, we call setWriteExtendedLocalFileHeader(boolean writeExtendedLocalFileHeader) method. I assume this parameter refers to extra field added to local file header. More information about extra fields can be found here.

Tuesday, May 24, 2022

Java Tutorial: ResourceBundle

Chapters

Introduction

ResourceBundle enables us to pack and load locale-specific data in a persistent file. By using resource bundles, we can assign a name to our application's elements, such as GUI elements, with multiple-locale.

Also, updating locale-specific data in a resource bundle is much more easier than hardcoding it in our code. Thus, it's recommended to use resource bundles for storing names such as names of GUI elements. resource bundle has a hierarchy.

We define a base name to our base resource. Then, we can create a family of resources. For naming convention, Each bundle name in a resource bundle family, except for the base resource, should copy the name of the base resource with an abbreviation of language code. We may append country code if there's a language code in the name. We may append platform code if the country and language code are present in the name.

User underscore to separate base name and codes. For example:
//base name
MyResources

//sibling name with language code
MyResources_en

//sibling name with language code
//and country code
MyResources_en_US

//sibling name with language code,
//country code and platform code
MyResources_en_US_UNIX
For language code reference, you may look at this article. For country code reference, you may look at this article.

Codes in the file name are case-insensitive
e.g. MyResources_en_uS
However, by convention, lowercase language code; uppercase country and platform codes are preferrable. In a list resource file name, country and platform codes should in uppercase.

Resource bundles store data in the form of case-sensitive key-value pairs. Resource bundle is categorized into two types: PropertyResourceBundle and ListResourceBundle.

PropertyResourceBundle

PropertyResourceBundle is a concrete subclass of ResourceBundle that manages resources for a locale using a set of static strings from a property file. The file extension of a property file is .properties

These are the elements that we can write to our properties file.
# Labels
HiLabel = Hi

! Buttons
abortButton Abort
aboutButton: About
"#" and "!" denotes a comment. Characters after these symbols are ignored. Next, key-value pairs can be separated by whitespace ( ), colon (:) or equal (=) symbols.

API Note:
PropertyResourceBundle can be constructed either from an InputStream or a Reader, which represents a property file. Constructing a PropertyResourceBundle instance from an InputStream requires that the input stream be encoded in UTF-8.

By default, if a MalformedInputException or an UnmappableCharacterException occurs on reading the input stream, then the PropertyResourceBundle instance resets to the state before the exception, re-reads the input stream in ISO-8859-1, and continues reading.

If the system property java.util.PropertyResourceBundle.encoding is set to either "ISO-8859-1" or "UTF-8", the input stream is solely read in that encoding, and throws the exception if it encounters an invalid sequence.

If "ISO-8859-1" is specified, characters that cannot be represented in ISO-8859-1 encoding must be represented by Unicode Escapes as defined in section 3.3 of The Java Language Specification whereas the other constructor which takes a Reader does not have that limitation.

Other encoding values are ignored for this system property. The system property is read and evaluated when initializing this class. Changing or removing the property has no effect after the initialization.

ListResourceBundle

ListResourceBundle is an abstract subclass of ResourceBundle that manages resources for a locale in a convenient and easy to use list. File extension of this resource bundle is .java

This is how we create key-value pairs in this resource bundle.
import java.util.ListResourceBundle;
import java.time.LocalDate;

public class ListResource extends ListResourceBundle{

  @Override
  protected Object[][] getContents(){
    return new Object[][]{
      {"item1", "Toy Car"},
      {"release-date", LocalDate.of(2021, 10, 20)},
      {"tags", new String[]{"Automotive", "Toys"}}
    };
  }
}
Remember that keys are String type and values can be of any type.

Using ResourceBundle

Now we know how to create a resource bundle. Let's try using a resource bundle in our program. First off, let's create a properties files. Create a file and name it "PropertyResources.properties" and write this in the file:
# Labels
HiLabel: Hi

# Buttons
abortButton: Abort
aboutButton: About
We're going to create a family bundle so let's create another bundle and name it "PropertyResource_en_GB.properties" and write this in the file:
# Labels
HiLabel = Hello

# Buttons
okButton Okay
aboutButton: About
Next, let's create a code that will access our bundle.
import java.util.Locale;
import java.util.ResourceBundle;

public class SampleClass{

  public static void main(String[] args){
    ResourceBundle rb =
    ResourceBundle.getBundle
    ("MyResources", Locale.UK);
    //If your resource is in a package
    //("Package-Name.MyResources", Locale.UK);
    
    for(String key : rb.keySet()){
      System.out.println(key + " | " + 
      rb.getString(key));
    }
  }
}

Result
HiLabel | Hello
okButton | Okay
aboutButton | About
This code above also works with list resources which have .java file extension. Remember to compile your list resources first because java look for .class file of your list resources. In the getBundle method, we just need to put the base name of our resource bundle. We don't need to add file extensions or codes like country code.

One of the advantages of list resource to a property resource is that list resource contains values of any type whereas property resource only contain values of String type. We can use handleGetObject(String key) if we want to convert values to Object type that can be cast to other object types. handleGetObject(String key) gets an object for the given key.

handleGetObject(String key) has protected access in ResourceBundle. We need to use ListResourceBundle or PropertiesResourceBundle reference in order to access this method. For example:
ListResourceBundle prb = 
(ListResourceBundle)ResourceBundle.getBundle
("MyResources", Locale.US);
...
prb.handleGetObject(key);
ListResourceBundle has getContents() method. This method returns an array in which each item is a pair of objects in an Object array. Resource bundles can be deployed in different ways such as deploying them together with an application. You can read them in the documentation.

When java looks up for a resource bundle, it looks for a specific bundle with specified locale. If there's no match, java will look for a bundle in the family with a locale that is equivalent to our platform's default locale which is returned by invoking Locale.getDefault.

If there's still no match, java will look for default resource bundle or base resource bundle. If there's still no match, an exception will be thrown. If a property resource and list resource have the same name and in the same package or directory, java will prioritize the list resource.

Inheritance

Resource bundle hierarchy implements inheritance. A bundle with more specific name in the family inherits key-value pairs from a bundle with less specific name. For example, Assume we have three resource bundles in the same family and the base name of the family is "MyResources":
//base 
MyResources.properties
okButton = Ok

//specific
MyResources_en.properties
acceptButton = accept

//more specific
MyResources_en_GB.properties
confirmButton = confirm
When we use MyResources_en_GB.properties in our program, this resource bundle will inherit the values in MyResources.properties and MyResources_en.properties.

However, if the bundle with more specific name has equivalent key-value pairs to its less specific counterparts, the key-value pairs in the bundle with more specific name override the equivalent key-value pairs in the bundle with less specific name.

bundles with equivalent specificity don't inherit key-value pairs. For example:
...
MyResources_en_US.properties
confirmButton = confirm

MyResources_en_GB.properties
commitButton = commit
When we use MyResources_en_GB.properties, it won't inherit the key-value pair in MyResources_en_US.properties. Also, list resource and property resource have different hierarchies. Thus, a list resource can't inherit any key-value pairs from property resource and vice-versa.

ResourceBundle.Control

ResourceBundle.Control defines a set of callback methods that are invoked by the ResourceBundle.getBundle factory methods during the bundle loading process.

In other words, a ResourceBundle.Control collaborates with the factory methods for loading resource bundles. The default implementation of the callback methods provides the information necessary for the factory methods to perform the default behavior.

We can override some methods of ResourceBundle.Control to change some behaviors during bundle loading process. For example, we can override getCandidateLocales method to filter candidate locales.
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;

public class SampleClass{

  public static void main(String[] args){
    ResourceBundle rb = 
    ResourceBundle.getBundle
    ("MyResources", Locale.FRANCE, 
     new CustomResourceControl());
     
    for(String key : rb.keySet()){
      System.out.println(key + " | " + 
      rb.getString(key));
    }
     
  }
}

class CustomResourceControl extends 
ResourceBundle.Control{
  
  @Override
  public List<Locale> 
  getCandidateLocales(String s, Locale locale) {
    if(locale.getCountry().equals("US") || 
       locale.getCountry().equals("UK")){
       return super.getCandidateLocales(s, locale);
    }
    else{
      System.err.println
      ("Warning: Locale should be US or UK.");
      System.err.println("Locale has been "
      +"automatically set to Locale.ROOT");
      return Arrays.asList(Locale.ROOT);
      System.out.println();
    }
  }
}

Result
Warning: Locale should be US or UK.
Locale has been automatically set to Locale.ROOT

Thursday, May 19, 2022

Java Tutorial: Create Loggers using java.util.logging Package

Chapters

Overview

Logging is an essential part of developing an application. It lets us diagnose applications with ease. There are thid-party loggers that we can export to java such as Log4j2. In this tutorial, we're gonna use java.util.logging package to create loggers.

Creating a Logger

Creating a logger is easy. We just need to invoke getLogger method from Logger class. Take a look at this example.
import java.util.logging.*;

public class SampleClass{

  //a Logger instance is preferrable to be referenced
  //to a global variable to prevent garbage collector
  //from collecting the logger's instance
  //in unexpected manner
  static Logger logger =
  Logger.getLogger(SampleClass.class.getName());
  
  public static void main(String[] args){
    
    try{
      int num = 1/0;
    }
    catch(ArithmeticException e){
      logger.log(Level.SEVERE, "Divisor is 0!", e);
    }
    
  }
}

Result
May 17, 2022 4:40:44 PM SampleClass main
SEVERE: Divisor is 0!
java.lang.ArithmeticException: / zero
          at SampleClass.main(SampleClass.java:10)
getLogger(String name) creates a logger with the specified name. If the logger with the specified name is already created, this method gets that logger instead of creating a new one.

Loggers are normally named, using a hierarchical dot-separated namespace. Logger names can be arbitrary strings, but they should normally be based on the package name or class name of the logged component, such as java.net or javax.swing.

log method logs a message. When the message is logged, it will be passed to logger's handler and the handler. What is a handler? handlers process the logged messages and output the messages to a console, file, etc. We will learn handlers later. In the example above, we didn't specify any handler to our logger. Thus, our logger uses the default handler which outputs messages to a console.

In the example above, log(Level level, String msg, Throwable thrown) method logged the date and time where the logging happened, the source class or the class where the log happened, the method name where the log happened, level which we will learn later, the message and the exception that we put in the log method.

log method has other variants: logp and logrb methods. logp or "log precise" method is just like log method but the source class and source method needs to be specified explicitly.

logrb method is just like logp method but this method accepts a ResourceBundle. Resource bundles contain locale-specific objects.

Some methods use MessageFormat style to format parameters in the params or param argument. For example, let's invoke this method: log(Level level, String msg, Object[] params)

logger.log(Level.SEVERE, "Causes: {0}, {1}, {2}", new String[]{"Divisor is 0!", "Illegal Division", "Undefined"});

If you put this code in the example above, the result would be:
May 17, 2022 4:40:44 PM SampleClass main
SEVERE: Causes: Divisor is 0!, Illegal Division, Undefined


If we just casually trying out some things and don't wanna create a logger, we can use the global logger. This logger is designed for testing purposes. To get the global logger, we can invoke Logger.getGlobal() or use GLOBAL_LOGGER_NAME field. For example:
//via method
Logger logger = Logger.getGlobal();

//via constant
Logger logger = 
logger.getLogger(Logger.GLOBAL_LOGGER_NAME);

//via String literal
Logger logger = 
logger.getLogger("global");
"global" is the name the refers to the global logger. In production, it's recommended to create logger per top-level class and add the class names to the loggers' name.

Levels

The Level class defines a set of standard logging levels that can be used to control logging output. These are the levels in descending order:
  • SEVERE (highest value)
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST (lowest value)
To get logger's level, invoke getLevel method from Logger class. To set logger's level, invoke setLevel(Level newLevel) from Logger class. Additionally, two other constants, apart from the constants above, can be used in setLevel method. These are:
  • OFF
  • ALL
Level.OFF means that all logging levels from FINEST to SEVERE are not loggable. Level.ALL is the opposite of Level.OFF. When we set a level to be loggable by invoking setLevel, all levels on top of the level that has been set will be loggable. For example, if I invoke setLevel like this: setLevel(Level.INFO). Then WARNING and SEVERE are also going to be loggable.

Note that loggers and handlers have their own levels. The level of a logger determines if a log can be sent to the handlers. The level of a handler determines if the log that has been received will be exported to a console, file, etc.

If the return value of setLevel method is null, it means that the handler or the logger will use the level of its parent logger. If a level is loggable, the handler or log accepts logs with the specified level. Otherwise, the log will be ignored.

According to the documentation, FINEST, FINER and FINE are used to log messages regarding tracing messages such as debug point or some kind of marker in message form. These levels are pretty optional and can be hidden.

CONFIG is used to log messages regarding configurations, INFO is used to log informational messages, WARNING is used to log potential problem and SEVERE is used to log serious failure such as exceptions. These levels are pretty important and should be watched closely especially the SEVERE level.

Logger Hierarchy

When we create a logger, that logger wil be added to the LogManager namespace. Java registers all created loggers to this namespace. However, there's a logger that can't be registered in the namespace and that logger is an anonymous logger.

One of the disadvantages of a logger that is not registered in the namespace is that that logger won't receive some checks like security checks which are done in the namespace. If you intend to bypass some checks then you may need to use anonymous logger to achieve your specific goal. To create an anonymous logger, invoke getAnonymousLogger method from Logger class.

Now, let's talk about logger hierarchy. When we create a logger, the logger will be automatically become a childen of root logger. Root logger is a logger that is closest to the LogManager. We will learn about LogManager later. The default level of root logger is INFO if the "Default global logging level" attribute in the configuration file of LogManager is not specified.

Root logger was the reason we could print logs on the console. By default, a newly created logger doesn't have a handler. If a logger doesn't have a specified handler, it will pass the log to the handler of its parent logger. Take note that if the logger and its parent have handlers, the log will be passed to the handlers of the logger and its parent.

If we want a log to only be passed to our logger, invoke setUseParentHandlers method and set its argument to false. In this way, the log won't be passed to the parent of our logger.

To gain access to the root logger, invoke Logger.getLogger() with "" argument. For example:

Logger logger = Logger.getLogger("");

Root logger doesn't have a display name. That's why when we invoke getName method in root logger the result is blank. If we want to get the parent logger of a logger, invoke getParent method for Logger class. If the return value of the method is null, the logger doesn't have a parent logger or the logger is the root logger because root logger doesn't have a parent logger.

Take note that even anonymous logger is not part of the logging namespace, its parent is still the root logger. If we want to set a parent logger to another logger, invoke setParent method from Logger class. This example demonstrates getParent and setParent methods.
import java.util.logging.*;

public class SampleClass{
  static Logger logger = 
  Logger.getLogger(SampleClass.class.getName());
  
  public static void main(String[] args){
    
    System.out.println("Name: " + logger.getName());
    System.out.println("Parent: " + 
    logger.getParent());
    System.out.println();
    
    new SampleClass();
  }
  
  SampleClass(){
    new Inner();
  }
  
  class Inner{
    Logger logger =
    Logger.getLogger(Inner.class.getName());
    
    Inner(){
       System.out.println("Name: " + logger.getName());
       System.out.println("Parent: " + logger.getParent());
       System.out.println();
       
       System.out.println("Change Parent...");
       logger.setParent(SampleClass.logger);
       System.out.println("Parent: " + 
       logger.getParent().getName());
    }
  }
}

Result(may vary)
Name: SampleClass
Parent: java.util.logging.LogManager$RootLogger@194...

Name: SampleClass$Inner
Parent: java.util.logging.LogManager$RootLogger@194...

Change Parent...
Parent: SampleClass

If our class is in a package, the return value of getName method would be: package-name.class-name. Take note that the LogManager namespace manages the hierarchy of registered loggers.

Thus, the namespace will be updated if we change the hierarchy of the registered loggers. In the namespace, Loggers are organized into a naming hierarchy based on their dot separated names. Thus "a.b.c" is a child of "a.b", but "a.b1" and a.b2" are peers.

Configuring LogManager

LogManager is used to maintain a set of shared state about Loggers and log services. In every JVM, there is a single global LogManager object that maintains every logger and log service in our application.

We can get that object by invoking LogManager.getLogManager method. The LogManager object is created during class initialization and cannot subsequently be changed. One of the important functionalities of LogManager is that it loads a configuration file. During LogManager initialization, a configuration file sets up our logger framework based on the content of the file.

A configuration file can be in a class file or properties file. To explicitly assign a configuration file to our logger framework, we can type these commands on our terminal/cmd:
load class file
java -Djava.util.logging.config.class="class path"

load properties file
java -Djava.util.logging.config.file="file path"
If a configuration file is not explicity set. Java will use the default logging.properties file. In java 8 and below, this file is located in $JAVA_HOME/jre/lib/ path. In java 9 and above, this file is located in $JAVA_HOME/conf/. $JAVA_HOME or %JAVA_HOME% (Windows) is a system variable that refers to the path where our JDK directory is located.

Now, let's create our own configuration file. First off, let's start with file configuration. Put this text in your custom properties file:

handlers = java.util.logging.ConsoleHandler

#ConsoleHandler initial configuration
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

#initialize new logger
initLogger.level = CONFIG


I'll explain the content of the properties file later. For now, create a file and name it as "MyConfig.properties" and put the file in a directory that you like. Then compile this java code.
import java.util.logging.*;

public class LogTest{

  static Logger rootLogger =
  Logger.getLogger("");
  
  static Logger initLogger =
  Logger.getLogger("initLogger");
  
  static Logger globalLogger =
  Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
  
  public static void main(String[] args){
    System.out.println("Root: " + rootLogger.getLevel());
    System.out.println("User: " + initLogger.getLevel());
    System.out.println("Global: " + globalLogger.getLevel());
  }
}
After that, type this command:

java -Djava.util.logging.config.file=C:\test\logging-config-test\MyConfig.properties LogTest

The result would be:
Root: INFO
User: CONFIG
Global: null
Take note that this command is also valid:

java LogTest -Djava.util.logging.config.file=C:\test\logging-config-test\MyConfig.properties

However, our config file won't be loaded once our program runs. We need to load the config file first before running the program. Thus, order of commands matters.

Java will use the default logging.properties if our config file can't be found.java.util.logging.config.file system property doesn't look at classpath. That's why we use absolute path to refer to our custom config file.

Next, let's talk about the properties in a config file. First off, the "#" symbol means a comment. LogManager ignores characters that are after this symbol. "handlers" is a LogManager property that assigns a handler to the root logger. If you want to assign a handler to a specific logger apart from root logger, we use this property "<logger>.handlers".

For example, initLogger.handlers = java.util.logging.ConsoleHandler
Additionally, we can add multiple handlers in a logger. We separate the handlers using comma(,). For example:
handlers = java.util.logging.ConsoleHandler, java.util.logging.FileHandler
If you have a custom handler
handlers = MyPackage.MyHandler, java.util.logging.FileHandler

To initialize a new logger at startup, we just need to set one of the attributes of the new logger and java will create that logger for us. In the example above, I just write "initLogger.level = CONFIG" property and java creates a logger named "initLogger" and sets its level to CONFIG.

We can also set some properties of root and global logger in the config file. For example, if we want to change the level of the root logger, write this property: ".level = FINE". For global logger write this: "global.level = FINEST". Root and global loggers are automatically created by the LogManager.

In the discussion above, we now know that we can use "handlers" and "level" attributes to modify the handlers and levels of loggers. More information about properties of loggers can be found in the LogManager documentation.

"java.util.logging.ConsoleHandler" system property refers to the ConsoleHandler. We can initialize some attributes of ConsoleHandler in the config file such as level, filter, formatter and encoding. Take note that each instance of ConsoleHandler will copy the values of the properties assigned to it in the config file during their initialization.

If we have custom handler, we can initialize some of their attributes in the config file. For example:
//if your custom handler is in a package
MyPackage.MyHandler.level = FINE

//if your custom handler is not in a package
MyHandler.level = FINE
In this way, each instance of MyHandler will have a FINE level. If the specified attribute of your custom handler in the config file doesn't match any attribute in the implementation of your custom handler, the property that specified the attribute is invalid and will be ignored.

For example, your custom handler is a ConsoleHandler and you specify "MyHandler.maxLocks" property. In this case, "maxLocks" attribute is gonna be ignored. "maxLocks" is an attribute of FileHandler. For more information about properties of ConsoleHandler can be found in this documentation.

There are different types of handlers which we will learn in the next topic. We will also discuss formatters in another topic. Next, let's create a config file using class file. Write and compile this code:
import java.util.logging.*;

public class LoggerProperties{

  public LoggerProperties(){
    
    try{
      //use 
      //System.setProperty(String key, String value) method
      //to configure system property programatically
      //
      System.setProperty
      ("java.util.logging.ConsoleHandler.level","INFO");
    
      //initialize a logger
      Logger.getLogger("initLogger").
      setLevel(Level.CONFIG);
      
      //set global logger level
      Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).
      setLevel(Level.FINE);
    }
    catch(Exception e){
      e.printStackTrace();
    }
    
  }
}
After that, compile LogTest in the previous example and then type this command:

java -Djava.util.logging.config.class=LoggerProperties LogTest

And the result would be:
Root: INFO
User: CONFIG
Global: FINE
Use System.setProperty(String key, String value) to configure system properties. System properties are properties that are internally used by java. Unlike "java.util.logging.config.file" property, "java.util.logging.config.class" looks at classpath. That's why we can use a relative path relative to our program. Also, it's not required to include the .class file extension in the command.

There's another way to configure LogManager. We can use readConfiguration method in conjunction with "java.util.logging.config.file" property to directly load properties file in the classpath at startup. First off, compile this code:
import java.util.logging.*;
import java.io.InputStream;

public class LoggerProperties{

  public LoggerProperties(){
    
    //make variables final so they
    //can't be changed.
    //Thus, more security
    try{
      final LogManager lm = 
      LogManager.getLogManager();
      try(
      final InputStream is = 
      this.getClass().getResourceAsStream
      ("MyConfig.properties")){
        lm.readConfiguration(is);
      }
    }
    catch(Exception e){
      e.printStackTrace();
    }
    
  }
}
After that, compile LogTest in the previous example and then type this command:

java -Djava.util.logging.config.class=LoggerProperties LogTest

Assuming MyConfig.properties has this content:
global.level = FINEST

#initialize new logger
initLogger.level = CONFIG
The result would be:
Root: INFO
User: CONFIG
Global: FINEST
Custom Handler

There are different types of handlers. All handlers implement Handler class. Handler class has two direct subclasses: MemoryHandler and StreamHandler.

In this tutorial, we're gonna focus on StreamHandler. StreamHandler has three direct subclasses: ConsoleHandler, FileHandler and SocketHandler. In this tutorial, we're going to focus on ConsoleHandler and FileHandler

First off, let's create a custom ConsoleHandler. Write and compile this code:
import java.util.logging.*;
import java.io.IOException;

public class SampleClass{
  
  static Logger logger =
  Logger.getLogger("MyLogger");
  
  public static void main(String[] args){
    ConsoleHandler ch = 
    new CustomConsoleHandler();
    
    //add handler
    logger.addHandler(ch);
    
    //setLevel
    logger.setLevel(Level.FINE);
    ch.setLevel(Level.INFO);
    
    //this wil ensure that the log sent
    //to our logger won't be sent to 
    //its parent which is the root logger.
    //
    //If we don't do this, our console
    //will receive two identical messages.
    if(logger.getUseParentHandlers())
      logger.setUseParentHandlers(false);
    
    System.out.println("Logger Level: " + 
    logger.getLevel());
    System.out.println("Handler Level: " + 
    ch.getLevel());
    System.out.println();
    logger.log(Level.FINE, "This is fine!");
    
  }
}

class CustomConsoleHandler extends ConsoleHandler{

  @Override
  public void publish(LogRecord record){
    super.publish(logRecord(record));
  }
  
  private LogRecord logRecord(LogRecord log){
    if(log.getLevel() == Level.FINE){
      Level newLvl = Level.INFO;
      String message = 
      "\nOriginal Level: " + 
      log.getLevel() + "\n" +
      "New level: " + 
      newLvl + "\n" +
      "Message: " + "\n" +
      log.getMessage();
      
      
      LogRecord newLog = 
      new LogRecord(newLvl, message);
      return newLog;
    }
    else return log;
  }
}

Result
Logger Level: FINE
Handler Level: INFO

May 19, 2022 5:20:41 PM SampleClass main
INFO:
Original Level: FINE
New Level: INFO
Message:
This is fine!
We use addHandler method add handlers to our logger. Once our loggers creates a log, each handler in our logger will output the log. LogRecord contains information about a log that is logged by a logger. Once all information about the log is wrapped to a LogRecord, the LogRecord will be sent to the handlers.

Now, let's try a custom FileHandler. Write and compile this code:
import java.util.logging.*;
import java.io.IOException;

public class SampleClass{

  static Logger logger =
  Logger.getLogger("MyLogger");
  
  public static void main(String[] args)
                       throws IOException{
    FileHandler ch = 
    new CustomFileHandler();
    
    //add handler
    logger.addHandler(ch);
    
    //setLevel
    logger.setLevel(Level.FINE);
    ch.setLevel(Level.INFO);
    
    logger.log(Level.INFO, "Seems like trouble!");
    
  }
  
}

class CustomFileHandler extends FileHandler{

  CustomFileHandler() throws IOException{}
  
  @Override
  public void publish(LogRecord record){
    super.publish(logRecord(record));
  }
  
  private LogRecord logRecord(LogRecord log){
    if(log.getLevel() == Level.INFO){
      Level newLvl = Level.WARNING;
      String message =
      "\nOriginal Level: " +
      log.getLevel() + "\n" +
      "New level: " + 
      newLvl + "\n" +
      "Message: " + "\n" +
      log.getMessage();
      
      LogRecord newLog = 
      new LogRecord(newLvl, message);
      return newLog;
    }
    else return log;
  }
  
}

Result(On console)
May 19, 2022 7:37:57 PM SampleClass main
INFO: Seems like trouble!

Result(in file)
<?xml version="1.0" encoding="windows-1252" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
  <date>2022-05-19T11:37:57.256883500Z</date>
  <millis>1652960277256</millis>
  <nanos>883500</nanos>
  <sequence>1</sequence>
  <level>WARNING</level>
  <class>SampleClass</class>
  <method>main</method>
  <thread>1</thread>
  <message>
Original Level: INFO
New level: WARNING
Message: 
Seems like trouble!</message>
</record>
</log>
If you have been noticed, the date&time in the file and console are different. In my opinion, java.util.logging.SimpleTimeFormatter and java.util.logging.XMLFormatter use different timezones.

In the example above, I use the default logging.properties configuration file. Let's examine some properties of FileHandler used in the configuration file.

<handler-name>.pattern specifies a pattern for generating the output file name. The default value of this property is "%h/java%u.log". "%h" denotes "user.home" system property in linux. In windows, it denotes "C:\Users\%USERNAME%" system property.

"%u" denotes a unique number. This unique number is used to resolve conflicts between log files. This ensures that each generated log files during runtime is unique to one another.

<handler-name>.formatter specifies the name of a Formatter class to use. The default value of this property is "java.util.logging.XMLFormatter". That's why the result in the example above in file is in XML format. More information about FileHandler properties can be found in the documentation.

If you just want to instantiate a handler, just use their default constructors. For example:
//Instantiate default ConsoleHandler
ConsoleHandler ch = new ConsoleHandler();

//Instantiate default FileHandler
FileHandler fh = new FileHandler();

Custom Formatter

Java provided two formatters that are used by default. java.util.logging.SimpleFormatter system property is used by ConsoleHandler as its default formatter. java.util.logging.XMLFormatter system property is used by FileHandler as its default formatter. If those formatters are not enough, we can override java.util.logging.Formatter to create our custom formatter.

Write and compile this code:
import java.util.logging.*;
import java.util.Locale;
import java.time.format.*;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;

public class SampleClass{
  static Logger logger =
  Logger.getLogger("MyLogger");
  
  public static void main(String[] args){
    ConsoleHandler ch = new ConsoleHandler();
    
    //use set
    ch.setFormatter(new CustomFormatter());
    
    logger.addHandler(ch);
    logger.setUseParentHandlers(false);
    
    try{
      int num = 1/0;
    }
    catch(ArithmeticException e){
      logger.log(Level.SEVERE,
      "There's a problem here!", e);
    }
    
  }
}

class CustomFormatter extends Formatter{

  @Override
  public String format(LogRecord record){
    LocalDateTime ldt = 
    LocalDateTime.ofInstant(Instant.ofEpochMilli(
    record.getMillis()), ZoneId.of("Asia/Taipei"));
    
    String output = 
    "Date: " + 
    ldt.format(DateTimeFormatter.
    ofPattern("MM/dd/uu hh:mm:ss", 
               Locale.US))+ "\n" +
    "Thread ID: " + 
    record.getLongThreadID() + "\n" +
    "Source Class: " + 
    record.getSourceClassName() + "\n" +
    "Source Method: " + 
    record.getSourceMethodName() + "\n" +
    "Logger Name: " + 
    record.getLoggerName() + "\n" +
    "Log Level: " +
    record.getLevel() + "\n" +
    "Log Message: " +
    record.getMessage() + "\n";
    
    Throwable throwable = record.getThrown();
    if(throwable != null)
      output = output.concat
      ("Exception\n" + throwable);
    
    return output;
  }
}

Result
Date: 05/19/22 10:17:03
Thread ID: 1
Source Class: SampleClass
Source Method: main
Logger Name: MyLogger
Log Level: SEVERE
Log Message: There's a problem here!
Exception
java.lang.ArithmeticException: / by zero
Take note that if you change your formatter, you need to recreate some of the formatting of the default formatter if you need them. For example, log, logp and logrb methods with param1 or params parameters won't follow MessageFormat formatting if we use our custom formatter. To enable that feature to our formatter, we need to recreate it.

Custom Filter

By default, loggers and handlers don't have filters. However, we can create our custom filter that filters loggers and handlers. This example demonstrates adding filter to a logger.
import java.util.logging.*;

public class SampleClass{

  static Logger logger =
  Logger.getLogger("MyLogger");
  
  public static void main(String[] args){
    logger.setFilter(new MyFilter());
    
    logger.log(Level.INFO, "Good Line1");
    logger.log(Level.INFO, "Bad Line2");
    logger.log(Level.INFO, "Bad Line3");
    logger.log(Level.INFO, "Good Line4");
    logger.log(Level.INFO, "Bad Line5");
  }
}

class MyFilter implements Filter{
  
  @Override
  public boolean isLoggable(LogRecord record){
    return record.getMessage().startsWith("Good");
  }
}

Result
May 20, 2022 12:14:05 AM SampleClass main
INFO: Good Line1
May 20, 2022 12:14:05 AM SampleClass main
INFO: Good Line4
Take note that a filter can be applied to a logger, handler or both. If a filter is applied to a logger, logs are gonna be filtered before sending them to a handler. If a filter is applied to a handler, received logs are gonna be filtered before sending them to a console, file, etc.

Log Entry and Return of Methods

There are two convenient methods that we can use to log method entry and return. These methods are entering and exiting methods from Logger class. These methods have two overloaded forms. This example demonstrates their overloaded forms with two parameters.
import java.util.logging.*;

public class SampleClass{
  private static final String className = 
  SampleClass.class.getName();

  static Logger logger = 
  Logger.getLogger
  (className);
  
  public static void main(String[] args){
    ConsoleHandler ch = new ConsoleHandler();
    logger.setLevel(Level.FINER);
    ch.setLevel(Level.FINER);
    logger.addHandler(ch);
    
    ClassA ca = new ClassA();
    logger.entering(className, "main");
    ca.classAMeth();
    logger.exiting(className, "main");
  }
}

class ClassA{
  private final String className = 
  ClassA.class.getName();
  
  Logger logger = 
  Logger.getLogger(className);
  
  ClassA(){
    ConsoleHandler ch = new ConsoleHandler();
    logger.setLevel(Level.FINER);
    ch.setLevel(Level.FINER);
    logger.addHandler(ch);
  }
  
  void classAMeth(){
    logger.entering(className, "classAMeth");
    System.out.println("Class A Method!");
    logger.exiting(className, "classAMeth");
  }
}

Result
May 20, 2022 1:22:42 AM SampleClass main
FINER: ENTRY
May 20, 2022 1:22:42 AM SampleClass classAMeth
FINER: ENTRY
Class A Method!
May 20, 2022 1:22:42 AM SampleClass classAMeth
FINER: RETURN
May 20, 2022 1:22:42 AM SampleClass main
FINER: RETURN
In the example above, I logged the pre-entry and entry of execution in classAMeth; pre-return and return of execution from classAMeth and main. Take note that the logs of these two methods are loggable in FINER level. Thus, your logger and handler must be in FINER level or level lower than FINER.

Other forms of these two methods can be found in the documentation.