AbstractComposedIdentity.java

package dev.orne.beans;

/*-
 * #%L
 * Orne Beans
 * %%
 * Copyright (C) 2023 Orne Developments
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
 * #L%
 */

import javax.validation.constraints.NotNull;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;

/**
 * Abstract implementation for {@code Identity} for identities composed
 * of a multiple inner values.
 * 
 * @author <a href="mailto:wamphiry@orne.dev">(w) Iker Hernaez</a>
 * @version 1.0, 2023-05
 * @since 0.5
 */
@API(status=Status.MAINTAINED, since="0.5")
public abstract class AbstractComposedIdentity
extends AbstractIdentity {

    /** The serial version UID. */
    private static final long serialVersionUID = 1L;

    /** Default identity token body parts separator. */
    public static final String DEFAULT_SEPARATOR = ",";
    /** The default placeholder for null identity token parts. */
    public static final String DEFAULT_NULL_PLACEHOLDER = "\0";

    /**
     * Creates a new instance.
     */
    protected AbstractComposedIdentity() {
        super();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected String getIdentityTokenBody() {
        final String[] parts = getIdentityTokenBodyParts();
        if (parts.length == 0) {
            return null;
        }
        final String placeholder = getIdentityTokenBodyPartsNullPlaceholder();
        for (int i = 0; i < parts.length; i++) {
            if (parts[i] == null) {
                parts[i] = placeholder;
            }
        }
        return StringUtils.join(
                parts,
                getIdentityTokenBodyPartsSeparator());
    }

    /**
     * Returns the identity token body composed from the values of this
     * identity. Equal instances must return equal identity token body.
     * 
     * @return The identity token body for this instance
     */
    protected abstract @NotNull String[] getIdentityTokenBodyParts();

    /**
     * Return the identity token body parts separator.
     * Must return same value for instances of the same class.
     * 
     * @return The identity token body parts separator.
     */
    protected @NotNull String getIdentityTokenBodyPartsSeparator() {
        return DEFAULT_SEPARATOR;
    }

    /**
     * Return the placeholder for null identity token parts.
     * Must return same value for instances of the same class.
     * 
     * @return The placeholder for null identity token parts.
     */
    protected @NotNull String getIdentityTokenBodyPartsNullPlaceholder() {
        return DEFAULT_NULL_PLACEHOLDER;
    }

    /**
     * Extracts the {@code String} value of a token generated by
     * {@code StringIdentity}.
     * 
     * @param prefix The expected identity token prefix.
     * @param token The identity token.
     * @return The extracted identity token body parts.
     * @throws NullPointerException If any argument is {@code null}
     * @throws UnrecognizedIdentityTokenException If the identity token is not
     * a valid simple identity token or if it doesn't start with the expected
     * prefix.
     * @see #extractTokenBodyParts(String, String, String, String)
     */
    public static @NotNull String[] extractTokenBodyParts(
            final @NotNull String prefix,
            final @NotNull String token) {
        return extractTokenBodyParts(
                prefix,
                token,
                DEFAULT_SEPARATOR,
                DEFAULT_NULL_PLACEHOLDER);
    }

    /**
     * Extracts the {@code String} value of a token generated by
     * {@code StringIdentity}.
     * 
     * @param prefix The expected identity token prefix.
     * @param token The identity token.
     * @param separator The identity token body parts separator.
     * @return The extracted identity token body parts.
     * @throws NullPointerException If any argument is {@code null}
     * @throws UnrecognizedIdentityTokenException If the identity token is not
     * a valid simple identity token or if it doesn't start with the expected
     * prefix.
     * @see #extractTokenBodyParts(String, String, String, String)
     */
    public static @NotNull String[] extractTokenBodyParts(
            final @NotNull String prefix,
            final @NotNull String token,
            final @NotNull String separator) {
        return extractTokenBodyParts(
                prefix,
                token,
                separator,
                DEFAULT_NULL_PLACEHOLDER);
    }

    /**
     * Extracts the {@code String} value of a token generated by
     * {@code StringIdentity}.
     * 
     * @param prefix The expected identity token prefix.
     * @param token The identity token.
     * @param separator The identity token body parts separator.
     * @param placeholder The {@code null} identity token body part placeholder.
     * @return The extracted identity token body parts.
     * @throws NullPointerException If any argument is {@code null}
     * @throws UnrecognizedIdentityTokenException If the identity token is not
     * a valid simple identity token or if it doesn't start with the expected
     * prefix.
     */
    public static @NotNull String[] extractTokenBodyParts(
            final @NotNull String prefix,
            final @NotNull String token,
            final @NotNull String separator,
            final @NotNull String placeholder) {
        Validate.notNull(prefix);
        Validate.notNull(token);
        Validate.notNull(separator);
        Validate.notNull(placeholder);
        final String body = IdentityTokenFormatter.parse(prefix, token);
        final String[] result;
        if (body == null) {
            result = new String[0];
        } else {
            result = body.split(separator);
            for (int i = 0; i < result.length; i++) {
                if (placeholder.equals(result[i])) {
                    result[i] = null;
                }
            }
        }
        return result;
    }

    /**
     * Extracts the body parts of a token generated by
     * {@code AbstractComposedIdentity}.
     * <p>
     * If the resulting value is {@code null} or the body parts count is not
     * the expected one an exception is thrown.
     * 
     * @param prefix The expected identity token prefix.
     * @param token The identity token.
     * @param expectedParts The expected identity token body parts count.
     * @return The extracted identity token body parts.
     * @throws NullPointerException If any argument is {@code null}
     * @throws UnrecognizedIdentityTokenException If the identity token is not
     * a valid identity token, if it doesn't start with the expected prefix,
     * if the extracted body is null or the extracted body parts count does not
     * match the expected parts count.
     * @see #extractTokenBodyParts(String, String)
     */
    public static @NotNull String[] extractRequiredTokenBodyParts(
            final @NotNull String prefix,
            final @NotNull String token,
            final int expectedParts) {
        return extractRequiredTokenBodyParts(
                prefix,
                token,
                DEFAULT_SEPARATOR,
                DEFAULT_NULL_PLACEHOLDER,
                expectedParts);
    }

    /**
     * Extracts the body parts of a token generated by
     * {@code AbstractComposedIdentity}.
     * <p>
     * If the resulting value is {@code null} or the body parts count is not
     * the expected one an exception is thrown.
     * 
     * @param prefix The expected identity token prefix.
     * @param token The identity token.
     * @param separator The identity token body parts separator.
     * @param expectedParts The expected identity token body parts count.
     * @return The extracted identity token body parts.
     * @throws NullPointerException If any argument is {@code null}
     * @throws UnrecognizedIdentityTokenException If the identity token is not
     * a valid identity token, if it doesn't start with the expected prefix,
     * if the extracted body is null or the extracted body parts count does not
     * match the expected parts count.
     * @see #extractTokenBodyParts(String, String, String)
     */
    public static @NotNull String[] extractRequiredTokenBodyParts(
            final @NotNull String prefix,
            final @NotNull String token,
            final @NotNull String separator,
            final int expectedParts) {
        return extractRequiredTokenBodyParts(
                prefix,
                token,
                separator,
                DEFAULT_NULL_PLACEHOLDER,
                expectedParts);
    }

    /**
     * Extracts the body parts of a token generated by
     * {@code AbstractComposedIdentity}.
     * <p>
     * If the resulting value is {@code null} or the body parts count is not
     * the expected one an exception is thrown.
     * 
     * @param prefix The expected identity token prefix.
     * @param token The identity token.
     * @param separator The identity token body parts separator.
     * @param placeholder The {@code null} identity token body part placeholder.
     * @param expectedParts The expected identity token body parts count.
     * @return The extracted identity token body parts.
     * @throws NullPointerException If any argument is {@code null}
     * @throws UnrecognizedIdentityTokenException If the identity token is not
     * a valid identity token, if it doesn't start with the expected prefix,
     * if the extracted body is null or the extracted body parts count does not
     * match the expected parts count.
     * @see #extractTokenBodyParts(String, String, String)
     */
    public static @NotNull String[] extractRequiredTokenBodyParts(
            final @NotNull String prefix,
            final @NotNull String token,
            final @NotNull String separator,
            final @NotNull String placeholder,
            final int expectedParts) {
        final String[] result = extractTokenBodyParts(prefix, token, separator, placeholder);
        validateTokenBodyParts(token, result, expectedParts);
        return result;
    }

    /**
     * Validates that the specified identity token body parts is not null and
     * has the expected number of parts.
     * 
     * @param token The identity token.
     * @param parts The extracted identity token body parts.
     * @param expectedParts The expected identity token body parts count.
     * @throws UnrecognizedIdentityTokenException If the extracted body is
     * {@code null} or the extracted body parts count does not match the
     * expected parts count.
     */
    private static void validateTokenBodyParts(
            final @NotNull String token,
            final @NotNull String[] parts,
            final int expectedParts) {
        if (parts.length != expectedParts) {
            throw new UnrecognizedIdentityTokenException(
                    "Unrecognized identity token: " + token);
        }
    }
}