• 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 Map<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 
parsedTemplateForResource(String resourceName)117   static Template parsedTemplateForResource(String resourceName) {
118     try {
119       return Template.parseFrom(resourceName, TemplateVars::readerFromResource);
120     } catch (UnsupportedEncodingException e) {
121       throw new AssertionError(e);
122     } catch (IOException | NullPointerException | IllegalStateException e) {
123       // https://github.com/google/auto/pull/439 says that we can get NullPointerException.
124       // https://github.com/google/auto/issues/715 says that we can get IllegalStateException
125       return retryParseAfterException(resourceName, e);
126     }
127   }
128 
retryParseAfterException(String resourceName, Exception exception)129   private static Template retryParseAfterException(String resourceName, Exception exception) {
130     try {
131       return Template.parseFrom(resourceName, TemplateVars::readerFromUrl);
132     } catch (IOException t) {
133       // Chain the original exception so we can see both problems.
134       Throwables.getRootCause(exception).initCause(t);
135       throw new AssertionError(exception);
136     }
137   }
138 
readerFromResource(String resourceName)139   private static Reader readerFromResource(String resourceName) {
140     InputStream in = TemplateVars.class.getResourceAsStream(resourceName);
141     if (in == null) {
142       throw new IllegalArgumentException("Could not find resource: " + resourceName);
143     }
144     return new InputStreamReader(in, StandardCharsets.UTF_8);
145   }
146 
147   // This is an ugly workaround for https://bugs.openjdk.java.net/browse/JDK-6947916, as
148   // reported in https://github.com/google/auto/issues/365.
149   // The issue is that sometimes the InputStream returned by JarURLCollection.getInputStream()
150   // can be closed prematurely, which leads to an IOException saying "Stream closed".
151   // We catch all IOExceptions, and fall back on logic that opens the jar file directly and
152   // loads the resource from it. Since that doesn't use JarURLConnection, it shouldn't be
153   // susceptible to the same bug. We only use this as fallback logic rather than doing it always,
154   // because jars are memory-mapped by URLClassLoader, so loading a resource in the usual way
155   // through the getResourceAsStream should be a lot more efficient than reopening the jar.
readerFromUrl(String resourceName)156   private static Reader readerFromUrl(String resourceName) throws IOException {
157     URL resourceUrl = TemplateVars.class.getResource(resourceName);
158     if (resourceUrl == null) {
159       // This is unlikely, since getResourceAsStream has already succeeded for the same resource.
160       throw new IllegalArgumentException("Could not find resource: " + resourceName);
161     }
162     InputStream in;
163     try {
164       if (resourceUrl.getProtocol().equalsIgnoreCase("file")) {
165         in = inputStreamFromFile(resourceUrl);
166       } else if (resourceUrl.getProtocol().equalsIgnoreCase("jar")) {
167         in = inputStreamFromJar(resourceUrl);
168       } else {
169         throw new AssertionError("Template fallback logic fails for: " + resourceUrl);
170       }
171     } catch (URISyntaxException e) {
172       throw new IOException(e);
173     }
174     return new InputStreamReader(in, StandardCharsets.UTF_8);
175   }
176 
inputStreamFromJar(URL resourceUrl)177   private static InputStream inputStreamFromJar(URL resourceUrl)
178       throws URISyntaxException, IOException {
179     // Jar URLs look like this: jar:file:/path/to/file.jar!/entry/within/jar
180     // So take apart the URL to open the jar /path/to/file.jar and read the entry
181     // entry/within/jar from it.
182     String resourceUrlString = resourceUrl.toString().substring("jar:".length());
183     int bang = resourceUrlString.lastIndexOf('!');
184     String entryName = resourceUrlString.substring(bang + 1);
185     if (entryName.startsWith("/")) {
186       entryName = entryName.substring(1);
187     }
188     URI jarUri = new URI(resourceUrlString.substring(0, bang));
189     JarFile jar = new JarFile(new File(jarUri));
190     JarEntry entry = jar.getJarEntry(entryName);
191     InputStream in = jar.getInputStream(entry);
192     // We have to be careful not to close the JarFile before the stream has been read, because
193     // that would also close the stream. So we defer closing the JarFile until the stream is closed.
194     return new FilterInputStream(in) {
195       @Override
196       public void close() throws IOException {
197         super.close();
198         jar.close();
199       }
200     };
201   }
202 
203   // We don't really expect this case to arise, since the bug we're working around concerns jars
204   // not individual files. However, when running the test for this workaround from Maven, we do
205   // have files. That does mean the test is basically useless there, but Google's internal build
206   // system does run it using a jar, so we do have coverage.
207   private static InputStream inputStreamFromFile(URL resourceUrl)
208       throws IOException, URISyntaxException {
209     File resourceFile = new File(resourceUrl.toURI());
210     return new FileInputStream(resourceFile);
211   }
212 
213   private static Object fieldValue(Field field, Object container) {
214     try {
215       return field.get(container);
216     } catch (IllegalAccessException e) {
217       throw new RuntimeException(e);
218     }
219   }
220 
221   private static boolean isStaticFinal(Field field) {
222     int modifiers = field.getModifiers();
223     return Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers);
224   }
225 }
226