mirror of https://github.com/longtai-cn/hippo4j
commit
87faae5040
@ -1,202 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
||||||
* contributor license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright ownership.
|
|
||||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
|
||||||
* (the "License"); you may not use this file except in compliance with
|
|
||||||
* the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cn.hippo4j.tools.logrecord.compare;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 比对器抽象类.
|
|
||||||
*
|
|
||||||
* @author chen.ma
|
|
||||||
* @date 2021/10/24 20:25
|
|
||||||
*/
|
|
||||||
public class AbstractEquator implements Equator {
|
|
||||||
|
|
||||||
private static final List<Class<?>> WRAPPER =
|
|
||||||
Arrays
|
|
||||||
.asList(
|
|
||||||
Byte.class,
|
|
||||||
Short.class,
|
|
||||||
Integer.class,
|
|
||||||
Long.class,
|
|
||||||
Float.class,
|
|
||||||
Double.class,
|
|
||||||
Character.class,
|
|
||||||
Boolean.class,
|
|
||||||
String.class);
|
|
||||||
|
|
||||||
private List<String> includeFields;
|
|
||||||
|
|
||||||
private List<String> excludeFields;
|
|
||||||
|
|
||||||
private boolean bothExistFieldOnly = true;
|
|
||||||
|
|
||||||
public AbstractEquator() {
|
|
||||||
includeFields = Collections.emptyList();
|
|
||||||
excludeFields = Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param bothExistFieldOnly 是否仅比对两个类都包含的字段
|
|
||||||
*/
|
|
||||||
public AbstractEquator(boolean bothExistFieldOnly) {
|
|
||||||
includeFields = Collections.emptyList();
|
|
||||||
excludeFields = Collections.emptyList();
|
|
||||||
this.bothExistFieldOnly = bothExistFieldOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 指定包含或排除某些字段.
|
|
||||||
*
|
|
||||||
* @param includeFields 包含字段, 若为 null 或空集则不指定
|
|
||||||
* @param excludeFields 排除字段, 若为 null 或空集则不指定
|
|
||||||
*/
|
|
||||||
public AbstractEquator(List<String> includeFields, List<String> excludeFields) {
|
|
||||||
this.includeFields = includeFields;
|
|
||||||
this.excludeFields = excludeFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 指定包含或排除某些字段.
|
|
||||||
*
|
|
||||||
* @param includeFields 包含字段, 若为 null 或空集则不指定
|
|
||||||
* @param excludeFields 排除字段, 若为 null 或空集则不指定
|
|
||||||
* @param bothExistFieldOnly 是否只对比两个类都包含的字段, 默认为 true
|
|
||||||
*/
|
|
||||||
public AbstractEquator(List<String> includeFields, List<String> excludeFields, boolean bothExistFieldOnly) {
|
|
||||||
this.includeFields = includeFields;
|
|
||||||
this.excludeFields = excludeFields;
|
|
||||||
this.bothExistFieldOnly = bothExistFieldOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEquals(Object first, Object second) {
|
|
||||||
List<FieldInfo> diff = getDiffFields(first, second);
|
|
||||||
return diff == null || diff.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<FieldInfo> getDiffFields(Object first, Object second) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 对比两个对象的指定属性是否相等, 默认为两个对象是否 equals.
|
|
||||||
* <p>
|
|
||||||
* 子类可以通过覆盖此方法对某些特殊属性进行比对.
|
|
||||||
*
|
|
||||||
* @param fieldInfo
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
protected boolean isFieldEquals(FieldInfo fieldInfo) {
|
|
||||||
// 先判断排除, 如果需要排除, 则无论在不在包含范围, 都一律不比对
|
|
||||||
if (isExclude(fieldInfo)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// 如果有指定需要包含的字段而且当前字段不在需要包含的字段中则不比对
|
|
||||||
if (!isInclude(fieldInfo)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return nullableEquals(fieldInfo.getFirstVal(), fieldInfo.getSecondVal());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确定是否需要需要排除这个字段, 子类可以扩展这个方法, 自定义判断方式.
|
|
||||||
*
|
|
||||||
* @param fieldInfo
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
protected boolean isExclude(FieldInfo fieldInfo) {
|
|
||||||
// 如果有指定需要排除的字段,而且当前字段是需要排除字段,则直接返回 true
|
|
||||||
return excludeFields != null && !excludeFields.isEmpty() && excludeFields.contains(fieldInfo.getFieldName());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确定是否需要比较这个字段, 子类可以扩展这个方法, 自定义判断方式.
|
|
||||||
*
|
|
||||||
* @param fieldInfo
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
protected boolean isInclude(FieldInfo fieldInfo) {
|
|
||||||
// 没有指定需要包含的字段,则全部都包含
|
|
||||||
if (includeFields == null || includeFields.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return includeFields.contains(fieldInfo.getFieldName());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 如果简单数据类型的对象则直接进行比对.
|
|
||||||
*
|
|
||||||
* @param first
|
|
||||||
* @param second
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
protected List<FieldInfo> compareSimpleField(Object first, Object second) {
|
|
||||||
boolean eq = Objects.equals(first, second);
|
|
||||||
if (eq) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
} else {
|
|
||||||
Object obj = first == null ? second : first;
|
|
||||||
Class<?> clazz = obj.getClass();
|
|
||||||
// 不等的字段名称使用类的名称
|
|
||||||
return Collections.singletonList(new FieldInfo(clazz.getSimpleName(), clazz, first, second));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断是否为原始数据类型.
|
|
||||||
*
|
|
||||||
* @param first
|
|
||||||
* @param second
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
protected boolean isSimpleField(Object first, Object second) {
|
|
||||||
Object obj = first == null ? second : first;
|
|
||||||
Class<?> clazz = obj.getClass();
|
|
||||||
return clazz.isPrimitive() || WRAPPER.contains(clazz);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean nullableEquals(Object first, Object second) {
|
|
||||||
if (first instanceof Collection && second instanceof Collection) {
|
|
||||||
// 如果两个都是集合类型,尝试转换为数组再进行深度比较
|
|
||||||
return Objects.deepEquals(((Collection) first).toArray(), ((Collection) second).toArray());
|
|
||||||
}
|
|
||||||
return Objects.deepEquals(first, second);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Set<String> getAllFieldNames(Set<String> firstFields, Set<String> secondFields) {
|
|
||||||
Set<String> allFields;
|
|
||||||
// 只取交集
|
|
||||||
if (isBothExistFieldOnly()) {
|
|
||||||
allFields = firstFields.stream().filter(secondFields::contains).collect(Collectors.toSet());
|
|
||||||
} else {
|
|
||||||
// 否则取并集
|
|
||||||
allFields = new HashSet<>(firstFields);
|
|
||||||
allFields.addAll(secondFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
return allFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isBothExistFieldOnly() {
|
|
||||||
return bothExistFieldOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
||||||
* contributor license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright ownership.
|
|
||||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
|
||||||
* (the "License"); you may not use this file except in compliance with
|
|
||||||
* the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cn.hippo4j.tools.logrecord.compare;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 对象比对器.
|
|
||||||
*
|
|
||||||
* @author chen.ma
|
|
||||||
* @date 2021/10/24 20:27
|
|
||||||
*/
|
|
||||||
public interface Equator {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断两个对象是否相等.
|
|
||||||
*
|
|
||||||
* @param first
|
|
||||||
* @param second
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
boolean isEquals(Object first, Object second);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取两个对象不想等的属性.
|
|
||||||
*
|
|
||||||
* @param first
|
|
||||||
* @param second
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
List<FieldInfo> getDiffFields(Object first, Object second);
|
|
||||||
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
||||||
* contributor license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright ownership.
|
|
||||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
|
||||||
* (the "License"); you may not use this file except in compliance with
|
|
||||||
* the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cn.hippo4j.tools.logrecord.compare;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 对象比较中不同的字段.
|
|
||||||
*
|
|
||||||
* @author chen.ma
|
|
||||||
* @date 2021/10/24 20:03
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class FieldInfo {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 字段名称
|
|
||||||
*/
|
|
||||||
private String fieldName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 第一个字段的类型
|
|
||||||
*/
|
|
||||||
private Class<?> firstFieldType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 第二个字段的类型
|
|
||||||
*/
|
|
||||||
private Class<?> secondFieldType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 第一个对象的值
|
|
||||||
*/
|
|
||||||
private Object firstVal;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 第二个对象的值
|
|
||||||
*/
|
|
||||||
private Object secondVal;
|
|
||||||
|
|
||||||
public FieldInfo(String fieldName, Class<?> firstFieldType, Class<?> secondFieldType) {
|
|
||||||
this.fieldName = fieldName;
|
|
||||||
this.firstFieldType = firstFieldType;
|
|
||||||
this.secondFieldType = secondFieldType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FieldInfo(String fieldName, Class<?> fieldType, Object firstVal, Object secondVal) {
|
|
||||||
this.fieldName = fieldName;
|
|
||||||
this.firstFieldType = fieldType;
|
|
||||||
this.secondFieldType = fieldType;
|
|
||||||
this.firstVal = firstVal;
|
|
||||||
this.secondVal = secondVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) {
|
|
||||||
return true;
|
|
||||||
} else if (o == null || getClass() != o.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
FieldInfo fieldInfo = (FieldInfo) o;
|
|
||||||
return Objects.equals(fieldName, fieldInfo.fieldName) &&
|
|
||||||
Objects.equals(firstFieldType, fieldInfo.firstFieldType) &&
|
|
||||||
Objects.equals(secondFieldType, fieldInfo.secondFieldType) &&
|
|
||||||
Objects.equals(firstVal, fieldInfo.firstVal) &&
|
|
||||||
Objects.equals(secondVal, fieldInfo.secondVal);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(fieldName, firstFieldType, secondFieldType, firstVal, secondVal);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,161 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
||||||
* contributor license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright ownership.
|
|
||||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
|
||||||
* (the "License"); you may not use this file except in compliance with
|
|
||||||
* the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cn.hippo4j.tools.logrecord.compare;
|
|
||||||
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.lang.reflect.Modifier;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基于 getter 方法比对两个对象
|
|
||||||
* <p>
|
|
||||||
* 所有无参的 get 和 is 方法都认为是对象的属性
|
|
||||||
*
|
|
||||||
* @author chen.ma
|
|
||||||
* @date 2021/10/24 20:36
|
|
||||||
*/
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class GetterBaseEquator extends AbstractEquator {
|
|
||||||
|
|
||||||
private static final String GET = "get";
|
|
||||||
|
|
||||||
private static final String IS = "is";
|
|
||||||
|
|
||||||
private static final String GET_IS = "get|is";
|
|
||||||
|
|
||||||
private static final String GET_CLASS = "getClass";
|
|
||||||
|
|
||||||
private static final Map<Class<?>, Map<String, Method>> CACHE = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
public GetterBaseEquator(boolean bothExistFieldOnly) {
|
|
||||||
super(bothExistFieldOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
public GetterBaseEquator(List<String> includeFields, List<String> excludeFields) {
|
|
||||||
super(includeFields, excludeFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
public GetterBaseEquator(List<String> includeFields, List<String> excludeFields, boolean bothExistFieldOnly) {
|
|
||||||
super(includeFields, excludeFields, bothExistFieldOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<FieldInfo> getDiffFields(Object first, Object second) {
|
|
||||||
if (first == null && second == null) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
// 先尝试判断是否为普通数据类型
|
|
||||||
if (isSimpleField(first, second)) {
|
|
||||||
return compareSimpleField(first, second);
|
|
||||||
}
|
|
||||||
Set<String> allFieldNames;
|
|
||||||
// 获取所有字段
|
|
||||||
Map<String, Method> firstGetters = getAllGetters(first);
|
|
||||||
Map<String, Method> secondGetters = getAllGetters(second);
|
|
||||||
if (first == null) {
|
|
||||||
allFieldNames = secondGetters.keySet();
|
|
||||||
} else if (second == null) {
|
|
||||||
allFieldNames = firstGetters.keySet();
|
|
||||||
} else {
|
|
||||||
allFieldNames = getAllFieldNames(firstGetters.keySet(), secondGetters.keySet());
|
|
||||||
}
|
|
||||||
List<FieldInfo> diffFields = new LinkedList<>();
|
|
||||||
for (String fieldName : allFieldNames) {
|
|
||||||
try {
|
|
||||||
Method firstGetterMethod = firstGetters.getOrDefault(fieldName, null);
|
|
||||||
Method secondGetterMethod = secondGetters.getOrDefault(fieldName, null);
|
|
||||||
Object firstVal = firstGetterMethod != null ? firstGetterMethod.invoke(first) : null;
|
|
||||||
Object secondVal = secondGetterMethod != null ? secondGetterMethod.invoke(second) : null;
|
|
||||||
FieldInfo fieldInfo = new FieldInfo(fieldName, getReturnType(firstGetterMethod), getReturnType(secondGetterMethod));
|
|
||||||
fieldInfo.setFirstVal(firstVal);
|
|
||||||
fieldInfo.setSecondVal(secondVal);
|
|
||||||
if (!isFieldEquals(fieldInfo)) {
|
|
||||||
diffFields.add(fieldInfo);
|
|
||||||
}
|
|
||||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
|
||||||
throw new IllegalStateException("获取属性进行比对发生异常: " + fieldName, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return diffFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Class<?> getReturnType(Method method) {
|
|
||||||
return method == null ? null : method.getReturnType();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Method> getAllGetters(Object obj) {
|
|
||||||
if (obj == null) {
|
|
||||||
return Collections.emptyMap();
|
|
||||||
}
|
|
||||||
return CACHE.computeIfAbsent(obj.getClass(), k -> {
|
|
||||||
Class<?> clazz = obj.getClass();
|
|
||||||
Map<String, Method> getters = new LinkedHashMap<>(8);
|
|
||||||
while (clazz != Object.class) {
|
|
||||||
Method[] methods = clazz.getDeclaredMethods();
|
|
||||||
for (Method m : methods) {
|
|
||||||
// getter 方法必须是 public 且没有参数的
|
|
||||||
if (!Modifier.isPublic(m.getModifiers()) || m.getParameterTypes().length > 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (m.getReturnType() == Boolean.class || m.getReturnType() == boolean.class) {
|
|
||||||
// 如果返回值是 boolean 则兼容 isXxx 的写法
|
|
||||||
if (m.getName().startsWith(IS)) {
|
|
||||||
String fieldName = uncapitalize(m.getName().substring(2));
|
|
||||||
getters.put(fieldName, m);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 以 get 开头但排除 getClass() 方法
|
|
||||||
if (m.getName().startsWith(GET) && !GET_CLASS.equals(m.getName())) {
|
|
||||||
String fieldName = uncapitalize(m.getName().replaceFirst(GET_IS, ""));
|
|
||||||
getters.put(fieldName, m);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 得到父类, 然后赋给自己
|
|
||||||
clazz = clazz.getSuperclass();
|
|
||||||
}
|
|
||||||
return getters;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private String uncapitalize(final String str) {
|
|
||||||
int strLen;
|
|
||||||
if (str == null || (strLen = str.length()) == 0) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
final int firstCodepoint = str.codePointAt(0);
|
|
||||||
final int newCodePoint = Character.toLowerCase(firstCodepoint);
|
|
||||||
if (firstCodepoint == newCodePoint) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
final int[] newCodePoints = new int[strLen];
|
|
||||||
int outOffset = 0;
|
|
||||||
newCodePoints[outOffset++] = newCodePoint;
|
|
||||||
for (int inOffset = Character.charCount(firstCodepoint); inOffset < strLen;) {
|
|
||||||
final int codepoint = str.codePointAt(inOffset);
|
|
||||||
newCodePoints[outOffset++] = codepoint;
|
|
||||||
inOffset += Character.charCount(codepoint);
|
|
||||||
}
|
|
||||||
return new String(newCodePoints, 0, outOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
Reference in new issue