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