Home | History | Annotate | Download | only in router
      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 
     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