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 com.echothree.util.server.persistence.Session; 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 try(var ignored = CommandScopeExtension.getCommandScopeContext().push()) { 167 var sequenceControl = Session.getModelController(SequenceControl.class); 168 var sequenceValue = sequenceControl.getSequenceValueForUpdate(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 } 278 } 279 280 return result; 281 } 282 283 public String getNextSequenceValue(final ExecutionErrorAccumulator eea, final Sequence sequence) { 284 return getNextSequenceValue(sequence); 285 } 286 287 public String getNextSequenceValue(final ExecutionErrorAccumulator eea, final SequenceType sequenceType) { 288 var sequence = getDefaultSequence(eea, sequenceType); 289 290 return hasExecutionErrors(eea) ? null : getNextSequenceValue(eea, sequence); 291 } 292 293 public String getNextSequenceValue(final ExecutionErrorAccumulator eea, final String sequenceTypeName) { 294 var sequence = getDefaultSequence(eea, sequenceTypeName); 295 296 return hasExecutionErrors(eea) ? null : getNextSequenceValue(eea, sequence); 297 } 298 299 public Sequence getDefaultSequence(final ExecutionErrorAccumulator eea, final SequenceType sequenceType) { 300 var sequenceControl = Session.getModelController(SequenceControl.class); 301 var sequence = sequenceControl.getDefaultSequence(sequenceType); 302 303 if(sequence == null) { 304 handleExecutionError(UnknownSequenceNameException.class, eea, ExecutionErrors.MissingDefaultSequence.name(), sequenceType.getLastDetail().getSequenceTypeName()); 305 } 306 307 return sequence; 308 } 309 310 public Sequence getDefaultSequence(final ExecutionErrorAccumulator eea, final String sequenceTypeName) { 311 var sequenceType = SequenceTypeLogic.getInstance().getSequenceTypeByName(eea, sequenceTypeName); 312 Sequence sequence = null; 313 314 if(!hasExecutionErrors(eea)) { 315 sequence = getDefaultSequence(eea, sequenceType); 316 } 317 318 return sequence; 319 } 320 321 // -------------------------------------------------------------------------------- 322 // Identification 323 // -------------------------------------------------------------------------------- 324 325 private StringBuilder getPatternFromMask(final String mask) { 326 var maskChars = mask.toCharArray(); 327 var pattern = new StringBuilder(); 328 329 for(var maskChar : maskChars) { 330 switch(maskChar) { 331 case '9' -> pattern.append("[\\p{Digit}]"); 332 case 'A' -> pattern.append("\\p{Upper}"); 333 case 'Z' -> pattern.append("[\\p{Upper}\\p{Digit}]"); 334 } 335 } 336 337 return pattern; 338 } 339 340 private String getPattern(final Sequence sequence) { 341 var pattern = new StringBuilder("^"); 342 var sequenceDetail = sequence.getLastDetail(); 343 var sequenceTypeDetail = sequenceDetail.getSequenceType().getLastDetail(); 344 var prefix = sequenceTypeDetail.getPrefix(); 345 var suffix = sequenceTypeDetail.getSuffix(); 346 var mask = sequenceDetail.getMask(); 347 348 if(prefix != null) { 349 pattern.append(Pattern.quote(prefix)); 350 } 351 352 var encodedMask = encode(sequenceTypeDetail, mask); 353 pattern.append(getPatternFromMask(encodedMask)); 354 355 if(suffix != null) { 356 pattern.append(Pattern.quote(suffix)); 357 } 358 359 pattern.append(getSequenceChecksum(sequenceTypeDetail).regexp()); 360 361 return pattern.append('$').toString(); 362 } 363 364 public SequenceType identifySequenceType(final String value) { 365 var sequenceControl = Session.getModelController(SequenceControl.class); 366 var sequenceTypes = sequenceControl.getSequenceTypes(); 367 SequenceType result = null; 368 369 // Check all Sequence Types... 370 for(var sequenceType : sequenceTypes) { 371 // ...and each Sequence within them. 372 for(var sequence : sequenceControl.getSequencesBySequenceType(sequenceType)) { 373 // Check the regexp that's generated for this sequence against the value. 374 if(value.matches(getPattern(sequence))) { 375 // If the regexp matches, check the checksum. If it matches, we've 376 // probably got a match. If not, continue looking for other matches. 377 if(verifyValue(sequenceType, value)) { 378 result = sequenceType; 379 break; 380 } 381 } 382 } 383 384 // If the SequenceType was found, break out of the outer for loop as well. 385 if(result != null) { 386 break; 387 } 388 } 389 390 return result; 391 } 392 393 // -------------------------------------------------------------------------------- 394 // Verification 395 // -------------------------------------------------------------------------------- 396 397 public boolean verifyValue(final SequenceType sequenceType, final String value) { 398 var sequenceTypeDetail = sequenceType.getLastDetail(); 399 var sequenceChecksum = getSequenceChecksum(sequenceTypeDetail); 400 401 return sequenceChecksum == null || sequenceChecksum.verify(value); 402 } 403 404}