570 lines
22 KiB
Java
570 lines
22 KiB
Java
import java.sql.*;
|
||
import java.util.*;
|
||
|
||
/**
|
||
* 字段配置迁移工具(面向 V2 表结构)。
|
||
*
|
||
* 从旧库 G3HY2025.dbo.s_columnLib 同步到新库 FMS:
|
||
* 1. col_Caption → s_field.b_name (字段名称)
|
||
* 2. col_Visible → s_module_schema(b_schema_type='view') 的 columns (列表列)
|
||
* 3. col_edit → s_module_schema(b_schema_type='edit') 的 children (表单字段)
|
||
*
|
||
* 关联规则:
|
||
* s_columnLib.col_TableName = s_module.b_view_table
|
||
* s_columnLib.col_FieldName = s_field.b_field
|
||
* 两侧用 Chinese_PRC_CI_AS 比较,兼容 v_b_OtherCompany 这类大小写差异。
|
||
* 重命名字段走别名映射(b_vessel_name → b_name、b_name_c → b_name,见 FIELD_ALIAS)。
|
||
*
|
||
* 列表/表单的成员资格沿用旧库语义:旧库标志为 '1' 的字段才进入对应配置,
|
||
* 其余字段(标志为 '0'、空、无记录)**直接不写入 JSON**,不写 visible:false。
|
||
* view 的列宽取 col_Width、顺序取 col_Position;edit 的顺序取 col_edit_position。
|
||
*
|
||
* 写入方式为「整份重建」:view / edit 两份 b_schema_json 完整重新生成,
|
||
* 不保留模块管理界面里手工调整的布局,已有非空行需 --force 才覆盖。
|
||
*
|
||
* JSON 结构与消费侧一致,见 fms-vue/src/components/fms-module-common/schemaRender.js。
|
||
*
|
||
* 用法:
|
||
* javac -encoding UTF-8 -cp "mssql-jdbc-13.4.0.jre11.jar" FieldNameSync.java
|
||
* java -Dstdout.encoding=UTF-8 -cp ".;mssql-jdbc-13.4.0.jre11.jar" FieldNameSync <moduleId> [oldTableName] [选项]
|
||
*
|
||
* <moduleId> s_module.b_id,例如 v_b_othercompany
|
||
* [oldTableName] 可选,覆盖旧库表/视图名;缺省用 s_module.b_view_table
|
||
* --dry-run 只报告不写库
|
||
* --skip-schema 只同步 s_field.b_name,不重建 s_module_schema
|
||
* --force 允许覆盖已存在且非空的 s_module_schema 行
|
||
* --all-otherdata 批量模式:同步 base_otherdata 下所有数据模块(不需要位置参数,失败跳过继续)
|
||
*/
|
||
public class FieldNameSync {
|
||
|
||
static final String URL = "jdbc:sqlserver://118.89.70.199:1433;databaseName=FMS;encrypt=true;trustServerCertificate=true";
|
||
static final String USER = "sa";
|
||
static final String PASSWORD = "zhang520..";
|
||
|
||
// ---- 选项 ----
|
||
boolean dryRun = false;
|
||
boolean skipSchema = false;
|
||
boolean force = false;
|
||
boolean allOtherData = false;
|
||
boolean batchMode = false; // 批量子任务:出错抛异常交给批量汇总,而不是 System.exit
|
||
String moduleId;
|
||
String oldTableOverride;
|
||
|
||
// ---- 运行期数据 ----
|
||
Connection conn;
|
||
String oldTable;
|
||
String moduleName;
|
||
String moduleType;
|
||
final List<Field> fields = new ArrayList<>();
|
||
final Map<String, OldColumn> oldColumns = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||
|
||
/** s_field 行 */
|
||
static class Field {
|
||
String name;
|
||
String label;
|
||
}
|
||
|
||
/** s_columnLib 行(只取本工具用得到的列) */
|
||
static class OldColumn {
|
||
String caption;
|
||
String visible;
|
||
String edit;
|
||
Integer width; // col_Width,未配置时为 null
|
||
Integer position; // col_Position
|
||
Integer editPosition; // col_edit_position
|
||
}
|
||
|
||
/**
|
||
* 旧库字段名 → 新系统字段名。
|
||
* 表设计里做过重命名的字段:b_vessel_name → b_name(船名)、b_name_c → b_name(中文名)。
|
||
*/
|
||
private static final Map<String, String> FIELD_ALIAS = Map.of(
|
||
"b_vessel_name", "b_name",
|
||
"b_name_c", "b_name");
|
||
|
||
// ------------------------------------------------------------------ SQL
|
||
static final String QUERY_MODULE = """
|
||
SELECT b_id, b_view_table, b_name, b_module_type
|
||
FROM dbo.s_module
|
||
WHERE b_id = ?
|
||
""";
|
||
|
||
static final String QUERY_FIELDS = """
|
||
SELECT b_field, b_name
|
||
FROM dbo.s_field
|
||
WHERE b_module_id = ? AND b_canuse = 1
|
||
ORDER BY b_xh, b_field
|
||
""";
|
||
|
||
static final String QUERY_OLD_COLUMNS = """
|
||
SELECT col_FieldName, col_Caption, col_Visible, col_edit,
|
||
col_Width, col_Position, col_edit_position
|
||
FROM G3HY2025.dbo.s_columnLib
|
||
WHERE col_TableName COLLATE Chinese_PRC_CI_AS = CONVERT(varchar(128), ?) COLLATE Chinese_PRC_CI_AS
|
||
ORDER BY col_FieldName
|
||
""";
|
||
|
||
static final String UPDATE_FIELD_NAME = """
|
||
UPDATE dbo.s_field SET b_name = ?
|
||
WHERE b_module_id = ? AND b_field = ?
|
||
""";
|
||
|
||
static final String QUERY_SCHEMA_ROW = """
|
||
SELECT b_schema_json FROM dbo.s_module_schema
|
||
WHERE b_module_id = ? AND b_schema_type = ?
|
||
""";
|
||
|
||
static final String UPDATE_SCHEMA = """
|
||
UPDATE dbo.s_module_schema
|
||
SET b_schema_json = ?, b_canuse = 1, b_updated_at = SYSDATETIME()
|
||
WHERE b_module_id = ? AND b_schema_type = ?
|
||
""";
|
||
|
||
static final String INSERT_SCHEMA = """
|
||
INSERT INTO dbo.s_module_schema (b_module_id, b_schema_type, b_schema_json, b_canuse, b_updated_at)
|
||
VALUES (?, ?, ?, 1, SYSDATETIME())
|
||
""";
|
||
|
||
// --------------------------------------------------------------- 入口
|
||
public static void main(String[] args) throws Exception {
|
||
FieldNameSync tool = new FieldNameSync();
|
||
tool.parseArgs(args);
|
||
if (tool.allOtherData) {
|
||
tool.runBatch();
|
||
} else {
|
||
tool.run();
|
||
}
|
||
}
|
||
|
||
/** 批量:同步 base_otherdata 下所有数据模块;每个模块独立事务,失败跳过并继续 */
|
||
void runBatch() throws SQLException {
|
||
List<String> ids = new ArrayList<>();
|
||
try (Connection c = DriverManager.getConnection(URL, USER, PASSWORD);
|
||
PreparedStatement ps = c.prepareStatement(
|
||
"SELECT b_id FROM dbo.s_module WHERE b_path LIKE '/base/base_otherdata/%' "
|
||
+ "AND b_module_type = 'data' ORDER BY b_xh, b_id");
|
||
ResultSet rs = ps.executeQuery()) {
|
||
while (rs.next()) {
|
||
ids.add(rs.getString(1));
|
||
}
|
||
}
|
||
System.out.println("批量同步 " + ids.size() + " 个模块(base_otherdata)" + (dryRun ? "[dry-run]" : "") + "\n");
|
||
|
||
int ok = 0;
|
||
int failed = 0;
|
||
for (String id : ids) {
|
||
FieldNameSync one = new FieldNameSync();
|
||
one.dryRun = dryRun;
|
||
one.skipSchema = skipSchema;
|
||
one.force = force;
|
||
one.batchMode = true;
|
||
one.moduleId = id;
|
||
try {
|
||
one.run();
|
||
ok++;
|
||
} catch (Exception e) {
|
||
failed++;
|
||
System.err.println("[失败] " + id + ": " + e.getMessage());
|
||
}
|
||
}
|
||
System.out.println("\n批量完成:成功 " + ok + " 个,失败 " + failed + " 个");
|
||
}
|
||
|
||
void parseArgs(String[] args) {
|
||
List<String> positional = new ArrayList<>();
|
||
for (String raw : args) {
|
||
String arg = raw.trim();
|
||
switch (arg.toLowerCase(Locale.ROOT)) {
|
||
case "--dry-run" -> dryRun = true;
|
||
case "--skip-schema" -> skipSchema = true;
|
||
case "--force" -> force = true;
|
||
case "--all-otherdata" -> allOtherData = true;
|
||
default -> {
|
||
if (arg.startsWith("--")) {
|
||
System.err.println("未知选项: " + arg);
|
||
usage();
|
||
System.exit(1);
|
||
}
|
||
positional.add(arg);
|
||
}
|
||
}
|
||
}
|
||
if (positional.isEmpty()) {
|
||
if (!allOtherData) {
|
||
usage();
|
||
System.exit(1);
|
||
}
|
||
return;
|
||
}
|
||
moduleId = positional.get(0);
|
||
if (positional.size() > 1) oldTableOverride = positional.get(1);
|
||
if (positional.size() > 2) {
|
||
System.err.println("多余参数: " + positional.get(2));
|
||
usage();
|
||
System.exit(1);
|
||
}
|
||
}
|
||
|
||
static void usage() {
|
||
System.err.println("用法: java FieldNameSync <moduleId> [oldTableName] [--dry-run] [--skip-schema] [--force]");
|
||
System.err.println(" java FieldNameSync --all-otherdata [--dry-run] [--force] 批量同步 base_otherdata 下所有数据模块");
|
||
}
|
||
|
||
void run() throws SQLException {
|
||
try (Connection c = DriverManager.getConnection(URL, USER, PASSWORD)) {
|
||
conn = c;
|
||
conn.setAutoCommit(false);
|
||
try {
|
||
loadModule();
|
||
loadFields();
|
||
loadOldColumns();
|
||
|
||
if (fields.isEmpty()) {
|
||
System.out.println("\n模块 " + moduleId + " 在 s_field 中没有启用字段,无可同步内容。");
|
||
conn.rollback();
|
||
return;
|
||
}
|
||
|
||
printSnapshot("同步前");
|
||
|
||
if (!skipSchema) {
|
||
// 写入前先校验:已有非空行且未加 --force 时整体拒绝,避免半途写入
|
||
checkSchemaGuard();
|
||
}
|
||
|
||
int renamed = syncFieldNames();
|
||
int[] viewStat = null;
|
||
int[] editStat = null;
|
||
if (!skipSchema) {
|
||
viewStat = writeSchema("view", buildViewJson());
|
||
editStat = writeSchema("edit", buildEditJson());
|
||
}
|
||
|
||
if (dryRun) {
|
||
conn.rollback();
|
||
System.out.println("\n[Dry-run] 已回滚,未写入任何数据。");
|
||
} else {
|
||
conn.commit();
|
||
System.out.println("\n=== 事务已提交 ===");
|
||
}
|
||
|
||
printSummary(renamed, viewStat, editStat);
|
||
|
||
if (!dryRun) {
|
||
// 重新读一次 s_field,同步后的快照才反映实际写入的名称
|
||
fields.clear();
|
||
loadFields();
|
||
printSnapshot("同步后");
|
||
}
|
||
} catch (Exception e) {
|
||
conn.rollback();
|
||
if (batchMode) {
|
||
throw new SQLException(e.getMessage(), e);
|
||
}
|
||
System.err.println("\n[错误] " + e.getMessage());
|
||
System.exit(1);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ------------------------------------------------------------ 数据加载
|
||
void loadModule() throws SQLException {
|
||
String bViewTable;
|
||
try (PreparedStatement ps = conn.prepareStatement(QUERY_MODULE)) {
|
||
ps.setString(1, moduleId);
|
||
try (ResultSet rs = ps.executeQuery()) {
|
||
if (!rs.next()) {
|
||
throw new IllegalStateException("模块不存在: " + moduleId + "(s_module.b_id)");
|
||
}
|
||
bViewTable = trim(rs.getString("b_view_table"));
|
||
moduleName = trim(rs.getString("b_name"));
|
||
moduleType = trim(rs.getString("b_module_type"));
|
||
}
|
||
}
|
||
System.out.println("模块: " + moduleId
|
||
+ (moduleName != null ? "(" + moduleName + ")" : "")
|
||
+ " 类型: " + (moduleType != null ? moduleType : "(null)"));
|
||
|
||
if (oldTableOverride != null) {
|
||
oldTable = oldTableOverride;
|
||
System.out.println("关联旧库表: " + oldTable + "(命令行指定,覆盖 b_view_table=" + bViewTable + ")");
|
||
} else {
|
||
oldTable = bViewTable;
|
||
if (oldTable == null || oldTable.isEmpty()) {
|
||
throw new IllegalStateException(
|
||
"模块 " + moduleId + " 未配置 b_view_table,请用第二个参数显式指定旧库表/视图名");
|
||
}
|
||
System.out.println("关联旧库表: " + oldTable + "(取自 s_module.b_view_table)");
|
||
}
|
||
}
|
||
|
||
void loadFields() throws SQLException {
|
||
try (PreparedStatement ps = conn.prepareStatement(QUERY_FIELDS)) {
|
||
ps.setString(1, moduleId);
|
||
try (ResultSet rs = ps.executeQuery()) {
|
||
while (rs.next()) {
|
||
Field f = new Field();
|
||
f.name = trim(rs.getString("b_field"));
|
||
f.label = trim(rs.getString("b_name"));
|
||
fields.add(f);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void loadOldColumns() throws SQLException {
|
||
int rows = 0;
|
||
try (PreparedStatement ps = conn.prepareStatement(QUERY_OLD_COLUMNS)) {
|
||
ps.setString(1, oldTable);
|
||
try (ResultSet rs = ps.executeQuery()) {
|
||
while (rs.next()) {
|
||
rows++;
|
||
OldColumn col = new OldColumn();
|
||
col.caption = trim(rs.getString("col_Caption"));
|
||
col.visible = trim(rs.getString("col_Visible"));
|
||
col.edit = trim(rs.getString("col_edit"));
|
||
col.width = intOrNull(rs, "col_Width");
|
||
col.position = intOrNull(rs, "col_Position");
|
||
col.editPosition = intOrNull(rs, "col_edit_position");
|
||
String fieldName = trim(rs.getString("col_FieldName"));
|
||
oldColumns.put(FIELD_ALIAS.getOrDefault(fieldName.toLowerCase(Locale.ROOT), fieldName), col);
|
||
}
|
||
}
|
||
}
|
||
|
||
long inList = countIncluded("view");
|
||
long inForm = countIncluded("edit");
|
||
System.out.println("旧库 s_columnLib: " + rows + " 行,去重字段 " + oldColumns.size() + " 个;"
|
||
+ "当前模块 " + fields.size() + " 个字段中:进列表 " + inList + " 个、进表单 " + inForm + " 个");
|
||
if (inList == 0 && inForm == 0) {
|
||
System.out.println(" [警告] 没有任何字段匹配上,请确认旧库表名是否正确(可用第二个参数显式指定)");
|
||
}
|
||
}
|
||
|
||
static Integer intOrNull(ResultSet rs, String column) throws SQLException {
|
||
int v = rs.getInt(column);
|
||
return rs.wasNull() ? null : v;
|
||
}
|
||
|
||
// ------------------------------------------------------------ 成员与顺序
|
||
/** 旧库标志为 '1' 才进入对应配置 */
|
||
static boolean flagOn(String flag) {
|
||
return "1".equals(flag);
|
||
}
|
||
|
||
/** 取模块字段在旧库里的对应行;旧库没有该字段时返回 null */
|
||
OldColumn oldOf(Field f) {
|
||
return oldColumns.get(f.name);
|
||
}
|
||
|
||
/** 按 schemaType 取出旧库要求进入配置的字段,顺序取旧库位置列 */
|
||
List<Field> includedFields(String schemaType) {
|
||
boolean view = "view".equals(schemaType);
|
||
List<Field> result = new ArrayList<>();
|
||
for (Field f : fields) {
|
||
OldColumn old = oldOf(f);
|
||
if (old == null) continue;
|
||
if (flagOn(view ? old.visible : old.edit)) result.add(f);
|
||
}
|
||
result.sort(Comparator
|
||
.comparingInt((Field f) -> positionOf(oldOf(f), view))
|
||
.thenComparing(f -> f.name));
|
||
return result;
|
||
}
|
||
|
||
static int positionOf(OldColumn old, boolean view) {
|
||
Integer p = view ? old.position : old.editPosition;
|
||
// 位置缺失的排在最后
|
||
return p == null ? Integer.MAX_VALUE : p;
|
||
}
|
||
|
||
long countIncluded(String schemaType) {
|
||
return includedFields(schemaType).size();
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 步骤
|
||
int syncFieldNames() throws SQLException {
|
||
System.out.println("\n=== 1) 字段名称 (col_Caption → s_field.b_name) ===");
|
||
int updated = 0;
|
||
int missing = 0;
|
||
try (PreparedStatement ps = conn.prepareStatement(UPDATE_FIELD_NAME)) {
|
||
for (Field f : fields) {
|
||
OldColumn old = oldOf(f);
|
||
String caption = old != null ? old.caption : null;
|
||
if (caption == null || caption.isEmpty()) {
|
||
missing++;
|
||
continue;
|
||
}
|
||
if (caption.equals(f.label)) {
|
||
continue;
|
||
}
|
||
ps.setString(1, caption);
|
||
ps.setString(2, moduleId);
|
||
ps.setString(3, f.name);
|
||
updated += ps.executeUpdate();
|
||
}
|
||
}
|
||
System.out.println(" 更新: " + updated + " 条;旧库无名称、保留原值: " + missing + " 条");
|
||
return updated;
|
||
}
|
||
|
||
/** 备注列统一排到最后(其他数据字典表的展示习惯) */
|
||
private static void moveBzLast(List<Field> list) {
|
||
Field bz = null;
|
||
for (Iterator<Field> iterator = list.iterator(); iterator.hasNext(); ) {
|
||
Field field = iterator.next();
|
||
if ("b_bz".equalsIgnoreCase(field.name)) {
|
||
bz = field;
|
||
iterator.remove();
|
||
break;
|
||
}
|
||
}
|
||
if (bz != null) {
|
||
list.add(bz);
|
||
}
|
||
}
|
||
|
||
/** view 配置:{"schemaVersion":1,"columns":[{"field":..,"width":..}]},只含旧库 col_Visible='1' 的字段;备注列固定排最后 */
|
||
String buildViewJson() {
|
||
StringBuilder sb = new StringBuilder("{\"schemaVersion\":1,\"columns\":[");
|
||
List<Field> list = new ArrayList<>(includedFields("view"));
|
||
moveBzLast(list);
|
||
for (int i = 0; i < list.size(); i++) {
|
||
Field f = list.get(i);
|
||
if (i > 0) sb.append(',');
|
||
sb.append("{\"field\":\"").append(escape(f.name)).append('"');
|
||
Integer width = oldOf(f).width;
|
||
if (width != null && width > 0) sb.append(",\"width\":").append(width);
|
||
sb.append('}');
|
||
}
|
||
return sb.append("]}").toString();
|
||
}
|
||
|
||
/** edit 配置:{"schemaVersion":1,"children":[{"field":..,"span":24}]},只含旧库 col_edit='1' 的字段;备注列固定排最后 */
|
||
String buildEditJson() {
|
||
StringBuilder sb = new StringBuilder("{\"schemaVersion\":1,\"children\":[");
|
||
List<Field> list = new ArrayList<>(includedFields("edit"));
|
||
moveBzLast(list);
|
||
for (int i = 0; i < list.size(); i++) {
|
||
Field f = list.get(i);
|
||
if (i > 0) sb.append(',');
|
||
sb.append("{\"field\":\"").append(escape(f.name)).append('"');
|
||
sb.append(",\"span\":24");
|
||
sb.append('}');
|
||
}
|
||
return sb.append("]}").toString();
|
||
}
|
||
|
||
/** 字段名是 [A-Za-z0-9_],仍保留最小转义以防意外字符 */
|
||
static String escape(String s) {
|
||
return s == null ? "" : s.replace("\\", "\\\\").replace("\"", "\\\"");
|
||
}
|
||
|
||
void checkSchemaGuard() throws SQLException {
|
||
for (String type : List.of("view", "edit")) {
|
||
String existing = readSchemaRow(type);
|
||
if (existing != null && !existing.isEmpty() && !force) {
|
||
throw new IllegalStateException(
|
||
"s_module_schema 已有非空 " + type + " 配置(" + existing.length()
|
||
+ " 字符),整份重建会覆盖模块管理中手工调整的布局。确认后加 --force 重跑。");
|
||
}
|
||
}
|
||
}
|
||
|
||
String readSchemaRow(String schemaType) throws SQLException {
|
||
try (PreparedStatement ps = conn.prepareStatement(QUERY_SCHEMA_ROW)) {
|
||
ps.setString(1, moduleId);
|
||
ps.setString(2, schemaType);
|
||
try (ResultSet rs = ps.executeQuery()) {
|
||
if (rs.next()) return rs.getString(1);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** @return {节点数, 未进入配置的字段数, 写入方式(1=更新, 2=新增)} */
|
||
int[] writeSchema(String schemaType, String json) throws SQLException {
|
||
boolean exists = readSchemaRow(schemaType) != null;
|
||
if (exists) {
|
||
try (PreparedStatement ps = conn.prepareStatement(UPDATE_SCHEMA)) {
|
||
ps.setString(1, json);
|
||
ps.setString(2, moduleId);
|
||
ps.setString(3, schemaType);
|
||
ps.executeUpdate();
|
||
}
|
||
} else {
|
||
try (PreparedStatement ps = conn.prepareStatement(INSERT_SCHEMA)) {
|
||
ps.setString(1, moduleId);
|
||
ps.setString(2, schemaType);
|
||
ps.setString(3, json);
|
||
ps.executeUpdate();
|
||
}
|
||
}
|
||
int included = includedFields(schemaType).size();
|
||
return new int[]{included, fields.size() - included, exists ? 1 : 2};
|
||
}
|
||
|
||
// -------------------------------------------------------------- 输出
|
||
void printSummary(int renamed, int[] viewStat, int[] editStat) {
|
||
System.out.println("\n=== 摘要 ===");
|
||
System.out.println(" s_field.b_name 更新: " + renamed + " 条");
|
||
if (skipSchema) {
|
||
System.out.println(" s_module_schema: 已跳过(--skip-schema)");
|
||
return;
|
||
}
|
||
printStat("view", viewStat);
|
||
printStat("edit", editStat);
|
||
if (readSchemaRowSafe("query") == null) {
|
||
System.out.println(" 提示: 该模块尚无 query 配置行,工具不处理 query,需要时在模块管理中生成。");
|
||
}
|
||
}
|
||
|
||
static void printStat(String schemaType, int[] stat) {
|
||
if (stat == null) return;
|
||
System.out.println(" s_module_schema(" + schemaType + "): "
|
||
+ stat[0] + " 个节点,未进入配置 " + stat[1] + " 个字段,"
|
||
+ (stat[2] == 1 ? "更新已有行" : "新增行"));
|
||
}
|
||
|
||
String readSchemaRowSafe(String schemaType) {
|
||
try {
|
||
return readSchemaRow(schemaType);
|
||
} catch (SQLException e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
void printSnapshot(String title) throws SQLException {
|
||
System.out.println("\n=== " + title + " ===");
|
||
System.out.printf(" %-24s %-22s %-22s %-10s %-10s%n",
|
||
"b_field", "s_field.b_name", "col_Caption", "进列表", "进表单");
|
||
System.out.println(" " + "-".repeat(94));
|
||
for (Field f : fields) {
|
||
OldColumn old = oldOf(f);
|
||
System.out.printf(" %-24s %-22s %-22s %-10s %-10s%n",
|
||
truncate(f.name, 23),
|
||
truncate(orNull(f.label), 21),
|
||
truncate(orNull(old != null ? old.caption : null), 21),
|
||
yesNo(old != null && flagOn(old.visible)),
|
||
yesNo(old != null && flagOn(old.edit)));
|
||
}
|
||
}
|
||
|
||
static String yesNo(boolean b) {
|
||
return b ? "是" : "否";
|
||
}
|
||
|
||
static String orNull(String s) {
|
||
return s == null ? "(null)" : s;
|
||
}
|
||
|
||
static String truncate(String s, int max) {
|
||
if (s == null) return "";
|
||
return s.length() <= max ? s : s.substring(0, max - 1) + "…";
|
||
}
|
||
|
||
static String trim(String s) {
|
||
return s == null ? null : s.trim();
|
||
}
|
||
}
|