November 24, 2020

How to Best Map Object Inheritance in JPA/Hibernate

Introduction

Object inheritance can be mapped in 3 ways in JPA/Hibernate.

  • SINGLE_TABLE
  • TABLE_PER_CLASS
  • JOINED

SINGLE_TABLE - @Inheritance(strategy = InheritanceType.SINGLE_TABLE)

All Inheritance Java Classes are stored in one big database table.

Default @Inheritance(strategy = InheritanceType.SINGLE_TABLE)

pros

  • Faster query, since no joining of tables

cons

  • Data Integrity. Cannot uphold NOT NULL constraints, etc, but can be enforced via a CHECK of TRIGGER. Bean Validation can improve, but does not help if either integrate directly with the database.
  • Works when smaller set of different inhereted class, but single table grows

TABLE_PER_CLASS - @MappedSuperclass

https://vladmihalcea.com/how-to-inherit-properties-from-a-base-class-entity-using-mappedsuperclass-with-jpa-and-hibernate/

pros

  • Can enforce NOT NULL constraints, etc

cons

  • You rarely need to joind inhereted java class, so no real cons

JOINED - @Inheritance(strategy = InheritanceType.JOINED)

https://vladmihalcea.com/the-best-way-to-use-entity-inheritance-with-jpa-and-hibernate/

Do no use, no efficient SQL.

Summary

Use TABLE_PER_CLASS with @MappedSuperclass.

November 23, 2020

How to Create Tor Browser Desktop Shortcut in Fedora 31

You can download Tor Web Browser from https://www.torproject.org/download/.

Unzip to personal folder, e.g. ~/bin. You will find in unzipped Tor directory a .desktop file. To add that desktop file to GNOME meny, you copy that file to applications.

$ cp ~/bin/tor-browser-linux64-10.0.4_en-US/tor-browser_en-US/start-tor-browser.desktop ~/.local/share/applications/start-tor-browser-linux64-10.0.4_en-US.desktop
$ chmod +x ~/.local/share/applications/start-tor-browser-linux64-10.0.4_en-US.desktop

Reference: https://developer.gnome.org/integration-guide/stable/desktop-files.html.en

Bootstrap Admin Dashboard

Officiell Bootstrap Admin Dashboard

https://getbootstrap.com/docs/4.5/examples/dashboard/

Start Bootstrap Admin, SB Admin

Source code: https://github.com/startbootstrap/startbootstrap-sb-admin

License: The MIT License (MIT)

Live demo: https://startbootstrap.com/previews/sb-admin

Vendor behind SB Admin: https://startbootstrap.com/template/sb-admin

Bootstrap Admin Theme 3

Source Code: https://github.com/VinceG/Bootstrap-Admin-Theme-3

License: The MIT License (MIT)

Live demo: http://vinceg.github.io/Bootstrap-Admin-Theme-3/

Start Bootstrap Admin 2, SB Admin 2

Source code: https://github.com/startbootstrap/startbootstrap-sb-admin-2

License: The MIT License (MIT)

Live demo: https://startbootstrap.com/previews/sb-admin-2

Vendor behind SB Admin: https://startbootstrap.com/theme/sb-admin-2

November 21, 2020

JSON Binding (JSON-B) in Java EE 8

Instead of getting and adding single properties on JsonObjectBuilder or JsonObject, an entire Java Object could be serialized and deserialized.


        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

        <!-- JSON-B 1.1 Impl -->
        <dependency>
            <groupId>org.eclipse</groupId>
            <artifactId>yasson</artifactId>
            <version>1.0.5</version>
            <scope>test</scope>
        </dependency>
package se.magnuskkarlsson.example.javaee8_p6spy.control;

import java.util.Locale;

import javax.json.bind.JsonbBuilder;

import org.junit.Test;

import com.github.javafaker.Faker;

import se.magnuskkarlsson.example.javaee8_p6spy.entity.Address;
import se.magnuskkarlsson.example.javaee8_p6spy.entity.User;

public class JSONBTest {

    private final Faker faker = new Faker(new Locale("sv-SE")); // default Locale("en", "")

    @Test
    public void serialize() throws Exception {
        User user = new User();
        user.setName(faker.name().fullName());

        Address address1 = new Address();
        address1.setStreet(faker.address().streetAddress());
        user.addAddress(address1);

        Address address2 = new Address();
        address2.setStreet(faker.address().streetAddress());
        user.addAddress(address2);

        // https://javaee.github.io/jsonb-spec/
        String result = JsonbBuilder.create().toJson(user);

        System.out.println(result);
    }

}

And the POJO classes.

package se.magnuskkarlsson.example.javaee8_p6spy.entity;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Temporal(TemporalType.TIMESTAMP)
    @Column
    private Date created = new Date();

    @Column(columnDefinition = "BIT")
    private Boolean enabled = Boolean.TRUE;

    @NotBlank
    @Size(min = 2, max = 255)
    @Column(length = 255)
    private String name;

    // https://vladmihalcea.com/the-best-way-to-map-a-onetomany-association-with-jpa-and-hibernate/
    // Next best bidirectional @OneToMany.
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Address> addresses = new ArrayList<>();

    // ----------------------- Logic Methods -----------------------

    public void addAddress(Address address) {
        addresses.add(address);
        address.setUser(this);
    }

    public void removeAddress(Address address) {
        addresses.remove(address);
        address.setUser(null);
    }

    // ----------------------- Helper Methods -----------------------

    @Override
    public String toString() {
        return "User [id=" + id + ", created=" + created + ", enabled=" + enabled + ", name=" + name + ", addresses="
                + addresses + "]";
    }

    // ----------------------- Get and Set Methods -----------------------

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Date getCreated() {
        return created;
    }

    public void setCreated(Date created) {
        this.created = created;
    }

    public Boolean getEnabled() {
        return enabled;
    }

    public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Address> getAddresses() {
        return addresses;
    }

    public void setAddresses(List<Address> addresses) {
        this.addresses = addresses;
    }

}
package se.magnuskkarlsson.example.javaee8_p6spy.entity;

import javax.json.bind.annotation.JsonbTransient;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

@Entity
public class Address {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(min = 2, max = 255)
    @Column(length = 255)
    private String street;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    // https://javaee.github.io/tutorial/jsonb002.html
    @JsonbTransient
    private User user;

    // ----------------------- Logic Methods -----------------------

    // ----------------------- Helper Methods -----------------------

    @Override
    public String toString() {
        return "Address [id=" + id + ", street=" + street + "]";
    }

    // Note equals and hashCode are important to implement when working with detached objects.
    // https://vladmihalcea.com/the-best-way-to-implement-equals-hashcode-and-tostring-with-jpa-and-hibernate/
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Address)) {
            return false;
        }
        return id != null && id.equals(((Address) o).getId());
    }

    // Note equals and hashCode are important to implement when working with detached objects.
    @Override
    public int hashCode() {
        // Database-generated identifiers
        // https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/
        return 31;
    }

    // ----------------------- Get and Set Methods -----------------------

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

}

And when run

{"addresses":[{"street":"Hjördiss Väg 79"},{"street":"Granallén 27"}],"created":"2020-11-21T19:32:33.302Z[UTC]","enabled":true,"name":"Maria Änglund"}

JSON-P in JavaEE 7

JSON-P can be used to parse and stringify JSON data.

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

        <!-- JSON-P Impl -->
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>jakarta.json</artifactId>
            <version>1.1.6</version>
            <scope>test</scope>
        </dependency>
package se.magnuskkarlsson.example.javaee8_p6spy.control;

import java.io.StringReader;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.json.Json;
import javax.json.JsonObject;

import org.junit.Test;

public class JSONPTest {

    public static final String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";

    @Test
    public void stringify() throws Exception {
        // https://javaee.github.io/jsonp/
        JsonObject json = Json.createObjectBuilder() //
                .add("name", "Falco") //
                // "JSON does not have a built-in type for date/time values. The general consensus is to store the
                // date/time value as a string in ISO 8601 format."
                // https://docs.jsonata.org/date-time#json-and-iso-8601
                .add("created", new SimpleDateFormat(DATETIME_FORMAT).format(new Date())) //
                .add("age", BigDecimal.valueOf(3)) //
                .add("emails", Json.createArrayBuilder() //
                        .add("foo@domain.com") //
                        .add("boo@domain.se")) //
                .add("biteable", Boolean.FALSE) //
                .build();
        String result = json.toString();

        System.out.println(result);
    }

    @Test
    public void parse() throws Exception {
        // https://javaee.github.io/jsonp/getting-started.html
        String json = "{\"name\":\"John Doe\",\"created\":\"2001-07-04T12:08:56.235-0700\",\"age\":28,\"bitable\":true}";
        JsonObject obj = Json.createReader(new StringReader(json)).readObject();
        System.out.println(obj);
        System.out.println(obj.getString("name"));
        System.out.println(obj.getString("created"));
        System.out.println(obj.getInt("age"));
        System.out.println(obj.getBoolean("bitable"));
    }

}

How to Log Hibernate SQL Query , Parameter Values and Execution Time with P6Spy and JBoss EAP/Wild

You can configure Hibernate to log SQL query via

<property name="hibernate.show_sql" value="false" />

But that is not the best solution, because Hibernate:

  • Logs to Standard Output.
  • Does not log SQL Parameters (since Hibernate uses PreparedStatement, and the values are not available when created the PreparedStatement)
  • Does not out-of-the box logs the Execution TIme.

A better solution is to use P6Spy.

First we gona use P6Spy in automatic Integration Test and in-memory database H2 Cheet Sheat. So first add Maven test dependency.

        <!-- Hibernate 5 and EAP 7.3 Version -->
        <hibernate.version>5.3.14.Final</hibernate.version>
        <hibernate-validator.version>6.0.18.Final</hibernate-validator.version>

        <!-- H2 in-memory database -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.199</version>
            <scope>test</scope>
        </dependency>
        <!-- MySQL 8 Community Edition, CE -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.17</version>
            <scope>test</scope>
        </dependency>
        <!-- Hibernate 5 -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>${hibernate.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-envers</artifactId>
            <version>${hibernate.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>${hibernate-validator.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>jakarta.el</artifactId>
            <version>3.0.2</version>
            <scope>test</scope>
        </dependency>
        <!-- P6Spy -->
        <dependency>
            <groupId>p6spy</groupId>
            <artifactId>p6spy</artifactId>
            <version>3.9.1</version>
            <scope>test</scope>
        </dependency>

And now modify our src/test/resources/META-INF/persistence.xml. Note the updated hibernate.connection.driver_class and hibernate.connection.url.

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
    version="2.2">

    <persistence-unit name="IT" transaction-type="RESOURCE_LOCAL">
        <class>se.magnuskkarlsson.example.javaee8_p6spy.entity.User</class>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
            <!-- <property name="hibernate.connection.driver_class" value="org.h2.Driver" /> -->
            <!-- <property name="hibernate.connection.url" value="jdbc:h2:mem:" /> -->
            <property name="hibernate.connection.driver_class" value="com.p6spy.engine.spy.P6SpyDriver" />
            <property name="hibernate.connection.url" value="jdbc:p6spy:h2:mem:" />

            <property name="hibernate.hbm2ddl.auto" value="create-drop" />

            <property name="hibernate.show_sql" value="false" />
            <property name="hibernate.format_sql" value="false" />
            <property name="hibernate.generate_statistics" value="false" />
            <property name="hibernate.cache.infinispan.statistics" value="false" />
        </properties>
    </persistence-unit>

</persistence>

And in our IT class everything is the same. For rendering test data I use Generating Test Data - JavaFaker.

package se.magnuskkarlsson.example.javaee8_p6spy.control;

import java.util.Locale;

import javax.persistence.EntityManager;
import javax.persistence.Persistence;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import com.github.javafaker.Faker;

import se.magnuskkarlsson.example.javaee8_p6spy.entity.User;

public class UserIT {

    private static EntityManager em;

    private final Faker faker = new Faker(new Locale("sv-SE")); // default Locale("en", "")

    @BeforeClass
    public static void oneTimeSetUp() throws Exception {
        em = Persistence.createEntityManagerFactory("IT").createEntityManager();
    }

    @Before
    public void setUp() throws Exception {
        em.getTransaction().begin();
    }

    @After
    public void tearDown() {
        em.getTransaction().commit();
    }

    @AfterClass
    public static void oneTimeTearDown() throws Exception {
    }

    // ----------------------- Test Methods -----------------------

    @Test
    public void create() throws Exception {
        User user = new User();
        user.setName(faker.name().fullName());

        em.persist(user);
        System.out.println("Persisted " + user.getId());
    }

}

And my @Entity class.

package se.magnuskkarlsson.example.javaee8_p6spy.entity;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Temporal(TemporalType.TIMESTAMP)
    @Column
    private Date created = new Date();

    @NotBlank
    @Size(min = 2, max = 255)
    @Column(length = 255)
    private String name;

    // ----------------------- Logic Methods -----------------------

    // ----------------------- Helper Methods -----------------------

    @Override
    public String toString() {
        return "User [id=" + id + ", created=" + created + ", name=" + name + "]";
    }

    // ----------------------- Get and Set Methods -----------------------

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Date getCreated() {
        return created;
    }

    public void setCreated(Date created) {
        this.created = created;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

And finally we need a P6Spy configuration file src/test/resources/spy.properties, which we copy from https://p6spy.readthedocs.io/en/latest/configandusage.html.

Then we need to uncomment one line to set logging to console appender=com.p6spy.engine.spy.appender.StdoutLogger.

#################################################################
# P6Spy Options File                                            #
# See documentation for detailed instructions                   #
# http://p6spy.github.io/p6spy/2.0/configandusage.html          #
#################################################################

#################################################################
# MODULES                                                       #
#                                                               #
# Module list adapts the modular functionality of P6Spy.        #
# Only modules listed are active.                               #
# (default is com.p6spy.engine.logging.P6LogFactory and         #
# com.p6spy.engine.spy.P6SpyFactory)                            #
# Please note that the core module (P6SpyFactory) can't be      #
# deactivated.                                                  #
# Unlike the other properties, activation of the changes on     #
# this one requires reload.                                     #
#################################################################
#modulelist=com.p6spy.engine.spy.P6SpyFactory,com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory

################################################################
# CORE (P6SPY) PROPERTIES                                      #
################################################################

# A comma separated list of JDBC drivers to load and register.
# (default is empty)
#
# Note: This is normally only needed when using P6Spy in an
# application server environment with a JNDI data source or when
# using a JDBC driver that does not implement the JDBC 4.0 API
# (specifically automatic registration).
#driverlist=

# for flushing per statement
# (default is false)
#autoflush=false

# sets the date format using Java's SimpleDateFormat routine.
# In case property is not set, milliseconds since 1.1.1970 (unix time) is used (default is empty)
#dateformat=

# prints a stack trace for every statement logged
#stacktrace=false
# if stacktrace=true, specifies the stack trace to print
#stacktraceclass=

# determines if property file should be reloaded
# Please note: reload means forgetting all the previously set
# settings (even those set during runtime - via JMX)
# and starting with the clean table
# (default is false)
#reloadproperties=false

# determines how often should be reloaded in seconds
# (default is 60)
#reloadpropertiesinterval=60

# specifies the appender to use for logging
# Please note: reload means forgetting all the previously set
# settings (even those set during runtime - via JMX)
# and starting with the clean table
# (only the properties read from the configuration file)
# (default is com.p6spy.engine.spy.appender.FileLogger)
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
appender=com.p6spy.engine.spy.appender.StdoutLogger
#appender=com.p6spy.engine.spy.appender.FileLogger

# name of logfile to use, note Windows users should make sure to use forward slashes in their pathname (e:/test/spy.log)
# (used for com.p6spy.engine.spy.appender.FileLogger only)
# (default is spy.log)
#logfile=spy.log

# append to the p6spy log file. if this is set to false the
# log file is truncated every time. (file logger only)
# (default is true)
#append=true

# class to use for formatting log messages (default is: com.p6spy.engine.spy.appender.SingleLineFormat)
#logMessageFormat=com.p6spy.engine.spy.appender.SingleLineFormat

# Custom log message format used ONLY IF logMessageFormat is set to com.p6spy.engine.spy.appender.CustomLineFormat
# default is %(currentTime)|%(executionTime)|%(category)|connection%(connectionId)|%(sqlSingleLine)
# Available placeholders are:
#   %(connectionId)            the id of the connection
#   %(currentTime)             the current time expressing in milliseconds
#   %(executionTime)           the time in milliseconds that the operation took to complete
#   %(category)                the category of the operation
#   %(effectiveSql)            the SQL statement as submitted to the driver
#   %(effectiveSqlSingleLine)  the SQL statement as submitted to the driver, with all new lines removed
#   %(sql)                     the SQL statement with all bind variables replaced with actual values
#   %(sqlSingleLine)           the SQL statement with all bind variables replaced with actual values, with all new lines removed
#customLogMessageFormat=%(currentTime)|%(executionTime)|%(category)|connection%(connectionId)|%(sqlSingleLine)

# format that is used for logging of the java.util.Date implementations (has to be compatible with java.text.SimpleDateFormat)
# (default is yyyy-MM-dd'T'HH:mm:ss.SSSZ)
#databaseDialectDateFormat=yyyy-MM-dd'T'HH:mm:ss.SSSZ

# format that is used for logging of the java.sql.Timestamp implementations (has to be compatible with java.text.SimpleDateFormat)
# (default is yyyy-MM-dd'T'HH:mm:ss.SSSZ)
#databaseDialectTimestampFormat=yyyy-MM-dd'T'HH:mm:ss.SSSZ

# format that is used for logging booleans, possible values: boolean, numeric
# (default is boolean)
#databaseDialectBooleanFormat=boolean

# Specifies the format for logging binary data. Not applicable if excludebinary is true.
# (default is com.p6spy.engine.logging.format.HexEncodedBinaryFormat)
#databaseDialectBinaryFormat=com.p6spy.engine.logging.format.PostgreSQLBinaryFormat
#databaseDialectBinaryFormat=com.p6spy.engine.logging.format.MySQLBinaryFormat
#databaseDialectBinaryFormat=com.p6spy.engine.logging.format.HexEncodedBinaryFormat

# whether to expose options via JMX or not
# (default is true)
#jmx=true

# if exposing options via jmx (see option: jmx), what should be the prefix used?
# jmx naming pattern constructed is: com.p6spy(.<jmxPrefix>)?:name=<optionsClassName>
# please note, if there is already such a name in use it would be unregistered first (the last registered wins)
# (default is none)
#jmxPrefix=

# if set to true, the execution time will be measured in nanoseconds as opposed to milliseconds
# (default is false)
#useNanoTime=false

#################################################################
# DataSource replacement                                        #
#                                                               #
# Replace the real DataSource class in your application server  #
# configuration with the name com.p6spy.engine.spy.P6DataSource #
# (that provides also connection pooling and xa support).       #
# then add the JNDI name and class name of the real             #
# DataSource here                                               #
#                                                               #
# Values set in this item cannot be reloaded using the          #
# reloadproperties variable. Once it is loaded, it remains      #
# in memory until the application is restarted.                 #
#                                                               #
#################################################################
#realdatasource=/RealMySqlDS
#realdatasourceclass=com.mysql.jdbc.jdbc2.optional.MysqlDataSource

#################################################################
# DataSource properties                                         #
#                                                               #
# If you are using the DataSource support to intercept calls    #
# to a DataSource that requires properties for proper setup,    #
# define those properties here. Use name value pairs, separate  #
# the name and value with a semicolon, and separate the         #
# pairs with commas.                                            #
#                                                               #
# The example shown here is for mysql                           #
#                                                               #
#################################################################
#realdatasourceproperties=port;3306,serverName;myhost,databaseName;jbossdb,foo;bar

#################################################################
# JNDI DataSource lookup                                        #
#                                                               #
# If you are using the DataSource support outside of an app     #
# server, you will probably need to define the JNDI Context     #
# environment.                                                  #
#                                                               #
# If the P6Spy code will be executing inside an app server then #
# do not use these properties, and the DataSource lookup will   #
# use the naming context defined by the app server.             #
#                                                               #
# The two standard elements of the naming environment are       #
# jndicontextfactory and jndicontextproviderurl. If you need    #
# additional elements, use the jndicontextcustom property.      #
# You can define multiple properties in jndicontextcustom,      #
# in name value pairs. Separate the name and value with a       #
# semicolon, and separate the pairs with commas.                #
#                                                               #
# The example shown here is for a standalone program running on #
# a machine that is also running JBoss, so the JNDI context     #
# is configured for JBoss (3.0.4).                              #
#                                                               #
# (by default all these are empty)                              #
#################################################################
#jndicontextfactory=org.jnp.interfaces.NamingContextFactory
#jndicontextproviderurl=localhost:1099
#jndicontextcustom=java.naming.factory.url.pkgs;org.jboss.naming:org.jnp.interfaces

#jndicontextfactory=com.ibm.websphere.naming.WsnInitialContextFactory
#jndicontextproviderurl=iiop://localhost:900

################################################################
# P6 LOGGING SPECIFIC PROPERTIES                               #
################################################################

# filter what is logged
# please note this is a precondition for usage of: include/exclude/sqlexpression
# (default is false)
#filter=false

# comma separated list of strings to include
# please note that special characters escaping (used in java) has to be done for the provided regular expression
# (default is empty)
#include=
# comma separated list of strings to exclude
# (default is empty)
#exclude=

# sql expression to evaluate if using regex
# please note that special characters escaping (used in java) has to be done for the provided regular expression
# (default is empty)
#sqlexpression=

#list of categories to exclude: error, info, batch, debug, statement,
#commit, rollback, result and resultset are valid values
# (default is info,debug,result,resultset,batch)
#excludecategories=info,debug,result,resultset,batch

#whether the binary values (passed to DB or retrieved ones) should be logged with placeholder: [binary] or not.
# (default is false)
#excludebinary=false

# Execution threshold applies to the standard logging of P6Spy.
# While the standard logging logs out every statement
# regardless of its execution time, this feature puts a time
# condition on that logging. Only statements that have taken
# longer than the time specified (in milliseconds) will be
# logged. This way it is possible to see only statements that
# have exceeded some high water mark.
# This time is reloadable.
#
# executionThreshold=integer time (milliseconds)
# (default is 0)
#executionThreshold=

################################################################
# P6 OUTAGE SPECIFIC PROPERTIES                                #
################################################################
# Outage Detection
#
# This feature detects long-running statements that may be indicative of
# a database outage problem. If this feature is turned on, it will log any
# statement that surpasses the configurable time boundary during its execution.
# When this feature is enabled, no other statements are logged except the long
# running statements. The interval property is the boundary time set in seconds.
# For example, if this is set to 2, then any statement requiring at least 2
# seconds will be logged. Note that the same statement will continue to be logged
# for as long as it executes. So if the interval is set to 2, and the query takes
# 11 seconds, it will be logged 5 times (at the 2, 4, 6, 8, 10 second intervals).
#
# outagedetection=true|false
# outagedetectioninterval=integer time (seconds)
#
# (default is false)
#outagedetection=false
# (default is 60)
#outagedetectioninterval=30

Now lets run the integration test and we can now see P6Spy logging on one line SQL query, and parameter values. But we also see execution time, which is the second field.

1605965528692|0|commit|connection 0|url jdbc:p6spy:h2:mem:||
1605965528694|0|statement|connection 0|url jdbc:p6spy:h2:mem:|drop table User if exists|drop table User if exists
...
1605965528697|0|commit|connection 0|url jdbc:p6spy:h2:mem:||
1605965528705|7|statement|connection 0|url jdbc:p6spy:h2:mem:|create table User (id bigint generated by default as identity, created timestamp, name varchar(255), primary key (id))|create table User (id bigint generated by default as identity, created timestamp, name varchar(255), primary key (id))
...
1605965529531|1|statement|connection 0|url jdbc:p6spy:h2:mem:|insert into User (id, created, name) values (null, ?, ?)|insert into User (id, created, name) values (null, '2020-11-21T14:32:09.480+0100', 'Karin Nilsson')
...
1605965529540|0|commit|connection 0|url jdbc:p6spy:h2:mem:||

Now lets build a simple JAX-RS app and use P6Spy and MySQL Community Edition in JBoss EAP 7.3. How To Install MySQL Community Edition (CE) 8.0 on Fedora 32/31/30/29

After installing MySQL 8 CE we need to configure MySQL DataSource in EAP 7. First create folder and file jboss-eap-7.3.0/modules/com/mysql/main/module.xml. Then copy MySQL CE 8 JDBC Driver from above Maven dependency mysql:mysql-connector-java:8.0.17 to this folder. [1]

<?xml version="1.0" ?>
<module xmlns="urn:jboss:module:1.1" name="com.mysql">
    <resources>
        <resource-root path="mysql-connector-java-8.0.17.jar" />
    </resources>
    <dependencies>
        <module name="javax.api" />
        <module name="javax.transaction.api" />
    </dependencies>
</module>

Then we also is going to need to add P6Spy as a EAP/Wildfly module. Create folder and file jboss-eap-7.3.0/modules/com/p6spy/main/module.xml and copy P6Spy from above maven dependency p6spy:p6spy:3.9.1. Note that we also add EAP module org.slf4j and com.mysql, since in production we do not want to log to standard out, but use a logging framework. We are going to use standard jboss logging, but we need the SLF4J logging API bridge.

<?xml version="1.0" ?>
<module xmlns="urn:jboss:module:1.1" name="com.p6spy">
    <resources>
        <resource-root path="p6spy-3.9.1.jar" />
    </resources>
    <dependencies>
        <module name="javax.api" />
        <module name="javax.transaction.api" />
        <module name="org.slf4j"/>
        <module name="com.mysql"/>
    </dependencies>
</module>

Then we going to run EAP in standalone mode, so lets edit standalone.xml. Note that we need to add MySQL and P6Spy driver and in datasoruce connection-url and driver.

        <subsystem xmlns="urn:jboss:domain:datasources:5.0">
            <datasources>
...
                <datasource jndi-name="java:jboss/datasources/MySQLDS" pool-name="MySQLDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
                    <connection-url>jdbc:p6spy:mysql://localhost:3306/p6spy?serverTimezone=UTC</connection-url>
                    <driver>p6spy</driver>
                    <security>
                        <user-name>root</user-name>
                        <password>root</password>
                    </security>
                    <validation>
                        <valid-connection-checker class-name="org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker"/>
                        <validate-on-match>true</validate-on-match>
                        <background-validation>false</background-validation>
                        <exception-sorter class-name="org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLExceptionSorter"/>
                    </validation>
                </datasource>
                <drivers>
...
                    <driver name="mysql" module="com.mysql">
                        <driver-class>com.mysql.cj.jdbc.Driver</driver-class>
                        <xa-datasource-class>com.mysql.cj.jdbc.MysqlXADataSource</xa-datasource-class>
                    </driver>
                    <driver name="p6spy" module="com.p6spy">
                        <driver-class>com.p6spy.engine.spy.P6SpyDriver</driver-class>
                    </driver>
                </drivers>
            </datasources>
        </subsystem>

The final thing to do is to copy jboss-eap-7.3.0/bin/spy.properties. And change

appender=com.p6spy.engine.spy.appender.Slf4JLogger
#appender=com.p6spy.engine.spy.appender.StdoutLogger
#appender=com.p6spy.engine.spy.appender.FileLogger

Now when we run this and invoke our simple JAX-RS service from curl.

$ curl -v -X POST -H "Content-Type: application/json" -H "Accept: application/json" --data '{"name":"MY_NAME"}' http://localhost:8080/javaee8-p6spy/api/users

We see in server.log

2020-11-19 02:45:06,446 INFO  [p6spy] (ServerService Thread Pool -- 82) 1605750306446|0|statement|connection 0|url jdbc:p6spy:mysql://localhost:3306/p6spy?serverTimezone=UTC|SELECT 1|SELECT 1
...
2020-11-19 02:45:06,497 INFO  [p6spy] (ServerService Thread Pool -- 82) 1605750306497|0|statement|connection 0|url jdbc:p6spy:mysql://localhost:3306/p6spy?serverTimezone=UTC|SELECT 1|SELECT 1
...
2020-11-19 02:45:06,867 INFO  [se.magnuskkarlsson.example.javaee8_p6spy.boundary.UserResource] (default task-1) Creating User [id=null, created=Thu Nov 19 02:45:06 CET 2020, name=MY_NAME] ...
2020-11-19 02:45:06,870 INFO  [p6spy] (default task-1) 1605750306870|0|statement|connection 0|url jdbc:p6spy:mysql://localhost:3306/p6spy?serverTimezone=UTC|SELECT 1|SELECT 1
2020-11-19 02:45:06,873 INFO  [p6spy] (default task-1) 1605750306873|0|statement|connection 0|url jdbc:p6spy:mysql://localhost:3306/p6spy?serverTimezone=UTC|insert into User (created, name) values (?, ?)|insert into User (created, name) values ('2020-11-19T02:45:06.816+0100', 'MY_NAME')
2020-11-19 02:45:06,874 INFO  [se.magnuskkarlsson.example.javaee8_p6spy.boundary.UserResource] (default task-1) Created User [id=5, created=Thu Nov 19 02:45:06 CET 2020, name=MY_NAME].
2020-11-19 02:45:06,895 INFO  [p6spy] (default task-1) 1605750306895|9|commit|connection 0|url jdbc:p6spy:mysql://localhost:3306/p6spy?serverTimezone=UTC||
...

Reference:

November 17, 2020

JPA 2.2 MySQL 8 Mapping Boolean, Date, Enum and CLOB

Here I will show how to map Boolean, java.util.Date, Boolean and enum in JPA 2.2 (Java EE 8) and test it against MySQL 8 CE and in-memory database H2.

package se.magnuskkarlsson.clearca.ca.entity;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(columnDefinition = "BIT")
    private Boolean enabled;

    @Temporal(TemporalType.TIMESTAMP)
    @Column
    private Date created;

    @Enumerated(EnumType.STRING)
    @Column
    private Status status;

    // https://dev.mysql.com/doc/refman/8.0/en/storage-requirements.html#data-types-storage-reqs-strings
    @Lob
    @Column(columnDefinition = "TEXT") // 2^16 = 65 536 = 65 KB
    private String data1;

    @Lob
    @Column(columnDefinition = "MEDIUMTEXT") // 2^24 = 16 777 216 = 16 MB
    private String data2;

    @Lob
    @Column(columnDefinition = "LONGTEXT") // 2^32 = 4 294 967 296 = 4 GB
    private String data3;

    // ----------------------- Logic Methods -----------------------

    // ----------------------- Helper Methods -----------------------

    public enum Status {

        ACTIVE, DISABLE;

    }

    // ----------------------- Get and Set Methods -----------------------

    public Long getId() {
        return id;
    }

    public boolean isEnabled() {
        return enabled;
    }

    public User setEnabled(boolean enabled) {
        this.enabled = enabled;
        return this;
    }

    public Date getCreated() {
        return created;
    }

    public User setCreated(Date created) {
        this.created = created;
        return this;
    }

    public Status getStatus() {
        return status;
    }

    public User setStatus(Status status) {
        this.status = status;
        return this;
    }

    public String getData1() {
        return data1;
    }

    public User setData1(String data1) {
        this.data1 = data1;
        return this;
    }

    public String getData2() {
        return data2;
    }

    public User setData2(String data2) {
        this.data2 = data2;
        return this;
    }

    public String getData3() {
        return data3;
    }

    public User setData3(String data3) {
        this.data3 = data3;
        return this;
    }

}

And src/test/resources/META-INF/persistence.xml with 2 persistence unit for integration testing.

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
    version="2.2">

    <persistence-unit name="IT" transaction-type="RESOURCE_LOCAL">
        <class>se.magnuskkarlsson.clearca.ca.entity.User</class>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
            <property name="hibernate.connection.driver_class" value="org.h2.Driver" />
            <property name="hibernate.connection.url" value="jdbc:h2:mem:" />
            <property name="hibernate.hbm2ddl.auto" value="create-drop" />

            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true" />
            <property name="hibernate.generate_statistics" value="false" />
            <property name="hibernate.cache.infinispan.statistics" value="false" />
        </properties>
    </persistence-unit>

    <persistence-unit name="IT-MySQL" transaction-type="RESOURCE_LOCAL">
        <class>se.magnuskkarlsson.clearca.ca.entity.User</class>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL57InnoDBDialect" />

            <!-- <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" /> -->
            <property name="hibernate.connection.driver_class" value="com.mysql.cj.jdbc.Driver" />
            <property name="hibernate.connection.url"
                value="jdbc:mysql://localhost:3306/clearca?serverTimezone=UTC" />
            <property name="hibernate.connection.username" value="root" />
            <property name="hibernate.connection.password" value="root" />
            <property name="hibernate.hbm2ddl.auto" value="create-drop" />

            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true" />
            <property name="hibernate.generate_statistics" value="false" />
            <property name="hibernate.cache.infinispan.statistics" value="false" />
        </properties>
    </persistence-unit>

</persistence>

And maven dependency for JUnit testing

        <hibernate.version>5.3.14.Final</hibernate.version>
        <hibernate-validator.version>6.0.18.Final</hibernate-validator.version>
...
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.199</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.17</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>${hibernate.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-envers</artifactId>
            <version>${hibernate.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>${hibernate-validator.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>jakarta.el</artifactId>
            <version>3.0.2</version>
            <scope>test</scope>
        </dependency>

And junit code for integration Test

package se.magnuskkarlsson.clearca.ca.entity;

import java.util.Date;
import java.util.Locale;

import javax.persistence.EntityManager;
import javax.persistence.Persistence;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import com.github.javafaker.Faker;

import se.magnuskkarlsson.clearca.Configuration;

public class UserMySQLIT {

    private static EntityManager em;

    private final Faker faker = new Faker(new Locale("sv-SE")); // default Locale("en", "")

    @BeforeClass
    public static void oneTimeSetUp() throws Exception {
        new Configuration().getCryptoProvider();
        em = Persistence.createEntityManagerFactory("IT-MySQL").createEntityManager();
    }

    @Before
    public void setUp() throws Exception {
        em.getTransaction().begin();
    }

    @After
    public void tearDown() {
        em.getTransaction().commit();
    }

    @AfterClass
    public static void oneTimeTearDown() throws Exception {
    }

    // ----------------------- Test Methods -----------------------

    @Test
    public void getCert() throws Exception {
        User user = new User() //
                .setEnabled(true) //
                .setCreated(new Date()) //
                .setData1(faker.lorem().fixedString(65 * 1000)) //
                .setData2(faker.lorem().fixedString(16 * 1000 * 1000)) //
                .setData3(faker.lorem().fixedString(4 * 1000 * 1000 * 1000));

        em.persist(user);

        System.out.println("Persisted: " + user.getId());
    }

}

And when run.

...
Nov 17, 2020 8:38:10 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001005: using driver [com.mysql.cj.jdbc.Driver] at URL [jdbc:mysql://localhost:3306/clearca?serverTimezone=UTC]
...
Nov 17, 2020 8:23:42 AM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MySQL57InnoDBDialect
...
    create table User (
       id bigint not null auto_increment,
        created datetime(6),
        data1 TEXT,
        data2 MEDIUMTEXT,
        data3 LONGTEXT,
        enabled BIT,
        status varchar(255),
        primary key (id)
    ) engine=InnoDB
    
==================================

Nov 17, 2020 8:20:46 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001005: using driver [org.h2.Driver] at URL [jdbc:h2:mem:]
...
Nov 17, 2020 8:20:46 AM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
...
    create table User (
       id bigint generated by default as identity,
        created timestamp,
        data1 TEXT,
        data2 MEDIUMTEXT,
        data3 LONGTEXT,
        enabled BIT,
        status varchar(255),
        primary key (id)
    )

November 16, 2020

How To Install MySQL Community Edition 8.0 on Fedora 32/31/30/29

Follow the guide https://computingforgeeks.com/how-to-install-mysql-8-on-fedora/

$ sudo dnf install mysql-community-server mysql-community-client

$ sudo systemctl start mysqld

$ sudo grep 'A temporary password' /var/log/mysqld.log |tail -1
2020-11-16T19:23:52.286885Z 6 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: sggCqq50Qh/Y

There is a gotcha when installing the MySQL Community Edition and that is root password is generated randomly. This is good for production, but for development it is more easy to have a simple password.

To set a trivial root password you first need to disable password policy.

$ mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 8.0.22
...

mysql> UNINSTALL COMPONENT 'file://component_validate_password';
Query OK, 0 rows affected (0.00 sec)

mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'root';
Query OK, 0 rows affected (0.02 sec)

mysql> flush privileges;
Query OK, 0 rows affected (0.01 sec)

mysql> exit
Bye

November 15, 2020

Fedora 31 don't Group Application when Alt + Tab

https://fedoramagazine.org/alt-tab-gnome-shell-extension/

How to Change to Normal Ctrl+Tab Switching Tabs in Firefox

Open Edit -> Preferences -> General | Tabs and uncheck "Ctrl+Tab cycles through tabs in recently used order".

How to Upgrade Fedora 30 to Fedora 31

Prerequisite

$ cat /etc/fedora-release 
Fedora release 30 (Thirty)

Update software and back up your system.

$ sudo dnf upgrade --refresh

Install the DNF plugin.

$ sudo dnf install dnf-plugin-system-upgrade

Start the update with DNF.

$ sudo dnf system-upgrade download --releasever=31

Reboot and upgrade.

$ sudo dnf system-upgrade reboot

March 2, 2020

Creating X509 Certificate with Bouncy Castle

Bouncy Castle has X509 Certificate builder but in my opinion it is still quite low tech.


package se.magnuskkarlsson.example.bouncycastle;

import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.util.Calendar;
import java.util.Date;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERTaggedObject;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AccessDescription;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.asn1.x509.CertificatePolicies;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.PolicyInformation;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x509.TBSCertificate;
import org.bouncycastle.asn1.x509.Time;
import org.bouncycastle.asn1.x509.V3TBSCertificateGenerator;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

// org.bouncycastle.cert.X509v3CertificateBuilder
public class X509CertificateBuilder {

    public static final ASN1ObjectIdentifier MSUPN_OID = new ASN1ObjectIdentifier("1.3.6.1.4.1.311.20.2.3");

    private V3TBSCertificateGenerator tbsGen;
    private ExtensionsGenerator extGenerator;

    public X509CertificateBuilder() {
        this.tbsGen = new V3TBSCertificateGenerator();
        this.extGenerator = new ExtensionsGenerator();
    }

    // ----------------------- Logic Methods -----------------------

    //
    // Mandatory Fields
    //

    public X509CertificateBuilder setSerialNumber(BigInteger serialNumber) {
        tbsGen.setSerialNumber(new ASN1Integer(serialNumber));
        return this;
    }

    public X509CertificateBuilder setIssuerDN(X500Name issuerDN) {
        this.tbsGen.setIssuer(issuerDN);
        return this;
    }

    public X509CertificateBuilder setValidity(int numberOfYears) {
        Date validityNotBefore = new Date(System.currentTimeMillis());
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(validityNotBefore);
        // calendar.add(Calendar.DAY_OF_YEAR, numberOfYears);
        calendar.add(Calendar.YEAR, numberOfYears);
        Date validityNotAfter = calendar.getTime();
        this.tbsGen.setStartDate(new Time(validityNotBefore));
        this.tbsGen.setEndDate(new Time(validityNotAfter));
        return this;
    }

    public X509CertificateBuilder setSubjectDN(X500Name subjectDN) {
        this.tbsGen.setSubject(subjectDN);
        return this;
    }

    public X509CertificateBuilder setSubjectPublicKeyInfo(PublicKey publicKey) {
        SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
        this.tbsGen.setSubjectPublicKeyInfo(subjectPublicKeyInfo);
        return this;
    }

    //
    // Certificate Extensions
    //

    public X509CertificateBuilder setBasicContraints(boolean ca, boolean critical) throws IOException {
        this.extGenerator.addExtension(Extension.basicConstraints, critical, new BasicConstraints(ca));
        return this;
    }

    public X509CertificateBuilder setAuthorityKeyIdentifier(PublicKey caPublicKey, boolean critical)
            throws NoSuchAlgorithmException, IOException {

        this.extGenerator.addExtension(Extension.authorityKeyIdentifier, critical,
                new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(caPublicKey));
        return this;
    }

    public X509CertificateBuilder setSubjectKeyIdentifier(PublicKey publicKey, boolean critical)
            throws NoSuchAlgorithmException, IOException {

        this.extGenerator.addExtension(Extension.subjectKeyIdentifier, critical,
                new JcaX509ExtensionUtils().createSubjectKeyIdentifier(publicKey));
        return this;
    }

    public X509CertificateBuilder setAuthorityInformationAccess(String ocspUri, String caIssuersUri, boolean critical)
            throws IOException {

        GeneralName ocspName = new GeneralName(GeneralName.uniformResourceIdentifier, ocspUri);
        GeneralName caIssuersName = new GeneralName(GeneralName.uniformResourceIdentifier, caIssuersUri);

        AccessDescription ocsp = new AccessDescription(AccessDescription.id_ad_ocsp, ocspName);
        AccessDescription caIssuers = new AccessDescription(AccessDescription.id_ad_caIssuers, caIssuersName);

        AuthorityInformationAccess authorityInformationAccess = new AuthorityInformationAccess(
                new AccessDescription[] { ocsp, caIssuers });
        this.extGenerator.addExtension(Extension.authorityInfoAccess, critical, authorityInformationAccess);
        return this;
    }

    public X509CertificateBuilder setCRLDistributionPoints(String cdpUri, boolean critical) throws IOException {
        GeneralNames generalNames = new GeneralNames(new GeneralName(GeneralName.uniformResourceIdentifier, cdpUri));
        DistributionPoint[] distPoints = new DistributionPoint[] {
                new DistributionPoint(new DistributionPointName(generalNames), null, null) };
        this.extGenerator.addExtension(Extension.cRLDistributionPoints, critical, new CRLDistPoint(distPoints));
        return this;
    }

    public X509CertificateBuilder setCertificatePolicies(String[] certificatePolicyIds, boolean critical)
            throws IOException {

        PolicyInformation[] policyInfos = new PolicyInformation[certificatePolicyIds.length];
        for (int i = 0; i < certificatePolicyIds.length; ++i) {
            policyInfos[i] = new PolicyInformation(new ASN1ObjectIdentifier(certificatePolicyIds[i]));
        }
        this.extGenerator.addExtension(Extension.certificatePolicies, critical, new CertificatePolicies(policyInfos));
        return this;
    }

    public X509CertificateBuilder setKeyUsage(KeyUsage keyUsage1, boolean critical) throws IOException {
        this.extGenerator.addExtension(Extension.keyUsage, critical,
                new org.bouncycastle.asn1.x509.KeyUsage(keyUsage1.getValue()));
        return this;
    }

    public X509CertificateBuilder setKeyUsage(KeyUsage keyUsage1, KeyUsage keyUsage2, boolean critical)
            throws IOException {

        this.extGenerator.addExtension(Extension.keyUsage, critical,
                new org.bouncycastle.asn1.x509.KeyUsage(keyUsage1.getValue() | keyUsage2.getValue()));
        return this;
    }

    public X509CertificateBuilder setKeyUsage(KeyUsage keyUsage1, KeyUsage keyUsage2, KeyUsage keyUsage3,
            boolean critical) throws IOException {

        this.extGenerator.addExtension(Extension.keyUsage, critical, new org.bouncycastle.asn1.x509.KeyUsage(
                keyUsage1.getValue() | keyUsage2.getValue() | keyUsage3.getValue()));
        return this;
    }

    public X509CertificateBuilder setExtendedKeyUsage(KeyPurposeId[] keyPurposeIds, boolean critical)
            throws IOException {

        this.extGenerator.addExtension(Extension.extendedKeyUsage, critical, new ExtendedKeyUsage(keyPurposeIds));
        return this;
    }

    public X509CertificateBuilder setSubjectAlternativeNamesDNS(String[] hostnames, boolean critical)
            throws IOException {

        GeneralName[] generalNames = new GeneralName[hostnames.length];
        for (int i = 0; i < hostnames.length; ++i) {
            generalNames[i] = new GeneralName(GeneralName.dNSName, hostnames[i]);
        }
        this.extGenerator.addExtension(Extension.subjectAlternativeName, critical, new GeneralNames(generalNames));
        return this;
    }

    public X509CertificateBuilder setSubjectAlternativeNamesUPN(String upn, boolean critical) throws IOException {
        this.extGenerator.addExtension(Extension.subjectAlternativeName, critical, new GeneralNames(new GeneralName(
                GeneralName.otherName,
                new DERSequence(new ASN1Encodable[] { MSUPN_OID, new DERTaggedObject(0, new DERUTF8String(upn)) }))));
        return this;
    }

    public java.security.cert.X509Certificate build(PrivateKey caPrivateKey, String signatureAlgorithm)
            throws OperatorCreationException, CertificateException, NoSuchAlgorithmException, IOException {

        ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithm)
                .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(caPrivateKey);

        this.tbsGen.setSignature(contentSigner.getAlgorithmIdentifier());
        this.tbsGen.setExtensions(this.extGenerator.generate());

        TBSCertificate tbsCert = tbsGen.generateTBSCertificate();
        X509CertificateHolder X509CertificateHolder = new X509CertificateHolder(generateStructure(tbsCert,
                contentSigner.getAlgorithmIdentifier(), generateSig(contentSigner, tbsCert)));

        return new JcaX509CertificateConverter().getCertificate(X509CertificateHolder);
    }

    // ----------------------- Helper Methods -----------------------

    // org.bouncycastle.asn1.x509.KeyUsage
    public enum KeyUsage {

        DIGITAL_SIGNATURE(org.bouncycastle.asn1.x509.KeyUsage.digitalSignature), //
        NON_REPUDIATION(org.bouncycastle.asn1.x509.KeyUsage.nonRepudiation), //
        KEY_ENCIPHERMENT(org.bouncycastle.asn1.x509.KeyUsage.keyEncipherment), //
        DATA_ENCIPHERMENT(org.bouncycastle.asn1.x509.KeyUsage.dataEncipherment), //
        KEY_AGREEMENT(org.bouncycastle.asn1.x509.KeyUsage.keyAgreement), //
        KEY_CERT_SIGN(org.bouncycastle.asn1.x509.KeyUsage.keyCertSign), //
        CRL_SIGN(org.bouncycastle.asn1.x509.KeyUsage.cRLSign);

        private final int value;

        KeyUsage(int value) {
            this.value = value;
        }

        public int getValue() {
            return value;
        }
    }

    protected byte[] generateSig(ContentSigner signer, ASN1Object tbsObj) throws IOException {
        OutputStream sOut = signer.getOutputStream();
        tbsObj.encodeTo(sOut, ASN1Encoding.DER);
        sOut.close();
        return signer.getSignature();
    }

    protected Certificate generateStructure(TBSCertificate tbsCert, AlgorithmIdentifier sigAlgId, byte[] signature) {
        ASN1EncodableVector v = new ASN1EncodableVector();
        v.add(tbsCert);
        v.add(sigAlgId);
        v.add(new DERBitString(signature));
        return Certificate.getInstance(new DERSequence(v));
    }

    // ----------------------- Get and Set Methods -----------------------

}

And certificate profile builder.


package se.magnuskkarlsson.example.bouncycastle;

import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.operator.OperatorCreationException;

import se.magnuskkarlsson.example.bouncycastle.X509CertificateBuilder.KeyUsage;

public class X509CertificateProfile {

    // ----------------------- Logic Methods -----------------------

    public X509Certificate createCA(KeyPair caKeyPair, X500Name subjectDN, String cdpUri)
            throws OperatorCreationException, CertificateException, NoSuchAlgorithmException, IOException {

        return new X509CertificateBuilder() //
                // mandatory fields
                .setSerialNumber(getSerialNumber()) //
                .setIssuerDN(subjectDN) //
                .setValidity(10) //
                .setSubjectDN(subjectDN) //
                .setSubjectPublicKeyInfo(caKeyPair.getPublic()) //
                // certificate extensions
                .setBasicContraints(true, true) //
                .setAuthorityKeyIdentifier(caKeyPair.getPublic(), false) //
                .setSubjectKeyIdentifier(caKeyPair.getPublic(), false) //
                .setCRLDistributionPoints(cdpUri, false) //
                .setKeyUsage(KeyUsage.DIGITAL_SIGNATURE, KeyUsage.KEY_CERT_SIGN, KeyUsage.CRL_SIGN, true) //
                .build(caKeyPair.getPrivate(), CAKeyStore.SHA256_RSA_SIGNATURE);
    }

    public X509Certificate createServer(KeyPair caKeyPair, KeyPair keyPair, X500Name issuerDN, X500Name subjectDN,
            String ocspUri, String caIssuersUri, String cdpUri, String[] hostnames)
            throws OperatorCreationException, CertificateException, NoSuchAlgorithmException, IOException {

        return new X509CertificateBuilder() //
                // mandatory fields
                .setSerialNumber(getSerialNumber()) //
                .setIssuerDN(issuerDN) //
                .setValidity(1) //
                .setSubjectDN(subjectDN) //
                .setSubjectPublicKeyInfo(keyPair.getPublic()) //
                // certificate extensions
                .setBasicContraints(false, true) //
                .setAuthorityKeyIdentifier(caKeyPair.getPublic(), false) //
                .setSubjectKeyIdentifier(keyPair.getPublic(), false) //
                .setAuthorityInformationAccess(ocspUri, caIssuersUri, false) //
                .setCRLDistributionPoints(cdpUri, false) //
                .setKeyUsage(KeyUsage.DIGITAL_SIGNATURE, KeyUsage.KEY_ENCIPHERMENT, false) //
                .setExtendedKeyUsage(new KeyPurposeId[] { KeyPurposeId.id_kp_serverAuth }, false)
                .setSubjectAlternativeNamesDNS(hostnames, false) //
                .build(caKeyPair.getPrivate(), CAKeyStore.SHA256_RSA_SIGNATURE);
    }

    public X509Certificate createClient(KeyPair caKeyPair, KeyPair keyPair, X500Name issuerDN, X500Name subjectDN,
            String ocspUri, String caIssuersUri, String cdpUri, String upn)
            throws OperatorCreationException, CertificateException, NoSuchAlgorithmException, IOException {

        return new X509CertificateBuilder() //
                // mandatory fields
                .setSerialNumber(getSerialNumber()) //
                .setIssuerDN(issuerDN) //
                .setValidity(1) //
                .setSubjectDN(subjectDN) //
                .setSubjectPublicKeyInfo(keyPair.getPublic()) //
                // certificate extensions
                .setBasicContraints(false, true) //
                .setAuthorityKeyIdentifier(caKeyPair.getPublic(), false) //
                .setSubjectKeyIdentifier(keyPair.getPublic(), false) //
                .setAuthorityInformationAccess(ocspUri, caIssuersUri, false) //
                .setCRLDistributionPoints(cdpUri, false) //
                .setKeyUsage(KeyUsage.DIGITAL_SIGNATURE, KeyUsage.NON_REPUDIATION, KeyUsage.KEY_ENCIPHERMENT, true) //
                .setExtendedKeyUsage(new KeyPurposeId[] { KeyPurposeId.id_kp_clientAuth }, false)
                .setSubjectAlternativeNamesUPN(upn, false) //
                .build(caKeyPair.getPrivate(), CAKeyStore.SHA256_RSA_SIGNATURE);
    }

    // ----------------------- Helper Methods -----------------------

    protected BigInteger getSerialNumber() {
        // RFC 5280 and X.690

        // Effective September 30, 2016, CAs SHALL generate non-sequential Certificate serial numbers greater than zero
        // (0) containing at least 64 bits of output from a CSPRNG.
        return BigInteger.valueOf(System.currentTimeMillis());
    }

    // ----------------------- Get and Set Methods -----------------------

}

And unit test.


package se.magnuskkarlsson.example.bouncycastle;

import java.io.File;
import java.security.KeyPair;
import java.security.Security;
import java.security.cert.X509Certificate;

import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class X509CertificateProfileTest {

    private static KeyPair caKeyPair;
    private static X500Name caSubjectDN;
    private String ocspUri = "http://ocsp.mkk.se/";
    private String caIssuersUri = "http://pki.mkk.se/ca.crt";
    private String cdpUri = "http://crl.mkk.se/ca.crl";

    @BeforeClass
    public static void oneTimeSetUp() throws Exception {
        Security.addProvider(new BouncyCastleProvider());
        caKeyPair = new CAKeyStore(new Configurations()).createKeyPair(CAKeyStore.RSA_ALGORITHM, 4096);
        caSubjectDN = new X500NameBuilder() //
                .addRDN(BCStyle.CN, "FOO CA") //
                .addRDN(BCStyle.O, "MKK") //
                .addRDN(BCStyle.C, "SE") //
                .build();
    }

    @Before
    public void setUp() throws Exception {
    }

    @After
    public void tearDown() {
    }

    @AfterClass
    public static void oneTimeTearDown() throws Exception {
    }

    // ----------------------- Test Methods -----------------------

    @Test
    public void createCA() throws Exception {
        X509Certificate caCert = new X509CertificateProfile().createCA(caKeyPair, caSubjectDN, cdpUri);

        System.out.println(caCert);
        EncryptionUtils.writePEM(caCert, File.createTempFile("caCert-", ".crt.pem"));
    }

    @Test
    public void createServer() throws Exception {
        KeyPair serverKeyPair = new CAKeyStore(new Configurations()).createKeyPair(CAKeyStore.RSA_ALGORITHM, 2048);
        X500Name serverSubjectDN = new X500NameBuilder() //
                .addRDN(BCStyle.CN, "FQDN") //
                .addRDN(BCStyle.OU, "mywebapp") //
                .addRDN(BCStyle.O, "MKK") //
                .addRDN(BCStyle.C, "SE") //
                .build();
        String[] hostnames = new String[] { "localhost", "127.0.0.1" };

        X509Certificate serverCert = new X509CertificateProfile().createServer(caKeyPair, serverKeyPair, caSubjectDN,
                serverSubjectDN, ocspUri, caIssuersUri, cdpUri, hostnames);

        System.out.println(serverCert);
        EncryptionUtils.writePEM(serverCert, File.createTempFile("serverCert-", ".crt.pem"));
    }

    @Test
    public void createClient() throws Exception {
        X509Certificate caCert = new X509CertificateProfile().createCA(caKeyPair, caSubjectDN, cdpUri);

        KeyPair clientKeyPair = new CAKeyStore(new Configurations()).createKeyPair(CAKeyStore.RSA_ALGORITHM, 2048);
        X500Name clientSubjectDN = new X500NameBuilder() //
                .addRDN(BCStyle.CN, "Magnus K Karlsson") //
                .addRDN(BCStyle.OU, "1000") //
                .addRDN(BCStyle.O, "MKK") //
                .addRDN(BCStyle.C, "SE") //
                .build();
        String upn = "1000@mkk.se";

        X509Certificate clientCert = new X509CertificateProfile().createClient(caKeyPair, clientKeyPair, caSubjectDN,
                clientSubjectDN, ocspUri, caIssuersUri, cdpUri, upn);

        System.out.println(clientCert);
        EncryptionUtils.writePEM(clientCert, File.createTempFile("clientCert-", ".cert.pem"));
        System.out.println(EncryptionUtils.getMSUPN(clientCert));
        EncryptionUtils.writeKeyStore("PKCS12", clientKeyPair.getPrivate(), "changeit",
                new X509Certificate[] { clientCert, caCert }, File.createTempFile("clientCert-", ".p12"));
    }

}

And when run.


[
[
  Version: V3
  Subject: C=SE, O=MKK, OU=1000, CN=Magnus K Karlsson
  Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11

  Key:  Sun RSA public key, 2048 bits
  params: null
  modulus: 25571568211886603124840374772893412632803008320276441343266420556001859527212495329059106511242070860135631150070936589994653782417968024014212169649126808218158560179639047989539457693799118359900429037050920421546627123472786524557783697159392478858239658746621633838155562268034256430543588459559201972100435495705680421356703692111342204230448132868538272476016901917159213800485636336183819543618065791989738031811366034365221624184676389882604646322575407826629234052470133086130458566129601941794090632783578168688778972919128563980379914647410944834699287955276441401709227130144873341655798718948150387040663
  public exponent: 65537
  Validity: [From: Mon Mar 02 20:35:28 CET 2020,
               To: Tue Mar 02 20:35:28 CET 2021]
  Issuer: C=SE, O=MKK, CN=FOO CA
  SerialNumber: [    01709cbf 0140]

Certificate Extensions: 8
[1]: ObjectId: 1.3.6.1.5.5.7.1.1 Criticality=false
AuthorityInfoAccess [
  [
   accessMethod: ocsp
   accessLocation: URIName: http://ocsp.mkk.se/
, 
   accessMethod: caIssuers
   accessLocation: URIName: http://pki.mkk.se/ca.crt
]
]

[2]: ObjectId: 2.5.29.35 Criticality=false
AuthorityKeyIdentifier [
KeyIdentifier [
0000: 13 5B F4 D2 4E 05 F0 73   9D D0 60 C4 42 A3 80 95  .[..N..s..`.B...
0010: 37 7B 9A 1B                                        7...
]
]

[3]: ObjectId: 2.5.29.19 Criticality=true
BasicConstraints:[
  CA:false
  PathLen: undefined
]

[4]: ObjectId: 2.5.29.31 Criticality=false
CRLDistributionPoints [
  [DistributionPoint:
     [URIName: http://crl.mkk.se/ca.crl]
]]

[5]: ObjectId: 2.5.29.37 Criticality=false
ExtendedKeyUsages [
  clientAuth
]

[6]: ObjectId: 2.5.29.15 Criticality=true
KeyUsage [
  DigitalSignature
  Non_repudiation
  Key_Encipherment
]

[7]: ObjectId: 2.5.29.17 Criticality=false
SubjectAlternativeName [
  Other-Name: Unrecognized ObjectIdentifier: 1.3.6.1.4.1.311.20.2.3
]

[8]: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 63 31 5B A3 22 8C C0 4B   DF 2D 7B 39 4C 43 D2 DB  c1[."..K.-.9LC..
0010: 11 96 37 46                                        ..7F
]
]

]
  Algorithm: [SHA256withRSA]
  Signature:
0000: 85 9B 22 22 44 4E E0 59   44 31 12 53 39 D5 A4 78  ..""DN.YD1.S9..x
0010: DA 39 80 BC 53 47 94 D1   B3 B6 E3 2B 8F D4 39 92  .9..SG.....+..9.
0020: 7B 63 CE FA 48 53 33 BF   B6 4B E7 83 90 47 AA 2D  .c..HS3..K...G.-
0030: 32 B6 02 9F A9 1B EE 27   02 7C 68 B2 9F 50 D4 D2  2......'..h..P..
0040: 55 2C A4 B6 1C 09 F1 BF   AE 0A A6 39 60 DE 93 CA  U,.........9`...
0050: 74 32 54 8F 74 E0 5B F8   0D 32 FE 6E 02 6B 88 9D  t2T.t.[..2.n.k..
0060: 2F D7 0C 9D A7 DE 20 5B   10 0A 08 61 1A 76 B0 35  /..... [...a.v.5
0070: 80 E3 AF C6 4C A9 06 E0   38 2F 9F D8 48 E8 BE DC  ....L...8/..H...
0080: 89 C0 DF 64 E2 F3 72 3C   BC 51 A8 D9 F2 1E 18 4A  ...d..r<.Q.....J
0090: 1B 52 1D E1 2E D7 EA 23   37 4C CA 4C A6 8A E6 85  .R.....#7L.L....
00A0: 60 7D 29 63 4F A0 90 37   29 EE CF 0E 0E 05 0E 85  `.)cO..7).......
00B0: 93 47 63 BC 7C CF CE 58   58 71 72 56 76 45 63 0A  .Gc....XXqrVvEc.
00C0: D6 33 BD EF 53 9A 8C F1   19 EE E7 AE 8E 8E 76 A0  .3..S.........v.
00D0: 37 7B BE 45 B5 C8 7B 72   FD 29 8B 80 0B 74 5E 24  7..E...r.)...t^$
00E0: 71 66 3D C9 39 A0 6B 24   B2 91 09 57 9C B7 2D 2E  qf=.9.k$...W..-.
00F0: 82 B8 D7 CA 7A 71 CF 05   2C 97 89 76 DC 3D 2A B8  ....zq..,..v.=*.
0100: 10 A0 8C 27 8D 49 2A 23   C9 8E FB D1 2B 55 00 6D  ...'.I*#....+U.m
0110: 76 CE 2D 86 8A E1 4D B4   1C 9E 8C 47 15 BC 7F DF  v.-...M....G....
0120: 20 23 D1 09 27 A3 DB 9B   7F AC D9 AE D7 72 D3 A3   #..'........r..
0130: E0 20 63 91 1A 49 A6 77   3E B2 EE F0 B1 57 8D 77  . c..I.w>....W.w
0140: 62 11 3C E2 17 28 6F CB   BE 44 14 A3 06 23 89 74  b.<..(o..D...#.t
0150: DE 05 75 DC CB D7 2A 86   3D 9A FA 29 94 00 82 09  ..u...*.=..)....
0160: 3D B1 DE A9 12 F1 48 30   F2 5C 52 10 0D 81 CA 5A  =.....H0.\R....Z
0170: 69 8A 9B F5 3B 7A F4 CB   8A D7 51 AE 5A CD A1 51  i...;z....Q.Z..Q
0180: FA F8 66 AF FC 02 68 2D   E9 CC D7 AF C1 FA 21 84  ..f...h-......!.
0190: 0C 1B 7A 0F DE B9 3D 9A   8A 1A 61 BC 46 01 3A FD  ..z...=...a.F.:.
01A0: 62 E3 79 35 07 66 64 A7   17 83 5E 82 E9 93 A2 FA  b.y5.fd...^.....
01B0: 6F D7 83 38 99 5F 44 69   6C AA F3 32 E4 B4 48 59  o..8._Dil..2..HY
01C0: 50 EC 64 14 72 51 9A E9   95 BC 4E CE EA 98 B3 BD  P.d.rQ....N.....
01D0: 7C F4 BD D2 60 D3 07 7C   71 26 24 55 94 E5 AA D5  ....`...q&$U....
01E0: 44 A1 8D AC 30 AE 47 CA   FF 96 FD CA 9E 03 25 EC  D...0.G.......%.
01F0: 55 5B 97 5B 27 24 A3 23   8F 0D CD 9A DC 52 29 BA  U[.['$.#.....R).

]
Mar 02, 2020 8:35:28 PM se.magnuskkarlsson.example.bouncycastle.EncryptionUtils writePEM
INFO: Wrote /tmp/clientCert-9146518211353882041.cert.pem
Mar 02, 2020 8:35:28 PM se.magnuskkarlsson.example.bouncycastle.EncryptionUtils getMSUPN
INFO: Read X509 SAN [[0, [1.3.6.1.4.1.311.20.2.3, [0]1000@mkk.se]]]
1000@mkk.se
Mar 02, 2020 8:35:28 PM se.magnuskkarlsson.example.bouncycastle.EncryptionUtils writeKeyStore
INFO: Wrote /tmp/clientCert-14620146327205577598.p12
[
[
  Version: V3
  Subject: C=SE, O=MKK, OU=mywebapp, CN=FQDN
  Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11

  Key:  Sun RSA public key, 2048 bits
  params: null
  modulus: 23472554064383360225540326318244541407904103528288156342671235877676704006249990542080185400202587647452428079635384512450814661982177331214162132359638484764547017758245274776079283522876250682767182225000730754889702275736425322221042334223219623764657811814147865593555449798942802445890347917633973214547390327111222760190458870434688746315828193389701570278446761804026480172159526376218012304574882822703261195947214666386714673839238432567588108434506291951836819612851494156727230958145708637864415669786989760083242288799613934351495483902217961576331883073677917645326766725734237025938297058790630425487583
  public exponent: 65537
  Validity: [From: Mon Mar 02 20:35:28 CET 2020,
               To: Tue Mar 02 20:35:28 CET 2021]
  Issuer: C=SE, O=MKK, CN=FOO CA
  SerialNumber: [    01709cbf 02ae]

Certificate Extensions: 8
[1]: ObjectId: 1.3.6.1.5.5.7.1.1 Criticality=false
AuthorityInfoAccess [
  [
   accessMethod: ocsp
   accessLocation: URIName: http://ocsp.mkk.se/
, 
   accessMethod: caIssuers
   accessLocation: URIName: http://pki.mkk.se/ca.crt
]
]

[2]: ObjectId: 2.5.29.35 Criticality=false
AuthorityKeyIdentifier [
KeyIdentifier [
0000: 13 5B F4 D2 4E 05 F0 73   9D D0 60 C4 42 A3 80 95  .[..N..s..`.B...
0010: 37 7B 9A 1B                                        7...
]
]

[3]: ObjectId: 2.5.29.19 Criticality=true
BasicConstraints:[
  CA:false
  PathLen: undefined
]

[4]: ObjectId: 2.5.29.31 Criticality=false
CRLDistributionPoints [
  [DistributionPoint:
     [URIName: http://crl.mkk.se/ca.crl]
]]

[5]: ObjectId: 2.5.29.37 Criticality=false
ExtendedKeyUsages [
  serverAuth
]

[6]: ObjectId: 2.5.29.15 Criticality=false
KeyUsage [
  DigitalSignature
  Key_Encipherment
]

[7]: ObjectId: 2.5.29.17 Criticality=false
SubjectAlternativeName [
  DNSName: localhost
  DNSName: 127.0.0.1
]

[8]: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: C4 54 8A E7 43 33 30 53   AD B5 7F 07 81 57 17 3F  .T..C30S.....W.?
0010: 74 B2 6A 97                                        t.j.
]
]

]
  Algorithm: [SHA256withRSA]
  Signature:
0000: 5D C5 A5 AC 37 CA F7 AE   A8 34 80 C7 17 DF 27 01  ]...7....4....'.
0010: 12 C1 CB 8A 8D 85 77 98   01 EB FD 44 21 63 1F 98  ......w....D!c..
0020: 51 69 67 E8 96 16 AA CF   9C 2A 6A 10 8B 3B F4 60  Qig......*j..;.`
0030: 8D 5C 66 46 6C E4 C4 A8   A0 60 A6 3E 43 35 40 FC  .\fFl....`.>C5@.
0040: 77 5C 9A DA 13 D8 F9 F1   63 35 FD 0F 4B 6C A4 11  w\......c5..Kl..
0050: 5D 67 CF DA 73 9B E0 A0   35 05 6E 37 7A D9 E2 AF  ]g..s...5.n7z...
0060: 10 80 A1 CA F5 5F D4 01   A7 6B 76 92 99 2F 74 25  ....._...kv../t%
0070: A6 27 D2 67 05 07 D9 E6   27 58 E6 90 D1 C0 2D B1  .'.g....'X....-.
0080: 7A 1B F9 5B 91 6E BE DD   AB A8 AA EB B4 BB AE 96  z..[.n..........
0090: 9E 26 A8 CB 31 CE 4B 02   BE EA D4 6A AC B5 E5 FA  .&..1.K....j....
00A0: 43 76 E4 8E C9 16 BF 55   34 E6 D0 A8 FA 1C 6D 48  Cv.....U4.....mH
00B0: C9 54 65 37 84 89 F6 13   C4 FD 0C 62 41 FF 57 5B  .Te7.......bA.W[
00C0: 88 28 35 AF 83 1C D1 A3   8C A4 1F 7F CD E0 F4 F9  .(5.............
00D0: DA 64 C2 C3 51 D7 C5 56   2C 3D 6C A3 C0 B1 40 1B  .d..Q..V,=l...@.
00E0: 2E 42 B5 6F FF 86 2F DB   F2 1F E6 E0 24 A1 CD 7C  .B.o../.....$...
00F0: A6 94 20 95 B8 95 74 05   0B 0E CE 08 43 2E CF FA  .. ...t.....C...
0100: 43 EE 30 3B 40 2C 3E 10   4F BD B2 BC 48 55 07 23  C.0;@,>.O...HU.#
0110: 2B 06 FB F0 E5 76 A2 15   1D E1 8A 33 E6 76 A2 30  +....v.....3.v.0
0120: 6C 07 4C 2A B4 57 C3 62   51 DE 58 1A 5D 82 11 84  l.L*.W.bQ.X.]...
0130: 3F AF EE 17 EC 8B C9 8F   EA F8 6F E4 C1 6A 1F C8  ?.........o..j..
0140: 41 2A 15 62 4C DC 99 37   49 9E 58 1D 95 E1 A6 2E  A*.bL..7I.X.....
0150: AC F4 D3 4F 7F 44 87 B8   18 CD 34 D0 C5 85 9B A6  ...O.D....4.....
0160: C1 46 CD 68 80 84 BE F0   1D F0 E2 B2 2F 0E D3 A6  .F.h......../...
0170: DB 68 89 FD 4A F8 A5 35   AA CB 6A CC FA 45 9E 20  .h..J..5..j..E. 
0180: 9D AA 1D 04 2F 14 8D D0   A4 2B 78 09 9A 23 DA 44  ..../....+x..#.D
0190: 65 76 54 CF D3 7B 7F 37   78 46 18 45 60 98 0D 63  evT....7xF.E`..c
01A0: 2A 7D F8 0D A6 0C 0B 1A   81 61 B8 2B 1F A3 92 E9  *........a.+....
01B0: F2 9C 5B 26 E9 81 50 6F   D9 79 8B 03 54 64 6C EB  ..[&..Po.y..Tdl.
01C0: AA 4A A1 B6 B3 6F 82 C8   20 9D D1 33 D5 D3 87 DA  .J...o.. ..3....
01D0: 5A 3E 93 2A 22 FA BE 0A   A8 1F B6 A4 5C AA E0 73  Z>.*".......\..s
01E0: 17 76 73 33 61 B0 7E ED   09 5E FD DB 4F B7 5A B4  .vs3a....^..O.Z.
01F0: 76 21 1B FA 43 F0 52 AE   78 DC 3B 09 06 66 45 B2  v!..C.R.x.;..fE.

]
Mar 02, 2020 8:35:28 PM se.magnuskkarlsson.example.bouncycastle.EncryptionUtils writePEM
INFO: Wrote /tmp/serverCert-12014046357869706442.crt.pem
[
[
  Version: V3
  Subject: C=SE, O=MKK, CN=FOO CA
  Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11

  Key:  Sun RSA public key, 4096 bits
  params: null
  modulus: 618097494562805236224635134817934912453172250162709722041733040687097334018651185958049454716082071938067246709420916424794899282956550425042006414596550620844500042869814998708415847406596390541333435551567170239109985394967160893875673187989640416587377196814478200867973805718141593244866119267189530684758333327813430426324393329942084968006256517216438373301377640102657162289647735616023682850661875444729876016663906609090034297679982852109216895370934293109110480164998867564217281544316832079056015576106175918287779836237538131308742105170670592053973122534872165219807886684564067083803402486128679568799840125501397600441124322949161232699891631680388114085515962034617420771247637630876469717153740862760325747621475058600558255078199328046206047780514609430807802825141363344880107809474319017977033492293123813011848757820259401796458615662171411629964165721128392643210117405003344910890867964280963490337780332547309122266515542815534797820516503892594199239179401978336582637760762233031975748737882775045900183794816021065220038962704464619646627020923143975929999810020514328195611426571022736233127780297192743641966846131645647190538362059522204380559481497214913394154203383211767929658852311603605904982991909
  public exponent: 65537
  Validity: [From: Mon Mar 02 20:35:28 CET 2020,
               To: Sat Mar 02 20:35:28 CET 2030]
  Issuer: C=SE, O=MKK, CN=FOO CA
  SerialNumber: [    01709cbf 02c1]

Certificate Extensions: 5
[1]: ObjectId: 2.5.29.35 Criticality=false
AuthorityKeyIdentifier [
KeyIdentifier [
0000: 13 5B F4 D2 4E 05 F0 73   9D D0 60 C4 42 A3 80 95  .[..N..s..`.B...
0010: 37 7B 9A 1B                                        7...
]
]

[2]: ObjectId: 2.5.29.19 Criticality=true
BasicConstraints:[
  CA:true
  PathLen:2147483647
]

[3]: ObjectId: 2.5.29.31 Criticality=false
CRLDistributionPoints [
  [DistributionPoint:
     [URIName: http://crl.mkk.se/ca.crl]
]]

[4]: ObjectId: 2.5.29.15 Criticality=true
KeyUsage [
  DigitalSignature
  Key_CertSign
  Crl_Sign
]

[5]: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 13 5B F4 D2 4E 05 F0 73   9D D0 60 C4 42 A3 80 95  .[..N..s..`.B...
0010: 37 7B 9A 1B                                        7...
]
]

]
  Algorithm: [SHA256withRSA]
  Signature:
0000: 6B 5A 8D 8E 93 F0 97 BC   6C 62 DD 1A 65 22 75 10  kZ......lb..e"u.
0010: D1 E3 95 AF BA B8 C1 93   45 C9 44 0A EB 36 93 A8  ........E.D..6..
0020: FD 3C 6F 62 28 FF 59 68   14 8E 7E 45 BB 8E C9 68  .<ob(.Yh...E...h
0030: 0C F3 ED 7F 6D E1 9B FC   D1 22 58 06 FB 6A EF ED  ....m...."X..j..
0040: DC 34 E9 BA CD 7C 69 A9   73 FB F7 C6 7C FD 7A 43  .4....i.s.....zC
0050: 86 68 8A B9 10 F7 36 BC   1E E5 98 99 C4 1A 9D 43  .h....6........C
0060: 31 00 3B 22 38 BB 7E 96   1C 32 E6 8C 67 F4 66 3A  1.;"8....2..g.f:
0070: 37 D9 58 24 18 74 30 40   9C 4B 0C 23 BE 11 4E B1  7.X$.t0@.K.#..N.
0080: 11 4F AD 1D F2 C0 94 1D   B3 B1 A5 CB FE C9 86 5B  .O.............[
0090: 39 80 C9 B1 86 5A 0F 76   C9 4A 54 04 CC 0D 3B E0  9....Z.v.JT...;.
00A0: 94 0F 51 86 8F D8 5E FE   81 21 20 B7 38 9E 91 C4  ..Q...^..! .8...
00B0: FE B3 78 67 80 BF 0E 95   4F C7 E8 48 59 DB CA 43  ..xg....O..HY..C
00C0: E7 69 48 52 C1 78 DA 89   E1 2E 98 63 5E 5A B5 E6  .iHR.x.....c^Z..
00D0: 7B C5 DB D6 B9 35 4E 8A   52 5E 6F 22 EA AD CE 8E  .....5N.R^o"....
00E0: 8C CF BF D3 41 0A 46 35   66 8A C1 E6 53 BE 1C 00  ....A.F5f...S...
00F0: EF 86 FE D7 C6 D9 62 EF   11 AC 44 49 6B 06 4E EC  ......b...DIk.N.
0100: 59 1F 5B 03 D1 CC 63 2F   05 49 1A B4 B0 66 01 55  Y.[...c/.I...f.U
0110: 93 FF 89 CB F1 DA 1E E5   3A 21 E1 81 A5 9D E1 A5  ........:!......
0120: A5 5F 3D AF CF 7C 88 B6   F9 03 EF C8 23 A5 04 3F  ._=.........#..?
0130: F6 CE EE 3A AB A0 A2 7F   0D 33 69 18 E3 A2 22 D8  ...:.....3i...".
0140: 7A 2E 09 1A B2 E5 7A 7C   EA 15 08 92 3D D5 D8 8A  z.....z.....=...
0150: 83 AE 6F CC 4D 30 73 CB   30 A1 0D D1 CC 47 B8 16  ..o.M0s.0....G..
0160: E3 2A 4B 06 32 79 99 4C   1E DD 92 5A 7D CD CB F3  .*K.2y.L...Z....
0170: 00 07 65 86 33 83 06 9C   F9 5C 45 2C F9 F2 E7 36  ..e.3....\E,...6
0180: BB 78 8C 47 32 B6 A6 8B   6F 11 91 73 CF 92 EF B7  .x.G2...o..s....
0190: 60 22 12 48 2F 01 1F 99   81 6C C0 0D 9B 6F 76 00  `".H/....l...ov.
01A0: D3 CD 1D F9 89 F8 F9 F0   83 7D 34 3E F2 6C 08 41  ..........4>.l.A
01B0: F9 EA 1B 8D BC CF 7A 41   7B A0 89 F4 55 8E 09 94  ......zA....U...
01C0: AC FC 50 4B 99 E6 BA 21   04 CF 33 BD B5 2F B7 B0  ..PK...!..3../..
01D0: 06 38 59 89 8C 04 E8 4D   C0 A4 D1 13 DD 47 25 4A  .8Y....M.....G%J
01E0: 02 CE AC CA BE 75 C3 4A   30 A7 9C 84 0A 7E 1D 59  .....u.J0......Y
01F0: BA 2A AA 6C 58 C7 0C 7B   BA 60 60 B3 77 E2 72 AE  .*.lX....``.w.r.

]
Mar 02, 2020 8:35:28 PM se.magnuskkarlsson.example.bouncycastle.EncryptionUtils writePEM
INFO: Wrote /tmp/caCert-12617038054227389688.crt.pem

February 27, 2020

X.509 Certificate Profiles

Mandatory Fields

Serial Number 
Issuer DN 
Validity 
Subject DN 
Subject Public Key InfoContains key algorithm, size and info.

Certificate Extensions

Basic ContraintsCriticalIf CA or not.
Authority Key Identifier Hash of CA public key.
Subject Key Identifier Hash of public key.
Authority Information Access Contains URL to OCSP and CA certificate.
CRL Distribution Points URL to CRL.
Certificate Policies Organization OID for their certificate policy..
Key UsageCriticalKey usage attribute derived from Extended Key Usage.
Extended Key Usage Typical values are from RFC 5280.
Subject Alternative Names Their are different SAN: DNS (for web servers), email (S-MIME) and UPN (Windows login).

RFC 5280

https://tools.ietf.org/html/rfc5280#section-4.2.1.3

Key Usage:

  • digitalSignature -
  • nonRepudiation -
  • keyEncipherment - "subject public key (e.g. RSA) is used for enciphering private or secret keys"
  • dataEncipherment - "NOTE that the use of this bit is extremely uncommon"
  • keyAgreement - "subject public key is used for key agreement (Diffie-Hellman key)"
  • keyCertSign - "If set then CA bit in the basic constraints extension MUST also be set"
  • cRLSign -

Extended Key Usage:

  • serverAuth - Key Usage may be: digitalSignature, keyEncipherment or keyAgreement
  • clientAuth - Key Usage may be: digitalSignature and/or keyAgreement
  • codeSigning - Key Usage may be: digitalSignature
  • emailProtection - Key Usage may be: digitalSignature, nonRepudiation, and/or (keyEncipherment or keyAgreement)
  • timeStamping - Key Usage may be: digitalSignature and/or nonRepudiation
  • OCSPSigning - Key Usage may be: digitalSignature and/or nonRepudiation

EJBCA CE ROOT CA


Basic Constraints - CRITICAL

CA:TRUE

Path Length Constraint: Unlimited

Authority Key ID

Subject Key ID

Key Usage - CRITICAL:
  digitalSignature
  keyCertSign
  cRLSign
  
Extended Key Usage:
    -

Dogtag caCert


https://github.com/dogtagpki/pki/blob/master/base/ca/shared/conf/caCert.profile

Basic Constraints - CRITICAL

CA:TRUE

Path Length Constraint: Unlimited

Authority Key ID

Subject Key ID

Key Usage - CRITICAL:
  digitalSignature
  nonRepudiation
  keyCertSign
  cRLSign
  
Extended Key Usage:
    -

EJBCA CE SERVER


Basic Constraints - CRITICAL

Authority Key ID

Subject Key ID

Key Usage - CRITICAL:
  digitalSignature
  keyEncipherment
  
Extended Key Usage:
    serverAuth

Dogtag rsaServerCert


https://github.com/dogtagpki/pki/blob/master/base/ca/shared/conf/rsaServerCert.profile

Authority Key ID

Key Usage - CRITICAL:
  digitalSignature
  dataEncipherment
  keyEncipherment

Extended Key Usage:
    serverAuth

EJBCA CE END USER


Basic Constraints - CRITICAL

Authority Key ID

Subject Key ID

Key Usage - Critical:
  digitalSignature
  nonRepudiation
  keyEncipherment
  
Extended Key Usage:
    clientAuth
    emailProtection

Dogtag rsaSubsystemCert


https://github.com/dogtagpki/pki/blob/master/base/ca/shared/conf/rsaSubsystemCert.profile

Authority Key ID

Key Usage - Critical:
  digitalSignature
  nonRepudiation
  dataEncipherment
  keyEncipherment
  
Extended Key Usage:
    clientAuth

EJBCA CE OCSP


Basic Constraints - CRITICAL

Authority Key ID

Subject Key ID

Key Usage - CRITICAL:
  digitalSignature
  
Extended Key Usage:
    OCSPSigning 

OCSP No Check

Dogtag caOCSPCert


https://github.com/dogtagpki/pki/blob/master/base/ca/shared/conf/caOCSPCert.profile

Basic Constraints - CRITICAL

Authority Key ID

Subject Key ID

Key Usage:
  -
  
Extended Key Usage:
    OCSPSigning 

com.netscape.cms.profile.def.OCSPNoCheckExtDefault

February 7, 2020

Tomcat RPM installation on RHEL and CentOS

Introduction

Using a RPM installation from a trusted RPM repository is a huge advantage, since then is life cycle with patching made easy. Otherwise you will have to manage all patching yourself, which in the long run, will always be more costly than using RPM installation from a trusted RPM repository. And you will also be more exposed by security weakness, because you have not a current patched software version.

RHEL 7 and CentoOS 7

Both RHEL 7 and CentOS 7 comes with a ready to use Tomcat 7. The only drawback is that the version is rather old.

But do you really need a newer version? The difference between Servlet 3.0 (part of Java EE 6) and Servlet 4.0 (part of Java EE 8) is quite small.

And if you are using websockets it is the same version of the spec between Tomcat 7 and 9. Other bigger differences is the support for HTTP/2, but do really need this? Maybe you do or you do not. And if you do not use these new feature you are probably fine with an older version of Tomcat.

From a security perspective, the difference between Tomcat 7 and 9 (as long they are patched), are minimal. Alla the security configuration in web.xml are in place in Tomcat 7. See (Mis)Configure web.xml in Java EE 6 . The only new feature in Servlet 4 and web.xml is

  1. default-context-path
  2. request-character-encoding
  3. response-character-encoding
  4. deny-uncovered-http-methods
  5. absolute-ordering (only relevant if you are using web-fragment.xml)

And the difference between security specific feature between Tomcat 7 and 9 are

  1. Tomcat 9 new supports OCSP, but only for Tomcat Native Connector. See https://tomcat.apache.org/tomcat-9.0-doc/ssl-howto.html#Using_OCSP_Certificates
  2. Both Tomcat 7 and 9 both rely on MBean technology for monitoring, which is a problem, since remote JMX connection does not support strong authentication, only username and password and default requires random ports. The random ports can be fixed with catalina-jmx-remote.jar, see Enable JMX Remote in Tomcat 7
  3. Pure Tomcat High Availability (HA) is the same, i.e. 1. replication is the same, but do you really need this, maybe you do or you do not. 2. Pure Tomcat load balancing is seldom used in production. If you are looking for a software solution I recommend using mod_cluster, which works very well. If that is not an option use mod_proxy. Otherwise most people use a hardware load balancer like BigIP F5.
  4. Tomcat 9 still have crazy logging handling. 1. logging to multiple files which make error search hard and general overview of the tomcat health hard. 2. The only project using custom logging handler JULI. Do Java really need another logging framework? Consider using a single log file and Log4J, see Use Log4J in Tomcat 7

Otherwise the good security filter are still the same, such as CORS Filter, CSRF Filter, Expires Filter, Failed Request Filter and HTTP Header Security Filter, see Tomcat 7 Container Provided Filters.

The nest lockout realm, see Tomcat 7 Lockout Realm.


# yum install tomcat

# systemctl restart tomcat; systemctl enable tomcat

# rpm -ql tomcat
/etc/logrotate.d/tomcat
/etc/sysconfig/tomcat
/etc/tomcat
/etc/tomcat/Catalina
/etc/tomcat/Catalina/localhost
/etc/tomcat/catalina.policy
/etc/tomcat/catalina.properties
/etc/tomcat/conf.d
/etc/tomcat/conf.d/README
/etc/tomcat/context.xml
/etc/tomcat/log4j.properties
/etc/tomcat/logging.properties
/etc/tomcat/server.xml
/etc/tomcat/tomcat-users.xml
/etc/tomcat/tomcat.conf
/etc/tomcat/web.xml
/usr/bin/tomcat-digest
/usr/bin/tomcat-tool-wrapper
/usr/lib/systemd/system/tomcat.service
/usr/lib/systemd/system/tomcat@.service
/usr/libexec/tomcat
/usr/libexec/tomcat/functions
/usr/libexec/tomcat/preamble
/usr/libexec/tomcat/server
/usr/sbin/tomcat
/usr/share/doc/tomcat-7.0.76
/usr/share/doc/tomcat-7.0.76/LICENSE
/usr/share/doc/tomcat-7.0.76/NOTICE
/usr/share/doc/tomcat-7.0.76/RELEASE-NOTES
/usr/share/tomcat
/usr/share/tomcat/bin/bootstrap.jar
/usr/share/tomcat/bin/catalina-tasks.xml
/usr/share/tomcat/conf
/usr/share/tomcat/lib
/usr/share/tomcat/logs
/usr/share/tomcat/temp
/usr/share/tomcat/webapps
/usr/share/tomcat/work
/var/cache/tomcat
/var/cache/tomcat/temp
/var/cache/tomcat/work
/var/lib/tomcat
/var/lib/tomcat/webapps
/var/lib/tomcats
/var/log/tomcat
/var/log/tomcat/catalina.out

Configuration:
/etc/tomcat/tomcat.conf
(/etc/sysconfig/tomcat)

# systemctl cat tomcat.service
# /usr/lib/systemd/system/tomcat.service
# Systemd unit file for default tomcat
# 
# To create clones of this service:
# DO NOTHING, use tomcat@.service instead.

[Unit]
Description=Apache Tomcat Web Application Container
After=syslog.target network.target

[Service]
Type=simple
EnvironmentFile=/etc/tomcat/tomcat.conf
Environment="NAME="
EnvironmentFile=-/etc/sysconfig/tomcat
ExecStart=/usr/libexec/tomcat/server start
SuccessExitStatus=143
User=tomcat

[Install]
WantedBy=multi-user.target


# cat /etc/logrotate.d/tomcat 
/var/log/tomcat/catalina.out {
    copytruncate
    weekly
    rotate 52
    compress
    missingok
    create 0644 tomcat tomcat
}

-----------------------------

# mkdir /usr/share/tomcat/webapps/ROOT
# echo "<html><body><h1>TOMCAT ROOT</h1></body></html>" > /usr/share/tomcat/webapps/ROOT/index.html
# chown root:tomcat -Rv /usr/share/tomcat/webapps/
# curl http://127.0.0.1:8080/
<html><body><h1>TOMCAT ROOT</h1></body></html>

-----------------------------
# yum list installed | grep tomcat
tomcat.noarch                     7.0.76-10.el7_7            @rhel-7-server-rpms
tomcat-el-2.2-api.noarch          7.0.76-10.el7_7            @rhel-7-server-rpms
tomcat-jsp-2.2-api.noarch         7.0.76-10.el7_7            @rhel-7-server-rpms
tomcat-lib.noarch                 7.0.76-10.el7_7            @rhel-7-server-rpms
tomcat-servlet-3.0-api.noarch     7.0.76-10.el7_7            @rhel-7-server-rpms

RHEL 8 and CentOS 8

Now too the sad story. Tomcat is no longer available on either RHEL 8 or CentOS 8.

"The Apache Tomcat server has been removed from Red Hat Enterprise Linux. Apache Tomcat is a servlet container for the Java Servlet and JavaServer Pages (JSP) technologies. Red Hat recommends that users requiring a servlet container use the JBoss Web Server."
  1. https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html-single/considerations_in_adopting_rhel_8/index#tomcat-removal_dynamic-programming-languages-web-servers-database-servers
  2. https://forums.centos.org/viewtopic.php?t=71787
  3. https://bugzilla.redhat.com/show_bug.cgi?id=1700823