View Javadoc
1   /*-
2    * #%L
3    * io.earcam.instrumental.fluency
4    * %%
5    * Copyright (C) 2018 earcam
6    * %%
7    * SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT)
8    * 
9    * You <b>must</b> choose to accept, in full - any individual or combination of 
10   * the following licenses:
11   * <ul>
12   * 	<li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li>
13   * 	<li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li>
14   * 	<li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li>
15   * 	<li><a href="https://opensource.org/licenses/MIT">MIT</a></li>
16   * </ul>
17   * #L%
18   */
19  package io.earcam.instrumental.fluency;
20  
21  import static java.util.Locale.ROOT;
22  import static java.util.stream.Collectors.joining;
23  import static java.util.stream.Collectors.toList;
24  import static javax.lang.model.element.Modifier.PRIVATE;
25  import static javax.lang.model.element.Modifier.STATIC;
26  
27  import java.io.BufferedWriter;
28  import java.io.IOException;
29  import java.io.PrintWriter;
30  import java.io.StringWriter;
31  import java.util.AbstractMap;
32  import java.util.ArrayList;
33  import java.util.Collections;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Set;
37  
38  import javax.annotation.processing.AbstractProcessor;
39  import javax.annotation.processing.ProcessingEnvironment;
40  import javax.annotation.processing.RoundEnvironment;
41  import javax.annotation.processing.SupportedOptions;
42  import javax.lang.model.SourceVersion;
43  import javax.lang.model.element.Element;
44  import javax.lang.model.element.ExecutableElement;
45  import javax.lang.model.element.Modifier;
46  import javax.lang.model.element.Name;
47  import javax.lang.model.element.PackageElement;
48  import javax.lang.model.element.TypeElement;
49  import javax.lang.model.type.TypeMirror;
50  import javax.tools.Diagnostic.Kind;
51  import javax.tools.JavaFileObject;
52  
53  import io.earcam.instrumental.fluent.Fluent;
54  
55  /**
56   * <p>
57   * FluencyProcessor; processes {@link Fluent}ly annotated class members.
58   * </p>
59   *
60   */
61  @SupportedOptions({ FluencyProcessor.OPTION_NAME })
62  public class FluencyProcessor extends AbstractProcessor {
63  
64  	static final String OPTION_NAME = "name";
65  
66  	/*
67  	 * TODO
68  	 * type parameters,
69  	 * package-private/protected scope into same package if not signed,
70  	 * support constructors as wrapped as fluid methods
71  	 */
72  	private class FluentMethod {
73  
74  		final ExecutableElement methodElement;
75  		final String comment;
76  
77  
78  		FluentMethod(ExecutableElement methodElement)
79  		{
80  			this.methodElement = methodElement;
81  			this.comment = nabComment();
82  		}
83  
84  
85  		Name methodName()
86  		{
87  			return methodElement.getSimpleName();
88  		}
89  
90  
91  		Name returnTypeName()
92  		{
93  			return typeName(returnType());
94  		}
95  
96  
97  		TypeMirror returnType()
98  		{
99  			return methodElement.getReturnType();
100 		}
101 
102 
103 		private Name typeName(TypeMirror type)
104 		{
105 			return type.getKind().isPrimitive() ? processingEnv.getElementUtils().getName(type.getKind().name().toLowerCase(ROOT))
106 					: ((TypeElement) processingEnv.getTypeUtils().asElement(type)).getQualifiedName();
107 		}
108 
109 
110 		Name ownerTypeSimpleName()
111 		{
112 			return ownerType().getSimpleName();
113 		}
114 
115 
116 		TypeElement ownerType()
117 		{
118 			return (TypeElement) methodElement.getEnclosingElement();
119 		}
120 
121 
122 		PackageElement pkg()
123 		{
124 			return (PackageElement) ownerType().getEnclosingElement();
125 		}
126 
127 
128 		Name packageName()
129 		{
130 			return pkg().getQualifiedName();
131 		}
132 
133 
134 		List<Map.Entry<Name, Name>> parameterNames()
135 		{
136 			return methodElement.getParameters().stream()
137 					.map(p -> new AbstractMap.SimpleEntry<>(p.getSimpleName(), typeName(p.asType())))
138 					.collect(toList());
139 		}
140 
141 
142 		String javadoc()
143 		{
144 			return comment;
145 		}
146 
147 
148 		private String nabComment()
149 		{
150 			String javadoc = processingEnv.getElementUtils().getDocComment(methodElement);
151 			return (javadoc == null) ? "" : makeComment(javadoc);
152 		}
153 
154 
155 		private String makeComment(String javadoc)
156 		{
157 			return "\n\t/**" + javadoc.replace("\n", "\n\t *") + "\n\t*/\n";
158 		}
159 
160 
161 		public Appendable appendTo(Appendable text) throws IOException
162 		{
163 			text.append(javadoc())
164 					.append('\t')
165 					.append("public static final ")
166 					.append(returnTypeName())
167 					.append(' ')
168 					.append(methodName())
169 					.append(parametersToString())
170 					.append("\n\t{\n\t\t");
171 
172 			if(!returnTypeName().contentEquals("void")) {
173 				text.append("return ");
174 			}
175 			return text.append(packageName())
176 					.append('.')
177 					.append(ownerTypeSimpleName())
178 					.append('.')
179 					.append(methodName())
180 					.append(argumentsToString())
181 					.append(";\n\t}\n");
182 		}
183 
184 
185 		private String parametersToString()
186 		{
187 			return parameterNames().stream().map(e -> e.getValue() + " " + e.getKey()).collect(joining(", ", "(", ")"));
188 		}
189 
190 
191 		private String argumentsToString()
192 		{
193 			return parameterNames().stream().map(Map.Entry::getKey).collect(joining(", ", "(", ")"));
194 		}
195 	}
196 
197 	private final List<FluentMethod> methods = new ArrayList<>();
198 	private String name;
199 
200 
201 	@Override
202 	public synchronized void init(ProcessingEnvironment processingEnv)
203 	{
204 		super.init(processingEnv);
205 
206 		name = processingEnv.getOptions().getOrDefault(OPTION_NAME, "FluentApi");
207 	}
208 
209 
210 	@Override
211 	public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
212 	{
213 		processingEnv.getMessager().printMessage(Kind.NOTE, "starting processing round ...");
214 		// needs to be a one-per package where a package-private member is annotated ...
215 
216 		for(Element element : roundEnv.getElementsAnnotatedWith(Fluent.class)) {
217 			ExecutableElement method = (ExecutableElement) element;
218 
219 			Set<Modifier> modifiers = method.getModifiers();
220 			if(!modifiers.contains(STATIC) || modifiers.contains(PRIVATE)) {
221 				processingEnv.getMessager().printMessage(Kind.WARNING, "Skipping as private or not static, modifiers:" + modifiers, element);
222 				continue;
223 			}
224 
225 			FluentMethod fluentMethod = new FluentMethod(method);
226 
227 			methods.add(fluentMethod);
228 		}
229 
230 		if(roundEnv.processingOver()) {
231 			generateSource();
232 		}
233 
234 		return true;
235 	}
236 
237 
238 	private void generateSource()
239 	{
240 		String paquet = "com.acme";
241 		String fqn = paquet + '.' + name;
242 		JavaFileObject jfo;
243 		try {
244 			jfo = processingEnv.getFiler().createSourceFile(fqn);
245 			try(BufferedWriter bw = new BufferedWriter(jfo.openWriter())) {
246 				bw.append("package ").append(paquet).append(";\n\n");
247 
248 				// TODO date must be optional (as otherwise "different" source each time)
249 				// JAVA9 FAILS due to split package issues with `javax.annotation`
250 				// bw.append('@').append("javax.annotation.Generated")
251 				// .append("(value=\"").append(FluencyProcessor.class.getCanonicalName()).append('"')
252 				// .append(")\n")
253 
254 				bw.append("public final class ").append(name).append(" {\n\n");
255 
256 				for(FluentMethod method : methods) {
257 					method.appendTo(bw);
258 				}
259 
260 				bw.append("\n}\n");
261 			}
262 		} catch(IOException e) {
263 			StringWriter writer = new StringWriter();
264 			e.printStackTrace(new PrintWriter(writer));
265 			processingEnv.getMessager().printMessage(Kind.ERROR, writer.toString());
266 		}
267 	}
268 
269 
270 	@Override
271 	public Set<String> getSupportedAnnotationTypes()
272 	{
273 		return Collections.singleton(Fluent.class.getCanonicalName());
274 	}
275 
276 
277 	@Override
278 	public SourceVersion getSupportedSourceVersion()
279 	{
280 		return SourceVersion.latestSupported();
281 	}
282 }