1 /* 2 * Copyright 2019 Google Inc. All Rights Reserved. 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 17 package com.google.turbine.processing; 18 19 import static com.google.common.base.Preconditions.checkArgument; 20 import static java.nio.charset.StandardCharsets.UTF_8; 21 import static java.util.Objects.requireNonNull; 22 23 import com.google.common.base.Supplier; 24 import com.google.common.base.Suppliers; 25 import com.google.common.collect.ImmutableMap; 26 import com.google.turbine.diag.SourceFile; 27 import java.io.ByteArrayInputStream; 28 import java.io.ByteArrayOutputStream; 29 import java.io.FileNotFoundException; 30 import java.io.IOException; 31 import java.io.InputStream; 32 import java.io.InputStreamReader; 33 import java.io.OutputStream; 34 import java.io.OutputStreamWriter; 35 import java.io.Reader; 36 import java.io.StringReader; 37 import java.io.Writer; 38 import java.net.URI; 39 import java.net.URISyntaxException; 40 import java.util.ArrayList; 41 import java.util.Collection; 42 import java.util.LinkedHashMap; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Set; 46 import java.util.function.Function; 47 import javax.annotation.processing.Filer; 48 import javax.annotation.processing.FilerException; 49 import javax.lang.model.element.Element; 50 import javax.lang.model.element.Modifier; 51 import javax.lang.model.element.NestingKind; 52 import javax.tools.FileObject; 53 import javax.tools.JavaFileManager.Location; 54 import javax.tools.JavaFileObject; 55 import javax.tools.JavaFileObject.Kind; 56 import javax.tools.StandardLocation; 57 58 /** Turbine's implementation of {@link Filer}. */ 59 public class TurbineFiler implements Filer { 60 61 /** 62 * Existing paths of file objects that cannot be regenerated, including the original compilation 63 * inputs and source or class files generated during any annotation processing round. 64 */ 65 private final Set<String> seen; 66 67 /** 68 * File objects generated during the current processing round. Each entry has a unique path, which 69 * is enforced by {@link #seen}. 70 */ 71 private final List<TurbineJavaFileObject> files = new ArrayList<>(); 72 73 /** Loads resources from the classpath. */ 74 private final Function<String, Supplier<byte[]>> classPath; 75 76 /** The {@link ClassLoader} for the annotation processor path, for loading resources. */ 77 private final ClassLoader loader; 78 79 private final Map<String, SourceFile> generatedSources = new LinkedHashMap<>(); 80 private final Map<String, byte[]> generatedClasses = new LinkedHashMap<>(); 81 82 /** Generated source file objects from all rounds. */ generatedSources()83 public ImmutableMap<String, SourceFile> generatedSources() { 84 return ImmutableMap.copyOf(generatedSources); 85 } 86 87 /** Generated class file objects from all rounds. */ generatedClasses()88 public ImmutableMap<String, byte[]> generatedClasses() { 89 return ImmutableMap.copyOf(generatedClasses); 90 } 91 TurbineFiler( Set<String> seen, Function<String, Supplier<byte[]>> classPath, ClassLoader loader)92 public TurbineFiler( 93 Set<String> seen, Function<String, Supplier<byte[]>> classPath, ClassLoader loader) { 94 this.seen = seen; 95 this.classPath = classPath; 96 this.loader = loader; 97 } 98 99 /** 100 * Called when the current annotation processing round is complete, and returns the sources 101 * generated in that round. 102 */ finishRound()103 public Collection<SourceFile> finishRound() { 104 Map<String, SourceFile> roundSources = new LinkedHashMap<>(); 105 for (TurbineJavaFileObject e : files) { 106 String path = e.getName(); 107 switch (e.getKind()) { 108 case SOURCE: 109 roundSources.put(path, new SourceFile(path, e.contents())); 110 break; 111 case CLASS: 112 generatedClasses.put(path, e.bytes()); 113 break; 114 case OTHER: 115 switch (e.location()) { 116 case CLASS_OUTPUT: 117 generatedClasses.put(path, e.bytes()); 118 break; 119 case SOURCE_OUTPUT: 120 this.generatedSources.put(path, new SourceFile(path, e.contents())); 121 break; 122 default: 123 throw new AssertionError(e.location()); 124 } 125 break; 126 case HTML: 127 throw new UnsupportedOperationException(String.valueOf(e.getKind())); 128 } 129 } 130 files.clear(); 131 this.generatedSources.putAll(roundSources); 132 return roundSources.values(); 133 } 134 135 @Override createSourceFile(CharSequence n, Element... originatingElements)136 public JavaFileObject createSourceFile(CharSequence n, Element... originatingElements) 137 throws IOException { 138 String name = n.toString(); 139 checkArgument(!name.contains("/"), "invalid type name: %s", name); 140 return create(StandardLocation.SOURCE_OUTPUT, Kind.SOURCE, name.replace('.', '/') + ".java"); 141 } 142 143 @Override createClassFile(CharSequence n, Element... originatingElements)144 public JavaFileObject createClassFile(CharSequence n, Element... originatingElements) 145 throws IOException { 146 String name = n.toString(); 147 checkArgument(!name.contains("/"), "invalid type name: %s", name); 148 return create(StandardLocation.CLASS_OUTPUT, Kind.CLASS, name.replace('.', '/') + ".class"); 149 } 150 151 @Override createResource( Location location, CharSequence p, CharSequence r, Element... originatingElements)152 public FileObject createResource( 153 Location location, CharSequence p, CharSequence r, Element... originatingElements) 154 throws IOException { 155 checkArgument(location instanceof StandardLocation, "%s", location); 156 String pkg = p.toString(); 157 String relativeName = r.toString(); 158 checkArgument(!pkg.contains("/"), "invalid package: %s", pkg); 159 String path = packageRelativePath(pkg, relativeName); 160 return create((StandardLocation) location, Kind.OTHER, path); 161 } 162 create(StandardLocation location, Kind kind, String path)163 private JavaFileObject create(StandardLocation location, Kind kind, String path) 164 throws FilerException { 165 checkArgument(location.isOutputLocation()); 166 if (!seen.add(path)) { 167 throw new FilerException("already created " + path); 168 } 169 TurbineJavaFileObject result = new TurbineJavaFileObject(location, kind, path); 170 files.add(result); 171 return result; 172 } 173 174 @Override getResource(Location location, CharSequence p, CharSequence r)175 public FileObject getResource(Location location, CharSequence p, CharSequence r) 176 throws IOException { 177 String pkg = p.toString(); 178 String relativeName = r.toString(); 179 checkArgument(!pkg.contains("/"), "invalid package: %s", pkg); 180 checkArgument(location instanceof StandardLocation, "unsupported location %s", location); 181 StandardLocation standardLocation = (StandardLocation) location; 182 String path = packageRelativePath(pkg, relativeName); 183 switch (standardLocation) { 184 case CLASS_OUTPUT: 185 byte[] generated = generatedClasses.get(path); 186 if (generated == null) { 187 throw new FileNotFoundException(path); 188 } 189 return new BytesFileObject(path, Suppliers.ofInstance(generated)); 190 case SOURCE_OUTPUT: 191 SourceFile source = generatedSources.get(path); 192 if (source == null) { 193 throw new FileNotFoundException(path); 194 } 195 return new SourceFileObject(path, source.source()); 196 case ANNOTATION_PROCESSOR_PATH: 197 if (loader.getResource(path) == null) { 198 throw new FileNotFoundException(path); 199 } 200 return new ResourceFileObject(loader, path); 201 case CLASS_PATH: 202 Supplier<byte[]> bytes = classPath.apply(path); 203 if (bytes == null) { 204 throw new FileNotFoundException(path); 205 } 206 return new BytesFileObject(path, bytes); 207 default: 208 throw new IllegalArgumentException(standardLocation.getName()); 209 } 210 } 211 packageRelativePath(String pkg, String relativeName)212 private static String packageRelativePath(String pkg, String relativeName) { 213 if (pkg.isEmpty()) { 214 return relativeName; 215 } 216 return pkg.replace('.', '/') + '/' + relativeName; 217 } 218 219 private abstract static class ReadOnlyFileObject implements FileObject { 220 221 protected final String path; 222 ReadOnlyFileObject(String path)223 public ReadOnlyFileObject(String path) { 224 this.path = path; 225 } 226 227 @Override getName()228 public final String getName() { 229 return path; 230 } 231 232 @Override toUri()233 public URI toUri() { 234 return URI.create("file:///" + path); 235 } 236 237 @Override openOutputStream()238 public final OutputStream openOutputStream() { 239 throw new IllegalStateException(); 240 } 241 242 @Override openWriter()243 public final Writer openWriter() { 244 throw new IllegalStateException(); 245 } 246 247 @Override getLastModified()248 public final long getLastModified() { 249 return 0; 250 } 251 252 @Override delete()253 public final boolean delete() { 254 throw new IllegalStateException(); 255 } 256 } 257 258 private abstract static class WriteOnlyFileObject implements FileObject { 259 260 @Override openInputStream()261 public final InputStream openInputStream() { 262 throw new IllegalStateException(); 263 } 264 265 @Override openReader(boolean ignoreEncodingErrors)266 public final Reader openReader(boolean ignoreEncodingErrors) { 267 throw new IllegalStateException(); 268 } 269 270 @Override getCharContent(boolean ignoreEncodingErrors)271 public final CharSequence getCharContent(boolean ignoreEncodingErrors) { 272 throw new IllegalStateException(); 273 } 274 } 275 276 private static class TurbineJavaFileObject extends WriteOnlyFileObject implements JavaFileObject { 277 278 private final StandardLocation location; 279 private final Kind kind; 280 private final CharSequence name; 281 private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); 282 TurbineJavaFileObject(StandardLocation location, Kind kind, CharSequence name)283 public TurbineJavaFileObject(StandardLocation location, Kind kind, CharSequence name) { 284 this.location = location; 285 this.kind = kind; 286 this.name = name; 287 } 288 289 @Override getKind()290 public Kind getKind() { 291 return kind; 292 } 293 294 @Override isNameCompatible(String simpleName, Kind kind)295 public boolean isNameCompatible(String simpleName, Kind kind) { 296 throw new UnsupportedOperationException(); 297 } 298 299 @Override getNestingKind()300 public NestingKind getNestingKind() { 301 throw new UnsupportedOperationException(); 302 } 303 304 @Override getAccessLevel()305 public Modifier getAccessLevel() { 306 throw new UnsupportedOperationException(); 307 } 308 309 @Override toUri()310 public URI toUri() { 311 return URI.create("file:///" + name + kind.extension); 312 } 313 314 @Override getName()315 public String getName() { 316 return name.toString(); 317 } 318 319 @Override openOutputStream()320 public OutputStream openOutputStream() { 321 return baos; 322 } 323 324 @Override openWriter()325 public Writer openWriter() { 326 return new OutputStreamWriter(openOutputStream(), UTF_8); 327 } 328 329 @Override getLastModified()330 public long getLastModified() { 331 return 0; 332 } 333 334 @Override delete()335 public boolean delete() { 336 throw new IllegalStateException(); 337 } 338 bytes()339 public byte[] bytes() { 340 return baos.toByteArray(); 341 } 342 contents()343 public String contents() { 344 return new String(baos.toByteArray(), UTF_8); 345 } 346 location()347 public StandardLocation location() { 348 return location; 349 } 350 } 351 352 private static class ResourceFileObject extends ReadOnlyFileObject { 353 354 private final ClassLoader loader; 355 ResourceFileObject(ClassLoader loader, String path)356 public ResourceFileObject(ClassLoader loader, String path) { 357 super(path); 358 this.loader = loader; 359 } 360 361 @Override toUri()362 public URI toUri() { 363 try { 364 return requireNonNull(loader.getResource(path)).toURI(); 365 } catch (URISyntaxException e) { 366 throw new AssertionError(e); 367 } 368 } 369 370 @Override openInputStream()371 public InputStream openInputStream() { 372 return loader.getResourceAsStream(path); 373 } 374 375 @Override openReader(boolean ignoreEncodingErrors)376 public Reader openReader(boolean ignoreEncodingErrors) throws IOException { 377 return new InputStreamReader(openInputStream(), UTF_8); 378 } 379 380 @Override getCharContent(boolean ignoreEncodingErrors)381 public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { 382 return new String(openInputStream().readAllBytes(), UTF_8); 383 } 384 } 385 386 private static class BytesFileObject extends ReadOnlyFileObject { 387 388 private final Supplier<byte[]> bytes; 389 BytesFileObject(String path, Supplier<byte[]> bytes)390 public BytesFileObject(String path, Supplier<byte[]> bytes) { 391 super(path); 392 this.bytes = bytes; 393 } 394 395 @Override openInputStream()396 public InputStream openInputStream() { 397 return new ByteArrayInputStream(bytes.get()); 398 } 399 400 @Override openReader(boolean ignoreEncodingErrors)401 public Reader openReader(boolean ignoreEncodingErrors) { 402 return new InputStreamReader(openInputStream(), UTF_8); 403 } 404 405 @Override getCharContent(boolean ignoreEncodingErrors)406 public CharSequence getCharContent(boolean ignoreEncodingErrors) { 407 return new String(bytes.get(), UTF_8); 408 } 409 } 410 411 private static class SourceFileObject extends ReadOnlyFileObject { 412 413 private final String source; 414 SourceFileObject(String path, String source)415 public SourceFileObject(String path, String source) { 416 super(path); 417 this.source = source; 418 } 419 420 @Override openInputStream()421 public InputStream openInputStream() { 422 return new ByteArrayInputStream(source.getBytes(UTF_8)); 423 } 424 425 @Override openReader(boolean ignoreEncodingErrors)426 public Reader openReader(boolean ignoreEncodingErrors) { 427 return new StringReader(source); 428 } 429 430 @Override getCharContent(boolean ignoreEncodingErrors)431 public CharSequence getCharContent(boolean ignoreEncodingErrors) { 432 return source; 433 } 434 } 435 } 436