5 min read

The Java source code is the next step that we need to have a great back-end system in JSF web application in the real world.

/src/main/java/org/example/kickoff/model/

package org.example.kickoff.model;

import static org.hibernate.annotations.CacheConcurrencyStrategy.TRANSACTIONAL;
import static org.omnifaces.utils.security.MessageDigests.digest;

import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import javax.validation.constraints.NotNull;

import org.hibernate.annotations.Cache;
import org.omnifaces.persistence.model.GeneratedIdEntity;

@Entity
public class Credentials extends GeneratedIdEntity<Long> {

	private static final long serialVersionUID = 1L;

	private static final int HASH_LENGTH = 32;
	private static final int SALT_LENGTH = 40;

	@ManyToOne(optional = false)
	@Cache(usage = TRANSACTIONAL)
	private @NotNull User user;

	@Column(length = HASH_LENGTH, nullable = false)
	private @NotNull byte[] passwordHash;

	@Column(length = SALT_LENGTH, nullable = false)
	private @NotNull byte[] salt = new byte[SALT_LENGTH];

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

	public void setPassword(String password) {
		ThreadLocalRandom.current().nextBytes(salt);
		passwordHash = hash(password);
	}

	public boolean isValid(String password) {
		return Arrays.equals(passwordHash, hash(password));
	}

	private byte[] hash(String password) {
		return digest(password, salt, "SHA-256");
	}

}
package org.example.kickoff.model;

import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;
import static java.util.stream.Collectors.toList;
import static org.example.kickoff.model.Role.ACCESS_API;
import static org.example.kickoff.model.Role.EDIT_OWN_PROFILE;
import static org.example.kickoff.model.Role.EDIT_PROFILES;
import static org.example.kickoff.model.Role.VIEW_ADMIN_PAGES;
import static org.example.kickoff.model.Role.VIEW_USER_PAGES;

import java.util.Arrays;
import java.util.List;

public enum Group {

	USER(VIEW_USER_PAGES, EDIT_OWN_PROFILE, ACCESS_API),

	ADMIN(VIEW_ADMIN_PAGES, EDIT_PROFILES);

	private final List<Role> roles;

	private Group(Role... roles) {
		this.roles = unmodifiableList(asList(roles));
	}

	public List<Role> getRoles() {
		return roles;
	}

	public static List<Group> getByRole(Role role) {
		return Arrays.stream(values())
		             .filter(group -> group.getRoles().contains(role))
		             .collect(toList());
	}

}
package org.example.kickoff.model;

import static java.time.temporal.ChronoUnit.MONTHS;
import static javax.persistence.EnumType.STRING;
import static org.hibernate.annotations.CacheConcurrencyStrategy.TRANSACTIONAL;

import java.time.Instant;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.ManyToOne;
import javax.persistence.PrePersist;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.hibernate.annotations.Cache;
import org.omnifaces.persistence.model.GeneratedIdEntity;

@Entity
public class LoginToken extends GeneratedIdEntity<Long> {

	private static final long serialVersionUID = 1L;

	private static final int HASH_LENGTH = 32;
	public static final int IP_ADDRESS_MAXLENGTH = 45;
	public static final int DESCRIPTION_MAXLENGTH = 255;

	public enum TokenType {
		REMEMBER_ME,
		API,
		RESET_PASSWORD
	}

	@Column(length = HASH_LENGTH, nullable = false, unique = true)
	private @NotNull byte[] tokenHash;

	@Column(nullable = false)
	private @NotNull Instant created;

	@Column(nullable = false)
	private @NotNull Instant expiration;

	@Column(length = IP_ADDRESS_MAXLENGTH, nullable = false)
	private @NotNull @Size(max = IP_ADDRESS_MAXLENGTH) String ipAddress;

	@Column(length = DESCRIPTION_MAXLENGTH)
	private @Size(max = DESCRIPTION_MAXLENGTH) String description;

	@ManyToOne(optional = false)
	@Cache(usage = TRANSACTIONAL)
	private User user;

	@Enumerated(STRING)
	private TokenType type;

	public User getUser() {
		return user;
	}

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

	public byte[] getTokenHash() {
		return tokenHash;
	}

	public void setTokenHash(byte[] tokenHash) {
		this.tokenHash = tokenHash;
	}

	public Instant getCreated() {
		return created;
	}

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

	public Instant getExpiration() {
		return expiration;
	}

	public void setExpiration(Instant expiration) {
		this.expiration = expiration;
	}

	public String getIpAddress() {
		return ipAddress;
	}

	public void setIpAddress(String ipAddress) {
		this.ipAddress = ipAddress;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public TokenType getType() {
		return type;
	}

	public void setType(TokenType type) {
		this.type = type;
	}

	@PrePersist
	public void setTimestamps() {
		created = Instant.now();

		if (expiration == null) {
			expiration = created.plus(1, MONTHS);
		}
	}

}
package org.example.kickoff.model;

public enum Role {

	// Viewing stuff.
	VIEW_USER_PAGES,
	VIEW_ADMIN_PAGES,

	// Editing stuff.
	EDIT_OWN_PROFILE,
	EDIT_PROFILES,

	// Actions.
	ACCESS_API;

}
package org.example.kickoff.model;

import static java.util.stream.Collectors.toSet;
import static javax.persistence.CascadeType.ALL;
import static javax.persistence.EnumType.STRING;
import static javax.persistence.FetchType.EAGER;
import static javax.persistence.FetchType.LAZY;
import static org.hibernate.annotations.CacheConcurrencyStrategy.TRANSACTIONAL;

import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Transient;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.example.kickoff.model.validator.Email;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.Formula;
import org.omnifaces.persistence.model.TimestampedEntity;

@Entity
public class User extends TimestampedEntity<Long> {

	private static final long serialVersionUID = 1L;

	public static final int EMAIL_MAXLENGTH = 254;
	public static final int NAME_MAXLENGTH = 32;

	@Column(length = EMAIL_MAXLENGTH, nullable = false, unique = true)
	private @NotNull @Size(max = EMAIL_MAXLENGTH) @Email String email;

	@Column(length = NAME_MAXLENGTH, nullable = false)
	private @NotNull @Size(max = NAME_MAXLENGTH) String firstName;

	@Column(length = NAME_MAXLENGTH, nullable = false)
	private @NotNull @Size(max = NAME_MAXLENGTH) String lastName;

	@Formula("CONCAT(firstName, ' ', lastName)")
	private String fullName;

	/*
	 * TODO: implement.
	 */
	@Column(nullable = false)
	private boolean emailVerified = true; // For now.

	@OneToOne(mappedBy = "user", fetch = LAZY, cascade = ALL)
	private Credentials credentials;

	@OneToMany(mappedBy = "user", fetch = LAZY, cascade = ALL, orphanRemoval = true)
	@Cache(usage = TRANSACTIONAL)
	private List<LoginToken> loginTokens = new ArrayList<>();

	@ElementCollection(fetch = EAGER)
	private @Enumerated(STRING) Set<Group> groups = new HashSet<>();

	@Column
	private Instant lastLogin;

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public String getFullName() {
		return fullName;
	}

	public boolean isEmailVerified() {
		return emailVerified;
	}

	public void setEmailVerified(boolean emailVerified) {
		this.emailVerified = emailVerified;
	}

	public Credentials getCredentials() {
		return credentials;
	}

	public void setCredentials(Credentials credentials) {
		this.credentials = credentials;
	}

	public List<LoginToken> getLoginTokens() {
		return loginTokens;
	}

	public Set<Group> getGroups() {
		return groups;
	}

	public void setGroups(Set<Group> groups) {
		this.groups = groups;
	}

	public Instant getLastLogin() {
		return lastLogin;
	}

	public void setLastLogin(Instant lastLogin) {
		this.lastLogin = lastLogin;
	}

	@Transient
	public Set<Role> getRoles() {
		return groups.stream().flatMap(g -> g.getRoles().stream()).collect(toSet());
	}

	@Transient
	public Set<String> getRolesAsStrings() {
		return getRoles().stream().map(Role::name).collect(toSet());
	}

}

/src/main/java/org/example/kickoff/model/producer/

package org.example.kickoff.model.producer;

import java.util.logging.Logger;

import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;
import javax.enterprise.inject.spi.InjectionPoint;

@Dependent
public class LoggerProducer {

	@Produces
	public Logger getLogger(InjectionPoint injectionPoint) {
		return Logger.getLogger(injectionPoint.getMember().getDeclaringClass().getName());
	}

}

/src/main/java/org/example/kickoff/model/validator/

package org.example.kickoff.model.validator;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.Size;

@Constraint(validatedBy = EmailValidator.class)
@Size(max = 254, message = "{invalid.email}")
@Documented
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
public @interface Email {

	String message() default "{invalid.email}";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};

}
package org.example.kickoff.model.validator;

import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class EmailValidator implements ConstraintValidator<Email, String> {

	@Override
	public void initialize(Email constraintAnnotation) {
		//
	}

	@Override
	public boolean isValid(String email, ConstraintValidatorContext context) {
		if (email == null) {
			return true; // Let @NotNull handle this.
		}

		try {
			new InternetAddress(email).validate();
		}
		catch (AddressException e) {
			return false;
		}

		return true;
	}

}
package org.example.kickoff.model.validator;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Constraint(validatedBy = PasswordValidator.class)
@Documented
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
public @interface Password {

	String message() default "{invalid.password}";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};

}
package org.example.kickoff.model.validator;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class PasswordValidator implements ConstraintValidator<Password, String> {

	@Override
	public void initialize(Password constraintAnnotation) {
		//
	}

	@Override
	public boolean isValid(String password, ConstraintValidatorContext context) {
		if (password == null) {
			return true; // Let @NotNull handle this.
		}

		return password.length() >= 8
			&& password.chars()
				.filter(c -> !isLatinLetter(c))
				.findFirst()
				.isPresent();
	}

	private static boolean isLatinLetter(int c) {
	    return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
	}

}
Orestis Pantazos

Orestis Pantazos

DevOps Engineer