11package com .appland .appmap .config ;
22
3- import static com .appland .appmap .util .ClassUtil .safeClassForName ;
4-
53import java .util .regex .Pattern ;
64
75import org .tinylog .TaggedLogger ;
86
9- import com .appland .appmap .transform .annotations .CtClassUtil ;
107import com .appland .appmap .util .FullyQualifiedName ;
8+ import com .appland .appmap .util .PrefixTrie ;
119import com .fasterxml .jackson .annotation .JsonCreator ;
1210import com .fasterxml .jackson .annotation .JsonProperty ;
1311
1412import javassist .CtBehavior ;
13+
14+ /**
15+ * Represents a package configuration for AppMap recording.
16+ *
17+ * <p>
18+ * Configuration modes (mutually exclusive):
19+ * <ul>
20+ * <li><b>Exclude mode:</b> When {@code methods} is null, records all methods in
21+ * the package
22+ * except those matching {@code exclude} patterns.</li>
23+ * <li><b>Methods mode:</b> When {@code methods} is set, records only methods
24+ * matching the
25+ * specified patterns. The {@code exclude} field is ignored in this mode.</li>
26+ * </ul>
27+ *
28+ * @see <a href=
29+ * "https://appmap.io/docs/reference/appmap-java.html#configuration">AppMap
30+ * Java Configuration</a>
31+ */
1532public class AppMapPackage {
1633 private static final TaggedLogger logger = AppMapConfig .getLogger (null );
1734 private static String tracePrefix = Properties .DebugClassPrefix ;
1835
1936 public String path ;
37+ public final String packagePrefix ;
2038 public String [] exclude = new String [] {};
2139 public boolean shallow = false ;
22- public Boolean allMethods = true ;
40+ private final PrefixTrie excludeTrie = new PrefixTrie () ;
2341
24- public static class LabelConfig {
42+ @ JsonCreator
43+ public AppMapPackage (@ JsonProperty ("path" ) String path ,
44+ @ JsonProperty ("exclude" ) String [] exclude ,
45+ @ JsonProperty ("shallow" ) Boolean shallow ,
46+ @ JsonProperty ("methods" ) LabelConfig [] methods ) {
47+ this .path = path ;
48+ this .exclude = exclude == null ? new String [] {} : exclude ;
49+ this .shallow = shallow != null && shallow ;
50+ this .methods = methods ;
51+ this .packagePrefix = this .path == null ? "!!dummy!!" : this .path + "." ;
52+
53+ // Warn if both exclude and methods are specified (methods takes precedence)
54+ if (exclude != null && exclude .length > 0 && methods != null && methods .length > 0 ) {
55+ logger .warn ("Package '{}': both 'exclude' and 'methods' are specified. " +
56+ "The 'exclude' field will be ignored when 'methods' is set." , path );
57+ }
58+
59+ // Build the exclusion trie only if we're in exclude mode
60+ if (exclude != null && methods == null ) {
61+ for (String exclusion : exclude ) {
62+ // Allow exclusions to use both '.' and '#' as separators
63+ // for backward compatibility
64+ exclusion = exclusion .replace ('#' , '.' );
65+ if (exclusion .startsWith (this .packagePrefix )) {
66+ // Absolute path: strip the package prefix
67+ this .excludeTrie .insert (exclusion .substring (this .packagePrefix .length ()));
68+ } else {
69+ // Relative path: use as-is
70+ this .excludeTrie .insert (exclusion );
71+ }
72+ }
73+ }
74+ }
2575
76+ /**
77+ * Configuration for matching specific methods with labels.
78+ * Used in "methods mode" to specify which methods to record.
79+ */
80+ public static class LabelConfig {
2681 private Pattern className = null ;
2782 private Pattern name = null ;
28-
2983 private String [] labels = new String [] {};
30- private Class <?> cls ;
3184
85+ /** Empty constructor for exclude mode (no labels). */
3286 public LabelConfig () {}
3387
3488 @ JsonCreator
35- public LabelConfig (@ JsonProperty ("class" ) String className , @ JsonProperty ("name" ) String name ,
89+ public LabelConfig (@ JsonProperty ("class" ) String className ,
90+ @ JsonProperty ("name" ) String name ,
3691 @ JsonProperty ("labels" ) String [] labels ) {
92+ // Anchor patterns to match whole symbols only
3793 this .className = Pattern .compile ("\\ A(" + className + ")\\ z" );
38- this .cls = safeClassForName (Thread .currentThread ().getContextClassLoader (), className );
39- logger .trace ("this.cls: {}" , this .cls );
4094 this .name = Pattern .compile ("\\ A(" + name + ")\\ z" );
4195 this .labels = labels ;
4296 }
@@ -45,99 +99,183 @@ public String[] getLabels() {
4599 return this .labels ;
46100 }
47101
48- public boolean matches (FullyQualifiedName name ) {
49- return matches (name .className , name .methodName );
50- }
51-
52- public boolean matches (String className , String methodName ) {
53- boolean traceClass = tracePrefix == null || className .startsWith (tracePrefix );
54- Class <?> cls = safeClassForName (Thread .currentThread ().getContextClassLoader (), className );
55-
56- if (traceClass ) {
57- logger .trace ("this.cls: {} cls: {}, isChildOf?: {}" , this .cls , cls , CtClassUtil .isChildOf (cls , this .cls ));
102+ /**
103+ * Checks if the given fully qualified name matches this configuration.
104+ * Supports matching against both simple and fully qualified class names for
105+ * flexibility.
106+ *
107+ * @param fqn the fully qualified name to check
108+ * @return true if the patterns match
109+ */
110+ public boolean matches (FullyQualifiedName fqn ) {
111+ // Try matching with simple class name (package-relative)
112+ if (matches (fqn .className , fqn .methodName )) {
113+ return true ;
58114 }
59115
60- return this .className .matcher (className ).matches () && this .name .matcher (methodName ).matches ();
116+ // Also try matching with fully qualified class name for better UX
117+ String fullyQualifiedClassName = fqn .getClassName ();
118+ return matches (fullyQualifiedClassName , fqn .methodName );
61119 }
62120
121+ /**
122+ * Checks if the given class name and method name match this configuration.
123+ *
124+ * @param className the class name (simple or fully qualified)
125+ * @param methodName the method name
126+ * @return true if both patterns match
127+ */
128+ public boolean matches (String className , String methodName ) {
129+ return this .className .matcher (className ).matches ()
130+ && this .name .matcher (methodName ).matches ();
131+ }
63132 }
64133
65134 public LabelConfig [] methods = null ;
66135
67136 /**
68- * Check if a class/method is included in the configuration.
69- *
70- * @param canonicalName the canonical name of the class/method to be checked
71- * @return {@code true} if the class/method is included in the configuration. {@code false} if it
72- * is not included or otherwise explicitly excluded.
137+ * Determines if a class/method should be recorded based on this package
138+ * configuration.
139+ *
140+ * <p>
141+ * Behavior depends on configuration mode:
142+ * <ul>
143+ * <li><b>Exclude mode</b> ({@code methods} is null): Returns a LabelConfig for
144+ * methods
145+ * in this package that are not explicitly excluded.</li>
146+ * <li><b>Methods mode</b> ({@code methods} is set): Returns a LabelConfig only
147+ * for methods
148+ * that match the specified patterns. The {@code exclude} field is ignored.</li>
149+ * </ul>
150+ *
151+ * @param canonicalName the fully qualified name of the method to check
152+ * @return the label config if the method should be recorded, or null otherwise
73153 */
74154 public LabelConfig find (FullyQualifiedName canonicalName ) {
75- String className = canonicalName != null ? canonicalName .getClassName () : null ;
76- boolean traceClass = tracePrefix == null || className .startsWith (tracePrefix );
77- if (traceClass ) {
78- logger .trace (canonicalName );
155+ // Early validation
156+ if (this .path == null || canonicalName == null ) {
157+ return null ;
79158 }
80159
81- if (this .path == null ) {
82- return null ;
160+ // Debug logging
161+ if (tracePrefix == null || canonicalName .getClassName ().startsWith (tracePrefix )) {
162+ logger .trace ("Checking {}" , canonicalName );
83163 }
84164
85- if (canonicalName == null ) {
86- return null ;
165+ if (isExcludeMode ()) {
166+ return findInExcludeMode (canonicalName );
167+ } else {
168+ return findInMethodsMode (canonicalName );
87169 }
170+ }
88171
89- // If no method configs are set, use the old matching behavior.
90- if (this .methods == null ) {
91- if (!canonicalName .toString ().startsWith (this .path )) {
172+ /**
173+ * Checks if this package is configured in exclude mode (records everything
174+ * except exclusions).
175+ */
176+ private boolean isExcludeMode () {
177+ return this .methods == null ;
178+ }
179+
180+ /**
181+ * Finds a method in exclude mode: match if in package and not excluded.
182+ */
183+ private LabelConfig findInExcludeMode (FullyQualifiedName canonicalName ) {
184+ String canonicalString = canonicalName .toString ();
185+
186+ // Check if the method is in this package or a subpackage
187+ if (!canonicalString .startsWith (this .path )) {
188+ return null ;
189+ } else if (canonicalString .length () > this .path .length ()) {
190+ // Must either equal the path exactly or start with "path." or "path#"
191+ // The "#" check is needed for unnamed packages
192+ // or when path specifies a class name
193+ final char nextChar = canonicalString .charAt (this .path .length ());
194+ if (nextChar != '.' && nextChar != '#' ) {
92195 return null ;
93196 }
197+ }
94198
95- return this .excludes (canonicalName ) ? null : new LabelConfig ();
199+ // Check if it's explicitly excluded
200+ if (this .excludes (canonicalName )) {
201+ return null ;
96202 }
97203
204+ // Include it (no labels in exclude mode)
205+ return new LabelConfig ();
206+ }
207+
208+ /**
209+ * Finds a method in methods mode: match only if it matches a configured
210+ * pattern.
211+ */
212+ private LabelConfig findInMethodsMode (FullyQualifiedName canonicalName ) {
213+ // Must be in the exact package (not subpackages)
98214 if (!canonicalName .packageName .equals (this .path )) {
99215 return null ;
100216 }
101217
102- for (LabelConfig ls : this .methods ) {
103- if (ls .matches (canonicalName )) {
104- return ls ;
218+ // Check each method pattern
219+ for (LabelConfig config : this .methods ) {
220+ if (config .matches (canonicalName )) {
221+ return config ;
105222 }
106223 }
107224
108225 return null ;
109226 }
110227
111228 /**
112- * Returns whether or not the canonical name is explicitly excluded
113- *
114- * @param canonicalName the canonical name of the class/method to be checked
229+ * Converts a fully qualified class name to a package-relative name.
230+ * For example, "com.example.foo.Bar" with package "com.example" becomes
231+ * "foo.Bar".
232+ *
233+ * @param fqcn the fully qualified class name
234+ * @return the relative class name, or the original if it doesn't start with the
235+ * package prefix
236+ */
237+ private String getRelativeClassName (String fqcn ) {
238+ if (fqcn .startsWith (this .packagePrefix )) {
239+ return fqcn .substring (this .packagePrefix .length ());
240+ }
241+ return fqcn ;
242+ }
243+
244+ /**
245+ * Checks whether a behavior is explicitly excluded by this package
246+ * configuration.
247+ * Only meaningful in exclude mode; in methods mode, use {@link #find} instead.
248+ *
249+ * @param behavior the behavior to check
250+ * @return true if the behavior matches an exclusion pattern
115251 */
116252 public Boolean excludes (CtBehavior behavior ) {
117- FullyQualifiedName fqn = null ;
118- for (String exclusion : this .exclude ) {
119- if (behavior .getDeclaringClass ().getName ().startsWith (exclusion )) {
120- return true ;
121- } else {
122- if (fqn == null ) {
123- fqn = new FullyQualifiedName (behavior );
124- }
125- if (fqn .toString ().startsWith (exclusion )) {
126- return true ;
127- }
128- }
253+ String fqClass = behavior .getDeclaringClass ().getName ();
254+ String relativeClassName = getRelativeClassName (fqClass );
255+
256+ // Check if the class itself is excluded
257+ if (this .excludeTrie .startsWith (relativeClassName )) {
258+ return true ;
129259 }
130260
131- return false ;
261+ // Check if the specific method is excluded
262+ String methodName = behavior .getName ();
263+ String relativeMethodPath = String .format ("%s.%s" , relativeClassName , methodName );
264+ return this .excludeTrie .startsWith (relativeMethodPath );
132265 }
133266
267+ /**
268+ * Checks whether a fully qualified method name is explicitly excluded.
269+ * Only meaningful in exclude mode; in methods mode, use {@link #find} instead.
270+ *
271+ * @param canonicalName the fully qualified method name
272+ * @return true if the method matches an exclusion pattern
273+ */
134274 public Boolean excludes (FullyQualifiedName canonicalName ) {
135- for (String exclusion : this .exclude ) {
136- if (canonicalName .toString ().startsWith (exclusion )) {
137- return true ;
138- }
139- }
140-
141- return false ;
275+ String fqcn = canonicalName .toString ();
276+ String relativeName = getRelativeClassName (fqcn );
277+ // Convert # to . to match the format stored in the trie
278+ relativeName = relativeName .replace ('#' , '.' );
279+ return this .excludeTrie .startsWith (relativeName );
142280 }
143281}
0 commit comments