1 /* 2 * Copyright (C) 2015 Square, Inc. 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 package com.squareup.javapoet; 17 18 import java.io.ByteArrayInputStream; 19 import java.io.File; 20 import java.io.IOException; 21 import java.io.InputStream; 22 import java.io.OutputStreamWriter; 23 import java.io.Writer; 24 import java.net.URI; 25 import java.nio.file.Files; 26 import java.nio.file.Path; 27 import java.util.Arrays; 28 import java.util.Collections; 29 import java.util.List; 30 import java.util.Map; 31 import java.util.Set; 32 import java.util.TreeSet; 33 import javax.annotation.processing.Filer; 34 import javax.lang.model.element.Element; 35 import javax.tools.JavaFileObject; 36 import javax.tools.JavaFileObject.Kind; 37 import javax.tools.SimpleJavaFileObject; 38 39 import static com.squareup.javapoet.Util.checkArgument; 40 import static com.squareup.javapoet.Util.checkNotNull; 41 import static java.nio.charset.StandardCharsets.UTF_8; 42 43 /** A Java file containing a single top level class. */ 44 public final class JavaFile { 45 private static final Appendable NULL_APPENDABLE = new Appendable() { 46 @Override public Appendable append(CharSequence charSequence) { 47 return this; 48 } 49 @Override public Appendable append(CharSequence charSequence, int start, int end) { 50 return this; 51 } 52 @Override public Appendable append(char c) { 53 return this; 54 } 55 }; 56 57 public final CodeBlock fileComment; 58 public final String packageName; 59 public final TypeSpec typeSpec; 60 public final boolean skipJavaLangImports; 61 private final Set<String> staticImports; 62 private final String indent; 63 JavaFile(Builder builder)64 private JavaFile(Builder builder) { 65 this.fileComment = builder.fileComment.build(); 66 this.packageName = builder.packageName; 67 this.typeSpec = builder.typeSpec; 68 this.skipJavaLangImports = builder.skipJavaLangImports; 69 this.staticImports = Util.immutableSet(builder.staticImports); 70 this.indent = builder.indent; 71 } 72 writeTo(Appendable out)73 public void writeTo(Appendable out) throws IOException { 74 // First pass: emit the entire class, just to collect the types we'll need to import. 75 CodeWriter importsCollector = new CodeWriter(NULL_APPENDABLE, indent, staticImports); 76 emit(importsCollector); 77 Map<String, ClassName> suggestedImports = importsCollector.suggestedImports(); 78 79 // Second pass: write the code, taking advantage of the imports. 80 CodeWriter codeWriter = new CodeWriter(out, indent, suggestedImports, staticImports); 81 emit(codeWriter); 82 } 83 84 /** Writes this to {@code directory} as UTF-8 using the standard directory structure. */ writeTo(Path directory)85 public void writeTo(Path directory) throws IOException { 86 checkArgument(Files.notExists(directory) || Files.isDirectory(directory), 87 "path %s exists but is not a directory.", directory); 88 Path outputDirectory = directory; 89 if (!packageName.isEmpty()) { 90 for (String packageComponent : packageName.split("\\.")) { 91 outputDirectory = outputDirectory.resolve(packageComponent); 92 } 93 Files.createDirectories(outputDirectory); 94 } 95 96 Path outputPath = outputDirectory.resolve(typeSpec.name + ".java"); 97 try (Writer writer = new OutputStreamWriter(Files.newOutputStream(outputPath), UTF_8)) { 98 writeTo(writer); 99 } 100 } 101 102 /** Writes this to {@code directory} as UTF-8 using the standard directory structure. */ writeTo(File directory)103 public void writeTo(File directory) throws IOException { 104 writeTo(directory.toPath()); 105 } 106 107 /** Writes this to {@code filer}. */ writeTo(Filer filer)108 public void writeTo(Filer filer) throws IOException { 109 String fileName = packageName.isEmpty() 110 ? typeSpec.name 111 : packageName + "." + typeSpec.name; 112 List<Element> originatingElements = typeSpec.originatingElements; 113 JavaFileObject filerSourceFile = filer.createSourceFile(fileName, 114 originatingElements.toArray(new Element[originatingElements.size()])); 115 try (Writer writer = filerSourceFile.openWriter()) { 116 writeTo(writer); 117 } catch (Exception e) { 118 try { 119 filerSourceFile.delete(); 120 } catch (Exception ignored) { 121 } 122 throw e; 123 } 124 } 125 emit(CodeWriter codeWriter)126 private void emit(CodeWriter codeWriter) throws IOException { 127 codeWriter.pushPackage(packageName); 128 129 if (!fileComment.isEmpty()) { 130 codeWriter.emitComment(fileComment); 131 } 132 133 if (!packageName.isEmpty()) { 134 codeWriter.emit("package $L;\n", packageName); 135 codeWriter.emit("\n"); 136 } 137 138 if (!staticImports.isEmpty()) { 139 for (String signature : staticImports) { 140 codeWriter.emit("import static $L;\n", signature); 141 } 142 codeWriter.emit("\n"); 143 } 144 145 int importedTypesCount = 0; 146 for (ClassName className : new TreeSet<>(codeWriter.importedTypes().values())) { 147 if (skipJavaLangImports && className.packageName().equals("java.lang")) continue; 148 codeWriter.emit("import $L;\n", className.withoutAnnotations()); 149 importedTypesCount++; 150 } 151 152 if (importedTypesCount > 0) { 153 codeWriter.emit("\n"); 154 } 155 156 typeSpec.emit(codeWriter, null, Collections.emptySet()); 157 158 codeWriter.popPackage(); 159 } 160 equals(Object o)161 @Override public boolean equals(Object o) { 162 if (this == o) return true; 163 if (o == null) return false; 164 if (getClass() != o.getClass()) return false; 165 return toString().equals(o.toString()); 166 } 167 hashCode()168 @Override public int hashCode() { 169 return toString().hashCode(); 170 } 171 toString()172 @Override public String toString() { 173 try { 174 StringBuilder result = new StringBuilder(); 175 writeTo(result); 176 return result.toString(); 177 } catch (IOException e) { 178 throw new AssertionError(); 179 } 180 } 181 toJavaFileObject()182 public JavaFileObject toJavaFileObject() { 183 URI uri = URI.create((packageName.isEmpty() 184 ? typeSpec.name 185 : packageName.replace('.', '/') + '/' + typeSpec.name) 186 + Kind.SOURCE.extension); 187 return new SimpleJavaFileObject(uri, Kind.SOURCE) { 188 private final long lastModified = System.currentTimeMillis(); 189 @Override public String getCharContent(boolean ignoreEncodingErrors) { 190 return JavaFile.this.toString(); 191 } 192 @Override public InputStream openInputStream() throws IOException { 193 return new ByteArrayInputStream(getCharContent(true).getBytes(UTF_8)); 194 } 195 @Override public long getLastModified() { 196 return lastModified; 197 } 198 }; 199 } 200 builder(String packageName, TypeSpec typeSpec)201 public static Builder builder(String packageName, TypeSpec typeSpec) { 202 checkNotNull(packageName, "packageName == null"); 203 checkNotNull(typeSpec, "typeSpec == null"); 204 return new Builder(packageName, typeSpec); 205 } 206 toBuilder()207 public Builder toBuilder() { 208 Builder builder = new Builder(packageName, typeSpec); 209 builder.fileComment.add(fileComment); 210 builder.skipJavaLangImports = skipJavaLangImports; 211 builder.indent = indent; 212 return builder; 213 } 214 215 public static final class Builder { 216 private final String packageName; 217 private final TypeSpec typeSpec; 218 private final CodeBlock.Builder fileComment = CodeBlock.builder(); 219 private final Set<String> staticImports = new TreeSet<>(); 220 private boolean skipJavaLangImports; 221 private String indent = " "; 222 Builder(String packageName, TypeSpec typeSpec)223 private Builder(String packageName, TypeSpec typeSpec) { 224 this.packageName = packageName; 225 this.typeSpec = typeSpec; 226 } 227 addFileComment(String format, Object... args)228 public Builder addFileComment(String format, Object... args) { 229 this.fileComment.add(format, args); 230 return this; 231 } 232 addStaticImport(Enum<?> constant)233 public Builder addStaticImport(Enum<?> constant) { 234 return addStaticImport(ClassName.get(constant.getDeclaringClass()), constant.name()); 235 } 236 addStaticImport(Class<?> clazz, String... names)237 public Builder addStaticImport(Class<?> clazz, String... names) { 238 return addStaticImport(ClassName.get(clazz), names); 239 } 240 addStaticImport(ClassName className, String... names)241 public Builder addStaticImport(ClassName className, String... names) { 242 checkArgument(className != null, "className == null"); 243 checkArgument(names != null, "names == null"); 244 checkArgument(names.length > 0, "names array is empty"); 245 for (String name : names) { 246 checkArgument(name != null, "null entry in names array: %s", Arrays.toString(names)); 247 staticImports.add(className.canonicalName + "." + name); 248 } 249 return this; 250 } 251 252 /** 253 * Call this to omit imports for classes in {@code java.lang}, such as {@code java.lang.String}. 254 * 255 * <p>By default, JavaPoet explicitly imports types in {@code java.lang} to defend against 256 * naming conflicts. Suppose an (ill-advised) class is named {@code com.example.String}. When 257 * {@code java.lang} imports are skipped, generated code in {@code com.example} that references 258 * {@code java.lang.String} will get {@code com.example.String} instead. 259 */ skipJavaLangImports(boolean skipJavaLangImports)260 public Builder skipJavaLangImports(boolean skipJavaLangImports) { 261 this.skipJavaLangImports = skipJavaLangImports; 262 return this; 263 } 264 indent(String indent)265 public Builder indent(String indent) { 266 this.indent = indent; 267 return this; 268 } 269 build()270 public JavaFile build() { 271 return new JavaFile(this); 272 } 273 } 274 } 275