• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package fi.iki.elonen.router;
2 
3 /*
4  * #%L
5  * NanoHttpd-Samples
6  * %%
7  * Copyright (C) 2012 - 2015 nanohttpd
8  * %%
9  * Redistribution and use in source and binary forms, with or without modification,
10  * are permitted provided that the following conditions are met:
11  *
12  * 1. Redistributions of source code must retain the above copyright notice, this
13  *    list of conditions and the following disclaimer.
14  *
15  * 2. Redistributions in binary form must reproduce the above copyright notice,
16  *    this list of conditions and the following disclaimer in the documentation
17  *    and/or other materials provided with the distribution.
18  *
19  * 3. Neither the name of the nanohttpd nor the names of its contributors
20  *    may be used to endorse or promote products derived from this software without
21  *    specific prior written permission.
22  *
23  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
24  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
26  * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
27  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
28  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
29  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
30  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
31  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
32  * OF THE POSSIBILITY OF SUCH DAMAGE.
33  * #L%
34  */
35 
36 import java.io.BufferedInputStream;
37 import java.io.File;
38 import java.io.FileInputStream;
39 import java.io.IOException;
40 import java.io.InputStream;
41 import java.util.ArrayList;
42 import java.util.Collections;
43 import java.util.Comparator;
44 import java.util.HashMap;
45 import java.util.Iterator;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.logging.Level;
49 import java.util.logging.Logger;
50 import java.util.regex.Matcher;
51 import java.util.regex.Pattern;
52 
53 import fi.iki.elonen.NanoHTTPD;
54 import fi.iki.elonen.NanoHTTPD.Response.IStatus;
55 import fi.iki.elonen.NanoHTTPD.Response.Status;
56 
57 /**
58  * @author vnnv
59  * @author ritchieGitHub
60  */
61 public class RouterNanoHTTPD extends NanoHTTPD {
62 
63     /**
64      * logger to log to.
65      */
66     private static final Logger LOG = Logger.getLogger(RouterNanoHTTPD.class.getName());
67 
68     public interface UriResponder {
69 
get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session)70         public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session);
71 
put(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session)72         public Response put(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session);
73 
post(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session)74         public Response post(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session);
75 
delete(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session)76         public Response delete(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session);
77 
other(String method, UriResource uriResource, Map<String, String> urlParams, IHTTPSession session)78         public Response other(String method, UriResource uriResource, Map<String, String> urlParams, IHTTPSession session);
79     }
80 
81     /**
82      * General nanolet to inherit from if you provide stream data, only chucked
83      * responses will be generated.
84      */
85     public static abstract class DefaultStreamHandler implements UriResponder {
86 
getMimeType()87         public abstract String getMimeType();
88 
getStatus()89         public abstract IStatus getStatus();
90 
getData()91         public abstract InputStream getData();
92 
get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session)93         public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
94             return NanoHTTPD.newChunkedResponse(getStatus(), getMimeType(), getData());
95         }
96 
post(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session)97         public Response post(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
98             return get(uriResource, urlParams, session);
99         }
100 
put(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session)101         public Response put(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
102             return get(uriResource, urlParams, session);
103         }
104 
delete(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session)105         public Response delete(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
106             return get(uriResource, urlParams, session);
107         }
108 
other(String method, UriResource uriResource, Map<String, String> urlParams, IHTTPSession session)109         public Response other(String method, UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
110             return get(uriResource, urlParams, session);
111         }
112     }
113 
114     /**
115      * General nanolet to inherit from if you provide text or html data, only
116      * fixed size responses will be generated.
117      */
118     public static abstract class DefaultHandler extends DefaultStreamHandler {
119 
getText()120         public abstract String getText();
121 
getStatus()122         public abstract IStatus getStatus();
123 
get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session)124         public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
125             return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), getText());
126         }
127 
128         @Override
getData()129         public InputStream getData() {
130             throw new IllegalStateException("this method should not be called in a text based nanolet");
131         }
132     }
133 
134     /**
135      * General nanolet to print debug info's as a html page.
136      */
137     public static class GeneralHandler extends DefaultHandler {
138 
139         @Override
getText()140         public String getText() {
141             throw new IllegalStateException("this method should not be called");
142         }
143 
144         @Override
getMimeType()145         public String getMimeType() {
146             return "text/html";
147         }
148 
149         @Override
getStatus()150         public IStatus getStatus() {
151             return Status.OK;
152         }
153 
get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session)154         public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
155             StringBuilder text = new StringBuilder("<html><body>");
156             text.append("<h1>Url: ");
157             text.append(session.getUri());
158             text.append("</h1><br>");
159             Map<String, String> queryParams = session.getParms();
160             if (queryParams.size() > 0) {
161                 for (Map.Entry<String, String> entry : queryParams.entrySet()) {
162                     String key = entry.getKey();
163                     String value = entry.getValue();
164                     text.append("<p>Param '");
165                     text.append(key);
166                     text.append("' = ");
167                     text.append(value);
168                     text.append("</p>");
169                 }
170             } else {
171                 text.append("<p>no params in url</p><br>");
172             }
173             return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), text.toString());
174         }
175     }
176 
177     /**
178      * General nanolet to print debug info's as a html page.
179      */
180     public static class StaticPageHandler extends DefaultHandler {
181 
getPathArray(String uri)182         private static String[] getPathArray(String uri) {
183             String array[] = uri.split("/");
184             ArrayList<String> pathArray = new ArrayList<String>();
185 
186             for (String s : array) {
187                 if (s.length() > 0)
188                     pathArray.add(s);
189             }
190 
191             return pathArray.toArray(new String[]{});
192 
193         }
194 
195         @Override
getText()196         public String getText() {
197             throw new IllegalStateException("this method should not be called");
198         }
199 
200         @Override
getMimeType()201         public String getMimeType() {
202             throw new IllegalStateException("this method should not be called");
203         }
204 
205         @Override
getStatus()206         public IStatus getStatus() {
207             return Status.OK;
208         }
209 
get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session)210         public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
211             String baseUri = uriResource.getUri();
212             String realUri = normalizeUri(session.getUri());
213             for (int index = 0; index < Math.min(baseUri.length(), realUri.length()); index++) {
214                 if (baseUri.charAt(index) != realUri.charAt(index)) {
215                     realUri = normalizeUri(realUri.substring(index));
216                     break;
217                 }
218             }
219             File fileOrdirectory = uriResource.initParameter(File.class);
220             for (String pathPart : getPathArray(realUri)) {
221                 fileOrdirectory = new File(fileOrdirectory, pathPart);
222             }
223             if (fileOrdirectory.isDirectory()) {
224                 fileOrdirectory = new File(fileOrdirectory, "index.html");
225                 if (!fileOrdirectory.exists()) {
226                     fileOrdirectory = new File(fileOrdirectory.getParentFile(), "index.htm");
227                 }
228             }
229             if (!fileOrdirectory.exists() || !fileOrdirectory.isFile()) {
230                 return new Error404UriHandler().get(uriResource, urlParams, session);
231             } else {
232                 try {
233                     return NanoHTTPD.newChunkedResponse(getStatus(), getMimeTypeForFile(fileOrdirectory.getName()), fileToInputStream(fileOrdirectory));
234                 } catch (IOException ioe) {
235                     return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.REQUEST_TIMEOUT, "text/plain", null);
236                 }
237             }
238         }
239 
fileToInputStream(File fileOrdirectory)240         protected BufferedInputStream fileToInputStream(File fileOrdirectory) throws IOException {
241             return new BufferedInputStream(new FileInputStream(fileOrdirectory));
242         }
243     }
244 
245     /**
246      * Handling error 404 - unrecognized urls
247      */
248     public static class Error404UriHandler extends DefaultHandler {
249 
getText()250         public String getText() {
251             return "<html><body><h3>Error 404: the requested page doesn't exist.</h3></body></html>";
252         }
253 
254         @Override
getMimeType()255         public String getMimeType() {
256             return "text/html";
257         }
258 
259         @Override
getStatus()260         public IStatus getStatus() {
261             return Status.NOT_FOUND;
262         }
263     }
264 
265     /**
266      * Handling index
267      */
268     public static class IndexHandler extends DefaultHandler {
269 
getText()270         public String getText() {
271             return "<html><body><h2>Hello world!</h3></body></html>";
272         }
273 
274         @Override
getMimeType()275         public String getMimeType() {
276             return "text/html";
277         }
278 
279         @Override
getStatus()280         public IStatus getStatus() {
281             return Status.OK;
282         }
283 
284     }
285 
286     public static class NotImplementedHandler extends DefaultHandler {
287 
getText()288         public String getText() {
289             return "<html><body><h2>The uri is mapped in the router, but no handler is specified. <br> Status: Not implemented!</h3></body></html>";
290         }
291 
292         @Override
getMimeType()293         public String getMimeType() {
294             return "text/html";
295         }
296 
297         @Override
getStatus()298         public IStatus getStatus() {
299             return Status.OK;
300         }
301     }
302 
normalizeUri(String value)303     public static String normalizeUri(String value) {
304         if (value == null) {
305             return value;
306         }
307         if (value.startsWith("/")) {
308             value = value.substring(1);
309         }
310         if (value.endsWith("/")) {
311             value = value.substring(0, value.length() - 1);
312         }
313         return value;
314 
315     }
316 
317     public static class UriResource {
318 
319         private static final Pattern PARAM_PATTERN = Pattern.compile("(?<=(^|/)):[a-zA-Z0-9_-]+(?=(/|$))");
320 
321         private static final String PARAM_MATCHER = "([A-Za-z0-9\\-\\._~:/?#\\[\\]@!\\$&'\\(\\)\\*\\+,;=]+)";
322 
323         private static final Map<String, String> EMPTY = Collections.unmodifiableMap(new HashMap<String, String>());
324 
325         private final String uri;
326 
327         private final Pattern uriPattern;
328 
329         private final int priority;
330 
331         private final Class<?> handler;
332 
333         private final Object[] initParameter;
334 
335         private List<String> uriParams = new ArrayList<String>();
336 
UriResource(String uri, int priority, Class<?> handler, Object... initParameter)337         public UriResource(String uri, int priority, Class<?> handler, Object... initParameter) {
338             this.handler = handler;
339             this.initParameter = initParameter;
340             if (uri != null) {
341                 this.uri = normalizeUri(uri);
342                 parse();
343                 this.uriPattern = createUriPattern();
344             } else {
345                 this.uriPattern = null;
346                 this.uri = null;
347             }
348             this.priority = priority + uriParams.size() * 1000;
349         }
350 
parse()351         private void parse() {
352         }
353 
createUriPattern()354         private Pattern createUriPattern() {
355             String patternUri = uri;
356             Matcher matcher = PARAM_PATTERN.matcher(patternUri);
357             int start = 0;
358             while (matcher.find(start)) {
359                 uriParams.add(patternUri.substring(matcher.start() + 1, matcher.end()));
360                 patternUri = new StringBuilder(patternUri.substring(0, matcher.start()))//
361                         .append(PARAM_MATCHER)//
362                         .append(patternUri.substring(matcher.end())).toString();
363                 start = matcher.start() + PARAM_MATCHER.length();
364                 matcher = PARAM_PATTERN.matcher(patternUri);
365             }
366             return Pattern.compile(patternUri);
367         }
368 
process(Map<String, String> urlParams, IHTTPSession session)369         public Response process(Map<String, String> urlParams, IHTTPSession session) {
370             String error = "General error!";
371             if (handler != null) {
372                 try {
373                     Object object = handler.newInstance();
374                     if (object instanceof UriResponder) {
375                         UriResponder responder = (UriResponder) object;
376                         switch (session.getMethod()) {
377                             case GET:
378                                 return responder.get(this, urlParams, session);
379                             case POST:
380                                 return responder.post(this, urlParams, session);
381                             case PUT:
382                                 return responder.put(this, urlParams, session);
383                             case DELETE:
384                                 return responder.delete(this, urlParams, session);
385                             default:
386                                 return responder.other(session.getMethod().toString(), this, urlParams, session);
387                         }
388                     } else {
389                         return NanoHTTPD.newFixedLengthResponse(Status.OK, "text/plain", //
390                                 new StringBuilder("Return: ")//
391                                         .append(handler.getCanonicalName())//
392                                         .append(".toString() -> ")//
393                                         .append(object)//
394                                         .toString());
395                     }
396                 } catch (Exception e) {
397                     error = "Error: " + e.getClass().getName() + " : " + e.getMessage();
398                     LOG.log(Level.SEVERE, error, e);
399                 }
400             }
401             return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR, "text/plain", error);
402         }
403 
404         @Override
toString()405         public String toString() {
406             return new StringBuilder("UrlResource{uri='").append((uri == null ? "/" : uri))//
407                     .append("', urlParts=").append(uriParams)//
408                     .append('}')//
409                     .toString();
410         }
411 
getUri()412         public String getUri() {
413             return uri;
414         }
415 
initParameter(Class<T> paramClazz)416         public <T> T initParameter(Class<T> paramClazz) {
417             return initParameter(0, paramClazz);
418         }
419 
initParameter(int parameterIndex, Class<T> paramClazz)420         public <T> T initParameter(int parameterIndex, Class<T> paramClazz) {
421             if (initParameter.length > parameterIndex) {
422                 return paramClazz.cast(initParameter[parameterIndex]);
423             }
424             LOG.severe("init parameter index not available " + parameterIndex);
425             return null;
426         }
427 
match(String url)428         public Map<String, String> match(String url) {
429             Matcher matcher = uriPattern.matcher(url);
430             if (matcher.matches()) {
431                 if (uriParams.size() > 0) {
432                     Map<String, String> result = new HashMap<String, String>();
433                     for (int i = 1; i <= matcher.groupCount(); i++) {
434                         result.put(uriParams.get(i - 1), matcher.group(i));
435                     }
436                     return result;
437                 } else {
438                     return EMPTY;
439                 }
440             }
441             return null;
442         }
443 
444     }
445 
446     public static class UriRouter {
447 
448         private List<UriResource> mappings;
449 
450         private UriResource error404Url;
451 
452         private Class<?> notImplemented;
453 
UriRouter()454         public UriRouter() {
455             mappings = new ArrayList<UriResource>();
456         }
457 
458         /**
459          * Search in the mappings if the given url matches some of the rules If
460          * there are more than one marches returns the rule with less parameters
461          * e.g. mapping 1 = /user/:id mapping 2 = /user/help if the incoming uri
462          * is www.example.com/user/help - mapping 2 is returned if the incoming
463          * uri is www.example.com/user/3232 - mapping 1 is returned
464          *
465          * @param url
466          * @return
467          */
process(IHTTPSession session)468         public Response process(IHTTPSession session) {
469             String work = normalizeUri(session.getUri());
470             Map<String, String> params = null;
471             UriResource uriResource = error404Url;
472             for (UriResource u : mappings) {
473                 params = u.match(work);
474                 if (params != null) {
475                     uriResource = u;
476                     break;
477                 }
478             }
479             return uriResource.process(params, session);
480         }
481 
addRoute(String url, int priority, Class<?> handler, Object... initParameter)482         private void addRoute(String url, int priority, Class<?> handler, Object... initParameter) {
483             if (url != null) {
484                 if (handler != null) {
485                     mappings.add(new UriResource(url, priority + mappings.size(), handler, initParameter));
486                 } else {
487                     mappings.add(new UriResource(url, priority + mappings.size(), notImplemented));
488                 }
489                 sortMappings();
490             }
491         }
492 
sortMappings()493         private void sortMappings() {
494             Collections.sort(mappings, new Comparator<UriResource>() {
495 
496                 @Override
497                 public int compare(UriResource o1, UriResource o2) {
498                     return o1.priority - o2.priority;
499                 }
500             });
501         }
502 
removeRoute(String url)503         private void removeRoute(String url) {
504             String uriToDelete = normalizeUri(url);
505             Iterator<UriResource> iter = mappings.iterator();
506             while (iter.hasNext()) {
507                 UriResource uriResource = iter.next();
508                 if (uriToDelete.equals(uriResource.getUri())) {
509                     iter.remove();
510                     break;
511                 }
512             }
513         }
514 
setNotFoundHandler(Class<?> handler)515         public void setNotFoundHandler(Class<?> handler) {
516             error404Url = new UriResource(null, 100, handler);
517         }
518 
setNotImplemented(Class<?> handler)519         public void setNotImplemented(Class<?> handler) {
520             notImplemented = handler;
521         }
522 
523     }
524 
525     private UriRouter router;
526 
RouterNanoHTTPD(int port)527     public RouterNanoHTTPD(int port) {
528         super(port);
529         router = new UriRouter();
530     }
531 
532     /**
533      * default routings, they are over writable.
534      *
535      * <pre>
536      * router.setNotFoundHandler(GeneralHandler.class);
537      * </pre>
538      */
539 
addMappings()540     public void addMappings() {
541         router.setNotImplemented(NotImplementedHandler.class);
542         router.setNotFoundHandler(Error404UriHandler.class);
543         router.addRoute("/", Integer.MAX_VALUE / 2, IndexHandler.class);
544         router.addRoute("/index.html", Integer.MAX_VALUE / 2, IndexHandler.class);
545     }
546 
addRoute(String url, Class<?> handler, Object... initParameter)547     public void addRoute(String url, Class<?> handler, Object... initParameter) {
548         router.addRoute(url, 100, handler, initParameter);
549     }
550 
removeRoute(String url)551     public void removeRoute(String url) {
552         router.removeRoute(url);
553     }
554 
555     @Override
serve(IHTTPSession session)556     public Response serve(IHTTPSession session) {
557         // Try to find match
558         return router.process(session);
559     }
560 }
561