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}