gen_pojo_from_xls_json.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. // Usage:
  5. // node gen_pojo_from_xls_json.js <sourceJsonDir> <sourceXlsDir> <packageName> <outputDir> [Tables...] [--ignore=a,b,c]
  6. // Tables can be names with or without .json/.xls, case-insensitive. Multiple can be space- or comma-separated.
  7. const [,, jsonDir, xlsDir, packageName, outputDir, ...rest] = process.argv;
  8. if (!jsonDir || !xlsDir || !packageName || !outputDir) {
  9. console.log('Usage: node gen_pojo_from_xls_json.js <sourceJsonDir> <sourceXlsDir> <packageName> <outputDir> [Tables...] [--ignore=a,b,c]');
  10. process.exit(1);
  11. }
  12. function normalizeName(n) {
  13. return String(n).toLowerCase().replace(/\.[^.]+$/, '');
  14. }
  15. function parseArgs(args) {
  16. const whitelist = [];
  17. const blacklist = [];
  18. for (let i = 0; i < args.length; i++) {
  19. const a = String(args[i]);
  20. if (a.startsWith('--ignore=')) {
  21. const v = a.substring('--ignore='.length);
  22. v.split(',').map(s => s.trim()).filter(Boolean).forEach(x => blacklist.push(normalizeName(x)));
  23. continue;
  24. }
  25. if (a === '--ignore' && i + 1 < args.length) {
  26. const v = String(args[++i]);
  27. v.split(',').map(s => s.trim()).filter(Boolean).forEach(x => blacklist.push(normalizeName(x)));
  28. continue;
  29. }
  30. whitelist.push(normalizeName(a));
  31. }
  32. return { whitelist: new Set(whitelist.filter(Boolean)), blacklist: new Set(blacklist) };
  33. }
  34. const { whitelist: whiteSet, blacklist: blackSet } = parseArgs(rest);
  35. function listJsonFiles(dir) {
  36. return fs.readdirSync(dir)
  37. .filter(name => name.toLowerCase().endsWith('.json'))
  38. .map(name => path.join(dir, name));
  39. }
  40. function readJson(filePath) {
  41. let content = fs.readFileSync(filePath, 'utf8');
  42. if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1);
  43. return JSON.parse(content);
  44. }
  45. function pickSample(root) {
  46. if (Array.isArray(root)) return root.length > 0 ? root[0] : null;
  47. if (root && typeof root === 'object') {
  48. const keys = Object.keys(root);
  49. return keys.length > 0 ? root[keys[0]] : null;
  50. }
  51. return null;
  52. }
  53. function upperFirst(str) {
  54. if (!str) return str;
  55. return str.charAt(0).toUpperCase() + str.slice(1);
  56. }
  57. // Convert filename like M_item.json -> MItem
  58. function toClassName(fileName) {
  59. const base = fileName.replace(/\.[^.]+$/, '');
  60. return base.split(/[_\-\s]+/).filter(Boolean).map(upperFirst).join('');
  61. }
  62. function isInteger(num) {
  63. return typeof num === 'number' && Number.isInteger(num);
  64. }
  65. function inferJavaType(value) {
  66. if (value === null || value === undefined) return 'String';
  67. if (typeof value === 'number') return isInteger(value) ? 'Integer' : 'BigDecimal';
  68. if (typeof value === 'boolean') return 'Boolean';
  69. if (typeof value === 'string') return 'String';
  70. if (Array.isArray(value)) {
  71. if (value.length === 0) return 'List<Object>';
  72. return `List<${inferJavaType(value[0])}>`;
  73. }
  74. if (typeof value === 'object') return 'Object';
  75. return 'String';
  76. }
  77. const JAVA_KEYWORDS = new Set([
  78. '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'
  79. ]);
  80. function sanitizeFieldName(name) {
  81. let safe = name.replace(/[^A-Za-z0-9_]/g, '_');
  82. if (!/^[A-Za-z_]/.test(safe)) safe = '_' + safe;
  83. if (JAVA_KEYWORDS.has(safe)) safe = safe + '_';
  84. return safe;
  85. }
  86. function readXlsComments(xlsFile) {
  87. try {
  88. const wb = xlsx.readFile(xlsFile);
  89. const sheetName = wb.SheetNames[0];
  90. const ws = wb.Sheets[sheetName];
  91. const rows = xlsx.utils.sheet_to_json(ws, { header: 1, raw: false });
  92. // Heuristics: within first 5 rows, pick field row as the one with max ASCII ratio and contains 'ID' or many word tokens
  93. const maxCheck = Math.min(5, rows.length);
  94. let fieldRowIdx = -1, commentRowIdx = -1;
  95. let bestScore = -1;
  96. for (let i = 0; i < maxCheck; i++) {
  97. const row = rows[i] || [];
  98. const joined = row.join(' ').trim();
  99. if (!joined) continue;
  100. const asciiCount = (joined.match(/[\x20-\x7E]/g) || []).length;
  101. const ratio = asciiCount / Math.max(1, joined.length);
  102. const wordCount = (joined.match(/[A-Za-z_][A-Za-z0-9_]*/g) || []).length;
  103. const score = ratio * 0.7 + Math.min(wordCount / Math.max(1, row.length), 1) * 0.3 + (joined.includes('ID') ? 0.2 : 0);
  104. if (score > bestScore) { bestScore = score; fieldRowIdx = i; }
  105. }
  106. // Comment row: prefer a different row with lower ASCII ratio (more likely Chinese)
  107. let bestCR = -1;
  108. for (let i = 0; i < maxCheck; i++) {
  109. if (i === fieldRowIdx) continue;
  110. const row = rows[i] || [];
  111. const joined = row.join(' ').trim();
  112. if (!joined) continue;
  113. const asciiCount = (joined.match(/[\x20-\x7E]/g) || []).length;
  114. const ratio = asciiCount / Math.max(1, joined.length);
  115. const score = 1 - ratio; // prefer non-ASCII
  116. if (score > bestCR) { bestCR = score; commentRowIdx = i; }
  117. }
  118. const fields = rows[fieldRowIdx] || [];
  119. const comments = rows[commentRowIdx] || [];
  120. const map = new Map();
  121. for (let i = 0; i < fields.length; i++) {
  122. const k = String(fields[i] || '').trim();
  123. if (!k) continue;
  124. const v = String(comments[i] || '').trim();
  125. if (k) map.set(k, v);
  126. }
  127. return { sheetName, comments: map };
  128. } catch (e) {
  129. return { sheetName: '', comments: new Map() };
  130. }
  131. }
  132. function findXlsForBase(xlsDir, baseName) {
  133. // Try .xls then .xlsx
  134. const cands = [path.join(xlsDir, baseName + '.xls'), path.join(xlsDir, baseName + '.xlsx')];
  135. for (const f of cands) {
  136. if (fs.existsSync(f)) return f;
  137. }
  138. // Case-insensitive search fallback
  139. const files = fs.readdirSync(xlsDir).filter(n => /\.xlsx?$/.test(n));
  140. for (const n of files) {
  141. if (normalizeName(n) === normalizeName(baseName)) return path.join(xlsDir, n);
  142. }
  143. return null;
  144. }
  145. function generateJavaClass(pkg, className, fieldsOrder, fieldTypes, fieldComments, classComment, jsonFileName) {
  146. const lines = [];
  147. lines.push(`package ${pkg};`);
  148. lines.push('');
  149. lines.push('import lombok.Data;');
  150. // Only add imports that are actually used
  151. const usedTypes = new Set();
  152. for (const fname of fieldsOrder) {
  153. const type = fieldTypes.get(fname) || 'String';
  154. if (type === 'List<Integer>') {
  155. usedTypes.add('java.util.List');
  156. } else if (type === 'List<String>') {
  157. usedTypes.add('java.util.List');
  158. } else if (type === 'List<Object>') {
  159. usedTypes.add('java.util.List');
  160. } else if (type === 'List<BigDecimal>') {
  161. usedTypes.add('java.util.List');
  162. usedTypes.add('java.math.BigDecimal');
  163. } else if (type === 'BigDecimal') {
  164. usedTypes.add('java.math.BigDecimal');
  165. }
  166. }
  167. // Add only the imports that are actually used
  168. usedTypes.forEach(importType => {
  169. lines.push(`import ${importType};`);
  170. });
  171. if (usedTypes.size > 0) {
  172. lines.push('');
  173. }
  174. if (classComment) {
  175. lines.push('/**');
  176. lines.push(` * 配置表:${classComment}`);
  177. lines.push(' */');
  178. }
  179. lines.push('@Data');
  180. lines.push(`public class ${className} {`);
  181. // Add path field using the actual JSON filename
  182. lines.push('\t/** JSON 配置文件路径 */');
  183. lines.push('\tpublic static final String path = "' + jsonFileName + '";');
  184. lines.push('');
  185. // Add fields
  186. for (const fname of fieldsOrder) {
  187. const type = fieldTypes.get(fname) || 'String';
  188. const cmt = fieldComments.get(fname);
  189. const safe = sanitizeFieldName(fname);
  190. if (cmt) {
  191. lines.push(`\t/** ${cmt} */`);
  192. }
  193. if (safe !== fname) {
  194. lines.push(`\t@JsonProperty("${fname}")`);
  195. }
  196. lines.push(`\tpublic ${type} ${safe};`);
  197. }
  198. lines.push('');
  199. lines.push('}');
  200. return lines.join('\n');
  201. }
  202. (function main() {
  203. if (!fs.existsSync(jsonDir)) {
  204. console.error('JSON dir not found:', jsonDir);
  205. process.exit(2);
  206. }
  207. if (!fs.existsSync(xlsDir)) {
  208. console.error('XLS dir not found:', xlsDir);
  209. process.exit(2);
  210. }
  211. const outPkgDir = path.join(outputDir, packageName.replace(/\./g, path.sep));
  212. fs.mkdirSync(outPkgDir, { recursive: true });
  213. let files = listJsonFiles(jsonDir);
  214. // Apply whitelist filter if provided
  215. if (whiteSet.size > 0) {
  216. files = files.filter(f => whiteSet.has(normalizeName(path.basename(f))));
  217. }
  218. // Apply blacklist filter if provided
  219. if (blackSet.size > 0) {
  220. files = files.filter(f => !blackSet.has(normalizeName(path.basename(f))));
  221. }
  222. if (files.length === 0) {
  223. console.log('No JSON files to process.');
  224. process.exit(0);
  225. }
  226. let generated = 0;
  227. for (const jf of files) {
  228. try {
  229. const base = path.basename(jf).replace(/\.[^.]+$/, '');
  230. const jsonRoot = readJson(jf);
  231. const sample = pickSample(jsonRoot);
  232. if (!sample || typeof sample !== 'object') { console.warn('Skip (no object sample):', jf); continue; }
  233. const xlsPath = findXlsForBase(xlsDir, base);
  234. const { sheetName, comments } = xlsPath ? readXlsComments(xlsPath) : { sheetName: '', comments: new Map() };
  235. const className = toClassName(base) + 'Pojo';
  236. const fieldsOrder = Object.keys(sample);
  237. const fieldTypes = new Map();
  238. for (const k of fieldsOrder) fieldTypes.set(k, inferJavaType(sample[k]));
  239. const classComment = sheetName ? `${base}(${sheetName})` : `${base}`;
  240. const jsonFileName = path.basename(jf);
  241. const content = generateJavaClass(packageName, className, fieldsOrder, fieldTypes, comments, classComment, jsonFileName);
  242. const outPath = path.join(outPkgDir, `${className}.java`);
  243. fs.writeFileSync(outPath, content, 'utf8');
  244. generated++;
  245. console.log('Generated', outPath);
  246. } catch (e) {
  247. console.warn('Failed for', jf, e.message);
  248. }
  249. }
  250. console.log(`Done. Generated ${generated} classes.`);
  251. })();