1+ /*
2+ *
3+ * Copyright (c) 2018-2025 Green Button Alliance, Inc.
4+ *
5+ * Portions (c) 2013-2018 EnergyOS.org
6+ *
7+ * Licensed under the Apache License, Version 2.0 (the "License");
8+ * you may not use this file except in compliance with the License.
9+ * You may obtain a copy of the License at
10+ *
11+ * http://www.apache.org/licenses/LICENSE-2.0
12+ *
13+ * Unless required by applicable law or agreed to in writing, software
14+ * distributed under the License is distributed on an "AS IS" BASIS,
15+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+ * See the License for the specific language governing permissions and
17+ * limitations under the License.
18+ *
19+ */
20+
21+ package org .greenbuttonalliance .espi .common .service .export ;
22+
23+ import org .greenbuttonalliance .espi .common .dto .atom .AtomEntryDto ;
24+ import org .greenbuttonalliance .espi .common .dto .atom .AtomFeedDto ;
25+ import org .greenbuttonalliance .espi .common .dto .atom .AtomContentDto ;
26+ import org .greenbuttonalliance .espi .common .dto .atom .LinkDto ;
27+ import org .greenbuttonalliance .espi .common .mapper .GreenButtonMapper ;
28+ import org .springframework .beans .factory .annotation .Autowired ;
29+ import org .springframework .beans .factory .annotation .Qualifier ;
30+ import org .springframework .oxm .jaxb .Jaxb2Marshaller ;
31+ import org .springframework .stereotype .Service ;
32+
33+ import javax .xml .transform .stream .StreamResult ;
34+ import java .io .OutputStream ;
35+ import java .time .OffsetDateTime ;
36+ import java .util .List ;
37+ import java .util .UUID ;
38+ import java .util .function .Function ;
39+ import java .util .stream .Collectors ;
40+
41+ /**
42+ * Modern generic export service for Green Button resources.
43+ *
44+ * Replaces the legacy ExportServiceImpl with 40+ specific methods with a clean,
45+ * generic approach using MapStruct for Entity-DTO conversion and modern Java patterns.
46+ *
47+ * This service handles the complete export pipeline:
48+ * 1. Entity → DTO conversion via MapStruct
49+ * 2. DTO → Atom Entry/Feed wrapping
50+ * 3. JAXB marshalling to XML output stream
51+ */
52+ @ Service
53+ public class AtomExportService {
54+
55+ private final GreenButtonMapper mapper ;
56+ private final Jaxb2Marshaller marshaller ;
57+
58+ @ Autowired
59+ public AtomExportService (
60+ GreenButtonMapper mapper ,
61+ @ Qualifier ("fragmentMarshaller" ) Jaxb2Marshaller marshaller
62+ ) {
63+ this .mapper = mapper ;
64+ this .marshaller = marshaller ;
65+ }
66+
67+ /**
68+ * Exports a single resource as an Atom entry.
69+ *
70+ * Generic method that replaces 20+ specific export methods from legacy ExportServiceImpl.
71+ * Uses MapStruct to convert Entity to DTO, then wraps in Atom entry format.
72+ *
73+ * @param <E> the entity type
74+ * @param <D> the DTO type
75+ * @param entity the entity to export
76+ * @param mapperFunction function to convert entity to DTO using MapStruct
77+ * @param resourceName the resource name for Atom entry
78+ * @param selfHref the self link href
79+ * @param stream the output stream to write XML
80+ */
81+ public <E , D > void exportResource (
82+ E entity ,
83+ Function <E , D > mapperFunction ,
84+ String resourceName ,
85+ String selfHref ,
86+ OutputStream stream
87+ ) {
88+ if (entity == null ) {
89+ throw new IllegalArgumentException ("Entity cannot be null" );
90+ }
91+
92+ // Convert Entity to DTO using MapStruct
93+ D dto = mapperFunction .apply (entity );
94+
95+ // Wrap DTO in Atom entry
96+ AtomEntryDto entry = createAtomEntry (dto , resourceName , selfHref );
97+
98+ // Marshal to XML
99+ marshaller .marshal (entry , new StreamResult (stream ));
100+ }
101+
102+ /**
103+ * Gets the GreenButtonMapper for external access.
104+ *
105+ * @return the mapper instance
106+ */
107+ public GreenButtonMapper getMapper () {
108+ return mapper ;
109+ }
110+
111+ /**
112+ * Exports a collection of resources as an Atom feed.
113+ *
114+ * Generic method that replaces 20+ specific collection export methods.
115+ * Converts each entity to DTO, wraps in Atom entries, and creates an Atom feed.
116+ *
117+ * @param <E> the entity type
118+ * @param <D> the DTO type
119+ * @param entities the entities to export
120+ * @param mapperFunction function to convert entity to DTO using MapStruct
121+ * @param resourceName the resource name for feed
122+ * @param feedTitle the feed title
123+ * @param feedId the feed ID
124+ * @param selfHref the feed self link
125+ * @param hrefGenerator function to generate href for each entity
126+ * @param stream the output stream to write XML
127+ */
128+ public <E , D > void exportResourceCollection (
129+ List <E > entities ,
130+ Function <E , D > mapperFunction ,
131+ String resourceName ,
132+ String feedTitle ,
133+ String feedId ,
134+ String selfHref ,
135+ Function <E , String > hrefGenerator ,
136+ OutputStream stream
137+ ) {
138+ if (entities == null ) {
139+ throw new IllegalArgumentException ("Entities list cannot be null" );
140+ }
141+
142+ // Convert entities to DTOs and wrap in Atom entries
143+ List <AtomEntryDto > entries = entities .stream ()
144+ .map (entity -> {
145+ D dto = mapperFunction .apply (entity );
146+ String entryHref = hrefGenerator .apply (entity );
147+ return createAtomEntry (dto , resourceName , entryHref );
148+ })
149+ .collect (Collectors .toList ());
150+
151+ // Create Atom feed
152+ AtomFeedDto feed = createAtomFeed (
153+ feedTitle ,
154+ feedId ,
155+ selfHref ,
156+ entries
157+ );
158+
159+ // Marshal to XML
160+ marshaller .marshal (feed , new StreamResult (stream ));
161+ }
162+
163+ /**
164+ * Convenience method for exporting UsagePoint entities.
165+ *
166+ * @param entity the usage point entity
167+ * @param stream the output stream
168+ */
169+ public void exportUsagePoint (Object entity , OutputStream stream ) {
170+ // TODO: Implement with proper type-safe casting when entity classes are fixed
171+ throw new UnsupportedOperationException ("exportUsagePoint not yet implemented - requires entity class fixes" );
172+ }
173+
174+ public void exportMeterReading (Object entity , OutputStream stream ) {
175+ // TODO: Implement with proper type-safe casting when entity classes are fixed
176+ throw new UnsupportedOperationException ("exportMeterReading not yet implemented - requires entity class fixes" );
177+ }
178+
179+ public void exportIntervalBlock (Object entity , OutputStream stream ) {
180+ // TODO: Implement with proper type-safe casting when entity classes are fixed
181+ throw new UnsupportedOperationException ("exportIntervalBlock not yet implemented - requires entity class fixes" );
182+ }
183+
184+ public void exportUsageSummary (Object entity , OutputStream stream ) {
185+ // TODO: Implement with proper type-safe casting when entity classes are fixed
186+ throw new UnsupportedOperationException ("exportUsageSummary not yet implemented - requires entity class fixes" );
187+ }
188+
189+ public void exportElectricPowerQualitySummary (Object entity , OutputStream stream ) {
190+ // TODO: Implement with proper type-safe casting when entity classes are fixed
191+ throw new UnsupportedOperationException ("exportElectricPowerQualitySummary not yet implemented - requires entity class fixes" );
192+ }
193+
194+ public void exportCustomer (Object entity , OutputStream stream ) {
195+ // TODO: Implement with proper type-safe casting when entity classes are fixed
196+ throw new UnsupportedOperationException ("exportCustomer not yet implemented - requires entity class fixes" );
197+ }
198+
199+ /**
200+ * Creates an Atom entry wrapping a DTO resource.
201+ *
202+ * @param dto the DTO resource
203+ * @param resourceName the resource name
204+ * @param selfHref the self link href
205+ * @return the Atom entry
206+ */
207+ private AtomEntryDto createAtomEntry (Object dto , String resourceName , String selfHref ) {
208+ String entryId = generateEntryId (resourceName );
209+ String title = generateEntryTitle (resourceName );
210+
211+ AtomContentDto content = new AtomContentDto ("application/xml" , dto );
212+
213+ // Create self link
214+ LinkDto selfLink = new LinkDto (
215+ "self" ,
216+ selfHref ,
217+ "application/atom+xml"
218+ );
219+
220+ return new AtomEntryDto (
221+ entryId ,
222+ title ,
223+ OffsetDateTime .now (),
224+ OffsetDateTime .now (),
225+ List .of (selfLink ),
226+ content
227+ );
228+ }
229+
230+ /**
231+ * Creates an Atom feed containing multiple entries.
232+ *
233+ * @param title the feed title
234+ * @param feedId the feed ID
235+ * @param selfHref the feed self link
236+ * @param entries the entries to include
237+ * @return the Atom feed
238+ */
239+ private AtomFeedDto createAtomFeed (
240+ String title ,
241+ String feedId ,
242+ String selfHref ,
243+ List <AtomEntryDto > entries
244+ ) {
245+ // Create self link for feed
246+ LinkDto selfLink = new LinkDto (
247+ "self" ,
248+ selfHref ,
249+ "application/atom+xml"
250+ );
251+
252+ return new AtomFeedDto (
253+ feedId ,
254+ title ,
255+ OffsetDateTime .now (),
256+ OffsetDateTime .now (),
257+ List .of (selfLink ),
258+ entries
259+ );
260+ }
261+
262+ /**
263+ * Generates a unique entry ID for Atom entries.
264+ *
265+ * @param resourceName the resource name
266+ * @return unique entry ID
267+ */
268+ private String generateEntryId (String resourceName ) {
269+ return "urn:uuid:" + resourceName .toLowerCase () + "-" + UUID .randomUUID ();
270+ }
271+
272+ /**
273+ * Generates a human-readable entry title.
274+ *
275+ * @param resourceName the resource name
276+ * @return entry title
277+ */
278+ private String generateEntryTitle (String resourceName ) {
279+ return resourceName + " Entry" ;
280+ }
281+
282+ // Href generation methods (would be extracted to a separate service in production)
283+
284+ private String generateUsagePointHref (Object entity ) {
285+ // Extract UUID from entity and generate href
286+ return "/espi/1_1/resource/UsagePoint/" + extractUuid (entity );
287+ }
288+
289+ private String generateMeterReadingHref (Object entity ) {
290+ return "/espi/1_1/resource/MeterReading/" + extractUuid (entity );
291+ }
292+
293+ private String generateIntervalBlockHref (Object entity ) {
294+ return "/espi/1_1/resource/IntervalBlock/" + extractUuid (entity );
295+ }
296+
297+ private String generateUsageSummaryHref (Object entity ) {
298+ return "/espi/1_1/resource/UsageSummary/" + extractUuid (entity );
299+ }
300+
301+ private String generateElectricPowerQualitySummaryHref (Object entity ) {
302+ return "/espi/1_1/resource/ElectricPowerQualitySummary/" + extractUuid (entity );
303+ }
304+
305+ private String generateCustomerHref (Object entity ) {
306+ return "/espi/1_1/resource/Customer/" + extractUuid (entity );
307+ }
308+
309+ /**
310+ * Extracts UUID from entity using reflection (fallback method).
311+ * In production, this would use proper entity interfaces.
312+ *
313+ * @param entity the entity
314+ * @return the UUID string
315+ */
316+ private String extractUuid (Object entity ) {
317+ try {
318+ var method = entity .getClass ().getMethod ("getUuid" );
319+ Object uuid = method .invoke (entity );
320+ return uuid != null ? uuid .toString () : "unknown" ;
321+ } catch (Exception e ) {
322+ return "unknown-" + System .currentTimeMillis ();
323+ }
324+ }
325+ }
0 commit comments