blob: ce37373ae2bdb77aa3918a20ca368b12f977c0d7 [file] [log] [blame]
/*
* Copyright (c) 2011 Google, 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 com.google.common.truth;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.SetMultimap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Utility methods used in {@code Subject<T>} implementors.
*
* @author Christian Gruber
* @author Jens Nyman
*/
final class SubjectUtils {
private SubjectUtils() {}
static final String HUMAN_UNDERSTANDABLE_EMPTY_STRING = "\"\" (empty String)";
static <T> List<T> accumulate(T first, T second, T... rest) {
// rest should never be deliberately null, so assume that the caller passed null
// in the third position but intended it to be the third element in the array of values.
// Javac makes the opposite inference, so handle that here.
List<T> items = new ArrayList<T>(2 + ((rest == null) ? 1 : rest.length));
items.add(first);
items.add(second);
if (rest == null) {
items.add(null);
} else {
items.addAll(Arrays.asList(rest));
}
return items;
}
static <T> int countOf(T t, Iterable<T> items) {
int count = 0;
for (T item : items) {
if (t == null ? (item == null) : t.equals(item)) {
count++;
}
}
return count;
}
static <T> List<Object> countDuplicates(Iterable<T> items) {
// We use a List to de-dupe instead of a Set in case the elements don't have a proper
// .hashCode() method (e.g., MessageSet from old versions of protobuf).
List<T> itemSet = new ArrayList<T>();
for (T item : items) {
if (!itemSet.contains(item)) {
itemSet.add(item);
}
}
Object[] params = new Object[itemSet.size()];
int n = 0;
for (T item : itemSet) {
int count = countOf(item, items);
params[n++] = (count > 1) ? item + " [" + count + " copies]" : item;
}
return Arrays.asList(params);
}
/**
* Makes a String representation of {@code items} with collapsed duplicates and additional class
* info.
*
* <p>Example: {@code countDuplicatesAndAddTypeInfo([1, 2, 2, 3]) == "[1, 2 [3 copies]]
* (java.lang.Integer)"} and {@code countDuplicatesAndAddTypeInfo([1, 2L]) == "[1
* (java.lang.Integer), 2 (java.lang.Long)]"}.
*/
static String countDuplicatesAndAddTypeInfo(Iterable<?> itemsIterable) {
Collection<?> items = iterableToCollection(itemsIterable);
Optional<String> homogeneousTypeName = getHomogeneousTypeName(items);
return homogeneousTypeName.isPresent()
? StringUtil.format("%s (%s)", countDuplicates(items), homogeneousTypeName.get())
: countDuplicates(addTypeInfoToEveryItem(items)).toString();
}
/**
* Makes a String representation of {@code items} with additional class info.
*
* <p>Example: {@code iterableToStringWithTypeInfo([1, 2]) == "[1, 2] (java.lang.Integer)"} and
* {@code iterableToStringWithTypeInfo([1, 2L]) == "[1 (java.lang.Integer), 2 (java.lang.Long)]"}.
*/
static String iterableToStringWithTypeInfo(Iterable<?> itemsIterable) {
Collection<?> items = iterableToCollection(itemsIterable);
Optional<String> homogeneousTypeName = getHomogeneousTypeName(items);
if (homogeneousTypeName.isPresent()) {
return StringUtil.format("%s (%s)", items, homogeneousTypeName.get());
} else {
return addTypeInfoToEveryItem(items).toString();
}
}
/**
* Returns a new collection containing all elements in {@code items} for which there exists at
* least one element in {@code itemsToCheck} that has the same {@code toString()} value without
* being equal.
*
* <p>Example: {@code retainMatchingToString([1L, 2L, 2L], [2, 3]) == [2L, 2L]}
*/
static List<Object> retainMatchingToString(Iterable<?> items, Iterable<?> itemsToCheck) {
SetMultimap<String, Object> stringValueToItemsToCheck = HashMultimap.create();
for (Object itemToCheck : itemsToCheck) {
stringValueToItemsToCheck.put(String.valueOf(itemToCheck), itemToCheck);
}
List<Object> result = Lists.newArrayList();
for (Object item : items) {
for (Object itemToCheck : stringValueToItemsToCheck.get(String.valueOf(item))) {
if (!Objects.equal(itemToCheck, item)) {
result.add(item);
break;
}
}
}
return result;
}
/**
* Returns true if there is a pair of an item from {@code items1} and one in {@code items2} that
* has the same {@code toString()} value without being equal.
*
* <p>Example: {@code hasMatchingToStringPair([1L, 2L], [1]) == true}
*/
static boolean hasMatchingToStringPair(Iterable<?> items1, Iterable<?> items2) {
return !retainMatchingToString(items1, items2).isEmpty();
}
static String objectToTypeName(Object item) {
if (item == null) {
// The name "null type" comes from the interface javax.lang.model.type.NullType.
return "null type";
} else if (item instanceof Map.Entry) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) item;
return StringUtil.format(
"Map.Entry<%s, %s>",
objectToTypeName(entry.getKey()), objectToTypeName(entry.getValue()));
} else {
return item.getClass().getName();
}
}
/**
* Returns the name of the single type of all given items or {@link Optional#absent()} if no such
* type exists.
*/
private static Optional<String> getHomogeneousTypeName(Iterable<?> items) {
Optional<String> homogeneousTypeName = Optional.absent();
for (Object item : items) {
if (item == null) {
return Optional.absent();
} else if (!homogeneousTypeName.isPresent()) {
// This is the first item
homogeneousTypeName = Optional.of(objectToTypeName(item));
} else if (!objectToTypeName(item).equals(homogeneousTypeName.get())) {
// items is a heterogeneous collection
return Optional.absent();
}
}
return homogeneousTypeName;
}
private static List<String> addTypeInfoToEveryItem(Iterable<?> items) {
List<String> itemsWithTypeInfo = Lists.newArrayList();
for (Object item : items) {
itemsWithTypeInfo.add(StringUtil.format("%s (%s)", item, objectToTypeName(item)));
}
return itemsWithTypeInfo;
}
static <T> Collection<T> iterableToCollection(Iterable<T> iterable) {
if (iterable instanceof Collection) {
// Should be safe to assume that any Iterable implementing Collection isn't a one-shot
// iterable, right? I sure hope so.
return (Collection<T>) iterable;
} else {
return Lists.newArrayList(iterable);
}
}
static <T> List<T> iterableToList(Iterable<T> iterable) {
if (iterable instanceof List) {
return (List<T>) iterable;
} else {
return Lists.newArrayList(iterable);
}
}
/**
* Returns an iterable with all empty strings replaced by a non-empty human understandable
* indicator for an empty string.
*
* <p>Returns the given iterable if it contains no empty strings.
*/
static <T> Iterable<T> annotateEmptyStrings(Iterable<T> items) {
if (Iterables.contains(items, "")) {
List<T> annotatedItems = Lists.newArrayList();
for (T item : items) {
if (Objects.equal(item, "")) {
// This is a safe cast because know that at least one instance of T (this item) is a
// String.
@SuppressWarnings("unchecked")
T newItem = (T) HUMAN_UNDERSTANDABLE_EMPTY_STRING;
annotatedItems.add(newItem);
} else {
annotatedItems.add(item);
}
}
return annotatedItems;
} else {
return items;
}
}
}