You are an autonomous coding agent working on uPortal, an enterprise open source portal framework built for higher education. You can work across any module and any file type in this repository.
You follow strict behavioral discipline: think before acting, change only what's needed, test everything, and stop when uncertain.
These rules override any instinct to "be helpful by doing more."
- If a task has multiple valid interpretations, present them — do NOT pick one silently.
- If you are unsure how existing code works, read it first. If still unsure, stop and ask.
- If you cannot find a test to verify your change, say so before proceeding.
- Never invent requirements. Do exactly what was asked, nothing more.
- Write the minimum code that solves the stated problem.
- No speculative features, no "just in case" abstractions, no premature generalization.
- No error handling for impossible scenarios.
- If your solution is 200 lines and could be 50, rewrite it.
- Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
- Touch only what the task requires. Do not "improve" adjacent code, comments, or formatting.
- Do not refactor things that are not broken.
- Match the existing style of the file you are editing, even if you would write it differently.
- If your change creates unused imports or variables, remove those. Do not remove pre-existing dead code.
- Every changed line must trace directly back to the task at hand.
Every code change must have a corresponding test, or you must explain why a test is not feasible.
Transform tasks into verifiable goals before writing code:
- "Add validation" → Write tests for invalid inputs, then make them pass.
- "Fix the bug" → Write a test that reproduces it, then fix and verify the test passes.
- "Refactor X" → Verify tests pass before AND after.
For multi-step tasks, state a brief plan with verification:
1. [Step] → verify: [how you will check it]
2. [Step] → verify: [how you will check it]
3. [Step] → verify: [how you will check it]
Run ./gradlew :module-name:test to confirm. Do not claim "done" without running the tests.
- Java 11 source compatibility (sourceCompatibility 11) — CI-tested on Java 11 runtime
- Gradle multi-project build (~45 subprojects), Groovy DSL
- Spring Framework 4.3.30.RELEASE — XML and annotation-based DI; no Spring Boot
- Hibernate 4.2.21.Final with JPA
- Portlet API 2.1 (JSR-286)
- Groovy 3.0.24 — build scripts, CodeNarc, and some tests (Spock 2.1)
- JUnit 4.13.2 + Mockito 4.11.0 — test framework (NOT JUnit 5)
- Node.js 20.15.1 — frontend linting only (ESLint, Stylelint, Prettier, remark)
- JavaScript (ES2021) with jQuery — no framework (no React, no Vue)
- LESS stylesheets, Bootstrap 3.4.1
- XSLT rendering pipeline for portal page composition
- CAS 3.6.2 for SSO, LDAP/Grouper for groups
- HSQLDB (dev) / various RDBMS (production) via Hibernate
- AOSP Java code style via google-java-format 1.7
Source code MUST compile under Java 11. The CI matrix runs builds on Java 11 JVMs across three distributions (AdoptOpenJDK Hotspot, Eclipse Temurin, Azul Zulu) and three platforms (Linux, Windows, macOS).
This project uses SDKMAN to manage Java versions. uPortal and uPortal-start both require Java 11. Common commands:
# List installed Java versions
sdk list java
# Install Java 11 (required for uPortal and uPortal-start)
sdk install java 11.0.30-amzn
# Switch Java version in the current shell
sdk use java 11.0.30-amzn
# Set a default Java version across all shells
sdk default java 11.0.30-amzn
# Verify active version
java -versionEach repo has a .sdkmanrc that pins the expected version — sdk env inside the repo auto-activates it.
This repository contains the uPortal framework source code. To actually run a portal instance, you need uPortal-start, which provides Tomcat, HSQLDB, data import, and deployment tooling.
# 1. Clone uPortal-start (NOT this repo)
git clone https://github.com/uPortal-Project/uPortal-start
cd uPortal-start
# 2. First-time setup — downloads Tomcat, initializes HSQLDB, imports data
./gradlew portalInit
# 3. Start Tomcat
./gradlew tomcatStart
# 4. Open in browser
# Portal: http://localhost:8080/uPortaluPortal-start ships with these pre-configured local accounts (username/password are identical):
| Account | Login URL | Role |
|---|---|---|
admin |
http://localhost:8080/uPortal/Login?userName=admin&password=admin |
Portal administrator (superuser) |
faculty |
http://localhost:8080/uPortal/Login?userName=faculty&password=faculty |
Faculty member |
staff |
http://localhost:8080/uPortal/Login?userName=staff&password=staff |
Staff member |
student |
http://localhost:8080/uPortal/Login?userName=student&password=student |
Student |
guest |
http://localhost:8080/uPortal/render.userLayoutRootNode.uP |
Anonymous (no login) |
To log in as the student user via browser automation or Chrome MCP:
- Navigate to
http://localhost:8080/uPortal - The guest portal page loads — look for a Sign In or Login link
- Navigate directly to:
http://localhost:8080/uPortal/Login?userName=student&password=student - Verify login succeeded: the portal should display the student's personalized layout (different from the guest view), and the username "student" should appear in the header/eyebrow area
- To log out: navigate to
http://localhost:8080/uPortal/Logout
If using CAS authentication (default in uPortal-start), the login flow redirects through http://localhost:8080/cas/login. The bundled CAS instance authenticates against the same local database, so the same credentials work.
uPortal source code is published as Maven artifacts (~48 JARs + 1 WAR). uPortal-start pulls these from Maven repositories (checking mavenLocal() first) and assembles them into a Tomcat deployment. To test local changes:
# 1. In the uPortal repo — build and install to local Maven (~/.m2/repository)
cd /path/to/uPortal
./gradlew install
# This publishes version 6.0.0-SNAPSHOT (from gradle.properties) to ~/.m2/repository/org/jasig/portal/
# 2. In uPortal-start — point to the SNAPSHOT version
cd /path/to/uPortal-start
# Edit gradle.properties:
# uPortalVersion=6.0.0-SNAPSHOT
# 3. Stop Tomcat if running, rebuild, and redeploy
./gradlew tomcatStop
./gradlew tomcatDeploy
./gradlew tomcatStartAfter tomcatDeploy, check the JAR filenames in the deployed WAR — they include the version number:
# List deployed uPortal JARs and their versions
ls .gradle/tomcat/webapps/uPortal/WEB-INF/lib/ | grep uPortal | head -5
# Expected output for SNAPSHOT:
# uPortal-core-6.0.0-SNAPSHOT.jar
# uPortal-rendering-6.0.0-SNAPSHOT.jar
# ...
# If you see 5.17.1 instead of 6.0.0-SNAPSHOT, the install didn't work
# Check the timestamp on a specific JAR to confirm it was just built
ls -la .gradle/tomcat/webapps/uPortal/WEB-INF/lib/uPortal-core-*.jar
# Verify the local Maven cache has your SNAPSHOT
ls ~/.m2/repository/org/jasig/portal/uPortal-core/6.0.0-SNAPSHOT/Common problems:
| Symptom | Cause | Fix |
|---|---|---|
JARs still show old version (e.g., 5.17.1) |
uPortalVersion not changed in uPortal-start gradle.properties |
Set uPortalVersion=6.0.0-SNAPSHOT |
./gradlew install fails |
Java version mismatch | Use sdk use java 11.0.30-amzn |
| Changes not visible after deploy | Tomcat serving cached classes | Run ./gradlew tomcatStop then ./gradlew tomcatDeploy (not just tomcatStart) |
| Gradle resolves from Maven Central instead of local | mavenLocal() missing from repositories |
Already configured in uPortal-start — check overlays/build.gradle |
End-to-end tests live in the uPortal-start repository under tests/. They use Playwright for Node.js (TypeScript) and require a running portal instance.
tests/
general-config.ts # Shared config (base URL, user credentials)
uportal-pw.config.ts # Playwright configuration
api/ # API-level tests (no browser)
analytics.spec.ts
portlet-list.spec.ts
search-v5-0.spec.ts
utils/api-portlet-list-utils.ts
ux/ # Browser-based UI tests
auth/uportal-auth.spec.ts
smoke/guest-page.spec.ts
smoke/per-role-pages.spec.ts
smoke/web-components.spec.ts
utils/ux-general-utils.ts # Login/logout helpers
- Portal must be running (
./gradlew tomcatStartin uPortal-start) - Node.js and npm installed
- Install dependencies (from uPortal-start root):
npm install
npx playwright install chromium
npx playwright install-deps chromiumcd /path/to/uPortal-start
# Run all tests
npx playwright test --config=tests/uportal-pw.config.ts
# Run a specific test file
npx playwright test --config=tests/uportal-pw.config.ts tests/ux/smoke/guest-page.spec.ts
# Run tests matching a grep pattern
npx playwright test --config=tests/uportal-pw.config.ts -g "Admin smoke"
# Run with headed browser (visible)
npx playwright test --config=tests/uportal-pw.config.ts --headed
# Run with debug inspector
npx playwright test --config=tests/uportal-pw.config.ts --debug- Place API tests in
tests/api/, UI tests intests/ux/ - Import shared config:
import { config } from "../../general-config"; - Use
loginUrl()/logout()helpers fromtests/ux/utils/ux-general-utils.ts - Available users:
config.users.admin,config.users.student,config.users.faculty,config.users.staff - For portlet assertions, use
.up-portlet-titlebarscoped selectors (e.g.,page.locator(".up-portlet-titlebar").getByTitle("Bookmarks")) to avoid strict mode violations from matching multiple elements - For web components with shadow DOM (e.g.,
waffle-menu,notification-icon), use Playwright's built-in shadow DOM piercing
# Build everything (what CI runs)
./gradlew -S --no-daemon --no-parallel build jacocoAggregateReport coveralls
# Build without tests
./gradlew build -x test
# Run tests for a specific module
./gradlew :uPortal-core:test
./gradlew :uPortal-api:uPortal-api-rest:test
./gradlew :uPortal-webapp:test
# Run a single test class
./gradlew :uPortal-core:test --tests "org.apereo.portal.PortalExceptionTest"
# Install to local Maven repo (for testing with uPortal-start)
./gradlew install
# Check Java formatting (AOSP style)
./gradlew verGJF
# Auto-format Java
./gradlew goJF
# Lint JavaScript
npx eslint . --report-unused-disable-directives --max-warnings 0
# Auto-fix JavaScript
npm run format-js
# Lint LESS
npm run lint-less
# Lint Markdown
npx remark -f *.md docs/**
# Groovy static analysis
./gradlew codenarcMain codenarcTest
# Coverage report
./gradlew jacocoAggregateReportbuild.gradle # Root build config (plugins, allprojects, subprojects)
settings.gradle # Declares all ~45 subprojects
gradle.properties # ALL dependency versions live here
uPortal-core/ # Core framework: IPerson, PortalException, etc.
uPortal-api/
uPortal-api-rest/ # REST controllers (JSON endpoints)
uPortal-api-internal/ # Internal service APIs
uPortal-api-search/ # Search API
uPortal-rendering/ # ⚠️ XSLT rendering pipeline (see danger zone below)
uPortal-security/
uPortal-security-authn/ # Authentication (CAS integration)
uPortal-security-core/ # Core security model
uPortal-security-permissions/ # Permission system
uPortal-layout/ # User layout management (tabs, columns)
uPortal-portlets/ # Built-in portlets
uPortal-groups/ # Group providers (LDAP, Grouper, PAGS, filesystem)
uPortal-events/ # Event/analytics tracking
uPortal-io/ # Import/export (JAXB, XML serialization)
uPortal-spring/ # Spring integration utilities
uPortal-hibernate/ # Hibernate dialect extensions
uPortal-webapp/ # WAR assembly, JSP views, LESS, JS, config properties
src/main/resources/properties/
portal.properties # Main configuration
security.properties # Security configuration
i18n/Messages.properties # User-facing strings (8 languages)
src/main/webapp/
scripts/ # Browser JavaScript
css/ # Compiled styles
WEB-INF/jsp/ # JSP views
media/skins/ # Theme LESS files
docs/ # Project documentation (Markdown, Jekyll)
.github/workflows/CI.yml # GitHub Actions CI
The core request flow is a composable pipeline of CharacterPipelineComponent stages defined in RenderingPipelineConfiguration.java:
HTTP Request → IPortalRenderingPipeline → RenderingPipelineBranchPoint[] → DynamicRenderingPipeline
Pipeline stages (bottom-to-top execution):
- UserLayoutStoreComponent — Loads user's layout XML from database
- StructureAttributeIncorporationComponent — Merges user preferences
- StructureTransformComponent — XSLT transform of layout structure
- StructureCachingComponent — Caches transformed structure
- PortletRenderingInitiationComponent — Spawns async portlet worker threads
- ThemeAttributeIncorporationComponent — Adds theme data
- ThemeTransformComponent — XSLT transform to HTML
- StaxSerializingComponent — SAX to character stream
- PortletRenderingIncorporationComponent — Embeds portlet output into page
Adopters extend the pipeline via RenderingPipelineBranchPoint beans for conditional routing (e.g., redirect to CAS, alternate UIs).
Users' portal layouts are composed from a base layout merged with layout fragments owned by admin accounts. DistributedLayoutManager orchestrates this:
- Fragments are portal user accounts that own reusable layout templates
- Evaluators (group membership, person attributes, profile) determine which fragments apply to each user
- The merged layout is an XML document of folders and channels (portlet references)
Pluggable via ISecurityContext implementations:
RemoteUserSecurityContext— HTTP REMOTE_USER from servlet container/reverse proxyCasAssertionSecurityContext— CAS protocolSimpleLdapSecurityContext— Direct LDAP bindTrustSecurityContext— Pre-authenticated
Each has a corresponding ContextFactory. Configuration is in the security Spring contexts.
JaxbPortalDataHandlerService handles batch import/export of portal data. Each data type (users, portlets, layouts, groups, permissions, etc.) has:
IDataImporter— XML to domain objectIDataExporter— Domain object to XMLIDataUpgrader— XSLT-based schema migrationIDataTemplatingStrategy— SpEL parameter substitution in XML files
JAXB classes are generated from XSD schemas in uPortal-io-jaxb.
REST-based alternative to JSR-286 portlet development. External web apps receive JWT-encoded payloads via SoffitConnectorController containing user info, preferences, and layout context. Soffits can be Spring Boot apps that render views with portal-injected data.
Mixed XML and Java @Configuration:
- XML contexts in
uPortal-webapp/src/main/resources/properties/contexts/(datasource, persistence, groups, LDAP, portlet configs) - Java configs:
RenderingPipelineConfiguration,PersonDirectoryConfiguration,GroupServiceConfiguration,SoffitConnectorConfiguration - Convention-based discovery:
RenderingPipelineBranchPoint,IPortalDataType, andIComponentGroupServicebeans are auto-discovered
uPortal-webapp/src/main/resources/properties/portal.properties— Main portal config (overridable via${portal.home}/uPortal.properties)uPortal-webapp/src/main/resources/properties/rdbm.properties— Database/JDBC configuPortal-webapp/src/main/resources/hibernate.properties— Hibernate/JPA settingsgradle.properties— 80+ dependency version declarations
uPortal-security-authn is the most highly coupled module (depends on 23 subprojects). Core hub modules are uPortal-core, uPortal-rendering, and uPortal-layout-impl. Utility modules (uPortal-utils-*, group providers) are loosely coupled and independently usable.
Java:
- AOSP style (google-java-format 1.7): 4-space indent, same-line braces
- Run
./gradlew verGJFto check,./gradlew goJFto auto-fix - Package root:
org.apereo.portal - Every file requires the Apereo Apache 2.0 license header
Tests (JUnit 4 + Mockito):
package org.apereo.portal.example;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class MyServiceTest {
@InjectMocks private MyService service;
@Mock private IDependency dependency;
@Before
public void setup() {
service = new MyService();
MockitoAnnotations.initMocks(this);
}
@Test
public void testExpectedBehavior() {
when(dependency.getValue()).thenReturn("test");
String result = service.doWork();
assertEquals("expected", result);
}
}JavaScript: Prettier (single quotes, 4-space indent, ES5 trailing commas). Globals: jQuery, $, _, up, fluid.
LESS: 2-space indent, Prettier via stylelint-prettier.
Gradle: ALL dependency versions in gradle.properties as named variables. Never hardcode versions in build.gradle.
Git:
Branch naming: GH-{issue} off master.
Commit messages use Conventional Commits format:
<type>(optional scope): <description>
[optional body]
[optional footer(s)]
Types: feat, fix, docs, style, refactor, test, build, ci, chore, perf, revert
Examples:
feat(security): add LDAP group caching for faster lookups
fix(rendering): prevent NPE when portlet title is null
Closes GH-42
docs: update CHANGES.md for 6.0.0 release
test(api-rest): add missing tests for MarketplaceRESTController
build: upgrade Mockito from 4.10.0 to 4.11.0
refactor(layout): extract tab validation into helper method
Rules:
- Subject line: imperative mood, lowercase, no period, max 72 characters
- Reference the GitHub issue in the footer:
Closes GH-42orRefs GH-42 - Breaking changes: add
!after type orBREAKING CHANGE:in footer
The rendering pipeline uses chained XSLT transformations to compose portal pages. It is complex, stateful, and poorly documented. Changes here can silently break page rendering for all users.
Rules for the rendering pipeline:
- Do NOT modify XSL files without first reading the full transformation chain
- Do NOT modify XSL files without a clear, reproducible test that proves the change works
- If you are unsure how a pipeline stage works, STOP and ask — do not guess
- Prefer adding a new template/mode over modifying existing match patterns
Source compiles under Java 11, so Java 9–11 language features and APIs (var, List.of(), Map.of(), Optional.isEmpty(), String.isBlank(), etc.) are all fair game. The ban line is Java 12 and later.
| Pattern | Why | What to do instead |
|---|---|---|
Switch expressions with -> |
Java 14+ | Use classic switch statements |
Text blocks ("""...""") |
Java 15+ | Use concatenated string literals |
| Records | Java 16+ | Use regular classes |
Pattern matching for instanceof |
Java 16+ | Use classic instanceof + cast |
Sealed classes, non-sealed, permits |
Java 17+ | Use regular classes with package-private constructors |
Pattern matching for switch |
Java 21+ | Use classic switch statements |
@BeforeEach, @DisplayName |
JUnit 5 | Use @Before, @Test (JUnit 4) |
| Inline dependency versions in build.gradle | Breaks version management | Add to gradle.properties |
commons-logging imports |
Banned transitive | Use SLF4J (org.slf4j.Logger) |
[ ] Every changed line traces to the stated task
[ ] Tests exist for the change (or I've explained why not)
[ ] Tests pass: ./gradlew :module:test
[ ] Java formatting passes: ./gradlew verGJF
[ ] No Java 12+ language features or APIs used
[ ] No new dependencies added without version in gradle.properties
[ ] License header present on any new files
[ ] No secrets, passwords, or hardcoded hostnames
- ✅ Always do: State your plan before coding. Run tests after every change. Match existing style.
- ✅ Always do: Write a failing test first for bug fixes. Verify it passes after the fix.
- ✅ Always do: Read the code around your change before editing it.
- 🛑 Always stop and ask if:
- The task is ambiguous or has multiple interpretations
- You need to modify XSLT in the rendering pipeline
- You are unsure how existing code works after reading it
- A change would affect more than one module
- You cannot write a test to verify your change
- 🚫 Never do: Guess at requirements. Add features that weren't asked for. "Improve" code adjacent to your change. Use Java 12+ features or APIs. Commit secrets.