#!/usr/bin/env node const fs = require('fs'); const path = require('path'); // Usage: // node gen_pojo_from_xls_json.js [Tables...] [--ignore=a,b,c] // Tables can be names with or without .json/.xls, case-insensitive. Multiple can be space- or comma-separated. const [,, jsonDir, xlsDir, packageName, outputDir, ...rest] = process.argv; if (!jsonDir || !xlsDir || !packageName || !outputDir) { console.log('Usage: node gen_pojo_from_xls_json.js [Tables...] [--ignore=a,b,c]'); process.exit(1); } function normalizeName(n) { return String(n).toLowerCase().replace(/\.[^.]+$/, ''); } function parseArgs(args) { const whitelist = []; const blacklist = []; for (let i = 0; i < args.length; i++) { const a = String(args[i]); if (a.startsWith('--ignore=')) { const v = a.substring('--ignore='.length); v.split(',').map(s => s.trim()).filter(Boolean).forEach(x => blacklist.push(normalizeName(x))); continue; } if (a === '--ignore' && i + 1 < args.length) { const v = String(args[++i]); v.split(',').map(s => s.trim()).filter(Boolean).forEach(x => blacklist.push(normalizeName(x))); continue; } whitelist.push(normalizeName(a)); } return { whitelist: new Set(whitelist.filter(Boolean)), blacklist: new Set(blacklist) }; } const { whitelist: whiteSet, blacklist: blackSet } = parseArgs(rest); function listJsonFiles(dir) { return fs.readdirSync(dir) .filter(name => name.toLowerCase().endsWith('.json')) .map(name => path.join(dir, name)); } function readJson(filePath) { let content = fs.readFileSync(filePath, 'utf8'); if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1); return JSON.parse(content); } function pickSample(root) { if (Array.isArray(root)) return root.length > 0 ? root[0] : null; if (root && typeof root === 'object') { const keys = Object.keys(root); return keys.length > 0 ? root[keys[0]] : null; } return null; } function upperFirst(str) { if (!str) return str; return str.charAt(0).toUpperCase() + str.slice(1); } // Convert filename like M_item.json -> MItem function toClassName(fileName) { const base = fileName.replace(/\.[^.]+$/, ''); return base.split(/[_\-\s]+/).filter(Boolean).map(upperFirst).join(''); } function isInteger(num) { return typeof num === 'number' && Number.isInteger(num); } function inferJavaType(value) { if (value === null || value === undefined) return 'String'; if (typeof value === 'number') return isInteger(value) ? 'Integer' : 'BigDecimal'; if (typeof value === 'boolean') return 'Boolean'; if (typeof value === 'string') return 'String'; if (Array.isArray(value)) { if (value.length === 0) return 'List'; return `List<${inferJavaType(value[0])}>`; } if (typeof value === 'object') return 'Object'; return 'String'; } const JAVA_KEYWORDS = new Set([ 'abstract','assert','boolean','break','byte','case','catch','char','class','const','continue','default','do','double','else','enum','extends','final','finally','float','for','goto','if','implements','import','instanceof','int','interface','long','native','new','package','private','protected','public','return','short','static','strictfp','super','switch','synchronized','this','throw','throws','transient','try','void','volatile','while','null','true','false' ]); function sanitizeFieldName(name) { let safe = name.replace(/[^A-Za-z0-9_]/g, '_'); if (!/^[A-Za-z_]/.test(safe)) safe = '_' + safe; if (JAVA_KEYWORDS.has(safe)) safe = safe + '_'; return safe; } function readXlsComments(xlsFile) { try { const wb = xlsx.readFile(xlsFile); const sheetName = wb.SheetNames[0]; const ws = wb.Sheets[sheetName]; const rows = xlsx.utils.sheet_to_json(ws, { header: 1, raw: false }); // Heuristics: within first 5 rows, pick field row as the one with max ASCII ratio and contains 'ID' or many word tokens const maxCheck = Math.min(5, rows.length); let fieldRowIdx = -1, commentRowIdx = -1; let bestScore = -1; for (let i = 0; i < maxCheck; i++) { const row = rows[i] || []; const joined = row.join(' ').trim(); if (!joined) continue; const asciiCount = (joined.match(/[\x20-\x7E]/g) || []).length; const ratio = asciiCount / Math.max(1, joined.length); const wordCount = (joined.match(/[A-Za-z_][A-Za-z0-9_]*/g) || []).length; const score = ratio * 0.7 + Math.min(wordCount / Math.max(1, row.length), 1) * 0.3 + (joined.includes('ID') ? 0.2 : 0); if (score > bestScore) { bestScore = score; fieldRowIdx = i; } } // Comment row: prefer a different row with lower ASCII ratio (more likely Chinese) let bestCR = -1; for (let i = 0; i < maxCheck; i++) { if (i === fieldRowIdx) continue; const row = rows[i] || []; const joined = row.join(' ').trim(); if (!joined) continue; const asciiCount = (joined.match(/[\x20-\x7E]/g) || []).length; const ratio = asciiCount / Math.max(1, joined.length); const score = 1 - ratio; // prefer non-ASCII if (score > bestCR) { bestCR = score; commentRowIdx = i; } } const fields = rows[fieldRowIdx] || []; const comments = rows[commentRowIdx] || []; const map = new Map(); for (let i = 0; i < fields.length; i++) { const k = String(fields[i] || '').trim(); if (!k) continue; const v = String(comments[i] || '').trim(); if (k) map.set(k, v); } return { sheetName, comments: map }; } catch (e) { return { sheetName: '', comments: new Map() }; } } function findXlsForBase(xlsDir, baseName) { // Try .xls then .xlsx const cands = [path.join(xlsDir, baseName + '.xls'), path.join(xlsDir, baseName + '.xlsx')]; for (const f of cands) { if (fs.existsSync(f)) return f; } // Case-insensitive search fallback const files = fs.readdirSync(xlsDir).filter(n => /\.xlsx?$/.test(n)); for (const n of files) { if (normalizeName(n) === normalizeName(baseName)) return path.join(xlsDir, n); } return null; } function generateJavaClass(pkg, className, fieldsOrder, fieldTypes, fieldComments, classComment, jsonFileName) { const lines = []; lines.push(`package ${pkg};`); lines.push(''); lines.push('import lombok.Data;'); // Only add imports that are actually used const usedTypes = new Set(); for (const fname of fieldsOrder) { const type = fieldTypes.get(fname) || 'String'; if (type === 'List') { usedTypes.add('java.util.List'); } else if (type === 'List') { usedTypes.add('java.util.List'); } else if (type === 'List') { usedTypes.add('java.util.List'); } else if (type === 'List') { usedTypes.add('java.util.List'); usedTypes.add('java.math.BigDecimal'); } else if (type === 'BigDecimal') { usedTypes.add('java.math.BigDecimal'); } } // Add only the imports that are actually used usedTypes.forEach(importType => { lines.push(`import ${importType};`); }); if (usedTypes.size > 0) { lines.push(''); } if (classComment) { lines.push('/**'); lines.push(` * 配置表:${classComment}`); lines.push(' */'); } lines.push('@Data'); lines.push(`public class ${className} {`); // Add path field using the actual JSON filename lines.push('\t/** JSON 配置文件路径 */'); lines.push('\tpublic static final String path = "' + jsonFileName + '";'); lines.push(''); // Add fields for (const fname of fieldsOrder) { const type = fieldTypes.get(fname) || 'String'; const cmt = fieldComments.get(fname); const safe = sanitizeFieldName(fname); if (cmt) { lines.push(`\t/** ${cmt} */`); } if (safe !== fname) { lines.push(`\t@JsonProperty("${fname}")`); } lines.push(`\tpublic ${type} ${safe};`); } lines.push(''); lines.push('}'); return lines.join('\n'); } (function main() { if (!fs.existsSync(jsonDir)) { console.error('JSON dir not found:', jsonDir); process.exit(2); } if (!fs.existsSync(xlsDir)) { console.error('XLS dir not found:', xlsDir); process.exit(2); } const outPkgDir = path.join(outputDir, packageName.replace(/\./g, path.sep)); fs.mkdirSync(outPkgDir, { recursive: true }); let files = listJsonFiles(jsonDir); // Apply whitelist filter if provided if (whiteSet.size > 0) { files = files.filter(f => whiteSet.has(normalizeName(path.basename(f)))); } // Apply blacklist filter if provided if (blackSet.size > 0) { files = files.filter(f => !blackSet.has(normalizeName(path.basename(f)))); } if (files.length === 0) { console.log('No JSON files to process.'); process.exit(0); } let generated = 0; for (const jf of files) { try { const base = path.basename(jf).replace(/\.[^.]+$/, ''); const jsonRoot = readJson(jf); const sample = pickSample(jsonRoot); if (!sample || typeof sample !== 'object') { console.warn('Skip (no object sample):', jf); continue; } const xlsPath = findXlsForBase(xlsDir, base); const { sheetName, comments } = xlsPath ? readXlsComments(xlsPath) : { sheetName: '', comments: new Map() }; const className = toClassName(base) + 'Pojo'; const fieldsOrder = Object.keys(sample); const fieldTypes = new Map(); for (const k of fieldsOrder) fieldTypes.set(k, inferJavaType(sample[k])); const classComment = sheetName ? `${base}(${sheetName})` : `${base}`; const jsonFileName = path.basename(jf); const content = generateJavaClass(packageName, className, fieldsOrder, fieldTypes, comments, classComment, jsonFileName); const outPath = path.join(outPkgDir, `${className}.java`); fs.writeFileSync(outPath, content, 'utf8'); generated++; console.log('Generated', outPath); } catch (e) { console.warn('Failed for', jf, e.message); } } console.log(`Done. Generated ${generated} classes.`); })();