20 changed files with 484 additions and 156 deletions
@ -1,10 +1,16 @@
@@ -1,10 +1,16 @@
|
||||
/* © SRSoftware 2024 */ |
||||
package de.srsoftware.cal.api; |
||||
|
||||
import java.util.Map; |
||||
import org.json.JSONObject; |
||||
|
||||
/** |
||||
* cartesian coords |
||||
* @param longitude the longitude |
||||
* @param latitude the latitude |
||||
*/ |
||||
public record Coords(double longitude, double latitude) { |
||||
public JSONObject json() { |
||||
return new JSONObject(Map.of("lon", longitude, "lat", latitude)); |
||||
} |
||||
} |
||||
|
@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
|
||||
/* © SRSoftware 2024 */ |
||||
package de.srsoftware.cal.app; |
||||
|
||||
import static de.srsoftware.tools.Optionals.nullable; |
||||
import static java.lang.System.Logger; |
||||
import static java.lang.System.Logger.Level.WARNING; |
||||
import static java.lang.System.getLogger; |
||||
|
||||
import com.sun.net.httpserver.HttpExchange; |
||||
import de.srsoftware.cal.api.Appointment; |
||||
import de.srsoftware.cal.db.Database; |
||||
import de.srsoftware.tools.Error; |
||||
import de.srsoftware.tools.PathHandler; |
||||
import de.srsoftware.tools.Payload; |
||||
import de.srsoftware.tools.Result; |
||||
import java.io.IOException; |
||||
import java.sql.SQLException; |
||||
import java.time.LocalDate; |
||||
import java.time.LocalDateTime; |
||||
import java.time.format.DateTimeFormatter; |
||||
import java.util.Map; |
||||
import org.json.JSONObject; |
||||
|
||||
public class ApiHandler extends PathHandler { |
||||
private static final Logger LOG = getLogger(ApiHandler.class.getSimpleName()); |
||||
private final Database db; |
||||
|
||||
public ApiHandler(Database db) { |
||||
this.db = db; |
||||
} |
||||
|
||||
@Override |
||||
public boolean doGet(String path, HttpExchange ex) throws IOException { |
||||
var params = queryParam(ex); |
||||
return switch (path) { |
||||
case "/event" -> loadEvent(ex,params); |
||||
case "/events/list" -> listEvents(ex,params); |
||||
default -> notFound(ex); |
||||
}; |
||||
|
||||
} |
||||
|
||||
private boolean listEvents(HttpExchange ex, Map<String, String> params) throws IOException { |
||||
var start = nullable(params.get("start")).map(ApiHandler::toLocalDateTime).orElse(null); |
||||
var end = nullable(params.get("end")).map(ApiHandler::toLocalDateTime).orElse(null); |
||||
try { |
||||
return sendContent(ex,db.list(start, end).stream().map(Appointment::json).toList()); |
||||
} catch (SQLException e) { |
||||
LOG.log(WARNING,"Failed to fetch events (start = {0}, end = {1}!",start,end,e); |
||||
} |
||||
return notFound(ex); |
||||
} |
||||
|
||||
private boolean loadEvent(HttpExchange ex, Map<String, String> params) throws IOException { |
||||
var id = params.get("id"); |
||||
if (id != null) try { |
||||
return sendContent(ex,db.loadEvent(Long.parseLong(id)).map(ApiHandler::toJson)); |
||||
} catch (NumberFormatException | IOException nfe){ |
||||
return sendContent(ex, Error.format("%s is not a numeric event id!",id)); |
||||
} |
||||
return sendContent(ex,Error.of("ID missing")); |
||||
} |
||||
|
||||
private static Result<JSONObject> toJson(Result<Appointment> appointmentResult) { |
||||
var opt = appointmentResult.optional(); |
||||
return opt.isEmpty() ? transform(appointmentResult) : |
||||
Payload.of(opt.get().json()); |
||||
} |
||||
|
||||
private static <T> Result<T> transform(Result<?> res) { |
||||
return res instanceof Error<?> err ? err.transform() : Error.format("Invalid parameter: %s", res.getClass().getSimpleName()); |
||||
} |
||||
|
||||
private static LocalDateTime toLocalDateTime(String dateString) { |
||||
try { |
||||
return LocalDate.parse(dateString + "-01", DateTimeFormatter.ISO_LOCAL_DATE).atTime(0, 0); |
||||
} catch (Exception e) { |
||||
return null; |
||||
} |
||||
} |
||||
} |
@ -1,62 +0,0 @@
@@ -1,62 +0,0 @@
|
||||
/* © SRSoftware 2024 */ |
||||
package de.srsoftware.cal.app; |
||||
|
||||
import static de.srsoftware.tools.TagFilter.ofType; |
||||
import static java.lang.System.Logger.Level.DEBUG; |
||||
import static java.lang.System.Logger.Level.INFO; |
||||
|
||||
import com.sun.net.httpserver.HttpExchange; |
||||
import de.srsoftware.cal.api.Appointment; |
||||
import de.srsoftware.cal.db.Database; |
||||
import de.srsoftware.tools.PathHandler; |
||||
import de.srsoftware.tools.Tag; |
||||
import java.io.IOException; |
||||
import java.sql.SQLException; |
||||
import java.util.List; |
||||
|
||||
public class EventList extends PathHandler { |
||||
private static final System.Logger LOG = System.getLogger(EventList.class.getSimpleName()); |
||||
|
||||
private final Database db; |
||||
|
||||
public EventList(Database db) { |
||||
this.db = db; |
||||
} |
||||
|
||||
@Override |
||||
public boolean doGet(String path, HttpExchange ex) throws IOException { |
||||
try { |
||||
var events = db.list(null, null); |
||||
LOG.log(DEBUG, () -> "Found %s events in database".formatted(events.size())); |
||||
var scaffold = scaffold(); |
||||
var body = scaffold.find(ofType("body")).getFirst(); |
||||
body.add(createTable(events)); |
||||
return sendContent(ex, scaffold.toString(2)); |
||||
} catch (SQLException e) { |
||||
return serverError(ex, "Failed to fetch list of events!"); |
||||
} |
||||
} |
||||
|
||||
private Tag createTable(List<Appointment> events) { |
||||
var table = Tag.of("table"); |
||||
var head = Tag.of("tr"); |
||||
head.add(Tag.of("th").content("Start")).add(Tag.of("th").content("Ort")).add(Tag.of("th").content("Event")); |
||||
table.add(head); |
||||
for (var event : events) { |
||||
LOG.log(INFO, event.title()); |
||||
var row = Tag.of("tr"); |
||||
row.add(Tag.of("td").content(event.start().toString())).add(Tag.of("td").content(event.location())).add(Tag.of("td").content(event.title())).addTo(table); |
||||
} |
||||
return table; |
||||
} |
||||
|
||||
private Tag scaffold() { |
||||
var html = new Tag("html"); |
||||
var head = new Tag("head") //
|
||||
.add(new Tag("meta").attr("charset", "UTF-8")) |
||||
.add(new Tag("title").content("OpenCloudCal")); |
||||
var body = new Tag("body"); |
||||
html.add(head, body); |
||||
return html; |
||||
} |
||||
} |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
/* © SRSoftware 2024 */ |
||||
package de.srsoftware.cal.app; |
||||
|
||||
import com.sun.net.httpserver.HttpExchange; |
||||
import de.srsoftware.tools.PathHandler; |
||||
import java.io.IOException; |
||||
|
||||
public class IndexHandler extends PathHandler { |
||||
PathHandler staticPages; |
||||
public IndexHandler(PathHandler staticPages) { |
||||
this.staticPages = staticPages; |
||||
} |
||||
|
||||
@Override |
||||
public boolean doGet(String path, HttpExchange ex) throws IOException { |
||||
switch (path) { |
||||
case "/": |
||||
return staticPages.doGet("/index", ex); |
||||
} |
||||
return super.doGet(path, ex); |
||||
} |
||||
} |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
/* © SRSoftware 2024 */ |
||||
package de.srsoftware.cal.app; |
||||
|
||||
import com.sun.net.httpserver.HttpExchange; |
||||
import de.srsoftware.tools.PathHandler; |
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.IOException; |
||||
import java.nio.file.Path; |
||||
import java.util.Optional; |
||||
|
||||
|
||||
public class StaticHandler extends PathHandler { |
||||
private final Optional<String> staticPath; |
||||
|
||||
public StaticHandler(Optional<String> staticPath) { |
||||
this.staticPath = staticPath; |
||||
} |
||||
|
||||
@Override |
||||
public boolean doGet(String path, HttpExchange ex) throws IOException { |
||||
if (!path.contains(".")) path += ".html"; |
||||
if (path.startsWith("/")) path = path.substring(1); |
||||
var url = getClass().getClassLoader().getResource(path); |
||||
if (staticPath.isPresent()) { |
||||
var file = Path.of(staticPath.get()).resolve(path).toFile(); |
||||
if (file.exists() && file.isFile()) url = file.toURI().toURL(); |
||||
} |
||||
if (url == null) return notFound(ex); |
||||
var conn = url.openConnection(); |
||||
var mime = conn.getContentType(); |
||||
try (var input = conn.getInputStream()) { |
||||
var bos = new ByteArrayOutputStream(); |
||||
input.transferTo(bos); |
||||
ex.getResponseHeaders().add(CONTENT_TYPE, mime); |
||||
return sendContent(ex, bos.toByteArray()); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
<html> |
||||
<head> |
||||
<meta charset="UTF-8" /> |
||||
<title>SRSoftware OpenCloudCal</title> |
||||
<script src="/static/script/occ.js"></script> |
||||
</head> |
||||
<body> |
||||
<nav /> |
||||
<h1>Loading…</h1> |
||||
<div id="time">Loading…</div> |
||||
<div id="description">Loading…</div> |
||||
<div id="tags">Loading…</div> |
||||
<div id="links">Loading…</div> |
||||
<div id="attachments">Loading…</div> |
||||
<script> |
||||
document.addEventListener("DOMContentLoaded", function(event){ |
||||
console.log("page loaded…"); |
||||
loadEventData(); |
||||
}); |
||||
</script> |
||||
</body> |
||||
</html> |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
<html> |
||||
<head> |
||||
<meta charset="UTF-8" /> |
||||
<title>SRSoftware OpenCloudCal</title> |
||||
<script src="/static/script/occ.js"></script> |
||||
</head> |
||||
<body> |
||||
<nav /> |
||||
<div> |
||||
<h1>Event List</h1> |
||||
<table id="eventlist"> |
||||
<tr> |
||||
<th>ID</th> |
||||
<th>Start</th> |
||||
<th>End</th> |
||||
<th>Location</th> |
||||
<th>Title</th> |
||||
<th>Tags</th> |
||||
</tr> |
||||
</table> |
||||
</div> |
||||
<script> |
||||
document.addEventListener("DOMContentLoaded", function(event){ |
||||
console.log("page loaded…"); |
||||
loadCurrentEvents(); |
||||
}); |
||||
</script> |
||||
</body> |
||||
</html> |
@ -0,0 +1,82 @@
@@ -0,0 +1,82 @@
|
||||
var start = null; |
||||
var end = null; |
||||
|
||||
function fetchEvents(start, end){ |
||||
var path = '/api/events/list'; |
||||
if (start) { |
||||
path += '?start='+start; |
||||
if (end) path+= '&end='+end; |
||||
} |
||||
fetch(path).then(handleEvents); |
||||
} |
||||
|
||||
function addCell(row,content,id){ |
||||
var a = document.createElement('a'); |
||||
if (content){ |
||||
a.href = '/static/event?id='+id; |
||||
a.innerHTML = content; |
||||
} |
||||
row.insertCell().appendChild(a); |
||||
} |
||||
|
||||
function addRow(json){ |
||||
var table = document.getElementById('eventlist'); |
||||
var row = table.insertRow(); |
||||
addCell(row,json.id,json.id); |
||||
addCell(row,json.start,json.id); |
||||
addCell(row,json.end,json.id); |
||||
addCell(row,json.location,json.id); |
||||
addCell(row,json.title,json.id); |
||||
row.appendChild(createTags(json.tags)); |
||||
} |
||||
|
||||
function createTags(tagList){ |
||||
var td = document.createElement('td'); |
||||
tagList.forEach(val => { |
||||
var btn = document.createElement('button'); |
||||
btn.onclick = e => toggleTag(val); |
||||
btn.appendChild(document.createTextNode(val)); |
||||
td.appendChild(btn); |
||||
td.appendChild(document.createTextNode(' ')); |
||||
}); |
||||
return td; |
||||
} |
||||
|
||||
async function handleEventData(response){ |
||||
if (response.ok){ |
||||
var json = await response.json(); |
||||
document.getElementsByTagName('h1')[0].innerHTML = json.title; |
||||
document.getElementById('time').innerHTML = json.start + (json.end ? '…'+json.end : ''); |
||||
document.getElementById('description').innerHTML = json.description; |
||||
} |
||||
} |
||||
|
||||
async function handleEvents(response){ |
||||
if (response.ok){ |
||||
var json = await response.json(); |
||||
json.forEach(addRow) |
||||
} |
||||
} |
||||
|
||||
function loadCurrentEvents(){ |
||||
if (start == null){ |
||||
var now = new Date(); |
||||
var year = now.getFullYear(); |
||||
var month = now.getMonth() + 1; |
||||
start = year + '-' + (month < 10 ? '0' : '') + month; |
||||
fetchEvents(start,end); |
||||
} |
||||
} |
||||
|
||||
function loadEventData(){ |
||||
const urlParams = new URLSearchParams(window.location.search); |
||||
var id = urlParams.get('id'); |
||||
if (id){ |
||||
fetch('/api/event?id='+id).then(handleEventData) |
||||
} |
||||
|
||||
} |
||||
|
||||
function toggleTag(tag){ |
||||
alert(`toggleTag(${tag})`); |
||||
} |
Loading…
Reference in new issue