|
10 | 10 | import java.util.Comparator; |
11 | 11 | import java.util.HashMap; |
12 | 12 | import java.util.HashSet; |
| 13 | +import java.util.List; |
13 | 14 | import java.util.Map; |
14 | 15 | import java.util.Objects; |
| 16 | +import java.util.Optional; |
15 | 17 | import java.util.ServiceLoader; |
16 | 18 | import java.util.Set; |
17 | 19 | import java.util.concurrent.ConcurrentHashMap; |
@@ -51,77 +53,91 @@ Map<String, ProviderAndConfigType> load(Class<?> pluginInterface) { |
51 | 53 | } |
52 | 54 |
|
53 | 55 | private static Map<String, ProviderAndConfigType> loadProviders(Class<?> pluginInterface) { |
54 | | - HashMap<String, Set<ProviderAndConfigType>> nameToProviders = new HashMap<>(); |
55 | | - ServiceLoader<?> load = ServiceLoader.load(pluginInterface); |
56 | | - load.stream().forEach(provider -> { |
57 | | - Class<?> providerType = provider.type(); |
58 | | - Plugin annotation = providerType.getAnnotation(Plugin.class); |
59 | | - if (annotation == null) { |
60 | | - LOGGER.atWarn() |
61 | | - .addKeyValue("providerType", providerType) |
62 | | - .addKeyValue("service", pluginInterface) |
63 | | - .log("Failed to find @Plugin on provider of service"); |
64 | | - } |
65 | | - else { |
66 | | - ProviderAndConfigType providerAndConfigType = new ProviderAndConfigType(provider, annotation.configType()); |
67 | | - Stream<String> names = Stream.of(providerType.getName(), providerType.getSimpleName()); |
68 | | - names = maybeAddOldNames(providerType, names); |
69 | | - names.forEach(name -> nameToProviders.compute(name, (k2, v) -> { |
70 | | - if (v == null) { |
71 | | - v = new HashSet<>(); |
72 | | - } |
73 | | - v.add(providerAndConfigType); |
74 | | - return v; |
75 | | - })); |
76 | | - } |
77 | | - }); |
78 | | - var bySingleton = nameToProviders.entrySet().stream().collect( |
| 56 | + Map<String, Set<ProviderAndConfigType>> nameToProviders = new HashMap<>(); |
| 57 | + ServiceLoader.load(pluginInterface).stream() |
| 58 | + .forEach(provider -> registerProvider(provider, nameToProviders, pluginInterface)); |
| 59 | + var partitioned = nameToProviders.entrySet().stream().collect( |
79 | 60 | Collectors.partitioningBy(e -> e.getValue().size() == 1)); |
| 61 | + var ambiguousEntries = partitioned.get(false); |
| 62 | + var unambiguousEntries = partitioned.get(true); |
80 | 63 | if (LOGGER.isWarnEnabled()) { |
81 | | - for (Map.Entry<String, Set<ProviderAndConfigType>> ambiguousInstanceNameToProviders : bySingleton.get(false)) { |
82 | | - String ambiguousKey = ambiguousInstanceNameToProviders.getKey(); |
83 | | - var implementationClasses = ambiguousInstanceNameToProviders.getValue().stream() |
84 | | - .map(p -> p.provider().type()) |
85 | | - .sorted(Comparator.comparing(Class::getName)) |
86 | | - .toList(); |
87 | | - var fqCollision = implementationClasses.stream().filter(c -> c.isAnnotationPresent(DeprecatedPluginName.class)) |
88 | | - .flatMap(c -> implementationClasses.stream() |
89 | | - .filter(c2 -> { |
90 | | - var cOldName = c.getAnnotation(DeprecatedPluginName.class).oldName(); |
91 | | - return !c.equals(c2) && (c2.getName().equals(cOldName) |
92 | | - || (c2.isAnnotationPresent(DeprecatedPluginName.class) && |
93 | | - c2.getAnnotation(DeprecatedPluginName.class).oldName().equals(cOldName))); |
94 | | - }) |
95 | | - .map(c2 -> Map.entry(c, c2))) |
96 | | - .findFirst(); |
97 | | - if (fqCollision.isPresent()) { |
98 | | - var entry = fqCollision.get(); |
99 | | - var annotatedClass = entry.getKey(); |
100 | | - var classWithCollidingFqName = entry.getValue(); |
101 | | - LOGGER.atWarn() |
102 | | - .addKeyValue("annotatedClass", annotatedClass.getName()) |
103 | | - .addKeyValue("annotation", DeprecatedPluginName.class.getSimpleName()) |
104 | | - .addKeyValue("oldName", annotatedClass.getAnnotation(DeprecatedPluginName.class).oldName()) |
105 | | - .addKeyValue("collidingClass", classWithCollidingFqName.getName()) |
106 | | - .log("Plugin implementation class is annotated with @DeprecatedPluginName which collides with another plugin implementation class, you must remove one of these classes from the class path"); |
107 | | - throw new RuntimeException("Ambiguous plugin implementation name '" + ambiguousKey + "'"); |
108 | | - } |
109 | | - else { |
110 | | - LOGGER.atWarn() |
111 | | - .addKeyValue("ambiguousKey", ambiguousKey) |
112 | | - .addKeyValue("pluginInterface", pluginInterface.getSimpleName()) |
113 | | - .addKeyValue("candidates", implementationClasses.stream() |
114 | | - .map(Class::getName) |
115 | | - .collect(Collectors.joining(", "))) |
116 | | - .log("Ambiguous reference to provider, it could refer to multiple implementations so to avoid ambiguous behaviour those fully qualified names must be used"); |
117 | | - } |
| 64 | + for (Map.Entry<String, Set<ProviderAndConfigType>> ambiguousEntry : ambiguousEntries) { |
| 65 | + handleAmbiguousEntry(ambiguousEntry, pluginInterface); |
118 | 66 | } |
119 | 67 | } |
120 | | - return bySingleton.get(true).stream().collect(Collectors.toMap( |
| 68 | + return unambiguousEntries.stream().collect(Collectors.toMap( |
121 | 69 | Map.Entry::getKey, |
122 | 70 | e -> e.getValue().iterator().next())); |
123 | 71 | } |
124 | 72 |
|
| 73 | + private static void registerProvider(ServiceLoader.Provider<?> provider, |
| 74 | + Map<String, Set<ProviderAndConfigType>> nameToProviders, |
| 75 | + Class<?> pluginInterface) { |
| 76 | + Class<?> providerType = provider.type(); |
| 77 | + Plugin annotation = providerType.getAnnotation(Plugin.class); |
| 78 | + if (annotation == null) { |
| 79 | + LOGGER.atWarn() |
| 80 | + .addKeyValue("providerType", providerType) |
| 81 | + .addKeyValue("service", pluginInterface) |
| 82 | + .log("Failed to find @Plugin on provider of service"); |
| 83 | + return; |
| 84 | + } |
| 85 | + ProviderAndConfigType providerAndConfigType = new ProviderAndConfigType(provider, annotation.configType()); |
| 86 | + Stream<String> names = Stream.of(providerType.getName(), providerType.getSimpleName()); |
| 87 | + names = maybeAddOldNames(providerType, names); |
| 88 | + names.forEach(name -> nameToProviders.computeIfAbsent(name, k -> new HashSet<>()).add(providerAndConfigType)); |
| 89 | + } |
| 90 | + |
| 91 | + private static void handleAmbiguousEntry(Map.Entry<String, Set<ProviderAndConfigType>> ambiguousEntry, |
| 92 | + Class<?> pluginInterface) { |
| 93 | + String ambiguousKey = ambiguousEntry.getKey(); |
| 94 | + List<Class<?>> implementationClasses = ambiguousEntry.getValue().stream() |
| 95 | + .<Class<?>> map(p -> p.provider().type()) |
| 96 | + .sorted(Comparator.comparing(Class::getName)) |
| 97 | + .toList(); |
| 98 | + Optional<Map.Entry<Class<?>, Class<?>>> fqCollision = findDeprecatedNameCollision(implementationClasses); |
| 99 | + if (fqCollision.isPresent()) { |
| 100 | + var entry = fqCollision.get(); |
| 101 | + var annotatedClass = entry.getKey(); |
| 102 | + var classWithCollidingFqName = entry.getValue(); |
| 103 | + LOGGER.atWarn() |
| 104 | + .addKeyValue("annotatedClass", annotatedClass.getName()) |
| 105 | + .addKeyValue("annotation", DeprecatedPluginName.class.getSimpleName()) |
| 106 | + .addKeyValue("oldName", annotatedClass.getAnnotation(DeprecatedPluginName.class).oldName()) |
| 107 | + .addKeyValue("collidingClass", classWithCollidingFqName.getName()) |
| 108 | + .log("Plugin implementation class is annotated with @DeprecatedPluginName which collides with another plugin implementation class, you must remove one of these classes from the class path"); |
| 109 | + throw new RuntimeException("Ambiguous plugin implementation name '" + ambiguousKey + "'"); |
| 110 | + } |
| 111 | + else { |
| 112 | + LOGGER.atWarn() |
| 113 | + .addKeyValue("ambiguousKey", ambiguousKey) |
| 114 | + .addKeyValue("pluginInterface", pluginInterface.getSimpleName()) |
| 115 | + .addKeyValue("candidates", implementationClasses.stream() |
| 116 | + .map(Class::getName) |
| 117 | + .collect(Collectors.joining(", "))) |
| 118 | + .log("Ambiguous reference to provider, it could refer to multiple implementations so to avoid ambiguous behaviour those fully qualified names must be used"); |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + private static Optional<Map.Entry<Class<?>, Class<?>>> findDeprecatedNameCollision(List<Class<?>> implementationClasses) { |
| 123 | + return implementationClasses.stream() |
| 124 | + .filter(c -> c.isAnnotationPresent(DeprecatedPluginName.class)) |
| 125 | + .flatMap(c -> implementationClasses.stream() |
| 126 | + .filter(c2 -> isDeprecatedNameCollision(c, c2)) |
| 127 | + .map(c2 -> Map.<Class<?>, Class<?>> entry(c, c2))) |
| 128 | + .findFirst(); |
| 129 | + } |
| 130 | + |
| 131 | + private static boolean isDeprecatedNameCollision(Class<?> annotatedClass, Class<?> other) { |
| 132 | + if (annotatedClass.equals(other)) { |
| 133 | + return false; |
| 134 | + } |
| 135 | + String oldName = annotatedClass.getAnnotation(DeprecatedPluginName.class).oldName(); |
| 136 | + return other.getName().equals(oldName) |
| 137 | + || (other.isAnnotationPresent(DeprecatedPluginName.class) |
| 138 | + && other.getAnnotation(DeprecatedPluginName.class).oldName().equals(oldName)); |
| 139 | + } |
| 140 | + |
125 | 141 | private static Stream<String> maybeAddOldNames(Class<?> providerType, Stream<String> names) { |
126 | 142 | if (providerType.isAnnotationPresent(DeprecatedPluginName.class)) { |
127 | 143 | String oldName = providerType.getAnnotation(DeprecatedPluginName.class).oldName(); |
|
0 commit comments