Skip to content

Commit 000807f

Browse files
authored
Create ElpriserAPI.java
1 parent c4c8b5f commit 000807f

File tree

1 file changed

+320
-0
lines changed

1 file changed

+320
-0
lines changed
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
package com.example.api;
2+
3+
import java.io.IOException;
4+
import java.net.URI;
5+
import java.net.http.HttpClient;
6+
import java.net.http.HttpRequest;
7+
import java.net.http.HttpResponse;
8+
import java.time.LocalDate;
9+
import java.time.ZonedDateTime;
10+
import java.time.format.DateTimeFormatter;
11+
import java.util.ArrayList;
12+
import java.util.Collections;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.concurrent.ConcurrentHashMap;
16+
import java.util.function.Supplier;
17+
18+
/**
19+
* Ett enkelt API för att hämta elpriser från elprisetjustnu.se.
20+
* Klassen använder endast standardbibliotek från Java 21+ (HttpClient, Records, etc.).
21+
*/
22+
public final class ElpriserAPI {
23+
24+
// Baskonstanter för API-anrop
25+
private static final String API_BASE_URL = "https://www.elprisetjustnu.se/api/v1/prices";
26+
private static final DateTimeFormatter URL_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM-dd");
27+
28+
// En återanvändbar HttpClient-instans
29+
private final HttpClient httpClient;
30+
31+
// Flagga för att styra cachlagring
32+
private final boolean cachingEnabled;
33+
34+
// Ett enkelt minnes-cache. Nyckeln är en kombination av datum och prisklass, t.ex. "2025-08-30_SE3"
35+
private final Map<String, List<Elpris>> inMemoryCache;
36+
37+
/**
38+
* En record som representerar ett enskilt elpris för en given tidsperiod.
39+
* Användningen av 'record' genererar automatiskt constructor, getters, equals, hashCode och toString.
40+
*/
41+
public record Elpris(
42+
double sekPerKWh,
43+
double eurPerKWh,
44+
double exr,
45+
ZonedDateTime timeStart,
46+
ZonedDateTime timeEnd
47+
) {}
48+
49+
/**
50+
* Enum för de svenska elprisområdena för typsäkerhet.
51+
*/
52+
public enum Prisklass {
53+
SE1, SE2, SE3, SE4
54+
}
55+
56+
// --- Static fields for the test hook ---
57+
/**
58+
* This supplier is used ONLY for testing. If it's not null, the class will
59+
* use the String it provides instead of making a real HTTP call.
60+
*/
61+
private static Supplier<String> mockResponseSupplier = null;
62+
63+
/**
64+
* FOR TESTS ONLY: Sets a mock JSON response to be returned by the next API call.
65+
* This bypasses the actual network request.
66+
* @param jsonResponse The fake JSON string the API should parse.
67+
*/
68+
public static void setMockResponse(String jsonResponse) {
69+
mockResponseSupplier = () -> jsonResponse;
70+
}
71+
72+
/**
73+
* FOR TESTS ONLY: Clears the mock response, causing the API to resume
74+
* making real network requests. This should be called after each test.
75+
*/
76+
public static void clearMockResponse() {
77+
mockResponseSupplier = null;
78+
}
79+
// --- End of test fields ---
80+
81+
/**
82+
* Standardkonstruktor som aktiverar cachning.
83+
*/
84+
public ElpriserAPI() {
85+
this(true);
86+
}
87+
88+
/**
89+
* Konstruktor för att explicit styra om cachning ska användas.
90+
* @param enableCaching Sätt till true för att aktivera minnes-cachning, annars false.
91+
*/
92+
public ElpriserAPI(boolean enableCaching) {
93+
this.httpClient = HttpClient.newBuilder()
94+
.followRedirects(HttpClient.Redirect.NORMAL)
95+
.build();
96+
this.cachingEnabled = enableCaching;
97+
// ConcurrentHashMap är trådsäker om klassen skulle användas i flera trådar
98+
this.inMemoryCache = new ConcurrentHashMap<>();
99+
System.out.println("ElpriserAPI initialiserat. Cachning: " + (enableCaching ? "På" : "Av"));
100+
}
101+
102+
/**
103+
* Hämtar elpriser för ett specifikt datum och prisklass.
104+
* Detta är en överlagrad metod som accepterar datumet som en sträng i formatet "YYYY-MM-DD".
105+
*
106+
* @param datumStr En sträng som representerar datumet (t.ex. "2025-08-30").
107+
* @param prisklass Elprisområdet (SE1, SE2, SE3 eller SE4).
108+
* @return En lista av {@link Elpris}-objekt, eller en tom lista om data inte kunde hämtas.
109+
*/
110+
public List<Elpris> getPriser(String datumStr, Prisklass prisklass) {
111+
try {
112+
LocalDate datum = LocalDate.parse(datumStr, DateTimeFormatter.ISO_LOCAL_DATE);
113+
return getPriser(datum, prisklass);
114+
} catch (Exception e) {
115+
System.err.println("Ogiltigt datumformat. Använd YYYY-MM-DD. Fel: " + e.getMessage());
116+
return Collections.emptyList();
117+
}
118+
}
119+
120+
/**
121+
* Hämtar elpriser för ett specifikt datum och prisklass.
122+
*
123+
* @param datum Ett {@link LocalDate}-objekt som representerar dagen att hämta priser för.
124+
* @param prisklass Elprisområdet (SE1, SE2, SE3 eller SE4).
125+
* @return En lista av {@link Elpris}-objekt, eller en tom lista om data inte kunde hämtas.
126+
*/
127+
public List<Elpris> getPriser(LocalDate datum, Prisklass prisklass) {
128+
String cacheKey = getCacheKey(datum, prisklass);
129+
130+
// Steg 1: Kolla minnes-cachen
131+
if (cachingEnabled && inMemoryCache.containsKey(cacheKey)) {
132+
System.out.println("Hämtar från minnes-cache för " + cacheKey);
133+
return inMemoryCache.get(cacheKey);
134+
}
135+
136+
// Steg 2: Försök ladda från disk-cache (framtida implementation)
137+
var priserFrånDisk = loadFromDiskCache(cacheKey);
138+
if (cachingEnabled && priserFrånDisk != null && !priserFrånDisk.isEmpty()) {
139+
System.out.println("Hämtar från disk-cache för " + cacheKey);
140+
inMemoryCache.put(cacheKey, priserFrånDisk); // Lägg i minnes-cachen för snabbare åtkomst nästa gång
141+
return priserFrånDisk;
142+
}
143+
144+
// Check for a mock response before making a network call ---
145+
if (mockResponseSupplier != null) {
146+
System.out.println("!!! ANVÄNDER MOCK-DATA FÖR TEST !!!");
147+
String mockJson = mockResponseSupplier.get();
148+
// If the mock is null or empty, simulate a "not found" scenario
149+
if (mockJson == null || mockJson.isBlank()) {
150+
return Collections.emptyList();
151+
}
152+
List<Elpris> priser = parseSimpleJson(mockJson);
153+
if (cachingEnabled && !priser.isEmpty()) {
154+
inMemoryCache.put(cacheKey, priser);
155+
}
156+
return priser;
157+
}
158+
// --- End of mock check ---
159+
160+
// Steg 3: Hämta från nätverket om det inte finns i cachen
161+
System.out.println("Hämtar från nätverket för " + cacheKey);
162+
String url = buildUrl(datum, prisklass);
163+
try {
164+
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
165+
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
166+
167+
// Om sidan inte finns (t.ex. priser för morgondagen) returneras 404
168+
if (response.statusCode() == 404) {
169+
System.out.println("Inga priser hittades för " + cacheKey + " (HTTP 404).");
170+
return Collections.emptyList();
171+
}
172+
if (response.statusCode() != 200) {
173+
System.err.println("Misslyckades med att hämta priser. Statuskod: " + response.statusCode());
174+
return Collections.emptyList();
175+
}
176+
177+
List<Elpris> priser = parseSimpleJson(response.body());
178+
179+
// Steg 4: Spara i cache om cachning är på
180+
if (cachingEnabled && !priser.isEmpty()) {
181+
inMemoryCache.put(cacheKey, priser);
182+
saveToDiskCache(cacheKey, response.body()); // Spara rådata till disk (framtida implementation)
183+
}
184+
return priser;
185+
186+
} catch (IOException | InterruptedException e) {
187+
System.err.println("Ett fel inträffade vid hämtning av elpriser: " + e.getMessage());
188+
// I ett produktionssystem skulle man vilja logga detta fel mer utförligt
189+
Thread.currentThread().interrupt(); // Bra praxis vid InterruptedException
190+
return Collections.emptyList();
191+
}
192+
}
193+
194+
// --- Privata hjälpmetoder ---
195+
196+
private String buildUrl(LocalDate datum, Prisklass prisklass) {
197+
String formattedDate = datum.format(URL_DATE_FORMATTER);
198+
return String.format("%s/%s_%s.json", API_BASE_URL, formattedDate, prisklass.name());
199+
}
200+
201+
private String getCacheKey(LocalDate datum, Prisklass prisklass) {
202+
return datum.format(DateTimeFormatter.ISO_LOCAL_DATE) + "_" + prisklass.name();
203+
}
204+
205+
/**
206+
* En mycket enkel JSON-parser som är skräddarsydd för just detta API:s svarsformat.
207+
* Denna metod är inte en generell JSON-parser och är känslig för ändringar i formatet.
208+
*/
209+
private List<Elpris> parseSimpleJson(String json) {
210+
List<Elpris> priser = new ArrayList<>();
211+
// Ta bort yttre [ och ], samt eventuella blanksteg
212+
String trimmedJson = json.trim();
213+
if (!trimmedJson.startsWith("[") || !trimmedJson.endsWith("]")) {
214+
return Collections.emptyList();
215+
}
216+
String content = trimmedJson.substring(1, trimmedJson.length() - 1).trim();
217+
if (content.isEmpty()) {
218+
return Collections.emptyList();
219+
}
220+
221+
// Dela upp i enskilda JSON-objekt
222+
String[] objects = content.split("\\},\\{");
223+
224+
for (String objStr : objects) {
225+
// Rensa bort resterande { och }
226+
String cleanObjStr = objStr.replace("{", "").replace("}", "");
227+
228+
try {
229+
// Skapa en temporär map för att hålla värdena för ett objekt
230+
Map<String, String> valueMap = new java.util.HashMap<>();
231+
String[] pairs = cleanObjStr.split(",");
232+
for (String pair : pairs) {
233+
String[] keyValue = pair.split(":", 2);
234+
String key = keyValue[0].trim().replace("\"", "");
235+
String value = keyValue[1].trim().replace("\"", "");
236+
valueMap.put(key, value);
237+
}
238+
239+
// Skapa ett Elpris-objekt från värdena i mappen
240+
priser.add(new Elpris(
241+
Double.parseDouble(valueMap.get("SEK_per_kWh")),
242+
Double.parseDouble(valueMap.get("EUR_per_kWh")),
243+
Double.parseDouble(valueMap.get("EXR")),
244+
ZonedDateTime.parse(valueMap.get("time_start")),
245+
ZonedDateTime.parse(valueMap.get("time_end"))
246+
));
247+
} catch (Exception e) {
248+
// Hoppa över objekt som inte kan parsas, logga ett fel
249+
System.err.println("Kunde inte tolka ett elpris-objekt: " + cleanObjStr + " - Fel: " + e.getMessage());
250+
}
251+
}
252+
return priser;
253+
}
254+
255+
// --- Stub-metoder för disk-cache ---
256+
257+
/**
258+
* STUB: Spara data till en fil i en dold katalog i användarens hemkatalog.
259+
* Oimplementerad tills vidare.
260+
*/
261+
private void saveToDiskCache(String cacheKey, String jsonData) {
262+
// Framtida implementation:
263+
// Path cacheDir = Paths.get(System.getProperty("user.home"), ".elpriser_cache");
264+
// Files.createDirectories(cacheDir);
265+
// Path cacheFile = cacheDir.resolve(cacheKey + ".json");
266+
// Files.writeString(cacheFile, jsonData);
267+
// System.out.println("Simulerar: Sparar " + cacheKey + " till disk.");
268+
}
269+
270+
/**
271+
* STUB: Läs data från en fil i en dold katalog i användarens hemkatalog.
272+
* Oimplementerad tills vidare.
273+
* @return En lista av Elpris-objekt om filen finns och kan läsas, annars null.
274+
*/
275+
private List<Elpris> loadFromDiskCache(String cacheKey) {
276+
// Framtida implementation:
277+
// Path cacheFile = Paths.get(System.getProperty("user.home"), ".elpriser_cache", cacheKey + ".json");
278+
// if (Files.exists(cacheFile)) {
279+
// String jsonData = Files.readString(cacheFile);
280+
// return parseSimpleJson(jsonData);
281+
// }
282+
// System.out.println("Simulerar: Försöker läsa " + cacheKey + " från disk. Fanns ej.");
283+
return null;
284+
}
285+
286+
287+
// --- Exempel på användning ---
288+
289+
public static void main(String[] args) {
290+
System.out.println("--- Testar Elpriser API ---");
291+
ElpriserAPI api = new ElpriserAPI(); // Cachning är på som standard
292+
293+
// Hämta dagens priser för SE3 med LocalDate
294+
LocalDate idag = LocalDate.now();
295+
List<Elpris> dagensPriser = api.getPriser(idag, Prisklass.SE3);
296+
297+
if (dagensPriser.isEmpty()) {
298+
System.out.println("Kunde inte hämta några priser för idag i SE3.");
299+
} else {
300+
System.out.println("\nDagens elpriser för " + Prisklass.SE3 + " (" + dagensPriser.size() + " st värden):");
301+
// Skriv bara ut de 3 första för att hålla utskriften kort
302+
dagensPriser.stream().limit(3).forEach(pris ->
303+
System.out.printf("Tid: %s, Pris: %.4f SEK/kWh\n",
304+
pris.timeStart().toLocalTime(), pris.sekPerKWh())
305+
);
306+
if(dagensPriser.size() > 3) System.out.println("...");
307+
}
308+
309+
// Anropa igen för samma dag, bör nu komma från cachen
310+
System.out.println("\n--- Anropar igen för samma dag ---");
311+
api.getPriser(idag, Prisklass.SE3);
312+
313+
// Hämta priser för en annan dag med sträng-metoden
314+
System.out.println("\n--- Hämtar priser för 2025-09-15 i SE4 ---");
315+
List<Elpris> framtidaPriser = api.getPriser("2025-09-15", Prisklass.SE4);
316+
if (framtidaPriser.isEmpty()) {
317+
System.out.println("Inga priser hittades (som förväntat).");
318+
}
319+
}
320+
}

0 commit comments

Comments
 (0)