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 gradle itself. Gradle 9 accepts JDK 17–25. Check with gradle --version.
  • Toolchain JVM — compiles + runs your code. Declared in build.gradle. Gradle resolves it from local installs, then downloads via foojay-resolver if configured.
  • If JDK 21 is absent locally, the foojay-resolver-convention plugin in settings.gradle auto-downloads it (~150 MB, cached).
  • Verify: gradle -q javaToolchains.
  • To pin the launcher JVM: set JAVA_HOME or org.gradle.java.home in gradle.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 is com.palmyralabs.palmyra.ext.* (no trailing n). 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: /api

The Mental Model page explains why ddl-auto: validate (or none) 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.jar

Endpoints: 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);
  • Criteria lives at com.zitlab.palmyra.sqlbuilder.condition.Criteria; factory helpers include EQ, NE, IN, LIKE, etc.
  • A project-level AbstractHandler base class (app-defined, not framework-provided) is a common place to centralise cross-cutting behaviour such as aclCheck.

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, AuthenticationProvider

JPA 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.secondaryMapping is required whenever the handler implements ReadHandler, UpdateHandler (PUT), or DeleteHandler. Omission causes a 404 on single-record operations.
  • ACLRights.ALL is an int, not an enum — no .getMask() call.
  • MutableAction.INSERT does not exist — use MutableAction.CREATE. Full set: CREATE, UPDATE, DELETE, NONE, NULL, SAVE, CREATE_IFNOT_EXISTS, NO_PROCESS, PARENT_ACTION.
  • SaveHandler / CreateHandler / UpdateHandler require an explicit aclCheck implementation — no default.
  • All javax.*jakarta.* (Spring Boot 3 / Jakarta EE 10).
  • Custom (non-CRUD) actions — use plain Spring @RestController + @Service. Do not use @ActionMapping as a general substitute.
  • foojay-resolver-convention 0.8.x fails on Gradle 9 — must be 1.0.0+.
  • Maven group .extn, Java package .ext — no trailing n in packages.
  • Palmyra update is a full-body PUT, not PATCH. Partial update bodies return 500 with DataValidationException: Mandatory attribute … must be provided.

See also#