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