/*
http://www.apache.org/licenses/LICENSE-2.0
This document outlines coding conventions and guidelines for the Apache Unomi project. It covers both standard Java best practices and project-specific conventions.
This project follows standard Java best practices. For comprehensive guidelines, refer to:
Key Principles: Code clarity, consistency, documentation, proper error handling, resource management, immutability, thread safety.
Key rules: JavaDoc for public classes/methods, remove unused imports, UPPER_CASE constants, no magic numbers (exceptions: -1, 0, 1, 2, 3, 17, 24, 31, 37, 60, 255, 256, 1000), always use braces, equals/hashCode together.
Run: mvn clean install -P checkstyle
All classes extending org.apache.unomi.api.Item must define:
public static final String ITEM_TYPE = "myItem";
Example:
public class Profile extends Item { public static final String ITEM_TYPE = "profile"; }
org.apache.unomi.api.* - Public APIsorg.apache.unomi.services.impl.* - Internal implementationsorg.apache.unomi.*.services.* - Extension-specific servicesorg.apache.unomi.plugin.* - Plugin implementations*Service (interface), *ServiceImpl (implementation)*ActionExecutor*ConditionEvaluator*QueryBuilderUse SLF4J with LoggerFactory.getLogger(ClassName.class) (not .getName()):
private static final Logger LOGGER = LoggerFactory.getLogger(MyClass.class); LOGGER.info("Processing item: {}", itemId); // Parameterized logging LOGGER.error("Error processing item: {}", itemId, exception); // Exception last
Best Practices: Include context (itemId, tenantId, eventType), use parameterized logging, pass exceptions as last parameter.
ConcurrentHashMap, ConcurrentLinkedQueue, AtomicBoolean/Integer/LongCollections.emptyList(), Collections.singletonList(), Collections.unmodifiableList()map.computeIfAbsent(key, k -> new ArrayList<>()).add(value)try { // operation } catch (Exception e) { LOGGER.error("Error processing item: {}", itemId, e); throw new ProcessingException("Failed to process item: " + itemId, e); }
ExceptionMapper for REST endpointsObjects.requireNonNull(item, "Item cannot be null"); if (collection.isEmpty()) { /* ... */ } // Not size() == 0
Objects.requireNonNull() for required parametersisEmpty()try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { // use resource }
AutoCloseable@Deactivate/preDestroy(): close trackers, shutdown executors, cancel timersExecutorService for async operations (shutdown in @Deactivate)ConcurrentHashMap over synchronizedMapvolatile for simple flags, AtomicReference for object referencescontextManager.executeAsSystem(() -> { /* system operation */ }); contextManager.executeAsTenant(tenantId, () -> { /* tenant operation */ });
ThreadLocal - don't share across threadsfinally blocksOSGi DS (preferred):
@Component(service = MyService.class, configurationPid = "org.apache.unomi.myservice") public class MyServiceImpl implements MyService { @Activate public void activate(MyServiceConfig config) { /* init */ } @Deactivate public void deactivate() { /* cleanup */ } @Modified public void modified(MyServiceConfig config) { /* config change */ } }
Blueprint (legacy): Use postConstruct() and preDestroy().
@Path("/profiles") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Component(service = ProfileEndpoint.class, property = "osgi.jaxrs.resource=true") public class ProfileEndpoint { @Reference private ProfileService profileService; @GET @Path("/{id}") public Profile getProfile(@PathParam("id") String id) { return profileService.load(id); } }
@Reference(cardinality = ReferenceCardinality.MANDATORY) private PersistenceService persistenceService; @Reference(cardinality = ReferenceCardinality.OPTIONAL) private MetricsService metricsService; @Reference(cardinality = ReferenceCardinality.MULTIPLE) private List<ActionExecutor> actionExecutors;
Dynamic binding: Use bind/unbind methods with CopyOnWriteArrayList or ConcurrentHashMap.
Service Trackers: Always close in @Deactivate:
@Deactivate public void deactivate() { if (serviceTracker != null) { serviceTracker.close(); serviceTracker = null; } }
Item subclasses must be SerializableserialVersionUID for version controlCustomObjectMapper for Unomi-specific serializationItemDeserializer for polymorphic Item deserializationConditionBuilder for complex condition treesTaskBuilder for scheduled tasksthis from builder methods for chainingint result = new MetricAdapter<Integer>(metricsService, "ClassName.operation") { @Override public Integer execute(Object... args) throws Exception { return performOperation(); } }.runWithTimer();
metricsService != null && metricsService.isActivated() before updatingClassName.operationNameUse ValidationError with context (parameter name, condition ID, etc.):
errors.add(new ValidationError( param.getId(), "Required parameter is missing", ValidationErrorType.MISSING_REQUIRED_PARAMETER, condition.getConditionTypeId(), type.getItemId(), context, null ));
private volatile boolean shutdownNow = false; public void preDestroy() { shutdownNow = true; // Set flag first // Cancel tasks, close trackers, shutdown executors } public void processItems() { while (!shutdownNow) { if (shutdownNow) return; // Check flag in loop processItem(getNextItem()); } }
Shutdown sequence: Set flag → Cancel tasks → Close trackers → Shutdown executors → Release references → Clear collections.
Goal: Use OSGi Declarative Services (DS) annotations instead of Blueprint XML.
Benefits: Type-safe references, better IDE support, reduced boilerplate, compile-time validation.
Strategy: New services use DS; migrate existing services gradually when modifying.
CRITICAL: Use OSGi Managed Services with @Modified for real-time updates (no restart required).
Example:
@Component(service = WebConfig.class, configurationPid = "org.apache.unomi.web") @Designate(ocd = WebConfig.Config.class) public class WebConfig { @ObjectClassDefinition(name = "Apache Unomi Web Configuration") public @interface Config { @AttributeDefinition(name = "Context Server Domain") String contextserver_domain() default ""; } @Activate public void activate(Config config) { modified(config); } @Modified public void modified(Config config) { // Configuration updated in real-time without restart this.contextserverDomain = config.contextserver_domain(); } }
CRITICAL: All configuration must be wired to environment variables for Docker.
Pattern in custom.system.properties:
org.apache.unomi.my.property=${env:UNOMI_MY_PROPERTY:-defaultValue}
Naming: UNOMI_{CATEGORY}_{PROPERTY_NAME} (uppercase, underscores, replace dots with underscores).
Example:
org.osgi.service.http.port=${env:UNOMI_HTTP_PORT:-8181} org.apache.unomi.elasticsearch.addresses=${env:UNOMI_ELASTICSEARCH_ADDRESSES:-localhost:9200}
Docker:
docker run -e UNOMI_HTTP_PORT=8080 -e UNOMI_ELASTICSEARCH_ADDRESSES=elasticsearch:9200 apache/unomi:latest
Priority (highest to lowest): Environment variables → Custom config files → Default config files.
Real-time updates: Environment variables set initial state; @Modified allows runtime changes without restart (for tests, future UI, operational flexibility).
Adding new property:
custom.system.properties with ${env:UNOMI_*:-default}@ObjectClassDefinition interfaceorg.apache.unomi.*)Standard API Dependencies: javax.*, jakarta.*, org.osgi.* packages (typically scope=provided).
Example:
<dependencies> <!-- Unomi dependencies --> <dependency> <groupId>org.apache.unomi</groupId> <artifactId>unomi-api</artifactId> </dependency> <!-- Standard API Dependencies --> <dependency> <groupId>org.osgi</groupId> <artifactId>osgi.core</artifactId> <scope>provided</scope> </dependency> <!-- Libraries --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <!-- Test dependencies --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> </dependencies>
CRITICAL: Do NOT hardcode versions. All versions managed through:
unomi-bom) - third-party dependenciesunomi-bom-artifacts) - Unomi internal dependenciesAdding a dependency:
bom/pom.xml or bom/artifacts/pom.xmlpom.xml, then add to appropriate BOMpom.xml without <version> tagExample:
<!-- Root pom.xml --> <properties> <my-library.version>1.2.3</my-library.version> </properties> <!-- bom/pom.xml --> <dependencyManagement> <dependency> <groupId>com.example</groupId> <artifactId>my-library</artifactId> <version>${my-library.version}</version> </dependency> </dependencyManagement> <!-- Module pom.xml --> <dependency> <groupId>com.example</groupId> <artifactId>my-library</artifactId> <!-- NO <version> tag --> </dependency>
Exceptions: Versions allowed only in root pom.xml properties, BOM files, and parent POM references.
See also: DEPENDENCY_ORGANIZATION_REPORT.md
Framework: JUnit 5 (Jupiter), Mockito with @ExtendWith(MockitoExtension.class)
Conventions:
org.junit.jupiter.api.* exclusively@BeforeEach/@AfterEach for lifecycle@Tag instead of @CategoryassertThrows() instead of @Test(expected = ...)Example:
@ExtendWith(MockitoExtension.class) class MyServiceTest { @Mock private PersistenceService persistenceService; @Test void testMethod() { String itemId = "test-item-123"; when(persistenceService.load(itemId, MyItem.class)).thenReturn(new MyItem(itemId)); MyItem result = myService.loadItem(itemId); assertNotNull(result, "Should load item with id: " + itemId); assertEquals(itemId, result.getItemId(), "Item ID should match"); } }
Framework: JUnit 4 (Pax Exam)
Conventions:
org.apache.unomi.itests.BaseITgetOsgiService(ServiceClass.class, timeout) for OSGi services@AfterExample:
public class MyServiceIT extends BaseIT { private String testItemId; @Before public void setUp() { testItemId = "test-item-" + System.currentTimeMillis(); } @Test public void testMyService() throws Exception { // Test implementation } @After public void tearDown() { if (testItemId != null) { try { persistenceService.remove(testItemId, MyItem.class); } catch (Exception e) { // Log but don't fail } } } }
Running: mvn clean install -P integration-tests (or -Duse.opensearch=true for OpenSearch)
AVOID Thread.sleep() - Use:
await().atMost(10, TimeUnit.SECONDS).until(() -> condition)keepTrying("message", supplier, predicate, timeout, retries)When Thread.sleep() is acceptable: Only in helper methods implementing retry logic, testing time-based behavior, or as last resort (< 100ms with justification).
Test Execution Time:
@Tag("slow"), ensure value justifies timePerformance and Reliability:
ExecutorService for concurrency tests (shutdown in teardown)new Random(42)@TempDir (JUnit 5) or Files.createTempDirectory() (integration), always clean upTimeUnit constants, not raw millisecond literalstoString() (unless that's the behavior)static final@BeforeEach/@AfterEach)See also: itests/README.md
Status: NOT YET STARTED - Planned migration for consistency.
Key Differences:
org.junit.* → org.junit.jupiter.api.*@Before/@After → @BeforeEach/@AfterEach@Test(expected = ...) → assertThrows(...)@Category → @Tag@RunWith → @ExtendWithStrategy: Verify Pax Exam JUnit 5 support, migrate incrementally in separate branch/PR.
Goal: All services use OSGi DS annotations.
Strategy: New services use DS; migrate existing when modifying.
Goal: Consolidate plugins/ and extensions/ into unified extension mechanism.
Rationale: Reduces confusion, simplifies structure, easier to understand.
Goal: Use plugins for new functionality instead of core services.
When to use plugins: Custom actions/conditions, external integrations, domain-specific features, optional functionality.
When to use core services: Fundamental persistence, core event processing, essential profile/segment management, critical infrastructure.
Status: NOT YET STARTED - See JUnit 4 to JUnit 5 Migration Plan.
Focus: Service implementations, complex business logic, utility classes, error handling paths.
Strategy: Add tests when modifying code, require tests for new features, use JaCoCo to track progress.
Goal: Use LoggerFactory.getLogger(ClassName.class) (not .getName()).
Strategy: New code uses standard form; update existing when modifying.
Ongoing: Reduce duplication, improve documentation, refactor complex methods, follow SOLID principles, keep dependencies updated, address technical debt incrementally.
itests/README.mdDEPENDENCY_ORGANIZATION_REPORT.mdLast updated: November 2025