package jnpf.onlinedev.model; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jnpf.exception.DataException; import lombok.Getter; import jnpf.util.visiual.JnpfKeyConsts; import java.util.*; /** Shared field discovery for document annotation and display-value rendering. */ public final class FormDisplaySchema { private static final ObjectMapper JSON = new ObjectMapper(); // Use the same constants as OnlineSwapDataUtils; UI labels are not control keys. private static final Set TEXT_TYPES = Set.of("", JnpfKeyConsts.COM_INPUT, JnpfKeyConsts.TEXTAREA, JnpfKeyConsts.NUM_INPUT, JnpfKeyConsts.CALCULATE, JnpfKeyConsts.DATE, JnpfKeyConsts.TIME, JnpfKeyConsts.DATE_CALCULATE, JnpfKeyConsts.SWITCH, JnpfKeyConsts.RATE, JnpfKeyConsts.SLIDER, JnpfKeyConsts.BILLRULE, JnpfKeyConsts.CREATETIME, JnpfKeyConsts.MODIFYTIME, JnpfKeyConsts.RELATIONFORM_ATTR, JnpfKeyConsts.POPUPSELECT_ATTR); private static final Set REFERENCE_TYPES = Set.of(JnpfKeyConsts.DEPSELECT, JnpfKeyConsts.COMSELECT, JnpfKeyConsts.USERSELECT, JnpfKeyConsts.POSSELECT, JnpfKeyConsts.ROLESELECT, JnpfKeyConsts.GROUPSELECT, JnpfKeyConsts.CURRDEPT, JnpfKeyConsts.CURRORGANIZE, JnpfKeyConsts.CURRPOSITION, JnpfKeyConsts.CREATEUSER, JnpfKeyConsts.MODIFYUSER, JnpfKeyConsts.SELECT, JnpfKeyConsts.RADIO, JnpfKeyConsts.CHECKBOX, JnpfKeyConsts.TREESELECT, JnpfKeyConsts.CASCADER, JnpfKeyConsts.RELATIONFORM, JnpfKeyConsts.POPUPSELECT, JnpfKeyConsts.POPUPTABLESELECT, JnpfKeyConsts.ADDRESS); private FormDisplaySchema() { } public static List parse(String source) { try { List result = new ArrayList<>(); collect(JSON.readTree(source), "", "", result); Set seen = new HashSet<>(); for (Field field : result) if (!seen.add(field.path)) throw new DataException("表单字段路径重复: " + field.path); return result; } catch (DataException e) { throw e; } catch (Exception e) { throw new DataException("无法解析签批页表单配置: " + e.getMessage()); } } private static void collect(JsonNode node, String parent, String label, List result) throws Exception { if (node == null || node.isNull()) return; if (node.isTextual()) { collect(JSON.readTree(node.asText()), parent, label, result); return; } if (node.isArray()) { for (JsonNode child : node) collect(child, parent, label, result); return; } if (!node.isObject()) return; if (node.has("fields")) { collect(node.get("fields"), parent, label, result); return; } JsonNode config = node.path("__config__"); String key = node.path("__vModel__").asText(node.path("vModel").asText("")); String type = config.path("jnpfKey").asText(""); String title = config.path("label").asText(node.path("label").asText(key)); JsonNode children = config.has("children") ? config.get("children") : node.get("children"); if (children != null && children.isTextual()) children = JSON.readTree(children.asText()); boolean table = "table".equals(type) || (type.isEmpty() && validKey(key) && children != null && !children.isEmpty()); if (children != null && !children.isEmpty()) { if (table && (!parent.isEmpty() || !validKey(key))) { result.add(new Field(join(parent, key), joinLabel(label, title), type, node, "暂不支持嵌套子表")); return; } collect(children, table ? key : parent, table ? joinLabel(label, title) : label, result); return; } if (!validKey(key) || "biz_sign".equals(key)) return; result.add(new Field(join(parent, key), joinLabel(label, title), type, node, TEXT_TYPES.contains(type) || REFERENCE_TYPES.contains(type) ? "" : "暂不支持此控件的文本回填")); } public static boolean validKey(String value) { return value != null && value.matches("[A-Za-z_][A-Za-z0-9_]*"); } private static String join(String a, String b) { return a.isEmpty() ? b : a + "." + b; } private static String joinLabel(String a, String b) { return a.isEmpty() ? b : a + "-" + b; } @Getter public static final class Field { private final String path; private final String label; private final String type; private final JsonNode configuration; private final String reason; Field(String path, String label, String type, JsonNode configuration, String reason) { this.path = path; this.label = label; this.type = type; this.configuration = configuration; this.reason = reason; } public String displaySignature() { // Ignore presentation/layout, but never merge incompatible lookup/format definitions. return type + "|" + configuration.path("format") + "|" + configuration.path("multiple") + "|" + configuration.path("options") + "|" + configuration.path("props") + "|" + configuration.path("__config__").path("dataType") + "|" + configuration.path("__config__").path("dictionaryType") + "|" + configuration.path("__config__").path("propsUrl") + "|" + configuration.path("__config__").path("templateJson"); } public boolean isSupported() { return reason.isEmpty(); } public boolean isReference() { return REFERENCE_TYPES.contains(type); } public String getTable() { int dot = path.indexOf('.'); return dot < 0 ? "" : path.substring(0, dot); } public String getColumn() { return path.substring(path.lastIndexOf('.') + 1); } } }