blob: 11f5d920dd6389375e3feed9044a3d9f9e3f4919 [file] [log] [blame]
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);
}
}