/* * sulky-modules - several general-purpose modules. * Copyright (C) 2007-2015 Joern Huxhorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright 2007-2015 Joern Huxhorn * * 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 de.huxhorn.sulky.formatting; import java.time.DateTimeException; import java.time.Instant; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.IdentityHashMap; import java.util.Map; import java.util.Objects; public final class SafeString { public enum StringWrapping { /** * Strings are not wrapped at all. */ NONE, /** * Only Strings contained in Collection, Map or array are wrapped. */ CONTAINED, /** * All Strings are wrapped. */ ALL } public enum StringStyle { /** * String is rendered as "String". */ JAVA('"'), /** * String is rendered as 'String'. */ GROOVY('\''); private char quoteChar; StringStyle(char quoteChar) { this.quoteChar=quoteChar; } public char getQuoteChar() { return quoteChar; } } public enum MapStyle { /** * Map is rendered as {key=value, key2=value2}. */ JAVA('{', '}', '='), /** * Map is rendered as [key:value, key2:value2]. */ GROOVY('[', ']', ':'); private char prefix; private char suffix; private char keyValueSeparator; MapStyle(char prefix, char suffix, char keyValueSeparator) { this.prefix = prefix; this.suffix = suffix; this.keyValueSeparator = keyValueSeparator; } public char getPrefix() { return prefix; } public char getSuffix() { return suffix; } public char getKeyValueSeparator() { return keyValueSeparator; } } public static final String ERROR_PREFIX = "[!!!"; public static final String ERROR_SEPARATOR = "=>"; public static final char ERROR_MSG_SEPARATOR = ':'; public static final String ERROR_SUFFIX = "!!!]"; public static final String RECURSION_PREFIX = "[..."; public static final String RECURSION_SUFFIX = "...]"; private static final char CONTAINER_PREFIX = '['; private static final String CONTAINER_SEPARATOR = ", "; private static final char CONTAINER_SUFFIX = ']'; private static final char IDENTITY_SEPARATOR = '@'; private static final String NULL_VALUE = "null"; private static final DateTimeFormatter ISO_DATE_TIME_FORMATTER = new DateTimeFormatterBuilder() .parseCaseInsensitive() .append(DateTimeFormatter.ISO_LOCAL_DATE) .appendLiteral('T') .appendValue(ChronoField.HOUR_OF_DAY, 2) .appendLiteral(':') .appendValue(ChronoField.MINUTE_OF_HOUR, 2) .optionalStart() .appendLiteral(':') .appendValue(ChronoField.SECOND_OF_MINUTE, 2) .optionalStart() .appendFraction(ChronoField.MILLI_OF_SECOND, 3, 3, true) .appendZoneId() .toFormatter() .withZone(ZoneOffset.UTC); private static final String BYTE_PREFIX = "0x"; private static final String[] BYTE_STRINGS; static { // for the sake of coverage new SafeString(); final char[] hexChars = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', }; BYTE_STRINGS = new String[256]; for(int i=0;i<256;i++) { @SuppressWarnings("StringBufferReplaceableByString") StringBuilder sb=new StringBuilder(2); sb.append(hexChars[i >>> 4]); sb.append(hexChars[i & 0xf]); BYTE_STRINGS[i]=sb.toString(); } } private SafeString() {} public static String toString(Object o) { return toString(o, StringWrapping.NONE, StringStyle.JAVA, MapStyle.JAVA); } public static String toString(Object o, StringWrapping stringWrapping, StringStyle stringStyle, MapStyle mapStyle) { Objects.requireNonNull(stringWrapping, "stringWrapping must not be null!"); Objects.requireNonNull(stringStyle, "stringStyle must not be null!"); Objects.requireNonNull(mapStyle, "mapStyle must not be null!"); if(o == null) { return NULL_VALUE; } if(o instanceof String) { String string = (String) o; if(stringWrapping != StringWrapping.ALL) { return string; } char quoteChar=stringStyle.getQuoteChar(); return ""+quoteChar+string+quoteChar; } StringBuilder stringBuilder = new StringBuilder(); append(o, stringBuilder, stringWrapping, stringStyle, mapStyle); return stringBuilder.toString(); } public static void append(Object o, StringBuilder stringBuilder) { append(o, stringBuilder, StringWrapping.NONE, StringStyle.JAVA, MapStyle.JAVA); } public static void append(Object o, StringBuilder stringBuilder, StringWrapping stringWrapping, StringStyle stringStyle, MapStyle mapStyle) { Objects.requireNonNull(stringBuilder, "stringBuilder must not be null!"); Objects.requireNonNull(stringWrapping, "stringWrapping must not be null!"); Objects.requireNonNull(stringStyle, "stringStyle must not be null!"); Objects.requireNonNull(mapStyle, "mapStyle must not be null!"); if(o == null) { stringBuilder.append(NULL_VALUE); return; } if(o instanceof String) { String string = (String) o; if(stringWrapping != StringWrapping.ALL) { stringBuilder.append(string); return; } char quoteChar=stringStyle.getQuoteChar(); stringBuilder.append(quoteChar).append(string).append(quoteChar); return; } IdentityHashMap<Object, Object> dejaVu = new IdentityHashMap<>(); // that's actually a neat name ;) if(stringWrapping == StringWrapping.NONE) { stringStyle = null; } recursiveAppend(o, stringBuilder, stringStyle, mapStyle, dejaVu); } /** * This method returns the same as if {@code Object.toString()} would not have been * overridden in obj. * * <p>Note that this isn't 100% secure as collisions can always happen with hash codes. * * <p>Copied from {@code Object.hashCode()}: * * <blockquote> * As much as is reasonably practical, the hashCode method defined by * class {@code Object} does return distinct integers for distinct * objects. (This is typically implemented by converting the internal * address of the object into an integer, but this implementation * technique is not required by the * Java™ programming language.) * </blockquote> * * @param obj the Object that is to be converted into an identity string. * @return the identity string as also defined in Object.toString() */ public static String identityToString(Object obj) { if(obj == null) { return null; } return obj.getClass().getName() + IDENTITY_SEPARATOR + Integer.toHexString(System.identityHashCode(obj)); } /** * This method performs a deep toString of the given Object. * * <p>Primitive arrays are converted using their respective Arrays.toString methods while * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could * contain themselves. * * <p>dejaVu is used in case of those container types to prevent an endless recursion. * * <p>It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a behavior. * They only check if the container is directly contained in itself, but not if a contained container contains the * original one. Because of that, Arrays.toString(Object[]) isn't safe either. * * <p>Confusing? Just read the last paragraph again and check the respective toString() implementation. * * <p>This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o) * would produce a relatively hard-to-debug StackOverflowError. * * @param o the Object to convert into a String * @param stringBuilder the StringBuilder that o will be appended to * @param stringStyle the String quoting style. * @param mapStyle the Map printing style. * @param dejaVu used to detect recursions. */ private static void recursiveAppend(Object o, StringBuilder stringBuilder, StringStyle stringStyle, MapStyle mapStyle, IdentityHashMap<Object, Object> dejaVu) { // o will never be null or String at this point since those cases are already handled by shortcuts. Class oClass = o.getClass(); if(oClass.isArray()) { if(oClass == byte[].class) { stringBuilder.append(CONTAINER_PREFIX); byte[] array = (byte[]) o; boolean first = true; for(byte current : array) { if (first) { first = false; } else { stringBuilder.append(CONTAINER_SEPARATOR); } appendByte(current, stringBuilder); } stringBuilder.append(CONTAINER_SUFFIX); return; } if(oClass == Byte[].class) { stringBuilder.append(CONTAINER_PREFIX); Byte[] array = (Byte[]) o; boolean first = true; for(Byte current : array) { if (first) { first = false; } else { stringBuilder.append(CONTAINER_SEPARATOR); } appendByte(current, stringBuilder); } stringBuilder.append(CONTAINER_SUFFIX); return; } if(oClass == short[].class) { stringBuilder.append(Arrays.toString((short[]) o)); return; } if(oClass == int[].class) { stringBuilder.append(Arrays.toString((int[]) o)); return; } if(oClass == long[].class) { stringBuilder.append(Arrays.toString((long[]) o)); return; } if(oClass == float[].class) { stringBuilder.append(Arrays.toString((float[]) o)); return; } if(oClass == double[].class) { stringBuilder.append(Arrays.toString((double[]) o)); return; } if(oClass == boolean[].class) { stringBuilder.append(Arrays.toString((boolean[]) o)); return; } if(oClass == char[].class) { stringBuilder.append(Arrays.toString((char[]) o)); return; } // special handling of container Object[] if(dejaVu.containsKey(o)) { stringBuilder.append(RECURSION_PREFIX).append(identityToString(o)).append(RECURSION_SUFFIX); return; } dejaVu.put(o, null); Object[] oArray = (Object[]) o; stringBuilder.append(CONTAINER_PREFIX); boolean first = true; for(Object current : oArray) { if(first) { first = false; } else { stringBuilder.append(CONTAINER_SEPARATOR); } if(!handleSimpleContainerValue(current, stringBuilder, stringStyle)) { recursiveAppend(current, stringBuilder, stringStyle, mapStyle, new IdentityHashMap<>(dejaVu)); } } stringBuilder.append(CONTAINER_SUFFIX); return; } if(o instanceof Byte) { appendByte((Byte)o, stringBuilder); return; } if(o instanceof Map) { // special handling of container Map if(dejaVu.containsKey(o)) { stringBuilder.append(RECURSION_PREFIX).append(identityToString(o)).append(RECURSION_SUFFIX); return; } dejaVu.put(o, null); Map<?, ?> oMap = (Map<?, ?>) o; stringBuilder.append(mapStyle.getPrefix()); boolean first = true; for(Map.Entry<?, ?> current : oMap.entrySet()) { if(first) { first = false; } else { stringBuilder.append(CONTAINER_SEPARATOR); } Object key = current.getKey(); if(!handleSimpleContainerValue(key, stringBuilder, stringStyle)) { recursiveAppend(key, stringBuilder, stringStyle, mapStyle, new IdentityHashMap<>(dejaVu)); } stringBuilder.append(mapStyle.getKeyValueSeparator()); Object value = current.getValue(); if(!handleSimpleContainerValue(value, stringBuilder, stringStyle)) { recursiveAppend(value, stringBuilder, stringStyle, mapStyle, new IdentityHashMap<>(dejaVu)); } } stringBuilder.append(mapStyle.getSuffix()); return; } if(o instanceof Collection) { // special handling of container Collection if(dejaVu.containsKey(o)) { stringBuilder.append(RECURSION_PREFIX).append(identityToString(o)).append(RECURSION_SUFFIX); return; } dejaVu.put(o, null); Collection<?> oCol = (Collection<?>) o; stringBuilder.append(CONTAINER_PREFIX); boolean first = true; for(Object current : oCol) { if(first) { first = false; } else { stringBuilder.append(CONTAINER_SEPARATOR); } if(!handleSimpleContainerValue(current, stringBuilder, stringStyle)) { recursiveAppend(current, stringBuilder, stringStyle, mapStyle, new IdentityHashMap<>(dejaVu)); } } stringBuilder.append(CONTAINER_SUFFIX); return; } if(o instanceof Date) { ISO_DATE_TIME_FORMATTER.formatTo(Instant.ofEpochMilli(((Date)o).getTime()), stringBuilder); return; } if(o instanceof TemporalAccessor) { try { ISO_DATE_TIME_FORMATTER.formatTo((TemporalAccessor) o, stringBuilder); return; } catch(DateTimeException ignore) { // this is not a bug. fall through to simple Object handling. } } // it's just some other Object, we can only use toString(). try { stringBuilder.append(o.toString()); } catch(Throwable t) { stringBuilder.append(ERROR_PREFIX); stringBuilder.append(identityToString(o)); stringBuilder.append(ERROR_SEPARATOR); String msg = t.getMessage(); String className = t.getClass().getName(); stringBuilder.append(className); if(msg != null && !className.equals(msg)) { stringBuilder.append(ERROR_MSG_SEPARATOR); stringBuilder.append(msg); } stringBuilder.append(ERROR_SUFFIX); } } private static boolean handleSimpleContainerValue(Object current, StringBuilder str, StringStyle stringStyle) { if(current == null) { str.append(NULL_VALUE); return true; } if(current instanceof String) { String string = (String) current; if(stringStyle == null) { str.append(string); } else { char quoteChar=stringStyle.getQuoteChar(); str.append(quoteChar).append(string).append(quoteChar); } return true; } // not calling recursiveAppend here to preserve stack space. return false; } private static void appendByte(int byteIndex, StringBuilder stringBuilder) { stringBuilder.append(BYTE_PREFIX); stringBuilder.append(BYTE_STRINGS[0x000000FF & byteIndex]); } }