/* * JBoss, Home of Professional Open Source. * * Copyright 2018 Red Hat, Inc., and individual contributors * as indicated by the @author tags. * * 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.ext.util; 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 <a href="mailto:[email protected]">James R. Perkins</a> */ @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. * <p> * The key/value pairs are separated by a comma ({@code ,}). The key and value are separated by an equals * ({@code =}). * </p> * <p> * If a key contains a {@code \} or an {@code =} it must be escaped by a preceding {@code \}. Example: {@code * key\==value,\\key=value}. * </p> * <p> * 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}. * </p> * * <p> * If the value for a key is empty there is no trailing {@code =} after a key the will be {@code null}. * </p> * * @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<String, String> stringToMap(final String s) { if (s == null || s.isEmpty()) return Collections.emptyMap(); final Map<String, String> 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}. * <p> * 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 _}). * </p> * <p> * 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}. * </p> * * <p> * If the value for a key is empty there is no trailing {@code =} after a key the value will be {@code null}. * </p> * * @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 <E extends Enum<E>> EnumMap<E, String> stringToEnumMap(final Class<E> enumType, final String s) { return stringToEnumMap(enumType, s, true); } /** * Parses a string of key/value pairs into an {@linkplain EnumMap enum map}. * <p> * 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. * </p> * <p> * 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}. * </p> * * <p> * If the value for a key is empty there is no trailing {@code =} after a key the value will be {@code null}. * </p> * * @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 <E extends Enum<E>> EnumMap<E, String> stringToEnumMap(final Class<E> enumType, final String s, final boolean convertKeyCase) { final EnumMap<E, String> 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 <K> 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 <K> String mapToString(final Map<K, String> 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<Map.Entry<K, String>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { final Map.Entry<K, String> 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 <K> 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 <K extends Enum<K>> String mapToString(final EnumMap<K, String> map) { if (map == null || map.isEmpty()) { return null; } final StringBuilder sb = new StringBuilder(map.size() * 32); final Iterator<Map.Entry<K, String>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { final Map.Entry<K, String> 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); } } } } }