• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.inject.internal;
17 
18 import static com.google.common.base.Preconditions.checkArgument;
19 
20 import com.google.common.base.Equivalence;
21 import com.google.common.base.Objects;
22 import com.google.common.base.Throwables;
23 import com.google.common.collect.ImmutableList;
24 import com.google.common.collect.Lists;
25 import com.google.common.collect.Maps;
26 import com.google.inject.Key;
27 import com.google.inject.TypeLiteral;
28 import com.google.inject.internal.util.Classes;
29 import com.google.inject.internal.util.StackTraceElements;
30 import com.google.inject.spi.Dependency;
31 import com.google.inject.spi.ElementSource;
32 import com.google.inject.spi.InjectionPoint;
33 import com.google.inject.spi.Message;
34 import java.lang.reflect.Field;
35 import java.lang.reflect.Member;
36 import java.util.Arrays;
37 import java.util.Collection;
38 import java.util.Formatter;
39 import java.util.List;
40 import java.util.Map;
41 
42 /** Utility methods for {@link Message} objects */
43 public final class Messages {
Messages()44   private Messages() {}
45 
46   /** Prepends the list of sources to the given {@link Message} */
mergeSources(List<Object> sources, Message message)47   static Message mergeSources(List<Object> sources, Message message) {
48     List<Object> messageSources = message.getSources();
49     // It is possible that the end of getSources() and the beginning of message.getSources() are
50     // equivalent, in this case we should drop the repeated source when joining the lists.  The
51     // most likely scenario where this would happen is when a scoped binding throws an exception,
52     // due to the fact that InternalFactoryToProviderAdapter applies the binding source when
53     // merging errors.
54     if (!sources.isEmpty()
55         && !messageSources.isEmpty()
56         && Objects.equal(messageSources.get(0), sources.get(sources.size() - 1))) {
57       messageSources = messageSources.subList(1, messageSources.size());
58     }
59     return new Message(
60         ImmutableList.builder().addAll(sources).addAll(messageSources).build(),
61         message.getMessage(),
62         message.getCause());
63   }
64 
65   /**
66    * Calls {@link String#format} after converting the arguments using some standard guice formatting
67    * for {@link Key}, {@link Class} and {@link Member} objects.
68    */
format(String messageFormat, Object... arguments)69   public static String format(String messageFormat, Object... arguments) {
70     for (int i = 0; i < arguments.length; i++) {
71       arguments[i] = convert(arguments[i]);
72     }
73     return String.format(messageFormat, arguments);
74   }
75 
76   /** Returns the formatted message for an exception with the specified messages. */
formatMessages(String heading, Collection<Message> errorMessages)77   public static String formatMessages(String heading, Collection<Message> errorMessages) {
78     Formatter fmt = new Formatter().format(heading).format(":%n%n");
79     int index = 1;
80     boolean displayCauses = getOnlyCause(errorMessages) == null;
81 
82     Map<Equivalence.Wrapper<Throwable>, Integer> causes = Maps.newHashMap();
83     for (Message errorMessage : errorMessages) {
84       int thisIdx = index++;
85       fmt.format("%s) %s%n", thisIdx, errorMessage.getMessage());
86 
87       List<Object> dependencies = errorMessage.getSources();
88       for (int i = dependencies.size() - 1; i >= 0; i--) {
89         Object source = dependencies.get(i);
90         formatSource(fmt, source);
91       }
92 
93       Throwable cause = errorMessage.getCause();
94       if (displayCauses && cause != null) {
95         Equivalence.Wrapper<Throwable> causeEquivalence = ThrowableEquivalence.INSTANCE.wrap(cause);
96         if (!causes.containsKey(causeEquivalence)) {
97           causes.put(causeEquivalence, thisIdx);
98           fmt.format("Caused by: %s", Throwables.getStackTraceAsString(cause));
99         } else {
100           int causeIdx = causes.get(causeEquivalence);
101           fmt.format(
102               "Caused by: %s (same stack trace as error #%s)",
103               cause.getClass().getName(), causeIdx);
104         }
105       }
106 
107       fmt.format("%n");
108     }
109 
110     if (errorMessages.size() == 1) {
111       fmt.format("1 error");
112     } else {
113       fmt.format("%s errors", errorMessages.size());
114     }
115 
116     return fmt.toString();
117   }
118 
119   /**
120    * Creates a new Message without a cause.
121    *
122    * @param messageFormat Format string
123    * @param arguments format string arguments
124    */
create(String messageFormat, Object... arguments)125   public static Message create(String messageFormat, Object... arguments) {
126     return create(null, messageFormat, arguments);
127   }
128 
129   /**
130    * Creates a new Message with the given cause.
131    *
132    * @param cause The exception that caused the error
133    * @param messageFormat Format string
134    * @param arguments format string arguments
135    */
create(Throwable cause, String messageFormat, Object... arguments)136   public static Message create(Throwable cause, String messageFormat, Object... arguments) {
137     return create(cause, ImmutableList.of(), messageFormat, arguments);
138   }
139 
140   /**
141    * Creates a new Message with the given cause and a binding source stack.
142    *
143    * @param cause The exception that caused the error
144    * @param sources The binding sources for the source stack
145    * @param messageFormat Format string
146    * @param arguments format string arguments
147    */
create( Throwable cause, List<Object> sources, String messageFormat, Object... arguments)148   public static Message create(
149       Throwable cause, List<Object> sources, String messageFormat, Object... arguments) {
150     String message = format(messageFormat, arguments);
151     return new Message(sources, message, cause);
152   }
153 
154   /** Formats an object in a user friendly way. */
convert(Object o)155   static Object convert(Object o) {
156     ElementSource source = null;
157     if (o instanceof ElementSource) {
158       source = (ElementSource) o;
159       o = source.getDeclaringSource();
160     }
161     return convert(o, source);
162   }
163 
convert(Object o, ElementSource source)164   static Object convert(Object o, ElementSource source) {
165     for (Converter<?> converter : converters) {
166       if (converter.appliesTo(o)) {
167         return appendModules(converter.convert(o), source);
168       }
169     }
170     return appendModules(o, source);
171   }
172 
appendModules(Object source, ElementSource elementSource)173   private static Object appendModules(Object source, ElementSource elementSource) {
174     String modules = moduleSourceString(elementSource);
175     if (modules.length() == 0) {
176       return source;
177     } else {
178       return source + modules;
179     }
180   }
181 
moduleSourceString(ElementSource elementSource)182   private static String moduleSourceString(ElementSource elementSource) {
183     // if we only have one module (or don't know what they are), then don't bother
184     // reporting it, because the source already is going to report exactly that module.
185     if (elementSource == null) {
186       return "";
187     }
188     List<String> modules = Lists.newArrayList(elementSource.getModuleClassNames());
189     // Insert any original element sources w/ module info into the path.
190     while (elementSource.getOriginalElementSource() != null) {
191       elementSource = elementSource.getOriginalElementSource();
192       modules.addAll(0, elementSource.getModuleClassNames());
193     }
194     if (modules.size() <= 1) {
195       return "";
196     }
197 
198     // Ideally we'd do:
199     //    return Joiner.on(" -> ")
200     //        .appendTo(new StringBuilder(" (via modules: "), Lists.reverse(modules))
201     //        .append(")").toString();
202     // ... but for some reason we can't find Lists.reverse, so do it the boring way.
203     StringBuilder builder = new StringBuilder(" (via modules: ");
204     for (int i = modules.size() - 1; i >= 0; i--) {
205       builder.append(modules.get(i));
206       if (i != 0) {
207         builder.append(" -> ");
208       }
209     }
210     builder.append(")");
211     return builder.toString();
212   }
213 
formatSource(Formatter formatter, Object source)214   static void formatSource(Formatter formatter, Object source) {
215     ElementSource elementSource = null;
216     if (source instanceof ElementSource) {
217       elementSource = (ElementSource) source;
218       source = elementSource.getDeclaringSource();
219     }
220     formatSource(formatter, source, elementSource);
221   }
222 
formatSource(Formatter formatter, Object source, ElementSource elementSource)223   static void formatSource(Formatter formatter, Object source, ElementSource elementSource) {
224     String modules = moduleSourceString(elementSource);
225     if (source instanceof Dependency) {
226       Dependency<?> dependency = (Dependency<?>) source;
227       InjectionPoint injectionPoint = dependency.getInjectionPoint();
228       if (injectionPoint != null) {
229         formatInjectionPoint(formatter, dependency, injectionPoint, elementSource);
230       } else {
231         formatSource(formatter, dependency.getKey(), elementSource);
232       }
233 
234     } else if (source instanceof InjectionPoint) {
235       formatInjectionPoint(formatter, null, (InjectionPoint) source, elementSource);
236 
237     } else if (source instanceof Class) {
238       formatter.format("  at %s%s%n", StackTraceElements.forType((Class<?>) source), modules);
239 
240     } else if (source instanceof Member) {
241       formatter.format("  at %s%s%n", StackTraceElements.forMember((Member) source), modules);
242 
243     } else if (source instanceof TypeLiteral) {
244       formatter.format("  while locating %s%s%n", source, modules);
245 
246     } else if (source instanceof Key) {
247       Key<?> key = (Key<?>) source;
248       formatter.format("  while locating %s%n", convert(key, elementSource));
249 
250     } else if (source instanceof Thread) {
251       formatter.format("  in thread %s%n", source);
252 
253     } else {
254       formatter.format("  at %s%s%n", source, modules);
255     }
256   }
257 
formatInjectionPoint( Formatter formatter, Dependency<?> dependency, InjectionPoint injectionPoint, ElementSource elementSource)258   private static void formatInjectionPoint(
259       Formatter formatter,
260       Dependency<?> dependency,
261       InjectionPoint injectionPoint,
262       ElementSource elementSource) {
263     Member member = injectionPoint.getMember();
264     Class<? extends Member> memberType = Classes.memberType(member);
265 
266     if (memberType == Field.class) {
267       dependency = injectionPoint.getDependencies().get(0);
268       formatter.format("  while locating %s%n", convert(dependency.getKey(), elementSource));
269       formatter.format("    for field at %s%n", StackTraceElements.forMember(member));
270 
271     } else if (dependency != null) {
272       formatter.format("  while locating %s%n", convert(dependency.getKey(), elementSource));
273       formatter.format("    for %s%n", formatParameter(dependency));
274 
275     } else {
276       formatSource(formatter, injectionPoint.getMember());
277     }
278   }
279 
formatParameter(Dependency<?> dependency)280   static String formatParameter(Dependency<?> dependency) {
281     int ordinal = dependency.getParameterIndex() + 1;
282     return String.format(
283         "the %s%s parameter of %s",
284         ordinal,
285         getOrdinalSuffix(ordinal),
286         StackTraceElements.forMember(dependency.getInjectionPoint().getMember()));
287   }
288 
289   /**
290    * Maps {@code 1} to the string {@code "1st"} ditto for all non-negative numbers
291    *
292    * @see <a href="https://en.wikipedia.org/wiki/English_numerals#Ordinal_numbers">
293    *     https://en.wikipedia.org/wiki/English_numerals#Ordinal_numbers</a>
294    */
getOrdinalSuffix(int ordinal)295   private static String getOrdinalSuffix(int ordinal) {
296     // negative ordinals don't make sense, we allow zero though because we are programmers
297     checkArgument(ordinal >= 0);
298     if ((ordinal / 10) % 10 == 1) {
299       // all the 'teens' are weird
300       return "th";
301     } else {
302       // could use a lookup table? any better?
303       switch (ordinal % 10) {
304         case 1:
305           return "st";
306         case 2:
307           return "nd";
308         case 3:
309           return "rd";
310         default:
311           return "th";
312       }
313     }
314   }
315 
316   private abstract static class Converter<T> {
317 
318     final Class<T> type;
319 
Converter(Class<T> type)320     Converter(Class<T> type) {
321       this.type = type;
322     }
323 
appliesTo(Object o)324     boolean appliesTo(Object o) {
325       return o != null && type.isAssignableFrom(o.getClass());
326     }
327 
convert(Object o)328     String convert(Object o) {
329       return toString(type.cast(o));
330     }
331 
toString(T t)332     abstract String toString(T t);
333   }
334 
335   @SuppressWarnings({"unchecked", "rawtypes"}) // rawtypes aren't avoidable
336   private static final Collection<Converter<?>> converters =
337       ImmutableList.of(
338           new Converter<Class>(Class.class) {
339             @Override
340             public String toString(Class c) {
341               return c.getName();
342             }
343           },
344           new Converter<Member>(Member.class) {
345             @Override
346             public String toString(Member member) {
347               return Classes.toString(member);
348             }
349           },
350           new Converter<Key>(Key.class) {
351             @Override
352             public String toString(Key key) {
353               if (key.getAnnotationType() != null) {
354                 return key.getTypeLiteral()
355                     + " annotated with "
356                     + (key.getAnnotation() != null ? key.getAnnotation() : key.getAnnotationType());
357               } else {
358                 return key.getTypeLiteral().toString();
359               }
360             }
361           });
362 
363   /**
364    * Returns the cause throwable if there is exactly one cause in {@code messages}. If there are
365    * zero or multiple messages with causes, null is returned.
366    */
getOnlyCause(Collection<Message> messages)367   public static Throwable getOnlyCause(Collection<Message> messages) {
368     Throwable onlyCause = null;
369     for (Message message : messages) {
370       Throwable messageCause = message.getCause();
371       if (messageCause == null) {
372         continue;
373       }
374 
375       if (onlyCause != null && !ThrowableEquivalence.INSTANCE.equivalent(onlyCause, messageCause)) {
376         return null;
377       }
378 
379       onlyCause = messageCause;
380     }
381 
382     return onlyCause;
383   }
384 
385   private static final class ThrowableEquivalence extends Equivalence<Throwable> {
386     static final ThrowableEquivalence INSTANCE = new ThrowableEquivalence();
387 
388     @Override
doEquivalent(Throwable a, Throwable b)389     protected boolean doEquivalent(Throwable a, Throwable b) {
390       return a.getClass().equals(b.getClass())
391           && Objects.equal(a.getMessage(), b.getMessage())
392           && Arrays.equals(a.getStackTrace(), b.getStackTrace())
393           && equivalent(a.getCause(), b.getCause());
394     }
395 
396     @Override
doHash(Throwable t)397     protected int doHash(Throwable t) {
398       return Objects.hashCode(t.getClass().hashCode(), t.getMessage(), hash(t.getCause()));
399     }
400   }
401 }
402