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}