gen_pojo_from_xls_json.js 9.3 KB

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