xml 분석기

const fs = require('fs/promises'); const path = require('path'); const SQL_RESERVED_WORDS = new Set([ 'AND', 'AS', 'ASC', 'BETWEEN', 'BY', 'CASE', 'CONNECT', 'CROSS', 'DESC', 'ELSE', 'END', 'FROM', 'FULL', 'GROUP', 'HAVING', 'INNER', 'JOIN', 'LEFT', 'LIKE', 'NOT', 'ON', 'OR', 'ORDER', 'OUTER', 'RIGHT', 'START', 'THEN', 'UNION', 'WHEN', 'WHERE', 'WITH', ]); async function main() { const appDir = __dirname; const inputPath = path.join(appDir, 'input.ini'); const targetsPath = path.join(appDir, 'crypto-targets.json'); const outputPath = path.join(appDir, 'output.txt'); const targetMapperPath = await readIniValue(inputPath, 'targetmapperpath'); if (!targetMapperPath) { throw new Error(`input.ini has no targetmapperpath value: ${inputPath}`); } const mapperRoot = path.isAbsolute(targetMapperPath) ? targetMapperPath : path.resolve(appDir, targetMapperPath); const targets = await readCryptoTargets(targetsPath); const xmlFiles = await findXmlFiles(mapperRoot); const lines = []; for (const xmlFile of xmlFiles) { const xml = await fs.readFile(xmlFile, 'utf8'); const summary = summarizeTargetMapperItems(xml, targets); if (summary.total === 0) continue; lines.push([ xmlFile, path.basename(xmlFile), `${summary.total},${summary.select},${summary.insert},${summary.update}`, ].join('|')); } await fs.writeFile(outputPath, `${lines.join('\n')}${lines.length ? '\n' : ''}`, 'utf8'); console.log(`Done. XML files: ${xmlFiles.length}, output: ${outputPath}`); } async function readIniValue(filePath, key) { const text = await fs.readFile(filePath, 'utf8'); const pattern = new RegExp(`^${escapeRegExp(key)}\\s*=\\s*(.+)$`, 'i'); for (const rawLine of text.split(/\r?\n/)) { const line = rawLine.trim(); if (!line || line.startsWith('#') || line.startsWith(';')) continue; const match = line.match(pattern); if (!match) continue; return match[1].trim().replace(/^['"]|['"]$/g, ''); } return ''; } async function readCryptoTargets(filePath) { const text = await fs.readFile(filePath, 'utf8'); const parsed = JSON.parse(text); if (!Array.isArray(parsed)) { throw new Error(`crypto-targets.json must be an array: ${filePath}`); } return parsed .map((target) => ({ tableName: normalizeRequired(target.tableName), plainColumn: normalizeRequired(target.plainColumn), cryptoType: normalizeOptional(target.cryptoType), encColumn: normalizeRequired(target.encColumn), hashColumn: normalizeOptional(target.hashColumn), javaProperty: normalizeOptional(target.javaProperty) || toCamel(normalizeRequired(target.plainColumn)), encryptTypeHandler: normalizeRequired(target.encryptTypeHandler), hashTypeHandler: normalizeOptional(target.hashTypeHandler), })) .filter((target) => target.tableName && target.plainColumn); } async function findXmlFiles(rootDir) { const result = []; const queue = [rootDir]; while (queue.length > 0) { const current = queue.shift(); const entries = await fs.readdir(current, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(current, entry.name); if (entry.isDirectory()) { queue.push(fullPath); } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.xml')) { result.push(fullPath); } } } return result.sort((left, right) => left.localeCompare(right)); } function summarizeTargetMapperItems(mapperXml, targets) { const summary = { total: 0, select: 0, insert: 0, update: 0 }; const itemPattern = /<(insert|update|select|delete)\b([^>]*)>[\s\S]*?<\/\1>/gi; for (const match of mapperXml.matchAll(itemPattern)) { const itemText = match[0]; const mapperItem = parseMapperItem(itemText); const expandedItemText = expandMapperItemIncludesForTargets(itemText, mapperXml, targets, mapperItem.tag).text; const isTarget = targets.some((target) => ( hasBoundaryToken(expandedItemText, target.tableName) && hasBoundaryToken(expandedItemText, target.plainColumn) )); if (!isTarget) continue; summary.total += 1; if (mapperItem.tag === 'select') summary.select += 1; if (mapperItem.tag === 'insert') summary.insert += 1; if (mapperItem.tag === 'update') summary.update += 1; } return summary; } function parseMapperItem(text) { const match = text.trim().match(/^<(insert|update|select|delete)\b([^>]*)>[\s\S]*<\/\1>$/i); if (!match) { throw new Error('Invalid mapper item text.'); } return { tag: match[1].toLowerCase(), attrs: match[2], id: attrValue(match[2], 'id') || 'anonymousSql', text, }; } function expandMapperItemIncludesForTargets(itemText, mapperXml, targets, tag) { const tableTargets = targets.filter((target) => hasBoundaryToken(itemText, target.tableName)); if (tableTargets.length === 0 || !/]*(?:\/>|>[\s\S]*?<\/include\s*>)/gi, (includeText) => { const attrs = includeText.match(/^]*?)(?:\/>|>)/i)?.[1] ?? ''; const refid = attrValue(attrs, 'refid'); const fragment = findSqlFragment(fragments, refid); if (!fragment) return includeText; const replacement = applyIncludeProperties(fragment, includeText); if (!includeFragmentHasCryptoTargetColumn(replacement, tableTargets, tag)) return includeText; changed = true; return replacement; }); if (!changed) break; } return next; } function parseSqlFragments(mapperXml) { const fragments = new Map(); const sqlPattern = /]*)>([\s\S]*?)<\/sql\s*>/gi; for (const match of mapperXml.matchAll(sqlPattern)) { const id = attrValue(match[1], 'id'); if (!id) continue; fragments.set(id, trimSqlFragmentBody(match[2])); } return fragments; } function trimSqlFragmentBody(body) { return body.replace(/^\s*\r?\n/, '').replace(/\r?\n\s*$/, ''); } function findSqlFragment(fragments, refid) { if (!refid) return ''; return fragments.get(refid) ?? fragments.get(refid.replace(/^.*\./, '')) ?? ''; } function applyIncludeProperties(fragment, includeText) { const properties = new Map(); const propertyPattern = /]*?)\/?>/gi; for (const match of includeText.matchAll(propertyPattern)) { const name = attrValue(match[1], 'name'); if (!name) continue; properties.set(name, attrValue(match[1], 'value')); } if (properties.size === 0) return fragment; return fragment.replace(/\$\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}/g, (match, name) => ( properties.has(name) ? properties.get(name) : match )); } function includeFragmentHasCryptoTargetColumn(fragment, tableTargets, tag) { return tableTargets.some((target) => { if (tag === 'select') return hasSelectRewritePoint(fragment, target); return hasBoundaryToken(fragment, target.plainColumn) || hasBoundaryToken(fragment, target.encColumn) || Boolean(target.hashColumn && hasBoundaryToken(fragment, target.hashColumn)); }); } function hasSelectRewritePoint(text, target) { return selectedColumns(text).some((column) => equalsIgnoreCase(column, target.plainColumn) || equalsIgnoreCase(column, target.encColumn)) || hasBoundaryToken(text, target.plainColumn) || hasBoundaryToken(text, target.encColumn) || hasWhereRewritePoint(text, target); } function selectedColumns(selectedText) { const columnRanges = selectColumnRanges(selectedText); if (columnRanges.length === 0) return []; return columnRanges .flatMap((columnRange) => splitTopLevelCsv(selectedText.slice(columnRange.columnsStart, columnRange.columnsEnd))) .map((item) => stripAlias(item)) .filter((item) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(item)); } function selectColumnRanges(xmlText) { const ranges = []; 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); for (let index = 0; index < body.length; index += 1) { if (!matchesSqlKeywordAt(body, 'select', index)) continue; const fromIndex = findTopLevelSqlKeyword(body.slice(index), 'from', 'select'.length); if (fromIndex < 0) continue; ranges.push({ columnsStart: bodyStart + index + 'select'.length, columnsEnd: bodyStart + index + fromIndex, }); } return ranges; } function findTopLevelSqlKeyword(text, keyword, start) { 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, keyword, index) { return text.toLowerCase().startsWith(keyword.toLowerCase(), index) && isSqlKeywordBoundary(text, index, keyword.length); } function isSqlKeywordBoundary(text, index, length) { 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, start) { const end = text.indexOf('\n', start); return end < 0 ? text.length : end; } function skipBlockComment(text, start) { const end = text.indexOf('*/', start); return end < 0 ? text.length : end + 1; } function splitTopLevelCsv(text) { return splitTopLevelCsvItems(text).map((item) => item.text.trim()); } function splitTopLevelCsvItems(text) { const items = []; 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) { 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 && !isSqlReservedWord(implicitAlias[2])) return stripAlias(implicitAlias[1]); return normalized.replace(/^.*\.([A-Za-z_][A-Za-z0-9_]*)$/, '$1').trim(); } function hasWhereRewritePoint(text, target) { 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, token) { return token ? boundaryPattern(token).test(text) : false; } function boundaryPattern(token) { return new RegExp(`(^|[^A-Za-z0-9_])${escapeRegExp(token)}(?=$|[^A-Za-z0-9_])`, 'i'); } function attrValue(text, name) { return text.match(new RegExp(`\\b${name}\\s*=\\s*['"]([^'"]+)['"]`, 'i'))?.[1] ?? ''; } function normalizeRequired(value) { return String(value ?? '').trim(); } function normalizeOptional(value) { return String(value ?? '').trim(); } function toCamel(value) { return value .toLowerCase() .replace(/_([a-z0-9])/g, (_match, char) => char.toUpperCase()); } function equalsIgnoreCase(left, right) { return left.toUpperCase() === right.toUpperCase(); } function isSqlReservedWord(value) { return SQL_RESERVED_WORDS.has(value.toUpperCase()); } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } main().catch((error) => { console.error(error instanceof Error ? error.message : error); process.exitCode = 1; });

댓글

이 블로그의 인기 게시물

food eff privacy

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

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