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}