优化EscapeUtil,兼容不规范的转义(pr#4150@Github)

This commit is contained in:
Looly 2025-11-25 17:07:12 +08:00
parent 13a04feab7
commit 7bd0585a39
16 changed files with 357 additions and 79 deletions

View File

@ -1790,12 +1790,13 @@ public class CollUtil {
} }
/** /**
* 获取集合中指定多个下标的元素值下标可以为负数例如-1表示最后一个元素 * 获取集合中指定多个下标的元素值下标可以为负数例如-1表示最后一个元素<br>
* 如果下标越界转换负数索引后仍然越界则抛出异常
* *
* @param <T> 元素类型 * @param <T> 元素类型
* @param collection 集合 * @param collection 集合
* @param indexes 下标支持负数 * @param indexes 下标列表支持负数负数表示从末尾倒数-1为最后一个元素-2为倒数第二个以此类推
* @return 元素值列表 * @return 元素值列表返回 {@link ArrayList}只包含有效下标对应的元素
* @since 4.0.6 * @since 4.0.6
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View File

@ -243,7 +243,7 @@ public class IterUtil {
* @param <V> 对象类型 * @param <V> 对象类型
* @param iterable 对象列表 * @param iterable 对象列表
* @param fieldName 字段名会通过反射获取其值 * @param fieldName 字段名会通过反射获取其值
* @return 某个字段值与对象对应Map * @return 字段值列表
* @since 4.6.2 * @since 4.6.2
*/ */
public static <V, R> List<R> fieldValueList(final Iterable<V> iterable, final String fieldName) { public static <V, R> List<R> fieldValueList(final Iterable<V> iterable, final String fieldName) {
@ -257,7 +257,7 @@ public class IterUtil {
* @param <V> 对象类型 * @param <V> 对象类型
* @param iter 对象列表 * @param iter 对象列表
* @param fieldName 字段名会通过反射获取其值 * @param fieldName 字段名会通过反射获取其值
* @return 某个字段值与对象对应Map * @return 字段值列表
* @since 4.0.10 * @since 4.0.10
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View File

@ -512,10 +512,11 @@ public class CalendarUtil {
} }
/** /**
* 获取指定日期字段的最小值例如分钟的最小值是0 * 获取指定日期字段的最小值例如分钟的最小值是0<br>
* 对于 {@link DateField#DAY_OF_WEEK} 字段会返回一周的第一天详见 {@link #getBeginValue(Calendar, int)}
* *
* @param calendar {@link Calendar} * @param calendar {@link Calendar}
* @param dateField {@link DateField} * @param dateField {@link DateField} 日期字段
* @return 字段最小值 * @return 字段最小值
* @see Calendar#getActualMinimum(int) * @see Calendar#getActualMinimum(int)
* @since 5.4.2 * @since 5.4.2
@ -525,7 +526,10 @@ public class CalendarUtil {
} }
/** /**
* 获取指定日期字段的最小值例如分钟的最小值是0 * 获取指定日期字段的最小值例如分钟的最小值是0<br>
* 对于 {@link Calendar#DAY_OF_WEEK} 字段需要特殊处理<br>
* 由于 DAY_OF_WEEK 的值范围是 1-7取决于一周的第一天设置周日或周一<br>
* 本方法会直接返回 {@link Calendar#getFirstDayOfWeek()} 的值作为一周的第一天
* *
* @param calendar {@link Calendar} * @param calendar {@link Calendar}
* @param dateField {@link DateField} * @param dateField {@link DateField}
@ -535,18 +539,21 @@ public class CalendarUtil {
*/ */
public static int getBeginValue(final Calendar calendar, final int dateField) { public static int getBeginValue(final Calendar calendar, final int dateField) {
if (Calendar.DAY_OF_WEEK == dateField) { if (Calendar.DAY_OF_WEEK == dateField) {
// DAY_OF_WEEK 的值范围是 1-7直接返回一周的第一天
return calendar.getFirstDayOfWeek(); return calendar.getFirstDayOfWeek();
} }
return calendar.getActualMinimum(dateField); return calendar.getActualMinimum(dateField);
} }
/** /**
* 获取指定日期字段的最大值例如分钟的最大值是59 * 获取指定日期字段的最大值例如分钟的最大值是59<br>
* 对于 {@link DateField#DAY_OF_WEEK} 字段会根据一周的第一天设置计算最后一天详见 {@link #getEndValue(Calendar, int)}
* *
* @param calendar {@link Calendar} * @param calendar {@link Calendar}
* @param dateField {@link DateField} * @param dateField {@link DateField} 日期字段
* @return 字段最大值 * @return 字段最大值
* @see Calendar#getActualMaximum(int) * @see Calendar#getActualMaximum(int)
* @see #getEndValue(Calendar, int)
* @since 5.4.2 * @since 5.4.2
*/ */
public static int getEndValue(final Calendar calendar, final DateField dateField) { public static int getEndValue(final Calendar calendar, final DateField dateField) {
@ -554,16 +561,22 @@ public class CalendarUtil {
} }
/** /**
* 获取指定日期字段的最大值例如分钟的最大值是59 * 获取指定日期字段的最大值例如分钟的最大值是59<br>
* 对于 {@link Calendar#DAY_OF_WEEK} 字段需要特殊处理<br>
* 由于 DAY_OF_WEEK 的值范围是 1-7取决于一周的第一天设置周日或周一<br>
* 本方法会根据 {@link Calendar#getFirstDayOfWeek()} 计算出一周的最后一天<br>
* 计算逻辑最后一天 = 第一天 + 6如果结果 &gt; 7则减去 7 以保持在 1-7 范围内<br>
* 例如如果第一天是周一(2)则最后一天是周日(1)如果第一天是周日(1)则最后一天是周六(7)
* *
* @param calendar {@link Calendar} * @param calendar {@link Calendar}
* @param dateField {@link DateField} * @param dateField {@link DateField} 日期字段使用 {@link Calendar} 中的字段常量 {@link Calendar#MINUTE}{@link Calendar#DAY_OF_WEEK}
* @return 字段最大值 * @return 字段最大值
* @see Calendar#getActualMaximum(int) * @see Calendar#getActualMaximum(int)
* @since 4.5.7 * @since 4.5.7
*/ */
public static int getEndValue(final Calendar calendar, final int dateField) { public static int getEndValue(final Calendar calendar, final int dateField) {
if (Calendar.DAY_OF_WEEK == dateField) { if (Calendar.DAY_OF_WEEK == dateField) {
// DAY_OF_WEEK 的值范围是 1-7根据一周的第一天计算最后一天
return (calendar.getFirstDayOfWeek() + 6) % 7; return (calendar.getFirstDayOfWeek() + 6) % 7;
} }
return calendar.getActualMaximum(dateField); return calendar.getActualMaximum(dateField);
@ -584,10 +597,10 @@ public class CalendarUtil {
} }
/** /**
* Calendar{@link Instant}对象 * {@link Calendar} 转换为 {@link Instant} 对象
* *
* @param calendar Date对象 * @param calendar {@link Calendar} 对象
* @return {@link Instant}对象 * @return {@link Instant} 对象如果 calendar {@code null} 则返回 {@code null}
* @since 5.0.5 * @since 5.0.5
*/ */
public static Instant toInstant(final Calendar calendar) { public static Instant toInstant(final Calendar calendar) {

View File

@ -165,7 +165,7 @@ public class StopWatch {
/** /**
* 开始默认的新任务 * 开始默认的新任务
* *
* @throws IllegalStateException 前一个任务没有结束 * @throws IllegalStateException 当前已有任务正在运行时抛出此异常必须先调用{@link #stop()}停止当前任务
*/ */
public void start() throws IllegalStateException { public void start() throws IllegalStateException {
start(StrUtil.EMPTY); start(StrUtil.EMPTY);
@ -175,7 +175,7 @@ public class StopWatch {
* 开始指定名称的新任务 * 开始指定名称的新任务
* *
* @param taskName 新开始的任务名称 * @param taskName 新开始的任务名称
* @throws IllegalStateException 前一个任务没有结束 * @throws IllegalStateException 当前已有任务正在运行时抛出此异常必须先调用{@link #stop()}停止当前任务
*/ */
public void start(final String taskName) throws IllegalStateException { public void start(final String taskName) throws IllegalStateException {
if (null != this.currentTaskName) { if (null != this.currentTaskName) {
@ -188,7 +188,7 @@ public class StopWatch {
/** /**
* 停止当前任务 * 停止当前任务
* *
* @throws IllegalStateException 任务没有开始 * @throws IllegalStateException 当前没有正在运行的任务时抛出此异常必须先调用{@link #start()}{@link #start(String)}开始任务
*/ */
public void stop() throws IllegalStateException { public void stop() throws IllegalStateException {
if (null == this.currentTaskName) { if (null == this.currentTaskName) {

View File

@ -229,7 +229,7 @@ public class IoUtil extends NioUtil {
/** /**
* 获得一个Writer默认编码UTF-8 * 获得一个Writer默认编码UTF-8
* *
* @param out * @param out
* @return OutputStreamWriter对象 * @return OutputStreamWriter对象
* @since 5.1.6 * @since 5.1.6
*/ */
@ -240,7 +240,7 @@ public class IoUtil extends NioUtil {
/** /**
* 获得一个Writer * 获得一个Writer
* *
* @param out * @param out
* @param charset 字符集 * @param charset 字符集
* @return OutputStreamWriter对象 * @return OutputStreamWriter对象
*/ */

View File

@ -49,6 +49,15 @@ public class BiMap<K, V> extends MapWrapper<K, V> {
super(raw); super(raw);
} }
/**
* 将指定的键值对放入Map中,同时同步更新反向Map
* <p>
* 如果key已存在但value不同,会在反向Map中移除旧的value到key的映射,并建立新的value到key的映射
*
* @param key
* @param value
* @return 与key关联的旧值,如果key不存在则返回null
*/
@Override @Override
public V put(final K key, final V value) { public V put(final K key, final V value) {
final V oldValue = super.put(key, value); final V oldValue = super.put(key, value);
@ -63,6 +72,11 @@ public class BiMap<K, V> extends MapWrapper<K, V> {
return oldValue; return oldValue;
} }
/**
* 将指定Map中的所有映射关系复制到此Map中,同时同步更新反向Map
*
* @param m 要存储在此Map中的映射关系
*/
@Override @Override
public void putAll(final Map<? extends K, ? extends V> m) { public void putAll(final Map<? extends K, ? extends V> m) {
super.putAll(m); super.putAll(m);
@ -71,6 +85,12 @@ public class BiMap<K, V> extends MapWrapper<K, V> {
} }
} }
/**
* 从Map中移除指定键的映射关系,同时同步从反向Map中移除对应的反向映射
*
* @param key 要从Map中移除其映射关系的键
* @return 与key关联的旧值,如果key不存在则返回null
*/
@Override @Override
public V remove(final Object key) { public V remove(final Object key) {
final V v = super.remove(key); final V v = super.remove(key);
@ -80,11 +100,21 @@ public class BiMap<K, V> extends MapWrapper<K, V> {
return v; return v;
} }
/**
* 仅当指定键当前映射到指定值时才移除该键的映射关系,同时同步从反向Map中移除对应的反向映射
*
* @param key 与指定值关联的键
* @param value 与指定键关联的值
* @return 如果删除成功则返回true
*/
@Override @Override
public boolean remove(final Object key, final Object value) { public boolean remove(final Object key, final Object value) {
return super.remove(key, value) && null != this.inverse && this.inverse.remove(value, key); return super.remove(key, value) && null != this.inverse && this.inverse.remove(value, key);
} }
/**
* 从Map中移除所有映射关系,同时清空反向Map
*/
@Override @Override
public void clear() { public void clear() {
super.clear(); super.clear();
@ -113,14 +143,34 @@ public class BiMap<K, V> extends MapWrapper<K, V> {
return getInverse().get(value); return getInverse().get(value);
} }
/**
* 如果指定的键尚未与值关联(或映射到null),则将其与给定值关联,同时同步更新反向Map
* <p>
* 只有当键成功添加到主Map时(即键之前不存在),才会同步更新反向Map,确保双向映射的一致性
*
* @param key 与指定值关联的键
* @param value 与指定键关联的值
* @return 与指定键关联的当前值,如果没有映射则返回null
*/
@Override @Override
public V putIfAbsent(final K key, final V value) { public V putIfAbsent(final K key, final V value) {
if (null != this.inverse) { final V oldValue = super.putIfAbsent(key, value);
this.inverse.putIfAbsent(value, key); // 只有当oldValue为null时(即key之前不存在),才更新反向Map
if (null == oldValue && null != this.inverse) {
this.inverse.put(value, key);
} }
return super.putIfAbsent(key, value); return oldValue;
} }
/**
* 如果指定的键尚未与值关联(或映射到null),则使用给定的映射函数计算其值并放入此Map中
* <p>
* 由于此操作可能会修改Map的映射关系,因此在操作完成后会重置反向Map,下次访问时会重新构建
*
* @param key 与计算值关联的键
* @param mappingFunction 用于计算值的映射函数
* @return 与指定键关联的当前(现有的或计算的),如果计算的值为null则返回null
*/
@Override @Override
public V computeIfAbsent(final K key, final Function<? super K, ? extends V> mappingFunction) { public V computeIfAbsent(final K key, final Function<? super K, ? extends V> mappingFunction) {
final V result = super.computeIfAbsent(key, mappingFunction); final V result = super.computeIfAbsent(key, mappingFunction);
@ -128,6 +178,16 @@ public class BiMap<K, V> extends MapWrapper<K, V> {
return result; return result;
} }
/**
* 如果指定键的值存在且非null,则使用给定的重新映射函数计算新值
* <p>
* 如果重新映射函数返回null,则移除该映射关系由于此操作会修改Map的映射关系,
* 因此在操作完成后会重置反向Map,下次访问时会重新构建
*
* @param key 与指定值关联的键
* @param remappingFunction 用于计算值的重新映射函数
* @return 与指定键关联的新值,如果不存在则返回null
*/
@Override @Override
public V computeIfPresent(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) { public V computeIfPresent(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
final V result = super.computeIfPresent(key, remappingFunction); final V result = super.computeIfPresent(key, remappingFunction);
@ -135,6 +195,16 @@ public class BiMap<K, V> extends MapWrapper<K, V> {
return result; return result;
} }
/**
* 尝试计算指定键及其当前映射值的映射关系(如果当前映射不存在,则为null)
* <p>
* 此方法用于为指定的键计算新的值如果重新映射函数返回null,则移除该映射关系(如果存在)
* 由于此操作会修改Map的映射关系,因此在操作完成后会重置反向Map,下次访问时会重新构建
*
* @param key 与指定值关联的键
* @param remappingFunction 用于计算值的重新映射函数
* @return 与指定键关联的新值,如果不存在则返回null
*/
@Override @Override
public V compute(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) { public V compute(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
final V result = super.compute(key, remappingFunction); final V result = super.compute(key, remappingFunction);
@ -142,6 +212,17 @@ public class BiMap<K, V> extends MapWrapper<K, V> {
return result; return result;
} }
/**
* 如果指定的键尚未与值关联或关联的值为null,则将其与给定的非null值关联
* <p>
* 否则,使用给定的重新映射函数的结果替换关联的值,如果结果为null则移除该映射关系
* 由于此操作会修改Map的映射关系,因此在操作完成后会重置反向Map,下次访问时会重新构建
*
* @param key 与指定值关联的键
* @param value 要与键关联的非null值
* @param remappingFunction 用于重新计算值的重新映射函数
* @return 与指定键关联的新值,如果不存在则返回null
*/
@Override @Override
public V merge(final K key, final V value, final BiFunction<? super V, ? super V, ? extends V> remappingFunction) { public V merge(final K key, final V value, final BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
final V result = super.merge(key, value, remappingFunction); final V result = super.merge(key, value, remappingFunction);

View File

@ -139,6 +139,24 @@ public class MapProxy implements Map<Object, Object>, TypeGetter<Object>, Invoca
return map.entrySet(); return map.entrySet();
} }
/**
* 实现InvocationHandler接口的调用处理方法
* <p>
* 此方法用于动态代理,支持以下功能:
* <ul>
* <li>将getXXX方法调用转换为从Map中获取对应key的值</li>
* <li>将isXXX方法调用(针对boolean类型)转换为从Map中获取对应key的值</li>
* <li>将setXXX方法调用转换为向Map中设置对应key的值</li>
* <li>支持驼峰命名和下划线命名的自动转换</li>
* <li>自动进行类型转换,将Map中的值转换为方法返回类型</li>
* </ul>
*
* @param proxy 代理实例
* @param method 被调用的方法
* @param args 方法参数
* @return 方法调用的结果
* @throws UnsupportedOperationException 如果方法不支持代理
*/
@Override @Override
public Object invoke(final Object proxy, final Method method, final Object[] args) { public Object invoke(final Object proxy, final Method method, final Object[] args) {
final Class<?>[] parameterTypes = method.getParameterTypes(); final Class<?>[] parameterTypes = method.getParameterTypes();

View File

@ -56,7 +56,7 @@ public interface Table<R, C, V> extends Iterable<Table.Cell<R, C, V>> {
/** /**
* 行是否存在 * 行是否存在
* *
* @param rowKey 行键 * @param rowKey 行键set
* @return 行是否存在 * @return 行是否存在
*/ */
default boolean containsRow(final R rowKey) { default boolean containsRow(final R rowKey) {
@ -124,7 +124,7 @@ public interface Table<R, C, V> extends Iterable<Table.Cell<R, C, V>> {
/** /**
* 返回所有列的key列的key如果实现Map是可重复key则返回对应不去重的List * 返回所有列的key列的key如果实现Map是可重复key则返回对应不去重的List
* *
* @return set * @return List
* @since 5.8.0 * @since 5.8.0
*/ */
default List<C> columnKeys() { default List<C> columnKeys() {

View File

@ -77,7 +77,7 @@ public class ConstructorUtil {
* @param <T> 构造的对象类型 * @param <T> 构造的对象类型
* @param beanClass {@code null} * @param beanClass {@code null}
* @return 字段列表 * @return 字段列表
* @throws SecurityException 安全检查异常 * @throws SecurityException 当安全管理器存在且拒绝访问时抛出
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static <T> Constructor<T>[] getConstructors(final Class<T> beanClass) throws SecurityException { public static <T> Constructor<T>[] getConstructors(final Class<T> beanClass) throws SecurityException {
@ -90,7 +90,7 @@ public class ConstructorUtil {
* *
* @param beanClass * @param beanClass
* @return 字段列表 * @return 字段列表
* @throws SecurityException 安全检查异常 * @throws SecurityException 当安全管理器存在且拒绝访问时抛出
*/ */
public static Constructor<?>[] getConstructorsDirectly(final Class<?> beanClass) throws SecurityException { public static Constructor<?>[] getConstructorsDirectly(final Class<?> beanClass) throws SecurityException {
return beanClass.getDeclaredConstructors(); return beanClass.getDeclaredConstructors();

View File

@ -76,7 +76,7 @@ public class FieldReflect {
* *
* @param predicate 过滤器 * @param predicate 过滤器
* @return 字段数组 * @return 字段数组
* @throws SecurityException 安全检查异常 * @throws SecurityException 当安全管理器存在且拒绝访问时抛出
*/ */
public Field[] getDeclaredFields(final Predicate<Field> predicate) { public Field[] getDeclaredFields(final Predicate<Field> predicate) {
if (null == declaredFields) { if (null == declaredFields) {
@ -94,7 +94,7 @@ public class FieldReflect {
* *
* @param predicate 过滤器 * @param predicate 过滤器
* @return 字段数组 * @return 字段数组
* @throws SecurityException 安全检查异常 * @throws SecurityException 当安全管理器存在且拒绝访问时抛出
*/ */
public Field[] getAllFields(final Predicate<Field> predicate) { public Field[] getAllFields(final Predicate<Field> predicate) {
if (null == allFields) { if (null == allFields) {
@ -113,7 +113,7 @@ public class FieldReflect {
* *
* @param withSuperClassFields 是否包括父类的字段列表 * @param withSuperClassFields 是否包括父类的字段列表
* @return 字段列表 * @return 字段列表
* @throws SecurityException 安全检查异常 * @throws SecurityException 当安全管理器存在且拒绝访问时抛出
*/ */
public Field[] getFieldsDirectly(final boolean withSuperClassFields) throws SecurityException { public Field[] getFieldsDirectly(final boolean withSuperClassFields) throws SecurityException {
Field[] allFields = null; Field[] allFields = null;

View File

@ -158,7 +158,7 @@ public class FieldUtil {
* *
* @param beanClass * @param beanClass
* @return 字段列表 * @return 字段列表
* @throws SecurityException 安全检查异常 * @throws SecurityException 当安全管理器存在且拒绝访问时抛出
*/ */
public static Field[] getFields(final Class<?> beanClass) throws SecurityException { public static Field[] getFields(final Class<?> beanClass) throws SecurityException {
return getFields(beanClass, null); return getFields(beanClass, null);
@ -172,7 +172,7 @@ public class FieldUtil {
* @param beanClass * @param beanClass
* @param fieldPredicate field过滤器过滤掉不需要的field{@link Predicate#test(Object)}{@code true}保留null表示全部保留 * @param fieldPredicate field过滤器过滤掉不需要的field{@link Predicate#test(Object)}{@code true}保留null表示全部保留
* @return 字段列表 * @return 字段列表
* @throws SecurityException 安全检查异常 * @throws SecurityException 当安全管理器存在且拒绝访问时抛出
* @since 5.7.14 * @since 5.7.14
*/ */
public static Field[] getFields(final Class<?> beanClass, final Predicate<Field> fieldPredicate) throws SecurityException { public static Field[] getFields(final Class<?> beanClass, final Predicate<Field> fieldPredicate) throws SecurityException {
@ -186,7 +186,7 @@ public class FieldUtil {
* @param beanClass * @param beanClass
* @param fieldPredicate field过滤器过滤掉不需要的field{@link Predicate#test(Object)}{@code true}保留null表示全部保留 * @param fieldPredicate field过滤器过滤掉不需要的field{@link Predicate#test(Object)}{@code true}保留null表示全部保留
* @return 字段列表 * @return 字段列表
* @throws SecurityException 安全检查异常 * @throws SecurityException 当安全管理器存在且拒绝访问时抛出
* @since 6.0.0 * @since 6.0.0
*/ */
public static Field[] getDeclaredFields(final Class<?> beanClass, final Predicate<Field> fieldPredicate) throws SecurityException { public static Field[] getDeclaredFields(final Class<?> beanClass, final Predicate<Field> fieldPredicate) throws SecurityException {
@ -201,7 +201,7 @@ public class FieldUtil {
* @param beanClass * @param beanClass
* @param withSuperClassFields 是否包括父类的字段列表 * @param withSuperClassFields 是否包括父类的字段列表
* @return 字段列表 * @return 字段列表
* @throws SecurityException 安全检查异常 * @throws SecurityException 当安全管理器存在且拒绝访问时抛出
*/ */
public static Field[] getFieldsDirectly(final Class<?> beanClass, final boolean withSuperClassFields) throws SecurityException { public static Field[] getFieldsDirectly(final Class<?> beanClass, final boolean withSuperClassFields) throws SecurityException {
return FieldReflect.of(beanClass).getFieldsDirectly(withSuperClassFields); return FieldReflect.of(beanClass).getFieldsDirectly(withSuperClassFields);

View File

@ -149,7 +149,7 @@ public class MethodReflect {
* @param withSupers 是否包括父类或接口的方法列表 * @param withSupers 是否包括父类或接口的方法列表
* @param withMethodFromObject 是否包括Object中的方法 * @param withMethodFromObject 是否包括Object中的方法
* @return 方法列表 * @return 方法列表
* @throws SecurityException 安全检查异常 * @throws SecurityException 当安全管理器存在且拒绝访问时抛出
*/ */
public Method[] getMethodsDirectly(final boolean withSupers, final boolean withMethodFromObject) throws SecurityException { public Method[] getMethodsDirectly(final boolean withSupers, final boolean withMethodFromObject) throws SecurityException {
final Class<?> clazz = this.clazz; final Class<?> clazz = this.clazz;

View File

@ -286,7 +286,7 @@ public class MethodUtil {
* *
* @param clazz {@code null} * @param clazz {@code null}
* @return 方法列表 * @return 方法列表
* @throws SecurityException 安全检查异常 * @throws SecurityException 当安全管理器存在且拒绝访问时抛出
*/ */
public static Method[] getMethods(final Class<?> clazz) throws SecurityException { public static Method[] getMethods(final Class<?> clazz) throws SecurityException {
return getMethods(clazz, null); return getMethods(clazz, null);
@ -298,7 +298,7 @@ public class MethodUtil {
* @param clazz {@code null} * @param clazz {@code null}
* @param predicate 方法过滤器{@code null}表示无过滤 * @param predicate 方法过滤器{@code null}表示无过滤
* @return 方法列表 * @return 方法列表
* @throws SecurityException 安全检查异常 * @throws SecurityException 当安全管理器存在且拒绝访问时抛出
*/ */
public static Method[] getMethods(final Class<?> clazz, final Predicate<Method> predicate) throws SecurityException { public static Method[] getMethods(final Class<?> clazz, final Predicate<Method> predicate) throws SecurityException {
return METHODS_CACHE.computeIfAbsent(Assert.notNull(clazz), MethodReflect::of).getAllMethods(predicate); return METHODS_CACHE.computeIfAbsent(Assert.notNull(clazz), MethodReflect::of).getAllMethods(predicate);
@ -330,7 +330,7 @@ public class MethodUtil {
* *
* @param clazz {@code null} * @param clazz {@code null}
* @return 方法列表 * @return 方法列表
* @throws SecurityException 安全检查异常 * @throws SecurityException 当安全管理器存在且拒绝访问时抛出
*/ */
public static Method[] getDeclaredMethods(final Class<?> clazz) throws SecurityException { public static Method[] getDeclaredMethods(final Class<?> clazz) throws SecurityException {
return getDeclaredMethods(clazz, null); return getDeclaredMethods(clazz, null);
@ -342,7 +342,7 @@ public class MethodUtil {
* @param clazz {@code null} * @param clazz {@code null}
* @param predicate 方法过滤器{@code null}表示无过滤 * @param predicate 方法过滤器{@code null}表示无过滤
* @return 方法列表 * @return 方法列表
* @throws SecurityException 安全检查异常 * @throws SecurityException 当安全管理器存在且拒绝访问时抛出
*/ */
public static Method[] getDeclaredMethods(final Class<?> clazz, final Predicate<Method> predicate) throws SecurityException { public static Method[] getDeclaredMethods(final Class<?> clazz, final Predicate<Method> predicate) throws SecurityException {
return METHODS_CACHE.computeIfAbsent(Assert.notNull(clazz), MethodReflect::of).getDeclaredMethods(predicate); return METHODS_CACHE.computeIfAbsent(Assert.notNull(clazz), MethodReflect::of).getDeclaredMethods(predicate);
@ -363,7 +363,7 @@ public class MethodUtil {
* @param withSupers 是否包括父类或接口的方法列表 * @param withSupers 是否包括父类或接口的方法列表
* @param withMethodFromObject 是否包括Object中的方法 * @param withMethodFromObject 是否包括Object中的方法
* @return 方法列表 * @return 方法列表
* @throws SecurityException 安全检查异常 * @throws SecurityException 当安全管理器存在且拒绝访问时抛出
*/ */
public static Method[] getMethodsDirectly(final Class<?> beanClass, final boolean withSupers, final boolean withMethodFromObject) throws SecurityException { public static Method[] getMethodsDirectly(final Class<?> beanClass, final boolean withSupers, final boolean withMethodFromObject) throws SecurityException {
return MethodReflect.of(Assert.notNull(beanClass)).getMethodsDirectly(withSupers, withMethodFromObject); return MethodReflect.of(Assert.notNull(beanClass)).getMethodsDirectly(withSupers, withMethodFromObject);

View File

@ -34,12 +34,23 @@ public class TextSimilarity {
* <li>只比较两个字符串字母数字汉字部分其他符号去除</li> * <li>只比较两个字符串字母数字汉字部分其他符号去除</li>
* <li>计算出两个字符串最大子串除以最长的字符串结果即为相似度</li> * <li>计算出两个字符串最大子串除以最长的字符串结果即为相似度</li>
* </ul> * </ul>
* 空值处理
* <ul>
* <li>如果两个字符串都为{@code null}返回1视为完全相同</li>
* <li>如果其中一个字符串为{@code null}返回0视为完全不同</li>
* </ul>
* *
* @param strA 字符串1 * @param strA 字符串1
* @param strB 字符串2 * @param strB 字符串2
* @return 相似度 * @return 相似度取值范围为[0, 1]其中0表示完全不同1表示完全相同
*/ */
public static double similar(final String strA, final String strB) { public static double similar(final String strA, final String strB) {
if (null == strA && null == strB) {
return 1;
}
if (null == strA || null == strB) {
return 0;
}
final String newStrA; final String newStrA;
final String newStrB; final String newStrB;
if (strA.length() < strB.length()) { if (strA.length() < strB.length()) {
@ -79,9 +90,12 @@ public class TextSimilarity {
* *
* @param strA 字符串1 * @param strA 字符串1
* @param strB 字符串2 * @param strB 字符串2
* @return 最长公共子串 * @return 最长公共子串如果任一参数为{@code null}则返回{@code null}如果没有公共子串则返回空字符串""
*/ */
public static String longestCommonSubstring(final String strA, final String strB) { public static String longestCommonSubstring(final String strA, final String strB) {
if (null == strA || null == strB) {
return null;
}
// 初始化矩阵数据,matrix[0][0]的值为0 如果字符数组chars_strA和chars_strB的对应位相同则matrix[i][j]的值为左上角的值加1 // 初始化矩阵数据,matrix[0][0]的值为0 如果字符数组chars_strA和chars_strB的对应位相同则matrix[i][j]的值为左上角的值加1
// 否则matrix[i][j]的值等于左上方最近两个位置的较大值 矩阵中其余各点的值为0. // 否则matrix[i][j]的值等于左上方最近两个位置的较大值 矩阵中其余各点的值为0.
final int[][] matrix = generateMatrix(strA, strB); final int[][] matrix = generateMatrix(strA, strB);
@ -108,6 +122,7 @@ public class TextSimilarity {
} }
// --------------------------------------------------------------------------------------------------- Private method start // --------------------------------------------------------------------------------------------------- Private method start
/** /**
* 将字符串的所有数据依次写成一行去除无意义字符串 * 将字符串的所有数据依次写成一行去除无意义字符串
* *

View File

@ -131,9 +131,10 @@ public class EscapeUtil {
return StrUtil.toStringOrNull(content); return StrUtil.toStringOrNull(content);
} }
final StringBuilder tmp = new StringBuilder(content.length() * 6); final int len = content.length();
final StringBuilder tmp = new StringBuilder(len * 6);
char c; char c;
for (int i = 0; i < content.length(); i++) { for (int i = 0; i < len; i++) {
c = content.charAt(i); c = content.charAt(i);
if (!filter.test(c)) { if (!filter.test(c)) {
tmp.append(c); tmp.append(c);
@ -156,7 +157,26 @@ public class EscapeUtil {
} }
/** /**
* Escape解码 * Escape解码支持两种转义格式的解码
* <ul>
* <li>%XX - 两位十六进制数字用于表示ASCII字符0-255</li>
* <li>%uXXXX - 四位十六进制数字用于表示Unicode字符</li>
* </ul>
* <p>
* 对于不完整的转义序列本方法会将其原样保留而不抛出异常
* <ul>
* <li>字符串末尾的单独"%"字符会被原样保留</li>
* <li>"%u"后面不足4位十六进制数字时整个不完整序列会被原样保留</li>
* <li>"%"后面不足2位十六进制数字时%u格式整个不完整序列会被原样保留</li>
* </ul>
* 例如
* <pre>
* unescape("test%") = "test%" // 末尾的%被保留
* unescape("test%u12") = "test%u12" // 不足4位原样保留
* unescape("test%2") = "test%2" // 不足2位原样保留
* unescape("test%20") = "test " // 正常解码空格
* unescape("test%u4E2D") = "test中" // 正常解码中文字符
* </pre>
* *
* @param content 被转义的内容 * @param content 被转义的内容
* @return 解码后的字符串 * @return 解码后的字符串
@ -166,26 +186,40 @@ public class EscapeUtil {
return content; return content;
} }
final StringBuilder tmp = new StringBuilder(content.length()); final int len = content.length();
final StringBuilder tmp = new StringBuilder(len);
int lastPos = 0; int lastPos = 0;
int pos; int pos;
char ch; char ch;
while (lastPos < content.length()) { while (lastPos < len) {
pos = content.indexOf("%", lastPos); pos = content.indexOf("%", lastPos);
if (pos == lastPos) { if (pos == lastPos) {
if (content.charAt(pos + 1) == 'u') { if (pos + 1 < len && content.charAt(pos + 1) == 'u') {
if (pos + 6 <= len) {
ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16); ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16);
tmp.append(ch); tmp.append(ch);
lastPos = pos + 6; lastPos = pos + 6;
} else { } else {
// Not enough characters, append as-is
tmp.append(content.substring(pos));
lastPos = len;
}
} else {
// Check if there's enough characters for hex escape (%XX)
if (pos + 3 <= len) {
ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16); ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16);
tmp.append(ch); tmp.append(ch);
lastPos = pos + 3; lastPos = pos + 3;
} else {
// Not enough characters, append as-is
tmp.append(content.substring(pos));
lastPos = len;
}
} }
} else { } else {
if (pos == -1) { if (pos == -1) {
tmp.append(content.substring(lastPos)); tmp.append(content.substring(lastPos));
lastPos = content.length(); lastPos = len;
} else { } else {
tmp.append(content, lastPos, pos); tmp.append(content, lastPos, pos);
lastPos = pos; lastPos = pos;

View File

@ -16,31 +16,33 @@
package cn.hutool.v7.core.text.escape; package cn.hutool.v7.core.text.escape;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
public class EscapeUtilTest { public class EscapeUtilTest {
@Test @Test
public void escapeHtml4Test() { public void escapeHtml4Test() {
final String escapeHtml4 = EscapeUtil.escapeHtml4("<a>你好</a>"); final String escapeHtml4 = EscapeUtil.escapeHtml4("<a>你好</a>");
Assertions.assertEquals("&lt;a&gt;你好&lt;/a&gt;", escapeHtml4); assertEquals("&lt;a&gt;你好&lt;/a&gt;", escapeHtml4);
final String result = EscapeUtil.unescapeHtml4("&#25391;&#33633;&#22120;&#31867;&#22411;"); final String result = EscapeUtil.unescapeHtml4("&#25391;&#33633;&#22120;&#31867;&#22411;");
Assertions.assertEquals("振荡器类型", result); assertEquals("振荡器类型", result);
final String escape = EscapeUtil.escapeHtml4("*@-_+./(123你好)"); final String escape = EscapeUtil.escapeHtml4("*@-_+./(123你好)");
Assertions.assertEquals("*@-_+./(123你好)", escape); assertEquals("*@-_+./(123你好)", escape);
} }
@Test @Test
public void escapeTest(){ public void escapeTest(){
final String str = "*@-_+./(123你好)ABCabc"; final String str = "*@-_+./(123你好)ABCabc";
final String escape = EscapeUtil.escape(str); final String escape = EscapeUtil.escape(str);
Assertions.assertEquals("*@-_+./%28123%u4f60%u597d%29ABCabc", escape); assertEquals("*@-_+./%28123%u4f60%u597d%29ABCabc", escape);
final String unescape = EscapeUtil.unescape(escape); final String unescape = EscapeUtil.unescape(escape);
Assertions.assertEquals(str, unescape); assertEquals(str, unescape);
} }
@Test @Test
@ -48,24 +50,24 @@ public class EscapeUtilTest {
final String str = "*@-_+./(123你好)ABCabc"; final String str = "*@-_+./(123你好)ABCabc";
final String escape = EscapeUtil.escapeAll(str); final String escape = EscapeUtil.escapeAll(str);
Assertions.assertEquals("%2a%40%2d%5f%2b%2e%2f%28%31%32%33%u4f60%u597d%29%41%42%43%61%62%63", escape); assertEquals("%2a%40%2d%5f%2b%2e%2f%28%31%32%33%u4f60%u597d%29%41%42%43%61%62%63", escape);
final String unescape = EscapeUtil.unescape(escape); final String unescape = EscapeUtil.unescape(escape);
Assertions.assertEquals(str, unescape); assertEquals(str, unescape);
} }
/** /**
* https://gitee.com/chinabugotech/hutool/issues/I49JU8 * <a href="https://gitee.com/chinabugotech/hutool/issues/I49JU8">issue#I49JU8</a>
*/ */
@Test @Test
public void escapeAllTest2(){ public void escapeAllTest2(){
final String str = "٩"; final String str = "٩";
final String escape = EscapeUtil.escapeAll(str); final String escape = EscapeUtil.escapeAll(str);
Assertions.assertEquals("%u0669", escape); assertEquals("%u0669", escape);
final String unescape = EscapeUtil.unescape(escape); final String unescape = EscapeUtil.unescape(escape);
Assertions.assertEquals(str, unescape); assertEquals(str, unescape);
} }
@Test @Test
@ -73,21 +75,135 @@ public class EscapeUtilTest {
// 单引号不做转义 // 单引号不做转义
final String str = "'some text with single quotes'"; final String str = "'some text with single quotes'";
final String s = EscapeUtil.escapeHtml4(str); final String s = EscapeUtil.escapeHtml4(str);
Assertions.assertEquals("'some text with single quotes'", s); assertEquals("'some text with single quotes'", s);
} }
@Test @Test
public void unescapeSingleQuotesTest(){ public void unescapeSingleQuotesTest(){
final String str = "&apos;some text with single quotes&apos;"; final String str = "&apos;some text with single quotes&apos;";
final String s = EscapeUtil.unescapeHtml4(str); final String s = EscapeUtil.unescapeHtml4(str);
Assertions.assertEquals("'some text with single quotes'", s); assertEquals("'some text with single quotes'", s);
} }
@Test @Test
public void escapeXmlTest(){ public void escapeXmlTest(){
final String a = "<>"; final String a = "<>";
final String escape = EscapeUtil.escapeXml(a); final String escape = EscapeUtil.escapeXml(a);
Assertions.assertEquals("&lt;&gt;", escape); assertEquals("&lt;&gt;", escape);
Assertions.assertEquals("中文“双引号”", EscapeUtil.escapeXml("中文“双引号”")); assertEquals("中文“双引号”", EscapeUtil.escapeXml("中文“双引号”"));
}
@Test
void testUnescapeNull() {
assertNull(EscapeUtil.unescape(null));
}
@Test
void testUnescapeEmpty() {
assertEquals("", EscapeUtil.unescape(""));
}
@Test
void testUnescapeBlank() {
assertEquals(" ", EscapeUtil.unescape(" "));
}
@Test
void testUnescapeAsciiCharacters() {
// 测试ASCII字符转义
assertEquals("hello", EscapeUtil.unescape("hello"));
assertEquals("test space", EscapeUtil.unescape("test%20space"));
assertEquals("A", EscapeUtil.unescape("%41"));
assertEquals("a", EscapeUtil.unescape("%61"));
assertEquals("0", EscapeUtil.unescape("%30"));
assertEquals("!", EscapeUtil.unescape("%21"));
assertEquals("@", EscapeUtil.unescape("%40"));
assertEquals("#", EscapeUtil.unescape("%23"));
}
@Test
void testUnescapeUnicodeCharacters() {
// 测试Unicode字符转义
assertEquals("", EscapeUtil.unescape("%u4E2D"));
assertEquals("", EscapeUtil.unescape("%u6587"));
assertEquals("", EscapeUtil.unescape("%u6D4B"));
assertEquals("", EscapeUtil.unescape("%u8BD5"));
assertEquals("😊", EscapeUtil.unescape("%uD83D%uDE0A")); // 笑脸表情
}
@Test
void testUnescapeMixedContent() {
// 测试混合内容
assertEquals("Hello 世界!", EscapeUtil.unescape("Hello%20%u4E16%u754C%21"));
assertEquals("测试: 100%", EscapeUtil.unescape("%u6D4B%u8BD5%3A%20100%25"));
assertEquals("a+b=c", EscapeUtil.unescape("a%2Bb%3Dc"));
}
@Test
void testUnescapeIncompleteEscapeSequences() {
// 测试不完整的转义序列
assertEquals("test%", EscapeUtil.unescape("test%"));
assertEquals("test%u", EscapeUtil.unescape("test%u"));
assertEquals("test%u1", EscapeUtil.unescape("test%u1"));
assertEquals("test%u12", EscapeUtil.unescape("test%u12"));
assertEquals("test%u123", EscapeUtil.unescape("test%u123"));
assertEquals("test%1", EscapeUtil.unescape("test%1"));
assertEquals("test%2", EscapeUtil.unescape("test%2"));
}
@Test
void testUnescapeEdgeCases() {
// 测试边界情况
assertEquals("%", EscapeUtil.unescape("%"));
assertEquals("%u", EscapeUtil.unescape("%u"));
assertEquals("%%", EscapeUtil.unescape("%%"));
assertEquals("%u%", EscapeUtil.unescape("%u%"));
assertEquals("100% complete", EscapeUtil.unescape("100%25%20complete"));
}
@Test
void testUnescapeMultipleEscapeSequences() {
// 测试多个连续的转义序列
assertEquals("ABC", EscapeUtil.unescape("%41%42%43"));
assertEquals("中文测试", EscapeUtil.unescape("%u4E2D%u6587%u6D4B%u8BD5"));
assertEquals("A 中 B", EscapeUtil.unescape("%41%20%u4E2D%20%42"));
}
@Test
void testUnescapeSpecialCharacters() {
// 测试特殊字符
assertEquals("\n", EscapeUtil.unescape("%0A"));
assertEquals("\r", EscapeUtil.unescape("%0D"));
assertEquals("\t", EscapeUtil.unescape("%09"));
assertEquals(" ", EscapeUtil.unescape("%20"));
assertEquals("<", EscapeUtil.unescape("%3C"));
assertEquals(">", EscapeUtil.unescape("%3E"));
assertEquals("&", EscapeUtil.unescape("%26"));
}
@Test
void testUnescapeComplexScenario() {
// 测试复杂场景
final String original = "Hello 世界! 这是测试。Email: test@example.com";
final String escaped = "Hello%20%u4E16%u754C%21%20%u8FD9%u662F%u6D4B%u8BD5%u3002Email%3A%20test%40example.com";
assertEquals(original, EscapeUtil.unescape(escaped));
}
@Test
void testUnescapeWithIncompleteAtEnd() {
// 测试末尾有不完整转义序列
assertEquals("normal%", EscapeUtil.unescape("normal%"));
assertEquals("normal%u", EscapeUtil.unescape("normal%u"));
assertEquals("normal%u1", EscapeUtil.unescape("normal%u1"));
assertEquals("normal%1", EscapeUtil.unescape("normal%1"));
}
@Test
void testUnescapeUppercaseHex() {
// 测试大写十六进制
assertEquals("A", EscapeUtil.unescape("%41"));
assertEquals("A", EscapeUtil.unescape("%41"));
assertEquals("", EscapeUtil.unescape("%u4E2D"));
assertEquals("", EscapeUtil.unescape("%u4E2D"));
} }
} }