package org.adoptopenjdk.lambda.tutorial.util; /* * #%L * lambda-tutorial * %% * Copyright (C) 2013 Adopt OpenJDK * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-2.0.html>. * #L% */ import org.hamcrest.Description; import org.hamcrest.TypeSafeDiagnosingMatcher; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Opcodes; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.Optional; import static java.util.stream.Collectors.toList; public final class CodeUsesMethodReferencesMatcher extends TypeSafeDiagnosingMatcher<Class<?>> { private final String methodName; private CodeUsesMethodReferencesMatcher(String methodName) { this.methodName = methodName; } public static CodeUsesMethodReferencesMatcher usesMethodReferences(String methodName) { return new CodeUsesMethodReferencesMatcher(methodName); } @Override public void describeTo(Description description) { description.appendText("a source file using a method reference to invoke ").appendValue(methodName); } @Override protected boolean matchesSafely(Class<?> clazz, Description mismatchDescription) { try { Optional<String> sourceFileContent = getSourceContent(clazz); return sourceFileContent.map(c -> usesMethodReference(c, mismatchDescription)).orElseGet(() -> { mismatchDescription.appendText("could not read source file to discover if you used method references."); return false; }); } catch (IOException e) { mismatchDescription.appendText("could not read source file to discover if you used method references."); mismatchDescription.appendValue(e); return false; } } private boolean usesMethodReference(String sourceCode, Description mismatchDescription) { if (sourceCode.contains("::"+methodName)) { return true; } else { mismatchDescription.appendText("source code did not use a method reference to invoke " + methodName + ". "); context(sourceCode, methodName, mismatchDescription); return false; } } private void context(String sourceCode, String methodName, Description mismatchDescription) { if (!sourceCode.contains(methodName)) { mismatchDescription.appendText("You did not appear to invoke the method at all."); } else { String[] lines = sourceCode.split("\\n"); mismatchDescription.appendText("Actual invocations: "); mismatchDescription.appendValueList("[", ",", "]", Arrays.stream(lines).filter(l -> l.contains(methodName)).map(String::trim).collect(toList())); } } private Optional<String> getSourceContent(Class<?> clazz) throws IOException { String sourceFileName = getSourceFileName(clazz); Optional<File> sourceFile = findPathTo(sourceFileName); return sourceFile.map(this::toContent); } private Optional<File> findPathTo(String sourceFileName) throws IOException { File cwd = new File("."); File rootOfProject = findRootOfProject(cwd); return findSourceFile(rootOfProject, sourceFileName); } private String toContent(File file) { try { byte[] encoded = Files.readAllBytes(Paths.get(file.toURI())); return StandardCharsets.UTF_8.decode(ByteBuffer.wrap(encoded)).toString(); } catch (IOException e) { throw new RuntimeException("Could not read Java source file.", e); } } private Optional<File> findSourceFile(File rootOfProject, String sourceFileName) throws IOException { Path startingDir = Paths.get(rootOfProject.toURI()); return Files.find(startingDir, 15, (path, attrs) -> path.endsWith(sourceFileName)) .map(p -> new File(p.toUri())) .findFirst(); } private File findRootOfProject(File cwd) { File[] pomFiles = cwd.listFiles((file, name) -> { return name.equals("pom.xml"); }); if (pomFiles != null && pomFiles.length == 1) { return cwd; } else if (cwd.getParentFile() == null) { throw new RuntimeException("Couldn't find directory containing pom.xml. Last looked in: " + cwd.getAbsolutePath()); } else { return findRootOfProject(cwd.getParentFile()); } } private String getSourceFileName(Class<?> clazz) throws IOException { String resourceName = clazz.getName().replace(".", "/").concat(".class"); ClassReader reader = new ClassReader(clazz.getClassLoader().getResourceAsStream(resourceName)); SourceFileNameVisitor sourceFileNameVisitor = new SourceFileNameVisitor(); reader.accept(sourceFileNameVisitor, 0); return sourceFileNameVisitor.getSourceFile(); } private static final class SourceFileNameVisitor extends ClassVisitor { private String sourceFile = null; private boolean visitedYet = false; public SourceFileNameVisitor() { super(Opcodes.ASM5); } @Override public void visitSource(String source, String debug) { this.visitedYet = true; this.sourceFile = source; super.visitSource(source, debug); } public String getSourceFile() { if (!visitedYet) throw new IllegalStateException("Must visit a class before asking for source file"); return this.sourceFile; } } }