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}