001package ball.annotation.processing; 002/*- 003 * ########################################################################## 004 * Utilities 005 * %% 006 * Copyright (C) 2008 - 2022 Allen D. Ball 007 * %% 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 * ########################################################################## 020 */ 021import ball.annotation.ServiceProviderFor; 022import java.io.PrintWriter; 023import java.lang.reflect.Method; 024import java.nio.file.Files; 025import java.nio.file.Paths; 026import java.util.Collections; 027import java.util.List; 028import java.util.Map; 029import java.util.Objects; 030import java.util.Set; 031import java.util.TreeMap; 032import java.util.TreeSet; 033import java.util.stream.Stream; 034import javax.annotation.processing.Processor; 035import javax.annotation.processing.RoundEnvironment; 036import javax.lang.model.element.AnnotationMirror; 037import javax.lang.model.element.AnnotationValue; 038import javax.lang.model.element.Element; 039import javax.lang.model.element.ExecutableElement; 040import javax.lang.model.element.TypeElement; 041import javax.lang.model.type.TypeMirror; 042import javax.tools.FileObject; 043import javax.tools.JavaFileManager; 044import lombok.NoArgsConstructor; 045import lombok.ToString; 046 047import static java.lang.reflect.Modifier.isAbstract; 048import static java.nio.charset.StandardCharsets.UTF_8; 049import static java.util.stream.Collectors.toList; 050import static javax.lang.model.element.Modifier.ABSTRACT; 051import static javax.lang.model.element.Modifier.PUBLIC; 052import static javax.tools.Diagnostic.Kind.ERROR; 053import static javax.tools.StandardLocation.CLASS_OUTPUT; 054import static org.apache.commons.lang3.StringUtils.EMPTY; 055 056/** 057 * {@link Processor} implementation to check {@link Class}es annotated with 058 * {@link ServiceProviderFor} to verify the annotated {@link Class}: 059 * <ol> 060 * <li value="1">Is concrete</li> 061 * <li value="2">Has a public no-argument constructor</li> 062 * <li value="3"> 063 * Implements the {@link Class}es specified by 064 * {@link ServiceProviderFor#value()} 065 * </li> 066 * </ol> 067 * or implements Java 9's {@code java.util.ServiceLoader.Provider} 068 * {@code public static T provider()} method. 069 * <p> 070 * Note: Google offers a similar 071 * {@link.uri https://github.com/google/auto/tree/master/service target=newtab AutoService} 072 * library. 073 * </p> 074 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 075 */ 076@ServiceProviderFor({ Processor.class }) 077@For({ ServiceProviderFor.class }) 078@NoArgsConstructor @ToString 079public class ServiceProviderForProcessor extends AnnotatedProcessor implements ClassFileProcessor { 080 private static abstract class PROTOTYPE { 081 public static Object provider() { return null; } 082 } 083 084 private static final Method PROTOTYPE = PROTOTYPE.class.getDeclaredMethods()[0]; 085 086 static { PROTOTYPE.setAccessible(true); } 087 088 private static final String PATH = "META-INF/services/%s"; 089 090 private final Map<String,Set<String>> map = new TreeMap<>(); 091 092 @Override 093 protected void process(RoundEnvironment roundEnv, TypeElement annotation, Element element) { 094 super.process(roundEnv, annotation, element); 095 096 TypeElement type = (TypeElement) element; 097 AnnotationMirror mirror = getAnnotationMirror(type, annotation); 098 AnnotationValue value = getAnnotationValue(mirror, "value"); 099 100 if (! isEmptyArray(value)) { 101 ExecutableElement method = getMethod(type, PROTOTYPE); 102 103 if (method != null) { 104 if (! method.getModifiers().containsAll(getModifiers(PROTOTYPE))) { 105 print(ERROR, method, 106 "@%s: %s is not %s", 107 annotation.getSimpleName(), method.getKind(), modifiers(PROTOTYPE.getModifiers())); 108 } 109 } else { 110 if (! withoutModifiers(ABSTRACT).test(element)) { 111 print(ERROR, element, 112 "%s: %s must not be %s", 113 annotation.getSimpleName(), element.getKind(), ABSTRACT); 114 } 115 116 ExecutableElement constructor = getConstructor((TypeElement) element, Collections.emptyList()); 117 boolean found = (constructor != null && constructor.getModifiers().contains(PUBLIC)); 118 119 if (! found) { 120 print(ERROR, element, "@%s: No %s NO-ARG constructor", annotation.getSimpleName(), PUBLIC); 121 } 122 } 123 124 String provider = elements.getBinaryName(type).toString(); 125 List<TypeElement> services = 126 Stream.of(value) 127 .filter(Objects::nonNull) 128 .map(t -> (List<?>) t.getValue()) 129 .flatMap(List::stream) 130 .map(t -> (AnnotationValue) t) 131 .map(t -> t.getValue()) 132 .filter(t -> t instanceof TypeMirror) 133 .map(t -> (TypeElement) types.asElement((TypeMirror) t)) 134 .collect(toList()); 135 136 for (TypeElement service : services) { 137 if (isAssignable(type, service)) { 138 if (method == null || isAssignable(method.getReturnType(), service.asType())) { 139 map.computeIfAbsent(service.getQualifiedName().toString(), k -> new TreeSet<>()) 140 .add(provider); 141 } else { 142 print(ERROR, method, 143 "@%s: %s does not return %s", 144 annotation.getSimpleName(), method.getKind(), service.getQualifiedName()); 145 } 146 } else { 147 print(ERROR, type, 148 "@%s: %s does not implement %s", 149 annotation.getSimpleName(), type.getKind(), service.getQualifiedName()); 150 } 151 } 152 } else { 153 print(ERROR, type, mirror, value, "value() is empty"); 154 } 155 } 156 157 private boolean isAssignable(Element from, Element to) { 158 return isAssignable(from.asType(), to.asType()); 159 } 160 161 private boolean isAssignable(TypeMirror from, TypeMirror to) { 162 return types.isAssignable(types.erasure(from), types.erasure(to)); 163 } 164 165 @Override 166 public void process(Set<Class<?>> set, JavaFileManager fm) throws Exception { 167 if (! map.isEmpty()) { 168/* 169 for (String service : map.keySet()) { 170 FileObject file = fm.getFileForOutput(CLASS_OUTPUT, EMPTY, String.format(PATH, service), null); 171 172 try { 173 Files.readAllLines(Paths.get(file.toUri()), UTF_8).stream() 174 .map(t -> t.split("#", 2)[0]) 175 .map(t -> t.trim()) 176 .filter(t -> (! t.isEmpty())) 177 .forEach(t -> map.get(service).add(t)); 178 } catch (Exception exception) { 179 } 180 } 181*/ 182 } else { 183 for (Class<?> provider : set) { 184 ServiceProviderFor annotation = provider.getAnnotation(ServiceProviderFor.class); 185 186 if (annotation != null) { 187 for (Class<?> service : annotation.value()) { 188 if (service.isAssignableFrom(provider)) { 189 map.computeIfAbsent(service.getName(), k -> new TreeSet<>()) 190 .add(provider.getName()); 191 } 192 } 193 } 194 } 195 } 196 197 for (Map.Entry<String,Set<String>> entry : map.entrySet()) { 198 String service = entry.getKey(); 199 FileObject file = fm.getFileForOutput(CLASS_OUTPUT, EMPTY, String.format(PATH, service), null); 200 201 try (PrintWriter writer = new PrintWriter(file.openWriter())) { 202 writer.println("# " + service); 203 204 entry.getValue().stream().forEach(t -> writer.println(t)); 205 } 206 } 207 } 208}