sviolet.thistle.util.conversion.SimpleKeyValueEncoder Maven / Gradle / Ivy
/*
* Copyright (C) 2015-2018 S.Violet
*
* Licensed 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.
*
* Project GitHub: https://github.com/shepherdviolet/thistle
* Email: [email protected]
*/
package sviolet.thistle.util.conversion;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Encode Map to String, Decode String to Map
* Map对转String, String转Map
*
* For simple configuration, or message payload
* 用于简单的配置, 或消息报文体
*
* Format 1
*
* key1=value1,key2=value2,key3=value3
*
*
* Format 2
*
* key1=value1
* key2=value2
* key3=value3
*
*
* @author S.Violet
*/
public class SimpleKeyValueEncoder {
//raw split
private static final char RAW_SPLIT = ',';
private static final char RAW_NEWLINE = '\n';
private static final char RAW_RETURN = '\r';
//raw equal
private static final char RAW_EQUAL = '=';
//raw others
private static final char RAW_ESCAPE = '\\';
private static final char RAW_SPACE = ' ';
private static final char RAW_TAB = '\t';
//escape split
private static final char ESCAPE_NEWLINE = 'n';
private static final char ESCAPE_RETURN = 'r';
//escape others
private static final char ESCAPE_NULL = '0';
private static final char ESCAPE_SPACE = 's';
private static final char ESCAPE_TAB = 't';
//full escape
private static final String FULL_ESCAPE_NEWLINE = "\\n";
private static final String FULL_ESCAPE_RETURN = "\\r";
//full others
private static final String FULL_ESCAPE_NULL = "\\0";
private static final String FULL_ESCAPE_SPACE = "\\s";
private static final String FULL_ESCAPE_TAB = "\\t";
/**
* Encode Map to String
*
*
* key1=value1,key2=value2,key3=value3
*
*
* @param keyValue Map
* @return Encoded string
*/
public static String encode(Map keyValue){
return encode(keyValue, false);
}
/**
* Encode Map to String
*
* newLineSplit == false
*
* key1=value1,key2=value2,key3=value3
*
*
* newLineSplit == true
*
* key1=value1
* key2=value2
* key3=value3
*
*
* @param keyValue Map
* @param newLineSplit true: Using \n to split key-value element, false: Using , to split key-value element
* @return Encoded string
*/
public static String encode(Map keyValue, boolean newLineSplit){
if (keyValue == null || keyValue.size() <= 0) {
return "";
}
StringBuilder stringBuilder = new StringBuilder();
int i = 0;
for (Map.Entry entry : keyValue.entrySet()) {
if (i++ > 0) {
stringBuilder.append(newLineSplit ? RAW_NEWLINE : RAW_SPLIT);
}
encodeAppend(stringBuilder, entry.getKey());
stringBuilder.append(RAW_EQUAL);
encodeAppend(stringBuilder, entry.getValue());
}
return stringBuilder.toString();
}
private static void encodeAppend(StringBuilder stringBuilder, String str){
if (str == null) {
stringBuilder.append(FULL_ESCAPE_NULL);
return;
}
char[] chars = str.toCharArray();
int start = 0;
for (int i = 0 ; i < chars.length ; i++) {
char c = chars[i];
if (c == RAW_SPLIT ||
c == RAW_EQUAL ||
c == RAW_ESCAPE){
stringBuilder.append(chars, start, i - start);
stringBuilder.append(RAW_ESCAPE);
stringBuilder.append(c);
start = i + 1;
} else if (c == RAW_SPACE) {
stringBuilder.append(chars, start, i - start);
stringBuilder.append(FULL_ESCAPE_SPACE);
start = i + 1;
} else if (c == RAW_TAB) {
stringBuilder.append(chars, start, i - start);
stringBuilder.append(FULL_ESCAPE_TAB);
start = i + 1;
} else if (c == RAW_NEWLINE) {
stringBuilder.append(chars, start, i - start);
stringBuilder.append(FULL_ESCAPE_NEWLINE);
start = i + 1;
} else if (c == RAW_RETURN) {
stringBuilder.append(chars, start, i - start);
stringBuilder.append(FULL_ESCAPE_RETURN);
start = i + 1;
}
}
if (start < chars.length) {
stringBuilder.append(chars, start, chars.length - start);
}
}
/**
* Decode String to Map
*
* Format 1
*
* key1=value1,key2=value2,key3=value3
*
*
* Format 2
*
* key1=value1
* key2=value2
* key3=value3
*
*
* @param encoded encoded string
* @return Map
* @throws DecodeException throw if encoded string invalid
*/
public static Map decode(String encoded) throws DecodeException {
if (encoded == null || encoded.length() <= 0) {
return new LinkedHashMap<>(0);
}
Map resultMap = new LinkedHashMap<>();
char[] chars = encoded.toCharArray();
Visitor visitor = new Visitor();
boolean escaping = false;
boolean splitting = false;
int start = 0;
for (int i = 0 ; i < chars.length ; i++) {
char c = chars[i];
if (splitting) {
//handle splitting, to skip duplicate split char
if (c <= RAW_SPACE) {
//skip control char
start = i + 1;
continue;
} else {
//finish splitting, find normal char
splitting = false;
}
}
if (escaping) {
//handle escape
visitor.onEscape(resultMap, chars, start, i, c);
start = i + 1;
//finish escape
escaping = false;
//next char
continue;
}
if (c == RAW_ESCAPE) {
//find escape
escaping = true;
} else if (c == RAW_SPLIT ||
c == RAW_NEWLINE ||
c == RAW_RETURN) {
//element finish
visitor.onElementFinish(resultMap, chars, start, i);
start = i + 1;
//mark is splitting, to skip duplicate split char
splitting = true;
} else if (c == RAW_EQUAL) {
//find equal
visitor.onEqual(resultMap, chars, start, i);
start = i + 1;
}
}
//finish all
visitor.onElementFinish(resultMap, chars, start, chars.length);
return resultMap;
}
private static class Visitor {
//true: decoding key, false: decoding value
private boolean keyDecoding = true;
private StringBuilder keyBuilder = new StringBuilder();
private boolean keyNull = false;
private int keyStart = Integer.MAX_VALUE;
private int keyEnd = Integer.MIN_VALUE;
private StringBuilder valueBuilder = new StringBuilder();
private boolean valueNull = false;
private int valueStart = Integer.MAX_VALUE;
private int valueEnd = Integer.MIN_VALUE;
/**
* when we find a char after escape \
*/
private void onEscape(Map map, char[] chars, int startIndex, int currentIndex, char c) throws DecodeException {
//append previous chars (skip previous escape char \)
appendPrevious(chars, startIndex, currentIndex - 1);
//append escape char
if (c == RAW_SPLIT ||
c == RAW_EQUAL ||
c == RAW_ESCAPE){
//normal escape
appendChar(chars, c);
} else if (c == ESCAPE_SPACE) {
//record position of valid space or tab, avoid to trimmed
recordStartEnd();
//space escape
appendChar(chars, RAW_SPACE);
} else if (c == ESCAPE_TAB) {
//record position of valid space or tab, avoid to trimmed
recordStartEnd();
//tab escape
appendChar(chars, RAW_TAB);
} else if (c == ESCAPE_NEWLINE) {
//record position of valid space or tab, avoid to trimmed
recordStartEnd();
//newline escape
appendChar(chars, RAW_NEWLINE);
} else if (c == ESCAPE_RETURN) {
//record position of valid space or tab, avoid to trimmed
recordStartEnd();
//return escape
appendChar(chars, RAW_RETURN);
} else if (c == ESCAPE_NULL) {
//null escape
if (keyDecoding) {
//space escape can only be used alone
if (keyBuilder.length() > 0 || keyNull) {
throw new DecodeException("Invalid data, '\\0' can only be used alone, example \\0=abc or abc=\\0, data:" + new String(chars));
}
//set key null
keyNull = true;
} else {
//space escape can only be used alone
if (valueBuilder.length() > 0 || valueNull) {
throw new DecodeException("Invalid data, '\\0' can only be used alone, example \\0=abc or abc=\\0, data:" + new String(chars));
}
//set value null
valueNull = true;
}
} else {
throw new DecodeException("Invalid data, undefined escape \\" + c + ", data:" + new String(chars));
}
}
/**
* when we find equal =
*/
private void onEqual(Map map, char[] chars, int startIndex, int currentIndex) throws DecodeException {
//check state
if (!keyDecoding) {
throw new DecodeException("Invalid data, element has two '=', use escape char '\\=' instead, problem key:" + keyBuilder.toString() + ", data:" + new String(chars));
}
//append previous chars
appendPrevious(chars, startIndex, currentIndex);
//start to decoding value
keyDecoding = false;
}
/**
* when element finish
*/
private void onElementFinish(Map map, char[] chars, int startIndex, int currentIndex) throws DecodeException {
//check state
if (keyDecoding) {
throw new DecodeException("Invalid data, element has no value, problem key:" + keyBuilder.toString() + ", data:" + new String(chars));
}
//append previous chars
appendPrevious(chars, startIndex, currentIndex);
//get and trim key/value
String key = keyNull ? null : trim(keyBuilder.toString(), keyStart, keyEnd);
String value = valueNull ? null : trim(valueBuilder.toString(), valueStart, valueEnd);
//put map
map.put(key, value);
//reset
reset();
}
/**
* remove the leading and trailing spaces, and it will not remove valid spaces and tabs (record by keyStart/keyEnd/valueStart/valueEnd)
*/
private String trim(String value, int start, int end){
char[] chars = value.toCharArray();
int from = 0;
while (from < chars.length && chars[from] <= RAW_SPACE && from < start) {
from++;
}
int to = chars.length - 1;
while (to >= from && chars[to] <= RAW_SPACE && to > end) {
to--;
}
return from <= 0 && to >= chars.length - 1 ? value : value.substring(from, to + 1);
}
/**
* reset context
*/
private void reset() {
keyDecoding = true;
keyBuilder.setLength(0);
keyNull = false;
keyStart = Integer.MAX_VALUE;
keyEnd = Integer.MIN_VALUE;
valueBuilder.setLength(0);
valueNull = false;
valueStart = Integer.MAX_VALUE;
valueEnd = Integer.MIN_VALUE;
}
/**
* append from startIndex to currentIndex
*/
private void appendPrevious(char[] chars, int startIndex, int currentIndex) throws DecodeException {
if (startIndex < currentIndex) {
if (keyDecoding) {
//space escape can only be used alone
if (keyNull) {
throw new DecodeException("Invalid data, '\\0' can only be used alone, example \\0=abc or abc=\\0, data:" + new String(chars));
}
keyBuilder.append(chars, startIndex, currentIndex - startIndex);
} else {
//space escape can only be used alone
if (valueNull) {
throw new DecodeException("Invalid data, '\\0' can only be used alone, example \\0=abc or abc=\\0, data:" + new String(chars));
}
valueBuilder.append(chars, startIndex, currentIndex - startIndex);
}
}
}
/**
* append char
*/
private void appendChar(char[] chars, char c) throws DecodeException {
if (keyDecoding) {
//space escape can only be used alone
if (keyNull) {
throw new DecodeException("Invalid data, '\\0' can only be used alone, example \\0=abc or abc=\\0, data:" + new String(chars));
}
keyBuilder.append(c);
} else {
//space escape can only be used alone
if (valueNull) {
throw new DecodeException("Invalid data, '\\0' can only be used alone, example \\0=abc or abc=\\0, data:" + new String(chars));
}
valueBuilder.append(c);
}
}
/**
* record position of valid space or tab, avoid to trimmed
*/
private void recordStartEnd(){
if (keyDecoding) {
int currPosition = keyBuilder.length();
if (currPosition < keyStart) {
keyStart = currPosition;
}
if (currPosition > keyEnd) {
keyEnd = currPosition;
}
} else {
int currPosition = valueBuilder.length();
if (currPosition < valueStart) {
valueStart = currPosition;
}
if (currPosition > valueEnd) {
valueEnd = currPosition;
}
}
}
}
public static class DecodeException extends Exception {
private static final long serialVersionUID = 202454797847050441L;
public DecodeException(String message) {
super(message);
}
public DecodeException(String message, Throwable cause) {
super(message, cause);
}
}
}