-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathAppMapPackage.java
More file actions
281 lines (248 loc) · 9.3 KB
/
AppMapPackage.java
File metadata and controls
281 lines (248 loc) · 9.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
package com.appland.appmap.config;
import java.util.regex.Pattern;
import org.tinylog.TaggedLogger;
import com.appland.appmap.util.FullyQualifiedName;
import com.appland.appmap.util.PrefixTrie;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import javassist.CtBehavior;
/**
* Represents a package configuration for AppMap recording.
*
* <p>
* Configuration modes (mutually exclusive):
* <ul>
* <li><b>Exclude mode:</b> When {@code methods} is null, records all methods in
* the package
* except those matching {@code exclude} patterns.</li>
* <li><b>Methods mode:</b> When {@code methods} is set, records only methods
* matching the
* specified patterns. The {@code exclude} field is ignored in this mode.</li>
* </ul>
*
* @see <a href=
* "https://appmap.io/docs/reference/appmap-java.html#configuration">AppMap
* Java Configuration</a>
*/
public class AppMapPackage {
private static final TaggedLogger logger = AppMapConfig.getLogger(null);
private static String tracePrefix = Properties.DebugClassPrefix;
public String path;
public final String packagePrefix;
public String[] exclude = new String[] {};
public boolean shallow = false;
private final PrefixTrie excludeTrie = new PrefixTrie();
@JsonCreator
public AppMapPackage(@JsonProperty("path") String path,
@JsonProperty("exclude") String[] exclude,
@JsonProperty("shallow") Boolean shallow,
@JsonProperty("methods") LabelConfig[] methods) {
this.path = path;
this.exclude = exclude == null ? new String[] {} : exclude;
this.shallow = shallow != null && shallow;
this.methods = methods;
this.packagePrefix = this.path == null ? "!!dummy!!" : this.path + ".";
// Warn if both exclude and methods are specified (methods takes precedence)
if (exclude != null && exclude.length > 0 && methods != null && methods.length > 0) {
logger.warn("Package '{}': both 'exclude' and 'methods' are specified. " +
"The 'exclude' field will be ignored when 'methods' is set.", path);
}
// Build the exclusion trie only if we're in exclude mode
if (exclude != null && methods == null) {
for (String exclusion : exclude) {
// Allow exclusions to use both '.' and '#' as separators
// for backward compatibility
exclusion = exclusion.replace('#', '.');
if (exclusion.startsWith(this.packagePrefix)) {
// Absolute path: strip the package prefix
this.excludeTrie.insert(exclusion.substring(this.packagePrefix.length()));
} else {
// Relative path: use as-is
this.excludeTrie.insert(exclusion);
}
}
}
}
/**
* Configuration for matching specific methods with labels.
* Used in "methods mode" to specify which methods to record.
*/
public static class LabelConfig {
private Pattern className = null;
private Pattern name = null;
private String[] labels = new String[] {};
/** Empty constructor for exclude mode (no labels). */
public LabelConfig() {}
@JsonCreator
public LabelConfig(@JsonProperty("class") String className,
@JsonProperty("name") String name,
@JsonProperty("labels") String[] labels) {
// Anchor patterns to match whole symbols only
this.className = Pattern.compile("\\A(" + className + ")\\z");
this.name = Pattern.compile("\\A(" + name + ")\\z");
this.labels = labels;
}
public String[] getLabels() {
return this.labels;
}
/**
* Checks if the given fully qualified name matches this configuration.
* Supports matching against both simple and fully qualified class names for
* flexibility.
*
* @param fqn the fully qualified name to check
* @return true if the patterns match
*/
public boolean matches(FullyQualifiedName fqn) {
// Try matching with simple class name (package-relative)
if (matches(fqn.className, fqn.methodName)) {
return true;
}
// Also try matching with fully qualified class name for better UX
String fullyQualifiedClassName = fqn.getClassName();
return matches(fullyQualifiedClassName, fqn.methodName);
}
/**
* Checks if the given class name and method name match this configuration.
*
* @param className the class name (simple or fully qualified)
* @param methodName the method name
* @return true if both patterns match
*/
public boolean matches(String className, String methodName) {
return this.className.matcher(className).matches()
&& this.name.matcher(methodName).matches();
}
}
public LabelConfig[] methods = null;
/**
* Determines if a class/method should be recorded based on this package
* configuration.
*
* <p>
* Behavior depends on configuration mode:
* <ul>
* <li><b>Exclude mode</b> ({@code methods} is null): Returns a LabelConfig for
* methods
* in this package that are not explicitly excluded.</li>
* <li><b>Methods mode</b> ({@code methods} is set): Returns a LabelConfig only
* for methods
* that match the specified patterns. The {@code exclude} field is ignored.</li>
* </ul>
*
* @param canonicalName the fully qualified name of the method to check
* @return the label config if the method should be recorded, or null otherwise
*/
public LabelConfig find(FullyQualifiedName canonicalName) {
// Early validation
if (this.path == null || canonicalName == null) {
return null;
}
// Debug logging
if (tracePrefix == null || canonicalName.getClassName().startsWith(tracePrefix)) {
logger.trace("Checking {}", canonicalName);
}
if (isExcludeMode()) {
return findInExcludeMode(canonicalName);
} else {
return findInMethodsMode(canonicalName);
}
}
/**
* Checks if this package is configured in exclude mode (records everything
* except exclusions).
*/
private boolean isExcludeMode() {
return this.methods == null;
}
/**
* Finds a method in exclude mode: match if in package and not excluded.
*/
private LabelConfig findInExcludeMode(FullyQualifiedName canonicalName) {
String canonicalString = canonicalName.toString();
// Check if the method is in this package or a subpackage
if (!canonicalString.startsWith(this.path)) {
return null;
} else if (canonicalString.length() > this.path.length()) {
// Must either equal the path exactly or start with "path." or "path#"
// The "#" check is needed for unnamed packages
// or when path specifies a class name
final char nextChar = canonicalString.charAt(this.path.length());
if (nextChar != '.' && nextChar != '#') {
return null;
}
}
// Check if it's explicitly excluded
if (this.excludes(canonicalName)) {
return null;
}
// Include it (no labels in exclude mode)
return new LabelConfig();
}
/**
* Finds a method in methods mode: match only if it matches a configured
* pattern.
*/
private LabelConfig findInMethodsMode(FullyQualifiedName canonicalName) {
// Must be in the exact package (not subpackages)
if (!canonicalName.packageName.equals(this.path)) {
return null;
}
// Check each method pattern
for (LabelConfig config : this.methods) {
if (config.matches(canonicalName)) {
return config;
}
}
return null;
}
/**
* Converts a fully qualified class name to a package-relative name.
* For example, "com.example.foo.Bar" with package "com.example" becomes
* "foo.Bar".
*
* @param fqcn the fully qualified class name
* @return the relative class name, or the original if it doesn't start with the
* package prefix
*/
private String getRelativeClassName(String fqcn) {
if (fqcn.startsWith(this.packagePrefix)) {
return fqcn.substring(this.packagePrefix.length());
}
return fqcn;
}
/**
* Checks whether a behavior is explicitly excluded by this package
* configuration.
* Only meaningful in exclude mode; in methods mode, use {@link #find} instead.
*
* @param behavior the behavior to check
* @return true if the behavior matches an exclusion pattern
*/
public Boolean excludes(CtBehavior behavior) {
String fqClass = behavior.getDeclaringClass().getName();
String relativeClassName = getRelativeClassName(fqClass);
// Check if the class itself is excluded
if (this.excludeTrie.startsWith(relativeClassName)) {
return true;
}
// Check if the specific method is excluded
String methodName = behavior.getName();
String relativeMethodPath = String.format("%s.%s", relativeClassName, methodName);
return this.excludeTrie.startsWith(relativeMethodPath);
}
/**
* Checks whether a fully qualified method name is explicitly excluded.
* Only meaningful in exclude mode; in methods mode, use {@link #find} instead.
*
* @param canonicalName the fully qualified method name
* @return true if the method matches an exclusion pattern
*/
public Boolean excludes(FullyQualifiedName canonicalName) {
String fqcn = canonicalName.toString();
String relativeName = getRelativeClassName(fqcn);
// Convert # to . to match the format stored in the trie
relativeName = relativeName.replace('#', '.');
return this.excludeTrie.startsWith(relativeName);
}
}