Skip to content
Open
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 @@ -106,6 +106,12 @@ public class AuthorizationServerConfig {
@Value("${espi.authorization-server.client-secret:datacustodian-secret}")
private String clientSecret;

@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri:}")
private String jwtIssuerUri;

@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri:}")
private String jwtJwkSetUri;


/**
* OAuth2 Authorization Server Security Filter Chain
Expand All @@ -125,7 +131,11 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
.requestMatchers("/assets/**", "/webjars/**", "/login").permitAll())
.formLogin(Customizer.withDefaults())
.oauth2AuthorizationServer(authorizationServer ->
authorizationServer.oidc(Customizer.withDefaults()) // Enable OpenID Connect 1.0
{
if (isJwtResourceServerConfigured()) {
authorizationServer.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
}
}
)
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.csrf(Customizer.withDefaults())
Expand All @@ -137,11 +147,13 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
)
)
// Accept access tokens for User Info and/or Client Registration
.oauth2ResourceServer(resourceServer -> resourceServer
.opaqueToken(Customizer.withDefaults())

//.jwt(Customizer.withDefaults())
)
.oauth2ResourceServer(resourceServer -> {
if (isJwtResourceServerConfigured()) {
resourceServer.jwt(Customizer.withDefaults());
} else {
resourceServer.opaqueToken(Customizer.withDefaults());
Copy link
Contributor

Choose a reason for hiding this comment

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

Blocking: Opaque Token Fallback Will Fail at Runtime

When JWT properties are not set, this falls back to opaqueToken(Customizer.withDefaults()), but no OpaqueTokenIntrospector bean is defined. The introspectionUri, clientId, and clientSecret fields are injected via @Value but never wired into any bean. This will cause a startup failure.

Suggested fix — wire the introspector:

resourceServer.opaqueToken(opaque -> opaque
    .introspectionUri(introspectionUri)
    .introspectionClientCredentials(clientId, clientSecret));

}
})
// HTTPS Channel Security for Production
//should be able to use property server.ssl.enabled=true
//todo - test this
Expand Down Expand Up @@ -172,6 +184,11 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
return http.build();
}

private boolean isJwtResourceServerConfigured() {
return (jwtIssuerUri != null && !jwtIssuerUri.isBlank())
|| (jwtJwkSetUri != null && !jwtJwkSetUri.isBlank());
}

/**
* Default Security Filter Chain for non-OAuth2 endpoints
* <p>
Expand Down Expand Up @@ -406,4 +423,4 @@ private static KeyPair generateRsaKey() {
}
return keyPair;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@

/**
* MapStruct mapper for converting between ElectricPowerQualitySummaryEntity and ElectricPowerQualitySummaryDto.
*
* <p>
* Maps only ElectricPowerQualitySummary fields. IdentifiedObject fields are NOT part of the usage.xsd
* definition and are handled by AtomFeedDto/AtomEntryDto.
*
* <p>
* Handles the conversion between the JPA entity used for persistence and the DTO
* used for JAXB XML marshalling in the Green Button API.
*/
Expand All @@ -59,6 +59,14 @@ public interface ElectricPowerQualitySummaryMapper {
* @param dto the electric power quality summary DTO
* @return the electric power quality summary entity
*/
@Mapping(target = "updated", ignore = true)
@Mapping(target = "upLink", ignore = true)
@Mapping(target = "selfLink", ignore = true)
@Mapping(target = "relatedLinks", ignore = true)
@Mapping(target = "relatedLinkHrefs", ignore = true)
@Mapping(target = "published", ignore = true)
@Mapping(target = "description", ignore = true)
@Mapping(target = "created", ignore = true)
@Mapping(target = "id", ignore = true) // IdentifiedObject field handled by Atom layer
@Mapping(target = "usagePoint", ignore = true) // Relationships handled separately
@Mapping(target = "upResource", ignore = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public interface LineItemMapper {
* @param dto the line item DTO
* @return the line item entity
*/
@Mapping(target = "dateTimeFromLocalDateTime", ignore = true)
@Mapping(target = "dateTimeFromInstant", ignore = true)
@Mapping(target = "id", ignore = true)
@Mapping(target = "usageSummary", ignore = true)
LineItemEntity toEntity(LineItemDto dto);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.UUID;

Expand All @@ -35,7 +34,6 @@
* Removed queries: findByCustomerName, findByKind, findByPucNumber, findVipCustomers,
* findCustomersWithSpecialNeeds, findByLocale, findByPriorityRange, findByOrganisationName
*/
@Repository
public interface CustomerRepository extends JpaRepository<CustomerEntity, UUID> {
// Only default JpaRepository methods are supported (findById, findAll, save, delete, etc.)
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,16 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.UUID;

@Repository
public interface IntervalBlockRepository extends JpaRepository<IntervalBlockEntity, UUID> {

@Query("SELECT i.id FROM IntervalBlockEntity i")
List<UUID> findAllIds();

@Query("SELECT i FROM IntervalBlockEntity i WHERE i.meterReading.id = :meterReadingId")
List<IntervalBlockEntity> findAllByMeterReadingId(@Param("meterReadingId") UUID meterReadingId);
List<IntervalBlockEntity> findAllByMeterReadingId(UUID meterReadingId);

@Query("SELECT i.id FROM IntervalBlockEntity i WHERE i.meterReading.usagePoint.id = :usagePointId")
List<UUID> findAllIdsByUsagePointId(@Param("usagePointId") UUID usagePointId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
package org.greenbuttonalliance.espi.common.service;

import org.greenbuttonalliance.espi.common.domain.usage.ApplicationInformationEntity;
import org.greenbuttonalliance.espi.common.dto.usage.ApplicationInformationDto;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.UUID;

public interface ApplicationInformationService {

Expand Down Expand Up @@ -53,4 +56,17 @@ public ApplicationInformationEntity findByDataCustodianClientId(
*/
public ApplicationInformationEntity importResource(InputStream stream);

List<ApplicationInformationEntity> findAll();

ApplicationInformationEntity findById(UUID id);

ApplicationInformationEntity save(ApplicationInformationEntity entity);

void deleteById(UUID id);

void export(List<ApplicationInformationEntity> entities, OutputStream outputStream);

void export(ApplicationInformationEntity entity, OutputStream outputStream);

ApplicationInformationEntity fromDto(ApplicationInformationDto dto);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

/**
* Service for generating NAESB ESPI compliant UUID type 5 identifiers.
*
* <p>
* This service generates deterministic UUID5 identifiers based on href URLs
* to ensure ESPI compliance and consistency across the system.
*/
Expand All @@ -45,7 +45,7 @@ public class EspiIdGeneratorService {

/**
* Generates a NAESB ESPI compliant UUID5 based on the provided href URL.
*
* <p>
* UUID5 uses SHA-1 hashing to create deterministic identifiers, ensuring
* that the same href will always generate the same UUID.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
*
* Copyright (c) 2025 Green Button Alliance, Inc.
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*
*/

package org.greenbuttonalliance.espi.common.service.impl;

import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import org.greenbuttonalliance.espi.common.service.BaseExportService;
import org.springframework.stereotype.Service;

import java.util.Set;

/**
* Export service for ESPI ApplicationInformation resource.
* <p>
* This service handles XML marshalling for the ApplicationInformation resource defined in espi.xsd.
* <p>
* Namespace configuration:
* - Atom namespace (http://www.w3.org/2005/Atom) - default namespace
* - ESPI namespace (http://naesb.org/espi) - with "espi:" prefix
*/
@Service("applicationInformationExportService")
public class ApplicationInformationExportService extends BaseExportService {

private static final String ATOM_NAMESPACE = "http://www.w3.org/2005/Atom";
private static final String ESPI_NAMESPACE = "http://naesb.org/espi";

/**
* Creates JAXBContext with Atom + ApplicationInformation domain classes.
*
* @return JAXBContext configured for ApplicationInformation resource
* @throws JAXBException if context creation fails
*/
@Override
protected JAXBContext createJAXBContext() throws JAXBException {
return JAXBContext.newInstance(
// Atom protocol classes
org.greenbuttonalliance.espi.common.dto.atom.AtomFeedDto.class,
org.greenbuttonalliance.espi.common.dto.atom.LinkDto.class,
org.greenbuttonalliance.espi.common.dto.atom.UsageAtomEntryDto.class,

// ApplicationInformation resource class (http://naesb.org/espi)
org.greenbuttonalliance.espi.common.dto.usage.ApplicationInformationDto.class
);
}

/**
* Returns the 2 namespaces for ApplicationInformation domain.
*
* @return set containing Atom and ESPI namespaces
*/
@Override
protected Set<String> getDomainNamespaces() {
return Set.of(ATOM_NAMESPACE, ESPI_NAMESPACE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
import org.springframework.util.Assert;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Slf4j
@Service
Expand All @@ -44,6 +47,7 @@ public class ApplicationInformationServiceImpl implements

private final ApplicationInformationRepository applicationInformationRepository;
private final ApplicationInformationMapper applicationInformationMapper;
private final ApplicationInformationExportService applicationInformationExportService;

@Override
public ApplicationInformationEntity findByClientId(String clientId) {
Expand All @@ -66,10 +70,8 @@ public ApplicationInformationEntity findByDataCustodianClientId(
String dataCustodianClientId) {
Assert.notNull(dataCustodianClientId, "dataCustodianClientId is required");

// TODO: Add repository method findByDataCustodianClientId if needed
log.info("Finding ApplicationInformation by dataCustodianClientId: " + dataCustodianClientId);

return null;
return applicationInformationRepository.findByDataCustodianId(dataCustodianClientId).orElse(null);
}

@Override
Expand All @@ -91,4 +93,44 @@ public ApplicationInformationEntity importResource(InputStream stream) {
return null;
}
}

@Override
@Transactional(readOnly = true)
public List<ApplicationInformationEntity> findAll() {
return applicationInformationRepository.findAll();
}

@Override
@Transactional(readOnly = true)
public ApplicationInformationEntity findById(UUID id) {
return applicationInformationRepository.findById(id).orElse(null);
}

@Override
public ApplicationInformationEntity save(ApplicationInformationEntity entity) {
return applicationInformationRepository.save(entity);
}

@Override
public void deleteById(UUID id) {
applicationInformationRepository.deleteById(id);
}

@Override
public void export(List<ApplicationInformationEntity> entities, OutputStream outputStream) {
List<ApplicationInformationDto> dtos = entities.stream()
.map(applicationInformationMapper::toDto)
.toList();
applicationInformationExportService.exportDto(dtos, outputStream);
Copy link
Contributor

Choose a reason for hiding this comment

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

Blocking: Batch Export Will Throw JAXBException at Runtime

This passes a raw List<ApplicationInformationDto> to exportDto(). JAXB Marshaller.marshal() cannot serialize a plain java.util.List — it requires an @XmlRootElement-annotated wrapper.

Suggested fix — either wrap in an AtomFeedDto:

AtomFeedDto feed = new AtomFeedDto();
// populate feed with entries from dtos
applicationInformationExportService.exportDto(feed, outputStream);

Or export entities individually:

for (ApplicationInformationEntity entity : entities) {
    export(entity, outputStream);
}

}

@Override
public void export(ApplicationInformationEntity entity, OutputStream outputStream) {
applicationInformationExportService.exportDto(applicationInformationMapper.toDto(entity), outputStream);
}

@Override
public ApplicationInformationEntity fromDto(ApplicationInformationDto dto) {
return applicationInformationMapper.toEntity(dto);
}
}
Loading
Loading