/* Copyright 2013 Jonatan Jönsson
*
* 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 se.softhouse.common.strings;
import static com.google.common.base.Preconditions.checkNotNull;
import static se.softhouse.common.strings.StringsUtil.NEWLINE;
import java.io.File;
import java.text.NumberFormat;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
/**
* Gives you static access to implementations of the {@link Describer} interface.
*/
@Immutable
public final class Describers
{
private Describers()
{
}
private static final String NULL = "null";
/**
* Always describes any value of type {@code T} with the given {@code constant}. For instance,
* if you have implemented a time related parser and don't want different times in the
* usage depending on when the usage is printed you could pass in "The current time", then this
* describer would describe any time object with "The current time".
*/
@Nonnull
@CheckReturnValue
public static <T> Describer<T> withConstantString(final String constant)
{
return new ConstantStringDescriber<T>(constant);
}
private static final class ConstantStringDescriber<T> implements Describer<T>
{
private final String constant;
private ConstantStringDescriber(final String constant)
{
this.constant = checkNotNull(constant);
}
@Override
public String describe(T value, Locale inLocale)
{
return constant;
}
}
/**
* Calls {@link String#valueOf(Object)} for input values. As this goes
* against the very purpose of the {@link Describer} interface it may seem
* odd but this {@link Describer} works really well as a null object, it can
* also act as a functor for calling toString.
*/
@Nonnull
@CheckReturnValue
public static <T> Describer<T> toStringDescriber()
{
@SuppressWarnings("unchecked")
Describer<T> toStringDescriber = (Describer<T>) ToStringDescriber.INSTANCE;
return toStringDescriber;
}
private static final class ToStringDescriber implements Describer<Object>
{
private static final Describer<Object> INSTANCE = new ToStringDescriber();
private ToStringDescriber()
{
}
@Override
public String describe(Object value, Locale inLocale)
{
return String.valueOf(value);
}
}
/**
* Describes {@link Character}s by printing explanations for unprintable characters.
*/
@Nonnull
@CheckReturnValue
public static Describer<Character> characterDescriber()
{
return CharDescriber.INSTANCE;
}
private static final class CharDescriber implements Describer<Character>
{
private static final Describer<Character> INSTANCE = new CharDescriber();
@Override
public String describe(Character value, Locale inLocale)
{
if(value == null)
return NULL;
// TODO(jontejj): describe more characters? All ASCII characters perhaps?
// Character.isISOControl...
return ((int) value == 0) ? "the Null character" : value.toString();
}
}
/**
* Describes {@link File}s with {@link File#getAbsolutePath()} instead of {@link File#getPath()}
* as {@link File#toString()} does.
*/
@Nonnull
@CheckReturnValue
public static Describer<File> fileDescriber()
{
return FileDescriber.INSTANCE;
}
private static final class FileDescriber implements Describer<File>
{
private static final Describer<File> INSTANCE = new FileDescriber();
@Override
public String describe(File file, Locale inLocale)
{
if(file == null)
return NULL;
return file.getAbsolutePath();
}
}
/**
* Describes a boolean as enabled when {@code true} and disabled when {@code false}
*/
@Nonnull
@CheckReturnValue
public static Describer<Boolean> booleanAsEnabledDisabled()
{
return BooleanDescribers.ENABLED_DISABLED;
}
/**
* Describes a boolean as on when {@code true} and off when {@code false}
*/
@Nonnull
@CheckReturnValue
public static Describer<Boolean> booleanAsOnOff()
{
return BooleanDescribers.ON_OFF;
}
@VisibleForTesting
enum BooleanDescribers implements Describer<Boolean>
{
ENABLED_DISABLED
{
@Override
public String describe(Boolean value, Locale inLocale)
{
return value ? "enabled" : "disabled";
}
},
ON_OFF
{
@Override
public String describe(Boolean value, Locale inLocale)
{
return value ? "on" : "off";
}
};
}
/**
* Describes {@link Number}s in a {@link Locale} sensitive manner using {@link NumberFormat}.
*/
public static Describer<Number> numberDescriber()
{
return NumberDescriber.INSTANCE;
}
private static final class NumberDescriber implements Describer<Number>
{
private static final Describer<Number> INSTANCE = new NumberDescriber();
@Override
public String describe(Number number, Locale locale)
{
if(number == null)
return NULL;
return NumberFormat.getInstance(locale).format(number);
}
@Override
public String toString()
{
return "NumberDescriber";
}
}
/**
* Describes what a key=value in a {@link Map} means by fetching a
* description from {@code descriptions} for each key in a given map. So {@code descriptions}
* need to have values (descriptions) for all keys in any given map,
* otherwise a {@link NullPointerException} is thrown. For example:
*
* <pre class="prettyprint">
* <code class="language-java">
* Map<String, Integer> defaults = newLinkedHashMap();
* defaults.put("population", 42);
* defaults.put("hello", 1);
*
* Map<String, String> descriptions = newLinkedHashMap();
* descriptions.put("population", "The number of citizens in the world");
* descriptions.put("hello", "The number of times to say hello");
* Describer<Map<String, Integer>> d = mapDescriber(descriptions);
*
* String describedMap = d.describe(defaults);
* </code>
* </pre>
*
* would return:
*
* <pre>
* <code>
* population=42
* The number of citizens in the world
* hello=1
* The number of times to say hello
* </code>
* </pre>
*
* You could even implement a
* {@code addProperty(String key, int defaultValue, String description)} method to enforce the
* use of descriptions at compile-time.
*/
@Nonnull
@CheckReturnValue
public static <K, V> Describer<Map<K, V>> mapDescriber(Map<K, String> descriptions)
{
return new MapDescription<K, V>(ImmutableMap.copyOf(descriptions), Describers.<K>toStringDescriber());
}
/**
* Works like {@link #mapDescriber(Map)} but it describes keys in any given {@link Map} with
* {@code keyDescriber} instead of with {@link #toString()}
*
* @param descriptions a map with strings describing what each key in it means
* @param keyDescriber {@link Describer} used to transform keys into {@link String}s with
* @return a {@link Describer} that can describe a map of the type {@code Map<K, V>} and where
* each key must have a corresponding description in {@code descriptions}
*/
public static <K, V> Describer<Map<K, V>> mapDescriber(Map<K, String> descriptions, Describer<K> keyDescriber)
{
return new MapDescription<K, V>(ImmutableMap.copyOf(descriptions), checkNotNull(keyDescriber));
}
private static final class MapDescription<K, V> implements Describer<Map<K, V>>
{
private final Map<K, String> descriptions;
private final Describer<K> keyDescriber;
private MapDescription(Map<K, String> descriptions, Describer<K> keyDescriber)
{
this.descriptions = descriptions;
this.keyDescriber = keyDescriber;
}
@Override
public String describe(Map<K, V> values, Locale inLocale)
{
StringBuilder result = new StringBuilder();
for(Entry<K, V> entry : values.entrySet())
{
K key = entry.getKey();
result.append(keyDescriber.describe(key, inLocale));
result.append("=");
result.append(entry.getValue());
String descriptionForEntry = descriptions.get(key);
checkNotNull(descriptionForEntry, "Undescribed key: %s", key);
result.append(NEWLINE).append(" ").append(descriptionForEntry).append(NEWLINE);
}
return result.toString();
}
}
/**
* Describes key values in a {@link Map}. Keys are described with
* {@link Describers#toStringDescriber()} and values with {@code valueDescriber}. "=" is used as
* the separator between key and value. {@link StringsUtil#NEWLINE} separates entries.
*/
@CheckReturnValue
@Nonnull
public static <K, V> Describer<Map<K, V>> mapDescriber(Describer<V> valueDescriber)
{
return new MapDescriber<K, V>(Describers.<K>toStringDescriber(), valueDescriber, "=");
}
/**
* Describes key values in a {@link Map}. Keys are described with
* {@link Describers#toStringDescriber()} and values with {@code valueDescriber}.
* {@code valueSeparator} is used as the separator between key and value.
* {@link StringsUtil#NEWLINE} separates entries.
*/
@CheckReturnValue
@Nonnull
public static <K, V> Describer<Map<K, V>> mapDescriber(Describer<V> valueDescriber, String valueSeparator)
{
return new MapDescriber<K, V>(Describers.<K>toStringDescriber(), valueDescriber, valueSeparator);
}
/**
* Describes key values in a {@link Map}. Keys are described with {@code keyDescriber} and
* values with {@code valueDescriber}.
* "=" is used as the separator between key and value. {@link StringsUtil#NEWLINE} separates
* entries.
*/
@CheckReturnValue
@Nonnull
public static <K, V> Describer<Map<K, V>> mapDescriber(Describer<K> keyDescriber, Describer<V> valueDescriber)
{
return new MapDescriber<K, V>(keyDescriber, valueDescriber, "=");
}
/**
* Describes key values in a {@link Map}. Keys are described with {@code keyDescriber} and
* values with {@code valueDescriber}. {@code valueSeparator} is used as the separator between
* key and value. {@link StringsUtil#NEWLINE} separates entries.
*/
@CheckReturnValue
@Nonnull
public static <K, V> Describer<Map<K, V>> mapDescriber(Describer<K> keyDescriber, Describer<V> valueDescriber, String valueSeparator)
{
return new MapDescriber<K, V>(keyDescriber, valueDescriber, valueSeparator);
}
private static final class MapDescriber<K, V> implements Describer<Map<K, V>>
{
private final Describer<V> valueDescriber;
private final Describer<K> keyDescriber;
private final String valueSeparator;
private MapDescriber(Describer<K> keyDescriber, Describer<V> valueDescriber, String valueSeparator)
{
this.keyDescriber = checkNotNull(keyDescriber);
this.valueDescriber = checkNotNull(valueDescriber);
this.valueSeparator = checkNotNull(valueSeparator);
}
@Override
public String describe(Map<K, V> values, Locale inLocale)
{
if(values == null)
return NULL;
Iterator<Entry<K, V>> iterator = values.entrySet().iterator();
if(!iterator.hasNext())
return "Empty map";
StringBuilder firstKeyValue = new StringBuilder();
describeEntry(iterator.next(), inLocale, firstKeyValue);
StringBuilder result = StringBuilders.withExpectedSize(firstKeyValue.length() * values.size());
result.append(firstKeyValue);
while(iterator.hasNext())
{
result.append(NEWLINE);
describeEntry(iterator.next(), inLocale, result);
}
return result.toString();
}
private void describeEntry(Entry<K, V> entry, Locale inLocale, StringBuilder output)
{
output.append(keyDescriber.describe(entry.getKey(), inLocale));
output.append(valueSeparator);
output.append(valueDescriber.describe(entry.getValue(), inLocale));
}
}
/**
* <pre>
* Exposes a {@link Describer} as a Guava {@link Function}.
* <b>Note:</b>This method may be removed in the future if Guava is removed as a dependency.
*
* @param describer the describer to convert to a {@link Function}
* @return a {@link Function} that uses {@link Describer#describe(Object, Locale) describe(input, Locale.getDefault()}.
* </pre>
*/
@Beta
@Nonnull
@CheckReturnValue
public static <T> Function<? super T, String> asFunction(final Describer<T> describer)
{
checkNotNull(describer);
return new Function<T, String>(){
@Override
public String apply(@Nonnull T input)
{
return describer.describe(input, Locale.getDefault());
}
};
}
/**
* Like {@link #asFunction(Describer)} but with {@code locale} instead of
* {@link Locale#getDefault()}.
*/
@Beta
@Nonnull
@CheckReturnValue
public static <T> Function<? super T, String> asFunction(final Describer<T> describer, final Locale locale)
{
checkNotNull(describer);
checkNotNull(locale);
return new Function<T, String>(){
@Override
public String apply(@Nonnull T input)
{
return describer.describe(input, locale);
}
};
}
/**
* <pre>
* Exposes a {@link Function} as a {@link Describer}.
* <b>Note:</b>This method may be removed in the future if Guava is removed as a dependency.
*
* @param describerFunction a function that can convert {@code T} values into {@link String}s
* @return a {@link Describer} that applies {@link Function#apply(Object)} to {@link Describer#describe(Object, Locale)} input values.
* </pre>
*/
@Beta
@Nonnull
@CheckReturnValue
public static <T> Describer<T> usingFunction(final Function<T, String> describerFunction)
{
checkNotNull(describerFunction);
return new Describer<T>(){
@Override
public String describe(T value, Locale inLocale)
{
return describerFunction.apply(value);
}
};
}
/**
* Describes values in lists with {@code valueDescriber} and separates
* elements with ", ". Lists with {@link List#size()} = zero is described
* with the string "Empty List".
*/
@Nonnull
@CheckReturnValue
public static <T> Describer<List<? extends T>> listDescriber(Describer<T> valueDescriber)
{
return new ListDescriber<T>(valueDescriber, ", ");
}
/**
* Describes values in lists with {@code valueDescriber} and separates
* values with {@code valueSeperator}. Lists with {@link List#size()} = zero
* is described with the string "Empty List".
*/
@Nonnull
@CheckReturnValue
public static <T> Describer<List<? extends T>> listDescriber(Describer<T> valueDescriber, String valueSeparator)
{
return new ListDescriber<T>(valueDescriber, valueSeparator);
}
private static final class ListDescriber<T> implements Describer<List<? extends T>>
{
private final Describer<T> valueDescriber;
private final String valueSeparator;
ListDescriber(Describer<T> valueDescriber, String valueSeparator)
{
this.valueDescriber = checkNotNull(valueDescriber);
this.valueSeparator = checkNotNull(valueSeparator);
}
@Override
public String describe(List<? extends T> value, Locale inLocale)
{
if(value == null)
return NULL;
if(value.isEmpty())
return "Empty list";
Iterator<? extends T> values = value.iterator();
String firstValue = valueDescriber.describe(values.next(), inLocale);
StringBuilder sb = StringBuilders.withExpectedSize(value.size() * firstValue.length());
sb.append(firstValue);
while(values.hasNext())
{
sb.append(valueSeparator).append(valueDescriber.describe(values.next(), inLocale));
}
return sb.toString();
}
}
}