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