Skip to content

Refactor: Generalize OCPP Task Management#91

Merged
juherr merged 4 commits intomainfrom
refactor-task-management
Sep 15, 2025
Merged

Refactor: Generalize OCPP Task Management#91
juherr merged 4 commits intomainfrom
refactor-task-management

Conversation

@google-labs-jules
Copy link
Copy Markdown

@google-labs-jules google-labs-jules bot commented Sep 13, 2025

This commit refactors the OCPP task management system to be more generic and maintainable. The previous implementation had a rigid class hierarchy and coupled the task logic directly to specific OCPP versions, making it difficult to add new tasks or support new protocol versions.

The new architecture introduces a mapper-based approach to decouple the core task logic from the version-specific OCPP message formats.

Key changes:

  • TaskDefinition: A new interface to define the parameters of a task.
  • RequestMapper and ResponseMapper: Interfaces for mapping between the generic TaskDefinition and the version-specific OCPP request/response objects.
  • OcppVersionHandler: A new interface that groups the RequestMapper and ResponseMapper for a specific OCPP version. This was implemented based on user feedback to improve the design.
  • CommunicationTask: The base task class has been refactored to use the new mapper-based system, removing the need for version-specific abstract methods.
  • All existing task classes (ResetTask, SetChargingProfileTask, etc.) have been updated to use the new architecture.
  • ChargePointServiceInvokerImpl: The invoker has been updated to work with the new generic task system.

This refactoring significantly improves the extensibility of the task management system, reduces code duplication, and makes it easier to add support for future OCPP versions or custom tasks.


PR created automatically by Jules for task 10686966425854007946

Summary by CodeRabbit

  • New Features

    • Unified, version-aware handling for OCPP operations for consistent behavior across protocols.
    • Expanded charge point metadata (endpoint, vendor/model/serial, firmware, SIM/meter info, diagnostics, last heartbeat) exposed in UI.
  • Refactor

    • Centralized task/version mapping and standardized task invocation and callbacks for more reliable multi-version support.
    • Transport layers consolidated to share dispatch logic.
  • Documentation

    • Internal clarity improvements around version routing and callbacks (no user-facing docs changed).

@google-labs-jules
Copy link
Copy Markdown
Author

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll acknowledge your comments with a 👀 emoji and then get to work. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

I will automatically address your feedback. For any comments you don't want me to act on, just include (aside).



For security, I will only act on instructions from the user who triggered this task for this pull request.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 13, 2025

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Refactors OCPP task handling to a mapper-driven, version-aware framework: introduces TaskDefinition, RequestMapper, ResponseMapper, and OcppVersionHandler; updates CommunicationTask and RequestResult generics; migrates many tasks to the new model; removes legacy per-version handlers/subclasses; adapts SOAP/JSON invokers and service wiring.

Changes

Cohort / File(s) Change summary
Core OCPP task framework
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/CommunicationTask.java, .../RequestResult.java, .../task/impl/RequestMapper.java, .../ResponseMapper.java, .../OcppVersionHandler.java, .../TaskDefinition.java
Introduces TaskDefinition/OcppVersionHandler and mapper interfaces; changes CommunicationTask API to mapper-driven methods (getRequest, createHandler, typed addNewResponse), adds taskDefinition/defaultCallback; RequestResult.response becomes generic; removes old 1.5/1.6 base classes.
Tasks migrated to mapper-driven model
steve-core/src/main/java/.../task/*Task.java (CancelReservationTask, ChangeAvailabilityTask, ChangeConfigurationTask, ClearCacheTask, ClearChargingProfileTask, DataTransferTask, GetCompositeScheduleTask, GetConfigurationTask, GetDiagnosticsTask, GetLocalListVersionTask, RemoteStartTransactionTask, RemoteStopTransactionTask, ReserveNowTask, ResetTask, SendLocalListTask, SetChargingProfileTask, TriggerMessageTask, UnlockConnectorTask, UpdateFirmwareTask, Get* / Remote* variants)`
Each task now extends CommunicationTask<S, U> and supplies a static TASK_DEFINITION mapping OcppVersion → RequestMapper/ResponseMapper; removes per-version request/handler methods and AsyncHandler usage; constructors often add caller; callbacks unified.
Removed legacy specialized tasks
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTaskAdhoc.java, .../SetChargingProfileTaskFromDB.java
Deletes ad-hoc and DB-backed subclasses; functionality consolidated into new SetChargingProfileTask (constructors updated).
Invoker & transport updates
steve-ocpp-transport-soap/.../ChargePointServiceSoapInvoker.java, steve/src/main/java/.../ChargePointServiceInvokerImpl.java, steve-ocpp-transport-websocket/.../OcppConfiguration.java, steve-ocpp-transport-websocket/.../CommunicationContext.java
Standardizes version dispatch: invokers call task.getRequest(ocppVersion) and task.createHandler(...); ChargePointServiceInvokerImpl adds generic invoke helper and replaces duplicated SOAP/JSON conditional blocks; websocket config/context adapt to the new API and nullable handlers.
Service wiring & callers
steve-core/src/main/java/de/rwth/idsg/steve/service/ChargePointServiceClient.java
Updates callers to pass caller arg into task constructors; replaces usages of removed SetChargingProfileTaskFromDB with SetChargingProfileTask.
DTO / DB mapping changes
steve-core/.../repository/dto/ChargePoint.java, steve-jooq/.../ChargePointMapper.java, steve-jooq/.../TransactionMapper.java, steve-jooq/.../ChargePointRepositoryImpl.java
Expands ChargePoint.Details with device/diagnostics fields; mapper switches Optional→@nullable address and uses instant conversions; TransactionMapper enum mapping adjusted.
UI and templates
steve-ui-jsp/.../TasksController.java, .../00-cp-misc.jsp, .../chargepointDetails.jsp
Views and controller updated to reflect moved DTO fields (top-level cp.* properties) and RequestResult.getResponse() usage.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant Invoker as ChargePointServiceInvokerImpl
  participant Transport as ChargePointServiceSoapInvoker
  participant Task as CommunicationTask
  participant TD as TaskDefinition
  participant VH as OcppVersionHandler
  participant CP as ChargePoint

  Caller->>Invoker: request operation (task)
  Invoker->>Transport: invoke(cp, task, soapAction, jsonAction)
  Transport->>Task: getRequest(ocppVersion)
  Task->>TD: versionHandlers.get(ocppVersion)
  TD->>VH: resolve handler
  VH->>Task: RequestMapper.map(task) -> RequestType
  Transport->>CP: send version-specific request
  CP-->>Transport: response
  Transport->>Task: createHandler(chargeBoxId)
  VH->>Task: ResponseMapper used inside handler -> typed RESPONSE
  Task->>Task: addNewResponse(chargeBoxId, RESPONSE)
  Task-->>Invoker: results available
  Invoker-->>Caller: return aggregated result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

I’m a rabbit with a mapper bag,
Hopping versions, tail a wag.
Requests and responses now align,
One definition, one design.
I nibble bugs and stitch the flow—hooray! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.54% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Refactor: Generalize OCPP Task Management" accurately and concisely summarizes the primary change in the diff — a refactor that generalizes OCPP task handling into a mapper-based, version-agnostic system. It is specific enough for a teammate scanning PR history and avoids vague phrasing or extraneous detail.

Comment @coderabbitai help to get the list of available commands and usage tips.

@juherr
Copy link
Copy Markdown
Owner

juherr commented Sep 13, 2025

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 13, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@juherr
Copy link
Copy Markdown
Owner

juherr commented Sep 13, 2025

@google-labs-jules build is failing

@google-labs-jules
Copy link
Copy Markdown
Author

@coderabbitai review

Great! I'm ready to address any feedback that comes from the review.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 13, 2025

Skipped: Cannot respond to another bot.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetConfigurationTask.java (1)

45-49: Fix NPE when unknownKeys is null

JOINER.join(unknownKeys) will NPE if response has no unknown keys. Guard it.

+import java.util.Collections;
@@
-        private ResponseWrapper(List<KeyValue> configurationKeys, List<String> unknownKeys) {
+        private ResponseWrapper(List<KeyValue> configurationKeys, List<String> unknownKeys) {
             this.configurationKeys = configurationKeys;
-            this.unknownKeys = JOINER.join(unknownKeys);
+            this.unknownKeys = JOINER.join(unknownKeys == null ? Collections.emptyList() : unknownKeys);
         }
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/CommunicationTask.java (1)

133-141: Null-guard addNewError to avoid NPE for unknown chargeBoxId

addNewResponse guards unknown IDs; addNewError should mirror that.

-    public void addNewError(String chargeBoxId, @Nullable String errorMessage) {
-        resultMap.get(chargeBoxId).setErrorMessage(errorMessage);
+    public void addNewError(String chargeBoxId, @Nullable String errorMessage) {
+        var result = resultMap.get(chargeBoxId);
+        if (result == null) {
+            log.warn("Received error for unknown chargeBoxId '{}'", chargeBoxId);
+            return;
+        }
+        result.setErrorMessage(errorMessage);
🧹 Nitpick comments (43)
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/RequestResult.java (1)

31-34: Prevent ambiguous states: ensure only one of response/error is set

Right now both fields can be non-null, which is ambiguous. Consider adding small helpers and guiding callers to use them, or enforce via factories.

Example additions (outside the shown lines):

public boolean isSuccess() {
    return errorMessage == null;
}

public static <U> RequestResult<U> success(@Nullable U response) {
    var r = new RequestResult<U>();
    r.setResponse(response);
    return r;
}

public static <U> RequestResult<U> error(String message) {
    var r = new RequestResult<U>();
    r.setErrorMessage(message);
    return r;
}
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/RequestMapper.java (2)

24-27: Align exception policy with ResponseMapper to avoid asymmetry.

Either let RequestMapper#map declare a checked exception too, or make ResponseMapper#map non-throwing (preferred) and surface mapping errors as unchecked (e.g., MappingException). This prevents awkward lambda usage inconsistencies.


19-27: Consider moving SPI out of .impl package.

Since this is part of the public contract used by tasks/handlers, place it under ...ocpp.task.api (or similar) to signal “stable API”, not implementation detail.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/ResponseMapper.java (1)

23-26: Prefer non-throwing mapper; use unchecked exceptions for mapping failures.

Checked exceptions on functional interfaces make method refs and composition clumsy. Suggest removing throws Exception and, where needed, throwing an unchecked MappingException instead.

Apply within this hunk:

 @FunctionalInterface
 public interface ResponseMapper<T extends ResponseType, U> {
-    U map(T response) throws Exception;
+    U map(T response);
 }
steve/src/main/java/de/rwth/idsg/steve/ocpp/ChargePointServiceInvokerImpl.java (2)

61-65: Nice generic router; allow contravariance for broader method-ref compatibility.

Widen the second type of BiConsumer to ? super T to accept supertypes without additional casts.

-    private <T extends CommunicationTask<?, ?>> void invoke(
+    private <T extends CommunicationTask<?, ?>> void invoke(
             ChargePointSelect cp,
             T task,
-            BiConsumer<ChargePointSelect, T> soapAction,
-            BiConsumer<ChargePointSelect, T> jsonAction) {
+            BiConsumer<ChargePointSelect, ? super T> soapAction,
+            BiConsumer<ChargePointSelect, ? super T> jsonAction) {

102-108: Add missing @OverRide.

All other interface impls are annotated; add for consistency and compiler checks.

-    public void changeAvailability(ChargePointSelect cp, ChangeAvailabilityTask task) {
+    @Override
+    public void changeAvailability(ChargePointSelect cp, ChangeAvailabilityTask task) {
steve-ocpp-transport-soap/src/main/java/de/rwth/idsg/steve/ocpp/soap/ChargePointServiceSoapInvoker.java (2)

295-311: Don’t reuse a static Exception instance; create a fresh, descriptive one per call.

Reusing EXCEPTION_V12/EXCEPTION_V15 across threads loses stack context and reduces diagnostics.

-            case V_12 -> task.failed(cp.getChargeBoxId(), EXCEPTION_V12);
+            case V_12 -> task.failed(cp.getChargeBoxId(), unsupported(ocppVersion, "DataTransfer"));

Add a helper (outside this hunk) and apply similarly to other unsupported cases:

private static IllegalArgumentException unsupported(OcppVersion v, String op) {
    return new IllegalArgumentException("Operation " + op + " is not supported by OCPP " + v + " station");
}

69-92: Guard against mismatched request type per version (debug-only).

Because generic typing can’t enforce the concrete SOAP request class, consider a lightweight assert/log to catch wrong mapper wiring early (e.g., verify package prefix vs. version) before invoking *Async. Optional, but helps during refactors.

steve-core/src/main/java/de/rwth/idsg/steve/service/ChargePointServiceClient.java (3)

366-379: Extract hard-coded caller to a constant.

Avoid magic string; centralize for reuse/testing.

 public class ChargePointServiceClient {
+    private static final String CALLER_STEVE = "SteVe";
@@
-        CancelReservationTask task = new CancelReservationTask(params, reservationRepository, "SteVe");
+        CancelReservationTask task = new CancelReservationTask(params, reservationRepository, CALLER_STEVE);

414-421: Use the same constant for setChargingProfile.

-        SetChargingProfileTask task = new SetChargingProfileTask(params, details, chargingProfileRepository, "SteVe");
+        SetChargingProfileTask task = new SetChargingProfileTask(
+                params, details, chargingProfileRepository, CALLER_STEVE);

366-421: API consistency: consider overloads that accept caller.

You've added caller variants for RemoteStart/Stop/Unlock; mirroring that for CancelReservation and SetChargingProfile would keep the client API uniform.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/TaskDefinition.java (1)

30-31: Provide a helper to fetch a handler with a clear error if missing.

Improves diagnostics when a version isn’t configured.

// Add inside class (no change to the builder API)
public OcppVersionHandler<S, U> handlerFor(OcppVersion v) {
    OcppVersionHandler<S, U> h = versionHandlers.get(v);
    if (h == null) {
        throw new IllegalArgumentException("No OCPP handler configured for version: " + v);
    }
    return h;
}
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/UnlockConnectorTask.java (2)

32-56: Reduce duplication and add lightweight param validation in request mappers.

  • The three lambdas are identical except package/type names. Consider a small static helper to validate connectorId and build the request; reduces repetition and guards bad input early.
-                            OcppVersion.V_12,
-                                    new OcppVersionHandler<>(
-                                            task -> new ocpp.cp._2010._08.UnlockConnectorRequest()
-                                                    .withConnectorId(
-                                                            task.getParams().getConnectorId()),
+                            OcppVersion.V_12,
+                                    new OcppVersionHandler<>(
+                                            task -> new ocpp.cp._2010._08.UnlockConnectorRequest()
+                                                    .withConnectorId(requirePositiveConnectorId(task.getParams())),
                                             (ocpp.cp._2010._08.UnlockConnectorResponse r) ->
                                                     r.getStatus().value()),
@@
-                            OcppVersion.V_15,
+                            OcppVersion.V_15,
                                     new OcppVersionHandler<>(
-                                            task -> new ocpp.cp._2012._06.UnlockConnectorRequest()
-                                                    .withConnectorId(
-                                                            task.getParams().getConnectorId()),
+                                            task -> new ocpp.cp._2012._06.UnlockConnectorRequest()
+                                                    .withConnectorId(requirePositiveConnectorId(task.getParams())),
                                             (ocpp.cp._2012._06.UnlockConnectorResponse r) ->
                                                     r.getStatus().value()),
@@
-                            OcppVersion.V_16,
+                            OcppVersion.V_16,
                                     new OcppVersionHandler<>(
-                                            task -> new ocpp.cp._2015._10.UnlockConnectorRequest()
-                                                    .withConnectorId(
-                                                            task.getParams().getConnectorId()),
+                                            task -> new ocpp.cp._2015._10.UnlockConnectorRequest()
+                                                    .withConnectorId(requirePositiveConnectorId(task.getParams())),
                                             (ocpp.cp._2015._10.UnlockConnectorResponse r) ->
                                                     r.getStatus().value())))

Add (outside the shown hunk):

private static int requirePositiveConnectorId(UnlockConnectorParams p) {
    int id = p.getConnectorId();
    if (id <= 0) throw new IllegalArgumentException("connectorId must be > 0");
    return id;
}

If upstream DTO validation already enforces this, feel free to skip and confirm.


32-56: Consider returning typed enums instead of String for status.

Using the OCPP enum (e.g., UnlockStatus) avoids fragile string matching downstream.

Would you like a follow-up patch converting the generic type from String to the concrete status enums for this and similar tasks?

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/RemoteStopTransactionTask.java (2)

32-56: Validate transactionId and consider de-duplicating mapper boilerplate.

  • Add a simple positive check for transactionId.
  • The three mappers are identical; a helper would reduce repetition.
-                                            task -> new ocpp.cp._2010._08.RemoteStopTransactionRequest()
-                                                    .withTransactionId(
-                                                            task.getParams().getTransactionId()),
+                                            task -> new ocpp.cp._2010._08.RemoteStopTransactionRequest()
+                                                    .withTransactionId(requirePositiveTxId(task.getParams())),
@@
-                                            task -> new ocpp.cp._2012._06.RemoteStopTransactionRequest()
-                                                    .withTransactionId(
-                                                            task.getParams().getTransactionId()),
+                                            task -> new ocpp.cp._2012._06.RemoteStopTransactionRequest()
+                                                    .withTransactionId(requirePositiveTxId(task.getParams())),
@@
-                                            task -> new ocpp.cp._2015._10.RemoteStopTransactionRequest()
-                                                    .withTransactionId(
-                                                            task.getParams().getTransactionId()),
+                                            task -> new ocpp.cp._2015._10.RemoteStopTransactionRequest()
+                                                    .withTransactionId(requirePositiveTxId(task.getParams())),

Add (outside the shown hunk):

private static int requirePositiveTxId(RemoteStopTransactionParams p) {
    int id = p.getTransactionId();
    if (id <= 0) throw new IllegalArgumentException("transactionId must be > 0");
    return id;
}

32-56: Return typed enums instead of String for status to avoid fragile comparisons.

If agreeable, I can draft a follow-up converting the generic type to the status enum and adapting downstream uses.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetCompositeScheduleTask.java (2)

35-48: Validate params before building the request.

Guard common mistakes early (duration <= 0, connectorId <= 0); keeps failures deterministic before transport.

-                            new OcppVersionHandler<>(
-                                    task -> {
-                                        GetCompositeScheduleParams p = task.getParams();
-                                        return new GetCompositeScheduleRequest()
-                                                .withConnectorId(p.getConnectorId())
-                                                .withDuration(p.getDurationInSeconds())
-                                                .withChargingRateUnit(p.getChargingRateUnit());
-                                    },
+                            new OcppVersionHandler<>(
+                                    task -> {
+                                        GetCompositeScheduleParams p = task.getParams();
+                                        if (p.getConnectorId() <= 0) {
+                                            throw new IllegalArgumentException("connectorId must be > 0");
+                                        }
+                                        if (p.getDurationInSeconds() <= 0) {
+                                            throw new IllegalArgumentException("durationInSeconds must be > 0");
+                                        }
+                                        return new GetCompositeScheduleRequest()
+                                                .withConnectorId(p.getConnectorId())
+                                                .withDuration(p.getDurationInSeconds())
+                                                .withChargingRateUnit(p.getChargingRateUnit());
+                                    },

35-49: Consider centralizing “status/response passthrough” mappers.

Multiple tasks use identical patterns; a small factory (e.g., Mappers.statusValue(), Mappers.identity()) would cut boilerplate and ensure consistency.

I can draft a minimal utility in task.impl if desired.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ClearChargingProfileTask.java (3)

71-74: Log the unexpected enum value for easier diagnostics

Include the actual filterType in the log entry.

Apply:

-                        log.warn("Unexpected {} enum value", ClearChargingProfileFilterType.class.getSimpleName());
+                        log.warn("Unexpected {} value: {}", 
+                                 ClearChargingProfileFilterType.class.getSimpleName(),
+                                 getParams().getFilterType());

86-93: Harden error-to-string handling

Avoid possible nulls; use safe conversion for both error and exception message.

-            public void successError(String chargeBoxId, Object error) {
-                addNewError(chargeBoxId, error.toString());
+            public void successError(String chargeBoxId, Object error) {
+                addNewError(chargeBoxId, String.valueOf(error));
             }
@@
-            public void failed(String chargeBoxId, Exception e) {
-                addNewError(chargeBoxId, e.getMessage());
+            public void failed(String chargeBoxId, Exception e) {
+                addNewError(chargeBoxId, e.getMessage() != null ? e.getMessage() : e.toString());
             }

110-118: Apply same safe error conversion in defaultCallback

Mirror the above null-safe handling.

-            public void successError(String chargeBoxId, Object error) {
-                addNewError(chargeBoxId, error.toString());
+            public void successError(String chargeBoxId, Object error) {
+                addNewError(chargeBoxId, String.valueOf(error));
             }
@@
-            public void failed(String chargeBoxId, Exception e) {
-                addNewError(chargeBoxId, e.getMessage());
+            public void failed(String chargeBoxId, Exception e) {
+                addNewError(chargeBoxId, e.getMessage() != null ? e.getMessage() : e.toString());
             }
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/UpdateFirmwareTask.java (1)

92-99: Null-safe error propagation in defaultCallback

Use safe conversions.

-            public void successError(String chargeBoxId, Object error) {
-                addNewError(chargeBoxId, error.toString());
+            public void successError(String chargeBoxId, Object error) {
+                addNewError(chargeBoxId, String.valueOf(error));
             }
@@
-            public void failed(String chargeBoxId, Exception e) {
-                addNewError(chargeBoxId, e.getMessage());
+            public void failed(String chargeBoxId, Exception e) {
+                addNewError(chargeBoxId, e.getMessage() != null ? e.getMessage() : e.toString());
             }
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetLocalListVersionTask.java (1)

64-71: Null-safe error propagation in defaultCallback

Same pattern as other tasks.

-            public void successError(String chargeBoxId, Object error) {
-                addNewError(chargeBoxId, error.toString());
+            public void successError(String chargeBoxId, Object error) {
+                addNewError(chargeBoxId, String.valueOf(error));
             }
@@
-            public void failed(String chargeBoxId, Exception e) {
-                addNewError(chargeBoxId, e.getMessage());
+            public void failed(String chargeBoxId, Exception e) {
+                addNewError(chargeBoxId, e.getMessage() != null ? e.getMessage() : e.toString());
             }
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ResetTask.java (1)

78-85: Null-safe error propagation in defaultCallback

Apply the same safe conversions.

-            public void successError(String chargeBoxId, Object error) {
-                addNewError(chargeBoxId, error.toString());
+            public void successError(String chargeBoxId, Object error) {
+                addNewError(chargeBoxId, String.valueOf(error));
             }
@@
-            public void failed(String chargeBoxId, Exception e) {
-                addNewError(chargeBoxId, e.getMessage());
+            public void failed(String chargeBoxId, Exception e) {
+                addNewError(chargeBoxId, e.getMessage() != null ? e.getMessage() : e.toString());
             }
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ReserveNowTask.java (2)

91-101: Null-safe error propagation (constructor callback and defaultCallback)

Use safe conversions for error and exception messages.

-            public void successError(String chargeBoxId, Object error) {
-                addNewError(chargeBoxId, error.toString());
+            public void successError(String chargeBoxId, Object error) {
+                addNewError(chargeBoxId, String.valueOf(error));
             }
@@
-            public void failed(String chargeBoxId, Exception e) {
-                addNewError(chargeBoxId, e.getMessage());
+            public void failed(String chargeBoxId, Exception e) {
+                addNewError(chargeBoxId, e.getMessage() != null ? e.getMessage() : e.toString());
             }

Also applies to: 118-125


38-73: Minor duplication between 1.5 and 1.6 mappers

Consider extracting a small helper to build common fields; pass only the version-specific request type constructor. Keeps the lambdas tighter.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/OcppVersionHandler.java (2)

31-32: Fail fast on misconfiguration: mark mappers as non-null.

Constructor-level null-checks avoid late NPEs when resolving handlers.

Apply this diff:

-    private final RequestMapper<? extends CommunicationTask<S, U>, ? extends RequestType> requestMapper;
-    private final ResponseMapper<? extends ResponseType, U> responseMapper;
+    private final @NonNull RequestMapper<? extends CommunicationTask<S, U>, ? extends RequestType> requestMapper;
+    private final @NonNull ResponseMapper<? extends ResponseType, U> responseMapper;

Add this import near other Lombok imports:

import lombok.NonNull;

30-33: Optional: make this a Java record to remove Lombok and express intent.

A record cleanly models this as an immutable tuple and drops Lombok.

Example:

public record OcppVersionHandler<S extends ChargePointSelection, U>(
    RequestMapper<? extends CommunicationTask<S, U>, ? extends RequestType> requestMapper,
    ResponseMapper<? extends ResponseType, U> responseMapper) {}
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ChangeConfigurationTask.java (1)

66-83: Default callback is duplicated across tasks—centralize in base.

The same success/successError/failed logic appears in multiple tasks; consider providing a protected default in CommunicationTask to DRY this.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/TriggerMessageTask.java (2)

39-44: Guard against null triggerMessage to avoid NPE during mapping.

Add a null check before calling value()/fromValue.

Apply this diff:

-                                    task -> new ocpp.cp._2015._10.TriggerMessageRequest()
-                                            .withConnectorId(task.getParams().getConnectorId())
-                                            .withRequestedMessage(MessageTrigger.fromValue(task.getParams()
-                                                    .getTriggerMessage()
-                                                    .value())),
+                                    task -> {
+                                        var p = task.getParams();
+                                        Objects.requireNonNull(p.getTriggerMessage(), "triggerMessage must not be null");
+                                        return new ocpp.cp._2015._10.TriggerMessageRequest()
+                                                .withConnectorId(p.getConnectorId())
+                                                .withRequestedMessage(
+                                                        MessageTrigger.fromValue(p.getTriggerMessage().value()));
+                                    },

Add import:

import java.util.Objects;

56-73: Same callback duplication note as other tasks.

Consider centralizing the default callback in the base class.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/RemoteStartTransactionTask.java (2)

37-58: Validate idTag and connectorId before sending requests.

OCPP requires non-empty idTag; connectorId, when present, must be > 0. Add lightweight validation to fail fast.

Apply this diff:

-                                            task -> new ocpp.cp._2010._08.RemoteStartTransactionRequest()
-                                                    .withIdTag(task.getParams().getIdTag())
-                                                    .withConnectorId(
-                                                            task.getParams().getConnectorId()),
+                                            task -> new ocpp.cp._2010._08.RemoteStartTransactionRequest()
+                                                    .withIdTag(requireNonBlank(task.getParams().getIdTag(), "idTag"))
+                                                    .withConnectorId(
+                                                            validateConnectorId(task.getParams().getConnectorId())),
@@
-                                            task -> new ocpp.cp._2012._06.RemoteStartTransactionRequest()
-                                                    .withIdTag(task.getParams().getIdTag())
-                                                    .withConnectorId(
-                                                            task.getParams().getConnectorId()),
+                                            task -> new ocpp.cp._2012._06.RemoteStartTransactionRequest()
+                                                    .withIdTag(requireNonBlank(task.getParams().getIdTag(), "idTag"))
+                                                    .withConnectorId(
+                                                            validateConnectorId(task.getParams().getConnectorId())),
@@
-                                            task -> new ocpp.cp._2015._10.RemoteStartTransactionRequest()
-                                                    .withIdTag(task.getParams().getIdTag())
-                                                    .withConnectorId(
-                                                            task.getParams().getConnectorId()),
+                                            task -> new ocpp.cp._2015._10.RemoteStartTransactionRequest()
+                                                    .withIdTag(requireNonBlank(task.getParams().getIdTag(), "idTag"))
+                                                    .withConnectorId(
+                                                            validateConnectorId(task.getParams().getConnectorId())),

Add these helpers in the class:

private static Integer validateConnectorId(Integer cid) {
    if (cid == null) return null;
    if (cid <= 0) throw new IllegalArgumentException("connectorId must be > 0");
    return cid;
}
private static String requireNonBlank(String s, String name) {
    if (s == null || s.isBlank()) throw new IllegalArgumentException(name + " must not be blank");
    return s;
}

70-87: Callback duplication.

Same DRY suggestion as in other tasks: move the default callback to the base class.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetDiagnosticsTask.java (1)

90-107: Callback duplication note.

Same DRY suggestion: centralize default callback in CommunicationTask.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SendLocalListTask.java (2)

41-69: Avoid brittle cast inside mapper

(SendLocalListTask) task works but couples the mapper to the concrete class. Consider shaping the framework so request mappers receive the concrete task type (or capture ocppTagsService via instance-aware mapper) to avoid accidental ClassCastException.


100-121: Minor: drop param in createOcpp16Request

The method uses the field; the param is redundant.

-    private ocpp.cp._2015._10.SendLocalListRequest createOcpp16Request(OcppTagsService ocppTagsService) {
+    private ocpp.cp._2015._10.SendLocalListRequest createOcpp16Request() {
@@
-            auths.addAll(ocppTagsService.getAuthData(getParams().getAddUpdateList()));
+            auths.addAll(this.ocppTagsService.getAuthData(getParams().getAddUpdateList()));
@@
-                values = ocppTagsService.getAuthDataOfAllTags();
+                values = this.ocppTagsService.getAuthDataOfAllTags();

And in mappers:

- ocpp.cp._2015._10.SendLocalListRequest ocpp16Request = t.createOcpp16Request(t.ocppTagsService);
+ ocpp.cp._2015._10.SendLocalListRequest ocpp16Request = t.createOcpp16Request();
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/DataTransferTask.java (1)

40-47: Prevent log/monitoring bloat in toString()

Large data payloads can overwhelm logs. Truncate defensively.

-            if (data != null) {
-                builder.append(" / Data: ").append(data);
+            if (data != null) {
+                var s = data.length() > 512 ? (data.substring(0, 512) + "...") : data;
+                builder.append(" / Data: ").append(s);
             }
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetConfigurationTask.java (2)

64-72: Only set keys when provided (preserve “all keys” semantics)

When getAllKeys() is null/empty, omit .withKey(...) to request all keys per spec.

-                                            task -> new ocpp.cp._2012._06.GetConfigurationRequest()
-                                                    .withKey(task.getParams().getAllKeys()),
+                                            task -> {
+                                                var req = new ocpp.cp._2012._06.GetConfigurationRequest();
+                                                var keys = task.getParams().getAllKeys();
+                                                if (keys != null && !keys.isEmpty()) {
+                                                    req.withKey(keys);
+                                                }
+                                                return req;
+                                            },

75-83: Mirror the same keys handling for 1.6

Apply the same guard to V_16 mapping.

-                                            task -> new ocpp.cp._2015._10.GetConfigurationRequest()
-                                                    .withKey(task.getParams().getAllKeys()),
+                                            task -> {
+                                                var req = new ocpp.cp._2015._10.GetConfigurationRequest();
+                                                var keys = task.getParams().getAllKeys();
+                                                if (keys != null && !keys.isEmpty()) {
+                                                    req.withKey(keys);
+                                                }
+                                                return req;
+                                            },
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTask.java (3)

85-88: Use the actual sent connectorId for DB update

Tie the DB assignment to the connectorId in the request payload, not the original params.

-                    repo.setProfile(details.getChargingProfilePk(), chargeBoxId, params.getConnectorId());
+                    repo.setProfile(details.getChargingProfilePk(), chargeBoxId, request.getConnectorId());

125-133: Minor: improve readability of lambda variable

Rename k to periodDto for clarity.

-        var schedulePeriods = details.getPeriods().stream()
-                .map(k -> {
+        var schedulePeriods = details.getPeriods().stream()
+                .map(periodDto -> {
                     ChargingSchedulePeriod p = new ChargingSchedulePeriod();
-                    p.setStartPeriod(k.getStartPeriodInSeconds());
-                    p.setLimit(k.getPowerLimit());
-                    p.setNumberPhases(k.getNumberPhases());
+                    p.setStartPeriod(periodDto.getStartPeriodInSeconds());
+                    p.setLimit(periodDto.getPowerLimit());
+                    p.setNumberPhases(periodDto.getNumberPhases());
                     return p;
                 })
                 .toList();

45-46: Hide version-specific request: make request getter private

Verified: no external call sites invoke a no-arg getRequest(); external callers use CommunicationTask.getRequest(OcppVersion). Change steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTask.java to suppress the Lombok-generated public getter for the request field and provide a private accessor used internally by TASK_DEFINITION.

Suggested change (minimal):

@Getter
public class SetChargingProfileTask extends CommunicationTask<...> {
@Getter(lombok.AccessLevel.NONE) // prevent public getter for the request field
private SetChargingProfileRequest request;
...
private SetChargingProfileRequest getRequest() { return request; }
}

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/CommunicationTask.java (1)

153-161: Avoid potential ConcurrentModificationException when iterating callbacks

Snapshot the callback list before iterating. Low risk, but safer if callbacks are added concurrently.

-    public void success(String chargeBoxId, RESPONSE response) {
-        for (OcppCallback<RESPONSE> c : callbackList) {
+    public void success(String chargeBoxId, RESPONSE response) {
+        var callbacks = new ArrayList<>(callbackList);
+        for (OcppCallback<RESPONSE> c : callbacks) {
             try {
                 c.success(chargeBoxId, response);
             } catch (Exception e) {
                 log.error("Exception occurred in OcppCallback", e);
             }
         }
     }

-    public void failed(String chargeBoxId, Exception exception) {
-        for (OcppCallback<RESPONSE> c : callbackList) {
+    public void failed(String chargeBoxId, Exception exception) {
+        var callbacks = new ArrayList<>(callbackList);
+        for (OcppCallback<RESPONSE> c : callbacks) {
             try {
                 c.failed(chargeBoxId, exception);
             } catch (Exception e) {
                 log.error("Exception occurred in OcppCallback", e);
             }
         }
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9db4ce7 and 75a502f.

📒 Files selected for processing (30)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/CommunicationTask.java (5 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/RequestResult.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/CancelReservationTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ChangeAvailabilityTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ChangeConfigurationTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ClearCacheTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ClearChargingProfileTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/DataTransferTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetCompositeScheduleTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetConfigurationTask.java (2 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetDiagnosticsTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetLocalListVersionTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/RemoteStartTransactionTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/RemoteStopTransactionTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ReserveNowTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ResetTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SendLocalListTask.java (3 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTaskAdhoc.java (0 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTaskFromDB.java (0 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/TriggerMessageTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/UnlockConnectorTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/UpdateFirmwareTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/OcppVersionHandler.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/RequestMapper.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/ResponseMapper.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/TaskDefinition.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/service/ChargePointServiceClient.java (2 hunks)
  • steve-ocpp-transport-soap/src/main/java/de/rwth/idsg/steve/ocpp/soap/ChargePointServiceSoapInvoker.java (2 hunks)
  • steve/src/main/java/de/rwth/idsg/steve/ocpp/ChargePointServiceInvokerImpl.java (2 hunks)
💤 Files with no reviewable changes (2)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTaskAdhoc.java
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTaskFromDB.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-04-05T13:09:02.595Z
Learnt from: juherr
PR: juherr/evolve#1
File: src/main/resources/OCPP-2.0.1_part3_JSON_schemas/ClearCacheRequest.json:21-27
Timestamp: 2025-04-05T13:09:02.595Z
Learning: In the OCPP (Open Charge Point Protocol) standard, the ClearCacheRequest schema is correctly designed with only the optional customData property, as this operation doesn't require additional parameters.

Applied to files:

  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ClearCacheTask.java
🔇 Additional comments (25)
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/RequestResult.java (3)

32-33: Type-safe response field — good refactor

Switching response to @Nullable T aligns with the mapper-based design and removes casting at call sites.


21-24: Nullness annotations consistency — resolved: repository consistently uses org.jspecify.annotations.Nullable

Search for imports matching import\s+.*\.Nullable; returned only org.jspecify.annotations.Nullable across the codebase; no javax, org.jetbrains, androidx, or other @Nullable imports found.


32-32: Fix raw RequestResult usages — no getResponse() call sites found

  • Repo search found no usages of getResponse(); the original claim that callers do String s = result.getResponse() is incorrect.
  • Raw RequestResult declarations to update:
    • steve-api/src/main/java/de/rwth/idsg/steve/web/api/dto/ApiTaskInfo.java:23
    • steve-ui-jsp/src/main/java/de/rwth/idsg/steve/web/controller/TasksController.java:23
      Change those to RequestResult<...> with the appropriate response type (e.g., RequestResult if that’s intended).

Likely an incorrect or invalid review comment.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/RequestMapper.java (1)

24-27: Solid, minimal SPI. LGTM.

Simple, focused functional interface enabling clean mapper injection.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/ResponseMapper.java (1)

19-26: Otherwise looks good.

Interface is clear and complements RequestMapper.

steve/src/main/java/de/rwth/idsg/steve/ocpp/ChargePointServiceInvokerImpl.java (1)

73-209: Router simplification LGTM.

Replacing per-method cp.isSoap() branches with a single helper reduces duplication without changing behavior.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/CancelReservationTask.java (1)

82-100: Don't remove — defaultCallback() is required by CommunicationTask.

CommunicationTask declares defaultCallback() as abstract and calls it in its constructor (callbackList.add(defaultCallback()); — steve-core/src/main/java/de/rwth/idsg/steve/ocpp/CommunicationTask.java:104) and the abstract method is declared at :163; CommunicationContext also calls task.defaultCallback().successError(...) (steve-ocpp-transport-websocket/src/main/java/de/rwth/idsg/steve/ocpp/ws/data/CommunicationContext.java:79). Subclasses must implement this override even if they add additional callbacks in their constructors.

Likely an incorrect or invalid review comment.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ClearChargingProfileTask.java (2)

62-75: DB cleanup happens regardless of response; confirm intended parity

You clear local profiles for both Accepted and non-Accepted responses. This matches the log message, but please confirm it matches prior behavior and operational intent (risk: CP may retain profiles if the request was rejected).


43-49: Incorrect — chargingProfilePk is intentionally used as the OCPP ChargingProfileId

SetChargingProfileTask builds the OCPP ChargingProfile with details.getChargingProfilePk() (SetChargingProfileTask.java:143) and stores/uses that PK via repo.setProfile(...); ClearChargingProfileTask sends params.getChargingProfilePk() in ClearChargingProfileRequest (ClearChargingProfileTask.java:44) and calls chargingProfileRepository.clearProfile(...). The DB PK is used consistently as the OCPP id — no mapper change required.

Likely an incorrect or invalid review comment.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/UpdateFirmwareTask.java (1)

34-73: Version-mapped request/response wiring looks solid

Good use of TaskDefinition and per-version mappers; response normalization to "OK" is consistent.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetLocalListVersionTask.java (1)

32-45: Straightforward mapping; aligns with new architecture

Per-version request and integer-to-string mapping LGTM.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ResetTask.java (2)

32-59: Per-version Reset mapping looks correct

Use of fromValue(params.getResetType().value()) across versions is consistent.


37-43: Validate ResetType string mapping across OCPP versions

ResetParams.resetType.value() -> "Hard"/"Soft" (steve-core/src/main/java/de/rwth/idsg/steve/web/dto/ocpp/ResetType.java). The generated ocpp.cp.* ResetType enum sources are not in this repository — verify that ocpp.cp._2010._08.ResetType, ocpp.cp._2012._06.ResetType and ocpp.cp._2015._10.ResetType accept the exact literals "Hard"/"Soft"; if any differ, add an explicit mapping in steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ResetTask.java (current block around lines 35–58). Also applies to the analogous call sites (lines 45–51 and 53–59).

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ReserveNowTask.java (2)

35-74: Version-mapped ReserveNow construction looks good

Request fields and status extraction for 1.5/1.6 are correct.


84-101: Repository side‑effects are correct for non‑Accepted / failed paths

ReservationRepository.delete(...) is documented for “station does not accept” or technical failures and ReservationRepository.accepted(...) sets the reservation status to ACCEPTED; ReserveNowTask calls accepted() on "Accepted" and delete(...) on all other/error paths — behavior matches the repository API and prior expectations.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ChangeConfigurationTask.java (2)

32-56: Nice consolidation of per-version wiring.

The TaskDefinition + OcppVersionHandler mapping removes duplication and is clear.


37-55: Validate ChangeConfiguration key/value early

ChangeConfigurationParams permits null/blank: confKey/customConfKey are @nullable and the @notblank for value is commented out; only keyType is @NotNull (steve-core/src/main/java/de/rwth/idsg/steve/web/dto/ocpp/ChangeConfigurationParams.java, ~lines 36–49). Add lightweight precondition checks in the request lambda to reject null/blank key or value before sending the OCPP request, or re-enable bean validation on the DTO (aware @NotBlank/@pattern were disabled due to issues steve-community#148 and steve-community#920). Confirm intended behavior.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/TriggerMessageTask.java (1)

39-44: Confirm enum value alignment with ocpp.cp._2015._10.MessageTrigger

TriggerMessageEnum.value() = BootNotification, DiagnosticsStatusNotification, FirmwareStatusNotification, Heartbeat, MeterValues, StatusNotification. ocpp.cp._2015._10.MessageTrigger source not in the repo — confirm MessageTrigger.fromValue(...) accepts these exact strings; if not, update TriggerMessageEnum or map/handle the values (or catch IllegalArgumentException) before calling.
Locations: steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/TriggerMessageTask.java (call site) and steve-core/src/main/java/de/rwth/idsg/steve/web/dto/ocpp/TriggerMessageEnum.java.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetDiagnosticsTask.java (1)

39-79: No additional checks needed — DTO enforces constraints.

GetDiagnosticsParams (steve-core/src/main/java/de/rwth/idsg/steve/web/dto/ocpp/GetDiagnosticsParams.java) declares @NotBlank/@pattern for location, @min(1) for retries/retryInterval, @past on start/stop and an @AssertTrue isValid() that ensures stop.isAfter(start).

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ChangeAvailabilityTask.java (1)

70-76: Constructors look good

Constructors correctly delegate to the new TaskDefinition-based super signatures.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SendLocalListTask.java (2)

110-119: Confirm FULL update semantics for empty list

When sendEmptyListWhenFull is null, you send an empty list (clears CP’s list). Validate this matches intended behavior; otherwise treat null as false.


132-141: IdTagInfo.status could be null — verify assumption

ocpp16.getStatus().value() will NPE if status is absent. If upstream guarantees non-null, fine; otherwise add a guard or default.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ClearCacheTask.java (1)

32-50: LGTM

Version mapping and response extraction are correct for 1.2/1.5/1.6.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/DataTransferTask.java (1)

50-73: Validate mandatory vendorId

OCPP 1.5/1.6 require vendorId. Ensure DTO enforces non-null/non-empty to avoid REJECTED responses.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/CommunicationTask.java (1)

165-175: Version-driven request mapping looks solid

Clean separation via TaskDefinition/RequestMapper with explicit unsupported-version guard. No issues.

Comment on lines +177 to +201
@SuppressWarnings("unchecked")
public <T extends ResponseType> AsyncHandler<T> createHandler(String chargeBoxId) {
OcppVersion version = versionMap.get(chargeBoxId);
OcppVersionHandler<S, RESPONSE> handler =
taskDefinition.getVersionHandlers().get(version);
if (handler == null) {
throw new UnsupportedOperationException("Operation not supported for version " + version);
}
ResponseMapper<T, RESPONSE> mapper = (ResponseMapper<T, RESPONSE>) handler.getResponseMapper();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validate chargeBoxId before resolving version

Provide a clearer error when chargeBoxId is not part of this task, instead of an UnsupportedOperationException with version null.

-    public <T extends ResponseType> AsyncHandler<T> createHandler(String chargeBoxId) {
-        OcppVersion version = versionMap.get(chargeBoxId);
+    public <T extends ResponseType> AsyncHandler<T> createHandler(String chargeBoxId) {
+        if (!versionMap.containsKey(chargeBoxId)) {
+            throw new IllegalArgumentException("Unknown chargeBoxId for this task: " + chargeBoxId);
+        }
+        OcppVersion version = versionMap.get(chargeBoxId);
         OcppVersionHandler<S, RESPONSE> handler =
                 taskDefinition.getVersionHandlers().get(version);
         if (handler == null) {
             throw new UnsupportedOperationException("Operation not supported for version " + version);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@SuppressWarnings("unchecked")
public <T extends ResponseType> AsyncHandler<T> createHandler(String chargeBoxId) {
OcppVersion version = versionMap.get(chargeBoxId);
OcppVersionHandler<S, RESPONSE> handler =
taskDefinition.getVersionHandlers().get(version);
if (handler == null) {
throw new UnsupportedOperationException("Operation not supported for version " + version);
}
ResponseMapper<T, RESPONSE> mapper = (ResponseMapper<T, RESPONSE>) handler.getResponseMapper();
@SuppressWarnings("unchecked")
public <T extends ResponseType> AsyncHandler<T> createHandler(String chargeBoxId) {
if (!versionMap.containsKey(chargeBoxId)) {
throw new IllegalArgumentException("Unknown chargeBoxId for this task: " + chargeBoxId);
}
OcppVersion version = versionMap.get(chargeBoxId);
OcppVersionHandler<S, RESPONSE> handler =
taskDefinition.getVersionHandlers().get(version);
if (handler == null) {
throw new UnsupportedOperationException("Operation not supported for version " + version);
}
ResponseMapper<T, RESPONSE> mapper = (ResponseMapper<T, RESPONSE>) handler.getResponseMapper();
🤖 Prompt for AI Agents
In steve-core/src/main/java/de/rwth/idsg/steve/ocpp/CommunicationTask.java
around lines 177 to 186, the code calls versionMap.get(chargeBoxId) and proceeds
even if the key is absent, which results in an UnsupportedOperationException
containing "version null"; validate the chargeBoxId first and fail fast with a
clear error: check that versionMap.containsKey(chargeBoxId) (or that the
returned version is non-null) and throw an IllegalArgumentException or a
descriptive exception stating that the given chargeBoxId is not registered for
this task, before attempting to resolve the version and handler so subsequent
code never operates on a null version.

Comment on lines 56 to 55
addCallback(new OcppCallback<>() {
@Override
public void success(String chargeBoxId, String statusValue) {
addNewResponse(chargeBoxId, statusValue);

if ("Accepted".equalsIgnoreCase(statusValue)) {
reservationRepository.cancelled(params.getReservationId());
reservationRepository.cancelled(getParams().getReservationId());
}
}
};
}

@Override
public CancelReservationRequest getOcpp15Request() {
return new CancelReservationRequest().withReservationId(params.getReservationId());
@Override
public void successError(String chargeBoxId, Object error) {
addNewError(chargeBoxId, error.toString());
}

@Override
public void failed(String chargeBoxId, Exception e) {
addNewError(chargeBoxId, e.getMessage());
}
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make repository side-effect idempotent across multi-target executions.

If this task targets multiple charge boxes, multiple “Accepted” responses could trigger repeated cancelled() calls. Guard to execute once.

-    public CancelReservationTask(
-            CancelReservationParams params, ReservationRepository reservationRepository, String caller) {
+    public CancelReservationTask(
+            CancelReservationParams params, ReservationRepository reservationRepository, String caller) {
         super(params, caller, TASK_DEFINITION);
-
-        addCallback(new OcppCallback<>() {
+        java.util.concurrent.atomic.AtomicBoolean done = new java.util.concurrent.atomic.AtomicBoolean(false);
+        addCallback(new OcppCallback<>() {
@@
-            public void success(String chargeBoxId, String statusValue) {
+            public void success(String chargeBoxId, String statusValue) {
                 addNewResponse(chargeBoxId, statusValue);
 
-                if ("Accepted".equalsIgnoreCase(statusValue)) {
-                    reservationRepository.cancelled(getParams().getReservationId());
-                }
+                if ("Accepted".equalsIgnoreCase(statusValue)
+                        && done.compareAndSet(false, true)) {
+                    reservationRepository.cancelled(getParams().getReservationId());
+                }
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
addCallback(new OcppCallback<>() {
@Override
public void success(String chargeBoxId, String statusValue) {
addNewResponse(chargeBoxId, statusValue);
if ("Accepted".equalsIgnoreCase(statusValue)) {
reservationRepository.cancelled(params.getReservationId());
reservationRepository.cancelled(getParams().getReservationId());
}
}
};
}
@Override
public CancelReservationRequest getOcpp15Request() {
return new CancelReservationRequest().withReservationId(params.getReservationId());
@Override
public void successError(String chargeBoxId, Object error) {
addNewError(chargeBoxId, error.toString());
}
@Override
public void failed(String chargeBoxId, Exception e) {
addNewError(chargeBoxId, e.getMessage());
}
});
}
java.util.concurrent.atomic.AtomicBoolean done = new java.util.concurrent.atomic.AtomicBoolean(false);
addCallback(new OcppCallback<>() {
@Override
public void success(String chargeBoxId, String statusValue) {
addNewResponse(chargeBoxId, statusValue);
if ("Accepted".equalsIgnoreCase(statusValue)
&& done.compareAndSet(false, true)) {
reservationRepository.cancelled(getParams().getReservationId());
}
}
@Override
public void successError(String chargeBoxId, Object error) {
addNewError(chargeBoxId, error.toString());
}
@Override
public void failed(String chargeBoxId, Exception e) {
addNewError(chargeBoxId, e.getMessage());
}
});
}
🤖 Prompt for AI Agents
In
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/CancelReservationTask.java
around lines 56 to 76, the success callback may call
reservationRepository.cancelled(...) multiple times if multiple charge boxes
return "Accepted"; make the side-effect idempotent by ensuring cancelled(...) is
invoked only once per task execution — add a task-scoped guard (e.g. an
AtomicBoolean or synchronized boolean flag) that flips when you first call
cancelled and check that flag before subsequent calls, or alternatively call a
repository method that is itself idempotent (e.g. cancelledIfNotAlready) and
handle concurrency there so repeated successes do not trigger duplicate state
changes.

Comment on lines +66 to +75
@Override
public void successError(String chargeBoxId, Object error) {
addNewError(chargeBoxId, error.toString());
}

@Override
public void failed(String chargeBoxId, Exception e) {
addNewError(chargeBoxId, e.getMessage());
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Normalize error messages (avoid nulls, add exception type).

-            public void successError(String chargeBoxId, Object error) {
-                addNewError(chargeBoxId, error.toString());
+            public void successError(String chargeBoxId, Object error) {
+                addNewError(chargeBoxId, String.valueOf(error));
@@
-            public void failed(String chargeBoxId, Exception e) {
-                addNewError(chargeBoxId, e.getMessage());
+            public void failed(String chargeBoxId, Exception e) {
+                String msg = e.getClass().getSimpleName() + ": " + (e.getMessage() != null ? e.getMessage() : "<no message>");
+                addNewError(chargeBoxId, msg);

Also applies to: 90-98

🤖 Prompt for AI Agents
In
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/CancelReservationTask.java
around lines 66-75 (and similarly 90-98), the error handling calls pass raw
error.toString() or e.getMessage() which can be null and omit exception type;
change both to build a normalized string that includes the error/exception type
and a non-null message (e.g. for Exception use e.getClass().getSimpleName() + ":
" + (e.getMessage() != null ? e.getMessage() : "<no message>"), and for the
Object error check for null and use error.getClass().getSimpleName() + ": " +
(String.valueOf(error) or "<no message>") ), then pass that constructed string
to addNewError so logs always include type and a safe message.

Comment on lines +32 to +61
private static final TaskDefinition<ChangeAvailabilityParams, String> TASK_DEFINITION =
TaskDefinition.<ChangeAvailabilityParams, String>builder()
.versionHandlers(Map.of(
OcppVersion.V_12,
new OcppVersionHandler<>(
task -> new ocpp.cp._2010._08.ChangeAvailabilityRequest()
.withConnectorId(
task.getParams().getConnectorId())
.withType(ocpp.cp._2010._08.AvailabilityType.fromValue(
task.getParams()
.getAvailType()
.value())),
(ocpp.cp._2010._08.ChangeAvailabilityResponse r) ->
r.getStatus().value()),
OcppVersion.V_15,
new OcppVersionHandler<>(
task -> new ocpp.cp._2012._06.ChangeAvailabilityRequest()
.withConnectorId(
task.getParams().getConnectorId())
.withType(ocpp.cp._2012._06.AvailabilityType.fromValue(
task.getParams()
.getAvailType()
.value())),
(ocpp.cp._2012._06.ChangeAvailabilityResponse r) ->
r.getStatus().value()),
OcppVersion.V_16,
new OcppVersionHandler<>(
task -> new ocpp.cp._2015._10.ChangeAvailabilityRequest()
.withConnectorId(
task.getParams().getConnectorId())
.withType(ocpp.cp._2015._10.AvailabilityType.fromValue(
task.getParams()
.getAvailType()
.value())),
(ocpp.cp._2015._10.ChangeAvailabilityResponse r) ->
r.getStatus().value())))
.build();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

NPE risk: guard null availType before .value()

getParams().getAvailType() may be null; .value() will NPE. Add a null check (or validation) in each mapper.

Apply:

@@
-import java.util.Map;
+import java.util.Map;
+import java.util.Objects;
@@
-                                            task -> new ocpp.cp._2010._08.ChangeAvailabilityRequest()
-                                                    .withConnectorId(
-                                                            task.getParams().getConnectorId())
-                                                    .withType(ocpp.cp._2010._08.AvailabilityType.fromValue(
-                                                            task.getParams()
-                                                                    .getAvailType()
-                                                                    .value())),
+                                            task -> {
+                                                var type = Objects.requireNonNull(task.getParams().getAvailType(), "availType");
+                                                return new ocpp.cp._2010._08.ChangeAvailabilityRequest()
+                                                        .withConnectorId(task.getParams().getConnectorId())
+                                                        .withType(ocpp.cp._2010._08.AvailabilityType.fromValue(type.value()));
+                                            },
@@
-                                            task -> new ocpp.cp._2012._06.ChangeAvailabilityRequest()
-                                                    .withConnectorId(
-                                                            task.getParams().getConnectorId())
-                                                    .withType(ocpp.cp._2012._06.AvailabilityType.fromValue(
-                                                            task.getParams()
-                                                                    .getAvailType()
-                                                                    .value())),
+                                            task -> {
+                                                var type = Objects.requireNonNull(task.getParams().getAvailType(), "availType");
+                                                return new ocpp.cp._2012._06.ChangeAvailabilityRequest()
+                                                        .withConnectorId(task.getParams().getConnectorId())
+                                                        .withType(ocpp.cp._2012._06.AvailabilityType.fromValue(type.value()));
+                                            },
@@
-                                            task -> new ocpp.cp._2015._10.ChangeAvailabilityRequest()
-                                                    .withConnectorId(
-                                                            task.getParams().getConnectorId())
-                                                    .withType(ocpp.cp._2015._10.AvailabilityType.fromValue(
-                                                            task.getParams()
-                                                                    .getAvailType()
-                                                                    .value())),
+                                            task -> {
+                                                var type = Objects.requireNonNull(task.getParams().getAvailType(), "availType");
+                                                return new ocpp.cp._2015._10.ChangeAvailabilityRequest()
+                                                        .withConnectorId(task.getParams().getConnectorId())
+                                                        .withType(ocpp.cp._2015._10.AvailabilityType.fromValue(type.value()));
+                                            },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static final TaskDefinition<ChangeAvailabilityParams, String> TASK_DEFINITION =
TaskDefinition.<ChangeAvailabilityParams, String>builder()
.versionHandlers(Map.of(
OcppVersion.V_12,
new OcppVersionHandler<>(
task -> new ocpp.cp._2010._08.ChangeAvailabilityRequest()
.withConnectorId(
task.getParams().getConnectorId())
.withType(ocpp.cp._2010._08.AvailabilityType.fromValue(
task.getParams()
.getAvailType()
.value())),
(ocpp.cp._2010._08.ChangeAvailabilityResponse r) ->
r.getStatus().value()),
OcppVersion.V_15,
new OcppVersionHandler<>(
task -> new ocpp.cp._2012._06.ChangeAvailabilityRequest()
.withConnectorId(
task.getParams().getConnectorId())
.withType(ocpp.cp._2012._06.AvailabilityType.fromValue(
task.getParams()
.getAvailType()
.value())),
(ocpp.cp._2012._06.ChangeAvailabilityResponse r) ->
r.getStatus().value()),
OcppVersion.V_16,
new OcppVersionHandler<>(
task -> new ocpp.cp._2015._10.ChangeAvailabilityRequest()
.withConnectorId(
task.getParams().getConnectorId())
.withType(ocpp.cp._2015._10.AvailabilityType.fromValue(
task.getParams()
.getAvailType()
.value())),
(ocpp.cp._2015._10.ChangeAvailabilityResponse r) ->
r.getStatus().value())))
.build();
private static final TaskDefinition<ChangeAvailabilityParams, String> TASK_DEFINITION =
TaskDefinition.<ChangeAvailabilityParams, String>builder()
.versionHandlers(Map.of(
OcppVersion.V_12,
new OcppVersionHandler<>(
task -> {
var type = Objects.requireNonNull(task.getParams().getAvailType(), "availType");
return new ocpp.cp._2010._08.ChangeAvailabilityRequest()
.withConnectorId(task.getParams().getConnectorId())
.withType(ocpp.cp._2010._08.AvailabilityType.fromValue(type.value()));
},
(ocpp.cp._2010._08.ChangeAvailabilityResponse r) ->
r.getStatus().value()),
OcppVersion.V_15,
new OcppVersionHandler<>(
task -> {
var type = Objects.requireNonNull(task.getParams().getAvailType(), "availType");
return new ocpp.cp._2012._06.ChangeAvailabilityRequest()
.withConnectorId(task.getParams().getConnectorId())
.withType(ocpp.cp._2012._06.AvailabilityType.fromValue(type.value()));
},
(ocpp.cp._2012._06.ChangeAvailabilityResponse r) ->
r.getStatus().value()),
OcppVersion.V_16,
new OcppVersionHandler<>(
task -> {
var type = Objects.requireNonNull(task.getParams().getAvailType(), "availType");
return new ocpp.cp._2015._10.ChangeAvailabilityRequest()
.withConnectorId(task.getParams().getConnectorId())
.withType(ocpp.cp._2015._10.AvailabilityType.fromValue(type.value()));
},
(ocpp.cp._2015._10.ChangeAvailabilityResponse r) ->
r.getStatus().value())))
.build();
🤖 Prompt for AI Agents
In
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ChangeAvailabilityTask.java
around lines 32 to 68, the mappers call task.getParams().getAvailType().value()
and will NPE if getAvailType() is null; guard against null before calling
.value() in each OcppVersionHandler mapper by first extracting the availType
into a local variable, validate it (e.g. if (availType == null) throw new
IllegalArgumentException("availType is required for ChangeAvailability task") or
a task-specific validation exception), and only then call .value() (or map to an
appropriate default/response) so the code fails fast with a clear error instead
of throwing an NPE.

Comment on lines 79 to 95
public OcppCallback<String> defaultCallback() {
return new OcppCallback<>() {
@Override
public void success(String chargeBoxId, String response) {
addNewResponse(chargeBoxId, response);
}
};
}

@Override
public AsyncHandler<ocpp.cp._2012._06.ChangeAvailabilityResponse> getOcpp15Handler(String chargeBoxId) {
return res -> {
try {
success(chargeBoxId, res.get().getStatus().value());
} catch (Exception e) {
failed(chargeBoxId, e);
@Override
public void successError(String chargeBoxId, Object error) {
addNewError(chargeBoxId, error.toString());
}
};
}

@Override
public AsyncHandler<ocpp.cp._2015._10.ChangeAvailabilityResponse> getOcpp16Handler(String chargeBoxId) {
return res -> {
try {
success(chargeBoxId, res.get().getStatus().value());
} catch (Exception e) {
failed(chargeBoxId, e);
@Override
public void failed(String chargeBoxId, Exception e) {
addNewError(chargeBoxId, e.getMessage());
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make callback error handling null-safe and more informative

Use String.valueOf(error) and e.toString() to avoid NPE and retain exception type.

-            public void successError(String chargeBoxId, Object error) {
-                addNewError(chargeBoxId, error.toString());
+            public void successError(String chargeBoxId, Object error) {
+                addNewError(chargeBoxId, String.valueOf(error));
             }
@@
-            public void failed(String chargeBoxId, Exception e) {
-                addNewError(chargeBoxId, e.getMessage());
+            public void failed(String chargeBoxId, Exception e) {
+                addNewError(chargeBoxId, e.toString());
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public OcppCallback<String> defaultCallback() {
return new OcppCallback<>() {
@Override
public void success(String chargeBoxId, String response) {
addNewResponse(chargeBoxId, response);
}
};
}
@Override
public AsyncHandler<ocpp.cp._2012._06.ChangeAvailabilityResponse> getOcpp15Handler(String chargeBoxId) {
return res -> {
try {
success(chargeBoxId, res.get().getStatus().value());
} catch (Exception e) {
failed(chargeBoxId, e);
@Override
public void successError(String chargeBoxId, Object error) {
addNewError(chargeBoxId, error.toString());
}
};
}
@Override
public AsyncHandler<ocpp.cp._2015._10.ChangeAvailabilityResponse> getOcpp16Handler(String chargeBoxId) {
return res -> {
try {
success(chargeBoxId, res.get().getStatus().value());
} catch (Exception e) {
failed(chargeBoxId, e);
@Override
public void failed(String chargeBoxId, Exception e) {
addNewError(chargeBoxId, e.getMessage());
}
};
public OcppCallback<String> defaultCallback() {
return new OcppCallback<>() {
@Override
public void success(String chargeBoxId, String response) {
addNewResponse(chargeBoxId, response);
}
@Override
public void successError(String chargeBoxId, Object error) {
addNewError(chargeBoxId, String.valueOf(error));
}
@Override
public void failed(String chargeBoxId, Exception e) {
addNewError(chargeBoxId, e.toString());
}
};
🤖 Prompt for AI Agents
In
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ChangeAvailabilityTask.java
around lines 79 to 95, the callback error handlers call error.toString() and
e.getMessage(), which can produce NPEs or lose exception type information;
change them to use String.valueOf(error) in successError and e.toString() in
failed so they are null-safe and include exception type, then update the
addNewError calls to pass those String results.

Comment on lines 80 to 97
@Override
public OcppCallback<String> defaultCallback() {
return new StringOcppCallback();
}

@Override
public ocpp.cp._2012._06.SendLocalListRequest getOcpp15Request() {
ocpp.cp._2015._10.SendLocalListRequest ocpp16Request = getOcpp16Request();

return new ocpp.cp._2012._06.SendLocalListRequest()
.withListVersion(ocpp16Request.getListVersion())
.withUpdateType(ocpp.cp._2012._06.UpdateType.fromValue(
ocpp16Request.getUpdateType().value()))
.withLocalAuthorisationList(toOcpp15(ocpp16Request.getLocalAuthorizationList()));
}

@Override
public ocpp.cp._2015._10.SendLocalListRequest getOcpp16Request() {
return request;
}
return new OcppCallback<>() {
@Override
public void success(String chargeBoxId, String response) {
addNewResponse(chargeBoxId, response);
}

@Override
public AsyncHandler<ocpp.cp._2012._06.SendLocalListResponse> getOcpp15Handler(String chargeBoxId) {
return res -> {
try {
success(chargeBoxId, res.get().getStatus().value());
} catch (Exception e) {
failed(chargeBoxId, e);
@Override
public void successError(String chargeBoxId, Object error) {
addNewError(chargeBoxId, error.toString());
}
};
}

@Override
public AsyncHandler<ocpp.cp._2015._10.SendLocalListResponse> getOcpp16Handler(String chargeBoxId) {
return res -> {
try {
success(chargeBoxId, res.get().getStatus().value());
} catch (Exception e) {
failed(chargeBoxId, e);
@Override
public void failed(String chargeBoxId, Exception e) {
addNewError(chargeBoxId, e.getMessage());
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Null-safe callback errors

Align with other tasks; avoid NPE on error == null and preserve exception types.

-            public void successError(String chargeBoxId, Object error) {
-                addNewError(chargeBoxId, error.toString());
+            public void successError(String chargeBoxId, Object error) {
+                addNewError(chargeBoxId, String.valueOf(error));
             }
@@
-            public void failed(String chargeBoxId, Exception e) {
-                addNewError(chargeBoxId, e.getMessage());
+            public void failed(String chargeBoxId, Exception e) {
+                addNewError(chargeBoxId, e.toString());
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public OcppCallback<String> defaultCallback() {
return new StringOcppCallback();
}
@Override
public ocpp.cp._2012._06.SendLocalListRequest getOcpp15Request() {
ocpp.cp._2015._10.SendLocalListRequest ocpp16Request = getOcpp16Request();
return new ocpp.cp._2012._06.SendLocalListRequest()
.withListVersion(ocpp16Request.getListVersion())
.withUpdateType(ocpp.cp._2012._06.UpdateType.fromValue(
ocpp16Request.getUpdateType().value()))
.withLocalAuthorisationList(toOcpp15(ocpp16Request.getLocalAuthorizationList()));
}
@Override
public ocpp.cp._2015._10.SendLocalListRequest getOcpp16Request() {
return request;
}
return new OcppCallback<>() {
@Override
public void success(String chargeBoxId, String response) {
addNewResponse(chargeBoxId, response);
}
@Override
public AsyncHandler<ocpp.cp._2012._06.SendLocalListResponse> getOcpp15Handler(String chargeBoxId) {
return res -> {
try {
success(chargeBoxId, res.get().getStatus().value());
} catch (Exception e) {
failed(chargeBoxId, e);
@Override
public void successError(String chargeBoxId, Object error) {
addNewError(chargeBoxId, error.toString());
}
};
}
@Override
public AsyncHandler<ocpp.cp._2015._10.SendLocalListResponse> getOcpp16Handler(String chargeBoxId) {
return res -> {
try {
success(chargeBoxId, res.get().getStatus().value());
} catch (Exception e) {
failed(chargeBoxId, e);
@Override
public void failed(String chargeBoxId, Exception e) {
addNewError(chargeBoxId, e.getMessage());
}
};
@Override
public OcppCallback<String> defaultCallback() {
return new OcppCallback<>() {
@Override
public void success(String chargeBoxId, String response) {
addNewResponse(chargeBoxId, response);
}
@Override
public void successError(String chargeBoxId, Object error) {
addNewError(chargeBoxId, String.valueOf(error));
}
@Override
public void failed(String chargeBoxId, Exception e) {
addNewError(chargeBoxId, e.toString());
}
};
🤖 Prompt for AI Agents
In steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SendLocalListTask.java
around lines 80-97, the callback implementations can NPE when error == null and
they lose exception type info by converting Exceptions to messages; update
successError to guard against null (e.g., use Objects.toString(error, "null") or
a null check) and update failed to pass the Exception itself to addNewError (or
overload/change addNewError to accept Throwable/Object) so the original
exception type is preserved instead of calling e.getMessage().

Comment on lines +82 to +101
addCallback(new OcppCallback<>() {
@Override
public void success(String chargeBoxId, String statusValue) {
addNewResponse(chargeBoxId, statusValue);
if ("Accepted".equalsIgnoreCase(statusValue)) {
repo.setProfile(details.getChargingProfilePk(), chargeBoxId, params.getConnectorId());
}
}

@Override
public void successError(String chargeBoxId, Object error) {
addNewError(chargeBoxId, error.toString());
}

@Override
public void failed(String chargeBoxId, Exception e) {
addNewError(chargeBoxId, e.getMessage());
}
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove duplicate result accounting in custom callback

This callback calls addNewResponse/addNewError while the default callback already records results, causing double increments and premature task completion. Keep this callback side-effect-only (DB update) and let the default callback manage resultMap.

Apply:

         addCallback(new OcppCallback<>() {
             @Override
             public void success(String chargeBoxId, String statusValue) {
-                addNewResponse(chargeBoxId, statusValue);
-                if ("Accepted".equalsIgnoreCase(statusValue)) {
-                    repo.setProfile(details.getChargingProfilePk(), chargeBoxId, params.getConnectorId());
-                }
+                if ("Accepted".equalsIgnoreCase(statusValue)) {
+                    repo.setProfile(details.getChargingProfilePk(), chargeBoxId, request.getConnectorId());
+                }
             }

             @Override
             public void successError(String chargeBoxId, Object error) {
-                addNewError(chargeBoxId, error.toString());
+                // no-op; default callback records error
             }

             @Override
             public void failed(String chargeBoxId, Exception e) {
-                addNewError(chargeBoxId, e.getMessage());
+                // no-op; default callback records error
             }
         });

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTask.java
around lines 82 to 101, the custom OcppCallback currently calls
addNewResponse/addNewError which duplicates the default callback's result
accounting; remove those calls so this callback performs only side-effects (e.g.
DB update via repo.setProfile) and does not touch the task resultMap or
completion tracking. Specifically, in success(...) remove addNewResponse(...)
but keep the repo.setProfile(...) call; in successError(...) and failed(...)
remove addNewError(...) and only perform any necessary non-result actions (or
nothing) so the default callback remains solely responsible for recording
responses and errors.

Comment on lines +159 to +161
private static void checkAdditionalConstraints(SetChargingProfileRequest request) {
Optional.ofNullable(request.getCsChargingProfiles())
.map(ocpp.cp._2015._10.ChargingProfile::getChargingProfilePurpose)
.ifPresent(purpose -> {
if (purpose == ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE
&& request.getConnectorId() != 0) {
throw new SteveException.InternalError(
"ChargePointMaxProfile can only be set at Charge Point ConnectorId 0");
}

if (ChargingProfilePurposeType.TX_PROFILE == purpose && request.getConnectorId() < 1) {
throw new SteveException.InternalError("TxProfile should only be set at Charge Point ConnectorId > 0");
}
if (purpose == ChargingProfilePurposeType.TX_PROFILE && request.getConnectorId() < 1) {
throw new SteveException.InternalError(
"TxProfile should only be set at Charge Point ConnectorId > 0");
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Guard against null connectorId to avoid NPE during unboxing

Ad-hoc requests may omit connectorId; current comparisons auto-unbox and can NPE.

-        Optional.ofNullable(request.getCsChargingProfiles())
+        final Integer connectorId = request.getConnectorId();
+        if (connectorId == null) {
+            throw new SteveException.InternalError("ConnectorId must be provided for SetChargingProfile");
+        }
+        Optional.ofNullable(request.getCsChargingProfiles())
                 .map(ocpp.cp._2015._10.ChargingProfile::getChargingProfilePurpose)
                 .ifPresent(purpose -> {
-                    if (purpose == ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE
-                            && request.getConnectorId() != 0) {
+                    if (purpose == ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE
+                            && connectorId != 0) {
                         throw new SteveException.InternalError(
                                 "ChargePointMaxProfile can only be set at Charge Point ConnectorId 0");
                     }

-                    if (purpose == ChargingProfilePurposeType.TX_PROFILE && request.getConnectorId() < 1) {
+                    if (purpose == ChargingProfilePurposeType.TX_PROFILE && connectorId < 1) {
                         throw new SteveException.InternalError(
                                 "TxProfile should only be set at Charge Point ConnectorId > 0");
                     }
                 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static void checkAdditionalConstraints(SetChargingProfileRequest request) {
Optional.ofNullable(request.getCsChargingProfiles())
.map(ocpp.cp._2015._10.ChargingProfile::getChargingProfilePurpose)
.ifPresent(purpose -> {
if (purpose == ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE
&& request.getConnectorId() != 0) {
throw new SteveException.InternalError(
"ChargePointMaxProfile can only be set at Charge Point ConnectorId 0");
}
if (ChargingProfilePurposeType.TX_PROFILE == purpose && request.getConnectorId() < 1) {
throw new SteveException.InternalError("TxProfile should only be set at Charge Point ConnectorId > 0");
}
if (purpose == ChargingProfilePurposeType.TX_PROFILE && request.getConnectorId() < 1) {
throw new SteveException.InternalError(
"TxProfile should only be set at Charge Point ConnectorId > 0");
}
});
private static void checkAdditionalConstraints(SetChargingProfileRequest request) {
final Integer connectorId = request.getConnectorId();
if (connectorId == null) {
throw new SteveException.InternalError("ConnectorId must be provided for SetChargingProfile");
}
Optional.ofNullable(request.getCsChargingProfiles())
.map(ocpp.cp._2015._10.ChargingProfile::getChargingProfilePurpose)
.ifPresent(purpose -> {
if (purpose == ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE
&& connectorId != 0) {
throw new SteveException.InternalError(
"ChargePointMaxProfile can only be set at Charge Point ConnectorId 0");
}
if (purpose == ChargingProfilePurposeType.TX_PROFILE && connectorId < 1) {
throw new SteveException.InternalError(
"TxProfile should only be set at Charge Point ConnectorId > 0");
}
});
}
🤖 Prompt for AI Agents
In
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTask.java
around lines 159 to 173, the code compares request.getConnectorId() using
primitive comparisons which can NPE if connectorId is null; guard against null
by first retrieving the Integer connectorId = request.getConnectorId() and if
it's null throw a clear SteveException (e.g. BadRequest or InternalError per
project convention) indicating connectorId is missing, otherwise perform the
existing comparisons against the non-null Integer (or convert to int) to avoid
unboxing NPEs.

Comment on lines 74 to 82
@Override
public void successError(String chargeBoxId, Object error) {
addNewError(chargeBoxId, error.toString());
}
};
}

@Override
public AsyncHandler<ocpp.cp._2015._10.UnlockConnectorResponse> getOcpp16Handler(String chargeBoxId) {
return res -> {
try {
success(chargeBoxId, res.get().getStatus().value());
} catch (Exception e) {
failed(chargeBoxId, e);
@Override
public void failed(String chargeBoxId, Exception e) {
addNewError(chargeBoxId, e.getMessage());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden error reporting to avoid null-related NPEs and include exception type.

error.toString() NPEs if null; e.getMessage() may be null.

-            public void successError(String chargeBoxId, Object error) {
-                addNewError(chargeBoxId, error.toString());
+            public void successError(String chargeBoxId, Object error) {
+                addNewError(chargeBoxId, String.valueOf(error));
@@
-            public void failed(String chargeBoxId, Exception e) {
-                addNewError(chargeBoxId, e.getMessage());
+            public void failed(String chargeBoxId, Exception e) {
+                String msg = e.getClass().getSimpleName() + ": " + (e.getMessage() != null ? e.getMessage() : "<no message>");
+                addNewError(chargeBoxId, msg);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public void successError(String chargeBoxId, Object error) {
addNewError(chargeBoxId, error.toString());
}
};
}
@Override
public AsyncHandler<ocpp.cp._2015._10.UnlockConnectorResponse> getOcpp16Handler(String chargeBoxId) {
return res -> {
try {
success(chargeBoxId, res.get().getStatus().value());
} catch (Exception e) {
failed(chargeBoxId, e);
@Override
public void failed(String chargeBoxId, Exception e) {
addNewError(chargeBoxId, e.getMessage());
}
@Override
public void successError(String chargeBoxId, Object error) {
addNewError(chargeBoxId, String.valueOf(error));
}
@Override
public void failed(String chargeBoxId, Exception e) {
String msg = e.getClass().getSimpleName() + ": " + (e.getMessage() != null ? e.getMessage() : "<no message>");
addNewError(chargeBoxId, msg);
}
🤖 Prompt for AI Agents
In
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/UnlockConnectorTask.java
around lines 74 to 82, the current handlers call error.toString() and
e.getMessage() which can be null and cause NPEs or lose useful context; change
them to produce safe, non-null strings and include the exception type: for the
successError use a null-safe conversion (e.g. Objects.toString(error, "null") or
String.valueOf(error)) and for failed construct a message that prefixes the
exception class name (e.getClass().getSimpleName()) and appends a null-safe
message (falling back to String.valueOf(e) or a default text) before passing
into addNewError so no NPEs occur and the exception type is recorded.

Comment on lines +44 to +47
.withRetries(
task.getParams().getRetries())
.withRetryInterval(
task.getParams().getRetryInterval()),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Confirm parameter semantics (units and optionality)

Ensure retryInterval matches OCPP units (seconds) and that retries/retryInterval nulls are handled per CP version expectations.

Run to verify DTO docs and types:

Also applies to: 68-71


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "UpdateFirmwareParams definition and fields:"
rg -n --type=java -C3 'class\s+UpdateFirmwareParams\b|getRetryInterval|getRetries|getRetrieve|getLocation'

Length of output: 20062


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== Find UpdateFirmwareParams definitions ==="
rg -n --hidden -S 'class\s+UpdateFirmwareParams\b' || true

echo
echo "=== steve-core: UpdateFirmwareParams (ocpp) ==="
sed -n '1,240p' steve-core/src/main/java/de/rwth/idsg/steve/web/dto/ocpp/UpdateFirmwareParams.java || true

echo
echo "=== steve-core: UpdateFirmwareParams (common) ==="
sed -n '1,240p' steve-core/src/main/java/de/rwth/idsg/steve/web/dto/common/UpdateFirmwareParams.java || true

echo
echo "=== MultipleChargePointSelect (common) ==="
sed -n '1,240p' steve-core/src/main/java/de/rwth/idsg/steve/web/dto/common/MultipleChargePointSelect.java || true

echo
echo "=== UpdateFirmwareTask (around usage) ==="
sed -n '1,240p' steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/UpdateFirmwareTask.java || true

echo
echo "=== Search for retries / retryInterval usages & definitions ==="
rg -n --hidden -S '\b(retries|retryInterval|getRetries|getRetryInterval|setRetries|setRetryInterval)\b' || true

echo
echo "=== Search for generated OCPP UpdateFirmwareRequest / withRetries signatures ==="
rg -n --hidden -S 'class\s+UpdateFirmwareRequest\b|withRetries\(|withRetryInterval\(' || true

Length of output: 17874


Confirm parameter semantics — allow 0 for retries; retryInterval is seconds

  • Change DTO validation: replace @min(1) with @min(0) for retries and retryInterval in steve-core/src/main/java/de/rwth/idsg/steve/web/dto/ocpp/UpdateFirmwareParams.java and steve-core/src/main/java/de/rwth/idsg/steve/web/dto/ocpp/GetDiagnosticsParams.java — OCPP allows 0 (meaning "no retries") and treats retryInterval as seconds. (ocpp-spec.org)
  • Ensure null-handling: when retries/retryInterval == null the generated UpdateFirmware/GetDiagnostics request MUST omit those elements (WSDL minOccurs=0) — verify withRetries()/withRetryInterval() marshalling does not force a default.
  • Update UI/docs (steve-ui-jsp/src/main/resources/webapp/WEB-INF/views/op-forms/UpdateFirmwareForm.jsp) to state "0 = no retries" and "retryInterval in seconds".
🤖 Prompt for AI Agents
In steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/UpdateFirmwareTask.java
around lines 44-47, and the related DTOs and UI files: change DTO validation
annotations in
steve-core/src/main/java/de/rwth/idsg/steve/web/dto/ocpp/UpdateFirmwareParams.java
and
steve-core/src/main/java/de/rwth/idsg/steve/web/dto/ocpp/GetDiagnosticsParams.java
from @Min(1) to @Min(0) for both retries and retryInterval; update
UpdateFirmwareTask.java so it does not call withRetries(...) or
withRetryInterval(...) when the DTO values are null (only call those builder
methods when value != null) to ensure the generated request omits the elements
(minOccurs=0); verify the marshaller does not insert defaults if methods are not
called; and update
steve-ui-jsp/src/main/resources/webapp/WEB-INF/views/op-forms/UpdateFirmwareForm.jsp
to document "0 = no retries" and "retryInterval in seconds".

@juherr juherr force-pushed the refactor-task-management branch from 00a7c85 to fcc05f3 Compare September 15, 2025 13:09
google-labs-jules bot and others added 3 commits September 15, 2025 15:24
This commit refactors the OCPP task management system to be more generic and maintainable by introducing a mapper-based architecture.

- Decouples task logic from OCPP versions using `TaskDefinition` and mappers.
- Groups version-specific mappers in `OcppVersionHandler`.
- Simplifies the `CommunicationTask` base class and all concrete task implementations.
- Updates the `ChargePointServiceInvoker` to use the new generic system.
@juherr juherr force-pushed the refactor-task-management branch from fcc05f3 to a284a8c Compare September 15, 2025 13:40
@juherr
Copy link
Copy Markdown
Owner

juherr commented Sep 15, 2025

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 15, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@google-labs-jules
Copy link
Copy Markdown
Author

@coderabbitai review

Great! I'm ready to address any feedback that comes from the review.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 15, 2025

Skipped: Cannot respond to another bot.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
steve-jooq/src/main/java/de/rwth/idsg/steve/jooq/mapper/TransactionMapper.java (1)

56-59: Fix NullAway: method can return null but signature is non-null.

return null; triggers the pipeline warning. Mark the method as nullable (or return a default if your DTO supports it).

Apply:

+import javax.annotation.Nullable;
@@
-    private static TransactionStopEventActor toDto(jooq.steve.db.enums.TransactionStopEventActor actor) {
+    private static @Nullable TransactionStopEventActor toDto(jooq.steve.db.enums.TransactionStopEventActor actor) {
         if (actor == null) {
             return null;
         }
steve-jooq/src/main/java/de/rwth/idsg/steve/repository/impl/ChargePointRepositoryImpl.java (1)

208-209: Align nullability with interface: parameter should be @nullable

The interface declares @Nullable ConnectorStatusForm; annotate the override to fix NullAway warning.

-    public List<ConnectorStatus> getChargePointConnectorStatus(ConnectorStatusForm form) {
+    public List<ConnectorStatus> getChargePointConnectorStatus(@Nullable ConnectorStatusForm form) {
steve-ui-jsp/src/main/resources/webapp/WEB-INF/views/data-man/00-cp-misc.jsp (1)

37-45: Fix latitude/longitude condition typo to enable Maps link

The condition checks longitude twice; include latitude.

-    <c:if test="${(not empty cp.locationLongitude) and (not empty cp.locationLongitude)}">
+    <c:if test="${(not empty cp.locationLatitude) and (not empty cp.locationLongitude)}">
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SendLocalListTask.java (1)

104-111: Fix build: replace Stream.toList() (JDK 16+) with Collectors.toList()

Likely cause of the reported build failure on older JDK targets. Use Collectors.toList() and add the missing import.

@@
-        return ocpp16.stream()
-                .map(k -> new ocpp.cp._2012._06.AuthorisationData()
-                        .withIdTag(k.getIdTag())
-                        .withIdTagInfo(toOcpp15(k.getIdTagInfo())))
-                .toList();
+        return ocpp16.stream()
+                .map(k -> new ocpp.cp._2012._06.AuthorisationData()
+                        .withIdTag(k.getIdTag())
+                        .withIdTagInfo(toOcpp15(k.getIdTagInfo())))
+                .collect(java.util.stream.Collectors.toList());
@@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors; // if you prefer a simple name, adjust the call above accordingly

Also applies to: 30-33

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ClearChargingProfileTask.java (1)

68-92: Only clear DB when status is Accepted; also null‑guard filterType.

Clearing profiles on non‑Accepted responses desynchronizes DB and CP state; switching on a null filterType will NPE.

Apply:

-            public void success(String chargeBoxId, String statusValue) {
-                addNewResponse(chargeBoxId, statusValue);
-
-                switch (params.getFilterType()) {
+            public void success(String chargeBoxId, String statusValue) {
+                addNewResponse(chargeBoxId, statusValue);
+                final var filterType = params.getFilterType();
+                if (filterType == null) {
+                    log.warn("ClearChargingProfile: filterType is null for chargebox '{}'", chargeBoxId);
+                    return;
+                }
+
+                if ("Accepted".equalsIgnoreCase(statusValue)) {
+                    switch (filterType) {
                     case ChargingProfileId ->
                         chargingProfileRepository.clearProfile(params.getChargingProfilePk(), chargeBoxId);
                     case OtherParameters ->
                         chargingProfileRepository.clearProfile(
                                 chargeBoxId,
                                 params.getConnectorId(),
                                 params.getChargingProfilePurpose(),
                                 params.getStackLevel());
-                    default -> {
-                        log.warn("Unexpected {} enum value", ClearChargingProfileFilterType.class.getSimpleName());
-                        return;
-                    }
-                }
-
-                if (!"Accepted".equalsIgnoreCase(statusValue)) {
-                    log.info(
-                            "Deleted charging profile(s) for chargebox '{}' from DB even though the response was {}",
-                            chargeBoxId,
-                            statusValue);
-                }
+                    default -> log.warn("Unexpected {} enum value",
+                            ClearChargingProfileFilterType.class.getSimpleName());
+                    }
+                } else {
+                    log.info("ClearChargingProfile rejected for '{}': {}", chargeBoxId, statusValue);
+                }
             }
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/CommunicationTask.java (1)

134-142: Fix NullAway: guard null from resultMap.get()

resultMap.get(chargeBoxId) can be null; addNull check mirrors addNewResponse().

-    public void addNewError(String chargeBoxId, @Nullable String errorMessage) {
-        resultMap.get(chargeBoxId).setErrorMessage(errorMessage);
+    public void addNewError(String chargeBoxId, @Nullable String errorMessage) {
+        var result = resultMap.get(chargeBoxId);
+        if (result == null) {
+            log.warn("Received error for unknown chargeBoxId '{}'", chargeBoxId);
+            return;
+        }
+        result.setErrorMessage(errorMessage);
♻️ Duplicate comments (8)
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/UnlockConnectorTask.java (1)

54-60: Constructor wiring to TASK_DEFINITION: good; prior NPE risks from handler strings are gone.

Removing per-version handlers also eliminates earlier error.toString()/e.getMessage() pitfalls flagged before.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/CancelReservationTask.java (2)

62-83: Race condition when canceling reservations across multiple charge boxes.

When this task targets multiple charge boxes, multiple "Accepted" responses could trigger repeated cancelled() calls on the same reservation. This could lead to inconsistent state or errors if the repository method is not idempotent.

Apply this diff to ensure the cancellation is executed only once:

 @Override
 protected OcppCallback<String> createDefaultCallback() {
+    java.util.concurrent.atomic.AtomicBoolean cancelled = new java.util.concurrent.atomic.AtomicBoolean(false);
     return new OcppCallback<>() {
         @Override
         public void success(String chargeBoxId, String statusValue) {
             addNewResponse(chargeBoxId, statusValue);

-            if ("Accepted".equalsIgnoreCase(statusValue)) {
+            if ("Accepted".equalsIgnoreCase(statusValue) && cancelled.compareAndSet(false, true)) {
                 reservationRepository.cancelled(params.getReservationId());
             }
         }

73-81: Improve error handling to avoid NPEs and include exception types.

The error handlers can throw NPEs if the error object is null or if the exception message is null. Additionally, logging just the message without the exception type loses valuable debugging context.

Apply this diff to make error handling more robust:

         @Override
         public void successError(String chargeBoxId, Object error) {
-            addNewError(chargeBoxId, error.toString());
+            addNewError(chargeBoxId, String.valueOf(error));
         }

         @Override
         public void failed(String chargeBoxId, Exception e) {
-            addNewError(chargeBoxId, e.getMessage());
+            String msg = e.getClass().getSimpleName() + ": " + (e.getMessage() != null ? e.getMessage() : "<no message>");
+            addNewError(chargeBoxId, msg);
         }
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/UpdateFirmwareTask.java (1)

31-63: Verify null handling for optional retry parameters.

The code directly passes getRetries() and getRetryInterval() to the request builders without null checks. According to OCPP specification, these are optional parameters (minOccurs=0) and should be omitted from the request when null.

Let me verify how the UpdateFirmwareParams handles these optional fields:

#!/bin/bash
# Check the UpdateFirmwareParams DTO for validation annotations and null handling
echo "=== UpdateFirmwareParams definition and validations ==="
cat steve-core/src/main/java/de/rwth/idsg/steve/web/dto/ocpp/UpdateFirmwareParams.java 2>/dev/null || \
  fd -t f "UpdateFirmwareParams.java" --exec cat {}

echo -e "\n=== Check if the OCPP request builders handle null values ==="
# Search for withRetries/withRetryInterval method signatures in generated code
fd -e java -x grep -l "withRetries\|withRetryInterval" {} \; | head -5 | xargs grep -A2 "withRetries\|withRetryInterval"

Based on the verification results, apply this improvement to handle null values properly:

 .versionHandler(
         OcppVersion.V_12,
         new OcppVersionHandler<>(
-                task -> new ocpp.cp._2010._08.UpdateFirmwareRequest()
-                        .withLocation(task.getParams().getLocation())
-                        .withRetrieveDate(toOffsetDateTime(
-                                task.getParams().getRetrieve()))
-                        .withRetries(task.getParams().getRetries())
-                        .withRetryInterval(task.getParams().getRetryInterval()),
+                task -> {
+                    var req = new ocpp.cp._2010._08.UpdateFirmwareRequest()
+                            .withLocation(task.getParams().getLocation())
+                            .withRetrieveDate(toOffsetDateTime(
+                                    task.getParams().getRetrieve()));
+                    if (task.getParams().getRetries() != null) {
+                        req.withRetries(task.getParams().getRetries());
+                    }
+                    if (task.getParams().getRetryInterval() != null) {
+                        req.withRetryInterval(task.getParams().getRetryInterval());
+                    }
+                    return req;
+                },
                 r -> "OK"))

Apply the same pattern for V_15 and V_16 handlers.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ChangeAvailabilityTask.java (1)

34-38: Guard null availType before .value() to avoid NPE.

getAvailType() can be null; current .value() will NPE. Validate first and pass the mapped enum.

Apply:

@@
-                                    task -> new ocpp.cp._2010._08.ChangeAvailabilityRequest()
-                                            .withConnectorId(task.getParams().getConnectorId())
-                                            .withType(ocpp.cp._2010._08.AvailabilityType.fromValue(task.getParams()
-                                                    .getAvailType()
-                                                    .value())),
+                                    task -> {
+                                        var type = java.util.Objects.requireNonNull(
+                                                task.getParams().getAvailType(), "availType");
+                                        return new ocpp.cp._2010._08.ChangeAvailabilityRequest()
+                                                .withConnectorId(task.getParams().getConnectorId())
+                                                .withType(ocpp.cp._2010._08.AvailabilityType.fromValue(type.value()));
+                                    },
@@
-                                    task -> new ocpp.cp._2012._06.ChangeAvailabilityRequest()
+                                    task -> {
+                                        var type = java.util.Objects.requireNonNull(
+                                                task.getParams().getAvailType(), "availType");
+                                        return new ocpp.cp._2012._06.ChangeAvailabilityRequest()
                                             .withConnectorId(task.getParams().getConnectorId())
-                                            .withType(ocpp.cp._2012._06.AvailabilityType.fromValue(task.getParams()
-                                                    .getAvailType()
-                                                    .value())),
+                                            .withType(ocpp.cp._2012._06.AvailabilityType.fromValue(type.value()));
+                                    },
@@
-                                    task -> new ocpp.cp._2015._10.ChangeAvailabilityRequest()
+                                    task -> {
+                                        var type = java.util.Objects.requireNonNull(
+                                                task.getParams().getAvailType(), "availType");
+                                        return new ocpp.cp._2015._10.ChangeAvailabilityRequest()
                                             .withConnectorId(task.getParams().getConnectorId())
-                                            .withType(ocpp.cp._2015._10.AvailabilityType.fromValue(task.getParams()
-                                                    .getAvailType()
-                                                    .value())),
+                                            .withType(ocpp.cp._2015._10.AvailabilityType.fromValue(type.value()));
+                                    },

Also applies to: 46-49, 56-58

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetDiagnosticsTask.java (1)

40-44: Fix SOAP date types: use XMLGregorianCalendar instead of OffsetDateTime.

withStartTime/withStopTime in ocpp.cp SOAP bindings expect XMLGregorianCalendar; passing OffsetDateTime will break marshalling. Convert via DatatypeFactory (or a shared util) and update all three version handlers.

Apply this diff to the shown lines:

-                                            .withStartTime(toOffsetDateTime(
-                                                    task.getParams().getStart()))
-                                            .withStopTime(toOffsetDateTime(
-                                                    task.getParams().getStop())),
+                                            .withStartTime(toXmlDateTime(toOffsetDateTime(task.getParams().getStart())))
+                                            .withStopTime(toXmlDateTime(toOffsetDateTime(task.getParams().getStop()))),

Repeat the same replacement for V_15 and V_16 blocks.

Add a helper in this class (or in DateTimeUtils and import statically):

private static javax.xml.datatype.XMLGregorianCalendar toXmlDateTime(java.time.OffsetDateTime odt) {
    if (odt == null) return null;
    var gc = java.util.GregorianCalendar.from(odt.toZonedDateTime());
    try {
        return javax.xml.datatype.DatatypeFactory.newInstance().newXMLGregorianCalendar(gc);
    } catch (javax.xml.datatype.DatatypeConfigurationException e) {
        throw new IllegalStateException("Cannot create XMLGregorianCalendar", e);
    }
}

Also applies to: 52-56, 64-68

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTask.java (1)

147-161: Avoid unboxing NPE; validate connectorId upfront.

request.getConnectorId() is an Integer; comparing with primitives can NPE. Also enforce presence per OCPP spec.

-    private static void checkAdditionalConstraints(SetChargingProfileRequest request) {
-        Optional.ofNullable(request.getCsChargingProfiles())
+    private static void checkAdditionalConstraints(SetChargingProfileRequest request) {
+        final Integer connectorId = request.getConnectorId();
+        if (connectorId == null) {
+            throw new SteveException.InternalError("ConnectorId must be provided for SetChargingProfile");
+        }
+        Optional.ofNullable(request.getCsChargingProfiles())
                 .map(ocpp.cp._2015._10.ChargingProfile::getChargingProfilePurpose)
                 .ifPresent(purpose -> {
                     if (purpose == ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE
-                            && request.getConnectorId() != 0) {
+                            && connectorId != 0) {
                         throw new SteveException.InternalError(
                                 "ChargePointMaxProfile can only be set at Charge Point ConnectorId 0");
                     }
 
-                    if (purpose == ChargingProfilePurposeType.TX_PROFILE && request.getConnectorId() < 1) {
+                    if (purpose == ChargingProfilePurposeType.TX_PROFILE && connectorId < 1) {
                         throw new SteveException.InternalError(
                                 "TxProfile should only be set at Charge Point ConnectorId > 0");
                     }
                 });
     }
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/CommunicationTask.java (1)

193-201: Validate chargeBoxId before resolving version (prevents "version null")

Add an explicit check and fail fast with a clear message.

     public <T extends ResponseType> AsyncHandler<T> createHandler(String chargeBoxId) {
-        var version = versionMap.get(chargeBoxId);
+        if (!versionMap.containsKey(chargeBoxId)) {
+            throw new IllegalArgumentException("Unknown chargeBoxId for this task: " + chargeBoxId);
+        }
+        var version = versionMap.get(chargeBoxId);
         var handler = taskDefinition.getVersionHandlers().get(version);
         if (handler == null) {
             throw new UnsupportedOperationException("Operation not supported for version " + version);
         }
         var mapper = (ResponseMapper<T, RESPONSE>) handler.getResponseMapper();

Also applies to: 202-209

🧹 Nitpick comments (30)
steve-jooq/src/main/java/de/rwth/idsg/steve/jooq/mapper/TransactionMapper.java (2)

60-60: Use locale‑independent uppercasing.

Avoid locale surprises (e.g., Turkish i). Use Locale.ROOT.

Apply:

+import java.util.Locale;
@@
-        return TransactionStopEventActor.valueOf(actor.getLiteral().toUpperCase());
+        return TransactionStopEventActor.valueOf(actor.getLiteral().toUpperCase(Locale.ROOT));

56-61: Behavior change: mapping now uses DB literal instead of enum name. Verify one‑to‑one alignment.

If any DB literal doesn’t match the repository enum (modulo uppercasing), valueOf will throw. Confirm literals align or add a guarded fallback.

Optionally harden:

-        return TransactionStopEventActor.valueOf(actor.getLiteral().toUpperCase(Locale.ROOT));
+        final String lit = actor.getLiteral();
+        if (lit == null) return null;
+        try {
+            return TransactionStopEventActor.valueOf(lit.toUpperCase(Locale.ROOT));
+        } catch (IllegalArgumentException ex) {
+            // TODO: consider logging and/or mapping to a safe default if available
+            return null;
+        }
steve-ocpp-transport-websocket/src/main/java/de/rwth/idsg/steve/ocpp/ws/data/CommunicationContext.java (2)

77-80: Align with callback semantics and document intent

getDefaultCallback().successError(...) reads odd; if this is the intended “error path,” add a short comment to clarify that this forwards JSON-RPC errors to the task’s default callback.


64-65: Consider clearing handlers after use to avoid stale references

If these handlers are one-shot per request, clear them once a response/error arrives to prevent accidental reuse or memory retention on long-lived sessions.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/RequestResult.java (1)

32-34: Add helpers to enforce invariants and reduce null checks

With both response and errorMessage nullable, callers can easily misuse. Provide small helpers like boolean isSuccess(), T getOrThrow(), and factory methods success(T)/error(String) to encode invariants.

 public class RequestResult<T> {
     private @Nullable T response;
     private @Nullable String errorMessage;
+
+    public boolean isSuccess() { return errorMessage == null; }
+    public T getOrThrow() {
+        if (response != null) return response;
+        throw new IllegalStateException(errorMessage == null ? "No response" : errorMessage);
+    }
+    public static <T> RequestResult<T> success(T r) {
+        var rr = new RequestResult<T>();
+        rr.setResponse(r);
+        return rr;
+    }
+    public static <T> RequestResult<T> error(String msg) {
+        var rr = new RequestResult<T>();
+        rr.setErrorMessage(msg);
+        return rr;
+    }
 }
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/OcppVersionHandler.java (1)

31-33: Wildcards erode type-safety; consider parameterizing over Task and Request

The current ? extends wildcards force unchecked casts in consumers. Prefer a fully-parameterized handler to preserve compile-time safety.

-public class OcppVersionHandler<S extends ChargePointSelection, U> {
-    private final RequestMapper<? extends CommunicationTask<S, U>, ? extends RequestType> requestMapper;
-    private final ResponseMapper<? extends ResponseType, U> responseMapper;
+public class OcppVersionHandler<
+        S extends ChargePointSelection,
+        U,
+        C extends CommunicationTask<S, U>,
+        R extends RequestType> {
+    private final RequestMapper<C, R> requestMapper;
+    private final ResponseMapper<? extends ResponseType, U> responseMapper;
 }

If refactoring scope is tight, at least add an internal, clearly documented unchecked cast helper that returns RequestMapper<C,R> to centralize the suppression.

steve-core/src/main/java/de/rwth/idsg/steve/repository/dto/ChargePoint.java (1)

47-61: Nullable annotations for optional DB-backed fields

Most of these detail fields can be null at the DB level. Mark them @Nullable to match reality and silence downstream NullAway issues when mappers pass nulls.

 import lombok.Getter;
+import org.jspecify.annotations.Nullable;
 ...
-        private final String endpointAddress;
+        private final @Nullable String endpointAddress;
         private final String ocppProtocol;
-        private final String chargePointVendor;
-        private final String chargePointModel;
-        private final String chargePointSerialNumber;
-        private final String chargeBoxSerialNumber;
-        private final String fwVersion;
-        private final Instant fwUpdateTimestamp;
-        private final String iccid;
-        private final String imsi;
-        private final String meterType;
-        private final String meterSerialNumber;
-        private final String diagnosticsStatus;
-        private final Instant diagnosticsTimestamp;
-        private final Instant lastHeartbeatTimestamp;
+        private final @Nullable String chargePointVendor;
+        private final @Nullable String chargePointModel;
+        private final @Nullable String chargePointSerialNumber;
+        private final @Nullable String chargeBoxSerialNumber;
+        private final @Nullable String fwVersion;
+        private final @Nullable Instant fwUpdateTimestamp;
+        private final @Nullable String iccid;
+        private final @Nullable String imsi;
+        private final @Nullable String meterType;
+        private final @Nullable String meterSerialNumber;
+        private final @Nullable String diagnosticsStatus;
+        private final @Nullable Instant diagnosticsTimestamp;
+        private final @Nullable Instant lastHeartbeatTimestamp;
steve-jooq/src/main/java/de/rwth/idsg/steve/jooq/mapper/ChargePointMapper.java (1)

34-35: Remove no-op call to r.getAdminAddress()

This early call has no side effects in jOOQ records and is redundant since you set it on the builder later.

-        r.getAdminAddress();
steve-jooq/src/main/java/de/rwth/idsg/steve/repository/impl/ChargePointRepositoryImpl.java (2)

61-66: Add summary line to Javadoc to satisfy style check

Provide a one-line summary before tags.

-/**
- * @author Sevket Goekay <sevketgokay@gmail.com>
- * @since 14.08.2014
- */
+/**
+ * Repository implementation for ChargePoint queries and mutations.
+ *
+ * @author Sevket Goekay <sevketgokay@gmail.com>
+ * @since 14.08.2014
+ */

173-184: Avoid default time-zone; pass an explicit ZoneId

Replace LocalDate.now() with LocalDate.now(zoneId) to meet the rule and avoid environment-dependent results.

-        switch (form.getHeartbeatPeriod()) {
+        var zoneId = java.time.ZoneId.systemDefault();
+        switch (form.getHeartbeatPeriod()) {
           case ALL -> {}
           case TODAY ->
-                selectQuery.addConditions(
-                        date(CHARGE_BOX.LAST_HEARTBEAT_TIMESTAMP).eq(LocalDate.now()));
+                selectQuery.addConditions(date(CHARGE_BOX.LAST_HEARTBEAT_TIMESTAMP).eq(LocalDate.now(zoneId)));
           case YESTERDAY ->
-                selectQuery.addConditions(date(CHARGE_BOX.LAST_HEARTBEAT_TIMESTAMP)
-                        .eq(LocalDate.now().minusDays(1)));
+                selectQuery.addConditions(date(CHARGE_BOX.LAST_HEARTBEAT_TIMESTAMP)
+                        .eq(LocalDate.now(zoneId).minusDays(1)));
           case EARLIER ->
-                selectQuery.addConditions(date(CHARGE_BOX.LAST_HEARTBEAT_TIMESTAMP)
-                        .lessThan(LocalDate.now().minusDays(1)));
+                selectQuery.addConditions(date(CHARGE_BOX.LAST_HEARTBEAT_TIMESTAMP)
+                        .lessThan(LocalDate.now(zoneId).minusDays(1)));
steve-ui-jsp/src/main/java/de/rwth/idsg/steve/web/controller/TasksController.java (1)

107-111: Handle error cases when response is null

getResponse() can be null; consider surfacing errorMessage to the view or guarding to avoid NPEs in JSPs.

-        var response = extractResult(task, chargeBoxId).getResponse();
+        var rr = extractResult(task, chargeBoxId);
+        var response = rr.getResponse();
+        model.addAttribute("errorMessage", rr.getErrorMessage());

Also applies to: 115-119

steve-ui-jsp/src/main/resources/webapp/WEB-INF/views/data-man/chargepointDetails.jsp (1)

87-101: Bindings migrated to cp.*: OK; format timestamps to avoid raw toString and add null-safe fallback.

Suggest formatting date-like fields; current output may be unlocalized or “null”.

Apply to the three timestamp rows:

- <tr><td>Firmware Update Timestamp:</td><td><encode:forHtml value="${cp.fwUpdateTimestamp}" /></td></tr>
+ <tr><td>Firmware Update Timestamp:</td><td><c:choose><c:when test="${not empty cp.fwUpdateTimestamp}"><fmt:formatDate value="${cp.fwUpdateTimestamp}" pattern="yyyy-MM-dd HH:mm:ss" /></c:when><c:otherwise>-</c:otherwise></c:choose></td></tr>
- <tr><td>Diagnostics Timestamp:</td><td><encode:forHtml value="${cp.diagnosticsTimestamp}" /></td></tr>
+ <tr><td>Diagnostics Timestamp:</td><td><c:choose><c:when test="${not empty cp.diagnosticsTimestamp}"><fmt:formatDate value="${cp.diagnosticsTimestamp}" pattern="yyyy-MM-dd HH:mm:ss" /></c:when><c:otherwise>-</c:otherwise></c:choose></td></tr>
- <tr><td>Last Heartbeat Timestamp:</td><td><encode:forHtml value="${cp.lastHeartbeatTimestamp}" /></td></tr>
+ <tr><td>Last Heartbeat Timestamp:</td><td><c:choose><c:when test="${not empty cp.lastHeartbeatTimestamp}"><fmt:formatDate value="${cp.lastHeartbeatTimestamp}" pattern="yyyy-MM-dd HH:mm:ss" /></c:when><c:otherwise>-</c:otherwise></c:choose></td></tr>

Please confirm these three fields are java.util.Date/java.time types; if already Strings, we’ll keep the current <encode:forHtml> and add a “-” fallback instead.

steve/src/main/java/de/rwth/idsg/steve/ocpp/ChargePointServiceInvokerImpl.java (2)

61-71: Nice dispatch dedup; consider making helper static and add minimal precondition.

invoke is pure; make it static and guard cp with Objects.requireNonNull(cp) for clearer NPEs.

-    private <T extends CommunicationTask<?, ?>> void invoke(
+    private static <T extends CommunicationTask<?, ?>> void invoke(
             ChargePointSelect cp,
             T task,
             BiConsumer<ChargePointSelect, T> soapAction,
             BiConsumer<ChargePointSelect, T> jsonAction) {
-        if (cp.isSoap()) {
+        java.util.Objects.requireNonNull(cp, "cp");
+        if (cp.isSoap()) {
             soapAction.accept(cp, task);
         } else {
             jsonAction.accept(cp, task);
         }
     }

102-108: Add @OverRide to implemented public methods in ChargePointServiceInvokerImpl

Multiple public methods in steve/src/main/java/de/rwth/idsg/steve/ocpp/ChargePointServiceInvokerImpl.java lack @OverRide; add @OverRide to all implemented interface/superclass methods to catch signature drift.

Affected methods: reset, clearCache, getDiagnostics, updateFirmware, unlockConnector, changeAvailability, changeConfiguration, remoteStartTransaction, remoteStopTransaction, dataTransfer, getConfiguration, getLocalListVersion, sendLocalList, reserveNow, cancelReservation, clearChargingProfile, setChargingProfile, getCompositeSchedule, triggerMessage.

-    public void changeAvailability(ChargePointSelect cp, ChangeAvailabilityTask task) {
+    @Override
+    public void changeAvailability(ChargePointSelect cp, ChangeAvailabilityTask task) {
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/UnlockConnectorTask.java (1)

29-53: Validate connectorId before building requests.

withConnectorId(...) will blow up on null; fail fast with a clear message.

+import java.util.Objects;
@@
-                                    task -> new ocpp.cp._2010._08.UnlockConnectorRequest()
-                                            .withConnectorId(task.getParams().getConnectorId()),
+                                    task -> new ocpp.cp._2010._08.UnlockConnectorRequest()
+                                            .withConnectorId(Objects.requireNonNull(task.getParams().getConnectorId(), "connectorId")),
@@
-                                    task -> new ocpp.cp._2012._06.UnlockConnectorRequest()
-                                            .withConnectorId(task.getParams().getConnectorId()),
+                                    task -> new ocpp.cp._2012._06.UnlockConnectorRequest()
+                                            .withConnectorId(Objects.requireNonNull(task.getParams().getConnectorId(), "connectorId")),
@@
-                                    task -> new ocpp.cp._2015._10.UnlockConnectorRequest()
-                                            .withConnectorId(task.getParams().getConnectorId()),
+                                    task -> new ocpp.cp._2015._10.UnlockConnectorRequest()
+                                            .withConnectorId(Objects.requireNonNull(task.getParams().getConnectorId(), "connectorId")),
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ChangeConfigurationTask.java (1)

29-55: Add null/blank guard for key/value.

OCPP expects both; enforce minimal validation to surface issues early.

+import java.util.Objects;
@@
-                                    task -> new ocpp.cp._2010._08.ChangeConfigurationRequest()
-                                            .withKey(task.getParams().getKey())
-                                            .withValue(task.getParams().getValue()),
+                                    task -> new ocpp.cp._2010._08.ChangeConfigurationRequest()
+                                            .withKey(Objects.requireNonNull(task.getParams().getKey(), "key"))
+                                            .withValue(Objects.requireNonNull(task.getParams().getValue(), "value")),
@@
-                                    task -> new ocpp.cp._2012._06.ChangeConfigurationRequest()
-                                            .withKey(task.getParams().getKey())
-                                            .withValue(task.getParams().getValue()),
+                                    task -> new ocpp.cp._2012._06.ChangeConfigurationRequest()
+                                            .withKey(Objects.requireNonNull(task.getParams().getKey(), "key"))
+                                            .withValue(Objects.requireNonNull(task.getParams().getValue(), "value")),
@@
-                                    task -> new ocpp.cp._2015._10.ChangeConfigurationRequest()
-                                            .withKey(task.getParams().getKey())
-                                            .withValue(task.getParams().getValue()),
+                                    task -> new ocpp.cp._2015._10.ChangeConfigurationRequest()
+                                            .withKey(Objects.requireNonNull(task.getParams().getKey(), "key"))
+                                            .withValue(Objects.requireNonNull(task.getParams().getValue(), "value")),
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SendLocalListTask.java (3)

82-88: Null-safe handling for add/update and delete lists

getDeleteList() and getAddUpdateList() may be null; current loop/addAll would NPE. Guard with empty lists.

-        if (getParams().getUpdateType() == SendLocalListUpdateType.DIFFERENTIAL) {
-            List<ocpp.cp._2015._10.AuthorizationData> auths = new ArrayList<>();
-            for (String idTag : getParams().getDeleteList()) {
+        if (getParams().getUpdateType() == SendLocalListUpdateType.DIFFERENTIAL) {
+            List<ocpp.cp._2015._10.AuthorizationData> auths = new ArrayList<>();
+            List<String> deleteList = getParams().getDeleteList() != null
+                    ? getParams().getDeleteList() : Collections.emptyList();
+            for (String idTag : deleteList) {
                 auths.add(new ocpp.cp._2015._10.AuthorizationData().withIdTag(idTag));
             }
-            auths.addAll(ocppTagsService.getAuthData(getParams().getAddUpdateList()));
+            List<String> addUpdateList = getParams().getAddUpdateList() != null
+                    ? getParams().getAddUpdateList() : Collections.emptyList();
+            auths.addAll(ocppTagsService.getAuthData(addUpdateList));

49-51: Tidy: drop method parameter and use the field directly

createOcpp16Request receives OcppTagsService but always uses the task’s field. Simplify the signature and call sites.

@@
-                                        ocpp.cp._2015._10.SendLocalListRequest ocpp16Request =
-                                                t.createOcpp16Request(t.ocppTagsService);
+                                        ocpp.cp._2015._10.SendLocalListRequest ocpp16Request =
+                                                t.createOcpp16Request();
@@
-                                        return t.createOcpp16Request(t.ocppTagsService);
+                                        return t.createOcpp16Request();
@@
-    private ocpp.cp._2015._10.SendLocalListRequest createOcpp16Request(OcppTagsService ocppTagsService) {
+    private ocpp.cp._2015._10.SendLocalListRequest createOcpp16Request() {

Also applies to: 66-67, 81-81


113-122: Defensive mapping: handle null IdTagInfo.status gracefully

If status is unexpectedly null, AuthorizationStatus.fromValue(...) will NPE. Safe fallback avoids crashing the task.

         return new ocpp.cp._2012._06.IdTagInfo()
                 .withParentIdTag(ocpp16.getParentIdTag())
                 .withExpiryDate(ocpp16.getExpiryDate())
-                .withStatus(ocpp.cp._2012._06.AuthorizationStatus.fromValue(
-                        ocpp16.getStatus().value()));
+                .withStatus(ocpp16.getStatus() == null
+                        ? null
+                        : ocpp.cp._2012._06.AuthorizationStatus.fromValue(ocpp16.getStatus().value()));
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ResetTask.java (1)

34-37: Guard against null resetType before fromValue(...)

A null resetType will NPE at .value(). Add a quick precondition.

-                                    task -> new ocpp.cp._2010._08.ResetRequest()
-                                            .withType(ocpp.cp._2010._08.ResetType.fromValue(task.getParams()
-                                                    .getResetType()
-                                                    .value())),
+                                    task -> {
+                                        var rt = java.util.Objects.requireNonNull(
+                                                task.getParams().getResetType(), "resetType is required");
+                                        return new ocpp.cp._2010._08.ResetRequest()
+                                                .withType(ocpp.cp._2010._08.ResetType.fromValue(rt.value()));
+                                    },
@@
-                                    task -> new ocpp.cp._2012._06.ResetRequest()
-                                            .withType(ocpp.cp._2012._06.ResetType.fromValue(task.getParams()
-                                                    .getResetType()
-                                                    .value())),
+                                    task -> {
+                                        var rt = java.util.Objects.requireNonNull(
+                                                task.getParams().getResetType(), "resetType is required");
+                                        return new ocpp.cp._2012._06.ResetRequest()
+                                                .withType(ocpp.cp._2012._06.ResetType.fromValue(rt.value()));
+                                    },
@@
-                                    task -> new ocpp.cp._2015._10.ResetRequest()
-                                            .withType(ocpp.cp._2015._10.ResetType.fromValue(task.getParams()
-                                                    .getResetType()
-                                                    .value())),
+                                    task -> {
+                                        var rt = java.util.Objects.requireNonNull(
+                                                task.getParams().getResetType(), "resetType is required");
+                                        return new ocpp.cp._2015._10.ResetRequest()
+                                                .withType(ocpp.cp._2015._10.ResetType.fromValue(rt.value()));
+                                    },

Also applies to: 43-46, 53-55

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/DataTransferTask.java (1)

37-44: Null-safe toString to avoid NPE when status is null

StringBuilder(String) throws NPE on null. Wrap with String.valueOf.

-        public String toString() {
-            StringBuilder builder = new StringBuilder(status);
+        public String toString() {
+            StringBuilder builder = new StringBuilder(String.valueOf(status));
             if (data != null) {
                 builder.append(" / Data: ").append(data);
             }
             return builder.toString();
         }
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ClearChargingProfileTask.java (1)

94-102: Null‑safe, more informative error recording.

Use String.valueOf(error) and e.toString().

-            public void successError(String chargeBoxId, Object error) {
-                addNewError(chargeBoxId, error.toString());
+            public void successError(String chargeBoxId, Object error) {
+                addNewError(chargeBoxId, String.valueOf(error));
             }
@@
-            public void failed(String chargeBoxId, Exception e) {
-                addNewError(chargeBoxId, e.getMessage());
+            public void failed(String chargeBoxId, Exception e) {
+                addNewError(chargeBoxId, e == null ? "null" : e.toString());
             }
steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetConfigurationTask.java (2)

48-53: Avoid NPE when unknownKey list is null.

JAXB may return null for getUnknownKey(). Sanitize before joining.

-                                        return new ResponseWrapper(keyValues, r.getUnknownKey());
+                                        var u = r.getUnknownKey();
+                                        return new ResponseWrapper(keyValues, u == null ? java.util.List.of() : u);
@@
-                                        return new ResponseWrapper(keyValues, r.getUnknownKey());
+                                        var u = r.getUnknownKey();
+                                        return new ResponseWrapper(keyValues, u == null ? java.util.List.of() : u);

Also applies to: 59-64


75-83: Consider preserving unknownKeys as List for callers.

Flattening to a single String loses structure; exposing both List<String> unknownKeys and a derived String unknownKeysJoined keeps flexibility.

Do consumers rely on a String here, or would a List be preferable?

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTask.java (2)

92-97: Use request connectorId for DB write; make error recording null‑safe.

Align stored connectorId with the request; improve error recording.

-                addNewResponse(chargeBoxId, statusValue);
-                if ("Accepted".equalsIgnoreCase(statusValue)) {
-                    repo.setProfile(details.getChargingProfilePk(), chargeBoxId, params.getConnectorId());
-                }
+                addNewResponse(chargeBoxId, statusValue);
+                if ("Accepted".equalsIgnoreCase(statusValue)) {
+                    repo.setProfile(details.getChargingProfilePk(), chargeBoxId, request.getConnectorId());
+                }
@@
-            public void successError(String chargeBoxId, Object error) {
-                addNewError(chargeBoxId, error.toString());
+            public void successError(String chargeBoxId, Object error) {
+                addNewError(chargeBoxId, String.valueOf(error));
             }
@@
-            public void failed(String chargeBoxId, Exception e) {
-                addNewError(chargeBoxId, e.getMessage());
+            public void failed(String chargeBoxId, Exception e) {
+                addNewError(chargeBoxId, e == null ? "null" : e.toString());
             }

Also applies to: 100-106


47-55: Minor: casting to this class inside mapper couples TASK_DEFINITION to impl.

Safe here, but consider passing the request via params or a supplier to avoid (SetChargingProfileTask) task cast.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/CommunicationTask.java (4)

101-106: Make callbacks collection concurrency-safe

ArrayList can throw ConcurrentModificationException if addCallback is called while iterating in success/failed. Prefer CopyOnWriteArrayList.

@@
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
@@
-    private final List<OcppCallback<RESPONSE>> callbackList;
+    private final CopyOnWriteArrayList<OcppCallback<RESPONSE>> callbackList;
@@
-        this.callbackList = new ArrayList<>(2);
+        this.callbackList = new CopyOnWriteArrayList<>();

Also applies to: 72-72, 39-41


164-175: Avoid potential NPE in successError

Use String.valueOf(error) instead of error.toString().

-            public void successError(String chargeBoxId, Object error) {
-                addNewError(chargeBoxId, error.toString());
+            public void successError(String chargeBoxId, Object error) {
+                addNewError(chargeBoxId, String.valueOf(error));

183-191: Address TypeParameterUnusedInFormals warning (minimal or refactor)

The generic T is only in the return type; Error Prone flags this.

Minimal (suppress warning now):

-    @SuppressWarnings("unchecked")
+    @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
     public <T extends RequestType> T getRequest(OcppVersion version) {

Better (follow-up): add a type token to bind T and remove the cast:

-    public <T extends RequestType> T getRequest(OcppVersion version) {
+    public <T extends RequestType> T getRequest(OcppVersion version, Class<T> requestType) {
@@
-        var mapper = (RequestMapper<CommunicationTask<S, RESPONSE>, T>) handler.getRequestMapper();
-        return mapper.map(this);
+        var mapper = handler.getRequestMapper();
+        return requestType.cast(((RequestMapper<CommunicationTask<S, RESPONSE>, ?>) mapper).map(this));

If you choose the refactor, I can help generate call-site changes across tasks.


145-152: Note: iteration safety depends on callbacks list fix

Once callbackList uses CopyOnWriteArrayList, these loops are safe even if callbacks are added concurrently. Otherwise, they risk CME.

Also applies to: 155-162

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75a502f and a284a8c.

📒 Files selected for processing (39)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/CommunicationTask.java (7 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/RequestResult.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/CancelReservationTask.java (2 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ChangeAvailabilityTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ChangeConfigurationTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ClearCacheTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ClearChargingProfileTask.java (2 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/DataTransferTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetCompositeScheduleTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetConfigurationTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetDiagnosticsTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetLocalListVersionTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/RemoteStartTransactionTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/RemoteStopTransactionTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ReserveNowTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ResetTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SendLocalListTask.java (3 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTaskAdhoc.java (0 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTaskFromDB.java (0 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/TriggerMessageTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/UnlockConnectorTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/UpdateFirmwareTask.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/OcppVersionHandler.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/RequestMapper.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/ResponseMapper.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/TaskDefinition.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/repository/dto/ChargePoint.java (1 hunks)
  • steve-core/src/main/java/de/rwth/idsg/steve/service/ChargePointServiceClient.java (2 hunks)
  • steve-jooq/src/main/java/de/rwth/idsg/steve/jooq/mapper/ChargePointMapper.java (2 hunks)
  • steve-jooq/src/main/java/de/rwth/idsg/steve/jooq/mapper/TransactionMapper.java (1 hunks)
  • steve-jooq/src/main/java/de/rwth/idsg/steve/repository/impl/ChargePointRepositoryImpl.java (1 hunks)
  • steve-ocpp-transport-soap/src/main/java/de/rwth/idsg/steve/ocpp/soap/ChargePointServiceSoapInvoker.java (2 hunks)
  • steve-ocpp-transport-websocket/src/main/java/de/rwth/idsg/steve/config/OcppConfiguration.java (1 hunks)
  • steve-ocpp-transport-websocket/src/main/java/de/rwth/idsg/steve/ocpp/ws/data/CommunicationContext.java (1 hunks)
  • steve-ui-jsp/src/main/java/de/rwth/idsg/steve/web/controller/TasksController.java (1 hunks)
  • steve-ui-jsp/src/main/resources/webapp/WEB-INF/views/data-man/00-cp-misc.jsp (2 hunks)
  • steve-ui-jsp/src/main/resources/webapp/WEB-INF/views/data-man/chargepointDetails.jsp (1 hunks)
  • steve/src/main/java/de/rwth/idsg/steve/ocpp/ChargePointServiceInvokerImpl.java (2 hunks)
💤 Files with no reviewable changes (2)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTaskFromDB.java
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SetChargingProfileTaskAdhoc.java
🚧 Files skipped from review as they are similar to previous changes (7)
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/TaskDefinition.java
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/ResponseMapper.java
  • steve-ocpp-transport-soap/src/main/java/de/rwth/idsg/steve/ocpp/soap/ChargePointServiceSoapInvoker.java
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/impl/RequestMapper.java
  • steve-core/src/main/java/de/rwth/idsg/steve/service/ChargePointServiceClient.java
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/TriggerMessageTask.java
  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ReserveNowTask.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-04-05T13:09:02.595Z
Learnt from: juherr
PR: juherr/evolve#1
File: src/main/resources/OCPP-2.0.1_part3_JSON_schemas/ClearCacheRequest.json:21-27
Timestamp: 2025-04-05T13:09:02.595Z
Learning: In the OCPP (Open Charge Point Protocol) standard, the ClearCacheRequest schema is correctly designed with only the optional customData property, as this operation doesn't require additional parameters.

Applied to files:

  • steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ClearCacheTask.java
🪛 GitHub Actions: build and run tests
steve-jooq/src/main/java/de/rwth/idsg/steve/jooq/mapper/TransactionMapper.java

[warning] 58-58: NullAway: returning @nullable expression from method with @nonnull return type

steve-jooq/src/main/java/de/rwth/idsg/steve/repository/impl/ChargePointRepositoryImpl.java

[warning] 61-61: MissingSummary: A summary line is required on public/protected Javadocs.


[warning] 177-177: JavaTimeDefaultTimeZone: LocalDate.now() is not allowed because it silently uses the system default time-zone. You must pass an explicit time-zone (e.g., ZoneId.of("America/Los_Angeles")) to this method.


[warning] 180-180: JavaTimeDefaultTimeZone: LocalDate.now() is not allowed because it silently uses the system default time-zone. You must pass an explicit time-zone (e.g., ZoneId.of("America/Los_Angeles")) to this method.


[warning] 183-183: JavaTimeDefaultTimeZone: LocalDate.now() is not allowed because it silently uses the system default time-zone. You must pass an explicit time-zone (e.g., ZoneId.of("America/Los_Angeles")) to this method.


[warning] 209-209: NullAway: parameter form is @nonnull, but parameter in superclass method de.rwth.idsg.steve.repository.ChargePointRepository.getChargePointConnectorStatus(...) is @nullable

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/CommunicationTask.java

[warning] 135-135: NullAway: dereferenced expression resultMap.get(chargeBoxId) is @nullable


[warning] 184-184: TypeParameterUnusedInFormals: Declaring a type parameter that is only used in the return type is a misuse of generics.

steve-jooq/src/main/java/de/rwth/idsg/steve/jooq/mapper/ChargePointMapper.java

[warning] 52-52: NullAway: passing @nullable parameter 'toInstant(r.getLastHeartbeatTimestamp())' where @nonnull is required

🔇 Additional comments (18)
steve-jooq/src/main/java/de/rwth/idsg/steve/repository/impl/ChargePointRepositoryImpl.java (1)

205-206: LGTM: switching from Optional to nullable for mapper interop

Passing ar.orElse(null) matches the new mapper signature.

steve-ui-jsp/src/main/resources/webapp/WEB-INF/views/data-man/00-cp-misc.jsp (1)

29-31: LGTM: bind Admin Address directly from cp.adminAddress

UI link logic now matches the DTO change.

steve-ocpp-transport-websocket/src/main/java/de/rwth/idsg/steve/ocpp/ws/data/CommunicationContext.java (1)

71-75: Validate handler type contract and document DummyResponse

Found: CommunicationTask#createHandler is declared as public <T extends ResponseType> AsyncHandler<T> createHandler(String) (steve-core/.../CommunicationTask.java:194). I could not locate a definitive handleResponse(...) declaration nor the DummyResponse implementation in the search results. Before removing the TODO in CommunicationContext (steve-ocpp-transport-websocket/.../CommunicationContext.java:71-75):

  • Confirm the exact handler method signature (should be void handleResponse(Response<T> res) if using javax.xml.ws.AsyncHandler<T>) and that DummyResponse implements the matching Response<T> for the same T returned by createHandler(...).
  • If the types don't match, construct/pass a proper Response<T> (or adapt via a typed factory) instead of relying on erasure-based casts.
  • Replace the inline TODO with a one-line comment explaining why DummyResponse is required (what Response type it implements and why a wrapper is needed).
steve/src/main/java/de/rwth/idsg/steve/ocpp/ChargePointServiceInvokerImpl.java (1)

73-209: Method refactors to invoke(...): LGTM.

Consistent SOAP/JSON dispatch via method references reduces duplication and risk of divergence.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ChangeConfigurationTask.java (1)

57-63: Constructors aligned to TASK_DEFINITION: LGTM.

Matches the new architecture and keeps API consistent with other tasks.

steve-ocpp-transport-websocket/src/main/java/de/rwth/idsg/steve/config/OcppConfiguration.java (1)

56-60: Uniform request resolver looks good — getRequest fails fast.
getRequest(OcppVersion) throws UnsupportedOperationException("Operation not supported for version " + version), so task -> task.getRequest(V_X) will fail fast with a clear message.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetCompositeScheduleTask.java (1)

21-44: LGTM! Clean migration to mapper-based architecture.

The refactoring successfully migrates from the legacy Ocpp16AndAboveTask to the new TaskDefinition-based approach. The version handler properly maps request parameters and returns the response unchanged, which is appropriate for this operation.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/RemoteStopTransactionTask.java (1)

29-52: LGTM! Comprehensive version coverage with consistent status extraction.

The TaskDefinition properly handles all three OCPP versions (1.2, 1.5, 1.6) with consistent status extraction pattern. The implementation correctly passes the transaction ID and extracts the status value from each version-specific response.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetLocalListVersionTask.java (1)

29-43: LGTM! Clean implementation with proper string conversion.

The task definition correctly handles both OCPP 1.5 and 1.6 versions. The use of String.valueOf() for the list version ensures proper null handling and consistent string representation.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/SendLocalListTask.java (1)

93-101: Confirm FULL update default when sendEmptyListWhenFull is null

Boolean.FALSE.equals(...) treats null as “send empty list.” Is that intended for backward compatibility? If not, default to false explicitly.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ResetTask.java (1)

29-58: LGTM: clean, versioned mapping

Consistent TaskDefinition across V_12/V_15/V_16; status extraction is uniform. Constructors look good.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/DataTransferTask.java (1)

47-67: LGTM: versioned request/response mapping is consistent

Clear per-version constructors and uniform ResponseWrapper creation.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/ClearCacheTask.java (1)

29-49: LGTM: straightforward, uniform ClearCache mapping across versions

Simple request creation and status mapping; constructors wired correctly.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/RemoteStartTransactionTask.java (2)

29-55: Confirm omission of optional ChargingProfile (OCPP 1.6)

If RemoteStartTransactionParams supports a charging profile, it’s not mapped here. Please confirm it’s intentional or add mapping per version.


29-58: LGTM: base mapping and status extraction look correct

IdTag/connectorId mapping matches 1.x schemas across versions.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/task/GetDiagnosticsTask.java (1)

31-69: Mapper wiring looks clean and version-accurate.

Good consolidation into a single TaskDefinition with filename mapping per version.

steve-core/src/main/java/de/rwth/idsg/steve/ocpp/CommunicationTask.java (2)

57-58: Good: decoupling via TaskDefinition

Injecting TaskDefinition<S, RESPONSE> is a solid step to centralize version mapping and reduce subclass surface.


94-99: No change required — project targets Java 21; HashMap.newHashMap is allowed.

pom.xml sets <java.version>21 and the maven-compiler-plugin uses ${java.version}>, so HashMap.newHashMap(int) is supported; keep as-is.

@juherr juherr marked this pull request as ready for review September 15, 2025 18:52
@juherr juherr merged commit 6e272c2 into main Sep 15, 2025
69 of 70 checks passed
@juherr juherr deleted the refactor-task-management branch September 15, 2025 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant