/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package spimedb.server; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Objects; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import io.undertow.Undertow; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import io.undertow.server.handlers.PathHandler; import io.undertow.server.handlers.cache.DirectBufferCache; import io.undertow.server.handlers.encoding.ContentEncodingRepository; import io.undertow.server.handlers.encoding.DeflateEncodingProvider; import io.undertow.server.handlers.encoding.EncodingHandler; import io.undertow.server.handlers.encoding.GzipEncodingProvider; import io.undertow.server.handlers.resource.CachingResourceManager; import io.undertow.server.handlers.resource.ClassPathResourceManager; import io.undertow.server.handlers.resource.FileResourceManager; import io.undertow.server.handlers.resource.ResourceHandler; import io.undertow.util.HttpString; import io.undertow.util.StatusCodes; import org.apache.commons.io.IOUtils; import org.apache.http.HttpStatus; import org.apache.lucene.facet.FacetResult; import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.suggest.Lookup; import org.eclipse.collections.api.set.ImmutableSet; import org.eclipse.collections.impl.factory.Sets; import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer; import org.jetbrains.annotations.Nullable; import org.slf4j.LoggerFactory; import org.xnio.BufferAllocator; import spimedb.FilteredNObject; import spimedb.NObject; import spimedb.SpimeDB; import spimedb.index.DObject; import spimedb.index.SearchResult; import spimedb.query.Query; import spimedb.util.HTTP; import spimedb.util.JSON; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Paths; import java.util.List; import java.util.function.BiConsumer; import java.util.stream.Stream; import static io.undertow.Handlers.resource; import static io.undertow.UndertowOptions.ENABLE_HTTP2; import static java.lang.Double.parseDouble; import static spimedb.util.HTTP.getStringParameter; /** * @author me * see: * https://docs.jboss.org/resteasy/docs/3.1.2.Final/userguide/html/ * https://docs.jboss.org/resteasy/docs/3.1.2.Final/userguide/html/RESTEasy_Embedded_Container.html#d4e1380 */ public class WebServer extends PathHandler { public UndertowJaxrsServer server; private static final org.slf4j.Logger logger = LoggerFactory.getLogger(WebServer.class); public static final String staticPath = Paths.get("src/main/resources/public/").toAbsolutePath().toString(); private static final int BUFFER_SIZE = 32 * 1024; private final SpimeDB db; @Deprecated private final double websocketOutputRateLimitBytesPerSecond = 64 * 1024; private int port = 0; private String host = null; static final ContentEncodingRepository compression = new ContentEncodingRepository() .addEncodingHandler("gzip", new GzipEncodingProvider(), 100) .addEncodingHandler("deflate", new DeflateEncodingProvider(), 50); //final Default nar = NARBuilder.newMultiThreadNAR(1, new RealTime.DS()); // @Override // public void handleRequest(HttpServerExchange exchange) throws Exception { // String s = exchange.getQueryString(); // nar.believe( // s.isEmpty() ? // // $.func( // $.the(exchange.getDestinationAddress().toString()), // $.quote(exchange.getRequestURL()) // ) // : // $.func( // $.the(exchange.getDestinationAddress().toString()), // $.quote(exchange.getRequestURL()), // $.the(s) ), // // Tense.Present // ); // // super.handleRequest(exchange); // } public WebServer(final SpimeDB db) { super(); this.db = db; // nar.log(); // nar.loop(10f); initStaticResource(db); // try { // addPrefixPath("/", WebdavServlet.get("/")); // } catch (ServletException e) { // logger.error("{}", e); // } addPrefixPath("/tag", ex -> HTTP.stream(ex, (o) -> { try { o.write(JSON.toJSONBytes(db.tags().stream().map(db::get).toArray(NObject[]::new))); } catch (IOException e) { logger.error("tag {}", e); } })); addPrefixPath("/suggest", ex -> HTTP.stream(ex, (o) -> { String qText = getStringParameter(ex, "q"); if (qText == null || (qText = qText.trim()).isEmpty()) return; try { List<Lookup.LookupResult> x = db.suggest(qText, 16); if (x != null) JSON.toJSON(Lists.transform(x, y -> y.key), o); } catch (Exception e) { logger.error("suggest: {}", e.getMessage()); /*(try { o.write(JSON.toJSONBytes(e)); } catch (IOException e1) { })*/ } })); addPrefixPath("/facet", ex -> HTTP.stream(ex, (o) -> { String dimension = getStringParameter(ex, "q"); if (dimension == null || (dimension = dimension.trim()).isEmpty()) return; try { FacetResult x = db.facets(dimension, 48); if (x != null) stream(o, x); } catch (Exception e) { logger.warn("facet: {}", e.getMessage()); } })); addPrefixPath("/thumbnail", ex -> { send(getStringParameter(ex, NObject.ID), "thumbnail", "image/jpg", ex); }); addPrefixPath("/data", ex -> { send(getStringParameter(ex, NObject.ID), "data", "application/pdf", ex); }); addPrefixPath("/earth", ex -> HTTP.stream(ex, (o) -> { String b = getStringParameter(ex, "r"); String[] bb = b.split("_"); if (bb.length != 4) { ex.setStatusCode(StatusCodes.BAD_REQUEST); return; } double[] lons = new double[2], lats = new double[2]; lons[0] = parseDouble(bb[0]); lats[0] = parseDouble(bb[1]); lons[1] = parseDouble(bb[2]); lats[1] = parseDouble(bb[3]); SearchResult r = db.get(new Query().limit(32).where(lons, lats)); send(r, o, searchResultSummary); })); addPrefixPath("/tell/json", (e) -> { //POST only if (e.getRequestMethod().equals(HttpString.tryFromString("POST"))) { //System.out.println(e); //System.out.println(e.getRequestHeaders()); e.getRequestReceiver().receiveFullString((ex, s) -> { JsonNode x = JSON.fromJSON(s); if (x != null) db.add(x); else { e.setStatusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY); } }); e.endExchange(); } }); addPrefixPath("/find", ex -> HTTP.stream(ex, (o) -> { String qText = getStringParameter(ex, "q"); if (qText == null || (qText = qText.trim()).isEmpty()) return; // db.add(new MutableNObject() // .name("find(\"" + qText + "\")") // //.withTags("") // .description(ex.toString()) // ); try { send(db.find(qText, 20), o, searchResultFull); } catch (ParseException f) { ex.setStatusCode(StatusCodes.BAD_REQUEST); } catch (Exception e) { logger.warn("{} -> {}", qText, e); ex.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); } })); /* client attention management */ //addPrefixPath("/client", websocket(new ClientSession(db, websocketOutputRateLimitBytesPerSecond))); // addPrefixPath("/anon", // websocket( new AnonymousSession(db) ) // ); // // addPrefixPath("/on/tag/", // //getRequestPath().substring(8) // websocket( AnonymousSession.tag(db, "public") ) ); // // addPrefixPath("/console", // //getRequestPath().substring(8) // websocket( new ConsoleSession(db) ) ); //addPrefixPath("/admin", websocket(new Admin(db))); restart(); } private void initStaticResource(SpimeDB db) { File staticPath = Paths.get(WebServer.staticPath).toFile(); File myStaticPath = db.file != null ? db.file.getParentFile().toPath().resolve("public").toFile() : null; int transferMinSize = 1024 * 1024; final int METADATA_MAX_AGE = 3 * 1000; //ms ResourceManagerChain res = new ResourceManagerChain(); if (db.indexPath != null && myStaticPath != null && myStaticPath.exists()) { //local override logger.info("static resource: {}", myStaticPath); res.add( new FileResourceManager(myStaticPath, transferMinSize, true, "/") ); } ResourceHandler rr; if (staticPath != null && staticPath.exists()) { //development mode: serve the files from the FS logger.info("static resource: {}", staticPath); res.add( new FileResourceManager(staticPath, transferMinSize, true, "/") ); rr = resource(res); } else { logger.info("static resource: (classloader)"); //production mode: serve from classpath res.add( new ClassPathResourceManager(getClass().getClassLoader(), "public") ); DirectBufferCache dataCache = new DirectBufferCache(1000, 10, 16 * 1024 * 1024, BufferAllocator.DIRECT_BYTE_BUFFER_ALLOCATOR, METADATA_MAX_AGE); CachingResourceManager cres = new CachingResourceManager( 100, transferMinSize /* max size */, dataCache, res, METADATA_MAX_AGE); rr = resource(cres); } rr.setCacheTime(24 * 60 * 60 * 1000); addPrefixPath("/", rr); } private void send(SearchResult r, OutputStream o, ImmutableSet<String> keys) { if (r != null) { try { o.write("[[".getBytes()); r.forEachDocument((y, x) -> { JSON.toJSON(searchResult( DObject.get(y), x, keys ), o, ','); return true; }); o.write("{}],".getBytes()); //<-- TODO search result metadata, query time etc if (r.facets != null) { stream(o, r.facets); o.write(']'); } else o.write("[]]".getBytes()); r.close(); } catch (IOException e) { } } //ex.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); } private void stream(OutputStream o, FacetResult x) { JSON.toJSON( Stream.of(x.labelValues).map(y -> new Object[]{y.label, y.value}).toArray(Object[]::new) /*Stream.of(x.labelValues).collect( Collectors.toMap(y->y.label, y->y.value ))*/, o); } public void setHost(String host) { if (!Objects.equal(this.host, host)) { this.host = host; restart(); } } private synchronized void restart() { String host = this.host; if (host == null) host = "0.0.0.0"; //any IPv4 if (port == 0) return; Undertow.Builder b = Undertow.builder() .addHttpListener(port, host) .setServerOption(ENABLE_HTTP2, true); if (compression != null) b.setHandler(new EncodingHandler(this, compression)); UndertowJaxrsServer nextServer = new UndertowJaxrsServer(); if (server != null) { try { logger.error("stop: {}", server); server.stop(); } catch (Exception e) { logger.error("http stop: {}", e); this.server = null; } } try { logger.info("listen {}:{}", host, port); (this.server = nextServer).start(b); // server.deploy(deployment() // .setDeploymentName("swagger") // .setContextPath("/swagger") // .setClassLoader(getClass().getClassLoader()) // .addServlet(servlet(Swagger.class)) // ); server.deploy(new WebAPI(this), "/api"); server.addResourcePrefixPath("/", new NotAServlet(this)); } catch (Exception e) { logger.error("http start: {}", e); this.server = null; } } /** * servlets - wtf!!!!!! */ private final static class NotAServlet extends ResourceHandler { private final HttpHandler notAServlet; public NotAServlet(HttpHandler thankfullyNotAServlet) { this.notAServlet = thankfullyNotAServlet; } @Override public void handleRequest(HttpServerExchange exchange) throws Exception { notAServlet.handleRequest(exchange); } } public void setPort(int port) { if (this.port != port) { this.port = port; restart(); } } private void send(@Nullable String id, String field, @Deprecated @Nullable String contentType, HttpServerExchange ex) { if (id != null) { DObject d = db.get(id); if (d != null) { Object f = d.get(field); if (f instanceof String) { //interpret the string stored at this as a URL or a redirect to another field String s = (String) f; switch (s) { case "data": if (!field.equals("data")) send(id, "data", contentType, ex); else { //infinite loop throw new UnsupportedOperationException("document field redirect cycle"); } break; default: if (s.startsWith("file:")) { File ff = new File(s.substring(5)); if (ff.exists()) { HTTP.stream(ex, (o) -> { try { IOUtils.copyLarge(new FileInputStream(ff), o, new byte[BUFFER_SIZE]); } catch (IOException e) { ex.setStatusCode(404); } }, contentType != null ? contentType : "text/plain"); } else { ex.setStatusCode(404); } } break; } } else if (f instanceof byte[]) { byte[] b = (byte[]) f; HTTP.stream(ex, (o) -> { try { o.write(b); } catch (IOException e) { } }, contentType != null ? contentType : "text/plain"); } else { ex.setStatusCode(404); } } else { ex.setStatusCode(404); } } else { ex.setStatusCode(404); } } public static final ImmutableSet<String> searchResultSummary = Sets.immutable.of( NObject.ID, NObject.NAME, NObject.INH, NObject.TAG, NObject.BOUND, "thumbnail", "score", NObject.LINESTRING, NObject.POLYGON, NObject.TYPE, "url" ); public static final ImmutableSet<String> searchResultFull = Sets.immutable.withAll(Iterables.concat(Sets.mutable.ofAll(searchResultSummary), Sets.immutable.of( NObject.DESC, "data" ))); private static FilteredNObject searchResult(NObject d, ScoreDoc x, ImmutableSet<String> keys) { return new FilteredNObject(d, keys) { @Override protected Object value(String key, Object v) { switch (key) { case "thumbnail": //rewrite the thumbnail blob byte[] as a String URL return d.id(); case "data": //rewrite the thumbnail blob byte[] as a String URL (if not already a string representing a URL) if (v instanceof byte[]) { return d.id(); } else if (v instanceof String) { String s = (String) v; if (s.startsWith("file:")) { return d.id(); //same as if it's a byte } else { return s; } } else { //?? } } return v; } @Override public void forEach(BiConsumer<String, Object> each) { super.forEach(each); each.accept("score", x.score); } }; } // @Test // public void testApplicationPath() throws Exception // { // server.deploy(MyApp.class); // Client client = ClientBuilder.newClient(); // String val = client.target(TestPortProvider.generateURL("/base/test")) // .request().get(String.class); // Assert.assertEquals("hello world", val); // client.close(); // } // // @Test // public void testApplicationContext() throws Exception // { // server.deploy(MyApp.class, "/root"); // Client client = ClientBuilder.newClient(); // String val = client.target(TestPortProvider.generateURL("/root/test")) // .request().get(String.class); // Assert.assertEquals("hello world", val); // client.close(); // } // // @Test // public void testDeploymentInfo() throws Exception // { // DeploymentInfo di = server.undertowDeployment(MyApp.class); // di.setContextPath("/di"); // di.setDeploymentName("DI"); // server.deploy(di); // Client client = ClientBuilder.newClient(); // String val = client.target(TestPortProvider.generateURL("/di/base/test")) // .request().get(String.class); // Assert.assertEquals("hello world", val); // client.close(); // } } //.setDirectoryListingEnabled(true) //.setHandler(path().addPrefixPath("/", ClientResources.handleClientResources()) // addPrefixPath("/tag/meta", new HttpHandler() { // // @Override // public void handleRequest(HttpServerExchange ex) throws Exception { // // sendTags( // db.searchID( // getStringArrayParameter(ex, "id"), 0, 60, "tag" // ), // ex); // // } // // }); // addPrefixPath("/style/meta", new HttpHandler() { // // @Override // public void handleRequest(HttpServerExchange ex) throws Exception { // // send(json( // db.searchID( // getStringArrayParameter(ex, "id"), 0, 60, "style" // )), // ex); // // } // // }); //CORS fucking sucks /* .header("Access-Control-Allow-Origin", "*") .header("Access-Control-Allow-Headers", "origin, content-type, accept, authorization") .header("Access-Control-Allow-CredentialMax-Age", "1209600") */ //https://github.com/undertow-io/undertow/blob/master/examples/src/main/java/io/undertow/examples/sessionhandling/SessionServer.java // addPrefixPath("/socket", new WebSocketCore( // index // ).handler()); // // addPrefixPath("/tag/index", new ChannelSnapshot(index)); // // // // addPrefixPath("/tag", (new WebSocketCore() { // // final String cachePath = "cache"; // final int cacheProxyPort = 16000; // // @Override // public synchronized Channel getChannel(WebSocketCore.WebSocketConnection socket, String id) { // Channel c = super.getChannel(socket, id); // // if (c == null) { // //Tag t = new Tag(id, id); // c = new ElasticChannel(db, id, "tag"); // super.addChannel(c); // } // // return c; // } // // @Override // protected void onOperation(String operation, Channel c, JsonNode param, WebSocketChannel socket) { // // //TODO prevent interrupting update operation if already in-progress // switch (operation) { // case "update": // try { // ObjectNode meta = (ObjectNode) c.getSnapshot().get("meta"); // if (meta!=null && meta.has("kmlLayer")) { // String kml = meta.get("kmlLayer").textValue(); // // { // ObjectNode nc = c.getSnapshot(); // meta = (ObjectNode) nc.get("meta"); // // meta.put("status", "Updating"); // c.commit(nc); // } // // System.out.println("Updating " + c); // // //TODO replace proxy with HttpRequestCached: //// try { //// new ImportKML(db, cache.proxy, c.id, kml).run(); //// } catch (Exception e) { //// ObjectNode nc = c.getSnapshot(); //// meta = (ObjectNode) nc.get("meta"); //// meta.put("status", e.toString()); //// c.commit(nc); //// throw e; //// } // // { // ObjectNode nc = c.getSnapshot(); // meta = (ObjectNode) nc.get("meta"); // // meta.put("status", "Ready"); // meta.put("modifiedAt", new Date().getTime()); // c.commit(nc); // // } // // } // } catch (Exception e) { // e.printStackTrace(); // } // // break; // } // // } // // }).handler()); // // // //addPrefixPath("/wikipedia", new Wikipedia());