1package 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
36import java.io.BufferedInputStream;
37import java.io.File;
38import java.io.FileInputStream;
39import java.io.IOException;
40import java.io.InputStream;
41import java.util.ArrayList;
42import java.util.Collections;
43import java.util.Comparator;
44import java.util.HashMap;
45import java.util.Iterator;
46import java.util.List;
47import java.util.Map;
48import java.util.logging.Level;
49import java.util.logging.Logger;
50import java.util.regex.Matcher;
51import java.util.regex.Pattern;
52
53import fi.iki.elonen.NanoHTTPD;
54import fi.iki.elonen.NanoHTTPD.Response.IStatus;
55import fi.iki.elonen.NanoHTTPD.Response.Status;
56
57/**
58 * @author vnnv
59 * @author ritchieGitHub
60 */
61public 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
70        public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session);
71
72        public Response put(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session);
73
74        public Response post(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session);
75
76        public Response delete(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session);
77
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
87        public abstract String getMimeType();
88
89        public abstract IStatus getStatus();
90
91        public abstract InputStream getData();
92
93        public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
94            return NanoHTTPD.newChunkedResponse(getStatus(), getMimeType(), getData());
95        }
96
97        public Response post(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
98            return get(uriResource, urlParams, session);
99        }
100
101        public Response put(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
102            return get(uriResource, urlParams, session);
103        }
104
105        public Response delete(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
106            return get(uriResource, urlParams, session);
107        }
108
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
120        public abstract String getText();
121
122        public abstract IStatus getStatus();
123
124        public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
125            return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), getText());
126        }
127
128        @Override
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
140        public String getText() {
141            throw new IllegalStateException("this method should not be called");
142        }
143
144        @Override
145        public String getMimeType() {
146            return "text/html";
147        }
148
149        @Override
150        public IStatus getStatus() {
151            return Status.OK;
152        }
153
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
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
196        public String getText() {
197            throw new IllegalStateException("this method should not be called");
198        }
199
200        @Override
201        public String getMimeType() {
202            throw new IllegalStateException("this method should not be called");
203        }
204
205        @Override
206        public IStatus getStatus() {
207            return Status.OK;
208        }
209
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
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
250        public String getText() {
251            return "<html><body><h3>Error 404: the requested page doesn't exist.</h3></body></html>";
252        }
253
254        @Override
255        public String getMimeType() {
256            return "text/html";
257        }
258
259        @Override
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
270        public String getText() {
271            return "<html><body><h2>Hello world!</h3></body></html>";
272        }
273
274        @Override
275        public String getMimeType() {
276            return "text/html";
277        }
278
279        @Override
280        public IStatus getStatus() {
281            return Status.OK;
282        }
283
284    }
285
286    public static class NotImplementedHandler extends DefaultHandler {
287
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
293        public String getMimeType() {
294            return "text/html";
295        }
296
297        @Override
298        public IStatus getStatus() {
299            return Status.OK;
300        }
301    }
302
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
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
351        private void parse() {
352        }
353
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
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
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
412        public String getUri() {
413            return uri;
414        }
415
416        public <T> T initParameter(Class<T> paramClazz) {
417            return initParameter(0, paramClazz);
418        }
419
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
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
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         */
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
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
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
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
515        public void setNotFoundHandler(Class<?> handler) {
516            error404Url = new UriResource(null, 100, handler);
517        }
518
519        public void setNotImplemented(Class<?> handler) {
520            notImplemented = handler;
521        }
522
523    }
524
525    private UriRouter router;
526
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
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
547    public void addRoute(String url, Class<?> handler, Object... initParameter) {
548        router.addRoute(url, 100, handler, initParameter);
549    }
550
551    public void removeRoute(String url) {
552        router.removeRoute(url);
553    }
554
555    @Override
556    public Response serve(IHTTPSession session) {
557        // Try to find match
558        return router.process(session);
559    }
560}
561