001// --------------------------------------------------------------------------------
002// Copyright 2002-2026 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017package com.echothree.model.control.sequence.server.logic;
018
019import com.echothree.model.control.sequence.common.SequenceChecksumTypes;
020import com.echothree.model.control.sequence.common.SequenceEncoderTypes;
021import com.echothree.model.control.sequence.common.exception.UnimplementedSequenceChecksumTypeException;
022import com.echothree.model.control.sequence.common.exception.UnimplementedSequenceEncoderTypeException;
023import com.echothree.model.control.sequence.common.exception.UnknownSequenceNameException;
024import com.echothree.model.control.sequence.server.control.SequenceControl;
025import com.echothree.model.control.sequence.server.logic.checksum.Mod10SequenceChecksum;
026import com.echothree.model.control.sequence.server.logic.checksum.Mod36SequenceChecksum;
027import com.echothree.model.control.sequence.server.logic.checksum.NoneSequenceChecksum;
028import com.echothree.model.control.sequence.server.logic.checksum.SequenceChecksum;
029import com.echothree.model.control.sequence.server.logic.encoder.NoneSequenceEncoder;
030import com.echothree.model.control.sequence.server.logic.encoder.ReverseSequenceEncoder;
031import com.echothree.model.control.sequence.server.logic.encoder.ReverseSwapSequenceEncoder;
032import com.echothree.model.data.sequence.server.entity.Sequence;
033import com.echothree.model.data.sequence.server.entity.SequenceDetail;
034import com.echothree.model.data.sequence.server.entity.SequenceType;
035import com.echothree.model.data.sequence.server.entity.SequenceTypeDetail;
036import com.echothree.util.common.message.ExecutionErrors;
037import com.echothree.util.server.cdi.CommandScopeExtension;
038import com.echothree.util.server.control.BaseLogic;
039import com.echothree.util.server.message.ExecutionErrorAccumulator;
040import java.util.ArrayDeque;
041import java.util.Deque;
042import java.util.EmptyStackException;
043import java.util.NoSuchElementException;
044import java.util.concurrent.ConcurrentHashMap;
045import java.util.concurrent.ConcurrentMap;
046import java.util.regex.Pattern;
047import javax.enterprise.context.ApplicationScoped;
048import javax.enterprise.inject.spi.CDI;
049import javax.inject.Inject;
050
051@ApplicationScoped
052public class SequenceGeneratorLogic
053        extends BaseLogic {
054
055    @Inject
056    SequenceControl sequenceControl;
057
058    @Inject
059    SequenceTypeLogic sequenceTypeLogic;
060
061    protected SequenceGeneratorLogic() {
062        super();
063    }
064
065    public static SequenceGeneratorLogic getInstance() {
066        return CDI.current().select(SequenceGeneratorLogic.class).get();
067    }
068
069    // --------------------------------------------------------------------------------
070    //   Generation
071    // --------------------------------------------------------------------------------
072
073    public final static String NUMERIC_VALUES = "0123456789";
074    public final static int NUMERIC_MAX_INDEX = NUMERIC_VALUES.length() - 1;
075    public final static String ALPHABETIC_VALUES = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
076    public final static int ALPHABETIC_MAX_INDEX = ALPHABETIC_VALUES.length() - 1;
077    public final static String ALPHANUMERIC_VALUES = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
078    public final static int ALPHANUMERIC_MAX_INDEX = ALPHANUMERIC_VALUES.length() - 1;
079
080    private final static int DEFAULT_CHUNK_SIZE = 10;
081
082    private final static ConcurrentMap<Long, Deque<String>> sequenceDeques = new ConcurrentHashMap<>();
083
084    private int getChunkSize(SequenceTypeDetail sequenceTypeDetail, SequenceDetail sequenceDetail) {
085        var chunkSize = sequenceDetail.getChunkSize();
086
087        if(chunkSize == null) {
088            chunkSize = sequenceTypeDetail.getChunkSize();
089        }
090
091        return chunkSize == null ? DEFAULT_CHUNK_SIZE : chunkSize;
092    }
093
094    // If the SequenceEncoders are ever modified to do anything other than swap characters
095    // around, getPattern(...) will need to have a special version of this. Right now, as it
096    // is used to identify Sequences based on the different masks, those masks must be altered
097    // to generate a regular expressions that's properly formatted for what's done here when
098    // creating new encoded values during generation.
099    private String encode(SequenceTypeDetail sequenceTypeDetail, String value) {
100        var sequenceEncoderTypeName = sequenceTypeDetail.getSequenceEncoderType().getSequenceEncoderTypeName();
101        var sequenceEncoderType = SequenceEncoderTypes.valueOf(sequenceEncoderTypeName);
102
103        return switch(sequenceEncoderType) {
104            case NONE -> NoneSequenceEncoder.getInstance().encode(value);
105            case REVERSE -> ReverseSequenceEncoder.getInstance().encode(value);
106            case REVERSE_SWAP -> ReverseSwapSequenceEncoder.getInstance().encode(value);
107            default -> throw new UnimplementedSequenceEncoderTypeException();
108        };
109    }
110
111    private SequenceChecksum getSequenceChecksum(SequenceChecksumTypes sequenceChecksumType) {
112        return switch(sequenceChecksumType) {
113            case NONE -> NoneSequenceChecksum.getInstance();
114            case MOD_10 -> Mod10SequenceChecksum.getInstance();
115            case MOD_36 -> Mod36SequenceChecksum.getInstance();
116            default -> throw new UnimplementedSequenceChecksumTypeException();
117        };
118    }
119
120    private SequenceChecksum getSequenceChecksum(SequenceTypeDetail sequenceTypeDetail) {
121        var sequenceChecksumType = sequenceTypeDetail.getSequenceChecksumType();
122        SequenceChecksum result = null;
123
124        // Needs to be very careful as the SequenceChecksumType may be null for a given SequenceTypeDetail.
125        // If it is, we'll just leave sequenceChecksum null and return that.
126        if(sequenceChecksumType != null) {
127            var sequenceChecksumTypeName = sequenceChecksumType.getSequenceChecksumTypeName();
128
129            result = getSequenceChecksum(SequenceChecksumTypes.valueOf(sequenceChecksumTypeName));
130        }
131
132        return result;
133    }
134
135    /**
136     * Generate and return the next value for a given Sequence.
137     * 
138     * @return A unique value for the sequence is returned. Null will be returned when the
139     * sequence is exhausted, the length of the mask is not equal to the length of the
140     * value, or an invalid character is encountered in the mask.
141     */
142    public String getNextSequenceValue(Sequence sequence) {
143        var sequenceEntityId = sequence.getPrimaryKey().getEntityId();
144        var sequenceDeque = sequenceDeques.get(sequenceEntityId);
145        String result = null;
146
147        if(sequenceDeque == null) {
148            // Create a new sequenceDeque (aka. a LinkedList), and try to put it into sequenceDeques.
149            // If it is already there, the new one is discarded, and the one that was already there
150            // is returned.
151            var newSequenceDeque = new ArrayDeque<String>();
152
153            sequenceDeque = sequenceDeques.putIfAbsent(sequenceEntityId, newSequenceDeque);
154            if(sequenceDeque == null) {
155                sequenceDeque = newSequenceDeque;
156            }
157        }
158
159        synchronized(sequenceDeque) {
160            try {
161                result = sequenceDeque.removeFirst();
162            } catch (NoSuchElementException nsee1) {
163                try(var ignored = CommandScopeExtension.getCommandScopeContext().push()) {
164                    var sequenceValue = sequenceControl.getSequenceValueForUpdate(sequence);
165
166                    if(sequenceValue != null) {
167                        var sequenceDetail = sequence.getLastDetail();
168                        var sequenceTypeDetail = sequenceDetail.getSequenceType().getLastDetail();
169                        var prefix = sequenceTypeDetail.getPrefix();
170                        var suffix = sequenceTypeDetail.getSuffix();
171                        var chunkSize = getChunkSize(sequenceTypeDetail, sequenceDetail);
172                        var mask = sequenceDetail.getMask();
173                        var maskChars = mask.toCharArray();
174                        var value = sequenceValue.getValue();
175                        var valueLength = value.length();
176                        var valueChars = value.toCharArray();
177
178                        // Mask and its value must be the same length.
179                        if(valueLength == mask.length()) {
180                            for(var i = 0; i < chunkSize; i++) {
181                                // Step through the string from the right to the left.
182                                var forceIncrement = false;
183
184                                for(var index = valueLength - 1; index > -1; index--) {
185                                    var maskChar = maskChars[index];
186                                    var valueChar = valueChars[index];
187
188                                    switch(maskChar) {
189                                        case '9' -> {
190                                            var currentIndex = NUMERIC_VALUES.indexOf(valueChar);
191                                            if(currentIndex != -1) {
192                                                int newCharIndex;
193                                                if(currentIndex == NUMERIC_MAX_INDEX) {
194                                                    newCharIndex = 0;
195                                                    forceIncrement = true;
196                                                } else {
197                                                    newCharIndex = currentIndex + 1;
198                                                }
199                                                valueChars[index] = NUMERIC_VALUES.charAt(newCharIndex);
200                                            } else {
201                                                value = null;
202                                            }
203                                        }
204                                        case 'A' -> {
205                                            var currentIndex = ALPHABETIC_VALUES.indexOf(valueChar);
206                                            if(currentIndex != -1) {
207                                                int newCharIndex;
208                                                if(currentIndex == ALPHABETIC_MAX_INDEX) {
209                                                    newCharIndex = 0;
210                                                    forceIncrement = true;
211                                                } else {
212                                                    newCharIndex = currentIndex + 1;
213                                                }
214                                                valueChars[index] = ALPHABETIC_VALUES.charAt(newCharIndex);
215                                            } else {
216                                                value = null;
217                                            }
218                                        }
219                                        case 'Z' -> {
220                                            var currentIndex = ALPHANUMERIC_VALUES.indexOf(valueChar);
221                                            if(currentIndex != -1) {
222                                                int newCharIndex;
223                                                if(currentIndex == ALPHANUMERIC_MAX_INDEX) {
224                                                    newCharIndex = 0;
225                                                    forceIncrement = true;
226                                                } else {
227                                                    newCharIndex = currentIndex + 1;
228                                                }
229                                                valueChars[index] = ALPHANUMERIC_VALUES.charAt(newCharIndex);
230                                            } else {
231                                                value = null;
232                                            }
233                                        }
234                                    }
235
236                                    // If an error occurred, or we do not need to increment any other positions in
237                                    // the sequences value, exit.
238                                    if((value == null) || !forceIncrement) {
239                                        break;
240                                    }
241
242                                    // If we reach the start of the sequences value, and have not yet exited, the
243                                    // sequence is at its maximum possible value, exit.
244                                    if(index == 0) {
245                                        value = null;
246                                    }
247
248                                    forceIncrement = false;
249                                }
250
251                                if(value != null) {
252                                    value = new String(valueChars);
253
254                                    var encodedValue = encode(sequenceTypeDetail, value);
255
256                                    var intermediateValue = (prefix != null ? prefix : "") + encodedValue + (suffix != null ? suffix : "");
257                                    var checksum = getSequenceChecksum(sequenceTypeDetail).calculate(intermediateValue);
258
259                                    sequenceDeque.add(intermediateValue + checksum);
260                                }
261                            }
262
263                            sequenceValue.setValue(value);
264
265                            try {
266                                result = sequenceDeque.removeFirst();
267                            } catch(EmptyStackException ese2) {
268                                // Shouldn't happen, if it does, result stays null
269                            }
270                        }
271                    }
272                }
273            }
274        }
275
276        return result;
277    }
278
279    public String getNextSequenceValue(final ExecutionErrorAccumulator eea, final Sequence sequence) {
280        return getNextSequenceValue(sequence);
281    }
282
283    public String getNextSequenceValue(final ExecutionErrorAccumulator eea, final SequenceType sequenceType) {
284        var sequence = getDefaultSequence(eea, sequenceType);
285
286        return hasExecutionErrors(eea) ? null : getNextSequenceValue(eea, sequence);
287    }
288
289    public String getNextSequenceValue(final ExecutionErrorAccumulator eea, final String sequenceTypeName) {
290        var sequence = getDefaultSequence(eea, sequenceTypeName);
291
292        return hasExecutionErrors(eea) ? null : getNextSequenceValue(eea, sequence);
293    }
294
295    public Sequence getDefaultSequence(final ExecutionErrorAccumulator eea, final SequenceType sequenceType) {
296        var sequence = sequenceControl.getDefaultSequence(sequenceType);
297
298        if(sequence == null) {
299            handleExecutionError(UnknownSequenceNameException.class, eea, ExecutionErrors.MissingDefaultSequence.name(), sequenceType.getLastDetail().getSequenceTypeName());
300        }
301
302        return sequence;
303    }
304
305    public Sequence getDefaultSequence(final ExecutionErrorAccumulator eea, final String sequenceTypeName) {
306        var sequenceType = sequenceTypeLogic.getSequenceTypeByName(eea, sequenceTypeName);
307        Sequence sequence = null;
308
309        if(!hasExecutionErrors(eea)) {
310            sequence = getDefaultSequence(eea, sequenceType);
311        }
312
313        return sequence;
314    }
315
316    // --------------------------------------------------------------------------------
317    //   Identification
318    // --------------------------------------------------------------------------------
319
320    private StringBuilder getPatternFromMask(final String mask) {
321        var maskChars = mask.toCharArray();
322        var pattern = new StringBuilder();
323
324        for(var maskChar : maskChars) {
325            switch(maskChar) {
326                case '9' -> pattern.append("[\\p{Digit}]");
327                case 'A' -> pattern.append("\\p{Upper}");
328                case 'Z' -> pattern.append("[\\p{Upper}\\p{Digit}]");
329            }
330        }
331
332        return pattern;
333    }
334
335    private String getPattern(final Sequence sequence) {
336        var pattern = new StringBuilder("^");
337        var sequenceDetail = sequence.getLastDetail();
338        var sequenceTypeDetail = sequenceDetail.getSequenceType().getLastDetail();
339        var prefix = sequenceTypeDetail.getPrefix();
340        var suffix = sequenceTypeDetail.getSuffix();
341        var mask = sequenceDetail.getMask();
342
343        if(prefix != null) {
344            pattern.append(Pattern.quote(prefix));
345        }
346
347        var encodedMask = encode(sequenceTypeDetail, mask);
348        pattern.append(getPatternFromMask(encodedMask));
349
350        if(suffix != null) {
351            pattern.append(Pattern.quote(suffix));
352        }
353
354        pattern.append(getSequenceChecksum(sequenceTypeDetail).regexp());
355
356        return pattern.append('$').toString();
357    }
358
359    public SequenceType identifySequenceType(final String value) {
360        var sequenceTypes = sequenceControl.getSequenceTypes();
361        SequenceType result = null;
362
363        // Check all Sequence Types...
364        for(var sequenceType : sequenceTypes) {
365            // ...and each Sequence within them.
366            for(var sequence : sequenceControl.getSequencesBySequenceType(sequenceType)) {
367                // Check the regexp that's generated for this sequence against the value.
368                if(value.matches(getPattern(sequence))) {
369                    // If the regexp matches, check the checksum. If it matches, we've
370                    // probably got a match. If not, continue looking for other matches.
371                    if(verifyValue(sequenceType, value)) {
372                        result = sequenceType;
373                        break;
374                    }
375                }
376            }
377
378            // If the SequenceType was found, break out of the outer for loop as well.
379            if(result != null) {
380                break;
381            }
382        }
383
384        return result;
385    }
386
387    // --------------------------------------------------------------------------------
388    //   Verification
389    // --------------------------------------------------------------------------------
390
391    public boolean verifyValue(final SequenceType sequenceType, final String value) {
392        var sequenceTypeDetail = sequenceType.getLastDetail();
393        var sequenceChecksum = getSequenceChecksum(sequenceTypeDetail);
394
395        return sequenceChecksum == null || sequenceChecksum.verify(value);
396    }
397
398}