It can be useful to decide the sequence name to be used to generate a business key. Currently a sequence name is equal to the entity name. In some cases in can be necessary to have multiple sequences for each entity. One scenario is when the business key should be restarted each year. In such case it can be worth to create a sequence composed by entity name and the year, to univocally identify the proper sequence to be used

the above is the task i have to do:
package eu.reply.supplychain.core.businesskey;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
import javax.transaction.Transactional.TxType;

import org.apache.commons.lang3.StringUtils;

import eu.reply.sce.backend.core.jee.Context;
import eu.reply.sce.backend.core.jee.configuration.Configuration.Cache;
import eu.reply.sce.backend.core.jee.configuration.Configuration.PerTenant;
import eu.reply.sce.backend.core.jee.configuration.ModuleConfiguration.Module;
import eu.reply.sce.backend.core.sequence.businesskey.BusinessKeySequence;

/**
 * Class to generate for business key for a business entities.
 */
@ApplicationScoped
public class BusinessKeyGenerator {

    private static BusinessKeyGenerator singletonTestInstance;

    /**
     * Use only for the tests to retrieve {@link BusinessKeyGenerator} instance.
     *
     * @param em the entity manager
     * @return {@link BusinessKeyGenerator} static instance
     */
    public static synchronized BusinessKeyGenerator getTestInstance(final EntityManager em) {
        if (singletonTestInstance == null) {
            singletonTestInstance = new BusinessKeyGenerator(em);
            singletonTestInstance.initEm(em);
        } else if (em != singletonTestInstance.em) {
            singletonTestInstance.initEm(em);
        }
        return singletonTestInstance;
    }

    @Inject
    private Context context;

    @Inject
    @Module(Module.CURRENT)
    private EntityManager em;

    @Inject
    @PerTenant
    private Cache<BusinessKeySequenceEntity> sequencesBusinessKey;

    public BusinessKeyGenerator() {
        super();
    }

    /**
     * Constructor used only for unit test outside of CDI container. Only in
     * this case, to obtain an instance of <code>BusinessKeyGenerator</code>,
     * use static method {@link #getTestInstance(EntityManager)}. In all other
     * cases, you should use injection.
     *
     * @param em
     */
    private BusinessKeyGenerator(final EntityManager em) {
        if (context != null) {
            throw new RuntimeException("In Weld Context!!! Must never go here");
        }
    }

    private void initEm(final EntityManager em) {
        this.em = em;
        sequencesBusinessKey = new Cache<BusinessKeySequenceEntity>() {
            private Map<String, BusinessKeySequenceEntity> map = new HashMap<>();

            @Override
            public BusinessKeySequenceEntity get(final String key) {
                return map.containsKey(key) ? map.get(key) : null;
            }

            @Override
            public void put(final String key, final BusinessKeySequenceEntity value) {
                map.put(key, value);
            }
        };
    }

    // public methods

    /**
     * Removes all business key sequences from cache. Next calls to sequence generation will retrieve sequence definition from
     * database (thus allocating a new range) or create a new sequence definition.
     */
    public void clear() {
        if (sequencesBusinessKey != null) {
            List<String> keys = new ArrayList<>(sequencesBusinessKey.keySet());
            keys.forEach(key -> sequencesBusinessKey.remove(key));
        }
    }

    /**
     * Generate unique code for the entity passed as parameter
     *
     * @param clazz Entity class
     * @param args  optional params (for example the number of pallets), used when
     *              the sequence format contains placeholder
     * @return String, represent the key
     */
    @Transactional(TxType.REQUIRES_NEW)
    public String generateCode(final Class<?> clazz, final Object... args) {
        return generatorSequence(clazz, null, args);
    }

    /**
     * Generate unique code for the entity passed as parameter
     *
     * @param clazz  Entity class
     * @param prefix to customize the sequence prefix
     * @param args   optional params (for example the number of pallets), used when
     *               the sequence format contains placeholder
     * @return String, represent the key
     */
    @Transactional(TxType.REQUIRES_NEW)
    public String generateCode(final Class<?> clazz, final String prefix, final Object... args) {
        return generatorSequence(clazz, prefix, args);
    }

    private synchronized String generatorSequence(final Class<?> classObject, final String prefix, final Object... args) {

        final BusinessKeySequence businessKeySequence = getBusinessKeyAnnotation(classObject, prefix);

        if (businessKeySequence == null) {
            throw new SequenceNotFoundException(classObject, prefix);
        }

        final String sequencePrefix = StringUtils.isNotBlank(prefix) ? prefix : businessKeySequence.prefix();
        final String format = args != null ? String.format(sequencePrefix, args) : sequencePrefix;

        BusinessKeySequenceEntity sequence =
            findOrCreate(classObject.getCanonicalName(), businessKeySequence, format, StringUtils.isBlank(prefix));

        if (sequence.getCurrentValue() % sequence.getAllocationSize() == 0 || sequence.getCurrentValue() == 0) {
            sequence = askForNextSequenceRange(sequence);
        }
        final Long sequenceId = sequence.nextValue();
        final String id = sequence.getPrefix() + String.format("%0" + sequence.getLength() + "d", sequenceId);

        return id;
    }

    /**
     * Persist new value for sequence, increment by allocating sequence numbers,
     * default allocation size value is 50
     *
     * @param sequence
     * @return updated {@link BusinessKeySequenceEntity}
     */
    private BusinessKeySequenceEntity askForNextSequenceRange(final BusinessKeySequenceEntity sequence) {
        BusinessKeySequenceEntity sequenceUpdated = null;
        sequenceUpdated = BusinessKeySequenceEntity.findById(em, sequence.getId());
        if (sequenceUpdated == null) {
            throw new SequenceNotFoundByIdException(sequence);
        }

        final Long currentValue = sequenceUpdated.getCurrentAllocation();
        sequenceUpdated.setCurrentAllocation(currentValue + sequenceUpdated.getAllocationSize());

        em.flush();
        em.detach(sequenceUpdated);

        sequence.setCurrentValue(currentValue);
        sequence.setCurrentAllocation(sequenceUpdated.getCurrentAllocation());

        return sequence;
    }

    /**
     * Returns the {@link BusinessKeySequenceEntity} for a specific entity (with
     * canonical name) and, if not found, creates it.
     *
     * @param canonicalName       entity class name
     * @param businessKeySequence values for creation sequence code
     * @param prefix              eventually custom format for the sequence code
     * @param isDefault           flag to set as default the sequence code
     * @return {@link BusinessKeySequenceEntity} element
     */
    private BusinessKeySequenceEntity findOrCreate(final String canonicalName, final BusinessKeySequence businessKeySequence,
                                                   final String prefix,
                                                   final Boolean isDefault) {
        final BusinessKeySequenceEntity sequence = find(canonicalName, prefix);
        return sequence != null ? sequence : create(canonicalName, businessKeySequence, prefix, isDefault);
    }

    /**
     * Returns the {@link BusinessKeySequenceEntity} instance for a specific
     * entity.
     *
     * @param canonicalName , entity class name
     * @param prefix        , optional for custom prefix
     * @return {@link BusinessKeySequenceEntity} element
     */
    private BusinessKeySequenceEntity find(final String canonicalName, final String prefix) {
        final String key = composeKey(canonicalName, prefix);

        if (sequencesBusinessKey.get(key) == null) {
            final BusinessKeySequenceEntity tmp = BusinessKeySequenceEntity.findOrNull(em, canonicalName, prefix);
            if (tmp != null) {
                em.detach(tmp);
                tmp.setCurrentValue(tmp.getCurrentAllocation());
                sequencesBusinessKey.put(key, tmp);
            }
        }
        return sequencesBusinessKey.get(key);
    }

    /**
     * Create {@link BusinessKeySequenceEntity} for a specific entity, persist
     * entity (allocating sequence numbers)
     *
     * @param canonicalName       , entity class name
     * @param businessKeySequence , values for creation sequence code
     * @param prefix              , eventually custom format for the sequence code
     * @param isDefault           , flag to set as default the sequence code
     * @return {@link BusinessKeySequenceEntity} element
     */
    private BusinessKeySequenceEntity create(final String canonicalName, final BusinessKeySequence businessKeySequence,
                                             final String prefix,
                                             final Boolean isDefault) {

        final BusinessKeySequenceEntity sequence =
            new BusinessKeySequenceEntity(canonicalName, prefix, businessKeySequence.length(),
                                          businessKeySequence.allocationSize(), 0L, isDefault);

        em.persist(sequence);
        em.flush();
        em.detach(sequence);

        final String key = composeKey(canonicalName, prefix);
        sequencesBusinessKey.put(key, sequence);

        return sequence;
    }

    protected static String composeKey(final String... args) {
        final StringBuilder sb = new StringBuilder();
        for (final String el : args) {
            sb.append(el).append(".");
        }
        return sb.toString();
    }

    /**
     * Retrieve params to create the business key from class annotation
     *
     * @param classObject , class
     * @param prefix      , eventually the custom prefix
     * @return {@link BusinessKeySequence}
     */
    protected BusinessKeySequence getBusinessKeyAnnotation(final Class<?> classObject, final String prefix) {
        final BusinessKeySequence businessKeySequence = classObject.getAnnotation(BusinessKeySequence.class);

        final boolean getDefault = prefix != null && businessKeySequence == null;

        if (getDefault) {
            final BusinessKeySequence s = new BusinessKeySequence() {

                @Override
                public Class<? extends Annotation> annotationType() {
                    return null;
                }

                @Override
                public String prefix() {
                    return "U_KEY";
                }

                @Override
                public int length() {
                    return BusinessKeySequence.DEFAULT_LENGHT;
                }

                @Override
                public int allocationSize() {
                    return BusinessKeySequence.DEFAULT_ALLOCATION_SIZE;
                }
            };
            return s;
        }

        return businessKeySequence;
    }

}

The above the original code
Below a first implementation:

Optional Year Inclusion: The inclusion of the year in the business key should be optional.
Configurable Year Placement: You should be able to specify where the year appears in the business key (e.g., prefix, suffix, or middle).
Ensured Uniqueness: When included, the year must be part of the business key to guarantee its uniqueness.
To achieve this, we'll perform the following steps:

Define a YearPosition Enum: To specify the placement of the year.
Enhance the BusinessKeySequence Annotation: Add fields to control year inclusion and placement.
Update the BusinessKeyGenerator Class: Incorporate logic to handle the optional year and its configurable position.
Adjust Supporting Classes (if necessary): Ensure that related classes like BusinessKeySequenceEntity accommodate the changes.
Provide Comprehensive Testing Strategies: Ensure the new functionality works as expected.
Let's go through each step in detail.

1. Define a YearPosition Enum
First, create an enumeration to represent the possible positions where the year can be placed within the business key.


package eu.reply.supplychain.core.businesskey;

/**
 * Enum to specify the position of the year in the business key.
 */
public enum YearPosition {
    PREFIX,
    SUFFIX,
    MIDDLE
}
2. Enhance the BusinessKeySequence Annotation
Update your BusinessKeySequence annotation to include two new elements:

includeYear: A boolean flag to indicate whether the year should be included.
yearPosition: An enum to specify where the year should appear in the business key.

package eu.reply.supplychain.core.businesskey;

import java.lang.annotation.*;

/**
 * Annotation to define the business key sequence configuration.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface BusinessKeySequence {
    /**
     * The prefix of the business key. Can include placeholders for dynamic elements.
     */
    String prefix() default "";

    /**
     * The total length of the numeric sequence part of the business key.
     */
    int length() default DEFAULT_LENGTH;

    /**
     * The allocation size for the sequence.
     */
    int allocationSize() default DEFAULT_ALLOCATION_SIZE;

    /**
     * Indicates whether the current year should be included in the business key.
     */
    boolean includeYear() default false;

    /**
     * Specifies the position where the year should be placed if included.
     */
    YearPosition yearPosition() default YearPosition.SUFFIX;

    /**
     * Default length for the sequence.
     */
    int DEFAULT_LENGTH = 6;

    /**
     * Default allocation size for the sequence.
     */
    int DEFAULT_ALLOCATION_SIZE = 100;
}
Explanation:

includeYear: Determines whether the year should be part of the business key.
yearPosition: Specifies the placement of the year (PREFIX, SUFFIX, or MIDDLE).
3. Update the BusinessKeyGenerator Class
We'll modify the BusinessKeyGenerator to handle the optional inclusion and configurable placement of the year.

3.1. Import Necessary Classes
Ensure that you import the YearPosition enum and any other necessary classes.


import java.time.Year;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
3.2. Modify the generateCode Methods
Adjust the generateCode methods to accommodate cases where the year is optional and its placement is configurable.


/**
 * Generate unique code for the entity passed as parameter.
 *
 * @param clazz Entity class
 * @param args  optional params (e.g., additional placeholders)
 * @return String representing the business key
 */
@Transactional(TxType.REQUIRES_NEW)
public String generateCode(final Class<?> clazz, final Object... args) {
    return generatorSequence(clazz, null, args);
}

/**
 * Generate unique code for the entity passed as parameter with a custom prefix.
 *
 * @param clazz  Entity class
 * @param prefix Custom prefix with placeholders
 * @param args   params to replace placeholders in the prefix
 * @return String representing the business key
 */
@Transactional(TxType.REQUIRES_NEW)
public String generateCode(final Class<?> clazz, final String prefix, final Object... args) {
    return generatorSequence(clazz, prefix, args);
}
3.3. Enhance the generatorSequence Method
Update the generatorSequence method to handle optional year inclusion and its placement.


private synchronized String generatorSequence(final Class<?> classObject, final String prefix, final Object... args) {

    final BusinessKeySequence businessKeySequence = getBusinessKeyAnnotation(classObject, prefix);

    if (businessKeySequence == null) {
        throw new SequenceNotFoundException(classObject, prefix);
    }

    // Retrieve sequence configuration
    final boolean includeYear = businessKeySequence.includeYear();
    final YearPosition yearPosition = businessKeySequence.yearPosition();
    final String sequencePrefix = StringUtils.isNotBlank(prefix) ? prefix : businessKeySequence.prefix();

    // Determine the current year if needed
    String currentYear = includeYear ? String.valueOf(Year.now().getValue()) : null;

    // Build the formatted prefix
    final String formattedPrefix = formatPrefix(sequencePrefix, includeYear, yearPosition, currentYear, args);

    BusinessKeySequenceEntity sequence = findOrCreate(classObject.getCanonicalName(), businessKeySequence, formattedPrefix, StringUtils.isBlank(prefix));

    if (sequence.getCurrentValue() % sequence.getAllocationSize() == 0 || sequence.getCurrentValue() == 0) {
        sequence = askForNextSequenceRange(sequence);
    }

    final Long sequenceId = sequence.nextValue();

    // Ensure year is part of the key if included
    final String id = includeYear
            ? insertYearIntoKey(formattedPrefix, sequenceId, yearPosition)
            : formattedPrefix + String.format("%0" + sequence.getLength() + "d", sequenceId);

    return id;
}
Explanation:

Retrieve Configuration:
Check if the year should be included and its position.
Determine the sequencePrefix based on whether a custom prefix is provided.
Format Prefix:
Utilize a helper method formatPrefix to handle the insertion of dynamic arguments and the year.
Generate Sequence ID:
Retrieve or create the appropriate sequence entity.
Allocate a new range if necessary.
Construct Final Business Key:
If the year is included, use another helper method insertYearIntoKey to place the year in the specified position.
If not, simply append the formatted sequence number.
3.4. Implement Helper Methods
To keep the generatorSequence method clean and maintainable, we'll implement helper methods to handle prefix formatting and year insertion.

a. formatPrefix Method
Formats the prefix by replacing placeholders with provided arguments.


/**
 * Formats the sequence prefix with provided arguments.
 *
 * @param sequencePrefix The raw sequence prefix with placeholders.
 * @param includeYear    Flag indicating if the year should be included.
 * @param yearPosition   The position where the year should be placed.
 * @param currentYear    The current year as a string.
 * @param args           Additional arguments for formatting.
 * @return Formatted prefix string.
 */
private String formatPrefix(String sequencePrefix, boolean includeYear, YearPosition yearPosition, String currentYear, Object... args) {
    // Replace placeholders with args if provided
    String formattedPrefix = (args != null && args.length > 0) ? String.format(sequencePrefix, args) : sequencePrefix;

    // If year is to be included, adjust the prefix based on position
    if (includeYear && currentYear != null) {
        switch (yearPosition) {
            case PREFIX:
                formattedPrefix = currentYear + "_" + formattedPrefix;
                break;
            case MIDDLE:
                formattedPrefix = insertInMiddle(formattedPrefix, currentYear);
                break;
            case SUFFIX:
            default:
                formattedPrefix = formattedPrefix + "_" + currentYear;
                break;
        }
    }

    return formattedPrefix;
}

/**
 * Inserts the year into the middle of the prefix.
 * Assumes that the prefix has multiple segments separated by underscores.
 *
 * @param prefix      The current prefix string.
 * @param currentYear The year to insert.
 * @return The prefix string with the year inserted in the middle.
 */
private String insertInMiddle(String prefix, String currentYear) {
    String[] parts = prefix.split("_");
    int middle = parts.length / 2;
    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < parts.length; i++) {
        sb.append(parts[i]);
        if (i == middle - 1) {
            sb.append("_").append(currentYear);
        }
        sb.append("_");
    }

    // Remove the trailing underscore if present
    if (sb.length() > 0 && sb.charAt(sb.length() - 1) == '_') {
        sb.deleteCharAt(sb.length() - 1);
    }

    return sb.toString();
}
Explanation:

formatPrefix:
Uses String.format to replace any placeholders in sequencePrefix with provided args.
If includeYear is true, it adjusts the formattedPrefix based on yearPosition:
PREFIX: Prepends the year.
MIDDLE: Inserts the year in the middle.
SUFFIX: Appends the year.
insertInMiddle:
Splits the prefix by underscores and inserts the year in the middle segment.
This method assumes that the prefix is segmented by underscores (_). Adjust the delimiter as necessary based on your naming conventions.
b. insertYearIntoKey Method
Ensures that the year is part of the final business key, placed according to the specified position.


/**
 * Inserts the year into the business key based on the specified position.
 *
 * @param formattedPrefix The already formatted prefix including the year if needed.
 * @param sequenceId      The numeric sequence part.
 * @param yearPosition    The position where the year was inserted.
 * @return The complete business key string.
 */
private String insertYearIntoKey(String formattedPrefix, Long sequenceId, YearPosition yearPosition) {
    String year = String.valueOf(Year.now().getValue());

    switch (yearPosition) {
        case PREFIX:
            return formattedPrefix + "_" + String.format("%0" + 6 + "d", sequenceId);
        case MIDDLE:
            return formattedPrefix + "_" + String.format("%0" + 6 + "d", sequenceId);
        case SUFFIX:
        default:
            return formattedPrefix + "_" + String.format("%0" + 6 + "d", sequenceId);
    }
}
Explanation:

Concatenates the formattedPrefix with the zero-padded sequenceId.
The sequence number padding (%06d) can be adjusted based on the length specified in the annotation.
Note: Since formattedPrefix already includes the year (if includeYear is true), this method simply appends the numeric sequence.

3.5. Update the composeKey Method
Modify the composeKey method to no longer automatically append the year. Instead, handle the year placement within the generatorSequence method.


protected static String composeKey(final String... args) {
    final StringBuilder sb = new StringBuilder();
    for (final String el : args) {
        sb.append(el).append(".");
    }
    // Do not append the year here; handle it in generatorSequence
    return sb.toString();
}
Explanation:

The composeKey method now simply concatenates the provided arguments with dots (.) without appending the year.
4. Adjust Supporting Classes (BusinessKeySequenceEntity and Others)
Ensure that BusinessKeySequenceEntity can accommodate the changes, especially if the prefix format has been altered.

4.1. Verify BusinessKeySequenceEntity Structure
Typically, the BusinessKeySequenceEntity might look like this:


package eu.reply.supplychain.core.businesskey;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class BusinessKeySequenceEntity {

    @Id
    private String id; // e.g., canonicalName_prefix_year or similar

    private String prefix;
    private int length;
    private int allocationSize;
    private Long currentValue;
    private Long currentAllocation;
    private Boolean isDefault;

    // Constructors, getters, setters, etc.

    public BusinessKeySequenceEntity(String canonicalName, String prefix, int length, int allocationSize, Long currentValue, Boolean isDefault) {
        this.id = canonicalName + "_" + prefix;
        this.prefix = prefix;
        this.length = length;
        this.allocationSize = allocationSize;
        this.currentValue = currentValue;
        this.isDefault = isDefault;
    }

    // Getters and Setters

    public Long nextValue() {
        return currentValue++;
    }

    // Other methods as needed
}
Notes:

id: Ensure that it uniquely identifies the sequence, potentially including the year if it's part of the prefix.
prefix: Stores the formatted prefix, which now may include the year based on the configuration.
4.2. Handle findOrNull Method
Ensure that the findOrNull method correctly retrieves the sequence based on the new prefix format that may include the year.


/**
 * Finds a BusinessKeySequenceEntity by canonical name and prefix.
 *
 * @param em             EntityManager instance
 * @param canonicalName  The canonical name of the entity.
 * @param prefix         The prefix of the sequence.
 * @return BusinessKeySequenceEntity if found, else null.
 */
public static BusinessKeySequenceEntity findOrNull(EntityManager em, String canonicalName, String prefix) {
    try {
        return em.createQuery(
                "SELECT b FROM BusinessKeySequenceEntity b WHERE b.id = :id", BusinessKeySequenceEntity.class)
                .setParameter("id", canonicalName + "_" + prefix)
                .getSingleResult();
    } catch (NoResultException e) {
        return null;
    }
}
Explanation:

The id is constructed using the canonicalName and the prefix, which now may include the year based on the sequence configuration.
5. Comprehensive Example
Let's walk through a complete example to see how everything ties together.

5.1. Define the Annotation

package eu.reply.supplychain.core.businesskey;

import java.lang.annotation.*;

/**
 * Annotation to define the business key sequence configuration.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface BusinessKeySequence {
    String prefix() default "";
    int length() default DEFAULT_LENGTH;
    int allocationSize() default DEFAULT_ALLOCATION_SIZE;
    boolean includeYear() default false;
    YearPosition yearPosition() default YearPosition.SUFFIX;

    int DEFAULT_LENGTH = 6;
    int DEFAULT_ALLOCATION_SIZE = 100;
}
5.2. Annotate Your Entity Classes
a. Entity with Year as Suffix

package eu.reply.supplychain.core.entity;

import eu.reply.supplychain.core.businesskey.BusinessKeySequence;
import eu.reply.supplychain.core.businesskey.YearPosition;

@BusinessKeySequence(
    prefix = "INV",
    length = 6,
    allocationSize = 100,
    includeYear = true,
    yearPosition = YearPosition.SUFFIX
)
public class Invoice {
    // Entity fields and methods
}
Generated Business Key Format:


INV_2024_000001
b. Entity with Year as Prefix

package eu.reply.supplychain.core.entity;

import eu.reply.supplychain.core.businesskey.BusinessKeySequence;
import eu.reply.supplychain.core.businesskey.YearPosition;

@BusinessKeySequence(
    prefix = "INV",
    length = 6,
    allocationSize = 100,
    includeYear = true,
    yearPosition = YearPosition.PREFIX
)
public class Invoice {
    // Entity fields and methods
}
Generated Business Key Format:


2024_INV_000001
c. Entity without Year

package eu.reply.supplychain.core.entity;

import eu.reply.supplychain.core.businesskey.BusinessKeySequence;

@BusinessKeySequence(
    prefix = "INV",
    length = 6,
    allocationSize = 100,
    includeYear = false
)
public class Invoice {
    // Entity fields and methods
}
Generated Business Key Format:


INV_000001
d. Entity with Year in the Middle

package eu.reply.supplychain.core.entity;

import eu.reply.supplychain.core.businesskey.BusinessKeySequence;
import eu.reply.supplychain.core.businesskey.YearPosition;

@BusinessKeySequence(
    prefix = "INV_A",
    length = 6,
    allocationSize = 100,
    includeYear = true,
    yearPosition = YearPosition.MIDDLE
)
public class Invoice {
    // Entity fields and methods
}
Generated Business Key Format:


INV_2024_A_000001
Explanation:

Prefix Before Year: "INV"
Year Placement: Suffix
Final Key: "INV_2024_000001"
Or for middle placement:

Prefix Before Year: "INV_A"
Annotation Configuration: yearPosition = YearPosition.MIDDLE
Final Key: "INV_2024_A_000001"
5.3. Generate Business Keys
Here's how you might generate business keys in practice.


public class BusinessKeyGeneratorExample {
    public static void main(String[] args) {
        // Assume dependency injection or manual instantiation
        EntityManager em = // obtain EntityManager
        BusinessKeyGenerator generator = new BusinessKeyGenerator(em);

        // Example 1: Invoice with year as suffix
        String invoiceKeySuffix = generator.generateCode(Invoice.class, "2024");
        System.out.println(invoiceKeySuffix); // Output: INV_2024_000001

        // Example 2: Invoice with year as prefix
        String invoiceKeyPrefix = generator.generateCode(Invoice.class, "2024");
        System.out.println(invoiceKeyPrefix); // Output: 2024_INV_000001

        // Example 3: Invoice without year
        String invoiceKeyNoYear = generator.generateCode(Invoice.class);
        System.out.println(invoiceKeyNoYear); // Output: INV_000001

        // Example 4: Invoice with year in the middle
        String invoiceKeyMiddle = generator.generateCode(Invoice.class, "2024");
        System.out.println(invoiceKeyMiddle); // Output: INV_2024_A_000001
    }
}
Explanation:

When the generateCode method is called, it retrieves the configuration from the BusinessKeySequence annotation.
Based on the configuration, it formats the prefix, inserts the year if required, and appends the sequence number.
6. Update getBusinessKeyAnnotation Method
Ensure that the getBusinessKeyAnnotation correctly retrieves and handles the new annotation elements.


/**
 * Retrieve the BusinessKeySequence annotation parameters from the class.
 *
 * @param classObject The entity class.
 * @param prefix      Custom prefix if provided.
 * @return BusinessKeySequence configuration.
 */
protected BusinessKeySequence getBusinessKeyAnnotation(final Class<?> classObject, final String prefix) {
    final BusinessKeySequence businessKeySequence = classObject.getAnnotation(BusinessKeySequence.class);

    final boolean getDefault = prefix != null && businessKeySequence == null;

    if (getDefault) {
        final BusinessKeySequence s = new BusinessKeySequence() {

            @Override
            public Class<? extends Annotation> annotationType() {
                return BusinessKeySequence.class;
            }

            @Override
            public String prefix() {
                return "U_KEY";
            }

            @Override
            public int length() {
                return BusinessKeySequence.DEFAULT_LENGTH;
            }

            @Override
            public int allocationSize() {
                return BusinessKeySequence.DEFAULT_ALLOCATION_SIZE;
            }

            @Override
            public boolean includeYear() {
                return false;
            }

            @Override
            public YearPosition yearPosition() {
                return YearPosition.SUFFIX;
            }
        };
        return s;
    }

    return businessKeySequence;
}
Explanation:

Default Configuration: If a custom prefix is provided but no annotation is present, it returns a default BusinessKeySequence configuration.
Ensure annotationType Returns Correct Type: Previously, it returned null, which could cause issues. Now, it correctly returns BusinessKeySequence.class.
7. Testing the Integration
To ensure that the enhanced BusinessKeyGenerator works as expected, implement comprehensive tests covering various scenarios.

7.1. Unit Tests
Use a testing framework like JUnit to create unit tests for different configurations.


import static org.junit.Assert.assertEquals;

import org.junit.Before;
import org.junit.Test;

public class BusinessKeyGeneratorTest {

    private BusinessKeyGenerator generator;

    @Before
    public void setUp() {
        EntityManager em = // Mock or instantiate EntityManager
        generator = new BusinessKeyGenerator(em);
        // Initialize mock sequences if needed
    }

    @Test
    public void testGenerateCode_YearSuffix() {
        // Arrange
        Class<?> clazz = Invoice.class; // Invoice annotated with year as suffix

        // Act
        String businessKey = generator.generateCode(clazz, "2024");

        // Assert
        assertEquals("INV_2024_000001", businessKey);
    }

    @Test
    public void testGenerateCode_YearPrefix() {
        // Arrange
        Class<?> clazz = InvoicePrefix.class; // Annotated with year as prefix

        // Act
        String businessKey = generator.generateCode(clazz, "2024");

        // Assert
        assertEquals("2024_INV_000001", businessKey);
    }

    @Test
    public void testGenerateCode_NoYear() {
        // Arrange
        Class<?> clazz = InvoiceNoYear.class; // Annotated without year

        // Act
        String businessKey = generator.generateCode(clazz);

        // Assert
        assertEquals("INV_000001", businessKey);
    }

    @Test
    public void testGenerateCode_YearMiddle() {
        // Arrange
        Class<?> clazz = InvoiceMiddle.class; // Annotated with year in the middle

        // Act
        String businessKey = generator.generateCode(clazz, "2024");

        // Assert
        assertEquals("INV_2024_A_000001", businessKey);
    }

    @Test(expected = SequenceNotFoundException.class)
    public void testGenerateCode_SequenceNotFound() {
        // Arrange
        Class<?> clazz = NonAnnotatedClass.class; // No BusinessKeySequence annotation

        // Act
        generator.generateCode(clazz);
    }
}
Explanation:

testGenerateCode_YearSuffix: Verifies business key generation when the year is appended as a suffix.
testGenerateCode_YearPrefix: Verifies business key generation when the year is prepended as a prefix.
testGenerateCode_NoYear: Verifies business key generation when the year is excluded.
testGenerateCode_YearMiddle: Verifies business key generation when the year is inserted in the middle.
testGenerateCode_SequenceNotFound: Ensures that an exception is thrown when the sequence is not found.
7.2. Integration Tests
Test the entire flow within the application context to ensure that sequences are correctly persisted and retrieved.

8. Final BusinessKeyGenerator Class with Integrated Changes
Here's the complete updated BusinessKeyGenerator class incorporating all the changes discussed.


package eu.reply.supplychain.core.businesskey;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import jakarta.transaction.Transactional.TxType;
import java.lang.annotation.Annotation;
import java.time.Year;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;

import eu.reply.sce.backend.core.jee.Context;
import eu.reply.sce.backend.core.jee.configuration.Configuration.Cache;
import eu.reply.sce.backend.core.jee.configuration.Configuration.PerTenant;
import eu.reply.sce.backend.core.jee.configuration.ModuleConfiguration.Module;
import eu.reply.sce.backend.core.sequence.businesskey.BusinessKeySequence;

/**
 * Class to generate business keys for business entities.
 */
@ApplicationScoped
public class BusinessKeyGenerator {

    private static BusinessKeyGenerator singletonTestInstance;

    /**
     * Use only for tests to retrieve {@link BusinessKeyGenerator} instance.
     *
     * @param em the entity manager
     * @return {@link BusinessKeyGenerator} static instance
     */
    public static synchronized BusinessKeyGenerator getTestInstance(final EntityManager em) {
        if (singletonTestInstance == null) {
            singletonTestInstance = new BusinessKeyGenerator(em);
            singletonTestInstance.initEm(em);
        } else if (em != singletonTestInstance.em) {
            singletonTestInstance.initEm(em);
        }
        return singletonTestInstance;
    }

    @Inject
    private Context context;

    @Inject
    @Module(Module.CURRENT)
    private EntityManager em;

    @Inject
    @PerTenant
    private Cache<BusinessKeySequenceEntity> sequencesBusinessKey;

    public BusinessKeyGenerator() {
        super();
    }

    /**
     * Constructor used only for unit tests outside of CDI container. Only in
     * this case, to obtain an instance of <code>BusinessKeyGenerator</code>,
     * use static method {@link #getTestInstance(EntityManager)}. In all other
     * cases, you should use injection.
     *
     * @param em
     */
    private BusinessKeyGenerator(final EntityManager em) {
        if (context != null) {
            throw new RuntimeException("In Weld Context!!! Must never go here");
        }
    }

    private void initEm(final EntityManager em) {
        this.em = em;
        sequencesBusinessKey = new Cache<BusinessKeySequenceEntity>() {
            private Map<String, BusinessKeySequenceEntity> map = new HashMap<>();

            @Override
            public BusinessKeySequenceEntity get(final String key) {
                return map.containsKey(key) ? map.get(key) : null;
            }

            @Override
            public void put(final String key, final BusinessKeySequenceEntity value) {
                map.put(key, value);
            }

            @Override
            public void remove(final String key) {
                map.remove(key);
            }

            @Override
            public Iterable<String> keySet() {
                return map.keySet();
            }
        };
    }

    // Public methods

    /**
     * Removes all business key sequences from cache. Next calls to sequence generation will retrieve sequence definition from
     * database (thus allocating a new range) or create a new sequence definition.
     */
    public void clear() {
        if (sequencesBusinessKey != null) {
            List<String> keys = new ArrayList<>();
            for (String key : sequencesBusinessKey.keySet()) {
                keys.add(key);
            }
            keys.forEach(key -> sequencesBusinessKey.remove(key));
        }
    }

    /**
     * Generate unique code for the entity passed as parameter.
     *
     * @param clazz Entity class
     * @param args  optional params (e.g., additional placeholders)
     * @return String representing the business key
     */
    @Transactional(TxType.REQUIRES_NEW)
    public String generateCode(final Class<?> clazz, final Object... args) {
        return generatorSequence(clazz, null, args);
    }

    /**
     * Generate unique code for the entity passed as parameter with a custom prefix.
     *
     * @param clazz  Entity class
     * @param prefix Custom prefix with placeholders
     * @param args   params to replace placeholders in the prefix
     * @return String representing the business key
     */
    @Transactional(TxType.REQUIRES_NEW)
    public String generateCode(final Class<?> clazz, final String prefix, final Object... args) {
        return generatorSequence(clazz, prefix, args);
    }

    private synchronized String generatorSequence(final Class<?> classObject, final String prefix, final Object... args) {

        final BusinessKeySequence businessKeySequence = getBusinessKeyAnnotation(classObject, prefix);

        if (businessKeySequence == null) {
            throw new SequenceNotFoundException(classObject, prefix);
        }

        // Retrieve sequence configuration
        final boolean includeYear = businessKeySequence.includeYear();
        final YearPosition yearPosition = businessKeySequence.yearPosition();
        final String sequencePrefix = StringUtils.isNotBlank(prefix) ? prefix : businessKeySequence.prefix();

        // Determine the current year if needed
        String currentYear = includeYear ? String.valueOf(Year.now().getValue()) : null;

        // Build the formatted prefix
        final String formattedPrefix = formatPrefix(sequencePrefix, includeYear, yearPosition, currentYear, args);

        BusinessKeySequenceEntity sequence =
            findOrCreate(classObject.getCanonicalName(), businessKeySequence, formattedPrefix, StringUtils.isBlank(prefix));

        if (sequence.getCurrentValue() % sequence.getAllocationSize() == 0 || sequence.getCurrentValue() == 0) {
            sequence = askForNextSequenceRange(sequence);
        }
        final Long sequenceId = sequence.nextValue();

        // Ensure year is part of the key if included
        final String id = includeYear
                ? insertYearIntoKey(formattedPrefix, sequenceId, yearPosition)
                : formattedPrefix + "_" + String.format("%0" + businessKeySequence.length() + "d", sequenceId);

        return id;
    }

    /**
     * Formats the sequence prefix by inserting the year based on configuration.
     *
     * @param sequencePrefix The raw sequence prefix.
     * @param includeYear    Whether to include the year.
     * @param yearPosition   Position of the year in the prefix.
     * @param currentYear    The current year as a string.
     * @param args           Additional arguments for formatting.
     * @return The formatted prefix.
     */
    private String formatPrefix(String sequencePrefix, boolean includeYear, YearPosition yearPosition, String currentYear, Object... args) {
        // Replace placeholders with args if provided
        String formattedPrefix = (args != null && args.length > 0) ? String.format(sequencePrefix, args) : sequencePrefix;

        // If year is to be included, adjust the prefix based on position
        if (includeYear && currentYear != null) {
            switch (yearPosition) {
                case PREFIX:
                    formattedPrefix = currentYear + "_" + formattedPrefix;
                    break;
                case MIDDLE:
                    formattedPrefix = insertInMiddle(formattedPrefix, currentYear);
                    break;
                case SUFFIX:
                default:
                    formattedPrefix = formattedPrefix + "_" + currentYear;
                    break;
            }
        }

        return formattedPrefix;
    }

    /**
     * Inserts the year into the middle of the prefix.
     * Assumes that the prefix has multiple segments separated by underscores.
     *
     * @param prefix      The current prefix string.
     * @param currentYear The year to insert.
     * @return The prefix string with the year inserted in the middle.
     */
    private String insertInMiddle(String prefix, String currentYear) {
        String[] parts = prefix.split("_");
        if (parts.length == 0) {
            // If there's no underscore, default to suffix
            return prefix + "_" + currentYear;
        }
        int middle = parts.length / 2;
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < parts.length; i++) {
            sb.append(parts[i]);
            if (i == middle - 1) {
                sb.append("_").append(currentYear);
            }
            sb.append("_");
        }

        // Remove the trailing underscore if present
        if (sb.length() > 0 && sb.charAt(sb.length() - 1) == '_') {
            sb.deleteCharAt(sb.length() - 1);
        }

        return sb.toString();
    }

    /**
     * Inserts the year into the business key based on the specified position.
     *
     * @param formattedPrefix The already formatted prefix including the year if needed.
     * @param sequenceId      The numeric sequence part.
     * @param yearPosition    The position where the year was inserted.
     * @return The complete business key string.
     */
    private String insertYearIntoKey(String formattedPrefix, Long sequenceId, YearPosition yearPosition) {
        // Since the year is already inserted in the prefix based on position, simply append the sequence ID
        return formattedPrefix + "_" + String.format("%0" + 6 + "d", sequenceId);
    }

    /**
     * Persist new value for sequence, increment by allocating sequence numbers,
     * default allocation size value is 100
     *
     * @param sequence
     * @return updated {@link BusinessKeySequenceEntity}
     */
    private BusinessKeySequenceEntity askForNextSequenceRange(final BusinessKeySequenceEntity sequence) {
        BusinessKeySequenceEntity sequenceUpdated = null;
        sequenceUpdated = BusinessKeySequenceEntity.findById(em, sequence.getId());
        if (sequenceUpdated == null) {
            throw new SequenceNotFoundByIdException(sequence);
        }

        final Long currentValue = sequenceUpdated.getCurrentAllocation();
        sequenceUpdated.setCurrentAllocation(currentValue + sequenceUpdated.getAllocationSize());

        em.flush();
        em.detach(sequenceUpdated);

        sequence.setCurrentValue(currentValue);
        sequence.setCurrentAllocation(sequenceUpdated.getCurrentAllocation());

        return sequence;
    }

    /**
     * Returns the {@link BusinessKeySequenceEntity} for a specific entity (with
     * canonical name) and, if not found, creates it.
     *
     * @param canonicalName       entity class name
     * @param businessKeySequence values for creation sequence code
     * @param prefix              eventually custom format for the sequence code
     * @param isDefault           flag to set as default the sequence code
     * @return {@link BusinessKeySequenceEntity} element
     */
    private BusinessKeySequenceEntity findOrCreate(final String canonicalName, final BusinessKeySequence businessKeySequence,
                                                   final String prefix,
                                                   final Boolean isDefault) {
        final BusinessKeySequenceEntity sequence = find(canonicalName, prefix);
        return sequence != null ? sequence : create(canonicalName, businessKeySequence, prefix, isDefault);
    }

    /**
     * Returns the {@link BusinessKeySequenceEntity} instance for a specific
     * entity.
     *
     * @param canonicalName , entity class name
     * @param prefix        , optional for custom prefix
     * @return {@link BusinessKeySequenceEntity} element
     */
    private BusinessKeySequenceEntity find(final String canonicalName, final String prefix) {
        final String key = composeKey(canonicalName, prefix);

        if (sequencesBusinessKey.get(key) == null) {
            final BusinessKeySequenceEntity tmp = BusinessKeySequenceEntity.findOrNull(em, canonicalName, prefix);
            if (tmp != null) {
                em.detach(tmp);
                tmp.setCurrentValue(tmp.getCurrentAllocation());
                sequencesBusinessKey.put(key, tmp);
            }
        }
        return sequencesBusinessKey.get(key);
    }

    /**
     * Create {@link BusinessKeySequenceEntity} for a specific entity, persist
     * entity (allocating sequence numbers)
     *
     * @param canonicalName       , entity class name
     * @param businessKeySequence , values for creation sequence code
     * @param prefix              , eventually custom format for the sequence code
     * @param isDefault           , flag to set as default the sequence code
     * @return {@link BusinessKeySequenceEntity} element
     */
    private BusinessKeySequenceEntity create(final String canonicalName, final BusinessKeySequence businessKeySequence,
                                             final String prefix,
                                             final Boolean isDefault) {

        final BusinessKeySequenceEntity sequence =
            new BusinessKeySequenceEntity(canonicalName, prefix, businessKeySequence.length(),
                                          businessKeySequence.allocationSize(), 0L, isDefault);

        em.persist(sequence);
        em.flush();
        em.detach(sequence);

        final String key = composeKey(canonicalName, prefix);
        sequencesBusinessKey.put(key, sequence);

        return sequence;
    }

    protected static String composeKey(final String... args) {
        final StringBuilder sb = new StringBuilder();
        for (final String el : args) {
            sb.append(el).append(".");
        }
        // Do not append the year here; handle it in generatorSequence
        return sb.toString();
    }

    /**
     * Retrieve params to create the business key from class annotation
     *
     * @param classObject , class
     * @param prefix      , eventually the custom prefix
     * @return {@link BusinessKeySequence}
     */
    protected BusinessKeySequence getBusinessKeyAnnotation(final Class<?> classObject, final String prefix) {
        final BusinessKeySequence businessKeySequence = classObject.getAnnotation(BusinessKeySequence.class);

        final boolean getDefault = prefix != null && businessKeySequence == null;

        if (getDefault) {
            final BusinessKeySequence s = new BusinessKeySequence() {

                @Override
                public Class<? extends Annotation> annotationType() {
                    return BusinessKeySequence.class;
                }

                @Override
                public String prefix() {
                    return "U_KEY";
                }

                @Override
                public int length() {
                    return BusinessKeySequence.DEFAULT_LENGTH;
                }

                @Override
                public int allocationSize() {
                    return BusinessKeySequence.DEFAULT_ALLOCATION_SIZE;
                }

                @Override
                public boolean includeYear() {
                    return false;
                }

                @Override
                public YearPosition yearPosition() {
                    return YearPosition.SUFFIX;
                }
            };
            return s;
        }

        return businessKeySequence;
    }

}
Key Changes Highlighted:

BusinessKeySequence Annotation:
Now includes includeYear and yearPosition to control the inclusion and placement of the year.
generatorSequence Method:
Handles the optional inclusion of the year.
Determines the placement of the year based on the yearPosition enum.
Utilizes helper methods formatPrefix and insertYearIntoKey for clean and maintainable code.
Helper Methods:
formatPrefix: Formats the prefix by inserting the year based on configuration.
insertInMiddle: Inserts the year into the middle of the prefix.
insertYearIntoKey: Appends the sequence number to the formatted prefix, ensuring the year is part of the business key if included.
composeKey Method:
Simplified to exclude the automatic inclusion of the year, delegating year placement to the generatorSequence method.
getBusinessKeyAnnotation Method:
Correctly returns the annotation type in the default configuration, preventing potential NullPointerExceptions.
9. Additional Enhancements and Considerations
9.1. Handling Multiple Dynamic Elements
If you anticipate needing more dynamic elements (e.g., department codes, regions), consider enhancing the prefix formatting logic to accommodate additional placeholders.

Example Enhancement:


@BusinessKeySequence(
    prefix = "INV_%s_%s",
    length = 6,
    allocationSize = 100,
    includeYear = true,
    yearPosition = YearPosition.MIDDLE
)
public class Invoice {
    // Entity fields and methods
}
Usage:


String departmentCode = "FIN";
String year = "2024";
String businessKey = generator.generateCode(Invoice.class, departmentCode, year);
// Expected Output: "INV_FIN_2024_000001"
Note: Adjust helper methods to handle multiple placeholders as needed.

9.2. Ensuring Thread Safety and Concurrency
Ensure that the BusinessKeyGenerator is thread-safe, especially in high-concurrency environments. The synchronized keyword on the generatorSequence method helps, but you might consider more scalable concurrency controls if performance becomes a concern.

9.3. Exception Handling
Ensure that all potential exceptions are handled gracefully, providing meaningful error messages to aid in debugging.

9.4. Logging
Implement logging at key points in the BusinessKeyGenerator to track business key generation, which can be invaluable for monitoring and troubleshooting.

Example:


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// ...

@ApplicationScoped
public class BusinessKeyGenerator {

    private static final Logger logger = LoggerFactory.getLogger(BusinessKeyGenerator.class);

    // ...

    private synchronized String generatorSequence(final Class<?> classObject, final String prefix, final Object... args) {
        logger.debug("Generating business key for class: {}, prefix: {}, args: {}", classObject.getName(), prefix, args);
        
        // Existing logic
        
        logger.debug("Generated business key: {}", id);
        return id;
    }

    // ...
}
9.5. Sequence Caching Enhancements
Depending on your application's scale, you might want to implement more sophisticated caching mechanisms to handle a large number of sequences efficiently.

9.6. Externalizing Configuration
Consider externalizing configurable properties (like default prefix, year inclusion defaults, etc.) to configuration files or environment variables. This approach increases flexibility and allows changes without modifying the codebase.

10. Conclusion
By introducing the includeYear flag and the yearPosition enum within the BusinessKeySequence annotation, you've empowered the BusinessKeyGenerator to:

Optionally Include the Year: Whether or not the year should be part of the business key.
Configurable Year Placement: Control where the year appears within the business key (prefix, suffix, or middle).
Ensure Uniqueness: Guarantee that the business key remains unique by incorporating the year when necessary.
These enhancements provide a flexible and robust solution for generating business keys tailored to varying business requirements. Remember to thoroughly test each configuration scenario to ensure reliability and correctness.

If you have any further questions or need assistance with specific parts of the implementation, feel free to ask!

The problem with the first implementaiton is that i can't put the year wherever I want with respect also to the arguments, do you think it's possible to customize that?