Skip to content

Commit

Permalink
#87 feat: IdGenerator 생성기 구현 추가
Browse files Browse the repository at this point in the history
- 이후 책임 분리하기
  • Loading branch information
rivkode committed Jun 6, 2024
1 parent cefe694 commit 4c4cc38
Showing 1 changed file with 325 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
package com.seoultech.synergybe.domain.common.idgenerator;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.security.SecureRandom;
import java.time.Instant;
import java.util.HashMap;
import java.util.random.RandomGenerator;

@Slf4j
@Component
public class IdGeneratorRivkodeImpl implements IdGenerator {
private static final int ASCII_LOWER_CASE_START = 97;
private static final int ASCII_LOWER_CASE_END = 122;
private static final int ASCII_UPPER_CASE_START = 65;
private static final int ASCII_UPPER_CASE_END = 90;
private static final int ASCII_DIGIT_START = 48;
private static final int ASCII_DIGIT_END = 57;
private static final int TIME_NUMBER_START = 0;
private static final int TIME_UPPER_CASE_START = 10;
private static final int TIME_LOWER_CASE_START = 36;


private final HashMap<Integer, Integer> asciiMap = new HashMap<>();
private final HashMap<Integer, Character> timeMap = new HashMap<>();

public IdGeneratorRivkodeImpl() {
initializeAsciiMap();
initializeTimeMap();
}

public String generateId(IdPrefix idPrefix) {
// 생성 시간
Instant createAt = Instant.now();

// 난수
String randomString = createRandomStr(4, false);
Integer pseudo = createPseudo();

// hash값
String hash = createSecureHash(randomString, pseudo);

// 시간 bit
String timeBit = calculateInstant(createAt);

String generatedId = timeBit + "-" + idPrefix.getValue() + "-" + hash; // ex) generatedId : 244C85-user-XNc9rQ4i
return generatedId;
}

public Integer createPseudo() {
RandomGenerator generator = RandomGenerator.of("L128X256MixRandom");
Integer pseudo = generator.nextInt(10000);
return pseudo;
}

/**
* Instant에서 년, 월, 일, 시, 분을 추출
* 년도는 숫자 그대로 나타내며
* 월, 일, 시, 분에 대해 timeMap에서 값을 가져와 String 생성
* timeMap은 0-9 / A-Z / a-z 순으로 이루어진 hashMap
* 이렇게 함으로써 시간순으로 정렬이 가능하도록 됨
* @param createAt
* @return
*/
private String calculateInstant(Instant createAt) {

int year = createAt.atZone(java.time.ZoneOffset.UTC).getYear();
int yearSecondDigit = year % 100;
int month = createAt.atZone(java.time.ZoneOffset.UTC).getMonthValue();
int day = createAt.atZone(java.time.ZoneOffset.UTC).getDayOfMonth();
int hour = createAt.atZone(java.time.ZoneOffset.UTC).getHour();
int minute = createAt.atZone(java.time.ZoneOffset.UTC).getMinute();

String total = String.valueOf(yearSecondDigit);

Character CMonth = timeMap.getOrDefault(month, '0');
Character CDay = timeMap.getOrDefault(day, '0');
Character CHour = timeMap.getOrDefault(hour, '0');
Character CMinute = timeMap.getOrDefault(minute, '0');

total = total + CMonth + CDay + CHour + CMinute;
log.info(total);

return total;
}

/**
* 자릿수(length) 만큼 랜덤한 문자열을 대문자/소문자에 따라 반환 받습니다.
*
* @param length 자릿수
* @param isUpperCase 대문자 여부
* @return 랜덤한 문자열
*/
public String createRandomStr(int length, boolean isUpperCase) {
String alphabet = "abcdefghijklmnopqrstuvwxyz";
SecureRandom secureRandom = new SecureRandom();
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append(alphabet.charAt(secureRandom.nextInt(alphabet.length())));
}
return isUpperCase ? sb.toString().toUpperCase() : sb.toString().toLowerCase();
}

/**
* createSecureHash 함수 동작 원리
* ---- String -> 2진수 변환 시작 ----
* 1. name, createAt을 입력
* 2. 이름의 끝 4자리 + 날짜 끝 4자리를 사용
* 3. 이름날짜이름날짜... 순으로 서로를 번갈아가며 1개의 조합을 만들고 String에 저장
* 4. 해당 String을 char단위로 나누어 저장
* 5. char들을 char -> 16진수 -> 10진수 -> 2진수 순으로 변환
* 5. 변환한 값을 4bit 만큼 left shift 수행
* 6. 왼쪽으로 shift하고 남은 자리인 4bit에 대해 입력받은 name의 길이정보를 2진수로 변환하여 넣음
* 7. leftshit를 1bit만큼 다시 수행
* 8. ---- 2진수로 변환 완료 ----
*
* ---- 2진수 -> String 변환 시작 ----
* 9. 2진수에 대해 숫자, 알파벳으로 변환하기 위해 ASCII 범위(0-127) 내에 속하는지 체크 (8bit는 256만큼 표현이 가능하므로)
* 10. 속하지 않는다면 1의 보수법으로 변환
* 11. 숫자, 글자범위에 속하는지 체크
* 12. 속하지 않는다면 hashMap으로 매핑
* 13. 2진수 -> 10진수 -> String 순으로 변환
* ---- String으로 변환 완료 ----
*
*
* @param name
* @param pseudo
*/
private String createSecureHash(String name, Integer pseudo) {
StringBuffer hexSb = new StringBuffer();
StringBuffer binarySb = new StringBuffer();

String lastFourChar = name.substring(name.length() - 4);
String SPseudo = String.format("%4s", pseudo).replace(' ', '0');

String mixedString = mixString(lastFourChar, SPseudo);
// log.info("mixedString : " + mixedString);

int j = 0;
for (int i = 0; i < 8; i++) {
char c = mixedString.charAt(i);
// log.info("c : " + c);
String hex = Integer.toHexString(c);
hexSb.append(hex.charAt(0));
hexSb.append(Character.isDigit(hex.charAt(1)) ? hex.charAt(1) : Character.toUpperCase(hex.charAt(1)));
// log.info("hex : " + hex);

// log.info("StringBuffer : " + hexSb.charAt(j));
// log.info("StringBuffer : " + hexSb.charAt(j + 1));
j +=2;
}
// log.info("end StringBuffer-----------------------------------------");

for (int i = 0; i < 16; i++) {
int value;
char ch = hexSb.charAt(i);
// log.info("ch : " + ch);
if (Character.isDigit(ch)) {
value = Character.digit(ch, 16);
} else {
value = Character.digit(ch, 16);
}
// log.info("value : " + value);
String binary = String.format("%4s", Integer.toBinaryString(value)).replace(' ', '0');
// log.info("final binary : " + binary);
binarySb.append(binary);
}
// log.info("end binary transfer -----------------------------------------");

int n = name.length();
String nameLengthBinary = String.format("%4s", Integer.toBinaryString(n)).replace(' ', '0');
binarySb.append(nameLengthBinary);

String binarySbString = binarySb.substring(4);
String transferedBinary = binarySbString.substring(1) + "0";
// log.info("leftShiftOneBit : " + transferedBinary);

StringBuilder transferedToChar = new StringBuilder();

for (int i = 0; i < 8; i++) {
boolean isASCII;
// 토큰에서 2진수를 총 8bit만큼 추출 -> 16진수로 만들기 위해
String binary = transferedBinary.substring(i, i + 8);
// 아스키 범위안에 속하는지 체크
if (binary.charAt(0) == '0') {
isASCII = true;
} else {
isASCII = false;
}

// 범위가 아니라면 1의 보수를 통해 아스키범위로 전환
if (!isASCII) {
binary = onesComplement(binary);
}

// log.info("isASCII : " + isASCII);
// log.info("binary ASCII : " + binary);

int first = Integer.parseInt(binary.substring(0, 4), 2);
int last = Integer.parseInt(binary.substring(4, 8), 2);
int decimalNumber = (first * (int)Math.pow(2, 4)) + last;
// log.info("first : " + first);
// log.info("last : " + last);
//
// log.info("decimalNumber : " + decimalNumber);

if (!isValidAscii(decimalNumber)) {
decimalNumber = asciiMap.get(decimalNumber);
}

// 10진수를 알파벳으로 변환
char asciiChar = (char) decimalNumber;

// StringBuffer에 추가
transferedToChar.append(asciiChar);
}

return transferedToChar.toString();
}

private boolean isValidAscii(int decimalNumber) {
return (decimalNumber >= ASCII_DIGIT_START && decimalNumber <= ASCII_DIGIT_END) ||
(decimalNumber >= ASCII_UPPER_CASE_START && decimalNumber <= ASCII_UPPER_CASE_END) ||
(decimalNumber >= ASCII_LOWER_CASE_START && decimalNumber <= ASCII_LOWER_CASE_END);
}

private String mixString(String lastFourChar, String lastFourDigits) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < 4; i++) {
result.append(lastFourChar.charAt(i));
result.append(lastFourDigits.charAt(i));
}
return result.toString();
}

// 1의 보수 계산 함수
public String onesComplement(String binary) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < binary.length(); i++) {
char bit = binary.charAt(i);
if (bit == '0') {
result.append('1');
} else if (bit == '1') {
result.append('0');
} else {
// 예외 처리: 0 또는 1이 아닌 문자가 있을 경우
throw new IllegalArgumentException("Invalid binary input");
}
}
return result.toString();
}

private void initializeTimeMap() {
char v = '0';
for (int i = TIME_NUMBER_START; i < 10; i++) {
timeMap.put(i, v);
// log.info("v : " + v);
v++;

}

v = 'A';

for (int i = TIME_UPPER_CASE_START; i < 36; i++) {
timeMap.put(i, v);
// log.info("v : " + v);
v++;

}

v = 'a';

for (int i = TIME_LOWER_CASE_START; i < 62; i++) {
timeMap.put(i, v);
// log.info("v : " + v);
v++;
}
}


private void initializeAsciiMap() {
// ASCII_DIGIT_START - 48
int v = ASCII_DIGIT_START;
for (int i = 0; i < 10; i++) {
asciiMap.put(i, v);
v++;
}

// ASCII_UPPER_CASE_START - 65
v = ASCII_UPPER_CASE_START;
for (int i = 10; i < 36; i++) {
asciiMap.put(i, v);
v++;
}

// ASCII_LOWER_CASE_START - 97
v = ASCII_LOWER_CASE_START;
for (int i = 36; i < 48; i++) {
asciiMap.put(i, v);
v++;
}

// 중간값 계산
v = 109;
for (int i = 58; i < 65; i++) {
asciiMap.put(i, v);
v++;
}

// 중간값 계산
v = 115;
for (int i = 91; i < 97; i++) {
asciiMap.put(i, v);
v++;
}

// ASCII_DIGIT_START - 48
v = ASCII_DIGIT_START;
for (int i = 123; i < 128; i++) {
asciiMap.put(i, v);
v++;
}
}
}

0 comments on commit 4c4cc38

Please sign in to comment.