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