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 urlParams, IHTTPSession session); public Response put(UriResource uriResource, Map urlParams, IHTTPSession session); public Response post(UriResource uriResource, Map urlParams, IHTTPSession session); public Response delete(UriResource uriResource, Map urlParams, IHTTPSession session); public Response other(String method, UriResource uriResource, Map 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 urlParams, IHTTPSession session) { return NanoHTTPD.newChunkedResponse(getStatus(), getMimeType(), getData()); } public Response post(UriResource uriResource, Map urlParams, IHTTPSession session) { return get(uriResource, urlParams, session); } public Response put(UriResource uriResource, Map urlParams, IHTTPSession session) { return get(uriResource, urlParams, session); } public Response delete(UriResource uriResource, Map urlParams, IHTTPSession session) { return get(uriResource, urlParams, session); } public Response other(String method, UriResource uriResource, Map 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 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 urlParams, IHTTPSession session) { StringBuilder text = new StringBuilder(""); text.append("

Url: "); text.append(session.getUri()); text.append("


"); Map queryParams = session.getParms(); if (queryParams.size() > 0) { for (Map.Entry entry : queryParams.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); text.append("

Param '"); text.append(key); text.append("' = "); text.append(value); text.append("

"); } } else { text.append("

no params in url


"); } 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 pathArray = new ArrayList(); 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 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 "

Error 404: the requested page doesn't exist.

"; } @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 "

Hello world!

"; } @Override public String getMimeType() { return "text/html"; } @Override public IStatus getStatus() { return Status.OK; } } public static class NotImplementedHandler extends DefaultHandler { public String getText() { return "

The uri is mapped in the router, but no handler is specified.
Status: Not implemented!

"; } @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 EMPTY = Collections.unmodifiableMap(new HashMap()); private final String uri; private final Pattern uriPattern; private final int priority; private final Class handler; private final Object[] initParameter; private List uriParams = new ArrayList(); 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 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 initParameter(Class paramClazz) { return initParameter(0, paramClazz); } public T initParameter(int parameterIndex, Class paramClazz) { if (initParameter.length > parameterIndex) { return paramClazz.cast(initParameter[parameterIndex]); } LOG.severe("init parameter index not available " + parameterIndex); return null; } public Map match(String url) { Matcher matcher = uriPattern.matcher(url); if (matcher.matches()) { if (uriParams.size() > 0) { Map result = new HashMap(); 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 mappings; private UriResource error404Url; private Class notImplemented; public UriRouter() { mappings = new ArrayList(); } /** * 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 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() { @Override public int compare(UriResource o1, UriResource o2) { return o1.priority - o2.priority; } }); } private void removeRoute(String url) { String uriToDelete = normalizeUri(url); Iterator 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. * *
     * router.setNotFoundHandler(GeneralHandler.class);
     * 
*/ 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); } }