• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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