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