org.jboss.logmanager.PropertyValues Maven / Gradle / Ivy
/*
* Copyright 2018 Red Hat, Inc.
*
* 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.
*/
package org.jboss.logmanager;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* A utility for converting objects into strings and strings into objects for storage in logging configurations.
*
* @author James R. Perkins
*/
@SuppressWarnings("WeakerAccess")
public class PropertyValues {
private static final int KEY = 0;
private static final int VALUE = 1;
/**
* Parses a string of key/value pairs into a map.
*
* The key/value pairs are separated by a comma ({@code ,}). The key and value are separated by an equals
* ({@code =}).
*
*
* If a key contains a {@code \} or an {@code =} it must be escaped by a preceding {@code \}. Example: {@code
* key\==value,\\key=value}.
*
*
* If a value contains a {@code \} or a {@code ,} it must be escaped by a preceding {@code \}. Example: {@code
* key=part1\,part2,key2=value\\other}.
*
*
*
* If the value for a key is empty there is no trailing {@code =} after a key the will be {@code null}.
*
*
* @param s the string to parse
*
* @return a map of the key value pairs or an empty map if the string is {@code null} or empty
*/
public static Map stringToMap(final String s) {
if (s == null || s.isEmpty()) return Collections.emptyMap();
final Map map = new LinkedHashMap<>();
final StringBuilder key = new StringBuilder();
final StringBuilder value = new StringBuilder();
final char[] chars = s.toCharArray();
int state = 0;
for (int i = 0; i < chars.length; i++) {
final char c = chars[i];
switch (state) {
case KEY: {
switch (c) {
case '\\': {
// Handle escapes
if (chars.length > ++i) {
final char next = chars[i];
if (next == '=' || next == '\\') {
key.append(next);
continue;
}
}
throw new IllegalStateException("Escape character found at invalid position " + i + ". Only characters '=' and '\\' need to be escaped for a key.");
}
case '=': {
state = VALUE;
continue;
}
default: {
key.append(c);
continue;
}
}
}
case VALUE: {
switch (c) {
case '\\': {
// Handle escapes
if (chars.length > ++i) {
final char next = chars[i];
if (next == ',' || next == '\\') {
value.append(next);
continue;
}
}
throw new IllegalStateException("Escape character found at invalid position " + i + ". Only characters ',' and '\\' need to be escaped for a value.");
}
case ',': {
// Only add if the key isn't empty
if (key.length() > 0) {
// Add the entry
if (value.length() == 0) {
map.put(key.toString(), null);
} else {
map.put(key.toString(), value.toString());
}
// Clear the key
key.setLength(0);
}
// Clear the value
value.setLength(0);
state = KEY;
continue;
}
default: {
value.append(c);
continue;
}
}
}
default:
// not reachable
throw new IllegalStateException();
}
}
// Add the last entry
if (key.length() > 0) {
// Add the entry
if (value.length() == 0) {
map.put(key.toString(), null);
} else {
map.put(key.toString(), value.toString());
}
}
return Collections.unmodifiableMap(map);
}
/**
* Parses a string of key/value pairs into an {@linkplain EnumMap enum map}.
*
* The key/value pairs are separated by a comma ({@code ,}). The key and value are separated by an equals
* ({@code =}). The key must be a valid {@linkplain Enum#valueOf(Class, String) enum value}. For convenience the
* case of each character will be converted to uppercase and any dashes ({@code -}) will be converted to
* underscores ({@code _}).
*
*
* If a value contains a {@code \} or a {@code ,} it must be escaped by a preceding {@code \}. Example: {@code
* key=part1\,part2,key2=value\\other}.
*
*
*
* If the value for a key is empty there is no trailing {@code =} after a key the value will be {@code null}.
*
*
* @param enumType the enum type
* @param s the string to parse
*
* @return a map of the key value pairs or an empty map if the string is {@code null} or empty
*/
public static > EnumMap stringToEnumMap(final Class enumType, final String s) {
return stringToEnumMap(enumType, s, true);
}
/**
* Parses a string of key/value pairs into an {@linkplain EnumMap enum map}.
*
* The key/value pairs are separated by a comma ({@code ,}). The key and value are separated by an equals
* ({@code =}). The key must be a valid {@linkplain Enum#valueOf(Class, String) enum value}. For convenience any
* dashes ({@code -}) will be converted to underscores ({@code _}). If {@code convertKeyCase} is set to
* {@code true} the case will also be converted to uppercase for each key character.
*
*
* If a value contains a {@code \} or a {@code ,} it must be escaped by a preceding {@code \}. Example: {@code
* key=part1\,part2,key2=value\\other}.
*
*
*
* If the value for a key is empty there is no trailing {@code =} after a key the value will be {@code null}.
*
*
* @param enumType the enum type
* @param s the string to parse
* @param convertKeyCase {@code true} if the each character from the key should be converted to uppercase,
* otherwise {@code false} to keep the case as is
*
* @return a map of the key value pairs or an empty map if the string is {@code null} or empty
*/
@SuppressWarnings("SameParameterValue")
public static > EnumMap stringToEnumMap(final Class enumType, final String s, final boolean convertKeyCase) {
final EnumMap result = new EnumMap<>(enumType);
if (s == null || s.isEmpty()) return result;
final StringBuilder key = new StringBuilder();
final StringBuilder value = new StringBuilder();
final char[] chars = s.toCharArray();
int state = 0;
for (int i = 0; i < chars.length; i++) {
final char c = chars[i];
switch (state) {
case KEY: {
switch (c) {
case '=': {
state = VALUE;
continue;
}
case '-': {
key.append('_');
continue;
}
default: {
if (convertKeyCase) {
key.append(Character.toUpperCase(c));
} else {
key.append(c);
}
continue;
}
}
}
case VALUE: {
switch (c) {
case '\\': {
// Handle escapes
if (chars.length > ++i) {
final char next = chars[i];
if (next == ',' || next == '\\') {
value.append(next);
continue;
}
}
throw new IllegalStateException("Escape character found at invalid position " + i + ". Only characters ',' and '\\' need to be escaped for a value.");
}
case ',': {
// Only add if the key isn't empty
if (key.length() > 0) {
// Add the value
if (value.length() == 0) {
result.put(E.valueOf(enumType, key.toString()), null);
} else {
result.put(E.valueOf(enumType, key.toString()), value.toString());
}
// Clear the key
key.setLength(0);
}
// Clear the value
value.setLength(0);
state = KEY;
continue;
}
default: {
value.append(c);
continue;
}
}
}
default:
// not reachable
throw new IllegalStateException();
}
}
// Add the last entry
if (key.length() > 0) {
// Add the value
if (value.length() == 0) {
result.put(E.valueOf(enumType, key.toString()), null);
} else {
result.put(E.valueOf(enumType, key.toString()), value.toString());
}
}
return result;
}
/**
* Converts a map into a string that can be parsed by {@link #stringToMap(String)}. Note that if this is an
* {@link EnumMap} the {@link #mapToString(EnumMap)} will be used and the key will be the
* {@linkplain Enum#name() enum name}.
*
* @param map the map to convert to a string
* @param the type of the key
*
* @return a string value for that map that can be used for configuration properties
*
* @see #escapeKey(StringBuilder, String)
* @see #escapeValue(StringBuilder, String)
*/
@SuppressWarnings("unchecked")
public static String mapToString(final Map map) {
if (map == null || map.isEmpty()) {
return null;
}
if (map instanceof EnumMap) {
return mapToString((EnumMap) map);
}
final StringBuilder sb = new StringBuilder(map.size() * 32);
final Iterator> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
final Map.Entry entry = iterator.next();
escapeKey(sb, String.valueOf(entry.getKey()));
sb.append('=');
escapeValue(sb, entry.getValue());
if (iterator.hasNext()) {
sb.append(',');
}
}
return sb.toString();
}
/**
* Converts a map into a string that can be parsed by {@link #stringToMap(String)}. The kwy will be the
* {@linkplain Enum#name() enum name}.
*
* @param map the map to convert to a string
* @param the type of the key
*
* @return a string value for that map that can be used for configuration properties
*
* @see #escapeKey(StringBuilder, String)
* @see #escapeValue(StringBuilder, String)
*/
public static > String mapToString(final EnumMap map) {
if (map == null || map.isEmpty()) {
return null;
}
final StringBuilder sb = new StringBuilder(map.size() * 32);
final Iterator> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
final Map.Entry entry = iterator.next();
sb.append(entry.getKey().name());
sb.append('=');
escapeValue(sb, entry.getValue());
if (iterator.hasNext()) {
sb.append(',');
}
}
return sb.toString();
}
/**
* Escapes a maps key value for serialization to a string. If the key contains a {@code \} or an {@code =} it will
* be escaped by a preceding {@code \}. Example: {@code key\=} or {@code \\key}.
*
* @param sb the string builder to append the escaped key to
* @param key the key
*/
public static void escapeKey(final StringBuilder sb, final String key) {
final char[] chars = key.toCharArray();
for (int i = 0; i < chars.length; i++) {
final char c = chars[i];
// Ensure that \ and = are escaped
if (c == '\\') {
final int n = i + 1;
if (n >= chars.length) {
sb.append('\\').append('\\');
} else {
final char next = chars[n];
if (next == '\\' || next == '=') {
// Nothing to do, already properly escaped
sb.append(c);
sb.append(next);
i = n;
} else {
// Now we need to escape the \
sb.append('\\').append('\\');
}
}
} else if (c == '=') {
sb.append('\\').append(c);
} else {
sb.append(c);
}
}
}
/**
* Escapes a maps value for serialization to a string. If a value contains a {@code \} or a {@code ,} it will be
* escaped by a preceding {@code \}. Example: {@code part1\,part2} or {@code value\\other}.
*
* @param sb the string builder to append the escaped value to
* @param value the value
*/
public static void escapeValue(final StringBuilder sb, final String value) {
if (value != null) {
final char[] chars = value.toCharArray();
for (int i = 0; i < chars.length; i++) {
final char c = chars[i];
// Ensure that \ and , are escaped
if (c == '\\') {
final int n = i + 1;
if (n >= chars.length) {
sb.append('\\').append('\\');
} else {
final char next = chars[n];
if (next == '\\' || next == ',') {
// Nothing to do, already properly escaped
sb.append(c);
sb.append(next);
i = n;
} else {
// Now we need to escape the \
sb.append('\\').append('\\');
}
}
} else if (c == ',') {
sb.append('\\').append(c);
} else {
sb.append(c);
}
}
}
}
}