diff options
Diffstat (limited to 'nanolets/src/main/java/fi/iki/elonen/router/RouterNanoHTTPD.java')
-rw-r--r-- | nanolets/src/main/java/fi/iki/elonen/router/RouterNanoHTTPD.java | 560 |
1 files changed, 560 insertions, 0 deletions
diff --git a/nanolets/src/main/java/fi/iki/elonen/router/RouterNanoHTTPD.java b/nanolets/src/main/java/fi/iki/elonen/router/RouterNanoHTTPD.java new file mode 100644 index 0000000..11f5d92 --- /dev/null +++ b/nanolets/src/main/java/fi/iki/elonen/router/RouterNanoHTTPD.java @@ -0,0 +1,560 @@ +package fi.iki.elonen.router; + +/* + * #%L + * NanoHttpd-Samples + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import fi.iki.elonen.NanoHTTPD; +import fi.iki.elonen.NanoHTTPD.Response.IStatus; +import fi.iki.elonen.NanoHTTPD.Response.Status; + +/** + * @author vnnv + * @author ritchieGitHub + */ +public class RouterNanoHTTPD extends NanoHTTPD { + + /** + * logger to log to. + */ + private static final Logger LOG = Logger.getLogger(RouterNanoHTTPD.class.getName()); + + public interface UriResponder { + + public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session); + + public Response put(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session); + + public Response post(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session); + + public Response delete(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session); + + public Response other(String method, UriResource uriResource, Map<String, String> urlParams, IHTTPSession session); + } + + /** + * General nanolet to inherit from if you provide stream data, only chucked + * responses will be generated. + */ + public static abstract class DefaultStreamHandler implements UriResponder { + + public abstract String getMimeType(); + + public abstract IStatus getStatus(); + + public abstract InputStream getData(); + + public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) { + return NanoHTTPD.newChunkedResponse(getStatus(), getMimeType(), getData()); + } + + public Response post(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) { + return get(uriResource, urlParams, session); + } + + public Response put(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) { + return get(uriResource, urlParams, session); + } + + public Response delete(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) { + return get(uriResource, urlParams, session); + } + + public Response other(String method, UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) { + return get(uriResource, urlParams, session); + } + } + + /** + * General nanolet to inherit from if you provide text or html data, only + * fixed size responses will be generated. + */ + public static abstract class DefaultHandler extends DefaultStreamHandler { + + public abstract String getText(); + + public abstract IStatus getStatus(); + + public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) { + return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), getText()); + } + + @Override + public InputStream getData() { + throw new IllegalStateException("this method should not be called in a text based nanolet"); + } + } + + /** + * General nanolet to print debug info's as a html page. + */ + public static class GeneralHandler extends DefaultHandler { + + @Override + public String getText() { + throw new IllegalStateException("this method should not be called"); + } + + @Override + public String getMimeType() { + return "text/html"; + } + + @Override + public IStatus getStatus() { + return Status.OK; + } + + public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) { + StringBuilder text = new StringBuilder("<html><body>"); + text.append("<h1>Url: "); + text.append(session.getUri()); + text.append("</h1><br>"); + Map<String, String> queryParams = session.getParms(); + if (queryParams.size() > 0) { + for (Map.Entry<String, String> entry : queryParams.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + text.append("<p>Param '"); + text.append(key); + text.append("' = "); + text.append(value); + text.append("</p>"); + } + } else { + text.append("<p>no params in url</p><br>"); + } + return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), text.toString()); + } + } + + /** + * General nanolet to print debug info's as a html page. + */ + public static class StaticPageHandler extends DefaultHandler { + + private static String[] getPathArray(String uri) { + String array[] = uri.split("/"); + ArrayList<String> pathArray = new ArrayList<String>(); + + for (String s : array) { + if (s.length() > 0) + pathArray.add(s); + } + + return pathArray.toArray(new String[]{}); + + } + + @Override + public String getText() { + throw new IllegalStateException("this method should not be called"); + } + + @Override + public String getMimeType() { + throw new IllegalStateException("this method should not be called"); + } + + @Override + public IStatus getStatus() { + return Status.OK; + } + + public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) { + String baseUri = uriResource.getUri(); + String realUri = normalizeUri(session.getUri()); + for (int index = 0; index < Math.min(baseUri.length(), realUri.length()); index++) { + if (baseUri.charAt(index) != realUri.charAt(index)) { + realUri = normalizeUri(realUri.substring(index)); + break; + } + } + File fileOrdirectory = uriResource.initParameter(File.class); + for (String pathPart : getPathArray(realUri)) { + fileOrdirectory = new File(fileOrdirectory, pathPart); + } + if (fileOrdirectory.isDirectory()) { + fileOrdirectory = new File(fileOrdirectory, "index.html"); + if (!fileOrdirectory.exists()) { + fileOrdirectory = new File(fileOrdirectory.getParentFile(), "index.htm"); + } + } + if (!fileOrdirectory.exists() || !fileOrdirectory.isFile()) { + return new Error404UriHandler().get(uriResource, urlParams, session); + } else { + try { + return NanoHTTPD.newChunkedResponse(getStatus(), getMimeTypeForFile(fileOrdirectory.getName()), fileToInputStream(fileOrdirectory)); + } catch (IOException ioe) { + return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.REQUEST_TIMEOUT, "text/plain", null); + } + } + } + + protected BufferedInputStream fileToInputStream(File fileOrdirectory) throws IOException { + return new BufferedInputStream(new FileInputStream(fileOrdirectory)); + } + } + + /** + * Handling error 404 - unrecognized urls + */ + public static class Error404UriHandler extends DefaultHandler { + + public String getText() { + return "<html><body><h3>Error 404: the requested page doesn't exist.</h3></body></html>"; + } + + @Override + public String getMimeType() { + return "text/html"; + } + + @Override + public IStatus getStatus() { + return Status.NOT_FOUND; + } + } + + /** + * Handling index + */ + public static class IndexHandler extends DefaultHandler { + + public String getText() { + return "<html><body><h2>Hello world!</h3></body></html>"; + } + + @Override + public String getMimeType() { + return "text/html"; + } + + @Override + public IStatus getStatus() { + return Status.OK; + } + + } + + public static class NotImplementedHandler extends DefaultHandler { + + public String getText() { + return "<html><body><h2>The uri is mapped in the router, but no handler is specified. <br> Status: Not implemented!</h3></body></html>"; + } + + @Override + public String getMimeType() { + return "text/html"; + } + + @Override + public IStatus getStatus() { + return Status.OK; + } + } + + public static String normalizeUri(String value) { + if (value == null) { + return value; + } + if (value.startsWith("/")) { + value = value.substring(1); + } + if (value.endsWith("/")) { + value = value.substring(0, value.length() - 1); + } + return value; + + } + + public static class UriResource { + + private static final Pattern PARAM_PATTERN = Pattern.compile("(?<=(^|/)):[a-zA-Z0-9_-]+(?=(/|$))"); + + private static final String PARAM_MATCHER = "([A-Za-z0-9\\-\\._~:/?#\\[\\]@!\\$&'\\(\\)\\*\\+,;=]+)"; + + private static final Map<String, String> EMPTY = Collections.unmodifiableMap(new HashMap<String, String>()); + + private final String uri; + + private final Pattern uriPattern; + + private final int priority; + + private final Class<?> handler; + + private final Object[] initParameter; + + private List<String> uriParams = new ArrayList<String>(); + + public UriResource(String uri, int priority, Class<?> handler, Object... initParameter) { + this.handler = handler; + this.initParameter = initParameter; + if (uri != null) { + this.uri = normalizeUri(uri); + parse(); + this.uriPattern = createUriPattern(); + } else { + this.uriPattern = null; + this.uri = null; + } + this.priority = priority + uriParams.size() * 1000; + } + + private void parse() { + } + + private Pattern createUriPattern() { + String patternUri = uri; + Matcher matcher = PARAM_PATTERN.matcher(patternUri); + int start = 0; + while (matcher.find(start)) { + uriParams.add(patternUri.substring(matcher.start() + 1, matcher.end())); + patternUri = new StringBuilder(patternUri.substring(0, matcher.start()))// + .append(PARAM_MATCHER)// + .append(patternUri.substring(matcher.end())).toString(); + start = matcher.start() + PARAM_MATCHER.length(); + matcher = PARAM_PATTERN.matcher(patternUri); + } + return Pattern.compile(patternUri); + } + + public Response process(Map<String, String> urlParams, IHTTPSession session) { + String error = "General error!"; + if (handler != null) { + try { + Object object = handler.newInstance(); + if (object instanceof UriResponder) { + UriResponder responder = (UriResponder) object; + switch (session.getMethod()) { + case GET: + return responder.get(this, urlParams, session); + case POST: + return responder.post(this, urlParams, session); + case PUT: + return responder.put(this, urlParams, session); + case DELETE: + return responder.delete(this, urlParams, session); + default: + return responder.other(session.getMethod().toString(), this, urlParams, session); + } + } else { + return NanoHTTPD.newFixedLengthResponse(Status.OK, "text/plain", // + new StringBuilder("Return: ")// + .append(handler.getCanonicalName())// + .append(".toString() -> ")// + .append(object)// + .toString()); + } + } catch (Exception e) { + error = "Error: " + e.getClass().getName() + " : " + e.getMessage(); + LOG.log(Level.SEVERE, error, e); + } + } + return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR, "text/plain", error); + } + + @Override + public String toString() { + return new StringBuilder("UrlResource{uri='").append((uri == null ? "/" : uri))// + .append("', urlParts=").append(uriParams)// + .append('}')// + .toString(); + } + + public String getUri() { + return uri; + } + + public <T> T initParameter(Class<T> paramClazz) { + return initParameter(0, paramClazz); + } + + public <T> T initParameter(int parameterIndex, Class<T> paramClazz) { + if (initParameter.length > parameterIndex) { + return paramClazz.cast(initParameter[parameterIndex]); + } + LOG.severe("init parameter index not available " + parameterIndex); + return null; + } + + public Map<String, String> match(String url) { + Matcher matcher = uriPattern.matcher(url); + if (matcher.matches()) { + if (uriParams.size() > 0) { + Map<String, String> result = new HashMap<String, String>(); + for (int i = 1; i <= matcher.groupCount(); i++) { + result.put(uriParams.get(i - 1), matcher.group(i)); + } + return result; + } else { + return EMPTY; + } + } + return null; + } + + } + + public static class UriRouter { + + private List<UriResource> mappings; + + private UriResource error404Url; + + private Class<?> notImplemented; + + public UriRouter() { + mappings = new ArrayList<UriResource>(); + } + + /** + * Search in the mappings if the given url matches some of the rules If + * there are more than one marches returns the rule with less parameters + * e.g. mapping 1 = /user/:id mapping 2 = /user/help if the incoming uri + * is www.example.com/user/help - mapping 2 is returned if the incoming + * uri is www.example.com/user/3232 - mapping 1 is returned + * + * @param url + * @return + */ + public Response process(IHTTPSession session) { + String work = normalizeUri(session.getUri()); + Map<String, String> params = null; + UriResource uriResource = error404Url; + for (UriResource u : mappings) { + params = u.match(work); + if (params != null) { + uriResource = u; + break; + } + } + return uriResource.process(params, session); + } + + private void addRoute(String url, int priority, Class<?> handler, Object... initParameter) { + if (url != null) { + if (handler != null) { + mappings.add(new UriResource(url, priority + mappings.size(), handler, initParameter)); + } else { + mappings.add(new UriResource(url, priority + mappings.size(), notImplemented)); + } + sortMappings(); + } + } + + private void sortMappings() { + Collections.sort(mappings, new Comparator<UriResource>() { + + @Override + public int compare(UriResource o1, UriResource o2) { + return o1.priority - o2.priority; + } + }); + } + + private void removeRoute(String url) { + String uriToDelete = normalizeUri(url); + Iterator<UriResource> iter = mappings.iterator(); + while (iter.hasNext()) { + UriResource uriResource = iter.next(); + if (uriToDelete.equals(uriResource.getUri())) { + iter.remove(); + break; + } + } + } + + public void setNotFoundHandler(Class<?> handler) { + error404Url = new UriResource(null, 100, handler); + } + + public void setNotImplemented(Class<?> handler) { + notImplemented = handler; + } + + } + + private UriRouter router; + + public RouterNanoHTTPD(int port) { + super(port); + router = new UriRouter(); + } + + /** + * default routings, they are over writable. + * + * <pre> + * router.setNotFoundHandler(GeneralHandler.class); + * </pre> + */ + + public void addMappings() { + router.setNotImplemented(NotImplementedHandler.class); + router.setNotFoundHandler(Error404UriHandler.class); + router.addRoute("/", Integer.MAX_VALUE / 2, IndexHandler.class); + router.addRoute("/index.html", Integer.MAX_VALUE / 2, IndexHandler.class); + } + + public void addRoute(String url, Class<?> handler, Object... initParameter) { + router.addRoute(url, 100, handler, initParameter); + } + + public void removeRoute(String url) { + router.removeRoute(url); + } + + @Override + public Response serve(IHTTPSession session) { + // Try to find match + return router.process(session); + } +} |