3. Publish handlers#

The clinic sample does not use the composite CrudHandler — every handler explicitly composes the granular interfaces it needs. The common pattern is QueryHandler + ReadHandler + SaveHandler, extending a project-local AbstractHandler that owns the shared concerns.

The AbstractHandler base#

A local base class keeps ACL, audit, and shared dependencies out of every handler:

// handler/AbstractHandler.java
public abstract class AbstractHandler implements PreProcessor {

    @Autowired
    protected AuthProvider authProvider;

    @Override
    public int aclCheck(Tuple tuple, HandlerContext ctx) {
        // Fine-grained ACL is enforced by palmyra-dbacl-mgmt at the filter
        // level; handlers get full rights once authentication has passed.
        return AclRights.ALL;
    }

    protected String currentUser() {
        return authProvider.getUser();
    }
}

Minimal handler — master data#

The simplest handler is just a composition declaration:

// handler/MstManufacturerHandler.java
@Component
@CrudMapping(
    mapping          = "/mstManufacturer",
    type             = MstManufacturerModel.class,
    secondaryMapping = "/mstManufacturer/{id}"
)
public class MstManufacturerHandler extends AbstractHandler
        implements QueryHandler, ReadHandler, SaveHandler { }

secondaryMapping is what makes GET /api/mstManufacturer/{id} work alongside the paginated collection at GET /api/mstManufacturer.

Handler with hooks — stock entries#

Real handlers override applyQueryFilter for default ordering, onQueryResult for computed fields, and preProcessRaw for input normalization:

// handler/StockEntryHandler.java
@Component
@CrudMapping(
    mapping          = "/stockEntry",
    type             = StockEntryModel.class,
    secondaryMapping = "/stockEntry/{id}"
)
public class StockEntryHandler extends AbstractHandler
        implements QueryHandler, ReadHandler, SaveHandler {

    @Override
    public QueryFilter applyQueryFilter(QueryFilter filter, HandlerContext ctx) {
        filter.addOrderDesc("id");                       // newest first by default
        return QueryHandler.super.applyQueryFilter(filter, ctx);
    }

    @Override
    public Tuple onQueryResult(Tuple tuple, Action action) {
        // derive a boolean status from the stock level
        Integer currQty = tuple.getAttributeAsInt("currentQuantity");
        tuple.setAttribute("status", (currQty == null || currQty == 0) ? 0 : 1);
        return QueryHandler.super.onQueryResult(tuple, action);
    }

    @Override
    public Tuple preProcessRaw(Tuple data, HandlerContext ctx) {
        Object currQty = data.get("currentQuantity");
        data.set("status", Objects.equals(currQty, 0) ? 0 : 1);
        return super.preProcessRaw(data, ctx);
    }
}

Handler with a preSave default#

Stamp defaults on mutations without exposing them to clients:

@Component
@CrudMapping(mapping = "/user", type = UserModel.class, secondaryMapping = "/user/{id}")
public class UserManagementHandler extends AbstractHandler
        implements QueryHandler, ReadHandler, CreateHandler, UpdateHandler {

    @Override
    public Tuple preCreate(Tuple tuple, HandlerContext ctx) {
        // the DB has a unique constraint on loginName, not email — keep them in sync
        tuple.setAttribute("loginName", tuple.get("email"));
        tuple.setAttribute("createdBy", currentUser());
        return tuple;
    }
}

Why granular over CrudHandler?#

The clinic project deliberately avoids the CrudHandler composite because:

  • Read-only endpoints (reports, pickers) need QueryHandler + ReadHandler but not mutations.
  • Master-data handlers use SaveHandler (upsert) instead of separate CreateHandler + UpdateHandler.
  • Some endpoints need CreateHandler + UpdateHandler but never DeleteHandler (audit requires soft-delete through status columns instead).

Composing the exact interfaces your endpoint needs keeps the surface honest.

Try it#

curl "http://localhost:8080/api/mstManufacturer?limit=10&sort=-id"
curl -X POST "http://localhost:8080/api/mstManufacturer" \
     -H 'Content-Type: application/json' \
     -d '{"name":"Acme Pharma","rating":4,"address":"..."}'

See also: QueryHandler, ReadHandler, SaveHandler, PreProcessor.