Skip to content

Latest commit

 

History

History
328 lines (268 loc) · 11.9 KB

JAVA_STANDARDS.md

File metadata and controls

328 lines (268 loc) · 11.9 KB

Java Development Standards

Runtime vs Syntax Compatibility

  • Runtime Environment: see .sdkmanrc for the current Java version
  • Syntax Requirement: core modules compile to whatever dotcms.core.compiler.release is set to in parent/pom.xml — check there before assuming a syntax level is safe
  • CLI Tools Exception: tools/dotcms-cli targets whatever maven.compiler.release is set to in tools/dotcms-cli/pom.xml — historically the most conservative target in the repo, kept for portability, not an exception for newer syntax

✅ Modern Java Syntax (Core Modules)

// Records, switch expressions, and text blocks become available once
// core modules compile to a high enough release — check
// dotcms.core.compiler.release in parent/pom.xml
var query = """
    SELECT c.identifier, c.title FROM contentlet c 
    WHERE c.structure_inode = ?
    """;

var status = switch (contentlet.getBaseType()) {
    case CONTENT -> "Content";
    case HTMLPAGE -> "Page";
    default -> "Unknown";
};

public record UserInfo(String id, String email, String name) {}

⚠️ Conservative Syntax Required (tools/dotcms-cli only)

// tools/dotcms-cli targets an older release than core — check
// maven.compiler.release in tools/dotcms-cli/pom.xml. Avoid
// records, switch expressions, and text blocks unless that changes
var users = userAPI.findActiveUsers();
var contentTypes = contentTypeAPI.findAll();

Optional<String> value = getValue();
String result = value.orElse("default");

List<String> names = users.stream()
    .map(User::getName)
    .filter(Objects::nonNull)
    .collect(Collectors.toList());

String query = "SELECT c.identifier, c.title FROM contentlet c " +
               "WHERE c.structure_inode = ?";

String status;
switch (contentlet.getBaseType()) {
    case CONTENT:
        status = "Content";
        break;
    case HTMLPAGE:
        status = "Page";
        break;
    default:
        status = "Unknown";
}

Core Development Patterns

API Locator Pattern (Required)

// Service access - ALWAYS use APILocator
UserAPI userAPI = APILocator.getUserAPI();
ContentletAPI contentletAPI = APILocator.getContentletAPI();

// Web services
UserWebAPI userWebAPI = WebAPILocator.getUserWebAPI();

// ❌ NEVER instantiate services directly
// UserAPIImpl userAPI = new UserAPIImpl();

Configuration Management (Required)

Security: All configuration must follow Security Principles

import com.dotmarketing.util.Config;

// Hierarchical naming for new properties
boolean enabled = Config.getBooleanProperty("experiments.enabled", false);
String url = Config.getStringProperty("experiments.auto-js-injection.url", "");

// Environment variables automatically get DOT_ prefix
// experiments.enabled → DOT_EXPERIMENTS_ENABLED

Property Resolution Order

  1. Environment variables with DOT_ prefix (e.g., DOT_EXPERIMENTS_ENABLED)
  2. System table for both transformed and original names
  3. Properties files for both transformed and original names

New Property Naming Convention

# Use hierarchical domain-driven naming (RECOMMENDED)
experiments.enabled=true
experiments.auto-js-injection.enabled=true
experiments.auto-js-injection.url=https://example.com/script.js
experiments.auto-js-injection.max-retries=3

health.check.database.timeout.seconds=2
health.include.system-details=true

Logging Standards (Required)

import com.dotmarketing.util.Logger;

Logger.info(this, "Operation completed successfully");
Logger.error(this, "Operation failed: " + error.getMessage(), error);

// ❌ NEVER use: System.out.println(), printStackTrace(), System.err.println()

Javadoc Requirements (Required)

  • New methods: Every new Java method must have Javadoc.
  • Edited methods: If you edit an existing method that has no Javadoc, add it as part of that edit — don't leave it undocumented.
  • Human-readable, not just AI-oriented: Write for a human developer, not for an AI tool. Plain language, no restating the method signature, no jargon dump. If a person wouldn't find it clear on their own, rewrite it.
  • Link dotCMS types: When a parameter or return type is a dotCMS domain object (e.g. com.dotmarketing.beans.Host, com.liferay.portal.model.User), reference it with {@link ClassName} so IDEs can jump straight to its definition. This is already the convention throughout the codebase — see HostAPI.search(...).
/**
 * Finds the entity matching the given id, for the given user.
 *
 * @param id the entity's identifier
 * @param user the {@link User} requesting the lookup, used for permission checks
 * @return the matching {@link MyEntity}
 * @throws DotDataException if the entity cannot be found
 */
public MyEntity findById(String id, User user) throws DotDataException {
    ...
}

Immutable Objects (Critical Pattern)

@Value.Immutable
@JsonSerialize(as = ImmutableMyEntity.class)
@JsonDeserialize(as = ImmutableMyEntity.class)
public abstract class MyEntity {
    public abstract String name();
    public abstract Optional<String> description();
    
    @Value.Default
    public boolean enabled() { return true; }
    
    public static Builder builder() { return ImmutableMyEntity.builder(); }
}

// Usage: MyEntity.builder().name("test").description("desc").build()
// IMPORTANT: Run ./mvnw compile after creating @Value.Immutable classes

REST view objects use a different style. The example above generates Immutable*-prefixed types. REST response views instead use the Abstract* interface style (@Value.Style(typeAbstract = "Abstract*")) so the generated type drops the prefix, plus passAnnotations = Schema.class for Swagger. See REST_API_PATTERNS.md → View Object Pattern.

Exception Handling (dotCMS Hierarchy)

try {
    riskyOperation();
} catch (SQLException e) {
    Logger.error(this, "Database operation failed: " + e.getMessage(), e);
    throw new DotDataException("Failed to process request", e);
} catch (SecurityException e) {
    throw new DotSecurityException("Access denied", e);
}

// Exception types: DotDataException, DotSecurityException, DotRuntimeException, DotStateException

Utility Methods (Null-Safe Patterns)

import com.dotmarketing.util.UtilMethods;

// ALWAYS use UtilMethods.isSet() for null checking
if (UtilMethods.isSet(myString)) {  // checks null, empty, and "null" string
    processString(myString);
}

// Collections
List<String> list = CollectionsUtils.list("item1", "item2");
Map<String, Object> map = CollectionsUtils.map("key1", "value1", "key2", "value2");

// Safe supplier pattern (avoids NullPointerException)
String value = UtilMethods.isSet(() -> complex.getObject().getValue()) 
    ? complex.getObject().getValue() 
    : "default";

Database Access Patterns

import com.dotmarketing.common.db.DotConnect;

// Query with parameters
DotConnect dotConnect = new DotConnect();
List<Map<String, Object>> results = dotConnect
    .setSQL("SELECT * FROM my_table WHERE column1 = ? AND column2 = ?")
    .addParam("value1")
    .addParam("value2")
    .loadResults();

// Use with LocalTransaction for atomic operations
LocalTransaction.wrapReturn(() -> {
    return dotConnect.setSQL("UPDATE my_table SET column1 = ?")
        .addParam("newValue")
        .executeUpdate();
});

Permission Checks — Batch vs Scalar

Always prefer the batch overload when filtering a collection. The scalar doesUserHavePermission(Permissionable, int, User, boolean) runs one DB/cache lookup per item; on a list of N items that is N round-trips (the N+1 problem). The batch overload resolves the entire collection in a single SQL UNION query.

// ❌ N+1 — one DB lookup per folder
for (Folder folder : folders) {
    if (permissionAPI.doesUserHavePermission(folder, PermissionAPI.PERMISSION_READ, user, false)) {
        visible.add(folder);
    }
}

// ✅ Batch — one SQL round-trip for the whole collection
List<Folder> visible = permissionAPI.filterCollection(
        folders, PermissionAPI.PERMISSION_READ, user, false);

Signature (PermissionAPI):

// Batch overload — uses getPermittedIds() in PermissionBitFactoryImpl under the hood
<P extends Permissionable> List<P> filterCollection(
        Collection<P> permissionables,
        int permissionType,
        User user,
        boolean respectFrontendRoles) throws DotDataException, DotSecurityException;

Notes:

  • Admin/system-user fast-path returns the full collection immediately (no SQL).
  • respectFrontendRoles=false excludes anonymous and front-end roles from the role set.
  • The existing filterCollection(List, int, boolean, User) overload is still available but does N+1 internally — prefer the batch overload for any new code that filters collections.
  • Implementation: PermissionBitAPIImpl + PermissionBitFactoryImpl.getPermittedIds().

CDI Patterns (For New Components)

@ApplicationScoped
public class MyService {
    private final JobQueueManagerAPI jobQueueManagerAPI;
    
    // Default constructor required for CDI proxy
    public MyService() {
        this.jobQueueManagerAPI = null;
    }
    
    @Inject
    public MyService(JobQueueManagerAPI jobQueueManagerAPI) {
        this.jobQueueManagerAPI = jobQueueManagerAPI;
    }
}

// Safe CDI bean access
Optional<MyService> service = CDIUtils.getBean(MyService.class);
MyService service = CDIUtils.getBeanThrows(MyService.class);

Build Integration Requirements

Maven Dependencies

  • Add dependency versions to bom/application/pom.xml only — never to dotCMS/pom.xml

Integration Testing

// Integration tests for REST endpoints extend IntegrationTestBase (JUnit 4)
public class MyResourceIntegrationTest extends IntegrationTestBase {

    @Test
    public void testEndpoint() {
        // Test implementation
    }
}

See Integration Tests for the full pattern, including the MainSuite registration gate.

After Code Changes

  • Immutable classes: Run ./mvnw compile after @Value.Immutable changes
  • Fast iteration: ./mvnw install -pl :dotcms-core --am -DskipTests (the --am flag matters — see CLAUDE.md's Build & Test Commands: without it, the build can fail on missing in-project deps)
  • Docker updates: Run ./mvnw clean install (without -Ddocker.skip)

Critical Docker Build Workflow

Docker image updates only happen when building WITHOUT -Ddocker.skip:

# Fast development cycle (no Docker image update)
./mvnw install -pl :dotcms-core --am -DskipTests -Ddocker.skip

# When ready to test in Docker (REQUIRED for new servlets/endpoints):
./mvnw -DskipTests clean install  # Updates Docker image
./mvnw -pl :dotcms-core -Pdocker-start -Dtomcat.port=8080

Legacy Patterns to Avoid in New Code

See: Progressive Enhancement for safe improvement strategies

// ❌ Avoid in new development
@Deprecated public class MyPortletAction extends PortletAction {}
@Deprecated public class MyAjax extends WfBaseAction {}
System.out.println("message");
System.getProperty("property");
StructureAPI structureAPI = APILocator.getStructureAPI();

// ✅ Use modern alternatives
@Path("/v1/resource") public class MyResource {}
Logger.info(this, "message");
Config.getStringProperty("property", "default");
ContentTypeAPI contentTypeAPI = APILocator.getContentTypeAPI();

// ✅ For class metadata analysis, prefer Jandex over reflection
List<Class<?>> annotatedClasses = JandexClassMetadataScanner.findClassesWithAnnotation(
    MyAnnotation.class, "com.dotcms.mypackage");
// See: docs/backend/JANDEX_METADATA_SCANNING.md

Location Information

  • Package Structure: com.dotcms.api, com.dotcms.business, com.dotcms.rest, com.dotcms.util
  • Legacy Packages: com.dotmarketing.*
  • Configuration: Use Config class for all configuration access
  • Exceptions: Use dotCMS exception hierarchy (DotDataException, DotSecurityException, etc.)