
com.moon.core.util.PropertiesParser Maven / Gradle / Ivy
package com.moon.core.util;
import com.moon.core.enums.Arrays2;
import com.moon.core.enums.Const;
import com.moon.core.enums.Strings;
import com.moon.core.io.FileUtil;
import com.moon.core.lang.ArrayUtil;
import com.moon.core.lang.StringUtil;
import com.moon.core.util.interfaces.Parser;
import java.util.*;
import static com.moon.core.lang.StringUtil.*;
/**
* properties 文件解析器,以下是解析规则:
*
* 与 spring.profiles.include、spring.profiles.active 相似,解析器引入了——引入文件、活跃文件、引用分割符的概念;
*
* 其中 {@code spring.profiles} 就是是命名空间(namespace),这里也引入了相同意义的命名空间: moon,
* 当然也可以自定义这个命名空间,但是这里要求命名空间必须是非空白字符串;
*
* 在命名空间下有:
* 三个关键的键值映射 “import”、“active”、“delimiters”;
* 两个关键字 “name:”、“path:”;
* 和一个符号 “:”。
*
* 分别对应:namespace.import、namespace.active、namespace.delimiters;
*
* 默认是:moon.import、moon.active、moon.delimiters;
*
* 【注意】这里是 import 而不是 include。
*
* 以下以解析 properties 文件 moon.properties 为例:
*
* 【若 1 】:基本
*
* moon.import=pre
*
* moon.active=dev
*
* 在解析 moon.properties 前将先依次解析:moon-pre.properties、moon-dev.properties,
*
* 如果 import、active 文件里还有对应的 import、active 字段,将递归解析,然后再返回;
*
* 【若 2 】:可分别在 import、active 上重新定义解析命名空间(namespace),使用英文逗号(:)
*
* moon.import=pre:spring
*
* moon.active=dev:other
*
* 在解析 moon.properties 前将先依次解析:moon-pre.properties、moon-dev.properties,
*
* 并且使用各自的命名空间:spring、other;
*
* 【若 3 】:可指定为全名
*
* moon.import=name:application-pre:spring
*
* moon.active=name:application-dev
*
* 在解析 moon.properties 前将先依次解析:application-pre.properties、application-dev.properties,
*
* 同样支持在后面用冒号自定义命名空间;
*
* 【若 4 】:可指定为绝对路径
*
* 在指定全名中,如果 moon.properties 实际上不是根目录,而是 otherDir/moon.properties,
* 最后解析的将是 otherDir/application-pre.properties,但是这里可以自定义完整路径
*
* moon.import=path:anotherDir/application-pre:spring
*
* moon.import=path:anotherDir/application-dev
*
* 在解析 moon.properties 前将先依次解析:anotherDir/application-pre.properties、anotherDir/application-dev.properties,
*
* 若在文件中指定了 moon.delimiters 字段,则要求是用英文逗号分割的两个非空白字符串(超出的将忽略),用来包裹取值的键,
* 通常对应的值在 moon.import 或 moon.active 中,也可以在当前文件中,并且不分顺序,递归引用(但不能循环引用),如:
*
* moon.delimiters=${,}
* other.key=${from.moon.active.key}
* current.key=${other.key}
*
* 同时如果当前文件定义了 delimiters 字段,此字段的值将会向 active 和 import 传递,不需要重新设置,
* 当然,active、import 文件里还是可以进一步设置此 delimiters 字段,在进行递归解析的时候,将继续传递。
*
* 最后返回所有键值对,如果存在相同键,active 文件覆盖 import 文件,当前文件覆盖 active 文件
*
* @author moonsky
*/
public class PropertiesParser implements Parser {
private final static PropertiesHashMap EMPTY_MAP = EmptyHashMap.DEFAULT;
private final static String[] EMPTY_STRINGS = Arrays2.STRINGS.empty();
private final static String[] DEFAULT_NAMES = {"import", "active"};
private final static boolean DEFAULT_BUBBLE_DELIMITERS = false;
private final static String DEFAULT_NAMESPACE = "moon";
private final static String NAME = "name:";
private final static String PATH = "path:";
private final static String COLON = Strings.COLON.value;
private final static String DOT = Strings.DOT.value;
private final static String DELIMITERS_NAME = "delimiters";
private final static String SUFFIX = ".properties";
private final String currentNamespace;
private final String currentDelimitersName;
/**
* delimiters 是否可以从当前文件向 import、active 引用文件传递
* 默认不可传递
*/
private boolean bubbleDelimiters;
/**
* 默认 KEY
*/
private final Set includes;
/**
* 包括:{@link #includes}、{@link #currentDelimitersName}
*/
private final Set excludesKey;
/**
* 防止循环引用
*/
private final Map parsedSources;
/*
* ----------------------------------------------------------------------------
* constructor
* ----------------------------------------------------------------------------
*/
public PropertiesParser() { this(DEFAULT_NAMESPACE); }
public PropertiesParser(String namespace) { this(namespace, DEFAULT_BUBBLE_DELIMITERS); }
public PropertiesParser(String namespace, String[] names) {
this(namespace, names, DEFAULT_BUBBLE_DELIMITERS, new HashMap<>());
}
public PropertiesParser(String namespace, boolean bubbleDelimiters) {
this(namespace, EMPTY_STRINGS, bubbleDelimiters, new HashMap<>());
}
public PropertiesParser(String namespace, String[] names, boolean bubbleDelimiters) {
this(namespace, names, bubbleDelimiters, new HashMap<>());
}
PropertiesParser(String namespace, boolean bubbleDelimiters, Map parsedSources) {
this(namespace, EMPTY_STRINGS, bubbleDelimiters, parsedSources);
}
private PropertiesParser(
String namespace, String[] names, boolean bubbleDelimiters, Map parsedSources
) {
this(namespace, namesToIncludesSet(namespace, names), bubbleDelimiters, parsedSources);
}
private PropertiesParser(
String namespace, Set includes, boolean bubbleDelimiters, Map parsedSources
) {
String ns = this.currentNamespace = ValidateUtil.requireNotEmpty(trimToNull(namespace));
this.currentDelimitersName = ns + DOT + DELIMITERS_NAME;
this.bubbleDelimiters = bubbleDelimiters;
this.parsedSources = parsedSources;
this.includes = includes;
excludesKey = SetUtil.add(new HashSet<>(includes), currentDelimitersName);
}
/**
* 实现
*
* @param namespace 命名空间
* @param bubbleDelimiters 参数是否冒泡
* @param parsedSources 已解析的数据
*
* @return 返回带有命名空间的解析器
*/
protected PropertiesParser getParser(
String namespace, boolean bubbleDelimiters, Map parsedSources
) { return new PropertiesParser(namespace, bubbleDelimiters, parsedSources); }
/**
* 解析
*
* @param sourcePath 指定资源配置属性列表
*
* @return 配置属性
*/
protected PropertiesHashMap getResources(String sourcePath) {
return new PropertiesHashMap(PropertiesUtil.get(sourcePath));
}
/*
* ----------------------------------------------------------------------------
* public method
* ----------------------------------------------------------------------------
*/
public Properties resolveProperties(String propertiesSource) {
return MapUtil.putAll(new Properties(), parse(propertiesSource));
}
@Override
public PropertiesHashMap parse(final String propertiesSource) { return parse(propertiesSource, DELIMITERS); }
public PropertiesGroup parseAsGroup(String propertiesSource) { return PropertiesGroup.of(parse(propertiesSource)); }
/*
* ----------------------------------------------------------------------------
* parse core
* ----------------------------------------------------------------------------
*/
private PropertiesHashMap parse(final String propertiesSource, IDelimiters defaultDelimiters) {
if (parsedSources.containsKey(propertiesSource)) {
return parsedSources.getOrDefault(propertiesSource, EMPTY_MAP);
} else {
parsedSources.put(propertiesSource, null);
}
final String sourcePath = FileUtil.formatFilepath(propertiesSource);
final String activeName = activeName(sourcePath);
final String activePath = activePath(sourcePath);
PropertiesHashMap currentProps = getResources(sourcePath);
IDelimiters delimiters = parseDelimiters(currentProps, bubbleDelimiters ? defaultDelimiters : DELIMITERS);
Set imports = parseIncludes(currentProps, activePath, activeName, delimiters);
PropertiesHashMap[] parameters = imports.toArray(new PropertiesHashMap[imports.size()]);
PropertiesHashMap properties = computeProps(delimiters, currentProps, parameters);
parsedSources.put(propertiesSource, properties);
return properties;
}
/*
* -------------------------------------------------------------------------
* compute result properties
* -------------------------------------------------------------------------
*/
private PropertiesHashMap computeProps(
IDelimiters delimiters, PropertiesHashMap currentProps, PropertiesHashMap... includesProps
) {
PropertiesHashMap computed = new PropertiesHashMap(currentProps.size());
PropertiesHashMap presents = new PropertiesHashMap(8);
PropertiesHashMap[] params = formatParams(computed, currentProps, includesProps);
for (Map.Entry entry : currentProps.entrySet()) {
String key = entry.getKey(), value = entry.getValue();
if (!excludesKey.contains(key)) {
value = recursiveGetValue(key, value, delimiters, presents, params);
presents.clear();
}
computed.put(key, value);
}
return new PropertiesHashMap(ArrayUtil.reverse(params));
}
private PropertiesHashMap[] formatParams(
PropertiesHashMap computed, PropertiesHashMap currentProps, PropertiesHashMap... includesProps
) {
int length = includesProps.length, index = 0;
PropertiesHashMap[] params = new PropertiesHashMap[length + 2];
params[index++] = computed;
params[index++] = currentProps;
System.arraycopy(includesProps, 0, params, 0 + index, length);
return params;
}
private static String recursiveGetValue(
String key, String value, IDelimiters delimiters, PropertiesHashMap presents, PropertiesHashMap... propMaps
) {
if (presents.containsKey(key)) {
throw new StackOverflowError("The stack overflow of key: " + key);
} else {
presents.put(key, null);
}
String start = delimiters.getStart(), end = delimiters.getEnd();
if ((value = trimToNull(value)) != null && value.startsWith(start) && value.endsWith(end)) {
String importKey = trimToNull(value.substring(start.length(), value.indexOf(end)));
String importValue = getValue(importKey, propMaps);
return recursiveGetValue(importKey, importValue, delimiters, presents, propMaps);
}
return value;
}
private static String getValue(String key, PropertiesHashMap... propMaps) {
String value = null;
for (PropertiesHashMap props : propMaps) {
value = value == null ? getValue(props, key) : value;
}
return value;
}
private static String getValue(PropertiesHashMap map, String key) {
String value = map.get(key);
return value == null && map.containsKey(key) ? Const.EMPTY : value;
}
/*
* -------------------------------------------------------------------------
* parse delimiters
* -------------------------------------------------------------------------
*/
private IDelimiters parseDelimiters(PropertiesHashMap props, IDelimiters defaultDelimiters) {
String delimitersValue = trimToNull(props.get(currentDelimitersName));
if (delimitersValue != null) {
try {
String[] delimiters = delimitersValue.split(",");
return new Delimiters(delimiters[0], delimiters[1]);
} catch (Throwable e) {
throw new IllegalArgumentException("无效分隔符(delimiters)" + ",必须是用英文逗号(,)分割的两个非空字符串,但是配置的是:" + delimitersValue);
}
}
return defaultDelimiters;
}
/*
* -------------------------------------------------------------------------
* parse includes
* -------------------------------------------------------------------------
*/
private Set parseIncludes(
PropertiesHashMap props, String activePath, String activeName, IDelimiters delimiters
) {
Set maps = new LinkedHashSet<>(includes.size());
for (String include : includes) {
maps.add(parseInclude(props, include, activePath, activeName, delimiters));
}
return maps;
}
private PropertiesHashMap parseInclude(
PropertiesHashMap props, String includeTargetName, String activePath, String activeName, IDelimiters delimiters
) {
PropertiesHashMap includeProps = EMPTY_MAP;
String includeName = StringUtil.trimToNull(props.get(includeTargetName));
if (StringUtil.isNotEmpty(includeName)) {
includeProps = parseIncludeProps(includeName, activePath, activeName, delimiters);
}
return includeProps;
}
private PropertiesHashMap parseIncludeProps(
String inputName, String activePath, String activeName, IDelimiters delimiters
) {
String[] inputs = inputName.split(",");
PropertiesHashMap properties = new PropertiesHashMap(16 * inputs.length);
for (String input : inputs) {
properties.putAll(parseInputName(input, activePath, activeName, delimiters));
}
return properties;
}
private PropertiesHashMap parseInputName(
String inputName, String activePath, String activeName, IDelimiters delimiters
) {
PropertiesHashMap props = EMPTY_MAP;
String formatted, simpleName = trimToEmpty(inputName);
if (simpleName.startsWith(NAME)) {
String sourceDir = activeDir(activePath, activeName);
String[] strings = simpleName.split(COLON);
String name = trimToNull(strings[1]);
formatted = toPropertiesName(sourceDir + ValidateUtil.requireNotEmpty(name));
props = parseNSMap(formatted, strings, delimiters);
} else if (simpleName.startsWith(PATH)) {
String[] strings = simpleName.split(COLON);
formatted = toPropertiesName(ValidateUtil.requireNotEmpty(trimToNull(strings[1])));
props = parseNSMap(formatted, strings, delimiters);
} else if (simpleName.contains(COLON)) {
String[] strings = simpleName.split(COLON);
String name = trimToNull(strings[0]);
formatted = toPropertiesName(activePath + '-' + ValidateUtil.requireNotEmpty(name));
props = parseNSMap(trimToNull(strings[1]), formatted, delimiters);
} else if (!simpleName.isEmpty()) {
formatted = toPropertiesName(activePath + '-' + ValidateUtil.requireNotEmpty(simpleName));
props = parseNSMap(null, formatted, delimiters);
}
return props;
}
/**
* 自定义解析 delimiters 的功能在这添加,ns 即为包含 delimiters 的数据
* 解析过程和逻辑待实现
*
* @param ns
* @param formatted
* @param delimiters
*
* @return
*/
private PropertiesHashMap parseNSMap(String ns, String formatted, IDelimiters delimiters) {
return ns == null || StringUtil.equals(ns, currentNamespace) ? parse(formatted, delimiters) : getParser(ns,
bubbleDelimiters,
this.parsedSources).parse(formatted, delimiters);
}
private PropertiesHashMap parseNSMap(String formatted, String[] strings, IDelimiters delimiters) {
return strings.length > 2 ? parseNSMap(trimToNull(strings[2]), formatted, delimiters) : parse(formatted,
delimiters);
}
/*
* -------------------------------------------------------------------------
* inner tools
* -------------------------------------------------------------------------
*/
private static String activePath(String sourcePath) {
int end = ValidateUtil.requireGtOf(sourcePath.lastIndexOf(SUFFIX), 0);
return sourcePath.substring(0, end);
}
private static String activeName(String sourcePath) {
int begin = minimumWithZero(sourcePath.lastIndexOf('/'));
int end = ValidateUtil.requireGtOf(sourcePath.lastIndexOf(SUFFIX), begin);
return sourcePath.substring(incrementIfPositive(begin), end);
}
private static String activeDir(String sourceName, String sourceBaseName) {
return sourceName.substring(0, minimumWithZero(sourceName.indexOf(sourceBaseName)));
}
private static int incrementIfPositive(int value) { return value > 0 ? value + 1 : value; }
private static int minimumWithZero(int value) { return Math.max(value, 0); }
private static String toPropertiesName(String name) { return name.endsWith(SUFFIX) ? name : name + SUFFIX; }
private static String requireNonDelimitersName(String ns, String name) {
if (DELIMITERS_NAME.equals(name)) { throw new IllegalArgumentException(name); }
return ns + DOT + name;
}
private static Set namesToIncludesSet(String namespace, String... names) {
final String ns = ValidateUtil.requireNotEmpty(trimToNull(namespace));
names = Arrays2.STRINGS.defaultIfEmpty(names, DEFAULT_NAMES);
Set ret = new LinkedHashSet<>();
for (String name : names) {
ret.add(requireNonDelimitersName(ns, name));
}
return ret;
}
/*
* -------------------------------------------------------------------------
* delimiters
* -------------------------------------------------------------------------
*/
private final static IDelimiters DELIMITERS = new IDelimiters();
private static class IDelimiters {
String getStart() { return "${"; }
String getEnd() { return "}"; }
}
private final static class Delimiters extends IDelimiters {
private final String start;
private final String end;
Delimiters(String s, String e) {
start = trimToDefault(s, DELIMITERS.getStart());
end = trimToDefault(e, DELIMITERS.getEnd());
}
@Override
String getStart() { return start; }
@Override
String getEnd() { return end; }
}
}