21D 작업

import * as fs from 'fs/promises' import * as path from 'path' import * as vscode from 'vscode' type CryptoTarget = { tableName: string plainColumn: string cryptoType: string encColumn: string hashColumn: string javaProperty: string encryptTypeHandler: string hashTypeHandler: string } type ConvertRule = { before: string tobe: string tableMustExist: boolean } type ConvertPlaceholder = { key: string htype?: string } type ConvertCaptureResult = { captured: Map targets: CryptoTarget[] } type ResultMapEntry = { kind: 'id' | 'result' column: string property: string typeHandler?: string } type ResultMapDefinition = { id: string type: string entries: ResultMapEntry[] } type MapperItem = { tag: 'insert' | 'update' | 'select' | 'delete' id: string attrs: string text: string } type MapperItemRange = { start: number end: number text: string } const LEGACY_DECRYPT_TARGET_COMMENT_PATTERN = String.raw`/\*\s*복호화 대상\s*\*/` const DECRYPT_TARGET_COMMENT_PATTERN = String.raw`/\* \[KMS-CIPHER\]\s*수정일자:\s*\d{4}-\d{2}-\d{2}\s*수정자\s*:\s*[^*]*\*/` const CRYPTO_TARGET_CHECK_COMMENT = '/* @@@ 암호화 대상 확인 필요 @@@ */' const CONVERT_XML_PATH = 'c:/xml/convert.xml' export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('cryptoMapper.copyTypeHandlerRecommendation', async () => { try { await applySelectedSelectColumnTypeHandlerRecommendation() } catch (error) { const message = error instanceof Error ? error.message : String(error) vscode.window.showErrorMessage(`Crypto Mapper 추천 생성 실패: ${message}`) } }), vscode.commands.registerCommand('cryptoMapper.copyFullFileTypeHandlerRecommendation', async () => { try { await copyFullFileTypeHandlerRecommendation() } catch (error) { const message = error instanceof Error ? error.message : String(error) vscode.window.showErrorMessage(`Crypto Mapper full XML recommendation failed: ${message}`) } }), vscode.commands.registerCommand('cryptoMapper.markCryptoTargetColumns', async () => { try { await markCryptoTargetColumnsInFullFile() } catch (error) { const message = error instanceof Error ? error.message : String(error) vscode.window.showErrorMessage(`Crypto Mapper target mark failed: ${message}`) } }), vscode.commands.registerCommand('cryptoMapper.markCryptoTargetColumnsInCurrentItem', async () => { try { await markCryptoTargetColumnsInCurrentItem() } catch (error) { const message = error instanceof Error ? error.message : String(error) vscode.window.showErrorMessage(`Crypto Mapper current item target mark failed: ${message}`) } }), vscode.commands.registerCommand('cryptoMapper.applyConvertXmlRulesToSelection', async () => { try { await applyConvertXmlRulesToSelection() } catch (error) { const message = error instanceof Error ? error.message : String(error) vscode.window.showErrorMessage(`Crypto Mapper convert.xml replace failed: ${message}`) } }), ) } export function deactivate() { // no-op } async function applySelectedSelectColumnTypeHandlerRecommendation() { const editor = vscode.window.activeTextEditor if (!editor) throw new Error('No active editor.') if (editor.selection.isEmpty) throw new Error('Ctrl+Shift+5 needs a selected select-column range.') const originalXml = editor.document.getText() const selectionStart = editor.document.offsetAt(editor.selection.start) const selectionEnd = editor.document.offsetAt(editor.selection.end) const focusedItem = findMapperItemRangeAtOffset(originalXml, selectionStart) if (!focusedItem || selectionEnd > focusedItem.end) { throw new Error('Select columns inside one mapper item, then press Ctrl+Shift+5.') } const mapperItem = parseSelectedMapperItem(focusedItem.text) if (mapperItem.tag !== 'select') { throw new Error('Ctrl+Shift+5 selected-column conversion supports , 중 하나의 XML 태그 전체를 선택해야 합니다.') } return { tag: match[1].toLowerCase() as MapperItem['tag'], attrs: match[2], id: attrValue(match[2], 'id') || 'anonymousSql', text, } } function buildRecommendation( mapperItem: MapperItem, selectedText: string, mapperXml: string, matchedTargets: CryptoTarget[], date: string, markerUserId: string, ) { const tag = mapperItem.tag.toUpperCase() const warnings: string[] = [] let afterSql = selectedText let supportSnippets: string[] = [] if (tag === 'INSERT') { afterSql = matchedTargets.reduce((next, target) => addInsertEncryption(next, target, date), afterSql) } else if (tag === 'UPDATE') { afterSql = matchedTargets.reduce((next, target) => addUpdateEncryption(next, target, date), afterSql) } else if (tag === 'SELECT') { afterSql = addSelectEncryption(selectedText, matchedTargets, date, markerUserId) if (hasDecryptTargetComment(afterSql)) { const desiredResultMap = desiredResultMapDefinition(mapperItem, selectedText, mapperXml, matchedTargets) const existingResultMap = findExistingResultMap(mapperXml, desiredResultMap) const resultMapId = existingResultMap?.id || desiredResultMap.id afterSql = applySelectResultMap(afterSql, resultMapId) if (!existingResultMap) { supportSnippets = [resultMapSnippet(desiredResultMap)] warnings.push(`resultMap 추가 추천: ${desiredResultMap.id}`) } else { warnings.push(`기존 resultMap 재사용: ${existingResultMap.id}`) } } } else { afterSql = matchedTargets.reduce((next, target) => addWhereHashRewrite(next, target, date, markerUserId), afterSql) } if (afterSql === selectedText && supportSnippets.length === 0) { throw new Error('대상은 찾았지만 적용 가능한 치환 지점을 찾지 못했습니다.') } const header = [ ``, ].filter(Boolean).join('\n') const clipboardText = [header, afterSql.trim(), ...supportSnippets.map((snippet) => snippet.trim())].join('\n\n') return { clipboardText, supportSnippets } } function buildFullFileRecommendation(mapperXml: string, targets: CryptoTarget[], date: string, markerUserId: string) { const itemPattern = /<(insert|update|select|delete)\b([^>]*)>[\s\S]*?<\/\1>/gi const supportSnippets: string[] = [] const chunks: string[] = [] let changedItemCount = 0 let lastIndex = 0 let mapperXmlForLookup = mapperXml for (const match of mapperXml.matchAll(itemPattern)) { const itemText = match[0] const index = match.index ?? 0 chunks.push(mapperXml.slice(lastIndex, index)) const mapperItem = parseSelectedMapperItem(itemText) const matchedTargets = targets.filter((target) => matchesTarget(itemText, target, mapperItem.tag)) if (matchedTargets.length === 0) { chunks.push(itemText) lastIndex = index + itemText.length continue } const rewrite = rewriteMapperItemForFullFile(mapperItem, itemText, mapperXmlForLookup, matchedTargets, date, markerUserId) chunks.push(rewrite.afterSql) if (rewrite.afterSql !== itemText || rewrite.supportSnippets.length > 0) changedItemCount += 1 for (const snippet of rewrite.supportSnippets) { if (appendUniqueResultMapSnippet(supportSnippets, mapperXmlForLookup, snippet)) { mapperXmlForLookup += `\n${snippet}` } } lastIndex = index + itemText.length } chunks.push(mapperXml.slice(lastIndex)) if (changedItemCount === 0) { throw new Error('No mapper item matched crypto targets or no rewrite point was found.') } const rewrittenXml = insertResultMapsAtMapperTop(chunks.join(''), supportSnippets) return { clipboardText: rewrittenXml, changedItemCount, supportSnippets, } } function markCryptoTargetColumns(mapperXml: string, targets: CryptoTarget[]) { const itemPattern = /<(insert|update|select|delete)\b([^>]*)>[\s\S]*?<\/\1>/gi const chunks: string[] = [] let changedCount = 0 let lastIndex = 0 for (const match of mapperXml.matchAll(itemPattern)) { const itemText = match[0] const index = match.index ?? 0 chunks.push(mapperXml.slice(lastIndex, index)) const result = markCryptoTargetColumnsInMapperItem(itemText, targets) changedCount += result.changedCount chunks.push(result.text) lastIndex = index + itemText.length } chunks.push(mapperXml.slice(lastIndex)) return { text: chunks.join(''), changedCount } } function markCryptoTargetColumnsInMapperItem(itemText: string, targets: CryptoTarget[]) { let nextItemText = itemText let changedCount = 0 const matchedTargets = targets.filter((target) => hasBoundaryToken(itemText, target.tableName) && hasBoundaryToken(itemText, target.plainColumn)) for (const target of matchedTargets) { const result = markCryptoTargetColumnInItem(nextItemText, target) nextItemText = result.text changedCount += result.changedCount } return { text: nextItemText, changedCount } } function findMapperItemRangeAtOffset(mapperXml: string, offset: number): MapperItemRange | undefined { const itemPattern = /<(insert|update|select|delete)\b([^>]*)>[\s\S]*?<\/\1>/gi for (const match of mapperXml.matchAll(itemPattern)) { const start = match.index ?? 0 const end = start + match[0].length if (offset >= start && offset <= end) { return { start, end, text: match[0] } } } return undefined } function markCryptoTargetColumnInItem(itemText: string, target: CryptoTarget) { const commentRanges = commentTokenRanges(itemText) let changedCount = 0 const pattern = new RegExp( `(^|[^A-Za-z0-9_\\.])((?:[A-Za-z_][A-Za-z0-9_]*\\s*\\.\\s*)?${escapeRegExp(target.plainColumn)})(?=$|[^A-Za-z0-9_])`, 'gi', ) const text = itemText.replace(pattern, (match: string, prefix: string, column: string, offset: number) => { const columnStart = offset + prefix.length const afterMatch = itemText.slice(offset + match.length) if (isInsideRanges(columnStart, commentRanges)) return match if (new RegExp(`^\\s*${escapeRegExp(CRYPTO_TARGET_CHECK_COMMENT)}`).test(afterMatch)) return match changedCount += 1 return `${prefix}${column} ${CRYPTO_TARGET_CHECK_COMMENT}` }) return { text, changedCount } } function commentTokenRanges(text: string) { const ranges: Array<{ start: number; end: number }> = [] const commentPattern = /|\/\*[\s\S]*?\*\//g for (const match of text.matchAll(commentPattern)) { const start = match.index ?? 0 ranges.push({ start, end: start + match[0].length }) } return ranges } function isInsideRanges(index: number, ranges: Array<{ start: number; end: number }>) { return ranges.some((range) => index >= range.start && index < range.end) } function rewriteMapperItemForFullFile( mapperItem: MapperItem, itemText: string, mapperXml: string, matchedTargets: CryptoTarget[], date: string, markerUserId: string, ) { const tag = mapperItem.tag.toUpperCase() let afterSql = itemText let supportSnippets: string[] = [] if (tag === 'INSERT') { afterSql = matchedTargets.reduce((next, target) => addInsertEncryption(next, target, date), afterSql) } else if (tag === 'UPDATE') { afterSql = matchedTargets.reduce((next, target) => addUpdateEncryption(next, target, date), afterSql) } else if (tag === 'SELECT') { afterSql = addSelectEncryption(itemText, matchedTargets, date, markerUserId) if (hasDecryptTargetComment(afterSql)) { let desiredResultMap = desiredResultMapDefinition(mapperItem, itemText, mapperXml, matchedTargets) const existingResultMap = findExistingResultMap(mapperXml, desiredResultMap) if (!existingResultMap) { desiredResultMap = { ...desiredResultMap, id: uniqueResultMapId(desiredResultMap.id, mapperXml), } supportSnippets = [resultMapSnippet(desiredResultMap)] } afterSql = applySelectResultMap(afterSql, existingResultMap?.id || desiredResultMap.id) } } else { afterSql = matchedTargets.reduce((next, target) => addWhereHashRewrite(next, target, date, markerUserId), afterSql) } return { afterSql, supportSnippets } } function appendUniqueResultMapSnippet(snippets: string[], mapperXml: string, snippet: string) { const id = attrValue(snippet.match(/^]*)>/i)?.[1] ?? '', 'id') if (!id) return false if (parseResultMaps(mapperXml).some((resultMap) => resultMap.id === id)) return false if (snippets.some((item) => attrValue(item.match(/^]*)>/i)?.[1] ?? '', 'id') === id)) return false snippets.push(snippet) return true } function insertResultMapsAtMapperTop(mapperXml: string, supportSnippets: string[]) { if (supportSnippets.length === 0) return mapperXml const block = supportSnippets.map((snippet) => indentResultMapSnippet(snippet, ' ')).join('\n\n') return mapperXml.replace(/(]*>\s*)/i, `$1\n${block}\n\n`) } function indentResultMapSnippet(snippet: string, indent: string) { return snippet .split(/\r?\n/) .map((line) => `${indent}${line}`) .join('\n') } function uniqueResultMapId(baseId: string, mapperXml: string) { const existingIds = new Set(parseResultMaps(mapperXml).map((resultMap) => resultMap.id)) if (!existingIds.has(baseId)) return baseId for (let index = 2; index < 1000; index += 1) { const candidate = `${baseId}${index}` if (!existingIds.has(candidate)) return candidate } return `${baseId}${Date.now()}` } function addInsertEncryption(xmlText: string, target: CryptoTarget, date: string) { if (hasBoundaryToken(xmlText, target.encColumn) && (!target.hashColumn || hasBoundaryToken(xmlText, target.hashColumn))) { return xmlText } const valuesAt = xmlText.search(/\bVALUES\b/i) if (valuesAt < 0) return xmlText const head = xmlText.slice(0, valuesAt) const tail = xmlText.slice(valuesAt) const columnLine = new RegExp(`^(\\s*)${escapeRegExp(target.plainColumn)}(\\s*,?)\\s*$`, 'mi') const bindLine = new RegExp(`^(\\s*)#\\{\\s*${escapeRegExp(target.javaProperty)}\\s*\\}(\\s*,?)\\s*$`, 'mi') const columns = [target.plainColumn, target.encColumn, target.hashColumn].filter(Boolean) const binds = [ `#{${target.javaProperty}}`, withComment(typeHandlerBind(target.javaProperty, target.encryptTypeHandler), date), target.hashColumn ? withComment(typeHandlerBind(target.javaProperty, target.hashTypeHandler), date) : '', ].filter(Boolean) const nextHead = head.replace(columnLine, (_match, indent: string, comma: string) => { return columns.map((column, index) => { const suffix = index < columns.length - 1 || comma ? ',' : '' return `${indent}${column}${suffix}` }).join('\n') }) const nextTail = tail.replace(bindLine, (_match, indent: string, comma: string) => { return binds.map((bind, index) => { const suffix = index < binds.length - 1 || comma ? ',' : '' return `${indent}${bind}${suffix}` }).join('\n') }) if (nextHead !== head && nextTail !== tail) return nextHead + nextTail return replaceFirstBoundary(head, target.plainColumn, columns.join(',\n ')) + replaceFirstBoundary(tail, `#{${target.javaProperty}}`, binds.join(',\n ')) } function addUpdateEncryption(xmlText: string, target: CryptoTarget, date: string) { if (hasBoundaryToken(xmlText, target.encColumn) && (!target.hashColumn || hasBoundaryToken(xmlText, target.hashColumn))) { return xmlText } const plainAssign = new RegExp( `(^\\s*${escapeRegExp(target.plainColumn)}\\s*=\\s*#\\{\\s*${escapeRegExp(target.javaProperty)}\\s*\\}\\s*,?)(?:\\s*)?`, 'mi', ) return xmlText.replace(plainAssign, (match, plainAssignment: string) => { const indent = plainAssignment.match(/^\s*/)?.[0] ?? '' const additions = [ hasBoundaryToken(xmlText, target.encColumn) ? '' : `${indent}${target.encColumn} = ${withComment(typeHandlerBind(target.javaProperty, target.encryptTypeHandler), date)},`, target.hashColumn && !hasBoundaryToken(xmlText, target.hashColumn) ? `${indent}${target.hashColumn} = ${withComment(typeHandlerBind(target.javaProperty, target.hashTypeHandler), date)},` : '', ].filter(Boolean) if (additions.length === 0) return match const plainAssignmentWithComma = /,\s*$/.test(plainAssignment) ? plainAssignment : `${plainAssignment},` return [plainAssignmentWithComma, ...additions].join('\n') }) } function addSelectEncryption(xmlText: string, targets: CryptoTarget[], date: string, markerUserId: string) { let next = xmlText for (const target of targets) { const tableAlias = findTableAlias(next, target.tableName) const aliasPattern = new RegExp( `\\b${escapeRegExp(target.encColumn)}\\s+AS\\s+${escapeRegExp(target.plainColumn)}\\b(?:\\s*(?:${LEGACY_DECRYPT_TARGET_COMMENT_PATTERN}|${DECRYPT_TARGET_COMMENT_PATTERN}))?`, 'i', ) if (!aliasPattern.test(next)) { next = replaceFirstSelectColumn(next, target, tableAlias, date, markerUserId) } else { next = next.replace(aliasPattern, (match) => { if (decryptTargetCommentPattern().test(match)) return match return `${match.replace(legacyDecryptTargetCommentPattern(), '').trimEnd()} ${decryptTargetComment(date, markerUserId)}` }) } next = addWhereHashRewrite(next, target, date, markerUserId) } return next } function applySelectResultMap(xmlText: string, resultMapId: string) { return xmlText.replace(/^(\s*)]*)>/i, (_match, indent: string, attrs: string) => { const nextAttrs = attrs .replace(/\s+resultType\s*=\s*['"][^'"]*['"]/i, '') .replace(/\s+resultMap\s*=\s*['"][^'"]*['"]/i, '') .trimEnd() return `${indent}` }) } function hasDecryptTargetComment(xmlText: string) { return decryptTargetCommentPattern().test(xmlText) || legacyDecryptTargetCommentPattern().test(xmlText) } function decryptTargetComment(date: string, markerUserId: string) { return `/* [KMS-CIPHER] 수정일자: ${date} 수정자 : ${markerUserId} */` } function decryptTargetCommentPattern() { return new RegExp(DECRYPT_TARGET_COMMENT_PATTERN, 'i') } function legacyDecryptTargetCommentPattern() { return new RegExp(LEGACY_DECRYPT_TARGET_COMMENT_PATTERN, 'i') } function selectDecryptAlias(target: CryptoTarget, tableAlias: string, date: string, markerUserId: string) { const qualifier = tableAlias ? `${tableAlias}.` : '' return `NVL(${qualifier}${target.encColumn}, ${qualifier}${target.plainColumn}) AS ${target.plainColumn} ${decryptTargetComment(date, markerUserId)}` } function selectDecryptAliasPattern(target: CryptoTarget) { const qualifier = String.raw`(?:[A-Za-z_][A-Za-z0-9_]*\s*\.\s*)?` return new RegExp( String.raw`\bNVL\s*\(\s*${qualifier}${escapeRegExp(target.encColumn)}\s*,\s*${qualifier}${escapeRegExp(target.plainColumn)}\s*\)\s+AS\s+${escapeRegExp(target.plainColumn)}\b`, 'i', ) } function replaceFirstSelectColumn(xmlText: string, target: CryptoTarget, tableAlias: string, date: string, markerUserId: string) { if (selectDecryptAliasPattern(target).test(xmlText)) return xmlText const columnRange = selectColumnRange(xmlText) if (!columnRange) return xmlText const { columnsStart, columnsEnd } = columnRange const before = xmlText.slice(0, columnsStart) const columns = xmlText.slice(columnsStart, columnsEnd) const after = xmlText.slice(columnsEnd) const columnPattern = selectPlainColumnPattern(target) const columnItems = splitTopLevelCsvItems(columns) for (const item of columnItems) { const cleanItemText = removeDecryptTargetComments(item.text) if (!isTargetSelectColumnItem(cleanItemText, target)) continue const replacedItem = cleanItemText.replace(columnPattern, (_match, prefix: string, column: string) => { const columnAlias = column.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*\./)?.[1] ?? '' return `${prefix}${selectDecryptAlias(target, columnAlias || tableAlias, date, markerUserId)}` }) return `${before}${columns.slice(0, item.start)}${replacedItem}${columns.slice(item.end)}${after}` } return xmlText } function selectPlainColumnPattern(target: CryptoTarget) { return new RegExp( `(^|[^A-Za-z0-9_\\.])((?:[A-Za-z_][A-Za-z0-9_]*\\s*\\.\\s*)?${escapeRegExp(target.plainColumn)})(?=$|[^A-Za-z0-9_])`, 'i', ) } function isTargetSelectColumnItem(itemText: string, target: CryptoTarget) { const column = stripAlias(itemText) return equalsIgnoreCase(column, target.plainColumn) || equalsIgnoreCase(column, target.encColumn) } function findTableAlias(xmlText: string, tableName: string) { const fromMatch = xmlText.match(/\bfrom\b([\s\S]*?)(?:\bwhere\b|\bgroup\s+by\b|\border\s+by\b|\bhaving\b|\bunion\b|<\/select>|$)/i) if (!fromMatch) return '' const tablePattern = new RegExp( `\\b${escapeRegExp(tableName)}\\b\\s+(?:AS\\s+)?([A-Za-z_][A-Za-z0-9_]*)\\b`, 'i', ) const alias = fromMatch[1].match(tablePattern)?.[1] ?? '' return alias && !/^(where|inner|left|right|full|cross|join|on|group|order|having|union)$/i.test(alias) ? alias : '' } function addWhereHashRewrite(xmlText: string, target: CryptoTarget, date: string, markerUserId = '') { if (!target.hashColumn || !target.hashTypeHandler) return xmlText const whereMatch = xmlText.match(/\bwhere\b/i) if (!whereMatch || whereMatch.index === undefined) return xmlText const beforeWhereBody = xmlText.slice(0, whereMatch.index + whereMatch[0].length) const whereBody = xmlText.slice(whereMatch.index + whereMatch[0].length) const tableAlias = findTableAlias(xmlText, target.tableName) const qualifier = String.raw`(?:(?[A-Za-z_][A-Za-z0-9_]*)\s*\.\s*)?` const column = escapeRegExp(target.plainColumn) const property = escapeRegExp(target.javaProperty) const parameter = String.raw`#\{\s*${property}\s*\}` const equalsPattern = new RegExp(String.raw`${qualifier}\b${column}\b\s*=\s*${parameter}`, 'i') const likePattern = new RegExp( String.raw`${qualifier}\b${column}\b\s+LIKE\s*'%'\s*\|\|\s*${parameter}\s*\|\|\s*'%'`, 'i', ) const replaceCondition = (match: string, alias?: string) => { const conditionAlias = alias || tableAlias return hashWhereFallbackCondition(target, conditionAlias, date, markerUserId) } if (equalsPattern.test(whereBody)) { return `${beforeWhereBody}${whereBody.replace(equalsPattern, replaceCondition)}` } if (likePattern.test(whereBody)) { return `${beforeWhereBody}${whereBody.replace(likePattern, replaceCondition)}` } return xmlText } function hashWhereFallbackCondition(target: CryptoTarget, alias: string, date: string, markerUserId: string) { const qualifier = alias ? `${alias}.` : '' const hashColumn = `${qualifier}${target.hashColumn}` const plainColumn = `${qualifier}${target.plainColumn}` const marker = cipherMarkerComment(date, markerUserId) return [ `(${hashColumn} IS NOT NULL AND ${hashColumn} = ${typeHandlerBind(target.javaProperty, target.hashTypeHandler)})`, ` OR (${hashColumn} IS NULL AND UPPER(${plainColumn}) = UPPER(#{${target.javaProperty}})) ${marker}`, ].join('\n') } function cipherMarkerComment(date: string, markerUserId: string) { return `/* [KMS-CIPHER] 수정일자: ${date} 수정자 : ${markerUserId} */` } function desiredResultMapDefinition( mapperItem: MapperItem, selectedText: string, mapperXml: string, targets: CryptoTarget[], ): ResultMapDefinition { const existingResultMapId = attrValue(mapperItem.attrs, 'resultMap') const existingResultMap = existingResultMapId ? parseResultMaps(mapperXml).find((item) => item.id === existingResultMapId) : undefined const type = attrValue(mapperItem.attrs, 'resultType') || existingResultMap?.type || inferDtoTypeFromMapperNamespace(mapperXml) || 'java.util.Map' return { id: resultMapId(mapperItem, selectedText, targets), type, entries: resultMapEntries(selectedText, targets), } } function resultMapEntries(selectedText: string, targets: CryptoTarget[]) { const entries: ResultMapEntry[] = [] const seen = new Set() for (const column of selectedColumns(selectedText)) { const target = targets.find((item) => equalsIgnoreCase(item.plainColumn, column) || equalsIgnoreCase(item.encColumn, column)) if (!target) continue const property = target.javaProperty const normalizedColumn = target.plainColumn const key = `${normalizedColumn.toUpperCase()}|${property}` if (seen.has(key)) continue seen.add(key) entries.push({ kind: entries.length === 0 && /_ID$/i.test(normalizedColumn) ? 'id' : 'result', column: normalizedColumn, property, typeHandler: fqcn(target.encryptTypeHandler), }) } return entries } function selectedColumns(selectedText: string) { const columnRange = selectColumnRange(selectedText) if (!columnRange) return [] return splitTopLevelCsv(selectedText.slice(columnRange.columnsStart, columnRange.columnsEnd)) .map((item) => stripAlias(item)) .filter((item) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(item)) } function rewriteSelectedSelectColumns(itemText: string, selectedText: string, targets: CryptoTarget[], date: string, markerUserId: string) { const textWithoutCheckComments = removeCryptoTargetCheckComments(selectedText) const columnItems = splitTopLevelCsvItems(textWithoutCheckComments) const changedTargets: CryptoTarget[] = [] const seenTargets = new Set() let next = '' let lastIndex = 0 for (const item of columnItems) { next += textWithoutCheckComments.slice(lastIndex, item.start) const cleanItemText = removeDecryptTargetComments(item.text) const target = targets.find((candidate) => isSelectedCryptoColumnItem(itemText, cleanItemText, candidate)) if (!target) { next += item.text lastIndex = item.end continue } const tableAlias = findTableAlias(itemText, target.tableName) const replaced = replaceSelectedColumnItem(cleanItemText, target, tableAlias, date, markerUserId) if (replaced !== cleanItemText) { const key = `${target.tableName.toUpperCase()}|${target.plainColumn.toUpperCase()}` if (!seenTargets.has(key)) { seenTargets.add(key) changedTargets.push(target) } } next += replaced lastIndex = item.end } next += textWithoutCheckComments.slice(lastIndex) return { text: next, changedTargets } } function removeCryptoTargetCheckComments(text: string) { return text.split(CRYPTO_TARGET_CHECK_COMMENT).join('') } function removeDecryptTargetComments(text: string) { return text .replace(decryptTargetCommentPattern(), '') .replace(legacyDecryptTargetCommentPattern(), '') } function isSelectedCryptoColumnItem(itemText: string, columnItemText: string, target: CryptoTarget) { if (!hasBoundaryToken(itemText, target.tableName)) return false return selectColumnItemReferencesTarget(columnItemText, itemText, target, target.plainColumn) || selectColumnItemReferencesTarget(columnItemText, itemText, target, target.encColumn) } function selectColumnItemReferencesTarget(columnItemText: string, itemText: string, target: CryptoTarget, columnName: string) { if (!columnName) return false const tableAlias = findTableAlias(itemText, target.tableName) const pattern = new RegExp( `(^|[^A-Za-z0-9_\\.])((?:([A-Za-z_][A-Za-z0-9_]*)\\s*\\.\\s*)?${escapeRegExp(columnName)})(?=$|[^A-Za-z0-9_])`, 'i', ) const match = columnItemText.match(pattern) if (!match) return false const columnAlias = match[3] ?? '' return !columnAlias || !tableAlias || equalsIgnoreCase(columnAlias, tableAlias) } function replaceSelectedColumnItem(columnItemText: string, target: CryptoTarget, tableAlias: string, date: string, markerUserId: string) { if (selectDecryptAliasPattern(target).test(columnItemText)) return columnItemText const columnPattern = new RegExp( `(^|[^A-Za-z0-9_\\.])((?:[A-Za-z_][A-Za-z0-9_]*\\s*\\.\\s*)?(?:${escapeRegExp(target.plainColumn)}|${escapeRegExp(target.encColumn)}))(?=$|[^A-Za-z0-9_])`, 'i', ) return columnItemText.replace(columnPattern, (_match, prefix: string, column: string) => { const columnAlias = column.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*\./)?.[1] ?? '' return `${prefix}${selectDecryptAlias(target, columnAlias || tableAlias, date, markerUserId)}` }) } function desiredResultMapDefinitionForTargets( mapperItem: MapperItem, selectedText: string, mapperXml: string, targets: CryptoTarget[], ): ResultMapDefinition { const existingResultMapId = attrValue(mapperItem.attrs, 'resultMap') const existingResultMap = existingResultMapId ? parseResultMaps(mapperXml).find((item) => item.id === existingResultMapId) : undefined const type = attrValue(mapperItem.attrs, 'resultType') || existingResultMap?.type || inferDtoTypeFromMapperNamespace(mapperXml) || 'java.util.Map' return { id: resultMapId(mapperItem, selectedText, targets), type, entries: resultMapEntriesFromTargets(targets), } } function resultMapEntriesFromTargets(targets: CryptoTarget[]) { const entries: ResultMapEntry[] = [] const seen = new Set() for (const target of targets) { const normalizedColumn = target.plainColumn const key = `${normalizedColumn.toUpperCase()}|${target.javaProperty}` if (seen.has(key)) continue seen.add(key) entries.push({ kind: entries.length === 0 && /_ID$/i.test(normalizedColumn) ? 'id' : 'result', column: normalizedColumn, property: target.javaProperty, typeHandler: fqcn(target.encryptTypeHandler), }) } return entries } function lineStartOffset(text: string, offset: number) { return text.lastIndexOf('\n', Math.max(0, offset - 1)) + 1 } function selectColumnRange(xmlText: string) { const openTag = xmlText.match(/]*>/i) const bodyStart = openTag && openTag.index !== undefined ? openTag.index + openTag[0].length : 0 const closeTag = xmlText.match(/<\/select\s*>/i) const bodyEnd = closeTag && closeTag.index !== undefined ? closeTag.index : xmlText.length const body = xmlText.slice(bodyStart, bodyEnd) const selectIndex = findTopLevelSqlKeyword(body, 'select', 0) if (selectIndex < 0) return undefined const fromIndex = findTopLevelSqlKeyword(body, 'from', selectIndex + 'select'.length) if (fromIndex < 0) return undefined return { columnsStart: bodyStart + selectIndex + 'select'.length, columnsEnd: bodyStart + fromIndex, } } function findTopLevelSqlKeyword(text: string, keyword: string, start: number) { let quote = '' let depth = 0 for (let index = start; index < text.length; index += 1) { const char = text[index] const next = text[index + 1] if (quote) { if (char === quote) { if (quote === "'" && next === "'") { index += 1 } else { quote = '' } } continue } if (char === '-' && next === '-') { index = skipLineComment(text, index + 2) continue } if (char === '/' && next === '*') { index = skipBlockComment(text, index + 2) continue } if (char === "'" || char === '"') { quote = char continue } if (char === '(') { depth += 1 continue } if (char === ')') { depth = Math.max(0, depth - 1) continue } if (depth === 0 && matchesSqlKeywordAt(text, keyword, index)) return index } return -1 } function matchesSqlKeywordAt(text: string, keyword: string, index: number) { return text.toLowerCase().startsWith(keyword.toLowerCase(), index) && isSqlKeywordBoundary(text, index, keyword.length) } function isSqlKeywordBoundary(text: string, index: number, length: number) { const before = text[index - 1] ?? '' const after = text[index + length] ?? '' return !/[A-Za-z0-9_]/.test(before) && !/[A-Za-z0-9_]/.test(after) } function skipLineComment(text: string, start: number) { const end = text.indexOf('\n', start) return end < 0 ? text.length : end } function skipBlockComment(text: string, start: number) { const end = text.indexOf('*/', start) return end < 0 ? text.length : end + 1 } function splitTopLevelCsv(text: string) { return splitTopLevelCsvItems(text).map((item) => item.text.trim()) } function splitTopLevelCsvItems(text: string) { const items: Array<{ text: string; start: number; end: number }> = [] let current = '' let quote = false let depth = 0 let itemStart = 0 for (let index = 0; index < text.length; index += 1) { const char = text[index] const next = text[index + 1] if (char === "'") { current += char if (quote && next === "'") { current += next index += 1 } else { quote = !quote } continue } if (!quote && char === '(') depth += 1 if (!quote && char === ')') depth = Math.max(0, depth - 1) if (!quote && depth === 0 && char === ',') { items.push({ text: current, start: itemStart, end: index }) current = '' itemStart = index + 1 continue } current += char } if (current.trim()) items.push({ text: current, start: itemStart, end: text.length }) return items } function stripAlias(value: string): string { const normalized = value .replace(//g, '') .replace(/\/\*[\s\S]*?\*\//g, '') .trim() const explicitAlias = normalized.match(/\s+AS\s+([A-Za-z_][A-Za-z0-9_]*)$/i) if (explicitAlias) return explicitAlias[1] const implicitAlias = normalized.match(/^((?:[A-Za-z_][A-Za-z0-9_]*\s*\.\s*)?[A-Za-z_][A-Za-z0-9_]*)\s+[A-Za-z_][A-Za-z0-9_]*$/i) if (implicitAlias) return stripAlias(implicitAlias[1]) return normalized .replace(/^.*\.([A-Za-z_][A-Za-z0-9_]*)$/, '$1') .trim() } function resultMapId(mapperItem: MapperItem, selectedText: string, targets: CryptoTarget[]) { const existing = attrValue(mapperItem.attrs, 'resultMap') if (existing) return existing const dtoResultMapId = resultMapIdFromDtoResultType(attrValue(mapperItem.attrs, 'resultType')) if (dtoResultMapId) return dtoResultMapId const sqlPart = toPascal(mapperItem.id.replace(/^(select|get|find|query|list)/i, '')) || toPascal(mapperItem.id) const tablePart = toPascal(commonTableName(targets)) const suffix = /result$/i.test(sqlPart) ? 'Map' : 'ResultMap' const id = `${lowerFirst(sqlPart || tablePart)}${tablePart && !sqlPart.includes(tablePart) ? tablePart : ''}Crypto${suffix}` return id.replace(/^[A-Z]/, (char) => char.toLowerCase()) } function resultMapIdFromDtoResultType(resultType: string) { if (!resultType) return '' const dtoName = resultType.split('.').pop() ?? '' return /^[A-Za-z_][A-Za-z0-9_]*Dto$/.test(dtoName) ? `${dtoName}Result` : '' } function commonTableName(targets: CryptoTarget[]) { const table = targets[0]?.tableName || 'Crypto' return table.replace(/^TB_/i, '') } function parseResultMaps(mapperXml: string): ResultMapDefinition[] { const resultMaps: ResultMapDefinition[] = [] const resultMapPattern = /]*)>([\s\S]*?)<\/resultMap>/gi for (const match of mapperXml.matchAll(resultMapPattern)) { const attrs = match[1] const body = match[2] const entries: ResultMapEntry[] = [] for (const entryMatch of body.matchAll(/<(id|result)\b([^/>]*?)\/?>/gi)) { const entryAttrs = entryMatch[2] const column = attrValue(entryAttrs, 'column') const property = attrValue(entryAttrs, 'property') if (!column || !property) continue entries.push({ kind: entryMatch[1].toLowerCase() === 'id' ? 'id' : 'result', column, property, typeHandler: attrValue(entryAttrs, 'typeHandler') || undefined, }) } resultMaps.push({ id: attrValue(attrs, 'id'), type: attrValue(attrs, 'type'), entries, }) } return resultMaps } function findExistingResultMap(mapperXml: string, desired: ResultMapDefinition) { return parseResultMaps(mapperXml).find((resultMap) => sameResultMapDefinition(resultMap, desired)) } function sameResultMapDefinition(left: ResultMapDefinition, right: ResultMapDefinition) { if (left.type !== right.type) return false const leftEntries = left.entries.map(resultMapEntryKey).sort() const rightEntries = right.entries.map(resultMapEntryKey).sort() return leftEntries.length === rightEntries.length && leftEntries.every((entry, index) => entry === rightEntries[index]) } function resultMapEntryKey(entry: ResultMapEntry) { return [ entry.kind, entry.column.toUpperCase(), entry.property, fqcn(entry.typeHandler).toLowerCase(), ].join('|') } function resultMapSnippet(definition: ResultMapDefinition) { const lines = [ ``, ...definition.entries.map((entry) => { const typeHandler = entry.typeHandler ? ` typeHandler="${fqcn(entry.typeHandler)}"` : '' return ` <${entry.kind} column="${entry.column}" property="${entry.property}"${typeHandler}/>` }), ``, ] return lines.join('\n') } function inferDtoTypeFromMapperNamespace(mapperXml: string) { const namespace = mapperXml.match(/]*\bnamespace\s*=\s*['"]([^'"]+)['"]/i)?.[1] if (!namespace) return '' if (/Mapper$/i.test(namespace)) return namespace.replace(/Mapper$/i, 'Dto') return `${namespace}Dto` } function matchesTarget(text: string, target: CryptoTarget, tag?: MapperItem['tag']) { if (!hasBoundaryToken(text, target.tableName)) return false if (tag === 'select') return hasSelectRewritePoint(text, target) return hasBoundaryToken(text, target.plainColumn) || hasBoundaryToken(text, target.encColumn) || Boolean(target.hashColumn && hasBoundaryToken(text, target.hashColumn)) } function hasSelectRewritePoint(text: string, target: CryptoTarget) { return selectedColumns(text).some((column) => equalsIgnoreCase(column, target.plainColumn) || equalsIgnoreCase(column, target.encColumn)) || hasWhereRewritePoint(text, target) } function hasWhereRewritePoint(text: string, target: CryptoTarget) { const whereMatch = text.match(/\bwhere\b/i) if (!whereMatch || whereMatch.index === undefined) return false const whereBody = text.slice(whereMatch.index + whereMatch[0].length) const qualifier = String.raw`(?:[A-Za-z_][A-Za-z0-9_]*\s*\.\s*)?` const column = escapeRegExp(target.plainColumn) const property = escapeRegExp(target.javaProperty) const parameter = String.raw`#\{\s*${property}\s*\}` const equalsPattern = new RegExp(String.raw`${qualifier}\b${column}\b\s*=\s*${parameter}`, 'i') const likePattern = new RegExp( String.raw`${qualifier}\b${column}\b\s+LIKE\s*'%'\s*\|\|\s*${parameter}\s*\|\|\s*'%'`, 'i', ) return equalsPattern.test(whereBody) || likePattern.test(whereBody) } function hasBoundaryToken(text: string, token: string) { return token ? boundaryPattern(token).test(text) : false } function replaceFirstBoundary(text: string, token: string, replacement: string) { const pattern = new RegExp(`(^|[^A-Za-z0-9_])(${escapeRegExp(token)})(?=$|[^A-Za-z0-9_])`, 'i') return text.replace(pattern, (_match, prefix: string) => `${prefix}${replacement}`) } function boundaryPattern(token: string) { return new RegExp(`(^|[^A-Za-z0-9_])${escapeRegExp(token)}(?=$|[^A-Za-z0-9_])`, 'i') } function attrValue(text: string, name: string) { return text.match(new RegExp(`\\b${name}\\s*=\\s*['"]([^'"]+)['"]`, 'i'))?.[1] ?? '' } function typeHandlerBind(property: string, handler?: string) { const typeHandler = fqcn(handler) return typeHandler ? `#{${property}, typeHandler=${typeHandler}}` : `#{${property}}` } function fqcn(handler?: string) { if (!handler) return '' return handler.includes('.') ? handler : `${handler}` } function withComment(text: string, date: string) { return `${text} ` } function toCamel(value: string) { return value .toLowerCase() .replace(/_([a-z0-9])/g, (_match, char: string) => char.toUpperCase()) } function toPascal(value: string) { const words = value .replace(/^TB_/i, '') .replace(/([a-z0-9])([A-Z])/g, '$1_$2') .split(/[^A-Za-z0-9]+|_/) .filter(Boolean) return words.map((word) => word.slice(0, 1).toUpperCase() + word.slice(1).toLowerCase()).join('') } function lowerFirst(value: string) { return value.replace(/^./, (char) => char.toLowerCase()) } function equalsIgnoreCase(left: string, right: string) { return left.toUpperCase() === right.toUpperCase() } function includesIgnoreCase(text: string, value: string) { return value ? text.toUpperCase().includes(value.toUpperCase()) : false } function escapeRegExp(value: string) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } function formatLocalDate(date: Date) { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } function formatLocalDateTimeMinute(date: Date) { const hours = String(date.getHours()).padStart(2, '0') const minutes = String(date.getMinutes()).padStart(2, '0') return `${formatLocalDate(date)} ${hours}:${minutes}` } ============================= { "name": "typehandler-helper", "displayName": "TypeHandler Helper", "description": "MyBatis mapper XML 선택 영역에 암복호화 TypeHandler 적용 추천 텍스트를 생성해 클립보드에 복사합니다.", "version": "0.1.6", "publisher": "leesumin", "engines": { "vscode": "^1.74.0", "node": ">=16.0.0" }, "categories": [ "Other" ], "activationEvents": [ "onCommand:cryptoMapper.copyTypeHandlerRecommendation", "onCommand:cryptoMapper.copyFullFileTypeHandlerRecommendation", "onCommand:cryptoMapper.markCryptoTargetColumns", "onCommand:cryptoMapper.markCryptoTargetColumnsInCurrentItem", "onCommand:cryptoMapper.applyConvertXmlRulesToSelection" ], "main": "./dist/extension.js", "contributes": { "commands": [ { "command": "cryptoMapper.copyTypeHandlerRecommendation", "title": "Crypto Mapper: Copy TypeHandler Recommendation" }, { "command": "cryptoMapper.copyFullFileTypeHandlerRecommendation", "title": "Crypto Mapper: Copy Full XML TypeHandler Recommendation" }, { "command": "cryptoMapper.markCryptoTargetColumns", "title": "Crypto Mapper: Mark Crypto Target Columns" }, { "command": "cryptoMapper.markCryptoTargetColumnsInCurrentItem", "title": "Crypto Mapper: Mark Crypto Target Columns In Current Mapper Item" }, { "command": "cryptoMapper.applyConvertXmlRulesToSelection", "title": "Crypto Mapper: Apply convert.xml Rules To Selection" } ], "keybindings": [ { "command": "cryptoMapper.copyTypeHandlerRecommendation", "key": "ctrl+shift+4", "mac": "cmd+shift+4", "when": "editorTextFocus" }, { "command": "cryptoMapper.copyFullFileTypeHandlerRecommendation", "key": "ctrl+shift+9", "mac": "cmd+shift+9", "when": "editorTextFocus" }, { "command": "cryptoMapper.markCryptoTargetColumns", "key": "ctrl+shift+8", "mac": "cmd+shift+8", "when": "editorTextFocus" }, { "command": "cryptoMapper.markCryptoTargetColumnsInCurrentItem", "key": "ctrl+shift+3", "mac": "cmd+shift+3", "when": "editorTextFocus" }, { "command": "cryptoMapper.applyConvertXmlRulesToSelection", "key": "ctrl+shift+5", "mac": "cmd+shift+5", "when": "editorTextFocus" } ] }, "scripts": { "compile": "tsc -p ./", "watch": "tsc -watch -p ./", "package": "node -r ./scripts/node16-web-streams-polyfill.js ./node_modules/@vscode/vsce/vsce package", "vscode:prepublish": "npm run compile" }, "devDependencies": { "@types/node": "16.18.126", "@types/vscode": "^1.74.0", "@vscode/vsce": "2.15.0", "typescript": "^4.9.5" }, "overrides": { "cheerio": "1.0.0-rc.12" } }

댓글

이 블로그의 인기 게시물

food eff privacy

판다 스픽 , 개인정보 처리방침 , Panda Speak Privacy Term

판다 수학 개인정보 처리방침 , Privacy , Panda Math