/* * Copyright (C) 2006 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.inject.internal; import static com.google.common.base.Preconditions.checkArgument; import com.google.common.base.Equivalence; import com.google.common.base.Objects; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.google.common.primitives.Primitives; import com.google.inject.Binding; import com.google.inject.ConfigurationException; import com.google.inject.CreationException; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.MembersInjector; import com.google.inject.Provides; import com.google.inject.ProvisionException; import com.google.inject.Scope; import com.google.inject.TypeLiteral; import com.google.inject.internal.util.Classes; import com.google.inject.internal.util.SourceProvider; import com.google.inject.internal.util.StackTraceElements; import com.google.inject.spi.Dependency; import com.google.inject.spi.ElementSource; import com.google.inject.spi.InjectionListener; import com.google.inject.spi.InjectionPoint; import com.google.inject.spi.Message; import com.google.inject.spi.ScopeBinding; import com.google.inject.spi.TypeConverterBinding; import com.google.inject.spi.TypeListenerBinding; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Formatter; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; /** * A collection of error messages. If this type is passed as a method parameter, the method is * considered to have executed successfully only if new errors were not added to this collection. * * <p>Errors can be chained to provide additional context. To add context, call {@link #withSource} * to create a new Errors instance that contains additional context. All messages added to the * returned instance will contain full context. * * <p>To avoid messages with redundant context, {@link #withSource} should be added sparingly. A * good rule of thumb is to assume a method's caller has already specified enough context to * identify that method. When calling a method that's defined in a different context, call that * method with an errors object that includes its context. * * @author jessewilson@google.com (Jesse Wilson) */ public final class Errors implements Serializable { private static final Logger logger = Logger.getLogger(Guice.class.getName()); /** When a binding is not found, show at most this many bindings with the same type */ private static final int MAX_MATCHING_TYPES_REPORTED = 3; /** When a binding is not found, show at most this many bindings that have some similarities */ private static final int MAX_RELATED_TYPES_REPORTED = 3; /** * Throws a ConfigurationException with an NullPointerExceptions as the cause if the given * reference is {@code null}. */ static <T> T checkNotNull(T reference, String name) { if (reference != null) { return reference; } NullPointerException npe = new NullPointerException(name); throw new ConfigurationException(ImmutableSet.of(new Message(npe.toString(), npe))); } /** * Throws a ConfigurationException with a formatted {@link Message} if this condition is {@code * false}. */ static void checkConfiguration(boolean condition, String format, Object... args) { if (condition) { return; } throw new ConfigurationException(ImmutableSet.of(new Message(Errors.format(format, args)))); } /** * If the key is unknown and it is one of these types, it generally means there is a missing * annotation. */ private static final ImmutableSet<Class<?>> COMMON_AMBIGUOUS_TYPES = ImmutableSet.<Class<?>>builder() .add(Object.class) .add(String.class) .addAll(Primitives.allWrapperTypes()) .build(); private static final Set<Dependency<?>> warnedDependencies = Collections.newSetFromMap(new ConcurrentHashMap<Dependency<?>, Boolean>()); /** The root errors object. Used to access the list of error messages. */ private final Errors root; /** The parent errors object. Used to obtain the chain of source objects. */ private final Errors parent; /** The leaf source for errors added here. */ private final Object source; /** null unless (root == this) and error messages exist. Never an empty list. */ private List<Message> errors; // lazy, use getErrorsForAdd() public Errors() { this.root = this; this.parent = null; this.source = SourceProvider.UNKNOWN_SOURCE; } public Errors(Object source) { this.root = this; this.parent = null; this.source = source; } private Errors(Errors parent, Object source) { this.root = parent.root; this.parent = parent; this.source = source; } /** Returns an instance that uses {@code source} as a reference point for newly added errors. */ public Errors withSource(Object source) { return source == this.source || source == SourceProvider.UNKNOWN_SOURCE ? this : new Errors(this, source); } /** * We use a fairly generic error message here. The motivation is to share the same message for * both bind time errors: * * <pre><code>Guice.createInjector(new AbstractModule() { * public void configure() { * bind(Runnable.class); * } * }</code></pre> * * ...and at provide-time errors: * * <pre><code>Guice.createInjector().getInstance(Runnable.class);</code></pre> * * Otherwise we need to know who's calling when resolving a just-in-time binding, which makes * things unnecessarily complex. */ public Errors missingImplementation(Key key) { return addMessage("No implementation for %s was bound.", key); } /** Within guice's core, allow for better missing binding messages */ <T> Errors missingImplementationWithHint(Key<T> key, Injector injector) { StringBuilder sb = new StringBuilder(); sb.append(format("No implementation for %s was bound.", key)); // Keys which have similar strings as the desired key List<String> possibleMatches = new ArrayList<String>(); // Check for other keys that may have the same type, // but not the same annotation TypeLiteral<T> type = key.getTypeLiteral(); List<Binding<T>> sameTypes = injector.findBindingsByType(type); if (!sameTypes.isEmpty()) { sb.append(format("%n Did you mean?")); int howMany = Math.min(sameTypes.size(), MAX_MATCHING_TYPES_REPORTED); for (int i = 0; i < howMany; ++i) { // TODO: Look into a better way to prioritize suggestions. For example, possbily // use levenshtein distance of the given annotation vs actual annotation. sb.append(format("%n * %s", sameTypes.get(i).getKey())); } int remaining = sameTypes.size() - MAX_MATCHING_TYPES_REPORTED; if (remaining > 0) { String plural = (remaining == 1) ? "" : "s"; sb.append(format("%n %d more binding%s with other annotations.", remaining, plural)); } } else { // For now, do a simple substring search for possibilities. This can help spot // issues when there are generics being used (such as a wrapper class) and the // user has forgotten they need to bind based on the wrapper, not the underlying // class. In the future, consider doing a strict in-depth type search. // TODO: Look into a better way to prioritize suggestions. For example, possbily // use levenshtein distance of the type literal strings. String want = type.toString(); Map<Key<?>, Binding<?>> bindingMap = injector.getAllBindings(); for (Key<?> bindingKey : bindingMap.keySet()) { String have = bindingKey.getTypeLiteral().toString(); if (have.contains(want) || want.contains(have)) { Formatter fmt = new Formatter(); formatSource(fmt, bindingMap.get(bindingKey).getSource()); String match = String.format("%s bound%s", convert(bindingKey), fmt.toString()); possibleMatches.add(match); // TODO: Consider a check that if there are more than some number of results, // don't suggest any. if (possibleMatches.size() > MAX_RELATED_TYPES_REPORTED) { // Early exit if we have found more than we need. break; } } } if ((possibleMatches.size() > 0) && (possibleMatches.size() <= MAX_RELATED_TYPES_REPORTED)) { sb.append(format("%n Did you mean?")); for (String possibleMatch : possibleMatches) { sb.append(format("%n %s", possibleMatch)); } } } // If where are no possibilities to suggest, then handle the case of missing // annotations on simple types. This is usually a bad idea. if (sameTypes.isEmpty() && possibleMatches.isEmpty() && key.getAnnotation() == null && COMMON_AMBIGUOUS_TYPES.contains(key.getTypeLiteral().getRawType())) { // We don't recommend using such simple types without annotations. sb.append(format("%nThe key seems very generic, did you forget an annotation?")); } return addMessage(sb.toString()); } public Errors jitDisabled(Key key) { return addMessage("Explicit bindings are required and %s is not explicitly bound.", key); } public Errors jitDisabledInParent(Key<?> key) { return addMessage( "Explicit bindings are required and %s would be bound in a parent injector.%n" + "Please add an explicit binding for it, either in the child or the parent.", key); } public Errors atInjectRequired(Class clazz) { return addMessage( "Explicit @Inject annotations are required on constructors," + " but %s has no constructors annotated with @Inject.", clazz); } public Errors converterReturnedNull( String stringValue, Object source, TypeLiteral<?> type, TypeConverterBinding typeConverterBinding) { return addMessage( "Received null converting '%s' (bound at %s) to %s%n using %s.", stringValue, convert(source), type, typeConverterBinding); } public Errors conversionTypeError( String stringValue, Object source, TypeLiteral<?> type, TypeConverterBinding typeConverterBinding, Object converted) { return addMessage( "Type mismatch converting '%s' (bound at %s) to %s%n" + " using %s.%n" + " Converter returned %s.", stringValue, convert(source), type, typeConverterBinding, converted); } public Errors conversionError( String stringValue, Object source, TypeLiteral<?> type, TypeConverterBinding typeConverterBinding, RuntimeException cause) { return errorInUserCode( cause, "Error converting '%s' (bound at %s) to %s%n using %s.%n Reason: %s", stringValue, convert(source), type, typeConverterBinding, cause); } public Errors ambiguousTypeConversion( String stringValue, Object source, TypeLiteral<?> type, TypeConverterBinding a, TypeConverterBinding b) { return addMessage( "Multiple converters can convert '%s' (bound at %s) to %s:%n" + " %s and%n" + " %s.%n" + " Please adjust your type converter configuration to avoid overlapping matches.", stringValue, convert(source), type, a, b); } public Errors bindingToProvider() { return addMessage("Binding to Provider is not allowed."); } public Errors subtypeNotProvided( Class<? extends javax.inject.Provider<?>> providerType, Class<?> type) { return addMessage("%s doesn't provide instances of %s.", providerType, type); } public Errors notASubtype(Class<?> implementationType, Class<?> type) { return addMessage("%s doesn't extend %s.", implementationType, type); } public Errors recursiveImplementationType() { return addMessage("@ImplementedBy points to the same class it annotates."); } public Errors recursiveProviderType() { return addMessage("@ProvidedBy points to the same class it annotates."); } public Errors missingRuntimeRetention(Class<? extends Annotation> annotation) { return addMessage(format("Please annotate %s with @Retention(RUNTIME).", annotation)); } public Errors missingScopeAnnotation(Class<? extends Annotation> annotation) { return addMessage(format("Please annotate %s with @ScopeAnnotation.", annotation)); } public Errors optionalConstructor(Constructor constructor) { return addMessage( "%s is annotated @Inject(optional=true), but constructors cannot be optional.", constructor); } public Errors cannotBindToGuiceType(String simpleName) { return addMessage("Binding to core guice framework type is not allowed: %s.", simpleName); } public Errors scopeNotFound(Class<? extends Annotation> scopeAnnotation) { return addMessage("No scope is bound to %s.", scopeAnnotation); } public Errors scopeAnnotationOnAbstractType( Class<? extends Annotation> scopeAnnotation, Class<?> type, Object source) { return addMessage( "%s is annotated with %s, but scope annotations are not supported " + "for abstract types.%n Bound at %s.", type, scopeAnnotation, convert(source)); } public Errors misplacedBindingAnnotation(Member member, Annotation bindingAnnotation) { return addMessage( "%s is annotated with %s, but binding annotations should be applied " + "to its parameters instead.", member, bindingAnnotation); } private static final String CONSTRUCTOR_RULES = "Classes must have either one (and only one) constructor " + "annotated with @Inject or a zero-argument constructor that is not private."; public Errors missingConstructor(Class<?> implementation) { return addMessage( "Could not find a suitable constructor in %s. " + CONSTRUCTOR_RULES, implementation); } public Errors tooManyConstructors(Class<?> implementation) { return addMessage( "%s has more than one constructor annotated with @Inject. " + CONSTRUCTOR_RULES, implementation); } public Errors constructorNotDefinedByType(Constructor<?> constructor, TypeLiteral<?> type) { return addMessage("%s does not define %s", type, constructor); } public Errors duplicateScopes( ScopeBinding existing, Class<? extends Annotation> annotationType, Scope scope) { return addMessage( "Scope %s is already bound to %s at %s.%n Cannot bind %s.", existing.getScope(), annotationType, existing.getSource(), scope); } public Errors voidProviderMethod() { return addMessage("Provider methods must return a value. Do not return void."); } public Errors missingConstantValues() { return addMessage("Missing constant value. Please call to(...)."); } public Errors cannotInjectInnerClass(Class<?> type) { return addMessage( "Injecting into inner classes is not supported. " + "Please use a 'static' class (top-level or nested) instead of %s.", type); } public Errors duplicateBindingAnnotations( Member member, Class<? extends Annotation> a, Class<? extends Annotation> b) { return addMessage( "%s has more than one annotation annotated with @BindingAnnotation: %s and %s", member, a, b); } public Errors staticInjectionOnInterface(Class<?> clazz) { return addMessage("%s is an interface, but interfaces have no static injection points.", clazz); } public Errors cannotInjectFinalField(Field field) { return addMessage("Injected field %s cannot be final.", field); } public Errors cannotInjectAbstractMethod(Method method) { return addMessage("Injected method %s cannot be abstract.", method); } public Errors cannotInjectNonVoidMethod(Method method) { return addMessage("Injected method %s must return void.", method); } public Errors cannotInjectMethodWithTypeParameters(Method method) { return addMessage("Injected method %s cannot declare type parameters of its own.", method); } public Errors duplicateScopeAnnotations( Class<? extends Annotation> a, Class<? extends Annotation> b) { return addMessage("More than one scope annotation was found: %s and %s.", a, b); } public Errors recursiveBinding() { return addMessage("Binding points to itself."); } public Errors bindingAlreadySet(Key<?> key, Object source) { return addMessage("A binding to %s was already configured at %s.", key, convert(source)); } public Errors jitBindingAlreadySet(Key<?> key) { return addMessage( "A just-in-time binding to %s was already configured on a parent injector.", key); } public Errors childBindingAlreadySet(Key<?> key, Set<Object> sources) { Formatter allSources = new Formatter(); for (Object source : sources) { if (source == null) { allSources.format("%n (bound by a just-in-time binding)"); } else { allSources.format("%n bound at %s", source); } } Errors errors = addMessage( "Unable to create binding for %s." + " It was already configured on one or more child injectors or private modules" + "%s%n" + " If it was in a PrivateModule, did you forget to expose the binding?", key, allSources.out()); return errors; } public Errors errorCheckingDuplicateBinding(Key<?> key, Object source, Throwable t) { return addMessage( "A binding to %s was already configured at %s and an error was thrown " + "while checking duplicate bindings. Error: %s", key, convert(source), t); } public Errors errorInjectingMethod(Throwable cause) { return errorInUserCode(cause, "Error injecting method, %s", cause); } public Errors errorNotifyingTypeListener( TypeListenerBinding listener, TypeLiteral<?> type, Throwable cause) { return errorInUserCode( cause, "Error notifying TypeListener %s (bound at %s) of %s.%n Reason: %s", listener.getListener(), convert(listener.getSource()), type, cause); } public Errors errorInjectingConstructor(Throwable cause) { return errorInUserCode(cause, "Error injecting constructor, %s", cause); } public Errors errorInProvider(Throwable cause) { Throwable unwrapped = unwrap(cause); return errorInUserCode(unwrapped, "Error in custom provider, %s", unwrapped); } public Errors errorInUserInjector( MembersInjector<?> listener, TypeLiteral<?> type, RuntimeException cause) { return errorInUserCode( cause, "Error injecting %s using %s.%n Reason: %s", type, listener, cause); } public Errors errorNotifyingInjectionListener( InjectionListener<?> listener, TypeLiteral<?> type, RuntimeException cause) { return errorInUserCode( cause, "Error notifying InjectionListener %s of %s.%n Reason: %s", listener, type, cause); } public Errors exposedButNotBound(Key<?> key) { return addMessage("Could not expose() %s, it must be explicitly bound.", key); } public Errors keyNotFullySpecified(TypeLiteral<?> typeLiteral) { return addMessage("%s cannot be used as a key; It is not fully specified.", typeLiteral); } public Errors errorEnhancingClass(Class<?> clazz, Throwable cause) { return errorInUserCode(cause, "Unable to method intercept: %s", clazz); } public static Collection<Message> getMessagesFromThrowable(Throwable throwable) { if (throwable instanceof ProvisionException) { return ((ProvisionException) throwable).getErrorMessages(); } else if (throwable instanceof ConfigurationException) { return ((ConfigurationException) throwable).getErrorMessages(); } else if (throwable instanceof CreationException) { return ((CreationException) throwable).getErrorMessages(); } else { return ImmutableSet.of(); } } public Errors errorInUserCode(Throwable cause, String messageFormat, Object... arguments) { Collection<Message> messages = getMessagesFromThrowable(cause); if (!messages.isEmpty()) { return merge(messages); } else { return addMessage(cause, messageFormat, arguments); } } private Throwable unwrap(Throwable runtimeException) { if (runtimeException instanceof Exceptions.UnhandledCheckedUserException) { return runtimeException.getCause(); } else { return runtimeException; } } public Errors cannotInjectRawProvider() { return addMessage("Cannot inject a Provider that has no type parameter"); } public Errors cannotInjectRawMembersInjector() { return addMessage("Cannot inject a MembersInjector that has no type parameter"); } public Errors cannotInjectTypeLiteralOf(Type unsupportedType) { return addMessage("Cannot inject a TypeLiteral of %s", unsupportedType); } public Errors cannotInjectRawTypeLiteral() { return addMessage("Cannot inject a TypeLiteral that has no type parameter"); } public Errors cannotProxyClass(Class<?> expectedType) { return addMessage( "Tried proxying %s to support a circular dependency, but it is not an interface.", expectedType); } public Errors circularDependenciesDisabled(Class<?> expectedType) { return addMessage( "Found a circular dependency involving %s, and circular dependencies are disabled.", expectedType); } public void throwCreationExceptionIfErrorsExist() { if (!hasErrors()) { return; } throw new CreationException(getMessages()); } public void throwConfigurationExceptionIfErrorsExist() { if (!hasErrors()) { return; } throw new ConfigurationException(getMessages()); } public void throwProvisionExceptionIfErrorsExist() { if (!hasErrors()) { return; } throw new ProvisionException(getMessages()); } private Message merge(Message message) { List<Object> sources = Lists.newArrayList(); sources.addAll(getSources()); List<Object> messageSources = message.getSources(); // It is possible that the end of getSources() and the beginning of message.getSources() are // equivalent, in this case we should drop the repeated source when joining the lists. The // most likely scenario where this would happen is when a scoped binding throws an exception, // due to the fact that InternalFactoryToProviderAdapter applies the binding source when // merging errors. if (!sources.isEmpty() && !messageSources.isEmpty() && Objects.equal(messageSources.get(0), sources.get(sources.size() - 1))) { messageSources = messageSources.subList(1, messageSources.size()); } sources.addAll(messageSources); return new Message(sources, message.getMessage(), message.getCause()); } public Errors merge(Collection<Message> messages) { for (Message message : messages) { addMessage(merge(message)); } return this; } public Errors merge(Errors moreErrors) { if (moreErrors.root == root || moreErrors.root.errors == null) { return this; } merge(moreErrors.root.errors); return this; } public List<Object> getSources() { List<Object> sources = Lists.newArrayList(); for (Errors e = this; e != null; e = e.parent) { if (e.source != SourceProvider.UNKNOWN_SOURCE) { sources.add(0, e.source); } } return sources; } public void throwIfNewErrors(int expectedSize) throws ErrorsException { if (size() == expectedSize) { return; } throw toException(); } public ErrorsException toException() { return new ErrorsException(this); } public boolean hasErrors() { return root.errors != null; } public Errors addMessage(String messageFormat, Object... arguments) { return addMessage(null, messageFormat, arguments); } private Errors addMessage(Throwable cause, String messageFormat, Object... arguments) { String message = format(messageFormat, arguments); addMessage(new Message(getSources(), message, cause)); return this; } public Errors addMessage(Message message) { if (root.errors == null) { root.errors = Lists.newArrayList(); } root.errors.add(message); return this; } public static String format(String messageFormat, Object... arguments) { for (int i = 0; i < arguments.length; i++) { arguments[i] = Errors.convert(arguments[i]); } return String.format(messageFormat, arguments); } public List<Message> getMessages() { if (root.errors == null) { return ImmutableList.of(); } return new Ordering<Message>() { @Override public int compare(Message a, Message b) { return a.getSource().compareTo(b.getSource()); } }.sortedCopy(root.errors); } /** Returns the formatted message for an exception with the specified messages. */ public static String format(String heading, Collection<Message> errorMessages) { Formatter fmt = new Formatter().format(heading).format(":%n%n"); int index = 1; boolean displayCauses = getOnlyCause(errorMessages) == null; Map<Equivalence.Wrapper<Throwable>, Integer> causes = Maps.newHashMap(); for (Message errorMessage : errorMessages) { int thisIdx = index++; fmt.format("%s) %s%n", thisIdx, errorMessage.getMessage()); List<Object> dependencies = errorMessage.getSources(); for (int i = dependencies.size() - 1; i >= 0; i--) { Object source = dependencies.get(i); formatSource(fmt, source); } Throwable cause = errorMessage.getCause(); if (displayCauses && cause != null) { Equivalence.Wrapper<Throwable> causeEquivalence = ThrowableEquivalence.INSTANCE.wrap(cause); if (!causes.containsKey(causeEquivalence)) { causes.put(causeEquivalence, thisIdx); fmt.format("Caused by: %s", Throwables.getStackTraceAsString(cause)); } else { int causeIdx = causes.get(causeEquivalence); fmt.format( "Caused by: %s (same stack trace as error #%s)", cause.getClass().getName(), causeIdx); } } fmt.format("%n"); } if (errorMessages.size() == 1) { fmt.format("1 error"); } else { fmt.format("%s errors", errorMessages.size()); } return fmt.toString(); } /** * Returns {@code value} if it is non-null or allowed to be null. Otherwise a message is added and * an {@code ErrorsException} is thrown. */ public <T> T checkForNull(T value, Object source, Dependency<?> dependency) throws ErrorsException { if (value != null || dependency.isNullable()) { return value; } // Hack to allow null parameters to @Provides methods, for backwards compatibility. if (dependency.getInjectionPoint().getMember() instanceof Method) { Method annotated = (Method) dependency.getInjectionPoint().getMember(); if (annotated.isAnnotationPresent(Provides.class)) { switch (InternalFlags.getNullableProvidesOption()) { case ERROR: break; // break out & let the below exception happen case IGNORE: return value; // user doesn't care about injecting nulls to non-@Nullables. case WARN: // Warn only once, otherwise we spam logs too much. if (!warnedDependencies.add(dependency)) { return value; } logger.log( Level.WARNING, "Guice injected null into {0} (a {1}), please mark it @Nullable." + " Use -Dguice_check_nullable_provides_params=ERROR to turn this into an" + " error.", new Object[] {formatParameter(dependency), convert(dependency.getKey())}); return null; // log & exit. } } } Object formattedDependency = (dependency.getParameterIndex() != -1) ? formatParameter(dependency) : StackTraceElements.forMember(dependency.getInjectionPoint().getMember()); addMessage( "null returned by binding at %s%n but %s is not @Nullable", source, formattedDependency); throw toException(); } /** * Returns the cause throwable if there is exactly one cause in {@code messages}. If there are * zero or multiple messages with causes, null is returned. */ public static Throwable getOnlyCause(Collection<Message> messages) { Throwable onlyCause = null; for (Message message : messages) { Throwable messageCause = message.getCause(); if (messageCause == null) { continue; } if (onlyCause != null && !ThrowableEquivalence.INSTANCE.equivalent(onlyCause, messageCause)) { return null; } onlyCause = messageCause; } return onlyCause; } public int size() { return root.errors == null ? 0 : root.errors.size(); } private abstract static class Converter<T> { final Class<T> type; Converter(Class<T> type) { this.type = type; } boolean appliesTo(Object o) { return o != null && type.isAssignableFrom(o.getClass()); } String convert(Object o) { return toString(type.cast(o)); } abstract String toString(T t); } private static final Collection<Converter<?>> converters = ImmutableList.of( new Converter<Class>(Class.class) { @Override public String toString(Class c) { return c.getName(); } }, new Converter<Member>(Member.class) { @Override public String toString(Member member) { return Classes.toString(member); } }, new Converter<Key>(Key.class) { @Override public String toString(Key key) { if (key.getAnnotationType() != null) { return key.getTypeLiteral() + " annotated with " + (key.getAnnotation() != null ? key.getAnnotation() : key.getAnnotationType()); } else { return key.getTypeLiteral().toString(); } } }); public static Object convert(Object o) { ElementSource source = null; if (o instanceof ElementSource) { source = (ElementSource) o; o = source.getDeclaringSource(); } return convert(o, source); } public static Object convert(Object o, ElementSource source) { for (Converter<?> converter : converters) { if (converter.appliesTo(o)) { return appendModules(converter.convert(o), source); } } return appendModules(o, source); } private static Object appendModules(Object source, ElementSource elementSource) { String modules = moduleSourceString(elementSource); if (modules.length() == 0) { return source; } else { return source + modules; } } private static String moduleSourceString(ElementSource elementSource) { // if we only have one module (or don't know what they are), then don't bother // reporting it, because the source already is going to report exactly that module. if (elementSource == null) { return ""; } List<String> modules = Lists.newArrayList(elementSource.getModuleClassNames()); // Insert any original element sources w/ module info into the path. while (elementSource.getOriginalElementSource() != null) { elementSource = elementSource.getOriginalElementSource(); modules.addAll(0, elementSource.getModuleClassNames()); } if (modules.size() <= 1) { return ""; } // Ideally we'd do: // return Joiner.on(" -> ") // .appendTo(new StringBuilder(" (via modules: "), Lists.reverse(modules)) // .append(")").toString(); // ... but for some reason we can't find Lists.reverse, so do it the boring way. StringBuilder builder = new StringBuilder(" (via modules: "); for (int i = modules.size() - 1; i >= 0; i--) { builder.append(modules.get(i)); if (i != 0) { builder.append(" -> "); } } builder.append(")"); return builder.toString(); } public static void formatSource(Formatter formatter, Object source) { ElementSource elementSource = null; if (source instanceof ElementSource) { elementSource = (ElementSource) source; source = elementSource.getDeclaringSource(); } formatSource(formatter, source, elementSource); } public static void formatSource(Formatter formatter, Object source, ElementSource elementSource) { String modules = moduleSourceString(elementSource); if (source instanceof Dependency) { Dependency<?> dependency = (Dependency<?>) source; InjectionPoint injectionPoint = dependency.getInjectionPoint(); if (injectionPoint != null) { formatInjectionPoint(formatter, dependency, injectionPoint, elementSource); } else { formatSource(formatter, dependency.getKey(), elementSource); } } else if (source instanceof InjectionPoint) { formatInjectionPoint(formatter, null, (InjectionPoint) source, elementSource); } else if (source instanceof Class) { formatter.format(" at %s%s%n", StackTraceElements.forType((Class<?>) source), modules); } else if (source instanceof Member) { formatter.format(" at %s%s%n", StackTraceElements.forMember((Member) source), modules); } else if (source instanceof TypeLiteral) { formatter.format(" while locating %s%s%n", source, modules); } else if (source instanceof Key) { Key<?> key = (Key<?>) source; formatter.format(" while locating %s%n", convert(key, elementSource)); } else if (source instanceof Thread) { formatter.format(" in thread %s%n", source); } else { formatter.format(" at %s%s%n", source, modules); } } public static void formatInjectionPoint( Formatter formatter, Dependency<?> dependency, InjectionPoint injectionPoint, ElementSource elementSource) { Member member = injectionPoint.getMember(); Class<? extends Member> memberType = Classes.memberType(member); if (memberType == Field.class) { dependency = injectionPoint.getDependencies().get(0); formatter.format(" while locating %s%n", convert(dependency.getKey(), elementSource)); formatter.format(" for field at %s%n", StackTraceElements.forMember(member)); } else if (dependency != null) { formatter.format(" while locating %s%n", convert(dependency.getKey(), elementSource)); formatter.format(" for %s%n", formatParameter(dependency)); } else { formatSource(formatter, injectionPoint.getMember()); } } private static String formatParameter(Dependency<?> dependency) { int ordinal = dependency.getParameterIndex() + 1; return String.format( "the %s%s parameter of %s", ordinal, getOrdinalSuffix(ordinal), StackTraceElements.forMember(dependency.getInjectionPoint().getMember())); } /** * Maps {@code 1} to the string {@code "1st"} ditto for all non-negative numbers * * @see <a href="https://en.wikipedia.org/wiki/English_numerals#Ordinal_numbers"> * https://en.wikipedia.org/wiki/English_numerals#Ordinal_numbers</a> */ private static String getOrdinalSuffix(int ordinal) { // negative ordinals don't make sense, we allow zero though because we are programmers checkArgument(ordinal >= 0); if ((ordinal / 10) % 10 == 1) { // all the 'teens' are weird return "th"; } else { // could use a lookup table? any better? switch (ordinal % 10) { case 1: return "st"; case 2: return "nd"; case 3: return "rd"; default: return "th"; } } } static class ThrowableEquivalence extends Equivalence<Throwable> { static final ThrowableEquivalence INSTANCE = new ThrowableEquivalence(); @Override protected boolean doEquivalent(Throwable a, Throwable b) { return a.getClass().equals(b.getClass()) && Objects.equal(a.getMessage(), b.getMessage()) && Arrays.equals(a.getStackTrace(), b.getStackTrace()) && equivalent(a.getCause(), b.getCause()); } @Override protected int doHash(Throwable t) { return Objects.hashCode(t.getClass().hashCode(), t.getMessage(), hash(t.getCause())); } } }