gen_pojo_from_xls_json.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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' : 'Double';
  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. }
  161. }
  162. // Add only the imports that are actually used
  163. usedTypes.forEach(importType => {
  164. lines.push(`import ${importType};`);
  165. });
  166. if (usedTypes.size > 0) {
  167. lines.push('');
  168. }
  169. if (classComment) {
  170. lines.push('/**');
  171. lines.push(` * 配置表:${classComment}`);
  172. lines.push(' */');
  173. }
  174. lines.push('@Data');
  175. lines.push(`public class ${className} {`);
  176. // Add path field using the actual JSON filename
  177. lines.push('\t/** JSON 配置文件路径 */');
  178. lines.push('\tpublic static final String path = "table/' + jsonFileName + '";');
  179. lines.push('');
  180. // Add fields
  181. for (const fname of fieldsOrder) {
  182. const type = fieldTypes.get(fname) || 'String';
  183. const cmt = fieldComments.get(fname);
  184. const safe = sanitizeFieldName(fname);
  185. if (cmt) {
  186. lines.push(`\t/** ${cmt} */`);
  187. }
  188. if (safe !== fname) {
  189. lines.push(`\t@JsonProperty("${fname}")`);
  190. }
  191. lines.push(`\tpublic ${type} ${safe};`);
  192. }
  193. lines.push('');
  194. lines.push('}');
  195. return lines.join('\n');
  196. }
  197. (function main() {
  198. if (!fs.existsSync(jsonDir)) {
  199. console.error('JSON dir not found:', jsonDir);
  200. process.exit(2);
  201. }
  202. if (!fs.existsSync(xlsDir)) {
  203. console.error('XLS dir not found:', xlsDir);
  204. process.exit(2);
  205. }
  206. const outPkgDir = path.join(outputDir, packageName.replace(/\./g, path.sep));
  207. fs.mkdirSync(outPkgDir, { recursive: true });
  208. let files = listJsonFiles(jsonDir);
  209. // Apply whitelist filter if provided
  210. if (whiteSet.size > 0) {
  211. files = files.filter(f => whiteSet.has(normalizeName(path.basename(f))));
  212. }
  213. // Apply blacklist filter if provided
  214. if (blackSet.size > 0) {
  215. files = files.filter(f => !blackSet.has(normalizeName(path.basename(f))));
  216. }
  217. if (files.length === 0) {
  218. console.log('No JSON files to process.');
  219. process.exit(0);
  220. }
  221. let generated = 0;
  222. for (const jf of files) {
  223. try {
  224. const base = path.basename(jf).replace(/\.[^.]+$/, '');
  225. const jsonRoot = readJson(jf);
  226. const sample = pickSample(jsonRoot);
  227. if (!sample || typeof sample !== 'object') { console.warn('Skip (no object sample):', jf); continue; }
  228. const xlsPath = findXlsForBase(xlsDir, base);
  229. const { sheetName, comments } = xlsPath ? readXlsComments(xlsPath) : { sheetName: '', comments: new Map() };
  230. const className = toClassName(base) + 'Pojo';
  231. const fieldsOrder = Object.keys(sample);
  232. const fieldTypes = new Map();
  233. for (const k of fieldsOrder) fieldTypes.set(k, inferJavaType(sample[k]));
  234. const classComment = sheetName ? `${base}(${sheetName})` : `${base}`;
  235. const jsonFileName = path.basename(jf);
  236. const content = generateJavaClass(packageName, className, fieldsOrder, fieldTypes, comments, classComment, jsonFileName);
  237. const outPath = path.join(outPkgDir, `${className}.java`);
  238. fs.writeFileSync(outPath, content, 'utf8');
  239. generated++;
  240. console.log('Generated', outPath);
  241. } catch (e) {
  242. console.warn('Failed for', jf, e.message);
  243. }
  244. }
  245. console.log(`Done. Generated ${generated} classes.`);
  246. })();