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 ForgeRock AS. 015 */ 016 017package org.forgerock.openig.handler.router; 018 019import static java.lang.String.*; 020import static org.forgerock.openig.config.Environment.*; 021import static org.forgerock.openig.util.Json.*; 022 023import java.io.File; 024import java.io.IOException; 025import java.util.Comparator; 026import java.util.HashMap; 027import java.util.Map; 028import java.util.SortedSet; 029import java.util.TreeSet; 030import java.util.concurrent.locks.Lock; 031import java.util.concurrent.locks.ReadWriteLock; 032import java.util.concurrent.locks.ReentrantReadWriteLock; 033 034import org.forgerock.openig.config.Environment; 035import org.forgerock.openig.handler.GenericHandler; 036import org.forgerock.openig.handler.Handler; 037import org.forgerock.openig.handler.HandlerException; 038import org.forgerock.openig.heap.GenericHeaplet; 039import org.forgerock.openig.heap.HeapException; 040import org.forgerock.openig.heap.HeapImpl; 041import org.forgerock.openig.http.Exchange; 042import org.forgerock.util.time.TimeService; 043 044/** 045 * Auto-configured {@link org.forgerock.openig.handler.DispatchHandler}. 046 * It looks for route configuration files (very similar to the usual general config file) 047 * in a defined directory (by default it looks in {@literal config/routes/}). 048 * <pre> 049 * { 050 * "name": "Router", 051 * "type": "Router", 052 * "config": { 053 * "directory": "/tmp/routes", 054 * "defaultHandler": "404NotFound", 055 * "scanInterval": 2 056 * } 057 * } 058 * </pre> 059 * 060 * Note that {@literal scanInterval} is defined in seconds. If {@literal -1} (or any negative value) is 061 * provided, only an initial scan is performed at startup, synchronously. 062 * 063 * @since 2.2 064 */ 065public class RouterHandler extends GenericHandler implements FileChangeListener { 066 067 /** 068 * Toolkit to load Routes from monitored files. 069 */ 070 private final RouteBuilder builder; 071 072 /** 073 * Monitor a given directory and emit notifications on add/remove/update of files. 074 */ 075 private final DirectoryScanner directoryScanner; 076 077 /** 078 * Keep track of managed routes. 079 */ 080 private final Map<File, Route> routes = new HashMap<File, Route>(); 081 082 /** 083 * Ordered set of managed routes. 084 */ 085 private SortedSet<Route> sorted = new TreeSet<Route>(new LexicographicalRouteComparator()); 086 087 /** 088 * Protect routes access. 089 */ 090 private final Lock read; 091 092 /** 093 * Protects write access to the routes. 094 */ 095 private final Lock write; 096 097 /** 098 * The optional handler which should be invoked when no routes match the 099 * request. 100 */ 101 private Handler defaultHandler; 102 103 /** 104 * Builds a router that loads its configuration from the given directory. 105 * @param builder route builder 106 * @param scanner {@link DirectoryScanner} that will be invoked at each incoming request 107 */ 108 public RouterHandler(final RouteBuilder builder, final DirectoryScanner scanner) { 109 this.builder = builder; 110 this.directoryScanner = scanner; 111 ReadWriteLock lock = new ReentrantReadWriteLock(); 112 this.read = lock.readLock(); 113 this.write = lock.writeLock(); 114 } 115 116 /** 117 * Changes the ordering of the managed routes. 118 * @param comparator route comparator 119 */ 120 public void setRouteComparator(final Comparator<Route> comparator) { 121 write.lock(); 122 try { 123 SortedSet<Route> newSet = new TreeSet<Route>(comparator); 124 newSet.addAll(sorted); 125 sorted = newSet; 126 } finally { 127 write.unlock(); 128 } 129 } 130 131 /** 132 * Sets the handler which should be invoked when no routes match the 133 * request. 134 * 135 * @param handler 136 * the handler which should be invoked when no routes match the 137 * request 138 */ 139 public void setDefaultHandler(final Handler handler) { 140 write.lock(); 141 try { 142 this.defaultHandler = handler; 143 } finally { 144 write.unlock(); 145 } 146 } 147 148 /** 149 * Starts this handler, executes an initial directory scan. 150 */ 151 public void start() { 152 directoryScanner.scan(this); 153 } 154 155 /** 156 * Stops this handler, shutting down and clearing all the managed routes. 157 */ 158 public void stop() { 159 write.lock(); 160 try { 161 // Un-register all the routes 162 sorted.clear(); 163 // Destroy the routes 164 for (Route route : routes.values()) { 165 route.destroy(); 166 } 167 routes.clear(); 168 } finally { 169 write.unlock(); 170 } 171 } 172 173 @Override 174 public void onChanges(final FileChangeSet changes) { 175 write.lock(); 176 try { 177 178 for (File file : changes.getRemovedFiles()) { 179 onRemovedFile(file); 180 } 181 182 for (File file : changes.getAddedFiles()) { 183 onAddedFile(file); 184 } 185 186 for (File file : changes.getModifiedFiles()) { 187 onModifiedFile(file); 188 } 189 190 } finally { 191 write.unlock(); 192 } 193 } 194 195 private void onAddedFile(final File file) { 196 try { 197 Route route = builder.build(file); 198 String name = route.getName(); 199 if (sorted.contains(route)) { 200 throw new HeapException(format("A route named '%s' is already registered", name)); 201 } 202 sorted.add(route); 203 routes.put(file, route); 204 logger.info(format("Added route '%s' defined in file '%s'", name, file)); 205 } catch (Throwable e) { 206 logger.error(format("The route defined in file '%s' cannot be added", 207 file)); 208 logger.error(e); 209 } 210 } 211 212 private void onRemovedFile(final File file) { 213 Route route = routes.remove(file); 214 if (route != null) { 215 sorted.remove(route); 216 route.destroy(); 217 logger.info(format("Removed route '%s' defined in file '%s'", route.getName(), file)); 218 } 219 } 220 221 private void onModifiedFile(final File file) { 222 Route newRoute; 223 try { 224 newRoute = builder.build(file); 225 } catch (Throwable e) { 226 logger.error(format("The route defined in file '%s' cannot be modified", 227 file)); 228 logger.error(e); 229 return; 230 } 231 Route oldRoute = routes.remove(file); 232 if (oldRoute != null) { 233 sorted.remove(oldRoute); 234 oldRoute.destroy(); 235 } 236 sorted.add(newRoute); 237 routes.put(file, newRoute); 238 logger.info(format("Modified route '%s' defined in file '%s'", newRoute.getName(), file)); 239 } 240 241 @Override 242 public void handle(final Exchange exchange) throws HandlerException, IOException { 243 // Run the directory scanner 244 directoryScanner.scan(this); 245 246 // Traverse the routes 247 read.lock(); 248 try { 249 for (Route route : sorted) { 250 if (route.accept(exchange)) { 251 route.handle(exchange); 252 return; 253 } 254 } 255 if (defaultHandler != null) { 256 defaultHandler.handle(exchange); 257 return; 258 } 259 throw new HandlerException("no handler to dispatch to"); 260 } finally { 261 read.unlock(); 262 } 263 } 264 265 /** Creates and initializes a routing handler in a heap environment. */ 266 public static class Heaplet extends GenericHeaplet { 267 268 @Override 269 public Object create() throws HeapException { 270 271 // By default, uses the config/routes from the environment 272 Environment env = heap.get(ENVIRONMENT_HEAP_KEY, Environment.class); 273 File directory = new File(env.getConfigDirectory(), "routes"); 274 275 // Configuration can override that value 276 String evaluation = evaluate(config.get("directory")); 277 if (evaluation != null) { 278 directory = new File(evaluation); 279 } 280 281 DirectoryScanner scanner = new DirectoryMonitor(directory); 282 283 int period = config.get("scanInterval").defaultTo(PeriodicDirectoryScanner.TEN_SECONDS).asInteger(); 284 if (period > 0) { 285 // Wrap the scanner in another scanner that will trigger scan at given interval 286 PeriodicDirectoryScanner periodic = 287 new PeriodicDirectoryScanner(scanner, TimeService.SYSTEM); 288 289 // configuration values is expressed in seconds, needs to convert it to milliseconds 290 periodic.setScanInterval(period * 1000); 291 scanner = periodic; 292 } else { 293 // Only scan once when handler.start() is called 294 scanner = new OnlyOnceDirectoryScanner(scanner); 295 } 296 297 RouterHandler handler = new RouterHandler(new RouteBuilder((HeapImpl) heap, qualified), scanner); 298 handler.setDefaultHandler(heap.resolve(config.get("defaultHandler"), 299 Handler.class, true)); 300 return handler; 301 } 302 303 @Override 304 public void start() throws HeapException { 305 ((RouterHandler) object).start(); 306 } 307 308 @Override 309 public void destroy() { 310 ((RouterHandler) object).stop(); 311 } 312 } 313}