Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.greenbuttonalliance.espi.common.domain.usage.ApplicationInformationEntity;
import org.greenbuttonalliance.espi.common.service.ApplicationInformationService;
import org.greenbuttonalliance.espi.common.service.NotificationService;
import org.greenbuttonalliance.espi.common.uri.EspiBatchUri;
import org.springframework.core.env.Environment;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
Expand All @@ -31,14 +32,20 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

/**
* Custodian "Notify Third Party" page (#177). Lets an admin compose and send an ESPI
* {@code BatchList} (Atom) of resource URLs — ApplicationInformation, Authorization feed,
* Authorization entry, Subscription — to a Third Party notification endpoint, reusing the #158
* notification contract via {@link NotificationService#notifyBatchList}.
* Custodian "Notify Third Party" page (#177, #181). The admin composes an ESPI {@code BatchList}
* (Atom) and POSTs it to a Third Party notification endpoint, reusing the #158 notification contract
* via {@link NotificationService#notifyBatchList}.
*
* <p>The admin <strong>selects which and how many</strong> of the available resource URLs to include
* (each has an "include" checkbox and an editable URL). The available set covers ApplicationInformation,
* the Authorization feed, an Authorization entry, and the two subscription formats — an <em>energy</em>
* subscription ({@code …/Batch/Subscription/{id}}) and a <em>PII/customer</em> subscription
* ({@code …/Batch/RetailCustomer/{id}}), built via {@link EspiBatchUri}. Only checked, non-blank URLs
* are marshalled into the BatchList.</p>
*/
@Controller
@PreAuthorize("hasRole('ROLE_CUSTODIAN')")
Expand Down Expand Up @@ -77,13 +84,14 @@ public String form(Model model) {

@PostMapping("/custodian/notifications/send")
public String send(@ModelAttribute("notifyForm") NotifyForm form, RedirectAttributes redirectAttributes) {
List<String> resources = Stream.of(
form.getApplicationInformationUrl(),
form.getAuthorizationFeedUrl(),
form.getAuthorizationEntryUrl(),
form.getSubscriptionUrl())
.filter(u -> u != null && !u.isBlank())
.toList();
// Only the checked, non-blank URLs go into the BatchList — the admin chooses which/how many.
List<String> resources = new ArrayList<>();
addIf(resources, form.isIncludeApplicationInformation(), form.getApplicationInformationUrl());
addIf(resources, form.isIncludeAuthorizationFeed(), form.getAuthorizationFeedUrl());
addIf(resources, form.isIncludeAuthorizationEntry(), form.getAuthorizationEntryUrl());
addIf(resources, form.isIncludeEnergySubscription(), form.getEnergySubscriptionUrl());
addIf(resources, form.isIncludePiiSubscription(), form.getPiiSubscriptionUrl());

try {
notificationService.notifyBatchList(form.getNotificationUri(), resources);
redirectAttributes.addFlashAttribute("message",
Expand All @@ -94,11 +102,17 @@ public String send(@ModelAttribute("notifyForm") NotifyForm form, RedirectAttrib
redirectAttributes.addFlashAttribute("message", "Failed to send BatchList: " + e.getMessage());
redirectAttributes.addFlashAttribute("messageType", "danger");
}
// Preserve what the admin typed so they can correct and resend.
// Preserve what the admin selected/typed so they can correct and resend.
redirectAttributes.addFlashAttribute("notifyForm", form);
return "redirect:/custodian/notifications";
}

private static void addIf(List<String> resources, boolean include, String url) {
if (include && url != null && !url.isBlank()) {
resources.add(url.trim());
}
}

private NotifyForm defaultForm(List<ApplicationInformationEntity> apps) {
String base = environment.getProperty("espi.datacustodian.base-url",
"http://localhost:8081/DataCustodian");
Expand All @@ -112,27 +126,53 @@ private NotifyForm defaultForm(List<ApplicationInformationEntity> apps) {
f.setApplicationInformationUrl(resourceBase + "/ApplicationInformation/{applicationInformationId}");
f.setAuthorizationFeedUrl(resourceBase + "/Authorization");
f.setAuthorizationEntryUrl(resourceBase + "/Authorization/{authorizationId}");
f.setSubscriptionUrl(resourceBase + "/Subscription/{subscriptionId}");
// Two distinct ESPI subscription formats, built from the canonical URI builder (#160).
f.setEnergySubscriptionUrl(EspiBatchUri.batchSubscription(resourceBase, "{subscriptionId}"));
f.setPiiSubscriptionUrl(EspiBatchUri.batchRetailCustomer(resourceBase, "{retailCustomerId}"));
return f;
}

/** Backing form for the notify page. */
/** Backing form: each candidate resource has an include flag + an editable URL. */
public static class NotifyForm {
private String notificationUri;

private boolean includeApplicationInformation = true;
private String applicationInformationUrl;
private boolean includeAuthorizationFeed = true;
private String authorizationFeedUrl;
private boolean includeAuthorizationEntry = true;
private String authorizationEntryUrl;
private String subscriptionUrl;
private boolean includeEnergySubscription = true;
private String energySubscriptionUrl;
private boolean includePiiSubscription = true;
private String piiSubscriptionUrl;

public String getNotificationUri() { return notificationUri; }
public void setNotificationUri(String notificationUri) { this.notificationUri = notificationUri; }
public void setNotificationUri(String v) { this.notificationUri = v; }

public boolean isIncludeApplicationInformation() { return includeApplicationInformation; }
public void setIncludeApplicationInformation(boolean v) { this.includeApplicationInformation = v; }
public String getApplicationInformationUrl() { return applicationInformationUrl; }
public void setApplicationInformationUrl(String v) { this.applicationInformationUrl = v; }

public boolean isIncludeAuthorizationFeed() { return includeAuthorizationFeed; }
public void setIncludeAuthorizationFeed(boolean v) { this.includeAuthorizationFeed = v; }
public String getAuthorizationFeedUrl() { return authorizationFeedUrl; }
public void setAuthorizationFeedUrl(String v) { this.authorizationFeedUrl = v; }

public boolean isIncludeAuthorizationEntry() { return includeAuthorizationEntry; }
public void setIncludeAuthorizationEntry(boolean v) { this.includeAuthorizationEntry = v; }
public String getAuthorizationEntryUrl() { return authorizationEntryUrl; }
public void setAuthorizationEntryUrl(String v) { this.authorizationEntryUrl = v; }
public String getSubscriptionUrl() { return subscriptionUrl; }
public void setSubscriptionUrl(String v) { this.subscriptionUrl = v; }

public boolean isIncludeEnergySubscription() { return includeEnergySubscription; }
public void setIncludeEnergySubscription(boolean v) { this.includeEnergySubscription = v; }
public String getEnergySubscriptionUrl() { return energySubscriptionUrl; }
public void setEnergySubscriptionUrl(String v) { this.energySubscriptionUrl = v; }

public boolean isIncludePiiSubscription() { return includePiiSubscription; }
public void setIncludePiiSubscription(boolean v) { this.includePiiSubscription = v; }
public String getPiiSubscriptionUrl() { return piiSubscriptionUrl; }
public void setPiiSubscriptionUrl(String v) { this.piiSubscriptionUrl = v; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
<div class="container">
<h2 class="mt-4">Notify Third Party</h2>
<p class="text-muted">
Compose an ESPI <code>BatchList</code> of resource URLs and POST it to a Third Party's
notification endpoint. Leave a field blank to omit that resource.
Choose which resource URLs to include, then POST them as an ESPI <code>BatchList</code> to a
Third Party's notification endpoint. Only checked rows with a non-blank URL are sent.
</p>

<div th:if="${message}" class="alert"
Expand All @@ -28,31 +28,75 @@ <h2 class="mt-4">Notify Third Party</h2>
<datalist id="notifyUriOptions">
<option th:each="uri : ${notifyUris}" th:value="${uri}"></option>
</datalist>
<div class="form-text">Registered third-party endpoints are suggested; you may edit.</div>
</div>

<hr>
<h6 class="text-muted">BatchList resources</h6>
<h6 class="text-muted">BatchList resources — select which to send</h6>

<div class="mb-3">
<label for="applicationInformationUrl" class="form-label">ApplicationInformation URL</label>
<input type="text" id="applicationInformationUrl" class="form-control"
th:field="*{applicationInformationUrl}"/>
<div class="row g-2 align-items-center mb-2">
<div class="col-auto form-check">
<input type="checkbox" id="includeApplicationInformation" class="form-check-input"
th:field="*{includeApplicationInformation}"/>
</div>
<label for="applicationInformationUrl" class="col-sm-3 col-form-label">ApplicationInformation</label>
<div class="col">
<input type="text" id="applicationInformationUrl" class="form-control"
th:field="*{applicationInformationUrl}"/>
</div>
</div>
<div class="mb-3">
<label for="authorizationFeedUrl" class="form-label">Authorization feed URL</label>
<input type="text" id="authorizationFeedUrl" class="form-control"
th:field="*{authorizationFeedUrl}"/>

<div class="row g-2 align-items-center mb-2">
<div class="col-auto form-check">
<input type="checkbox" id="includeAuthorizationFeed" class="form-check-input"
th:field="*{includeAuthorizationFeed}"/>
</div>
<label for="authorizationFeedUrl" class="col-sm-3 col-form-label">Authorization feed</label>
<div class="col">
<input type="text" id="authorizationFeedUrl" class="form-control"
th:field="*{authorizationFeedUrl}"/>
</div>
</div>
<div class="mb-3">
<label for="authorizationEntryUrl" class="form-label">Authorization entry URL</label>
<input type="text" id="authorizationEntryUrl" class="form-control"
th:field="*{authorizationEntryUrl}"/>

<div class="row g-2 align-items-center mb-2">
<div class="col-auto form-check">
<input type="checkbox" id="includeAuthorizationEntry" class="form-check-input"
th:field="*{includeAuthorizationEntry}"/>
</div>
<label for="authorizationEntryUrl" class="col-sm-3 col-form-label">Authorization entry</label>
<div class="col">
<input type="text" id="authorizationEntryUrl" class="form-control"
th:field="*{authorizationEntryUrl}"/>
</div>
</div>
<div class="mb-3">
<label for="subscriptionUrl" class="form-label">Subscription URL</label>
<input type="text" id="subscriptionUrl" class="form-control"
th:field="*{subscriptionUrl}"/>

<div class="row g-2 align-items-center mb-2">
<div class="col-auto form-check">
<input type="checkbox" id="includeEnergySubscription" class="form-check-input"
th:field="*{includeEnergySubscription}"/>
</div>
<label for="energySubscriptionUrl" class="col-sm-3 col-form-label">
Subscription — energy
<span class="d-block text-muted small">/Batch/Subscription/{id}</span>
</label>
<div class="col">
<input type="text" id="energySubscriptionUrl" class="form-control"
th:field="*{energySubscriptionUrl}"/>
</div>
</div>

<div class="row g-2 align-items-center mb-3">
<div class="col-auto form-check">
<input type="checkbox" id="includePiiSubscription" class="form-check-input"
th:field="*{includePiiSubscription}"/>
</div>
<label for="piiSubscriptionUrl" class="col-sm-3 col-form-label">
Subscription — PII / customer
<span class="d-block text-muted small">/Batch/RetailCustomer/{id}</span>
</label>
<div class="col">
<input type="text" id="piiSubscriptionUrl" class="form-control"
th:field="*{piiSubscriptionUrl}"/>
</div>
</div>

<div class="d-flex gap-2">
Expand Down
Loading