Project setup — deep walk-through#
Installation is the three-line quick-start. This page is the fuller version — the one you want when starting a new service from scratch: toolchain pins, full build.gradle, project layout, and the gotchas that bite on day one.
Verified with Gradle 9.4.1 and palmyra-spring:1.4.4.
1. Runtime requirements#
| Component | Version |
|---|---|
| JDK | 21 or later |
| Build tool | Gradle 9.4+ (lower versions not supported) |
| Spring Boot | Latest stable (Jakarta EE 10 — jakarta.*, not javax.*) |
io.spring.dependency-management |
Latest stable |
| Database | MariaDB / MySQL / PostgreSQL / Oracle / DB2 |
Always pin org.springframework.boot and io.spring.dependency-management to their latest stable releases at project inception; review on a regular cadence.
Running Gradle on JDK 21#
- Launcher JVM — runs
gradleitself. Gradle 9 accepts JDK 17–25. Check withgradle --version. - Toolchain JVM — compiles + runs your code. Declared in
build.gradle. Gradle resolves it from local installs, then downloads viafoojay-resolverif configured. - If JDK 21 is absent locally, the
foojay-resolver-conventionplugin insettings.gradleauto-downloads it (~150 MB, cached). - Verify:
gradle -q javaToolchains. - To pin the launcher JVM: set
JAVA_HOMEororg.gradle.java.homeingradle.properties. Do not use this to pin the build JDK — use the toolchain block.
2. Artifact coordinates#
Repository: https://repo.palmyralabs.com/releases (public, no auth).
Core dependency: com.palmyralabs.palmyra:palmyra-spring:1.4.4 — transitively pulls the full stack (palmyra-core, palmyra-base, palmyra-shared, palmyra-api2db, sqlstore-*, dbstore-mariadb, palmyra-java-client).
Optional extensions:
| Artifact | Purpose | Guide |
|---|---|---|
com.palmyralabs.palmyra.extn:palmyra-dbpwd-mgmt:1.4.4 |
DB-password auth | User Management |
com.palmyralabs.palmyra.extn:palmyra-dbacl-mgmt:1.4.4 |
Group / menu / permission ACL | ACL Management |
Maven group uses
.extn; Java package root iscom.palmyralabs.palmyra.ext.*(no trailingn). This trips new developers every time.
3. Minimal project scaffold#
settings.gradle#
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
rootProject.name = 'my-service'foojay-resolver-convention 0.8.x fails on Gradle 9 — use 1.0.0 or later.
build.gradle#
plugins {
id 'org.springframework.boot' version '4.0.5' // latest stable
id 'io.spring.dependency-management' version '1.1.7' // latest stable
id 'java'
id 'application'
}
group = 'com.example'
version = '1.0.0-SNAPSHOT'
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }
application { mainClass = 'com.example.app.AppMain' }
repositories {
mavenCentral()
maven { url 'https://repo.palmyralabs.com/releases' }
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'com.palmyralabs.palmyra:palmyra-spring:1.4.4'
compileOnly 'org.projectlombok:lombok:1.18.34'
annotationProcessor 'org.projectlombok:lombok:1.18.34'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:3.4.0'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') { useJUnitPlatform() }AppMain.java#
package com.example.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import com.palmyralabs.palmyra.spring.PalmyraSpringConfiguration;
@SpringBootApplication
@Import(PalmyraSpringConfiguration.class)
@EnableJpaRepositories(basePackages = "com.example.app")
@EntityScan(basePackages = "com.example.app.entity")
public class AppMain {
public static void main(String[] args) {
SpringApplication.run(AppMain.class, args);
}
}@Import(PalmyraSpringConfiguration.class) is mandatory — the jar has no auto-configuration metadata. Drop @EnableJpaRepositories / @EntityScan for tuple-only services. When adding palmyra-dbacl-mgmt, also import com.palmyralabs.palmyra.ext.aclmgmt.AclMgmtConfiguration.
application.yml#
spring:
application:
name: my-service
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
jdbc:
time_zone: UTC
datasource:
url: jdbc:mariadb://localhost:3306/mydb
username: mydb
password: mydb
driverClassName: org.mariadb.jdbc.Driver
server:
port: 8080
servlet:
context-path: /apiThe Mental Model page explains why
ddl-auto: validate(ornone) is the right default — the DB schema is the source of truth for Palmyra’s JDBC-metadata discovery.
4. Build and run#
gradle build # build/libs/<name>-<version>.jar
gradle bootRun
java -jar build/libs/my-service-1.0.0-SNAPSHOT.jarEndpoints: http://localhost:8080/api/<mapping> — the URL is context-path + the path on @CrudMapping.
5. @CrudMapping in practice#
Every handler is bound to a REST endpoint and a model type via @CrudMapping.
| Argument | Type | Required | Purpose |
|---|---|---|---|
mapping |
String[] |
yes | Collection URL path(s). Supports path variables (e.g. /asset/{empId}/detail). |
type |
Class<?> |
yes | Palmyra model class (@PalmyraType) bound to this endpoint. |
secondaryMapping |
String |
conditional | Per-record URL. Required for ReadHandler, UpdateHandler (PUT), and DeleteHandler. Must end with an id path variable. |
queryType |
Class<?> |
no | Optional separate query-shape class (defaults to type). |
@Component
@CrudMapping(
mapping = "/asset/{empId}/detail",
type = AssetDetailModel.class,
secondaryMapping = "/asset/{empId}/detail/{id}"
)
public class AssetDetailHandler
implements QueryHandler, ReadHandler, CreateHandler, UpdateHandler, DeleteHandler {
public int aclCheck(Tuple item, HandlerContext ctx) { return ACLRights.ALL; }
}Omit secondaryMapping only when the handler implements QueryHandler / CreateHandler alone — any handler that touches a single record by id needs it.
Path variables#
Path-variable placeholders declared in mapping / secondaryMapping (e.g. {empId}, {id}) are resolved per request and exposed through HandlerContext.getParams() as Map<String, String>. Both collection- and single-record paths feed the same map.
ctx.getParams().get("empId")returns the matched value for that placeholder.- Typical usage — inside
applyQueryFilter, read the variable and narrow the filter:filter.addCondition(Criteria.EQ("employeeId", ctx.getParams().get("empId"))); return QueryHandler.super.applyQueryFilter(filter, ctx); Criterialives atcom.zitlab.palmyra.sqlbuilder.condition.Criteria; factory helpers includeEQ,NE,IN,LIKE, etc.- A project-level
AbstractHandlerbase class (app-defined, not framework-provided) is a common place to centralise cross-cutting behaviour such asaclCheck.
6. Project layout convention#
src/main/java/com/example/app/
├── AppMain.java
├── config/ # @Configuration classes
├── entity/ # JPA @Entity (only if using Spring Data JPA for ancillary queries)
├── model/ # @PalmyraType POJOs
├── apihandler/ # @CrudMapping handlers
├── service/ # business services
├── dao/ # hand-written JDBC / native queries
├── integration/ # external HTTP clients
└── security/ # SecurityFilterChain, AuthenticationProviderJPA entities are optional — Palmyra reads schema from JDBC metadata directly (see Mental Model). Add entity/ only if you also need JpaRepository.findBy… for ancillary Java-side queries.
7. Day-one gotchas#
@Import(PalmyraSpringConfiguration.class)is mandatory — no auto-configuration metadata in the jar.@CrudMapping.secondaryMappingis required whenever the handler implementsReadHandler,UpdateHandler(PUT), orDeleteHandler. Omission causes a 404 on single-record operations.ACLRights.ALLis anint, not an enum — no.getMask()call.MutableAction.INSERTdoes not exist — useMutableAction.CREATE. Full set:CREATE, UPDATE, DELETE, NONE, NULL, SAVE, CREATE_IFNOT_EXISTS, NO_PROCESS, PARENT_ACTION.SaveHandler/CreateHandler/UpdateHandlerrequire an explicitaclCheckimplementation — no default.- All
javax.*→jakarta.*(Spring Boot 3 / Jakarta EE 10). - Custom (non-CRUD) actions — use plain Spring
@RestController+@Service. Do not use@ActionMappingas a general substitute. foojay-resolver-convention 0.8.xfails on Gradle 9 — must be 1.0.0+.- Maven group
.extn, Java package.ext— no trailingnin packages. - Palmyra
updateis a full-body PUT, not PATCH. Partial update bodies return 500 withDataValidationException: Mandatory attribute … must be provided.
See also#
- Mental Model — the framework’s worldview, in one page
@CrudMapping- Handler overview
- User Management extension
- ACL Management extension