Helper.java

/*
 * The coLAB project
 * Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
 *
 * Licensed under the MIT License
 */
package ch.colabproject.colab.api;

import ch.colabproject.colab.api.model.user.HashMethod;
import ch.colabproject.colab.api.ws.channel.model.WebsocketChannel;
import java.security.SecureRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.naming.InitialContext;
import javax.naming.NamingException;

/**
 * Some global helper methods
 *
 * @author maxence
 */
public class Helper {

    /**
     * The co.LAB base uniform resource name
     */
    public static final String COLAB_BASE_URN = "urn:coLAB:/";

    /**
     * pattern to check if a string looks like an email address
     */
    private static final Pattern EMAIL_PATTERN = Pattern
        .compile(
            "(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])");

    /**
     * never-called private constructor
     */
    private Helper() {
        throw new UnsupportedOperationException(
            "This is a utility class and cannot be instantiated");
    }

    /**
     * Convert byte array to hex string
     *
     * @param hash byte array to convert
     *
     * @return hex string representation of the byte array
     */
    public static String bytesToHex(byte[] hash) {
        StringBuilder hexString = new StringBuilder(2 * hash.length);
        for (int i = 0; i < hash.length; i++) {
            String hex = Integer.toHexString(0xff & hash[i]);
            if (hex.length() == 1) {
                hexString.append('0');
            }
            hexString.append(hex);
        }
        return hexString.toString();
    }

    /**
     * Convert byte array to hex string
     *
     * @param hex hex string to convert
     *
     * @return byte array
     */
    public static byte[] hextToBytes(String hex) {
        if (hex != null) {
            byte[] bytes = new byte[hex.length() / 2];

            int j = 0;
            for (int i = 0; i < hex.length(); i += 2) {
                bytes[j] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
                j++;
            }
            return bytes;
        } else {
            return new byte[0];
        }
    }

    /**
     * Make a full comparison of array. Do not return false as soon as array. Prevent timing-attack.
     *
     * @param a first array
     * @param b second array
     *
     * @return array equals or not
     */
    public static boolean constantTimeArrayEquals(byte[] a, byte[] b) {
        boolean result = true;
        int aSize = a.length;
        int bSize = b.length;
        int max = Math.max(aSize, bSize);
        for (int i = 0; i < max; i++) {
            if (i >= aSize || i >= bSize) {
                result = false;
            } else {
                result = a[i] == b[i] && result;
            }
        }
        return result;
    }

    /**
     * Concatenate arguments
     *
     * @param args list of string to concatenate
     *
     * @return the one string
     */
    public static String concat(String... args) {
        StringBuilder sb = new StringBuilder();
        for (String s : args) {
            if (s != null) {
                sb.append(s);
            }
        }
        return sb.toString();
    }

    /**
     * Check if given address match email address pattern
     *
     * @param address address to check
     *
     * @return true if address looks like an email address
     */
    public static boolean isEmailAddress(String address) {
        if (address != null) {
            Matcher matcher = EMAIL_PATTERN.matcher(address);
            return matcher.matches();
        } else {
            return false;
        }
    }

    /**
     * Generate secure random bytes
     *
     * @param length number of byte to generate
     *
     * @return byte array of request length filled with secured-random data
     */
    public static byte[] generateSalt(int length) {
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[length];
        random.nextBytes(salt);
        return salt;
    }

    /**
     * Same as {@link #generateSalt(int) } but return salt as hex string
     *
     * @param length number of bytes to generate
     *
     * @return hex-encoded byte array
     */
    public static String generateHexSalt(int length) {
        return Helper.bytesToHex(Helper.generateSalt(length));
    }

    /**
     * @return the hash method to use for new accounts
     */
    public static HashMethod getDefaultHashMethod() {
        return HashMethod.PBKDF2WithHmacSHA512_65536_64;
    }

    /**
     * Convert camelCase string to userscrore_separated_lower_case_string
     *
     * @param camelCase eg myAsewomeIdentifier
     *
     * @return eg my_aswesome_identifier
     */
    public static String camelCaseToUnderscore(String camelCase) {
        return camelCase
            .trim()
            // prefix all uppercase char preceded by something with an underscore
            .replaceAll("(?<!^)[A-Z](?!$)", "_$0")
            .toLowerCase();
    }

    /**
     * Get base urn to identify websocket channel.
     *
     * @param channel the channel to identify
     *
     * @return uniform resource name for the given channel
     */
    public static String getColabBaseUrn(WebsocketChannel channel) {
        return COLAB_BASE_URN + "WebsocketChannel/" + channel.getJsonDiscriminator();
    }

    /**
     * Convert given stack trace to string but skip some first elements.
     *
     * @param stackTrace list of stack trace element to print
     * @param skip       number of element to slip
     *
     * @return string to be logged
     */
    private static String prettyPrintColabStackTrace(StackTraceElement[] stackTrace, int skip) {
        StringBuilder sb = new StringBuilder();

        for (int i = skip; i < stackTrace.length; i++) {
            StackTraceElement elem = stackTrace[i];
            if (elem.getClassName().startsWith("ch.colab")) {
                sb.append("\n\tat ");
                sb.append(elem);
            }
        }
        return sb.toString();
    }

    /**
     * Export the current colab-only stack trace to string.
     *
     * @return string which represent the current stack trace
     */
    public static String getStringifiedColabStackTrace() {
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        return prettyPrintColabStackTrace(stackTrace, 2);
    }

    /**
     * Dump the colab-only stack trace to string from given throwable.
     *
     * @param t the throwable to dump the stack from
     *
     * @return string which represent the throwable stack trace
     */
    public static String getStringifiedColabStackTrace(Throwable t) {
        StackTraceElement[] stackTrace = t.getStackTrace();
        return prettyPrintColabStackTrace(stackTrace, 0);
    }

    /**
     * Convert given stack trace to string but skip some first elements.
     *
     * @param stackTrace list of stack trace element to print
     * @param skip       number of element to slip
     *
     * @return string to be logged
     */
    private static String prettyPrintStackTrace(StackTraceElement[] stackTrace, int skip) {
        StringBuilder sb = new StringBuilder();

        for (int i = skip; i < stackTrace.length; i++) {
            StackTraceElement elem = stackTrace[i];
            sb.append("\n\tat ");
            sb.append(elem);
        }
        return sb.toString();
    }

    /**
     * Export the current stack trace to string.
     *
     * @return string which represent the current stack trace
     */
    public static String getStringifiedStackTrace() {
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        return prettyPrintStackTrace(stackTrace, 2);
    }

    /**
     * Dump the stack trace to string from given throwable.
     *
     * @param t the throwable to dump the stack from
     *
     * @return string which represent the throwable stack trace
     */
    public static String getStringifiedStackTrace(Throwable t) {
        StackTraceElement[] stackTrace = t.getStackTrace();
        return prettyPrintStackTrace(stackTrace, 0);
    }

    /**
     * Lookup instances
     *
     * @param <T>   class to lookup
     * @param klass class to lookup
     *
     * @return instance of <code>klass</code>
     *
     * @throws NamingException if lookup failed
     */
    @SuppressWarnings("unchecked")
    public static <T> T lookup(Class<T> klass) throws NamingException {
        return (T) new InitialContext().lookup(
            "java:global/coLAB/" + klass.getSimpleName() + "!" + klass.getName());
    }
}