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

com.google.inject.internal.PackageNameCompressor Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2020 The Dagger Authors.
 *
 * 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 java.util.Comparator.comparing;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Munges an error message to remove/shorten package names and adds a legend at the end.
 */
final class PackageNameCompressor {

  static final String LEGEND_HEADER =
      "\n\n======================\nFull classname legend:\n======================\n";
  static final String LEGEND_FOOTER =
      "========================\nEnd of classname legend:\n========================\n";

  private static final ImmutableSet PACKAGES_SKIPPED_IN_LEGEND = ImmutableSet.of(
      "java.lang.",
      "java.util.");

  private static final Splitter PACKAGE_SPLITTER = Splitter.on('.');

  private static final Joiner PACKAGE_JOINER = Joiner.on('.');

  // TODO(erichang): Consider validating this regex by also passing in all of the known types from
  // keys, module names, component names, etc and checking against that list. This may have some
  // extra complications with taking apart types like List to get the inner class names.
  private static final Pattern CLASSNAME_PATTERN =
      // Match lowercase package names with trailing dots. Start with a non-word character so we
      // don't match substrings in like Bar.Foo and match the com.foo.Foo. Require at least 2
      // package names to avoid matching non package names like a sentence ending with a period and
      // starting with an upper case letter without space, for example:
      // foo.Must in message "Invalid value for foo.Must not be empty." should not be compressed.
      // Start a group to not include the non-word character.
      Pattern.compile(
          "[\\W](([a-z_0-9]++[.]){2,}+"
              // Then match a name starting with an uppercase letter. This is the outer class name.
              + "[A-Z][\\w$]*)");

  // Pattern used to filter out quoted strings that should not have their package name compressed.
  // Picked '"' here because Guice uses it when including a string literal in an error message. This
  // will allow user to include class names in the error message and disable the compressor by
  // putting the name in a pair of '"'.
  // The pattern without the escapes: ([^"]+)((")?[^"\r\n]*")?
  // First group captures non quoted strings
  // Second group captures either a single quote or a string with a pair of quotes within a line
  // Class names in second group will not be compressed.
  private static final Pattern QUOTED_PATTERN =
      Pattern.compile("([^\\\"]+)((\\\")?[^\\\"\\r\\n]*\\\")?");

  /**
   * Compresses an error message by stripping the packages out of class names and adding them
   * to a legend at the bottom of the error.
   */
  static String compressPackagesInMessage(String input) {
    Matcher matcher = CLASSNAME_PATTERN.matcher(input);

    Set names = new HashSet<>();
    // Find all classnames in the error. Note that if our regex isn't complete, it just means the
    // classname is left in the full form, which is a fine fallback.
    while (matcher.find()) {
      String name = matcher.group(1);
      names.add(name);
    }
    // Now dedupe any conflicts. Use a TreeMap since we're going to need the legend sorted anyway.
    // This map is from short name to full name.
    Map replacementMap = shortenNames(names);

    // If we have nothing to replace, just return the original.
    if (replacementMap.isEmpty()) {
      return input;
    }

    StringBuilder output = new StringBuilder();
    Set replacedShortNames = replaceFullNames(input, replacementMap, output);
    if (replacedShortNames.isEmpty()) {
      return input;
    }

    String classNameLegend =
        buildClassNameLegend(Maps.filterKeys(replacementMap, replacedShortNames::contains));
    return output.append(classNameLegend).toString();
  }

  /**
   * Replaces full class names in {@code input} and append the replaced content to {@code output}
   * and then returns a set of short names that were used as replacement.
   *
   * 

String literals that are quoted in the {@code input} will be added to the {@code output} * unchanged. So any full class name that only appear in the string literal will not be included * in the returned short names set. */ private static ImmutableSet replaceFullNames( String input, Map replacementMap, StringBuilder output) { ImmutableSet.Builder replacedShortNames = ImmutableSet.builder(); // Sort short names in reverse alphabetical order. This is necessary so that a short name that // has a prefix that is another short name will be replaced first, otherwise the longer name // will not be collected as one of the replacedShortNames. List shortNames = replacementMap.keySet().stream() .sorted(Ordering.natural().reverse()) .collect(Collectors.toList()); Matcher matcher = QUOTED_PATTERN.matcher(input); while (matcher.find()) { String replaced = matcher.group(1); for (String shortName : shortNames) { String fullName = replacementMap.get(shortName); int beforeLen = replaced.length(); replaced = replaced.replace(fullName, shortName); // If the replacement happened then put the short name in replacedShortNames. // Only values in replacedShortNames are included in the full class name legend. if (replaced.length() < beforeLen) { replacedShortNames.add(shortName); } } output.append(replaced); String quoted = matcher.group(2); if (quoted != null) { output.append(quoted); } } return replacedShortNames.build(); } private static String buildClassNameLegend(Map replacementMap) { StringBuilder legendBuilder = new StringBuilder(); // Find the longest key for building the legend int longestKey = replacementMap.keySet().stream().max(comparing(String::length)).get().length(); for (Map.Entry entry : replacementMap.entrySet()) { String shortName = entry.getKey(); String fullName = entry.getValue(); // Skip certain prefixes. We need to check the shortName for a . though in case // there was some type of conflict like java.util.concurrent.Future and // java.util.foo.Future that got shortened to concurrent.Future and foo.Future. // In those cases we do not want to skip the legend. We only skip if the class // is directly in that package. String prefix = fullName.substring(0, fullName.length() - shortName.length()); if (PACKAGES_SKIPPED_IN_LEGEND.contains(prefix) && !shortName.contains(".")) { continue; } // Add to the legend legendBuilder .append(shortName) .append(": ") // Add enough spaces to adjust the columns .append(Strings.repeat(" ", longestKey - shortName.length())) // Surround the full class name with quotes to avoid them getting compressed again if // the error is wrapped inside another Guice error. .append('"') .append(fullName) .append('"') .append("\n"); } return legendBuilder.length() == 0 ? "" : Messages.bold(LEGEND_HEADER) + Messages.faint(legendBuilder.toString()) + Messages.bold(LEGEND_FOOTER); } /** * Returns a map from short name to full name after resolving conflicts. This resolves conflicts * by adding on segments of the package name until they are unique. For example, com.foo.Baz and * com.bar.Baz will conflict on Baz and then resolve with foo.Baz and bar.Baz as replacements. */ private static Map shortenNames(Collection names) { HashMultimap> shortNameToPartsMap = HashMultimap.create(); for (String name : names) { List parts = new ArrayList<>(PACKAGE_SPLITTER.splitToList(name)); // Start with the just the class name as the simple name String className = parts.remove(parts.size() - 1); shortNameToPartsMap.put(className, parts); } // Iterate through looking for conflicts adding the next part of the package until there are no // more conflicts while (true) { // Save the keys with conflicts to avoid concurrent modification issues List conflictingShortNames = new ArrayList<>(); for (Map.Entry>> entry : shortNameToPartsMap.asMap().entrySet()) { if (entry.getValue().size() > 1) { conflictingShortNames.add(entry.getKey()); } } if (conflictingShortNames.isEmpty()) { break; } // For all conflicts, add in the next part of the package for (String conflictingShortName : conflictingShortNames) { Set> partsCollection = shortNameToPartsMap.removeAll(conflictingShortName); for (List parts : partsCollection) { String newShortName = parts.remove(parts.size() - 1) + "." + conflictingShortName; // If we've removed the last part of the package, then just skip it entirely because // now we're not shortening it at all. if (!parts.isEmpty()) { shortNameToPartsMap.put(newShortName, parts); } } } } // Turn the multimap into a regular map now that conflicts have been resolved. Use a TreeMap // since we're going to need the legend sorted anyway. This map is from short name to full name. Map replacementMap = new TreeMap<>(); for (Map.Entry>> entry : shortNameToPartsMap.asMap().entrySet()) { replacementMap.put( entry.getKey(), PACKAGE_JOINER.join(Iterables.getOnlyElement(entry.getValue())) + "." + entry.getKey()); } return replacementMap; } private PackageNameCompressor() {} }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy