|
|
|
@ -1,9 +1,11 @@
@@ -1,9 +1,11 @@
|
|
|
|
|
/* © SRSoftware 2024 */ |
|
|
|
|
package de.srsoftware.cal; |
|
|
|
|
|
|
|
|
|
import static de.srsoftware.tools.Optionals.nullIfEmpty; |
|
|
|
|
import static de.srsoftware.tools.Optionals.nullable; |
|
|
|
|
import static java.lang.System.*; |
|
|
|
|
import static java.lang.System.Logger.Level.WARNING; |
|
|
|
|
import static java.time.format.DateTimeFormatter.ISO_DATE_TIME; |
|
|
|
|
|
|
|
|
|
import com.sun.net.httpserver.HttpExchange; |
|
|
|
|
import de.srsoftware.cal.api.Appointment; |
|
|
|
@ -22,7 +24,13 @@ import java.util.Map;
@@ -22,7 +24,13 @@ import java.util.Map;
|
|
|
|
|
import org.json.JSONObject; |
|
|
|
|
|
|
|
|
|
public class ApiHandler extends PathHandler { |
|
|
|
|
private static final Logger LOG = getLogger(ApiHandler.class.getSimpleName()); |
|
|
|
|
private static final Logger LOG = getLogger(ApiHandler.class.getSimpleName()); |
|
|
|
|
private static final String SLUG = "slug"; |
|
|
|
|
private static final String DESCRIPTION = "description"; |
|
|
|
|
private static final String TITLE = "title"; |
|
|
|
|
private static final String START = "start"; |
|
|
|
|
private static final String END = "end"; |
|
|
|
|
private static final String LOCATION = "location"; |
|
|
|
|
private final Database db; |
|
|
|
|
|
|
|
|
|
public ApiHandler(Database db) { |
|
|
|
@ -41,58 +49,106 @@ public class ApiHandler extends PathHandler {
@@ -41,58 +49,106 @@ public class ApiHandler extends PathHandler {
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private boolean listTags(HttpExchange ex, Map<String, String> params) throws IOException { |
|
|
|
|
var infix = params.get("infix"); |
|
|
|
|
if (infix == null)return sendContent(ex,Error.of("No infix set in method call parameters")); |
|
|
|
|
var res = db.findTags(infix).map(ApiHandler::sortTags); |
|
|
|
|
return sendContent(ex,res); |
|
|
|
|
@Override |
|
|
|
|
public boolean doPost(String path, HttpExchange ex) throws IOException { |
|
|
|
|
return switch (path) { |
|
|
|
|
case "/event/edit" -> editEvent(ex); |
|
|
|
|
default -> PathHandler.notFound(ex); |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private static Result<List<String>> sortTags(Result<List<String>> listResult) { |
|
|
|
|
if (listResult.optional().isEmpty()) return listResult; |
|
|
|
|
List<String> list = listResult.optional().get(); |
|
|
|
|
while (list.size() > 15) { |
|
|
|
|
int longest = list.stream().map(String::length).reduce(0, Integer::max); |
|
|
|
|
var subset = list.stream().filter(s -> s.length() < longest).toList(); |
|
|
|
|
if (subset.size()<3) return Payload.of(list); |
|
|
|
|
list = subset; |
|
|
|
|
private boolean editEvent(HttpExchange ex) throws IOException { |
|
|
|
|
var json = json(ex); |
|
|
|
|
// spotless:off
|
|
|
|
|
var slug = json.has(SLUG) ? nullIfEmpty(json.getString(SLUG)) : null; |
|
|
|
|
// spotless:on
|
|
|
|
|
if (slug == null) sendContent(ex, Error.of("No slug value in appointment")); |
|
|
|
|
var existingAppointment = db.loadEvent(slug); |
|
|
|
|
return switch (existingAppointment) { |
|
|
|
|
case Payload<Appointment> payload //
|
|
|
|
|
-> update(ex, payload.get(), json); |
|
|
|
|
case Error<Appointment> err //
|
|
|
|
|
-> err.toString().startsWith("Failed to find appointment with slug") ? createEvent(ex, json): |
|
|
|
|
sendContent(ex, err); |
|
|
|
|
default -> serverError(ex, existingAppointment); |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
return Payload.of(list); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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 PathHandler.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); |
|
|
|
|
private boolean createEvent(HttpExchange ex, JSONObject json) throws IOException { |
|
|
|
|
var description = json.has(DESCRIPTION) ? nullIfEmpty(json.getString(DESCRIPTION)) : null; |
|
|
|
|
var title = json.has(TITLE) ? nullIfEmpty(json.getString(TITLE)) : null; |
|
|
|
|
if (title == null) return sendContent(ex, Error.of("title missing")); |
|
|
|
|
var start = json.has(START) ? nullIfEmpty(json.getString(START)) : null; |
|
|
|
|
if (start == null) return sendContent(ex, Error.of("start missing")); |
|
|
|
|
var startDate = nullable(start).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null); |
|
|
|
|
var end = json.has(END) ? nullIfEmpty(json.getString(END)) : null; |
|
|
|
|
var endDate = nullable(end).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null); |
|
|
|
|
var location = json.has(LOCATION) ? json.getString(LOCATION) : null; |
|
|
|
|
if (location == null) return sendContent(ex, Error.of("location missing")); |
|
|
|
|
var clientSlug = json.has(SLUG) ? json.getString(SLUG) : null; |
|
|
|
|
var serverSlug = Database.slug(location, startDate); |
|
|
|
|
if (!serverSlug.equals(clientSlug)) return sendContent(ex, Error.of("Slug mismatch!")); |
|
|
|
|
var event = new BaseAppointment(0, title, description, startDate, endDate, location, serverSlug); |
|
|
|
|
db.add(event); |
|
|
|
|
return sendContent(ex, Error.of("createEvent not implemented")); |
|
|
|
|
} |
|
|
|
|
return PathHandler.notFound(ex); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private boolean loadEvent(HttpExchange ex, Map<String, String> params) throws IOException { |
|
|
|
|
var id = params.get("id"); |
|
|
|
|
if (id != null) try { |
|
|
|
|
return PathHandler.sendContent(ex,db.loadEvent(Long.parseLong(id)).map(ApiHandler::toJson)); |
|
|
|
|
} catch (NumberFormatException | IOException nfe){ |
|
|
|
|
return PathHandler.sendContent(ex, Error.format("%s is not a numeric event id!",id)); |
|
|
|
|
private boolean update(HttpExchange ex, Appointment event, JSONObject json) throws IOException { |
|
|
|
|
return sendContent(ex, Error.of("update not implemented")); |
|
|
|
|
} |
|
|
|
|
return PathHandler.sendContent(ex,Error.of("ID missing")); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private static Result<JSONObject> toJson(Result<Appointment> res) { |
|
|
|
|
var opt = res.optional(); |
|
|
|
|
if (opt.isEmpty()) return Result.transform(res); |
|
|
|
|
return Payload.of(opt.get().json()); |
|
|
|
|
} |
|
|
|
|
private boolean listTags(HttpExchange ex, Map<String, String> params) throws IOException { |
|
|
|
|
var infix = params.get("infix"); |
|
|
|
|
if (infix == null) return sendContent(ex, Error.of("No infix set in method call parameters")); |
|
|
|
|
var res = db.findTags(infix).map(ApiHandler::sortTags); |
|
|
|
|
return sendContent(ex, res); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private static Result<List<String>> sortTags(Result<List<String>> listResult) { |
|
|
|
|
if (listResult.optional().isEmpty()) return listResult; |
|
|
|
|
List<String> list = listResult.optional().get(); |
|
|
|
|
while (list.size() > 15) { |
|
|
|
|
int longest = list.stream().map(String::length).reduce(0, Integer::max); |
|
|
|
|
var subset = list.stream().filter(s -> s.length() < longest).toList(); |
|
|
|
|
if (subset.size() < 3) return Payload.of(list); |
|
|
|
|
list = subset; |
|
|
|
|
} |
|
|
|
|
return Payload.of(list); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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 PathHandler.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 PathHandler.notFound(ex); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private boolean loadEvent(HttpExchange ex, Map<String, String> params) throws IOException { |
|
|
|
|
var id = params.get("id"); |
|
|
|
|
if (id != null) try { |
|
|
|
|
return PathHandler.sendContent(ex, db.loadEvent(Long.parseLong(id)).map(ApiHandler::toJson)); |
|
|
|
|
} catch (NumberFormatException | IOException nfe) { |
|
|
|
|
return PathHandler.sendContent(ex, Error.format("%s is not a numeric event id!", id)); |
|
|
|
|
} |
|
|
|
|
return PathHandler.sendContent(ex, Error.of("ID missing")); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private static Result<JSONObject> toJson(Result<Appointment> res) { |
|
|
|
|
var opt = res.optional(); |
|
|
|
|
if (opt.isEmpty()) return Result.transform(res); |
|
|
|
|
return Payload.of(opt.get().json()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static LocalDateTime toLocalDateTime(String dateString) { |
|
|
|
|
try { |
|
|
|
|
return LocalDate.parse(dateString + "-01", DateTimeFormatter.ISO_LOCAL_DATE).atTime(0, 0); |
|
|
|
|
} catch (Exception e) { |
|
|
|
|
return null; |
|
|
|
|
private static LocalDateTime toLocalDateTime(String dateString) { |
|
|
|
|
try { |
|
|
|
|
return LocalDate.parse(dateString + "-01", DateTimeFormatter.ISO_LOCAL_DATE).atTime(0, 0); |
|
|
|
|
} catch (Exception e) { |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|