001/* 002 * Copyright (C) 2022 - 2023, the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package io.github.ascopes.jct.filemanagers; 017 018import io.github.ascopes.jct.utils.LoomPolyfill; 019import io.github.ascopes.jct.utils.ToStringBuilder; 020import java.lang.reflect.InvocationHandler; 021import java.lang.reflect.Method; 022import java.lang.reflect.Parameter; 023import java.lang.reflect.Proxy; 024import java.util.Objects; 025import java.util.function.Supplier; 026import java.util.stream.Collectors; 027import java.util.stream.Stream; 028import org.apiguardian.api.API; 029import org.apiguardian.api.API.Status; 030import org.jspecify.annotations.Nullable; 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033 034/** 035 * A proxy that wraps a {@link JctFileManager} in a proxy that can log all interactions with the 036 * JavaFileManager, along with a corresponding stacktrace. 037 * 038 * <p>This is useful for diagnosing difficult-to-find errors being produced by {@code javac} 039 * during testing, however, it may produce a hefty performance overhead when in use. 040 * 041 * <p>All logs are emitted with the {@code DEBUG} logging level. 042 * 043 * @author Ashley Scopes 044 * @since 0.0.1 045 */ 046@API(since = "0.0.1", status = Status.STABLE) 047public final class LoggingFileManagerProxy implements InvocationHandler { 048 049 private final Logger logger; 050 private final JctFileManager inner; 051 private final boolean stackTraces; 052 private final ThreadLocal<Integer> stackDepth; 053 054 private LoggingFileManagerProxy(JctFileManager inner, boolean stackTraces) { 055 // Instance scoped for testing purposes. 056 logger = LoggerFactory.getLogger(LoggingFileManagerProxy.class); 057 this.inner = inner; 058 this.stackTraces = stackTraces; 059 stackDepth = ThreadLocal.withInitial(() -> 0); 060 } 061 062 /** 063 * Invoke the given call. 064 * 065 * @param proxy the proxy. 066 * @param method the method. 067 * @param args the arguments. 068 * @return the result. 069 * @throws Throwable any exception that is thrown. 070 */ 071 @Override 072 public Object invoke(Object proxy, Method method, Object @Nullable ... args) throws Throwable { 073 if (method.getName().equals("toString")) { 074 return toString(); 075 } 076 077 var thread = LoomPolyfill.getCurrentThread(); 078 var threadId = LoomPolyfill.getThreadId(thread); 079 080 var depth = incrementStackDepth(); 081 var returnType = method.getReturnType().getSimpleName(); 082 var methodName = method.getName(); 083 var paramStr = Stream 084 .of(method.getParameters()) 085 .map(Parameter::getType) 086 .map(Class::getSimpleName) 087 .collect(Collectors.joining(", ")); 088 089 // When no arguments are passed, the args array is 090 // null rather than a zero length array (i.e. Object[0]). 091 var argsStr = args == null ? "" : Stream 092 .of(args) 093 .map(Objects::toString) 094 .collect(Collectors.joining(", ")); 095 096 logger 097 .atDebug() 098 .setMessage(">>> [thread={}, depth={}] {} {}({}) called with ({}){}") 099 .addArgument(threadId) 100 .addArgument(depth) 101 .addArgument(returnType) 102 .addArgument(methodName) 103 .addArgument(paramStr) 104 .addArgument(argsStr) 105 .addArgument(stackTraceFormatter(thread.getStackTrace())) 106 .log(); 107 108 try { 109 var result = method.invoke(inner, args); 110 111 if (method.getReturnType().equals(void.class)) { 112 logger 113 .atDebug() 114 .setMessage("<<< [thread={}, depth={}] {} {}({}) completed") 115 .addArgument(threadId) 116 .addArgument(depth) 117 .addArgument(returnType) 118 .addArgument(methodName) 119 .addArgument(paramStr) 120 .log(); 121 } else { 122 logger 123 .atDebug() 124 .setMessage("<<< [thread={}, depth={}] {} {}({}) returned {}") 125 .addArgument(threadId) 126 .addArgument(depth) 127 .addArgument(returnType) 128 .addArgument(methodName) 129 .addArgument(paramStr) 130 .addArgument(result) 131 .log(); 132 } 133 134 return result; 135 136 } catch (ReflectiveOperationException ex) { 137 138 logger 139 .atDebug() 140 .setMessage("!!! [thread={}, depth={}] {} {}({}) threw exception") 141 .addArgument(threadId) 142 .addArgument(depth) 143 .addArgument(returnType) 144 .addArgument(methodName) 145 .addArgument(paramStr) 146 .addArgument(ex.getCause()) 147 .log(); 148 149 throw ex.getCause(); 150 151 } finally { 152 decrementStackDepth(); 153 } 154 } 155 156 @Override 157 public String toString() { 158 return new ToStringBuilder(this) 159 .attribute("inner", inner) 160 .attribute("stackTraces", stackTraces) 161 .toString(); 162 } 163 164 private int incrementStackDepth() { 165 var depth = stackDepth.get() + 1; 166 stackDepth.set(depth); 167 return depth; 168 } 169 170 private void decrementStackDepth() { 171 var depth = stackDepth.get() - 1; 172 stackDepth.set(depth); 173 } 174 175 private Supplier<String> stackTraceFormatter(StackTraceElement[] stackTrace) { 176 if (!stackTraces) { 177 return () -> ""; 178 } 179 180 return () -> Stream 181 .of(stackTrace) 182 .map(frame -> "\n\t" + frame) 183 .collect(Collectors.joining()); 184 } 185 186 /** 187 * Wrap the given {@link JctFileManager} in a proxy that logs any calls. 188 * 189 * @param manager the manager to wrap. 190 * @param stackTraces {@code true} to dump stacktraces on each interception, or {@code false} to 191 * omit them. 192 * @return the proxy {@link JctFileManager} to use. 193 */ 194 public static JctFileManager wrap(JctFileManager manager, boolean stackTraces) { 195 return (JctFileManager) Proxy.newProxyInstance( 196 JctFileManager.class.getClassLoader(), 197 new Class<?>[]{JctFileManager.class}, 198 new LoggingFileManagerProxy(manager, stackTraces) 199 ); 200 } 201}