diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java index a67afd221..a56eaf695 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java @@ -88,6 +88,11 @@ public IWatchpoint createWatchPoint(String className, String fieldName, String a @Override public void setExceptionBreakpoints(boolean notifyCaught, boolean notifyUncaught) { + setExceptionBreakpoints(notifyCaught, notifyUncaught, null, null); + } + + @Override + public void setExceptionBreakpoints(boolean notifyCaught, boolean notifyUncaught, String[] classFilters, String[] classExclusionFilters) { EventRequestManager manager = vm.eventRequestManager(); ArrayList legacy = new ArrayList<>(manager.exceptionRequests()); manager.deleteEventRequests(legacy); @@ -108,6 +113,16 @@ public void setExceptionBreakpoints(boolean notifyCaught, boolean notifyUncaught // get only the uncaught exceptions ExceptionRequest request = manager.createExceptionRequest(null, notifyCaught, notifyUncaught); request.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); + if (classFilters != null) { + for (String classFilter : classFilters) { + request.addClassFilter(classFilter); + } + } + if (classExclusionFilters != null) { + for (String exclusionFilter : classExclusionFilters) { + request.addClassExclusionFilter(exclusionFilter); + } + } request.enable(); } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSettings.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSettings.java index 5933517de..f3975fbfe 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSettings.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSettings.java @@ -11,14 +11,21 @@ package com.microsoft.java.debug.core; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; import com.google.gson.JsonSyntaxException; import com.google.gson.annotations.SerializedName; import com.microsoft.java.debug.core.protocol.JsonUtils; +import com.microsoft.java.debug.core.protocol.Requests.ClassFilters; +import com.microsoft.java.debug.core.protocol.Requests.StepFilters; public final class DebugSettings { private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); + private static Set listeners = + Collections.newSetFromMap(new ConcurrentHashMap()); private static DebugSettings current = new DebugSettings(); public int maxStringLength = 0; @@ -31,6 +38,9 @@ public final class DebugSettings { public String logLevel; public String javaHome; public HotCodeReplace hotCodeReplace = HotCodeReplace.MANUAL; + public StepFilters stepFilters = new StepFilters(); + public ClassFilters exceptionFilters = new ClassFilters(); + public boolean exceptionFiltersUpdated = false; public static DebugSettings getCurrent() { return current; @@ -44,7 +54,11 @@ public static DebugSettings getCurrent() { */ public void updateSettings(String jsonSettings) { try { + DebugSettings oldSettings = current; current = JsonUtils.fromJson(jsonSettings, DebugSettings.class); + for (IDebugSettingChangeListener listener : listeners) { + listener.update(oldSettings, current); + } } catch (JsonSyntaxException ex) { logger.severe(String.format("Invalid json for debugSettings: %s, %s", jsonSettings, ex.getMessage())); } @@ -54,6 +68,14 @@ private DebugSettings() { } + public static boolean addDebugSettingChangeListener(IDebugSettingChangeListener listener) { + return listeners.add(listener); + } + + public static boolean removeDebugSettingChangeListener(IDebugSettingChangeListener listener) { + return listeners.remove(listener); + } + public static enum HotCodeReplace { @SerializedName("manual") MANUAL, @@ -62,4 +84,8 @@ public static enum HotCodeReplace { @SerializedName("never") NEVER } + + public static interface IDebugSettingChangeListener { + public void update(DebugSettings oldSettings, DebugSettings newSettings); + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugUtility.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugUtility.java index edfb6d4b3..1202a30f3 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugUtility.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugUtility.java @@ -313,7 +313,21 @@ public static IDebugSession attach(VirtualMachineManager vmManager, String hostN * @return the new step request. */ public static StepRequest createStepOverRequest(ThreadReference thread, String[] stepFilters) { - return createStepRequest(thread, StepRequest.STEP_LINE, StepRequest.STEP_OVER, stepFilters); + return createStepOverRequest(thread, null, stepFilters); + } + + /** + * Create a step over request on the specified thread. + * @param thread + * the target thread. + * @param classFilters + * restricts the step event to those matching the given class patterns when stepping. + * @param classExclusionFilters + * restricts the step event to those not matching the given class patterns when stepping. + * @return the new step request. + */ + public static StepRequest createStepOverRequest(ThreadReference thread, String[] classFilters, String[] classExclusionFilters) { + return createStepRequest(thread, StepRequest.STEP_LINE, StepRequest.STEP_OVER, classFilters, classExclusionFilters); } /** @@ -325,7 +339,21 @@ public static StepRequest createStepOverRequest(ThreadReference thread, String[] * @return the new step request. */ public static StepRequest createStepIntoRequest(ThreadReference thread, String[] stepFilters) { - return createStepRequest(thread, StepRequest.STEP_LINE, StepRequest.STEP_INTO, stepFilters); + return createStepIntoRequest(thread, null, stepFilters); + } + + /** + * Create a step into request on the specified thread. + * @param thread + * the target thread. + * @param classFilters + * restricts the step event to those matching the given class patterns when stepping. + * @param classExclusionFilters + * restricts the step event to those not matching the given class patterns when stepping. + * @return the new step request. + */ + public static StepRequest createStepIntoRequest(ThreadReference thread, String[] classFilters, String[] classExclusionFilters) { + return createStepRequest(thread, StepRequest.STEP_LINE, StepRequest.STEP_INTO, classFilters, classExclusionFilters); } /** @@ -337,14 +365,33 @@ public static StepRequest createStepIntoRequest(ThreadReference thread, String[] * @return the new step request. */ public static StepRequest createStepOutRequest(ThreadReference thread, String[] stepFilters) { - return createStepRequest(thread, StepRequest.STEP_LINE, StepRequest.STEP_OUT, stepFilters); + return createStepOutRequest(thread, null, stepFilters); } - private static StepRequest createStepRequest(ThreadReference thread, int stepSize, int stepDepth, String[] stepFilters) { + /** + * Create a step out request on the specified thread. + * @param thread + * the target thread. + * @param classFilters + * restricts the step event to those matching the given class patterns when stepping. + * @param classExclusionFilters + * restricts the step event to those not matching the given class patterns when stepping. + * @return the new step request. + */ + public static StepRequest createStepOutRequest(ThreadReference thread, String[] classFilters, String[] classExclusionFilters) { + return createStepRequest(thread, StepRequest.STEP_LINE, StepRequest.STEP_OUT, classFilters, classExclusionFilters); + } + + private static StepRequest createStepRequest(ThreadReference thread, int stepSize, int stepDepth, String[] classFilters, String[] classExclusionFilters) { StepRequest request = thread.virtualMachine().eventRequestManager().createStepRequest(thread, stepSize, stepDepth); - if (stepFilters != null) { - for (String stepFilter : stepFilters) { - request.addClassExclusionFilter(stepFilter); + if (classFilters != null) { + for (String classFilter : classFilters) { + request.addClassFilter(classFilter); + } + } + if (classExclusionFilters != null) { + for (String exclusionFilter : classExclusionFilters) { + request.addClassExclusionFilter(exclusionFilter); } } request.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IDebugSession.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IDebugSession.java index ec09aff29..24138fef1 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IDebugSession.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IDebugSession.java @@ -34,6 +34,8 @@ public interface IDebugSession { void setExceptionBreakpoints(boolean notifyCaught, boolean notifyUncaught); + void setExceptionBreakpoints(boolean notifyCaught, boolean notifyUncaught, String[] classFilters, String[] classExclusionFilters); + // TODO: createFunctionBreakpoint Process process(); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java index bd29f9946..248f0f803 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java @@ -13,17 +13,24 @@ import java.nio.charset.Charset; import java.nio.file.Path; +import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; +import com.microsoft.java.debug.core.DebugSettings; import com.microsoft.java.debug.core.IDebugSession; import com.microsoft.java.debug.core.adapter.variables.IVariableFormatter; import com.microsoft.java.debug.core.adapter.variables.VariableFormatterFactory; import com.microsoft.java.debug.core.protocol.IProtocolServer; import com.microsoft.java.debug.core.protocol.Requests.StepFilters; +import org.apache.commons.lang3.ArrayUtils; + public class DebugAdapterContext implements IDebugAdapterContext { private static final int MAX_CACHE_ITEMS = 10000; + private final StepFilters defaultFilters = new StepFilters(); private Map sourceMappingCache = Collections.synchronizedMap(new LRUCache<>(MAX_CACHE_ITEMS)); private IProviderContext providerContext; private IProtocolServer server; @@ -235,12 +242,28 @@ public String getMainClass() { @Override public void setStepFilters(StepFilters stepFilters) { + // For backward compatibility, merge the classNameFilters to skipClasses. + if (stepFilters != null && ArrayUtils.isNotEmpty(stepFilters.classNameFilters)) { + Set patterns = new LinkedHashSet<>(); + if (ArrayUtils.isNotEmpty(stepFilters.skipClasses)) { + patterns.addAll(Arrays.asList(stepFilters.skipClasses)); + } + + patterns.addAll(Arrays.asList(stepFilters.classNameFilters)); + stepFilters.skipClasses = patterns.toArray(new String[0]); + } this.stepFilters = stepFilters; } @Override public StepFilters getStepFilters() { - return stepFilters; + if (stepFilters != null) { + return stepFilters; + } else if (DebugSettings.getCurrent().stepFilters != null) { + return DebugSettings.getCurrent().stepFilters; + } + + return defaultFilters; } @Override diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RestartFrameHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RestartFrameHandler.java index 71ed2764e..3479c9a8a 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RestartFrameHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RestartFrameHandler.java @@ -102,7 +102,7 @@ private void popStackFrames(IDebugAdapterContext context, ThreadReference thread } private void stepInto(IDebugAdapterContext context, ThreadReference thread) { - StepRequest request = DebugUtility.createStepIntoRequest(thread, context.getStepFilters().classNameFilters); + StepRequest request = DebugUtility.createStepIntoRequest(thread, context.getStepFilters().allowClasses, context.getStepFilters().skipClasses); context.getDebugSession().getEventHub().stepEvents().filter(debugEvent -> request.equals(debugEvent.event.request())).take(1).subscribe(debugEvent -> { debugEvent.shouldResume = false; // Have to send two events to keep the UI sync with the step in operations: diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetExceptionBreakpointsRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetExceptionBreakpointsRequestHandler.java index 6584e791b..3a4e642c8 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetExceptionBreakpointsRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetExceptionBreakpointsRequestHandler.java @@ -17,17 +17,27 @@ import org.apache.commons.lang3.ArrayUtils; +import com.microsoft.java.debug.core.DebugSettings; +import com.microsoft.java.debug.core.IDebugSession; +import com.microsoft.java.debug.core.DebugSettings.IDebugSettingChangeListener; import com.microsoft.java.debug.core.adapter.AdapterUtils; import com.microsoft.java.debug.core.adapter.ErrorCode; import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; import com.microsoft.java.debug.core.protocol.Messages.Response; import com.microsoft.java.debug.core.protocol.Requests.Arguments; +import com.microsoft.java.debug.core.protocol.Requests.ClassFilters; import com.microsoft.java.debug.core.protocol.Requests.Command; import com.microsoft.java.debug.core.protocol.Requests.SetExceptionBreakpointsArguments; import com.microsoft.java.debug.core.protocol.Types; +import com.sun.jdi.event.VMDeathEvent; +import com.sun.jdi.event.VMDisconnectEvent; -public class SetExceptionBreakpointsRequestHandler implements IDebugRequestHandler { +public class SetExceptionBreakpointsRequestHandler implements IDebugRequestHandler, IDebugSettingChangeListener { + private IDebugSession debugSession = null; + private boolean isInitialized = false; + private boolean notifyCaught = false; + private boolean notifyUncaught = false; @Override public List getTargetCommands() { @@ -35,17 +45,28 @@ public List getTargetCommands() { } @Override - public CompletableFuture handle(Command command, Arguments arguments, Response response, IDebugAdapterContext context) { + public synchronized CompletableFuture handle(Command command, Arguments arguments, Response response, IDebugAdapterContext context) { if (context.getDebugSession() == null) { return AdapterUtils.createAsyncErrorResponse(response, ErrorCode.EMPTY_DEBUG_SESSION, "Empty debug session."); } + if (!isInitialized) { + isInitialized = true; + debugSession = context.getDebugSession(); + DebugSettings.addDebugSettingChangeListener(this); + debugSession.getEventHub().events().subscribe(debugEvent -> { + if (debugEvent.event instanceof VMDeathEvent + || debugEvent.event instanceof VMDisconnectEvent) { + DebugSettings.removeDebugSettingChangeListener(this); + } + }); + } + String[] filters = ((SetExceptionBreakpointsArguments) arguments).filters; try { - boolean notifyCaught = ArrayUtils.contains(filters, Types.ExceptionBreakpointFilter.CAUGHT_EXCEPTION_FILTER_NAME); - boolean notifyUncaught = ArrayUtils.contains(filters, Types.ExceptionBreakpointFilter.UNCAUGHT_EXCEPTION_FILTER_NAME); - - context.getDebugSession().setExceptionBreakpoints(notifyCaught, notifyUncaught); + this.notifyCaught = ArrayUtils.contains(filters, Types.ExceptionBreakpointFilter.CAUGHT_EXCEPTION_FILTER_NAME); + this.notifyUncaught = ArrayUtils.contains(filters, Types.ExceptionBreakpointFilter.UNCAUGHT_EXCEPTION_FILTER_NAME); + setExceptionBreakpoints(context.getDebugSession(), this.notifyCaught, this.notifyUncaught); return CompletableFuture.completedFuture(response); } catch (Exception ex) { throw AdapterUtils.createCompletionException( @@ -55,4 +76,21 @@ public CompletableFuture handle(Command command, Arguments arguments, } } + private void setExceptionBreakpoints(IDebugSession debugSession, boolean notifyCaught, boolean notifyUncaught) { + ClassFilters exceptionFilters = DebugSettings.getCurrent().exceptionFilters; + String[] classFilters = (exceptionFilters == null ? null : exceptionFilters.allowClasses); + String[] classExclusionFilters = (exceptionFilters == null ? null : exceptionFilters.skipClasses); + debugSession.setExceptionBreakpoints(notifyCaught, notifyUncaught, classFilters, classExclusionFilters); + } + + @Override + public synchronized void update(DebugSettings oldSettings, DebugSettings newSettings) { + try { + if (newSettings != null && newSettings.exceptionFiltersUpdated) { + setExceptionBreakpoints(debugSession, notifyCaught, notifyUncaught); + } + } catch (Exception ex) { + DebugSettings.removeDebugSettingChangeListener(this); + } + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StepRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StepRequestHandler.java index 2c75462bc..a148ce20b 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StepRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StepRequestHandler.java @@ -76,13 +76,14 @@ public CompletableFuture handle(Command command, Arguments arguments, if (command == Command.STEPIN) { threadState.pendingStepRequest = DebugUtility.createStepIntoRequest(thread, - context.getStepFilters().classNameFilters); + context.getStepFilters().allowClasses, + context.getStepFilters().skipClasses); } else if (command == Command.STEPOUT) { threadState.pendingStepRequest = DebugUtility.createStepOutRequest(thread, - context.getStepFilters().classNameFilters); + context.getStepFilters().allowClasses, + context.getStepFilters().skipClasses); } else { - threadState.pendingStepRequest = DebugUtility.createStepOverRequest(thread, - context.getStepFilters().classNameFilters); + threadState.pendingStepRequest = DebugUtility.createStepOverRequest(thread, null); } threadState.pendingStepRequest.enable(); DebugUtility.resumeThread(thread); @@ -133,21 +134,14 @@ private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, if (threadState.pendingStepType == Command.STEPIN) { int currentStackDepth = thread.frameCount(); Location currentStepLocation = getTopFrame(thread).location(); - // Check if the step into operation stepped through the filtered code and stopped at an un-filtered location. - if (threadState.stackDepth + 1 < thread.frameCount()) { - // Create another stepOut request to return back where we started the step into. - threadState.pendingStepRequest = DebugUtility.createStepOutRequest(thread, - context.getStepFilters().classNameFilters); - threadState.pendingStepRequest.enable(); - debugEvent.shouldResume = true; - return; - } + // If the ending step location is filtered, or same as the original location where the step into operation is originated, // do another step of the same kind. if (shouldFilterLocation(threadState.stepLocation, currentStepLocation, context) || shouldDoExtraStepInto(threadState.stackDepth, threadState.stepLocation, currentStackDepth, currentStepLocation)) { threadState.pendingStepRequest = DebugUtility.createStepIntoRequest(thread, - context.getStepFilters().classNameFilters); + context.getStepFilters().allowClasses, + context.getStepFilters().skipClasses); threadState.pendingStepRequest.enable(); debugEvent.shouldResume = true; return; @@ -169,7 +163,8 @@ private boolean isStepFiltersConfigured(StepFilters filters) { if (filters == null) { return false; } - return ArrayUtils.isNotEmpty(filters.classNameFilters) || filters.skipConstructors + return ArrayUtils.isNotEmpty(filters.allowClasses) || ArrayUtils.isNotEmpty(filters.skipClasses) + || ArrayUtils.isNotEmpty(filters.classNameFilters) || filters.skipConstructors || filters.skipStaticInitializers || filters.skipSynthetics; } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java index 1874825e7..f62a241a1 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java @@ -41,8 +41,36 @@ public static class InitializeArguments extends Arguments { public boolean supportsRunInTerminalRequest; } - public static class StepFilters { - public String[] classNameFilters = new String[0]; + public static class ClassFilters { + /** + * Restricts the events generated by the request to those whose location is + * in a class whose name matches this restricted regular expression. Regular + * expressions are limited to exact matches and patterns that begin with '*' + * or end with '*'; for example, "*.Foo" or "java.*". + * + * This property corresponds to the ClassFilter (include filter). Multiple + * filters are applied with CUT-OFF AND. Only events that satisfied all + * filters are placed in the event queue, that means several include filters + * are handled as "A and B and C", not "A or B or C". + */ + public String[] allowClasses = new String[0]; + + /** + * Restricts the events generated by the request to those whose location is + * in a class whose name does not match this restricted regular expression, e.g. + * "java.*" or "*.Foo". + * + * This property corrsponds to the ClassExclusionFilter (exclude filter). + */ + public String[] skipClasses = new String[0]; + } + + public static class StepFilters extends ClassFilters { + /** + * Deprecated - please use {@link ClassFilters#skipClasses } instead. + */ + @Deprecated + public String[] classNameFilters; public boolean skipSynthetics; public boolean skipStaticInitializers; public boolean skipConstructors; @@ -54,7 +82,7 @@ public static class LaunchBaseArguments extends Arguments { public String request; public String projectName; public String[] sourcePaths = new String[0]; - public StepFilters stepFilters = new StepFilters(); + public StepFilters stepFilters; } public static enum CONSOLE { diff --git a/com.microsoft.java.debug.plugin/plugin.xml b/com.microsoft.java.debug.plugin/plugin.xml index 111e4118f..cd999ab09 100644 --- a/com.microsoft.java.debug.plugin/plugin.xml +++ b/com.microsoft.java.debug.plugin/plugin.xml @@ -18,6 +18,7 @@ + diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaClassFilter.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaClassFilter.java new file mode 100644 index 000000000..5d5bc7b0b --- /dev/null +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaClassFilter.java @@ -0,0 +1,307 @@ +/******************************************************************************* + * Copyright (c) 2020 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Microsoft Corporation - initial API and implementation + *******************************************************************************/ + +package com.microsoft.java.debug.plugin.internal; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Map.Entry; +import java.util.Queue; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import com.microsoft.java.debug.core.Configuration; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.core.runtime.IPath; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.launching.JavaRuntime; +import org.eclipse.jdt.ls.core.internal.ProjectUtils; + +public class JavaClassFilter { + private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); + private static final int BLOCK_NONE = 0; + private static final int BLOCK_JDK = 1; + private static final int BLOCK_LIB = 2; + private static final int BLOCK_BIN = 3; + + /** + * Substitute the variables in the exclusion filter list. + * + *

+ * For example, a sample input could be: + * [ + * "$JDK", + * "$Libraries", + * "junit.*", + * "java.lang.ClassLoader" + * ]. + * + * Variable "$JDK" means skipping the classes from the JDK, and variable "$Libraries" + * means skipping the classes from the application libraries. + * + *

+ * This function will resolve the packages belonging to the variable group first, and + * then use a greedy algorithm to generate a list of wildcards to cover these packages. + */ + public static String[] resolveClassFilters(List unresolvedFilters) { + if (unresolvedFilters == null || unresolvedFilters.isEmpty()) { + return new String[0]; + } + + int variableScope = BLOCK_NONE; + Set hardcodePatterns = new LinkedHashSet<>(); + for (Object filter : unresolvedFilters) { + if (Objects.equals("$JDK", filter)) { + variableScope = variableScope | BLOCK_JDK; + } else if (Objects.equals("$Libraries", filter)) { + variableScope = variableScope | BLOCK_LIB; + } else if (filter instanceof String) { + String value = (String) filter; + if (StringUtils.isNotBlank(value)) { + hardcodePatterns.add(value.trim()); + } + } + } + + if (variableScope == BLOCK_NONE) { + return hardcodePatterns.toArray(new String[0]); + } + + Set blackList = new LinkedHashSet<>(); + Set whiteList = new LinkedHashSet<>(); + IJavaProject[] javaProjects = ProjectUtils.getJavaProjects(); + for (IJavaProject javaProject : javaProjects) { + try { + IPackageFragmentRoot[] roots = javaProject.getAllPackageFragmentRoots(); + for (IPackageFragmentRoot root : roots) { + if (isOnBlackList(root, variableScope)) { + collectPackages(root, blackList); + } else { + collectPackages(root, whiteList); + } + } + } catch (JavaModelException e) { + logger.log(Level.SEVERE, String.format("Failed to get the classpath entry for the PackageFragmentRoot: %s", e.toString()), e); + } + } + + return convertToExclusionPatterns(blackList, whiteList, hardcodePatterns); + } + + private static boolean isOnBlackList(IPackageFragmentRoot root, int variableScope) throws JavaModelException { + if (root.isArchive()) { + if (variableScope == BLOCK_BIN) { + return true; + } + + boolean isJDK = isJDKPackageFragmentRoot(root); + return (variableScope == BLOCK_JDK && isJDK) || (variableScope == BLOCK_LIB && !isJDK); + } + + return false; + } + + private static boolean isJDKPackageFragmentRoot(IPackageFragmentRoot root) throws JavaModelException { + if (root.getRawClasspathEntry() != null) { + IPath path = root.getRawClasspathEntry().getPath(); + return path != null && path.segmentCount() > 0 + && Objects.equals(JavaRuntime.JRE_CONTAINER, path.segment(0)); + } + + return false; + } + + private static void collectPackages(IPackageFragmentRoot root, Set result) throws JavaModelException { + for (IJavaElement javaElement : root.getChildren()) { + String elementName = javaElement.getElementName(); + if (javaElement instanceof IPackageFragment + && ((IPackageFragment) javaElement).hasChildren()) { + if (StringUtils.isNotBlank(elementName)) { + result.add(elementName); + } + } + } + } + + private static String[] convertToExclusionPatterns(Collection blackList, + Collection whiteList, Collection hardcodePatterns) { + List hardcodeBlockedPackages = hardcodePatterns.stream() + .filter(pattern -> pattern.endsWith(".*")) + .map(pattern -> pattern.substring(0, pattern.length() - 2)) + .collect(Collectors.toList()); + Trie hardcodeBlackTree = new Trie(hardcodeBlockedPackages); + + // Remove those packages that are on the user hardcode black list. + List newWhiteList = whiteList.stream() + .filter(pattern -> !hardcodeBlackTree.isPrefix(pattern)) + .collect(Collectors.toList()); + + // Superimpose the white tree on the black tree, then gray out the nodes + // with the same name. + Trie whiteTree = new Trie(newWhiteList); + Trie blackTree = new Trie(blackList); + superimpose(whiteTree, blackTree); + + // Generate some wildcard patterns to cover all items in the black list. + List wildcardPatterns = new ArrayList<>(); + traverse(blackTree.root, 0, wildcardPatterns, new ArrayList<>()); + + // Append the hardcode patterns to the result. + for (String name : hardcodePatterns) { + if (!blackTree.wildcardMatch(name)) { + wildcardPatterns.add(name); + } + } + + return wildcardPatterns.toArray(new String[0]); + } + + private static void superimpose(Trie upTree, Trie downTree) { + Queue upQueue = new LinkedList<>(); + Queue downQueue = new LinkedList<>(); + upQueue.offer(upTree.root); + downQueue.offer(downTree.root); + while (!upQueue.isEmpty()) { + TrieNode upNode = upQueue.poll(); + TrieNode downNode = downQueue.poll(); + downNode.isGray = true; + for (Entry entry : upNode.children.entrySet()) { + if (downNode.children.containsKey(entry.getKey())) { + upQueue.offer(entry.getValue()); + downQueue.offer(downNode.children.get(entry.getKey())); + } + } + } + } + + private static void traverse(TrieNode root, int depth, List result, List parent) { + // If the node is gray, that means it also appears in the white list. + // We cannot use it to generate a wildcard pattern. + if (!root.isGray) { + String[] names = new String[depth + 1]; + for (int i = 0; i < depth - 1; i++) { + names[i] = parent.get(i); + } + + names[depth - 1] = root.name; + names[depth] = "*"; + result.add(String.join(".", names)); + return; + } + + if (depth > 0) { + if (parent.size() < depth) { + parent.add(root.name); + } else { + parent.set(depth - 1, root.name); + } + } + + for (TrieNode child : root.children.values()) { + traverse(child, depth + 1, result, parent); + } + } + + private static class Trie { + private TrieNode root = new TrieNode(); + + public Trie(Collection names) { + for (String name : names) { + insert(name); + } + } + + public void insert(String name) { + if (StringUtils.isBlank(name)) { + return; + } + + String[] names = name.split("\\."); + TrieNode currentNode = this.root; + for (int i = 0; i < names.length; i++) { + TrieNode node; + if (currentNode.children.containsKey(names[i])) { + node = currentNode.children.get(names[i]); + } else { + node = new TrieNode(names[i]); + currentNode.children.put(names[i], node); + } + + currentNode = node; + } + + currentNode.isLeaf = true; + } + + public boolean isPrefix(String name) { + String[] names = name.split("\\."); + TrieNode currentNode = this.root; + for (int i = 0; i < names.length; i++) { + TrieNode node = currentNode.children.get(names[i]); + if (node == null) { + break; + } + + currentNode = node; + } + + return currentNode != this.root && currentNode.isLeaf; + } + + /** + * Check whether the name can be covered by the wildcards generated from + * this trie. + */ + public boolean wildcardMatch(String name) { + String[] names = name.split("\\."); + Map children = this.root.children; + for (int i = 0; i < names.length; i++) { + TrieNode node = children.get(names[i]); + if (node == null) { + break; + } else if (!node.isGray) { + return true; + } + + children = node.children; + } + + return false; + } + } + + private static class TrieNode { + private String name; + private Map children = new LinkedHashMap<>(); + private boolean isLeaf = false; + private boolean isGray = false; + + public TrieNode() { + } + + public TrieNode(String name) { + this.name = name; + } + } +} diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebugDelegateCommandHandler.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebugDelegateCommandHandler.java index 025949a9b..5db924442 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebugDelegateCommandHandler.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebugDelegateCommandHandler.java @@ -45,6 +45,7 @@ public class JavaDebugDelegateCommandHandler implements IDelegateCommandHandler public static final String IS_ON_CLASSPATH = "vscode.java.isOnClasspath"; public static final String RESOLVE_JAVA_EXECUTABLE = "vscode.java.resolveJavaExecutable"; public static final String FETCH_PLATFORM_SETTINGS = "vscode.java.fetchPlatformSettings"; + public static final String RESOLVE_CLASSFILTERS = "vscode.java.resolveClassFilters"; @Override public Object executeCommand(String commandId, List arguments, IProgressMonitor progress) throws Exception { @@ -84,6 +85,8 @@ public Object executeCommand(String commandId, List arguments, IProgress return ResolveJavaExecutableHandler.resolveJavaExecutable(arguments); case FETCH_PLATFORM_SETTINGS: return PlatformSettings.getPlatformSettings(); + case RESOLVE_CLASSFILTERS: + return JavaClassFilter.resolveClassFilters(arguments); default: break; }