• 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 static java.nio.charset.StandardCharsets.UTF_8;
19 
20 import com.google.common.base.Ascii;
21 import com.google.common.collect.ImmutableList;
22 import com.google.common.collect.ImmutableMap;
23 import com.google.common.io.ByteStreams;
24 import com.google.escapevelocity.Template;
25 import java.io.File;
26 import java.io.FileInputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.io.InputStreamReader;
30 import java.io.Reader;
31 import java.io.StringReader;
32 import java.io.UncheckedIOException;
33 import java.lang.reflect.Field;
34 import java.lang.reflect.Modifier;
35 import java.net.URI;
36 import java.net.URISyntaxException;
37 import java.net.URL;
38 import java.util.Map;
39 import java.util.TreeMap;
40 import java.util.jar.JarFile;
41 
42 /**
43  * A template and a set of variables to be substituted into that template. A concrete subclass of
44  * this class defines a set of fields that are template variables, and an implementation of the
45  * {@link #parsedTemplate()} method which is the template to substitute them into. Once the values
46  * of the fields have been assigned, the {@link #toText()} method returns the result of substituting
47  * them into the template.
48  *
49  * <p>The subclass may be a direct subclass of this class or a more distant descendant. Every field
50  * in the starting class and its ancestors up to this class will be included. Fields cannot be
51  * static unless they are also final. They cannot be private, though they can be package-private if
52  * the class is in the same package as this class. They cannot be primitive or null, so that there
53  * is a clear indication when a field has not been set.
54  *
55  * @author Éamonn McManus
56  */
57 abstract class TemplateVars {
parsedTemplate()58   abstract Template parsedTemplate();
59 
60   private final ImmutableList<Field> fields;
61 
TemplateVars()62   TemplateVars() {
63     this.fields = getFields(getClass());
64   }
65 
getFields(Class<?> c)66   private static ImmutableList<Field> getFields(Class<?> c) {
67     ImmutableList.Builder<Field> fieldsBuilder = ImmutableList.builder();
68     while (c != TemplateVars.class) {
69       addFields(fieldsBuilder, c.getDeclaredFields());
70       c = c.getSuperclass();
71     }
72     return fieldsBuilder.build();
73   }
74 
addFields( ImmutableList.Builder<Field> fieldsBuilder, Field[] declaredFields)75   private static void addFields(
76       ImmutableList.Builder<Field> fieldsBuilder, Field[] declaredFields) {
77     for (Field field : declaredFields) {
78       if (field.isSynthetic() || isStaticFinal(field)) {
79         continue;
80       }
81       if (Modifier.isPrivate(field.getModifiers())) {
82         throw new IllegalArgumentException("Field cannot be private: " + field);
83       }
84       if (Modifier.isStatic(field.getModifiers())) {
85         throw new IllegalArgumentException("Field cannot be static unless also final: " + field);
86       }
87       if (field.getType().isPrimitive()) {
88         throw new IllegalArgumentException("Field cannot be primitive: " + field);
89       }
90       fieldsBuilder.add(field);
91     }
92   }
93 
94   /**
95    * Returns the result of substituting the variables defined by the fields of this class (a
96    * concrete subclass of TemplateVars) into the template returned by {@link #parsedTemplate()}.
97    */
toText()98   String toText() {
99     ImmutableMap<String, Object> vars = toVars();
100     return parsedTemplate().evaluate(vars);
101   }
102 
toVars()103   private ImmutableMap<String, Object> toVars() {
104     Map<String, Object> vars = new TreeMap<>();
105     for (Field field : fields) {
106       Object value = fieldValue(field, this);
107       if (value == null) {
108         throw new IllegalArgumentException("Field cannot be null (was it set?): " + field);
109       }
110       Object old = vars.put(field.getName(), value);
111       if (old != null) {
112         throw new IllegalArgumentException("Two fields called " + field.getName() + "?!");
113       }
114     }
115     return ImmutableMap.copyOf(vars);
116   }
117 
118   @Override
toString()119   public String toString() {
120     return getClass().getSimpleName() + toVars();
121   }
122 
parsedTemplateForResource(String resourceName)123   static Template parsedTemplateForResource(String resourceName) {
124     try {
125       return Template.parseFrom(resourceName, TemplateVars::readerFromUrl);
126     } catch (IOException e) {
127       throw new UncheckedIOException(e);
128     }
129   }
130 
131   // This is an ugly workaround for https://bugs.openjdk.java.net/browse/JDK-6947916, as
132   // reported in https://github.com/google/auto/issues/365.
133   // The issue is that sometimes the InputStream returned by JarURLCollection.getInputStream()
134   // can be closed prematurely, which leads to an IOException saying "Stream closed".
135   // To avoid that issue, we open the jar file directly and load the resource from it. Since that
136   // doesn't use JarURLConnection, it shouldn't be susceptible to the same bug.
readerFromUrl(String resourceName)137   private static Reader readerFromUrl(String resourceName) throws IOException {
138     URL resourceUrl = TemplateVars.class.getResource(resourceName);
139     if (resourceUrl == null) {
140       throw new IllegalArgumentException("Could not find resource: " + resourceName);
141     }
142     try {
143       if (Ascii.equalsIgnoreCase(resourceUrl.getProtocol(), "file")) {
144         return readerFromFile(resourceUrl);
145       } else if (Ascii.equalsIgnoreCase(resourceUrl.getProtocol(), "jar")) {
146         return readerFromJar(resourceUrl);
147       } else {
148         throw new AssertionError("Template search logic fails for: " + resourceUrl);
149       }
150     } catch (URISyntaxException e) {
151       throw new IOException(e);
152     }
153   }
154 
readerFromJar(URL resourceUrl)155   private static Reader readerFromJar(URL resourceUrl) throws URISyntaxException, IOException {
156     // Jar URLs look like this: jar:file:/path/to/file.jar!/entry/within/jar
157     // So take apart the URL to open the jar /path/to/file.jar and read the entry
158     // entry/within/jar from it.
159     // We could use the methods from JarURLConnection here, but that would risk provoking the same
160     // problem that prompted this workaround in the first place.
161     String resourceUrlString = resourceUrl.toString().substring("jar:".length());
162     int bang = resourceUrlString.lastIndexOf('!');
163     String entryName = resourceUrlString.substring(bang + 1);
164     if (entryName.startsWith("/")) {
165       entryName = entryName.substring(1);
166     }
167     URI jarUri = new URI(resourceUrlString.substring(0, bang));
168     try (JarFile jar = new JarFile(new File(jarUri));
169         InputStream in = jar.getInputStream(jar.getJarEntry(entryName))) {
170       String contents = new String(ByteStreams.toByteArray(in), UTF_8);
171       return new StringReader(contents);
172     }
173   }
174 
175   // In most execution environments, we'll be dealing with a jar, but we handle individual files
176   // just for cases like running our tests with Maven.
readerFromFile(URL resourceUrl)177   private static Reader readerFromFile(URL resourceUrl)
178       throws IOException, URISyntaxException {
179     File resourceFile = new File(resourceUrl.toURI());
180     return new InputStreamReader(new FileInputStream(resourceFile), UTF_8);
181   }
182 
fieldValue(Field field, Object container)183   private static Object fieldValue(Field field, Object container) {
184     try {
185       return field.get(container);
186     } catch (IllegalAccessException e) {
187       throw new RuntimeException(e);
188     }
189   }
190 
isStaticFinal(Field field)191   private static boolean isStaticFinal(Field field) {
192     int modifiers = field.getModifiers();
193     return Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers);
194   }
195 }
196