Monday, December 26, 2022

Test-Driven Development with Spring Boot Starter Test

In this example we utilise Spring Boot's utilities to test our application, particularly JUnit. The spring-boot-starter-test imports Spring Boot test modules as well as JUnit, AssertJ, Hamcrest, etc. So let's get to it.

Create the Project

Create your project using Spring Initializr. We'll be using Maven, Java 8, Spring Boot 2.7.7, and without dependencies. Before generating it, explore the POM, there should be spring-boot-starter-test in the test scpope. As for the project Metadata, you can put whatever you like for the group, artifact, name, etc. But this is what I've got:

Project Metadata

  • Group: com.blogspot.jpllosa
  • Artifact: tdd-junit
  • Name: TDD with JUnit
  • Description: Test-Driven Development with JUnit
  • Package name: com.blogspot.jpllosa

When you are ready, import the project to your IDE. I use Spring Tool Suite 4.

Test-Driven Development Coding Rythm

At the heart of TDD (Test-Driven Development) is unit testing. We rely on unit testing to prevent bugs. With unit tests, regression testing can be run many times a day. A comprehensive test prevents fixed bugs from coming back or finding side effect of changes.

In TDD, the rythm of coding is test a little... code a little... test a little... code a little...

So we follow these 5 steps:

  1. Write a test
  2. Watch it fail
  3. Write the code
  4. Run the tests
  5. Refactor

Write a Test

For this example we will create a greatest common factor finder between two numbers. Let's create a skeleton class called MathFun in /src/main/java. Now, let's follow the first step and write a test in /src/test/java, create MathFunTest. This signifies that MathFun is the class being tested. We're using JUnit 5 so annotate it with @SpringBootTest.

In order for us to write a test, we need to know what is a greatest common factor. For in depth details, google it. Here's a quick explanation. The greatest common factor (GCF) of a set of numbers is the largest factor that all the numbers share. The greatest common factor of 12 and 16 is 4. Factors of 12 are 1, 2, 3, 4, 6, and 12. Factors of 16 are 1, 2, 4, 8, and 16. Factors are numbers we multiply together to get another number. E.g. 2 X 6 = 12, so 2 and 6 are factors of 12. E.g. 4 x 4 = 16. The common factors are 1, 2, 4. Making 4 the GCF. Understood?

Okay, so our input will be 12 and 16 and the expected result is 4. Create a method testWithTwoPositiveNumbers and watch it fail. Right click + Run as JUnit test or you can go command line, "mvn test".

MathFun.java


package com.blogspot.jpllosa;

public class MathFun {

	public int getGCF(int x, int y) {
		return 0;
	}
}

MathFunTest.java


package com.blogspot.jpllosa;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class MathFunTest {
	
	@Test
	public void testWithTwoPositiveNumbers( ) {
		MathFun mf = new MathFun();
		
		assertEquals(4, mf.getGCF(12, 16));
	}

}

Failure Result


org.opentest4j.AssertionFailedError: expected: <4> but was: <0>

Write the Code

Now that we have seen it fail. Time to make it pass. This is a quick one. We will use Euclid's algorithm for finding the GCF. Change the implementation to return the result of (y == 0) ? x : getGCF(y, x % y). Then run the tests again. This time you should have a green for a pssing test as shwon below. 

TDD with JUnit

Great!

Refactor

We trust Euclid's algorithm so we don't need to refator the code. This example is not about finding the GCF anyway but an exercise on TDD. The next thing would be to think of another test case that we can exercise on our getGCF method. Let's add another test, testWithZero, to test with a zero input. Run the test again. You should have two passing tests.


@Test
public void testWithZero( ) {
	MathFun mf = new MathFun();
	
	assertEquals(6, mf.getGCF(0, 6));
}

Here's how it looks on the command line:


2022-12-26 07:45:24.125  INFO 15812 --- [           main] com.blogspot.jpllosa.MathFunTest         : Started MathFunTest in 0.98 seconds (JVM running for 2.322)
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.804 s - in com.blogspot.jpllosa.MathFunTest

Brilliant! Now you are getting the hang of the rythm of coding.

There you have it. A simple TDD example. The complete project can be cloned from GitHub.

Tuesday, November 22, 2022

Behaviour-Driven Development Example

Behaviour-Driven Development (BDD) is a way of closing the gap between business (non-technical) people and technical people. Whereas Test-Driven Development (TDD) is testing at a component level, BDD is testing the behaviour at application level. In TDD, the test asserts the result of a specific method, while the BDD test is only concerned about the result of a high level scenario. Both are similar in way that the developer writes the test before writing the code.

Another way of saying this is business people will understand the BDD tests more easily than the unit tests. You'll understand this line better after this example code.

Assumptions

  • At least Java 8 installed
  • At least Maven 3.3.1 installed
  • Nice to have an IDE (e.g. IntelliJ)

Gherkin

In this example, we will be using the Cucumber JVM tool to do BDD. First off, we need to write a scenario in Gherkin. Gherkin is a set of rules that makes structured plain text for Cucumber to understand.


Scenario: Doxycycline order - HIV
	Given A customer orders Doxycycline
	And Suffers the condition HIV
	Then Reply should be Cannot prescribe

Scenario: Doxycycline order - None
	Given A customer orders Doxycycline
	And Suffers the condition, None
	Then Reply should be Can prescribe

As you can see, the above is clearly understandable to non-technical people. If you work in the medical software field, the above scenario tests are clearly understandable to clinicians checking if the software is working as it should.

Each scenario is a list of steps for Cucumber to work through. Cucumber verifies that the software conforms with the specification and generates a report indicating success or failure for each scenario. Apart from automating the testing, we have also documented how the system actually behaves. This is saved as a .feature text file.

Step Definition

We need to wire up the Gherkin steps to programming code. A step definition carries out the action that should be performed by the step. We'll use the cucumber-archetype Maven plugin to create a new project. Run the following command:


mvn archetype:generate                        \
   "-DarchetypeGroupId=io.cucumber"           \
   "-DarchetypeArtifactId=cucumber-archetype" \
   "-DarchetypeVersion=7.5.0"                 \
   "-DgroupId=bddexample"                     \
   "-DartifactId=bddexample"                  \
   "-Dpackage=bddexample"                     \
   "-Dversion=1.0.0-SNAPSHOT"                 \
   "-DinteractiveMode=false"

You should see something like below:


[INFO] Project created from Archetype in dir: C:\workspace\bddexample
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

Change into the directory you created and open it in your IDE. Check if all is on order by running mvn test. You should see something like below:


[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.172 s - in bddexample.RunCucumberTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

Plus, you should have some kick-off code ready:

  • RunCucumberTest.java
  • StepDefinitions.java
  • example.feature

Write the Scenarios

Open up example.feature and change the scenarios as mentioned above. You should have something like below:


Feature: Doxycycline orders

	Scenario: Has HIV
		Given A customer orders Doxycycline
		And Suffers the condition "HIV"
		Then Reply should be "Cannot prescribe"
	
	Scenario: Condition is None
		Given A customer orders Doxycycline
		And Suffers the condition, "None"
		Then Reply should be "Can prescribe"

As for the StepDefinitions.java, change it as per below. This wires the steps to methods.


public class StepDefinitions {

    @Given("A customer orders Doxycycline")
    public void orderDoxycycline() {
    }

    @And("Suffers the condition {string}")
    public void suffersCondition(String condition) {
    }

    @Then("Reply should be {string}")
    public void replyIs(String expectedAnswer) {
    }

}

Business Logic

The next thing to do is create the business logic. We'll just do a simple class that checks for the condition and tell us if the order should go through. Add the code below under src/main/java, bddexample package.


package bddexample;

public class DoxyOrder {

	private String condition;
	
	public void setCondition(String condition) {
		this.condition = condition;
	}
	
	public String getPrescribeMessage() {
		if (condition.equals("HIV")) {
			return "Cannot prescribe";
		}
		
		return "Can prescribe";
	}
}

Let's put some meat in the empty step definitions.


package bddexample;

import io.cucumber.java.en.*;

import static org.junit.jupiter.api.Assertions.*;

public class StepDefinitions {
	
	private DoxyOrder order;

    @Given("A customer orders Doxycycline")
    public void orderDoxycycline() {
    	order = new DoxyOrder();
    }

    @And("Suffers the condition {string}")
    public void suffersCondition(String condition) {
    	order.setCondition(condition);
    }

    @Then("Reply should be {string}")
    public void replyIs(String expectedAnswer) {
    	assertEquals(expectedAnswer, order.getPrescribeMessage());
    }

}

Run the tests again and you should see something like below. As you can see below, it shows how many tests were ran and the steps that were undertaken. Clearly, it is understandable to the non-technical people that this piece of software is behaving as it should just by looking at the test scenarios that were performed.


[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running bddexample.RunCucumberTest

Scenario: Has HIV                         # bddexample/example.feature:3
  Given A customer orders Doxycycline     # bddexample.StepDefinitions.orderDoxycycline()
  And Suffers the condition "HIV"         # bddexample.StepDefinitions.suffersCondition(java.lang.String)
  Then Reply should be "Cannot prescribe" # bddexample.StepDefinitions.replyIs(java.lang.String)

Scenario: Condition is None            # bddexample/example.feature:8
  Given A customer orders Doxycycline  # bddexample.StepDefinitions.orderDoxycycline()
  And Suffers the condition "None"     # bddexample.StepDefinitions.suffersCondition(java.lang.String)
  Then Reply should be "Can prescribe" # bddexample.StepDefinitions.replyIs(java.lang.String)

... snipped ...

[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.368 s - in bddexample.RunCucumberTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

There you have it. A simple BDD example. The complete project can be cloned from github.com/jpllosa/bddexample.

Tuesday, October 4, 2022

Starting Out on the go-chi Framework Example

Picture this. The company you work for has decided to move from Java to Golang to create microservices. What would you do? As for me, I went straight into creating an HTTP GET endpoint that returns a nice "hello world" message. As you'll see below, the code uses the go chi framework. Why the go-chi framework? I don't really know but maybe because it was recommended by the long time IT provider of the company. Based on the quickstart examples, the Gin Web framework seemed easier to understand.

First of all, I will not be talking about setup and installation of Go. I am assuming that the reader has sufficient knowledge in setting up Go. I'm on Windows 10 using Visual Studio Code along with it's Go extensions to edit my code. I'd recommend running go install github.com/go-delve/delve/cmd/dlv@latest on the command line so that you can do "Run and Debug" in VS Code.

Baby Steps

  1. Create a go-chi-hello directory and go in.
  2. Execute go mod init go-chi-hello on the command line. This will create a go.mod file in the current directory.
  3. Create main.go. Copy the contents from below.
  4. Execute go mod tidy on the command line. This will update go.mod, adding the dependencies.
  5. Execute go run main.go on the command line. Hit the endpoint using a browser, Postman or whatever you like.

main.go


package main

import (
	"net/http"

	"github.com/go-chi/chi/middleware"
	"github.com/go-chi/chi/v5"
)

func main() {
	r := chi.NewRouter()
	r.Use(middleware.Logger)
	r.Get("/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte("{ \"message\": \"Hello World!\" }"))
	})
	http.ListenAndServe(":3000", r)
}

Pretty simple program. An HTTP GET request triggers a response of "Hello World!" in JSON format. The server is listening on port 3000.

You should see something like below when a request comes if you have the logger set.


2022/10/04 15:27:43 "GET http://localhost:3000/ HTTP/1.1" from 127.0.0.1:56005 - 200 29B in 0s
2022/10/04 15:27:45 "GET http://localhost:3000/ HTTP/1.1" from 127.0.0.1:56005 - 200 29B in 0s

On Firefox developer, you should see something like below if the content type is set to JSON otherwise you'll see the other one.  

go-chi JSON Response


 
go-chi Plaintext Response

There you have it. The simplest endpoint to start with. Next step would be to hook this up with Accessing MySQL using Go Example.

Tuesday, July 5, 2022

Accessing MySQL using Go Example

In this example, I will be accessing a relational database (MySQL) using the Go programming language. I have followed the tutorial from the official Go website. Unfortunately, on my first try, I wasn't able to connect to my database. I'm writing this to supplement what was written on the Go website.

First of all, I will not be talking about setup and installation of MySQL and Go. I am assuming that the reader has sufficient knowledge in setting up MySQL and Go. I'm using Visual Studio Code along with it's Go extensions to edit my code.

MySQL setup

Create a database called recordings. In the recordings schema, create the table and values as seen below. Would be easier if you have MySQL Workbench but you can also go command line or however you like. Don't forget to use recordings.


DROP TABLE IF EXISTS album;
CREATE TABLE album (
  id         INT AUTO_INCREMENT NOT NULL,
  title      VARCHAR(128) NOT NULL,
  artist     VARCHAR(255) NOT NULL,
  price      DECIMAL(5,2) NOT NULL,
  PRIMARY KEY (`id`)
);

INSERT INTO album
  (title, artist, price)
VALUES
  ('Blue Train', 'John Coltrane', 56.99),
  ('Giant Steps', 'John Coltrane', 63.99),
  ('Jeru', 'Gerry Mulligan', 17.99),
  ('Sarah Vaughan', 'Sarah Vaughan', 34.98);

Next is to create your username and password. I have created "golang" as my username and password in MySQL.

main.go


package main

import (
	"database/sql"
	"fmt"
	"log"

	"github.com/go-sql-driver/mysql"
)

var db *sql.DB

func main() {
	// Capture connection properties.
	cfg := mysql.Config{
		User:   os.Getenv("DBUSER"),
		Passwd: os.Getenv("DBPASS"),
		Net:    "tcp",
		Addr:                 "127.0.0.1:3306",
		DBName:               "recordings",
		AllowNativePasswords: true, // if not included, mysql native password authentication error is generated
	}
	// Get a database handle.
	var err error
	db, err = sql.Open("mysql", cfg.FormatDSN())
	if err != nil {
		log.Fatal(err)
	}

	pingErr := db.Ping()
	if pingErr != nil {
		log.Fatal(pingErr)
	}
	fmt.Println("Connected!")
}

The above code is the same as the one in the Go Dev Tutorial. Except for one line, AllowNativePasswords: true. This line was missing in the Go Dev Tutorial and caused an error on my first go run .. This led me to some googling around. I found interesting stuff and I've collated them here. Hopefully it will help others who are starting to learn the Go languange. By the way, this should show Connected!.

Environment Variables

I'm on a Windows 10 machine so to set an environment variable on the command line, do set DBUSER=golang. If you don't want to use the command line because you are using Visual Studio Code. You'd want to run the code on the VSCode terminal, do $env:DBUSER = "golang". That's how to set an environment variable in powershell.

Addr

So I've tried this code using two MySQL databases. On MySQL 5.7.33, Addr: "127.0.0.1:3306" works fine. On MySQL 5.6.43, Addr: "localhost:3306" works fine. But if you swap the hostnames, you get and access denied for user error.

AllowNativePasswords

If you don't want to use AllowNativePasswords then you could replace cfg := mysql.Config() with cfg := mysql.NewConfig(). Something like below:


cfg := mysql.NewConfig()
cfg.User = os.Getenv("DBUSER")
cfg.Passwd = os.Getenv("DBPASS")
cfg.Net = "tcp"
cfg.Addr = "127.0.0.1:3306"
cfg.DBName = "recordings"

There you have it. Now you don't have to do too much googling around. The complete project can be cloned from github.com/jpllosa/go-relational-database.

Thursday, June 2, 2022

Java RSA Encryption Decryption Example

In this example, we will be encrypting a string using a public key and decrypting it using a private key. As title says, we will be using the RSA (Rivest-Shamir-Adleman) public key cryptosystem. Making our life easy will be the Bouncy Castle Crypto APIs.

Here we go, let's create a Maven project:

mvn archetype:generate -DgroupId=com.blogspot.jpllosa -DartifactId=ecnrypt-decrypt -DarchetypeArtifactId=maven-archetype-quickstart

This template will create a Maven quick start project. Edit the pom.xml as shown below.


<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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.blogspot.jpllosa</groupId>
  <artifactId>encrypt-decrypt</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>ecnrypt-decrypt</name>
  <url>http://maven.apache.org</url>
  
  <properties>
      <maven.compiler.source>1.8</maven.compiler.source>
      <maven.compiler.target>1.8</maven.compiler.target>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  
  <dependencies>
      <dependency>
          <groupId>org.bouncycastle</groupId>
        <artifactId>bcpkix-jdk15on</artifactId>
        <version>1.68</version>
    </dependency>
    
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.15</version>
    </dependency>
  
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <!-- <scope>test</scope> -->
    </dependency>
  </dependencies>
</project>

Next, let's create two RSA key pairs. One with no password and the other with a password. We can create the key pairs using openssl. Like so:


$ openssl genrsa -out my-private-key.pem 4096

Generating RSA private key, 4096 bit long modulus
........................................................++
....++
e is 65537 (0x10001)

$ openssl rsa -in my-private-key.pem -outform PEM -pubout -out my-public-key.pem
writing RSA key

$ openssl genrsa -out mypassword-private-key.pem 4096 -passout pass:mypassword
Generating RSA private key, 4096 bit long modulus
.........................................................................................................++
....................++
e is 65537 (0x10001)

$ openssl rsa -in mypassword-private-key.pem -outform PEM -pubout -out mypassword-public-key.pem
writing RSA key

We will use the PEM (Privacy Enhanced Mail) format as this is a de facto file format for storing and sending cryptographic keys. Our private key has a key size of 4096 bits. The first two commands is the RSA key pair without a password and the next two are with a password.

First and foremost, we ask help from the Bouncy Castle API to read our PEM files.

CryptographyKeyReader.java

package com.blogspot.jpllosa;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.Security;

import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.util.io.pem.PemObject;

public class CryptographyKeyReader {

	public CryptographyKeyReader() {
		Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
	}
	
	public byte[] readPublicKey(String filename) {
		try (PEMParser pemParser = new PEMParser(new InputStreamReader(new FileInputStream(filename)))) {
			PemObject keyContent = pemParser.readPemObject();
			return keyContent.getContent();
		} catch (NullPointerException | IOException e) {
			System.out.println("Cannot read public key PEM format from file.");
			return null;
		}
	}
	
	public byte[] readPrivateKey(String filename, String password) {
		try (PEMParser pemParser = new PEMParser(new InputStreamReader(new FileInputStream(filename)))) {
			Object keyPair = pemParser.readObject();
			PrivateKeyInfo keyInfo;
			if (keyPair instanceof PEMEncryptedKeyPair) {
				PEMDecryptorProvider decryptorProvider = new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
				PEMKeyPair decryptedKeyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptorProvider);
				keyInfo = decryptedKeyPair.getPrivateKeyInfo();
			} else {
				keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
			}
			
			return keyInfo.getEncoded();
		} catch (NullPointerException | IOException e) {
			System.out.println("Cannot read private key PEM format from file.");
			return null;
		}
	}
}

We use PEMParser to read the public and private keys. For an encrypted key pair (i.e. the one with a password) we have an additional step. We pass the password to get the decrypted key pair.

Now that we can retrieve the key pairs, let's encrypt and decrypt some stuff.

App.java

package com.blogspot.jpllosa;

import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import org.apache.commons.codec.binary.Base64;

import static org.junit.Assert.*;

public class App {

	private CryptographyKeyReader ckReader;
	private Cipher cipher;
	
	PublicKey publicKey;
    PrivateKey privateKey;
    
    KeyPair keyPair;

	public App() {
		ckReader = new CryptographyKeyReader();
		try {
        	cipher = Cipher.getInstance("RSA");
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
        	e.printStackTrace();
        }
		
		privateKey = getPrivateKey("mypassword-private-key.pem", "mypassword");        
		publicKey = getPublicKey("mypassword-public-key.pem");
        
        keyPair = new KeyPair(publicKey, privateKey);
	}
    
    public String encrypt(String data) {
    	try {
    		cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
    		return Base64.encodeBase64String(cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)));
    	} catch (BadPaddingException | IllegalBlockSizeException | InvalidKeyException e) {
    		System.out.println("Cannot encrypt data");
    		return "";
    	}
    }
    
    public String decrypt(String data) {
    	try {
    		cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
    		return new String(cipher.doFinal(Base64.decodeBase64(data)), StandardCharsets.UTF_8);
    	} catch (BadPaddingException | IllegalBlockSizeException | InvalidKeyException e) {
    		System.out.println("Cannot decrypt data");
    		return "";
    	}
    }
    
    private PublicKey getPublicKey(String filename) {
    	PublicKey key = null;
    	
    	try {
    		byte[] keyBytes = ckReader.readPublicKey(filename);
    		X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); // Represents ASN.1 encoding of a public key
    		KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    		key = keyFactory.generatePublic(spec);
    	} catch (NullPointerException npe) {
    		System.out.println("Cannot read public key from file.");
    	} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
    		System.out.println("Cannot create public key instance from file.");
    	}
    	
    	return key;
    }
    
    private PrivateKey getPrivateKey(String filename, String password) {
    	PrivateKey key = null;
    	
    	try {
    		byte[] keyBytes = ckReader.readPrivateKey(filename, password);
    		PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); // Represents ASN.1 encoding of a private key
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            key = keyFactory.generatePrivate(spec);
    	} catch (NullPointerException fileE) {
    		System.out.println("Cannot read private key from file.");
        } catch (NoSuchAlgorithmException | InvalidKeySpecException cryptoE) {
        	System.out.println("Cannot create private key instance from file.");
        }
        return key;
    }
    
    public static void main(String[] args) {
        System.out.println( "Encrypt/Decrypt with Public/Private keys." );

        App app = new App();
        
        String encryptedString = app.encrypt("once more unto the breach...");
        System.out.println("Encrypted: " + encryptedString);
        
        String decryptedString = app.decrypt(encryptedString);
        System.out.println("Decrypted: " + decryptedString);
        assertEquals("once more unto the breach...", decryptedString);
    }
}

On initialization, we get the key pairs and set the cipher instance to RSA. To encrypt, we initialize the cipher to encrypt mode and decrypt mode if we want to decrypt.

I use Spring Tool Suite to run the application (right click + Run As > Java Application). Your output should look something like this.


Encrypt/Decrypt with Public/Private keys.
Encrypted: fVmZilpad+Fdb2tw2fRmsRWbwpStk7zP64UGpFGZuopiY1Mi593O8NpsSm4Nd9cYun4m4jMfTY7VFE8MDHwimAPrIUAH2IaeKvR3qAGWSmalQp9v7e38g0QwCDzGR4Ql/+XBnCiBlEOW89djPDmXqEtT+OSpMi95cH6aQMdZ9tbd6e2ES0ioGM6MvciKcew5KPyzZdYJyMI1T5HmxpUjbRo0cjxE2yX8VUU9sVL9NZMWLZuIw6LqZfA7taRp5+i0dymYCrydJc0bkZRJkGMxdy06XNPhHSplaHQqJDat0YpzU2IAxcajgJ8Nxw/zklqK0jzqraTOKJydH2ghwWSzmoukIlojPQnDBX6yiyBtLoIjlRgB++WSYXOp/8bilypfb3OncHonSKft+CyF9fn0e+96HMKxew4DnG0pBLu4KYFlDVe7q0G9OxvhOn7TZcHlU+8G5izhk/xRD6vEVOe9Kedzz8LR00hCcK50dWa4x1SwPVquhlhZJdSw33R+9UTBshnmTWVNo1PXRVVlj0EUq5mqLyg3AGAeJS7nzaNWhXMVtKkPOkLkLyy9d8lAX9kxlNXvzyYxhPi6fySXBsJ+uFhxRaJBkspUuv4rcg63qRO4yKEm++0XjNQV1mseSCJZKdShneqpDnCbKd470i0PQuK+/oFFa1wEdIPj7Gw1KM4=
Decrypted: once more unto the breach...

There you have it. Hope this helps in your encryption needs. Full source code is found in GitHub.