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}