diff --git a/src/test/java/com/baloise/azure/DevServer.java b/src/test/java/com/baloise/azure/DevServer.java index 4206577..079066e 100644 --- a/src/test/java/com/baloise/azure/DevServer.java +++ b/src/test/java/com/baloise/azure/DevServer.java @@ -1,337 +1,353 @@ -package com.baloise.azure; - -import static java.lang.String.format; -import static java.util.Arrays.asList; -import static java.util.Arrays.stream; -import static java.util.Objects.isNull; -import static java.util.UUID.randomUUID; -import static java.util.logging.Logger.getLogger; - -import java.io.IOException; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLDecoder; -import java.net.UnknownHostException; -import java.nio.charset.StandardCharsets; -import java.util.AbstractMap.SimpleEntry; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.Map; -import java.util.Optional; -import java.util.Scanner; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import com.microsoft.azure.functions.ExecutionContext; -import com.microsoft.azure.functions.HttpMethod; -import com.microsoft.azure.functions.HttpRequestMessage; -import com.microsoft.azure.functions.HttpResponseMessage; -import com.microsoft.azure.functions.HttpResponseMessage.Builder; -import com.microsoft.azure.functions.HttpStatus; -import com.microsoft.azure.functions.HttpStatusType; -import com.microsoft.azure.functions.annotation.BindingName; -import com.microsoft.azure.functions.annotation.FunctionName; -import com.microsoft.azure.functions.annotation.HttpTrigger; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpServer; - -public class DevServer { - - private static final int HTTP_PORT = 7071; - final String HTTP_HOST; - final String API_CONTEXT = "/api"; - final String HTTP_HOST_API_CONTEXT; - - - private class ParameterMapping { - - private Parameter[] parameters; - Map> bindings = new HashMap<>(); - - public ParameterMapping(Method method) { - parameters = method.getParameters(); - for (Parameter p: parameters) { - HttpTrigger trigger = p.getAnnotation(HttpTrigger.class); - if(trigger != null) { - int i = 1; - for (String pathElement: parsePath(trigger.route())) { - if(pathElement.startsWith("{") && pathElement.endsWith("}")) { - pathElement=pathElement.substring(1, pathElement.length()-1); - String[] nameAndDefault = pathElement.split("=", 2); - bindings.put(nameAndDefault[0], new SimpleEntry(i, nameAndDefault.length >1 ? nameAndDefault[1] : null)); - } - i++; - } - break; - } - } - } - - public Object[] map(LinkedList path, HttpExchange exg) { - return stream(parameters).map(p->{ - if(p.getType().isAssignableFrom(ExecutionContextImpl.class)) { - return new ExecutionContextImpl(); - } - if(p.getType().isAssignableFrom(HttpRequestMessageImpl.class)) { - return new HttpRequestMessageImpl(exg); - } - BindingName bindingName = p.getAnnotation(BindingName.class); - if(bindingName != null) { - SimpleEntry binding = bindings.get(bindingName.value()); - if(binding == null) { - throw new IllegalArgumentException("Binding not found for "+bindingName); - } - return path.size() > binding.getKey() ? path.get(binding.getKey()) : binding.getValue(); - } - throw new IllegalArgumentException("Don't know how to map parameter "+p); - }).toArray(); - } - - } - - private Map functionMapping = new HashMap<>(); - private Map, Object> instanceMapping = new HashMap<>(); - private Map parameterMappings = new HashMap<>(); - - @SuppressWarnings("unchecked") - public DevServer(Class ... functionClasses) { - String host = null; - try { - host = InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException e) { - host = "127.0.0.1"; - } - HTTP_HOST = format("http://%s:%s", host, HTTP_PORT); - HTTP_HOST_API_CONTEXT = HTTP_HOST+API_CONTEXT+"/"; - stream(functionClasses).distinct() - .map(clazz ->{ - return stream(clazz.getMethods()) - .mapMulti((method, consumer)-> { - FunctionName functionName = method.getAnnotation(FunctionName.class); - if(!isNull(functionName)) { - parameterMappings.put(method, new ParameterMapping(method)); - consumer.accept(new SimpleEntry(functionName.value(), method)); - } - }) - .collect(Collectors.toList()); - }) - .flatMap(Collection::stream) - .forEach(e -> functionMapping.put(((SimpleEntry)e).getKey(), ((SimpleEntry)e).getValue())); - } - - private final class ExecutionContextImpl implements ExecutionContext { - private String invocationId = randomUUID().toString(); - - @Override - public Logger getLogger() { - return logger; - } - - @Override - public String getInvocationId() { - return invocationId; - } - - @Override - public String getFunctionName() { - //TODO return name from annotation - return name; - } - } - - private final class HttpRequestMessageImpl implements HttpRequestMessage> { - private HttpExchange exg; - private Map queryParameters; - private Map headers; - private URI uri; - - public HttpRequestMessageImpl(HttpExchange exg) { - this.exg = exg; - try { - uri = new URI(HTTP_HOST+exg.getRequestURI().toString()); - } catch (URISyntaxException e) { - uri = exg.getRequestURI(); - } - } - - @Override - public URI getUri() { - return uri; - } - - @Override - public Map getQueryParameters() { - if(queryParameters == null) { - queryParameters = new LinkedHashMap(); - try { - String rawQuery = exg.getRequestURI().getRawQuery(); - if(rawQuery!=null) { - for (String pair : rawQuery.split("&")) { - int idx = pair.indexOf("="); - if(idx>-1) { - queryParameters.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"), URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); - } - } - } - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException(e); - } - } - return queryParameters; - } - - @Override - public HttpMethod getHttpMethod() { - return HttpMethod.value(exg.getRequestMethod()); - } - - @Override - public Map getHeaders() { - if(headers == null) { - headers = new HashMap<>(); - exg.getRequestHeaders().forEach((k,vs)-> headers.put(k, vs.get(0))); - } - return headers; - } - - @Override - public Optional getBody() { - try (Scanner scanner = new Scanner(exg.getRequestBody(), StandardCharsets.UTF_8.name())) { - return Optional.of(scanner.useDelimiter("\\A").next()); - } - } - - @Override - public Builder createResponseBuilder(HttpStatusType status) { - return new ResponseBuilderImpl(status); - } - - @Override - public Builder createResponseBuilder(HttpStatus status) { - - return createResponseBuilder((HttpStatusType) status); - } - } - - private final class ResponseBuilderImpl implements Builder { - private final class HttpResponseMessageImpl implements HttpResponseMessage { - @Override - public HttpStatusType getStatus() { - return status; - } - - @Override - public String getHeader(String key) { - return headers.getOrDefault(key, "undefined"); - } - - public Map getHeaders() { - return headers; - } - - @Override - public Object getBody() { - return body; - } - } - - private HttpStatusType status; - private Object body; - Map headers = new HashMap<>(); - - private ResponseBuilderImpl(HttpStatusType status) { - this.status = status; - } - - @Override - public Builder status(HttpStatusType status) { - this.status = status; - return this; - } - - @Override - public Builder header(String key, String value) { - headers.put(key, value); - return this; - } - - @Override - public Builder body(Object body) { - this.body = body; - return this; - } - - @Override - public HttpResponseMessage build() { - return new HttpResponseMessageImpl(); - } - } - - String name = getClass().getSimpleName(); - Logger logger = getLogger(name); - - public void start() throws IOException { - HttpServer server = HttpServer.create(new InetSocketAddress(HTTP_PORT), 0); - functionMapping.keySet().stream().map(HTTP_HOST_API_CONTEXT::concat).forEach(System.out::println); - server.createContext("/api", exg -> { - try(OutputStream out = exg.getResponseBody()) { - LinkedList path = parsePath(exg.getRequestURI().toString()); - path.pop(); // api - Method method = functionMapping.get(path.peek()); - ResponseBuilderImpl.HttpResponseMessageImpl resp = (ResponseBuilderImpl.HttpResponseMessageImpl) method.invoke(getInstance(method), parameterMappings.get(method).map(path, exg)); - resp.getHeaders().forEach((k,v)-> exg.getResponseHeaders().add(k,v)); - if(hasBody(resp)) { - byte response[] = getResponseBytes(resp); - exg.sendResponseHeaders(resp.getStatusCode(), response.length); - out.write(response); - } else { - exg.sendResponseHeaders(resp.getStatusCode(), -1); - } - } catch (Throwable t) { - logger.log(Level.WARNING, t.getLocalizedMessage(), t); - } - }); - - server.start(); - } - - private boolean hasBody(com.baloise.azure.DevServer.ResponseBuilderImpl.HttpResponseMessageImpl resp) { - return resp.getBody()!= null; - } - - private byte[] getResponseBytes(com.baloise.azure.DevServer.ResponseBuilderImpl.HttpResponseMessageImpl resp) throws UnsupportedEncodingException { - Object body = resp.getBody(); - if(body == null) return new byte[0]; - if(body instanceof byte[]) { - return (byte[]) body; - } - return body.toString().getBytes("UTF-8"); - } - - private LinkedList parsePath(String string) { - String[] pathAndQuery = string.split(Pattern.quote("?"),2); - LinkedList path = new LinkedList(asList(pathAndQuery[0].split("/"))); - path.pop(); // empty - return path; - } - - private Object getInstance(Method method) throws Exception { - Class declaringClass = method.getDeclaringClass(); - Object object = instanceMapping.get(declaringClass); - if(object == null) { - object = declaringClass.getDeclaredConstructor().newInstance(); - instanceMapping.put(declaringClass, object); - } - return object; - } +package com.baloise.azure; + +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static java.util.Objects.isNull; +import static java.util.UUID.randomUUID; +import static java.util.logging.Logger.getLogger; + +import java.awt.Desktop; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.AbstractMap.SimpleEntry; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Optional; +import java.util.Scanner; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import com.baloise.azure.MiniCLI.Command; +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.HttpMethod; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.HttpResponseMessage; +import com.microsoft.azure.functions.HttpResponseMessage.Builder; +import com.microsoft.azure.functions.HttpStatus; +import com.microsoft.azure.functions.HttpStatusType; +import com.microsoft.azure.functions.annotation.BindingName; +import com.microsoft.azure.functions.annotation.FunctionName; +import com.microsoft.azure.functions.annotation.HttpTrigger; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; + +public class DevServer { + + private static final int HTTP_PORT = 7071; + final String HTTP_HOST; + final String API_CONTEXT = "/api"; + final String HTTP_HOST_API_CONTEXT; + + + private class ParameterMapping { + + private Parameter[] parameters; + Map> bindings = new HashMap<>(); + + public ParameterMapping(Method method) { + parameters = method.getParameters(); + for (Parameter p: parameters) { + HttpTrigger trigger = p.getAnnotation(HttpTrigger.class); + if(trigger != null) { + int i = 1; + for (String pathElement: parsePath(trigger.route())) { + if(pathElement.startsWith("{") && pathElement.endsWith("}")) { + pathElement=pathElement.substring(1, pathElement.length()-1); + String[] nameAndDefault = pathElement.split("=", 2); + bindings.put(nameAndDefault[0], new SimpleEntry(i, nameAndDefault.length >1 ? nameAndDefault[1] : null)); + } + i++; + } + break; + } + } + } + + public Object[] map(LinkedList path, HttpExchange exg) { + return stream(parameters).map(p->{ + if(p.getType().isAssignableFrom(ExecutionContextImpl.class)) { + return new ExecutionContextImpl(); + } + if(p.getType().isAssignableFrom(HttpRequestMessageImpl.class)) { + return new HttpRequestMessageImpl(exg); + } + BindingName bindingName = p.getAnnotation(BindingName.class); + if(bindingName != null) { + SimpleEntry binding = bindings.get(bindingName.value()); + if(binding == null) { + throw new IllegalArgumentException("Binding not found for "+bindingName); + } + return path.size() > binding.getKey() ? path.get(binding.getKey()) : binding.getValue(); + } + throw new IllegalArgumentException("Don't know how to map parameter "+p); + }).toArray(); + } + + } + + private Map functionMapping = new HashMap<>(); + private Map, Object> instanceMapping = new HashMap<>(); + private Map parameterMappings = new HashMap<>(); + + @SuppressWarnings("unchecked") + public DevServer(Class ... functionClasses) { + String host = null; + try { + host = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + host = "127.0.0.1"; + } + HTTP_HOST = format("http://%s:%s", host, HTTP_PORT); + HTTP_HOST_API_CONTEXT = HTTP_HOST+API_CONTEXT+"/"; + stream(functionClasses).distinct() + .map(clazz ->{ + return stream(clazz.getMethods()) + .mapMulti((method, consumer)-> { + FunctionName functionName = method.getAnnotation(FunctionName.class); + if(!isNull(functionName)) { + parameterMappings.put(method, new ParameterMapping(method)); + consumer.accept(new SimpleEntry(functionName.value(), method)); + } + }) + .collect(Collectors.toList()); + }) + .flatMap(Collection::stream) + .forEach(e -> functionMapping.put(((SimpleEntry)e).getKey(), ((SimpleEntry)e).getValue())); + } + + private final class ExecutionContextImpl implements ExecutionContext { + private String invocationId = randomUUID().toString(); + + @Override + public Logger getLogger() { + return logger; + } + + @Override + public String getInvocationId() { + return invocationId; + } + + @Override + public String getFunctionName() { + //TODO return name from annotation + return name; + } + } + + private final class HttpRequestMessageImpl implements HttpRequestMessage> { + private HttpExchange exg; + private Map queryParameters; + private Map headers; + private URI uri; + + public HttpRequestMessageImpl(HttpExchange exg) { + this.exg = exg; + try { + uri = new URI(HTTP_HOST+exg.getRequestURI().toString()); + } catch (URISyntaxException e) { + uri = exg.getRequestURI(); + } + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public Map getQueryParameters() { + if(queryParameters == null) { + queryParameters = new LinkedHashMap(); + try { + String rawQuery = exg.getRequestURI().getRawQuery(); + if(rawQuery!=null) { + for (String pair : rawQuery.split("&")) { + int idx = pair.indexOf("="); + if(idx>-1) { + queryParameters.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"), URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); + } + } + } + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + return queryParameters; + } + + @Override + public HttpMethod getHttpMethod() { + return HttpMethod.value(exg.getRequestMethod()); + } + + @Override + public Map getHeaders() { + if(headers == null) { + headers = new HashMap<>(); + exg.getRequestHeaders().forEach((k,vs)-> headers.put(k, vs.get(0))); + } + return headers; + } + + @Override + public Optional getBody() { + try (Scanner scanner = new Scanner(exg.getRequestBody(), StandardCharsets.UTF_8.name())) { + return Optional.of(scanner.useDelimiter("\\A").next()); + } + } + + @Override + public Builder createResponseBuilder(HttpStatusType status) { + return new ResponseBuilderImpl(status); + } + + @Override + public Builder createResponseBuilder(HttpStatus status) { + + return createResponseBuilder((HttpStatusType) status); + } + } + + private final class ResponseBuilderImpl implements Builder { + private final class HttpResponseMessageImpl implements HttpResponseMessage { + @Override + public HttpStatusType getStatus() { + return status; + } + + @Override + public String getHeader(String key) { + return headers.getOrDefault(key, "undefined"); + } + + public Map getHeaders() { + return headers; + } + + @Override + public Object getBody() { + return body; + } + } + + private HttpStatusType status; + private Object body; + Map headers = new HashMap<>(); + + private ResponseBuilderImpl(HttpStatusType status) { + this.status = status; + } + + @Override + public Builder status(HttpStatusType status) { + this.status = status; + return this; + } + + @Override + public Builder header(String key, String value) { + headers.put(key, value); + return this; + } + + @Override + public Builder body(Object body) { + this.body = body; + return this; + } + + @Override + public HttpResponseMessage build() { + return new HttpResponseMessageImpl(); + } + } + + String name = getClass().getSimpleName(); + Logger logger = getLogger(name); + + public void start() throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(HTTP_PORT), 0); + server.createContext("/api", exg -> { + try(OutputStream out = exg.getResponseBody()) { + LinkedList path = parsePath(exg.getRequestURI().toString()); + path.pop(); // api + Method method = functionMapping.get(path.peek()); + ResponseBuilderImpl.HttpResponseMessageImpl resp = (ResponseBuilderImpl.HttpResponseMessageImpl) method.invoke(getInstance(method), parameterMappings.get(method).map(path, exg)); + resp.getHeaders().forEach((k,v)-> exg.getResponseHeaders().add(k,v)); + if(hasBody(resp)) { + byte response[] = getResponseBytes(resp); + exg.sendResponseHeaders(resp.getStatusCode(), response.length); + out.write(response); + } else { + exg.sendResponseHeaders(resp.getStatusCode(), -1); + } + } catch (Throwable t) { + logger.log(Level.WARNING, t.getLocalizedMessage(), t); + } + }); + + server.start(); + + MiniCLI cli = new MiniCLI(); + AtomicInteger urlCount = new AtomicInteger(1); + functionMapping.keySet().stream().map(HTTP_HOST_API_CONTEXT::concat) + .map(url->{return new Command(String.valueOf(urlCount.getAndIncrement()), url, ()->{ + try { + Desktop.getDesktop().browse(new URI(url)); + } catch (IOException | URISyntaxException e) { + e.printStackTrace(); + } + });}) + .forEach(cli::add); + cli.add(new Command("q", "quit", ()->{System.exit(0);})); + cli.start(); + } + + private boolean hasBody(com.baloise.azure.DevServer.ResponseBuilderImpl.HttpResponseMessageImpl resp) { + return resp.getBody()!= null; + } + + private byte[] getResponseBytes(com.baloise.azure.DevServer.ResponseBuilderImpl.HttpResponseMessageImpl resp) throws UnsupportedEncodingException { + Object body = resp.getBody(); + if(body == null) return new byte[0]; + if(body instanceof byte[]) { + return (byte[]) body; + } + return body.toString().getBytes("UTF-8"); + } + + private LinkedList parsePath(String string) { + String[] pathAndQuery = string.split(Pattern.quote("?"),2); + LinkedList path = new LinkedList(asList(pathAndQuery[0].split("/"))); + path.pop(); // empty + return path; + } + + private Object getInstance(Method method) throws Exception { + Class declaringClass = method.getDeclaringClass(); + Object object = instanceMapping.get(declaringClass); + if(object == null) { + object = declaringClass.getDeclaredConstructor().newInstance(); + instanceMapping.put(declaringClass, object); + } + return object; + } } \ No newline at end of file diff --git a/src/test/java/com/baloise/azure/MiniCLI.java b/src/test/java/com/baloise/azure/MiniCLI.java new file mode 100644 index 0000000..54cc808 --- /dev/null +++ b/src/test/java/com/baloise/azure/MiniCLI.java @@ -0,0 +1,55 @@ +package com.baloise.azure; + +import static java.lang.String.format; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Map; +import java.util.TreeMap; + +public class MiniCLI extends Thread { + public static record Command (String cmdLine, String description, Runnable action) {} + final Map commands; + + public MiniCLI(Command ... commands) { + this.commands = new TreeMap<>(); + for (Command command : commands) { + add(command); + } + setDaemon(true); + setPriority(MIN_PRIORITY); + } + + public MiniCLI add(Command command) { + this.commands.put(command.cmdLine, command); + return this; + } + + @Override + public void run() { + print(); + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + while (true) { + try { + String cmdLine = br.readLine().toLowerCase(); + Command cmd = commands.get(cmdLine); + if(cmd!=null ) { + cmd.action.run(); + } else { + print(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private void print() { + System.out.flush(); + System.err.flush(); + for (Command command : commands.values()) { + System.out.println(format("[%s] %s", command.cmdLine, command.description)); + } + } +}