#800: ExceptionHelper.java
projectforge-business/src/main/java/org/projectforge/framework/utils/ExceptionHelper.java · 97 lines · 50 code · 39 comments · 8 blank
Three static utility methods for exception handling: filtered stack traces (collapse irrelevant frames and skip CGLIB proxies), stack trace to String (standard printStackTrace-to-writer), and recursive root cause resolution. Used primarily by the security/access layer to produce readable error messages that show only ProjectForge code paths, not Spring/Hibernate/Tomcat internals.
Methods
| Method | What it does | Why it exists |
getFilteredStackTrace(ex, namespace) | Builds a stack trace string containing only frames from the given namespace. Consecutive non-matching frames are collapsed into "at ...". Frames containing CGLIB$$ are always skipped — these are Hibernate proxy classes with mangled names like AddressDO$$EnhancerByCGLIB$$a1b2c3d4. | Access violation errors would otherwise show 50+ lines of Spring Security and Tomcat internals before showing the single relevant ProjectForge line. The filtered trace shows only the developer-relevant call chain. |
printStackTrace(ex) | Converts a full stack trace to String via StringWriter+PrintWriter. Standard Java pattern. | Logging frameworks need String, not PrintStream. Used when exceptions need to be serialized to JSON or stored in database columns. |
getRootCause(ex) | Recursively unwraps getCause() until the innermost exception is found. Returns the original exception if no cause exists. | Spring wraps exceptions heavily (UndeclaredThrowableException, InvocationTargetException, DataAccessException). The real error is always at the bottom. This method extracts it. |
Namespace Filtering Algorithm
The getFilteredStackTrace method uses a state-machine approach with two modes:
Normal mode: Printing frames. When a non-matching frame is encountered, it outputs "at ..." (collapsing the irrelevant frames) and switches to ignored mode.
Ignored mode: Skipping frames silently until a matching frame appears, then switching back to normal mode and printing it.
This produces compact output like: at org.projectforge.access.AccessChecker.check(AccessChecker.java:79) at ... at org.projectforge.task.TaskDao.hasAccess(TaskDao.java:176) — where "at ..." replaces all the Spring/Tomcat/Hibernate frames between the two relevant lines.
Consumers
Called by the access control layer (AccessCheckerImpl, AccessException) when building error messages for permission violations. Also used by ScriptExecutor to produce readable error output for Groovy/Kotlin script failures — without the filtered trace, a simple NPE in a script would produce a 200-line stack trace dominated by Groovy runtime and Spring proxy internals.
Git History
| Commit | What changed |
868d6abb7 | Copyright 2025→2026 |
63081666f | Copyright 2024→2025 |
b6092df09 | Copyright 2023→2024 |
ab45d51fa | Copyright 2001-2022→2001-2023 |
5f7ef41b8 | Copyright 2021→2022 |
ceb63e8a1 | Copyright 2001-2021 |
7c79f192 | Copyright 2020 |
73a9755d | Code cleanup pass across all utils. Collapsed identical catch blocks, replaced ArrayList<Class> with diamond operator ArrayList<>, replaced StringBuffer with StringBuilder, switched Collections.sort to List.sort. The ExceptionHelper code itself uses StringBuilder in getFilteredStackTrace — this commit ensured consistency with the project-wide migration from synchronized StringBuffer to unsynchronized StringBuilder. |
Gotcha — CGLIB$$ detection: The ignore() method checks for CGLIB$$ in the class name. When ProjectForge upgrades to Hibernate 6.x, proxies will use ByteBuddy instead of CGLIB — the class names will contain $HibernateProxy$ or $ByteBuddy$ instead. The ignore() method will need updating to handle both patterns.