001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2014-2015 ForgeRock AS. 015 */ 016 017package org.forgerock.openig.handler.router; 018 019import static java.lang.String.format; 020import static org.forgerock.http.routing.RouteMatchers.requestUriMatcher; 021import static org.forgerock.http.routing.RoutingMode.EQUALS; 022import static org.forgerock.openig.heap.Keys.*; 023import static org.forgerock.openig.util.JsonValues.evaluate; 024 025import java.io.File; 026import java.util.Comparator; 027import java.util.HashMap; 028import java.util.Map; 029import java.util.SortedSet; 030import java.util.TreeSet; 031import java.util.concurrent.locks.Lock; 032import java.util.concurrent.locks.ReadWriteLock; 033import java.util.concurrent.locks.ReentrantReadWriteLock; 034 035import org.forgerock.http.Handler; 036import org.forgerock.http.protocol.Request; 037import org.forgerock.http.protocol.Response; 038import org.forgerock.http.routing.Router; 039import org.forgerock.openig.config.Environment; 040import org.forgerock.openig.handler.Handlers; 041import org.forgerock.openig.heap.GenericHeapObject; 042import org.forgerock.openig.heap.GenericHeaplet; 043import org.forgerock.openig.heap.HeapException; 044import org.forgerock.openig.heap.HeapImpl; 045import org.forgerock.openig.http.EndpointRegistry; 046import org.forgerock.openig.http.Responses; 047import org.forgerock.services.context.Context; 048import org.forgerock.util.promise.NeverThrowsException; 049import org.forgerock.util.promise.Promise; 050import org.forgerock.util.promise.Promises; 051import org.forgerock.util.time.TimeService; 052 053/** 054 * Auto-configured {@link org.forgerock.openig.handler.DispatchHandler}. 055 * It looks for route configuration files (very similar to the usual general config file) 056 * in a defined directory (by default it looks in {@literal config/routes/}). 057 * <pre> 058 * {@code 059 * { 060 * "name": "Router", 061 * "type": "Router", 062 * "config": { 063 * "directory": "/tmp/routes", 064 * "defaultHandler": "404NotFound", 065 * "scanInterval": 2 066 * } 067 * } 068 * } 069 * </pre> 070 * 071 * Note that {@literal scanInterval} is defined in seconds. If {@literal -1} (or any negative value) is 072 * provided, only an initial scan is performed at startup, synchronously. 073 * 074 * @since 2.2 075 */ 076public class RouterHandler extends GenericHeapObject implements FileChangeListener, Handler { 077 078 /** 079 * Toolkit to load Routes from monitored files. 080 */ 081 private final RouteBuilder builder; 082 083 /** 084 * Monitor a given directory and emit notifications on add/remove/update of files. 085 */ 086 private final DirectoryScanner directoryScanner; 087 088 /** 089 * Keep track of managed routes. 090 */ 091 private final Map<File, Route> routes = new HashMap<>(); 092 093 /** 094 * Ordered set of managed routes. 095 */ 096 private SortedSet<Route> sorted = new TreeSet<>(new LexicographicalRouteComparator()); 097 098 /** 099 * Protect routes access. 100 */ 101 private final Lock read; 102 103 /** 104 * Protects write access to the routes. 105 */ 106 private final Lock write; 107 108 /** 109 * The optional handler which should be invoked when no routes match the 110 * request. 111 */ 112 private Handler defaultHandler; 113 114 /** 115 * Builds a router that loads its configuration from the given directory. 116 * @param builder route builder 117 * @param scanner {@link DirectoryScanner} that will be invoked at each incoming request 118 */ 119 public RouterHandler(final RouteBuilder builder, final DirectoryScanner scanner) { 120 this.builder = builder; 121 this.directoryScanner = scanner; 122 ReadWriteLock lock = new ReentrantReadWriteLock(); 123 this.read = lock.readLock(); 124 this.write = lock.writeLock(); 125 } 126 127 /** 128 * Changes the ordering of the managed routes. 129 * @param comparator route comparator 130 */ 131 public void setRouteComparator(final Comparator<Route> comparator) { 132 write.lock(); 133 try { 134 SortedSet<Route> newSet = new TreeSet<>(comparator); 135 newSet.addAll(sorted); 136 sorted = newSet; 137 } finally { 138 write.unlock(); 139 } 140 } 141 142 /** 143 * Sets the handler which should be invoked when no routes match the 144 * request. 145 * 146 * @param handler 147 * the handler which should be invoked when no routes match the 148 * request 149 */ 150 public void setDefaultHandler(final Handler handler) { 151 write.lock(); 152 try { 153 this.defaultHandler = handler; 154 } finally { 155 write.unlock(); 156 } 157 } 158 159 /** 160 * Starts this handler, executes an initial directory scan. 161 */ 162 public void start() { 163 directoryScanner.scan(this); 164 } 165 166 /** 167 * Stops this handler, shutting down and clearing all the managed routes. 168 */ 169 public void stop() { 170 write.lock(); 171 try { 172 // Un-register all the routes 173 sorted.clear(); 174 // Destroy the routes 175 for (Route route : routes.values()) { 176 route.destroy(); 177 } 178 routes.clear(); 179 } finally { 180 write.unlock(); 181 } 182 } 183 184 @Override 185 public void onChanges(final FileChangeSet changes) { 186 write.lock(); 187 try { 188 189 for (File file : changes.getRemovedFiles()) { 190 onRemovedFile(file); 191 } 192 193 for (File file : changes.getAddedFiles()) { 194 onAddedFile(file); 195 } 196 197 for (File file : changes.getModifiedFiles()) { 198 onModifiedFile(file); 199 } 200 201 } finally { 202 write.unlock(); 203 } 204 } 205 206 private void onAddedFile(final File file) { 207 Route route = null; 208 try { 209 route = builder.build(file); 210 } catch (Throwable e) { 211 logger.error(format("The route defined in file '%s' cannot be added", 212 file)); 213 logger.error(e); 214 return; 215 } 216 String name = route.getName(); 217 if (sorted.contains(route)) { 218 logger.error(format("The added file '%s' contains a route named '%s' that is already " 219 + "registered by the file '%s'", 220 file, 221 name, 222 lookupRouteFile(name))); 223 route.destroy(); 224 return; 225 } 226 route.start(); 227 sorted.add(route); 228 routes.put(file, route); 229 logger.info(format("Added route '%s' defined in file '%s'", name, file)); 230 } 231 232 private void onRemovedFile(final File file) { 233 Route route = routes.remove(file); 234 if (route != null) { 235 sorted.remove(route); 236 route.destroy(); 237 logger.info(format("Removed route '%s' defined in file '%s'", route.getName(), file)); 238 } 239 } 240 241 private void onModifiedFile(final File file) { 242 Route newRoute; 243 try { 244 newRoute = builder.build(file); 245 } catch (Throwable e) { 246 logger.error(format("The route defined in file '%s' cannot be modified", 247 file)); 248 logger.error(e); 249 return; 250 } 251 Route oldRoute = routes.get(file); 252 if (oldRoute != null) { 253 // Route did change its name, and the new name is already in use 254 if (!oldRoute.getName().equals(newRoute.getName()) && sorted.contains(newRoute)) { 255 logger.error(format("The modified file '%s' contains a route named '%s' that is already " 256 + "registered by the file '%s'", 257 file, 258 newRoute.getName(), 259 lookupRouteFile(newRoute.getName()))); 260 newRoute.destroy(); 261 return; 262 } 263 routes.remove(file); 264 sorted.remove(oldRoute); 265 oldRoute.destroy(); 266 } 267 newRoute.start(); 268 sorted.add(newRoute); 269 routes.put(file, newRoute); 270 logger.info(format("Modified route '%s' defined in file '%s'", newRoute.getName(), file)); 271 } 272 273 @Override 274 public Promise<Response, NeverThrowsException> handle(final Context context, final Request request) { 275 // Run the directory scanner 276 directoryScanner.scan(this); 277 278 // Traverse the routes 279 read.lock(); 280 try { 281 for (Route route : sorted) { 282 if (route.accept(context, request)) { 283 return route.handle(context, request); 284 } 285 } 286 if (defaultHandler != null) { 287 return defaultHandler.handle(context, request); 288 } 289 return Promises.newResultPromise(Responses.newNotFound("no handler to dispatch to")); 290 } finally { 291 read.unlock(); 292 } 293 } 294 295 private File lookupRouteFile(String routeName) { 296 for (Map.Entry<File, Route> entry : routes.entrySet()) { 297 File file = entry.getKey(); 298 Route route = entry.getValue(); 299 300 if (route.getName().equals(routeName)) { 301 return file; 302 } 303 } 304 return null; 305 } 306 307 /** Creates and initializes a routing handler in a heap environment. */ 308 public static class Heaplet extends GenericHeaplet { 309 310 private EndpointRegistry.Registration registration; 311 312 @Override 313 public Object create() throws HeapException { 314 315 // By default, uses the config/routes from the environment 316 Environment env = heap.get(ENVIRONMENT_HEAP_KEY, Environment.class); 317 File directory = new File(env.getConfigDirectory(), "routes"); 318 319 // Configuration can override that value 320 String evaluation = evaluate(config.get("directory")); 321 if (evaluation != null) { 322 directory = new File(evaluation); 323 } 324 325 DirectoryScanner scanner = new DirectoryMonitor(directory); 326 327 int period = config.get("scanInterval").defaultTo(PeriodicDirectoryScanner.TEN_SECONDS).asInteger(); 328 if (period > 0) { 329 TimeService time = heap.get(TIME_SERVICE_HEAP_KEY, TimeService.class); 330 // Wrap the scanner in another scanner that will trigger scan at given interval 331 PeriodicDirectoryScanner periodic = new PeriodicDirectoryScanner(scanner, time); 332 333 // configuration values is expressed in seconds, needs to convert it to milliseconds 334 periodic.setScanInterval(period * 1000); 335 scanner = periodic; 336 } else { 337 // Only scan once when handler.start() is called 338 scanner = new OnlyOnceDirectoryScanner(scanner); 339 } 340 341 // Register the /routes/* endpoint 342 Router routes = new Router(); 343 routes.addRoute(requestUriMatcher(EQUALS, ""), Handlers.NO_CONTENT); 344 EndpointRegistry registry = endpointRegistry(); 345 registration = registry.register("routes", routes); 346 347 RouterHandler handler = new RouterHandler(new RouteBuilder((HeapImpl) heap, 348 qualified, 349 new EndpointRegistry(routes, 350 registration.getPath())), 351 scanner); 352 handler.setDefaultHandler(heap.resolve(config.get("defaultHandler"), 353 Handler.class, 354 true)); 355 return handler; 356 } 357 358 @Override 359 public void start() throws HeapException { 360 ((RouterHandler) object).start(); 361 } 362 363 @Override 364 public void destroy() { 365 if (object != null) { 366 ((RouterHandler) object).stop(); 367 } 368 if (registration != null) { 369 registration.unregister(); 370 } 371 super.destroy(); 372 } 373 } 374}