Skip to content

Commit 1ec1f3b

Browse files
author
Graham Allan
committed
First example complete. A matcher that checks you're using method references by discovering the java source file from the class, scans the filesystem, and reads the source to check there's a :: before the method name. Phew.
1 parent 4d713df commit 1ec1f3b

4 files changed

Lines changed: 253 additions & 1 deletion

File tree

src/main/java/org/adoptopenjdk/lambda/tutorial/exercise4/Document.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,22 @@
55
import java.util.List;
66

77
public final class Document {
8+
private final String title;
89
private final List<Page> pages;
910

10-
public Document(List<Page> pages) {
11+
public Document(String title, List<Page> pages) {
12+
this.title = title;
1113
this.pages = Collections.unmodifiableList(new ArrayList<>(pages));
1214
}
1315

1416
public String getPageContent(Integer pageNumber) {
1517
return this.pages.get(pageNumber).getContent();
1618
}
1719

20+
public String getTitle() {
21+
return this.title;
22+
}
23+
1824
public static final class Page {
1925
private final String content;
2026

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.adoptopenjdk.lambda.tutorial;
2+
3+
/*
4+
* #%L
5+
* lambda-tutorial
6+
* %%
7+
* Copyright (C) 2013 Adopt OpenJDK
8+
* %%
9+
* This program is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU General Public License as
11+
* published by the Free Software Foundation, either version 2 of the
12+
* License, or (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU General Public
20+
* License along with this program. If not, see
21+
* <http://www.gnu.org/licenses/gpl-2.0.html>.
22+
* #L%
23+
*/
24+
25+
import org.adoptopenjdk.lambda.tutorial.exercise4.Document;
26+
27+
import java.util.Arrays;
28+
import java.util.List;
29+
30+
import static java.util.stream.Collectors.toList;
31+
32+
public class Documents {
33+
34+
/**
35+
* Return the titles from a list of documents.
36+
*/
37+
public static List<String> titlesOf(Document... documents) {
38+
return Arrays.stream(documents)
39+
.map(d -> d.getTitle())
40+
.collect(toList());
41+
}
42+
}

src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_4_Test.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,17 @@
2222
* #L%
2323
*/
2424

25+
import org.adoptopenjdk.lambda.tutorial.exercise4.Document;
26+
import org.adoptopenjdk.lambda.tutorial.exercise4.Document.Page;
27+
import org.hamcrest.Matchers;
28+
import org.junit.Test;
29+
30+
import java.util.Arrays;
2531
import java.util.function.Consumer;
2632

33+
import static org.adoptopenjdk.lambda.tutorial.util.CodeUsesMethodReferencesMatcher.usesMethodReferences;
34+
import static org.hamcrest.MatcherAssert.assertThat;
35+
2736
/**
2837
* Exercise 4 - Method References
2938
* <p>
@@ -109,6 +118,7 @@
109118
* </p>
110119
* <p>
111120
* <em>Constructor belonging to a particular class</em>
121+
* <br>
112122
* By now, we know how to use method references for static methods and instance methods, that leaves an odd case:
113123
* constructors.
114124
* <p>
@@ -134,5 +144,30 @@
134144
@SuppressWarnings("unchecked")
135145
public class Exercise_4_Test {
136146

147+
/**
148+
* The <code>Documents</code> class has a method which transforms a list of <code>Document</code> into a list of
149+
* their titles. The implementation has already been filled out, but it uses a lambda, as in:
150+
* <code>.map(document -> document.getTitle())</code>
151+
* <br>
152+
* Instead of using a lambda, use a method reference instead.
153+
*
154+
* @see Documents#titlesOf(Document...)
155+
* @see Document#getTitle()
156+
*
157+
*/
158+
@Test
159+
public void getListOfDocumentTitlesUsingInstanceMethodReference() {
160+
Document expenses = new Document("My Expenses",
161+
Arrays.asList(new Page("LJC Open Conference ticket: £25"), new Page("Beer stipend: £100")));
162+
Document toDoList = new Document("My ToDo List",
163+
Arrays.asList(new Page("Build a todo app"), new Page("Pick up dry cleaning")));
164+
Document certificates = new Document("My Certificates",
165+
Arrays.asList(new Page("Oracle Certified Professional"), new Page("Swimming 10m")));
166+
167+
assertThat(Documents.titlesOf(expenses, toDoList, certificates),
168+
Matchers.contains("My Expenses", "My ToDo List", "My Certificates"));
169+
assertThat(Documents.class, usesMethodReferences("getTitle"));
170+
171+
}
137172

138173
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package org.adoptopenjdk.lambda.tutorial.util;
2+
3+
/*
4+
* #%L
5+
* lambda-tutorial
6+
* %%
7+
* Copyright (C) 2013 Adopt OpenJDK
8+
* %%
9+
* This program is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU General Public License as
11+
* published by the Free Software Foundation, either version 2 of the
12+
* License, or (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU General Public
20+
* License along with this program. If not, see
21+
* <http://www.gnu.org/licenses/gpl-2.0.html>.
22+
* #L%
23+
*/
24+
25+
import org.hamcrest.Description;
26+
import org.hamcrest.TypeSafeDiagnosingMatcher;
27+
import org.objectweb.asm.ClassReader;
28+
import org.objectweb.asm.ClassVisitor;
29+
import org.objectweb.asm.Opcodes;
30+
31+
import java.io.File;
32+
import java.io.IOException;
33+
import java.nio.ByteBuffer;
34+
import java.nio.charset.StandardCharsets;
35+
import java.nio.file.Files;
36+
import java.nio.file.Path;
37+
import java.nio.file.Paths;
38+
import java.util.Arrays;
39+
import java.util.Optional;
40+
41+
import static java.util.stream.Collectors.toList;
42+
43+
public final class CodeUsesMethodReferencesMatcher extends TypeSafeDiagnosingMatcher<Class<?>> {
44+
45+
private final String methodName;
46+
47+
private CodeUsesMethodReferencesMatcher(String methodName) {
48+
this.methodName = methodName;
49+
}
50+
51+
public static CodeUsesMethodReferencesMatcher usesMethodReferences(String methodName) {
52+
return new CodeUsesMethodReferencesMatcher(methodName);
53+
}
54+
55+
@Override
56+
public void describeTo(Description description) {
57+
description.appendText("a source file using a method reference to invoke ").appendValue(methodName);
58+
}
59+
60+
61+
@Override
62+
protected boolean matchesSafely(Class<?> clazz, Description mismatchDescription) {
63+
try {
64+
Optional<String> sourceFileContent = getSourceContent(clazz);
65+
return sourceFileContent.map(c -> usesMethodReference(c, mismatchDescription)).orElseGet(() -> {
66+
mismatchDescription.appendText("could not read source file to discover if you used method references.");
67+
return false;
68+
});
69+
} catch (IOException e) {
70+
mismatchDescription.appendText("could not read source file to discover if you used method references.");
71+
mismatchDescription.appendValue(e);
72+
return false;
73+
}
74+
}
75+
76+
private boolean usesMethodReference(String sourceCode, Description mismatchDescription) {
77+
if (sourceCode.contains("::"+methodName)) {
78+
return true;
79+
} else {
80+
mismatchDescription.appendText("source code did not use a method reference to invoke " + methodName + ". ");
81+
context(sourceCode, methodName, mismatchDescription);
82+
return false;
83+
}
84+
}
85+
86+
private void context(String sourceCode, String methodName, Description mismatchDescription) {
87+
if (!sourceCode.contains(methodName)) {
88+
mismatchDescription.appendText("You did not appear to invoke the method at all.");
89+
} else {
90+
String[] lines = sourceCode.split("\\n");
91+
mismatchDescription.appendText("Actual invocations: ");
92+
mismatchDescription.appendValueList("[", ",", "]",
93+
Arrays.stream(lines).filter(l -> l.contains(methodName)).map(String::trim).collect(toList()));
94+
}
95+
}
96+
97+
private Optional<String> getSourceContent(Class<?> clazz) throws IOException {
98+
String sourceFileName = getSourceFileName(clazz);
99+
Optional<File> sourceFile = findPathTo(sourceFileName);
100+
101+
return sourceFile.map(this::toContent);
102+
}
103+
104+
private Optional<File> findPathTo(String sourceFileName) throws IOException {
105+
File cwd = new File(".");
106+
File rootOfProject = findRootOfProject(cwd);
107+
return findSourceFile(rootOfProject, sourceFileName);
108+
}
109+
110+
private String toContent(File file) {
111+
try {
112+
byte[] encoded = Files.readAllBytes(Paths.get(file.toURI()));
113+
return StandardCharsets.UTF_8.decode(ByteBuffer.wrap(encoded)).toString();
114+
} catch (IOException e) {
115+
throw new RuntimeException("Could not read Java source file.", e);
116+
}
117+
}
118+
119+
private Optional<File> findSourceFile(File rootOfProject, String sourceFileName) throws IOException {
120+
Path startingDir = Paths.get(rootOfProject.toURI());
121+
return Files.find(startingDir, 15, (path, attrs) -> path.endsWith(sourceFileName))
122+
.map(p -> new File(p.toUri()))
123+
.findFirst();
124+
}
125+
126+
private File findRootOfProject(File cwd) {
127+
File[] pomFiles = cwd.listFiles((file, name) -> { return name.equals("pom.xml"); });
128+
if (pomFiles != null && pomFiles.length == 1) {
129+
return cwd;
130+
} else if (cwd.getParentFile() == null) {
131+
throw new RuntimeException("Couldn't find directory containing pom.xml. Last looked in: " + cwd.getAbsolutePath());
132+
} else {
133+
return findRootOfProject(cwd.getParentFile());
134+
}
135+
}
136+
137+
private String getSourceFileName(Class<?> clazz) throws IOException {
138+
String resourceName = clazz.getName().replace(".", "/").concat(".class");
139+
ClassReader reader = new ClassReader(clazz.getClassLoader().getResourceAsStream(resourceName));
140+
SourceFileNameVisitor sourceFileNameVisitor = new SourceFileNameVisitor();
141+
reader.accept(sourceFileNameVisitor, 0);
142+
143+
return sourceFileNameVisitor.getSourceFile();
144+
}
145+
146+
147+
private static final class SourceFileNameVisitor extends ClassVisitor {
148+
149+
private String sourceFile = null;
150+
private boolean visitedYet = false;
151+
152+
public SourceFileNameVisitor() {
153+
super(Opcodes.ASM5);
154+
}
155+
156+
@Override
157+
public void visitSource(String source, String debug) {
158+
this.visitedYet = true;
159+
this.sourceFile = source;
160+
super.visitSource(source, debug);
161+
}
162+
163+
public String getSourceFile() {
164+
if (!visitedYet) throw new IllegalStateException("Must visit a class before asking for source file");
165+
return this.sourceFile;
166+
}
167+
}
168+
169+
}

0 commit comments

Comments
 (0)