All Downloads are FREE. Search and download functionalities are using the official Maven repository.

se.softhouse.common.strings.Describers Maven / Gradle / Ivy

/* 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 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.
 */
public final class Describers
{
	private Describers()
	{
	}

	/**
	 * 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  Describer withConstantString(final String constant)
	{
		checkNotNull(constant);
		return new ConstantStringDescriber(constant);
	}

	private static final class ConstantStringDescriber implements Describer
	{
		private final String constant;

		private ConstantStringDescriber(final String constant)
		{
			this.constant = 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  Describer toStringDescriber()
	{
		@SuppressWarnings("unchecked")
		Describer toStringDescriber = (Describer) ToStringDescriber.INSTANCE;
		return toStringDescriber;
	}

	private static final class ToStringDescriber implements Describer
	{
		private static final Describer 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 characterDescriber()
	{
		return CharDescriber.INSTANCE;
	}

	private static final class CharDescriber implements Describer
	{
		private static final Describer INSTANCE = new CharDescriber();

		@Override
		public String describe(Character value, Locale inLocale)
		{
			if(value == null)
				return "null";
			// TODO: 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 fileDescriber()
	{
		return FileDescriber.INSTANCE;
	}

	private static final class FileDescriber implements Describer
	{
		private static final Describer 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 booleanAsEnabledDisabled()
	{
		return BooleanDescribers.ENABLED_DISABLED;
	}

	/**
	 * Describes a boolean as on when {@code true} and off when {@code false}
	 */
	@Nonnull
	@CheckReturnValue
	public static Describer booleanAsOnOff()
	{
		return BooleanDescribers.ON_OFF;
	}

	@VisibleForTesting
	enum BooleanDescribers implements Describer
	{
		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 numberDescriber()
	{
		return NumberDescriber.INSTANCE;
	}

	private static final class NumberDescriber implements Describer
	{
		private static final Describer 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:
	 * 
	 * 
	 * 
	 * 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);
	 * 
	 * 
* * would return: * *
	 * 
	 * population=42
	 * 	The number of citizens in the world
	 * hello=1
	 * 	The number of times to say hello
	 * 
	 * 
* * 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 Describer> mapDescriber(Map descriptions) { return new MapDescription(ImmutableMap.copyOf(descriptions), Describers.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} and where * each key must have a corresponding description in {@code descriptions} */ public static Describer> mapDescriber(Map descriptions, Describer keyDescriber) { return new MapDescription(ImmutableMap.copyOf(descriptions), checkNotNull(keyDescriber)); } private static final class MapDescription implements Describer> { private final Map descriptions; private final Describer keyDescriber; private MapDescription(Map descriptions, Describer keyDescriber) { this.descriptions = descriptions; this.keyDescriber = keyDescriber; } @Override public String describe(Map values, Locale inLocale) { StringBuilder result = new StringBuilder(); for(Entry 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 Describer> mapDescriber(Describer valueDescriber) { return new MapDescriber(Describers.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 Describer> mapDescriber(Describer valueDescriber, String valueSeparator) { return new MapDescriber(Describers.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 Describer> mapDescriber(Describer keyDescriber, Describer valueDescriber) { return new MapDescriber(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 Describer> mapDescriber(Describer keyDescriber, Describer valueDescriber, String valueSeparator) { return new MapDescriber(keyDescriber, valueDescriber, valueSeparator); } private static final class MapDescriber implements Describer> { private final Describer valueDescriber; private final Describer keyDescriber; private final String valueSeparator; private MapDescriber(Describer keyDescriber, Describer valueDescriber, String valueSeparator) { this.keyDescriber = checkNotNull(keyDescriber); this.valueDescriber = checkNotNull(valueDescriber); this.valueSeparator = checkNotNull(valueSeparator); } @Override public String describe(Map values, Locale inLocale) { if(values == null) return "null"; Iterator> iterator = values.entrySet().iterator(); if(!iterator.hasNext()) return "Empty map"; StringBuilder firstKeyValue = new StringBuilder(); describeEntry(iterator.next(), inLocale, firstKeyValue); // TODO: what if it's expensive to call size()? 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 entry, Locale inLocale, StringBuilder output) { output.append(keyDescriber.describe(entry.getKey(), inLocale)); output.append(valueSeparator); output.append(valueDescriber.describe(entry.getValue(), inLocale)); } } /** *
	 * Exposes a {@link Describer} as a Guava {@link Function}.
	 * Note: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 applies {@link Describer#describe(Object, Locale)} to input values.
	 * 
*/ @Beta @Nonnull @CheckReturnValue public static Function asFunction(final Describer describer) { checkNotNull(describer); return new Function(){ @Override public String apply(@Nonnull T input) { // TODO: document locale return describer.describe(input, Locale.getDefault()); } }; } /** *
	 * Exposes a {@link Function} as a {@link Describer}.
	 * Note: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.
	 * 
*/ @Beta @Nonnull @CheckReturnValue public static Describer usingFunction(final Function describerFunction) { checkNotNull(describerFunction); return new Describer(){ @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 Describer> listDescriber(Describer valueDescriber) { checkNotNull(valueDescriber); return new ListDescriber(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 Describer> listDescriber(Describer valueDescriber, String valueSeparator) { checkNotNull(valueDescriber); checkNotNull(valueSeparator); return new ListDescriber(valueDescriber, valueSeparator); } private static final class ListDescriber implements Describer> { private final Describer valueDescriber; private final String valueSeparator; ListDescriber(Describer valueDescriber, String valueSeparator) { this.valueDescriber = valueDescriber; this.valueSeparator = valueSeparator; } @Override public String describe(List value, Locale inLocale) { if(value == null) return "null"; if(value.isEmpty()) return "Empty list"; Iterator 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(); } } }