jingbo 2 месяцев назад
Родитель
Сommit
c65eb197cb

BIN
_tmp_decompile/com/jeesite/common/service/CrudService.class


BIN
_tmp_decompile/com/jeesite/common/service/TreeService.class


+ 120 - 82
scripts/backup-ui.html

@@ -66,34 +66,61 @@
       padding: 12px; height: 200px; overflow: auto; font-family: Consolas, monospace;
       font-size: 12px; white-space: pre-wrap; color: #cbd5e1;
     }
-    .status { font-size: 13px; margin-left: 10px; }
+    .form {
+      display: grid;
+      grid-template-columns: 72px 1fr 72px 1fr;
+      gap: 10px 12px;
+      align-items: center;
+    }
+    .form label { color: var(--muted); font-size: 13px; }
+    .form input, .form select {
+      background: #0f172a; border: 1px solid var(--line); color: var(--text);
+      border-radius: 8px; padding: 8px 10px; font-size: 14px; width: 100%;
+    }
+    .form .readonly {
+      background: #0b1220; color: var(--muted);
+    }
+    .cfg-msg { display: block; margin-top: 10px; min-height: 22px; font-size: 14px; font-weight: 600; }
     .ok { color: var(--ok); } .bad { color: var(--bad); } .run { color: var(--warn); }
   </style>
 </head>
 <body>
   <div class="wrap">
-    <h1>MES 数据库手动备份</h1>
-    <p class="sub">独立工具。点磁盘或点「浏览文件夹」选择保存位置(U 盘 / 移动硬盘 / 本机磁盘),不用手输路径。数据会边导出边压缩;库很大时请预留足够空间,完成前不要拔盘。</p>
+    <h1>MES 数据库手动备份 · 选文件夹</h1>
+    <p class="sub">独立工具,仅支持 MySQL。填写连接后,点「浏览文件夹」选择 U 盘或硬盘目录(不用手输、打开时不扫描磁盘)。数据会边导出边压缩,完成前不要拔盘。</p>
 
     <div class="card">
       <div class="row">
-        <h2>1. 点选磁盘</h2>
-        <button class="btn ghost" id="btnRefresh" type="button">刷新磁盘</button>
+        <h2>1. MySQL 连接(可配置)</h2>
+      </div>
+      <div class="form">
+        <label>类型</label>
+        <input class="readonly" value="MySQL" readonly />
+        <label>端口</label>
+        <input id="dbPort" type="number" min="1" max="65535" value="3306" />
+        <label>IP/主机</label>
+        <input id="dbHost" type="text" placeholder="127.0.0.1 或 192.168.x.x" />
+        <label>库名</label>
+        <input id="dbName" type="text" placeholder="mes_cloud" />
+        <label>账号</label>
+        <input id="dbUser" type="text" autocomplete="off" />
+        <label>密码</label>
+        <input id="dbPass" type="password" autocomplete="new-password" />
       </div>
-      <div class="drives" id="drives">正在读取磁盘...</div>
-      <p class="hint" id="dbHint"></p>
+      <div class="form-actions">
+        <button class="btn ghost" id="btnTest" type="button">测试连接</button>
+        <button class="btn" id="btnSaveCfg" type="button">保存配置</button>
+      </div>
+      <div class="cfg-msg" id="cfgStatus">填写 IP、账号、密码后点「测试连接」</div>
     </div>
 
     <div class="card">
       <div class="row">
-        <h2>2. 保存位置(点选,不用输入)</h2>
-        <div>
-          <button class="btn ghost" id="btnUp" type="button">上一级</button>
-          <button class="btn" id="btnBrowse" type="button">浏览文件夹...</button>
-        </div>
+        <h2>2. 选择保存位置</h2>
+        <button class="btn" id="btnBrowse" type="button">浏览文件夹...</button>
       </div>
-      <div class="picked empty" id="picked">尚未选择。请点上方磁盘,或点「浏览文件夹」弹出系统窗口选择。</div>
-      <div class="folders" id="folders"></div>
+      <p class="hint">打开页面不会扫描磁盘。插入 U 盘后点「浏览文件夹」,在系统窗口里选目录即可。</p>
+      <div class="picked empty" id="picked">尚未选择。请点「浏览文件夹」。</div>
       <p class="hint" id="spaceHint"></p>
       <label class="check">
         <input id="keepOld" type="checkbox"/>
@@ -116,7 +143,35 @@
     var polling = null;
     var dbBytes = 0;
 
-    function $(id) { return document.getElementById(id); }
+    function $(id) {
+      return document.getElementById(id);
+    }
+
+    function dbBody() {
+      return {
+        engine: "mysql",
+        host: ($("dbHost").value || "").trim(),
+        port: parseInt($("dbPort").value || "3306", 10),
+        database: ($("dbName").value || "").trim(),
+        user: ($("dbUser").value || "").trim(),
+        password: $("dbPass").value || ""
+      };
+    }
+    function setCfgStatus(text, cls) {
+      $("cfgStatus").className = "cfg-msg " + (cls || "");
+      $("cfgStatus").textContent = text || "";
+    }
+    function loadConfig() {
+      api("/api/config").then(function (j) {
+        $("dbHost").value = j.host || "";
+        $("dbPort").value = j.port || 3306;
+        $("dbName").value = j.database || "";
+        $("dbUser").value = j.user || "";
+        $("dbPass").value = j.password || "";
+      }).catch(function (e) {
+        setCfgStatus(e.message, "bad");
+      });
+    }
     function asArray(x) {
       if (!x) return [];
       return Object.prototype.toString.call(x) === "[object Array]" ? x : [x];
@@ -137,7 +192,7 @@
       var el = $("picked");
       if (!selectedPath) {
         el.className = "picked empty";
-        el.textContent = "尚未选择。请点上方磁盘,或点「浏览文件夹」弹出系统窗口选择。";
+        el.textContent = "尚未选择。请点「浏览文件夹」。";
       } else {
         el.className = "picked";
         el.textContent = selectedPath;
@@ -145,45 +200,28 @@
     }
 
     function api(url, opt) {
-      return fetch(url, opt).then(function (r) {
+      opt = opt || {};
+      var ctrl = new AbortController();
+      var ms = opt.timeoutMs || 12000;
+      var timer = setTimeout(function () { ctrl.abort(); }, ms);
+      var fo = Object.assign({}, opt, { signal: ctrl.signal });
+      delete fo.timeoutMs;
+      return fetch(url, fo).then(function (r) {
         return r.text().then(function (t) {
           var j = {};
-          try { j = JSON.parse(t); } catch (e) { throw new Error(t || "接口无响应"); }
+          try { j = t ? JSON.parse(t) : {}; } catch (e) { throw new Error(t || "接口无响应"); }
           if (!r.ok || j.ok === false) throw new Error(j.message || "请求失败");
           return j;
         });
-      });
-    }
-
-    function loadDrives() {
-      $("drives").textContent = "正在读取磁盘...";
-      api("/api/drives").then(function (j) {
-        dbBytes = j.dbBytes || 0;
-        if (dbBytes > 0) {
-          $("dbHint").textContent = "当前数据库约 " + gb(dbBytes) + " GB。建议目标盘剩余空间大于该值。备份采用 gzip 压缩,边导边压。";
-        }
-        var html = "";
-        asArray(j.drives).forEach(function (d) {
-          var cls = "drive" + (d.removable ? " usb" : "");
-          var space = d.ready ? ("剩余 " + d.freeGB + " GB / 共 " + d.sizeGB + " GB") : "未就绪";
-          html += '<button type="button" class="' + cls + '" data-path="' + d.path + '" data-free="' + (d.freeBytes || 0) + '">'
-            + '<div class="t"><span class="tag' + (d.removable ? " usb" : "") + '">' + d.type + "</span></div>"
-            + '<div class="n">' + d.letter + "  " + (d.name || "") + "</div>"
-            + '<div class="s">' + space + "</div></button>";
-        });
-        $("drives").innerHTML = html || "没有发现可用磁盘";
-        Array.prototype.forEach.call($("drives").querySelectorAll(".drive"), function (el) {
-          el.onclick = function () {
-            Array.prototype.forEach.call($("drives").querySelectorAll(".drive"), function (x) { x.classList.remove("on"); });
-            el.classList.add("on");
-            var p = el.getAttribute("data-path");
-            setPicked(p + "MES_DB_Backup");
-            showSpace(parseInt(el.getAttribute("data-free") || "0", 10));
-            loadFolders(p);
-          };
-        });
       }).catch(function (e) {
-        $("drives").textContent = "读取磁盘失败:" + e.message;
+        if (e.name === "AbortError") throw new Error("等待超时,请确认已关掉旧的备份窗口后重新打开页面");
+        throw e;
+      }).then(function (v) {
+        clearTimeout(timer);
+        return v;
+      }, function (e) {
+        clearTimeout(timer);
+        throw e;
       });
     }
 
@@ -196,28 +234,6 @@
       $("spaceHint").textContent = msg;
     }
 
-    function loadFolders(path) {
-      if (!path) return;
-      api("/api/folders?path=" + encodeURIComponent(path)).then(function (j) {
-        var d = j.data || {};
-        parentPath = d.parent || "";
-        var html = "";
-        asArray(d.folders).forEach(function (f) {
-          html += '<button class="folder" type="button" data-path="' + f.path.replace(/"/g, "") + '">📁 ' + f.name + "</button>";
-        });
-        $("folders").innerHTML = html || '<span style="color:#94a3b8;font-size:13px;">该目录下没有子文件夹,将备份到上面显示的位置</span>';
-        Array.prototype.forEach.call($("folders").querySelectorAll(".folder"), function (el) {
-          el.onclick = function () {
-            var p = el.getAttribute("data-path");
-            setPicked(p);
-            loadFolders(p);
-          };
-        });
-      }).catch(function (e) {
-        $("folders").innerHTML = '<span style="color:#f87171;font-size:13px;">' + e.message + "</span>";
-      });
-    }
-
     function pollStatus() {
       api("/api/status").then(function (j) {
         setLog(asArray(j.log));
@@ -250,34 +266,29 @@
       }).catch(function () {});
     }
 
-    $("btnRefresh").onclick = loadDrives;
-    $("btnUp").onclick = function () {
-      if (parentPath) {
-        setPicked(parentPath);
-        loadFolders(parentPath);
-      }
-    };
     $("btnBrowse").onclick = function () {
       setStatus("请在弹出的系统窗口中选择文件夹(窗口可能在浏览器后面)", "run");
       api("/api/browse", { method: "POST" }).then(function (j) {
         if (j.cancelled) { setStatus("", ""); return; }
         setPicked(j.path);
         showSpace(j.freeBytes || 0);
-        loadFolders(j.path);
         setStatus("已选择保存位置", "ok");
       }).catch(function (e) {
         setStatus(e.message, "bad");
       });
     };
     $("btnBackup").onclick = function () {
-      if (!selectedPath) { setStatus("请先点选磁盘或浏览文件夹", "bad"); return; }
+      if (!selectedPath) { setStatus("请先点「浏览文件夹」选择保存位置", "bad"); return; }
       $("btnBackup").disabled = true;
       setStatus("正在启动备份...", "run");
       $("bar").className = "bar indeterminate";
       api("/api/backup", {
         method: "POST",
         headers: { "Content-Type": "application/json" },
-        body: JSON.stringify({ path: selectedPath, keepDays: $("keepOld").checked ? 7 : 0 })
+        body: JSON.stringify(Object.assign(dbBody(), {
+          path: selectedPath,
+          keepDays: $("keepOld").checked ? 7 : 0
+        }))
       }).then(function () {
         if (polling) clearInterval(polling);
         polling = setInterval(pollStatus, 1000);
@@ -289,7 +300,34 @@
       });
     };
 
-    loadDrives();
+    $("btnTest").onclick = function () {
+      setCfgStatus("正在测试连接,请稍候…", "run");
+      var b = dbBody();
+      var q = "host=" + encodeURIComponent(b.host)
+        + "&port=" + encodeURIComponent(b.port)
+        + "&database=" + encodeURIComponent(b.database)
+        + "&user=" + encodeURIComponent(b.user)
+        + "&password=" + encodeURIComponent(b.password);
+      api("/api/test?" + q, { timeoutMs: 15000 }).then(function (j) {
+        setCfgStatus(j.message || "连接成功", "ok");
+      }).catch(function (e) {
+        setCfgStatus(e.message || "连接失败", "bad");
+      });
+    };
+    $("btnSaveCfg").onclick = function () {
+      setCfgStatus("正在保存...", "run");
+      api("/api/config", {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify(dbBody())
+      }).then(function (j) {
+        setCfgStatus(j.message || "已保存", "ok");
+      }).catch(function (e) {
+        setCfgStatus(e.message, "bad");
+      });
+    };
+
+    loadConfig();
   </script>
 </body>
 </html>

+ 38 - 0
src/main/java/com/jeesite/modules/config/Utf8DisplayBodyAdvice.java

@@ -0,0 +1,38 @@
+package com.jeesite.modules.config;
+
+import com.jeesite.modules.mes.util.Utf8DisplayUtils;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.io.Resource;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
+
+/**
+ * 列表接口返回前还原中文乱码,只改本次响应内存,不写数据库。
+ */
+@ControllerAdvice
+public class Utf8DisplayBodyAdvice implements ResponseBodyAdvice<Object> {
+
+	@Override
+	public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
+		Class<?> type = returnType.getParameterType();
+		if (type == byte[].class || Resource.class.isAssignableFrom(type)) {
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
+			Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
+			ServerHttpResponse response) {
+		if (body instanceof String) {
+			return Utf8DisplayUtils.repair((String) body);
+		}
+		Utf8DisplayUtils.repairGraph(body);
+		return body;
+	}
+}

+ 104 - 0
src/main/java/com/jeesite/modules/config/Utf8DisplayJacksonConfig.java

@@ -0,0 +1,104 @@
+package com.jeesite.modules.config;
+
+import com.alibaba.fastjson.serializer.JSONSerializer;
+import com.alibaba.fastjson.serializer.ObjectSerializer;
+import com.alibaba.fastjson.serializer.SerializeConfig;
+import com.alibaba.fastjson.serializer.SerializeWriter;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.jeesite.modules.mes.util.Utf8DisplayUtils;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+/**
+ * 列表 JSON 输出时还原中文乱码,不修改数据库、不改实体字段。
+ */
+@Configuration
+public class Utf8DisplayJacksonConfig implements BeanPostProcessor {
+
+	private static final SimpleModule MODULE = buildModule();
+
+	public Utf8DisplayJacksonConfig() {
+		registerFastjson();
+		registerJeesiteJsonMapper();
+	}
+
+	@Bean
+	public Jackson2ObjectMapperBuilderCustomizer utf8DisplayJacksonCustomizer() {
+		return new Jackson2ObjectMapperBuilderCustomizer() {
+			@Override
+			public void customize(Jackson2ObjectMapperBuilder builder) {
+				builder.modulesToInstall(MODULE);
+			}
+		};
+	}
+
+	@Override
+	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+		if (bean instanceof ObjectMapper) {
+			registerJackson((ObjectMapper) bean);
+		}
+		return bean;
+	}
+
+	private static void registerJackson(ObjectMapper mapper) {
+		if (mapper == null) {
+			return;
+		}
+		try {
+			mapper.registerModule(MODULE);
+		} catch (Exception ignored) {
+		}
+	}
+
+	private static SimpleModule buildModule() {
+		SimpleModule module = new SimpleModule("utf8Display");
+		module.addSerializer(String.class, new JsonSerializer<String>() {
+			@Override
+			public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+				if (value == null) {
+					gen.writeNull();
+					return;
+				}
+				gen.writeString(Utf8DisplayUtils.repair(value));
+			}
+		});
+		return module;
+	}
+
+	private static void registerJeesiteJsonMapper() {
+		try {
+			Class<?> clazz = Class.forName("com.jeesite.common.mapper.JsonMapper");
+			Object mapper = clazz.getMethod("getInstance").invoke(null);
+			if (mapper instanceof ObjectMapper) {
+				registerJackson((ObjectMapper) mapper);
+			}
+		} catch (Exception ignored) {
+		}
+	}
+
+	private static void registerFastjson() {
+		SerializeConfig.getGlobalInstance().put(String.class, new ObjectSerializer() {
+			@Override
+			public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features)
+					throws IOException {
+				SerializeWriter out = serializer.out;
+				if (object == null) {
+					out.writeNull();
+					return;
+				}
+				out.writeString(Utf8DisplayUtils.repair(String.valueOf(object)));
+			}
+		});
+	}
+}

+ 204 - 0
src/main/java/com/jeesite/modules/mes/util/Utf8DisplayUtils.java

@@ -0,0 +1,204 @@
+package com.jeesite.modules.mes.util;
+
+import com.jeesite.common.entity.Page;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 仅用于页面/接口展示:把 UTF-8 字节被当成 ISO-8859-1 读出的乱码还原成中文。
+ * 已是汉字则原样返回,不写回数据库。
+ */
+public final class Utf8DisplayUtils {
+
+	private static final Charset WINDOWS_1252 = Charset.forName("windows-1252");
+
+	private Utf8DisplayUtils() {
+	}
+
+	/**
+	 * 还原对象图中的乱码字符串,只改内存,不写库。
+	 */
+	public static void repairGraph(Object root) {
+		if (root == null) {
+			return;
+		}
+		repairGraph(root, Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>()));
+	}
+
+	public static String repair(String value) {
+		if (value == null || value.isEmpty()) {
+			return value;
+		}
+		if (containsCjk(value)) {
+			return value;
+		}
+		if (!looksLikeMojibake(value)) {
+			return value;
+		}
+		String repaired = decodeToUtf8(value, StandardCharsets.ISO_8859_1);
+		if (isReadableChinese(repaired)) {
+			return repaired;
+		}
+		repaired = decodeToUtf8(value, WINDOWS_1252);
+		if (isReadableChinese(repaired)) {
+			return repaired;
+		}
+		return value;
+	}
+
+	private static String decodeToUtf8(String value, Charset sourceCharset) {
+		CharsetEncoder encoder = sourceCharset.newEncoder()
+				.onMalformedInput(CodingErrorAction.REPORT)
+				.onUnmappableCharacter(CodingErrorAction.REPORT);
+		try {
+			ByteBuffer buffer = encoder.encode(CharBuffer.wrap(value));
+			byte[] bytes = new byte[buffer.remaining()];
+			buffer.get(bytes);
+			return new String(bytes, StandardCharsets.UTF_8);
+		} catch (CharacterCodingException ex) {
+			return null;
+		}
+	}
+
+	private static boolean isReadableChinese(String value) {
+		return value != null && !value.isEmpty()
+				&& containsCjk(value)
+				&& value.indexOf('\uFFFD') < 0;
+	}
+
+	private static boolean looksLikeMojibake(String value) {
+		for (int i = 0; i < value.length(); i++) {
+			char c = value.charAt(i);
+			if ((c >= 0x80 && c <= 0xFF)
+					|| c == '\u2013' || c == '\u2014'
+					|| c == '\u2018' || c == '\u2019'
+					|| c == '\u201C' || c == '\u201D'
+					|| c == '\u0152' || c == '\u0153'
+					|| c == '\u0160' || c == '\u0161'
+					|| c == '\u0178' || c == '\u017D' || c == '\u017E'
+					|| c == '\u0192' || c == '\u02C6' || c == '\u2030'
+					|| c == '\u20AC' || c == '\u2020' || c == '\u2021'
+					|| c == '\u2022' || c == '\u2026' || c == '\u2039' || c == '\u203A'
+					|| c == '\u02DC' || c == '\u2122') {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private static void repairGraph(Object obj, Set<Object> seen) {
+		if (obj == null || obj instanceof String || obj instanceof Number || obj instanceof Boolean
+				|| obj instanceof Date || obj instanceof Enum || obj instanceof Class
+				|| obj instanceof byte[] || obj instanceof Charset) {
+			return;
+		}
+		if (!seen.add(obj)) {
+			return;
+		}
+		if (obj instanceof Page) {
+			List<?> list = ((Page<?>) obj).getList();
+			if (list != null) {
+				for (Object item : list) {
+					repairGraph(item, seen);
+				}
+			}
+			return;
+		}
+		if (obj instanceof Collection) {
+			int index = 0;
+			Collection<?> collection = (Collection<?>) obj;
+			for (Object item : collection) {
+				if (item instanceof String && obj instanceof List) {
+					String repaired = repair((String) item);
+					if (repaired != null && !repaired.equals(item)) {
+						@SuppressWarnings("unchecked")
+						List<Object> writable = (List<Object>) obj;
+						writable.set(index, repaired);
+					}
+				} else {
+					repairGraph(item, seen);
+				}
+				index++;
+			}
+			return;
+		}
+		if (obj instanceof Map) {
+			Map<?, ?> map = (Map<?, ?>) obj;
+			for (Map.Entry<?, ?> entry : map.entrySet()) {
+				Object value = entry.getValue();
+				if (value instanceof String) {
+					String repaired = repair((String) value);
+					if (repaired != null && !repaired.equals(value)) {
+						@SuppressWarnings("unchecked")
+						Map<Object, Object> writable = (Map<Object, Object>) map;
+						writable.put(entry.getKey(), repaired);
+					}
+				} else {
+					repairGraph(value, seen);
+				}
+			}
+			return;
+		}
+		String className = obj.getClass().getName();
+		if (!className.startsWith("com.jeesite.modules.mes")
+				&& !className.startsWith("com.jeesite.modules.sys")
+				&& !className.startsWith("com.jeesite.common.entity")) {
+			return;
+		}
+		for (Class<?> type = obj.getClass(); type != null && type != Object.class; type = type.getSuperclass()) {
+			if (type.getName().startsWith("java.")) {
+				break;
+			}
+			Field[] fields = type.getDeclaredFields();
+			for (Field field : fields) {
+				int modifiers = field.getModifiers();
+				if (Modifier.isStatic(modifiers) || Modifier.isFinal(modifiers)) {
+					continue;
+				}
+				if (field.getType().isPrimitive()) {
+					continue;
+				}
+				try {
+					field.setAccessible(true);
+					Object value = field.get(obj);
+					if (value instanceof String) {
+						String repaired = repair((String) value);
+						if (repaired != null && !repaired.equals(value)) {
+							field.set(obj, repaired);
+						}
+					} else {
+						repairGraph(value, seen);
+					}
+				} catch (Exception ignored) {
+				}
+			}
+		}
+	}
+
+	private static boolean containsCjk(String value) {
+		for (int i = 0; i < value.length(); i++) {
+			Character.UnicodeBlock block = Character.UnicodeBlock.of(value.charAt(i));
+			if (block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
+					|| block == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
+					|| block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A) {
+				return true;
+			}
+		}
+		return false;
+	}
+}