Skip to content

Commit 3f69cd2

Browse files
eamonnmcmanusGoogle Java Core Libraries
authored andcommitted
Apply a workaround for a JDK bug unconditionally.
We now always read template resources directly from the jar file containing them, rather than initially trying to use `getResourceAsStream`. That can trigger [JDK-6947916](https://bugs.openjdk.org/browse/JDK-6947916) and our existing fallback-to-workaround logic was ineffective with recent versions of EscapeVelocity. Even though the JDK bug was supposedly fixed in JDK 9 and later JDK 8 updates, people are still reporting issues with AutoValue that look exactly like it. Reading directly from the jar file should not be _too_ inefficient, and each read should only happen once per compilation, no matter how many `@AutoValue` classes there are in the compilation. Fixes #1572. RELNOTES=A workaround for a JDK bug with reading jar resources has been extended so it always applies, rather than just as a fallback. See #1572. PiperOrigin-RevId: 563814239
1 parent 389b6e7 commit 3f69cd2

File tree

2 files changed

+28
-209
lines changed

2 files changed

+28
-209
lines changed

value/src/main/java/com/google/auto/value/processor/TemplateVars.java

Lines changed: 28 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,28 @@
1515
*/
1616
package com.google.auto.value.processor;
1717

18-
import com.google.common.base.Throwables;
18+
import static java.nio.charset.StandardCharsets.UTF_8;
19+
20+
import com.google.common.base.Ascii;
1921
import com.google.common.collect.ImmutableList;
2022
import com.google.common.collect.ImmutableMap;
23+
import com.google.common.io.ByteStreams;
2124
import com.google.escapevelocity.Template;
2225
import java.io.File;
2326
import java.io.FileInputStream;
24-
import java.io.FilterInputStream;
2527
import java.io.IOException;
2628
import java.io.InputStream;
2729
import java.io.InputStreamReader;
2830
import java.io.Reader;
29-
import java.io.UnsupportedEncodingException;
31+
import java.io.StringReader;
32+
import java.io.UncheckedIOException;
3033
import java.lang.reflect.Field;
3134
import java.lang.reflect.Modifier;
3235
import java.net.URI;
3336
import java.net.URISyntaxException;
3437
import java.net.URL;
35-
import java.nio.charset.StandardCharsets;
3638
import java.util.Map;
3739
import java.util.TreeMap;
38-
import java.util.jar.JarEntry;
3940
import java.util.jar.JarFile;
4041

4142
/**
@@ -95,7 +96,7 @@ private static void addFields(
9596
* concrete subclass of TemplateVars) into the template returned by {@link #parsedTemplate()}.
9697
*/
9798
String toText() {
98-
Map<String, Object> vars = toVars();
99+
ImmutableMap<String, Object> vars = toVars();
99100
return parsedTemplate().evaluate(vars);
100101
}
101102

@@ -120,99 +121,63 @@ public String toString() {
120121
}
121122

122123
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-
134-
private static Template retryParseAfterException(String resourceName, Exception exception) {
135124
try {
136125
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);
126+
} catch (IOException e) {
127+
throw new UncheckedIOException(e);
141128
}
142129
}
143130

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-
152131
// This is an ugly workaround for https://bugs.openjdk.java.net/browse/JDK-6947916, as
153132
// reported in https://github.com/google/auto/issues/365.
154133
// The issue is that sometimes the InputStream returned by JarURLCollection.getInputStream()
155134
// 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.
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.
161137
private static Reader readerFromUrl(String resourceName) throws IOException {
162138
URL resourceUrl = TemplateVars.class.getResource(resourceName);
163139
if (resourceUrl == null) {
164-
// This is unlikely, since getResourceAsStream has already succeeded for the same resource.
165140
throw new IllegalArgumentException("Could not find resource: " + resourceName);
166141
}
167-
InputStream in;
168142
try {
169-
if (resourceUrl.getProtocol().equalsIgnoreCase("file")) {
170-
in = inputStreamFromFile(resourceUrl);
171-
} else if (resourceUrl.getProtocol().equalsIgnoreCase("jar")) {
172-
in = inputStreamFromJar(resourceUrl);
143+
if (Ascii.equalsIgnoreCase(resourceUrl.getProtocol(), "file")) {
144+
return readerFromFile(resourceUrl);
145+
} else if (Ascii.equalsIgnoreCase(resourceUrl.getProtocol(), "jar")) {
146+
return readerFromJar(resourceUrl);
173147
} else {
174-
throw new AssertionError("Template fallback logic fails for: " + resourceUrl);
148+
throw new AssertionError("Template search logic fails for: " + resourceUrl);
175149
}
176150
} catch (URISyntaxException e) {
177151
throw new IOException(e);
178152
}
179-
return new InputStreamReader(in, StandardCharsets.UTF_8);
180153
}
181154

182-
private static InputStream inputStreamFromJar(URL resourceUrl)
183-
throws URISyntaxException, IOException {
155+
private static Reader readerFromJar(URL resourceUrl) throws URISyntaxException, IOException {
184156
// Jar URLs look like this: jar:file:/path/to/file.jar!/entry/within/jar
185157
// So take apart the URL to open the jar /path/to/file.jar and read the entry
186158
// 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.
187161
String resourceUrlString = resourceUrl.toString().substring("jar:".length());
188162
int bang = resourceUrlString.lastIndexOf('!');
189163
String entryName = resourceUrlString.substring(bang + 1);
190164
if (entryName.startsWith("/")) {
191165
entryName = entryName.substring(1);
192166
}
193167
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-
};
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+
}
206173
}
207174

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)
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.
177+
private static Reader readerFromFile(URL resourceUrl)
213178
throws IOException, URISyntaxException {
214179
File resourceFile = new File(resourceUrl.toURI());
215-
return new FileInputStream(resourceFile);
180+
return new InputStreamReader(new FileInputStream(resourceFile), UTF_8);
216181
}
217182

218183
private static Object fieldValue(Field field, Object container) {

value/src/test/java/com/google/auto/value/processor/TemplateVarsTest.java

Lines changed: 0 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,15 @@
1515
*/
1616
package com.google.auto.value.processor;
1717

18-
import static com.google.common.base.StandardSystemProperty.JAVA_CLASS_PATH;
19-
import static com.google.common.base.StandardSystemProperty.PATH_SEPARATOR;
2018
import static com.google.common.truth.Truth.assertThat;
21-
import static java.util.logging.Level.WARNING;
2219
import static org.junit.Assert.fail;
2320

24-
import com.google.common.base.Splitter;
2521
import com.google.common.collect.ImmutableList;
26-
import com.google.common.reflect.Reflection;
2722
import com.google.escapevelocity.Template;
28-
import java.io.File;
2923
import java.io.IOException;
30-
import java.io.InputStream;
3124
import java.io.Reader;
3225
import java.io.StringReader;
33-
import java.net.MalformedURLException;
34-
import java.net.URL;
35-
import java.net.URLClassLoader;
3626
import java.util.List;
37-
import java.util.Set;
38-
import java.util.TreeSet;
39-
import java.util.concurrent.Callable;
40-
import java.util.logging.Logger;
4127
import org.junit.Test;
4228
import org.junit.runner.RunWith;
4329
import org.junit.runners.JUnit4;
@@ -172,136 +158,4 @@ public void testPrimitive() {
172158
} catch (IllegalArgumentException expected) {
173159
}
174160
}
175-
176-
@Test
177-
public void testBrokenInputStream_IOException() throws Exception {
178-
doTestBrokenInputStream(new IOException("BrokenInputStream"));
179-
}
180-
181-
@Test
182-
public void testBrokenInputStream_NullPointerException() throws Exception {
183-
doTestBrokenInputStream(new NullPointerException("BrokenInputStream"));
184-
}
185-
186-
@Test
187-
public void testBrokenInputStream_IllegalStateException() throws Exception {
188-
doTestBrokenInputStream(new IllegalStateException("BrokenInputStream"));
189-
}
190-
191-
// This is a complicated test that tries to simulates the failures that are worked around in
192-
// Template.parsedTemplateForResource. Those failures means that the InputStream returned by
193-
// ClassLoader.getResourceAsStream sometimes throws IOException or NullPointerException or
194-
// IllegalStateException while it is being read. To simulate that, we make a second ClassLoader
195-
// with the same configuration as the one that runs this test, and we override getResourceAsStream
196-
// so that it wraps the returned InputStream in a BrokenInputStream, which throws an exception
197-
// after a certain number of characters. We check that that exception was indeed seen, and that
198-
// we did indeed try to read the resource we're interested in, and that we succeeded in loading a
199-
// Template nevertheless.
200-
private void doTestBrokenInputStream(Exception exception) throws Exception {
201-
URLClassLoader shadowLoader = new ShadowLoader(getClass().getClassLoader(), exception);
202-
Runnable brokenInputStreamTest =
203-
(Runnable)
204-
shadowLoader
205-
.loadClass(BrokenInputStreamTest.class.getName())
206-
.getConstructor()
207-
.newInstance();
208-
brokenInputStreamTest.run();
209-
}
210-
211-
private static class ShadowLoader extends URLClassLoader implements Callable<Set<String>> {
212-
213-
private static final Logger logger = Logger.getLogger(ShadowLoader.class.getName());
214-
215-
private final Exception exception;
216-
private final Set<String> result = new TreeSet<String>();
217-
218-
ShadowLoader(ClassLoader original, Exception exception) {
219-
super(getClassPathUrls(original), original.getParent());
220-
this.exception = exception;
221-
}
222-
223-
private static URL[] getClassPathUrls(ClassLoader original) {
224-
return original instanceof URLClassLoader
225-
? ((URLClassLoader) original).getURLs()
226-
: parseJavaClassPath();
227-
}
228-
229-
/**
230-
* Returns the URLs in the class path specified by the {@code java.class.path} {@linkplain
231-
* System#getProperty system property}.
232-
*/
233-
// TODO(b/65488446): Use a new public API.
234-
private static URL[] parseJavaClassPath() {
235-
ImmutableList.Builder<URL> urls = ImmutableList.builder();
236-
for (String entry : Splitter.on(PATH_SEPARATOR.value()).split(JAVA_CLASS_PATH.value())) {
237-
try {
238-
try {
239-
urls.add(new File(entry).toURI().toURL());
240-
} catch (SecurityException e) { // File.toURI checks to see if the file is a directory
241-
urls.add(new URL("file", null, new File(entry).getAbsolutePath()));
242-
}
243-
} catch (MalformedURLException e) {
244-
logger.log(WARNING, "malformed classpath entry: " + entry, e);
245-
}
246-
}
247-
return urls.build().toArray(new URL[0]);
248-
}
249-
250-
@Override
251-
public Set<String> call() throws Exception {
252-
return result;
253-
}
254-
255-
@Override
256-
public InputStream getResourceAsStream(String resource) {
257-
// Make sure this is actually the resource we are expecting. If we're using JaCoCo or the
258-
// like, we might end up reading some other resource, and we don't want to break that.
259-
if (resource.startsWith("com/google/auto")) {
260-
result.add(resource);
261-
return new BrokenInputStream(super.getResourceAsStream(resource));
262-
} else {
263-
return super.getResourceAsStream(resource);
264-
}
265-
}
266-
267-
private class BrokenInputStream extends InputStream {
268-
private final InputStream original;
269-
private int count = 0;
270-
271-
BrokenInputStream(InputStream original) {
272-
this.original = original;
273-
}
274-
275-
@Override
276-
public int read() throws IOException {
277-
if (++count > 10) {
278-
result.add("threw");
279-
if (exception instanceof IOException) {
280-
throw (IOException) exception;
281-
}
282-
throw (RuntimeException) exception;
283-
}
284-
return original.read();
285-
}
286-
}
287-
}
288-
289-
public static class BrokenInputStreamTest implements Runnable {
290-
@Override
291-
public void run() {
292-
Template template = TemplateVars.parsedTemplateForResource("autovalue.vm");
293-
assertThat(template).isNotNull();
294-
String resourceName =
295-
Reflection.getPackageName(getClass()).replace('.', '/') + "/autovalue.vm";
296-
@SuppressWarnings("unchecked")
297-
Callable<Set<String>> myLoader = (Callable<Set<String>>) getClass().getClassLoader();
298-
try {
299-
Set<String> result = myLoader.call();
300-
assertThat(result).contains(resourceName);
301-
assertThat(result).contains("threw");
302-
} catch (Exception e) {
303-
throw new AssertionError(e);
304-
}
305-
}
306-
}
307161
}

0 commit comments

Comments
 (0)