import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; import java.io.IOException; import java.io.OutputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.ArrayList; /** * Kleine REST-API "tasks" ohne Framework (nur JDK): GET/POST/PUT/DELETE, JSON, Validierung, Speicherung in Datei. * * Aufruf: java TaskApi <port> <datei.json> */ public final class TaskApi { /** Eine Aufgabe. */ record Task(int id, String title, boolean done) { String json() { return "{\"id\":" + id + ",\"title\":" + Json.text(title) + ",\"done\":" + done + "}"; } } /** Kleiner JSON-Leser und -Schreiber (Objekte, Listen, Strings, Zahlen, Wahrheitswerte, null). */ static final class Json { private final String s; private int p; private Json(String s) { this.s = s; } static Object lesen(String text) { Json j = new Json(text); Object v = j.wert(); j.leer(); if (j.p != j.s.length()) { throw new IllegalArgumentException("Text nach dem JSON-Wert"); } return v; } static String text(String t) { StringBuilder sb = new StringBuilder("\""); for (int i = 0; i < t.length(); i++) { char c = t.charAt(i); switch (c) { case '"' -> sb.append("\\\""); case '\\' -> sb.append("\\\\"); case '\n' -> sb.append("\\n"); case '\r' -> sb.append("\\r"); case '\t' -> sb.append("\\t"); default -> { if (c < 0x20) { sb.append(String.format("\\u%04x", (int) c)); } else { sb.append(c); } } } } return sb.append('"').toString(); } private void leer() { while (p < s.length() && Character.isWhitespace(s.charAt(p))) { p++; } } private char vor() { leer(); if (p >= s.length()) { throw new IllegalArgumentException("JSON zu frueh zu Ende"); } return s.charAt(p); } private void erwarte(char c) { if (vor() != c) { throw new IllegalArgumentException("'" + c + "' erwartet bei Position " + p); } p++; } private Object wert() { char c = vor(); if (c == '{') { return objekt(); } if (c == '[') { return liste(); } if (c == '"') { return string(); } if (s.startsWith("true", p)) { p += 4; return Boolean.TRUE; } if (s.startsWith("false", p)) { p += 5; return Boolean.FALSE; } if (s.startsWith("null", p)) { p += 4; return null; } return zahl(); } private Map objekt() { Map m = new LinkedHashMap<>(); erwarte('{'); if (vor() == '}') { p++; return m; } while (true) { String k = string(); erwarte(':'); m.put(k, wert()); char c = vor(); p++; if (c == '}') { return m; } if (c != ',') { throw new IllegalArgumentException("',' oder '}' erwartet"); } } } private List liste() { List l = new ArrayList<>(); erwarte('['); if (vor() == ']') { p++; return l; } while (true) { l.add(wert()); char c = vor(); p++; if (c == ']') { return l; } if (c != ',') { throw new IllegalArgumentException("',' oder ']' erwartet"); } } } private String string() { erwarte('"'); StringBuilder sb = new StringBuilder(); while (true) { if (p >= s.length()) { throw new IllegalArgumentException("String nicht beendet"); } char c = s.charAt(p++); if (c == '"') { return sb.toString(); } if (c == '\\') { if (p >= s.length()) { throw new IllegalArgumentException("Escape am Ende"); } char e = s.charAt(p++); switch (e) { case '"', '\\', '/' -> sb.append(e); case 'n' -> sb.append('\n'); case 'r' -> sb.append('\r'); case 't' -> sb.append('\t'); case 'b' -> sb.append('\b'); case 'f' -> sb.append('\f'); case 'u' -> { if (p + 4 > s.length()) { throw new IllegalArgumentException("\\u zu kurz"); } sb.append((char) Integer.parseInt(s.substring(p, p + 4), 16)); p += 4; } default -> throw new IllegalArgumentException("Ungueltiges Escape"); } } else if (c < 0x20) { throw new IllegalArgumentException("Steuerzeichen im String"); } else { sb.append(c); } } } private Object zahl() { int a = p; while (p < s.length() && "+-0123456789.eE".indexOf(s.charAt(p)) >= 0) { p++; } if (a == p) { throw new IllegalArgumentException("Unerwartetes Zeichen bei Position " + p); } String t = s.substring(a, p); try { if (t.matches("-?\\d+")) { return Long.valueOf(t); } return Double.valueOf(t); } catch (NumberFormatException e) { throw new IllegalArgumentException("Ungueltige Zahl"); } } } private final Map tasks = new TreeMap<>(); private int naechsteId = 1; private final Path datei; TaskApi(Path datei) throws IOException { this.datei = datei; laden(); } private void laden() throws IOException { if (!Files.exists(datei)) { return; } Object w = Json.lesen(Files.readString(datei, StandardCharsets.UTF_8)); if (!(w instanceof List liste)) { throw new IOException("Datei enthaelt kein JSON-Array"); } for (Object o : liste) { if (o instanceof Map m && m.get("id") instanceof Long id && m.get("title") instanceof String t) { boolean done = Boolean.TRUE.equals(m.get("done")); tasks.put(id.intValue(), new Task(id.intValue(), t, done)); naechsteId = Math.max(naechsteId, id.intValue() + 1); } } } private void speichern() throws IOException { StringBuilder sb = new StringBuilder("["); boolean erste = true; for (Task t : tasks.values()) { if (!erste) { sb.append(','); } erste = false; sb.append(t.json()); } sb.append(']'); Path tmp = datei.resolveSibling(datei.getFileName() + ".tmp"); Files.writeString(tmp, sb.toString(), StandardCharsets.UTF_8); Files.move(tmp, datei, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); } /** Prueft den Anfragekoerper; wirft IllegalArgumentException mit lesbarer Meldung (-> 400). */ private static Task pruefen(int id, String body) { Object w = Json.lesen(body); if (!(w instanceof Map m)) { throw new IllegalArgumentException("JSON-Objekt erwartet"); } if (!(m.get("title") instanceof String titel) || titel.isBlank()) { throw new IllegalArgumentException("title fehlt oder ist leer"); } String sauber = titel.strip(); if (sauber.length() > 200) { throw new IllegalArgumentException("title ist laenger als 200 Zeichen"); } Object d = m.get("done"); if (d != null && !(d instanceof Boolean)) { throw new IllegalArgumentException("done muss true oder false sein"); } return new Task(id, sauber, Boolean.TRUE.equals(d)); } private static String fehler(String meldung) { return "{\"error\":" + Json.text(meldung) + "}"; } private static void senden(HttpExchange ex, int code, String json) throws IOException { byte[] b = json.getBytes(StandardCharsets.UTF_8); ex.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); ex.sendResponseHeaders(code, b.length); try (OutputStream os = ex.getResponseBody()) { os.write(b); } } private static void leer(HttpExchange ex, int code) throws IOException { ex.sendResponseHeaders(code, -1); ex.close(); } private static String koerper(HttpExchange ex) throws IOException { byte[] b = ex.getRequestBody().readNBytes(65537); if (b.length > 65536) { throw new IllegalArgumentException("Anfrage zu gross"); } return new String(b, StandardCharsets.UTF_8); } private synchronized void behandle(HttpExchange ex) throws IOException { try { String methode = ex.getRequestMethod(); String[] teile = ex.getRequestURI().getPath().substring(1).split("/"); if (!teile[0].equals("tasks") || teile.length > 2) { senden(ex, 404, fehler("nicht gefunden")); return; } if (teile.length == 1) { sammlung(ex, methode); } else { einzeln(ex, methode, teile[1]); } } catch (IllegalArgumentException e) { senden(ex, 400, fehler(e.getMessage())); } catch (RuntimeException e) { senden(ex, 500, fehler("interner Fehler")); } } private void sammlung(HttpExchange ex, String methode) throws IOException { switch (methode) { case "GET" -> { StringBuilder sb = new StringBuilder("["); boolean erste = true; for (Task t : tasks.values()) { if (!erste) { sb.append(','); } erste = false; sb.append(t.json()); } senden(ex, 200, sb.append(']').toString()); } case "POST" -> { Task neu = pruefen(naechsteId, koerper(ex)); tasks.put(neu.id(), neu); naechsteId++; speichern(); ex.getResponseHeaders().set("Location", "/tasks/" + neu.id()); senden(ex, 201, neu.json()); } default -> { ex.getResponseHeaders().set("Allow", "GET, POST"); senden(ex, 405, fehler("Methode nicht erlaubt")); } } } private void einzeln(HttpExchange ex, String methode, String idText) throws IOException { int id; try { id = Integer.parseInt(idText); } catch (NumberFormatException e) { senden(ex, 404, fehler("nicht gefunden")); return; } Task vorhanden = tasks.get(id); switch (methode) { case "GET" -> { if (vorhanden == null) { senden(ex, 404, fehler("nicht gefunden")); } else { senden(ex, 200, vorhanden.json()); } } case "PUT" -> { if (vorhanden == null) { senden(ex, 404, fehler("nicht gefunden")); return; } Task neu = pruefen(id, koerper(ex)); tasks.put(id, neu); speichern(); senden(ex, 200, neu.json()); } case "DELETE" -> { if (vorhanden == null) { senden(ex, 404, fehler("nicht gefunden")); return; } tasks.remove(id); speichern(); leer(ex, 204); } default -> { ex.getResponseHeaders().set("Allow", "GET, PUT, DELETE"); senden(ex, 405, fehler("Methode nicht erlaubt")); } } } public static void main(String[] args) throws IOException { if (args.length != 2) { System.err.println("Aufruf: java TaskApi "); System.exit(2); } TaskApi api = new TaskApi(Path.of(args[1])); HttpServer server = HttpServer.create( new InetSocketAddress(InetAddress.getLoopbackAddress(), Integer.parseInt(args[0])), 0); server.createContext("/tasks", api::behandle); server.setExecutor(null); server.start(); System.out.println("bereit " + args[0]); } }