diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..5a07848f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,255 @@ +# AGENTS.md - Agate Development Guide + +This document provides essential information for agentic coding agents working on the Agate authentication server project. + +## Project Overview + +Agate is a central authentication server for OBiBa applications built with: + - **Java 21** with Spring Boot 4.0.0 +- **Maven** multi-module project +- **MongoDB** for data persistence +- **Apache Shiro** for security +- **Jersey/JAX-RS** for REST API +- **Angular** frontend (TypeScript) + +## Module Structure + +``` +agate/ +├── agate-core/ # Core business logic, domain models, services +├── agate-web-model/ # Generated protobuf models and DTOs +├── agate-rest/ # REST API endpoints and mappers +├── agate-ui/ # Angular frontend application +├── agate-webapp/ # Spring Boot web application +└── agate-dist/ # Distribution and packaging +``` + +## Build and Development Commands + +### Core Maven Commands +```bash +# Build entire project +make install # or: mvn install + +# Build without tests +mvn install -Dmaven.test.skip=true + +# Clean build +make clean # or: mvn clean + +# Build specific modules +make core # Build agate-core module +make rest # Build agate-rest module +make ui # Build agate-ui module +make webapp # Build agate-webapp module +``` + +### Testing Commands +```bash +# Run all tests +mvn test + +# Run tests for specific module +mvn test -pl agate-core +mvn test -pl agate-rest + +# Run single test class +mvn test -Dtest=UserServiceTest + +# Run single test method +mvn test -Dtest=UserServiceTest#testCreateUser + +# Run tests with specific profile +mvn test -Pdev +``` + +### Development Server +```bash +# Start development server (HTTP:8081, HTTPS:8444) +make debug + +# Start with production profile +make run-prod + +# View logs +make log # Tail main log +make restlog # Tail REST log +``` + +### Other Useful Commands +```bash +# Dependency analysis +make dependencies-tree +make dependencies-update +make plugins-update + +# Database operations +make drop-mongo # Drop MongoDB database +make clear-log # Clear log files +``` + +## Code Style Guidelines + +### Package Structure +- Base package: `org.obiba.agate` +- Follow standard Maven directory layout +- Domain models in `org.obiba.agate.domain` +- Services in `org.obiba.agate.service` +- Repositories in `org.obiba.agate.repository` +- REST resources in `org.obiba.agate.web.rest` +- Controllers in `org.obiba.agate.web.controller` + +### Naming Conventions +- **Classes**: PascalCase (e.g., `UserService`, `UserRepository`) +- **Methods**: camelCase (e.g., `createUser()`, `findByEmail()`) +- **Constants**: UPPER_SNAKE_CASE (e.g., `DEFAULT_PAGE_SIZE`) +- **Variables**: camelCase with descriptive names +- **Packages**: lowercase with dots separating words + +### Import Organization +1. `java.*` and `jakarta.*` imports +2. Third-party library imports +3. `org.obiba.*` imports +- Use wildcard imports only for constant classes +- Remove unused imports + +### Annotations Usage +- `@Inject` for dependency injection +- `@Component` for Spring-managed beans +- `@Service` for service layer classes +- `@Repository` for data access classes +- `@Document` for MongoDB entities +- `@Path`, `@GET`, `@POST` for JAX-RS endpoints +- `@RequiresRoles` for Shiro security +- `@Timed` for metrics collection + +### Error Handling +- Create specific exception classes extending `RuntimeException` +- Use standard HTTP status codes in REST responses +- Implement exception mappers for REST layers +- Log errors at appropriate levels +- Return meaningful error messages + +### Testing Guidelines +- Test classes end with `Test` suffix +- Use JUnit for unit tests +- Use AssertJ for assertions +- Mock dependencies with EasyMock +- Follow AAA pattern (Arrange, Act, Assert) +- Test both happy path and error scenarios + +### Security Patterns +- Use Shiro annotations for method-level security +- Apply principle of least privilege +- Validate all input parameters +- Sanitize output data +- Use HTTPS in production +- Implement proper session management + +### Database Patterns +- Use `@Document` annotation for MongoDB entities +- Add `@Indexed` for frequently queried fields +- Extend `AbstractAuditableDocument` for audit trails +- Use repository pattern for data access +- Consider data consistency and transaction boundaries + +### API Design +- Use RESTful conventions +- Implement proper HTTP status codes +- Use DTOs for API responses +- Version APIs when breaking changes +- Document endpoints with proper comments +- Handle pagination for large datasets + +## Configuration Profiles + +### Available Profiles +- `dev` - Development with debug features +- `ci-build` - Continuous integration build +- Default production settings + +### Environment Variables +- `AGATE_HOME` - Application home directory +- `AGATE_LOG` - Log file directory + +## Key Dependencies + +### Core Framework +- Spring Boot 3.5.7 +- Spring Data MongoDB +- Apache Shiro 1.13.0 +- Jersey 3.1.3 + +### Utilities +- Google Guava +- Apache Commons Lang +- Joda Time + +### Testing +- JUnit +- AssertJ +- EasyMock + +## Development Notes + +- Java 21 is required (update alternatives if needed) +- Use MongoDB for development and testing +- Frontend builds are handled through Maven plugin +- Follow GPL3 license requirements +- Maintain backward compatibility when possible +- Write comprehensive tests for new features + +## Common Patterns + +### Service Layer +```java +@Service +public class UserService { + + @Inject + private UserRepository userRepository; + + @Timed + public User createUser(UserDTO dto) { + validateUserDTO(dto); + User user = new User(); + // mapping logic + return userRepository.save(user); + } +} +``` + +### REST Resource +```java +@Component +@Path("/users") +@RequiresRoles("agate-administrator") +public class UsersResource { + + @Inject + private UserService userService; + + @POST + @Timed + public Response createUser(UserDTO dto) { + // implementation + } +} +``` + +### Domain Entity +```java +@Document +public class User extends AbstractAuditableDocument { + + @Indexed(unique = true) + private String name; + + @Indexed(unique = true) + private String email; + + // getters and setters +} +``` + +This guide should help agents understand the project structure, conventions, and common patterns used in Agate development. \ No newline at end of file diff --git a/agate-core/pom.xml b/agate-core/pom.xml index b19f5a58..855b9a84 100644 --- a/agate-core/pom.xml +++ b/agate-core/pom.xml @@ -32,8 +32,12 @@ - org.eclipse.jetty.ee10 - jetty-ee10-servlet + org.eclipse.jetty.ee11 + jetty-ee11-servlet + + + org.springframework.boot + spring-boot-jdbc io.dropwizard.metrics @@ -132,7 +136,7 @@ org.springframework.boot - spring-boot-starter-aop + spring-boot-starter-aspectj org.springframework.boot diff --git a/agate-core/src/main/java/org/obiba/agate/security/AgateRealmFactory.java b/agate-core/src/main/java/org/obiba/agate/security/AgateRealmFactory.java index 99c6ee59..9c4e5864 100644 --- a/agate-core/src/main/java/org/obiba/agate/security/AgateRealmFactory.java +++ b/agate-core/src/main/java/org/obiba/agate/security/AgateRealmFactory.java @@ -21,7 +21,6 @@ import org.obiba.oidc.shiro.realm.OIDCRealm; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.jdbc.datasource.DriverManagerDataSource; import org.springframework.stereotype.Component; @@ -131,8 +130,6 @@ private JndiLdapContextFactory createLdapContextFactory(String configName, Strin } private DataSource createDataSource(String configName, JdbcRealmConfig content) { - DataSourceBuilder builder = DataSourceBuilder.create(); - String url = content.getUrl(); String driverClassName = getValidDriverClassName(configName, url); @@ -144,14 +141,13 @@ private DataSource createDataSource(String configName, JdbcRealmConfig content) return null; } - builder - .type(DriverManagerDataSource.class) - .url(url) - .username(content.getUsername()) - .password(content.getPassword()) - .driverClassName(driverClassName); + DriverManagerDataSource ds = new DriverManagerDataSource(); + ds.setUrl(url); + ds.setUsername(content.getUsername()); + ds.setPassword(content.getPassword()); + ds.setDriverClassName(driverClassName); - return builder.build(); + return ds; } private String getValidDriverClassName(String configName, String url) { diff --git a/agate-webapp/pom.xml b/agate-webapp/pom.xml index 57b16819..db4bb79e 100644 --- a/agate-webapp/pom.xml +++ b/agate-webapp/pom.xml @@ -77,8 +77,8 @@ - org.eclipse.jetty.ee10 - jetty-ee10-servlets + org.eclipse.jetty.ee11 + jetty-ee11-servlets diff --git a/agate-webapp/src/main/java/org/obiba/agate/Application.java b/agate-webapp/src/main/java/org/obiba/agate/Application.java index 93e5a3df..73bf62a1 100644 --- a/agate-webapp/src/main/java/org/obiba/agate/Application.java +++ b/agate-webapp/src/main/java/org/obiba/agate/Application.java @@ -18,7 +18,6 @@ import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.core.env.Environment; import org.springframework.core.env.SimpleCommandLinePropertySource; import org.springframework.scheduling.annotation.EnableScheduling; @@ -26,7 +25,7 @@ import jakarta.inject.Inject; import java.util.Arrays; -@SpringBootApplication(exclude = { SecurityAutoConfiguration.class }, scanBasePackages = "org.obiba") +@SpringBootApplication(scanBasePackages = "org.obiba") @EnableScheduling public class Application implements InitializingBean { diff --git a/agate-webapp/src/main/java/org/obiba/agate/config/WebConfiguration.java b/agate-webapp/src/main/java/org/obiba/agate/config/WebConfiguration.java index 32855035..43e3a1a2 100644 --- a/agate-webapp/src/main/java/org/obiba/agate/config/WebConfiguration.java +++ b/agate-webapp/src/main/java/org/obiba/agate/config/WebConfiguration.java @@ -33,8 +33,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer; -import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; + import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletContextInitializer; @@ -54,7 +53,7 @@ @ComponentScan({"org.obiba.agate", "org.obiba.shiro"}) @PropertySource("classpath:agate-webapp.properties") @AutoConfigureAfter(SecurityConfiguration.class) -public class WebConfiguration implements ServletContextInitializer, JettyServerCustomizer, EnvironmentAware { +public class WebConfiguration implements ServletContextInitializer, EnvironmentAware { private static final Logger log = LoggerFactory.getLogger(WebConfiguration.class); @@ -87,25 +86,7 @@ public void setEnvironment(Environment environment) { contextPath = environment.getProperty("server.servlet.context-path", ""); } - @Bean - public WebServerFactoryCustomizer containerCustomizer() throws Exception { - return factory -> { - factory.setServerCustomizers(Collections.singleton(WebConfiguration.this)); // FIXME is this necessary? - if (!Strings.isNullOrEmpty(contextPath) && contextPath.startsWith("/")) - factory.setContextPath(contextPath); - }; - } - @Override - public void customize(Server server) { - customizeSsl(server); - - GzipHandler gzipHandler = new GzipHandler(); - gzipHandler.setIncludedMethods("PUT", "POST", "GET"); - gzipHandler.setInflateBufferSize(2048); - gzipHandler.setHandler(server.getHandler()); - server.setHandler(gzipHandler); - } private void customizeSsl(Server server) { if (httpsPort <= 0) return; diff --git a/agate-webapp/src/main/java/org/obiba/agate/config/locale/AngularCookieLocaleResolver.java b/agate-webapp/src/main/java/org/obiba/agate/config/locale/AngularCookieLocaleResolver.java index ad4102c2..3d1ad744 100644 --- a/agate-webapp/src/main/java/org/obiba/agate/config/locale/AngularCookieLocaleResolver.java +++ b/agate-webapp/src/main/java/org/obiba/agate/config/locale/AngularCookieLocaleResolver.java @@ -48,9 +48,8 @@ public AngularCookieLocaleResolver(ConfigurationService configurationService) { this.configurationService = configurationService; } - @Override protected Locale determineDefaultLocale(HttpServletRequest request) { - Locale defaultLocale = super.determineDefaultLocale(request); + Locale defaultLocale = Locale.getDefault(); // validate default locale, which could come from the Accept-Language header List configLocales = configurationService.getConfiguration().getLocales(); List languageLocales = configLocales.stream() @@ -59,6 +58,10 @@ protected Locale determineDefaultLocale(HttpServletRequest request) { return languageLocales.isEmpty() ? configLocales.stream().findFirst().orElse(Locale.ENGLISH) : languageLocales.get(0); } + protected TimeZone determineDefaultTimeZone(HttpServletRequest request) { + return TimeZone.getDefault(); + } + @Override public Locale resolveLocale(HttpServletRequest request) { parseLocaleCookieIfNecessary(request); diff --git a/agate-webapp/src/main/java/org/obiba/agate/web/controller/ErrorControllerImpl.java b/agate-webapp/src/main/java/org/obiba/agate/web/controller/ErrorControllerImpl.java index a0ca50d9..7706f6a2 100644 --- a/agate-webapp/src/main/java/org/obiba/agate/web/controller/ErrorControllerImpl.java +++ b/agate-webapp/src/main/java/org/obiba/agate/web/controller/ErrorControllerImpl.java @@ -1,7 +1,6 @@ package org.obiba.agate.web.controller; import org.owasp.esapi.ESAPI; -import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; @@ -10,7 +9,7 @@ import org.springframework.web.servlet.ModelAndView; @Controller -public class ErrorControllerImpl implements ErrorController { +public class ErrorControllerImpl { @GetMapping("/error") public ModelAndView error(@RequestParam(value = "error", required = false, defaultValue = "404") String status, @RequestParam(defaultValue = "") String message) { diff --git a/pom.xml b/pom.xml index 01d324bf..798b474d 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.7 + 4.0.1 @@ -48,7 +48,7 @@ 33.2.1-jre 1 3.1.3 - 12.0.10 + 12.1.5 0.7 0.12.3 2.8.2 @@ -61,7 +61,7 @@ 7.4 3.0.7 3.0.0 - 5.5.1 + 5.6.2 8.4.0 8.4.0 10.0.2 @@ -222,12 +222,12 @@ - org.eclipse.jetty.ee10 + org.eclipse.jetty.ee11 jetty-ee10-servlet ${jetty.version} - org.eclipse.jetty.ee10 + org.eclipse.jetty.ee11 jetty-ee10-servlets ${jetty.version}