• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2014 Google LLC
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.google.auto.value.processor;
17 
18 import com.google.common.base.Throwables;
19 import com.google.common.collect.ImmutableList;
20 import com.google.common.collect.ImmutableMap;
21 import com.google.escapevelocity.Template;
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.FilterInputStream;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.InputStreamReader;
28 import java.io.Reader;
29 import java.io.UnsupportedEncodingException;
30 import java.lang.reflect.Field;
31 import java.lang.reflect.Modifier;
32 import java.net.URI;
33 import java.net.URISyntaxException;
34 import java.net.URL;
35 import java.nio.charset.StandardCharsets;
36 import java.util.Map;
37 import java.util.TreeMap;
38 import java.util.jar.JarEntry;
39 import java.util.jar.JarFile;
40 
41 /**
42  * A template and a set of variables to be substituted into that template. A concrete subclass of
43  * this class defines a set of fields that are template variables, and an implementation of the
44  * {@link #parsedTemplate()} method which is the template to substitute them into. Once the values
45  * of the fields have been assigned, the {@link #toText()} method returns the result of substituting
46  * them into the template.
47  *
48  * <p>The subclass may be a direct subclass of this class or a more distant descendant. Every field
49  * in the starting class and its ancestors up to this class will be included. Fields cannot be
50  * static unless they are also final. They cannot be private, though they can be package-private if
51  * the class is in the same package as this class. They cannot be primitive or null, so that there
52  * is a clear indication when a field has not been set.
53  *
54  * @author Éamonn McManus
55  */
56 abstract class TemplateVars {
parsedTemplate()57   abstract Template parsedTemplate();
58 
59   private final ImmutableList<Field> fields;
60 
TemplateVars()61   TemplateVars() {
62     this.fields = getFields(getClass());
63   }
64 
getFields(Class<?> c)65   private static ImmutableList<Field> getFields(Class<?> c) {
66     ImmutableList.Builder<Field> fieldsBuilder = ImmutableList.builder();
67     while (c != TemplateVars.class) {
68       addFields(fieldsBuilder, c.getDeclaredFields());
69       c = c.getSuperclass();
70     }
71     return fieldsBuilder.build();
72   }
73 
addFields( ImmutableList.Builder<Field> fieldsBuilder, Field[] declaredFields)74   private static void addFields(
75       ImmutableList.Builder<Field> fieldsBuilder, Field[] declaredFields) {
76     for (Field field : declaredFields) {
77       if (field.isSynthetic() || isStaticFinal(field)) {
78         continue;
79       }
80       if (Modifier.isPrivate(field.getModifiers())) {
81         throw new IllegalArgumentException("Field cannot be private: " + field);
82       }
83       if (Modifier.isStatic(field.getModifiers())) {
84         throw new IllegalArgumentException("Field cannot be static unless also final: " + field);
85       }
86       if (field.getType().isPrimitive()) {
87         throw new IllegalArgumentException("Field cannot be primitive: " + field);
88       }
89       fieldsBuilder.add(field);
90     }
91   }
92 
93   /**
94    * Returns the result of substituting the variables defined by the fields of this class (a
95    * concrete subclass of TemplateVars) into the template returned by {@link #parsedTemplate()}.
96    */
toText()97   String toText() {
98     Map<String, Object> vars = toVars();
99     return parsedTemplate().evaluate(vars);
100   }
101 
toVars()102   private ImmutableMap<String, Object> toVars() {
103     Map<String, Object> vars = new TreeMap<>();
104     for (Field field : fields) {
105       Object value = fieldValue(field, this);
106       if (value == null) {
107         throw new IllegalArgumentException("Field cannot be null (was it set?): " + field);
108       }
109       Object old = vars.put(field.getName(), value);
110       if (old != null) {
111         throw new IllegalArgumentException("Two fields called " + field.getName() + "?!");
112       }
113     }
114     return ImmutableMap.copyOf(vars);
115   }
116 
117   @Override
toString()118   public String toString() {
119     return getClass().getSimpleName() + toVars();
120   }
121 
parsedTemplateForResource(String resourceName)122   static Template parsedTemplateForResource(String resourceName) {
123     try {
124       return Template.parseFrom(resourceName, TemplateVars::readerFromResource);
125     } catch (UnsupportedEncodingException e) {
126       throw new AssertionError(e);
127     } catch (IOException | NullPointerException | IllegalStateException e) {
128       // https://github.com/google/auto/pull/439 says that we can get NullPointerException.
129       // https://github.com/google/auto/issues/715 says that we can get IllegalStateException
130       return retryParseAfterException(resourceName, e);
131     }
132   }
133 
retryParseAfterException(String resourceName, Exception exception)134   private static Template retryParseAfterException(String resourceName, Exception exception) {
135     try {
136       return Template.parseFrom(resourceName, TemplateVars::readerFromUrl);
137     } catch (IOException t) {
138       // Chain the original exception so we can see both problems.
139       Throwables.getRootCause(exception).initCause(t);
140       throw new AssertionError(exception);
141     }
142   }
143 
readerFromResource(String resourceName)144   private static Reader readerFromResource(String resourceName) {
145     InputStream in = TemplateVars.class.getResourceAsStream(resourceName);
146     if (in == null) {
147       throw new IllegalArgumentException("Could not find resource: " + resourceName);
148     }
149     return new InputStreamReader(in, StandardCharsets.UTF_8);
150   }
151 
152   // This is an ugly workaround for https://bugs.openjdk.java.net/browse/JDK-6947916, as
153   // reported in https://github.com/google/auto/issues/365.
154   // The issue is that sometimes the InputStream returned by JarURLCollection.getInputStream()
155   // can be closed prematurely, which leads to an IOException saying "Stream closed".
156   // We catch all IOExceptions, and fall back on logic that opens the jar file directly and
157   // loads the resource from it. Since that doesn't use JarURLConnection, it shouldn't be
158   // susceptible to the same bug. We only use this as fallback logic rather than doing it always,
159   // because jars are memory-mapped by URLClassLoader, so loading a resource in the usual way
160   // through the getResourceAsStream should be a lot more efficient than reopening the jar.
readerFromUrl(String resourceName)161   private static Reader readerFromUrl(String resourceName) throws IOException {
162     URL resourceUrl = TemplateVars.class.getResource(resourceName);
163     if (resourceUrl == null) {
164       // This is unlikely, since getResourceAsStream has already succeeded for the same resource.
165       throw new IllegalArgumentException("Could not find resource: " + resourceName);
166     }
167     InputStream in;
168     try {
169       if (resourceUrl.getProtocol().equalsIgnoreCase("file")) {
170         in = inputStreamFromFile(resourceUrl);
171       } else if (resourceUrl.getProtocol().equalsIgnoreCase("jar")) {
172         in = inputStreamFromJar(resourceUrl);
173       } else {
174         throw new AssertionError("Template fallback logic fails for: " + resourceUrl);
175       }
176     } catch (URISyntaxException e) {
177       throw new IOException(e);
178     }
179     return new InputStreamReader(in, StandardCharsets.UTF_8);
180   }
181 
inputStreamFromJar(URL resourceUrl)182   private static InputStream inputStreamFromJar(URL resourceUrl)
183       throws URISyntaxException, IOException {
184     // Jar URLs look like this: jar:file:/path/to/file.jar!/entry/within/jar
185     // So take apart the URL to open the jar /path/to/file.jar and read the entry
186     // entry/within/jar from it.
187     String resourceUrlString = resourceUrl.toString().substring("jar:".length());
188     int bang = resourceUrlString.lastIndexOf('!');
189     String entryName = resourceUrlString.substring(bang + 1);
190     if (entryName.startsWith("/")) {
191       entryName = entryName.substring(1);
192     }
193     URI jarUri = new URI(resourceUrlString.substring(0, bang));
194     JarFile jar = new JarFile(new File(jarUri));
195     JarEntry entry = jar.getJarEntry(entryName);
196     InputStream in = jar.getInputStream(entry);
197     // We have to be careful not to close the JarFile before the stream has been read, because
198     // that would also close the stream. So we defer closing the JarFile until the stream is closed.
199     return new FilterInputStream(in) {
200       @Override
201       public void close() throws IOException {
202         super.close();
203         jar.close();
204       }
205     };
206   }
207 
208   // We don't really expect this case to arise, since the bug we're working around concerns jars
209   // not individual files. However, when running the test for this workaround from Maven, we do
210   // have files. That does mean the test is basically useless there, but Google's internal build
211   // system does run it using a jar, so we do have coverage.
212   private static InputStream inputStreamFromFile(URL resourceUrl)
213       throws IOException, URISyntaxException {
214     File resourceFile = new File(resourceUrl.toURI());
215     return new FileInputStream(resourceFile);
216   }
217 
218   private static Object fieldValue(Field field, Object container) {
219     try {
220       return field.get(container);
221     } catch (IllegalAccessException e) {
222       throw new RuntimeException(e);
223     }
224   }
225 
226   private static boolean isStaticFinal(Field field) {
227     int modifiers = field.getModifiers();
228     return Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers);
229   }
230 }
231