- Runtime Environment: see
.sdkmanrcfor the current Java version - Syntax Requirement: core modules compile to whatever
dotcms.core.compiler.releaseis set to inparent/pom.xml— check there before assuming a syntax level is safe - CLI Tools Exception:
tools/dotcms-clitargets whatevermaven.compiler.releaseis set to intools/dotcms-cli/pom.xml— historically the most conservative target in the repo, kept for portability, not an exception for newer syntax
// 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) {}// 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";
}// 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();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- Environment variables with
DOT_prefix (e.g.,DOT_EXPERIMENTS_ENABLED) - System table for both transformed and original names
- Properties files for both transformed and original names
# 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=trueimport 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()- 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 — seeHostAPI.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 {
...
}@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 classesREST view objects use a different style. The example above generates
Immutable*-prefixed types. REST response views instead use theAbstract*interface style (@Value.Style(typeAbstract = "Abstract*")) so the generated type drops the prefix, pluspassAnnotations = Schema.classfor Swagger. See REST_API_PATTERNS.md → View Object Pattern.
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, DotStateExceptionimport 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";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();
});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=falseexcludes 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().
@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);- Add dependency versions to
bom/application/pom.xmlonly — never todotCMS/pom.xml
// 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.
- Immutable classes: Run
./mvnw compileafter@Value.Immutablechanges - Fast iteration:
./mvnw install -pl :dotcms-core --am -DskipTests(the--amflag 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)
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=8080See: 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- Package Structure:
com.dotcms.api,com.dotcms.business,com.dotcms.rest,com.dotcms.util - Legacy Packages:
com.dotmarketing.* - Configuration: Use
Configclass for all configuration access - Exceptions: Use dotCMS exception hierarchy (
DotDataException,DotSecurityException, etc.)